Messenger和MVVM中的View Services

在前面的文章IoC容器和MVVM中, 介绍了IoC容器如何在大量用户类中帮助创建和分配用户类的实例。本文将介绍IoC容器如何帮助应用程序解耦,比如那些根据MVVM模式开发的应用。此模 式广泛应用在基于XAML的应用程序(Silverlignt, WPF, Windows Phone, Windows 8)中,因为此模式与数据绑定系统和用于这类程序设计的工具匹配的很好,尤其是在VS 设计器和Blend中。

在典型的XAML程序中,开发者利用数据绑定系统声明一个XAML UI元素的属性和应用程序中其他对象的属性之间的同步。这种绑定可以是单方向和双方向的。数据绑定非常方便,特别是在用可视化设计器的时候,比如VS设计 器或Blend。但这也有局限性。例如:一个简单的数据绑定不能触发UI中的动画或触发一个显示给用户的对话框。即使是基本的动作比如迁移到其他页面,这 也不能通过数据绑定来实现。

Figure 1 shows two-way data binding between a XAML view and its ViewModel (point 1). In addition, other possible interactions are represented.

Figure 1. Dependencies between Layers in MVVM

箭头2表明了一个普通的事件流。这可能是在XAML程序中和后台代码进行交互的最有名的方式。很多来自传统开发环境中的开发者对此也都很熟悉,比如 Windows Forms甚至是HTML/JavaScript。事件方式很有用,但是在XAML程序需要和代码进行解耦时则会引发问题。一种情况是XAML中的 DataTemplate需要移动到ResourceDictionary中的时候,另一种情况是处理事件的代码需要从后台移动到另一个对象,比如 ViewModel。在XAML和后台代码之间,事件导致了紧耦合,并且限制了代码的重构。

箭头3表明了XAML在代码中触发一个动作的另外一种方式。通常情况下,命令通过ViewModel的一个属性(实现了 ICommand接口)暴露出来,然后绑定到XAML UI元素上。例如:按钮支持命令属性,当按钮按下时,绑定的命令将会被触发。命令也有它的局限性,尤其是只有少数的UI元素才有Command属性并且只 能为一个事件(通常是Click事件)使用。如果有其他的事件需要处理,默认的命令显然是不够的。在这个系列文章中也会讨论命令局限性的解决办法,尤其是 使用MVVM Light的RelayCommand组件。

箭头4表明View的后台代码在ViewModel中直接调用方法,这听起来好像和View需要和ViewModel解耦是相违背的。事实上,View知 道它对应的ViewModel并不是问题,因为View很少需要抽象。一个View的后台代码很少需要单元测试。因为编程地触发一个事件来测试它的动作不 是那么简单。因此,ViewModel应该不知道View的存在,而反过来则没必要。下面的代码在MVVM程序中经常看到。根据MVVM的一个原则,开发 者应该保持后台代码量少,但有时用一点点后台代码来处理特殊情况比寻求一个复杂的解决方案来的更简单。

Figure 2. Getting the ViewModel in the View’s Code-Behind

public MainViewModel Vm
{  get
  {
    return (MainViewModel)DataContext;
  }
}

    在Windows 8中,从View中调用ViewModel是为了减轻一个问题:在绑定中没有UpdateSourceTrigger属性。而在WPF则不是这样。当一个 TextBox的Text属性绑定到ViewModel中的String类型字段时,我们可以使用下面这样的代码(只能在WPF中)

Figure 3. UpdateSourceTrigger Property in WPF XAML Markup

<TextBox Text="{Binding ZipCode,
    Mode=TwoWay,
    UpdateSourceTrigger=PropertyChanged}"
  Style="{StaticResource ZipCodeTextBoxStyle}" />

上面的代码能够运行是因为UpdateSourceTrigger的PropertyChanged,这个绑定在用户每次输入字符的时候都会触发。在 UpdateSourceTrigger中还有一个值是LostFocus,这意味着当用户把焦点移动到其他控件的时候,此绑定将会被触发。

在Windows RT中,绑定系统中并没有这个属性。而双向绑定在TextBox失去焦点时总会被触发。大部分情况下这都没问题,但这也会引发多余的问题。例如:假设显示一个验证对话框给用户,在用户输入的情况下更新验证对话框会是一个不错的体验。

Figure 4. Working Around the Lack of UpdateSourceTrigger.PropertyChanged in Windows RT

<!-- XAML markup -->
<TextBox Text="{Binding ZipCode, Mode=TwoWay}"
    TextChanged="ZipCodeTextChanged"
    Style="{StaticResource ZipCodeTextBoxStyle}" />
// Code behind
private void ZipCodeTextChanged(object sender, TextChangedEventArgs e)
{
  var textbox = (TextBox)sender;
  Vm.ZipCode = textbox.Text;
}

此时,ViewModel的ZipCode属性在用户输入字符的都回更新,这也会更新显示给用户的验证对话框的内容。

ViewModel到View的通信

有些人可能已经主要到在图1中没有从ViewModel到View的通信方式。文章的前面也说过,ViewModel应该对于使用它的View的存在是未知的。事实上,将一个ViewModel用于多个View是非常普遍的。

解决这个问题有很多途径。在这里要介绍的两个方案是使用MVVM Light的Messenger类和使用view services。

首先,要使用MVVM Light的Messenger实现一个状态消息系统。此类是消息总线的一个实现,它实现了消息发送方和消息接收方之间的解耦。这对于状态消息系统来说是十分方便的,因为应用程序的每一部分都可以选择显示给用户一个状态而不用担心依赖性。

一个关于Messenger的警告

Messenger是一个强大的组件,它可以极大地促进通信,但同时也会使得代码很难调试。因为消息的发送方和接收方相互之间都是未知的,所有也就很难第一眼就看出哪个接收方接收了消息。需要小心使用。

在MVVM Light工具包的GalaSoft.MvvmLight.Messaging名称空间下有很多预定义的消息类。同时也很方便定义自己的消息。比如在RssReader例子,在此会使用一个状态消息显示给用户来提示用户当前应用的状态。

首先,定义一个状态消息类,代码如下所示。

Figure 5. Defining the Message Type

public class StatusMessage
{
  public StatusMessage(
    string status,
    int timeoutMilliseconds)
  {
    Status = status;
    TimeoutMilliseconds = timeoutMilliseconds;
  }
  public string Status
  {
    get; private set;
  }
  public int TimeoutMilliseconds
  {
    get; private set;
  }
}

MainViewModel的Refresh方法要轻微地修改一下,代码如下。在调用异步方法之前,发送一个StatusMessage。在数据接收和处 理之后,再发送一个生命期为3秒的StatusMessage。这个前提条件是有消息接收方注册了并且接收方知道处理这个消息。 StatusMessage是发送方和接收方之间的合约。

Figure 6. Setting the Status

public async Task Refresh()
{
  Items.Clear();
  Messenger.Default.Send(new StatusMessage("Getting articles", 0));
  var list = await _rssService.GetArticles();
  foreach (var item in list)
  {
    Items.Add(item);
  }
  Messenger.Default.Send(new StatusMessage("Done", 3000));
}

    在view侧,需要一个类来显示这个状态(此时这里是MainPage),并且需要注册接收 StatusMessage类型。为了尽可能地保持Messenger简单清晰,我们将在OnNavigatedTo方法中注册消息处理方法,在 OnNavigatingFrom注销此方法。这个方法确保一次只有一个页面注册显示状态信息。

Figure 7. Showing the Status in Windows RT

protected override void OnNavigatedTo(NavigationEventArgs e)
{
  Messenger.Default.Register<StatusMessage>(
    this, HandleStatusMessage);
  base.OnNavigatedTo(e);
}
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
  Messenger.Default.Unregister<StatusMessage>(
    this, HandleStatusMessage);
  base.OnNavigatingFrom(e);
}
private void HandleStatusMessage(StatusMessage msg)
{
  Status.Message = msg.Status;
  Status.Show(msg.TimeoutMilliseconds);
}

      页面使用一个名字为Status的UserControl来显示消息。当需要显示消息是,设置此控件的Visibility属性为Visibility。当显示时间完了,将Visibility属性设置为Collapsed。

实现一个DialogService

使用Messenger服务从ViewModel显示一个消息到view是一个比较不错的方案。但是其中一个缺点就是对于当一个第一次看此代码的开发者来 说就不是那么清晰。因为消息的发送方和接收方互不知道。调试的时候工作流很难跟踪。一个有意思的替代方案就是向IoC容器中注册一个抽象的服务。在 RssService中,DialogService就是一个面向view的服务。

首先,在服务接口中定义一些常用到的方法。比如显示状态信息,错误信息等。在示例代码中,定义的方法如下。

Figure 8. The IDialogService Interface

public interface IDialogService
{
  void ShowMessage(string message, string title, string buttonText);
  void ShowError(string errorMessage, string title, string buttonText);
  void ShowError(Exception error, string title, string buttonText);
}

    接口的实现可以根据使用平台的不同而不同。为了保持简单,示例代码Windows Phone中使用了默认的MessageBox,Windows 8中使用了默认的MessageDialog。在现实工程中,可以使用自定义的消息框。DialogService的Windows RT实现版本如下所示。

Figure 9. Implementation of IDialogService in MainPage for Windows RT

public sealed partial class MainPage : IDialogService
{
  public MainPage()
  {
    InitializeComponent();
  } 

  // More methods removed for brevity... 

  // IDialogService implementation 

  public async void ShowMessage(string message, string title, string buttonText)
  {
    // Using the same MessageDialog for errors and messages.
    // In a real-life production implementation, a custom
    // dialog box would be designed, and these methods would probably
    // be passed into a Page base class (for instance in
    // the LayoutAwarePage class in the Common folder.
    var dialog = new MessageDialog(message, title); 

    dialog.Commands.Add(new UICommand(buttonText)); 

    dialog.CancelCommandIndex = 0;
    await dialog.ShowAsync();
  } 

  public void ShowError(string errorMessage, string title, string buttonText)
  {
    // Using the same MessageDialog for errors and normal messages.
    ShowMessage(errorMessage, title, buttonText);
  } 

  public void ShowError(Exception error, string title, string buttonText)
  {
    ShowMessage(error.Message, title, buttonText);
  }
}

    MainPage需要将它自己视为一个IDialogService注册到IoC容器中。因为一次只能由一个页面显示,所以同时需要在迁移到其他页面时注销。我们在OnNavigatedTo和OnNavigatedFrom方法中做这两个操作。

Figure 10. Registering and Unregistering the IDialogService in MainPage

protected override void OnNavigatedTo(NavigationEventArgs e)
{
  Messenger.Default.Register<StatusMessage>(
    this, HandleStatusMessage); 

  SimpleIoc.Default.Register<IDialogService>(() => this); 

  base.OnNavigatedTo(e);
} 

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
  Messenger.Default.Unregister<StatusMessage>(
    this, HandleStatusMessage); 

  SimpleIoc.Default.Unregister<IDialogService>(); 

  base.OnNavigatedFrom(e);
}

     在MainPageViewModel中,通过从IoC容器获取IDialogService并且通过一个属性字段暴露出来。代码如下。

Figure 11. Refresh Method with Error Handling

public IDialogService DialogService
{
  get
  {
    return ServiceLocator.Current.GetInstance<IDialogService>();
  }
} 

public async Task Refresh()
{
  Items.Clear(); 

  try
  {
    Messenger.Default.Send(new StatusMessage("Getting articles", 0)); 

    var list = await _rssService.GetArticles(); 

    if (list.Count == 0)
    {
      DialogService.ShowMessage(
        "We couldn‘t find any articles",
        "Nothing found",
        "Too bad!");
    } 

    foreach (var item in list)
    {
      Items.Add(item);
    } 

    Messenger.Default.Send(new StatusMessage("Done", 3000));
  }
  catch (Exception ex)
  {
    // Hide status
    Messenger.Default.Send(new StatusMessage(string.Empty, 1)); 

    DialogService.ShowError(
      ex,
      "Error when loading",
      "Oops");
  }
}

原文地址:http://msdn.microsoft.com/en-us/magazine/jj694937.aspx

源代码:http://archive.msdn.microsoft.com/mag201303mvvm2

时间: 2024-08-28 12:18:02

Messenger和MVVM中的View Services的相关文章

Messenger在MVVM模式中的应用

Messenger在MVVM模式中的应用 Messenger在MVVM中应用的前提 我们知道在MVVM架构中,系统平台的Silverlight客户端界面开发和业务逻辑已经被分开,XAML是SL的主要部分,界面设计者只需要绑定ViewModel里的数据即可.但是在ViewModel里有些时候是需要界面发出响应的,在这种情况下,Messenger显示出它的用处. Messenger的架构 Messager构件代码 定义Imessager接口 注册一个接收消息的类型,比如某一控件来接收消息 void

WPF MVVM 如何在ViewModel中操作View中的控件事件

(在学习Wpf的时候,做一个小例子,想在TextBox改变后,检验合法性,并弹出提示.在找了很多贴后,发现这个小例子,抄袭过来,仅供参考.) 虽然说MVVM模式下不建议在Viewmodel层中操控View层中控件,但是在某些情况下,比如想要得到某个事件的参数,在Viewmodel层中不太方便实现,这时候就可以用下面的方法了. 在XAML中 1.引用组件并设置 xmlns:Interaction="http://schemas.microsoft.com/expression/2010/inter

WPF MVVM中在ViewModel中关闭或者打开Window

这篇博客将介绍在MVVM模式ViewModel中关闭和打开View的方法. 1. ViewModel中关闭View public class MainViewModel { public DelegateCommand<Window> CloseWindowCommand { get; private set; } public MainViewModel() { CloseWindowCommand = new DelegateCommand<Window>(CloseWindo

在MVVM中使用PasswordBox控件

在MVVM中使用PasswordBox控件,碰到一个问题.由于**PasswordBox.Password**属性并不是一个依赖属性,所以无法将其作为Binding的目标. # 使用附加属性的解决方案 ![Password Demo.gif](http://upload-images.jianshu.io/upload_images/140233-dbd415eb4cf9aeb2.gif) **思路:**定义两个依赖属性**Attach**和**AttachPassoword** Attatch

Android组件Activity中的View绘画和动画(Animation)是否会重画?

Activity 就是Android中的活动,是Android系统中唯一一个可见组件. Activity中官网中有一句话: The visible lifetime of an activity happens between a call to onStart() until a corresponding call to onStop() 这句话的意思是可以看见Activity的生命周期是从 调用onStart()方法开始 直到调用onStop()方法.这句话开始我就理解错误了.因为设置Ac

解决在onCreate()过程中获取View的width和Height为0的4中方法

很经常当我们动态创建某些View时,需要通过获取他们的width和height来确定别的view的布局,但是在onCreate()获取view的width和height会得到0.view.getWidth()和view.getHeight()为0的根本原因是控件还没有完成绘制,你必须等待系统将绘制完View时,才能获得.这种情况当你需要使用动态布局(使用wrap_content或match_parent)就会出现.一般来讲在Activity.onCreate(...).onResume()方法中

Activity中获取view的高度和宽度为0的原因以及解决方案

在activity中可以调用View.getWidth.View.getHeight().View.getMeasuredWidth() .View.getgetMeasuredHeight()来获得某个view的宽度或高度,但是在onCreate().onStrart().onResume()方法中会返回0,这是应为当前activity所代表的界面还没显示出来没有添加到WindowPhone的DecorView上或要获取的view没有被添加到DecorView上或者该View的visibili

spring mvc DispatcherServlet详解之三---request通过ModelAndView中获取View实例的过程

整个spring mvc的架构如下图所示: 上篇文件讲解了DispatcherServlet第二步:通过request从Controller获取ModelAndView.现在来讲解第三步:request 从ModelAndView中获取view对象. 获取view对象一般是通过viewResolver来解析view name来完成的.若ModelAndView中view 不存在或者ModelAndView本身为null则填充默认值.代码如下: ModelAndView中view 不存在或者Mod

Android中自定义View的MeasureSpec使用

有时,Android系统控件无法满足我们的需求,因此有必要自定义View.具体方法参见官方开发文档:http://developer.android.com/guide/topics/ui/custom-components.html 一般来说,自定义控件都会去重写View的onMeasure方法,因为该方法指定该控件在屏幕上的大小. protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) onMeasure传