Saving & Restoring WPF DataGrid Columns’ Size, Sorting and Order

,

A data grid usually allows users to reorder, resize, and sort its columns. A refined data grid implementation remembers the user’s changes to its columns’ display, restoring the columns to match how they previously appeared each time the application is launched. How do we implement such a feat if WPF’s DataGrid is our control of choice?

Analyzing the task, we find that it readily divides into two parts: provide a mechanism to capture/restore the DataGrid columns’ state and set up the mechanics to persist this information across application executions. This article’s focus will be the first sub-task.

Let’s extend WPF’s DataGrid, adding a two-way bindable property named ColumnInfo which exposes the necessary column state information. When a column’s state is changed, this property will be updated with the relevant information. When the property is changed (i.e. by an outside source such as a view model), the data grid’s columns will adjust themselves as specified by the property’s new value.

Catching Column Changes

To keep ColumnInfo updated with information our grid columns’ current display characteristics, we need to find out when these characteristics change.

Determining when column re-ordering occurs is easy. Simply override OnColumnReordered.

1
2
3
4
5
protected override void OnColumnReordered(DataGridColumnEventArgs e)
{
    UpdateColumnInfo();
    Base.OnColumnReordered(e);
}

With this code in place, if the user drags column 1 to the right of column 2 so that the column order changes from “Column 1 | Column 2” to “Column 2 | Column 1”, our yet-to-be-defined UpdateColumnInfo will be called.

Catching column sort changes (e.g. the user clicks on a column header to sort the grid by that column’s values) requires more work. DataGrid provides an OnSorting override, but this method is called before the sort occurs. We want to inspect the grid after the sort completes but no OnSortCompleted override is provided. Are we stuck?

Thankfully not. Each of the columns has a sort direction dependency property. Let’s  iterate through the grid’s Columns collection and attach a DependencyPropertyDescriptor AddValueChanged handler to each column’s SortDirectionProperty.

At first glance, the following code seems like a good way of implementing this:

1
2
3
4
5
6
7
8
9
protected override void OnInitialized(EventArgs e)
{
    var sortDirectionPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(DataGridColumn.SortDirectionProperty, typeof(DataGridColumn));
    foreach (var column in Columns)
    {
        sortDirectionPropertyDescriptor.AddValueChanged(column, (sender, x) => UpdateColumnInfo());
    }
    base.OnInitialized(e);
}

However, the above creates a memory leak. The event handler lambda expression passed to AddValueChanged as its second argument captures a reference to this (our data grid). As long as this lambda expression is registered to be called when SortDirectionProperty changes, the captured this reference keeps the grid alive. The garbage collector won’t free up the memory used by our data grid, even if it is no longer being displayed.

We need a way to unregister our handler when the grid is no longer needed. Overriding Dispose (from IDisposable) would seem idea, except that DataGrid (along with most other WPF controls) does not implement IDisposable. Thankfully, FrameworkElement (a parent class of DataGrid) offers an Unloaded event. We will unregister our SorDirectionProperty change handler when this event is raised.

As it turns out, Unloaded is sometimes raised when the control needs to stay around, such as when a user-initiated system theme change occurs. We want our handler to say around as long as the control does, so we need a way to keep our handler are in place after theme changes. FrameworkElement’s Loaded event comes in handy. It’s raised whenever the control is loaded—at the initial load and at reloads after theme changes. Our OnInitialized override looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private bool inWidthChange = false;
protected override void OnInitialized(EventArgs e)
{
    EventHandler sortDirectionChangedHandler = (sender, x) => UpdateColumnInfo();
    EventHandler widthPropertyChangedHandler = (sender, x) => inWidthChange = true;
    var sortDirectionPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(DataGridColumn.SortDirectionProperty, typeof(DataGridColumn));
    var widthPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(DataGridColumn.WidthProperty, typeof(DataGridColumn));
    Loaded += (sender, x) =>
    {
    foreach (var column in Columns)
    {
        sortDirectionPropertyDescriptor.AddValueChanged(column, sortDirectionChangedHandler);
        widthPropertyDescriptor.AddValueChanged(column, widthPropertyChangedHandler);
    }
    };
    Unloaded += (sender, x) =>
    {
    foreach (var column in Columns)
    {
        sortDirectionPropertyDescriptor.RemoveValueChanged(column, sortDirectionChangedHandler);
        widthPropertyDescriptor.RemoveValueChanged(column, widthPropertyChangedHandler);
    }
    };
    base.OnInitialized(e);
}

In the above code, notice that we also added and removed a change notification handler for DataGridColumn.WidthProperty. This makes sense as one of our goals is to record column width changes. However, instead of calling UpdateColumnInfo, this event handler sets the bool instance variable inWidthChange to true. Why?

WidthProperty is changed almost continuously when the user drags to resize a column.  We’re only interested in the column’s final width when the resizing has finished. Setting inWidthChange indicates that resizing is happening. Overriding OnPreivewMouseLeftButtonUp lets us find out when resizing completes. Before calling UpdateColumnInfo in our override, we check inWidthChange to make sure that the button release corresponds with a resize. We don’t want to call UpdateColumnInfo for non-resize related button releases.

protected override void OnPreviewMouseLeftButtonUp(System.Windows.Input.MouseButtonEventArgs e)
{
    if (inWidthChange)
    {
	inWidthChange = false;
	UpdateColumnInfo();
    }
    base.OnPreviewMouseLeftButtonUp(e);
}

Processing Changes

Our code now calls UpdateColumnInfo whenever a relevant change occurs. UpdateColumnInfo sets the grid’s ColumnInfo property to a new ObservableCollection<ColumnInfo> that containing a ColumnInfo instance for each column in the grid’s Columns collection. The updatingColumnInfo bool will come in handy shortly.

1
2
3
4
5
6
7
8
9
10
11
12
private bool updatingColumnInfo = false;
public ObservableCollection<ColumnInfo> ColumnInfo
{
    get { /* ...  */ }
    set { /* ...  */ }
}
private void UpdateColumnInfo()
{
    updatingColumnInfo = true;
    ColumnInfo = new ObservableCollection<ColumnInfo>(Columns.Select((x) => new ColumnInfo(x)));
    updatingColumnInfo = false;
}

ColumnInfo is a struct holding information on a column’s display characteristics. The convenience constructor uses a DataGridColumn to populate the struct’s values. The Apply method reverses this process, applying the display characteristics described by the struct to the passed-in column.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public struct ColumnInfo
{
        public ColumnInfo(DataGridColumn column)
        {
            Header = column.Header;
            PropertyPath = ((Binding)((DataGridBoundColumn)column).Binding).Path.Path;
            WidthValue = column.Width.DisplayValue;
            WidthType = column.Width.UnitType;
            SortDirection = column.SortDirection;
            DisplayIndex = column.DisplayIndex;
        }
        public void Apply(DataGridColumn column, int gridColumnCount, SortDescriptionCollection sortDescriptions)
        {
            column.Width = new DataGridLength(WidthValue, WidthType);
            column.SortDirection = SortDirection;
            if (SortDirection != null)
            {
                sortDescriptions.Add(new SortDescription(PropertyPath, SortDirection.Value));
            }
            if (column.DisplayIndex != DisplayIndex)
            {
                var maxIndex = (gridColumnCount == 0) ? 0 : gridColumnCount - 1;
                column.DisplayIndex = (DisplayIndex <= maxIndex) ? DisplayIndex : maxIndex;
            }
        }
        public object Header;
        public string PropertyPath;
        public ListSortDirection? SortDirection;
        public int DisplayIndex;
        public double WidthValue;
        public DataGridLengthUnitType WidthType;
}

The struct’s convenience constructor sets SortDirection from the column’s SortDirection property while the Apply method both sets this property and modifies the grid’s SortDescription collection. Why?

SortDirection indicates if the user has clicked on a column header to sort the grid by that column. However, programmaticly setting this property does not sort the grid—it simply sets the sort direction indicator on the column’s header (the little arrow displayed to indicate that the column has been sorted). If we want the grid sorted by this column, we must also update its SortDescription collection.

Changes from the Other Direction

So far, we update ColumnInfo whenever the user makes a relevant modification to the grid. Our data grid also needs to update its display in response to external ColumnInfo changes.

We’d like to be able to two-way data-bind to ColumnInfo, allowing us to connect the grid’s ColumnInfo to our view model the same way we do with ItemsSource.

1
2
<local:EnhancedDataGrid ItemsSource="{Binding Items}"
                         ColumnInfo="{Binding Columns}" />

To support two-way data-binding, ColumnInfo must be a dependency property.

1
2
3
4
5
6
7
8
9
public static readonly DependencyProperty ColumnInfoProperty = DependencyProperty.Register("ColumnInfo",
    typeof(ObservableCollection<ColumnInfo>), typeof(EnhancedDataGrid),
    new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ColumnInfoChangedCallback)
    );
public ObservableCollection<ColumnInfo> ColumnInfo
{
    get { return (ObservableCollection<ColumnInfo>)GetValue(ColumnInfoProperty); }
    set { SetValue(ColumnInfoProperty, value); }
}

When this property is changed, we want to apply the changes to the data grid’s display. ColumnInfo’s dependency property definition wires in the necessary event handling.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void ColumnInfoChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
    var grid = (EnhancedDataGrid)dependencyObject;
    if (!grid.updatingColumnInfo) { grid.ColumnInfoChanged(); }
}
private void ColumnInfoChanged()
{
    Items.SortDescriptions.Clear();
    foreach (var column in ColumnInfo)
    {
    var realColumn = Columns.Where((x) => column.Header.Equals(x.Header)).FirstOrDefault();
    if (realColumn == null) { continue; }
    column.Apply(realColumn, Columns.Count, Items.SortDescriptions);
    }
}

The processing done in ColumnInfoChanged changes column widths and sort directions which will trigger the event handlers we registered in OnInitialized. The bool updatingColumnInfo (set in UpdateColumnInfo) prevents these changes from causing an recursive loop  of event handler calls.

Done!

There we have it—an enhanced data grid which allows us to easily persist user changes to its columns’ display characteristics!

Our EnhancedDataGrid has a few limitations. It does not support dynamic addition or removal of columns, as it add and remove column event handlers only when the grid is loaded and unloaded, respectively. It relies on ((Binding)((DataGridBoundColumn)column).Binding).Path.Path to properly identify the sort-by column(s), an assumption which may not hold true in certain scenarios. Lastly, external sources must replace ColumnInfo with a new ObservableCollection<ColumnInfo> in order to influence the grid’s display. Manipulations made to the existing ColumnInfo collection are ignored. Removing these constraints, if the need arises, is left as an exercise to the reader.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/*
The below example code is offered "as-is" with no warrantees of any kind. Use at your own risk.
*/
using System;
using System.Linq;
using System.Windows.Controls;
using System.Windows;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;
namespace EnhancedwDataGridExample
{
    class EnhancedDataGrid : DataGrid
    {
        private bool inWidthChange = false;
        private bool updatingColumnInfo = false;
        public static readonly DependencyProperty ColumnInfoProperty = DependencyProperty.Register("ColumnInfo",
                typeof(ObservableCollection<ColumnInfo>), typeof(EnhancedDataGrid),
                new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ColumnInfoChangedCallback)
            );
        private static void ColumnInfoChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            var grid = (EnhancedDataGrid)dependencyObject;
            if (!grid.updatingColumnInfo) { grid.ColumnInfoChanged(); }
        }
        protected override void OnInitialized(EventArgs e)
        {
            EventHandler sortDirectionChangedHandler = (sender, x) => UpdateColumnInfo();
            EventHandler widthPropertyChangedHandler = (sender, x) => inWidthChange = true;
            var sortDirectionPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(DataGridColumn.SortDirectionProperty, typeof(DataGridColumn));
            var widthPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(DataGridColumn.WidthProperty, typeof(DataGridColumn));
            Loaded += (sender, x) =>
            {
                foreach (var column in Columns)
                {
                    sortDirectionPropertyDescriptor.AddValueChanged(column, sortDirectionChangedHandler);
                    widthPropertyDescriptor.AddValueChanged(column, widthPropertyChangedHandler);
                }
            };
            Unloaded += (sender, x) =>
            {
                foreach (var column in Columns)
                {
                    sortDirectionPropertyDescriptor.RemoveValueChanged(column, sortDirectionChangedHandler);
                    widthPropertyDescriptor.RemoveValueChanged(column, widthPropertyChangedHandler);
                }
            };
            base.OnInitialized(e);
        }
        public ObservableCollection<ColumnInfo> ColumnInfo
        {
            get { return (ObservableCollection<ColumnInfo>)GetValue(ColumnInfoProperty); }
            set { SetValue(ColumnInfoProperty, value); }
        }
        private void UpdateColumnInfo()
        {
            updatingColumnInfo = true;
            ColumnInfo = new ObservableCollection<ColumnInfo>(Columns.Select((x) => new ColumnInfo(x)));
            updatingColumnInfo = false;
        }
        protected override void OnColumnReordered(DataGridColumnEventArgs e)
        {
            UpdateColumnInfo();
            base.OnColumnReordered(e);
        }
        protected override void OnPreviewMouseLeftButtonUp(System.Windows.Input.MouseButtonEventArgs e)
        {
            if (inWidthChange)
            {
                inWidthChange = false;
                UpdateColumnInfo();
            }
            base.OnPreviewMouseLeftButtonUp(e);
        }
        private void ColumnInfoChanged()
        {
            Items.SortDescriptions.Clear();
            foreach (var column in ColumnInfo)
            {
                var realColumn = Columns.Where((x) => column.Header.Equals(x.Header)).FirstOrDefault();
                if (realColumn == null) { continue; }
                column.Apply(realColumn, Columns.Count, Items.SortDescriptions);
            }
        }
    }
    public struct ColumnInfo
    {
        public ColumnInfo(DataGridColumn column)
        {
            Header = column.Header;
            PropertyPath = ((Binding)((DataGridBoundColumn)column).Binding).Path.Path;
            WidthValue = column.Width.DisplayValue;
            WidthType = column.Width.UnitType;
            SortDirection = column.SortDirection;
            DisplayIndex = column.DisplayIndex;
        }
        public void Apply(DataGridColumn column, int gridColumnCount, SortDescriptionCollection sortDescriptions)
        {
            column.Width = new DataGridLength(WidthValue, WidthType);
            column.SortDirection = SortDirection;
            if (SortDirection != null)
            {
                sortDescriptions.Add(new SortDescription(PropertyPath, SortDirection.Value));
            }
            if (column.DisplayIndex != DisplayIndex)
            {
                var maxIndex = (gridColumnCount == 0) ? 0 : gridColumnCount - 1;
                column.DisplayIndex = (DisplayIndex <= maxIndex) ? DisplayIndex : maxIndex;
            }
        }
        public object Header;
        public string PropertyPath;
        public ListSortDirection? SortDirection;
        public int DisplayIndex;
        public double WidthValue;
        public DataGridLengthUnitType WidthType;
    }
}

15 thoughts on “Saving & Restoring WPF DataGrid Columns’ Size, Sorting and Order

  1. Mark

    Thanks for the description of your interesting component.
    Where do you get the column information that you bind with

    ?
    I don’t have column information in my view-model. In fact, I use an auto-generating-column behavior, which I do not wish to give up. What do you bind to, and, who or what writes that data there initially?

    Reply
    1. Ben

      The collection of ColumnInfo structs is automatically generated by the EnhancedDataGrid when its private method ColumnInfoChanged() is called. You don’t need to provide the column information; EnhancedDataGrid takes care of retrieving it from the parent DataGrid class for you.

      Reply
  2. Balaji

    Hi Ben,

    It was an excellent article. I was trying to use your logic here for implementing the customization of user preferences.
    The below statement with XAML , did not bind correctly.
    ColumnInfo=”{Binding Columns}”

    during run time, am getting as and the ColumnInfo is always null

    System.Windows.Data Information: 10 : Cannot retrieve value using the binding and no valid fallback value exists; using default instead. BindingExpression:Path=Columns; DataItem=null; target element is ‘WPFEnhancedDataGrid’ (Name=’ab’); target property is ‘ColumnInfo’ (type ‘ObservableCollection`1’)

    You might have came through above scenario.

    would apprecaite if you could assist me

    Thanks

    Reply
  3. Michael Schröer

    Thanks for this but unfortunately this wouldn’t work if there is a DataGridComboBoxColumn within the grid. I simply changed the Line:
    PropertyPath = ((Binding)((DataGridBoundColumn)column).Binding).Path.Path;

    to:

    if (!(column is DataGridComboBoxColumn))
    PropertyPath = ((Binding)((DataGridBoundColumn)column).Binding).Path.Path;
    else
    PropertyPath = ((Binding)((DataGridComboBoxColumn)column).SelectedItemBinding).Path.Path;

    but the grid does not restore the Settings. Do you have any idea?

    Thanks a lot

    Reply
    1. Serg
      public ColumnInfo(DataGridColumn column)
      {
                  Header = column.Header;
                  WidthValue = column.Width.DisplayValue;
                  WidthType = column.Width.UnitType;
                  SortDirection = column.SortDirection;
                  DisplayIndex = column.DisplayIndex;
      
                  switch (column)
                  {
                      case DataGridTemplateColumn templateColumn:
                          PropertyPath = templateColumn.SortMemberPath;
                          break;
                      case DataGridComboBoxColumn boxColumn:
                          PropertyPath = ((Binding)boxColumn.SelectedItemBinding).Path.Path;
                          break;
                      default:
                          PropertyPath = ((Binding)((DataGridBoundColumn)column).Binding).Path.Path;
                          break;
                  }
      }
      Reply
  4. Dewey

    Hi Ben, Thanks for posting this. I’m trying to add visibility tracking to this as well as saving these values to properties settings. Can you help me out with this?
    thanks
    Dewey

    Reply
    1. Ben Gribaudo Post author

      Hum…I’d be tempted to try expanding struct ColumnInfo to include a property named Visibility which is set to the value of column.Visibility and which updates column.Visibility when ColumnInfo‘s Apply method is called. Does something like this work?

      By “properties settings,” are you referring to Application Settings?

      Reply
  5. Nico

    This is really great. it’s a shame it’s not compatible with columns of type DataGridTemplateColumn, they don’t inherit of DatagridBoundColumn so there isn’t a Binding property. If you have any solution to update your implementation it could be really great!

    Reply
  6. beetle16

    Works really smooth at first glance, but i ran into an issue that the column order is not always restored correctly – especially if multiple columns are repositioned.
    The solution was to always order the ColumnInfo collection by DisplayIndex before assigning it to the grid.

    Reply
  7. Guy Siedes

    Here is some info about persisting the data. I wish it could be made simpler – that the class could both “load” the data, and “save” it. In my current addition – the class can “Save”, but needs the application to “load” in different occasions.
    1) create a setting in settings that is the same name as your Grid.
    2) update the class as follows:

    private void UpdateColumnInfo()
            {
                updatingColumnInfo = true;
                ColumnInfo = new ObservableCollection(Columns.Select((x) => new ColumnInfo(x)));
                //persist data
                string json = JsonConvert.SerializeObject(ColumnInfo); //deserializing
                Settings1.Default[Name] =json; //in default apps this is usually called "Properties.Settings.Default...".
                Settings1.Default.Save();
                updatingColumnInfo = false;
            }

    3) Your window/control must load the setting, and assign it to the Tab/Control’s Grid.ColumnInfo, as you mention. In case of a TabControl, you have to do it once after the Grid binds to the model, and then each time the TabControl’s selection is changed, so, in the Main_Window I added:

    private void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                Model.MakeColumnChange();
            }
    

    and in the model, I load the data as follows:

    ublic async Task MakeColumnChange()
            {
                await Task.Delay(100);
                AccountsGrid.ColumnInfo = new ObservableCollection(JsonConvert.DeserializeObject<List>(Settings1.Default["AccountsGrid"].ToString()));
            }

    This is also called after the TabControl binds to the model:

            public AccountsControl()
            {
                InitializeComponent();
                AquireModel();
            }
    
            async Task AquireModel()
            {
                for (int i = 0; i < 100; i++)
                {
                    await Task.Delay(1);
                    if (DataContext != null)
                    {
                        MSystem = (MSystem)DataContext;
                        BindingOperations.EnableCollectionSynchronization(MSystem.Accounts, _syncLock);
                        break;
                    }
                }
                MSystem.Model.AccountsGrid = AccountsGrid; //so that model can access grid and apply changes to columns.
            }

    Last thing: I also call Model.MakeColumnChange(); when the DataContext of the control is changed.

    Reply
  8. grek40

    “DataGrid provides an OnSorting override, but this method is called before the sort occurs. We want to inspect the grid after the sort completes but no OnSortCompleted override is provided. Are we stuck?”

    No, because typically, when WPF signals an ongoing operation, we can just push an action into the dispatcher queue and it will be executed whenever the currently queued operations completed (more or less OnSortCompleted). I don’t see the advantage of the Loaded/Unloaded event handling that is used here instead of the Dispatcher.

    Reply
    1. grek40

      I was asked for a code example regarding my comment.

      Lets assume a most basic DataGrid that subscribes to the Sorting event:

      <DataGrid ItemsSource="{Binding Items}" Sorting="DataGrid_Sorting"/>

      And some code behind handler that demonstrates the difference of a direct action vs a deferred action with dispatcher:

              private void DataGrid_Sorting(object sender, DataGridSortingEventArgs e)
              {
                  UpdateImmediate(e.Column);
      
                  Dispatcher.BeginInvoke(new Action(d => UpdateDelayed(d)), e.Column);
              }
      
              ListSortDirection? immediateDirection; // when sorting completed, this contains the old value from before sorting
              private void UpdateImmediate(DataGridColumn c)
              {
                  immediateDirection = c.SortDirection;
              }
      
              ListSortDirection? delayedDirection; // when sorting completed, this contains the new value representing the current sorting
              private void UpdateDelayed(DataGridColumn c)
              {
                  delayedDirection = c.SortDirection;
              }
      

      So as commented, if you got a `Sorting` event and you want a `Sorted` action, just queue the action in the dispatcher queue.

      Reply
  9. Vinay Gupta

    I am facing one issue while using this code.
    As soon as screen is loaded it display data grid with default display index and visibility and after sometime(1 sec or so) display index and visibility are updated for all the columns.
    Which is causing very unpleasant experience for user.

    Reason behind for this. FIrst grid is loaded with default values and then ColumnInfo collection is assigned with the configuration. so COlumnInfoChangedCallback happen after the gridLoaded event.which is causing all this issue.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *