一个简单的 Silverlight 4 应用程序(MEF+ MVVM+ WCF RIA Services)第一部分

注:本文是Weidong Shen先生在CodeProject上的文章,为了学习方便,进行了全文翻译,后续将以此程序示例为蓝本,写出自已的实用LOB程序来,在此向Weidong Shen先生表示感谢,欢迎大家对我的翻译进行拍砖。
内容
Introduction简介

This sample application is the result of my initiative to learn Silverlight and WCF RIA Services. With my background of using WPF and MVVM for the past several years, I found that there is a lack of sample LOB applications that can combine the latest Silverlight enhancements with MVVM. This three part article series is my effort at creating such a sample. The choice of an issue tracking application comes from David Poll's PDC09 talk, and the design architecture is from Shawn Wildermuth's blog posts.

此示例应用程序是我学习的Silver light和WCF RIA服务的成果。过去几年我已经有了使用WPF与MVVM接合的难舍经验,但是在最新发布的Silverlight与MVVM接合上却鲜有LOB案例可供参考。本例因此而建。

The main features of this issue tracking application are:程序的主要功能如下:

  • Login screen provides custom authentication and password reset based on security question and answer.
  • My Profile screen is for updating user information, password, security questions and answers.
  • User Maintenance screen is only available to Admin users, and lets the Admin user add/delete/update users.
  • New Issue screen is for creating new issues (bugs, work items, spec defects, etc.).
  • My Issues screen is for tracking all active and resolved issues assigned to a user.
  • All Issues screen is for tracking all issues (Open, Active, Pending, or Resolved).
  • Bug Report screen provides a summary of bug trend, bug count, and the functionality to print the summary.
  • Four different Themes are available and can be applied dynamically at any time.
  • 登录屏幕提供自定义身份验证和密码重置安全问题和答案。
  • My Profile页面更新用户信息,密码,安全问题和答案。
  • 用户维护屏幕只提供给管理员,让管理员用户添加/删除/更新用户。
  • New Issue页面创建新的问题(错误,工作项目,规范的缺陷等。)
  • My Issues页面跟踪所有分配给用户的活动和解决问题。
  • All Issues页面跟踪所有问题(开放,活动,等待,或解决)。
  • 错误报告屏幕提供了一个简要的错误,错误计数的统计功能,并能够打印汇总信息。
  • 四个不同的主题可以任意动态切换。
Requirements系统需求

In order to build the sample application, you need:

  • Microsoft Visual Studio 2010 SP1
  • Silverlight 4 Toolkit April 2010 (included in the sample solution)
  • MVVM Light Toolkit V3 SP1 (included in the sample solution)
Installation安装

After downloading the setup package to a location on your local disk, we need to complete the following steps:

1. Installing the IssueVision Sample Database安装示例数据库

To install the sample database, please run SqlServer_IssueVision_Schema.sql andSqlServer_IssueVision_InitialDataLoad.sql included in the setup package zip file. SqlServer_IssueVision_Schema.sqlcreates the database schema and database user IVUser; SqlServer_IssueVision_InitialDataLoad.sql loads all the data needed to run this application, including the initial application user ID user1 and Admin user ID admin1, with passwords all set as P@ssword1234.

2. Installing the Web Setup Package安装Web安装包

After the database setup, run setup.exe also included in the setup package zip file. This will install the IssueVision for Silverlight website.

When done installing the website, we can access the Silverlight application as follows:安装完毕后就可以访问应用程序了,如下所示:

Architecture架构
1. Solution Structure解决方案架构

Inside the sample solution file, projects are further organized into either the Client folder or the Server folder. TheClient folder includes all the projects that will be compiled into the file IssueVision.Client.xap, and the Server folder consists of all the projects that will eventually run inside a web server environment.在解决方案文件中,项目被组织为Client与Server两个文件夹。Client文件夹包括最终将被编译成IssueVision.Client.xap的所有项目,Server文件夹包括所有最终在web服务器端运行的所有项目。

For the projects inside the Server folder:

  • IssueVision.Web project is the main startup project. It includes the startup page Default.aspx and the Silverlight application package IssueVision.Client.xap. IssueVision.
  • Web为主启动项目,包含启动页面Default.aspx和Silverlight应用程序包IssueVision.Client.xap。
  • IssueVision.Data.Web project is the server-side data access layer. It receives requests from clients, accesses the database through the database user IVUser, and returns the results back. The major components of this project include the IssueVision Entity data model and all related DomainService classes.
  • IssueVision.Data.Web项目是服务器端数据访问层。接收来自于客户端的请求,通过Database User IVUser访问数据库,并将结果返回。该项目主要由IssueVision实体数据模型和相应的域服务类组成;

For the projects inside the Client folder:在Client文件夹内部:

  • IssueVision.Data project has a WCF RIA Services link to IssueVision.Data.Web, and therefore hosts the generated client-side proxy code and shared source code. This project also includes all the client-side only partial classes that do not need to be duplicated on the server side.
  • IssueVision.Data项目有一个WCF RIA服务连接到IssueVision.Data.Web,因此可以访问由此而生成的客户端的代理代码与共享源代码。该项目包含所有仅客户端运行的部分类,这些类无需要从服务器端进行复制;
  • IssueVision.Common project, as the name suggests, includes all the common interface classes and helper classes shared among other client projects.
  • IssueVision.Common项目,包含所有通用接口类和helper类,可供所有客户端项目共享;
  • IssueVision.Model project defines the Model of MVVM, and it has the following three model classes:
  • IssueVision.Model项目定义了MVVM的Model,包含如下三个model类:
    • AuthenticationModel
    • PasswordResetModel
    • IssueVisionModel
  • IssueVision.ViewModel project is the ViewModel part of MVVM, and includes all the nine ViewModel classes.
  • IssueVision.ViewModel项目是MVVM的ViewModel部分,包含所有9个ViewModel类;
  • IssueVison.Client is the main client-side project, and is also the View of MVVM that hosts all the UI logic.
  • IssueVison.Client是客户端的主要项目,也是MVVM的View部分;

From the solution structure above, we should notice that MVVM provides good separation of concerns between the UI and the business logic in order to make those UIs easier to maintain by developers and designers. Next, let's visit theModel, ViewModel, and View classes in more detail.

从解决方案架构来看,MVVM使得UI与业务逻辑得到了较好的分离。下面对Model, ViewModel, and View进行详细分析:

2. IssueVisionModel Class

We will discuss the classes AuthenticationModel and PasswordResetModel in part 3. For now, let's focus on the class IssueVisionModel, the main Model (of MVVM) class for this application. IssueVisionModel is based on the following interface, IIssueVisionModel:

IssueVisionModel是本应用程序的主要Model(MVVM)类,基于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; }
}

We define a separate Model class and do not use the data context class itself as the Model because the Model is best expressed as a set of properties and operations that retrieve, add, delete, and update data. This makes theModel easier to maintain and test. Additionally, as Shawn mentioned in his blog, "creating a custom Model allows us to isolate what transport layer we're using so we can change it or even have several data providers specifying data for our Model".

在此定义了一个独立的Model类而没有使用数据的上下文类本身作为Model,这是因为Model承载着一系列属性和CRUD的方法。通过分离手段使得Model易于维护和测试。此外,正如Shawn提到的:“创建一个自定义的模型使我们能够孤立于传输层,我们可以改变它或甚至可以将几个数据引擎的数据指定为我们的模型”。

Next, let's look at how a retrieve method in the IssueVisionModel class is actually implemented:

下面,就看看IssueVisionModel类如何实现获取数据的方法:

public void GetIssueTypesAsync()
{
    PerformQuery(Context.GetIssueTypesQuery(), GetIssueTypesComplete);
}

GetIssueTypeAsync() calls the private method PerformQuery() and passes in an EntityQuery GetIssueTypesQuery() and an event GetIssueTypesComplete. When the retrieve call is done, the eventGetIssueTypesComplete will fire and pass back the result set, or any error message if something goes wrong. In fact, almost all retrieve methods are as simple as calling the PerformQuery() method defined below:

GetIssueTypeAsync()调用私有方法PerformQuery(),传递两个参数,一个是实体的查询方法GetIssueTypeQuery(),另一个是事件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);
}

Also, the Model class exports itself to the ViewModel classes by using the MEF Export attribute on the class as follows:

同时Model类通过在类上使用MEF的Export特性标记向ViewModel类暴露:

[Export(typeof(IIssueVisionModel))]
//指定将由 CompositionContainer 创建关联的 ComposablePart 的单个共享实例,并由所有请求者共享该实例。
[PartCreationPolicy(CreationPolicy.Shared)]
public class IssueVisionModel : IIssueVisionModel
3. The ViewModel Classes

Most of the ViewModel classes include six regions: Private Data Members region, Constructor region, Public Properties region, Public Commands region, ICleanup Interface region, and Private Methods region. The Public Properties and Public Commands regions expose all the necessary properties and commands to its View class. And, the constructor sets up event handling, sets the initial values for any private data, and registers theAppMessages needed inside the ViewModel class. Here is an example:

大多数据ViewModel类包括6个部分:私有数据成员区域,构造器区域,公开属性区域,公开命令区域,ICleanup接口实现区域以及私有方法区域。公有属性与公有命令区域暴露必要的属性和命令到视图类。构造器设置事件处理绑定,设置任何私有数据的初始化值并注册AppMessages(ViewModel类内部需要)。如下是一例:

//指定在创建部件时应使用哪一个构造函数
[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);
}

We can see from the code above that the ViewModel class gets an object that implements theIIssueVisionModel interface by using the ImportingConstructor attribute which tells MEF to supply the discovered model class into the ViewModel. In turn, all the ViewModel classes export themselves like the following:

由此可见,ViewModel类获取一个实现了IIssueVisionModel接口的对象作为构造器的参数,同时使用ImportingConstructor特性标记修饰,指示MEF查找模型类并注入ViewModel中。然后,所有的ViewModel类按如下方式暴露:

[Export(ViewModelTypes.IssueEditorViewModel, typeof(ViewModelBase))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class IssueEditorViewModel : ViewModelBase
4. The View Classes and the Code-behind Files视图类及后置代码文件

Before we discuss any View class, let us first take a look at how a global CompositionContainer object is defined inside the file App.xaml.cs.

在进一步讨论View类之前,首先看看在App.xaml.cs中如何定义全局的CompositionConstainer对象:

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();
    }
    ......
}

With access to the static Container object, we can easily request a new ViewModel object as follows:

通过访问静态的Container对象,可以很容易地请求到新的ViewModel对象,如下代码所示:

// Use MEF To load the View Model
//使用MEF加载视图模型
_viewModelExport = App.Container.GetExport<ViewModelBase>(
                       ViewModelTypes.AllIssuesViewModel);
if (_viewModelExport != null) DataContext = _viewModelExport.Value;

And, we can release a ViewModel object with the following three lines of code:

并且可以通过如下三行代码释放ViewModel对象:

// set DataContext to null and call ReleaseExport()
//设置DataContext 为null,调用ReleaseExport()
DataContext = null;
App.Container.ReleaseExport(_viewModelExport);
_viewModelExport = null;

Each View class finds its ViewModel object through a function call to _viewModelExport = App.Container.GetExport(), followed by DataContext = _viewModelExport.Value in the constructor of each View class. This function instructs MEF at runtime to fulfill a chain of dependencies, which in turn creates all the Model and ViewModel objects required. The beauty of using MEF is that we can keep these projects loosely coupled. In fact, the projects IssueVision.Model, IssueVision.ViewModel, and IssueVison.Client do not need a reference to the other two projects to compile successfully. The project IssueVison.Client has a reference to the other two projects only because we need to add them into the output IssueVision.Client.xap file.

每个视图类都能通过调用_viewModelExport = App.Container.GetExport()找到其对应的ViewModel对象,并在视图类的构造器里赋值到数据上下文,代码为DataContext = _viewModelExport.Value。这种方式使用MEF充当运行时的依赖链,依次创建所有的所需要的Model和ModelView对象。使用MEF的好处就是我们可以维持项目间的松耦合。事实上,IssueVision.Model, IssueVision.ViewModelIssueVison.Client这三个项目其中之一并不需要引用另外两个项目就可以成功编译。IssueVison.Client项目对另外两个项目的引用仅仅是因为需要将其添加到输出的IssueVision.Client.xap文件里而已。

In the same constructor, we also register AppMessages the View class will handle. The IssueEditor class below is a good sample:

在同一构造器里,也需要注册AppMessages到需要处理的视图类里。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"

    .........
}

Within the code-behind files, we define all the UI-related logic like event handlers to dynamically enable/disable a button or AppMessages that display an error message when something goes wrong. As long as the code is related to the UI logic, it is perfectly OK to add them into the code-behind file, like the following:

在后置代码文件里,我们定义了所有的UI相关逻辑,比如动态启用/禁用按钮的事件处理函数,AppMessages的事件处理函数以便能够在遇到错误时显示错误信息。只要代码是与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));
}
Custom Controls for Layout用于布局的自定义控件

The custom controls FlipControl and MainPageControl defined in the project IssueVision.Common are used as the basis for screen layout.

自定义控件FilpControl与MainPageControl都定义在项目IssueVision.Common里,用于页面布局的基础组件。

FlipControl is used in LoginForm.xaml, which hosts both the login screen and the password reset screen. Switching the Dependency Property IsFlipped will toggle between these two screens, with animations defined inside the VisualStateManager.

FlipControl用于LoginForm.xaml,在登陆页与密码重置页使用,当在两个页面中切换时,依赖属性IsFilipped将会触发,显示定义在VisualStateManger内部的动画。

Similarly, MainPageControl is used in MainPage.xaml, and divides the whole screen into title content, login/logout menu contents, login page content, and main page content. The Dependency Property IsLoggedIn switches between the login page and the main page.

类似地,MainPageControl用于MainPage.xaml,将整个页面切分为标题、登陆/登出菜单,登陆页面信息和主页面信息几部分。依赖属性IsLoggedIn用于在登陆页和主页间切换。

Using this layout, custom controls could be considered as another application of separation of concerns. The screen layout styles along with animations defined in VisualStateManager is encapsulated by itself. As long as they provide the same functionality, we can easily change them, let's say creating a new animation, without affecting anyView classes defined in the project IssueVision.Client.

使用这种布局,自定义控件可以视作为独立运行的应用程序。画面布局风格与动画定义在visualstatemanager,并进行了封装。只要保证提供相同的功能,就可以对自定义控件进行随意的修改,比如创建新的动画,这不会对定义在IssueVison.Client项目中的任何视图产生影响。

Dynamic Theming动态主题

There are four different themes defined in this application, and they are BureauBlue, ExpressionLight,ShinyBlue, and TwilightBlue. Each theme is included in the project IssueVision.Client as aResourceDictionary, which defines all the styles for built-in controls as well as styles for custom controls built specifically for this sample. They are in the Assets folder shown below:

程序示例中内置了自种不同的主题,分别是BureauBlue, ExpressionLight,ShinyBlueTwilightBlue。第种主题都以资源字典的形式包含在IssueVision.Client项目中,为内置控件和自定义控件定义风格。位于如下所示的Assets文件夹中。

When we want to dynamically change themes, ChangeThemmeCommand will get called, and following is the source code:

如果想要动态切换主题,需要设置ChangeThemmeComman属性,该属性的代码如下:

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);
    }
}

I like the flexibility of using ResourceDictionary directly for dynamic theming because we can easily modify them any time there is a bug found or any enhancements are needed. Also, we have the option to define our own styles for any custom controls, as follows:

我认为对动态主题直接使用资源字典很灵活,因为可以随时进行更改。还有一个方法可以为任何自定义控件定义自已的风格,如下所示:

<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>
Next Steps

In this article, we visited how the application is installed, as well as the design architecture, layout custom controls, and dynamic theming. In part 2, we will go through the topics of how the MVVM Light Toolkit is used: namely,RelayCommand, Messenger, EventToCommand, and ICleanup.

I hope you find this article useful, and please rate and/or leave feedback below. Thank you!

这一部分我们涉及了设计架构,布局用自定义控件和动态主题。在第2部分,我们深入讨论一下如何使用MVVM Light Toolkit:命名规则,RelayCommand, Messenger, EventToCommandICleanup.

希望本文对您有用,并请留下您的回馈,谢谢!

History
  • May 2010 - Initial release
  • July 2010 - Minor update based on feedback
  • November 2010 - Update to support VS2010 Express Edition
  • February 2011 - Update to fix multiple bugs including memory leak issues
  • March 2011 - Built with Visual Studio 2010 SP1
  • June 2011 - Update based on feedback
  • July 2011 - Update to fix multiple bugs
License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Weidong Shen

Software Developer (Senior)
United States United States
Member

Weidong has been an information system professional since 1990. He has a Master's degree in Computer Science, and is currently a MCSD .NET

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值