如何在WPF中创建比赛计分板

问题描述:

我正在尝试开发这样的比赛计分板:

Im trying to develop a race track scoreboard like this:

问题是我不知道哪种最佳做法.实际上,我尝试创建一个不断更新的Observable集合.好吧,问题在于,当我尝试按驾驶员最佳圈数对记分牌进行动态排序时,驾驶员位置始终是静态的.

The problem is that I dont know which is the best practice to do it. Actually I tryied to create an Observable Collection that is updated constantly. Well, the problem is that when I try to sort the scoreboard dinamically by drivers best lap, the drivers position is always static.

我使用了一个CollectionViewSource和一个ListBox来按属性"BestLap"对驱动程序进行排序,但似乎仅在我第一次运行程序时才对驱动程序位置进行排序,然后不对任何内容进行排序.

I used a CollectionViewSource combined with a ListBox for sort the drivers by the Property "BestLap" but It seems that the drivers position is sorted only when I run the program for the first time, then don't sort anything.

我还尝试在视图模型中不断对ObservableCollection进行排序,使Driver类成为IComparable并创建一个新的ObservableCollection,以按bestlap对比较驱动程序进行排序.但是它认为这不是一个好习惯.

I also tryied to sort the ObservableCollection constantly in the viewmodel, making the Driver class IComparable and creating a new ObservableCollection that sorts comparing drivers by bestlap. But it think that's not a good practice.

我想找到一个做类似事情的样品,但我还没有找到.如果您对此有任何建议,请告诉我.

I would like to find a sample that does something similar but I didn't find it yet. Please let me know if you have any suggestions about how to do this.

对不起,我的英语.

非常感谢!

使用ObservableCollection(OC),例如驱动程序是正确的方法.此外,使用CollectionViewSource(CVS)也是一种好方法.在您的情况下,最终的问题是,当Source(OC)发生更改时,您的CVS才被实现.这意味着如果添加或删除了驱动程序.

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.

您想要的是在Source对象的属性(例如"BestLap")发生更改时得到通知.

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 * and other sites dealing with this problem.

  • CollectionViewSource does not re-sort on property change
  • #988 – Enabling Live Sorting in a CollectionViewSource

现在有一个可行的解决方案(从第二个链接中提取):启用"IsLiveSortingRequested"并添加一个"SortDescription",其中包含用于排序的属性.

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>

这是一个使用正确的MVVM方法的(非常简单和基本的)工作示例:

Here is a (very simple and basic) working example using a proper MVVM approach:

模型(driver.cs):

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

ViewModel.cs:

The ViewModel.cs:

public class DriverViewModel
{
    public ObservableCollection<Driver> DriverList { get; set; }

    public DriverViewModel()
    {
        DriverList = new ObservableCollection<Driver>();
    }
} 

视图(MainWindow.xaml):

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>

最后是MainWindow.cs

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://*.com/questions/1064901/random-number-between-2-double-numbers)
            driver.BestLap = random.NextDouble() * (4.0 - 1.0) + 1.0;
        }
    }