Re-sort WPF DataGrid after bounded Data has changed
Asked Answered
B

5

34

I am looking for a way to re-sort my DataGrid when the underlying data has changed.

(The setting is quite standard: The DataGrid's ItemSource property is bound to an ObservableCollection; The columns are DataGridTextColumns; The data inside the DataGrid reacts correctly on changes inside the ObservableCollection; Sorting works fine when clicked with the mouse)

Any ideas ?

Bartle answered 16/7, 2012 at 13:22 Comment(2)
Did you explore CollectionViewSource?Reinhart
@WPF-it Now I did ;) You gave me the right hint and I was able to solve my problem with the CollectionViewSource. See my own answer to see what I did. Thank you !Bartle
B
32

It took me the whole afternoon but I finally found a solution that is surprisingly simple, short and efficient:

To control the behaviors of the UI control in question (here a DataGrid) one might simply use a CollectionViewSource. It acts as a kind of representative for the UI control inside your ViewModel without completely breaking the MVMM pattern.

In the ViewModel declare both a CollectionViewSource and an ordinary ObservableCollection<T> and wrap the CollectionViewSource around the ObservableCollection:

// Gets or sets the CollectionViewSource
public CollectionViewSource ViewSource { get; set; }

// Gets or sets the ObservableCollection
public ObservableCollection<T> Collection { get; set; }

// Instantiates the objets.
public ViewModel () {

    this.Collection = new ObservableCollection<T>();
    this.ViewSource = new CollectionViewSource();
    ViewSource.Source = this.Collection;
}

Then in the View part of the application you have nothing else to do as to bind the ItemsSource of the CollectionControl to the View property of the CollectionViewSource instead of directly to the ObservableCollection:

<DataGrid ItemsSource="{Binding ViewSource.View}" />

From this point on you can use the CollectionViewSource object in your ViewModel to directly manipulate the UI control in the View.

Sorting for example - as has been my primary problem - would look like this:

// Specify a sorting criteria for a particular column
ViewSource.SortDescriptions.Add(new SortDescription ("columnName", ListSortDirection.Ascending));

// Let the UI control refresh in order for changes to take place.
ViewSource.View.Refresh();

You see, very very simple and intuitive. Hope that this helps other people like it helped me.

Bartle answered 16/7, 2012 at 17:22 Comment(6)
This was great. The only thing is, once you've set ViewSource.Source to your ObservableCollection, you can no longer change your ObservableCollection in a BackgroundWorker. Or at least I couldn't.Rempe
It would also happen if you were binding the control directly to the ObservableCollection. IN both cases, you can actually makes changes to the collection, by simply invoking an action on the application dispatcher.Pennsylvania
Hi Marc Wellman, I am trying to get your method of sorting with a WPF datagrid working but the datagrid doesn't even show the data I bound to it let alone sort. Before I was doing a basic non sorting datagrid with gridName.ItemsSource = myObservableCollection; coupled with gridName.Items.Refresh(); and the grid loads the data properly using this method but when I switch to yours the datagrid shows no data at all. I was thinking of opening a new post so I could show my code and then reference this post but not sure if that is a Faux Pas in the stackoverflow world. Any advice?Carlin
It is working, but it takes much time to refresh. I have only 43 records in DataGridHoebart
I have same question as @CYarn
The Refresh() will make you lose the information which rows were selected before the Refresh().Conscientious
L
21

This is more for clarification than it is an answer, but WPF always binds to an ICollectionView and not the source collection. CollectionViewSource is just a mechanism used to create/retrieve the collection view.

Here's a great resource about the topic which should help you make better use of collection views in WPF: http://bea.stollnitz.com/blog/?p=387

Using CollectionViewSource in XAML can actually simplify your code some:

<Window
    ...
    xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase">

    <Window.Resources>
        <CollectionViewSource Source="{Binding MySourceCollection}" x:Key="cvs">
          <CollectionViewSource.SortDescriptions>
            <scm:SortDescription PropertyName="ColumnName" />
          </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
    </Window.Resources>
        
    ...
        
    <DataGrid ItemsSource="{Binding Source={StaticResource cvs}}">
    </DataGrid>
</Window>

Some people argue that when following the MVVM pattern, the view model should always expose the collection view but in my opinion, it just depends on the use case. If the view model is never going to directly interact with the collection view, it's just easier to configure it in XAML.

Loophole answered 16/7, 2012 at 18:22 Comment(4)
This is a good suggestion - the CollectionViewSource should either be a property in the code behind of the View (and can in turn bind to the ObservableCollection on the VM), or declare it as a local static resource as you've done.Ernestineernesto
This is the best solution, there's just one mistake in your XAML - it's "PropertyName" instead of "Property" in the SortDescription element.Unfounded
Thanks! This helped. I just had a hard time figuring out the scm namespace. Here it is for people trying to find it: xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"Bowe
That link is dead, but I found the text on The Internet Archive.Alaska
T
7

The answer by sellmeadog is either overly complicated or out of date. It's super simple. All you have to do is:

<UserControl.Resources>
    <CollectionViewSource 
        Source="{Binding MyCollection}" 
        IsLiveSortingRequested="True" 
        x:Key="MyKey" />
</UserControl.Resources>

<DataGrid ItemsSource="{Binding Source={StaticResource MyKey} }" >...
Throughcomposed answered 7/12, 2016 at 23:35 Comment(5)
Just a note, IsLiveSortingRequested requires .NET 4.5Wrac
How does it know which column to sort by? Doesn't it require the CollectionViewSource SortDescription somewhere?Unclad
Actually, you didn't mention how it identifies the column to sort by..Beira
According to the question, the user selects the sorting by clicking on a column header. In that case there is no need to defined the sorting in the CollectionViewSource.Conscientious
This is awesome. I use sort by modulo (absolute value) via RightClick on a column event and set SortMemberPath in .xaml.cs to AbsXXX properties depending on sender name. Everything else is MVVM. Normal click on a column sets SortMemberPath to a bound XXX. This solution just works for all cases. Answering another comment: empirically it uses SortMemberPath.Frazee
C
0

I cannot see any obviously easy ways, so I would try an Attached Behavior. It is a bit of a bastardization, but will give you what you want:

public static class DataGridAttachedProperty
{
     public static DataGrid _storedDataGrid;
     public static Boolean GetResortOnCollectionChanged(DataGrid dataGrid)
     {
         return (Boolean)dataGrid.GetValue(ResortOnCollectionChangedProperty);
     }

     public static void SetResortOnCollectionChanged(DataGrid dataGrid, Boolean value)
     {
         dataGrid.SetValue(ResortOnCollectionChangedProperty, value);
     }

    /// <summary>
    /// Exposes attached behavior that will trigger resort
    /// </summary>
    public static readonly DependencyProperty ResortOnCollectionChangedProperty = 
         DependencyProperty.RegisterAttached(
        "ResortOnCollectionChangedProperty", typeof (Boolean),
         typeof(DataGridAttachedProperty),
         new UIPropertyMetadata(false, OnResortOnCollectionChangedChange));

    private static void OnResortOnCollectionChangedChange
        (DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
      _storedDataGrid = dependencyObject as DataGrid;
      if (_storedDataGrid == null)
        return;

      if (e.NewValue is Boolean == false)
        return;

      var observableCollection = _storedDataGrid.ItemsSource as ObservableCollection;
      if(observableCollection == null)
        return;
      if ((Boolean)e.NewValue)
        observableCollection.CollectionChanged += OnCollectionChanged;
      else
        observableCollection.CollectionChanged -= OnCollectionChanged;
    }

    private static void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
      if (e.OldItems == e.NewItems)
        return;

      _storedDataGrid.Items.Refresh()
    }
}

Then, you can attach it via:

<DataGrid.Style>
  <Style TargetType="DataGrid">
    <Setter 
      Property="AttachedProperties:DataGridAttachedProperty.ResortOnCollectionChangedProperty" 
                                    Value="true" 
      />
   </Style>
 </DataGrid.Style>
Clamp answered 16/7, 2012 at 13:52 Comment(1)
Thank you very much for your detailed answer but unfortunately I was not able to make it work. I managed it to attach your property to my DataGrid and to have the _storedDataGrid.ItemsCollection.Refresh() being called on every CollectionChange event of my underlying collection. But for some reason the DataGrid wouldn't sort. I am not sure but maybe it's because I am using multiple threads and I had some problems propagating the Refresh-call to the UIThread. Anyways I found another lucky solution that works very fine - see my own answer for that. Nevertheless, again thank you!Bartle
F
-1

For anyone else having this problem, this may get you started... If you have a collection of INotifyPropertyChanged items, you can use this instead of ObservableCollection - it will refresh when individual items in the collection change: note: since this flags the items as removed then readded (even though they aren't actually removed and added,) selections may get out of sync. It's good enough for my small personal projects, but it's not ready to release to customers...

public class ObservableCollection2<T> : ObservableCollection<T>
{
    public ObservableCollection2()
    {
        this.CollectionChanged += ObservableCollection2_CollectionChanged;
    }

    void ObservableCollection2_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.OldItems != null)
            foreach (object o in e.OldItems)
                remove(o);
        if (e.NewItems != null)
            foreach (object o in e.NewItems)
                add(o);
    }
    void add(object o)
    {
        INotifyPropertyChanged ipc = o as INotifyPropertyChanged;
        if(ipc!=null)
            ipc.PropertyChanged += Ipc_PropertyChanged;
    }
    void remove(object o)
    {
        INotifyPropertyChanged ipc = o as INotifyPropertyChanged;
        if (ipc != null)
            ipc.PropertyChanged -= Ipc_PropertyChanged;
    }
    void Ipc_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        NotifyCollectionChangedEventArgs f;

        f = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, sender);
        base.OnCollectionChanged(f);
        f = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, sender);
        base.OnCollectionChanged(f);
    }
}
Fumikofumitory answered 14/6, 2016 at 16:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.