Chris Whitley
Chris Whitley

Reputation: 181

WPF: DataGrid Sort When Values Change

I'll try to make this as detailed as possible, searching all afternoon, I couldn't find anything similar enough to my problem to get a solution.

Brief Explination Of What My Application Does

The application I'm making provides an alarm type system for when a gathering item will be available in the game FFXIV. Specific items can be gathered at specific times within the time in the game world (Eorzea Time). My Application displays a list of the gathering items, their start and end times for gathering them, as well as a Next Spawn calculation (how soon until it is available again)

My Code

I'm trying to follow MVVM pattern as close as possible. I have a View which contains a DataGrid.

AlarmView.XAML

        <DataGrid Grid.Row="1"
                      Name="dgAlarms"
                      ItemsSource="{Binding AlarmsListCollection, UpdateSourceTrigger=PropertyChanged}"
                      SelectedValue="{Binding SelectedAlarm}"
                      AutoGenerateColumns="False"
                      IsReadOnly="True"
                      IsSynchronizedWithCurrentItem="True"
                      CanUserAddRows="False"
                      CanUserDeleteRows="False"
                      CanUserReorderColumns="False"
                      CanUserResizeColumns="False"
                      CanUserResizeRows="False"
                      CanUserSortColumns="True"
                      SelectionMode="Single"
                      >
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Item List"
                                Binding="{Binding Name}" 
                                        Width="auto"/>
                    <DataGridTextColumn Header="Next Spawn"
                                Binding="{Binding NextSpawn, UpdateSourceTrigger=PropertyChanged}" 
                                        Width="auto"
                                        SortMemberPath="{Binding NextSpawn, UpdateSourceTrigger=PropertyChanged}"
                                        SortDirection="Ascending"/>
                    <DataGridTextColumn Header="Start"
                                Binding="{Binding StartTime}" 
                                        Width="auto"/>
                    <DataGridTextColumn Header="End"
                                Binding="{Binding EndTime}" 
                                        Width="auto"/>
                </DataGrid.Columns>
            </DataGrid>

As you can see, the ItemsSource for the DataGridis bound to AlarmsListCollection

In the ViewModel form the AlarmView, I initilize the the AlarmListCollection as such

//=========================================================
//  Private Fields
//=========================================================

private ObservableCollection<Model.AlarmItem> _alarmsListCollection;

//=========================================================
//  Properties
//=========================================================
public ObservableCollection<Model.AlarmItem> AlarmsListCollection
{
    get { return this._alarmsListCollection; }
    set
    {
        if (this._alarmsListCollection == value) return;
        this._alarmsListCollection = value;
    }
}

//=========================================================
//  Constructor
//=========================================================
public AlarmsViewModel(DataGrid dgReference)
{
    if (_alarmItemRepository == null)
        _alarmItemRepository = new AlarmItemRepository();

    // Initilize the AlarmsListCollection
    this.AlarmsListCollection = new ObservableCollection<Model.AlarmItem>(_alarmItemRepository.GetAlarmItems());            

}

_alarmItemRepository.GetAlarmItems() just returns a List<Model.AlarmItem> containing a the objects. The important thing to know here is that a Model.AlarmItem contains a property called NextSpawn. This property is a String and it stores a representation of the how soon until the Model.AlarmItem will spawn.

The NextSpawn property string us updated every 1 second within the Elapsed event of a System.Timers.Timer

    struct AlarmInfo
    {
        public TimeSpan StartTime;
        public TimeSpan NextSpawn;
        public bool Armed;
        public bool IssueEarlyWarning;
    }

    private void UpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {

        //  Go through each of the alarm items
        foreach(Model.AlarmItem alarmItem in this.AlarmsListView)
        {
            //  Get the current eorzea time span
            TimeSpan currentEorzeaTimeSpan = this.EorzeaClock.GetEorzeaTimeSpan();

            //  Get info about the alarm item
            AlarmInfo alarmInfo = new AlarmInfo();
            TimeSpan.TryParse(alarmItem.StartTime, out alarmInfo.StartTime);
            alarmInfo.Armed = alarmItem.Armed;
            alarmInfo.IssueEarlyWarning = alarmItem.EarlyWarningIssued;
            TimeSpan.TryParse(alarmItem.NextSpawn, out alarmInfo.NextSpawn);


            #region CalculateTimeTillSpawn
            //  Get the time difference between the alarm time and eorzea time
            TimeSpan timeDiff;
            TimeSpan nextEorzeaSpawn;
            if (alarmInfo.StartTime.Equals(new TimeSpan(0, 0, 0)))
            {
                timeDiff = (new TimeSpan(24, 0, 0)).Subtract(currentEorzeaTimeSpan);
            }
            else
            {
                timeDiff = alarmInfo.StartTime.Subtract(currentEorzeaTimeSpan);
            }



            if (alarmInfo.StartTime > currentEorzeaTimeSpan)
            {
                nextEorzeaSpawn = alarmInfo.StartTime.Subtract(currentEorzeaTimeSpan);
            }
            else
            {
                //alarm.TimeTillSpawnEorzea = ((TimeSpan)new TimeSpan(23, 59, 59)).Subtract(currentEorzeaTimeSpan.Subtract(alarm.StartTime));
                nextEorzeaSpawn = ((TimeSpan)new TimeSpan(23, 59, 59)).Subtract(currentEorzeaTimeSpan.Subtract(alarmInfo.StartTime));
            }
            long earthTicks =nextEorzeaSpawn.Ticks / (long)Utilities.ClockController.EORZEA_MULTIPLIER;
            alarmInfo.NextSpawn = new TimeSpan(earthTicks);
            #endregion CalculateTimeTillSpawn


            //  Push the alarmInfo back into the alarmItem
            alarmItem.Armed = alarmInfo.Armed;
            alarmItem.EarlyWarningIssued = alarmInfo.IssueEarlyWarning;
            alarmItem.NextSpawn = alarmInfo.NextSpawn.ToString(@"h\h\:m\m\:s\s", System.Globalization.CultureInfo.InvariantCulture);

        }


        this.UpdateTimer.Start();
    }

Once this code runs, and the NextSpawn property is updated, the updated information is reflected back to the DataGrid with no problem. I can sit and watch the values in the NextSpawn column of the datagrid change every second as they are being updated. However, this leads to the problem I'm having.

The Problem

For ease of use, I want users to be able to click the Next Spawn column header of the DataGrid and it sort based on this column. This works 100% as expected. However, as the values of the NextSpawn property of the Model.AlarmItems updates, the sorting of the column does not update to reflect any changes.

I have tried everything I can think of and search exhaustively to find an solution for this. I've tried using Dispatcher.Invoke() on the DataGrid from within the timer's elapsed event, but this just leads to the UI becoming bogged down with how frequent it's call.

I create a gif to show what i'm talking about. In this gif, the NextSpawn column is sorted for Ascending and you can see the values updating. Once they get to 0h:0m:0s, 1 second after that, they update to their new values and at this point, the sorting should occur to move up the smaller values.

http://gfycat.com/RealisticInferiorAmericanriverotter

Any assistance on this would be phenomenally appreciated.

Upvotes: 4

Views: 2921

Answers (1)

Chris Whitley
Chris Whitley

Reputation: 181

The answer was increadibly simple, thanks for the reference to ICollectionViewLiveShaping.IsLiveSortin from @KornMuffin.

https://msdn.microsoft.com/en-us/library/system.componentmodel.icollectionviewliveshaping.islivesorting(v=vs.110).aspx

Here were my steps to implement and resolve this.

In my AlarmView.xaml code behind I added a static variable that contains the instance of the AlarmView that is created.

Here is the constructor of the AlarmView

public partial class AlarmsView : UserControl
{
    public static AlarmsView View;
    public AlarmsView()
    {
        InitializeComponent();
        View = this;
        this.DataContext = new ViewModel.AlarmsViewModel();

    }
}

I created the static view because I need access to the datagrid from the ViewModel for an event (will get to this in a moment)

In the AlarmViewModel.cs there is the ObservableCollection

    public CollectionViewSource ViewSource { get; set; }
    public ObservableCollection<Model.AlarmItem> Collection { get; set; }

Then, in the constructor of the AlarmViewModel, I instantiated the ObservableCollection and using ICollectionViewLiveShaping set the .IsLiveSorting to true. Here, I also make use of the static variable created in the AlarmView code behind to gain access to the datagrid so we can hook into the .Sorting event.

    public AlarmsViewModel()
    {

        if (_alarmItemRepository == null)
            _alarmItemRepository = new AlarmItemRepository();


        this.Collection = new ObservableCollection<Model.AlarmItem>(_alarmItemRepository.GetAlarmItems());
        ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.Collection);
        collectionView.SortDescriptions.Add(new SortDescription("NextSpawn", ListSortDirection.Ascending));
        var view = (ICollectionViewLiveShaping)CollectionViewSource.GetDefaultView(this.Collection);
        view.IsLiveSorting = true;

        //  Bind to the sorting event of the datagrid in the AlarmView
        AlarmsView.View.dgAlarms.Sorting += DgAlarms_Sorting;

        //  Other code
        // ...
        // ...


    }

Then, in the sorting event handling, we update the sort description when a user clicks a column header to sort the column.

    private void DgAlarms_Sorting(object sender, DataGridSortingEventArgs e)
    {

        ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.Collection);
        collectionView.SortDescriptions.Add(new SortDescription(e.Column.SortMemberPath, e.Column.SortDirection.GetValueOrDefault()));

    }

Upvotes: 4

Related Questions