跨层共享代码
很多时候都需要在中间层与显示层共享自定义代码。比如,想要在服务器与客户端之间共享自定义验证逻辑和业务逻辑。RIA服务可以实现在Web项目中书写代码,而在客户端项目中自动生成复制代码,达到了代码一处创建维护,多处自主调用的良好效果。实现这一功能只需要将需要共享代码(在服务器端)文件以.shared.cs扩展名命名即可,这样代码生成器就会自动完成在客户端生成代码的任务。当然注意不要使用平台依赖的技术(比如只适用Asp.Net的代码不能使用),否则不能正常工作。另外某些属性与方法只对客户端有用,这种情况下最好通过直接在客户端编写部分类来实现,防止代码的冗余,并便于维护。
实例:创建共享代码文件
场景说明:扩展Product实体,添加一个计算属性:ProfitMargin,这一属性通过计算售价(ListPrice)减成本(StandardCost)获取。要求代码在服务器与客户端同时可以访问使用:
1)在Web项目的Models\Shared文件夹创建一个新类:Product.shared.cs;
2) 移除System.Web的引用,因为该名称空间并没有包含在SilverLight里;
3)修改代码使之与下面的代码一致:
namespace AdventureWorks.Web { public partial class Product { public decimal ProfitMargin { get { return ListPrice - StandardCost; } } } }
与默认生成的代码相比,除了移除了相应的名称空间以外,还将类更该为Product的部分类,而不是默认的共享类名,这样就相当于为实体Product增加了一个计算出来的属性。
使用WCF RIA服务类库封装业务逻辑
为了实现各个层间的解耦,可以将业务逻辑移出Web项目中,放在单独的WCF RIA服务类库项目中。VS提供的WCF RIA服务类库模板是为现有SilverLight解决方案添加类库设计的,不是一个全功能的解决方案模板。如果使用该模板在解决方案中添加项目,会同时添加两个项目,一个以XXX.Web命名,另一个以XXX命名,如图所示。两个项目共享RIA 服务连接,即可以将Web项目与Silverlight项目间的连接移除,实现解耦。然后就可以将域服务,实体模型,元数据类,Presentation Model类以及共享代码等移到XXX.Web的类库中,RIA服务会为Silverlight类库生成相应的代码,与前面自动生成的方法类似。
需要说明的是,如果使用RIA服务的授权功能 ,在类库中使用AuthenticationService,WebContext对象不会在SIlverlight应用程序中生成,需要手工实现WebContext类,设置AuthenticationContext。
案例:将业务逻辑移到WCFRIA服务类库中,Web项目仅用于保存XAP文件。
1)使用WCF RIA Services Class Library project模板在解决方案内添加一个新项目:AdventureWorksMiddleTier,新项目下挂两个项目,客户端项目自动连接到RIA服务项目上;
2)移除现有的RIA服务连接;
3)删除类库中的Classic.cs文件;
4)打开AdventureWorksMiddleTier.Web类库的项目属性页,修改其默认名称空间为AdventureWorks.Web(这样做的好处是可以直接将该名称空间下的代码复制过来,无需进行项目引用操作).
5)打开AdventureWorksMiddleTier 类库的项目属性页,修改其默认名称空间为AdventureWorks。
6)将AdventureWorks.Web中所有域服务类,实体模型类,资源,元数据类,Presentation Model类和共享代码移到AdventureWorksMiddleTier.Web类库里;
7)向AdventureWorksMiddleTier.Web类库添加如下程序集:
• System.ComponentModel.DataAnnotations
• System.ServiceModel.DomainServices.EntityFramework
• System.ServiceModel.DomainServices.Server
• System.ServiceModel.DomainServices.Hosting
• System.Web
• System.Web.ApplicationServices
8)确保现在可以成功生成AdventureWorksMiddleTier.Web项目;
9)在AdventureWorksMiddleTier项目中添加Models文件夹,将RegistrationData.partial.cs,User.partial.cs从Silverlight项目中的Models文件夹中移到此新建文件夹。
10)RegistrationData.partial.cs,User.partial.cs的属性与方法需要从internal调整为public,包括PasswordAccessor, PasswordConfirmationAccessor, CurrentOperation和 UserNameEntered。为了防止这些属性在注册界面中作为输入字段,需要使用Display特性,将AutoGenerateField属性设置为false:
[Display(AutoGenerateField = false)]
public Func<string> PasswordAccessor { get; set; }
11)注意到SIlverlight项目中的Web/Resources文件夹中的.resx文件有一点图标指示无法找到,这是因为已经将源文件移到了AdventureWorksMiddleTier.Web项目中。删除Web文件夹和里面的内容,现在需要在AdventureWorksMiddleTier 类库项目中重建该结构;
12)在AdventureWorksMiddleTier 项目中创建一个文件夹Web,并在其下再创建一个文件夹:Resources;
13)左键单击Resources文件夹,选择添加---现有项,导航到AdventureWorksMiddleTier.Web项目文件夹下的Resources文件夹,选择该文件夹中的所有文件。现在想要添加连接到这些文件,可以通过点击添加按钮旁边的下拉图标,从菜单中选择作为连接添加。
14)确保现在可以成功生成AdventureWorksMiddleTier项目;
15)在AdventureWorks.Web 项目中添加对AdventureWorksMiddleTier.Web类库的引用,在AdventureWorks 项目中添加对AdventureWorksMiddleTier 类库的引用;
16)在AdventureWorks中创建一个新类:WebContext:
using System.ServiceModel.DomainServices.Client.ApplicationServices; using AdventureWorks.Web; namespace AdventureWorks { public sealed partial class WebContext : WebContextBase { partial void OnCreated(); public WebContext() { this.OnCreated(); } public new static WebContext Current { get { return ((WebContext)(WebContextBase.Current)); } } public new User User { get { return ((User)(base.User)); } } } }
17)设置应用程序启动时的授权上下文。打开AdventureWorks 项目的App.xaml.cs文件,添加如下代码到 Application_Startup方法上:
private void Application_Startup(object sender, StartupEventArgs e) { ((WebAuthenticationService)WebContext.Current.Authentication) .DomainContext = new AdventureWorks.Web.AuthenticationContext(); this.Resources.Add("WebContext", WebContext.Current); WebContext.Current.Authentication .LoadUser(this.Application_UserLoaded, null); this.InitializeRootVisual(); }
18)完成!现在可以编译并成功运行程序。
处理错误
错误发生时,最好对其进行记录,比如写入事件日志,数据库或文本文件中,以便跟踪异常发生的原因。通常在错误发生时可能希望执行一些其他任务,如发送e-mail通知系统管理员。可以在每个域操作中这样处理,但是更好的方法是在域服务级别处理,这可以通过覆写域服务的OnError方法实现 :
protected override void OnError(DomainServiceErrorInfo errorInfo) { // Log exception here. }
该方法在域服务内发生任何异常时都会调用,这样就可以记录错误并根据需要处理错误。错误信息内容包括:
- 何时发生错误
- 何用户遇到此错误
- 何方法发生错误
- 错误信息
- 错误堆栈
- 任何相关数据:如当前的变更集,可以通过域服务的ChangeSet属性获取,可以协助调查原因
处理数据并发冲突
默认情况下,RIA服务并不执行并发检查。在更新或删除数据库的数据之前应该执行这种检查,为了进行并发检查,需要
1)在实体中有一个属性可以对原始版本和存储版本的数据进行比较以检查是否存在并发充突;
2)该属性的原始值在更新实体的同时由客户端返回;
当客户端将实体返回服务器时,仅仅发送了更新后的实体。而任何修饰有ConcurrencyCheck,TimeStamp和RoundtripOriginal特性标识的属性将在执行更新操作的时返回原始值。这样通过比较这些属性就可以检查并发冲突。
实例:配置实体模型以检查并发冲突
场景:在Product实体中有一个属性ModifiedDate,是DateTIme类型的,可以用此属性来进行并发检查:
1)打开AdventureWorksModel实体模型
2)找到Product实体;
3)选择ModifiedDate属性,设置其Concurrency模式属性为Fixed;
4) ModifiedDate字段不会在记录更新时由数据库触发器进行更新(因为这是一个处理并发冲突的字段))。因此需要显示地更新该属性,找到域服务的更新操作方法UpdateProduct;
5)在代码中添加如下语句:
public void UpdateProduct(Product currentProduct) {
currentProduct.ModifiedDate = DateTime.Now; this.ObjectContext.Products.AttachAsModified(currentProduct, this.ChangeSet.GetOriginal(currentProduct)); }
为Presentation Model类实现并发冲突检查
同样需要指定哪个属性在更新对象时将原始值发回服务器,然后在更新数据库时执行并发检查。
实施步骤:
1)在需要进行并发检查的属性上修饰ConcurrencyCheck, TimeStamp, 或RoundtripOriginal,以使其返回服务器的值都是原始值;
[ConcurrencyCheck]
public DateTime ModifiedDate { get; set; }
2)在域服务的更新操作方法里对比该属性的原始值和存储值。但是方法只从参数中传入了更新后的实体,如何获取原始实体的数据呢?可以调用ChangeSet对象的GetOriginal方法,注意只有指定进行并发检查的属性才返回值(估计其他属性都只能返回空值):
var originalProduct = ChangeSet.GetOriginal<ProductSummary>(productSummary);
3)对比数据,以识别并发冲突,如果发现冲突想要通知客户端,需要获取即将更新对象的ChangeSetEntry对象,并为该对象的ConflictMembers, StoreEntity和IsDeleteConflict 属性赋值:
var originalProduct = ChangeSet.GetOriginal<ProductSummary>(productSummary); ProductSummary storedProduct = GetProduct(productSummary.ProductID); if (storedProduct.ModifiedDate != originalProduct.ModifiedDate) { ChangeSetEntry entry = ChangeSet.ChangeSetEntries.Single(p => p.Entity == productSummary); List<string> conflicts = new List<string>(); conflicts.Add("ModifiedDate"); entry.ConflictMembers = conflicts; entry.IsDeleteConflict = false; entry.StoreEntity = storedProduct; } else { // Save ProductSummary... }
事务
事务通常用于在数据库更新数据的过程,当更新失败(比如发生并发冲突),事务就会执行回滚操作,回到事务启动前的数据库状态。使用事务可以使数据库不会因变更进入未知或不一致的状态。一般来说,需要将提交到数据库的变更操作封装到事务中(EF框架已经实现了提交变更的事务操作)。如果需要手工封,需要覆写基类的Submit方法,将对Submit方法的调用封装在事务域中,如下所示:
public override bool Submit(ChangeSet changeSet) { bool result = false; TransactionOptions transactionOptions = new TransactionOptions(); transactionOptions.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted; using (TransactionScope transaction = new TransactionScope( TransactionScopeOption.Required, transactionOptions)) { result = base.Submit(changeSet); transaction.Complete(); } return result; }