您的位置:首页 > 产品设计 > UI/UE

Building a WPF Sudoku Game, Part 2: The Board UI and Validation (zz)

2007-07-26 15:13 666 查看

Building a WPF Sudoku Game, Part 2: The Board UI and Validation

Published 06 November 06 06:46 AM | Coding4Fun




















Building Sudoku using Windows Presentation Foundation and XAML, Microsoft's new declarative programming language. This is the 2nd article from a series of 5 articles and focusses on creating the Board UI and validations.
Lucas Magder

Difficulty: Easy
Time Required: 1-3 hours
Cost: Free

Software: Visual C# 2005 Express Edition .NET Framework 3.0 Runtime Components Windows Vista RTM SDK Visual Studio Extensions for the .NET Framework 3.0 November 2006 CTP
Hardware:
Download: Download (note: Tasos Valsamidis has an updated version that supports Expression Blend here)
Note: This article has been updated to work and compile with the RTM version of the Windows SDK.
Welcome to the second part of my Windows Presentation Foundation tutorial! If you’ve missed the first part you probably want to check it out here (link) since we’ll be building on what we did last. In this tutorial I’ll be covering creating a custom control for our Sudoku board and databinding it to the game logic.

First, I think it would be a good idea to go over just what databinding in WPF means, since it’s used quite differently than in say Windows Forms or MFC. Hopefully this will give more insight in to why the internal classes are designed the way they are; you can databind anything but you can only easily databind some things. First off all, despite its name, databinding in no way involves databases, complex schemas, or any boring stuff like that. It’s really all about tying your UI to your code. We’ve all written that WinForms or VB code that synchronizes a control like a listbox or treeview with an internal array, collection, or other data structure and we all remember how painful it is. Why!? Why can’t the control just use my object for data storage instead of its own memory that I have to keep synced? Well, we don’t have to worry about that anymore because that’s how it works in WPF, in fact, controls only have their own storage if you explicitly request it and you’ll find if you write clean code you’ll almost never need it. So where am I going with this? How does it tie in? Well, if we structure our code correctly, we can have our Sudoku board control automatically display our Sudoku board object and easily enter and validate moves. To do this we are going to use a hierarchy of listboxes. Before you think I’ve gone off the deep end, it’s important to note that in WPF listboxes are controls that list anything not just strings. So, lets start hammering out the data structures by looking at a typical Sudoku board for a 9x9 game:

5
3
7
6
1
9
5
9
8
6
8
6
3
4
8
3
1
7
2
6
6
2
8
4
1
9
5
8
7
9
Actually it’s a 3x3 grid of 3x3 grids. But let’s abstract this to any size. Starting at the deepest level we have a cell itself.
public class Cell
{
public bool ReadOnly = false;
public int? Value = null;
public bool IsValid = true;
}

It’s simple, but gets the job done and maintains which cells are part of the original puzzle. Unfortunately this won’t work so well with databinding. First of all, databinding only works on properties, not fields and how can the control know when a property has changed? To make this work we have to implement the INotifyPropertyChanged interface and turn the fields into properties like this:
public class Cell: INotifyPropertyChanged
{
bool readOnlyValue = false;
public bool ReadOnly
{
get
{
return readOnlyValue;
}
set
{
if (readOnlyValue != value)
{
readOnlyValue = value;
if (PropertyChanged != null) PropertyChanged(this,
new PropertyChangedEventArgs("ReadOnly"));
}
}
}
int? valueValue = null;
public int? Value
{
get
{
return valueValue;
}
set
{
if (valueValue != value)
{
valueValue = value;
if (PropertyChanged != null) PropertyChanged(this,
new PropertyChangedEventArgs("Value"));
}
}
}
bool isValidValue = true;
public bool IsValid
{
get
{
return isValidValue;
}
set
{
if (isValidValue != value)
{
isValidValue = value;
if (PropertyChanged != null) PropertyChanged(this,
new PropertyChangedEventArgs("IsValid"));
}
}
}
#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;

#endregion
}

Ok, before you run away screaming, bear with me for a second. First, you only have to do this for properties that will be databound then changed after they are initially read. Second, there are other methods such as dependency properties that accomplish something similar that I’ll discuss later. Dependency properties support animation, metadata, and all sorts other fun stuff but they have more overhead so this way is probably best if you just want the databindings to update. All that’s going on here is firing off the PropertyChanged event when one of the properties changes, pretty simple and a good candidate for a code snippet or good old copy and paste.
Next, let’s define the inner grid:
public class InnerGrid: INotifyPropertyChanged
{
ObservableCollection<ObservableCollection<Cell>> Rows;
public ObservableCollection<ObservableCollection<Cell>> GridRows
{
get
{
return Rows;
}
}


Here we initialize the collections in the inner grid cell and populate then with Cells. What’s neat here is that not only can the framework itself use the INotifyPropertyChanged interface but so can our code. We are adding a handler to the cell’s event in order to revalidate ourselves when one of our child cells is altered.

public InnerGrid(int size)
{
Rows = new ObservableCollection<ObservableCollection<Cell>>();
for (int i = 0; i < size; i++)
{
ObservableCollection<Cell> Col = new ObservableCollection<Cell>();
for (int j = 0; j < size; j++)
{
Cell c = new Cell();
c.PropertyChanged += new PropertyChangedEventHandler(c_PropertyChanged);
Col.Add(c);
}
Rows.Add(Col);
}
}

void c_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Value")
{
bool valid = CheckIsValid();

foreach (ObservableCollection<Cell> r in Rows)
{
foreach (Cell c in r)
{
c.IsValid = valid;
}
}

isValidValue = valid;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("IsValid"));
}
}


Here we cache the result of the last IsValid value. We can do this because we now have a notification event that is fired whenever there is a possibility of that value changing.
bool isValidValue = true;
public bool IsValid
{
get
{
return isValidValue;
}
}

And finally, here we have a private validation method that actually does the work. This method simply checks if there are any duplicate cells in our inner grid square.
private bool CheckIsValid()
{
bool[] used = new bool[Rows.Count * Rows.Count];
foreach (ObservableCollection<Cell> r in Rows)
{
foreach (Cell c in r)
{
if (c.Value.HasValue)
{
if (used[c.Value.Value-1])
{
return false; //this is a duplicate
}
else
{
used[c.Value.Value-1] = true;
}
}
}
}
return true;
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}


As you can see I’ve defined the class to store its data in collections of type ObservableCollection, this is because ObservableCollection already implements INotifyCollectionChanged and INotifyPropertyChanged and that’s great because I’m lazy. I’ve essentially defined an array of arrays containing the cells in the inner grid. This works great for databinding but it kind of sucks to access the cells from C# code. To solve this I also added an indexer to the class like so:

public Cell this[int row, int col]
{
get
{
if (row < 0 || row >= Rows.Count)
throw new ArgumentOutOfRangeException("row", row, "Invalid Row Index");
if (col < 0 || col >= Rows.Count)
throw new ArgumentOutOfRangeException("col", col, "Invalid Column Index");
return Rows[row][col];
}
}

The Board class is extremely similar to the InnerGrid except it stores InnerGrids instead of Cells so I wont list the entire thing here (you can check it out, and the IsValid implementations if you download the source). I also changed the constructor and the indexer to drill-down through the two levels of containers. If you’re not sure on how this would work you should probably check out the source before you continue just so we’re on the same page.
Ok, so now we’ve written all these class it’s time to hit the XAML and make a custom control. So we need to add a new WinFX user control to the project. I’ve modified some of the properties of the user control, but basically we start with this:

<UserControl x:Class="SudokuFX.SudokuBoard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

HorizontalAlignment ="Stretch"
HorizontalContentAlignment ="Stretch"
VerticalAlignment ="Stretch"
VerticalContentAlignment ="Stretch"
Background="{StaticResource ControlGradient}"
Foreground="White">
<UserControl/>


.csharpcode {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
MARGIN: 0em
}
.csharpcode .rem {
COLOR: #008000
}
.csharpcode .kwrd {
COLOR: #0000ff
}
.csharpcode .str {
COLOR: #006080
}
.csharpcode .op {
COLOR: #0000c0
}
.csharpcode .preproc {
COLOR: #cc6633
}
.csharpcode .asp {
BACKGROUND-COLOR: #ffff00
}
.csharpcode .html {
COLOR: #800000
}
.csharpcode .attr {
COLOR: #ff0000
}
.csharpcode .alt {
MARGIN: 0em; WIDTH: 100%; BACKGROUND-COLOR: #f4f4f4
}
.csharpcode .lnum {
COLOR: #606060
}

The basic idea here is that we’ll use a series of nested ItemsControls to display our board. An ItemsControl is a generic panel that contains any number of other items, for example the ListBox control inherits from ItemsControl and adds support for things like scrolling and selection but we don’t need those. We define the outer control that holds the rows on inner squares.


<ItemsControl  ItemTemplate ="{StaticResource OuterRowTemplate}"
ItemsSource ="{Binding Path=GridRows}" x:Name ="MainList">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns ="1"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>


.csharpcode {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
MARGIN: 0em
}
.csharpcode .rem {
COLOR: #008000
}
.csharpcode .kwrd {
COLOR: #0000ff
}
.csharpcode .str {
COLOR: #006080
}
.csharpcode .op {
COLOR: #0000c0
}
.csharpcode .preproc {
COLOR: #cc6633
}
.csharpcode .asp {
BACKGROUND-COLOR: #ffff00
}
.csharpcode .html {
COLOR: #800000
}
.csharpcode .attr {
COLOR: #ff0000
}
.csharpcode .alt {
MARGIN: 0em; WIDTH: 100%; BACKGROUND-COLOR: #f4f4f4
}
.csharpcode .lnum {
COLOR: #606060
}
Ok, so this is where the fun starts. Every item that can be databound has a DataContext property that specifies the object it’s bound to. We’ll set this later from C# code but it’s important to know that it’s there because it’s the basis for the binding syntax. When specifying a data binding, if no source is explicitly set the data context is used. In this code ItemsSource, the collection containing the child items is bound to the GridRows property of the data context, which will eventually be an instance of our Board class.
Another thing of note is the use of the UniformGrid class. By default the items control lays out items by stacking them from top to bottom. Instead of this behavior we would rather have the items stacked in a single column and stretched to fill the entire space evenly. The UniformGrid container does this and so I’ve substituted it for the default using the ItemsPanel property, but in general you could use any kind of standard or custom panel here, for example to arrange the items in a ring. Also you’ll notice ItemTemplate points to a resource:

<DataTemplate x:Key ="OuterRowTemplate">
<ItemsControl ItemsSource ="{Binding}"
ItemTemplate ="{StaticResource InnerGridTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows ="1"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>


.csharpcode {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
MARGIN: 0em
}
.csharpcode .rem {
COLOR: #008000
}
.csharpcode .kwrd {
COLOR: #0000ff
}
.csharpcode .str {
COLOR: #006080
}
.csharpcode .op {
COLOR: #0000c0
}
.csharpcode .preproc {
COLOR: #cc6633
}
.csharpcode .asp {
BACKGROUND-COLOR: #ffff00
}
.csharpcode .html {
COLOR: #800000
}
.csharpcode .attr {
COLOR: #ff0000
}
.csharpcode .alt {
MARGIN: 0em; WIDTH: 100%; BACKGROUND-COLOR: #f4f4f4
}
.csharpcode .lnum {
COLOR: #606060
}
A data template describes how each item in the source list is interpreted as an item in the ItemsControl. The data context is the current item, which is the list of blocks in the row, which we bind to the ItemsSource of another ItemsControl. Currently, we have a vertical list of horizontal lists. Then, we do this all over again for the cells inside each block. You can also add other controls in the template and I’ve added a border to the InnerGridTemplate to show the boundary between cells.
Finally, we reach the innermost data template, the one for the Cell:

<DataTemplate x:Key ="CellTemplate">
<Border x:Name ="Border" BorderBrush ="DimGray" BorderThickness ="1">
<TextBlock HorizontalAlignment ="Center" VerticalAlignment ="Center"
FontWeight ="Bold" FontSize ="16" Text ="{Binding Path=Value}"/>
</Border>
</DataTemplate>


.csharpcode {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
MARGIN: 0em
}
.csharpcode .rem {
COLOR: #008000
}
.csharpcode .kwrd {
COLOR: #0000ff
}
.csharpcode .str {
COLOR: #006080
}
.csharpcode .op {
COLOR: #0000c0
}
.csharpcode .preproc {
COLOR: #cc6633
}
.csharpcode .asp {
BACKGROUND-COLOR: #ffff00
}
.csharpcode .html {
COLOR: #800000
}
.csharpcode .attr {
COLOR: #ff0000
}
.csharpcode .alt {
MARGIN: 0em; WIDTH: 100%; BACKGROUND-COLOR: #f4f4f4
}
.csharpcode .lnum {
COLOR: #606060
}

Here, we draw a lighter border around the cell then display the Value property of the current cell, which is the data context of the template. Finally, we want the entire thing to stay square as it resizes. Normally this would require a few lines of C# code but thanks to the insanely flexible databinding available in WPF we can do this with no code! All you need to do it to bind the Width property of the UserControl to its ActualHeight property, which contains the final height of the control after it’s been laid out. We can do this with the RelativeSource property, to bind to object itself instead of the data context. This sounds complicated but it’s actually really simple:


Width="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight}"


.csharpcode {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
MARGIN: 0em
}
.csharpcode .rem {
COLOR: #008000
}
.csharpcode .kwrd {
COLOR: #0000ff
}
.csharpcode .str {
COLOR: #006080
}
.csharpcode .op {
COLOR: #0000c0
}
.csharpcode .preproc {
COLOR: #cc6633
}
.csharpcode .asp {
BACKGROUND-COLOR: #ffff00
}
.csharpcode .html {
COLOR: #800000
}
.csharpcode .attr {
COLOR: #ff0000
}
.csharpcode .alt {
MARGIN: 0em; WIDTH: 100%; BACKGROUND-COLOR: #f4f4f4
}
.csharpcode .lnum {
COLOR: #606060
}

Now, we need to put the control onto our main window. First, we need to map out C# namespace, SudokuFX, to an XML namespace that we can reference from XAML. To do this we define a new namespace in the document root using this syntax:

xmlns:clr="clr-namespace:SudokuFX"


.csharpcode {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
MARGIN: 0em
}
.csharpcode .rem {
COLOR: #008000
}
.csharpcode .kwrd {
COLOR: #0000ff
}
.csharpcode .str {
COLOR: #006080
}
.csharpcode .op {
COLOR: #0000c0
}
.csharpcode .preproc {
COLOR: #cc6633
}
.csharpcode .asp {
BACKGROUND-COLOR: #ffff00
}
.csharpcode .html {
COLOR: #800000
}
.csharpcode .attr {
COLOR: #ff0000
}
.csharpcode .alt {
MARGIN: 0em; WIDTH: 100%; BACKGROUND-COLOR: #f4f4f4
}
.csharpcode .lnum {
COLOR: #606060
}

Where the xmlns:clr indicates the xml namespace. “clr” is in no way special an I could have called it
xmlns:stuff="clr-namespace:SudokuFX" or whatever I wanted. Once that’s done I can use my control just like any other. I’ve replaced the stand-in canvas with our custom control tag:


<clr:SudokuBoard HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5"/>


.csharpcode {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
MARGIN: 0em
}
.csharpcode .rem {
COLOR: #008000
}
.csharpcode .kwrd {
COLOR: #0000ff
}
.csharpcode .str {
COLOR: #006080
}
.csharpcode .op {
COLOR: #0000c0
}
.csharpcode .preproc {
COLOR: #cc6633
}
.csharpcode .asp {
BACKGROUND-COLOR: #ffff00
}
.csharpcode .html {
COLOR: #800000
}
.csharpcode .attr {
COLOR: #ff0000
}
.csharpcode .alt {
MARGIN: 0em; WIDTH: 100%; BACKGROUND-COLOR: #f4f4f4
}
.csharpcode .lnum {
COLOR: #606060
}
If you run the app now you should see this:



Not that amazing, but we haven’t bound any data yet. We can modify the C# component of the control to add a data source:
public partial class SudokuBoard : UserControl
{
public Board GameBoard = new Board(9);
public SudokuBoard()
{
InitializeComponent();
MainList.DataContext = GameBoard;
}
}


Now, it should look more like this (I’ve added the numbers just to show that it works):



You can see it rebuilds correctly to match the data, when I change from a 9x9 grid to a 16x16 grid:



Ok, so now that we’ve got that working, to close of this tutorial lets add the ability to change the numbers on the board. I’ve added a new collection property to the Cell class called PossibleValues that is populated with all the possible values when the class is created. Let’s add a context menu that’s bound to this new property so that the cell values can be changed. To do this the XAML needs to be changed like this:


<DataTemplate x:Key ="CellTemplate">
<Border x:Name ="Border" BorderBrush ="DimGray" BorderThickness ="1" Background="#00112233">
<TextBlock Focusable ="False" HorizontalAlignment ="Center" VerticalAlignment ="Center"
FontWeight ="Bold" FontSize ="16" Text ="{Binding Path=Value}">
</TextBlock>
<Border.ContextMenu>
<ContextMenu>
<ContextMenu.ItemContainerStyle>
<Style TargetType ="{x:Type MenuItem}">
<Setter Property ="Template">
<Setter.Value>
<ControlTemplate TargetType ="{x:Type MenuItem}">
<ContentPresenter x:Name="Header" ContentSource="Header"
RecognizesAccessKey="True" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ContextMenu.ItemContainerStyle>
<ListBox BorderThickness="0" Width ="35" Margin ="0"
SelectedItem ="{Binding Path=Value,Mode=TwoWay}"
HorizontalAlignment ="Stretch" VerticalAlignment ="Stretch"
DataContext ="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=DataContext}"
ItemsSource="{Binding Path=PossibleValues}"/>
</ContextMenu>
</Border.ContextMenu>
</Border>
</DataTemplate>


.csharpcode {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
FONT-SIZE: small; COLOR: black; FONT-FAMILY: consolas, "Courier New", courier, monospace; BACKGROUND-COLOR: #ffffff
}
.csharpcode PRE {
MARGIN: 0em
}
.csharpcode .rem {
COLOR: #008000
}
.csharpcode .kwrd {
COLOR: #0000ff
}
.csharpcode .str {
COLOR: #006080
}
.csharpcode .op {
COLOR: #0000c0
}
.csharpcode .preproc {
COLOR: #cc6633
}
.csharpcode .asp {
BACKGROUND-COLOR: #ffff00
}
.csharpcode .html {
COLOR: #800000
}
.csharpcode .attr {
COLOR: #ff0000
}
.csharpcode .alt {
MARGIN: 0em; WIDTH: 100%; BACKGROUND-COLOR: #f4f4f4
}
.csharpcode .lnum {
COLOR: #606060
}
The key here is the binding of the listbox to our data source. Because the context menu exists in a separate hierarchy the data context isn’t automatically inherited. We can work around this by binding the new data context to the data content of the parent element just outside our sub-hierarchy. To do this we use the RelativeSource syntax to access our TemplatedParent. Next, we bind the ItemsSource property to the new property, which populates the listbox, and then we bind the SelectedItem property to the object’s Value property. Because we specify a two-way binding, when the value of Value changes, so will the selected item in the listbox, and, when the use selects a new item through the UI, Value will updated also. A listbox is needed because a menu doesn’t have the capability to retain item selections. To work around this I added the listbox to the menu as a single item, and then altered the MenuItem Template to remove all padding and selection logic. I haven’t covered altering control templates yet, so don’t worry about how exactly that part of the code works, we’ll revisit it in the next tutorial.
Now, if you run the application, you’ll see that you can edit the displayed grid:



This is really cool because, essentially, there is no code that ties the internal data classes to our UI! The UI automatically and dynamically displays and edits an instance of our custom class. How awesome is that?
Don’t worry, we’re not done yet! Come back for my next article were we take a break from data and spiff up the coolness of the app’s look and feel. I’ll be covering cool stuff like:

Automatically selecting data templates based on properties of the object

Modifying control templates to completely customize the look, and operation of a control

Adding animations

Automatically starting and stopping animations and altering objects based on UI events or other conditions

Adding reflections and other imagery based on existing controls

See you next time!

Filed under: puzzle, gaming

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐