There are several ways to do this, but the technique we chose involved refreshing the entire data source (which collapses all nodes) and enhancing the XamDataGrid to remember which nodes were expanded before the refresh and expand them again after the refresh. At the same time we want to persist the active record.
I sub-classed the XamDataGrid and added the functionality there. Pages that need this functionality replace the igDP:XamDataGrid with a reference to the new class and initialize a new dependency property called UniqueIDPath which specifies which property on the datasource(s) should be used to match old and new records.
Start a new Visual Studio Class Library project using C# called PersistantXamDataGrid and target any Framework from .4.0 or later.
Rename Class1 as PersistentXamDataGrid and also all references to it.
Add references to the Infragistics dlls.
Also add references to
- PresentationCore
- PresentationFramework
- WindowsBase
using System;
using System.Collections.Generic;
using Infragistics.Windows.DataPresenter;
using System.Windows;
using System.Collections;
using Infragistics.Windows.DataPresenter.Events;Our data grid inherits from XamDataGrid so...
public class PersistentXamDataGrid:XamDataGrid
{
}
We need the name of a property that can be used to match records from before the refresh with records after. Good database design requires every record to have a single unique identifier. We will allow the XAML to specify the name of this property which we will store in a new dependency property called UniqueIDPath in keeping with DisplayValuePath etc. used by Microsoft. Let's define that property.
Inside the class add...
static readonly DependencyProperty UniqueIDPathProperty;
public String UniqueIDPath
{
get { return (string)GetValue(UniqueIDPathProperty); }
set { SetValue(UniqueIDPathProperty, value); }
}
static PersistentXamDataGrid()
{
UniqueIDPathProperty =
DependencyProperty.Register("UniqueIDPath", typeof(string), typeof(PersistentXamDataGrid), new FrameworkPropertyMetadata("ID"));
}
We will maintain a list of expanded node's ids as well as the id of the active record. Note we do not attempt to track which level of the hierarchy a node is on. We assume IDs are unique for the entire dataset. If this is a problem, it's possible to alter GetUniqueID (later) to include the level.
private List<String> ExpandedIDs = new List<string>();
private string
SelectedID = "";
In the instance constructor and destructor we need to attach and remove the event handlers that do all the work.
public PersistentXamDataGrid()
{
this.DataSourceChanged += XamDataGrid_DataSourceChanged;
this.RecordCollapsed += XamDataGrid_RecordCollapsed;
this.RecordExpanded += XamDataGrid_RecordExpanded;
this.RecordActivated += XamDataGrid_RecordActivated;
}
~PersistentXamDataGrid()
{
this.DataSourceChanged -= XamDataGrid_DataSourceChanged;
this.RecordCollapsed -= XamDataGrid_RecordCollapsed;
this.RecordExpanded -= XamDataGrid_RecordExpanded;
this.RecordActivated -= XamDataGrid_RecordActivated;
}
Now it's time to write the code that does the actual work.
When a row is collapsed we find its ID and remove it from our list of expanded IDs.
private void
XamDataGrid_RecordCollapsed(object sender, RecordCollapsedEventArgs e)
{
object o = (e.Record as DataRecord).DataItem;
string ID = GetUniqueID(o);
if (ExpandedIDs.Contains(ID)) ExpandedIDs.Remove(ID);
}
private void
XamDataGrid_RecordExpanded(object sender, RecordExpandedEventArgs e)
{
object o = (e.Record as DataRecord).DataItem;
string ID = GetUniqueID(o);
if (!ExpandedIDs.Contains(ID)) ExpandedIDs.Add(ID);
}
When a record becomes the active record we find its ID and make a note of it.
private void
XamDataGrid_RecordActivated(object sender, RecordActivatedEventArgs e)
{
if (UniqueIDPath == "" || e.Record == null || !e.Record.IsDataRecord) return;
if (UniqueIDPath == "" || e.Record == null || !e.Record.IsDataRecord) return;
SelectedID = GetUniqueID((e.Record as DataRecord).DataItem);
}
All these functions require a function that can return the value of the ID using the path specified in UniqueIDPath. We use a little reflection to do this. The value is returned as a string. This works for all the types we tend to use as IDs.
private String GetUniqueID(object o)
{
if (UniqueIDPath == "") return "";
if (o == null) return "";
return o.GetType().GetProperty(UniqueIDPath).GetValue(o, null).ToString();
}
Now we have to write the code that will detect a change in the data source and re-expand previously expanded nodes.
private void
XamDataGrid_DataSourceChanged(object sender, RoutedPropertyChangedEventArgs<IEnumerable> e)
{
if (UniqueIDPath == "") return;
PersistentXamDataGrid cdg = sender as PersistentXamDataGrid;
ExpandChildrenAndSetFocus(cdg.ViewableRecords);
if (cdg.ActiveRecord != null)
cdg.BringRecordIntoView(cdg.ActiveRecord);
}
ExpandChildren recurses through the data grid's records looking for records with IDs in the Expanded IDs list and expanding them. There are different types of records. Only DataRecords can be expanded but all types of records can have interesting children. While we are recursing through the tree we may as well set the active record too.
private void
ExpandChildrenAndSetFocus(ViewableRecordCollection rc)
{
String ID;
foreach (Record r in rc)
{
if (r is DataRecord)
{
ID = GetUniqueID((r as DataRecord).DataItem);
if (ExpandedIDs.Contains(ID))
r.IsExpanded = true;
else
r.IsExpanded = false;
if (ID == SelectedID)
r.IsActive = true;
else
r.IsActive = false;
}
if (r.HasChildren)
ExpandChildren(r.ViewableChildRecords);
}
}
Add a WPF App project to the solution and call it Inventory.
Make Inventory the startup project and add a reference to PersistentXamDataGrid and Infragistics.
The Main Window will have one of our new grids displaying inventory data and two buttons.
The first button will populate the grid with some search results, the second button will populate our grid with different search results with some results overlapping the first search results to demonstrate how the grid handles changes to the data source.
In our example the top level of data is a list of pallets. In each pallet is a list of boxes, and in each box is a list of items. This provides our hierarchy.
Here's the XAML.
<Window x:Class="PersistExpansionAndFocus.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:custom="clr-namespace:CustomXamDataGrid;assembly=CustomXamDataGrid"
xmlns:local="clr-namespace:PersistExpansionAndFocus"
xmlns:igDP="http://infragistics.com/DataPresenter"
mc:Ignorable="d"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<RoutedCommand x:Key="RefreshAB"/>
<RoutedCommand x:Key="RefreshAC"/>
</Window.Resources>
<Window.CommandBindings>
<CommandBinding Command="{StaticResource RefreshAB}" Executed="RefreshAB_Executed"/>
<CommandBinding Command="{StaticResource RefreshAC}" Executed="RefreshAC_Executed"/>
</Window.CommandBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<custom:CustomXamDataGrid Grid.Row="0" DataSource="{Binding SearchResults}" ActiveDataItem="{Binding SelectedItem}" UniqueIDPath="ID" >
<custom:CustomXamDataGrid.FieldLayoutSettings>
<igDP:FieldLayoutSettings SelectionTypeRecord="Single" AutoGenerateFields="False" ExpansionIndicatorDisplayMode="CheckOnDisplay"/>
</custom:CustomXamDataGrid.FieldLayoutSettings>
<custom:CustomXamDataGrid.FieldSettings>
<igDP:FieldSettings AllowSummaries="False" AllowEdit="False"/>
</custom:CustomXamDataGrid.FieldSettings>
<custom:CustomXamDataGrid.FieldLayouts>
<igDP:FieldLayout Description="Pallet" Key="Pallet">
<igDP:FieldLayout.Fields>
<igDP:TextField Label="ID" Name="ID"/>
<igDP:TextField Label="Name" Name="Name"/>
<igDP:Field Label="" Name="Boxes"/>
</igDP:FieldLayout.Fields>
</igDP:FieldLayout>
<igDP:FieldLayout ParentFieldLayoutKey="Pallet" ParentFieldName="Boxes" Key="Box">
<igDP:FieldLayout.Fields>
<igDP:TextField Label="ID" Name="ID"/>
<igDP:TextField Label="Name" Name="Name"/>
<igDP:Field Label="" Name="Items"/>
</igDP:FieldLayout.Fields>
</igDP:FieldLayout>
<igDP:FieldLayout ParentFieldLayoutKey="Box" ParentFieldName="Items" Key="Item">
<igDP:FieldLayout.Fields>
<igDP:TextField Label="ID" Name="ID"/>
<igDP:TextField Label="Name" Name="Name"/>
</igDP:FieldLayout.Fields>
</igDP:FieldLayout>
</custom:CustomXamDataGrid.FieldLayouts>
</custom:CustomXamDataGrid>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<Button Content="Refresh
1 and 2" Command="{StaticResource Refresh12}"/>
<Button Content="Refresh
1 and 3" Command="{StaticResource Refresh13}"/>
</StackPanel>
</Grid>
</Window>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event
PropertyChangedEventHandler PropertyChanged;
private void
NotifyPropertyChanged(String PropertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
public class cPallet
{
public string ID { get; set; }
public string Name { get; set; }
public List<cBox> Boxes { get; set; }
}
public class cBox
{
public string ID { get; set; }
public string Name { get; set; }
public List<cItem> Items { get; set; }
}
public class cItem
{
public string ID { get; set; }
public string Name { get; set; }
}
Now let's create our Inventory data store so can pretend we're reading from a database.
private List<cPallet> Inventory;
Our Inventory is populated from our constructor.
public MainWindow()
{
InitializeInventory();
InitializeComponent();
}
public void
InitializeInventory()
{
Inventory = new List<cPallet>
{
new cPallet() {ID="1", Name="Pallet from UPS", Boxes = new
List<cBox>()
{
new cBox() {ID="1A", Name="Box A on Pallet 1", Items= new
List<cItem>() {new cItem()
{ID="1Aa", Name="Item a in Box A" }, new cItem() {ID="1Ab", Name="Item b in Box A"} } }
,
new cBox() {ID="1B", Name="Box B on Pallet 1", Items= new
List<cItem>() {new cItem()
{ID="1Ba", Name="Item a in Box B" }, new cItem() {ID="1Bb", Name="Item b in Box B"} } }
}
}
,
new cPallet() {ID="2", Name="Pallet from FedEx", Boxes = new
List<cBox>()
{
new cBox() {ID="2A", Name="Box A on Pallet 2", Items= new
List<cItem>() {new cItem()
{ID="2Aa", Name="Item a in Box A" }, new cItem() {ID="2Aa", Name="Item b in Box A"} } }
,
new cBox() {ID="2B", Name="Box B on Pallet 2", Items= new
List<cItem>() {new cItem()
{ID="2Ba", Name="Item a in Box B" }, new cItem() {ID="2Bb", Name="Item b in Box B"} } }
}
}
,
new cPallet() {ID="3", Name="Pallet from USPS", Boxes = new
List<cBox>()
{
new cBox() {ID="3A", Name="Box A on Pallet 3", Items= new
List<cItem>() {new cItem()
{ID="3Aa", Name="Item a in Box A" }, new cItem() {ID="3Aa", Name="Item b in Box A"} } }
,
new cBox() {ID="3B", Name="Box B on Pallet 3", Items= new
List<cItem>() {new cItem()
{ID="3Ba", Name="Item a in Box B" }, new cItem() {ID="3Bb", Name="Item b in Box B"} } }
}
} };
}
We will display parts of the inventory by selecting into SearchResults which our data grid is bound to.
private List<cPallet> _SearchResults = new List<cPallet>();
public List<cPallet> SearchResults
{
get { return
_SearchResults; }
set
{
_SearchResults = value;
NotifyPropertyChanged("SearchResults");
}
}
public String SelectedItem { get; set; }
Lastly we need command execute event handlers to refresh the SearchResults and demonstrate how the grid keeps expanded nodes open.
private void
Refresh12_Executed(object sender,
ExecutedRoutedEventArgs e)
{
SearchResults = new
List<cPallet>(Inventory.Where((i) => (i.ID == "1" || i.ID == "2")));
}
private void
Refresh13_Executed(object sender,
ExecutedRoutedEventArgs e)
{
SearchResults = new
List<cPallet>(Inventory.Where((i) => (i.ID == "1" || i.ID == "3")));
}
After [Refresh 1 and 2] and expanding some nodes |
Now click [Refresh 1 and 3]. Note how the nodes in pallet 1 are still expanded and the same row still has focus. It almost doesn't look like we refreshed - but we did.
After [Refresh 1 and 3]. |
Now click on [Refresh 1 and 2] again. Note how the grid remembers that last time the pallet 2 nodes were displayed they were expanded so it expands them again.
It remembers the node state from last time it was displayed. |
This has been a long post but I hope you see that once you've built the persistent data grid, adding the functionality to an existing screen is quite simple assuming you have well defined data structures.
No comments:
Post a Comment