Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Online Circular Chess in Silverlight, ASP.NET AJAX, WCF Web Services, and LINQ to SQL

0.00/5 (No votes)
31 Dec 2007 10  
An application for users to play Circular Chess over the internet based on Silverlight, ASP.NET AJAX, WCF Web Services, and LINQ to SQL.

board.jpg

Introduction

A few months ago, a friend of mine mentioned Circular Chess to me, and I thought it looks really cool. However, I was unable to find any online Circular Chess game. Implementing such a game with standard HTML and JavaScript would not be possible because of the box model, and I am not familiar with Flash or Java Applets, so I wasn't able to implement the game. And, then I heard about Silverlight, and watched some demos, and thought to myself what a great platform it is for the implementation of Circular Chess.

In this article, I want to discuss the methods I used for creating an online game with ASP.NET AJAX 3.5 and WCF Web Services, and with the help of LINQ to SQL and, of course, Silverlight 1.1.

If you're interested in the rules of Circular Chess, check out this site.

Background

You should be familiar with ASP.NET and databases, in general. I hope to take care of those not familiar with Web Services.

Contents

Running the Game

In order for the game to work, make sure you have the Silverlight 1.1 runtime, which you can get here, Siverlight 1.1 SDK installed, which you can get here, and ASP.NET 3.5 Extensions CTP Preview, which you can get here. Other than that, running the game is just a matter of running Default.aspx in the CircularChessWebsite, either with VS2008 or IIS. If you want to test it, you can do it with Firefox or IE. If you want to play with others, make sure they have the Silverlight 1.1 runtime installed, and put it in some server (or in your localhost, if you know how to configure it).

The Structure of a Silverlight Solution

First of all, you must be using Visual Studio 2008 (the Beta 2 worked just fine, but halfway through the writing, I passed to the complete version). Now, a Silverlight solution has to have at least two projects: The Silverlight Project (which creates a Siverlight 1.1 object) or Silverlight JavaScript Application, and an ASP.NET Website to make use of the Silverlight Project. The website doesn't necessarily need to be ASP.NET, but it is the preferred option, as we get better compatibility. I also suggest that if you can, you make an ASP.NET 3.5 Extensions Web Site, because it's easier to embed Silverlight there. If you need Web Services in your solution, create them as part of the website, because Silverlight can access them only in the same domain (unless you're using IIS).

Apart from those two main projects, you may add helper classes. Silverlight controls (or any code used by a Silverlight Project) will be made in Silverlight Class Libraries, because you won't be able to add a reference to them otherwise. In my case, I have a helper Drag and Drop object, and some helper classes used both by the Silverlight project and the ASP.NET Website. Note that if you want to include helper classes, you must make sure those classes do not include any Silverlight features (not even using directives, as some might prove to end up in runtime errors). The skeleton of such a file would be something like this:

using System;

namespace CChess
{
    public class CChessBoard
    {
        // Implementation of the class
    }
}

This is what our solution looks like:

Screenshot - solution_structure.jpg

So, that said, I'll leave the Silverlight subject for a while, and concentrate on the other parts of the application.

Using ASP.NET AJAX 3.5 with WCF Web Services

First of all, Web Services in a sentence (and believe me, I know people who think Web Services are the same as Web Applications): they're a standard way to expose information in your Web Application. Normally, you'd get access to the information in your database through the C# code in your code-behind files, but with Web Services, you can call a certain method through a URL, get your information back, and use it as you may.

So, you may be asking, why WCF Web Services? They are, after all, harder to wire up than simple ASMX Web Services. Well, there are some reasons to prefer them:

  • Their structure is better than that of ASMX Web Services.
  • You have better control of them.
  • You'll most probably get new features in favor of them in the near future.
  • A LINQ feature I'll talk about later.

So, even though today you may have a harder time building them, tomorrow you will feel happy you did.

In order to use WCF Web Services, you must configure them first to work on JSON serialization for now, as it's the only supported method (currently). To do that, you must configure your endpoint to enable client script. That is done in your web.config file, like this:

<system.serviceModel>
    <behaviors>
        <serviceBehaviors>
            <behavior name="CChess.CChessBehavior">
            <serviceMetadata />
                <serviceDebug includeExceptionDetailInFaults="True"/>
            </behavior>
        </serviceBehaviors>
        <endpointBehaviors>
            <behavior name="jsonConfiguration">
                <enableWebScript/>
            </behavior>
        </endpointBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
    <bindings>
        <webHttpBinding>
            <binding name="webBindingConfig" allowCookies="true"></binding>
        </webHttpBinding>
    </bindings>
    <services>
        <service name="CChess.CChessService" behaviorConfiguration="CChess.CChessBehavior">
            <endpoint 
                address="" 
                binding="webHttpBinding" 
                bindingConfiguration="webBindingConfig" 
                behaviorConfiguration="jsonConfiguration" 
                contract="CChess.ICChessService">
                <identity>
                    <dns value="localhost"/>
                </identity>
            </endpoint>
            <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
        </service>
    </services>
</system.serviceModel>

So, for those of you less familiar with WCF Web Services, I just gave the endpoint a behaviourConfiguration attribute, to which I refer in the endpointBehaviours, and added to it the empty enableWebScript node. Two other things to note, the endpoints have to be given a webHttpBinding for now, as it's the only one supported for our purposes, and we also added a <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/> node, which essentially gives us access to the HttpContext.Current, as if we were in a normal ASMX Web Service. For those who don't know what that is, it's the object that contains all the Session, Request, and Profile objects, just to name a few. If you're debugging, or you simply want to catch exceptions you throw, you'll also have to add the serviceDebug node I've added in the service behavior.

Another thing to take into consideration is the namespace given to the contract. That is the namespace that will later be used in the JavaScript proxy created by ASP.NET (the Microsoft AJAX Library provides an implementation of namespaces for JavaScript, if you were wondering). You do it this way:

[ServiceContract(Namespace = "CChessService")]
public interface ICChessService
{
    [OperationContract]
    void Login(CChessPlayer new_player);

    // Other Web Methods
}

One very last thing you must do, is to add an attribute to the implementation of your contract, like this:

[AspNetCompatibilityRequirements(
    RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class CChessService : ICChessService
{
    // Implementation of the Contract
}

Well, now that we're all set up for using WCF Web Services, let's get to use them!

First of all, you must make a ScriptManager element in the pages where you make use of Web Services, and add to it a reference to the Web Service, like this:

<asp:ScriptManager ID="ScriptManager1" runat="server">
    <Services>
        <asp:ServiceReference Path="~/CChessService.svc" />
    </Services>
    <Scripts>
        <asp:ScriptReference Path="~/App_Script/HelperFunctions.js" />
        <asp:ScriptReference Path="~/Default.aspx.js" />
    </Scripts>
</asp:ScriptManager>

That tells ASP.NET to create a proxy to the Web Service, which you can use in your JavaScript. Note that in VS2008, you even get Intellisense for them, which is pretty neat.

Screenshot - ajax_service_intellisense.jpg

Screenshot - ajax_service_params.jpg

My script manager also has references to two JavaScript files, which would be equal to referencing them in standard HTML, but this is the recommended way of doing this by Microsoft, so be it. One thing to note is that ASP.NET also creates a JavaScript version of classes or enums used by the Web Service, so you can make use of them like I'll show you in a moment. In order to use that proxy through JavaScript, you must first create an instance of it, and you can then call any of the methods you provided, with two callback functions, one for successful calls and another one for failed calls. Here's an example:

/// <reference path="Default.aspx" />
/// <reference path="App_Script/HelperFunctions.js" />

// Default.aspx.js

function pageLoad() 
{
    var nick_textbox = $get("NicknameInput");
    var button = $get("SubmitButton");
    
    button.onclick = LoginButtonClicked;
    // If 'enter' is pressed on nick_textbox,
    // fire whatever event is fired when button is
    // clicked
    BindTextboxToButton(nick_textbox, button); 
}

function LoginButtonClicked()
{
    var nick = $get("NicknameInput").value;
    var proxy = new CChessService.ICChessService();
    $get("ResultsDiv").innerHTML = "Please Wait...";
    var new_player = new CChessPlayer();
    new_player.player_nick = nick;
    proxy.Login(new_player, OnLoginComplete, OnLoginFailed);
}
  
function OnLoginComplete()
{
    $get("ResultsDiv").innerHTML = "Login Successful";
    window.location = "MainPage.aspx";
    // Redirect the user to the logged in page
}

function OnLoginFailed(error)
{
    $get("ResultsDiv").innerHTML = error.get_message();
}

Let's go step by step (like the name of the first ending of Detective Conan!). Because in VS2008 we have Intellisense in JavaScript, we also need a way to "include" .js files in other .js files, in order to get their variables and functions and classes and stuff in the Intellisense too. The way to do that is by using a triple-slash comment, and a reference element, like I did up in the example. pageLoad is a function called by the Microsoft AJAX Library when the page is loaded (if you're not content with the name or something, you can always hook the window.onload event yourself). There I hook up the click event of the login button (the $get function being a short for document.getElementById, again, courtesy of the Microsoft AJAX Library), and also use a little helper function I created to "bind" the textbox to that button (which does exactly what the comment says). When the button is clicked, we get the value from the textbox, and create an instance of the proxy. Note that the namespace matches the namespace given to the contract, and the proxy matches the name of the contract. We then create a new CChessPlayer (the "C" at the beginning is because I'm too lazy to write "Circular" all the time, it has nothing to do with MFC), and assign it a player_nick, much like we'd do in normal C#. We then call the Login method I showed you before, passing it the player object, and the two callbacks, so that if the login is successful, he'll be redirected; if it failed, he'll see what's his mistake.

That's more or less all there is to it really. If you were expecting a result to be returned by the method, it is passed as the first parameter to the successful callback, but because the Login method returns void, I left it empty. In case you don't get the correct Intellisense, try refreshing with Edit>Intellisense>Update JScript Intellisense. One thing I really encourage you to do is to use Firefox with Firebug installed; it's an extremely useful tool that shows you all the AJAX calls you make, has a JavaScript debugger, and much more. I find it extremely useful when developing this type of applications.

firebug_img.jpg

The Database

In this application, I'm using an SQL Server 2005 database. It's a pretty simple database, so let me introduce you to it.

Screenshot - database_relationships.jpg

The two main tables of the database are players and games. The players table contains all the players that entered the game ever. Online players are differentiated from offline players with the help of the last_activity field discussed later. The games table contains all the games ever created.

The games_players is the many-to-many table joining the games and the players. I chose this over hard-coding two players (white and black) into the games table, because this way, it's much easier to filter players, and it's more extensible. The games_moves table stores all the moves done in each game. It may seem like a bit expensive, and maybe it is, and a better solution would be to save the state of the chess board at any given moment, but I prefer it this way because it is, by far, much more flexible. For example, if I needed to implement an undo feature in the future, without storing each move, it wouldn't be possible.

The last table is used for the chat box, nothing interesting there.

LINQ to SQL

Before I begin, let me point out that this subject contains much more visual activity than our previous subject, and because of that, you may find it easier to watch some movies about it. You can find some here.

So, with ASP.NET AJAX and WCF Web Services behind us, let's jump to our next station, LINQ to SQL. As you probably already know, LINQ is a an abbreviation for Language Integrated Query, and it does a good job living up to its name. It is a mini-query language which you write from inside C#, or any other .NET language, and which lets you do queries on collections (IEnumerable objects). LINQ to SQL is a sub-category of LINQ, but instead of working on collections, it works on SQL tables. So, what are the advantages of LINQ to SQL? For starters, its syntax is simpler than the SQL one, and it produces efficient queries. You also get Intellisense through the whole process of writing the queries. It even has support for SQL Functions, and SQL Stored Procedures. But none of that is the reason I like LINQ to SQL so much. I used to spend hours at a time writing classes that each mapped to a table in my database, and writing Stored Procedures and CRUDs, and that was a terribly time consuming and boring job. But now with LINQ to SQL, and VS2008, all that is reduced to the drag and drop of a table to the visual editor! You not only get all the CRUD written for you for free, you also get extensible partial classes to which you can add any method or property you want.

Enough with the "whys", let's get to the hows! You start this by adding a "LINQ to SQL Classes" item to the App_Code folder of your application, which adds a .dbml file for you. You can then open your Server Explorer, open the connection to your database, and drag and drop your tables into the tables area. You can do the same for your Stored Procedures, only you'll drop them in the area on the right of the tables area. The end result looks like this:

LINQ to SQL

Note that, by default, the names of the classes will match the names of your tables, and the names of the attributes will match the names of the columns. If you're not content with this, you can always edit them, but make sure you never do it directly to the generated source code, as it'll be edited automatically sooner or later, but only through the visual editor. You can do that simply by double clicking on the names you want to change. For each of the Stored Procedures you created, also make sure they have the expected return value in the Properties window:

Stored Procedure Return Type

If you have problems changing the return type, it means your Stored Procedure isn't compatible with it. Now comes the moment when we become happy we chose WCF as our web service provider, because the LINQ to SQL editor provides one simple property which we can change, and all the generated classes instantly get the [DataContract], and all the properties instantly get the [DataMember] one. This means we can use the generated classes as return types from our WCF Web Services! In order to do that, change the Serialization Mode property of the DataContext from "None" to "Unidirectional" (you can see it by not having focus on any table or stored procedure).

WCF Serialization in LINQ

One last thing, before the examples. Because the classes created are partial, they can be extended (and are actually built with extension in mind). I wanted to show you an example of how it can be useful:

// From App_Code/CChessGamesPlayers.cs
using System;
using System.Web;
using System.Data.Linq;
using System.Runtime.Serialization;

public partial class CChessGamesPlayers
{
    [DataMember]
    public String player_nick
    {
        get { return CChessPlayer.player_nick; }
        set { }
    }

    partial void OnValidate(ChangeAction action)
    {
    // A player can't be inserted into a game,
    // if he's currently inside another game
        if (action == ChangeAction.Insert)
        {
            CChessDataContext cdc = new CChessDataContext();

            CChessPlayer current_player = 
              ((ProfileCommon)HttpContext.Current.Profile).CChessPlayer;
            if (current_player.IsInAGame())
            {
                throw new Exception("You're already in a game.");
            }
        }
    }
}

So, the first thing I added was a property to be serialized. Because CChessGamesPlayers is a table used for the many to many relationship of games and players, it has both a CChessGame property, and a CChessPlayer property, but neither of them are marked as DataMembers, which I find as a great thing, because if it were marked that way, it means we would get a lot of extra data that we probably won't need returned to us, and that's a waste. But, what if we wanted to return some of that data anyways? Well, that's no problem, just add a property, give it the DataMember attribute, and just return whatever you need in the get part (leave the set empty).

Another very neat feature is the use of partial methods. They're methods introduced in .NET 3.0 that are called only if an implementation is provided in another partial implementation of the class, otherwise they're totally ignored (unlike events, that are still called, and cause overhead). They're ideal for the creation of extensible classes, and LINQ to SQL makes awesome use of them. Here, we implement the OnValidate method, which is called when the object is going to be either inserted or updated to the database. If it does not pass the validation, we simply throw an exception. I prefer this way to validate my objects, rather than in the body of the method inserting them, because it makes much more sense.

OK, let's see some examples from the service:

public List<ChatMessage> GetLastNMessages(int n)
{
    CChessDataContext cdc = new CChessDataContext();
    List<ChatMessage> messages = (from msgs in cdc.ChatMessages
                                  orderby msgs.message_id descending
                                  select msgs).Take(n).ToList();

    return messages;
}

Pretty much self-explanatory for anyone used to writing SQL. You must first create a DataContext object, and then you can select from any of the tables in it (in this case, ChatMessages). Because we return a generic List, we must convert the result to List. We also don't take every record, but only n records. The SQL equivalent would be:

SELECT msgs.message_id, msgs.message_writer, msgs.message_content, msgs.sent_timestamp
FROM chat_table AS msgs
ORDER BY msgs.message_id DESC
LIMIT n

For simplicity's sake, I used the LIMIT statement that is not supported in T-SQL, but you get the point! In order to make a paging functionality, you can combine this with the Skip(n) method. OK, another example, now with Lambda Expressions (which I'll, of course, explain as well):

public CChessGamesPlayers GetYou()
{
    CChessDataContext cdc = new CChessDataContext();
    ProfileCommon profile = (ProfileCommon)HttpContext.Current.Profile;

    CChessGamesPlayers you = cdc.CChessGamesPlayers.Single(
        p => p.player_id == profile.CChessPlayer.player_id);

    return you;
}

As you can see, the tables inside the data context have some methods (here, we use Single, but there are several others) that accept a Lambda Expression as a parameter. A Lambda Expression is just a shorter version of anonymous methods introduced in .NET 2.0. After defining the parameters, you use the => operator, and write some operations. In this case, the Lambda Expression gets one player as a parameter, and checks the player's ID against another ID. It'll return all the players with that player_id. Because we're talking about an ID, it's only one or 0. But because we're also using the Single method, it must be 1, or an exception will be thrown. If Single does not meet your needs, you can choose any other one (they have self-explanatory names as well).

Because we're talking about a database, chances are you're going to want to update, insert, and delete records. Here's how to insert:

public void SendMessage(String content)
{
    CChessPlayer current_player = 
        ((ProfileCommon)HttpContext.Current.Profile).CChessPlayer;
    if (current_player == null)
    {
        return;
    }

    ChatMessage msg = new ChatMessage
    {
        message_writer = current_player.player_id,
        message_content = content,
        sent_timestamp = UnixDate.GetCurrentUnixTimestamp()
    };
    CChessDataContext cdc = new CChessDataContext();
    cdc.ChatMessages.InsertOnSubmit(msg);
    cdc.SubmitChanges();
}

Just create a new message, initialize it (note that I used here the new syntax in .NET 3.5), call its table's InsertOnSubmit method with it as a parameter, and submit changes. The very same would be done for deleting, only instead of InsertOnSubmit, call DeleteOnSubmit. If you have a collection, use InsertAllOnSubmit and DeleteAllOnSubmit, respectively.

Updating is a bit more tricky. In order for a record to be updated, the DataContext must be aware it has changed, and that happens only if we're talking about records just pulled out of the DataContext. You would then make your changes, call the SubmitChanges method, like you did with the insert and delete, and your records would be updated. But, what if there's an object you have kept around for some time, and want to update it? You have two choices: either pull it back again (with the Single method, or any other way you prefer), edit it, and submit the changes; or, alternatively, you could use Stored Procedures, like this:

public void UpdateUserActivity()
{
    CChessPlayer player = 
       ((ProfileCommon)HttpContext.Current.Profile).CChessPlayer;
    if (player == null)
    {
        throw new Exception("You're not online.");
    }

    CChessDataContext cdc = new CChessDataContext();

    // Now, the reason I've used a stored procedure instead of standard Linq
    // is because in order to get player to be traced by the DataContext,
    // I'd need to select it again, and then update it. In this case, a 
    // standard stored procedure is a far better option (I'd even say
    // an easier solution).
    int affected_rows = cdc.UpdateUserActivity(player.player_id, 
                               UnixDate.GetCurrentUnixTimestamp());
    if (affected_rows < 1)
    {
        throw new Exception("Unable to update activity.");
    }
}

And if you're interested, here's the SQL of the procedure:

ALTER PROCEDURE UpdateUserActivity
    @player_id bigint,
    @timestamp int
AS

UPDATE players
SET last_activity = @timestamp
WHERE player_id = @player_id
RETURN @@ROWCOUNT

Note that I return @@ROWCOUNT implicitly. Other than that, I think this is pretty self-explanatory.

OK, so we're done with LINQ to SQL. Let's take a break from the new functionality for a while, and take a look at how the system works!

Login and Logout

unable_username.jpg

Unlike normal websites, where a user logs in and is authorized for certain things, and can then logout, this website needs to keep track of the users at real time. Imagine what would happen if a player disappeared in the middle of a game, and his opponent didn't even know! So, my method for keeping track of the user is by having a unix_timestamp column in my database that is updated at real time. For those not familiar with Unix Timestamps, they're the number of seconds elapsed since January 1, 1970, at 00:00. Having come from a PHP background, I find it really easy to use them. The way to check if a user is logged off would be that the current timestamp minus the last activity timestamp is greater than a certain amount. For those of you who prefer code to words, here you go:

// Short version of MainPage.aspx.js

function pageLoad() 
{
    GetOnlinePlayers();
    UpdateUserActivity();
    GetLastestFiveMessages();
    GetCurrentGamePlayers();
    
    BindTextboxToButton($get("NewMessageInput"), 
                        $get("MessageSubmitButton"));
}

function GetOnlinePlayers()
{
    var proxy = new CChessService.ICChessService();
    proxy.GetOnlineUsers(OnlinePlayersGotten);
}

function OnlinePlayersGotten(result)
{
    var found;
    for(var i = 0; i < result.length; i++)
    {
        found = false;
        
        for(var j = 0; j < online_players.length; j++)
        {
            if(result[i].player_id == online_players[j].player_id)
            {
                found = true;
                break;
            }
        }
        if(!found)
        {
            AddUser(result[i]);
        }
    }
    
    for(var i = 0; i < online_players.length; i++)
    {
        found = false;
        
        for(var j = 0; j < result.length; j++)
        {
            if(online_players[i].player_id == result[j].player_id)
            {
                found = true;
                break;
            }
        }
        if(!found)
        {
            RemoveUser(online_players[i]);
        }
    }
    
    online_players = result;
    setTimeout("GetOnlinePlayers()", 5000);
}

function AddUser(user)
{
    var tmp_div = document.createElement("div");
    tmp_div.id = "user_" + user.player_id.toString();
    tmp_div.innerHTML = user.player_nick;
    $get("OnlinePlayers").appendChild(tmp_div);
}

function RemoveUser(user)
{
    $get("OnlinePlayers").removeChild($get("user_" + user.player_id));
}

function UpdateUserActivity()
{
    var proxy = new CChessService.ICChessService();
    // Updates the last activity so you don't get offline
    proxy.UpdateUserActivity(UserActivityUpdated, FailedActivityUpdate);
}

function UserActivityUpdated()
{
    // Delay the next update 20 seconds
    setTimeout("UpdateUserActivity()", 20000);
}

function FailedActivityUpdate()
{
    $get("MessageDiv").innerHTML = "You have gone offline. Please login again.";
    // Wait 5 seconds for the user to read the message, then redirect him
    setInterval("window.location = 'Default.aspx';", 5000);
}

And the relevant service methods:

public void Login(CChessPlayer new_player)
{
    CChessDataContext cdc = new CChessDataContext();

    new_player.last_activity = UnixDate.GetCurrentUnixTimestamp();
    cdc.CChessPlayers.InsertOnSubmit(new_player);
    cdc.SubmitChanges();
    // Store the player in his profile
    ProfileCommon profile = (ProfileCommon)HttpContext.Current.Profile;
    profile.CChessPlayer = new_player;
}

public void UpdateUserActivity()
{
    CChessPlayer player = 
      ((ProfileCommon)HttpContext.Current.Profile).CChessPlayer;
    if (player == null)
    {
        throw new Exception("You're not online.");
    }

    CChessDataContext cdc = new CChessDataContext();

    // Now, the reason I've used a stored procedure instead of standard Linq
    // is because in order to get player to be traced by the DataContext,
    // I'd need to select it again, and then update it. In this case, a 
    // standard stored procedure is a far better option (I'd even say
    // an easier solution).
    int affected_rows = cdc.UpdateUserActivity(player.player_id, 
                            UnixDate.GetCurrentUnixTimestamp());
    if (affected_rows < 1)
    {
        throw new Exception("Unable to update activity.");
    }
}

public List<CChessPlayer> GetOnlineUsers()
{
    CChessDataContext cdc = new CChessDataContext();

    return cdc.GetOnlinePlayers(CChessPlayer.LEGAL_INACTIVITY_SECONDS, 
           UnixDate.GetCurrentUnixTimestamp()).ToList<CChessPlayer>();
}

And for completeness sake, the GetOnlinePlayers Stored Procedure SQL:

ALTER PROCEDURE GetOnlinePlayers
    @legal_inactivity_seconds int,
    @unix_timestamp int
AS
SELECT *
FROM players
WHERE @unix_timestamp - last_activity < @legal_inactivity_seconds

So, when the page loads, I call all the functions that update the UI regularly. They get the data back, update the UI, and call themselves again with a certain interval (as big as it can get, and still be natural).

Note that we could have very easily made a regular registration and login system, and the implementation would have changed much, and that's how you'd probably do it in a complete website, but because this is only a demo, I kept it as anonymous. Note that I used the Profile object to store data necessary across requests (again, I got access to it through the HttpContext.Current, which I enabled at the beginning).

Creating New Games

So, before starting to talk about Silverlight in the application, I'll review the creation of new games quickly. Any user who wants to start a new game can create it, and he'll be black or white, depending on his choice. After that, he's redirected to the Circular Chess Board, where he waits for someone to join him, and when someone does, they can start playing! One nice thing I wanted to point out was the function that creates the new game:

<a href="javascript: CreateNewGame(CChessGameColors.White);">Create new game as white</a> 
<a href="javascript: CreateNewGame(CChessGameColors.Black);">Create new game as black</a>

As you can see, we call the function with the enums we created in the server side. I think it's really neat that even those get translated by the proxy.

Silverlight

Even though I named the article Online Circular Chess in Silverlight, I get to talk about it only now... Oh well, here we go! At the introduction, I kind of compared Silverlight to Flash and Java Applets, and the truth is that Silverlight is Microsoft's competence towards Adobe's Flash. Because I don't have time to learn ActionScript and Flash from scratch, I learned Silverlight. In this article, I'll talk about the more technical parts of Silverlight, rather than artistic ones (because I'm not an artistic person). If what you need is to create complex XAML (Extensible Application Markup Language, the language that lets you be artistic in Silverlight) to decorate your application, download Expression Blend 2 here (at the moment of the writing of this article being the December Preview trial), and start playing with it. Also, take in mind that right now, there are no common controls in Silverlight, that means no Textboxes, no Buttons, Combo Boxes, etc. A few were released in September (which you can get here), but you can expect a full release in the first quarter of 2008. So, again, before beginning, I wanted to point you out to some great videos from which I learned a lot, you can get them here.

Silverlight Basics

A Silverlight 1.1 project has, by default, the following files:

  • A TestPage.html page, in which you embed your Silverlight object for testing.
  • A TestPage.html.js which defines a createSilverlight() function, which embeds the Silverlight with the parameters specified in it.
  • A Silverlight.js file, that's a helper file by Microsoft for embedding Silverlight.
  • Page.xaml, in which you define the elements of your Silverlight object (TextBlocks, Rectangles, etc.).
  • Page.xaml.cs, it works as a code-behind for Page.xaml, it's used pretty much like a code-behind is in ASP.NET.
  • You may also add custom controls.

Let's explore each of the files.

<Canvas
        xmlns="http://schemas.microsoft.com/client/2007"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Name="parentCanvas"
        x:Class="CircularChess.Page;assembly=ClientBin/CircularChess.dll"
        xmlns:CChessControls=
         "clr-namespace:CircularChess;assembly=ClientBin/CircularChess.dll"
        Width="800"
        Height="600"
        >

  <TextBlock Canvas.Left="5" Canvas.Top="3" x:Name="my_textblock" 
             Canvas.ZIndex="1" Text="Loading..." Foreground="#FFFFFF" />
  <CChessControls:PlayerPicture Canvas.Top="10" Canvas.Left="650" 
             x:Name="opponent_textblock"></CChessControls:PlayerPicture>
  <CChessControls:PlayerPicture Canvas.Top="450" Canvas.Left="650" 
             x:Name="your_textblock"></CChessControls:PlayerPicture>

</Canvas>

That is from ChessBoardPage.xaml, by default called Page.xaml. Self explanatory, isn't it? Well, everything is wrapped in a Canvas. We have a TextBlock where we display messages, and two custom controls that represent the white king with the name of the player under it. I'll explain the syntax for adding the controls, later in the article.

Let me talk about some of the attributes. The Canvas.Top and Canvas.Left attributes tell the elements where to be positioned in relation to their containing Canvas, and if for some reason, you were to take them out of the Canvas, they'd stop working. Note that everything is absolutely positioned (unlike the DOM). If you want a certain element to have a greater Z-Index than another, you do it with the Canvas.ZIndex property (which works the same as the CSS z-index property). There's also the x:Name attribute, which simply lets us reference the attribute in the code-behind by the name specified. We'll see more of it when we see the code-behind. For now, let's check TestPage.html and TestPage.html.js:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<!-- saved from url=(0014)about:internet -->
<head>
    <title>Silverlight Project Test Page </title>
    
    <script type="text/javascript" src="Silverlight.js"></script>
    <script type="text/javascript" src="TestPage.js"></script>
    <style type="text/css">
        body
        {
            padding: 0;
            margin: 0;
        }
        
        .silverlightHost { width: 800px; height: 800px; }
    </style>
</head>

<body>
    <div id="SilverlightControlHost" class="silverlightHost" >
        <script type="text/javascript">
            createSilverlight();
        </script>
    </div>
</body>
</html>

TestPage.html.js:

// JScript source code

//contains calls to silverlight.js, example below loads Page.xaml
function createSilverlight()
{
    Silverlight.createObjectEx({
        source: "ChessBoardPage.xaml",
        parentElement: document.getElementById("SilverlightControlHost"),
        id: "SilverlightControl",
        properties: {
            width: "100%",
            height: "100%",
            version: "1.1",
            enableHtmlAccess: "true"
        },
        events: {}
    });
       
    // Give the keyboard focus to the Silverlight control by default
    document.body.onload = function() {
      var silverlightControl = document.getElementById('SilverlightControl');
      if (silverlightControl)
      silverlightControl.focus();
    }

}

As you can see, TestPage.html is extremely simple. The only thing worth noting is that you must give the element containing the Silverlight object a width and height for it to be visible. We do it in the CSS at the top of the page. TestPage.html.js is pretty simple too. It uses the Silverlight.js created for us by VS2008 to create a simple createSilvelight function. It uses the static method in the Silverlight class with some self-explanatory parameters that embed the Silverlight into the page. Now, in order to embed the Silverlight in an existing website (because I doubt you'll embed it in the TestPage.html), you'd have to add a Silverlight Link to the Silverlight Project you have.

Add Silverlight Link

It'll copy Page.xaml into the website (note that VS2008 takes care of keeping it up to date with any changes you make to it in the Silverlight Project). You can then manually copy TestPage.html.js and Silverlight.js, and embed the Silverlight as shown above. Note that I didn't use this approach in order to embed the Silverlight into my page in the website though. I wanted to show you this approach because chances are your server doesn't yet have ASP.NET 3.5 Extensions installed, and if you want to embed Silverlight in your website, this would be the only way you have. If you do have access to the Extensions though, embedding Silvelight is as easy as inserting any other ASP.NET control into your page. After putting a ScriptManager in your page, just add a <asp:Silverlight runat="server" ID="Silverlight ID" Width="800" Height="800" Version="1.1" Source="~/Path/to/XAML.xaml" /> anywhere in your page, and you're ready to go!

Hooking Events on Silverlight Controls

This is quiet trivial, but I felt bad not to mention it at all. Hooking up an event on a Silverlight control is the same as hooking up an event on a WinForms application, or even on a DOM element. You can hook mouse and keyboard events on objects that derive from FrameworkElement. You can find an example in my DragAndDrop class:

public void MakeDraggable(FrameworkElement elem)
{
    if (_root == null)
    // Load the root
    {
        FrameworkElement tmp = elem.Parent as FrameworkElement;
        while (tmp.Parent != null)
        {
            tmp = tmp.Parent as FrameworkElement;
        }
        _root = tmp as FrameworkElement;
        _root.MouseMove += new MouseEventHandler(MouseMove);
        _root.MouseLeftButtonUp += new MouseEventHandler(MouseLeftButtonUp);
    }

    elem.MouseLeftButtonDown += new MouseEventHandler(MouseLeftButtonDown);
}

public void RemoveDraggable(FrameworkElement elem)
{
    elem.MouseLeftButtonDown -= MouseLeftButtonDown;
}

void MouseLeftButtonDown(object sender, MouseEventArgs e)
{
    FrameworkElement elem = sender as FrameworkElement;
    FrameworkElement parent = (FrameworkElement)elem.Parent;
    _current_element = elem;
    _starting_point = new Point((double)elem.GetValue(Canvas.LeftProperty), 
                                (double)elem.GetValue(Canvas.TopProperty));
    
    // Do Work.......
}

WCF Web Services in Silverlight 1.1

So we have the Silverlight embedded into the website, the next step is to create a proxy to the web service. And this is were Silverlight gets ugly, and that's because, for some reason, VS2008 is unable to create a proxy for WCF web services, although it works great with ASMX web services. I read that some people actually got it to work pretty easily, but no matter how many times I tried, it just doesn't work, and I know I'm not the only one. Well, fortunately, there's a workaround, and while not really nice, it works for now, until this bug, if that's indeed what it is, gets fixed. First, create an ASMX web service that implements only the "skeleton" of the web service. You can easily do this in VS2008 by implementing the contract interface and adding the [WebMethod] attribute to all the generated methods, like this:

Implement Contract

After that, back in your Silverlight Project, right click in your project, and select "Add Web Reference".

Add Web Reference

In the dialog that opens, go to "Web Services in this Solution", and select your ASMX web service, and not you SVC web service (don't worry, we'll end up using the SVC one).

Screenshot - asmx_web_service.jpg

You may now give the reference a name, click on Add Reference, and the proxy will be created for you. Now, luckily for us, the proxy created for ASMX web services works exactly the same for WCF Web Services. The only thing we'd need to do is upon creation of the proxy, we'll change the URL of the service to the SVC one, like this:

CChessProxy.CChessSkeleton proxy = new CChessProxy.CChessSkeleton();
proxy.Url = "CChessService.svc";
proxy.DoWork();

The Code-Behind File and Using the Proxy

Let's first bring the code-behind file, ChessBoardPage.xaml:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Collections.Generic;
using Silverlight.Controls;
using CChess.Controls;
using System.Windows.Browser;
using System.Net;
using CircularChess.CChessProxy;

namespace CircularChess
{
    public partial class Page : Canvas
    {
        public Page()
        {
            Loaded += new EventHandler(Page_Loaded);
            current_selected_square = null;
            _last_move = new CChessMove() { move_id = 0 };

            proxy = new CircularChess.CChessProxy.CChessSkeleton();
            // Because the proxy was generated
            // with an ASMX web service, we must
            // redirect it to the WCF web service.
            proxy.Url = "CChessService.svc";

            drag_drop = new DragAndDrop { CenterOnMouseDown = true };
            drag_drop.CustomMouseDown += 
              new DragAndDropEventHandler(_drag_drop_CustomMouseDown);
            drag_drop.CustomMouseMove += 
              new DragAndDropEventHandler(_drag_drop_CustomMouseMove);
            drag_drop.CustomMouseUp += 
              new DragAndDropEventHandler(_drag_drop_CustomMouseUp);
        }

        #region Fields

        private DragAndDrop drag_drop;
        private CChessSquare current_selected_square;
        private CChessBoard my_chessboard;
        private CChessProxy.CChessGamesPlayers you;
        private CChessProxy.CChessGamesPlayers opponent;
        private CChessProxy.CChessSkeleton proxy;
        private HtmlTimer opponent_get_timer;
        private HtmlTimer new_move_timer;
        private CChessMove _last_move;
        private const int tile_img_width = 159;
        private const int tile_img_height = 159;
        private const String tile_img_url = "images/wood7.jpg";

        #endregion

        public void Page_Loaded(object o, EventArgs e)
        {
            // Required to initialize variables
            InitializeComponent();

            proxy.BeginGetYou(new AsyncCallback(YouGotten), null);
            proxy.BeginGetOpponent(new AsyncCallback(OpponentGotten), null);

            opponent_get_timer = new HtmlTimer();
            opponent_get_timer.Interval = 5000;
            opponent_get_timer.Tick += new EventHandler(opponent_timer_Tick);

            new_move_timer = new HtmlTimer();
            new_move_timer.Interval = 4000;
            new_move_timer.Tick += new EventHandler(_new_move_timer_Tick);
            new_move_timer.Start();

            SetTileBackground();
        }

        public void SetTileBackground()
        {
            Image tmp;
            for (int i = 0; tile_img_width * i < Width; i++)
            {
                for (int j = 0; tile_img_height * j < Height; j++)
                {
                    tmp = new Image();
                    tmp.SetValue(Canvas.ZIndexProperty, -1);
                    tmp.SetValue(Canvas.LeftProperty, i * tile_img_width);
                    tmp.SetValue(Canvas.TopProperty, j * tile_img_height);
                    tmp.Source = new Uri(tile_img_url, UriKind.RelativeOrAbsolute);
                    Children.Add(tmp);
                }
            }
        }

        public void YouGotten(IAsyncResult iar)
        {
            you = proxy.EndGetOpponent(iar);
            your_textblock.Text = you.player_nick;

            proxy.BeginGetAllMoves(you.game_id, AllMovesGotten, null);
        }

        public void AllMovesGotten(IAsyncResult iar)
        {
            CChessMove[] moves = proxy.EndGetAllMoves(iar);
            CChess.CChessBoard board = new CChess.CChessBoard();
            for (int i = 0; i < moves.Length; i++)
            {
                board.MovePiece(moves[i].piece_id, moves[i].destination_ring, 
                                moves[i].destination_square);
                _last_move = moves[i];
            }

            Color your_color = (you.color == (byte)CChess.CChessColors.White) ? 
                                CChessBoard.white_color : CChessBoard.black_color;
            my_chessboard = new CChessBoard(your_color, 275, 100, board);
            my_chessboard.SetValue<double>(Canvas.TopProperty, 0);
            my_chessboard.SetValue<double>(Canvas.LeftProperty, 
                                          (Width / 2 - my_chessboard.Width / 2));
            my_chessboard.SetValue<double>(Canvas.TopProperty, 
                                          (Height / 2 - my_chessboard.Height / 2));
            my_chessboard.OnLoad += new CChessBoard.NoParametersEventHandler(
                                                    _my_chessboard_OnLoad);
            Children.Add(my_chessboard);

            my_textblock.Text = "Ready.";
        }

        void _my_chessboard_OnLoad()
        {
            foreach (CChessPiece piece in my_chessboard.Pieces)
            {
                if (piece.IsAlive)
                {
                    drag_drop.MakeDraggable(piece);
                }
            }

            foreach (CChessSquare square in my_chessboard.Squares)
            {
                square.MovingAnimationComplete += 
                   new EventHandler(piece_storyboard_Completed);
            }

            SelectCurrentPlayer();
        }

        void SelectCurrentPlayer()
        {
            if ((byte)my_chessboard.CurrentMoveColor == you.color)
            {
                opponent_textblock.UnselectPlayer();
                your_textblock.SelectPlayer();
            }
            else
            {
                your_textblock.UnselectPlayer();
                opponent_textblock.SelectPlayer();
            }
        }

        void _new_move_timer_Tick(object sender, EventArgs e)
        {
            if (opponent != null && (byte)my_chessboard.CurrentMoveColor != you.color)
            {
                new_move_timer.Stop();
                proxy.BeginGetLastMove(_last_move.move_id, 
                      new AsyncCallback(LastMoveGotten), null);
            }
        }

        public void LastMoveGotten(IAsyncResult iar)
        {
            CChessProxy.CChessMove last_move = proxy.EndGetLastMove(iar);
            if (last_move != null && _last_move != null && 
                last_move.move_id != _last_move.move_id)
            {
                _last_move = last_move;

                CChessPiece the_piece = my_chessboard.Pieces[last_move.piece_id - 1];
                CChessSquare the_square = 
                  my_chessboard.Squares[last_move.destination_ring - 1, 
                                        last_move.destination_square - 1];

                SetPieceOnTop(the_piece);

                the_piece.Square.AnimatePieceToSquare(the_square);
            }
            else
            {
                new_move_timer.Start();
            }
        }

        void piece_storyboard_Completed(object sender, EventArgs e)
        {
            SetPieceOnBottom(my_chessboard.Pieces[_last_move.piece_id - 1]);

            my_chessboard.MovePiece(_last_move.piece_id, _last_move.destination_ring, 
                                    _last_move.destination_square);
            SelectCurrentPlayer();
            new_move_timer.Start();
        }

        void opponent_timer_Tick(object sender, EventArgs e)
        {
            proxy.BeginGetOpponent(new AsyncCallback(OpponentGotten), null);
            opponent_get_timer.Stop();
        }

        public void OpponentGotten(IAsyncResult iar)
        {
            CChessProxy.CChessGamesPlayers p = proxy.EndGetOpponent(iar);
            if (p == null)
            {
                if (opponent == null)
                {
                    opponent_textblock.Text = "Waiting for someone to join.";
                }
                else
                {
                    opponent_textblock.Text = opponent.player_nick + " has left.";
                }
            }
            else
            {
                opponent = p;
                opponent_textblock.Text = opponent.player_nick;
            }

            opponent_get_timer.Start();
        }

        /// <summary>
        /// Sets a high z-index on the piece
        /// </summary>
        void SetPieceOnTop(CChessPiece piece)
        {
            CChessSquare square = piece.Square;
            // Keeps the piece on top while dragging it.
            square.SetValue<int>(Canvas.ZIndexProperty, 1);
        }

        /// <summary>
        /// Sets a low z-index on the piece
        /// </summary>
        void SetPieceOnBottom(CChessPiece piece)
        {
            CChessSquare square = piece.Square;
            square.SetValue<int>(Canvas.ZIndexProperty, 0);
        }

        void LastMoveSent(IAsyncResult iar)
        {
            try
            {
                _last_move = 
                 new CChessMove() { move_id = proxy.EndMovePiece(iar) };
                SelectCurrentPlayer();
            }
            catch (WebException ex)
            {
                my_textblock.Text = ex.GetBaseException().Message;
            }
        }
    }
}

(I left out the drag and drop region, I'll discuss it later). OK, let's explore this little by little. In the constructor, I initialize the fields of the page, between them the proxy like I described in the previous section, and hook the Loaded event of the Canvas. In Page_Loaded, I start asynchronous calls to the web service. When the proxy is created, not only are regular methods created, also asynchronous versions of them are added. Those have the "Begin" prefix, and "End" prefix. Asynchronous calls are preferred to normal calls because they don't stuck the browser while waiting for a response. In order to make such calls, call the "Begin" version of the method you want (for example, BeginGetOpponent), and give it an AsyncCallback delegate (in this case, we gave it the OpponentGotten(IAsyncResult iar) method). When the result arrives, the method you provided gets called. There, you call the "End" version of the method (EndGetOpponent, in our case), and retrieve the result of the call to the service. Now, because we're in an online game, these calls must be repeated constantly, but we'd also want to make intervals as not to overkill the server. We can manage this with an HtmlTimer object, and, while the compiler will tell you that the object is obsolete, it works perfectly well for our purposes, so until we get a better alternative, we'll stick to it. After you create a new Timer, give it an Interval, hook an event to call when the timer ticks, and call the Start method. When the timer ticks, stop it, do whatever you need, and start it again, and thus you successfully create intervals between calls to the web service in Silverlight.

Another thing to note in the code-behind, is how we add and manipulate controls programmatically. Take a look at the AllMovesGotten method. There, we create a new CChessBoard with its constructor, set its Canvas.TopProperty and Canvas.LeftProperty, and add it to the Children collection of the Page (that is a Canvas), that in it's turn, adds it to the UI.

Custom Silverlight Controls

In order to add a new control, add a new Silverlight User Control item. Let's take a look at the PlayerPicture control. First the XAML:

<Canvas xmlns="http://schemas.microsoft.com/client/2007" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Background="#442233"
        Width="100"
        >

  <Image Source="images/king_picture.png" x:Name="player_img"></Image>
  <!-- Set the Textblock under the image -->
  <TextBlock Canvas.Top="85" x:Name="player_name" 
      Foreground="#FFFFFF" TextWrapping="Wrap" Width="100"></TextBlock>
  <Rectangle x:Name="border" Canvas.Top="0" Canvas.Left="0"></Rectangle>
  
</Canvas>

And the code-behind:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace CircularChess
{
    public class PlayerPicture : Control
    {
        public PlayerPicture()
        {
            System.IO.Stream s = 
              this.GetType().Assembly.GetManifestResourceStream(
              "CircularChess.PlayerPicture.xaml");
            _canvas = (Canvas)this.InitializeFromXaml(new 
                              System.IO.StreamReader(s).ReadToEnd());
            _player_name = (TextBlock)_canvas.FindName("player_name");
            _player_img = (Image)_canvas.FindName("player_img");
            _border = (Rectangle)_canvas.FindName("border");

            _player_img.Width = img_width;
            _player_img.Height = img_height;
            _canvas.Height = img_height - 10;

            UpdateLayout();
        }

        Canvas _canvas;
        TextBlock _player_name;
        Image _player_img;
        Rectangle _border;
        const int img_width = 100;
        const int img_height = 100;
        const int border_width = 2;

        public String Text
        {
            get { return _player_name.Text; }
            set 
            { 
                _player_name.Text = value;
                _player_name.SetValue(Canvas.LeftProperty, 
                                     (img_width - _player_name.ActualWidth) / 2);
                UpdateLayout();
            }
        }

        private void UpdateLayout()
        {
            _canvas.Height = img_width + _player_name.ActualHeight - 10;
            _border.Width = _canvas.Width;
            _border.Height = _canvas.Height;
        }

        public void SelectPlayer()
        {
            _border.Stroke = new SolidColorBrush(Colors.Cyan);
            _border.StrokeThickness = border_width;
        }

        public void UnselectPlayer()
        {
            _border.StrokeThickness = 0;
        }
    }
}

If you take a look at the constructor, it has two lines, that draw the XAML from the final DLL, and initialize the control with it. You could as well pass the XAML string yourself, if you prefer it that way. The InitializeFromXaml method also returns a reference to the main control in the XAML, in our case a Canvas, so we save it in a _canvas field. In order to programmatically add elements to the control, you call the _canvas.Children.Add. In fact, every change done to the control must be done to _canvas. Note that in these controls, you must use FindName on the Canvas to get its children (it's not hard-coded as in the main page).

In order to embed the control into another XAML, as opposed to adding it programmatically, you have to define a namespace and give it the DLL in which the control resides. Let me bring back the Canvas from the main page for you to see it:

<Canvas
        xmlns="http://schemas.microsoft.com/client/2007"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Name="parentCanvas"
        x:Class="CircularChess.Page;assembly=ClientBin/CircularChess.dll"
        xmlns:CChessControls=
         "clr-namespace:CircularChess;assembly=ClientBin/CircularChess.dll"
        Width="800"
        Height="600"
        >
 </Canvas>

In this case, the namespace is CChessControls. We specify the namespace in which the controls reside inside the assembly by clr-namespace:namespace, and then the path to the assembly. You could then add elements, like this:

<CChessControls:PlayerPicture Canvas.Top="10" 
       Canvas.Left="650" x:Name="opponent_textblock">
</CChessControls:PlayerPicture>

Drag and Drop in Silverlight

Screenshot - drag_and_drop.jpg

So, I've browsed the internet for a good and extensible Drag and Drop library for Silverlight, but I was unable to find one, so I created one myself. The difference between my Drag and Drop and the Drag and Drop developed in the demos at MSDN or the Silverlight video tutorials is that I hook the MouseMove event to the parent canvas, as opposed to the moving object, and the LeftMouseButtonUp event to the parent canvas too, and thus I cancel the possibility of the dragged element being dropped in the middle because the mouse left it or something. I also cancel the scenario where the mouse leaves the Silverlight object, and the dragged object gets thrown in some edge somewhere. I also added some custom events to customize it. I wanted to explain a bit how it works, in case you're interested in using it.

First, you'd need to add a reference to the DragAndDrop project. Click File->Add->Existing Project, and select DragAndDrop.csproj in the DragAndDrop project file (the reason for making it a separate project was to ease the process of including it in other projects). Then, right click in your Silverlight Project, select Add Reference, and in the Projects tab, select the DragAndDrop project. Now, you're ready to go.

In order to use the DragAndDrop, create a new instance of it, and if you want, add some events, like this:

namespace CircularChess
{
    public partial class Page : Canvas
    {
        public Page()
        {
            Loaded += new EventHandler(Page_Loaded);

             // When a draggable element is clicked,
             // center it relatively to the mouse
            _drag_drop = new DragAndDrop { CenterOnMouseDown = true };
            _drag_drop.CustomMouseDown += 
               new DragAndDropEventHandler(_drag_drop_CustomMouseDown);
            _drag_drop.CustomMouseMove += 
               new DragAndDropEventHandler(_drag_drop_CustomMouseMove);
            _drag_drop.CustomMouseUp += 
               new DragAndDropEventHandler(_drag_drop_CustomMouseUp);
        }

        private DragAndDrop _drag_drop;
    }
}

Note that I support centering the dragged element in relation to the mouse. To turn this feature on, set the CenterOnMouseDown property of the DragAndDrop object to true (note that again, I used the new initializing manner in .NET 3.5, but you could certainly do it the old way). In order to make an element draggable, call the MakeDraggable method of the _drag_drop object with that element as its parameter.

void _my_chessboard_OnLoad()
{
    foreach (CChessPiece piece in _my_chessboard.Pieces)
    {
        if (piece.IsAlive)
        {
            _drag_drop.MakeDraggable(piece);
        }
    }
}

Now, you use the custom events in the following manner:

#region Drag and Drop Events Region

void _drag_drop_CustomMouseDown(FrameworkElement sender, 
                                DragAndDropEventArgs args)
{
    CChessPiece piece = sender as CChessPiece;
    CChessSquare square = piece.Square;
    // Keeps the piece on top while dragging it.
    square.SetValue<int>(Canvas.ZIndexProperty, 1);

    _current_selected_square = square;
    _current_selected_square.MarkSelected();
}

void _drag_drop_CustomMouseMove(FrameworkElement sender, 
                                DragAndDropEventArgs args)
{
    Point hit_point = args.MouseArgs.GetPosition(_my_chessboard);
    CChessSquare square = _my_chessboard.GetSquare(hit_point);

    if (square == null)
    {
        if (_current_selected_square != null)
        {
            _current_selected_square.UnmarkSelected();
            _current_selected_square = null;
        }
    }
    else
    {
        if (_current_selected_square == null)
        {
            _current_selected_square = square;
            _current_selected_square.MarkSelected();
        }
        else
        {
            if (!_current_selected_square.Equals(square))
            {
                _current_selected_square.UnmarkSelected();
                _current_selected_square = square;
                _current_selected_square.MarkSelected();
            }
        }
    }
}

void _drag_drop_CustomMouseUp(FrameworkElement sender, 
                              DragAndDropEventArgs args)
{
    CChessPiece piece = sender as CChessPiece;
    CChessSquare square = piece.Square;
    square.SetValue<int>(Canvas.ZIndexProperty, 0);

    Point hit_point = args.MouseArgs.GetPosition(_my_chessboard);
    CChessSquare dest_square = _my_chessboard.GetSquare(hit_point);

    if (dest_square == null || _opponent == null || 
       (byte)piece.CChessColor != _you.color)
    {
        CancelMovement(args);
    }
    else
    {
        try
        {
            _my_chessboard.MovePiece(piece.ID, 
                 dest_square.Ring, dest_square.Square);
            CChessProxy.CChessMove last_move = new CChessProxy.CChessMove()
            {
                piece_id = piece.ID,
                destination_ring = dest_square.Ring,
                destination_square = dest_square.Square,
                player_id = _you.player_id,
                game_id = _you.game_id
            };
            _proxy.BeginMovePiece(last_move, 
                   new AsyncCallback(LastMoveSent), null);
        }
        catch(Exception ex)
        {
            my_textblock.Text = ex.Message;
            CancelMovement(args);
        }
    }

    if (_current_selected_square != null)
    {
        _current_selected_square.UnmarkSelected();
        _current_selected_square = null;
    }
}

void CancelMovement(DragAndDropEventArgs args)
{
    args.ReturnToStartingPoint = true;
}

#endregion

The manner to interact with the Drag and Drop object is by manipulating the arguments in the DragAndDropEventArgs. For now, they support returning an element to the starting point after the drag and drop finishes (by doing args.ReturnToStartingPoint = true, I use it, for example, when a piece wasn't dropped in a valid square, and it can't move) and stop dragging at any certain moment (args.StopDragging = true, I ended up without using it, but you may find its use). You may also manipulate the UI and the dragged object at any moment throughout the drag and drop process. A common thing you'd probably want is to have the element being dragged have the greatest Z-Index while being dragged. You can accomplish this by giving it a bigger Z-index in the CustomMouseDownEvent, and returning its Z-Index in the CustomMouseUp event (note that in this case, I had to increase the Z-Index of the Square of the Piece, and not the Piece itself, because all the pieces are contained inside the squares, and therefore it's the Z-Index of the squares that matters). One last thing to note is that you can always drop your element anywhere you want in the CustomMouseUp event, by checking if the coordinates of the mouse overlap the coordinates of any "droppable" element.

Misc Subjects

So, here, I'll talk about all the other little things I used that don't deserve a section of their own.

Let's say you want to embed multiple images into your Silverlight application, but you want to give your users a feedback as to when they're all down, or even show them the process, if they're heavy images. Well, you can do it with the Downloader object. I used it to download all the images of the pieces in my chess board, which were stored in a Zip file, and then I saved them in a dictionary, by piece type and color. Here's the code to do that:

public class CChessBoard : Control
{
    #region Constructor

        public CChessBoard(Color orientation, double radius, 
               double inner_radius, CChess.CChessBoard data_board)
        {
            System.IO.Stream s = 
               this.GetType().Assembly.GetManifestResourceStream(
               "CircularChess.CChess.Controls.CChessBoard.xaml");
            _canvas = (Canvas)this.InitializeFromXaml(new 
                       System.IO.StreamReader(s).ReadToEnd());

            Downloader dl = new Downloader();
            dl.Open("GET", new Uri("images/images.zip", 
                                             UriKind.RelativeOrAbsolute));
            dl.DownloadFailed += new ErrorEventHandler(dl_DownloadFailed);
            dl.Completed += new EventHandler(dl_Completed);
            dl.Send();

            Draw();
        }
        
        #endregion

        private CChess.CChessBoard _data_board;
        private CChessPiece[] _pieces;
        private Dictionary<CChess.ChessPieceTypes, 
                Dictionary<CChessColors, Image>> _images;

        public static Color black_color = Colors.Black;
        public static Color white_color = Colors.White;
        public delegate void NoParametersEventHandler();

        #region Initializing

        void dl_DownloadFailed(object sender, ErrorEventArgs e)
        {
            throw new Exception(e.ErrorMessage);
        }

        void dl_Completed(object sender, EventArgs e)
        {
            Downloader dl = sender as Downloader;
            _images = new Dictionary<CChess.ChessPieceTypes, 
                          Dictionary<CChessColors, Image>>();
            Dictionary<CChessColors, Image> tmp_dictionary = 
                          new Dictionary<CChessColors, Image>();
            Image tmp_img;

            // Now we manually fill the dictionary
            // Rooks
            tmp_dictionary = new Dictionary<CChessColors, Image>();
            tmp_img = new Image();
            tmp_img.SetSource(dl, "black_rook.png");
            tmp_dictionary.Add(CChessColors.Black, tmp_img);
            tmp_img = new Image();
            tmp_img.SetSource(dl, "white_rook.png");
            tmp_dictionary.Add(CChessColors.White, tmp_img);
            _images.Add(CChess.ChessPieceTypes.Rook, tmp_dictionary);

            // Knights
            tmp_dictionary = new Dictionary<CChessColors, Image>();
            tmp_img = new Image();
            tmp_img.SetSource(dl, "black_knight.png");
            tmp_dictionary.Add(CChessColors.Black, tmp_img);
            tmp_img = new Image();
            tmp_img.SetSource(dl, "white_knight.png");
            tmp_dictionary.Add(CChessColors.White, tmp_img);
            _images.Add(CChess.ChessPieceTypes.Knight, tmp_dictionary);

            // Bishops
            tmp_dictionary = new Dictionary<CChessColors, Image>();
            tmp_img = new Image();
            tmp_img.SetSource(dl, "black_bishop.png");
            tmp_dictionary.Add(CChessColors.Black, tmp_img);
            tmp_img = new Image();
            tmp_img.SetSource(dl, "white_bishop.png");
            tmp_dictionary.Add(CChessColors.White, tmp_img);
            _images.Add(CChess.ChessPieceTypes.Bishop, tmp_dictionary);

            // Kings
            tmp_dictionary = new Dictionary<CChessColors, Image>();
            tmp_img = new Image();
            tmp_img.SetSource(dl, "black_king.png");
            tmp_dictionary.Add(CChessColors.Black, tmp_img);
            tmp_img = new Image();
            tmp_img.SetSource(dl, "white_king.png");
            tmp_dictionary.Add(CChessColors.White, tmp_img);
            _images.Add(CChess.ChessPieceTypes.King, tmp_dictionary);

            // Queens
            tmp_dictionary = new Dictionary<CChessColors, Image>();
            tmp_img = new Image();
            tmp_img.SetSource(dl, "black_queen.png");
            tmp_dictionary.Add(CChessColors.Black, tmp_img);
            tmp_img = new Image();
            tmp_img.SetSource(dl, "white_queen.png");
            tmp_dictionary.Add(CChessColors.White, tmp_img);
            _images.Add(CChess.ChessPieceTypes.Queen, tmp_dictionary);

            // Pawns
            tmp_dictionary = new Dictionary<CChessColors, Image>();
            tmp_img = new Image();
            tmp_img.SetSource(dl, "black_pawn.png");
            tmp_dictionary.Add(CChessColors.Black, tmp_img);
            tmp_img = new Image();
            tmp_img.SetSource(dl, "white_pawn.png");
            tmp_dictionary.Add(CChessColors.White, tmp_img);
            _images.Add(CChess.ChessPieceTypes.Pawn, tmp_dictionary);
        }
}

Again, non-relevant parts were edited out. As you can see, using the Downloader is quiet easy. You give it a method ("GET"), a Uri object, and hook its Completed event. If you wanted, you could also hook the DownloadProgressChanged event, and show the process to the user. Once the download completes, you create images, and use their SetSource method, passing it the Downloader object and the path to the image inside the Zip. Yes, it's as easy at that.

Now, .NET 3.5 introduced Extension Methods, that are actually methods added to existing classes, pretty much like adding a method to the prototype in JavaScript (is it just me, or is C# taking elements from JavaScript?). I make use of this new feature to implement a Clone method to the Image control in Silverlight, which I later use to clone images from the dictionary, and give them to the pieces. I did it this way because having a Dictionary with the images and being able to select them by color and type is pretty convenient. But because an image can be used in more than one piece, I needed to treat the Dictionary as "sample" images. Here's the code in Extensions.cs:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace CChess.Controls
{
    public static class Extensions
    {
        public static Image Clone(this Image img)
        {
            Image tmp_img = new Image();
            tmp_img.Source = img.Source;
            return tmp_img;
        }
    }
}

Note that the class holding the Extension Method must be static. The syntax for creating such a method is to create a static function that accepts an object of the type we want to extend, but adding the this keyword before the type. You can then call such a method as any other (with Intellisense and all):

private void AddPiece(int id, Image img, ChessPieceTypes type, CChessSquare square)
{
    // Note that img uses the extender method
    // inside the Extensions class (Clone).
    _pieces[id - 1] = new CChessPiece(_data_board.Pieces[id - 1], 
                                      square, img.Clone());
}

The last misc subject I wanted to introduce was paths. As you can guess, I used them to draw the board. Each square has a path, drawn dynamically, according to its ring and its square and a bit of trigonometry. Now, what are paths made of? They can be made of Bezier Curves, Lines, Arcs, and a few more geometric friends. If you look at my squares, you can see fast enough that they start with a curve, then have a line inwards in the same direction as the radius, then another arc, and then they close up. Let's see the syntax of that:

/// <summary>
/// Draws the square, and positions it according to _row and _col.
/// </summary>
public void Draw()
{
    // Remove any previous path.
    if (_path != null)
    {
        _canvas.Children.Remove(_path);
    }

    _path = new Path();
    _path.Fill = new SolidColorBrush(Color);
    _path.Stroke = new SolidColorBrush(Colors.Black);
    _path.StrokeThickness = 1;

    PathGeometry path_geometry = new PathGeometry();
    // For some reason I need to create a new instance
    // of that collection... I wonder why is that?
    path_geometry.Figures = new PathFigureCollection();
    PathFigure path_figure = new PathFigure();
    path_figure.StartPoint = new Point(0, 0);
    path_figure.IsClosed = true;
    path_geometry.Figures.Add(path_figure);

    // And again.
    path_figure.Segments = new PathSegmentCollection();

    double board_radius = BoardRadius();

    double top_property, left_property;
    // The (0, 0) point is not in the center
    // of the circle, but at the center-bottom,
    // so in order to calculate the coordinates
    // correctly, we add board_radius
    // to the projections
    top_property = board_radius + 
           YProjection(_outer_radius, _graphic_square - 1);
    left_property = board_radius + 
           XProjection(_outer_radius, _graphic_square - 1);
    this.SetValue<double>(Canvas.TopProperty, top_property);
    this.SetValue<double>(Canvas.LeftProperty, left_property);
    _position = new Point(left_property, top_property);

    ArcSegment outer_arc = new ArcSegment();
    outer_arc.Size = new Point(_outer_radius, _outer_radius);
    outer_arc.SweepDirection = SweepDirection.Counterclockwise;
    double outer_arc_p_x, outer_arc_p_y;
    outer_arc_p_x = XProjection(_outer_radius, _graphic_square) - 
                    XProjection(_outer_radius, _graphic_square - 1);
    outer_arc_p_y = YProjection(_outer_radius, _graphic_square) - 
                    YProjection(_outer_radius, _graphic_square - 1);
    _outer_arc_p = new Point(outer_arc_p_x, outer_arc_p_y);
    outer_arc.Point = _outer_arc_p;

    LineSegment line_segment = new LineSegment();
    double line_p_x, line_p_y;
    line_p_x = XProjection(_inner_radius, _graphic_square) - 
               XProjection(_outer_radius, _graphic_square - 1);
    line_p_y = YProjection(_inner_radius, _graphic_square) - 
               YProjection(_outer_radius, _graphic_square - 1);
    _line_p = new Point(line_p_x, line_p_y);
    line_segment.Point = _line_p;

    ArcSegment inner_arc = new ArcSegment();
    inner_arc.Size = new Point(_inner_radius, _inner_radius);
    double inner_arc_p_x, inner_arc_p_y;
    inner_arc_p_x = XProjection(_inner_radius, _graphic_square - 1) - 
                    XProjection(_outer_radius, _graphic_square - 1);
    inner_arc_p_y = YProjection(_inner_radius, _graphic_square - 1) - 
                    YProjection(_outer_radius, _graphic_square - 1);
    _inner_arc_p = new Point(inner_arc_p_x, inner_arc_p_y);
    inner_arc.Point = _inner_arc_p;
    inner_arc.SweepDirection = SweepDirection.Clockwise;

    path_figure.Segments.Add(outer_arc);
    path_figure.Segments.Add(line_segment);
    path_figure.Segments.Add(inner_arc);
    _path.Data = path_geometry;

    _canvas.Children.Add(_path);
}

It may seem a bit intimidating because of all the different calculations there, but here's what's going on: We have a path, and we want to add elements to it. We start by creating a PathGeometry, and giving it a PathFigureCollection. We assign the collection a new PathFigure, and set it up as we need to (the starting point in relation to the whole path, and if we want it to close) and assign to it a new PathSegmentCollection. Now, every new element (arc, line, etc., called segment from now on) will be added to the PathSegmentCollection. At the end, we assign the PathGeometry to the Data property of the path, and we're all set!

Wrapping it all Up

So, having spoken of all the elements in Silverlight you needed to know about, it's time to talk again, about how the game works.

There are three methods being called constantly: the one that updates your activity, that I do with ASP.NET AJAX, for simplicity's sake; the one that checks if your opponent is still online, so you don't wait for someone who's gone away; and the one that checks for a new move. As soon as a new move is gotten, the piece moves on the UI, and the turn passes.

One thing to note is that if a player leaves a game, someone else may join it, even though it has already started, and keep playing in the first player's place.

The last thing I wanted to talk about, was about security. Yes, I know this is only a game, but it would still suck if someone found a way to cheat. You must always make sure you put security measure on the server, and never to count on the client side. If a player can't move a piece with the UI, because it's protected, that doesn't mean he can't send a request directly to the web service, and "move" the piece he wants. That's something to take into account.

TODO

Because I'm sure no one will even look at it, and it's a long and technical work, I haven't yet made validation of moves, which means that a player can move any piece anywhere. Of course, if I were to upload the game for people to play, it would be a must, but because here I post it as sample code, and I wanted to post it as soon as possible, I'll leave it for later.

Conclusion

So, this is the end of the article. I hope you enjoyed it, and found it a good introduction to the new technologies in .NET 3.5. One of the reasons I wrote this article is to hear other people's opinion about it. So please, after reading this far, spare a few more minutes to leave a comment :) That said, having tried to create a normal chess game with normal AJAX and ASP.NET 2.0 in the past, I can't tell you enough how this approach proved by far better and cleaner. All in all, this turned out to be quiet a gratifying experience.

Change Log

  • 21/12/2007 - Posted the article.
  • 25/12/2007 - Fixed some typos. Added some screenshots, and added an introduction to the database.
  • 30/12/2007 - Redesigned the website (no more white-black!). Added animation to the pieces when the opponent moves them. Added a simple control with the image of a white king, that helps in keeping track of the current turn (and looks cool).

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here