WPF DataGrid default sort not working
Asked Answered
F

2

12

I have a DataGrid, with columns XAML as such:

<DataGridTextColumn Header="Time" Binding="{Binding Date, StringFormat='yyyy-MM-dd  HH:mm:ss'}" SortMemberPath="Date" SortDirection="Descending" Width="130" CanUserResize="True" />
<DataGridTextColumn Header="Level" Binding="{Binding Level}" Width="60" CanUserResize="True" />
<DataGridTextColumn Header="Source" Binding="{Binding Logger}" Width="150" CanUserResize="True" />
<DataGridTextColumn Header="Message" Binding="{Binding Message}" Width="*" CanUserResize="True" />

I bind this to an ObservableCollection<EalsLogEvent>, where EalsLogEvent.Date is typed DateTime:

public ObservableCollection<EalsLogEvent> LogEvents 
{
    get
    {
        return _logEvents;
    }
}

The grid viewmodel uses a timer to refresh itself, and everything seems fine with the grid, except when it first loads, on app startup. Then, the Time column appears to be sorted descending, but is sorted ascending.

To get the sort right, I must click the column header twice; the first time changes the order to ascending, which now matches the content of the column. The second click on the column header changes its sort order back to descending, and this time it sorts the column contents properly, i.e. descending.

If I use LINQ to order the collection when _logEvents gets refreshed, I lose whichever order the user had set for the column by clicking its header. If I have to have the view tell the model which order the LINQ sort should use, something smells bad.

Felt answered 11/3, 2015 at 11:14 Comment(3)
Have you tried to use a CollectionView for sorting?Adjoin
@Adjoin I think I had, sometime way back, but will try again today. All available references tell me the DataGrid is supposed to maintain its own CollectionView or one of its three derivatives.Felt
@dymanoid, How can I replace ObservableCollection with any CollectionView or its subclasses, ListCollectionView and BindingListCollectionView when none of these are generic while ObservableCollection is?Felt
A
19

You could use a CollectionViewSource in your XAML to define the default sorting.

Assuming we have a view model:

public class ViewModel : INotifyPropertyChanged
{
    public ObservableCollection<Item> Items { get; private set; }
}

We can create a custom CollectionView for the Items collection:

<Window xmlns:l="clr-namespace:YourNamespace"
        xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase">
    <Window.DataContext>
        <l:ViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <CollectionViewSource Source="{Binding Items}" x:Key="GridItems">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="Date" Direction="Descending"/>
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
    </Window.Resources>
    <DataGrid ItemsSource="{Binding Source={StaticResource GridItems}}" AutoGenerateColumns="False">
        <DataGrid.Columns>                    
            <DataGridTextColumn Header="Time" Binding="{Binding Date, StringFormat='yyyy-MM-dd  HH:mm:ss'}" Width="130" CanUserResize="True" />
            <DataGridTextColumn Header="Level" Binding="{Binding Level}" Width="60" CanUserResize="True" />
            <DataGridTextColumn Header="Source" Binding="{Binding Logger}" Width="150" CanUserResize="True" />
            <DataGridTextColumn Header="Message" Binding="{Binding Message}" Width="*" CanUserResize="True" />
        </DataGrid.Columns>
    </DataGrid>
</Window>

With this approach, your underlying source collection (Items in this example) will not be affected, the sorting occurs only in the view.

As you can read in MSDN:

You can think of a collection view as the layer on top of the binding source collection that allows you to navigate and display the collection based on sort, filter, and group queries, all without having to manipulate the underlying source collection itself. If the source collection implements the INotifyCollectionChanged interface, the changes raised by the CollectionChanged event are propagated to the views.

You should also note the following:

All collections have a default CollectionView. WPF always binds to a view rather than a collection. If you bind directly to a collection, WPF actually binds to the default view for that collection.

So, using the CollectionViewSource, you're just defining a custom view for your collection.

Adjoin answered 12/3, 2015 at 11:27 Comment(4)
One complication here is I have a timer (System.Timer) in the viewmodel that polls EalsLogEvent data source (a DbSet) regularly to update itself as rows get added to that table all the time, from all over, via NLog. In order for the timer thread to update the DataGrid, I pass the view's Dispatcher through the ctor: _viewModel = new LogViewModel(Dispatcher). Creating the viewmodel inside the view doesn't let me do this. Busy trying to see if ViewModelLocator might help me.Felt
@ProfK, you don't have to instantiate the viewmodel in the view's XAML, that's only an example.Adjoin
Great stuff, @dymanoid! Thank you so much. It does annoy me slightly, however, that so many resources I consulted seem to think the plain old SortMemberPath and SortDirection properties on the row itself are all that is required. Isn't the DataGrid supposed to use these XAML properties when it builds it's own, default CollectionView?Felt
You also need to keep the SortDirection attribute set on the column in question if you want the grid's initial visual state to reflect the active sort (i.e. show a little arrow in the column header). This solution works, but I wish I understood why it is necessary. Maybe the sort is applied initially when the grid is empty and isn't updated later when databinding occurs?Gourami
O
0

You should create 2 properties in your viewmodel:

private ObservableCollection<EalsLogEvent> logEvents = new ObservableCollection<EalsLogEvent>();
private ICollectionView logEventsView;

public ObservableCollection<EalsLogEvent> LogEvents
{
    get { return this.logEvents; }
    set { this.SetProperty(ref this.logEvents, value); }
}

public ICollectionView LogEventsView
{
    get
    {
        if (this.logEventsView == null)
        {
            this.logEventsView= CollectionViewSource.GetDefaultView(this.LogEvents);
            this.logEventsView.SortDescriptions.Add(new SortDescription("Time", ListSortDirection.Descending));
        }

        return this.logEventsView;
    }
}

Bind the DataGrid to LogEventsView, and use LogEvents to add or remove items from. This allows default sorting, and user sorting.

Ostrich answered 4/1 at 11:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.