Building a WPF Sudoku Game: Part 4 - Building a Least Privilege Plug-in System and Even More Custom Controls (zz)

来源:互联网 发布:ubuntu 16.04 lts中文 编辑:程序博客网 时间:2024/06/05 04:50

Building a WPF Sudoku Game: Part 4 - Building a Least Privilege Plug-in System and Even More Custom Controls

Published 30 November 06 06:19 AM | Coding4Fun 
  Building Sudoku using Windows Presentation Foundation and XAML, Microsoft's new declarative programming language. This is the 4th article from a series of 5 articles and focusses on building a least privilege plug-in system and some more custom controls.

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 fourth part of my Windows Presentation Foundation tutorial! In this tutorial we’ll be building a plug-in system for sudoku-solving algorithms, delving deeper into custom controls, including deriving from exiting controls and implementing custom properties. Also, we’ll complete more of the UI and game logic to start turning this pile of unrelated code into a game! First, let’s look at building a plug-in system: There are essentially two ways of providing an extensibility system in a .NET application:

  • Scripting: We could provide a custom programming language that plug-in authors need to write their code in.
  • Module Loading: We could dynamically load pre-compiled modules at runtime.


If we take the scripting approach we need to either create our own language, which is a lot of work and honestly, who likes to reinvent the wheel? We could also use the built-in compilation functionality in the .NET Framework to compiler, say C# code as a script, which takes less work but doesn’t allow us to create a “sandbox” language that can only perform certain actions. Plug-in modules suffer from the same issues, but they don’t require us to provide a set of tools for plug-in developers since Visual Studio is all that’s needed. With the availability of the free Visual Studio Express editions, there is really no excuse not to use precompiled modules not to mention that it’s less work and therefore less chance of creating bugs or security problems…at least that’s what I tell my boss. There’s only one flaw in this method: most of the time standard applications like our Sudoku game run as so-called “full trust” applications that means they have the full permissions of the user they are running under, which in most cases, at least on Windows XP, is the administrator. Obviously this raises a security concern. Most likely, the user trusts the Sudoku game itself, since they knowingly installed it on their system but do they trust each individual plug-in? What if, for example, the program could automatically download solver plug-ins from a central web repository; does the user trust this plug-in? One way around this is to use .NET code access security (CAS). We can partition our application into a set of “application domains” You probably don’t know it yet, but you’ve already used domains. When a .NET process starts a default domain is created. You can even access it from any of your “normal” code through the Current static property of the AppDomain class. We could just load more assemblies into the default domain but they would then run with same permissions as our application code:

This is bad for two reasons: first, the plug-in can do anything the application can. For example, if the application is allowed to write to a certain registry location by the operating system, even if the plug-in can’t break out of the Win32 limits it can still trash our game’s settings, which is bad. Second, because there is no clear segregation of code, the CLR errs on the side of caution and doesn’t allow us to unload the plug-in module since it’s impossible to reasonably determine if any portions of it are still in use. If we use the second approach:

The plug-in is sandboxed inside another domain. The only real caveat of this approach is that communication between domains requires the use of remoting, or in other words, the objects need to “self-contained” so that they can be serialized as they cross the boundary, but we’ll get back to that later.

At this point you’re probably saying “Application domains! .NET remoting! This isn’t what I signed on for! I want to make a game!” Don’t worry, most of this is automatic but it’s important to understand what’s going on so you can debug your code and so you can avoid creating security flaws. So, how do we go about actually getting this to work? First, we need to define a programming interface for plug-ins to use, to keep it simple, let’s build an interface that is exported from our SudokuFX.exe assembly that plug-in authors are required to implement:

 

namespace SudokuFX
{
public interface ISudokuSolver
{
string Name
{
get;
}

string Description
{
get;
}

string Author
{
get;
}

bool Solve(Board board);
}
}
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

This would work well in the single domain approach but unfortunately it’s not ideal for our situation. First, passing an instance of Board, while seemingly convenient, actually adds more complexity because of the serialization that occurs as an object crosses the domain boundary. This is a problem because the object contains links to event handlers and is databound to elements in the UI so it can’t be easily written to self-contained stream. Also, it’s generally a bad idea to expose unnecessary internal data to plug-in anyway. If instead of passing a Board, we say pass an ref int?[,], a simple type that exposes no implementation details can easily be serialized. Also, in order to use an object through remoting it must contain some boilerplate code, an easy way of adding this code is to derive from the MarshalByRef class. Ideally we also want to hide this detail from plug-in authors so it’s actually better to make an abstract class like this:

 

public abstract class SudokuSolver : MarshalByRefObject
{
public abstract string Name
{
get;
}

public abstract string Description
{
get;
}

public abstract string Author
{
get;
}

public abstract bool Solve(ref int?[,] board);
}

.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

That way, all plug-in classes automatically inherit the code. Wait no! STOP! It’s likely I’ve convinced you that this is the best way of doing it, but it’s not! Building something like this requires that you understand the security implications of what you’re writing. There is a subtle flaw in what I’ve just described, which I fell into the trap of the first time I wrote this code: In order to directly interact with another object through an app domain, the caller must load the other assembly to process its types. If a hostile plug-in writer embedded malicious code in the constructor of a class that is used in a static member, this code would be run when the assembly is loaded in our main domain! Instead we should build a proxy object that is defined in our main assembly, which can sit in the un-trusted domain and provide a layer between our objects. This way the un-trusted assembly will not be loaded in our full trust domain:

 

public class SudokuSolverContainer : MarshalByRefObject, ISudokuSolver
{
ISudokuSolver solver;

public void Init(Type t)
{
solver = Activator.CreateInstance(t) as ISudokuSolver;
}

public string Name
{
get { return solver.Name; }
}

public string Description
{
get { return solver.Description; }
}

public string Author
{
get { return solver.Author; }
}

public bool Solve(ref int?[,] board)
{
return solver.Solve(ref board);
}
}
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

Now, we can create our container object as our main assembly’s “ambassador” to the least-privilege domain, then call the Init method with the type of solver we want to create. This way, calls to the plug-in are still straightforward but since the class from the plug-in assembly is never accessed directly from our trusted domain, there is no need to load the assembly there.

Next, let’s start building a plug-in we can use to test our system:

In Visual Studio, right-click on the SudokuFX solution and add a new class library project called “SampleSolver”. Now, in the new project, right-click on references and select “add”. In the “projects” tab select “SudokuFX”. Also, we need to add a post-build event to the new project to copy the dll into our folder: copy "$(TargetPath)" "$(SolutionDir)SudokuFx/bin/Debug". Now, if we define a class that inherits from SudokuFX.SudokuSolver we can start building a plug-in:

 

namespace SampleSolver
{
public class SampleSolver : SudokuFX.ISudokuSolver
{
public string Name
{
get
{
return "Sample Sudoku Solver";
}
}

public string Description
{
get
{
return "This is a sample algorithm that uses a combination " +
"of logic and guess-and-check.";
}
}

public string Author
{
get
{
return "Lucas Magder";
}
}

public bool Solve(ref int?[,] board)
{
//Do stuff
return true;
}
}
}
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

Visual Studio will even set up the blank methods for you if you right-click on the “SudokuFX.SudokuSolver” and select “Implement Interface”. This is great if you’re still not 100% on how to correctly set things up. I’ll get back to how exactly my plug-in works in just a second but lets jump ahead and write some code to load it.

First, I’ve added a new field to the Window1 class:

 

AppDomain PluginDomain = null;
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

It’s important we keep a reference to our new domain because, like any other object, it can get garbage collected and if that happens all the objects in the domain die, and that’s bad. Next, I added a method to load a plug-in:

 

SudokuSolver LoadSolver(string path)
{
AppDomainSetup ads = new AppDomainSetup();
ads.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
PermissionSet ps = new PermissionSet(null);
ps.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
PluginDomain = AppDomain.CreateDomain("New AD", null, ads, ps);
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

Here, we first create an AppDomainSetup, which contains the parameters for our new domain. Right now, the only field we need to worry about is the ApplicationBase, this makes sure the loader code, which runs in the new domain can locate our executable to resolve the references our plug-in contains to it. Next, we create a new empty permission set and add only one permission to it: execution. This means the code in that domain can run but do nothing else, which means no file access, no registry, no network access, etc. Finally, we create the new domain and store a reference to it in our new field. Now, it’s time to load our DLL:

 

 
    FileStream stream = new FileStream(path,FileMode.Open,FileAccess.Read);
byte[] assemblyData = new byte[stream.Length];
stream.Read(assemblyData,0,(int)stream.Length);
stream.Close();
Assembly asm = PluginDomain.Load(assemblyData);
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

First, we read our plug-in from disk it’s important to note that were are not “loading” the assembly at this point. No initialization code executes and no processing of the data is done we are merely reading the bytes from disk. We need to do this because the module loader itself runs in our new domain, which if you recall, doesn’t have access to the file system, not even to load its own code. The actual module loading occurs in the new domain from our buffer, so there is no chance of any code “escaping” Finally, we search the assembly using reflection to find any classes that implement ISudokuSolver:

 

    Type[] ts = asm.GetTypes();
foreach (Type t in ts)
{
if (Array.IndexOf(t.GetInterfaces(),typeof(ISudokuSolver)) != -1)
{
Type containter = typeof(SudokuSolverContainer);
SudokuSolverContainer ssc = ad.CreateInstanceAndUnwrap(
containter.Assembly.FullName,containter.FullName) as SudokuSolverContainer;
ssc.Init(t);
return ssc;
}
}
return null;
}
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

The Type class is another way of representing the type of an object. Basically, imagine if you replaced all references to a type, say ArrayList, with a string variable, which you set to “ArrayList”, then at run-time you could change the string to, for example, “HashTable”, now all the code the used ArrayLists now uses HashTables, in a sense, you’ve made the type of variable, a variable itself. Ok, so if your head hasn’t exploded yet, no imagine that instead of string you used an instance of Type. Why? Well first of all string comparisons are bad form for things like this and second, Type contains a plethora of useful stuff that allows you to do things like loop through the methods of a class or inspect the inheritance tree. How do you get Types you say? Well it’s simple you just use the typeof keyword to extract it from your class. Here we search for a type that implements ISudokuSolver, then when we find one, we create an instance of it and return it. By default, creating an instance across a domain boundary creates an opaque proxy object that allows other domains to deal with types they don’t reference. Because our code does have access to the SudokuSolver base class we can unwrap the proxy to a transparent proxy. Finally, I added a new field to Window1 to contain the solver, which I then load in the Loaded event handler. Then to make it work, I added a button to the timer pane, labeled “I Give Up”, which executes the solver like this:

 

int?[,] a = Board.GameBoard.ToArray();
if (!Solver.Solve(ref a))
{
MessageBox.Show("No Solution!");
}
else
{
Board.GameBoard.FromArray(a);
}
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

Pretty straightforward eh? The proxy object does all the work of crossing the app domain. You can see that this works, if you try to put an offending line of code in the plug-in, execution stops immediately:

I also added a new board generation algorithm. Really, the one I wrote last tutorial is pretty bad. It generates lots of unsolvable grids, which if you’re strict, isn’t allowed. The real way of generating a Sudoku board is to generate a full valid board then blank out cells. To accomplish this I’ve added a new method to the Board class to generate a board based on a solver. Basically, it seeds the solver by filling in a single random cell with a random number.

 

Random rnd = new Random();
int row = rnd.Next(size);
int col = rnd.Next(size);
this[row, col].Value = rnd.Next(size)+1;
int?[,] a = this.ToArray();
s.Solve(ref a);
this.FromArray(a);

.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

Then, it runs the solver to fill the rest of the grid, which it then selectively blanks. This takes a little longer (depending on the solver) than the default method so I also added a radio button to choose the algorithm:

Ok, that’s all well and good but how did I go about writing a Sudoku solving algorithm? Well my algorithm is a recursive one. First, I build a new board structure of List<int>s that contain the possible number at each square, so for example, on givens, the list is 1 item long and contains the given number, whereas on blank squares it contains all possible numbers. Then the algorithm looks for all column, row, and box conflicts that eliminate numbers from lists until there are no more items that can be definitely removed. Then, the solver finds the shortest list with length greater than 1 in the grid and picks and random starting point in its list. Then for each possible “guess” it performs a deep copy of the board and recurses, this way it can backtrack if a certain guess results in no solution. When all guesses have been tried and no solution is found then the algorithm returns false. Alternatively, if the board is full, e.g. each list is exactly 1 long and there are no conflicts then the board is solved. You can check the code out in the download but I’ll be the first to tell you that this algorithm sucks, at least for solving the boards the program generates. I hereby challenge you to write a better one - That’s the great about supporting plug-ins, it allows you to offload functionality onto the user….err, I mean include extensibility.

Ok, so now that’s working, let’s start getting the game more playable. First we need to implement a timer, we could do this in two ways: a) create our own threaded or polling timer code or b) use the built-in WPF animation system….guess which one I’m going to cover (it really is easier, I promise). First, we need to redefine our timer display to more easily accept a bound input source:

 

<StackPanel Orientation ="Horizontal" FlowDirection ="LeftToRight">
<TextBlock x:Name ="MinNumber" FontSize ="36" FontWeight ="Bold">
</TextBlock>
<TextBlock FontSize="36" FontWeight="Bold" Text =":"/>
<TextBlock x:Name ="SecNumber" FontSize ="36" FontWeight="Bold">
</TextBlock>
<TextBlock Margin="0,5,0,0" VerticalAlignment="Center"
FontSize="24" FontWeight="Bold" Text =":"/>
<TextBlock Margin="0,5,0,0" VerticalAlignment="Center"
x:Name ="SubSecNumber"
FontSize ="24">
</TextBlock>
</StackPanel>

.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

We also need define the storyboard that will control our animation, since we don’t want the timer to associate with a particular event or control we can define it as “free floating” by putting in our <Window.Resources> tag (we’ll write the Completed event handler later):

 

 
<Storyboard x:Key ="TimerAnimation" Completed ="TimerDone">
<Int32Animation From ="1" To ="0" Storyboard.TargetName ="MinNumber"
Storyboard.TargetProperty ="Tag" Duration ="0:2:0"/>
<Int32Animation From ="59" To ="0" RepeatBehavior="Forever"
Storyboard.TargetName ="SecNumber" Storyboard.TargetProperty ="Tag"
Duration ="0:1:0"/>
<Int32Animation From ="59" To ="0" RepeatBehavior="Forever"
Storyboard.TargetName ="SubSecNumber" Storyboard.TargetProperty ="Tag"
Duration ="0:0:1"/>
</Storyboard>
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

There are a couple of important things to notice here: First, how come the durations of the second counter and the and 1/60th of a second time are only one second and one minute respectively? Well, the storyboard expands to fit the longest animation it contains. With RepeatBehavior="Forever" each animation repeats indefinitely, if we instead specify a duration, like we will in code later, then the animation will repeat within the overal duration, or in other words, if the total timer runs for 10 minutes then the second number animation will run 10 times since it counts down each second for a single minute. Second, why do the animations target the Tag property intead of Text? We have to do this because, the Text property is of type string, which cant be animated using an Int32Animation (there is no StringAnimation, in fact, could you even make one?) To make this work we can define the TextBlocks like so:

 

<TextBlock x:Name ="MinNumber" FontSize ="36" FontWeight ="Bold" 
Text ="{Binding RelativeSource={RelativeSource Self},Path=Tag}">
<TextBlock.Tag>
<s:Int32>59</s:Int32>
</TextBlock.Tag>
</TextBlock>
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

This way the control automatically displays the contents of its tag, which we initialize to an integer. Next, lets a pause button. Since this a WPF tutorial, a normal button just doesn’t cut it so let’s build a custom toggle button:

The needle in the stopwatch should animate as the timer advances, so we’ll also implement a custom dependency property that allows the needle to be animated and databound. First, add a new user control the project named “Stopwatch.xaml”, now in the .xaml and .cs files that compose the control replace the UserControl class with the ToggleButton type, this causes our control to inherit from ToggleButton instead of UserControl. Next, let’s start by defining our custom property, to do this we need to define a property description object as a static member of our class:

 

public static readonly DependencyProperty CurrentTimeProperty = 
DependencyProperty.Register("CurrentTime",
typeof(double),
typeof(Stopwatch),
new FrameworkPropertyMetadata(0.0,
FrameworkPropertyMetadataOptions.AffectsRender
)
);
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

When calling the DependencyProperty.Register method to create our descriptor we specify the name of our property (“CurrentTime”), the type it will contain ( a double), what type it is attached to (out new Stopwatch type), it’s default value (a double set to 0), and finally any extra flags. In this case we want our control to be redrawn when the property changes so we include the AffectsRender flag. This is all well and good but we still cant access the property from C# code, to make this work we also need to define a matching instance property:

 

public double CurrentTime
{
get
{
return (double)this.GetValue(CurrentTimeProperty);
}
set
{
this.SetValue(CurrentTimeProperty, value);
}
}
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

This property just wraps our WPF property in a more C# friendly package. We don’t actually require this in this particular instance, but its good practice to automatically define these properties in pairs since it avoids some cryptic “why doesn’t this work?!” situations down the road if you forget. As for the control itself, its really just a custom template, in fact if you want to get technical, we could have built this entire control as a ToggleButton style, storing the current time in the Tag property, but that’s no fun! To make the control itself work, I’ve added triggers to alter the top button image displayed (the lit one uses a another bitmap effect called OuterGlowBitmapEffect) and to “clunk” the control when it is clicked:

 

<Trigger Property ="ToggleButton.IsChecked" Value="True">
<Setter TargetName ="OffLight" Property="Visibility" Value="Hidden"/>
<Setter TargetName ="OnLight" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property ="ToggleButton.IsPressed" Value="True">
<Setter TargetName="MainGrid" Property="RenderTransform">
<Setter.Value>
<TranslateTransform X ="1" Y ="1"/>
</Setter.Value>
</Setter>
</Trigger>

.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

Because we derive from ToggleButton, we can use the existing control logic and properties. I’ll skip over most the actual XAML that defines the look of the control since you can find it in the download and we’ve covered doing basic shapes and gradients before, so the only relevant part is the definition of the needle, which is part of a larger drawing:

 

<GeometryDrawing Brush ="Red">
<GeometryDrawing.Geometry>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure IsClosed ="True" IsFilled ="True" StartPoint ="50,40">
<LineSegment Point ="51,66"/>
<LineSegment Point ="49,66"/>
</PathFigure>
</PathGeometry.Figures>
<PathGeometry.Transform>
<RotateTransform CenterX ="50" CenterY ="66"
Angle ="{Binding Path=CurrentTime, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource AngleConverter}}"/>
</PathGeometry.Transform>
</PathGeometry>
</GeometryDrawing.Geometry>
</GeometryDrawing>

.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

You can see here that the Angle property of our transform is bound to the custom CurrentTime property we defined. If you’ve been paying attention so far you’ll notice that the convention in WPF is for angles to be specified in degrees (0-360) while, completion is usually specified as a double ranging from 0-1.0. How can we multiply our CurrentTime value by 360 in the process of binding it? The answer is converters. By defining s custom converter you can perform any operation on the values as they are bound. Since we just want to multiply by a number we can use the follow converter:

 

[ValueConversion(typeof(double), typeof(double))]
public class AngleConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
return (double)value * 360;
}

public object ConvertBack(object value, Type targetType,

object parameter, CultureInfo culture)
{
return (double)value / 360;
}
}
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

Then, we just instantiate our AngleConverter class in our resources section and reference it from the binding as done above. Converters also support parameters so if we really wanted to be slick we could supply the multiplier as a parameter to a generic multiplication converter.

Just like the board control, I’ve placed the stopwatch on the main window, this time under the timer numbering:

 

<clr:Stopwatch Checked="ResumeTimer" Unchecked="PauseTimer" Margin="0,5,0,0"
x:Name="StopwatchControl" HorizontalAlignment="Stretch"/>
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

I’ve also hooked up some of standard ToggleButton events to make the button actually do something. This works transparently, again because our control derives from ToggleButton. Also added is a new animation in our timer storyboard to animate the stopwatch needle:

 

<DoubleAnimation From ="0" To ="1" Storyboard.TargetName ="StopwatchControl"
Duration="0:2:0" Storyboard.TargetProperty ="CurrentTime"/>
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

And finally a new set of radio buttons to select the timer length:

Now, in the New Game button click handler, we need to add code to start the timer:

 

Storyboard s = this.Resources["TimerAnimation"] as Storyboard;
s.Stop(this);
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

First, we get the storyboard out of the window’s resources section and stop it if it’s already running. Next, we either disable and ghost out the related control or setup the timer:

 

if (NoTimerRadio.IsChecked == true)
{
MinNumber.Tag = 59;
SecNumber.Tag = 59;
SubSecNumber.Tag = 59;
TimerControls.Opacity = 0.25;
TimerControls.IsEnabled = false;
StopwatchControl.IsChecked = false;
}
else
{
Int32 length;
if (EasyTimerRadio.IsChecked == true)
{
length = 15;
}
else if (MediumTimerRadio.IsChecked == true)
{
length = 10;
}
else
{
length = 5;
}

//the stopwatch controller
s.Children[0].Duration = new Duration(TimeSpan.FromMinutes(length));

//the minute ticker
s.Children[1].Duration = new Duration(TimeSpan.FromMinutes(length));

//the second ticker
s.Children[2].RepeatBehavior = new RepeatBehavior(TimeSpan.FromMinutes(length));

//the 1/60 second ticker
s.Children[3].RepeatBehavior = new RepeatBehavior(TimeSpan.FromMinutes(length));
((Int32Animation)s.Children[1]).From = length - 1;
StopwatchControl.IsChecked = true;
MinNumber.Tag = length - 1;
TimerControls.Opacity = 1;
TimerControls.IsEnabled = true;
s.Begin(this, true);
}
Board.IsEnabled = true;
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

And finally we begin the storyboard, setting the main window as its parent control. Then we need to define the event handler to handle the timer completion:

 

void TimerDone(object sender, EventArgs e)
{
TimerControls.Opacity = 0.25;
TimerControls.IsEnabled = false;
StopwatchControl.IsChecked = false;
if (Board.GameBoard.IsFull && Board.GameBoard.IsValid)
{
MessageBox.Show("You win!");
}
else
{
MessageBox.Show("You ran out of time! Better luck next time.");
}
}
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

And the handlers for the pressed events of the stopwatch:

 

void PauseTimer(object sender, RoutedEventArgs e)
{
Storyboard s = this.Resources["TimerAnimation"] as Storyboard;
s.Pause(this);
Board.IsEnabled = false;
}

void ResumeTimer(object sender, RoutedEventArgs e)
{
Storyboard s = this.Resources["TimerAnimation"] as Storyboard;
s.Resume(this);
Board.IsEnabled = true;
}
.csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.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 {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }

Since the storyboard supports pausing and resuming it’s dead simple! After adding some extra housekeeping code, an implementing a super-basic save game system using the built-in serialization functions in the .NET Framework to write the array representation of the game board to a file we now have a working Sudoku game! This code is included in the download so if you’re still not a .NET pro (hey, it’s ok!) you can check it out. Don’t forget to come back next time for the 5th and final article in the series were we finish off the app and sand off all the rough edges. I’ll be covering spiffy stuff like:

  • Implementing a cooler message box than MessageBox
  • Selecting and loading multiple solvers
  • Benchmarking solvers and displaying the results in a graphing control.

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

 
原创粉丝点击