WPF DataGrid ignores SortDescription
Asked Answered
H

7

11

I've got a strange problem here regarding sorting of a WPF DataGrid (System.Windows.Controls.DataGrid in .NET 4.0).

Its ItemsSource is bound to a property of the datacontext object:

<DataGrid HeadersVisibility="Column" SelectedIndex="0" MinHeight="30" ItemsSource="{Binding FahrtenView}" AutoGenerateColumns="False" x:Name="fahrtenDG">

FahrtenView looks like this:

    public ICollectionView FahrtenView
    {
        get
        {
            var view = CollectionViewSource.GetDefaultView(_fahrten);
            view.SortDescriptions.Add(new SortDescription("Index", ListSortDirection.Ascending));
            return view;
        }
    }

The DataGrid gets sorted. However it only gets sorted the very first time it's assigned a DataContext. After that, changing the DataContext (by selecting another "parental" object in a data hierarchy) still causes the property FahrtenView to be evaluated (I can put a BP in and debugger stops there) but the added sortdescription is completely ignored, hence sorting does not work anymore.

Even calling fahrtenDG.Items.Refresh() on every DataContextChange doesn't help.

I'm pretty sure this is the way to go when it comes to sorting a WPF DataGrid, isn't it? So why does it refuse to work so obstinately after doing its job perfectly the very first time it gets called?

Any idea? I'd be very grateful.

Cheers, Hendrik

Heavily answered 24/6, 2012 at 12:14 Comment(2)
you should add your update as an answer, then accept it (when you can)Totalizator
Similar to https://mcmap.net/q/1015552/-issue-sorting-datagrid & #6177271Monney
H
8

I've inherited from DataGrid to catch a brief glimpse on its guts. What I've found is that for some mysterious reasons, although the first time OnItemsSourceChanged gets called, everything looks fine, in every following call of OnItemsSourceChanged the SortDescription list of the ItemsSource collection view is empty.

For that reason I've added a custom SetupSortDescription event that is called at the end of OnItemsSourceChanged. Now I'm adding the sort descriptions in the event handler function, which's working like a charm.

I consider this a bug in the WPF toolkit DataGrid.

Here's my overridden OnItemsSourceChanged

    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        if (SetupSortDescriptions != null && (newValue != null)) 
            SetupSortDescriptions(this, new ValueEventArgs<CollectionView>((CollectionView)newValue)); 

        base.OnItemsSourceChanged(oldValue, newValue);
    }
Heavily answered 6/8, 2012 at 14:1 Comment(3)
Thank you so much for your post, Hendrik! It is indeed impossible to get around this bug through view models alone - one has to implement a custom DataGrid. I made a small modification using attached property instead of an event (code below).Potts
Thanks! I improved this a bit to use MVVM rather than an event - by providing a binding for a list of SortDescriptions which you setup once only. See my other answer.Nancynandor
Thanks! This was driving me batty! I modified your code to suite my needs. It basically persists the sort descriptions and restores them whenever they get blown away. This may help others: see below.Seminary
N
6

I improved on Hendrik's answer a bit to use MVVM rather than an event.

    public static readonly DependencyProperty SortDescriptionsProperty = DependencyProperty.Register("SortDescriptions", typeof(List<SortDescription>), typeof(ReSolverDataGrid), new PropertyMetadata(null));

    /// <summary>
    /// Sort descriptions for when grouped LCV is being used. Due to bu*g in WCF this must be set otherwise sort is ignored.
    /// </summary>
    /// <remarks>
    /// IN YOUR XAML, THE ORDER OF BINDINGS IS IMPORTANT! MAKE SURE SortDescriptions IS SET BEFORE ITEMSSOURCE!!! 
    /// </remarks>
    public List<SortDescription> SortDescriptions
    {
        get { return (List<SortDescription>)GetValue(SortDescriptionsProperty); }
        set { SetValue(SortDescriptionsProperty, value); }
    }

    protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
    {
        //Only do this if the newValue is a listcollectionview - in which case we need to have it re-populated with sort descriptions due to DG bug
        if (SortDescriptions != null && ((newValue as ListCollectionView) != null))
        {
            var listCollectionView = (ListCollectionView)newValue;
            listCollectionView.SortDescriptions.AddRange(SortDescriptions);
        }

        base.OnItemsSourceChanged(oldValue, newValue);
    }
Nancynandor answered 18/8, 2015 at 17:36 Comment(1)
Works well for me, as long as I provide a CollectionViewSource to the datagrid rather than the ObservableCollection. I have my CollectionViewSource defined in the control's resources, and another list of SortDescription in a static class that I bind to this new property.Threonine
L
4

I used the interited DataGrid from kat to create a Behavior for the WPF DataGrid.

The behavior saves the initial SortDescriptions and applies them on every change of ItemsSource. You can also provide a IEnumerable<SortDescription> which will cause a resort on every change.

Behavior

public class DataGridSortBehavior : Behavior<DataGrid>
{
    public static readonly DependencyProperty SortDescriptionsProperty = DependencyProperty.Register(
        "SortDescriptions",
        typeof (IEnumerable<SortDescription>),
        typeof (DataGridSortBehavior),
        new FrameworkPropertyMetadata(null, SortDescriptionsPropertyChanged));

    /// <summary>
    ///     Storage for initial SortDescriptions
    /// </summary>
    private IEnumerable<SortDescription> _internalSortDescriptions;

    /// <summary>
    ///     Property for providing a Binding to Custom SortDescriptions
    /// </summary>
    public IEnumerable<SortDescription> SortDescriptions
    {
        get { return (IEnumerable<SortDescription>) GetValue(SortDescriptionsProperty); }
        set { SetValue(SortDescriptionsProperty, value); }
    }


    protected override void OnAttached()
    {
        var dpd = DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof (DataGrid));
        if (dpd != null)
        {
            dpd.AddValueChanged(AssociatedObject, OnItemsSourceChanged);
        }
    }

    protected override void OnDetaching()
    {
        var dpd = DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof (DataGrid));
        if (dpd != null)
        {
            dpd.RemoveValueChanged(AssociatedObject, OnItemsSourceChanged);
        }
    }

    private static void SortDescriptionsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is DataGridSortBehavior)
        {
            ((DataGridSortBehavior) d).OnItemsSourceChanged(d, EventArgs.Empty);                
        }
    }

    public void OnItemsSourceChanged(object sender, EventArgs eventArgs)
    {
        // save description only on first call, SortDescriptions are always empty after ItemsSourceChanged
        if (_internalSortDescriptions == null)
        {
            // save initial sort descriptions
            var cv = (AssociatedObject.ItemsSource as ICollectionView);
            if (cv != null)
            {
                _internalSortDescriptions = cv.SortDescriptions.ToList();
            }
        }
        else
        {
            // do not resort first time - DataGrid works as expected this time
            var sort = SortDescriptions ?? _internalSortDescriptions;

            if (sort != null)
            {
                sort = sort.ToList();
                var collectionView = AssociatedObject.ItemsSource as ICollectionView;
                if (collectionView != null)
                {
                    using (collectionView.DeferRefresh())
                    {
                        collectionView.SortDescriptions.Clear();
                        foreach (var sorter in sort)
                        {
                            collectionView.SortDescriptions.Add(sorter);
                        }
                    }
                }
            }
        }
    }
}

XAML with optional SortDescriptions parameter

<DataGrid  ItemsSource="{Binding View}" >
    <i:Interaction.Behaviors>
        <commons:DataGridSortBehavior SortDescriptions="{Binding SortDescriptions}"/>
    </i:Interaction.Behaviors>
</DataGrid>

ViewModel ICollectionView Setup

View = CollectionViewSource.GetDefaultView(_collection);
View.SortDescriptions.Add(new SortDescription("Sequence", ListSortDirection.Ascending));

Optional: ViewModel Property for providing changable SortDescriptions

public IEnumerable<SortDescription> SortDescriptions
{
    get
    {
        return new List<SortDescription> {new SortDescription("Sequence", ListSortDirection.Ascending)};
    }
}
Liguria answered 16/7, 2015 at 22:14 Comment(1)
Works a treat. Thanks.Sunlit
F
1

If you call CollectionViewSource.GetDefaultView(..) on the same collection you get the same collectionview object back, that could explain why adding an identical sortdescription struct doesn't trigger a change.

fahrtenDG.Items.Refresh() can't work since you are not refreshing the bound collection.

CollectionViewSource.GetDefaultView(_fahrten).Refresh() should work - I would keep a reference to it.

From your explanation I don't quite get the change of datacontext - are you changing it to a new object? If so all your bindings should reevaluate. Is it the same collection always, and your Index property on the listelements change, and that is why you expect a change - if so your list element might need a INotifyPropertyChanged implementation, because if the collection doesn't change then there is no need to resort.

Your OnItemsSourceChanged(..) implementation seems like a hack :)

Fayfayal answered 3/8, 2012 at 11:7 Comment(0)
P
1

I tried to get around this problem with the view model - by recreating ICollectionView in the getter and frantically calling DeferRefresh(). However I can confirm that Hendrik's solution is the only one that works reliably. I wanted to post full code below in case it helps somebody.

VIEW

<controls:SortableDataGrid
    ItemsSource="{Binding InfoSorted}"
    PermanentSort="{Binding PermanentSort}"
    CanUserSortColumns="False" />

VIEW MODEL

public ObservableCollection<Foo> Info { get; private set; }
public ICollectionView InfoSorted { get; private set; }
public IEnumerable<SortDescription> PermanentSort { get; private set; }

CUSTOM CONTROL

public class SortableDataGrid : DataGrid
    {
        public static readonly DependencyProperty PermanentSortProperty = DependencyProperty.Register(
            "PermanentSort",
            typeof(IEnumerable<SortDescription>),
            typeof(SortableDataGrid),
            new FrameworkPropertyMetadata(null));

        public IEnumerable<SortDescription> PermanentSort
        {
            get { return (IEnumerable<SortDescription>)this.GetValue(PermanentSortProperty); }
            set { this.SetValue(PermanentSortProperty, value); }
        }

        protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
        {
            var sort = this.PermanentSort;
            if (sort != null)
            {
                sort = sort.ToList();
                var collectionView = newValue as ICollectionView;
                if (collectionView != null)
                {
                    using (collectionView.DeferRefresh())
                    {
                        collectionView.SortDescriptions.Clear();
                        foreach (SortDescription sorter in sort)
                        {
                            collectionView.SortDescriptions.Add(sorter);
                        }
                    }
                }
            }

            base.OnItemsSourceChanged(oldValue, newValue);
        }
    }
Potts answered 18/3, 2014 at 18:47 Comment(0)
M
0

I endorse Juergen's approach of using an attached behavior. However, since my version of this problem arose when I had declared the CollectionViewSource object in the view model class, I found it more direct to solve the problem by adding the event handler SortDescriptions_CollectionChanged as shown in the code below. This code is entirely within the view model class.

public CollectionViewSource FilteredOptionsView
{
    get
    {
        if (_filteredOptionsView == null)
        {
            _filteredOptionsView = new CollectionViewSource
            {
                Source = Options,
                IsLiveSortingRequested = true
            };
            SetOptionsViewSorting(_filteredOptionsView);
            _filteredOptionsView.View.Filter = o => ((ConstantOption)o).Value != null;
        }
        return _filteredOptionsView;
    }
}
private CollectionViewSource _filteredOptionsView;

protected void SetOptionsViewSorting(CollectionViewSource viewSource)
{
    // define the sorting
    viewSource.SortDescriptions.Add(_optionsViewSortDescription);
    // subscribe to an event in order to handle a bug caused by the DataGrid that may be
    // bound to the CollectionViewSource
    ((INotifyCollectionChanged)viewSource.View.SortDescriptions).CollectionChanged
                                    += SortDescriptions_CollectionChanged;
}

protected static SortDescription _optionsViewSortDescription
                    = new SortDescription("SortIndex", ListSortDirection.Ascending);

void SortDescriptions_CollectionChanged(Object sender, NotifyCollectionChangedEventArgs e)
{
    var collection = sender as SortDescriptionCollection;
    if (collection == null) return;
    // The SortDescriptions collection should always contain exactly one SortDescription.
    // However, when DataTemplate containing the DataGrid bound to the ICollectionView
    // is unloaded, the DataGrid erroneously clears the collection.
    if (collection.None())
        collection.Add(_optionsViewSortDescription);
}
Monney answered 24/11, 2016 at 0:47 Comment(0)
S
0

Thanks! This was driving me batty! I modified your code to suite my needs. It basically persists the sort descriptions and restores them whenever they get blown away. This may help others:

private List<SortDescription> SortDescriptions = null;

protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
 if (newValue is CollectionView collectionView)
  if (SortDescriptions == null)
   SortDescriptions = new List<SortDescription>(collectionView.SortDescriptions);
  else
   foreach (SortDescription sortDescription in SortDescriptions)
    collectionView.SortDescriptions.Add(sortDescription);

 base.OnItemsSourceChanged(oldValue, newValue);
}
Seminary answered 3/11, 2017 at 20:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.