65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 MEF、MVVM 和 WCF RIA Services 的 Silverlight 4 应用程序示例 - 第 1 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (108投票s)

2010 年 5 月 19 日

CPOL

9分钟阅读

viewsIcon

2147194

downloadIcon

30903

本系列文章的第一部分,介绍如何使用 MEF、MVVM Light 和 WCF RIA Services 创建 Silverlight 业务应用程序。

文章系列

本文是关于使用 MEF、MVVM Light 和 WCF RIA Services 开发 Silverlight 业务应用程序的系列文章的第一部分。

目录

引言

此示例应用程序是我学习 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.sqlSqlServer_IssueVision_InitialDataLoad.sqlSqlServer_IssueVision_Schema.sql 用于创建数据库架构和数据库用户 IVUserSqlServer_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 更易于开发人员和设计师维护。接下来,我们将更详细地介绍 ModelViewModelView 类。

2. IssueVisionModel 类

我们将在第 3 部分讨论 AuthenticationModelPasswordResetModel 类。现在,让我们专注于 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 PropertiesPublic 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 在运行时满足一系列依赖关系,从而创建所有必需的 ModelViewModel 对象。使用 MEF 的好处在于我们可以使这些项目保持松耦合。事实上,IssueVision.ModelIssueVision.ViewModelIssueVison.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 项目中定义的自定义控件 FlipControlMainPageControl 用作屏幕布局的基础。

FlipControl 用于 LoginForm.xaml,它同时承载登录屏幕和密码重置屏幕。切换 IsFlipped 依赖属性将在这两个屏幕之间切换,动画在 VisualStateManager 中定义。

类似地,MainPageControl 用于 MainPage.xaml,并将整个屏幕分为标题内容、登录/注销菜单内容、登录页面内容和主页面内容。IsLoggedIn 依赖属性会在登录页面和主页面之间切换。

使用此布局,自定义控件可以被视为关注点分离的另一种应用。屏幕布局样式以及在 VisualStateManager 中定义的动画被封装起来。只要它们提供相同的功能,我们就可以轻松更改它们,例如创建新的动画,而不会影响 IssueVision.Client 项目中定义的任何 View 类。

动态主题

此应用程序定义了四种不同的主题:BureauBlueExpressionLightShinyBlueTwilightBlue。每个主题都包含在 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 的使用方法,即 RelayCommandMessengerEventToCommandICleanup

希望您觉得本文有用,请在下方评分和/或留下反馈。谢谢!

历史

  • 2010 年 5 月 - 初始发布
  • 2010 年 7 月 - 基于反馈的次要更新
  • 2010 年 11 月 - 更新以支持 VS2010 Express Edition
  • 2011 年 2 月 - 更新以修复包括内存泄漏问题在内的多个 bug
  • 2011 年 3 月 - 使用 Visual Studio 2010 SP1 构建
  • 2011 年 6 月 - 基于反馈的更新
  • 2011 年 7 月 - 更新以修复多个 bug
© . All rights reserved.