I'm trying to develop a race track scoreboard like this:
I'm not sure what the best method for this is. I tried to create an ObservableCollection
that is constantly updated; the problem is that when I try to sort the scoreboard dynamically by the drivers' best lap, the positions are always static.
I used a CollectionViewSource
combined with a ListBox
to sort the drivers by the Property BestLap
, but it seems that the drivers' positions are sorted only when I run the program for the first time, then never again.
I also tried to sort the ObservableCollection
constantly in the ViewModel
, making the Driver class IComparable
and creating a new ObservableCollection
that sorts drivers by BestLap
. I think there's a better method, however.
I tried finding a sample that does what I need but could not find one. Please let me know if you have any suggestions about how to do this.
Using an ObservableCollection (OC) of e.g. drivers is the correct approach. Furthermore using an CollectionViewSource (CVS) is a good way, too. The resulting problem in your case is, that your CVS just gets actualised when the Source (the OC) changes. This means if a driver gets added or removed.
What you want is to be notified when a property (like "BestLap") of an object of your Source changes.
There are several questions/answers on stackoverflow and other sites dealing with this problem.
Now to a possible solution (extracted from the second link): Enable "IsLiveSortingRequested" and add a "SortDescription" containing the property being utilised for sorting.
<Window.Resources>
<CollectionViewSource x:Key="cvsDrivers" Source="{Binding DriversList}" IsLiveSortingRequested="True">
<CollectionViewSource.LiveSortingProperties>
<clr:String>BestLap</clr:String>
</CollectionViewSource.LiveSortingProperties>
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="BestLap" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
EDIT:
Here is a (very simple and basic) working example using a proper MVVM approach:
Model (driver.cs):
public class Driver : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set
{
name = value;
OnPropertyChanged("Name");
}
}
private double bestLap;
public double BestLap
{
get { return bestLap; }
set
{
bestLap = value;
OnPropertyChanged("BestLap");
}
}
private int startNr;
public int StartNr
{
get { return startNr; }
set
{
startNr = value;
OnPropertyChanged("StartNr");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
The ViewModel.cs:
public class DriverViewModel
{
public ObservableCollection<Driver> DriverList { get; set; }
public DriverViewModel()
{
DriverList = new ObservableCollection<Driver>();
}
}
The View (MainWindow.xaml):
<Window.Resources>
<CollectionViewSource x:Key="CvsDriver"
Source="{Binding DriverList}"
IsLiveSortingRequested="True">
<CollectionViewSource.SortDescriptions>
<componentModel:SortDescription PropertyName="BestLap" Direction="Ascending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
<Style x:Key="DriverListBoxItemContainerStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="Padding" Value="2,0,0,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
<StackPanel Height="Auto" Orientation="Horizontal">
<TextBlock TextWrapping="Wrap" Text="{Binding BestLap, StringFormat=\{0:F2\}}"/>
<TextBlock TextWrapping="Wrap" Text="{Binding StartNr}" Margin="8,0,0,0"/>
<TextBlock TextWrapping="Wrap" Text="{Binding Name}" Margin="8,0,0,0"/>
</StackPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="true">
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="true"/>
<Condition Property="Selector.IsSelectionActive" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding Source={StaticResource CvsDriver}}"
ItemContainerStyle="{DynamicResource DriverListBoxItemContainerStyle}" />
</Grid>
And finally the MainWindow.cs
public partial class MainWindow : Window
{
private readonly DriverViewModel driverViewModel;
public MainWindow()
{
// Timer generating random BestLap double values from 1.0 to 4.0 every 5 seconds
DispatcherTimer randomlyUpdateDriverBestLapTimer = new DispatcherTimer();
randomlyUpdateDriverBestLapTimer.Interval = TimeSpan.FromSeconds(5);
randomlyUpdateDriverBestLapTimer.Tick += RandomlyUpdateDriverBestLapTimerOnTick;
driverViewModel = new DriverViewModel();
Driver driver = new Driver { BestLap = 1.2, Name = "Meyer", StartNr = 1 };
driverViewModel.DriverList.Add(driver);
driver = new Driver { BestLap = 1.4, Name = "Sand", StartNr = 2 };
driverViewModel.DriverList.Add(driver);
driver = new Driver { BestLap = 1.5, Name = "Huntelaar", StartNr = 3 };
driverViewModel.DriverList.Add(driver);
this.DataContext = driverViewModel;
InitializeComponent();
randomlyUpdateDriverBestLapTimer.Start();
}
private void RandomlyUpdateDriverBestLapTimerOnTick(object sender, EventArgs eventArgs)
{
// Important to declare Random object not in the loop because it will generate the same random double for each driver
Random random = new Random();
foreach (var driver in driverViewModel.DriverList)
{
// Random double from 1.0 - 4.0 (Source code from https://stackoverflow.com/questions/1064901/random-number-between-2-double-numbers)
driver.BestLap = random.NextDouble() * (4.0 - 1.0) + 1.0;
}
}