Hendrik Wiese
Hendrik Wiese

Reputation: 2219

WPF DataGrid ignores SortDescription

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

Upvotes: 11

Views: 9782

Answers (7)

adamjhilton
adamjhilton

Reputation: 411

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);
}

Upvotes: 0

Tony Pulokas
Tony Pulokas

Reputation: 475

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);
}

Upvotes: 0

Jon Barker
Jon Barker

Reputation: 1828

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);
    }

Upvotes: 6

Juergen
Juergen

Reputation: 151

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)};
    }
}

Upvotes: 4

kat
kat

Reputation: 615

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);
        }
    }

Upvotes: 1

Hendrik Wiese
Hendrik Wiese

Reputation: 2219

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);
    }

Upvotes: 8

Rune Andersen
Rune Andersen

Reputation: 1705

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 :)

Upvotes: 1

Related Questions