سؤال

I'm currently creating a connect-4 game in order to learn WPF & XAML. I made the UI but I'm stuck on a problem.

Below you can see an extract of the XAML code concerning the board of the game :

<Grid DockPanel.Dock="Bottom" Background="#FF1506A4" MouseLeftButtonUp="Grid_MouseLeftButtonUp_1">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            ... 5 more rows
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            ... 6 more columns
        </Grid.ColumnDefinitions>
        <Ellipse Grid.Row="0" Grid.Column="0" Fill="White" Margin="8"/>
        ... 41 more ellipses
</Grid>

The board is stored in an array of Token (an enumeration with Empty, Red and Yellow) in the class GameState.

The colors of the Ellipses are provided using the class SolidBrushColor.

My problem is that I don't know how to change the color of the ellipses according to the game model.

I guess I should use data binding but I have to convert colors from the type Token to the type SolidBrushColor before binding the data. I think it could be achieved using some DataObjectProvider objects but it seems over complicated to create 42 DataObjectProvider objects for such a simple task...

So what would be the correct solution according to the best pratices ?

هل كانت مفيدة؟

المحلول

You'll want to use some sort of ViewModel at the backend, and then leverage DataBinding.

Assuming the following (contrived) ViewModel structure that represents a Connect Four board.

BoardViewModel.cs

public class BoardViewModel
{
    public BoardViewModel()
    {
        var rand = new Random();
        Squares = Enumerable
            .Range(1, 42)
            .Select(a => new SquareViewModel() { Token = rand.Next(-1, 2) })
            .ToList();
    }

    public List<SquareViewModel> Squares { get; set; }
}

SquareViewModel.cs

public class SquareViewModel : INotifyPropertyChanged
{
    private int _Token;
    public int Token
    {
        get
        {
            return _Token;
        }
        set
        {
            if (_Token.Equals(value)) return;

            _Token = value;
            RaisePropertyChanged("Token");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void RaisePropertyChanged(string property)
    {
        var handlers = PropertyChanged;
        if (handlers != null)
        {
            var args = new PropertyChangedEventArgs(property);
            handlers(this, args);
        }
    }
}

Then you can use the following XAML to represent your board.

MainWindow.xaml

<Window 
    x:Class="ConnectFour.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    mc:Ignorable="d" 
    xmlns:ConnectFour="clr-namespace:ConnectFour"
    Title="MainWindow" Height="350" Width="525"
    d:DataContext="{d:DesignInstance Type={x:Type ConnectFour:BoardViewModel}, IsDesignTimeCreatable=True}">
    <ItemsControl
        ItemsSource="{Binding Squares}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Ellipse
                    Stroke="Magenta">
                    <Ellipse.Style>
                        <Style TargetType="Ellipse">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding Token}" Value="0">
                                    <Setter Property="Fill" Value="Black" />
                                </DataTrigger>
                                <DataTrigger Binding="{Binding Token}" Value="1">
                                    <Setter Property="Fill" Value="Red" />
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Ellipse.Style>
                </Ellipse>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <UniformGrid IsItemsHost="True" Columns="6" />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>
</Window>

The important things to note are:

  • The DataTriggers that set the colour of the Ellipses based on the value of the Token property of a Square.
  • The usage of an ItemsPanel with a UniformGrid backing it up.
  • The implementation of INotifyPropertyChanged in the SquareViewModel so that when the value of Token changes the View represents that.
  • The usage of the d:DataContext attribute, which is setting the design time DataContext of the form to an instance of BoardViewModel (which initialises itself to random tokens).

At run-time, you will want to set the DataContext of your board View to a real instance of the BoardViewModel (or whatever your ViewModel is called), but the basic idea of how to change the colour of the tokens is present.

نصائح أخرى

If Token is a class that has a color enumeration property, you can add another property of type string in Token class. In the getter of property return a valid color name string according to color enum property. With that you can bind each Ellipse's Fill property to corresponding Token's color string property without building your own converter. WPF already has a string-to-color converter built in, thats why you can specify a color string in XAML and get a right color brush when it rendered.

<Ellipse Grid.Row="0" Grid.Column="0" Margin="8"
         Fill="{Binding TokenList[0].ColorStringProperty}" />

Note that above solution is a better practice then assigning name to each of Ellipse in XAML then change the color from code. And it is applicable to your existing code without radical modification. But best practice will direct you to utilize data-binding to more extent (as also indicated in @Iain's answer), and implement MVVM design pattern. Longer way to go through but you'll find it worth the effort.

The last, you'll also need to implement INotifyPropertyChanged in Token class or any other class that has UI bound to the class object's property (you can also see an example on INotifyPropertyChanged in @Heena's answer).

Yes you should be using databinding. You should bind the Fill colour to your model and use a ValueConverter to convert between the enum and a colour. Alternatively have a Colour property on your ViewModel and bind to that directly.

I think using a Grid is the wrong approach, you should model your board as a list Tokens (in fact a list of lists, for the rows and columns) and then use an ItemsControl with a ItemsTemplate to create the board. This will mean far less repetitive XAML.

As you are trying to learn WPF and XAML, try googling the above and see how far you get.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top