演练:使用客户端应用程序服务

本主题介绍如何创建一个 Windows 应用程序,该应用程序使用客户端应用程序服务来验证用户身份并检索用户角色和设置。

在本演练中,您将执行下列任务:

  • 创建一个 Windows 窗体应用程序,并使用 Visual Studio 项目设计器来启用和配置客户端应用程序服务。

  • 创建一个简单的 ASP.NET Web 服务应用程序来承载应用程序服务,并测试客户端配置。

  • 向应用程序添加 Forms 身份验证。 首先将使用硬编码的用户名和密码来测试服务。 然后添加一个登录窗体,并在应用程序配置中将其指定为一个凭据提供程序。

  • 添加基于角色的功能,从而启用并显示一个仅供属于“manager”角色的用户使用的按钮。

  • 访问 Web 设置。 首先,在项目设计器的**“设置”**页上为已通过身份验证的(测试)用户加载 Web 设置。 然后,使用 Windows 窗体设计器将一个文本框绑定到 Web 设置。 最后,将已修改的值保存回服务器。

  • 实现注销。 将向窗体添加一个注销选项并调用注销方法。

  • 启用脱机模式。 将提供一个复选框以便用户可以指定其连接状态。 然后将使用此值来指定客户端应用程序服务提供程序是否将使用本地缓存数据(而不是访问相应的 Web 服务)。 最后,当应用程序返回到联机模式时,将重新验证当前用户的身份。

系统必备

您需要以下组件来完成本演练:

  • Visual Studio 2008.

创建客户端应用程序

首先要完成的任务是创建一个 Windows 窗体项目。 由于更多的人熟悉 Windows 窗体,因此本演练将使用 Windows 窗体,但是 Windows Presentation Foundation (WPF) 项目的过程是类似的。

创建客户端应用程序并启用客户端应用程序服务

  1. 在 Visual Studio 中,选择**“文件”|“新建”|“项目”**菜单选项。

  2. 在**“新建项目”对话框中,在“项目类型”窗格中展开“Visual Basic”“Visual C#”节点,然后选择“Windows”**项目类型。

  3. 确保选择**“.NET Framework 3.5”,然后选择“Windows 窗体应用程序”**模板。

  4. 将项目的**“名称”更改为 ClientAppServicesDemo,然后单击“确定”**。

    将在 Visual Studio 中打开一个新的 Windows 窗体项目。

  5. 在**“项目”菜单上选择“ClientAppServicesDemo 属性”**。

    将显示项目设计器。

  6. 在**“服务”选项卡上,选择“启用客户端应用程序服务”**。

  7. 确保选择**“使用 Forms 身份验证”,然后将“身份验证服务位置”“角色服务位置”“Web 设置服务位置”**均设置为 https://localhost:55555/AppServices。

  8. 对于 Visual Basic,在**“应用程序”选项卡上,将“身份验证模式”设置为“由应用程序定义”**。

设计器将指定的设置存储在应用程序的 app.config 文件中。

此时,已将应用程序配置为访问全部来自相同主机的三个服务。 在下一节中,您将创建一个主机作为简单的 Web 服务应用程序,从而使您可以测试客户端配置。

创建应用程序服务主机

在本节中,将创建一个简单的 Web 服务应用程序,该应用程序从本地 SQL Server Compact 3.5 数据库文件访问用户数据。 然后,将使用 ASP.NET 网站管理工具填充该数据库。 通过这种简单配置,可以快速测试客户端应用程序。 作为替代方法,可以将 Web 服务主机配置为从完整 SQL Server 数据库或通过自定义 MembershipProviderRoleProvider 类来访问用户数据。 有关更多信息,请参见为 SQL Server 创建和配置应用程序服务数据库

在下面的过程中,创建和配置 AppServices Web 服务。

创建和配置应用程序服务主机

  1. 在**“解决方案资源管理器”中选择 ClientAppServicesDemo 解决方案,然后在“文件”菜单上选择“添加”|“新建项目”**。

  2. 在**“添加新项目”对话框中,在“项目类型”窗格中展开“Visual Basic”“Visual C#”节点,然后选择“Web”**项目类型。

  3. 确保选择**“.NET Framework 3.5”,然后选择“ASP.NET Web 服务应用程序”**模板。

  4. 将项目的**“名称”更改为 AppServices,然后单击“确定”**。

    新的 ASP.NET Web 服务应用程序项目即添加到解决方案中,编辑器中会出现 Service1.asmx.vb 或 Service1.asmx.cs 文件。

    提示

    此示例中未使用 Service1.asmx.vb 或 Service1.asmx.cs 文件。 如果要保持工作环境整洁,可以关闭该文件,然后从“解决方案资源管理器”中将其删除。

  5. 在**“解决方案资源管理器”中,选择 AppServices 项目,然后在“项目”菜单上选择“AppServices 属性”**。

    将显示项目设计器。

  6. 在**“Web”选项卡上,确保选择“使用 Visual Studio 开发服务器”**。

  7. 选择**“特定端口”,指定值 55555,然后将“虚拟路径”**设置为 /AppServices。

  8. 保存所有的文件。

  9. 在**“解决方案资源管理器”**中,打开 Web.config 并查找 <system.web> 开始标记。

  10. 将以下标记添加到 <system.web> 标记之前。

    此标记中的 authenticationService、profileService 和 roleService 元素将启用和配置应用程序服务。 出于测试目的,authenticationService 元素的 requireSSL 特性设置为“false”。 profileService 元素的 readAccessProperties 和 writeAccessProperties 特性指示 WebSettingsTestText 属性是读取/写入属性。

    提示

    在成品代码中,应始终通过安全套接字层(SSL,通过使用 HTTPS 协议)来访问身份验证服务。 有关如何设置 SSL 的信息,请参见 Configuring Secure Sockets Layer (IIS 6.0 Operations Guide)(配置安全套接字层(IIS 6.0 操作指南))。

    <system.web.extensions>
      <scripting>
        <webServices>
          <authenticationService enabled="true" requireSSL = "false"/>
          <profileService enabled="true"
            readAccessProperties="WebSettingsTestText"
            writeAccessProperties="WebSettingsTestText" />
          <roleService enabled="true"/>
        </webServices>
      </scripting>
    </system.web.extensions>
    
  11. 将以下标记添加到 <system.web> 开始标记之后,使该标记位于 <system.web> 元素中。

    profile 元素配置一个名为 WebSettingsTestText 的 Web 设置。

    <profile enabled="true" >
      <properties>
        <add name="WebSettingsTestText" type="string" 
          readOnly="false" defaultValue="DefaultText" 
          serializeAs="String" allowAnonymous="false" />
      </properties>
    </profile>
    

在下面的过程中,使用 ASP.NET 网站管理工具完成服务配置并填充本地数据库文件。 您将添加名为 employee 和 manager 的两个用户,这两个用户属于两个同名角色。 用户密码分别为 employee! 和 manager!。

配置成员资格和角色

  1. 在**“解决方案资源管理器”中选择 AppServices 项目,然后在“项目”菜单上选择“ASP.NET 配置”**。

    出现**“ASP.NET 网站管理工具”**。

  2. 在**“安全”选项卡,单击“使用安全设置向导按部就班地配置安全性”**。

    **“安全设置向导”出现,其中显示“欢迎”**步骤。

  3. 单击**“下一步”**。

    **“选择访问方法”**步骤出现。

  4. 选择**“通过 Internet”**。 这样会将服务配置为使用窗体身份验证(而不是 Windows 身份验证)。

  5. 单击**“下一步”**两次。

    **“定义角色”**步骤出现。

  6. 选择**“为此网站启用角色”**。

  7. 单击**“下一步”**。 **“创建新角色”**窗体出现。

  8. 在**“新角色名称”文本框中,键入 manager,然后单击“添加角色”**。

    出现包含指定值的**“现有角色”**表。

  9. 在**“新角色名称”文本框中,使用 employee 替换 manager,然后单击“添加角色”**。

    新值即出现在**“现有角色”**表中。

  10. 单击**“下一步”**。

    出现**“添加新用户”**步骤。

  11. 在**“创建用户”**窗体中,指定下面的值。

    用户名

    manager

    密码

    manager!

    确认密码

    manager!

    电子邮件

    manager@contoso.com

    安全提示问题

    manager

    安全答案

    manager

  12. 单击**“创建用户”**。

    出现一条成功消息。

    提示

    “电子邮件”“安全提示问题”“安全答案”值是窗体所必需的,但未在此示例使用。

  13. 单击**“继续”**。

    出现**“创建用户”**窗体。

  14. 在**“创建用户”**窗体中,指定下面的值。

    用户名

    employee

    密码

    employee!

    确认密码

    employee!

    电子邮件

    employee@contoso.com

    安全提示问题

    Employee

    安全答案

    employee

  15. 单击**“创建用户”**。

    出现一条成功消息。

  16. 单击**“完成”**。

    再次出现**“网站管理工具”**。

  17. 单击**“Manager 用户”**。

    出现用户列表。

  18. 对**“employee”用户单击“编辑角色”,然后选择“employee”**角色。

  19. 对**“manager”用户单击“编辑角色”,然后选择“manager”**角色。

  20. 关闭承载**“网站管理工具”**的浏览器窗口。

  21. 如果出现一个消息框,询问是否重新加载修改后的 Web.config 文件,请单击**“是”**。

这样便完成了 Web 服务的设置。 此时,按 F5 可以运行客户端应用程序,并且**“ASP.NET 开发服务器”**将随客户端应用程序自动启动。 在您退出应用程序后,服务器仍将继续运行,但在您重新启动应用程序时也会重新启动。 这样做可以允许它检测您对 Web.config 所做的任何更改。

若要手动停止服务器,请在任务栏的通知区域中右击 ASP.NET Development Server 图标,然后单击**“停止”**。 有时,这对确保进行完全的重新启动很有用。

添加 Forms 身份验证

在下面的过程中,将向主窗体中添加代码以尝试验证用户,并在用户提供无效凭据的情况下拒绝访问。 使用硬编码的用户名和密码来测试服务。

使用应用程序代码验证用户

  1. 在**“解决方案资源管理器”**中的 ClientAppServicesDemo 项目中,添加一个对 System.Web 程序集的引用。

  2. 选择 Form1 文件,然后从 Visual Studio 主菜单中选择**“视图”|“代码”**。

  3. 在代码编辑器中,将以下语句添加到 Form1 文件的顶部。

    Imports System.Net
    Imports System.Threading
    Imports System.Web.ClientServices
    Imports System.Web.ClientServices.Providers
    Imports System.Web.Security
    
    using System.Net;
    using System.Threading;
    using System.Web.ClientServices;
    using System.Web.ClientServices.Providers;
    using System.Web.Security;
    
  4. 在**“解决方案资源管理器”**中,双击 Form1 以显示设计器。

  5. 在设计器中,双击窗体设计图面以生成一个名为 Form1_Load 的 Form.Load 事件处理程序。

    将显示代码编辑器,并且光标位于 Form1_Load 方法中。

  6. 将以下代码添加到 Form1_Load 方法中。

    此代码将通过退出应用程序的方式拒绝未经身份验证的用户的访问。 或者,可以允许未经身份验证的用户访问窗体,但拒绝访问特定的功能。 通常,您将不会像此处一样对用户名和密码进行硬编码,但这对于测试目的很有用。 在下一节中,您将使用可显示登录对话框并包含异常处理的更可靠的代码来替换此代码。

    请注意,static Membership.ValidateUser 方法包含在 .NET Framework 2.0 版中。 此方法将其工作委托给已配置的身份验证提供程序,并在身份验证成功时返回 true。 您的应用程序不需要直接引用客户端身份验证提供程序。

    If Not Membership.ValidateUser("manager", "manager!") Then
    
        MessageBox.Show("Unable to authenticate.", "Not logged in", _
            MessageBoxButtons.OK, MessageBoxIcon.Error)
        Application.Exit()
    
    End If
    
    if (!Membership.ValidateUser("manager", "manager!"))
    {
        MessageBox.Show("Unable to authenticate.", "Not logged in",
            MessageBoxButtons.OK, MessageBoxIcon.Error);
        Application.Exit();
    }
    

现在,可以按 F5 来运行应用程序,由于您提供了正确的用户名和密码,您将可以看到窗体。

提示

如果无法运行应用程序,请尝试停止 ASP.NET Development Server。 重新启动该服务器时,请验证端口是否设置为 55555。

若要改为显示错误消息,请更改 ValidateUser 参数。 例如,将第二个 "manager!" 参数替换为不正确的密码,例如 "MANAGER"。

添加登录窗体作为凭据提供程序

可以通过应用程序代码获取用户凭据,然后将这些凭据传递给 ValidateUser 方法。 但是,将用于获取凭据的代码与应用程序代码分隔开的做法通常很有用,这样可以在以后万一需要修改用于获取凭据的代码时提供便利。

在下面的过程中,您将配置应用程序以使用凭据提供程序,然后更改 ValidateUser 方法调用来为两个参数传递 Empty。 这些空字符串通知 ValidateUser 方法调用已配置的凭据提供程序的 GetCredentials 方法。

配置应用程序以使用凭据提供程序

  1. 在**“解决方案资源管理器”中选择 ClientAppServicesDemo 项目,然后在“项目”菜单上选择“ClientAppServicesDemo 属性”**。

    将显示项目设计器。

  2. 在**“服务”选项卡上,将“可选: 凭据提供程序”**设置为下面的值。 此值指示程序集限定的类型名称。

    ClientAppServicesDemo.Login, ClientAppServicesDemo
    
  3. 在 Form1 代码文件中,用下面的代码替换 Form1_Load 方法中的代码。

    此代码将显示一条欢迎消息,然后调用您将在下一步中添加的 ValidateUsingCredentialsProvider 方法。 如果用户未通过身份验证,则 ValidateUsingCredentialsProvider 方法将返回 false,并且 Form1_Load 方法将返回。 这样将阻止在退出应用程序之前运行任何其他的代码。 欢迎消息可用于清楚地指示应用程序重新启动的时间。 您将添加相关代码,以便在此演练的后面实现注销时,重新启动应用程序。

    MessageBox.Show("Welcome to the Client Application Services Demo.", _
        "Welcome!")
    
    If Not ValidateUsingCredentialsProvider() Then Return
    
    MessageBox.Show("Welcome to the Client Application Services Demo.",
        "Welcome!");
    
    if (!ValidateUsingCredentialsProvider()) return;
    
  4. 在 Form1_Load 方法的后面添加下面的方法。

    此方法将空字符串传递给 static Membership.ValidateUser 方法,这样将导致出现登录对话框。 如果身份验证服务不可用,则 ValidateUser 方法将引发 WebException。 在此情况下,ValidateUsingCredentialsProvider 方法将显示一条警告消息,询问用户是否在脱机模式下重试一次。 实现此功能需要如何:配置客户端应用程序服务中所述的**“将密码哈希保存在本地以实现脱机登录”**功能。 默认情况下将为新项目启用此功能。

    如果用户未通过验证,则 ValidateUsingCredentialsProvider 方法将会显示一条错误消息并退出应用程序。 最后,此方法将返回身份验证尝试的结果。

    Private Function ValidateUsingCredentialsProvider() As Boolean
    
        Dim isAuthorized As Boolean = False
    
        Try
    
            ' Call ValidateUser with empty strings in order to display the 
            ' login dialog box configured as a credentials provider.
            isAuthorized = Membership.ValidateUser( _
                String.Empty, String.Empty)
    
        Catch ex As System.Net.WebException
    
            If DialogResult.OK = MessageBox.Show( _
                "Unable to access the authentication service." & _
                Environment.NewLine & "Attempt login in offline mode?", _
                "Warning", MessageBoxButtons.OKCancel, _
                MessageBoxIcon.Warning) Then
    
                ConnectivityStatus.IsOffline = True
                isAuthorized = Membership.ValidateUser( _
                    String.Empty, String.Empty)
    
            End If
    
        End Try
    
        If Not isAuthorized Then
    
            MessageBox.Show("Unable to authenticate.", "Not logged in", _
                MessageBoxButtons.OK, MessageBoxIcon.Error)
            Application.Exit()
    
        End If
    
        Return isAuthorized
    
    End Function
    
    private bool ValidateUsingCredentialsProvider()
    {
        bool isAuthorized = false;
        try
        {
            // Call ValidateUser with empty strings in order to display the 
            // login dialog box configured as a credentials provider.
            isAuthorized = Membership.ValidateUser(
                String.Empty, String.Empty);
        }
        catch (System.Net.WebException)
        {
            if (DialogResult.OK == MessageBox.Show(
                "Unable to access the authentication service." +
                Environment.NewLine + "Attempt login in offline mode?",
                "Warning", MessageBoxButtons.OKCancel, 
                MessageBoxIcon.Warning))
            {
                ConnectivityStatus.IsOffline = true;
                isAuthorized = Membership.ValidateUser(
                    String.Empty, String.Empty);
            }
        }
    
        if (!isAuthorized)
        {
            MessageBox.Show("Unable to authenticate.", "Not logged in",
                MessageBoxButtons.OK, MessageBoxIcon.Error);
            Application.Exit();
        }
        return isAuthorized;
    }
    

创建登录窗体

凭据提供程序是一个用于实现 IClientFormsAuthenticationCredentialsProvider 接口的类。 此接口只有一个名为 GetCredentials 的方法,该方法返回 ClientFormsAuthenticationCredentials 对象。 下面的过程描述如何创建一个实现 GetCredentials 的登录对话框,以显示登录对话框自身并返回用户指定的凭据。

由于 Visual Basic 提供了**“登录窗体”**模板,因此针对 Visual Basic 和 C# 提供了不同的过程。 这样可节省时间并减少编码工作。

在 Visual Basic 中创建登录对话框作为凭据提供程序

  1. 在**“解决方案资源管理器”中选择 ClientAppServicesDemo 项目,然后在“项目”菜单上选择“添加新项”**。

  2. 在**“添加新项”对话框中选择“登录窗体”模板,将该项的“名称”更改为 Login.vb,然后单击“添加”**。

    将在 Windows 窗体设计器中显示登录对话框。

  3. 在设计器中,选择**“确定”按钮,然后在“属性”**窗口中将 DialogResult 设置为 OK。

  4. 在设计器中,将一个 CheckBox 控件添加到窗体的**“密码”**文本框下。

  5. 在**“属性”窗口中,将“(Name)”的值指定为 rememberMeCheckBox 并将“Text”**的值指定为 &Remember me。

  6. 从 Visual Studio 主菜单选择**“视图”|“代码”**。

  7. 在代码编辑器中,将下面的代码添加到文件的顶部。

    Imports System.Web.ClientServices.Providers
    
  8. 修改类的签名,以便该类可以实现 IClientFormsAuthenticationCredentialsProvider 接口。

    Public Class Login
        Implements IClientFormsAuthenticationCredentialsProvider
    
  9. 确保光标位于 IClientformsAuthenticationCredentialsProvider 之后,然后按 Enter 生成 GetCredentials 方法。

  10. 找到 GetCredentials 实现并用下面的代码替换它。

    Public Function GetCredentials() As  _
        ClientFormsAuthenticationCredentials Implements _
        IClientFormsAuthenticationCredentialsProvider.GetCredentials
    
        If Me.ShowDialog() = DialogResult.OK Then
            Return New ClientFormsAuthenticationCredentials( _
                UsernameTextBox.Text, PasswordTextBox.Text, _
                rememberMeCheckBox.Checked)
        Else
            Return Nothing
        End If
    
    End Function
    

下面的 C# 过程提供了一个简单登录对话框的完整代码清单。 此对话框的布局有点粗糙,但重要的部分是 GetCredentials 实现。

在 C# 中创建登录对话框作为凭据提供程序

  1. 在**“解决方案资源管理器”中选择 ClientAppServicesDemo 项目,然后在“项目”菜单上选择“添加类”**。

  2. 在**“添加新项”对话框中,将“名称”更改为 Login.cs,然后单击“添加”**。

    将在代码编辑器中打开 Login.cs 文件。

  3. 用下面的代码替换默认的代码。

    using System.Windows.Forms;
    using System.Web.ClientServices.Providers;
    
    namespace ClientAppServicesDemo
    {
        class Login : Form,
            IClientFormsAuthenticationCredentialsProvider
        {
            private TextBox usernameTextBox;
            private TextBox passwordTextBox;
            private CheckBox rememberMeCheckBox;
            private Button OK;
            private Button cancel;
            private Label label1;
            private Label label2;
    
            public ClientFormsAuthenticationCredentials GetCredentials()
            {
                if (this.ShowDialog() == DialogResult.OK)
                {
                    return new ClientFormsAuthenticationCredentials(
                        usernameTextBox.Text, passwordTextBox.Text,
                        rememberMeCheckBox.Checked);
                }
                else
                {
                    return null;
                }
            }
    
            public Login()
            {
                InitializeComponent();
            }
    
            private void CloseForm(object sender, System.EventArgs e)
            {
                this.Close();
            }
    
            private void InitializeComponent()
            {
                this.label1 = new System.Windows.Forms.Label();
                this.usernameTextBox = new System.Windows.Forms.TextBox();
                this.label2 = new System.Windows.Forms.Label();
                this.passwordTextBox = new System.Windows.Forms.TextBox();
                this.rememberMeCheckBox = new System.Windows.Forms.CheckBox();
                this.OK = new System.Windows.Forms.Button();
                this.cancel = new System.Windows.Forms.Button();
                this.SuspendLayout();
                // 
                // label1
                // 
                this.label1.AutoSize = true;
                this.label1.Location = new System.Drawing.Point(13, 13);
                this.label1.Name = "label1";
                this.label1.Size = new System.Drawing.Size(58, 13);
                this.label1.TabIndex = 0;
                this.label1.Text = "&User name";
                // 
                // usernameTextBox
                // 
                this.usernameTextBox.Location = new System.Drawing.Point(13, 30);
                this.usernameTextBox.Name = "usernameTextBox";
                this.usernameTextBox.Size = new System.Drawing.Size(157, 20);
                this.usernameTextBox.TabIndex = 1;
                // 
                // label2
                // 
                this.label2.AutoSize = true;
                this.label2.Location = new System.Drawing.Point(13, 57);
                this.label2.Name = "label2";
                this.label2.Size = new System.Drawing.Size(53, 13);
                this.label2.TabIndex = 2;
                this.label2.Text = "&Password";
                // 
                // passwordTextBox
                // 
                this.passwordTextBox.Location = new System.Drawing.Point(13, 74);
                this.passwordTextBox.Name = "passwordTextBox";
                this.passwordTextBox.PasswordChar = '*';
                this.passwordTextBox.Size = new System.Drawing.Size(157, 20);
                this.passwordTextBox.TabIndex = 3;
                // 
                // rememberMeCheckBox
                // 
                this.rememberMeCheckBox.AutoSize = true;
                this.rememberMeCheckBox.Location = new System.Drawing.Point(13, 101);
                this.rememberMeCheckBox.Name = "rememberMeCheckBox";
                this.rememberMeCheckBox.Size = new System.Drawing.Size(94, 17);
                this.rememberMeCheckBox.TabIndex = 4;
                this.rememberMeCheckBox.Text = "&Remember me";
                this.rememberMeCheckBox.UseVisualStyleBackColor = true;
                // 
                // OK
                // 
                this.OK.DialogResult = System.Windows.Forms.DialogResult.OK;
                this.OK.Location = new System.Drawing.Point(13, 125);
                this.OK.Name = "OK";
                this.OK.Size = new System.Drawing.Size(75, 23);
                this.OK.TabIndex = 5;
                this.OK.Text = "&OK";
                this.OK.UseVisualStyleBackColor = true;
                // 
                // cancel
                // 
                this.cancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
                this.cancel.Location = new System.Drawing.Point(95, 125);
                this.cancel.Name = "cancel";
                this.cancel.Size = new System.Drawing.Size(75, 23);
                this.cancel.TabIndex = 6;
                this.cancel.Text = "&Cancel";
                this.cancel.UseVisualStyleBackColor = true;
                // 
                // Login
                // 
                this.AcceptButton = this.OK;
                this.CancelButton = this.cancel;
                this.ClientSize = new System.Drawing.Size(187, 168);
                this.Controls.Add(this.cancel);
                this.Controls.Add(this.OK);
                this.Controls.Add(this.rememberMeCheckBox);
                this.Controls.Add(this.passwordTextBox);
                this.Controls.Add(this.label2);
                this.Controls.Add(this.usernameTextBox);
                this.Controls.Add(this.label1);
                this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
                this.MaximizeBox = false;
                this.MinimizeBox = false;
                this.Name = "Login";
                this.Text = "Login";
                this.ResumeLayout(false);
                this.PerformLayout();
    
            }
        }
    
    }
    

现在可以运行应用程序,并会看到出现登录对话框。 为了测试此代码,请尝试不同的凭据(有效的和无效的),并确认只有使用有效的凭据才可以访问窗体。 有效的用户名为 employee 和 manager,密码分别为 employee! 和 manager!。

提示

此时,请不要选择“Remember me”(记住我),否则在此演练的后面实现注销之前,您将无法作为另一个用户登录。

添加基于角色的功能

在下面的过程中,将向窗体中添加一个按钮并且仅对属于 manager 角色的用户显示。

基于用户角色更改用户界面

  1. 在**“解决方案资源管理器”的 ClientAppServicesDemo 项目中选择 Form1,然后从 Visual Studio 主菜单中选择“视图”|“设计器”**。

  2. 在设计器中,从**“工具箱”**向窗体中添加 Button 控件。

  3. 在**“属性”**窗口中设置按钮的下列属性。

    Property

    (名称)

    managerOnlyButton

    Text

    &Manager task

    Visible

    False

  4. 在 Form1 的代码编辑器中,将下面的代码添加到 Form1_Load 方法的末尾。

    此代码调用您将在下一步中添加的 DisplayButtonForManagerRole 方法。

    DisplayButtonForManagerRole()
    
    DisplayButtonForManagerRole();
    
  5. 将下面的方法添加到 Form1 类的末尾。

    此方法将调用由 static Thread.CurrentPrincipal 属性返回的 IPrincipalIsInRole 方法。 对于配置为使用客户端应用程序服务的应用程序,此属性返回 ClientRolePrincipal。 因为此类实现 IPrincipal 接口,所以您不需要显式引用它。

    如果用户属于“manager”角色,则 DisplayButtonForManagerRole 方法会将 managerOnlyButton 的 Visible 属性设置为 true。 如果引发 WebException,则此方法也会显示一条错误消息,用于指示角色服务不可用。

    提示

    如果用户登录已过期,则 IsInRole 方法将总是返回 false。 如果应用程序在身份验证之后不久调用过一次 IsInRole 方法,将不会发生这种情况,如此演练的代码示例中所示。 如果应用程序必须在其他时间检索用户角色,则您可能需要添加代码来重新验证登录已过期的用户。 如果所有有效用户均指定了角色,则您可以通过调用 ClientRoleProvider.GetRolesForUser 方法来确定登录是否已过期。 如果未返回任何角色,则说明登录已过期。 有关此功能的示例,请参见 GetRolesForUser 方法。 只有在应用程序配置中选定“每次服务器 Cookie 到期时要求用户重新登录”之后,此功能才是必需的。 有关更多信息,请参见如何:配置客户端应用程序服务

    Private Sub DisplayButtonForManagerRole()
    
        Try
    
            If Thread.CurrentPrincipal.IsInRole("manager") Then
    
                managerOnlyButton.Visible = True
    
            End If
    
        Catch ex As System.Net.WebException
    
            MessageBox.Show("Unable to access the roles service.", _
                "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
    
        End Try
    
    End Sub
    
    private void DisplayButtonForManagerRole()
    {
        try
        {
            if (Thread.CurrentPrincipal.IsInRole("manager"))
            {
                managerOnlyButton.Visible = true;
            }
        }
        catch (System.Net.WebException)
        {
            MessageBox.Show("Unable to access the role service.",
                "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
    }
    

如果身份验证成功,则客户端身份验证提供程序会将 Thread.CurrentPrincipal 属性设置为 ClientRolePrincipal 类的实例。 此类将实现 IsInRole 方法,以便将工作委托给已配置的角色提供程序。 与以前一样,您的应用程序代码不需要直接引用服务提供程序。

现在,可以运行应用程序。首先作为 employee 登录,这时可以看到按钮不会显示;然后作为 manager 登录,这时便可以看到按钮会显示出来。

访问 Web 设置

在下面的过程中,将向窗体中添加一个文本框并将其绑定到 Web 设置。 与前面使用身份验证和角色的代码一样,您的设置代码不会直接访问设置提供程序。 设置代码而是会使用由 Visual Studio 为您的项目生成的强类型 Settings 类(在 C# 中作为 Properties.Settings.Default 进行访问,而在 Visual Basic 中作为 My.Settings 进行访问)。

在用户界面中使用 Web 设置

  1. 通过检查任务栏的通知区域,确保**“ASP.NET Web Development Server”**仍然在运行。 如果您已经停止该服务器,请重新启动应用程序(这时会自动启动该服务器),然后关闭登录对话框。

  2. 在**“解决方案资源管理器”中选择 ClientAppServicesDemo 项目,然后在“项目”菜单上选择“ClientAppServicesDemo 属性”**。

    将显示项目设计器。

  3. 在**“设置”选项卡中,单击“加载 Web 设置”**。

    将显示**“登录”**对话框。

  4. 输入 employee 或 manager 的凭据,再单击**“登录”。 您将使用的 Web 设置已配置为仅供经过身份验证的用户访问,因此单击“跳过登录”**将不会加载任何设置。

    WebSettingsTestText 设置将显示在设计器中,并且其默认值为 DefaultText。 此外,将为您的项目生成一个包含 WebSettingsTestText 属性的 Settings 类。

  5. 在**“解决方案资源管理器”的 ClientAppServicesDemo 项目中选择 Form1,然后从 Visual Studio 主菜单中选择“视图”|“设计器”**。

  6. 在设计器中,向窗体中添加一个 TextBox 控件。

  7. 在**“属性”窗口中,将“(Name)”**的值指定为 webSettingsTestTextBox。

  8. 在代码编辑器中,将以下代码添加到 Form1_Load 方法的末尾。

    此代码调用您将在下一步中添加的 BindWebSettingsTestTextBox 方法。

    BindWebSettingsTestTextBox()
    
    BindWebSettingsTestTextBox();
    
  9. 将下面的方法添加到 Form1 类的末尾。

    此方法将 webSettingsTestTextBox 的 Text 属性绑定到在此过程的前面生成的 Settings 类的 WebSettingsTestText 属性。 如果引发 WebException,则此方法也会显示一条错误消息,用于指示 Web 设置服务不可用。

    Private Sub BindWebSettingsTestTextBox()
    
        Try
    
            Me.webSettingsTestTextBox.DataBindings.Add("Text", _
                My.Settings, "WebSettingsTestText")
    
        Catch ex As WebException
    
            MessageBox.Show("Unable to access the Web settings service.", _
                "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
    
        End Try
    
    End Sub
    
    private void BindWebSettingsTestTextBox()
    {
        try
        {
            this.webSettingsTestTextBox.DataBindings.Add("Text",
                Properties.Settings.Default, "WebSettingsTestText");
        }
        catch (WebException)
        {
            MessageBox.Show("Unable to access the Web settings service.",
                "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
    }
    

    提示

    通常,将使用数据绑定来启用控件和 Web 设置之间的自动双向通信。 但是,也可以直接访问 Web 设置,如下面的示例所示:

    webSettingsTestTextBox.Text = My.Settings.WebSettingsTestText
    
    webSettingsTestTextBox.Text =
        Properties.Settings.Default.WebSettingsTestText;
    
  10. 在设计器中选择窗体,然后在**“属性”窗口中单击“事件”**按钮。

  11. 选择 FormClosing 事件,然后按 Enter 以生成事件处理程序。

  12. 用下面的代码替换生成的方法。

    FormClosing 事件处理程序将调用 SaveSettings 方法。您将在下一节中添加的注销功能也将使用该方法。 首先,SaveSettings 方法确认用户尚未注销。 这是通过检查当前主体返回的 IIdentityAuthenticationType 属性来完成的。 当前主体是通过 static CurrentPrincipal 属性检索的。 如果已经针对客户端应用程序服务验证用户的身份,则身份验证类型将为“ClientForms”。 因为在注销后用户可能具有有效的 Windows 标识,所以 SaveSettings 方法不能只检查 IIdentity.IsAuthenticated 属性。

    如果用户尚未注销,则 SaveSettings 方法将调用在此过程的前面生成的 Settings 类的 Save 方法。 此方法在身份验证 Cookie 已过期时会引发 WebException。 只有在应用程序配置中选定**“每次服务器 Cookie 到期时要求用户重新登录”**之后,这才会发生。 有关更多信息,请参见如何:配置客户端应用程序服务。 SaveSettings 方法通过调用 ValidateUser 以显示登录对话框,从而处理 Cookie 过期。 如果用户登录成功,则 SaveSettings 方法将通过调用其自身来再次尝试保存设置。

    与前面的代码一样,SaveSettings 方法也会在远程服务不可用时显示一条错误消息。 如果设置提供程序无法访问远程服务,则这些设置仍将保存在本地缓存中,并在应用程序重新启动时重新加载。

    Private Sub Form1_FormClosing(ByVal sender As Object, _
        ByVal e As FormClosingEventArgs) Handles Me.FormClosing
    
        SaveSettings()
    
    End Sub
    
    Private Sub SaveSettings()
    
        ' Return without saving if the authentication type is not
        ' "ClientForms". This indicates that the user is logged out.
        If Not Thread.CurrentPrincipal.Identity.AuthenticationType _
            .Equals("ClientForms") Then Return
    
        Try
    
            My.Settings.Save()
    
        Catch ex As WebException
    
            If ex.Message.Contains("You must log on to call this method.") Then
    
                MessageBox.Show( _
                    "Your login has expired. Please log in again to save " & _
                    "your settings.", "Attempting to save settings...")
    
                Dim isAuthorized As Boolean = False
    
                Try
    
                    ' Call ValidateUser with empty strings in order to 
                    ' display the login dialog box configured as a 
                    ' credentials provider.
                    If Not Membership.ValidateUser( _
                        String.Empty, String.Empty) Then
    
                        MessageBox.Show("Unable to authenticate. " & _
                            "Settings were not saved on the remote service.", _
                            "Not logged in", MessageBoxButtons.OK, _
                            MessageBoxIcon.Error)
    
                    Else
    
                        ' Try again.
                        SaveSettings()
    
                    End If
    
                Catch ex2 As System.Net.WebException
    
                    MessageBox.Show( _
                        "Unable to access the authentication service. " & _
                        "Settings were not saved on the remote service.", _
                        "Not logged in", MessageBoxButtons.OK, _
                        MessageBoxIcon.Warning)
    
                End Try
    
            Else
    
                MessageBox.Show("Unable to access the Web settings service. " & _
                    "Settings were not saved on the remote service.", _
                    "Not logged in", MessageBoxButtons.OK, _
                    MessageBoxIcon.Warning)
    
            End If
    
        End Try
    
    End Sub
    
    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        SaveSettings();
    }
    
    private void SaveSettings()
    {
        // Return without saving if the authentication type is not
        // "ClientForms". This indicates that the user is logged out.
        if (!Thread.CurrentPrincipal.Identity.AuthenticationType
          .Equals("ClientForms")) return;
    
        try
        {
            Properties.Settings.Default.Save();
        }
        catch (WebException ex)
        {
            if (ex.Message.Contains("You must log on to call this method."))
            {
                MessageBox.Show(
                    "Your login has expired. Please log in again to save " +
                    "your settings.", "Attempting to save settings...");
    
                try
                {
                    // Call ValidateUser with empty strings in order to 
                    // display the login dialog box configured as a 
                    // credentials provider.
                    if (!Membership.ValidateUser(String.Empty, String.Empty))
                    {
                        MessageBox.Show("Unable to authenticate. " +
                            "Settings were not saved on the remote service.",
                            "Not logged in", MessageBoxButtons.OK, 
                            MessageBoxIcon.Error);
                    }
                    else
                    {
                        // Try again.
                        SaveSettings();
                    }
                }
                catch (System.Net.WebException)
                {
                    MessageBox.Show(
                        "Unable to access the authentication service. " +
                        "Settings were not saved on the remote service.",
                        "Not logged in", MessageBoxButtons.OK, 
                        MessageBoxIcon.Warning);
                }
            }
            else
            {
                MessageBox.Show("Unable to access the Web settings service. " +
                    "Settings were not saved on the remote service.", 
                    "Not logged in", MessageBoxButtons.OK, 
                    MessageBoxIcon.Warning);
            }
        }
    }
    
  13. 将下面的方法添加到 Form1 类的末尾。

    此代码将处理 ClientSettingsProvider.SettingsSaved 事件,并在未能保存任何设置时显示警告。 如果设置服务不可用或身份验证 Cookie 已过期,则不会发生 SettingsSaved 事件。 例如,当用户已经注销时,将会发生 SettingsSaved 事件。 通过在 Save 方法调用之前直接向 SaveSettings 方法添加注销代码,可以测试此事件处理程序。 下一节中描述了您可以使用的注销代码。

    Private WithEvents settingsProvider As ClientSettingsProvider = My.Settings _
        .Providers("System.Web.ClientServices.Providers.ClientSettingsProvider")
    
    Private Sub Form1_SettingsSaved(ByVal sender As Object, _
        ByVal e As SettingsSavedEventArgs) _
        Handles settingsProvider.SettingsSaved
    
        ' If any settings were not saved, display a list of them.
        If e.FailedSettingsList.Count > 0 Then
    
            Dim failedSettings As String = String.Join( _
                Environment.NewLine, e.FailedSettingsList.ToArray())
    
            Dim message As String = String.Format("{0}{1}{1}{2}", _
                "The following setting(s) were not saved:", _
                Environment.NewLine, failedSettings)
    
            MessageBox.Show(message, "Unable to save settings", _
                MessageBoxButtons.OK, MessageBoxIcon.Warning)
    
        End If
    
    End Sub
    
    private void Form1_SettingsSaved(object sender,
        SettingsSavedEventArgs e)
    {
        // If any settings were not saved, display a list of them.
        if (e.FailedSettingsList.Count > 0)
        {
            String failedSettings = String.Join(
                Environment.NewLine,
                e.FailedSettingsList.ToArray());
    
            String message = String.Format("{0}{1}{1}{2}",
                "The following setting(s) were not saved:",
                Environment.NewLine, failedSettings);
    
            MessageBox.Show(message, "Unable to save settings",
                MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
    }
    
  14. 对于 C#,将下面的代码添加到 Form1_Load 方法的末尾。 此代码会将您在最后一步添加的方法与 SettingsSaved 事件关联起来。

    ((ClientSettingsProvider)Properties.Settings.Default.Providers
        ["System.Web.ClientServices.Providers.ClientSettingsProvider"])
        .SettingsSaved += 
        new EventHandler<SettingsSavedEventArgs>(Form1_SettingsSaved);
    

此时,为了测试应用程序,请以 employee 和 manager 的身份多次运行应用程序,并在文本框中键入不同的值。 针对每个用户,这些值将在会话间保持。

实现注销

如果用户在登录时选中**“Remember me”(记住我)**复选框,则应用程序将在后续运行中自动对用户进行身份验证。 在应用程序处于脱机模式时或在身份验证 Cookie 过期之前,自动身份验证仍将继续。 但有时,多个用户会需要同时访问应用程序,或单个用户偶尔可能会使用不同的凭据进行登录。 为了支持这种情况,必须实现注销功能,如下面的过程所述。

实现注销功能

  1. 在 Form1 设计器中,从**“工具箱”**向窗体中添加一个 Button 控件。

  2. 在**“属性”窗口中,将“(名称)”的值指定为 logoutButton,并将“文本”**的值指定为“&Log Out”。

  3. 双击 logoutButton 以生成 Click 事件处理程序。

    将显示代码编辑器,并且光标位于 logoutButton_Click 方法中。

  4. 用下面的代码替换生成的 logoutButton_Click 方法。

    首先,此事件处理程序将调用您在上一节中添加的 SaveSettings 方法。 然后,此事件处理程序调用 ClientFormsAuthenticationMembershipProvider.Logout 方法。 如果身份验证服务不可用,则 Logout 方法将引发 WebException。 在此情况下,logoutButton_Click 方法会显示一条警告消息,并临时切换到脱机模式以注销用户。 下一节将介绍脱机模式。

    注销操作会删除本地身份验证 Cookie,这样在重新启动应用程序时将会需要登录。 注销后,事件处理程序将重新启动应用程序。 重新启动应用程序时,将会在登录对话框出现之前显示欢迎消息。 欢迎消息可用于清楚地指示应用程序已经重新启动。 这样可防止出现可能的混乱情况:用户必须登录以保存设置,然后又由于应用程序已重新启动而必须再次登录。

    Private Sub logoutButton_Click(ByVal sender As Object, _
        ByVal e As EventArgs) Handles logoutButton.Click
    
        SaveSettings()
    
        Dim authProvider As ClientFormsAuthenticationMembershipProvider = _
            CType(System.Web.Security.Membership.Provider,  _
            ClientFormsAuthenticationMembershipProvider)
    
        Try
    
            authProvider.Logout()
    
        Catch ex As WebException
    
            MessageBox.Show("Unable to access the authentication service." & _
                Environment.NewLine & "Logging off locally only.", _
                "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
            ConnectivityStatus.IsOffline = True
            authProvider.Logout()
            ConnectivityStatus.IsOffline = False
    
        End Try
    
        Application.Restart()
    
    End Sub
    
    private void logoutButton_Click(object sender, EventArgs e)
    {
        SaveSettings();
    
        ClientFormsAuthenticationMembershipProvider authProvider =
            (ClientFormsAuthenticationMembershipProvider)
            System.Web.Security.Membership.Provider;
    
        try
        {
            authProvider.Logout();
        }
        catch (WebException ex)
        {
            MessageBox.Show("Unable to access the authentication service." +
                Environment.NewLine + "Logging off locally only.",
                "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            ConnectivityStatus.IsOffline = true;
            authProvider.Logout();
            ConnectivityStatus.IsOffline = false;
        }
    
        Application.Restart();
    }
    

若要测试注销功能,请运行应用程序并在“登录”对话框中选择**“Remember me”(记住我)**。 接下来,关闭并重新启动应用程序以确认您不再需要登录。 最后,通过单击“注销”重新启动应用程序。

启用脱机模式

在下面的过程中,将向窗体中添加一个复选框,以便用户可以进入脱机模式。 应用程序通过将 static ConnectivityStatus.IsOffline 属性设置为 true 来指示脱机模式。 脱机状态存储在本地硬盘上由 Application.UserAppDataPath 属性指示的位置。 这意味着脱机状态将基于每个用户、每个应用程序进行存储。

在脱机模式中,所有的客户端应用程序服务请求将从本地缓存中检索数据,而不是尝试访问服务。 在默认配置中,本地数据包括加密形式的用户密码。 这样就可以让用户在应用程序处于脱机模式时登录。 有关更多信息,请参见如何:配置客户端应用程序服务

在应用程序中启用脱机模式

  1. 在**“解决方案资源管理器”的 ClientAppServicesDemo 项目中选择 Form1,然后从 Visual Studio 主菜单中选择“视图”|“设计器”**。

  2. 在设计器中,向窗体中添加一个 CheckBox 控件。

  3. 在**“属性”窗口中,将“(名称)”的值指定为 workOfflineCheckBox,并将“文本”**的值指定为“&Work offline”。

  4. 在**“属性”窗口中单击“事件”**按钮。

  5. 选择 CheckedChanged 事件,然后按 Enter 以生成事件处理程序。

  6. 用下面的代码替换生成的方法。

    此代码将更新 IsOffline 值,并在应用程序返回联机模式时以无提示方式重新验证用户。 ClientFormsIdentity.RevalidateUser 方法将使用缓存的凭据,因此用户不必显式登录。 如果身份验证服务不可用,则将会出现警告消息,并且应用程序会保持脱机状态。

    提示

    RevalidateUser 方法仅仅是出于方便而提供的。 因为该方法没有返回值,所以不能指示重新验证是否已失败。 例如,如果服务器上的用户凭据已更改,则重新验证将会失败。 在这种情况下,您可能需要包括在服务调用失败后显式验证用户的代码。 有关更多信息,请参见本演练前面的“访问 Web 设置”一节。

    重新验证之后,此代码将通过调用前面添加的 SaveSettings 方法,保存对本地 Web 设置所做的所有更改。 然后,此代码通过调用项目的 Settings 类(在 C# 作为 Properties.Settings.Default 进行访问,而在 Visual Basic 中作为 My.Settings 进行访问)的 Reload 方法,检索服务器上的任何新值。

    Private Sub workOfflineCheckBox_CheckedChanged( _
        ByVal sender As Object, ByVal e As EventArgs) _
        Handles workOfflineCheckBox.CheckedChanged
    
        ConnectivityStatus.IsOffline = workOfflineCheckBox.Checked
    
        If Not ConnectivityStatus.IsOffline Then
    
            Try
    
                ' Silently re-validate the user.
                CType(System.Threading.Thread.CurrentPrincipal.Identity,  _
                    ClientFormsIdentity).RevalidateUser()
    
                ' If any settings have been changed locally, save the new
                ' new values to the Web settings service.
                SaveSettings()
    
                ' If any settings have not been changed locally, check 
                ' the Web settings service for updates. 
                My.Settings.Reload()
    
            Catch ex As WebException
    
                MessageBox.Show( _
                    "Unable to access the authentication service. " & _
                    Environment.NewLine + "Staying in offline mode.", _
                    "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
                workOfflineCheckBox.Checked = True
    
            End Try
    
        End If
    
    End Sub
    
    private void workOfflineCheckBox_CheckedChanged(
        object sender, EventArgs e)
    {
        ConnectivityStatus.IsOffline = workOfflineCheckBox.Checked;
        if (!ConnectivityStatus.IsOffline)
        {
            try
            {
                // Silently re-validate the user.
                ((ClientFormsIdentity)
                    System.Threading.Thread.CurrentPrincipal.Identity)
                    .RevalidateUser();
    
                // If any settings have been changed locally, save the new
                // new values to the Web settings service.
                SaveSettings();
    
                // If any settings have not been changed locally, check 
                // the Web settings service for updates. 
                Properties.Settings.Default.Reload();
            }
            catch (WebException)
            {
                MessageBox.Show(
                    "Unable to access the authentication service. " +
                    Environment.NewLine + "Staying in offline mode.",
                    "Warning", MessageBoxButtons.OK,
                    MessageBoxIcon.Warning);
                workOfflineCheckBox.Checked = true;
            }
        }
    }
    
  7. 将下面的代码添加到 Form1_Load 方法的末尾,以确保复选框显示当前的连接状态。

    workOfflineCheckBox.Checked = ConnectivityStatus.IsOffline
    
    workOfflineCheckBox.Checked = ConnectivityStatus.IsOffline;
    

这将完成示例应用程序。 若要测试脱机功能,请运行应用程序,以 employee 或 manager 的身份登录,然后选择**“Work offline”(脱机工作)。 修改文本框中的值,然后关闭应用程序。 重新启动应用程序。 登录之前,请在任务栏的通知区域中右击 ASP.NET Development Server 图标,然后单击“停止”**。 然后,正常登录。 即使在该服务器未运行的情况下,您仍可以登录。 修改文本框的值,退出并重新启动以查看修改的值。

摘要

在本演练中,您学习了如何在 Windows 窗体应用程序中启用和使用客户端应用程序服务。 设置测试服务器之后,将代码添加到应用程序中,以对用户进行身份验证,并从服务器中检索用户角色和应用程序设置。 您还学习了如何启用脱机模式,以便应用程序在连接不可用时使用本地数据缓存而不使用远程服务。

后续步骤

在实际的应用程序中,将会为许多用户从一个可能并非始终可用或可能在没有任何通知的情况下停机的远程服务器访问数据。 若要使应用程序可靠运行,必须以适当的方式对服务不可用的情形做出响应。 本演练中包含一些 try/catch 块,用于捕获 WebException 并在服务不可用时显示错误消息。 在成品代码中,可能需要通过以下方式处理此情况:切换到脱机模式,退出应用程序或拒绝对特定功能的访问。

为了增加应用程序的安全性,请确保在部署之前全面测试应用程序和服务器。

请参见

任务

如何:配置客户端应用程序服务

演练:使用 ASP.NET 应用程序服务

概念

客户端应用程序服务概述

其他资源

客户端应用程序服务

ASP.NET 网站管理工具

为 SQL Server 创建和配置应用程序服务数据库