Reputation: 181
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 DataGrid
is 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
Reputation: 181
The answer was increadibly simple, thanks for the reference to ICollectionViewLiveShaping.IsLiveSortin
from @KornMuffin.
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