Search code examples
wpfsortingmvvmobservablecollection

How to create a race track scoreboard in WPF


I'm trying to develop a race track scoreboard like this:

enter image description here

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.


Solution

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