October 16, 2011

WPF ListView persistable layout - MVVM style

First of all, this article doesn't claim to show you 'the right way' to persist control's layout, it is just my effort to make my wpf UI a bit comfortable. This code works for me and has no warranties for you. Many data-centric applications use the ListView for table representation of data, allowing user to resize and move columns of the GridView. But when you open the application next time, all columns again have their default size and position and there is no way to save the layout of GridView and restore it on next load. Ok, let's try to add this feature to our control derived from Listiew. I think it is reasonable to use XamlWriter and XamlReader to serialize xaml content so all I need is to call XamlReader.Load method when our conrol initialize and XamlWriter.Save on column resize or move. As a MVVM adherent I would prefer to implement any persistence logic in the ViewModel class. Then our control should expose serialized layout state as a DependencyProperty and use binding to pass value to the correspondent ViewModel's property. Also I wanted a property IsLayoutPersistable to switch off the whole thing.
public static DependencyProperty IsLayoutPersistableProperty = 
   DependencyProperty.Register("IsLayoutPersistable", typeof(bool), typeof(PersistableLayoutListView), 
   new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.Inherits, 
   OnIsLayoutPersistableChanged));
public static DependencyProperty PersistableLayoutProperty = 
   DependencyProperty.Register("PersistableLayout", typeof(string), typeof(PersistableLayoutListView), 
   new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, 
   OnLayoutChanged));
There is no problem to get starting event just overriding OnApplyTemplate() method but how to get column resizing and moving event? The solution was to handle DragCompletedEvent that bubbling up from the Thumb element (as I found it's used as a columns separator in the header). We just need to set to true the argument handledEventsToo. This is for column resize. And GridView.Columns.CollectionChanged event could be handled to get column reordering.
((GridView)listView.View).Columns.CollectionChanged += _columnsCollectionChangedHandler;
listView.AddHandler(Thumb.DragCompletedEvent, _columnsResizeHandler, true);
Both the handlers would call column saving method that updates PersistableLayout bindable property:
var coll = ((GridView)listView.View).Columns;
string savedState = XamlWriter.Save(coll);
listView.PersistableLayout = savedState;
Further I need to handle changing of my IsLayoutPersistable dependency property to add/remove just desribed event handlers.
static void OnIsLayoutPersistableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var listView = (PersistableLayoutListView)d;
    var persistable = (bool)e.NewValue;
    AddRemoveHandlers(persistable, listView);
}
private static void AddRemoveHandlers(bool persistable, PersistableLayoutListView listView)
{
    if (persistable)
    {
        _columnsResizeHandler = new RoutedEventHandler((o, ea) =>
                                                           {
                                                               if (!(ea.OriginalSource is Thumb)
                                                                   || !ea.Handled)
                                                                   return;
                                                               SaveLayout(listView);
                                                           });
        _columnsCollectionChangedHandler = 
             new NotifyCollectionChangedEventHandler((o, ea) => SaveLayout(listView));
        ((GridView)listView.View).Columns.CollectionChanged += _columnsCollectionChangedHandler;
        listView.AddHandler(Thumb.DragCompletedEvent, _columnsResizeHandler, true);
    }
    else
    {
        if (_columnsCollectionChangedHandler != null) 
            ((GridView)listView.View).Columns.CollectionChanged -= _columnsCollectionChangedHandler;
        if (_columnsResizeHandler != null) 
            listView.RemoveHandler(Thumb.DragCompletedEvent, _columnsResizeHandler);
    }
}
Nothing special. Full source code for the control is here.

Usage.

You can use this control instead of regular ListView like this:
<WpfGui:PersistableLayoutListView x:Name="listView" 
      IsLayoutPersistable="True"
      PersistableLayout="{Binding Columns, Mode=TwoWay}"
      ...>
     <ListView.View>
       <GridView AllowsColumnReorder="True" >
          <GridViewColumn Width="50" Header="State" DisplayMemberBinding="{Binding State,Mode=OneWay}">
          ...
       </GridView>
    </ListView.View>
</WpfGui:PersistableLayoutListView>
And in the ViewModel:
public string Columns
{
    get { return columns ?? (columns = UserConfig.GetConfiguration("Columns")); }
    set
    {
        if (value == columns) return;
        columns = value;
        UserConfig.SetConfiguration(columns, "Columns");
    }
}
private string columns;
Here, in getter and setter you can use any preferrable read/write methods that persist xaml string to any storage. For example, a file in isolated storage or in the app directory. Full source code: PersistableLayoutListView.cs

1 comment: