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

WinRT: StyleMVVM 演示 1/2

starIconstarIconstarIconstarIconstarIcon

5.00/5 (24投票s)

2013年10月30日

CPOL

17分钟阅读

viewsIcon

55816

一个优秀的 WinRT MVVM 框架概览。

目录

系列链接

第 1 部分:StyleMVVM 基础知识(本文)

第 2 部分:StyleMVVM 演示应用详解

引言

大约一年前,一位名叫 Ian Johnson 的非常聪明的美国人联系了我,问我是否能为他即将推出的 Windows 8 MVVM 库做个评测。我说我当时正在度假,但回来后很乐意这么做。在过去的一年里,我与 Ian 几乎保持着持续的联系,回答他的问题,我们甚至还合写了一篇文章(Expression API Cookbook)。Ian 的框架(以下称为 StyleMVVM)给我留下的第一印象是功能非常丰富,而且构思非常周全。请记住,我花了很多时间研究他的代码库。

当时,我很幸运地被邀请担任新的 PRISM for Windows 8(代号“Kona”)的顾问成员。我没有足够的时间在这上面做太多事情,但这仍然是一种荣誉。不过,后来我花了很多时间去了解“Kona”,它现在被称为“PRISM for Windows Runtime”。

我非常喜欢 PRISM,并且对那个团队以及之前的团队成员充满敬意,但我也不得不说,就目前而言,我更喜欢 StyleMVVM,因为我认为它功能更丰富,而且某些代码似乎比 PRISM for Windows Runtime 更好用。

StyleMVVM 中,我非常喜欢的一点是 Ian 从零开始编写了一个基于表达式树(速度极快)的 IOC 容器。他对这个容器进行了基准测试,结果显示其性能非常出色,速度远超 Castle / Unity 和 AutoFac。它基于 MEF 风格的特性模型,但流式注册也即将推出(我知道,我见过它的 beta 版本,看起来非常酷)。Ian 对 StyleMVVM 的这一部分感到非常自豪,这是理所当然的,它确实很棒。

对于个人开发者来说,能创造出功能如此丰富的框架,堪比 MVVM 领域的巨头之一,这绝非易事。向 Ian 致敬。

我非常相信 Ian 的工作,所以在业余时间一直在用它开发一个演示应用。这个演示应用将成为本系列下一篇文章的来源。

在本文中,我们将重点关注 StyleMVVM 的一些特性。

先决条件

如果您只是想将本文作为 StyleMVVM 的入门介绍,那么您不需要任何东西。如果您喜欢所读到的内容并想试用 StyleMVVM,您将需要:

  1. Visual Studio 2012 / 2013
  2. Windows 8 / 8.1

如果您更喜欢您所读到的内容,并决定使用附带的演示代码(这是我的演示代码,而不是 StyleMVVM 源代码附带的演示),您还需要额外安装以下内容:

  1. SQLite Windows 8 相关组件:https://sqlite.ac.cn/2013/sqlite-winrt80-3080002.vsix(尽管它已包含在演示代码下载中,您只需运行它即可。这是一个 Visual Studio 扩展,所以只需双击它)。
  2. 完成第一步后,请确保所有 NuGet 包都已存在,如果不存在,请恢复它们。另外,请确保您已经引用了已安装的(来自第一步的)SQLite 库。

StyleMVVM 开箱即用提供了什么?

正如我已经说过的,StyleMVVM 的功能非常丰富,因此提供了许多出色的 MVVM 框架所应具备的服务/帮助程序(而且 Ian 非常乐于添加新功能,我知道这一点,因为我曾请求他为我添加功能,他立刻就做了)。

以下是我从 StyleMVVM 的 codeplex 网站上“偷”来的一些核心 StyleMVVM 特性列表:

  • StyleMVVM 是唯一支持 C#、C++/CX 和 HTML/JS 这三种开发语言的 Window Store App 的 MVVM 工具包。它允许你导出用 C++/CX 编写的组件,并将其导入到 C# 应用程序中。
  • 内置验证引擎,根据您的需求支持 ValidationAttributes、流式验证和方法验证。您可以验证 ViewModel 和 DataModel。
  • 约定模块,简化开发流程,同时提供模板来设置您的项目以使用约定。
  • 使用简单的语法 View:EventHandler.Attach="EventName => ViewModelMethod($sender,$eventArgs); 等,可以轻松地将事件处理程序连接到您的视图模型。
  • 支持类似于 Prism 的区域(Regions),区域注册在 XAML 中进行,而导航则由 ViewModel 驱动。
  • 基于特性的 IoC 容器(Export, Import),容器也具备环境感知能力,并会相应地对导出的组件进行排序,在定位时(运行时、设计时、单元测试时)返回最佳匹配项。
  • 用于以编程方式配置 IoC 容器的流式接口(C++/CX 和 Windows Runtime 组件必需)。
  • 设计时 ViewModel 支持。ViewModel 在设计时通过 IOC 定位,因此您可以使用真实的 ViewModel,并用一个模拟版本替换您的 IDataService 以用于设计和单元测试。
  • 自动注册视图和 WCF 客户端(即所有继承自 ClientBase<T> & Page 的类)。
  • ICommand 和附加事件命令的实现。
  • 支持调度器(Dispatcher Aware)的消息服务(即处理程序在正确的调度器上被回调)。
  • 与 Metro 页面导航系统绑定的 NavigationViewModel(支持 Navigate 方法和导航事件)。
  • 可扩展的日志解决方案
  • 通过导出 IConfigurationStorageProvider 接口可扩展的配置服务。
  • 改进的 Suspension Manager(自动将 DataContract 类添加到 KnownTypes 中)。
  • IMessageBoxServiceIFilePickerService 包装器,允许您在设计和单元测试时模拟对话框。
  • ICharmService 通过自动向 SettingPane 注册超级按钮控件,使创建浮出控件变得容易。
  • ITileServiceIBadgeServiceIToastService 允许轻松更新磁贴和 toast 通知。
  • IActivationService 可以创建或克隆任何对象(满足导入和消息注册)。
  • ITransformService 可以使用反射和 LINQ 表达式将数据对象从一种类型转换为另一种类型,您可以使用 Transform 特性为服务提供有关如何转换属性的提示。
  • 用于项目和项的 Visual Studio 模板

更深入地了解如何使用 StyleMVVM

在本节中,我们将深入探讨如何使用 StyleMVVM 开发您自己的应用程序。

典型的 Boostrapper 代码

我得说,我并不是整个 WinRT 应用文件的忠实粉丝。老实说,我觉得它简直一团糟。话虽如此,事实就是如此,我们必须接受它。要启动 StyleMVVM,我们需要应用一个 BootStrapper(就像我们在 PRISM 中做的那样)。下面展示了一个使用 StyleMVVM 时可能用到的典型 App.xaml.cs 文件。

sealed partial class App : Application
{
    /// <summary>
    /// Initializes the singleton Application object.  This is the first line of authored code
    /// executed, and as such is the logical equivalent of main() or WinMain().
    /// </summary>
    public App()
    {
        this.InitializeComponent();
        this.Suspending += OnSuspending;

        CreateBootstrapper();
    }

    /// <summary>
    /// Invoked when the application is launched normally by the end user.  Other entry points
    /// will be used when the application is launched to open a specific file, to display
    /// search results, and so forth.
    /// </summary>
    /// <param name="args">Details about the launch request and process.</param>
    protected override async void OnLaunched(LaunchActivatedEventArgs args)
    {
        LaunchBootStrapper();

        // Do not repeat app initialization when already running, just ensure that
        // the window is active
        if (args.PreviousExecutionState == ApplicationExecutionState.Running)
        {
            Window.Current.Activate();
            return;
        }

        // Create a Frame to act as the navigation context and associate it with
        // a SuspensionManager key
        var rootFrame = new Frame();

        StyleMVVM.Suspension.SuspensionManager.RegisterFrame(rootFrame, "AppFrame");

        if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
        {
            // Restore the saved session state only when appropriate
            await StyleMVVM.Suspension.SuspensionManager.RestoreAsync();
        }

        if (rootFrame.Content == null)
        {
            Type navigateType = Bootstrapper.Instance.Container.LocateExportType("StartPage");

            // Could not find start page. make sure you have a page marked as [StartPage]
            if (navigateType == null)
            {
                throw new Exception("Could not find start page.");
            }

            if (!rootFrame.Navigate(navigateType))
            {
                throw new Exception("Failed to create initial page");
            }
        }

        // Place the frame in the current Window and ensure that it is active
        Window.Current.Content = rootFrame;
        Window.Current.Activate();
    }

    /// <summary>
    /// Invoked when application execution is being suspended.  Application state is saved
    /// without knowing whether the application will be terminated or resumed with the contents
    /// of memory still intact.
    /// </summary>
    /// <param name="sender">The source of the suspend request.</param>
    /// <param name="e">Details about the suspend request.</param>
    private async void OnSuspending(object sender, SuspendingEventArgs e)
    {
        var deferral = e.SuspendingOperation.GetDeferral();
        await StyleMVVM.Suspension.SuspensionManager.SaveAsync();
        deferral.Complete();
    }

    /// <summary>
    /// Create StyleMVVM BootStrapper
    /// </summary>    
    private void CreateBootstrapper()
    {
        if (!Bootstrapper.HasInstance)
        {
            Bootstrapper newBootstrapper = new Bootstrapper();

            newBootstrapper.Container.RegisterAssembly(
                GetType().GetTypeInfo().Assembly);

            // Required for Validation
            newBootstrapper.Container.RegisterAssembly(
                typeof(CSharpContainerLoader).GetTypeInfo().Assembly);

            newBootstrapper.Start(false);
        }
    }

    private void LaunchBootStrapper()
    {
        Bootstrapper.Instance.Launched();
    }
}

正如我所说,我认为必须放在这个文件里的代码量太糟糕了,这是一个糟糕的关注点分离,坦率地讲,完全是一场灾难。这与 Ian 或 StyleMVVM 无关,这是 WinRT 的方式。很酷,是吧?

IOC 的使用

Ian 在 StyleMVVM 中所做的是采用一种 MEF 类型的方法,即使用某些特性来指明您想要 Export 什么以及如何导出。

一个流式接口即将推出(我很高兴地说我参与了其中的一部分),很快就会发布。

现在,让我们来看一个表格,了解你可能如何做某些事情。

任务 我该做什么
将某物导出到容器
[Export]
public class ScheduleViewModel
将某物标记为单例
[Singleton]
[Export(typeof(ISqlLiteDatabaseService))]
public class SqlLiteDatabaseService : ISqlLiteDatabaseService
导出为特定类型
[Singleton]
[Export(typeof(ISqlLiteDatabaseService))]
public class SqlLiteDatabaseService : ISqlLiteDatabaseService
在构造函数中导入东西
[ImportConstructor]
public MainPageViewModel(IEnumerable pageInfos)
{
}
将特定视图作为应用程序的启动视图运行
[StartPage]
public sealed partial class MainPage : LayoutAwarePage
{
}

您可以期待在 StyleMVVM 的未来版本中看到更多关于容器的内容。

基础 ViewModel

StyleMVVM 包含许多方便的基础类 ViewModel,您可能希望继承它们。

以下是它们的列表

  • PageViewModel
    • 用于与 Page 类型视图配合使用的 ViewModel 的基类。它还提供了以下额外的辅助方法:
      • public INavigationService Navigation { get; }
      • public FrameworkElement View { get; set; }
      • public virtual void OnLoaded(object sender, RoutedEventArgs args);
      • public virtual void OnUnloaded(object sender, RoutedEventArgs args);
  • NavigatingViewModel
    • 用于与支持导航的视图配合使用的 ViewModel 的基类。它还提供了以下额外的辅助方法:
      • protected virtual Task InternalNavigatingFrom(object sender, StyleNavigatingCancelEventArgs e);
      • public void NavigatedFrom(object sender, StyleNavigationEventArgs e);
      • public void NavigatedTo(object sender, StyleNavigationEventArgs e);
      • public IAsyncAction NavigatingFrom(object sender, StyleNavigatingCancelEventArgs e);
      • protected virtual void OnNavigatedFrom(object sender, StyleNavigationEventArgs e);
      • protected virtual void OnNavigatedTo(object sender, StyleNavigationEventArgs e);
      • protected virtual Task OnNavigatingFrom(object sender, StyleNavigatingCancelEventArgs e);
  • BaseViewModel
    • 简单的支持容器的 INPC viewmodel

在我看来,这些基类非常宝贵,它们处理了 MVVM 中一些非常棘手的问题,例如:

  1. 当我的页面加载时,我想要运行某个方法。好的,只需重写 PageViewModel 中的 OnLoaded(..) 方法即可。
  2. 我需要获取导航参数。好的,只需重写 NavigatingViewModel 中的 NavigatedTo(..) 方法即可。

那些用过 PRISM 的人肯定会发现一些相似之处。

PageViewModel

既然我们已经讨论了 StyleMVVM 提供的一些基类,我只想详细探讨其中一个,即 PageViewModel

当您使用 StyleMVVMPageViewModel 时,您可以做一些很酷的事情,比如:

  • 将视图作为 Framework 元素进行交互,这意味着您可以获取其 Dispatcher
  • 在视图加载/卸载时执行操作
  • 重写 OnNavigatedTo(..) 方法并使用 StyleNavigationEventArgs 来获取导航上下文参数。

导航

如果您继承自 PageViewModel,您将可以访问 Navigation 属性(它为您提供了 INavigationService),您可以像下面这样简单地用它来进行导航:

string state = "Sending state";
Navigation.Navigate("SomeView",state);

然后,在页面内(前提是您继承自 PageViewModel),您可以通过使用 OnNavigatedTo(..) 并利用 StyleNavigationEventArgs 来获取 Navigation 上下文数据。

视图 / ViewModel 连接

StyleMVVM 支持通过附加的 DependencyProperty 将视图连接到其 ViewModel(这与我过去在自己的 Cinch 框架中所做的方式很像),具体做法如下。

在视图中,您只需执行以下操作:

<Common:LayoutAwarePage
    x:Class="Demo.Views.SomePage"
    ....
    xmlns:View="using:StyleMVVM.View"
    View:ViewModel.Name="SomePageViewModel">
...
...
...
</Common:LayoutAwarePage>

在视图的代码隐藏文件中,你需要这样做。这一步是**强制性的**,没有这个设置,视图将**无法**解析其 ViewModel。

[Export]
public sealed partial class SomePage : LayoutAwarePage
{
    public MainPage()
    {
        this.InitializeComponent();
    }
}

你的 ViewModel 可能看起来是这样的:

public class SomePageViewModel : PageViewModel
{

}

验证

StyleMVVM 开箱即用提供了几种不同风格的验证,您可以根据自己的需要选择使用哪一种。在我们查看 StyleMVVM 中不同类型的验证之前,让我们先创建一个简单的 ViewModel,下面的所有示例都将基于这个 ViewModel 来执行验证。

public class BasicValidationViewModel : PageViewModel, IValidationStateChangedHandler
{
    private string prefix;
    private string firstName;
    private string lastName;
    private string middleName;
    private string emailAddress;
    private DelegateCommand saveCommand;

    public DelegateCommand SaveCommand
    {
        get
        {
            if (saveCommand == null)
            {
                saveCommand = new DelegateCommand(SaveMethod,
                    x => ValidationContext.State == ValidationState.Valid);
            }
            return saveCommand;
        }
    }

    public IEnumerable<string> Prefixes
    {
        get { return new []{ "Mr","Mrs","Ms","MD","Phd"};}
    }
        
    [Required]
    public string Prefix
    {
        get { return prefix; }
        set { SetProperty(ref prefix, value); }
    }

    [Required]
    public string FirstName
    {
        get { return firstName; }
        set { SetProperty(ref firstName, value); }
    }

    [Required]
    public string LastName
    {
        get { return lastName; }
        set { SetProperty(ref lastName, value); }
    }

    [Required]
    public string EmailAddress
    {
        get { return emailAddress; }
        set { SetProperty(ref emailAddress, value); }
    }
        
    public string MiddleName
    {
        get { return middleName; }
        set { SetProperty(ref middleName, value); }
    }
        
    [Import]
    public IValidationContext ValidationContext { get; private set; }

    [Import]
    public IMessageBoxService MessageBox { get; set; }


    [ActivationComplete]
    public void Activated()
    {
        ValidationContext.RegisterValidationStateChangedHandler(this);
    }

    private void SaveMethod(object parameter)
    {
        MessageBox.Show("Save Clicked");
    }

    
    public void StateChanged(IValidationContext context, ValidationState validationState)
    {
        SaveCommand.RaiseCanExecuteChanged();
    }
}

在这个示例 ViewModel 中有几个要点:

  1. 可以看出,您可以自由使用 System.ComponentModel.DataAnnotations 命名空间中的特性,例如 RequiredAttributeStyleMVVM 可以很好地与 System.ComponentModel.DataAnnotations 命名空间中的特性协同工作。
  2. 您应该确保将 IValidationContext 标记为 [Import] 属性。它不能通过构造函数注入,因为我们需要在创建 IValidationContext 之前创建目标对象。因此,它**必须**是一个属性 [Import]
  3. 我们在 Activated 方法中连接 IValidationContext(参见 [ActivationComplete] 的用法),然后我们可以用它来根据当前对象的有效/无效状态禁用 ICommand 对象。看看上面的构造函数,你就会明白我的意思了。

流式验证框架

StyleMVVM 内置了一个流式 API 用于配置验证。以下是基于上述示例 ViewModel 的一个小例子。

[Export(typeof(IFluentRuleProvider<BasicValidationViewModel>))]
public class FluentValidationForBVVM : IFluentRuleProvider<BasicValidationViewModel>
{
    public void ProvideRules(IFluentRuleCollection<BasicValidationViewModel> collection)
    {
        collection.AddRule("MiddleNameRule")
                    .Property(x => x.MiddleName)
                    .IsRequired()
                    .When.Property(x => x.FirstName)
                    .IsNotEmpty();
    }
}

通过继承 IFluentRuleProvider<T> 并通过 [ExportAttribute] 导出它,StyleMVVM 将确保为已创建流式规则的 ViewModel 正确地进行设置。我认为,掌握 StyleMVVM 流式验证 API 的最佳方式就是亲自尝试一下。

方法类型验证

您也可以创建一个自定义的方法验证,在其中可以运行任何您想要的逻辑。和之前一样,这里有一个针对上面展示的 ViewModel 的例子。

[Export(typeof(IValidationMethodProvider<BasicValidationViewModel>))]
public class MethodValidationForBVVM : IValidationMethodProvider<BasicValidationViewModel>
{
    public void ProvideRules(IValidationMethodCollection<BasicValidationViewModel> methodCollection)
    {
        methodCollection.AddRule(IanJohnsonRule).
            MonitorProperty(x => x.FirstName).MonitorProperty(x => x.LastName).MonitorProperty(x => x.MiddleName);
    }

    private void IanJohnsonRule(IRuleExecutionContext<BasicValidationViewModel> obj)
    {
        if (string.Compare(obj.ValidationObject.FirstName, "Ian", 
                StringComparison.CurrentCultureIgnoreCase) == 0 &&
                string.Compare(obj.ValidationObject.MiddleName, 
                "Phillip", StringComparison.CurrentCultureIgnoreCase) == 0 &&
                string.Compare(obj.ValidationObject.LastName, 
                "Johnson", StringComparison.CurrentCultureIgnoreCase) == 0)
        {
            obj.AddError(x => x.FirstName, "Your first name can't be Ian when your last name is Johnson");
            obj.AddError(x => x.MiddleName,"Your middle name can't be Ian Phillip Johnson ... I am!");
            obj.AddError(x => x.LastName, "Your first name can't be Ian when your last name is Johnson");

            obj.Message = "You can not be Ian Phillip Johnson ... Identity Thief!";
        }
    }
}

通过继承 IValidationMethodProvider<T> 并通过 [ExportAttribute] 导出它,StyleMVVM 将确保为 ViewModel 正确设置该方法提供的规则。和以前一样,处理这个问题的最好方法是亲自尝试一下。

验证控件模板和样式

遗憾且奇怪的是,Windows 8/WinRT 似乎没有提供任何标准的验证样式/控件模板。幸运的是,Ian 在 StyleMVVM 源代码中提供了一些,从而解决了这个问题。你只需要在下载的 StyleMVVM 源代码中寻找以下 ResourceDictionary

Release-XXXX\Examples\ExampleApp\ExampleApp\Common\FrameworkStyles.xaml

你应该会看到,那个 ResourceDictionary 包含了一些有用的验证样式/控件模板,可以与 StyleMVVM 一起使用,例如:

  • ValidationTextbox
  • ValidationComboBox

然后可以像这样在 XAML 中应用它们:

<UserControl
    x:Class="ExampleApp.Views.Validation.BasicValidationView"
    .....
    xmlns:View="using:StyleMVVM.View"
    View:ViewModel.Name="BasicValidationViewModel">
    <Grid>
        <ComboBox Template="{StaticResource ValidationComboBox}" 
            SelectedItem="{Binding Prefix,Mode=TwoWay}" 
            View:Validation.Property="Prefix"      
            ItemsSource="{Binding Prefixes}"/>

        <TextBox Text="{Binding FirstName,Mode=TwoWay}" 
                 View:Validation.Property="FirstName" 
                 Template="{StaticResource ValidationTextbox}"/>

   </Grid>
</UserControl>

另外请注意,我们如何选择要用于验证过滤器的属性。运行时,这会产生类似这样的效果:

StyleMVVM 的验证方式的好处在于,每种类型的控件都可能显示验证错误。你只需要创建正确的样式/控件模板。正如我所说,Ian 已经创建了上面列出的两种,但如果你发现需要更多,只需借鉴 Ian 已经为你精心制作的 TextBoxValidationTextBox)和 ComboBoxValidationComboBox)的思路即可。

可恢复的 ViewModel

Windows 8 遵循与 Windows Phone 相同的生命周期类型,应用程序可以经历各种生命周期,包括暂停。Windows 8 自带一个标准的 SuspensionManager。然而,为了简化事情,StyleMVVM 提供了自己的 SuspensionManager,我们之前在查看 App.xaml.cs 代码时已经看到了它的连接方式。

现在,为了让事情变得超级简单,StyleMVVM 提供了两个特性(如下所示的 SyncableAttributeSyncAttribute),您可以使用它们来确保您的 ViewModel 状态在暂停时被存储,并在重新激活时正确恢复。

这是一个使用这两个 StyleMVVM 特性的小型 ViewModel 的示例。

[Export]
[Syncable]
public class ScheduleViewModel : BaseViewModel
{

    [Sync]
    public bool HasItems
    {
        get { return hasItems; }
        set { SetProperty(ref hasItems, value); }
    }
}

你只需要做这些就够了。StyleMVVM 在幕后使用 DataContractSerializer 为你处理好了一切。我说,这东西真不错。

DelegateCommand

一个老而弥坚的经典,如今已是著名的 DelegateCommand(这些天我倾向于使用更基于 Rx 的 ICommand,有兴趣的读者可以在这里阅读更多信息:http://sachabarbs.wordpress.com/2013/10/18/reactive-command-with-dynamic-predicates/

然而,回到正题,这里是 StyleMVVMDelegateCommand,您可以用它直接在 ViewModel 中运行命令逻辑。不过,我还是建议使用一个单独的控制器,或者甚至是多个微控制器,来让您的 ViewModel 保持精简。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace StyleMVVM.ViewModel
{
    public delegate void DelegateAction(object parameter);

    public delegate bool DelegateCanExecute(object parameter);

    /// <summary>
    /// A general implementation of ICommand used by ViewModels to bind to commands and events
    /// </summary>
    public sealed class DelegateCommand : ICommand
    {
        private readonly DelegateAction command;
        private readonly DelegateCanExecute canExecute;

        public DelegateCommand(DelegateAction command)
        {
            this.command = command;
        }

        public DelegateCommand(DelegateAction command, DelegateCanExecute canExecute)
        {
            this.command = command;
            this.canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
        {
            if (canExecute != null)
            {
                return canExecute(parameter);
            }

            return true;
        }

        public void Execute(object parameter)
        {
            command(parameter);
        }

        public void RaiseCanExecuteChanged()
        {
            if (InternalCanExecuteChanged != null)
            {
                InternalCanExecuteChanged(this, EventArgs.Empty);
            }
        }

        event EventHandler ICommand.CanExecuteChanged
        {
            add { InternalCanExecuteChanged += value; }
            remove { InternalCanExecuteChanged -= value; }
        }

        internal EventHandler InternalCanExecuteChanged;
    }
}

你可以像这样使用它(比如说):

CaptureImageCommand = new DelegateCommand(ExecuteCaptureImageCommand,
                        x => ValidationContext.State == ValidationState.Valid);

事件到命令 / 方法

现在需要注意的一点是,在 WinRT 开发中,你无法再依赖于过去在 WPF/SL 中使用的老技巧,即使用 Blend.Interactivity DLLs。它们不再起作用。

有一个 CodePlex 的产品 http://winrttriggers.codeplex.com/,如果你想用的话。在本文中,我将坚持使用 StyleMVVM 为最常见的交互场景所提供的功能。这个场景就是**事件到命令**,这在大多数 MVVM 框架中也是相当标准的功能。

以下是 StyleMVVM 的实现方式。

在 XAML 中,你可以这样做:

 <ListView View:EventHandlers.Attach="ItemClick => ItemClicked($sender,$eventArgs)" />

在您的 ViewModel 中,您会这样做:

public void ItemClicked(FrameworkElement element,ItemClickEventArgs eventArgs)
{
    MessageBoxService.Show("You clicked: " + eventArgs.ClickedItem);
}

核心服务

本节概述了 StyleMVVM 提供的一些核心服务。我不会在本文中逐一介绍它们(因为一天之内没有那么多时间)。不过,您可以查看 StyleMVVM 附带的示例应用程序。该示例位于源代码中,路径类似于:

Release-3.0.3\Examples\ExampleApp

消息器 / 中介器

任何一个称职的 MVVM 框架都应该有一个弱事件消息组件,无论是基于内部 Rx 流的方法(这是我现在使用的方法),还是标准的弱事件处理中介器,都应该有一个。StyleMVVM 正如你所期望的那样,也拥有一个。Ian 称之为 IDispatchedMessager(看看它如何与 JavaScript 协同工作),它看起来是这样的:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using StyleMVVM.Data;

namespace StyleMVVM.Messenger
{
    public delegate void JSCallback(object message);

    /// <summary>
    /// IDispatchedMessenger allows you to send messages between loosely coupled consumers.
    /// Note: The default implementation of DispatchedMessenger uses WeakActions to call back consumers
    /// so there is no need to unregister for garbage collection reasons.
    /// </summary>
    public interface IDispatchedMessenger
    {
        /// <summary>
        /// Sends a message of Type T to consumers.
        /// </summary>
        /// <param name="message">message to send, cannot be null</param>
        void Send(object message);

        /// <summary>
        /// Registers an action as a consumer of type T messages.
        /// </summary>
        /// <typeparam name="T">message Type</typeparam>
        /// <param name="callback">message handler</param>
        void Register(object callback);

        /// <summary>
        /// Registers an action as a consumer of type T messages.
        /// </summary>
        /// <typeparam name="T">message Type</typeparam>
        /// <param name="callback">message handler</param>
        /// <param name="background">should it be called back on a background thread</param>
        /// <param name="holdReference"> </param>
        void Register(object callback, bool background, bool holdReference);

        /// <summary>
        /// Registers a method info on a target for callback later.
        /// </summary>
        /// <typeparam name="T">message Type</typeparam>
        /// <param name="target">handler object</param>
        /// <param name="methodName"> </param>
        /// <param name="background"> </param>
        /// <param name="holdReference"> </param>
        void RegisterMethod(object target, string methodName, bool background, bool holdReference);

        /// <summary>
        /// Registers a JavaScript callback
        /// </summary>
        void RegisterJSCallback(TypeWrapper messageType, JSCallback callback, bool background);

        /// <summary>
        /// Unregisters a consumer handler
        /// </summary>
        /// <param name="callback">action to unregister</param>
        void Unregister(object callback);

        /// <summary>
        /// Allows you to register all methods on an object marked with MessageHandler attributes 
        /// </summary>
        /// <param name="handlerObject"></param>
        void RegisterObjectHandlers(object handlerObject);

        /// <summary>
        /// Allows you to unregister all methods on an object marked with MessageHandler attributes 
        /// </summary>
        /// <param name="handlerObject"></param>
        void UnregisterObjectHandlers(object handlerObject);
    }
}

以下是您可能如何在 IDispatchedMessager 上发布内容的方式,顺便说一句,它是一个单例:

DispatchedMessenger.Instance.Send(new TextMessage {Text = TextMessage});

以下是如何在 IDispatchedMessager 上订阅特定消息的方法:

[MessageHandler]
public void TextMessageHandler(TextMessage message)
{
    TextMessage = message.Text;
}

注意:消息订阅默认是弱引用的。

IMessageBoxService

这是一个简单的 MessageDialog 包装器。一旦您看到该服务暴露的接口,就应该完全明白如何使用它。以下是暴露的接口:

using System.Collections.Generic;
using System.Threading.Tasks;
#if !DOT_NET
using Windows.Foundation;
#endif
#if !NETFX_CORE
using System.Windows;

#endif

namespace StyleMVVM.View
{
    /// <summary>
    /// This service gives you easy access to the MessageDialog class
    /// as well as creates IMessageDialog wrappers
    /// </summary>
    public interface IMessageBoxService
    {
        /// <summary>
        /// Opens a message box to the user with just the ok button
        /// </summary>
        /// <param name="message">message to display to the user</param>
        void Notify(string message);

        /// <summary>
        /// Opens a message box to the user with just the ok button
        /// </summary>
        /// <param name="message">message to display to the user</param>
        /// <param name="title">title on the message box</param>
        void Notify(string message, string title);

#if NETFX_CORE
        void Notify(string message, string title, string buttonText);
        
        /// <summary>
        /// Show a new message dialog
        /// </summary>
        /// <param name="message"></param>
        /// <returns>returns the command clicked</returns>
        IAsyncOperation<string> Show(string message);
        
        /// <summary>
        /// Show a new message dialog with the specified title and commands
        /// </summary>
        /// <param name="message">message to display</param>
        /// <param name="title">title for the message dialog</param>
        /// <param name="commands">command to show in the dialog</param>
        /// <returns>returns the command that was clicked</returns>
        IAsyncOperation<string> Show(string message, string title, params string[] commands);

        /// <summary>
        /// Creates a new message Dialog object that can be configured then shown
        /// </summary>
        /// <param name="message">message for the dialog</param>
        /// <param name="title">title for the dialog</param>
        /// <returns>returns a new message dialog object that you configure then show</returns>
        IMessageDialog MessageDialog(string message, string title);

        /// <summary>
        /// Show a new message dialog
        /// </summary>
        /// <param name="message"></param>
        /// <returns>returns the command clicked</returns>
        IAsyncOperation<SystemMessageBoxResult> ShowSystemMessageBox(string message);

        /// <summary>
        /// Show a new message dialog with the specified title and commands
        /// </summary>
        /// <param name="message">message to display</param>
        /// <param name="title">title for the message dialog</param>
        /// <param name="commands">command to show in the dialog</param>
        /// <returns>returns the command that was clicked</returns>
        IAsyncOperation<SystemMessageBoxResult> ShowSystemMessageBox(
               string message, string title, SystemMessageBoxButton button);

#else

        /// <summary>
        /// Show a new message dialog
        /// </summary>
        /// <param name="message"></param>
        /// <returns>returns the command clicked</returns>
        Task<string> Show(string message);

#if DOT_NET
    /// <summary>
    /// 
    /// </summary>
    /// <param name="message"></param>
    /// <param name="title"></param>
    /// <param name="buttonText"></param>
        void Notify(string message, string title, string buttonText);

        /// <summary>
        /// Show a new message dialog with the specified title and commands
        /// </summary>
        /// <param name="message">message to display</param>
        /// <param name="title">title for the message dialog</param>
        /// <param name="commands">command to show in the dialog</param>
        /// <returns>returns the command that was clicked</returns>
        Task<string> Show(string message, string title, params string[] commands);

        /// <summary>
        /// Shows a new message dialog with specified title and commands
        /// </summary>
        /// <param name="message"></param>
        /// <param name="title"></param>
        /// <param name="commands"></param>
        /// <returns></returns>
        Task<MessageBoxCommand> Show(string message,
                                     string title,
                                     MessageBoxCommand command,
                                     params MessageBoxCommand[] commands);

        /// <summary>
        /// Shows a new message dialog with specified title and commands
        /// </summary>
        /// <param name="message"></param>
        /// <param name="title"></param>
        /// <param name="commands"></param>
        /// <returns></returns>
        Task<MessageBoxCommand> Show(string message, string title, IEnumerable<MessageBoxCommand> commands);
#endif

        /// <summary>
        /// Show a new message dialog
        /// </summary>
        /// <param name="message"></param>
        /// <returns>returns the command clicked</returns>
        Task<SystemMessageBoxResult> ShowSystemMessageBox(string message);

        /// <summary>
        /// Show a new message dialog with the specified title and commands
        /// </summary>
        /// <param name="message">message to display</param>
        /// <param name="title">title for the message dialog</param>
        /// <param name="commands">command to show in the dialog</param>
        /// <returns>returns the command that was clicked</returns>
        Task<SystemMessageBoxResult> ShowSystemMessageBox(string message, 
                    string title, SystemMessageBoxButton button);
#endif
    }
}

基于此,我们可以简单地这样做:

var result = await messageBoxService.Show("There was a problem save the Patient data");

IFilePickerService

这是一个简单的 OpenFileDialog / SaveFileDialog 包装器。一旦你看到该服务暴露的接口,就应该完全明白如何使用它。以下是暴露的接口:

using System.Collections.Generic;
using System.Threading.Tasks;
using StyleMVVM.Utilities;
#if !DOT_NET
using Windows.Foundation;
using Windows.Storage;
using Windows.Storage.Pickers;

#endif

namespace StyleMVVM.View
{
    /// <summary>
    /// The IFilePickerService acts as a wrapper around the default file picker class
    /// </summary>
    public interface IFilePickerService
    {
#if NETFX_CORE
        /// <summary>
        /// Opens a file picker and allows the user to pick multiple
        /// files and return a list of the files the user chose
        /// </summary>
        /// <param name="location"></param>
        /// <param name="filterTypes"></param>
        /// <returns></returns>
        IAsyncOperation<IReadOnlyList<StorageFile>> PickMultipleFilesAsync(
            PickerLocationId location, params string[] filterTypes);

        /// <summary>
        /// Opens a file picker and allows the user to pick one file and returns a StorageFile to the caller
        /// </summary>
        /// <param name="location"></param>
        /// <param name="filterTypes"></param>
        /// <returns></returns>
        IAsyncOperation<StorageFile> PickFileAsync(PickerLocationId location, params string[] filterTypes);
#elif WINDOWS_PHONE
        /// <summary>
        /// Opens a file picker and allows the user to pick multiple
        /// files and return a list of the files the user chose
        /// </summary>
        /// <param name="location"></param>
        /// <param name="filterTypes"></param>
        /// <returns></returns>
        Task<IReadOnlyList<StorageFile>> PickMultipleFilesAsync(
            PickerLocationId location, params string[] filterTypes);

        /// <summary>
        /// Opens a file picker and allows the user to pick one file and returns a StorageFile to the caller
        /// </summary>
        /// <param name="location"></param>
        /// <param name="filterTypes"></param>
        /// <returns></returns>
        Task<StorageFile> PickFileAsync(PickerLocationId location, params string[] filterTypes);
#elif DOT_NET
        /// <summary>
        /// Opens a file picker and allows the user to pick multiple files and return a list of the files the user chose
        /// </summary>
        /// <param name="location"></param>
        /// <param name="filterTypes"></param>
        /// <returns></returns>
        Task<IReadOnlyList<string>> PickMultipleFilesAsync(
            PickerLocationId location, params string[] filterTypes);

        /// <summary>
        /// Opens a file picker and allows the user to pick one file and returns a StorageFile to the caller
        /// </summary>
        /// <param name="location"></param>
        /// <param name="filterTypes"></param>
        /// <returns></returns>
        Task<string> PickFileAsync(PickerLocationId location, params string[] filterTypes);

        /// <summary>
        /// Opens the Save file dialog allowing a user to pick a file to save to
        /// </summary>
        /// <param name="location"></param>
        /// <param name="filterTypes"></param>
        /// <returns></returns>
        Task<string> PickSaveFileAsync(PickerLocationId location, params string[] filterTypes);
#endif
    }
}

基于此,我们可以简单地这样做:

StorageFile storageFile = await FilePicker.PickFileAsync(PickerLocationId.DocumentsLibrary, ".txt");

ICharmService

此服务控制设置窗格的创建并激活超级按钮。本文将不涉及此内容,请参阅 StyleMVVM 示例。

ITileService

该接口提供了 TileUpdater 的包装,并提供了一种简单的方式来生成磁贴更新。本文将不涉及此内容,请参阅 StyleMVVM 示例。

IBadgeService

围绕徽章功能的服务包装器。本文将不涉及此内容,请参阅 StyleMVVM 示例。

IToastService

代表一个允许您创建 Toast 通知的服务。本文将不涉及此内容,请参阅 StyleMVVM 示例。

ITransformService

该服务提供了一种无需编写代码即可将一种类型转换为另一种类型的方法。这在从 ViewModel 转换为 Model 或反之亦然时可能很有用。本文将不涉及此内容,请参阅 StyleMVVM 示例。

就这样

总之,这次我想说的就这些了。我相信你们会同意 Ian 在 StyleMVVM 上的工作非常出色,而且使用这个出色的框架在某种程度上弥补了目前 Windows 8 的各种麻烦。如果不是 StyleMVVM,我在当前状态下的 WinRT 开发中肯定会完全抓狂。唯一让我坚持下来的是 Ian 的代码能够正常工作,并且开箱即用就能完全满足我的需求。而当它不能满足时,我告诉他,他就会添加相应的功能,真是个好人。

下次我们将看一个我用 StyleMVVM 制作的演示应用,它使用了上面概述的大部分功能,还与摄像头进行了交互,利用了搜索协定,并使用了一个小型的 SQLite 数据库,使得这个演示应用更贴近真实世界的场景。

希望我能尽快把那篇文章写出来。如果你等不及想看看它,可以在本文顶部获取下一篇文章的演示应用。以下是下一篇文章演示应用功能的简要概述:

  • 用于捕获患者详细信息的页面(期望在此处为患者捕获图像)
  • 根据已知医生列表创建预约
  • 查看某一天的预约
  • 深入查看特定医生的预约
  • 显示某一天的统计数据(简单的饼图,显示某一天每位医生安排了多少预约)
  • 已实现搜索协定,这样你就可以直接从 Windows 8 的搜索窗格中搜索演示应用。

一如既往,欢迎任何投票/评论,我相信 Ian 也会很感激大家试用他的心血之作(当然是 StyleMVVM)。

© . All rights reserved.