一个 ContactLOB 业务应用程序示例 - 登录:Silverlight, MVVM, WCF RIA Services
一个 Silverlight LOB 应用程序教程。
引言
有人说:“Silverlight 已死”。 另一些人补充说:“Silverlight + 桌面 = WinRT”。 我在微软公布 Silverlight 计划之前很久就开始开发我的 Silverlight 项目了。我认为使用 Silverlight 的经验非常有价值,而且我确信它将在微软未来的技术中派上用场。
然而,在我开发 Silverlight 应用程序的过程中,我遇到了一些问题。我花了大量时间在网上搜索解决方案。为了节省您一些时间,我决定分享这些解决方案。
我并没有发明轮子。我在文章中看到的大部分代码都是在网上找到的。我只是把它们整理在一起。我会尽力提供指向原始代码来源的适当链接。如果我遗漏了什么,请告诉我,我会相应地更新我的文章。我提前为遗漏链接可能带来的任何不便道歉。
我将在我的文章中使用“问题-设计-解决方案”的方法。我认为它非常适合这篇文章,而且我希望它能让阅读更容易。
还有一件事:英语不是我的母语,所以我提前为任何语法和/或风格上的奇怪之处道歉。
那么,让我们开始吧。
必备组件
在 ContactLOB 项目中,我使用了以下技术:
- Visual Studio 2010
- Silverlight 5
- Prism 4.0
- WCF RIA Services
我想给您一个关于 Prism 4.0 的小提示,它是在 Silverlight 5 发布之前发布的 - 使用 Silverlight 5 引用重新编译 Prism 4.0 库。
问题
我遇到的一个问题是用户将如何登录您的应用程序。有很多方法可以做到这一点。
其中一种场景是在应用程序启动时显示登录页面,该页面在接收到用户凭据后会将用户导航到主页面。因此,如果用户运行您的应用程序,他们将看到如图 1 所示的登录页面。
图1。
成功登录后,您将把用户导航到如图 2 所示的主页面。
图2。
如果用户进入主页面,他们无法使用浏览器的后退按钮返回登录页面 - 后退按钮被禁用。
在 Silverlight 中,您必须提供一个且仅一个主页面作为应用程序的入口点。
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new LoginPage();
}
所以您需要以某种方式用主页面替换登录页面。
private void Application_Startup(object sender, StartupEventArgs e)
{
// this.RootVisual = new LoginPage();
this.RootVisual = new MainPage();
}
问题在于,您只能为 RootVisual 分配一次页面。如果您尝试为 RootVisual 分配多次,则什么也不会发生。为了方便参考,我们将此问题称为“导航问题”。
下一个问题是,您可能希望使用表单身份验证,如这个,来验证用户,但您不知道如何在服务器端使用您自己的数据库来验证用户。我们将此问题称为“身份验证问题”。
另一个问题是,您可能希望加密用户密码,这样它就不会以明文形式通过 Internet 发送。加密用户密码的最佳方法之一是使用 MD5 哈希。不幸的是,微软没有提供在 Silverlight 客户端上获取 MD5 哈希的库。我们将此问题称为“加密问题”。
设计
让我们一个一个地解决上述问题。
为了解决“导航问题”,我使用了 Prism 的区域。在 Prism 文档中,这被称为“基于视图的导航”。
为了解决“身份验证问题”,我使用了一个继承自 `AuthenticationBase` 类的类。该类有几个可以产生差异的重写函数。
为了解决“加密问题”,我使用了 MD5 哈希。这里的过程非常简单。您加密用户密码并将 MD5 哈希发送到 Web 服务器。您无需在服务器端解密用户密码。您只需要将加密后的密码与已知的 MD5 哈希进行比较。如果匹配,则可以授权用户。诀窍在于获取 MD5 哈希。
解决方案
我将引导您完成创建 Silverlight 应用程序并应用上述设计的过程。
- 启动 Visual Studio 2010,然后单击“新建项目...”
- 在“新建项目”窗体中,选择“Silverlight 应用程序”项目模板,并将项目名称键入为 ContactLOB。
- 在“新建 Silverlight 应用程序”窗体中,勾选“启用 WCF RIA Services”复选框,然后单击“确定”按钮。
- 构建项目。这样可以减少以后出现的问题。
- 将 Base、ViewModels 和 Views 文件夹添加到 ContactDB 项目。
- 添加 Prism 库引用。
- 转到 Mark Harris 的帖子,下载提供 MD5 哈希的代码。
- 将 `ViewModelBase.cs` 文件添加到 Base 文件夹。
- 将代码插入到文件中。
- 将 `LoginViewModel.cs` 文件添加到 ViewModels 项目文件夹,并插入代码。
- 将 `LoginView` 用户控件添加到 Views 文件夹,并插入以下代码。
- 将 `ShellView` 用户控件添加到 Views 文件夹,并插入以下代码。
- 将以下代码添加到 MainPage.xaml,以便当用户到达那里时可以显示用户信息。
- 将代码添加到 `MainPage.xaml.cs` 以显示用户信息。
- 将 `Bootstrapper.cs` 文件添加到 ContactLOB 项目,并插入代码。
- 打开 `App.xaml.cs` 文件并添加代码。
- 现在转到 ContactLOB.Web 项目并添加以下引用:
- System.ServiceModel.DomainServices.Server
- System.ServiceModel.DomainServices.Hosting
- System.Security。
- 将 Services 文件夹添加到 ContactLOB.Web 项目,并将 `AuthenticationDomainService.cs` 文件添加到该文件夹。将以下代码添加到文件中:
- 19. 要使这一切生效,您必须修改 `web.config` 文件。
- 按此顺序(先 ContactLOB.Web,再 ContactLOB)重新生成 ContactLOB.Web 项目,然后是 ContactLOB 项目。顺序在这里很重要。启动应用程序,输入用户名 `demo` 和密码 `demo`。如果输入正确,您将被重定向到 MainPage。
单击“确定”按钮。
将文件添加到项目中。
注意:您无需下载文章附加的 ContactLOB 项目的源代码 - 它已经在那里了。
using System.ComponentModel;
namespace ContactLOB.Base
{
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (null != PropertyChanged)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
MVVM 设计模式不是本文的目标。因此,`INotifyPropertyChanged` 接口的实现非常简单。
using System.ComponentModel;
using System.ServiceModel.DomainServices.Client.ApplicationServices;
using System.Text;
using System.Windows.Input;
using ContactLOB.Base;
using FlowGroup.Crypto;
using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.ServiceLocation;
using Microsoft.Practices.Unity;
namespace ContactLOB.ViewModels
{
public class LoginViewModel : ViewModelBase
{
private AuthenticationService authService;
public LoginViewModel()
{
LoginCommand = new DelegateCommand(ClickLogin);
if (!DesignerProperties.IsInDesignTool)
{
authService = WebContext.Current.Authentication;
}
}
public ICommand LoginCommand { get; private set; }
private string userName;
public string UserName
{
get
{
return userName;
}
set
{
userName = value;
OnPropertyChanged("UserName");
}
}
private string password;
public string Password
{
get
{
return password;
}
set
{
password = value;
OnPropertyChanged("Password");
}
}
internal void ClickLogin()
{
if (authService == null) return;
LoginParameters loginParams = new LoginParameters(UserName,
GetPasswordHash(Password));
var loginOperation = authService.Login(loginParams,
(loginOp) =>
{
if (loginOp.LoginSuccess)
{
GoToMainPage();
}
else if (loginOp.HasError)
{
loginOp.MarkErrorAsHandled();
}
},
null);
}
private string GetPasswordHash(string password)
{
UTF8Encoding encoder = new UTF8Encoding();
byte[] arr = encoder.GetBytes(password);
MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
byte[] md5arr = md5.ComputeHash(arr);
return BytesToHexString(md5arr);
}
private string BytesToHexString(byte[] value)
{
StringBuilder sb = new StringBuilder(value.Length * 2);
foreach (byte b in value)
{
sb.AppendFormat("{0:x2}", b);
}
return sb.ToString();
}
private void GoToMainPage()
{
IRegionManager regionManager =
ServiceLocator.Current.GetInstance<IRegionManager>();
if (regionManager == null) return;
IUnityContainer container =
ServiceLocator.Current.GetInstance<iunitycontainer>();
if (container == null) return;
IRegion mainRegion = regionManager.Regions["MainRegion"];
if (mainRegion == null) return;
// Check to see if we need to create an instance of the view.
MainPage view = mainRegion.GetView("MainPage") as MainPage;
if (view == null)
{
// Create a new instance of the MainPage using the Unity container.
view = container.Resolve<mainpage>();
// Add the view to the main region.
mainRegion.Add(view, "MainPage");
// Activate the view.
mainRegion.Activate(view);
}
else
{
// The view has already been added to the region so just activate it.
mainRegion.Activate(view);
}
}
}
}
重新生成项目以防万一。
上面代码的主要部分是 `GoToMainPage` 函数。逻辑非常直接。使用 Region Manager,我们检查 `MainPage` 视图是否已创建,如果没有,则创建一个并激活它。当我们激活 `MainPage` 视图时,用户将被导航到 `MainPage`。
<UserControl
x:Class="ContactLOB.Views.LoginView"
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:vm="clr-namespace:ContactLOB.ViewModels"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.DataContext>
<vm:LoginViewModel />
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="1" Grid.Column="1">
<Border BorderThickness="2" CornerRadius="4" BorderBrush="Black">
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="10" />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="10"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="User Name" Grid.Row="1" VerticalAlignment="Center" />
<TextBox FontSize="12" Margin="8" Grid.Column="1" Width="150" Grid.Row="1"
Text="{Binding Path=UserName, Mode=TwoWay, NotifyOnValidationError=True,
TargetNullValue=''}"
VerticalAlignment="Center"/>
<TextBlock Text="Password" Grid.Row="2" VerticalAlignment="Center" />
<PasswordBox FontSize="12" Margin="8" Grid.Column="1" Width="150" Grid.Row="2"
Password="{Binding Path=Password, Mode=TwoWay, NotifyOnValidationError=True, TargetNullValue=''}"
VerticalAlignment="Center"/>
<Button Content="Login" Grid.Column="1" Grid.Row="4" HorizontalAlignment="Left" Margin="8"
Command="{Binding Path=LoginCommand}" Width="80" />
</Grid>
</Border>
</StackPanel>
</Grid>
</UserControl>
<usercontrol
x:class="ContactLOB.Views.ShellView"
d:designwidth="400"
d:designheight="300"
mc:ignorable="d"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:prism="http://www.codeplex.com/prism">
<contentcontrol horizontalcontentalignment="Stretch"
verticalcontentalignment="Stretch"
prism:regionmanager.regionname="MainRegion" x:name="MainRegion">
</contentcontrol>
</usercontrol>
<grid>
<textblock x:name="txtWelcome" text="This is the Main Page">
</textblock>
</grid>
using System.Windows.Controls;
using ContactLOB.Web.Services;
namespace ContactLOB
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
this.Loaded += (s, e) =>
{
WebUser usr = WebContext.Current.User;
this.txtWelcome.Text = string.Format("Welcome {0} {1}",
usr.FirstName, usr.LastName);
};
}
}
}
using System.Windows;
using ContactLOB.Views;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.Prism.UnityExtensions;
using Microsoft.Practices.Unity;
namespace ContactLOB
{
public class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
// Use the container to create an instance of the shell.
ShellView view = this.Container.TryResolve<ShellView>();
// Set it as the root visual for the application.
Application.Current.RootVisual = view;
return view;
}
protected override void InitializeShell()
{
base.InitializeShell();
IRegionManager regionManager = RegionManager.GetRegionManager(Shell);
if (regionManager == null) return;
// Create a new instance of the LoginView using the Unity container.
var view = this.Container.Resolve<loginview>();
if (view == null) return;
// Add the view to the main region.
regionManager.Regions["MainRegion"].Add(view, "LoginView");
}
}
}
public App()
{
this.Startup += this.Application_Startup;
this.Exit += this.Application_Exit;
this.UnhandledException += this.Application_UnhandledException;
InitializeComponent();
// Create a WebContext and add it to the ApplicationLifetimeObjects
// collection. This will then be available as WebContext.Current.
WebContext webContext = new WebContext();
webContext.Authentication = new FormsAuthentication()
{ DomainContext = new AuthenticationDomainContext() };
this.ApplicationLifetimeObjects.Add(webContext);
}
private void Application_Startup(object sender, StartupEventArgs e)
{
new Bootstrapper().Run();
}
不用担心 `AuthenticationDomainContext` 处的代码行。完成应用程序的服务器部分后,它将得到修复。
using System.Security.Principal;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server.ApplicationServices;
namespace ContactLOB.Web.Services
{
/// <summary>
/// RIA Services DomainService responsible for authenticating users when
/// they try to log on to the application.
///
/// Most of the functionality is already provided by the base class
/// AuthenticationBase
///
[EnableClientAccess]
public class AuthenticationDomainService : AuthenticationBase<webuser>
{
protected override WebUser GetAuthenticatedUser(IPrincipal principal)
{
// TODO: Add code to retreive user info from database
WebUser user = new WebUser();
user.Name = principal.Identity.Name;
user.FirstName = "Bill";
user.LastName = "Gates";
return user;
}
protected override bool ValidateUser(string userName, string password)
{
// TODO: Add code to check user credentials against database
string usrName = "demo";
string pswHash = "fe01ce2a7fbac8fafaed7c982a04e229";
return (usrName.Equals(userName) && pswHash.Equals(password));
}
}
public class WebUser : UserBase
{
public string UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool IsAdmin { get; set; }
}
}
您可以在此处找到 `AuthenticationBase` 用法的更全面的示例。
我包含了 `WebUser` 类,以演示如何将一些用户信息从服务器传递到客户端。`WebUser` 类提供的用户信息用于主页面(参见第 14 项)。
<?xml version="1.0"?>
<!--
For more information on how to configure your ASP.NET application, please visit
http://go.microsoft.com/fwlink/?LinkId=169433
-->
<configuration>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<add name="DomainServiceModule" preCondition="managedHandler"
type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule,
System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</modules>
<validation validateIntegratedModeConfiguration="false"/>
</system.webServer>
<system.web>
<compilation debug="true" targetFramework="4.0" />
<httpModules>
<add name="DomainServiceModule"
type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule,
System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</httpModules>
<authentication mode="Forms"> <forms name=".ContactLOB_ASPXAUTH" /> </authentication>
</system.web>
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
</configuration>
请注意 `web.config` 中的 `authentication` 标签。它表明我们正在使用表单身份验证。
摘要
现在您知道如何在业务应用程序中提供用户登录、如何使用 MD5 哈希加密用户密码、如何使用 Prism Region Manager 从 LoginPage 导航到 MainPage,以及如何使用表单身份验证来验证用户。问题已解决。