使用 MEF、MVVM 和 WCF RIA Services 的 Silverlight 4 应用程序示例 - 第 1 部分
本系列文章的第一部分,介绍如何使用 MEF、MVVM Light 和 WCF RIA Services 创建 Silverlight 业务应用程序。
- 下载源文件 - 612 KB
- 下载安装包 - 1.59 MB
- 请访问 CodePlex 获取最新版本和源代码
文章系列
本文是关于使用 MEF、MVVM Light 和 WCF RIA Services 开发 Silverlight 业务应用程序的系列文章的第一部分。
- 第 1 部分 - 简介、安装和通用应用程序设计主题
- 第 2 部分 - MVVM Light 主题
- 第 3 部分 - 自定义身份验证、密码重置和用户维护
目录
引言
此示例应用程序是我学习 Silverlight 和 WCF RIA Services 的主动成果。鉴于我过去几年使用 WPF 和 MVVM 的经验,我发现缺乏能够结合最新的 Silverlight 增强功能和 MVVM 的 LOB 应用程序示例。这篇三部分文章系列是我创建此类示例的努力。选择一个问题跟踪应用程序是受到 David Poll 在 PDC09 演讲中的启发,而设计架构则来自 Shawn Wildermuth 的 博客文章。
此问题跟踪应用程序的主要功能是:
- 登录屏幕提供基于安全问题和答案的自定义身份验证和密码重置。
- 我的个人资料屏幕用于更新用户信息、密码、安全问题和答案。
- 用户维护屏幕仅对管理员用户可用,允许管理员用户添加/删除/更新用户。
- 新问题屏幕用于创建新问题(错误、工作项、规范缺陷等)。
- 我的问题屏幕用于跟踪分配给用户的 <$>所有活动和已解决的问题。
- 所有问题屏幕用于跟踪 <$>所有问题(打开、活动、待定或已解决)。
- 错误报告屏幕提供 bug 趋势摘要、bug 计数以及打印摘要的功能。
- 提供四种不同的主题,并可随时动态应用。
要求
要构建此示例应用程序,您需要:
- Microsoft Visual Studio 2010 SP1
- Silverlight 4 Toolkit 2010 年 4 月(包含在示例解决方案中)
- MVVM Light Toolkit V3 SP1(包含在示例解决方案中)
安装
将安装包下载到本地磁盘的某个位置后,我们需要完成以下步骤:
1. 安装 IssueVision 示例数据库
要安装示例数据库,请运行安装包 zip 文件中包含的 SqlServer_IssueVision_Schema.sql 和 SqlServer_IssueVision_InitialDataLoad.sql。SqlServer_IssueVision_Schema.sql 用于创建数据库架构和数据库用户 IVUser
;SqlServer_IssueVision_InitialDataLoad.sql 用于加载运行此应用程序所需的所有数据,包括初始应用程序用户 ID user1 和管理员用户 ID admin1,所有密码都设置为 P@ssword1234。
2. 安装 Web 安装包
数据库设置完成后,请运行安装包 zip 文件中也包含的 setup.exe。这将安装 IssueVision for Silverlight 网站。
安装完网站后,我们可以按以下方式访问 Silverlight 应用程序:
架构
1. 解决方案结构
在示例解决方案文件中,项目进一步组织在 Client 文件夹或 Server 文件夹中。Client 文件夹包含所有将编译到 IssueVision.Client.xap 文件中的项目,而 Server 文件夹包含所有将在 Web 服务器环境中运行的项目。
对于 Server 文件夹中的项目:
- IssueVision.Web 项目是主要的启动项目。它包含启动页 Default.aspx 和 Silverlight 应用程序包 IssueVision.Client.xap。
- IssueVision.Data.Web 项目是服务器端数据访问层。它接收来自客户端的请求,通过数据库用户 IVUser 访问数据库,然后返回结果。该项目的主要组件包括
IssueVision
实体数据模型和所有相关的DomainService
类。
对于 Client 文件夹中的项目:
- IssueVision.Data 项目与
IssueVision.Data.Web
有 WCF RIA Services 链接,因此托管生成的客户端代理代码和共享源代码。该项目还包括所有仅需在客户端使用的部分类,无需在服务器端重复。 - IssueVision.Common 项目顾名思义,包含其他客户端项目之间共享的所有通用接口类和辅助类。
- IssueVision.Model 项目定义了 MVVM 的 Model,它包含以下三个模型类:
AuthenticationModel
PasswordResetModel
IssueVisionModel
- IssueVision.ViewModel 项目是 MVVM 的 ViewModel 部分,包含所有九个 ViewModel 类。
- IssueVison.Client 是主要的客户端项目,也是 MVVM 的 View,承载所有 UI 逻辑。
从上面的解决方案结构可以看出,MVVM 在 UI 和业务逻辑之间提供了良好的关注点分离,以使这些 UI 更易于开发人员和设计师维护。接下来,我们将更详细地介绍 Model
、ViewModel
和 View
类。
2. IssueVisionModel 类
我们将在第 3 部分讨论 AuthenticationModel
和 PasswordResetModel
类。现在,让我们专注于 IssueVisionModel
类,它是此应用程序的主要 Model(MVVM 中的 Model)。IssueVisionModel
基于以下接口 IIssueVisionModel
:
public interface IIssueVisionModel : INotifyPropertyChanged
{
void GetIssueTypesAsync();
event EventHandler<EntityResultsArgs<IssueType>> GetIssueTypesComplete;
void GetPlatformsAsync();
event EventHandler<EntityResultsArgs<Platform>> GetPlatformsComplete;
void GetResolutionsAsync();
event EventHandler<EntityResultsArgs<Resolution>> GetResolutionsComplete;
void GetStatusesAsync();
event EventHandler<EntityResultsArgs<Status>> GetStatusesComplete;
void GetSubStatusesAsync();
event EventHandler<EntityResultsArgs<SubStatus>> GetSubStatusesComplete;
void GetUsersAsync();
event EventHandler<EntityResultsArgs<User>> GetUsersComplete;
void GetCurrentUserAsync();
event EventHandler<EntityResultsArgs<User>> GetCurrentUserComplete;
void GetSecurityQuestionsAsync();
event EventHandler<EntityResultsArgs<SecurityQuestion>>
GetSecurityQuestionsComplete;
void GetMyIssuesAsync();
event EventHandler<EntityResultsArgs<Issue>> GetMyIssuesComplete;
void GetAllIssuesAsync();
event EventHandler<EntityResultsArgs<Issue>> GetAllIssuesComplete;
void GetAllUnresolvedIssuesAsync();
event EventHandler<EntityResultsArgs<Issue>> GetAllUnresolvedIssuesComplete;
void GetActiveBugCountByMonthAsync(Int32 numberOfMonth);
event EventHandler<InvokeOperationEventArgs> GetActiveBugCountByMonthComplete;
void GetResolvedBugCountByMonthAsync(Int32 numberOfMonth);
event EventHandler<InvokeOperationEventArgs> GetResolvedBugCountByMonthComplete;
void GetActiveBugCountByPriorityAsync();
event EventHandler<InvokeOperationEventArgs> GetActiveBugCountByPriorityComplete;
Issue AddNewIssue();
void RemoveAttribute(IssueVision.Data.Web.Attribute attribute);
void RemoveFile(IssueVision.Data.Web.File file);
User AddNewUser();
void RemoveUser(IssueVision.Data.Web.User user);
void SaveChangesAsync();
event EventHandler<SubmitOperationEventArgs> SaveChangesComplete;
void RejectChanges();
Boolean HasChanges { get; }
Boolean IsBusy { get; }
}
我们定义了一个单独的 Model
类,而不将数据上下文类本身用作 Model
,因为 Model
最适合表示一组属性和操作,用于检索、添加、删除和更新数据。这使得 Model
更易于维护和测试。此外,正如 Shawn 在他的博客中所述,“创建自定义 Model
允许我们隔离正在使用的传输层,以便我们可以更改它,甚至可以拥有多个数据提供程序来为我们的 Model
指定数据”。
接下来,让我们看看 IssueVisionModel
类中的检索方法是如何实现的。
public void GetIssueTypesAsync()
{
PerformQuery(Context.GetIssueTypesQuery(), GetIssueTypesComplete);
}
GetIssueTypeAsync()
调用 private
方法 PerformQuery()
,并将 EntityQuery GetIssueTypesQuery()
和事件 GetIssueTypesComplete
传递进去。检索完成后,事件 GetIssueTypesComplete
将触发并传递结果集,或者在出现问题时传递错误消息。实际上,几乎所有的检索方法都像调用下面定义的 PerformQuery()
方法一样简单。
private void PerformQuery<T>(EntityQuery<T> qry,
EventHandler<EntityResultsArgs<T>> evt) where T : Entity
{
Context.Load(qry, LoadBehavior.RefreshCurrent, r =>
{
if (evt != null)
{
try
{
if (r.HasError)
{
evt(this, new EntityResultsArgs<T>(r.Error));
r.MarkErrorAsHandled();
}
else
{
evt(this, new EntityResultsArgs<T>(r.Entities));
}
}
catch (Exception ex)
{
evt(this, new EntityResultsArgs<T>(ex));
}
}
}, null);
}
此外,Model
类使用 MEF 的 Export
属性将其自身导出到 ViewModel
类,如下所示:
[Export(typeof(IIssueVisionModel))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class IssueVisionModel : IIssueVisionModel
3. ViewModel 类
大多数 ViewModel
类包含六个区域:Private Data Members 区域、Constructor 区域、Public Properties 区域、Public Commands 区域、ICleanup Interface 区域和 Private Methods 区域。Public Properties 和 Public Commands 区域将所有必需的属性和命令暴露给其 View
类。构造函数则负责设置事件处理、为任何 private
数据设置初始值,以及在 ViewModel
类中注册所需的 AppMessages
。下面是一个示例:
[ImportingConstructor]
public IssueEditorViewModel(IIssueVisionModel issueVisionModel)
{
_issueVisionModel = issueVisionModel;
// set up event handling
_issueVisionModel.GetIssueTypesComplete += _issueVisionModel_GetIssueTypesComplete;
_issueVisionModel.GetPlatformsComplete += _issueVisionModel_GetPlatformsComplete;
_issueVisionModel.GetResolutionsComplete += _issueVisionModel_GetResolutionsComplete;
_issueVisionModel.GetStatusesComplete += _issueVisionModel_GetStatusesComplete;
_issueVisionModel.GetSubStatusesComplete += _issueVisionModel_GetSubStatusesComplete;
_issueVisionModel.GetUsersComplete += _issueVisionModel_GetUsersComplete;
// set _currentIssueCache to null
_currentIssueCache = null;
// load issue type entries
IssueTypeEntries = null;
_issueVisionModel.GetIssueTypesAsync();
// load platform entries
PlatformEntries = null;
_issueVisionModel.GetPlatformsAsync();
//load resolution entries
ResolutionEntriesWithNull = null;
_issueVisionModel.GetResolutionsAsync();
// load status entries
StatusEntries = null;
_issueVisionModel.GetStatusesAsync();
// load substatus entries
SubstatusEntriesWithNull = null;
_issueVisionModel.GetSubStatusesAsync();
// load user entries
UserEntries = null;
UserEntriesWithNull = null;
_issueVisionModel.GetUsersAsync();
// register for EditIssueMessage
AppMessages.EditIssueMessage.Register(this, OnEditIssueMessage);
}
从上面的代码可以看出,ViewModel
类通过使用 ImportingConstructor
属性获取实现 IIssueVisionModel
接口的对象,该属性告诉 MEF 将发现的模型类提供给 ViewModel
。反过来,所有 ViewModel
类都像下面这样导出自身:
[Export(ViewModelTypes.IssueEditorViewModel, typeof(ViewModelBase))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class IssueEditorViewModel : ViewModelBase
4. View 类和代码隐藏文件
在讨论任何 View
类之前,让我们先看看如何在 App.xaml.cs 文件中定义一个全局的 CompositionContainer
对象。
public partial class App : Application
{
// CompositionContainer for the whole application
public static CompositionContainer Container;
public App()
{
Startup += Application_Startup;
Exit += Application_Exit;
UnhandledException += Application_UnhandledException;
InitializeComponent();
}
private void Application_Startup(object sender, StartupEventArgs e)
{
Container = new CompositionContainer(new DeploymentCatalog());
CompositionHost.Initialize(Container);
CompositionInitializer.SatisfyImports(this);
RootVisual = new MainPage();
}
......
}
通过访问 static Container
对象,我们可以轻松地按如下方式请求一个新的 ViewModel
对象:
// Use MEF To load the View Model
_viewModelExport = App.Container.GetExport<ViewModelBase>(
ViewModelTypes.AllIssuesViewModel);
if (_viewModelExport != null) DataContext = _viewModelExport.Value;
并且,我们可以通过以下三行代码释放一个 ViewModel
对象:
// set DataContext to null and call ReleaseExport()
DataContext = null;
App.Container.ReleaseExport(_viewModelExport);
_viewModelExport = null;
每个 View
类通过调用 _viewModelExport = App.Container.GetExport()
来查找其 ViewModel
对象,然后在每个 View
类的构造函数中执行 DataContext = _viewModelExport.Value
。此函数指示 MEF 在运行时满足一系列依赖关系,从而创建所有必需的 Model
和 ViewModel
对象。使用 MEF 的好处在于我们可以使这些项目保持松耦合。事实上,IssueVision.Model、IssueVision.ViewModel 和 IssueVison.Client 项目不需要对其他两个项目进行引用即可成功编译。IssueVison.Client 项目仅对其他两个项目有引用,因为我们需要将它们添加到输出文件 IssueVision.Client.xap 中。
在同一个构造函数中,我们还注册了 View 类将处理的 AppMessage
。下面的 IssueEditor
类是一个很好的示例:
public partial class IssueEditor : UserControl, ICleanup
{
#region "Private Data Members"
private Lazy<ViewModelBase> _viewModelExport;
#endregion "Private Data Members"
#region "Constructor"
public IssueEditor()
{
InitializeComponent();
// register for ReadOnlyIssueMessage
AppMessages.ReadOnlyIssueMessage.Register(this, OnReadOnlyIssueMessage);
// register for OpenFileMessage
AppMessages.OpenFileMessage.Register(this, OnOpenFileMessage);
// register for SaveFileMessage
AppMessages.SaveFileMessage.Register(this, OnSaveFileMessage);
if (!ViewModelBase.IsInDesignModeStatic)
{
// Use MEF To load the View Model
_viewModelExport = App.Container.GetExport<ViewModelBase>(
ViewModelTypes.IssueEditorViewModel);
if (_viewModelExport != null) DataContext = _viewModelExport.Value;
}
}
#endregion "Constructor"
.........
}
在代码隐藏文件中,我们定义了所有与 UI 相关的逻辑,例如动态启用/禁用按钮的事件处理程序,或者在出现问题时显示错误消息的 AppMessage
。只要代码与 UI 逻辑相关,将其添加到代码隐藏文件中是完全可以的,如下所示:
private void userNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
// dynamically enable/disable error message
if (!string.IsNullOrWhiteSpace(loginScreenErrorMessageTextBox.Text))
loginScreenErrorMessageTextBox.Text = string.Empty;
// dynamically enable/disable login button
loginButton.IsEnabled =
!(string.IsNullOrWhiteSpace(userNameTextBox.Text) ||
string.IsNullOrWhiteSpace(passwordPasswordBox.Password));
}
private void passwordPasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
// dynamically enable/disable error message
if (!string.IsNullOrWhiteSpace(loginScreenErrorMessageTextBox.Text))
loginScreenErrorMessageTextBox.Text = string.Empty;
// dynamically enable/disable login button
loginButton.IsEnabled =
!(string.IsNullOrWhiteSpace(userNameTextBox.Text) ||
string.IsNullOrWhiteSpace(passwordPasswordBox.Password));
}
用于布局的自定义控件
IssueVision.Common 项目中定义的自定义控件 FlipControl
和 MainPageControl
用作屏幕布局的基础。
FlipControl
用于 LoginForm.xaml,它同时承载登录屏幕和密码重置屏幕。切换 IsFlipped
依赖属性将在这两个屏幕之间切换,动画在 VisualStateManager
中定义。
类似地,MainPageControl
用于 MainPage.xaml,并将整个屏幕分为标题内容、登录/注销菜单内容、登录页面内容和主页面内容。IsLoggedIn
依赖属性会在登录页面和主页面之间切换。
使用此布局,自定义控件可以被视为关注点分离的另一种应用。屏幕布局样式以及在 VisualStateManager
中定义的动画被封装起来。只要它们提供相同的功能,我们就可以轻松更改它们,例如创建新的动画,而不会影响 IssueVision.Client 项目中定义的任何 View
类。
动态主题
此应用程序定义了四种不同的主题:BureauBlue
、ExpressionLight
、ShinyBlue
和 TwilightBlue
。每个主题都包含在 IssueVision.Client 项目中,作为一个 ResourceDictionary
,它定义了内置控件的所有样式以及为该示例专门构建的自定义控件的样式。它们在下面的 Assets 文件夹中:
当我们想要动态更改主题时,将调用 ChangeThemmeCommand
,下面是源代码:
private RelayCommand<string> _changeThemeCommand = null;
public RelayCommand<string> ChangeThemeCommand
{
get
{
if (_changeThemeCommand == null)
{
_changeThemeCommand = new RelayCommand<string>(
OnChangeThemeCommand,
g =>
{
var themeResource = Application.GetResourceStream
(new Uri("/IssueVision.Client;component/Assets/" +
g, UriKind.Relative));
return themeResource != null;
});
}
return _changeThemeCommand;
}
}
private void OnChangeThemeCommand(String g)
{
try
{
if (g == "BureauBlue.xaml" || g == "ExpressionLight.xaml" ||
g == "ShinyBlue.xaml" || g == "TwilightBlue.xaml")
{
// remove the old one
Application.Current.Resources.MergedDictionaries.RemoveAt
(Application.Current.Resources.MergedDictionaries.Count - 1);
// find and add the new one
var themeResource = Application.GetResourceStream(new Uri
("/IssueVision.Client;component/Assets/" +
g, UriKind.Relative));
var rd = (ResourceDictionary)(XamlReader.Load
(new StreamReader(themeResource.Stream).ReadToEnd()));
Application.Current.Resources.MergedDictionaries.Add(rd);
// notify the change
if (g == "BureauBlue.xaml")
{
IsBureauBlueTheme = true;
IsExpressionLightTheme = false;
IsShinyBlueTheme = false;
IsTwilightBlueTheme = false;
}
else if (g == "ExpressionLight.xaml")
{
IsBureauBlueTheme = false;
IsExpressionLightTheme = true;
IsShinyBlueTheme = false;
IsTwilightBlueTheme = false;
}
else if (g == "ShinyBlue.xaml")
{
IsBureauBlueTheme = false;
IsExpressionLightTheme = false;
IsShinyBlueTheme = true;
IsTwilightBlueTheme = false;
}
else if (g == "TwilightBlue.xaml")
{
IsBureauBlueTheme = false;
IsExpressionLightTheme = false;
IsShinyBlueTheme = false;
IsTwilightBlueTheme = true;
}
}
}
catch (Exception ex)
{
// notify user if there is any error
AppMessages.RaiseErrorMessage.Send(ex);
}
}
我喜欢使用 ResourceDictionary
直接进行动态主题设置的灵活性,因为在发现 bug 或需要增强功能时,我们可以随时轻松修改它们。此外,我们还可以选择为任何自定义控件定义自己的样式,如下所示:
<ResourceDictionary>
.........
<!--IssueVision Specific Styles-->
<LinearGradientBrush x:Key="IssueVisionBackgroundBrush" EndPoint="1,0.5"
StartPoint="0,0.5">
<GradientStop Color="#FFBFDBFF" Offset="0"/>
<GradientStop Color="#FFA6C2E5" Offset="1"/>
</LinearGradientBrush>
<!--common:MainPageControl-->
<Style TargetType="common:MainPageControl">
<Setter Property="Background"
Value="{StaticResource IssueVisionBackgroundBrush}"/>
</Style>
<!--common:FlipControl-->
<Style TargetType="common:FlipControl">
<Setter Property="Background"
Value="{StaticResource IssueVisionBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="DarkBlue"/>
<Setter Property="BorderThickness" Value="3"/>
<Setter Property="CornerRadius" Value="4"/>
</Style>
</ResourceDictionary>
下一步
在本文中,我们回顾了应用程序的安装方式、设计架构、布局自定义控件以及动态主题。在第 2 部分,我们将介绍 MVVM Light Toolkit 的使用方法,即 RelayCommand
、Messenger
、EventToCommand
和 ICleanup
。
希望您觉得本文有用,请在下方评分和/或留下反馈。谢谢!
历史
- 2010 年 5 月 - 初始发布
- 2010 年 7 月 - 基于反馈的次要更新
- 2010 年 11 月 - 更新以支持 VS2010 Express Edition
- 2011 年 2 月 - 更新以修复包括内存泄漏问题在内的多个 bug
- 2011 年 3 月 - 使用 Visual Studio 2010 SP1 构建
- 2011 年 6 月 - 基于反馈的更新
- 2011 年 7 月 - 更新以修复多个 bug