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

支持 JSON 的 WCF 服务(第 II 部分)

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.83/5 (3投票s)

2012年1月26日

CPOL

9分钟阅读

viewsIcon

36698

downloadIcon

534

实现基于 JavaScript 的客户端与 WCF 服务交互

引言

本文继续《启用 JSON 的 WCF 服务(第一部分)》一文,在文中我们描述了如何创建一个接收和传递 JSON 格式数据的 WCF 服务。

现在,我们将实现一个基于 JavaScript 的客户端,它将与我们的 WCF 服务进行交互。但是,我们不会编写一行纯 JavaScript 代码来实现这一点。相反,我们将使用 Script#。它允许我们使用我们熟悉的编程语言 C# 编写代码,然后将其转换为 JavaScript。

您可以从 Script# insights 文章或该产品的官方网站 [http://projects.nikhilk.net/ScriptSharp] 中获得有关 Script# 的一些基本知识。您可以在那里下载 Script# 的最新版本。

那么,让我们开始吧!

创建客户端数据模型类

我们从消息类开始:ContinentPopulationContinentDetailsContinentDetailRequestContinentDetailsResponseContinentsListResponse

您可能还记得,以上每个类的成员都是自动实现的属性。但是,如果您尝试使用此类属性来编译 Script# 项目,您将收到以下错误:“Feature ‘automatically implemented properties’ cannot be used because it is not part of the ISO-2 C# language specification”(自动实现属性功能由于不属于 ISO-2 C# 语言规范而无法使用)。因此,我们必须修改我们的类,使其符合语言规范。

这可以通过多种方式实现。我将向您展示所有这些方法,以便您可以选择最方便的一种。

第一种,也是可能最简单的方法是专门为 Script# 项目创建数据模型。例如,对于通用的 C# 项目,ContinentPopulation 消息类的写法如下:

[DataContract(Name = "ContinentPopulation")]
public class ContinentPopulation
{
[DataMember(Name = "ContinentName",IsRequired = true,Order = 0)]
public string ContinentName { get; set; }

[DataMember(Name = "TotalPopulation", IsRequired = true, Order = 1)]
public int TotalPopulation { get; set; }
}

在 Script# 语言中,它将如下所示:

   public class ContinentPopulation
{
public string ContinentName;
public int TotalPopulation;
}

这不是最好的方法,我也不推荐使用它。是的,如果您创建一个只包含少量数据模型类的示例应用程序,这可能很好。但实际项目包含大量的类,可能多达一百个。您可以想象,您将需要持续同步这些类中的更改。

为了避免数据模型类的重复,我建议使用预处理器指令。首先,我们需要向数据模型服务项目添加一个条件编译符号。我们称之为SERVICEMODEL。我们将在#IF指令中使用它来定义服务器端代码,而这些代码不会在 Script# 类中使用。

创建数据模型项目

现在,我们在解决方案中添加一个项目,它将包含 Script# 的数据模型。让我们将其命名为ClientDataModel

在添加项目时,系统会提示您指定一个文件夹来复制生成的 js 文件:让我们选择 Web 应用程序的Scripts文件夹。

现在,有必要在创建的项目中添加对数据模型服务类的引用。

让我们看看类的代码应该是什么样子。我不会展示每个类的代码,您可以在本文附带的示例中查看。让我们只看ContinentPopulation类的代码。对其他数据模型类的更改将是相同的。

1.  using System;
2.  using System.Collections.Generic;
3.  using System.Linq;
4.  using System.Text;
5.  using System.Runtime.Serialization;
6.
7.  namespace DataModel
8.  {
9.      [DataContract(Name = "ContinentPopulation")]
10.     public class ContinentPopulation
11.     {
12.         [DataMember(Name = "ContinentName", IsRequired = true, Order = 0)]
13.         public string ContinentName
14.         {
15.             get;
16.             set;
17.         }
18.
19.         [DataMember(Name = "TotalPopulation", IsRequired = true, Order = 1)]
20.         public long TotalPopulation
21.         {
22.             get;
23.             set;
24.         }
25.     }
26. }

这是包含客户端和服务器端混合代码的片段:

#if SERVICEMODEL

#else

#endif

现在让我们回顾一下上面的列表。我们的类不使用System.Text命名空间,因此第 4 行可以删除。System.Runtime.Serialization命名空间在 Script# 中不存在,但在 C# 中使用(包含DataContractDataMember类),因此第 5 行应替换为以下代码:

#if SERVICEMODEL
using System.Runtime.Serialization;
#endif

我们在 Script# 中不需要DataContractDataMember属性,因此我们将它们放在“#if SERVICEMODEL”指令中(第 9、12、19 行)。

我们将对属性做同样的事情。您可以在下面的代码中看到更改后的类的代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
#if SERVICEMODEL
using System.Runtime.Serialization;
#endif

namespace DataModel
{
#if SERVICEMODEL
[DataContract(Name = "ContinentPopulation")]
#endif
public class ContinentPopulation
{
#if SERVICEMODEL
[DataMember(Name = "ContinentName", IsRequired = true, Order = 0)]
#else
[PreserveCase()]
#endif
public string ContinentName
#if SERVICEMODEL
{
get;
set;
}
#else
;
#endif
#if SERVICEMODEL
[DataMember(Name = "TotalPopulation", IsRequired = true, Order = 1)]
#else
[PreserveCase()]
#endif
public long TotalPopulation
#if SERVICEMODEL
{
get;
set;
}
#else
;
#endif
}
}

所有数据模型类都应以这种方式进行转换。PreserveCase属性用于避免在生成 JavaScript 代码时转换类成员名称。最后,我们将数据模型类中的命名空间从ServiceDataModel更改为DataModel。现在,让我们继续实现我们应用程序的客户端。

创建客户端 Script# 应用程序

我们在解决方案中添加一个名为“ClientLibrary”的“jQuery Script Library”项目。

为了避免在编译 Script# 项目时将 js 文件复制到 Web 应用程序,您可以像添加 Script# 项目到数据模型时一样设置Scripts文件夹。让我们忘记 JavaScript 对接口或类的概念,完全忘记 JavaScript。让我们使用我们习惯的方式(基于 C# 规范 ISO-2)用 C# 编写代码。

服务交互层

我可以粗略地描述 WCF 服务和客户端应用程序之间的交互方式如下:

WCF 有两个public方法:GetContinentsGetContinentDetails,它们由IContinentsPopulation接口定义。

在客户端应用程序中,可以定义一个服务层,它将完成从服务获取数据的所有工作——调用服务方法:GetContinentsGetContinentDetails。我们定义了一个IService接口,其中包含以下方法:GetContinentsGetContinentDetails,它们的名称与服务方法名称类似。

与服务的交互将是异步的,因此接口方法将传递回调方法。这些回调方法将在查询成功或失败后执行。接口代码如下所示:

public interface IService
{
void GetContinents(AjaxRequestCallback successCallback, AjaxErrorCallback errorCallback);

void GetContinentDetails(ContinentDetailRequest request, 
    AjaxRequestCallback successCallback, AjaxErrorCallback errorCallback);
}

让我们创建一个实现IService接口的类,并将其命名为ServiceProviderServiceProvider类将形成 AJAX 查询到服务器,并设置查询成功或失败完成的处理程序。类的实现非常简单,因此我们在这里不进行审查。让我们更好地审查GetRequestDataPrepareDataForRequest方法。

GetRequestData方法使用 Script# 提供的标准类将对象转换为 JSON 格式的string

WCF 服务上的GetContinentDetails方法有一个名为“request”的参数。它略微限制了查询数据,应该如下提供:

{ “request” : { “ContinentName” : “” } }

在这种情况下,在 WCF 服务上序列化数据时,将创建一个类型为ContinentDetailRequest的对象,并带有请求名称。因此,我们在 Script# 项目中创建一个字典,并将所有类型为ContinentDetailRequest的对象放在键为“request”的条目下。将其转换为 JavaScript 后,此方法的代码将如下所示:

...
var dataRequest = {};
dataRequest['request'] = obj;
return dataRequest;
...

也可以通过另一种方式实现:创建一个包含带有请求名称和对象类型的字段的类。例如,如下所示的RequestData类:

public class RequestData
{
public object request;
}

然后,可以在PrepareDataForRequest方法中创建RequestData类型的对象,将字段值设置为请求,然后将其传递进行序列化:

private object PrepareDataForRequest(object obj)
{
RequestData t = new RequestData();
t.request = obj;
return t;
}

SetData方法添加到IService接口。此方法将处理从服务器传递的数据,删除不必要的信息。相应地,ServiceProvider类中此方法的实现将如下所示:

public object GetData(object serverObject)
{
return JsonDataConverter.GetData(serverObject);
}

应用程序的客户端将包含三个层——一层已经审查过——这是与服务器交互的层。下一层是数据处理层。它准备服务器端执行方法所需的数据,并处理来自服务器的响应。第三层负责在页面上显示数据。

现在,让我们向ClientLibrary项目添加一个管理从服务器获取数据(数据处理层)过程的类,并将其命名为ContinentsManager

我们不考虑此类代码,因为它很容易理解。只是值得一提的是,此类应该以某种方式通知数据显示层数据已加载。JavaScript 没有事件,但 Script# 允许我们以 C# 的方式描述事件,隐藏了我们整个机制的实现。因此,我们绝对可以在ContinentsManager类中描述以下事件:

public event ContinentDetailsLoadedCallback ContinentDetailsLoaded;
public event ContinentsListLoadedCallback ContinentsListLoaded;
Every event type should have a delegate. Corresponding delegates are provided below:
public delegate void ContinentsListLoadedCallback(List continents);
public delegate void ContinentDetailsLoadedCallback(ContinentDetails continentDetails);

现在让我们继续进行数据处理和显示层。

数据处理和显示层

让我向您展示带有数据的页面应该是什么样子:

表结构如下

<table id="tbt_t">
<thead>
<tr></tr>
<th>
 Name</th>
<th>
 Population</th>
</tr>
</thead>
<tbody></tbody>
<tr></tr>
<td></td>
[name]</td>
<td></td>
[population]</td>
</tr>
</tbody>
</table>

现在,我们将ViewManager类添加到ClientLibrary项目中。此类负责数据显示。在上一步中,我们创建了ContinentsManager类,该类允许从服务器获取数据。它有两个异步方法和相应的事件。让我们创建一个类型为ContinentsManagerprivate字段,名称为_continentsManager。在ViewMethod构造函数中初始化此字段。

public ViewManager()
{
_continentsManager = new ContinentsManager();
_continentsManager.ContinentDetailsLoaded += 
    new ContinentDetailsLoadedCallback(_continentsManager_ContinentDetailsLoaded);
_continentsManager.ContinentsListLoaded += 
    new ContinentsListLoadedCallback(_continentsManager_ContinentsListLoaded);
}

SetElement方法添加到ViewManager类。此类设置和配置用于数据显示的元素。让我们审查一下SetElement方法的工作。它调用InitializeViewElement方法(您可以在下面看到其代码):

1.  jQueryObject viewObject = jQuery.Select(selector);
2.  if (viewObject.Length > 0)
3.  {
4.      _viewElement = (TableElement)viewObject.GetElement(0);
5.      jQuery.FromElement(_viewElement).CSS("border", "1px solid black");
6.      InitializeHeader();
7.  }

在第一行代码中,我们获取page元素。在 JavaScript 中,这一行看起来是这样的:

var viewObject = $(selector);

第二行检查是否找到了元素。第四行获取第一个元素——JavaScript 等效代码:

this._viewElement = viewObject.get(0);

第五行使用 JQuery 为我们的元素设置“border”CSS 属性——JavaScript 等效代码:

$(this._viewElement).css('border', '1px solid black');

InitializeHeader方法的代码在下表中设置,其中 Script# 中的每一行代码都对应 JavaScript 中的一行代码。此方法创建一个包含 2 列的表格标题:“Name”和“Population”。

Script#

Element headElement = Document.CreateElement("thead");
Element trElement = Document.CreateElement("tr");
Element thNameElement = Document.CreateElement("th");
jQuery.FromElement(thNameElement).Text("Name");
Element thPopulationElement = Document.CreateElement("th");
jQuery.FromElement(thPopulationElement).Text("Population");
trElement.AppendChild(thNameElement);
trElement.AppendChild(thPopulationElement);
headElement.AppendChild(trElement);
_viewElement.AppendChild(headElement);

JavaScript

var headElement = document.createElement('thead');
var trElement = document.createElement('tr');
var thNameElement = document.createElement('th');
$(thNameElement).text('Name');
var thPopulationElement = document.createElement('th');
$(thPopulationElement).text('Population');
trElement.appendChild(thNameElement);
trElement.appendChild(thPopulationElement);
headElement.appendChild(trElement);
this._viewElement.appendChild(headElement);

当包含大陆和人口的列表从服务器加载时,将调用_continentsManager_ContinentsListLoaded方法。_continentsManager_ContinentsListLoaded方法搜索客户端列表并将数据添加到表中。当表行形成时,包含大陆名称数据的单元格将绑定到单击事件。此事件处理程序获取单元格内容——在本例中为大陆名称——并查询该大陆的详细数据。

tdNameElement.AddEventListener(
"click",
delegate(ElementEvent e)
{
_continentsManager.GetContinentDetails(e.Target.InnerText);
},
false);

当大陆的详细数据从服务器加载时,将调用_continentsManager_ContinentDetailsLoaded方法。此方法构建包含大陆数据的字符串,并在模态窗口中显示它们。

StringBuilder builder = new StringBuilder();
builder.AppendLine(string.Format("ContinentName: {0}", continentDetails.ContinentName));
builder.AppendLine(string.Format("Area: {0}", continentDetails.Area));
builder.AppendLine(string.Format("PercentOfTotalLandmass: {0}", 
    continentDetails.PercentOfTotalLandmass));
builder.AppendLine(string.Format("TotalPopulation: {0}", 
    continentDetails.TotalPopulation));
builder.AppendLine(string.Format("PercentOfTotalPopulation: {0}", 
    continentDetails.PercentOfTotalPopulation));
Script.Alert(builder.ToString());

ViewManager类已准备就绪,相应地,数据显示层也已准备就绪。

让我们看看如何使用我们创建的所有 Script# 内容,通过将所有必要的脚本添加到页面:

<script src=”Scripts/jquery-1.6.2.js” type=”text/javascript”></script>
<script src=”Scripts/mscorlib.debug.js” type=”text/javascript”></script>
<script src=”Scripts/ClientDataModel.debug.js” type=”text/javascript”></script>
<script src=”Scripts/ClientLibrary.debug.js” type=”text/javascript”></script>

并添加以下 JavaScript 代码:

var viewManager = new ClientLibrary.ViewManager();
$(document).ready(function () {
viewManager.setElement("#dataTable");
viewManager.loadData();
});

此代码创建ClientLibrary.ViewManager类的一个对象。在页面加载事件处理程序中设置一个元素,该元素将用于显示数据并调用数据上传方法。

应用程序在浏览器中的外观如下图所示。表格显示国家及其人口的列表。如果您单击国家名称,将从服务器加载该国家的详细数据,并在浏览器对话框窗口中显示。

© . All rights reserved.