2010年2月11日星期四

WCF RIA 服务 (三十五)-- 身份验证、角色、个性化 6

演练:在Silverlight Business应用程序中使用身份验证服务
Silverlight Business应用程序模板创建的解决方案自动允许身份验证(验证模式为Forms),角色和个性化功能。解决方案包含了数据表格来登录已经存在的用户和注册新用户。我们不用写额外的代码就可以使用这些特性。我们也可以通过定义角色、个性化属性来自定义解决方案。

在本演练中,我们将学习如何在Silverlight Business应用程序中使用身份验证、角色和个性化功能。我们将根据用户的凭证来限制对某些域操作的访问,并根据用户的偏好来定制用户界面。我们将使用ASP.NET网站管理工具来管理角色和用户。

创建网站、用户、角色
我们可以使用Silverlight Business应用程序模板提供的特性,来快速实施验证功能。在下面的章节,我们使用ASP.NET配置工具来创建用户和角色,并登陆此用户。我们还通过Silverlight Business应用程序提供的注册表格来注册新用户。

1. 在Visual Studio中,选择"文件->新建->项目"."新建项目"对话框打开。
2. 选择Silverlight项目类型。
3. 选择Silverlight Business Application模板,并命名为ExampleBusinessApplication.

4. 点击"OK".注意创建的项目结构。这个SL客户端项目中在Views文件夹中包含了SL页面。这些页面允许登陆用户和注册新用户。
5. 打开ASP.NET 网站管理工具。(首先,在资源管理器中,选择服务端项目,然后打开ASP.NET配置工具。
6. 在项目菜单中,选择ASP.NET Configuration.如果在项目菜单中,看不到ASP.NET Configuration选项,有可能是选择了客户端项目。

7.在ASP.NET网站管理工具中选择"安全"标签。

8. 在"角色"的部分,点击"创建或管理角色"链接。
9. 添加一个名为Managers的角色,并点击"添加角色"按钮。

10. 在右下角,点击"返回"按钮。
11. 在“用户"的部分,点击"创建用户"按钮。
12. 使用下面的值来创建新用户,并选择Managers角色复选框。
- User Name : CustomerManager
- Password : P@ssword
- E-mail : someone@example.com
- Security Question : Favorite color?
- Security Answer : Blue
- Managers role : selected

13. 点击"创建用户"按钮。
14. 关闭ASP.NET网站管理工具。
15. 运行解决方案。应用程序的首页将会显示在web浏览器中。
16. 在页面的右上角,点击”登陆"链接。登陆窗口将会出现。
17. 为用户名称输入CustomerManager,为密码输入p@ssword,并点击"OK"按钮。

现在我们就登陆这个用户了,注意到在页面右上角出现文本"Welcome CustomerManager"。
18. 点击"登出"按钮。这时我们就不再以CustomerManager登陆了。
下面的步骤,我们通过注册表格创建一个新用户。
19. 再次点击"登陆"链接。
20. 在登陆对话框中,点击"现在就注册"链接。注册表格就会出现了。
21. 用下面的信息填充注册表。
Username: SalesUser

Friendly name: SalesUser

Email: someone@example.com

Password: P@ssword

Security Question: What was the color of your first car?

Security Answer: Green

22. 点击"OK",创建一个新用户。注意,我们现在已经作为SalesUser登陆了。
23. 关闭浏览器。
24. 打开ASP.NET网站管理工具,点击"安全"标签。注意到已经有了两个用户和角色,即使我们只创建了一个角色。
25. 点击“创建或管理角色”,注意到Managers和Registered Users角色。Registered User角色是Business应用程序模板自动生成的。

26. 对"Registered Users",点击"管理"链接。注意,通过应用程序添加的名为SalesUser的用户已经在Registered Users角色中了。
27. 关闭ASP.NET网站管理工具。

定义访问权限和个性化属性

我们通过为域服务应用RequiresAuthenticationAttribute和RequiresRoleAttribute属性,来限制对域服务的访问。如果域操作没有属性,对所有用户有效。在域操作上添加属性,并不能阻止用户调用域操作。只是没有所需凭证的用户会收到一个异常。

根据角色限制显示数据
1. 在资源管理器中,在服务端项目中,点击App_Data文件夹,选择"添加->已存在项"。
2. 在"添加已存在项"对话框中,添加AdventureWorksLT示例数据库。
3. 在服务端,添加一个新项,并选择ADO.NET Entity Data Model模板。
4. 命名模型为AdventureWorksModel.edmx,并点击"添加"."实体数据模型向导"将会出现。
5. 选择"从数据库生成"选项,并点击"下一步".
6. 选择AdventureWorksLT数据库,并点击"下一步"。
7. 从数据库对象列表中,选择Customer,Product,以及SalesOrderHeader表,然后点击"完成"。实体数据模型将会出现在设计器中。
8. 生成解决方案。
9. 在服务端,添加一个新项,并选择Domain Service Class模板。
10. 命名为AdventureWorksDomainService,然后点击"添加"。
11. 在"添加新域服务类"对话框中,选择Customer,Product和SalesOrderHeader实体。

12. 点击"OK"以完成创建域服务。
13. 在AdventureWorksDomainService类中,对GetSalesOrderHeader方法添加RequiresAuthenticationAttribute属性。

[RequiresAuthentication()]
public IQueryable GetSalesOrderHeaders()
{
return this.ObjectContext.SalesOrderHeaders;
}

14. 对GetCustomers方法添加RequiresRoleAttribute属性,并设置所需的角色为"Managers"。

[RequiresRole("Managers")]
public IQueryable GetCustomers()
{
return this.ObjectContext.Customers;
}

GetProducts方法对所有用户都可用,GetSalesOrderHeader只对验证用户可用,GetCustomers方法只对属于Managers角色的用户可用。

[EnableClientAccess()]
public class AdventureWorksDomainService : LinqToEntitiesDomainService
{
[RequiresRole("Managers")]
public IQueryable GetCustomers()
{
return this.ObjectContext.Customers;
}

public IQueryable GetProducts()
{
return this.ObjectContext.Products;
}

[RequiresAuthentication()]
public IQueryable GetSalesOrderHeaders()
{
return this.ObjectContext.SalesOrderHeaders;
}
}


下面我们在Web.config文件中定义个性法属性。当我们把属性添加到服务端的用户类时,将会为客户端项目生成对应的属性。

1. 在服务端项目,打开Web.config文件。
2. 在元素内,添加名为DefaultRows的个性化属性。这个属性将保持用户对显示的数据数量的偏爱。








3. 保存Web.config文件。
4. 在服务端项目中,展开Models文件夹。
5. 打开User.cs或User.vb文件,添加名为DefaultRows属性。

namespace ExampleBusinessApplication.Web
{
using System.Runtime.Serialization;
using System.Web.Ria.ApplicationServices;

public partial class User : UserBase
{
[DataMember]
public string FriendlyName { get; set; }

public int DefaultRows { get; set; }
}
}


从客户端使用身份验证服务
在调用有限制权限的域操作时,我们应该对用户检查所需的凭证。否则,将会抛出一个异常。在下面的部分,我们将检查用户的凭证,并根据用户的凭证来填充一个到三个DataGrid控件。我们会根据用户个性化中的属性来得到显示的记录数量。对没有验证的用户,默认的值是10。这部分没有让用户更改DefaultRows个性化属性的方式,不过在以后的部分会添加这个方式。

添加一个Silverlight页面来显示数据
1. 在客户端,在Views文件夹中添加新项。
2. 选择Silverlight Page模板,并命名为Reports.xaml。
3. 打开MainPage.xaml文件,并名为Link2的超链接按钮下面添加一个指向Reports页面的链接。



NavigateUri="/Reports" TargetName="ContentFrame" Content="{Binding Path=ApplicationStrings.ReportsPageTitle, Source={StaticResource ResourceWrapper}}"/>

4. 在Assets\Resources文件夹内,打开ApplicationString.resx文件。
5. 添加一个新的名为ReportsPageTitle的字符串资源,并设置值为Reports。

6. 保存并关闭ApplicationString.resx文件。
7. 打开Reports.xaml文件,并添加如下XAML到Grid元素内。



Text="{Binding Path=ApplicationStrings.ReportsPageTitle, Source={StaticResource ResourceWrapper}}"/>
Text="Display reports based on user permissions"/>




8. 拖拽三个DataGrid控件到名为ContentStackPanel的stack面板内。当我们从工具栏中拖拽DataGrid控件时,会在项目中添加一个对System.Windows.Controls.Data程序集的应用,并在页面内添加System.Windows.Controls命名空间。
9. 命名DataGrid控件为ProductsGrid、SalesOrdersGrid、CustomerGrid。
10. 对每个DataGrid控件,设置Margin为5.

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"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
mc:Ignorable="d"
xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
d:DesignWidth="640" d:DesignHeight="480"
Title="Reports Page" >



Text="{Binding Path=ApplicationStrings.ReportsPageTitle, Source={StaticResource ResourceWrapper}}"/>
Text="Display reports based on user permissions"/>








11. 打开Reports.xaml.cs或Reports.xaml.vb。
12. 对c#,用using添加System.Windows.Ria,System.Windows.Ria.ApplicationServices,ExampleBusinessApplication.Web,和ExampleBusinessApplication.Resources命名空间。
13. 创建AdventureWorksDomainService的名为context的实例,并创建一个名为numberOfRows的变量来保存检索的记录数量。

private AdventureWorksDomainContext context = new AdventureWorksDomainContext();
int numberOfRows = 10;

14. 添加一个名为LoadRestrictedReports的方法,这个方法调用GetSalesOrderHeaderQuery方法和GetCustomerQuery方法。如果用户属于Managers角色,就显示对应的带结果的数据表格。
如果没有所需凭证的用户调用一个域操作时,域操作会返回一个异常。我们可以通过在调用域操作之前检查凭证来避免这种情况。

private void LoadRestrictedReports()
{
LoadOperation loadSales = context.Load(context.GetSalesOrderHeadersQuery().Take(numberOfRows));
SalesOrdersGrid.ItemsSource = loadSales.Entities;
SalesOrdersGrid.Visibility = System.Windows.Visibility.Visible;

if (WebContext.Current.User.IsInRole("Managers"))
{
LoadOperation loadCustomers = context.Load(context.GetCustomersQuery().Take(numberOfRows));
CustomersGrid.ItemsSource = loadCustomers.Entities;
CustomersGrid.Visibility = System.Windows.Visibility.Visible;
}
else
{
CustomersGrid.Visibility = System.Windows.Visibility.Collapsed;
}
}

15. 添加名为LoadReports的方法来检测是否用户已验证,如果已验证,就调用LoadRestricteReports方法。它还检索名为DefaultRows的个性化属性,并对User对象的PropertyChanged事件添加事件处理程序。最后,对所有用户调用GetProductsQuery方法。

private void LoadReports()
{
if (WebContext.Current.User.IsAuthenticated)
{
numberOfRows = WebContext.Current.User.DefaultRows;
WebContext.Current.User.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(User_PropertyChanged);
LoadRestrictedReports();
}
else
{
CustomersGrid.Visibility = System.Windows.Visibility.Collapsed;
SalesOrdersGrid.Visibility = System.Windows.Visibility.Collapsed;
}

LoadOperation loadProducts = context.Load(context.GetProductsQuery().Take(numberOfRows));
ProductsGrid.ItemsSource = loadProducts.Entities;
}

16. 对PropertyChanged事件添加事件处理程序,保证在DefaultRows属性改变时调用LoadReports方法。

void User_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "DefaultRows")
{
LoadReports();
}
}

17. 为LoggedIn和LoggedOut事件添加事件处理方法,并根据用户凭证的状况装载或隐藏数据。

void Authentication_LoggedIn(object sender, AuthenticationEventArgs e)
{
LoadReports();
}

void Authentication_LoggedOut(object sender, AuthenticationEventArgs e)
{
CustomersGrid.Visibility = System.Windows.Visibility.Collapsed;
SalesOrdersGrid.Visibility = System.Windows.Visibility.Collapsed;
}

18. 在构造函数内添加下面的代码:

public Reports()
{
InitializeComponent();

this.Title = ApplicationStrings.ReportsPageTitle;

WebContext.Current.Authentication.LoggedIn += new System.EventHandler(Authentication_LoggedIn);
WebContext.Current.Authentication.LoggedOut += new System.EventHandler(Authentication_LoggedOut);

LoadReports();
}

以下是完整代码

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.Navigation;
using System.Windows.Ria;
using System.Windows.Ria.ApplicationServices;
using ExampleBusinessApplication.Web;
using ExampleBusinessApplication.Resources;

namespace ExampleBusinessApplication.Views
{
public partial class Reports : Page
{
private AdventureWorksDomainContext context = new AdventureWorksDomainContext();
int numberOfRows = 10;

public Reports()
{
InitializeComponent();

this.Title = ApplicationStrings.ReportsPageTitle;

WebContext.Current.Authentication.LoggedIn += new System.EventHandler(Authentication_LoggedIn);
WebContext.Current.Authentication.LoggedOut += new System.EventHandler(Authentication_LoggedOut);

LoadReports();
}

private void LoadReports()
{
if (WebContext.Current.User.IsAuthenticated)
{
numberOfRows = WebContext.Current.User.DefaultRows;
WebContext.Current.User.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(User_PropertyChanged);
LoadRestrictedReports();
}
else
{
CustomersGrid.Visibility = System.Windows.Visibility.Collapsed;
SalesOrdersGrid.Visibility = System.Windows.Visibility.Collapsed;
}

LoadOperation loadProducts = context.Load(context.GetProductsQuery().Take(numberOfRows));
ProductsGrid.ItemsSource = loadProducts.Entities;
}

private void LoadRestrictedReports()
{
LoadOperation loadSales = context.Load(context.GetSalesOrderHeadersQuery().Take(numberOfRows));
SalesOrdersGrid.ItemsSource = loadSales.Entities;
SalesOrdersGrid.Visibility = System.Windows.Visibility.Visible;

if (WebContext.Current.User.IsInRole("Managers"))
{
LoadOperation loadCustomers = context.Load(context.GetCustomersQuery().Take(numberOfRows));
CustomersGrid.ItemsSource = loadCustomers.Entities;
CustomersGrid.Visibility = System.Windows.Visibility.Visible;
}
else
{
CustomersGrid.Visibility = System.Windows.Visibility.Collapsed;
}
}

void Authentication_LoggedIn(object sender, AuthenticationEventArgs e)
{
LoadReports();
}

void Authentication_LoggedOut(object sender, AuthenticationEventArgs e)
{
CustomersGrid.Visibility = System.Windows.Visibility.Collapsed;
SalesOrdersGrid.Visibility = System.Windows.Visibility.Collapsed;
}

void User_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "DefaultRows")
{
LoadReports();
}
}
}
}

19. 运行解决方案。
20. 点击Reports链接。注意到,当我们没登陆时,只有产品表显示在报告页面。
21. 点击"登陆"链接,并以SalesUser登陆。会发现,产品和销售订单表显示出来。

22. 登出并以CustomerManager登陆。会发现,三个表都显示了。
23. 关闭web浏览器。

添加一个窗口来设置个性化属性
我们可以通过添加一个子窗口来允许用户编辑DefaultRows个性属性。当值改变后,我们调用SaveUser方法来把值保存到数据源。我们通过当前WebContext实例的User对象上的属性来检索当前的值。
1. 在客户端,在Views文件夹中添加新项。
2. 选择Silverlight Child Window模板,并命名为ProfileWindow.xaml。

3. 点击"添加"按钮。
4. 在ProfileWindow.xaml文件内,在Grid.RowDefinitions元素后添加XAML,来添加一个下拉列表选择要在报表中显示的行数。



















5. 设置ChildWindow的Title属性为Select References。
6. 在ProfileWindow.xaml.cs中,添加下面的代码来检索和设置个性化属性。

public partial class ProfileWindow : ChildWindow
{
public ProfileWindow()
{
InitializeComponent();

string userDefaultRows = WebContext.Current.User.DefaultRows.ToString();
foreach (ComboBoxItem cbi in defaultRows.Items)
{
if (cbi.Content.ToString() == userDefaultRows)
{
defaultRows.SelectedItem = cbi;
break;
}
}
}

private void OKButton_Click(object sender, RoutedEventArgs e)
{
int newSelection = int.Parse(defaultRows.SelectionBoxItem.ToString());
if (newSelection != WebContext.Current.User.DefaultRows)
{
WebContext.Current.User.DefaultRows = newSelection;
WebContext.Current.Authentication.SaveUser(true);
}
this.DialogResult = true;
}

private void CancelButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
}
}

7. 如果是VB,添加Imports命令引用System.Windows.Controls和System.Windows命名空间。
8. 展开Views\Login文件夹,并打开LoginStatus.xaml文件。
9. 添加一个指向profile文件的设置链接,在Logout按钮前添加下面的XAML




10. 在LoginStatus.xaml.cs文件内,为设置链接添加点击事件处理方法。

private void SettingsButton_Click(object sender, RoutedEventArgs e)
{
ExampleBusinessApplication.Views.ProfileWindow settingsWindow = new ExampleBusinessApplication.Views.ProfileWindow();
settingsWindow.Show();
}

11. 运行解决方案。
12. 以CustomerManager或SalesUser登陆,注意到在登陆状态栏,现在包含一个"设置"的链接。

13. 点击"设置"链接,并设置默认的报表显示行数。

14. 打开报表页面,会注意到DataGrid现在包含我们所选的行数。

WCF RIA 服务 (三十四)-- 身份验证、角色、个性化 5

如何:创建自定义验证属性

WCF RIA Services框架提供了RequiresAuthenticationAttribute和RequiresRoleAttribute属性。这两个属性使我们很方便地指定哪个域操作是仅仅对验证用户或特定角色成员可用的。除了这两个属性,我们可以创建自制验证逻辑属性并对域操作应用这个属性。
这章我们演示如何为身份验证添加自制属性。我们通过创建一个派生于AuthorizationAttribute的类并重载Authorize方法用来提供我们自定义的逻辑,来创建一个自制验证属性。

1. 在服务端项目中,创建派生于AuthorizationAttribute的类。
2. 重载Authorize方法,并添加判断是否授权的逻辑。下面的例子演示一个名为CheckAttendeeName的定制属性,来检测用户的角色和用户名字的第一个字母。

public class CheckAttendeeNameAttribute : System.Web.DomainServices.AuthorizationAttribute
{
public override bool Authorize(System.Security.Principal.IPrincipal principal)
{
if (principal.IsInRole("Attendee") && principal.Identity.Name.StartsWith("A"))
{
return true;
}
else
{
return false;
}
}
}

3. 要执行自定义的验证逻辑,对域操作应用这个自定义的验证属性。

[CheckAttendeeName()]
public IQueryable GetCourses()
{
return this.ObjectContext.Courses;
}

WCF RIA 服务 (三十三)-- 身份验证、角色、个性化 4

如何:在RIA Services中允许个性化功能
使用个性化功能,我们可以为用户检索和保存属性。WCF RIA Services中的个性化功能建立在ASP.NET的个性化框架上。
我们只能在用户验证后检索或保存个性化属性。

配置服务端项目
1. 在服务端项目中,打开Web.config文件。
2. 在段内,添加元素。
3. 在元素内,添加个性化属性。下面示例如何创建个性化以及定义一个名为FriendlyName的属性。










4. 为验证服务打开包含User类的文件。
5. 在User类中,添加我们已在Web.config文件中添加过的个性化属性。

public partial class User : UserBase
{
public string FriendlyName { get; set; }
}


从客户端访问个性化属性
1. 在Silverlight客户端项目中,打开后台代码页面。
2. 在后台代码页面中,设置或检索当前WebContext实例的User对象上的个性化属性。

WebContext.Current.User.FriendlyName = "Mike";

3. 如果想让WebContext对象在XAML中可用,那么在创建RootVisual之前,在Application.Startup事件中把当前WebContext实例添加到应用程序资源中。

private void Application_Startup(object sender, StartupEventArgs e)
{
this.Resources.Add("WebContext", WebContext.Current);
this.RootVisual = new MainPage();
}

通过声明性语法,我们也可以检索个性化属性。示例如下:



WCF RIA 服务 (三十二)-- 身份验证、角色、个性化 3

如何:在RIA Services中允许角色功能
使用角色,我们可以指定哪个验证用户组可以访问某些资源。WCF RIA Services中的角色功能是建立在ASP.NET的角色功能上的。
我们只有在用户已经被验证后,才能检索用户的角色信息。通过在域操作中的方法上使用RequireRoleAttribute属性,我们就可以限制角色中的成员对域操作的访问。

配置服务端项目
1. 在服务端项目中,打开Web.config文件。
2. 在段中,添加元素。






3. 在成员数据库中,创建所需的角色并赋予用户所需的角色。更多详情,可看后面的章节。
4. 要确保只有指定角色中的成员才能访问域操作,我们需要对域操作应用RequireRoleAttribute属性。

[RequiresRole("Managers")]
public IQueryable GetCustomers()
{
return this.ObjectContext.Customers;
}


在客户端使用角色
1. 要检测是否用户属于要求的角色,使用Roles属性或调用WebContext.Current.User对象的IsInRole方法。下面示例了在调用域操作之前,检测是否用户属于Managers的角色。

private void LoadRestrictedReports()
{
LoadOperation loadSales = context.Load(context.GetSalesOrderHeadersQuery().Take(numberOfRows));
SalesOrdersGrid.ItemsSource = loadSales.Entities;
SalesOrdersGrid.Visibility = System.Windows.Visibility.Visible;

if (WebContext.Current.User.IsInRole("Managers"))
{
LoadOperation loadCustomers = context.Load(context.GetCustomersQuery().Take(numberOfRows));
CustomersGrid.ItemsSource = loadCustomers.Entities;
CustomersGrid.Visibility = System.Windows.Visibility.Visible;
}
else
{
CustomersGrid.Visibility = System.Windows.Visibility.Collapsed;
}
}

2. 如果想让WebContext对象在XAML中可用,那么在创建RootVisual之前,在Application.Startup事件中把当前WebContext实例添加到应用程序资源中。

private void Application_Startup(object sender, StartupEventArgs e)
{
this.Resources.Add("WebContext", WebContext.Current);
this.RootVisual = new MainPage();
}

WCF RIA 服务 (三十一)-- 身份验证、角色、个性化 2

如何:在RIA Services中允许进行身份验证
WCF RIA Services中的身份验证是建立在ASP.NET验证框架之上的。
本章节展示如后在我们的应用程序中通过RIA Services来允许用户身份验证。我们必须在服务端和客户端添加代码,来使身份验证可行。这个验证对客户端就如同一个服务。我们可以通过对域操作应用RequiresAuthenticationAttribute属性,来保证只有验证用户可以访问域操作。

配置服务端项目

1. 在服务端项目中,打开Web.config文件。
2. 在元素中,添加元素。
3. 设置mode属性为我们想在项目中使用的验证模式。下面的代码展示了mode属性设置为Forms的元素。当然,我们的Web.config文件可能包含其他元素。





4. 保存Web.config文件。
5. 在资源管理器中,右键点击服务端项目,选择"添加->新项"。出现"添加新项"对话框。
6. 选择Authentication Domain Service模板,并制定一个名字。

7. 点击"添加"。
8. 如果想只有验证用户可以访问域操作,对域操作应用RequiresAuthenticationAttribute属性。下面代码展示了只有验证用户才可以访问GetSalesOrderHeaders方法。

[RequiresAuthentication()]
public IQueryable GetSalesOrderHeaders()
{
return this.ObjectContext.SalesOrderHeaders;
}

9. 生成解决方案。

在客户端配置身份验证服务

1. 在客户端项目中,打开App.xaml文件。
2. 在application lifetime 对象集合中添加当前的WebContext对象。下面的app前缀引用包含生成的WebContext对象的命名空间。通常,这个WebContext用的是客户端项目的基命名空间。






3. 在WebContext元素内,添加验证模式。
我们可以添加WindowsAuthentication或者FormsAuthentication。然而,我们添加的类型应该与服务端的验证模式一致。下面展示了如何添加FormsAuthentication。

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:app="clr-namespace:ExampleApplication"
xmlns:appsvc="clr-namespace:System.Windows.Ria.ApplicationServices;assembly=System.Windows.Ria"
x:Class="ExampleApplication.App">











4. 如果必要的话,在客户端添加一个页面来收集用户凭证。
5. 在登录页面的后台代码文件中,调用Login方法来登录用户。下面的例子展示了如何从登录按钮的事件处理中调用Login方法。包含了一个回调函数来对登录操作的结果做反应。

private void LoginButton_Click(object sender, RoutedEventArgs e)
{
LoginParameters lp = new LoginParameters(UserName.Text, Password.Password);
WebContext.Current.Authentication.Login(lp, this.LoginOperation_Completed, null);
LoginButton.IsEnabled = false;
LoginResult.Text = "";
}

private void LoginOperation_Completed(LoginOperation lo)
{
if (lo.HasError)
{
LoginResult.Text = lo.Error.Message;
LoginResult.Visibility = System.Windows.Visibility.Visible;
lo.MarkErrorAsHandled();
}
else if (lo.LoginSuccess == false)
{
LoginResult.Text = "Login failed. Please check user name and password.";
LoginResult.Visibility = System.Windows.Visibility.Visible;
}
else if (lo.LoginSuccess == true)
{
SetControlVisibility(true);
}
LoginButton.IsEnabled = true;
}

6. 调用Logout方法来登出用户。

private void LogoutButton_Click(object sender, RoutedEventArgs e)
{
WebContext.Current.Authentication.Logout(this.LogoutOperation_Completed, null);
}

private void LogoutOperation_Completed(LogoutOperation lo)
{

if (!lo.HasError)
{
SetControlVisibility(false);
}
else
{
ErrorWindow ew = new ErrorWindow("Logout failed.", "Please try logging out again.");
ew.Show();
lo.MarkErrorAsHandled();
}
}

7. 要检测是否用户已登录,检索生成的User实体的IsAuthentication属性。下面的代码展示了在检索个性化信息和调用域操作之前,先检测是否当前用户已经验证了。

private void LoadReports()
{
if (WebContext.Current.User.IsAuthenticated)
{
numberOfRows = WebContext.Current.User.DefaultRows;
WebContext.Current.User.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(User_PropertyChanged);
LoadRestrictedReports();
}
else
{
CustomersGrid.Visibility = System.Windows.Visibility.Collapsed;
SalesOrdersGrid.Visibility = System.Windows.Visibility.Collapsed;
}

LoadOperation loadProducts = context.Load(context.GetProductsQuery().Take(numberOfRows));
ProductsGrid.ItemsSource = loadProducts.Entities;
}

8. 如果想让WebContext对象在XAML中可用,那么在创建RootVisual之前,在Application.Startup事件中把当前WebContext实例添加到应用程序资源中。

private void Application_Startup(object sender, StartupEventArgs e)
{
this.Resources.Add("WebContext", WebContext.Current);
this.RootVisual = new MainPage();
}

2010年2月10日星期三

WCF RIA 服务 (三十)-- 身份验证、角色、个性化

在Web应用中,我们经常会限制某些特定用户使用一些功能,并对每个用户保持性能。ASP.NET提供了身份验证、角色、个性化实现这些功能。身份验证允许我们核实用户的凭证并标记用户为已登录。角色允许我们根据职责来把用户分组,并对组中成员授予资源许可。个性化允许我们保持验证用户的属相并在web应用中检索这些属性。

WCF RIA Services框架在表现层和中间层都提供这些功能。这节就介绍RIA Services中的身份验证、角色、和个性化。

身份验证域服务
RIA Services提供身份验证域服务模板来简化在表示层中使用身份验证、角色和个性化。我们把验证域服务添加到服务端项目中。

当我们添加一个身份验证域服务时,RIA Services框架自动在服务端添加两个类。代表身份验证服务的类派生于AuthenticationBase(T)类。代表用户的类派生于UserBase类。用户类为已经验证的用户包含个性化属性。
通过客户端代码,我们调用身份验证域服务来请求我们需要的验证、角色或个性化信息。

WebContext
当我们添加一个身份验证域服务时,RIA Services自动在客户端生成一个WebContext类。这个类派生于WebContextBase类,并允许我们在客户端访问身份验证域服务和用户。用Current属性来获取WebContext的当前实例。

身份验证
要想在RIA Services解决方案中使用身份验证,我们必须在服务端和客户端来配置验证。
通过在客户端使用WebContext,我们可以从Silverlight应用程序同步登陆用户。当我们实施了身份验证时,我们通常会用到下面的方法和属性。
Authentication属性(WebContext.Current.Authentication): 提供对身份验证服务的访问。
User属性(WebContext.Current.User): 提供对包含用户状态的对象的访问。
Login(String,String)方法(WebContext.Current.Authentication.Login(String,String):同步验证用户凭证。
Logout(Boolean)方法(WebContext.Current.Authentication.Logout(boolean):同步登出一个已验证用户。

角色
在我们实施了身份验证后,我们就可以配置我们的解决方案来使用角色功能。当我们实施角色功能时,通常会用到下面的属性和方法。
WebContext.Current.User.Roles : 提供用户被赋予的角色的集合。
WebContext.Current.User.IsInRole(String) : 判断已验证用户是否是指定角色的成员。

个性化
要想使用个性化功能,我们必须为个性化功能配置我们的解决方案。当我们实施了个性化功能时,通常会用到下面的属性和方法。
WebContext.Current.User : 包含所有我们已经添加到User类中的属性。例如,User.PhoneNumber。
LoadUser()方法(WebContext.Current.Authentication.LoadUser()) : 刷新用户状态。
SaveUser(Boolean)方法(WebContext.Current.Authentication.SaveUser(boolean)) : 把所有的更改保存到用户状态中。

在客户端处理身份验证错误
我们可以处理在登录、登出、装载或保存用户时产生的错误。思路是通过在调用这些方法时提供回调参数。在回调方法中,我们添加处理错误的代码,并调用MarkErrorAsHandled方法来指定framwork不要抛出异常。AuthenticationService类允许我们在调用下面的方法时提供回调参数。
-- LoadUser
-- Login
-- Logout
-- SaveUser

限制对域服务的访问
在我们实施了身份验证和角色功能后,我们就可以限制特定用户对域服务的访问。可以对整个域服务或服务中个别的操作应用下面的属性。如果我们对整个服务使用了属性,它会应用到所有的操作。
- RequiresAuthenticationAttribute : 指定只有具有验证凭证的用户可以访问操作。
- RequiresRoleAttribute : 指定只有属于特定角色的验证用户可以访问操作。
我们也可以创建自己的验证属性,更多相关信息可以看后面的章节。

WCF RIA 服务 (二十九)-- Silverlight 客户端 10

如何:在客户端添加计算特性
我们可以在客户端添加成员属性,这些属性是有实体类中的成员属性计算而来。局部方法被用来引发事件,来通知用户界面元素数值已被更改。当我们添加计算的成员属性时,这些属性只存在于客户端项目中。

1. 在客户端项目中,添加一个类文件。
2. 声明一个局部类,这个类与我们想要修改的实体代理类具有相同的名字和命名空间。
3. 添加一个属性,创建一个基于一个或多个实体代理类中值的新值。
4. 对每一个用于计算新值的成员属性都实施On[CustomProperty]Changed局部方法,并且调用RaisePropertyChanged方法来通知框架计算的属性已经更改。

下面的示例演示了如何为一个员工计算总的不工作的有效时间,这个时间是基于休假时间和生病请假的时间。对休假时间或生病请假的时间的改动,都将会对总时间产生一个改动。

using System.Windows.Ria;

namespace RIAServicesExample.Web
{
public partial class Employee : Entity
{
public int TotalOffHours
{
get { return this.SickLeaveHours + this.VacationHours; }
}
partial void OnSickLeaveHoursChanged()
{
this.RaisePropertyChanged("TotalOffHours");
}
partial void OnVacationHoursChanged()
{
this.RaisePropertyChanged("TotalOffHours");
}
}
}


可以用下面的代码来对计算的属性进行数据绑定:




WCF RIA 服务 (二十八)-- Silverlight 客户端 9

自定义生成代码
对于WCF RIA Services,在某些情况下,我们想在客户端生成的代码中添加些东西。然而,我们不能直接定制生成代码,因为在下次中间层重新编译的时候会被覆盖掉。RIA Services在生成代码中提供了局部方法,这样我们可以在分开的代码文件中定制客户端代码。这些局部方法就如同“钩子”,通过它我们可以把自己的代码附加在生成代码上。只有当我们已经创建了对应的局部方法时,这些方法才能被调用。
局部方法
WCF RIA Services框架为域上下文类和实体类生成局部方法。
对于域上下文类,提供了下面的局部方法。
OnCreated():当DomainContext对象实例化时执行。

对于实体类,提供了下面的局部方法。
OnCreated():当实体对象实例化时执行。

OnLoaded(boolean):在第一次装载实体并反串行化时,或当实体从服务端反串行化但已经在客户端存在时执行。

On[PropertyName]Changing: 在验证之后,设定值之前,被调用。

On[PropertyName]Changed: 在设定值之后,调用RaiseDataMemberChanged方法之前,被调用。

On[CustomMethodName]Invoking: 调用定制方法之后,实际执行代码之前,被调用。

On[CustomMethodName]Invoked: 定制方法调用并执行代码之后,被调用。

实施局部方法
要使用这些方法,我们添加一个与想定制的生成类相同名字和命名空间的局部类。应为自动生成的客户端代码与服务端项目的代码有一样的命名空间,所以我们局部类的命名空间通常是projectname.Web的格式。然后,在客户端代码必须执行的情况下,我们实施想要执行的方法。例如在一个域上下文被创建并装载时,我们添加如下代码:

using System.Windows.Ria;

namespace RIAServiceExample.Web
{
public partial class EmployeeDomainContext : DomainContext
{
partial void OnCreated()
{
this.Load(this.GetEmployeesQuery());
}
}
}


我们还可以在生成的实体类的局部方法中设置成员属性。例如,如果数据库中的员工表中包含一个名为CreatedBy字段,那么我们可以通过OnCreated()局部方法来设置这个属性的值。

using System.Windows.Ria;

namespace RIAServiceExample.Web
{
public partial class Employee : Entity
{
partial void OnCreated()
{
this.CreatedBy = WebContext.Current.User.Name;
}
}
}

2010年2月7日星期日

WCF RIA 服务 (二十七)-- Silverlight 客户端 8

演练:在Silverlight商业应用程序中显示数据
在本演示中,我们将创建一个显示数据的Silverlight商业应用程序。Visual Studio提供了几个设计时工具来帮助我们创建SL商业应用程序。这个演练将展现如何使用DataSources窗口在RIA中创建与数据一起工作的用户界面。
演示将会满足下面的任务:
1. 创建SL商业应用程序,它包含SL客户端和ASP.NET Web应用两个项目。
2. 通过更改应用程序名字来修改应用程序资源,它存贮为资源字符。
3. 创建一个AdventureWorksClassLibrary示例数据库的实体数据模型。
4. 创建一个向客户端公开实体数据模型中数据的域服务。
5. 在域服务中添加和修改自定义查询。
6. 创建额外的SL页面来向用户显示数据。
7. 在默认的导航栏中添加按钮来访问SL中的页面。
8. 通过从Data Sources窗口向Silverlight设计器中拖拽条目来配置SL页面显示数据。
9. 排序和分页数据。
10. 配置用户界面来接受查询参数。

这个演练是在VS2010中进行的,如果是其他版本会有不同。

创建SL商业应用程序

1. 打开文件->新建->项目
2. 展开Visual c#或Visual Basic,并选择Silverlight。
3. 选择Silverlight Business Application
4. 在名字文本框内,输入AdventureWorksApp并点击OK。
这个解决方案包含两个项目:一个AdventureWorksApp客户端项目和一个AdventureWorksApp.Web Web应用项目。

命名和测试应用程序
Silverlight Business Application有内置功能。默认的,它有一个主页,一个关于页,一个导航栏,以及注册功能。提供了一个资源字符来作为默认的应用程序名字,我们可以更改它。
1. 在客户端解决方案资源管理器中,展开Resources文件夹。(VS2008中打开Assets文件夹下的Resources文件夹)
2. 双击ApplicationStrings.resx,打开资源编辑器。
3. 把资源字符串ApplicationName的值改为Adventure Works Application.
4. 保存并关闭ApplicationStrings.resx文件。
5. 运行应用程序。
主页会打开并显示默认的设计,包括已经更改过的应用程序名称。

为应用程序创建一个数据模型

1. 在解决方案资源管理器中,右键点击AdventureWorksApp.Web,并添加一个新项。
2. 在"添加新项"对话框中,选择ADO.NET Entity Data Model项。
3. 命名为AdventureWorksEDM.edmx,然后点击添加。实体数据模型向导会打开。
4. 在"选择模型内容"的页面上,点击"从数据库生成",然后点击"下一步"。
5. 在"选择数据连接"的页面上,选择或创建对AdventureWorks数据库的链接。
6. 确保选择了"Save entity connection settings in Web.Config as"选项,然后点击"下一步"。
7. 在"选择数据库对象"的页面上,重新命名模型命名空间Model为AdventureWorksDataModel。
8. 展开"表"节点,选择Customer表。
9. 点击"完成"。
10. 生成解决方案。

创建域服务

一个域服务会把数据模型中的数据实体和操作公开给客户端。它是添加在服务端项目中的。
1. 在"解决方案资源管理器"中,右键点击AdventureWorksApp.Web,并添加一个新项。
2. 在"添加新项"对话框中,选择"Domain Service Class"。
3. 命名为AdventureWorksService。
4. 点击"添加"。 "添加新项"对话框出现。
5. 选择下面的复选框:
. Enable client access
. Customer 和 Enable editing
. Generate associated classes for metadata.
6. 点击OK
7. 生成解决方案。

更改域服务的查询

域服务提供默认的操作,我们应该为我们特定的应用而修改它们。这个演练中,我们更改默认的查询,来返回按CustomerID排序的客户。

1. 在"解决方案资源管理器"中,双击AdventureWorksSercice.cs或AdventureWorksService.vb.
2. 更改GetCustomers方法,如下:

public IQueryable GetCustomer()
{
return this.ObjectContext.Customer.OrderBy(c=>c.CustomerID);
}

3. 生成解决方案。

创建Silverlight页面来显示数据
从Customer表返回的数据显示在自己的页面上,不显示在应用程序的首页上。

1. 在"解决方案资源管理器"中,右键点击客户端项目中的Views文件夹,并添加新项。
2. 在"添加新项"对话框中,选择Silverlight Page项。
3. 更改名字为CustomerList.xaml,并点击"添加"。
4. 从工具栏拖拽一个TextBlock放在CustomerList.xaml页面的顶部。
5. 把Text属性改为Customer List。
6. 保存CustomerList.xaml页面。

在首页上添加导航按钮
我们在应用程序的首页上添加一个导航到CustomerList页面的按钮。

1. 在"解决方案资源管理器"中,双击MainPage.xaml.
2. 在XAML视图里,在行下面添加如下代码:



NavigateUri="/CustomerList" TargetName="ContentFrame"/>

3. 运行应用程序,确认Customers按钮显示在导航栏上,并点击的时候显示CustomerList页面。

在CustomerList页面上显示客户数据
在DataGrid中显示客户数据。下面,我们就创建和配置一个DataGrid来显示客户数据,需要从Data Sources窗口拖拽一个Customer实体到设计窗口内。

1. 双击CustomerList.xaml。
2. 打开Data Sources窗口。可以在Data菜单中,选择Show Data Sources。注意Data Sources窗口已经包含了可用的实体。
3. 从Data Sources窗口中把Customer节点拖到设计器中,在TextBlock下面。
4. 运行应用程序并点击导航栏上的Customers按钮。
5. 核实在CustomerList页面上显示了Customer数据。
(我在VS2008中使用Data source有问题,生成不了数据节点。好像在Silverlight3中没有了ComponentModel.dll,所以直接使用域上下文来查询数据)

在域服务中添加自定义查询

1. 双击AdventureWorksService.cs。
2. 在类中添加如下代码:

public IQueryable GetCustomersByTitle()
{
return this.ObjectContext.Customer.Where(c => c.Title == "Mr.").OrderBy(c => c.CustomerID);
}

public IQueryable GetCustomersByLastName(string lastName)
{
return this.ObjectContext.Customer.Where(c => c.LastName==lastName).OrderBy(c=>c.CustomerID);
}

3. 生成解决方案。

显示自定义查询返回的数据

1. 双击CustomerList.xaml。
2. 在"数据源"窗口,选择Customer并点击下拉菜单。
3. 在查询列表中选择GetCustomersByTitle。
4. 把Customer节点拖拽到已经存在于设计器中的DataGrid。
5. 运行应用程序,并验证只有Title等于Mr.的顾客显示在页面上。

添加排序功能

VS2008中实现如下:
1. 双击CustomerList.xaml。
2. 在XAML视图中,在DomainDataSource控件中,添加SortDescriptors。
3. 设置PropertyPath属性为"LastName"。设置Direction属性为Ascending。
4. 运行应用程序,注意到顾客数据已经按LastName排序了。

添加分页功能

VS2008中实现如下:
1. 双击CustomerList.xaml。
2. 在XAML视图中,在DomainDataSource控件中,添加PageSize属性,并设置为15.添加LoadSize属性,并设置为30.
3. 添加DataPager控件。



4. 运行应用程序,注意到页面装载的速度已经大大加快了。并且可以通过DataPager控件浏览数据。

创建一个页面来根据LastName进行查询

1. 在AdventureWorksApp中的Views文件夹下,添加新项。
2. 在"添加新项"对话框中,选择Silverlight Page项。
3. 命名为CustomerSearchByLastName.xaml,并点击"添加"。
4. 保存这个页面。

在首页中添加一个导航按钮

1. 双击MainPage.xaml。
2. 在XAML视图中,在Link3的HyperlinkButton下面添加如下代码。






创建用户界面来运行GetCustomerByLastName查询
VS2008中实现如下:

1. 双击CustomerSearchByLastName.xaml
2. 添加命名空间引用

xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Ria"
xmlns:riaData="clr-namespace:System.Windows.Data;assembly=System.Windows.Controls.Ria"
xmlns:domain ="clr-namespace:AdventureWorksApp.Web"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"

3. 然后在Grid中添加数据源、DataGrid、文本框和按钮(实际代码中不需SL:前缀),如下代码:






















测试应用程序

1. 生成解决方案。
2. 运行应用程序。
3. 点击"Customer Search"。
4. 在文本框中输入想要查询的LastName,例如"liu"。
5. 点击Load按钮。
6. 将会显示名为"liu"的客户。

WCF RIA 服务 (二十六)-- Silverlight 客户端 7

演练:编辑来自域服务的数据
当我们在域服务中添加了更新、插入或删除方法时,我们就可以在Silverlight客户端创建一个接口来让用火修改数据。EntityChangesSet对象跟踪所有的改变,并且这些改变在我们调用SubmitChanges方法时一起提交。
在这个演练中,我们将学习如何创建一个让用户修改所显示数据的接口,并且将这些修改保存到数据库。

演练

1. 打开RIAServicesExample项目。(参见 WCF RIA 服务 三)
2. 在MainPage.xaml中,更改界面让用户可以保存或拒绝DataGrid中的更改。下面的XAML添加了一个Save Changes按钮和Reject Changes按钮,以及一个更改文本框。















3. 在MainPage.xaml的后台代码文件中,添加按钮的点击事件处理方法、RowEditEnded事件的处理方法、一个名为OnSubmitCompleted的回调方法,和一个处理行将发生的更改的方法。

private void SaveButton_Click(object sender, RoutedEventArgs e)
{
_customerContext.SubmitChanges(OnSubmitCompleted, null);
}

private void RejectButton_Click(object sender, RoutedEventArgs e)
{
_customerContext.RejectChanges();
CheckChanges();
}

private void CustomerGrid_RowEditEnded(object sender, DataGridRowEditEndedEventArgs e)
{
CheckChanges();
}

private void CheckChanges()
{
EntityChangeSet changeSet = _customerContext.EntityContainer.GetChanges();
ChangeText.Text = changeSet.ToString();

bool hasChanges = _customerContext.HasChanges;
SaveButton.IsEnabled = hasChanges;
RejectButton.IsEnabled = hasChanges;
}

private void OnSubmitCompleted(SubmitOperation so)
{
if (so.HasError)
{
MessageBox.Show(string.Format("Submit Failed: {0}", so.Error.Message));
so.MarkErrorAsHandled();
}
CheckChanges();
}


为要更改的实体设置元数据

1. 在服务端项目中,为Customer实体手动添加一个名为Customer.metadata.cs的元数据类。
2. 在元数据类中,对CustomerID和ModifiedData成员属性添加EditableAttribute属性并设置AllowEdit属性为false。我们对成员属性应用EditableAttribute来指定是否想让这个成员属性在客户端让用户编辑。当我们将AllowEdit为false时,这个成员属性在客户端是只读的。在这个例子中,我们只想显示CustomeId和ModifiedDate成员属性而不想用户修改它们。
3. 添加ExcludeAttribute属性给PasswordHash,PasswordSalt和rowguid成员属性。我们对不想包含客户端生成代码中的成员属性使用ExcludeAttribute。在这个例子中,我们没有必要把PasswordHash,PasswordSalt和rowguid成员属性向客户端公开。

[MetadataTypeAttribute(typeof(Customer.CustomerMetadata))]
public partial class Customer
{
internal sealed class CustomerMetadata
{

// Metadata classes are not meant to be instantiated.
private CustomerMetadata()
{
}

[Editable(false)]
public int CustomerID;

[Editable(false)]
public DateTime ModifiedDate;

[Exclude]
public string PasswordHash;

[Exclude]
public string PasswordSalt;

[Exclude]
public Guid rowguid;

}
}

4. 用using添加所需的命名空间,例如 System.ComponentModel.DataAnnotations和System.Web.DomainServices.
5. 运行应用程序。
注意我们可以在DataGrid中编辑值了。当我们离开所编辑的行时,ChangeText的值将会变为行将发生的更改的描述。当我们点击Save Changes按钮时,数据更改将会保存到数据库中。当点击Reject Changes按钮时,所有的行将发生的更改将恢复原样。

WCF RIA 服务 (二十五)-- Silverlight 客户端 6

演练:检索和显示来自域服务的数据
想要在Silverlight应用程序中检索数据,我们调用域上下文中的方法,这些方法对应着域服务中我们想要使用的查询方法。例如,在域服务中有个名为GetProducts的方法,那么在与上下文中有个名为GetProductsQuery的方法。在SL应用程序中,我们调用GetProductsQuery方法,此方法返回一个EntityQuery(TEntity)对象。
在SL应用程序中,我们可以对查询应用额外的过滤来限制返回的实体。虽然一个查询方法可以返回产品表的所有记录,但可能我们只想显示价格低于100的产品。我们使用LINQ和LINQ运算符的子集来改变从查询返回的结果。下面列出了可用的查询运算符:
1. Where
2. OrderBy
3. ThenBy
4. Skip
5. Take
当应用了额外的过滤时,我们把EntityQuery(TEntity)对象传递给Load方法的一个参数,来运行查询和得到结果。如果查询有一个IsComposable属性设为false的QueryAttribute([Query(IsComposable = false)])。我们不可以在查询上应用额外的过滤。通常,只有返回单一实体的查询把IsComposable设置为false。
我们可以把数据绑定到任何显示数据的SL控件。DataGrid控件可以以表格的格式来显示数据。

演练
1. 打开RIAServicesExample解决方案。(WCF RIA 服务 三
2. 在Silverlight应用程序中,打开MainPage.xaml的代码文件。
3. 调用GetCustomersQuery方法来创建EntityQuery(TEntity)实例。
4. 使用可用的查询运算符来过滤客户。
5. 把查询对象传递给DomainContext的Load方法,并报返回结果赋给LoadOperation(TEntity).
下面的代码演示如何从域服务检索客户信息。并且过滤电话号码以583开始的客户,同时以LastName按字母排序。结果显示在DataGrid控件中。

public partial class MainPage : UserControl
{
private CustomerDomainContext _customerContext = new CustomerDomainContext();

public MainPage()
{
InitializeComponent();
EntityQuery query =
from c in _customerContext.GetCustomersQuery()
where c.Phone.StartsWith("583")
orderby c.LastName
select c;
LoadOperation loadOp = this._customerContext.Load(query);
CustomerGrid.ItemsSource = loadOp.Entities;
}
}

结果显示如下:

2010年2月6日星期六

WCF RIA 服务 (二十四)-- Silverlight 客户端 5

在客户端处理错误
当我们在客户端检索或修改数据时,我们通常需要处理错误和对错误做出反应。通过WCF RIA Services,我们为数据操作提供一个回调方法来处理错误,并且在回调方法里检查错误。使用回调方法是必需的,因为调用数据操作都是异步的,比且异常也是异步抛出的。默认下,对域操作中的所有错误都抛出一个异常。RIA Services为我们提供了处理错误的方式,并且可以指定框架不抛出异常。

当装载数据时处理错误
当从一个查询装载数据时,我们可以选择处理错误或忽略之。明确地,我们从如下选项中选择:

1. 使用带有回调方法做为参数的Load方法。在回调方法内,处理错误,并调用MarkErrorAsHandled方法来指定不要抛出错误。

2. 使用带一个名为thowOnError的Boolean参数的Load方法。当调用Load方法时,设置throwOnError为false来指定我们不想为查询错误抛出异常。

3. 当Load方法没有回调参数和布尔参数时,任何查询错误都会导致一个未处理的异常。
下面的示例演示了如何从查询载入数据,并指定一个回调方法来检测装载操作中的错误。

private CustomerDomainContext _customerContext = new CustomerDomainContext();

public MainPage()
{
InitializeComponent();

LoadOperation loadOp = this._customerContext.Load(this._customerContext.GetCustomersQuery(), OnLoadCompleted, null);
CustomerGrid.ItemsSource = loadOp.Entities;
}

private void OnLoadCompleted(LoadOperation lo)
{
if (lo.HasError)
{
MessageBox.Show(string.Format("Retrieving data failed: {0}", lo.Error.Message));
lo.MarkErrorAsHandled();
}
}


提交数据时处理错误
当提交数据时,我们不能像Load方法那样选择关闭异常。所有提交数据时发生的错误都会导致一个异常。明确地,我们可以选择如下选项:

1. 使用带回调参数的SubmitChanges方法。在回调方法里,处理错误并调用MarkErrorAsHandled方法来指定不要抛出异常。

2. 使用SubmitChanges方法,所有在提交数据时发生的错误都导致一个异常。

下面的示例演示了如何调用带有回调参数的SubmitChanges方法。

private void SaveButton_Click(object sender, RoutedEventArgs e)
{
_customerContext.SubmitChanges(OnSubmitCompleted, null);
}

private void RejectButton_Click(object sender, RoutedEventArgs e)
{
_customerContext.RejectChanges();
CheckChanges();
}

private void CustomerGrid_RowEditEnded(object sender, DataGridRowEditEndedEventArgs e)
{
CheckChanges();
}

private void CheckChanges()
{
EntityChangeSet changeSet = _customerContext.EntityContainer.GetChanges();
ChangeText.Text = changeSet.ToString();

bool hasChanges = _customerContext.HasChanges;
SaveButton.IsEnabled = hasChanges;
RejectButton.IsEnabled = hasChanges;
}

private void OnSubmitCompleted(SubmitOperation so)
{
if (so.HasError)
{
MessageBox.Show(string.Format("Submit Failed: {0}", so.Error.Message));
so.MarkErrorAsHandled();
}
CheckChanges();
}


调用操作时处理错误
当调用一个操作时,我们有像提交数据时一样的选择。如下:

1. 当调用操作时包含一个回调参数。在回调参数内处理错误,并调用MarkErrorAsHandled方法来指定不要抛出异常。

2. 调用不包含回调参数的操作。所有在调用操作时发生的错误都会抛出异常。


InvokeOperation invokeOp = customerContext.GetLocalTemperature(selectedPostalCode, OnInvokeCompleted, null);

private void OnInvokeCompleted(InvokeOperation invOp)
{
if (invOp.HasError)
{
MessageBox.Show(string.Format("Method Failed: {0}", invOp.Error.Message));
invOp.MarkErrorAsHandled();
}
else
{
result = invokeOp.Value;
}
}


验证服务处理错误
AuthenticationService类允许我们在调用下面的方法时提供回调参数:
1.LoadUser 2.Login 3.Logout 4.SaveUser
在回调方法中,我们可以提供代码来处置从验证服务中来的错误。 下面的例子演示如何从登陆按钮的事件处理中调用Login方法。

private void LoginButton_Click(object sender, RoutedEventArgs e)
{
LoginParameters lp = new LoginParameters(UserName.Text, Password.Password);
WebContext.Current.Authentication.Login(lp, this.LoginOperation_Completed, null);
LoginButton.IsEnabled = false;
LoginResult.Text = "";
}

private void LoginOperation_Completed(LoginOperation lo)
{
if (lo.HasError)
{
LoginResult.Text = lo.Error.Message;
LoginResult.Visibility = System.Windows.Visibility.Visible;
lo.MarkErrorAsHandled();
}
else if (lo.LoginSuccess == false)
{
LoginResult.Text = "Login failed. Please check user name and password.";
LoginResult.Visibility = System.Windows.Visibility.Visible;
}
else if (lo.LoginSuccess == true)
{
SetControlVisibility(true);
}
LoginButton.IsEnabled = true;
}

2010年2月5日星期五

WCF RIA 服务 (二十三)-- Silverlight 客户端 4

DomainDataSource
WCF RIA Services提供DomainDataSource控件来简化用户界面和域上下文中数据的交互。通过DomainDataSource,我们可以只是用声明性语法来检索、编辑数据。我们指定域上下文与DomainDataSource一起使用,然后通过这个上下文来调用操作。
配置Silverlight应用程序的DomainDataSource
为了使用DomainDataSource控件,我们必须在包含DomainDataSource的SL控件中添加一个程序集的引用和命名空间。
在SL项目中,我们必须添加一个对System.Windows.Controls.Ria程序集的引用。如果选择DataGrid与DomainDataSource一起使用,我们还要添加对System.Windows.Controls.Data的引用。
在宿主控件内,例如UserControl,我们必须添加下面的命名空间引用:

xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Ria"
xmlns:riaData="clr-namespace:System.Windows.Data;assembly=System.Windows.Controls.Ria"
xmlns:domain="clr-namespace:SilverlightApplication17.Web"

如果选择使用DataGrid控件,还需添加下面的命名空间:

xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"

检索和显示数据
我们为DomainDataSource指定一个域上下文,并向用户提供方法的名字来装载数据。然后我们绑定表示控件,例如DataGrid对DomainDataSource。下面的例子演示了DomainDataSource检索从名为ProductDomainContext的域上下文而来的数据。在域服务中应该存在一个名为GetProduct()的查询方法。

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"
xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Ria"
xmlns:riaData="clr-namespace:System.Windows.Data;assembly=System.Windows.Controls.Ria"
xmlns:domain="clr-namespace:SilverlightApplication17.Web"
xmlns:datac="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
mc:Ignorable="d">













对查询添加参数
某些情况下,查询方法需要参数值。通常,一个查询方法需要一个参数值来过滤返回的数据。下面的示例演示如何添加参数值,这个值可通过声明性文本来指定。













还可以使用来自用户的值来为查询添加参数。我们使用ControlParameter对象来把来自用户输入控件的值传递到查询。下面的例子展示了如何指定来自下拉菜单中的值做为参数值。











ParameterName="color"
ControlName="colorCombo"
PropertyName="SelectedItem.Content"
RefreshEventName="SelectionChanged" />









排序
DomainDataSource提供SortDescriptors集合来简化数据的排序。在SortDescriptors集合中,我们提供SortDescriptor实例来向用户描述参数值。我们可以添加多个SortDescriptor实例。还可以指定数据排序的升降序。下面的示例演示DomainDataSource中的排序描述符,从查询中检索的数据按来自StandardPrice属性中的值排序。













分组
DomainDataSource提供了GroupDescriptors集合来通过属性值来简化分组数据。在GroupDescriptors集合中,我们提供GroupDescriptor示例来定义用来分组的值。可以添加多个GroupDescriptors实例。













过滤
DomainDataSource控件提供FilterDescriptors集合来允许我们过滤查询返回的数据。通过添加过滤,我们可以指定只装载满足条件的域上下文实体。在FIlterDescriptorCollection对象上设置LogicalOperator属性,我们可以在不同的过滤之间定义逻辑关系。
过滤描述符通过FilterOperator枚举器来支持操作。
当定义基于用户输入的过滤时,我们可以提供ControlParameter实例。下面的示例演示了两个通过逻辑AND连接的过滤描述符。一个过滤依赖于用户的输入,另一个过滤通过声明性语句来指定。














ControlName="MaxPrice"
PropertyName="SelectedItem.Content"
RefreshEventName="SelectionChanged" />












分页
当显示大量实体的时候,我们会希望在用户界面上提供分页功能。DomainDataSource控件允许我们指定在一个页面上装载和显现的实体数量。新纪录只有在用户导航到了还没有装载实体的页面时才装载。我们设置PageSize和LoadSize属性来指定分页的参数。然后,我们把一个DataPage实例绑定到DomainDataSource来实施分页的接口。
注意:如果在应用程序中DataPager与实体框架数据存贮一起使用,我们必须对为DataPager从查询返回的数据进行排序。因为实体框架不支持没有OrderBy子句或在SL客户端没指定排序的分页。


















编辑
进行数据更改,需要调用DomainDataSource对象的SubmitChanges方法。要取消更改,调用RejectChanges方法。

WCF RIA 服务 (二十二)-- Silverlight 客户端 3

DomainContext
从客户端项目内部,我们不直接与域服务交互。相反,会对服务端中的每个域服务在客户端生成一个域上下文类。我们在域上下文类上调用对应于域服务上想使用的方法。这个生成的域上下文类派生于DomainContext类。
查询
域上下文中的查询方法通常与域服务中的查询方法有相同的名字,并有后缀Query。例如,一个域上下文中的GetCustomersQuery方法生成于域服务中的GetCustomers方法。这个查询方法返回一个EntityQuery对象,我们可以在其他操作中应用这个对象。
域上下文中的所有查询方法都是异步执行的。为了执行这个查询,我们在Load方法中把EntityQuery对象作为参数传递。
修改数据
当域服务中包含更新、插入、删除实体的方法时,在域上下文中不会生成这些方法。反之,我们在与上下文中使用SubmitChanges方法,会调用域服务中的正确操作。直到我们调用SubmitChanges时,才会在数据源中进行更改。可以通过调用RejectChanges方法来取消更改。
DomainContext类还提供HasChanges和EntityContainer属性来允许我们评估行将发生的更改。对Domain context的EntityContainer对象跟踪行将发生的更改。行将发生的更改不包括对域服务中的操作的调用,因为这些操作会在被调用时立即执行。当调用SubmitChanges时,所有的行将发生的更改都一起发到域服务。
定制方法
对域服务中的那些有公共访问修饰符而且没有标记着IgnoreOperationAttribute属性的自定义方法,域上下文都会包含这些方法。域上下文中的这些方法的名字与域服务中的方法名字一样。在客户端,我们调用一个方法,直到SubmitChanges被调用时,这个方法才会被实际执行。EntityContainer会将所有对自定义方法的调用作为行将发生的更改来跟踪。当调用SubmitChanges时,方法是异步处理的。
在客户端项目中对实体也会生成同样的自定义方法,这些实体在自定义方法中做为参数传递。因此,可以通过一个域上下文的实例或实体的实例来调用定制的方法。如果打开生成的代码文件,会注意到域上下文中生成的方法只是简单的调用实体内的生成方法。在这两种情况下,我们还是需要调用Submitchanges方法来执行这些方法。
下面的示例演示如何调用一个名为ResetPassword的自定义方法,OnSubmitCompleted是我们想实施处理数据操作结果的回调函数。

selectedCustomer.ResetPassword();
customerContext.SubmitChanges(OnSubmitCompleted, null);

Invoke Operations 调用操作
对域服务上的每个服务操作,域上下文都包含一个方法。与域操作不同,服务操作都会立即执行。不用调用SubmitChanges方法,服务操作会异步执行。服务操作返回一个InvokeOperation对象。我们检索Value属性的值来得到从服务操作中返回的结果。
下面的示例演示了如果调用一个名为GetLocalTemperature的服务操作。

InvokeOperation invokeOp = customerContext.GetLocalTemperature(selectedPostalCode, OnInvokeCompleted, null);

private void OnInvokeCompleted(InvokeOperation invOp)
{
if (invOp.HasError)
{
MessageBox.Show(string.Format("Method Failed: {0}", invOp.Error.Message));
invOp.MarkErrorAsHandled();
}
else
{
result = invokeOp.Value;
}
}

处理错误
当我们检索或修改数据时,我们必须决定如果处理在这些操作中可能出现的错误。当我们在域上下文上调用检索或修改数据的方法时,我们包含了指定处理错误的步骤的参数。当装载数据时,我们可以指定忽略错误。但在修改数据时,我们必须处理返回的异常。更多详情,请看Silverlight 客户端 5.

WCF RIA 服务 (二十一)-- Silverlight 客户端 2

客户端代码生成
当我们使用RIA Services连接中间层和表示层时,RIA Services为客户端项目生成了客户端代理类,这些类是以中间层公开的实体和操作为基础的。因为RIA Services生成了这些类,所以我们不必再复制这些中间层和表示层中的应用逻辑。因为我们对中间层所做的任何修改,在重新生成客户端项目时都会自动与表示层同步的。

生成的代码位于客户端项目的Generated_Code文件夹内。想要看到这个文件夹,必须在客户端项目的解决方案资源管理器的窗口下,选择“显示所有文件”。我们不应直接改动此文件夹内的类,因为在客户端项目重新生成时,文件会被覆盖。然而,我们可以打开生成的文件,查看里面可被客户端项目利用的代码。


生成客户端代码的算法遵循以下规则:

1. 分析所有的程序集,包括生成的,或中间层为Domain service classes,Entity classes,shared code所引用的。

2. 对每个带有EnableClientAccessAttribute属性注释的域服务,都生成一个从DomainContext类派生的类。

3. 对每个域服务类中的查询方法、命名的更新方法或调用的操作,都在域上下文类中生成一个方法。

4. 对每个域服务公开的实体类,生成一个实体代理类。

5. 拷贝标记为共享的代码到客户端项目。

下图显示了一个中间层项目生成的客户端代码


DomainService 和 DomainContext

为每个域服务生成的派生于Domaincontext的类,遵循下面的规则:

1. Domain Context类须与Domain service具有一样的命名空间。

2. domain context类包含3个构造函数:

a. 默认的构造函数,嵌入了必要的URI,使用WebDomainClient(TContract)类在http上与域服务沟通

b. 允许用户指定一个可交替的URI的构造函数。

c. 允许用户提供一个自定义实现的DomainClient的构造函数。

3. 对每个与服务类中的查询方法,在domain context中都会生成一个EntityQuery(TEntity)方法。

4. 对每个命名的更新方法(named update method)或调用的操作,在域上下文中都会生成对应的方法。

5. 在domain service中执行插入、更新、删除的公共方法,会使域上下文中的EntityContainer在生成时带有EntitySetOperations标记,这个标记指定哪个操作在客户端是允许的。

实体类和实体代理类

生成实体代理类时,应遵循如下规则:

1. 代理类应该与中间层中的实体类具有一样的名字和命名空间。

2. 实体类中所有公共成员属性都会在代理类中生成,除非某些类型或成员属性已经在客户端项目中存在。

3. 每个属性设置器都包含执行验证和通知客户属性正在改变和已经改变的代码。

4. 元数据属性是与生成的代码中的实体类连在一起的。客户端不会存在元数据类。

5. 如果可能,定制的属性会传递到代理类。详情参阅下面的内容

定制的属性

如果定制的属性在客户端项目的编译中没有出现错误,那么这个定制的属性就可以传递到代理类中。为了保证属性能被传递,应满足如下条件:

1. 在客户端提供定制的属性的类型。

2. 在定制属性中指定的所有类型,在客户端都应该提供。

3. 这个定制属性必须对它所有的成员属性公开公共设置器,或者公开一个可以设置那些没有公共设置器的成员属性的构造函数。

如果所需的定制属性没有传递到客户端,我们可能需要在客户端中添加一个程序集引用。

共享代码

当在中间层和表示层之间共享代码时,代码被原封不动的拷到客户端项目中。通过命名文件为*.shared.cs来共享文件。

避免成员重复

当生成实体代理类时,有可能在客户端项目中已经定义了相同的类型和成员。我们可能已经在共享代码或只存在于客户端项目中的代码中定义了成员。RIA Service会在生成代理类之前检查已经存在的成员。所有已经存在的成员都不在代理类中生成。

WCF RIA 服务 (二十)-- Silverlight 客户端

Silverlight客户端
使用WCF RIA Services,我们可以创建一个当数据交互时知道中间层应用逻辑的Silverlight客户端。还可以对可见的和可编辑的数据提供用户接口,来在提交数据修改之前应用验证规则。我们的SL控件将会使用从中间层代码自动生成类。次章节介绍SL客户端如何使用domain context, 如何与数据一起工作,以及如何自定义生成的代码.
使用DomainContext
在中间层项目中会对每个domain service都生成一个DomainContext类来公开实体对象。在域上下文中包含着查询和修改等方法,这些方法与在域服务中对应的域操作进行沟通。当我们在SL应用程序中调用域上下文类上的一个查询方法时,这个查询方法会调用返回所需数据的域服务上的对应方法。这些域上下文上的方法都是异步执行的,所以在装载数据的时候,用户界面不会被锁定。
呈现和修改数据
我们使用SL控件,例如DataGrid控件,来呈现通过域上下文检索到的数据。我们把控件和查询结果绑定在一起。
我们也可以通过SL控件更新、插入和修改数据,当这些操作在域服务中公开时。当我们调用域服务上的数据修改操作时,中间层逻辑会处理来自SL客户端的数据,以确保商业规则应用到了修改操作上。
我们还可以使用DomainDataSource控件与来自域服务的数据交互。DomainDataSource控件允许我们使用声明语法来指定分页、排序、分组、和过滤数据。
自定义生成的代码
要想自定义生成的代码,我们不应该修改Generated_Code文件夹下的文件。因为当客户端项目重新生成时,这些文件将会被覆盖。反之,通过提供在域上下文中的局部方法和实体代理类,RIA Services允许我们为客户端自定义生成的代码。通过这些局部方法,我们可以在客户端添加计算性能,或添加当特定动作执行时需要的自定义逻辑。只有在我们已经实施了局部方法的情况下,生成的局部方法在会在运行时被调用。

2010年2月4日星期四

WCF RIA 服务 (十九)-- 共享代码 2

如何:通过源文件来共享代码

我们可以通过源文件来在中间层和表示层之间共享代码。当在中间层修改源文件,并重新生成应用程序时,在客户端会自动同步更新了的代码。当代码不属于domain Service或实体类,而且我们不想代码经历客户端代码生成步骤时,我们把代码放在共享的源文件中。

通过使用共享命名规范或文件链接来共享源文件。下面示例使用这两种方法来共享源文件。

使用共享命名规范


  1. 在中间层的解决方案资源管理器中,创建想放置共享文件的目录结构。这个目录结构将会在客户端的Generated_Code文件夹内重新创建。

  2. 在目录结构中添加一个C#文件。

  3. 使用共享命名规范。

  4. 在共享文件中添加代码。

  5. 生成解决方案。

  6. 在客户端项目中,打开Generated_Code文件夹,注意到代码文件已经被拷贝了。



  7. 打开代码文件,注意到在编译期间没有对代码做改动。

使用文件链接



  1. 在中间层项目中,添加一个新的代码文件。

  2. 在文件中添加代码。

  3. 在表示层项目中,右键点击项目。选择添加->已存在项

  4. 选择刚添加到中间层项目中的代码文件。

  5. 在添加按钮上,点击下拉菜单并选择Add As Link。

当编译这个项目时,会返现没有文件被拷贝到客户端项目中。相反,添加了一个文件引用。

WCF RIA 服务 (十八)-- 共享代码 1

共享代码
WCF RIA Services允许我们编写在中间层和表示层之间共享的应用逻辑。可以共享源文件或程序集中的代码。和在客户端代码生成主题中描述的自动代码生成方法不同,共享代码在编译时时不能改动的。相反,代码在层间是逐字地拷贝或共享的。

共享源文件

可以在中间层添加源文件,然后明确指定这些文件在表示层中共享。有两种方法来在层之间共享源文件。第一种方式是根据命名规范来命名源文件:*.shared.cs或*.shared.vb。第二种方法是使用VS中的文件链接特性。

共享命名规范

当对源文件使用共享命名规范(*.shared.cs)时,对共享的源代码文件实施了"push"模型。在编译时,源文件将会从中间层拷贝到客户端。只有在服务端和客户端存在RIA Services 连接时,共享命名规范才会对源文件起作用。



共享命名规范有以下优点

1.内置的支持:不需要程序员来保证共享文件同步。

2.透明性:名字清楚指明了文件想要共享。

3.自我维护:如果添加了一个共享文件,当解决方案编译时,所有与这个中间层联系的客户端项目都会自动更新。

4.易理解的调试经验:可以在服务端或客户端的文件中设置断点。

但它也有如下缺点:

1.新概念: 程序员必须了解共享命名规范。

2.文件被宝贝:共享文件是物理性地拷贝到项目中的,就可能造成程序员编辑了拷贝的文件,却在下次编译时丢失了所做的更改(因为源文件没有更改)。

链接文件

Linked files是VS中已有的特性,并不是只针对RIA Services的。在项目之间可以存在RIA Services链接,但不必使用链接文件。当使用链接文件方法时,就对共享源文件实施了"pull"模型。在客户端没有文件的拷贝。相反,客户端引用服务端的文件。



我们还可以让服务端和客户端都连接到其他项目中的文件。



链接文件方式有以下优点:

1. 已存在的VS特性:不必学习新的规范。

2. 文件不是拷贝的:文件仅存在服务端项目中。因此不可以修改拷贝的文件,在下次解决方案编译时也不会丢失更改。

链接文件方式的缺点:

1. 需要用户明确的指令 : 必须链接每个共享文件。

2. 不能自我维护 :当添加或移除共享文件时,客户端需要更新。

3. 缺乏透明性 : 程序员需要检查文件来确定哪个文件是共享的。

4. 不好的调试体验 : 不容易确定哪个层引起了中断。

共享程序集 - Shared Assemblies

与在项目中共享源文件相反,我们可以报代码编译到类库中,然后再通过程序集引用来共享这个类库。使用WCF RIA Services 类库来确保程序集是兼容的。

下面的图示例了一个使用WCF RIA Services类库的n层结构的应用程序如何共享代码。中间层和客户层对类库使用程序集引用。