John Papa
代码下载位置: SLDataServices2008_09a.exe (234 KB)
在线浏览代码
本专栏基于 Silverlight 2 的 Beta 2 版本。文中的所有信息均有可能发生变更。
下载本文中所用的代码: DataPoints2008_09a.exe (414 KB)
浏览在线代码
目录
示例应用程序
跨域通信
Silverlight 客户端
绑定产品列表
异步通信
产品详细信息和绑定模式
更改事件
结束语
毋庸置疑,Silverlight™ 2 使得利用大量图形处理技术构建丰富 Internet 应用程序 (RIA) 变得非常容易。但另一方面,Silverlight 2 可以轻松构建相当专业的业务线 (LOB) 应用程序也是不争的事实。Silverlight 2 支持已启用了 Windows® Presentation Foundation (WPF) 的功能强大且基于 XAML 的数据绑定子集。Silverlight 2 中的 XAML 绑定标记扩展简化了将实体绑定到 Silverlight 控件的过程。由于它们完全在客户端计算机上运行,因此 Silverlight 应用程序与通过服务器管理的实体是相互隔离的。这样一来,那些通过 RSS、具象状态传输 (REST) 以及 Windows Communication Foundation (WCF) 等技术实现的基于服务的通信必须是可供使用的。幸运的是,Silverlight 2 支持与这些技术和其他通信途径的交互,这使得 Silverlight 应用程序可以与后端 LOB 应用程序无缝交互。
我将演示如何构建 Silverlight 2 UI,以使其通过与 WCF 的通信实现与业务实体和数据库的交互。对于业务逻辑、实体模型和数据映射代码,任何表现层都可以使用它们。我会创建将由 Silverlight 2 应用程序使用的 WCF 服务,并建立托管 WCF 服务的服务器以允许跨域调用。请注意,您可以从《MSDN® 杂志》网站下载这些示例。
示例应用程序
在开始编写代码之前,我们先深入了解一下此示例。图 1 呈现的是完整的应用程序,其中显示了从 Northwind 数据库检索到的产品列表。从 ListBox 中选择了某个产品后,该产品将被绑定到页面下半部分的控件上。当用户通过 CheckBox 和 TextBox 控件编辑产品并单击“Save”(保存)按钮时,产品信息会随即通过 WCF 发送到数据库中。单击“Cancel”(取消)按钮可通过 WCF 从服务器获得最新产品列表,同时更新 ListBox 及其绑定。
图 1 示例 Silverlight 应用程序(单击图像可查看大图)
Silverlight 2 示例应用程序由为数不多的几个用户控件和样式构成。表现层利用异步调用通过 WCF 与服务器进行通信。它使用 WCF 服务引用并依照服务的操作约定和数据约定来实现 Silverlight 应用程序与服务的通信。数据约定(例如 Product 或 Category 实体类)公开了服务器应用程序中的实体结构。这使得 Silverlight 应用程序的控件可以轻松绑定到这些实体的实例及其属性上。操作约定定义了 Silverlight 应用程序调用 WCF 服务的方法。图 2 显示的是此体系结构的高层次概观。
图 2 示例应用程序的体系结构模型
我将从 WCF 服务本身着手,先利用较低的层开始构建示例应用程序。您可以通过在 Visual Studio® 中创新建一个 WCF 项目来构建可与 Silverlight 应用程序进行通信的 WCF 服务。只要 Silverlight 应用程序具有 basicHttpBinding 类型的绑定,它就可以调用标准的 WCF 服务。您必须确保自己可将 WCF 服务的默认绑定从 wsHttpBinding 更改为 basicHttpBinding,否则必须新建一个 basicHttpBinding 类型的绑定。例如,示例 WCF 服务宿主应用程序的 web.config 文件包含用来定义服务配置的以下 XML。请注意,端点绑定被定义为 basicHttpBinding:
<service behaviorConfiguration= "MySilverlightWcfService.NWServiceGatewayBehavior" name="MySilverlightWcfService.NWServiceGateway"> <endpoint address="" binding="basicHttpBinding" contract="MySilverlightWcfService.INWServiceGateway" /> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> </service>
作为创建 WCF 服务的替代方法,您可以在 Visual Studio 中选择文件项目模板来创建启用 Silverlight 的 WCF 服务。图 3 显示的是 Visual Studio 中的新项目模板。此模板会自动将绑定设置为 basicHttpBinding 并添加一些属性,以使服务与 ASP.NET 兼容。尽管此方法可为您设置正确的绑定配置,但不要忘记您仍可使用现有的 WCF 服务,但前提是这些绑定是针对 basicHttpBinding 设置的。
图 3 启用 Silverlight 的 WCF 模板(单击图像可查看大图)
WCF 服务必须能够请求产品列表来填充 ListBox,而且必须能够保存用户对产品所做的任何更改。这都是一些简单的操作,无需专门的 Silverlight 技术。示例应用程式使用一个名为 NWServiceGateway 的 WCF 服务类,用于实现接口 INWServiceGateway。此处所示的 INWServiceGateway 被修饰为 ServiceContract,它可以使实现此接口的所有类都通过 WCF 加以公开:
[ServiceContract(Namespace = "")] public interface INWServiceGateway { [OperationContract] Product FindProduct(int productId); [OperationContract] List<Product> FindProductList(); [OperationContract(Name="FindProductListByCategory")] List<Product> FindProductList(int categoryID); [OperationContract] List<Category> FindCategoryList(); [OperationContract] void SaveProduct(Product product); }
该接口列出了使用 OperationContract 属性修饰的多种方法。OperationContracts 可通过 WCF 服务调用。请注意,FindProductList 方法具有两个重载。一个接受参数,而另一个不接受。尽管这一点在 Microsoft® .NET Framework 方法中是完全可以接受的,但 WCF 却无法公开具有相同名称的两个方法。要解决此问题,您可以重命名该方法或在服务定义中使用 OperationContract 的 Name 属性指定一个不同的名称。图 4 显示的是如何在 WCF 服务中向一个使用了新名称的操作公开 FindProductList 方法及参数。
图 4 在数据层中收集产品
public List<Product> FindProductList() { var productList = new List<Product>(); using (var cn = new SqlConnection(nwCn)) { const string sql = @"SELECT p.*, c.CategoryName " + " FROM Products p INNER JOIN Categories c ON " + " p.CategoryID = c.CategoryID ORDER BY p.ProductName"; cn.Open(); using (var cmd = new SqlCommand(sql, cn)) { SqlDataReader rdr = cmd.ExecuteReader( CommandBehavior.CloseConnection); if (rdr != null) while (rdr.Read()) { var product = CreateProduct(rdr); productList.Add(product); } return productList; } } }
WCF 服务需要做的只是实现此服务约定接口并调用一个 Manager 类,此类的任务是从数据库获取产品并将其映射到 List<Product>,以便它们可以从服务中传送出去。图 4 显示的是用来从 Northwind 数据库获取产品列表、将各个产品映射到 Product 类并将各个产品实例添加到 List<Product> 中的代码。
由 WCF 返回的实体必须用 DataContract 属性进行修饰,以使其能够被正确序列化并发送给 Silverlight 应用程序。图 4 中涉及的 Product 类具有 DataContract 属性,并且它的所有属性都使用 DataMember 属性加以修饰。这将告知 WCF 开始序列化并将实体及其 DataMember 属性提供给 Silverlight 应用程序使用。当调用 WCF 服务的 FindProductList 方法时,Silverlight 客户端应用程序将接收 List<Product>,并且将能够引用使用 DataMember 属性修饰的所有特性。
跨域通信
Silverlight 应用程序将在客户端计算机环境中执行。这就会产生一些问题,即基于 ASP.NET Web 的应用程序当前并不会显示出来,因为它们都是在服务器上执行而在客户端上呈现 HTML 和编写脚本代码。由于 Silverlight 是在客户端上执行的,因此它必须使用面向服务的技术(如 WCF)从该服务器请求信息。不过,必须要对 WCF 服务进行保护以防止某些不必要的客户端应用程序利用它。示例 Silverlight 应用程序承载在您信任的 Web 服务器上,因此应允许它与示例应用程序的 WCF 服务进行交互。如果承载在另一 Web 服务器上的其他应用程序试图与示例 WCF 服务进行通信,应将其拒绝。
对这种服务访问的控制是通过跨域策略文件进行处理的。Adobe Flash 应用程序有一个标准文件,可用来处理这个名为 CrossDomain.xml 的文件(位于该服务的 Web 服务器的根目录下)。Silverlight 应用程序的行为也非常相似,首先在 Web 服务器的根目录(不是 Web 应用程序的根目录)中查找名为 ClientAccessPolicy.xml 的文件。如果找到该文件,应用程序将读取它以确定是允许还是拒绝请求。如果未找到,应用程序将继续查找 CrossDomain.xml 文件。如果均未找到,该请求将被拒绝,且 Silverlight 客户端应用程序也将无法调用该 WCF 服务。
每个文件的内容都必须允许调用方具有对这些服务的权限。由于示例应用程序仅存在于受保护的开发计算机中,因此其 ClientAccessPolicy.xml 将允许所有请求,如下所示:
<?xml version="1.0" encoding="utf-8"?> <access-policy> <cross-domain-access> <policy> <allow-from http-request-headers="*"> <domain uri="*"/> </allow-from> <grant-to> <resource path="/" include-subpaths="true"/> </grant-to> </policy> </cross-domain-access> </access-policy>
我创建的 ClientAccessPolicy.xml 文件允许从所有位置跨域访问全部路径。此文件的副本包括在示例应用程序中。在首次创建 WCF 项目后,默认选项将使用已被自动分配端口的 Visual Studio Development Server。但是,示例应用程序的 WCF 服务项目被设置为“使用 IIS Web Server”,此设置可以在 Web 选项卡的项目属性页面中进行更改。只要 ClientAccessPolicy.xml 文件被放置在 Web 站点的根目录下,每个选项就都有效。
这些策略可加以限制,以仅允许某些 URI 能够访问特定文件夹路径中的特定服务。要执行此操作,必须在此文件中指定承载 Silverlight 应用程序的 Web 服务器,这样它才能与 WCF 服务交互。例如,受限程度较高的跨域策略可能如下所示:
<?xml version="1.0" encoding="utf-8"?> <access-policy> <cross-domain-access> <policy> <allow-from http-request-headers="*"> <domain uri="http://johnpapa.net"/> </allow-from> <grant-to> <resource path="/MyAwesomeServices/" include-subpaths="true"/> </grant-to> </policy> </cross-domain-access> </access-policy>
请注意,源自 johnpapa.net 中某项服务的任何请求都被允许访问服务器上 /MyAwesomeServices 路径下的服务。
调试具有跨域调用的问题可能有一些难度。您可以使用诸如 Web Development Helper(我的最爱)或 Fiddler 之类的工具来检查 Silverlight 应用程序与基于服务器的服务之间的通信流量。这些工具将显示各个单独的 HTTP 请求和响应。您还应确保 ClientAccessPolicy.xml 文件被放置在 Web 根目录下,而不是应用程序根目录下。我不再占用篇幅来强调这一点。
此外,当在 Visual Studio 项目中承载服务时(例如,未直接位于 IIS 中),该项目会创建一个自动分配的端口,测试此服务的最简单方法是将 ClientAccessPolicy.xml 文件放置在项目自身的根目录下。由于该项目得到了一个自动分配的端口(如 localhost:32001/MyAwesomeService),它将在 Web 的根目录下寻找 ClientAccessPolicy.xml 文件(在本例中为 localhost:32001)。
Silverlight 客户端
编译好这些服务并建立了跨域策略后,即可建立 Silverlight 客户端以与 WCF 服务进行通信。示例应用程序中有一个名为 SilverlightWCF 的 Silverlight 客户端项目,它需要一个对示例 WCF 服务的服务引用。在“Solution Explorer”(解决方案资源管理器)中右键单击引用节点,然后选择“Add Service Reference”(添加服务引用)。输入该服务的 URL,单击“GO”(搜索)按钮,服务将会显示出来。单击“OK”(确定),服务客户端配置以及生成的代理类将被添加到 Silverlight 项目中,以简化调用服务和使用实体(如 DataContract 所定义)的过程。图 5 显示的是添加服务时出现的对话窗口。
图 5 添加服务引用(单击图像可查看大图)
请注意,默认情况下此示例将使用位于 localhost/MySilverlightWcfService/NWServiceGateway.svc 目录下的服务。毋庸置疑,如果移动了该服务,则此端点地址也需要相应的更新。如果在 WCF 服务中进行了少量更改(如添加新方法或 DataContract),您可以在“Solution Explorer”(解决方案资源管理器)中选择“ Update Service Reference”(更新服务引用)。
绑定产品列表
示例应用程序需要向用户显示产品列表。在这里 ListBox 控件将与 DataTemplate 配合使用,这样就可以在展示产品时带有一些特殊效果,而不再是简单地在行和列中列出值(ListBox 最初如图 1 所示)。通过设置 ListBox 的 ItemSource 属性,我们指明 ListBox 将从绑定数据源中获取它的值,如以下标记所示:
<ListBox x:Name="lbProducts" Height="220" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="10,10,10,10" Width="480" Style="{StaticResource ListBoxStyle}" ItemsSource="{Binding}" >
请注意,ItemSource 属性只指明它将被绑定,而无法指明任何具体的对象或属性。由于未指定任何源,ListBox 将引用 XAML 中任意继承的 DataContext 对象源。DataContext 可从任何父 FrameworkElement 中继承。在 WCF 服务返回产品列表后,示例应用程序将在代码隐藏中设置 ListBox 的 DataContext,其名称为 lbProducts:
lbProducts.DataContext = productList;
在执行此代码后,ListBox 将被绑定到产品列表,而 DataTemplate 中的每个 ListBoxItem 都被绑定到列表中的各个产品上。图 6 显示的是用于创建 ListBox 并将 DataTemplate 中展示的项目绑定到源的 XAML。请注意,被绑定到源的各个控件将使用绑定标记扩展来指示它们将被绑定到 Product 的哪个属性,并指出将使用 OneWay 绑定模式。例如,
Text="{Binding ProductName, Mode=OneWay}"
图 6 ListBox XAML
<ListBox x:Name="lbProducts" Height="220" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="10,10,10,10" Width="480" Style="{StaticResource ListBoxStyle}" ItemsSource="{Binding}" > <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <StackPanel Style="{StaticResource TitlePanel}" Orientation="Vertical"> <TextBlock Text="{Binding ProductName, Mode=OneWay}" Style="{StaticResource TitleTextBlock}" FontSize="14"/> <TextBlock Text="{Binding CategoryName, Mode=OneWay}" Style="{StaticResource SubTitleTextBlock}"/> </StackPanel> <Grid> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <TextBlock Text="Product ID" Style="{StaticResource TextBlockStyle}" Grid.Row="0" Grid.Column="0"/> <TextBlock Text="{Binding Id, Mode=OneWay}" Style="{StaticResource TextBlockStyle}" Foreground="#FF001070" Grid.Row="0" Grid.Column="1"/> <TextBlock Text="Price" Style="{StaticResource TextBlockStyle}" Grid.Row="1" Grid.Column="0"/> <TextBlock Text="{Binding UnitPrice, Mode=OneWay, Converter={StaticResource priceConverter}}" Foreground="#FF001070" Style="{StaticResource TextBlockStyle}" Grid.Row="1" Grid.Column="1"/> <TextBlock Text="Units" Style="{StaticResource TextBlockStyle}" Grid.Row="2" Grid.Column="0"/> <TextBlock Text="{Binding UnitsInStock, Mode=OneWay}" Foreground="#FF001070" Style="{StaticResource TextBlockStyle}" Grid.Row="2" Grid.Column="1"/> </Grid> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
所有绑定 TextBox 控件的绑定模式都被设置为 OneWay。这表示源(即 Product 实例)在最初加载时以及当源中发生任何变更时都应将其值推至目标(ListBox 和其中绑定的任意项目)(有关“模式”的详细信息,请参阅“绑定模式”部分)。
异步通信
在绑定得以完成之前,必须从 WCF 服务中获取产品列表。Silverlight 中的所有 WCF 服务调用都是通过异步通信进行的。示例 WCF 服务发布了一个名为 FindProductList 的 OperationContract。由于与 Silverlight 的通信是异步的,因此这一约定是使用异步方法(此异步方法可启动在操作完成时所引发的通信和事件)在生成的代理中实现的。
以下代码将创建一个服务代理实例:
private void FindProductList() { this.Cursor = Cursors.Wait; var proxy = new NWServiceGatewayClient(); proxy.FindProductListCompleted += new EventHandler<FindProductListCompletedEventArgs>( FindProductListCompleted); proxy.FindProductListAsync(); }
代理创建完成后,将向 FindProductListCompleted 事件添加一个事件处理程序。这就是将要接收异步 WCF 服务调用结果的方法。最后执行 FindProductListAsync 方法。
当异步 WCF 服务调用完成后,将执行事件处理程序。以下代码显示了事件处理程序 FindProductListCompleted 接收产品列表并将其绑定到 ListBox 的 DataContext 的过程:
private void FindProductListCompleted(object sender, FindProductListCompletedEventArgs e) { ObservableCollection<Product> productList = e.Result; lbProducts.DataContext = productList; if (lbProducts.Items.Count > 0) lbProducts.SelectedIndex = 0; this.Cursor = Cursors.Arrow; }
产品详细信息和绑定模式
在 ListBox 中选择了某个项目后,事件处理程序中的代码将从 ListBox 的 SelectedItem 中获取所选的 Product,然后将其设置为 Grid 布局控件的 DataContext。Grid 布局控件被用作产品详细信息部分的容器,其中包含一系列可供用户对所选产品进行编辑的控件。由于 DataContext 是在控件树中自上而下继承的,因此无需在各个 TextBox 和 CheckBox 控件中设置 DataContext。这些控件都是从其父 Grid 控件(已在 lbProducts_SelectionChanged 事件处理程序中设置)继承 DataContext 而来。
Grid 布局控件中有多个包含绑定声明的 TextBlock、TextBox 和 CheckBox 控件。此部分的几个 XAML 代码段如下所示:
<TextBox Style="{StaticResource TextBoxStyle}" Text="{Binding UnitsInStock, Mode=TwoWay}" Grid.Row="1" Grid.Column="1" Margin="3,3,3,3" Height="30" x:Name="tbUnitsInStock" Width="100" VerticalAlignment="Bottom" HorizontalAlignment="Left"/> <TextBlock Style="{StaticResource TextBlockStyle}" Text="{Binding CategoryName, Mode=OneWay}" Foreground="#FF001070" Grid.Row="1" Grid.Column="4" Margin="3,3,3,3" Height="22" HorizontalAlignment="Left" VerticalAlignment="Bottom"/>
请注意,第一个代码段表示的是 TextBox,它显示 Product 源对象中的 UnitsInStock 属性。第二个代码段显示的是 Product 实例的 CategoryName 属性的值。请注意,TextBox 中绑定的 Mode 属性被设置为 TwoWay,而在 TextBlock 中被设置为 OneWay(此为默认设置)。
绑定的 Mode 属性是一个重要的绑定设置,可用于确定在目标与源之间发生绑定的频率及方向。图 7 显示的是 Silverlight XAML 及其更新时可以使用的三种绑定模式。
图 7 Silverlight XAML 中的三种绑定模式
绑定模式
OneTime
OneWay
TwoWay
在首次设置了 DataContext 后目标会进行相应更新
Yes
Yes
Yes
在源发生更改时目标会进行相应更新
No
Yes
Yes
在目标发生更改时源会进行相应更新
No
No
Yes
当应用程序启动或 DataContext 发生更改时,OneTime 绑定将源发送到目标。OneWay 绑定也可以将源发送到目标。但是,如果源实现 INotifyPropertyChanged 接口,则当源发生更新时目标将接收到更新。TwoWay 绑定会将源数据发送到目标,但如果目标属性的值发生更改,则会将这些更改返回给源。如果源对象实现 INotifyPropertyChanged 并且如果源的属性 setter 引发了 PropertyChanged 事件,则 OneWay 和 TwoWay 绑定都只告知目标有关更改的消息。
在上一示例中,OneWay 绑定被用于 CategoryName,因为它显示在 TextBlock 中而且无法进行编辑。TwoWay 绑定被用于 UnitsInStock,因为它显示在可编辑的 TextBlock 中。当用户在 TextBox 中编辑 UnitsInStock 的值且 TextBox 失去焦点时,更改结果将被发送回源对象。
OneTime 绑定最适合表示那些在显示时从不会发生更改的只读信息。OneWay 绑定适合表示那些在显示过程中可能会在某个时刻发生更改的只读信息。进一步说,如果在 CategoryName TextBlock 中使用了 OneTime 绑定而不是 OneWay 绑定,则单击“取消”按钮并且产品列表被刷新后,使用 OneTime 绑定模式的 TextBlock 将不会更新。最后,当用户必须能够更改某个控件中的数据并能够使更改在数据源中反映出来时,最好选择使用 TwoWay 绑定。
更改事件
当源发生更改时,使 OneWay 和 TwoWay 绑定通知目标的关键一点是实现 INotifyPropertyChanged 接口。Product 类可实现此接口,该类由单个事件 PropertyChanged 组成。PropertyChanged 事件接受发送者和发生更改的属性的名称作为参数。Silverlight 数据绑定侦听这些来自数据源的事件。如果该类既不实现此接口也不在属性 setter 中引发 PropertyChanged 事件,则目标将永远不会收到更新。PropertyChangedEventHandler 在 Product 类中通过下列几行代码实现:
public override event PropertyChangedEventHandler PropertyChanged;
在这种情况下,该事件将被覆盖而不是简单地定义,因为 Product 类将从自定义的 EntityBase 类(此类可实现 INotifyPropertyChanged 接口)进行继承。事实上,这里介绍的 EntityBase 类可实现 PropertyChangedEventHandler,因此可以在 Product 或 Category 等派生类(如 )中将其覆盖:
[DataContract] public abstract class EntityBase : INotifyPropertyChanged { protected void PropertyChangedHandler(EntityBase sender, string propertyName) { if (PropertyChanged != null) PropertyChanged(sender, new PropertyChangedEventArgs( propertyName)); } public virtual event PropertyChangedEventHandler PropertyChanged; }
为方便起见,也可以使用 EntityBase 类为派生类定义 PropertyChangedHandler 方法,这反过来又可以引发 PropertyChanged 事件。这是一个简单的重构过程,它可将引发事件的逻辑放置到单一位置上并可减少代码量。例如,具有 UnitPrice 属性的 product 类可设置产品价格值并调用基础类的 PropertyChangedHandler 方法:
[DataMember] public decimal UnitPrice { get { return _unitPrice; } set { _unitPrice = value; base.PropertyChangedHandler(this, "UnitPrice"); } }
此方法反过来又会引发 PropertyChanged 事件。然后此事件将被发送到任意 OneWay 或 TwoWay 绑定,从而使这些绑定将目标控件更新为新值。Product 类中每个属性的 setter 都使用此模式。
属性更改通知可通过示例应用程序进行演示,方法是从 ListBox 中选择一个产品,然后在 TextBox 中编辑单价。由于 tbUnitPrice TextBox 使用 TwoWay 绑定,这会使新价格被发送到源。一旦源被更新,即会引发 PropertyChanged 事件。这会使侦听此事件的所有绑定都更新目标值。由于 ListBox 实现 OneWay 绑定,因此价格值会自动将其自身更新为新的价格值。
如您所见,利用示例应用程序中所示的声明式绑定语法可轻松实现 Silverlight 2 数据绑定功能。这些绑定将侦听 PropertyChanged 事件,以便能够更新其目标。在本专栏中,我向您演示了通过 Silverlight 实现绑定的容易程度,并介绍了以何种方式与 WCF 服务通信才能与业务对象和数据库进行交互,还介绍了如何定义 ClientAccessPolicy.xml 文件以允许和限制远程通信。
请将您想向 John 询问的问题和提出的意见发送至 mmdata@microsoft.com。