异步通信
在Silverlight中所有与服务器间的通信都是异步执行的。因此需要熟悉异步编程。典型的编程模式包括发起一个对服务器的调用,然后等待事件引发通知调用完成。调用在后台线程执行,一旦完成就立即返回,从而避免了UI因该方法的调用而阻塞。一般说来,如果异步调用在完成时引发事件,该事件通常都会在UI线程引发,更新UI线程上的显示结果。但是如果使用异步调用委托(使用AsyncCallback对象作为参数),回调方法会在后台线程执行,这样就无法在UI线程更新了,这种情况下,需要使用Dispatcher对象的BeginInvoke方法,在UI线程上调用代码执行。
使用RIA服务从服务器中获取数据
在Generated_Code文件夹下有很多文件,核心类,称之为AdventureWorks.Web.g.cs ,包含了大部分的生成代码,剩余的文件标记为shared,是直接从Web项目中复制过来的,保持了Web项目中相同的文件夹结构。检视生成的代码有助于识别代码生成错误。在调试时,可以打开这些文件并设置断点以协助分析问题。记住不要修改这些文件,重新编译就会覆盖所有修改。想要为生成的代码添加功能,创建单独的文件,使用部分类来扩展。
AdventureWorks.Web.g.cs (是根据【web项目名称】.g.cs来命名的)包含如下核心类:
- 域上下文类
- Model类
- WebContext类
域上下文类
RIA代码生成器为每个域服务生成相应的域上下文类。在Silverlight项目(客户端)所写的代码可以使用域上下文类(代理与桥梁)与服务器上相应的域服务进行通信。遵循默认命名规则,如果域服务命名为XXXService,则域上下文类为XXXContext。(不符合默认规则,需要进行查询)。域上下文类也有相对应于每个查询,调用与自定义操作的方法,可在客户端进行调用。insert/update/delete操作在域上下文没有相应的方法对应,这是因为这些操作不会在客户端显示调用。当在域上下文上调用SubmitChanges方法时,RIA服务会将变更集发送到服务器调用相应的insert/update/delete操作方法。
Entiy/Model类
每个由域服务暴露的实体(或Presentation Model类)都有相应的类在此文件中创建。任何在Web项目中应用到的特性标记(通过相应元数据类)也会直接应用于生成的客户端类;
WebContext类
WebContext类在Silverlight程序启动时初始化,该实例将在整个程序的执行周期内以“扩展服务”的形式进行保持。在AdventureWorks项目的App.xaml.cs文件中,该类在应用程序启动时实例化,并加入到ApplicationLifetimeObjects集合中。命名静态的Current属性就可以获得对该实例的引用。WebContext类充当应用程序 的上下文,维护当前用户对象,提供对AuthenticationContext类实例的访问,可以使用部分类扩展该类。
使用DomainDataSource控件
实例:使用DomainDataSource控件查询数据,显示在DataGrid控件上。
1)在View文件夹下添加ProductList.xaml文件;
2)打开数据源窗口(数据--显示数据源)。有关的域上下文类自动显示在数据源窗口里;
3)从数据源窗口拖动实体(Product)放置在设置界面上,系统自动为DomainDataSource控件进行了配置,并与DataGrid控件进行了绑定:
<riaControls:DomainDataSource AutoLoad="True" d:DesignData="{d:DesignInstance my:Product, CreateList=true}" LoadedData="productDomainDataSource_LoadedData" Name="productDomainDataSource" QueryName="GetProductsQuery" Height="0" Width="0"> <riaControls:DomainDataSource.DomainContext> <my:ProductContext /> </riaControls:DomainDataSource.DomainContext> </riaControls:DomainDataSource> <sdk:DataGrid AutoGenerateColumns="False" Height="200" HorizontalAlignment="Left" ItemsSource="{Binding ElementName=productDomainDataSource, Path=Data}" Margin="185,53,0,0" Name="productDataGrid" Width="400" RowDetailsVisibilityMode="VisibleWhenSelected" VerticalAlignment="Top"> <sdk:DataGrid.Columns> <sdk:DataGridTextColumn x:Name="classColumn" Binding="{Binding Path=Class}" Header="Class" Width="SizeToHeader" /> <sdk:DataGridTextColumn x:Name="colorColumn" Binding="{Binding Path=Color}" Header="Color" Width="SizeToHeader" /> <!-- Additional columns removed for brevity--> </sdk:DataGrid.Columns> </sdk:DataGrid>
4)运行程序,所有Product数据都从服务器获取并显示在客户端的DataGrid控件上。
使用域上下文类以代码方式获取数据
域服务上的查询/调用/自定义操作方法在域上下文对象中都有相对应的方法以供调用。这些方法通常的命名方法是在域服务的操作方法上附加Query后缀:
通过代码从服务器中获取数据需要几个步骤。简单地调用GetProductsQuery方法并不会向服务器请求数据,该方法只返回一个EntityQuery对象。
ProductContext context = new ProductContext();
EntityQuery<Product> qry = context.GetProductsQuery();
获得EntityQuery对象以后,可以将其传递给域上下文的Load方法,这时才真正向服务器请求数据:
LoadOperation<Product> operation = context.Load(qry);
Load方法返回的是LoadOperation对象,该对象包含了一个实体集属性,是由请求对象的集合构成的;事实上,现在集合还是空的,因为RIA服务框架要求所有对域服务的调用都是异步的,因此Load方法需要等到服务器响应结束以外才能获得数据,有两种方法可以达到这个目的:
1)通过LoadOperation对象的Completed事件,该事件在数据从服务器获取完毕后发生。该事件的的e参数有一个Entities属性,可以用于获取访问结果;这种方式可以识别在请求过程中是否有错误发生,可以对相应错误进行处理;
2)通过LoadOpertion对象的Entities属性。该属性初始时为空,但是当从服务器获取到数据以后,该属性自动完成数据充填。这是因为该属性集合实现了INotifyCollectionChanged接口,这个接口有一个CollectionChanged事件,该事件监听是否有项目添加或移出集合。Silverlight控件如ListBox或DataGrid可以直接以该属性集合作为数据源。
productDataGrid.ItemsSource = operation.Entities;
注意EntityQuery<T>是对实体集合的LINQ查询的范型类,可以直接在该类型的变量上使用Lambda表达式或Linq查询语法:
ProductContext context = new ProductContext();
EntityQuery<Product> qry = context.GetProductsQuery();
qry = qry.Where(p => p.SellStartDate <= DateTime.Now);
LoadOperation<Product> loadOperation = context.Load(qry);
productDataGrid.ItemsSource = loadOperation.Entities;
或者
ProductContext context = new ProductContext(); EntityQuery<Product> qry = from p in context.GetProductsQuery() where p.SellStartDate <= DateTime.Now select p; LoadOperation<Product> loadOperation = context.Load(qry); productDataGrid.ItemsSource = loadOperation.Entities;
处理加载错误
由于Silverlight应用程序调用方法的异步性,处理加载数据中出现的错误不能使用常规的方式(比如使用try/catch块)。正确的做法是处理服务器通信结束时引发的事件,如果使用DomainDataSource控件,则处理控件的LoadedData事件,如果使用基于代码访问数据的方式,则需要处理Loadperation的Completed事件。两种情况的处理代码如下:
1)拖放实体到设计界面,在创建DomainDataSource控件的同时,也为该控件生成了LoadedData事件处理程序:
private void productDomainDataSource_LoadedData(object sender, LoadedDataEventArgs e) { if (e.HasError) { System.Windows.MessageBox.Show(e.Error.ToString(), "Load Error", System.Windows.MessageBoxButton.OK); e.MarkErrorAsHandled(); } }
2)LoadOperation的Completed事件与DomainDataSource的 LoadedData事件类似,不同之处需要将sender参数强制转换为泛型的LoadOperation对象:
private void loadOperation_Completed(object sender, EventArgs e) { LoadOperation<Product> op = sender as LoadOperation<Product>; if (op.HasError) { System.Windows.MessageBox.Show(op.Error.ToString(), "Load Error", System.Windows.MessageBoxButton.OK); op.MarkErrorAsHandled(); } }
注意我们在两个事件处理函数里都调用了MarkErrorAsHandled方法。如果不调用这个方法,域上下文会抛出”未处理”异常,该异常可以由App类的Application_UnhandledException事件处理方法进行处理,弹出一个错误窗口以显示错误细节,避免应用程序崩溃。该异常可以强制转换为DomainOperationException类型,可以获取更多信息,比如该类型的Status属性,可以通过OperationErrorStatus枚举来判断异常发生的类型:
1)ServerError:指在服务器上发生的异常或者应用程序无法连接到服务器
2)Unauthorized:用户无权执行操作。
因此错误处理的部分还可以细化为:
if (e.HasError) { DomainOperationException error = e.Error as DomainOperationException; switch (error.Status) { case OperationErrorStatus.ServerError: // Handle server errors break; case OperationErrorStatus.Unauthorized: // Handle unauthorized domain operation access break; } }