【IT168专稿】在基于Silverlight技术开发RIA数据中心型应用的过程中,存在一个较别扭的问题就是,无论你选择的是何种数据访问技术,不管是传统型Web服务,是WCF服务,还是ADO.NET数据服务技术,都需要你以手工方式从Silverlight项目中手工添加对这些服务的引用,并进行较多的手工编码。也就是说,数据访问仍然是这类RIA应用的一个羁绊。
针对上述问题,微软在推出Silverlight 3.0的几乎同时,又推出了RIA Services的又一个版本-July 2009 Preview。
在对服务器端的数据访问中,CRUD操作是最典型的操作。为此,RIA服务对之提供了现成的支持,包括在域服务方法定义方面作了相应的规范化要求。但是,有些情况下,我们又经常使用到众多的非CRUD操作,例如条件性计算及普通服务型方法计算等。为此,RIA服务也提供了相应的规定。
在本文中,我们将通过一个简单的例子探讨Silverlight 3RIA服务编程中定义方法及普通服务的编程技巧及有关注意事项。
创建Silverlight 3示例工程
(1)打开Visual Studio 2008,选择"文件|新建|项目"菜单命令,打开"新建项目"对话框。
(2)选择"Silverlight Application"模板,创建一个Silverlight 3项目,并命名为S3RIACustomSample。
(3)单点"确定"按钮,进入到下一步以选择silverlight应用的宿主网站。从web project type下拉列表框中选择"ASP.NET Web Application Project"。选中对话框中最下面的"Enable .NET RIA Services"复选按钮,这样便把RIA框架支持添加到当前解决方案中。
至此,我们创建了两个工程:
1. S3RIACustomSample-此工程中包含了Silverlight代码,这个工程称为客户端工程,这是我们创建的应用程序的客户端层。
2. S3RIACustomSample.Web-此工程中包含了ASP.NET web应用程序代码,这个工程称为服务器端工程,这是我们创建的应用程序的中间层。
此时一个基本的Silverlight 3示例工程框架设计完成。
在Web工程上添加LINQ to SQL数据模型
(1)右键单击web工程S3RIACustomSample.Web,在弹出菜单中选择"添加|新建项"命令。在随后出现的"添加新项"对话框中选择"LINQ to SQL Classes"模板,使用默认的名称DataClasses1.dbml,最后单击"添加"按钮退出。
为了读者调试方便,我们使用微软提供的SQL Server 2008示例数据库AdventureWorks。在服务器资源管理器中添加AdventureWorks示例数据库连接的操作在此不再赘述,而是直接假设用户创建好了这一连接。
(2)在退出上面的"添加新项"对话框之后,系统会自动打开LINQ to SQL设计器。现在,我们可以在其中添加数据库表格对应的实体对象。为此,你仅需要在服务器资源管理器中找到添加进来的AdventureWorks示例数据库,并把Product表格从服务器资源管理器拖动到LINQ to SQL设计器内部,最后保存生成的文件。
(3)最后,选择菜单"生成|重新生成解决方案"。
创建对应于LINQ to SQL数据模型的域服务
(1)仍然右键单击web工程S3RIACustomSample.Web,在弹出菜单中选择"添加|新建项"命令。在随后出现的"添加新项"对话框中选择"Domain Service Classes"模板添加一个域服务,输入名称Catalog.csl,最后单击"添加"按钮退出。
(2)在随后出现的"Add New Domain Service Class"对话框。在对话框中选择Product实体,并确保选择了"Enable Editing"复选按钮。最后,再选择对话框最下部的"Generate associated classes for metadata"复选按钮,以保证在元数据中生成相关联的类。
(3)最后,选择菜单"生成|重新生成解决方案"。
注意,上面代码中的一些代码已经被注释掉,而且仅创建了基本的产品信息数据加载代码。更多的代码分析将在后面给出。
Silverlight用户界面及基本后台代码编程
其实,从分层角度来看,就客户端Silverlight项目这边来看,也可进一步分为若干子层。例如,.xaml文件可看作最上面的展示层,相应的后台代码文件.xaml.cs则又是一层。
Silverlight 3项目自动生成的主页面文件为MainPage.xaml。现在,打开此文件,并输入如下标记代码:
<UserControl
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" x:Class="S3RIACustomSample.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="Auto" d:DesignHeight="Auto">
<Grid x:Name="LayoutRoot">
<StackPanel Orientation="Vertical">
<data:DataGrid IsReadOnly="True" Name="productsGrid" Height="Auto" Width="Auto"
SelectionChanged="DataGrid_SelectionChanged"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Competitor's price" Margin="2"/>
<TextBlock x:Name="compPrice" Margin="2"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button Click="Button_Click" Content="Discount" x:Name="discountButton" Margin="2"/>
<TextBox x:Name="discountPercent" Text="10" Margin="2,2"/>
<TextBlock Text="%" Margin="2,5,0,2"/>
</StackPanel>
</StackPanel>
</Grid>
</UserControl>
上面的XAML代码非常简单,不再赘述。
相应的后台代码文件MainPage.xaml.cs的内容如下所示:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
//下面是新添加的命名空间引用
using System.Windows.Ria.Data;
using RIA=S3RIACustomSample.Web;
namespace S3RIACustomSample
{
public partial class MainPage : UserControl
{
RIA.Catalog catalog = new RIA.Catalog();//创建Catalog对象实例
public MainPage()
{
InitializeComponent();
productsGrid.ItemsSource = catalog.Products;
var query = from p in catalog.GetProductQuery()
where p.ListPrice > 3000 select p;
catalog.Load(query);//把满足要求的产品数据加载到DataGrid控件
}
private void Button_Click(object sender, RoutedEventArgs e)
{
RIA.Product selectedProduct = (RIA.Product)productsGrid.SelectedItem;
if (selectedProduct != null) {
//int percentage = int.Parse(discountPercent.Text);
//selectedProduct.DiscountProduct(percentage);
//catalog.SubmitChanges();
}
}
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) {
RIA.Product selectedProduct = (RIA.Product)productsGrid.SelectedItem;
if (selectedProduct != null) {
//catalog.GetCompetitorsPrice(selectedProduct,
// (invokeOperation) =>
// {
// compPrice.Text = invokeOperation.Value.ToString();
// }, null);
}
}
}
编写应用程序逻辑
本案例中的业务逻辑层是由宿主Web工程提供的。而且,在RIA服务编程环境下,编写应用程序逻辑部分主要是通过编写域服务(Domain Service)方法实现的。
现在,上面的用户界面仅仅做到了基本(或原始)产品数据的加载显示。下面,我们将新增一些功能:折扣选定的产品和显示竞争对手的价格。为了折扣选定的产品,我们需要把其价格降低几个百分点,此数据由用户给出。为了显示竞争对手的价格,我们需要进行一个带外服务器端查找。在实际应用开发中,这种查询可能相当复杂,如查询另一个网站以获得最好的价格,等待。为了简便起见,我们将仅仅返回一个低于标价5%的价格。
显然,实现上面的操作,要求编写不属于基本CRUD操作的域服务方法-自定义方法。
(一)关于自定义方法
自定义方法是异步域操作时使用一些额外的应用程序逻辑实现的除了基本CRUD操作(创建/更新/删除)外的操作。
自定义的方法具有与基本CRUD操作一样的变更跟踪和延迟执行特征。这意味着,在客户端的更改将不会立即被提交到服务器,而是直到调用DomainContext.SubmitChanges()方法之后完成。
(二)自定义方法的声明
有两种方式可以把DomainService内定义的方法标记为一个自定义的方法:
通过公共约定的方法:①返回void类型;②使用实体作为第一个参数;③不是创建/更新/删除方法约定的表达形式。
在方法前面修饰以CustomAttribute属性。
在下面的代码片断中,我们介绍了两种方式,但只有一种方式是必要的。此代码实现了一种简单的折扣算法。注意,通过前面的提示你可能已经猜出,此代码应加在DomainService Catalog的内部。
请看下面的代码:
[Custom]
public void DiscountProduct(Product product, int percentage)
{
this.Context.Product.Attach(product);
decimal newPrice = product.ListPrice * (1 - percentage / 100m);
product.ListPrice = newPrice; // Do logging/reporting here
}
在上面代码中,RIA服务把当前状态的实体作为参数传递给自定义方法。当然,如果你需要使用原始的实体状态的话,应当使用下面代码:
(三)自定义方法的用法
客户端可以采取两种方式调用自定义方法:
(1)使用生成的域上下文(Domain Context),并且传递进实体作参数:
或者:
(2)使用实体自身进行操作:
(四)普通服务操作(简称"服务操作")
与前面讨论过的其他DomainService类型相比,普通服务操作更酷似传统的Web方法。当服务器端的逻辑不适合于创建/更新/删除模式时,你可以选择使用普通服务操作。不同于其他DomainService操作的是,普通服务操作没有变更跟踪或延迟执行支持。这就是说,一旦在客户端发出了方法调用,普通服务操作即被提交给服务器端。
(五)普通服务操作的声明
没有命名约定的标记的方法被识别为一个普通服务操作。为了把一个方法标记为一个普通服务运行,请使用ServiceOperation属性加以标记。下面的示例服务操作将获取与指定产品相关联的竞争对手的价格。注意,通过前面的提示你也可能已经猜出,此代码应加在DomainService Catalog的内部。
[ServiceOperation]
public decimal GetCompetitorsPrice(Product product)
{
// Do some kind of price lookup.
return product.ListPrice * 0.95m;
}
参数和ServiceOperation的返回类型必须是一个实体,或是一个预定义的序列化类型。
(六)使用普通服务操作
需要在此指出的是,像RIA服务框架内其他的操作一样,普通服务方法的执行也是异步的。有三种方法可以访问普通服务操作的返回值。开发人员可以:
①直接绑定到InvokeOperation.Value,像这样:
InvokeOperation<decimal> invokeOp = catalog.GetCompetitorsPrice(selectedProduct);
compPrice.Text = invokeOp.Value.ToString();
②也可以使用传递回调函数的方式,类似如下:
catalog.GetCompetitorsPrice(selectedProduct,
(invokeOperation) =>
{
compPrice.Text = invokeOperation.Value.ToString();
}, null);
③还可以使用注册事件处理器函数的方式:
InvokeOperation<decimal> invokeOp = catalog.GetCompetitorsPrice(selectedProduct);
invokeOp.Completed += new System.EventHandler(invokeOp_Completed);
void invokeOp_Completed(object sender, System.EventArgs e)
{
InvokeOperation<decimal> invokeOp = (InvokeOperation<decimal>)sender;
compPrice.Text = invokeOp.Value.ToString();
}
观察运行结果
至此,你可以把前面文件MainPage.xaml.cs中调用方法DiscountProduct()和GetCompetitorsPrice()前面的注释去掉来运行一下应用程序了。下图展示了示例程序某一时刻的运行快照。
从控件DataGrid中选取某一产品,将会弹出相应的竞争者价格。然后,你可以通过单击"Discount"按钮把这一价格降低。
重要补充
在RIA服务编程中,遵守既定的域操作方法约定显得非常重要,其重要意义在于:
可以使开发人员投入较少的工作量。
提供一致性的、富于美感的编程体验。
实现通用型域驱动语言,而且提高了代码在团队开发中的可读性。
(一)适用于所有域操作的规则
归纳来看,适用于所有域操作的规则包括:
域操作必须带有public修改符。
如果第一个(或前面连续的几个)参数类型为实体类型的话,仅允许存在一个域操作方法。当查询方法返回一个IEnumberable或IQeryable类型时,只要应用到RiaServices相关操作,此类型将代表了一个实体。因此,要使其他域操作有效,必须确保存在一个查询类型的域操作方法。这种情况下存在的一个允许的例外是,不使用实体类型作为参数的域操作方法将是有效的域操作方法。
域操作方法必须使用可串行化的类型作为参数及返回类型。
(二)典型域操作方法编程约定
1. 插入操作
返回void类型,并且仅有一个实体参数类型。
命名格式:前缀是Insert,Add,Create之一,后面跟着实体名。
例如:
在使用InsertAttribute属性的情况下,可以使用任何命名形式,例如:
[Insert]
public void YourFavoriteMethodName(Employee newEmployee) {...}
2. 更新操作
返回void类型,并且仅有一个实体参数类型。
命名格式:前缀是Update,Change,Modify之一,后面跟着实体名。
例如:
在使用UpdateAttribute属性的情况下,可以使用任何命名形式,例如:
[Update]
public void YourFavoriteMethodName(Employee changedEmployee)
3. 删除操作
返回void类型,并且仅有一个实体参数类型。
命名格式:前缀是Delete,Remove之一,后面跟着实体名。例如:
在使用DeleteAttribute属性的情况下,可以使用任何命名形式,例如:
[Delete]
public void YourFavoriteMethodName(Employee currentEmployee)
4. 查询操作
可以是返回IEnumerable<T>,IQueryable<T>或T类型的任何方法。这里T是实体类型。例如:
public IQueryable<Employee> GetEmployee() {...}
//也可以返回一个实体类型而不是IQueryable/IEnumerable类型
public City GetCity() {...}
此外,我们还可以使用QueryAttribute属性显式地指定一个方法为查询操作,例如:
[Query]
public IQueryable<Employee> GetEmployee() {...}
5. 关于自定义操作
返回void类型,使用实体类型作为方法的第一个参数,并且方法命名没有遵循前面几种CRUD操作对应的约定,这样的方便将被识别为自定义方法,例如:
此外,我们还可以使用CustomAttribute:属性显式地指定一个方法为自定义操作,例如:
[Custom]
public void ApproveEmployee(Employee changedEmployee, ...)
6. 关于上述操作中可能导致的数据冲突对应的解析方法
返回bool类型,前三个参数必须使用实体类型,作为方法的第一个参数,第四个参数必须为一个bool类型。另外,前三个参数依次对应于:当前实体,最初的实体,要保存的实体。
另外,方法前面必须冠以Resolve,例如:
【注意】
A.要使用上面的解析方法,必须确保事先存在一个更新方法。
B.在Siverlight RIA服务编程中,当多个用户同时更新同一个表格的同一条记录数据时将导致异常抛出,因此建立上面的解析方法进行解析非常重要。
7. 服务操作方法
仅有一点要求,方法名前面必须冠以ServiceOperation属性,例如:
[ServiceOperation]
public byte[] GetProductImage(){...}
8. 关于IgnoreOperation属性
有些情况下,我们可能想把某些域方法显式地排除在外,即虽然已经建立了遵循上面约定的方法,但是不想让系统识别这是一个域方法。此时,我们可以在这些方法的前面冠以IgnoreOperation属性。于是,系统便会把此方法识别为可忽略的方法。
例如:
[IgnoreOperation]
public void InsertEmployee(Employee newEmployee) {...}
【注意】IgnoreOperation属性可以应用于所有域方法。
小结
在本文中,我们通过一个简单的例子讨论了Silverlight 3 RIA开发中涉及到的定制方法及普通服务操作问题,并且归纳对比了典型CRUD域操作、自定义型域操作及普通服务操作。所有这些,也都是Silverlight 3 RIA开发中的必需。
很显然,现在借助于RIA服务,Silverlight编程的数据访问及有关处理将得到极大程度的简化。但同时,也有许多新的知识需要更新与学习。