(翻译)Windows Communication Foundation (Workshop)-Part 4:使用WCF以面向服务方式在客户端和服务端之间操作数据
Windows Communication Foundation (Workshop)
原文以及代码下载
Part 4:使用WCF以面向服务方式在客户端和服务端之间操作数据
作者Robert Shelton, Jr.
译者LoveCherry
在WCF Workshop的Part 4中,我们主要研究在如果使用遵从面向服务原则的方式在服务端和客户端之间传递和管理复杂数据(也就是对象)。
完成这个workshop需要的东西
占位
从哪里获取源代码和最新资料
占位
目标…[见此系列的Part 1]
占位
更新后的目标…[针对此系列的Part 4]
在本系列的Part 1至Part 3中我们研究了一些WCF的基础:如何建立一个简单的服务并且关联一个客户端应用程序,如何处理会话状态以及处理事务的能力。然而我们只考虑了很小一部分的客户需求(对于一个实用的ATM来说)。特别是这个服务提供的功能非常有限,客户只能有一个银行帐户,而且所有的银行交易(帐户信息)都是硬编码的。虽然这对于学习WCF的基础不错(比如说事务、会话等等),但是现在是时候增加一些更加复杂的数据了。在此系列的这部分中,我们将要使用面向服务的技术在客户端和服务间使用复杂数据。特别的,在后面更新后的应用场景介绍中我们会引入ATM卡,它们被编码成一个或多个银行帐户,通过这些银行卡客户就能在ATM机器上使用关联的帐户。(可以看看后面的银行系统逻辑图)
应用场景[更新自Part 1]
Part 1中介绍过,银行客户需要我们建立一个用于处理简单银行业务的基本服务(为了简单期间只有存款、取款和转帐)。在Part 4中,我们想要增加一个功能,让ATM用户能从一个帐户列表中选择关联于某个由此银行发行的ATM卡的帐户。我前面也说过了,在前面的workshop中(Part 1到3),它们都是硬编码入ATM客户端的。在这个workshop中我们要复杂数据(包含银行帐户的ATM卡对象)从服务端传入客户端,这样ATM用户就能得到一个更为逼真的“用户体验”。
回顾Part 1: SO(A/D)的四大原则
占位
系统构架图
占位
银行系统逻辑图
逻辑上说(可以看下面的图表):
- 客户(还没有实现)能有一个或多个ATM卡
- ATM卡能用来处理一个或多个银行帐户(检查、保存等等)
- 如果卡有多个卡号的话,银行帐户能通过多个ATM卡进行访问(设想一下两个人拥有的ATM卡中有共有帐户)
注意:“客户”这个概念没有在本workshop中实现,但为了有一个更清晰的逻辑我们把它也画在了图上。
进行编码…(我们要做什么和为什么要这么做)
粗略看下我们要做的步骤…
如果你已经对WCF比较熟悉了,可能会仅仅想看下我们在此workshop做的基本步骤的提纲:
1. 更新数据库来处理新数据(数据库构架能从附录中查到)
2. 在服务中增加表现新的数据类型的类(ATM卡、银行帐户)
3. 使用[DataContract]和[DataMember]让这些对象能显示地为客户端所用
4. 更新服务契约来支持新的ATM卡相关的服务(也就是方法)。
5. 测试然后部署更新后的服务
6. 更新客户端来使用新功能和数据
下面,我们会来看下更详细的步骤。
步骤:更新数据库构架来处理新的数据类型(ATM卡、银行帐户)
做什么:我们需要更新Part 3中数据库的构架来处理新的银行帐户和ATM卡结构。
为什么:就像我在银行系统逻辑图中描述的那样,我们需要支持这样一种情况:ATM卡和一组银行帐户关联,用户能通过ATM机来访问它们。
步骤:
- 和Part 3一样,“如何”来实现数据库数据的更新根据你选择的数据库会有不同(SQL Server Express, SQL Server 2005, SQL Server 2000等等)。
- 你能在此workshop的附录中找到数据库的结构和脚本,如果你宁愿导入结构的话也能从“DAC and Database Scripts”文件夹中找到Text/CSV文件。
注意:请确保你连接字符串(在web.config中)中使用的用户账号对所有表有读写权限。
步骤:更新数据访问组件(bankDAC.cs)
做什么:在WCF Workshop#3中,我建立了一个数据访问组件(DAC)在我们的服务类和银行数据库间操作数据I/O。在这里我更新了DAC以便处理新的数据库功能(构架和面向数据的方法)。
为什么:通过使用DAC,我们能在服务和数据库间增加一个抽象层。它能给我们一个清晰的服务类(也就是一些代码)并使我们能改变访问数据库的方式和数据库所在的地方(也就是,我们可以把DAC转化成一个服务或者一个DLL,修改的时候无需修改服务代码)。
步骤:
- 右键点击App_Code文件夹(在BankService解决方案内)
- 选择“Add Existing Item…”
- 浏览此workshop 的“Database Script and DAC Code”文件夹并且选择bankDAC文件,然后点击Add(或者双击)
- 如果DAC已经包含在你的解决方案内(Part 3 workshop的那个),此步操作会覆盖以前的版本。
你的解决方案应该如下:
步骤:为ATM卡和银行帐户新增类
做什么: 我们需要建立类,或者说复杂数据类型这样客户端可以使用ATM卡和银行帐户对象。
为什么:客户端应用程序有了ATM卡对象等复杂数据类型后,客户端程序员就可以遍历ATM卡(根据某个ATM卡帐户号码)以及自动获得ATM卡对象内的所有关联银行帐户和银行帐户对象提供的帐户信息。
步骤:
- 打开BankServices解决方案
- 接着,打开BankServices.cs文件(在App_Code文件夹中)
- 添加System.Collections.Generic 命名空间,因为我们在类代码中要用到。
- 为ATMCard类添加代码(我把这段代码放在文件最上面,命名空间的后面,这纯粹是个人的喜好而已,其实位置没有关系)
你的代码应该如下:
最后…
- 接下来,我们添加BankAccount类
步骤:让我们的数据类型可以被客户端访问
做什么:现在,我们需要创建一个“数据契约”来显式地让这些复杂数据类型(对象)可以被客户端访问。WCF就会根据客户端请求自动序列化对象,所以客户端不是直接和NET类新型交互而是类的XML表现。这完全符合服务原则的第三点:共享构架和契约而不是类。
注意:WCF支持两种形式的XML序列化方式:XMLFormatter(WCF默认)和老的XMLSerialization。在这个的例子中,我们使用XMLFormatter选项,它会在WCF序列化的过程中为客户保留数据表现。
为什么:通过向ATMCard和BankAccount添加[DataContract],WCF会明白这些类和服务中其它没有标记[DataContract]的类不同,这些标记属性的类型需要序列化成XML被外部的客户端使用。目的就是:共享构架和契约而不是类! 还有另外的一步就是:为了显式地让类成员(变量和属性)能被访问,我们还为类地成员添加[DataMember]属性。[DataMember]会把类成员标记为“私有”让服务地的客户进行访问。如果你不希望类成员可用的话就不要使用[DataMember]。
注意:为了通用性,我并没有把数据由DataSet来表现。Java客户端是不能识别和处理DataSet的,因为它仅仅是微软的类。
尽管我们使用的都是DataContract和DataMember属性的默认行为,但是我还是想花一点时间来讨论一些相关选项。
DataContract选项:
- Name:[DataContract(Name=”….”)] 能让你获取,更重要的是能设置客户端应用程序看到的类名。在这样的情况下有用,比如说在服务内部你的类命名为“Card”但是你希望当客户端程序员引用类的时候他们看到的是“ATMCard”。
- Namespace:[DataContract(Namespace=”….”)] 能让你获取或设置类的命名空间。比如:在服务中我能有两个叫“Account”的类,一个在Back命名空间内(用作银行帐户),另外一个在User命名空间内(用作登陆帐户)。
DataMember选项:
- IsRequired:[DataMember(IsRequired=True/False)] 告诉序列化引擎这个成员的值客户端应用程序是否需要
- Name: [DataMember(Name=”…”)] 能让你设置数据成员的外部名字,而不是内部名字。比如说,一个变量在服务内部叫做fname,但在客户端可以显示为FirstName,这样就更容易理解了。
- Order: [DataMember(Order=”…”)] 能让你设置类成员序列化/反序列化的次序(由XML文档表示)。
我们更新后的ATM类应该如下:(注意:为了可读性更强,我收缩了部分代码块)
我们更新后的BankAccount类应该如下:(注意:同样,为了可读性更强,我收缩了部分代码块)
步骤:更新服务契约
做什么:我们需要为数据契约新增一个方法作为一个“ATMCard”对象的“访问方法”。客户端应用程序会调用这个方法传入卡号,然后服务就会返回ATM卡信息(比如到期时间等等)和相关的银行帐户以及帐户细节。
为什么:从技术上说,我们完全可以在代码中使用“NEW ATMCard()”然后在类中设置一个方法或构造方法来接受填充后的数据对象。而本例中我没有这么做,仅仅是个人选择在workshop中使用的应用程序构架而已。
步骤:
- 打开BankServices解决方案
- 接着,打开BankServices.cs文件(在App_Code文件夹内)
- 添加“getATMCardInfo”方法
你的代码应该如下:
- 为新的方法增加“实现”代码
你的代码应该如下:
步骤:测试我们更新后的服务
做什么:我们要测试服务来确保它正常监听,因为就算成功编译后还会有很多错误。
为什么:Part 1中我说过,没有客户端的话确实没有什么简单的方法来测试我们服务中的业务逻辑,因此我们在部署前所能做的也仅有测试服务端点(EndPoint)来保证服务能启动并正常响应。
步骤:
- 编译并运行服务
如果一切正常,屏幕应该显示如下:
步骤:部署更新后的服务
做什么:既然我们知道我们的服务至少能监听一个我们建立的端点(URL地址),我们接下来部署服务。
为什么:每次我们更新服务增加了一些行为和业务逻辑后必须把它重新部署到应用程序服务器(本例中为IIS)。
步骤:
- 第一步,让我们选择Build Menu菜单的Publish Web Site选项
- 我们的目标URL:http://localhost/services/bankaccountservices应该还在目标文本框内,如果没有的话请重新输入一次。
屏幕应该显示如下:
- 点击OK按钮开始部署。
- 如果做过Part 1中的例子或者以前部署过这个服务的话,你会看到下面的提示:
- 点击Yes继续,“覆盖”以前的网站。
- 从IDE中你应该能得到一个“Publish Succeeded”的消息,如果没有的话,请检查所有步骤并确保IIS已经正确配置为ASP.NET2.0。
步骤:更新客户端来使用更新后的服务
做什么:接下来,我们需要更新客户端来使用我们的服务。
为什么:这步操作会确认服务代理代码,来检查我们客户端应用程序中使用的更新后的方法、端点是否还有效。
步骤:
- 打开ATM客户端解决方案
- 删除app.config。尽管这步不是必须的,但我发现每次更新客户端的时候VS2005都会新在app.config中增一个节点而不是替换原来的。
- 现在,通过重新连接服务并得到最新的配置方案和代码代码来更新客户端。
o 右键点击ATMServices.map文件
o 点击“Update Service Reference”选项
o 这样,客户端就会联系服务来更新自身并且新建一个app.config。
我们的解决方案看上去应该如下:
步骤:更新客户端代码
做什么:我们需要从几个方面来更新客户端,让它能利用服务上设置为可访问的一些复杂数据(ATMCard和BackAccount对象)。在这个workshop中,我不推荐你手动更新这些客户端的代码来使用这些新特性,因为基本整个ATM图形界面和相关代码我都做了小修改。为了图形界面能(使用新的对象)实现新的功能,我做了太多小的修改了,这里那里。然而,我还是列出了一些大的改动,让你能体会到使用这些新的对象是多么简单。
为什么:和以前一样,当我们更新了服务后我们也需要更新客户端来使用这些服务提供的新功能。下面你会发现有很多方面改善,我把以前硬编码的相关银行帐户号都改成了服务提供的新的对象的数据。注意,对于ATM卡号我仍然使用了硬编码,但除此之外,其它的和以前都不同了,我们是从数据库读取真实数据(帐户号、交易和余额等等)。
一些客户端的改变:
- 我把“类级别”的用于保存服务代理的变量移至了类顶部,我又加入了一个用于表示ATMCard的新变量。现在Form的Main区域如下:
- 现在我们可以使用新的ATMCard来向帐户页面的DataGrid加载数据了。
代码:
在UI上显示:
- 另外一个我们需要利用ATMCard对象的区域是银行转帐页面。现在我们能访问多个帐户(实时),在这以前我们都是硬编码的(Workshop Part3中),这样我们就能实时得到各个帐户的余额。
代码:
在UI上显示:
步骤:使用ATM客户端测试我们更新后的服务
做什么:现在是测试我们客户端和更新过的服务的时候了。
为什么:我们要首先要确保所有以前的功能都正常工作(存款、取款和转帐),而且我们还要来看下由ATM Card和Bank Account对象提供的复杂数据结构是否都能访问。
注意:本例代码中有一个实现做好的ATM客户端可以使用,它已经更新了UI和相关代码,你可以用它来测试新特性。
步骤:
- 先存入一点钱,然后取出一点钱,然后转帐
- 如果你喜欢刺激点的话可以在为硬编码的ATM卡(“
WCF Workshop Part 4结束语
在这部分WCF workshop中,我们看到了要在服务和客户端之间共享复杂数据是多么简单,并且始终贯彻的是“共享构架和契约而不是类”这么一个服务原则。本练习中我们唯一没有做的就是允许不同的客户(更精确的说是各种ATM卡)来使用我们的服务。这可能还需要某种认证方案,用户就好像真的把ATM卡插入机器然后输入PIN号码(密码)那样。ATM机器读出卡号然后把它发送到认证服务取去验证。一旦我们确定卡号是有效后,接下来的客户端应用程序就和本例中差不多了。
附录I:数据库词典
Accounts表结构和数据
结构
原始数据:你需要自己添加
ATMCards表结构
结构
原始数据:你需要自己添加
Transactions表结构
结构
原始数据:你需要自己添加
附录II:数据库建立脚本(同样以文本文件形式存在于下载文档中)
注意:你首先需要建立一个名叫“WCFWorkshop”的数据库。
Accounts表脚本
USE [WCFWorkshop]
GO
/****** Object: Table [dbo].[Accounts] Script Date: 06/28/2006 13:39:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Accounts](
[Id] [int] IDENTITY(1,1) NOT NULL,
[AccountNumber] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[AccountType] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[AssociatedATMCard] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
) ON [PRIMARY]
AtmCards表脚本
USE [WCFWorkshop]
GO
/****** Object: Table [dbo].[ATMCards] Script Date: 06/28/2006 13:40:14 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[ATMCards](
[Id] [int] IDENTITY(1,1) NOT NULL,
[CardNumber] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[CardExpirationDate] [datetime] NULL
) ON [PRIMARY]
Transactions表脚本
USE [WCFWorkshop]
GO
/****** Object: Table [dbo].[Transactions] Script Date: 06/28/2006 13:40:49 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Transactions](
[id] [int] IDENTITY(1,1) NOT NULL,
[AccountNumber] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[TransactionType] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[Amount] [decimal](18, 2) NOT NULL CONSTRAINT [DF_Transactions_Amount] DEFAULT ((0.00)),
[TransactionDate] [datetime] NOT NULL CONSTRAINT [DF_Transactions_TransactionDate] DEFAULT (getdate()),
[TransactionSource] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
CONSTRAINT [PK_Transactions] PRIMARY KEY CLUSTERED
(
[AccountNumber] ASC,
[TransactionDate] ASC
)WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
USE [WCFWorkshop]
GO
/****** Object: Table [dbo].[Accounts] Script Date: 06/28/2006 13:39:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Accounts](
[Id] [int] IDENTITY(1,1) NOT NULL,
[AccountNumber] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[AccountType] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[AssociatedATMCard] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
) ON [PRIMARY]
AtmCards表脚本
USE [WCFWorkshop]
GO
/****** Object: Table [dbo].[ATMCards] Script Date: 06/28/2006 13:40:14 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[ATMCards](
[Id] [int] IDENTITY(1,1) NOT NULL,
[CardNumber] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[CardExpirationDate] [datetime] NULL
) ON [PRIMARY]
Transactions表脚本
USE [WCFWorkshop]
GO
/****** Object: Table [dbo].[Transactions] Script Date: 06/28/2006 13:40:49 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Transactions](
[id] [int] IDENTITY(1,1) NOT NULL,
[AccountNumber] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[TransactionType] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[Amount] [decimal](18, 2) NOT NULL CONSTRAINT [DF_Transactions_Amount] DEFAULT ((0.00)),
[TransactionDate] [datetime] NOT NULL CONSTRAINT [DF_Transactions_TransactionDate] DEFAULT (getdate()),
[TransactionSource] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
CONSTRAINT [PK_Transactions] PRIMARY KEY CLUSTERED
(
[AccountNumber] ASC,
[TransactionDate] ASC
)WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]