65.9K
CodeProject 正在变化。 阅读更多。
Home

Silverlight, ASP.NET AJAX, WCF Web Services 和 LINQ to SQL 在线回合制象棋

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (59投票s)

2007年12月20日

CPOL

34分钟阅读

viewsIcon

229506

downloadIcon

1875

一个基于Silverlight、ASP.NET AJAX、WCF Web Services 和 LINQ to SQL 的应用程序,供用户在互联网上玩回合制象棋。

board.jpg

引言

几个月前,一位朋友向我提到了回合制象棋,我觉得它看起来很酷。然而,我找不到任何在线的回合制象棋游戏。由于盒模型的原因,使用标准的HTML和JavaScript实现这样的游戏是不可能的,而且我对Flash或Java Applet不熟悉,所以我无法实现这个游戏。然后,我听说Silverlight,看了一些演示,我对自己说,这是一个实现回合制象棋的绝佳平台。

在本文中,我将讨论使用ASP.NET AJAX 3.5和WCF Web Services,并在LINQ to SQL和Silverlight 1.1的帮助下创建在线游戏的方法。

如果您对回合制象棋的规则感兴趣,请查看此网站

背景

你应该熟悉ASP.NET和数据库。我希望能够照顾到那些不熟悉Web Services的人。

目录

运行游戏

为了使游戏正常运行,请确保您已安装Silverlight 1.1运行时(可在此处下载)、Silverlight 1.1 SDK(可在此处下载)以及ASP.NET 3.5 Extensions CTP Preview(可在此处下载)。除此之外,运行游戏只需在CircularChessWebsite中运行Default.aspx,可以使用VS2008或IIS。如果您想测试它,可以使用Firefox或IE。如果您想与其他玩家一起玩,请确保他们安装了Silverlight 1.1运行时,并将其放在某个服务器上(或者在您的localhost上,如果您知道如何配置)。

Silverlight解决方案的结构

首先,您必须使用Visual Studio 2008(Beta 2就能很好地工作,但在编写过程中,我升级到了完整版)。现在,一个Silverlight解决方案至少需要两个项目:Silverlight项目(创建Silverlight 1.1对象)或Silverlight JavaScript应用程序,以及一个ASP.NET网站来利用Silverlight项目。该网站不一定是ASP.NET,但它是首选选项,因为我们获得了更好的兼容性。我还建议,如果可能的话,创建一个ASP.NET 3.5 Extensions网站,因为在那里嵌入Silverlight更容易。如果您需要在解决方案中使用Web Services,请将其作为网站的一部分创建,因为Silverlight只能在同一域中访问它们(除非您使用IIS)。

除了这两个主要项目外,您还可以添加辅助类。Silverlight控件(或Silverlight项目使用的任何代码)将在Silverlight类库中创建,因为否则您将无法引用它们。在我的例子中,我有一个辅助的拖放对象,以及一些Silverlight项目和ASP.NET网站都使用的辅助类。请注意,如果您想包含辅助类,您必须确保这些类不包含任何Silverlight功能(甚至不包含using指令,因为有些可能会导致运行时错误)。此类文件的骨架如下所示:

using System;

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

这就是我们的解决方案的样子

Screenshot - solution_structure.jpg

话虽如此,我暂时搁置Silverlight话题,转而关注应用程序的其他部分。

使用ASP.NET AJAX 3.5 与 WCF Web Services

首先,Web Services一句话(相信我,我知道有些人认为Web Services和Web Applications是相同的):它们是公开Web应用程序信息的标准方式。通常,您将通过代码隐藏文件中的C#代码访问数据库中的信息,但通过Web Services,您可以 URL 调用某个方法,获取信息,并按需使用。

所以,您可能会问,为什么要使用WCF Web Services?毕竟,它们比简单的ASMX Web Services更难配置。嗯,有几个原因可以偏爱它们:

  • 它们的结构比ASMX Web Services更好。
  • 您可以更好地控制它们。
  • 在不久的将来,您很可能在它们方面获得新功能。
  • 稍后我将讨论的一个LINQ功能。

所以,尽管今天您可能在构建它们时遇到更多困难,但明天您会很高兴您做了。

为了使用WCF Web Services,您必须先将其配置为支持JSON序列化,因为目前这是唯一支持的方法。要做到这一点,您必须在web.config文件中将端点配置为启用客户端脚本,如下所示:

<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>

所以,对于那些不太熟悉WCF Web Services的人来说,我给端点添加了一个behaviourConfiguration属性,我在endpointBehaviours中引用了它,并添加了一个空的enableWebScript节点。另外两点需要注意,端点必须暂时使用webHttpBinding,因为这是我们目前唯一支持的绑定,我们还添加了一个<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>节点,它基本上让我们能够访问HttpContext.Current,就像我们在普通的ASMX Web Service中一样。对于不知道它是什么的人来说,它是包含所有SessionRequestProfile对象的对象,仅举几例。如果您在调试,或者只是想捕获您抛出的异常,您还必须添加我在服务行为中添加的serviceDebug节点。

另一个需要考虑的事情是为契约指定的命名空间。这将在稍后由ASP.NET创建的JavaScript代理中使用(如果您想知道的话,Microsoft AJAX Library提供了JavaScript命名空间的实现)。您可以通过这种方式做到这一点:

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

    // Other Web Methods
}

最后一件必须做的事是,在您的契约实现中添加一个属性,如下所示:

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

好了,既然我们已经准备好使用WCF Web Services了,让我们开始使用它们吧!

首先,您必须在您使用Web Services的页面中放置一个ScriptManager元素,并添加Web Services的引用,如下所示:

<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>

这告诉ASP.NET创建一个Web Services的代理,您可以在JavaScript中使用它。请注意,在VS2008中,您甚至可以获得它们的Intellisense,这非常棒。

Screenshot - ajax_service_intellisense.jpg

Screenshot - ajax_service_params.jpg

我的脚本管理器还包含对两个JavaScript文件的引用,这相当于在标准HTML中引用它们,但这是Microsoft推荐的添加方式,所以就这样吧。需要注意的一点是,ASP.NET还会创建Web Services使用的类或枚举的JavaScript版本,所以您可以使用它们,就像我一会儿要展示的那样。为了通过JavaScript使用该代理,您必须首先创建它的一个实例,然后您可以调用您提供的任何方法,并带有两个回调函数,一个用于成功调用,另一个用于失败调用。这是一个例子:

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

我们一步一步来(就像《名侦探柯南》第一集的片尾曲的名字一样!)。因为在VS2008中,我们在JavaScript中也有Intellisense,所以我们也需要一种方法来“包含”.js文件到其他.js文件中,以便在Intellisense中获得它们的变量、函数和类等。方法是使用三斜杠注释和引用元素,就像我在上面的示例中所做的那样。pageLoad是Microsoft AJAX Library在页面加载时调用的一个函数(如果您对名称不满意,可以随时挂接window.onload事件)。在那里,我挂接了登录按钮的单击事件($get函数是document.getElementById的缩写,同样是Microsoft AJAX Library提供的),还使用了一个我创建的小型辅助函数来将文本框“绑定”到该按钮(它做的正是注释所说的)。当按钮被点击时,我们获取文本框中的值,并创建一个代理实例。请注意,命名空间与为契约指定的命名空间匹配,代理名称与契约名称匹配。然后,我们创建一个新的CChessPlayer(前面的“C”是因为我太懒得一直写“Circular”,与MFC无关),并为其分配一个player_nick,就像我们在普通C#中所做的那样。然后,我们调用前面展示的Login方法,传入玩家对象和两个回调函数,以便在登录成功时重定向;如果登录失败,则显示错误。

实际上,就这么多了。如果您期望方法返回结果,它将作为第一个参数传递给成功回调,但由于Login方法返回void,所以我将其留空。如果Intellisense不正确,请尝试通过Edit>Intellisense>Update JScript Intellisense进行刷新。我非常鼓励您使用安装了Firebug的Firefox;这是一个非常有用的工具,可以显示您进行的所有AJAX调用,拥有JavaScript调试器等等。我认为在开发此类应用程序时,它非常有帮助。

firebug_img.jpg

数据库

在这个应用程序中,我使用的是SQL Server 2005数据库。它是一个相当简单的数据库,让我来介绍一下。

Screenshot - database_relationships.jpg

数据库的两个主要表是playersgamesplayers表包含所有曾经进入过游戏的玩家。在线玩家和离线玩家通过稍后讨论的last_activity字段区分。games表包含所有创建过的游戏。

games_players是连接游戏和玩家的多对多表。我选择这样做而不是在games表中硬编码两个玩家(白棋和黑棋),因为这样更容易过滤玩家,而且更具可扩展性。games_moves表存储了每场游戏中进行的所有移动。这看起来可能有点昂贵,也许确实如此,更好的解决方案是保存棋盘在任何给定时刻的状态,但我更喜欢这种方式,因为它迄今为止更加灵活。例如,如果我将来需要实现撤销功能,而不存储每一步,那是不可能的。

最后一个表用于聊天框,没什么特别的。

LINQ to SQL

在开始之前,请允许我指出,这个主题比我们之前的主题有更多的视觉活动,因此,您可能会觉得观看一些关于它的电影更容易。您可以在此处找到一些。

所以,在学习了ASP.NET AJAX和WCF Web Services之后,让我们进入下一个环节,LINQ to SQL。正如您可能已经知道的,LINQ是Language Integrated Query的缩写,它很好地实现了它的名字。它是一种微型查询语言,您可以在C#或任何其他.NET语言中编写,它可以让您对集合(IEnumerable对象)进行查询。LINQ to SQL是LINQ的一个子类别,但它不处理集合,而是处理SQL表。那么,LINQ to SQL的优点是什么?首先,它的语法比SQL简单,并且它生成的查询效率很高。您还可以通过编写查询的整个过程获得Intellisense。它甚至支持SQL函数和SQL存储过程。但这些都不是我如此喜欢LINQ to SQL的原因。我过去花费了大量时间编写映射到数据库表中每个表的类,以及编写存储过程和CRUD操作,这是一项极其耗时且枯燥的工作。但现在有了LINQ to SQL和VS2008,所有这些都简化为将表拖放到可视化编辑器上!您不仅免费获得了所有CRUD操作,还获得了可扩展的偏类,您可以在其中添加任何您想要的函数或属性。

够了“为什么”,让我们来谈谈“怎么做”!您可以通过向应用程序的App_Code文件夹添加一个“LINQ to SQL Classes”项来开始,它会为您添加一个.dbml文件。然后,您可以打开Server Explorer,打开数据库连接,然后将您的表拖放到表区域。您可以对存储过程做同样的事情,只需将其拖放到表区域右侧的区域。最终结果如下所示:

LINQ to SQL

请注意,默认情况下,类的名称将与表名称匹配,属性的名称将与列名称匹配。如果您对此不满意,可以随时编辑它们,但请务必切勿直接编辑生成的源代码,因为它迟早会被自动编辑,而只能通过可视化编辑器进行。您可以通过双击要更改的名称来完成此操作。对于您创建的每个存储过程,请确保在Properties窗口中具有预期的返回类型。

Stored Procedure Return Type

如果您在更改返回类型时遇到问题,则表示您的存储过程与之不兼容。现在是时候庆幸我们选择了WCF作为Web服务提供商了,因为LINQ to SQL编辑器提供了一个简单的属性,我们可以更改它,所有生成的类将立即获得[DataContract]属性,所有属性也将立即获得[DataMember]属性。这意味着我们可以使用生成的类作为WCF Web Services的返回类型!要做到这一点,请将DataContext的Serialization Mode属性从“None”更改为“Unidirectional”(通过不聚焦任何表或存储过程可以看到它)。

WCF Serialization in LINQ

最后一点,在给出示例之前。因为创建的类是偏类,所以它们可以被扩展(并且实际上是为扩展而构建的)。我想举一个它有多么有用的例子:

// 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.");
            }
        }
    }
}

因此,我添加的第一项是用于序列化的属性。因为CChessGamesPlayers是用于游戏和玩家之间多对多关系的表,它既有CChessGame属性,也有CChessPlayer属性,但两者都没有标记为DataMember,我觉得这很棒,因为如果它们被标记了,这意味着我们将获得大量我们可能不需要返回的额外数据,这是一种浪费。但是,如果我们还是想返回一些数据呢?嗯,这没问题,只需添加一个属性,给它DataMember属性,然后在get部分返回您需要的内容(将set留空)。

另一个非常好的特性是使用偏方法。它们是在.NET 3.0中引入的方法,只有在类的另一个偏实现中提供了实现时才会被调用,否则它们将被完全忽略(与事件不同,事件仍然会被调用并导致开销)。它们非常适合创建可扩展的类,而LINQ to SQL充分利用了它们。在这里,我们实现了OnValidate方法,该方法在对象即将被插入或更新到数据库时被调用。如果验证不通过,我们只需抛出异常。我更喜欢这种方式来验证我的对象,而不是在插入它们的方法体中,因为它更有意义。

好的,让我们来看一些服务中的示例:

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

对于熟悉SQL编写的人来说,这几乎是显而易见的。您必须首先创建一个DataContext对象,然后您可以从其中的任何表中进行选择(在本例中是ChatMessages)。因为我们返回的是通用的List,所以我们必须将结果转换为List。我们也不获取所有记录,只获取n条记录。SQL等效语句是:

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

为简洁起见,我使用了T-SQL不支持的LIMIT语句,但您明白我的意思!为了实现分页功能,您可以将其与Skip(n)方法结合使用。好的,另一个例子,现在使用Lambda表达式(我当然也会解释它们):

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

正如您所见,数据上下文中的表包含一些方法(这里我们使用Single,但还有其他几种),它们接受Lambda表达式作为参数。Lambda表达式只是.NET 2.0中引入的匿名方法的简写形式。定义参数后,您使用=>运算符,并编写一些操作。在这种情况下,Lambda表达式将一个玩家作为参数,并将其玩家ID与另一个ID进行比较。它将返回所有具有该player_id的玩家。因为我们谈论的是ID,所以只有一个或零个。但因为我们还使用了Single方法,所以必须是1,否则将抛出异常。如果Single不满足您的需求,您可以选择其他任何一个(它们的名称也都自明)。

因为我们谈论的是数据库,所以您很可能需要更新、插入和删除记录。插入方法如下:

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

只需创建一个新消息,初始化它(请注意,我在这里使用了.NET 3.5中的新语法),以它为参数调用其表的InsertOnSubmit方法,然后提交更改。删除操作也一样,只是用DeleteOnSubmit代替InsertOnSubmit。如果您有一个集合,则分别使用InsertAllOnSubmitDeleteAllOnSubmit

更新有点棘手。为了更新记录,DataContext必须意识到它已更改,而这只有在我们谈论刚从DataContext中取出的记录时才会发生。然后您将进行更改,调用SubmitChanges方法,就像您进行插入和删除一样,您的记录将被更新。但是,如果有一个您持有了一段时间的对象,并且想要更新它怎么办?您有两个选择:要么重新获取它(使用Single方法或您喜欢的任何其他方式),编辑它,然后提交更改;或者,您可以使用存储过程,如下所示:

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

如果您有兴趣,这是存储过程的SQL:

ALTER PROCEDURE UpdateUserActivity
    @player_id bigint,
    @timestamp int
AS

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

请注意,我隐式返回了@@ROWCOUNT。除此之外,我认为这很容易理解。

好了,LINQ to SQL就到这里。我们暂时从新功能中休息一下,看看系统是如何工作的!

登录和登出

unable_username.jpg

与普通网站不同,在普通网站中,用户登录并被授权执行某些操作,然后可以退出,这个网站需要实时跟踪用户。想象一下,如果一个玩家在游戏中途消失了,而他的对手甚至不知道会发生什么!所以,我跟踪用户的方法是在数据库中有一个unix_timestamp字段,并实时更新它。对于不熟悉Unix时间戳的人来说,它们是自1970年1月1日00:00起经过的秒数。我曾有PHP背景,我觉得它们非常容易使用。检查用户是否离线的判断方法是当前时间戳减去上次活动时间戳大于某个特定值。对于那些喜欢代码而不是文字的人,看这里:

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

以及相关的服务方法:

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

为了完整起见,这是GetOnlinePlayers存储过程的SQL:

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

因此,当页面加载时,我会定期调用所有更新UI的函数。它们获取数据,更新UI,然后以一定的间隔再次调用自身(间隔尽可能大,但仍然显得自然)。

请注意,我们也可以非常轻松地创建一个常规的注册和登录系统,并且实现不会有太大变化,这可能是您在完整网站中会做的方式,但由于这只是一个演示,所以我保持了匿名的。请注意,我使用了Profile对象来存储跨请求所需的数据(同样,我通过HttpContext.Current访问了它,而我在开始时启用了它)。

创建新游戏

因此,在开始讨论应用程序中的Silverlight之前,我将快速回顾一下新游戏的创建。任何想开始新游戏的用户都可以创建它,他将根据自己的选择成为黑棋或白棋。之后,他将被重定向到回合制象棋棋盘,在那里他等待别人加入他,当有人加入时,他们就可以开始玩了!我想强调的一个函数是创建新游戏的功能:

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

正如您所见,我们使用在服务器端创建的枚举来调用该函数。我认为即使这些也能通过代理进行转换,这一点非常棒。

Silverlight

尽管我将文章命名为“Silverlight中的在线回合制象棋”,但我现在才开始谈论它……好吧,开始了!在引言中,我曾将Silverlight与Flash和Java Applet进行过比较,事实是Silverlight是微软对抗Adobe Flash的竞争产品。因为我没有时间从头学习ActionScript和Flash,所以我学习了Silverlight。在本文中,我将讨论Silverlight更技术性的部分,而不是艺术性的部分(因为我不是一个有艺术细胞的人)。如果您需要创建复杂的XAML(Extensible Application Markup Language,用于在Silverlight中展现艺术性的语言)来装饰您的应用程序,请在此处下载Expression Blend 2(在撰写本文时是12月预览试用版),然后开始玩它。另外,请注意,目前Silverlight中没有通用控件,这意味着没有文本框、按钮、组合框等。九月份发布了一些(您可以在此处下载),但您可以在2008年第一季度期待完整发布。所以,再次,在开始之前,我想向您推荐一些我学到很多东西的很棒的视频,您可以在此处下载

Silverlight基础

默认情况下,Silverlight 1.1项目具有以下文件:

  • 一个TestPage.html页面,您可以在其中嵌入Silverlight对象进行测试。
  • 一个TestPage.html.js文件,它定义了一个createSilverlight()函数,该函数嵌入了带有其中指定参数的Silverlight。
  • 一个Silverlight.js文件,这是Microsoft为嵌入Silverlight创建的辅助文件。
  • Page.xaml,您可以在其中定义Silverlight对象的元素(TextBlockRectangle等)。
  • Page.xaml.cs,它作为Page.xaml的代码隐藏文件,其用法与ASP.NET中的代码隐藏文件基本相同。
  • 您还可以添加自定义控件。

让我们来探讨一下每个文件。

<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>

这是来自ChessBoardPage.xaml的内容,默认情况下称为Page.xaml。不言而喻,对吧?嗯,一切都被包装在一个Canvas中。我们有一个TextBlock用于显示消息,以及两个自定义控件,分别代表白棋王和其下方的玩家姓名。我将在文章稍后解释添加控件的语法。

让我谈谈其中一些属性。Canvas.TopCanvas.Left属性告诉元素相对于其包含的Canvas的位置,如果出于某种原因,您将其移出Canvas,它们将停止工作。请注意,所有元素都是绝对定位的(与DOM不同)。如果您希望某个元素的Z-Index高于另一个元素,可以使用Canvas.ZIndex属性(其功能与CSS的z-index属性相同)。还有一个x:Name属性,它允许我们通过指定的名称在代码隐藏文件中引用该属性。当我们在代码隐藏中看到更多内容时,我们将进一步了解它。现在,让我们检查TestPage.htmlTestPage.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();
    }

}

如您所见,TestPage.html非常简单。唯一值得注意的是,您必须为包含Silverlight对象的元素指定宽度和高度才能使其可见。我们在页面顶部的CSS中这样做。TestPage.html.js也非常简单。它使用VS2008为我们创建的Silverlight.js来创建一个简单的createSilvelight函数。它使用Silverlight类中的静态方法,并带有一些自明的参数来将Silverlight嵌入页面。现在,为了将Silverlight嵌入现有网站(因为我怀疑您会将其嵌入TestPage.html),您需要向您拥有的Silverlight项目添加一个Silverlight链接。

Add Silverlight Link

它会将Page.xaml复制到网站中(请注意,VS2008会自动更新您在Silverlight项目中对其所做的任何更改)。然后,您可以手动复制TestPage.html.jsSilverlight.js,并按照上述方式嵌入Silverlight。请注意,我在网站中嵌入Silverlight到我的页面时并没有使用这种方法。我想向您展示这种方法,因为您的服务器很可能还没有安装ASP.NET 3.5 Extensions,如果您想在网站中嵌入Silverlight,这将是您唯一的方式。如果您确实可以访问Extensions,那么嵌入Silverlight就像将任何其他ASP.NET控件插入页面一样简单。在页面中放置ScriptManager后,只需在页面中的任何位置添加一个<asp:Silverlight runat="server" ID="Silverlight ID" Width="800" Height="800" Version="1.1" Source="~/Path/to/XAML.xaml" />,然后您就可以开始了!

Silverlight控件的事件挂钩

这很简单,但我还是觉得应该提一下。挂接Silverlight控件上的事件与挂接WinForms应用程序或DOM元素上的事件相同。您可以对派生自FrameworkElement的对象挂接鼠标和键盘事件。您可以在我的DragAndDrop类中找到一个例子。

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.......
}

Silverlight 1.1 中的 WCF Web Services

Silverlight已嵌入到网站中,下一步是创建Web Services的代理。这就是Silverlight变得丑陋的地方,因为出于某种原因,VS2008无法为WCF Web Services创建代理,尽管它与ASMX Web Services配合得很好。我读到有些人设法很容易地使其工作,但无论我尝试多少次,它就是不行,而且我知道我不是唯一一个。幸运的是,有一个变通方法,虽然不太理想,但目前有效,直到这个bug(如果它确实是bug的话)被修复。首先,创建一个仅实现Web Service“骨架”的ASMX Web Service。您可以在VS2008中通过实现契约接口并将[WebMethod]属性添加到所有生成的函数来轻松完成此操作,如下所示:

Implement Contract

之后,回到您的Silverlight项目中,右键单击您的项目,然后选择“添加Web引用”。

Add Web Reference

在打开的对话框中,转到“此解决方案中的Web Services”,然后选择您的ASMX Web Service,而不是您的SVC Web Service(别担心,我们最终会使用SVC的)。

Screenshot - asmx_web_service.jpg

您现在可以为引用命名,单击“添加引用”,代理就会为您创建。现在,幸运的是,为ASMX Web Services创建的代理与WCF Web Services的工作方式完全相同。唯一需要做的是在创建代理时,将服务的URL更改为SVC的URL,如下所示:

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

代码隐藏文件和代理的使用

让我们先来看代码隐藏文件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;
            }
        }
    }
}

(我省略了拖放部分,稍后将讨论它)。好的,让我们一点一点地来分析。在构造函数中,我初始化了页面的字段,其中包括前面介绍的代理,并挂接了CanvasLoaded事件。在Page_Loaded中,我开始对Web Services进行异步调用。当代理创建时,不仅会创建常规方法,还会添加它们的异步版本。这些方法带有“Begin”前缀和“End”前缀。异步调用优于常规调用,因为它们在等待响应时不会阻塞浏览器。为了进行此类调用,请调用您想要的方法的“Begin”版本(例如,BeginGetOpponent),并为其提供一个AsyncCallback委托(在本例中,我们提供的是OpponentGotten(IAsyncResult iar)方法)。当结果到达时,将调用您提供的方法。在那里,您调用方法的“End”版本(在本例中是EndGetOpponent),并检索到服务调用的结果。现在,因为我们在一个在线游戏中,这些调用必须不断重复,但我们也希望设置间隔,以免过度消耗服务器。我们可以使用一个HtmlTimer对象来管理这一点,并且,尽管编译器会告诉您该对象已过时,但它对我们来说工作得非常好,所以直到我们找到更好的替代方案,我们都会坚持使用它。创建新的Timer后,给它一个Interval,挂接一个事件用于在计时器滴答时调用,然后调用Start方法。当计时器滴答时,停止它,做您需要做的事情,然后再次启动它,这样您就成功地在Silverlight中创建了Web Service调用之间的间隔。

代码隐藏中需要注意的另一件事是如何以编程方式添加和操作控件。看看AllMovesGotten方法。在那里,我们用其构造函数创建一个新的CChessBoard,设置其Canvas.TopPropertyCanvas.LeftProperty,并将其添加到Page(它是一个Canvas)的Children集合中,该集合又将其添加到UI中。

自定义Silverlight控件

要添加一个新控件,请添加一个Silverlight用户控件项。让我们来看看PlayerPicture控件。首先是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>

然后是代码隐藏:

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

如果您查看构造函数,它有两行,用于从最终的DLL中绘制XAML,并用它初始化控件。如果您愿意,也可以自己传递XAML字符串。InitializeFromXaml方法还返回XAML中主控件的引用,在本例中是一个Canvas,因此我们将其保存在一个_canvas字段中。为了以编程方式向控件添加元素,您可以调用_canvas.Children.Add。事实上,对控件所做的任何更改都必须在_canvas上进行。请注意,在这些控件中,您必须在Canvas上使用FindName来获取其子元素(不像主页面那样硬编码)。

为了将控件嵌入到另一个XAML中(而不是以编程方式添加),您必须定义一个命名空间,并为其提供控件所在的DLL。让我为您带回主页面中的Canvas,以便您查看:

<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>

在这种情况下,命名空间是CChessControls。我们通过clr-namespace:namespace指定了控件所在的命名空间,然后是程序集的路径。之后,您可以添加元素,如下所示:

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

Silverlight中的拖放

Screenshot - drag_and_drop.jpg

因此,我浏览了互联网,寻找一个好的、可扩展的Silverlight拖放库,但未能找到,所以我自己创建了一个。我的拖放与MSDN演示或Silverlight视频教程中开发的拖放不同之处在于,我将MouseMove事件挂钩到父Canvas,而不是移动的对象,也将LeftMouseButtonUp事件挂钩到父Canvas,因此我取消了拖动元素的可能性,因为鼠标离开了它或其他原因。我还取消了鼠标离开Silverlight对象而拖动的对象被丢弃在某个边缘的场景。我还添加了一些自定义事件来定制它。我想解释一下它的工作原理,以防您有兴趣使用它。

首先,您需要添加对DragAndDrop项目的引用。单击File->Add->Existing Project,然后在DragAndDrop项目文件夹中选择DragAndDrop.csproj(将其设为单独的项目是为了方便将其包含到其他项目中)。然后,右键单击您的Silverlight项目,选择“添加引用”,然后在“项目”选项卡中,选择DragAndDrop项目。现在,您就可以开始了。

要使用DragAndDrop,请创建一个新的实例,如果您愿意,可以添加一些事件,如下所示:

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

请注意,我支持将拖动的元素居中于鼠标。要启用此功能,请将DragAndDrop对象的CenterOnMouseDown属性设置为true(请注意,我再次使用了.NET 3.5中的新初始化方式,但您也可以采用旧的方式)。为了使一个元素可拖动,请调用_drag_drop对象的MakeDraggable方法,并将该元素作为其参数。

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

现在,您可以通过以下方式使用自定义事件:

#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

与Drag and Drop对象交互的方式是通过操作DragAndDropEventArgs中的参数。目前,它们支持在拖放完成后将元素返回到起始点(通过设置args.ReturnToStartingPoint = true,我例如在棋子未放置在有效格子上时使用它,并且它无法移动),并随时停止拖动(args.StopDragging = true,我最终没有使用它,但您可能会发现它有用)。您还可以在拖放过程中的任何时刻操作UI和拖动的对象。您可能想要的一个常见操作是让被拖动的元素在拖动过程中具有最大的Z-Index。您可以通过在CustomMouseDownEvent中给它一个更大的Z-index,并在CustomMouseUp事件中返回其Z-Index来完成此操作(请注意,在这种情况下,我必须增加棋子的Square的Z-Index,而不是棋子本身,因为所有棋子都包含在Square中,因此Square的Z-Index才重要)。最后一点需要注意的是,您可以在CustomMouseUp事件中随时将您的元素放置在任何您想要的地方,方法是检查鼠标坐标是否与任何“可放置”元素的坐标重叠。

杂项

所以,在这里,我将讨论我使用的所有其他小东西,它们不值得拥有自己的章节。

假设您想在Silverlight应用程序中嵌入多个图像,但您想向用户提供反馈,告知他们何时全部加载完毕,甚至在它们是大型图像时显示进度。嗯,您可以使用Downloader对象做到这一点。我用它下载了棋盘上所有棋子的图像,它们存储在一个Zip文件中,然后我按棋子类型和颜色将它们保存在一个字典中。以下是执行此操作的代码:

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

同样,非相关部分已被省略。如您所见,使用Downloader非常简单。您需要提供一个方法(“GET”)、一个Uri对象,并挂接其Completed事件。如果您愿意,您也可以挂接DownloadProgressChanged事件,并向用户显示进度。下载完成后,您将创建图像,并使用它们的SetSource方法,传递Downloader对象以及Zip文件中图像的路径。是的,就这么简单。

现在,.NET 3.5引入了扩展方法,它们实际上是添加到现有类中的方法,有点像在JavaScript中向原型添加方法(只有我这样认为,还是C#正在借鉴JavaScript的元素?)。我利用这个新功能为Silverlight中的Image控件实现了一个Clone方法,然后我用它从字典中克隆图像,并将它们提供给棋子。我这样做是因为拥有一个包含图像的Dictionary并能够按颜色和类型进行选择非常方便。但由于一个图像可以用于多个棋子,我需要将Dictionary视为“样本”图像。这是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;
        }
    }
}

请注意,包含扩展方法的类必须是静态的。创建此类的方法的语法是创建一个静态函数,该函数接受我们要扩展的类型的对象,但在类型之前添加this关键字。然后,您可以像调用其他任何方法一样调用它(带有Intellisense等)。

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

我想要介绍的最后一个杂项是路径。正如您可以猜到的,我用它们来绘制棋盘。每个方格都有一个路径,根据其环和方格以及一些三角函数动态绘制。那么,路径是由什么构成的呢?它们可以由贝塞尔曲线、直线、圆弧以及一些其他几何形状组成。如果您看看我的方格,您很快就会发现它们以一条曲线开始,然后有一条朝同一方向的半径内线,然后是另一个圆弧,然后闭合。让我们看看其语法:

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

由于所有这些不同的计算,它可能看起来有点令人生畏,但这里发生了什么:我们有一个路径,我们想向其中添加元素。我们首先创建一个PathGeometry,并给它一个PathFigureCollection。我们将集合分配给一个新的PathFigure,并按需进行设置(相对于整个路径的起点,以及我们是否希望它闭合),并将其分配给一个新的PathSegmentCollection。现在,每个新元素(圆弧、直线等,从现在起称为段)都将被添加到PathSegmentCollection中。最后,我们将PathGeometry分配给路径的Data属性,然后我们就完成了!

总结

因此,在讲述了Silverlight中所有您需要了解的元素之后,现在是时候再次谈谈游戏是如何工作的了。

有三个方法被不断调用:一个更新您的活动的方法,我为了简单起见,使用ASP.NET AJAX来完成;一个检查您的对手是否还在线的方法,这样您就不会等待一个已经离开的人;以及一个检查新移动的方法。一旦获得新的移动,棋子就会在UI上移动,然后轮到对方。

需要注意的是,如果一个玩家离开了一个游戏,另一个人可以加入它,即使它已经开始,并继续代替第一个玩家玩。

我最后想谈的是安全性。是的,我知道这只是一个游戏,但如果有人找到作弊的方法,那仍然很糟糕。您必须始终确保在服务器端采取安全措施,切勿依赖客户端。如果玩家无法通过UI移动棋子,因为它受到保护,但这并不意味着他不能直接向Web Services发送请求并“移动”他想要的棋子。这是需要考虑的一点。

待办事项

因为我确信没有人会看它,而且这是一项冗长而技术性的工作,所以我还没有进行移动验证,这意味着玩家可以随意移动任何棋子。当然,如果我要上传游戏供人们玩,这将是必须的,但因为我在这里将其作为示例代码发布,并且我想尽快发布它,所以我将其留待以后。

结论

至此,文章结束。我希望您喜欢它,并发现它对.NET 3.5中的新技术是一个很好的介绍。我写这篇文章的原因之一是听取别人的意见。所以,请在阅读到这里之后,再花几分钟时间留下评论:) 话虽如此,鉴于过去尝试用普通的AJAX和ASP.NET 2.0创建普通的国际象棋游戏,我无法充分表达这种方法是多么更好、更简洁。总而言之,这是一次相当有益的经历。

更改日志

  • 2007/12/21 - 发布文章。
  • 2007/12/25 - 修复了一些拼写错误。添加了一些截图,并添加了数据库简介。
  • 2007/12/30 - 重新设计了网站(不再有黑白!)。为对手移动的棋子添加了动画。添加了一个简单的控件,上面有一个白棋王的图像,用于跟踪当前回合(看起来很酷)。
© . All rights reserved.