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

WPF:如果 Carlsberg 做了 MVVM 框架,第二部分(共 n 部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (110投票s)

2009年7月19日

CPOL

30分钟阅读

viewsIcon

869704

大概会是 Cinch,一个用于 WPF 的 MVVM 框架。

目录

Cinch 文章系列链接

介绍

上次我只是介绍了 Cinch 框架,这次我们将对 Cinch 及其内部机制进行演练。这篇文章会很长,所以实际上被分成了两篇。在这篇文章中,我将涵盖以下内容:

  • 允许视图在不维护任何硬引用链接且无需 IView 接口要求的情况下,将生命周期事件传达给 ViewModel。视图和 ViewModel 之间没有任何链接。
  • 为常见任务提供了一些附加行为,例如:
    • 数字文本输入
    • 基于 XAML FrameworkElementRoutedEvent 在 ViewModel 中运行 ICommand
    • 为单个 XAML FrameworkElement 提供一组此类 ICommand/RoutedEvent 事件。
  • 允许 ViewModel 确定模型数据是否可编辑;UI 仅通过绑定更新,基于 ViewModel 驱动的可编辑状态。这在单个模型字段级别可用,因此非常灵活。
  • 委派验证规则,允许验证规则尽可能细粒度。
  • 使用委派规则方法进行原生 IDataErrorInfo 支持。
  • IEditableObject 的用法,用于在编辑/取消编辑时存储/恢复对象状态。
  • 弱事件创建,允许创建 WeakEvent
  • 弱事件订阅,也允许自动取消订阅。
  • 中介者消息传递,开箱即用地支持 WeakReference。

所以我想唯一的方法就是开始,那么我们开始吧,好吗?但在我们这样做之前,我需要重复一下特别感谢的部分,并增加一位,保罗·斯托维尔(Paul Stovell),我上次忘了把他加进去。

先决条件

演示应用程序使用了

  • VS2008 SP1
  • .NET 3.5 SP1
  • SQL Server(请参阅 MVVM.DataAccess 项目中的 README.txt 以了解演示应用程序数据库需要设置什么)

特别感谢

在开始之前,我想特别感谢以下人员,没有他们,这篇文章以及后续的文章系列将是不可能完成的。基本上,我用 Cinch 所做的是研究了这些人的大部分作品,了解了什么好,什么不好,然后想出了 Cinch,我希望它能解决其他框架未涵盖的一些新领域。

  • Mark Smith (Julmar Technology),他出色的 MVVM 辅助库,极大地帮助了我。Mark,我知道,我曾请求你允许我使用你的一些代码,你非常慷慨地同意了,但我只想说非常感谢你那些很棒的想法,其中一些我真的没有想到过。我向你致敬,伙计。
  • Josh Smith / Marlon Grech(作为一个整体)他们出色的中介者实现。你们俩太棒了,一直都很愉快。
  • Karl Shifflett / Jaime Rodriguez(微软的哥们)他们出色的 MVVM Lob 游览,我参加了,干得好,伙计们。
  • Bill Kempf,仅仅因为他是 Bill,而且是一位疯狂的程序员,他还有一个很棒的 MVVM 框架叫做 Onyx,我很久以前写过一篇 文章。Bill 总是能回答棘手的问题,谢谢 Bill。
  • Paul Stovell 他出色的 委派验证想法,Cinch 使用它来验证业务对象。
  • 所有 WPF Disciples 的成员,在我看来,是最好的在线社区。

谢谢你们,伙计/女孩,你们懂的。

Cinch 内部机制 I

本节将开始深入探讨 Cinch 的内部机制。如我所说,内容太多,无法在一篇文章中讲完,所以我将 Cinch 的内部机制分成两篇文章。希望这里会有一些有用的东西。嗯,我希望如此。

视图生命周期事件

在使用 MVVM 模式进行开发时,理想状态是视图和 ViewModel 之间完全没有耦合。Cinch 实现了视图和 ViewModel 之间的这种清晰分离,它们之间根本没有联系,这很棒。然而,有时 ViewModel 了解某些视图事件是有益的,例如:

  • Loaded (加载)
  • Activated (激活)
  • Deactivated (停用)
  • Close

但我们刚才说过视图/ViewModel 之间没有引用,并且我们想在 ViewModel 中了解这些视图事件,那么我们如何实现呢?答案在于附加行为。我们所做的是在 ViewModel 中有一个基于 ICommand 的属性,我们使用附加的视图生命周期行为,并将正确的视图事件附加到正确的 ViewModel ICommand。因此,当引发视图事件时,它实际上会调用 ViewModel 中的 ICommand 实现。

Cinch 通过两种方式提供这一点:

  1. 一个名为 ViewModelBase 的 Cinch ViewModel 基类,它已经包含了您需要的所有视图生命周期 ICommand 实现,我应该指出,这些实现可以在 Cinch ViewModelBase 类的继承者中被覆盖。
  2. 一些视图生命周期附加行为。

让我们以一个视图生命周期事件 Activated 为例,看看它是如何工作的;其他事件也是一样的。

ViewModelBase 类开始,从下面的代码(为清晰起见已删除额外代码)可以看出,有一个名为 ActivatedICommand,其中精简后的 Cinch ViewModelBase 如下所示。

Cinch 还为每个视图生命周期事件提供了公共可绑定 (INPC) 属性(请注意,UserControl 没有 Activated/Deactivated,它们仅在 Window 上可用)。这些属性在 Cinch ViewModelBase 中设置,因此覆盖者请注意,如果您在覆盖生命周期方法时未能调用 Cinch ViewModelBase,则这些属性将不起作用。总之,这是 Activated 命令的完整示例,其余的也一样。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

using System.Linq.Expressions;

namespace Cinch
{

    public abstract class ViewModelBase : INotifyPropertyChanged, 
                    IDisposable, IParentablePropertyExposer
    {
        private SimpleCommand activatedCommand;
        private Boolean isActivated=false;


        /// <summary>
        /// Constructs a new ViewModelBase and wires up all the Window based Lifetime
        /// commands such as activatedCommand/deactivatedCommand/
        ///                  loadedCommand/closeCommand
        /// </summary>
        public ViewModelBase() : this(new UnityProvider())
        {

        }

        public ViewModelBase(IIOCProvider iocProvider)
        {
            activatedCommand= new SimpleCommand
            {
                CanExecuteDelegate = x => true,
                ExecuteDelegate = x => OnWindowActivated()
            };
        }

        /// <summary>
        /// Allows Window.Activated/UserControl.Activated hook
        /// Can be overriden if required in inheritors
        /// </summary>
        protected virtual void OnWindowActivated()
        {
            //Will only work as long as people, call base.OnWindowActivated()
            //when overriding
            IsActivated= true; 
        }

        /// <summary>
        /// ActivatedCommand : Window/UserControl Lifetime command
        /// </summary>
        public SimpleCommand ActivatedCommand 
        {
            get { return activatedCommand ; }
        }

        /// <summary>
        /// View Is Activated
        /// </summary>
        static PropertyChangedEventArgs isactivatedChangeArgs =
            ObservableHelper.CreateArgs<ViewModelBase>(x => x.IsActivated);

        /// <summary>
        /// Will only be reliable if users call base.OnWindowActivated()
        /// when overriding virtual ViewModelBase.OnWindowActivated()
        /// </summary>
        public Boolean IsActivated
        {
            get { return isActivated; }
            private set
            {
                isActivated = value;
                NotifyPropertyChanged(isactivatedChangeArgs );
            }
        }
    }
}

细心的您可能会注意到上面代码中有一个名为 ActivatedCommand 的属性,它实际上返回一个 SimpleCommand,而不是您可能期望的 ICommand,并且可能在其他文章中也见过。这都没问题,别担心,XAML 解析器和绑定系统足够智能,知道任何实现 ICommand 的类都可以用作绑定中的 ICommand。将 ICommand 暴露为 SimpleCommand 的原因是 SimpleCommand 有一个额外的属性可用于单元测试。即 CommandSucceeded,在命令执行完成后设置为 true,因此可用于单元测试的 Assert 语句。

这是 SimpleCommand 代码

using System;
using System.Windows.Input;

/// <summary>
/// This class provides a simple
/// delegate command that implements the ICommand
/// interface
/// </summary>
namespace Cinch
{
    /// <summary>
    /// Implements the ICommand and wraps up all the verbose stuff so that you 

    /// can just pass 2 delegates 1 for the CanExecute and one for the Execute
    /// </summary>

    public class SimpleCommand : ICommand
    {
        #region Public Properties
        public Boolean CommandSucceeded { get; set; }

        /// <summary>
        /// Gets or sets the Predicate to execute when the 
        /// CanExecute of the command gets called
        /// </summary>
        public Predicate<object> CanExecuteDelegate { get; set; }

        /// <summary>
        /// Gets or sets the action to be called when the 
        /// Execute method of the command gets called
        /// </summary>

        public Action<object> ExecuteDelegate { get; set; }
        #endregion

        #region ICommand Members

        /// <summary>
        /// Checks if the command Execute method can run
        /// </summary>
        /// <param name="parameter">
        ///    The command parameter to be passed</param>

        /// <returns>Returns true if the command can execute. 
        /// By default true is returned so that if the user of SimpleCommand 
        /// does not specify a CanExecuteCommand delegate the command 
        /// still executes.</returns>
        public bool CanExecute(object parameter)
        {
            if (CanExecuteDelegate != null)
                return CanExecuteDelegate(parameter);
            return true;// if there is no can execute default to true
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        /// <summary>
        /// Executes the actual command
        /// </summary>
        /// <param name="parameter">
        ///       THe command parameter to be passed</param>

        public void Execute(object parameter)
        {
            if (ExecuteDelegate != null)
            {
                ExecuteDelegate(parameter);
                CommandSucceeded = true;
            }
        }

        #endregion
    }
}

好的,现在我们有了一个 ViewModel,它有一个用于 ActivatedSimpleCommand,它将调用一个 protected virtual void OnWindowActivated() 方法,但是这个 Activated SimpleCommand 是如何被执行的呢?回想一下,我曾说过有一些附加行为的魔法,所以我们接下来看看那个。

首先,这是视图将如何连接视图 Activated 附加行为:

<Window x:Class="MVVM.Demo.Views.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Cinch="clr-namespace:Cinch;assembly=Cinch"
    Title="Window1" Height="300" Width="300">

      Cinch:LifetimeEvent.Activated="{Binding ActivatedCommand}"
      ....
      ....
</Window>

然后,这将使用下面显示的附加视图生命周期事件。

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;

/// <summary>
/// This allows a Windows lifecycle events to call ICommand(s)
/// within a ViewModel. This allows the ViewModel to know something
/// about the Views lifecycle without the need for a strong link
/// to the actual View
/// </summary>

namespace Cinch
{
    /// <summary>
    /// This class is used to attach the Window lifetime events to ICommand implementations.
    /// It allows a ViewModel to hook into the lifetime of the view (when necessary) 
    /// through simple XAML tags. Supported events are Loaded, Activated, Deactivated 
    /// and Closing/Closed. For the Closing/Closed event, the CanExecute handler is invoked
    /// in response to the Closing event - if it returns true, then the Closed event is 
    /// allowed and the Execute handler is called in response.
    public static class LifetimeEvent
    {
        #region Activated
        /// <summary>
        /// Dependency property which holds the ICommand for the Activated event
        /// </summary>
        public static readonly DependencyProperty ActivatedProperty =
            DependencyProperty.RegisterAttached("Activated", 
            typeof(ICommand), typeof(LifetimeEvent),
                new UIPropertyMetadata(null, OnActivatedEventInfoChanged));

        /// <summary>

        /// Attached Property getter to retrieve the ICommand
        /// </summary>
        /// <param name="source">Dependency Object</param>
        /// <returns>ICommand</returns>

        public static ICommand GetActivated(DependencyObject source)
        {
            return (ICommand)source.GetValue(ActivatedProperty);
        }

        /// <summary>
        /// Attached Property setter to change the ICommand
        /// </summary>
        /// <param name="source">Dependency Object</param>
        /// <param name="command">ICommand</param>

        public static void SetActivated(DependencyObject source, ICommand command)
        {
            source.SetValue(ActivatedProperty, command);
        }

        /// <summary>
        /// This is the property changed handler for the Activated property.
        /// </summary>
        /// <param name="sender">Dependency Object</param>
        /// <param name="e">EventArgs</param>

        private static void OnActivatedEventInfoChanged(DependencyObject sender, 
            DependencyPropertyChangedEventArgs e)
        {
            var win = sender as Window;
            if (win != null)
            {
                win.Activated -= OnWindowActivated;
                if (e.NewValue != null)
                    win.Activated += OnWindowActivated;
            }
        }

        /// <summary>
        /// This is the handler for the Activated event to raise the command.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>

        private static void OnWindowActivated(object sender, EventArgs e)
        {
            var dpo = (DependencyObject)sender;
            ICommand activatedCommand = GetActivated(dpo);
            if (activatedCommand != null)
                activatedCommand.Execute(GetCommandParameter(dpo));
        }
        #endregion
    }
}

对于以下事件,使用相同的机制:

  • 视图 Deactivated 事件,其中 Cinch ViewModelBase 中的 SimpleCommand 称为 DeactivatedCommand,Cinch ViewModelBase 中的虚拟方法称为 OnWindowDeactivated()
  • 视图 Closing/Closed 事件,其中 Cinch ViewModelBase 中的 SimpleCommand 称为 Closeommand,Cinch ViewModelBase 中的虚拟方法称为 OnWindowClose()
  • 视图 Loaded 事件,其中 Cinch ViewModelBase 中的 SimpleCommand 称为 LoadedCommand,Cinch ViewModelBase 中的虚拟方法称为 OnWindowLoaded()

同样,不要忘记 Cinch ViewModelBase 不提供标准实现,因为此功能应该通过覆盖继承自 Cinch ViewModelBase 的 ViewModel 中的这些方法来完成。

所以,您所要做的就是继承 Cinch ViewModelBase,然后您就可以使用这些视图生命周期事件/属性了,只需记住调用基类方法即可。

数字文本框附加行为

开发 LOB 应用程序时的一个非常常见的要求是文本框只能输入数字。虽然这可以通过使用正则表达式、一些代码隐藏以及可能的 ValidationRule 来实现,但如果从一开始就不允许文本框接受数字以外的任何内容,难道不更简单吗?为此,Cinch 包含了一个 NumericTextBoxBehavior 附加行为,如下所示。您还应该注意,此行为会通过 DataObject 粘贴事件来处理粘贴的文本。

using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System;
using System.Linq;
using System.Windows.Controls;
using System.Text.RegularExpressions;

/// <summary>
/// This forces a TextBoxBase control to be numeric-entry only
/// By using an attached behaviour
/// </summary>
namespace Cinch
{
    /// <summary>
    /// This forces a TextBoxBase control to be numeric-entry only
    /// </summary>
    /// <example>
    /// <![CDATA[  <TextBox Cinch:NumericTextBoxBehavior.IsEnabled="True" />  ]]>
    /// </example>
    public static class NumericTextBoxBehavior
    {
        #region IsEnabled DP
        /// <summary>
        /// Dependency Property for turning on numeric behavior in a TextBox.
        /// </summary>
        public static readonly DependencyProperty IsEnabledProperty =
            DependencyProperty.RegisterAttached("IsEnabled",
                typeof(bool), typeof(NumericTextBoxBehavior),
                    new UIPropertyMetadata(false, OnEnabledStateChanged));

        /// <summary>
        /// Attached Property getter for the IsEnabled property.
        /// </summary>
        /// <param name="source">Dependency Object</param>
        /// <returns>Current property value</returns>
        public static bool GetIsEnabled(DependencyObject source)
        {
            return (bool)source.GetValue(IsEnabledProperty);
        }

        /// <summary>
        /// Attached Property setter for the IsEnabled property.
        /// </summary>
        /// <param name="source">Dependency Object</param>
        /// <param name="value">Value to set on the object</param>
        public static void SetIsEnabled(DependencyObject source, bool value)
        {
            source.SetValue(IsEnabledProperty, value);
        }

        /// <summary>
        /// This is the property changed handler for the IsEnabled property.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void OnEnabledStateChanged(DependencyObject sender,
            DependencyPropertyChangedEventArgs e)
        {
            TextBox tb = sender as TextBox;
            if (tb == null)
                return;

            tb.PreviewTextInput -= tbb_PreviewTextInput;
            DataObject.RemovePastingHandler(tb, OnClipboardPaste);

            bool b = ((e.NewValue != null && e.NewValue.GetType() == typeof(bool))) ?
                (bool)e.NewValue : false;
            if (b)
            {
                tb.PreviewTextInput += tbb_PreviewTextInput;
                DataObject.AddPastingHandler(tb, OnClipboardPaste);
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// This method handles paste and drag/drop events
        /// onto the TextBox. It restricts the character
        /// set to numerics and ensures we have consistent behavior.
        /// </summary>
        /// <param name="sender">TextBox sender</param>
        /// <param name="e">EventArgs</param>
        private static void OnClipboardPaste(object sender, DataObjectPastingEventArgs e)
        {
            TextBox tb = sender as TextBox;
            string text = e.SourceDataObject.GetData(e.FormatToApply) as string;

            if (tb != null && !string.IsNullOrEmpty(text) && !Validate(tb, text))
                e.CancelCommand();
        }

        /// <summary>
        /// This checks if the resulting string will match the regex expression
        /// </summary>
        static void tbb_PreviewTextInput(object sender, TextCompositionEventArgs e)
        {
            TextBox tb = sender as TextBox;

            if (tb != null && !Validate(tb, e.Text))
                e.Handled = true;
        }

        #endregion

        private static bool Validate(TextBox tb, string newContent)
        {
            string testString = string.Empty;
            // replace selection with new text.
            if (!string.IsNullOrEmpty(tb.SelectedText))
            {
                string pre = tb.Text.Substring(0, tb.SelectionStart);
                string after = tb.Text.Substring(tb.SelectionStart + tb.SelectionLength, 
                               tb.Text.Length - (tb.SelectionStart + tb.SelectionLength));
                testString = pre + newContent + after;
            }
            else
            {
                string pre = tb.Text.Substring(0, tb.CaretIndex);
                string after = tb.Text.Substring(tb.CaretIndex, 
                                       tb.Text.Length - tb.CaretIndex);
                testString = pre + newContent + after;
            }

            Regex regExpr = new Regex(@"^([-+]?)(\d*)([,.]?)(\d*)$");
            if (regExpr.IsMatch(testString))
                return true;

            return false;
        }
    }
}

这可以轻松地应用于应用程序中的文本框,如下所示:

<Window x:Class="MVVM.Demo.Views.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Cinch="clr-namespace:Cinch;assembly=Cinch"
    Title="Window1" Height="300" Width="300">

    <Grid>
        <TextBox Cinch:NumericTextBoxBehavior.IsEnabled="True" />
    </Grid>
</Window>

启用此附加行为后,将只允许已应用 NumericTextBoxBehavior 附加行为的文本框进行数字输入。

附加命令行为

继续谈论附加行为,有时从 FrameworkElement RoutedEvent 触发 ViewModel ICommand 也非常方便。这是 WPF 框架开箱即用的功能,尽管 Blend 3 通过使用 Blend Interactivity DLL 允许这样做。如果您需要一个示例,请查看我的博客文章 WPF: Blend 3 Interactions / Behaviours

但我们现在所处的位置是这样,我希望人们知道如何在不使用来自不同产品的未发布 DLL 的情况下做到这一点,该 DLL 很可能在某个时候会成为 WPF 的一部分。所以让我们继续讨论如何在没有 Blend Interactivity DLL 的情况下做到这一点。

Cinch 在这里实际上提供了两种替代方案:将单个 ICommand 附加到单个 FrameworkElement RoutedEvent,或使用不同的 RoutedEventICommand 的集合附加到 FrameworkElement

让我们从简单的开始,然后逐步进行。

将单个 ICommand 附加到单个 FrameworkElement RoutedEvent

这可以通过另一个或两个附加属性轻松完成,您可以在视图 FrameworkElement 上这样设置:

<Window x:Class="MVVM.Demo.Views.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Cinch="clr-namespace:Cinch;assembly=Cinch"
    Title="Window1" Height="300" Width="300">

     <Grid Background="WhiteSmoke"
           Cinch:SingleEventCommand.RoutedEventName="MouseDown"
          Cinch:SingleEventCommand.TheCommandToRun=
           "{Binding Path=ShowWindowCommand}"/>
</Window>

所以我们设置 RoutedEvent 来触发 ICommand,并通过绑定设置 ViewModel ICommand 来执行。

那么剩下的就是看看这是如何实现的,这是通过一个或两个标准的 DP 和一点点反射来完成的,以从声明 RoutedEventName 附加 DP 的 DependencyObject 中获取 RoutedEvent,然后,我们创建一个 Delegate,当事件被引发时调用它,而它又会触发使用 TheCommandToRun DP 指定的 ICommand

这是代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.Reflection;
using System.Windows.Media;

namespace Cinch
{
    #region SCommandArgs Class
    /// <summary>
    /// Allows a CommandParameter to be associated with a SingleEventCommand
    /// </summary>
    public class SCommandArgs
    {
        #region Data
        public object Sender { get; set; }
        public object EventArgs { get; set; }
        public object CommandParameter { get; set; }
        #endregion

        #region Ctor
        public SCommandArgs()
        {
        }

        public SCommandArgs(object sender, object eventArgs, 
                            object commandParameter)
        {
            Sender = sender;
            EventArgs = eventArgs;
            CommandParameter = commandParameter;
        }
        #endregion
    }
    #endregion

    #region SingleEventCommand Class
    /// <summary>
    /// This class allows a single command to event mappings. 
    /// It is used to wire up View events to a
    /// ViewModel ICommand implementation. 
    /// </summary>
    /// <example>
    /// <![CDATA[
    /// 
    /// <ListBox ...    
    /// Cinch:SingleEventCommand.RoutedEventName="SelectionChanged"     
    /// Cinch:SingleEventCommand.TheCommandToRun="{Binding Path=BoxEditCommand}"     
    /// Cinch:SingleEventCommand.CommandParameter=
    ///         "{Binding ElementName=ListBoxVehicle, Path=SelectedItem}">
    /// </ListBox>
    /// 
    /// ]]>
    /// </example>
    public static class SingleEventCommand
    {
        #region TheCommandToRun

        /// <summary>
        /// TheCommandToRun : The actual ICommand to run
        /// </summary>
        public static readonly DependencyProperty TheCommandToRunProperty =
            DependencyProperty.RegisterAttached("TheCommandToRun",
                typeof(ICommand),
                typeof(SingleEventCommand),
                new FrameworkPropertyMetadata((ICommand)null));

        /// <summary>
        /// Gets the TheCommandToRun property. 
        /// </summary>
        public static ICommand GetTheCommandToRun(DependencyObject d)
        {
            return (ICommand)d.GetValue(TheCommandToRunProperty);
        }

        /// <summary>
        /// Sets the TheCommandToRun property. 
        /// </summary>
        public static void SetTheCommandToRun(DependencyObject d, ICommand value)
        {
            d.SetValue(TheCommandToRunProperty, value);
        }
        #endregion

        #region RoutedEventName

        /// <summary>
        /// RoutedEventName : The event that should actually execute the
        /// ICommand
        /// </summary>
        public static readonly DependencyProperty RoutedEventNameProperty =
            DependencyProperty.RegisterAttached("RoutedEventName", typeof(String),
            typeof(SingleEventCommand),
                new FrameworkPropertyMetadata((String)String.Empty,
                    new PropertyChangedCallback(OnRoutedEventNameChanged)));

        /// <summary>
        /// Gets the RoutedEventName property. 
        /// </summary>
        public static String GetRoutedEventName(DependencyObject d)
        {
            return (String)d.GetValue(RoutedEventNameProperty);
        }

        /// <summary>
        /// Sets the RoutedEventName property. 
        /// </summary>
        public static void SetRoutedEventName(DependencyObject d, String value)
        {
            d.SetValue(RoutedEventNameProperty, value);
        }

        /// <summary>
        /// Hooks up a Dynamically created EventHandler (by using the 
        /// <see cref="EventHooker">EventHooker</see> class) that when
        /// run will run the associated ICommand
        /// </summary>
        private static void OnRoutedEventNameChanged(DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            String routedEvent = (String)e.NewValue;

            if (d == null || String.IsNullOrEmpty(routedEvent))
                return;


            //If the RoutedEvent string is not null, create a new
            //dynamically created EventHandler that when run will execute
            //the actual bound ICommand instance (usually in the ViewModel)
            EventHooker eventHooker = new EventHooker();
            eventHooker.ObjectWithAttachedCommand = d;

            EventInfo eventInfo = d.GetType().GetEvent(routedEvent,
                BindingFlags.Public | BindingFlags.Instance);

            //Hook up Dynamically created event handler
            if (eventInfo != null)
            {
                eventInfo.RemoveEventHandler(d,
                    eventHooker.GetNewEventHandlerToRunCommand(eventInfo));

                eventInfo.AddEventHandler(d,
                    eventHooker.GetNewEventHandlerToRunCommand(eventInfo));
            }

        }
        #endregion

        #region CommandParameter
        public static readonly DependencyProperty CommandParameterProperty =
            DependencyProperty.RegisterAttached("CommandParameter", typeof(object),
            typeof(SingleEventCommand), new UIPropertyMetadata(null));

        /// <summary>        
        /// Gets the CommandParameter property.         
        /// </summary>        
        public static object GetCommandParameter(DependencyObject obj)
        {
            return (object)obj.GetValue(CommandParameterProperty);
        }


        /// <summary>        
        /// Sets the CommandParameter property.         
        /// </summary>        
        public static void SetCommandParameter(DependencyObject obj, object value)
        {
            obj.SetValue(CommandParameterProperty, value);
        }
        #endregion

    }
    #endregion

    #region EventHooker Class
    /// <summary>
    /// Contains the event that is hooked into the source RoutedEvent
    /// that was specified to run the ICommand
    /// </summary>
    sealed class EventHooker
    {
        #region Public Methods/Properties
        /// <summary>
        /// The DependencyObject, that holds a binding to the actual
        /// ICommand to execute
        /// </summary>
        public DependencyObject ObjectWithAttachedCommand { get; set; }

        /// <summary>
        /// Creates a Dynamic EventHandler that will be run the ICommand
        /// when the user specified RoutedEvent fires
        /// </summary>
        /// <param name="eventInfo">The specified RoutedEvent EventInfo</param>
        /// <returns>An Delegate that points to a new EventHandler
        /// that will be run the ICommand</returns>
        public Delegate GetNewEventHandlerToRunCommand(EventInfo eventInfo)
        {
            Delegate del = null;

            if (eventInfo == null)
                throw new ArgumentNullException("eventInfo");

            if (eventInfo.EventHandlerType == null)
                throw new ArgumentException("EventHandlerType is null");

            if (del == null)
                del = Delegate.CreateDelegate(eventInfo.EventHandlerType, this,
                      GetType().GetMethod("OnEventRaised",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance));

            return del;
        }
        #endregion

        #region Private Methods

        /// <summary>
        /// Runs the ICommand when the requested RoutedEvent fires
        /// </summary>
        private void OnEventRaised(object sender, EventArgs e)
        {
            ICommand command = (ICommand)(sender as DependencyObject).
                GetValue(SingleEventCommand.TheCommandToRunProperty);

            object commandParameter = (sender as DependencyObject).
                GetValue(SingleEventCommand.CommandParameterProperty);

            SCommandArgs commandArgs = new SCommandArgs(sender, e, commandParameter);
            if (command != null)
                command.Execute(commandArgs);

        }
        #endregion
    }
    #endregion

}

这允许单个 FrameworkElement RoutedEvent 触发单个 ViewModel ICommand,并将原始 EventArgs/SenderCommandParamtere 通过 SCommandArgs 对象传递给 ViewModel。这意味着,在 ViewModel 中使用原始 EventArgs 所要做的就是类似这样:

someCommand= new SimpleCommand
{
    CanExecuteDelegate = x => true,
    ExecuteDelegate = x => ExecuteSomeCommand(x)
};

private void ExecuteSomeCommand(Object o)
{
    //if using SingleEventCommand
    Cinch.SCommandArgs data =(Cinch.SCommandArgs)o;

    //now you have access to
    //data.Sender
    //data.EventArgs
    //data.CommandParameter

}

但 Cinch 不止于此。所以让我们看看更高级的场景。

使用不同的RoutedEvent(s) 将 ICommand(s) 的集合附加到 FrameworkElement

与之前一样,让我们看看如何从视图中使用 ICommand/RoutedEvent 的附加集合。您可能会这样做:

<Window x:Class="MVVM.Demo.Views.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Cinch="clr-namespace:Cinch;assembly=Cinch"
    Title="Window1" Height="300" Width="300">

    <Grid Background="WhiteSmoke">
       <Behaviors:EventCommander.Mappings>

          <Behaviors:CommandEvent 
              Command="{Binding MouseEnterCommand}" 
              Event="MouseEnter" />

          <Behaviors:CommandEvent 
              Command="{Binding MouseLeaveCommand}" 
              Event="MouseLeave" />

       </Behaviors:EventCommander.Mappings>
    </Grid>

</Window>

所以您可以看到,这次有一个附加属性(EventCommander.Mappings),它期望一个 CommandEvent 对象的集合。其背后的概念基本相同,只是这次有一个这些 CommandEvent 对象的集合,但它们的工作方式与上面描述的非常相似,因此我将专注于 EventCommander.Mappings 集合如何工作,并假设上面的讨论足以解释单个 CommandEvent 对象。

EventCommander.Mappings 集合看起来像这样:

public static class EventCommander
{
    #region InternalMappings DP
    // Make it internal so WPF ignores the property and always uses the 
    //public getter/setter. This is per John Gossman blog post - 07/2008.
    internal static readonly DependencyProperty MappingsProperty = 
        DependencyProperty.RegisterAttached("InternalMappings", 
                        typeof(CommandEventCollection), typeof(EventCommander),
                        new UIPropertyMetadata(null, OnMappingsChanged));

    /// <summary>
    /// Retrieves the mapping collection
    /// </summary>
    /// <param name="obj"></param>

    /// <returns></returns>
    internal static CommandEventCollection InternalGetMappingCollection(
        DependencyObject obj)
    {
        var map = obj.GetValue(MappingsProperty) as CommandEventCollection;
        if (map == null)
        {
            map = new CommandEventCollection();
            SetMappings(obj, map);
        }
        return map;
    }

    /// <summary>
    /// This retrieves the mapping collection
    /// </summary>
    /// <param name="obj">Dependency Object</param>

    /// <returns>Mapping collection</returns>
    public static IList GetMappings(DependencyObject obj)
    {
        return InternalGetMappingCollection(obj);
    }

    /// <summary>
    /// This sets the mapping collection.
    /// </summary>
    /// <param name="obj">Dependency Object</param>

    /// <param name="value">Mapping collection</param>
    public static void SetMappings(DependencyObject obj, 
        CommandEventCollection value)
    {
        obj.SetValue(MappingsProperty, value);
    }

    /// <summary>
    /// This changes the event mapping
    /// </summary>
    /// <param name="target"></param>

    /// <param name="e"></param>
    private static void OnMappingsChanged(DependencyObject target, 
        DependencyPropertyChangedEventArgs e)
    {
        if (e.OldValue != null)
        {
            CommandEventCollection cec = e.OldValue as CommandEventCollection;
            if (cec != null)
                cec.Unsubscribe(target);
        }
        if (e.NewValue != null)
        {
            CommandEventCollection cec = e.NewValue as CommandEventCollection;
            if (cec != null)
                cec.Subscribe(target);
        }
    }
    #endregion
}

可以看到,在后台,EventCommander.Mappings 集合正在填充一个 CommandEventCollectionCommandEventCollection 继承自 Freezable,因为这是一个可以用来获得 DataContext 继承的技巧。基本上,发生的情况是,通过继承自 Freezable,一个非 UI 元素也将获得当前 UI 元素的 DataContext(很可能是 ViewModel)。如果您不继承自 FreezableCommandEventCollection 将无法拾取 ViewModel 绑定的 ICommand;因为它不知道当前的 DataContext 对象,它将为 null。这是一个技巧,但它有效。如果您想了解更多信息,请阅读 Mike Hillberg 的博客文章:Mike Hillberg 的 Freezable 博客文章,或许可以看看 Josh Smith 的 DataContextSpy 文章,也非常有用。

总之,CommandEventCollection 看起来像这样,它的主要任务是维护当前 CommandEvent 的列表:

public class CommandEventCollection : FreezableCollection<CommandEvent>
{
    #region Data
    private object _target;
    private readonly List<CommandEvent> _currentList = new List<CommandEvent>();
    #endregion

    #region Ctor
    /// <summary>

    /// Constructor
    /// </summary>
    public CommandEventCollection()
    {
        ((INotifyCollectionChanged)this).CollectionChanged += 
                                         OnCollectionChanged;
    }
    #endregion

    #region Private/Internal Methods
    /// <summary>
    /// Wire up events to the target
    /// </summary>
    /// <param name="target"></param>

    internal void Subscribe(object target)
    {
        _target = target;
        foreach(var item in this)
            item.Subscribe(target);
    }

    /// <summary>
    /// Unwire all target events
    /// </summary>
    /// <param name="target"></param>
    internal void Unsubscribe(object target)
    {
        foreach (var item in this)
            item.Unsubscribe(target);
        _target = null;
    }

    /// <summary>

    /// This handles the collection change event - it then
    /// subscribes and unsubscribes events.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>

    private void OnCollectionChanged(object sender, 
                 NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                foreach (var item in e.NewItems)
                    OnItemAdded((CommandEvent)item);
                break;

            case NotifyCollectionChangedAction.Remove:
                foreach (var item in e.OldItems)
                    OnItemRemoved((CommandEvent)item);
                break;

            case NotifyCollectionChangedAction.Replace:
                foreach (var item in e.OldItems)
                    OnItemRemoved((CommandEvent)item);
                foreach (var item in e.NewItems)
                    OnItemAdded((CommandEvent)item);
                break;

            case NotifyCollectionChangedAction.Move:
                break;

            case NotifyCollectionChangedAction.Reset:
                _currentList.ForEach(i => i.Unsubscribe(_target));
                _currentList.Clear();
                foreach (var item in this)
                    OnItemAdded(item);
                break;

            default:
                return;
        }
    }

    /// <summary>

    /// A new item has been added to the event list
    /// </summary>
    /// <param name="item"></param>

    private void OnItemAdded(CommandEvent item)
    {
        if (item != null && _target != null)
        {
            _currentList.Add(item);
            item.Subscribe(_target);                
        }
    }

    /// <summary>
    /// An item has been removed from the event list.
    /// </summary>

    /// <param name="item"></param>

    private void OnItemRemoved(CommandEvent item)
    {
        if (item != null && _target != null)
        {
            _currentList.Remove(item);
            item.Unsubscribe(_target);
        }
    }
    #endregion
}

与之前一样,要在 ViewModel ICommand 中使用原始 EventArgs,我们只需在 ViewModel 中执行类似的操作:

someCommand= new SimpleCommand
{
    CanExecuteDelegate = x => true,
    ExecuteDelegate = x => ExecuteSomeCommand(x)
};


private void ExecuteSomeCommand(Object o)
{
    //if using EventCommander
    Cinch.EventParameters data =(Cinch.EventParameters)o;

    //now you have access to
    //data.Sender
    //data.EventArgs
}

更好的 INPC,无魔法字符串

在使用 WPF 时,ViewModel/Model 类实现 System.ComponentModel.INotifyPropertyChanged 接口以通知属性更改的绑定非常普遍。这通常这样实现:

using System.ComponentModel;

namespace SDKSample
{
  // This class implements INotifyPropertyChanged
  // to support one-way and two-way bindings
  // (such that the UI element updates when the source
  // has been changed dynamically)
  public class Person : INotifyPropertyChanged
  {
      private string name;
      // Declare the event
      public event PropertyChangedEventHandler PropertyChanged;

      public Person()
      {
      }

      public Person(string value)
      {
          this.name = value;
      }

      public string PersonName
      {
          get { return name; }
          set
          {
              name = value;
              // Call OnPropertyChanged whenever the property is updated
              OnPropertyChanged("PersonName");
          }
      }

      // Create the OnPropertyChanged method to raise the event
      protected void OnPropertyChanged(string name)
      {
          PropertyChangedEventHandler handler = PropertyChanged;
          if (handler != null)
          {
              handler(this, new PropertyChangedEventArgs(name));
          }
      }
  }
}

这种方法的问题在于代码中存在容易出错的魔法字符串。看看上面显示的 OnPropertyChanged("PersonName"); 代码。我最初曾使用一些静态反射,通过使用 LINQ 表达式树来获取属性名称。我使用了 Bill Kempf 出色的 Reflect 代码。静态反射表达式树的问题在于,每次创建 eventArgs 时都比较慢。本文的一位读者实际上提出了一个更好的解决方案,其中 INPC EventArgs 被创建一次然后重复使用。我非常喜欢这个主意,所以现在将其包含在 Cinch 中。如果您想了解更多关于此的信息,读者可以在此处找到他们的帖子:CinchII.aspx?msg=3141144#xx3141144xx

Cinch 现在采用了这种方法。它的工作原理如下。有一个静态帮助类,如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;

namespace Cinch
{
    /// <summary>
    /// A small helper class that has a method to help create
    /// PropertyChangedEventArgs when using the INotifyPropertyChanged
    /// interface
    /// </summary>
    public static class ObservableHelper
    {
        #region Public Methods
        /// <summary>
        /// Creates PropertyChangedEventArgs
        /// </summary>
        /// <param name="propertyExpression">Expression to make 
        /// PropertyChangedEventArgs out of</param>
        /// <returns>PropertyChangedEventArgs</returns>
        public static PropertyChangedEventArgs CreateArgs<T>(
            Expression<Func<T, Object>> propertyExpression)
        {
            return new PropertyChangedEventArgs(
                GetPropertyName<T>(propertyExpression));
        }

        /// <summary>
        /// Creates PropertyChangedEventArgs
        /// </summary>
        /// <param name="propertyExpression">Expression to make 
        /// PropertyChangedEventArgs out of</param>
        /// <returns>PropertyChangedEventArgs</returns>
        public static string GetPropertyName<T>(
            Expression<Func<T, Object>> propertyExpression)
        {
            var lambda = propertyExpression as LambdaExpression;
            MemberExpression memberExpression;
            if (lambda.Body is UnaryExpression)
            {
                var unaryExpression = lambda.Body as UnaryExpression;
                memberExpression = unaryExpression.Operand as MemberExpression;
            }
            else
            {
                memberExpression = lambda.Body as MemberExpression;
            }

            var propertyInfo = memberExpression.Member as PropertyInfo;

            return propertyInfo.Name;
        }

        #endregion
    }
}

然后我们可以从任何属性像这样使用它:

static PropertyChangedEventArgs currentCustomerChangeArgs =
    ObservableHelper.CreateArgs<AddEditOrderViewModel>(x => x.CurrentCustomer);

public CustomerModel CurrentCustomer
{
    get { return currentCustomer; }
    set
    {
        currentCustomer = value;
        NotifyPropertyChanged(currentCustomerChangeArgs);
    }
}

完全没有魔法字符串了。非常出色。

ViewModel 模式

我一直在努力解决的一个问题是,在使用 MVVM 生产 LOB 应用程序时,视图模式。例如,希望有一个只读的视图,然后用户点击编辑,那么视图上的所有字段都将是可编辑的。现在,这可以通过在 ViewModel 中有一个命令来实现,该命令从 ReadOnly 模式变为 EditMode,并且视图上的所有 UIElement 都可以绑定到 ViewModel 上的某个 CurrentMode 属性。听起来可行,但正如我们所知,事情从来不像那样干净利落。在我的工作场所,我们对数据输入有复杂的要求,单一模式根本无法应用于单个视图上的所有数据输入字段。不行,我们需要非常细粒度的数据输入权限,一直到单个字段级别。

这让我思考。我们需要的是每个 UI 模型数据项的可编辑状态。我对此进行了更多思考,并提出了一个通用的包装类,它包装单个属性,但也公开了一个 IsEditable 属性。现在 ViewModel 可以访问这些包装器,因为它们是公共属性,可以在 UI 模型类上访问,或者直接在 ViewModel 中访问(我公开了我的 ViewModel 的一个 CurrentX 对象,但其他人会在 ViewModel 中重复 UI 模型的所有属性;我,在从视图直接写入模型方面没有任何问题,只要它没有到达数据库并且 InValid,我就没问题)。因此,它可以将数据绑定到包装器的数据属性,并根据包装器的 IsEditable 属性禁用数据输入。

为此,我提出了一个简单的类,如下所示:

using System;
using System.Reflection;
using System.Diagnostics;
using System.Linq;
using System.ComponentModel;

using System.Collections.Generic;

namespace Cinch
{
    /// <summary>
    /// Abstract base class for DataWrapper which
    /// will support IsDirty. So in your ViewModel
    /// you could do something like
    /// 
    /// <example>
    /// <![CDATA[
    /// 
    ///public bool IsDirty
    ///{
    ///   get
    ///   {
    ///     return cachedListOfDataWrappers.Where(x => (x is IChangeIndicator)
    ///     &&  ((IChangeIndicator)x).IsDirty).Count() > 0;
    ///   }
    ///
    /// } 
    /// ]]>
    /// </example>
    /// </summary>
    public abstract class DataWrapperDirtySupportingBase : EditableValidatingObject
    {
        #region Public Properties
        /// <summary>
        /// Deteremines if a property has changes since is was put into edit mode
        /// </summary>
        /// <param name="propertyName">The property name</param>
        /// <returns>True if the property has changes
        ///    since is was put into edit mode</returns>
        public bool HasPropertyChanged(string propertyName)
        {
            if (_savedState == null)
                return false;

            object saveValue;
            object currentValue;
            if (!_savedState.TryGetValue(propertyName, out saveValue) ||
                  !this.GetFieldValues().TryGetValue(propertyName, out currentValue))
                return false;
            if (saveValue == null || currentValue == null)
                return saveValue != currentValue;

            return !saveValue.Equals(currentValue);
        }
        #endregion
    }

    /// <summary>
    /// Abstract base class for DataWrapper - allows easier access to
    /// methods for the DataWrapperHelper.
    /// </summary>
    public abstract class DataWrapperBase : DataWrapperDirtySupportingBase
    {
        #region Data
        private Boolean isEditable = false;

        private IParentablePropertyExposer parent = null;
        private PropertyChangedEventArgs parentPropertyChangeArgs = null;
        #endregion

        #region Ctors
        public DataWrapperBase()
        {
        }

        public DataWrapperBase(IParentablePropertyExposer parent,
            PropertyChangedEventArgs parentPropertyChangeArgs)
        {
            this.parent = parent;
            this.parentPropertyChangeArgs = parentPropertyChangeArgs;
        }
        #endregion

        #region Protected Methods

        /// <summary>
        /// Notifies all the parent (INPC) objects
        /// INotifyPropertyChanged.PropertyChanged subscribed delegates
        /// that an internal DataWrapper property value
        /// has changed, which in turn raises the appropriate
        /// INotifyPropertyChanged.PropertyChanged event on the parent (INPC) object
        /// </summary>
        protected internal void NotifyParentPropertyChanged()
        {
            if (parent == null || parentPropertyChangeArgs == null)
                return;

            //notify all delegates listening to DataWrapper<T> 
            //       parent objects PropertyChanged event
            Delegate[] subscribers = parent.GetINPCSubscribers();
            if (subscribers != null)
            {
                foreach (PropertyChangedEventHandler d in subscribers)
                {
                    d(parent, parentPropertyChangeArgs);
                }
            }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// The editable state of the data, the View
        /// is expected to use this to enable/disable
        /// data entry. The ViewModel would set this
        /// property
        /// </summary>
        static PropertyChangedEventArgs isEditableChangeArgs =
            ObservableHelper.CreateArgs<DataWrapperBase>(x => x.IsEditable);

        public Boolean IsEditable
        {
            get { return isEditable; }
            set
            {
                if (isEditable != value)
                {
                    isEditable = value;
                    NotifyPropertyChanged(isEditableChangeArgs);
                    NotifyParentPropertyChanged();
                }
            }
        }
        #endregion
    }

    /// <summary>
    /// This interface is here so to ensure that both DataWrapper of T
    /// and DataWrapperExt of T have a commonly named property for
    /// the data (DataValue) and that we can safely retrieve this
    /// name elsewhere via static reflection.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface IDataWrapper<T>
    {
        T DataValue { get; set; }
    }

    /// <summary>
    /// Allows IsDierty to be determined for a cached list of DataWrappers
    /// </summary>
    public interface IChangeIndicator
    {
        bool IsDirty { get; }
    }

    /// <summary>
    /// This interface is implemented by both the 
    /// <see cref="ValidatingObject">ValidatingObject</see> and the
    /// <see cref="ViewModelBase">ViewModelBase</see> classes, and is used
    /// to expose the list of delegates that are currently listening to the
    /// <see cref="System.ComponentModel.INotifyPropertyChanged">INotifyPropertyChanged</see>
    /// PropertyChanged event. This is done so that the internal 
    /// <see cref="DataWrapper">DataWrapper</see> classes can notify their parent object
    /// when an internal <see cref="DataWrapper">DataWrapper</see> property changes
    /// </summary>
    public interface IParentablePropertyExposer
    {
        Delegate[] GetINPCSubscribers();
    }

    /// <summary>
    /// Provides a wrapper around a single piece of data
    /// such that the ViewModel can put the data item
    /// into a editable state and the View can bind to
    /// both the DataValue for the actual Value, and to 
    /// the IsEditable to determine if the control which
    /// has the data is allowed to be used for entering data.
    /// 
    /// The Viewmodel is expected to set the state of the
    /// IsEditable property for all DataWrappers in a given Model
    /// </summary>
    /// <typeparam name="T">The type of the Data</typeparam>
    public class DataWrapper<T> : DataWrapperBase, 
                 IDataWrapper<T>, IChangeIndicator
    {
        #region Data
        private T dataValue = default(T);
        private bool isDirty = false;
        #endregion

        #region Ctors
        public DataWrapper()
        {
        }

        public DataWrapper(T initialValue)
        {
            dataValue = initialValue;
        }

        public DataWrapper(IParentablePropertyExposer parent,
            PropertyChangedEventArgs parentPropertyChangeArgs)
            : base(parent, parentPropertyChangeArgs)
        {
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// The actual data value, the View is
        /// expected to bind to this to display data
        /// </summary>
        static PropertyChangedEventArgs dataValueChangeArgs =
            ObservableHelper.CreateArgs<DataWrapper<T>>(x => x.DataValue);

        public T DataValue
        {
            get { return dataValue; }
            set
            {
                dataValue = value;
                NotifyPropertyChanged(dataValueChangeArgs);
                NotifyParentPropertyChanged();
                IsDirty = this.HasPropertyChanged("dataValue"); 
            }
        }


        /// <summary>
        /// The IsDirty status of this DataWrapper
        /// </summary>
        static PropertyChangedEventArgs isDirtyChangeArgs =
            ObservableHelper.CreateArgs<DataWrapper<T>>(x => x.IsDirty);

        public bool IsDirty
        {
            get { return isDirty; }
            set
            {
                isDirty = value;
                NotifyPropertyChanged(isDirtyChangeArgs);
                NotifyParentPropertyChanged();
            }
        }
        #endregion
    }

    /// <summary>
    /// Provides helper methods for dealing with DataWrappers
    /// within the Cinch library. 
    /// </summary>
    public class DataWrapperHelper
    {
        #region Public Methods
        // The following functions may be used when dealing with model/viewmodel objects
        // whose entire set of DataWrapper properties are immutable (only have a getter
        // for the property). They avoid having to do reflection to retrieve the list
        // of wrapper properties every time a mode change, edit state change

        /// <summary>
        /// Set all Cinch.DataWrapper properties to have
        /// the correct Cinch.DataWrapper.IsEditable 
        /// to the correct state based on the current ViewMode 
        /// </summary>
        /// <param name="wrapperProperties">The properties
        /// on which to change the mode</param>
        /// <param name="currentViewMode">The current ViewMode</param
        public static void SetMode(IEnumerable<DataWrapperBase> wrapperProperties,
            ViewMode currentViewMode)
        {
            bool isEditable = currentViewMode ==
                    ViewMode.EditMode || currentViewMode == ViewMode.AddMode;

            foreach (var wrapperProperty in wrapperProperties)
            {
                try
                {
                    wrapperProperty.IsEditable = isEditable;
                }
                catch (Exception)
                {
                    Debug.WriteLine("There was a problem setting the currentViewMode");
                }
            }
        }

        /// <summary>
        /// Loops through a source object (UI Model class is expected really) and attempts
        /// to call the BeginEdit() method of all the  Cinch.DataWrapper fields
        /// </summary>
        /// <param name="wrapperProperties">The DataWrapperBase objects</param>
        public static void SetBeginEdit(IEnumerable<DataWrapperBase> wrapperProperties)
        {
            foreach (var wrapperProperty in wrapperProperties)
            {
                try
                {
                    wrapperProperty.BeginEdit();
                    wrapperProperty.NotifyParentPropertyChanged();
                }
                catch (Exception)
                {
                    Debug.WriteLine("There was a problem calling the " + 
                          "BeginEdit method for the current DataWrapper");
                }
            }
        }

        /// <summary>
        /// Loops through a source object (UI Model class is expected really) and attempts
        /// to call the CancelEdit() method of all the  Cinch.DataWrapper fields
        /// </summary>
        /// <param name="wrapperProperties">The DataWrapperBase objects</param>
        public static void SetCancelEdit(IEnumerable<DataWrapperBase> wrapperProperties)
        {
            foreach (var wrapperProperty in wrapperProperties)
            {
                try
                {
                    wrapperProperty.CancelEdit();
                    wrapperProperty.NotifyParentPropertyChanged();
                }
                catch (Exception)
                {
                    Debug.WriteLine("There was a problem calling " + 
                          "the CancelEdit method for the current DataWrapper");
                }
            }
        }

        /// <summary>
        /// Loops through a source object (UI Model class is expected really) and attempts
        /// to call the EditEdit() method of all the  Cinch.DataWrapper fields
        /// </summary>
        /// <param name="wrapperProperties">The DataWrapperBase objects</param>
        public static void SetEndEdit(IEnumerable<DataWrapperBase> wrapperProperties)
        {
            foreach (var wrapperProperty in wrapperProperties)
            {
                try
                {
                    wrapperProperty.EndEdit();
                    wrapperProperty.NotifyParentPropertyChanged();
                }
                catch (Exception)
                {
                    Debug.WriteLine("There was a problem calling " + 
                          "the EndEdit method for the current DataWrapper");
                }
            }
        }

        /// <summary>
        /// Loops through a source object (UI Model
        /// class is expected really) and attempts
        /// to call the EditEdit() method of all the  Cinch.DataWrapper fields
        /// </summary>
        /// <param name="wrapperProperties">The DataWrapperBase objects</param>
        public static Boolean AllValid(IEnumerable<DataWrapperBase> wrapperProperties)
        {
            Boolean allValid = true;

            foreach (var wrapperProperty in wrapperProperties)
            {
                try
                {
                    allValid &= wrapperProperty.IsValid;
                    if (!allValid)
                        break;
                }
                catch (Exception)
                {
                    allValid = false;
                    Debug.WriteLine("There was a problem calling " + 
                          "the IsValid method for the current DataWrapper");
                }
            }

            return allValid;
        }

        /// <summary>
        /// Get a list of the wrapper properties on the parent object.
        /// </summary>
        /// <typeparam name="T">The type of object</typeparam>
        /// <param name="parentObject">The parent object to examine</param>
        /// <returns>A IEnumerable of DataWrapperBase</returns>
        public static IEnumerable<DataWrapperBase> GetWrapperProperties<T>(T parentObject)
        {
            var properties = parentObject.GetType().GetProperties(
                BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

            List<DataWrapperBase> wrapperProperties = new List<DataWrapperBase>();

            foreach (var propItem in parentObject.GetType().GetProperties(
                           BindingFlags.Public | 
                           BindingFlags.NonPublic | BindingFlags.Instance))
            {
                // check make sure can read and that
                // the property is not an indexed property
                if (propItem.CanRead && propItem.GetIndexParameters().Count() == 0)
                {
                    // we ignore any property whose
                    // type CANNOT store a DataWrapper;
                    // this means any property whose type
                    // is not in the inheritance hierarchy
                    // of DataWrapper. For example a property
                    // of type Object could potentially
                    // store a DataWrapper since Object
                    // is in DataWrapper's inheritance tree.
                    // However, a boolean property CANNOT
                    // since it's not in the wrapper's
                    // inheritance tree.
                    if (typeof(DataWrapperBase).IsAssignableFrom(
                                  propItem.PropertyType) == false)
                        continue;

                    // make sure properties value is not null ref
                    var propertyValue = propItem.GetValue(parentObject, null);
                    if (propertyValue != null && propertyValue is DataWrapperBase)
                    {
                        wrapperProperties.Add((DataWrapperBase)propertyValue);
                    }
                }
            }

            return wrapperProperties;
        }
        #endregion
    }
}

然后可以将其用作 UI 模型类(或直接在 ViewModel 中,如果您愿意)上的属性,如下所示(不用担心继承自 Cinch.EditableValidatingObject,我们稍后会讲到):

public class OrderModel : Cinch.EditableValidatingObject
{
    //Any data item is declared as a Cinch.DataWrapper, to allow the ViewModel
    //to decide what state the data is in, and the View just renders 
    //the data state accordingly
    private Cinch.DataWrapper<Int32> quantity;


    public OrderModel()
    {
        Quantity = new DataWrapper<int32>(this, quantityChangeArgs);
        ....
        ....
        //Setup rules etc etc
        
    }

    static PropertyChangedEventArgs quantityChangeArgs =
        ObservableHelper.CreateArgs<OrderModel>(x => x.Quantity);

    public Cinch.DataWrapper<Int32> Quantity
    {
        get { return quantity; }
        private set
        {
            quantity = value;
            NotifyPropertyChanged(quantityChangeArgs);
        }
    }
}

注意 setter 是 private,这是因为这些对象是不可变的,只能在构造函数中设置。但是,IsEditableDataValue 可以随时更改。另一件需要注意的事情是,模型/ViewModel 在构造时实际上会使用一些反射来获取一个 IEnumerable<DataWrapperBase>,它随后被用作缓存,因此从那时起设置任何缓存的 DataWrapper<T> 属性都非常快。这是通过以下方式实现的:

在构造函数中,我们有类似这样的东西:

using System;

using Cinch;
using MVVM.DataAccess;
using System.ComponentModel;
using System.Collections.Generic;

namespace MVVM.Models
{
    /// <summary>
    /// Respresents a UI Order Model, which has all the
    /// good stuff like Validation/INotifyPropertyChanged/IEditableObject
    /// which are all ready to use within the base class.
    /// 
    /// This class also makes use of <see cref="Cinch.DataWrapper">
    /// Cinch.DataWrapper</see>s. Where the idea is that the ViewModel
    /// is able to control the mode for the data, and as such the View
    /// simply binds to a instance of a <see cref="Cinch.DataWrapper">
    /// Cinch.DataWrapper</see> for both its data and its editable state.
    /// Where the View can disable a control based on the 
    /// <see cref="Cinch.DataWrapper">Cinch.DataWrapper</see> editable state.
    /// </summary>
    public class OrderModel : Cinch.EditableValidatingObject
    {
        #region Data
        //Any data item is declared as a Cinch.DataWrapper, to allow the ViewModel
        //to decide what state the data is in, and the View just renders 
        //the data state accordingly
        private Cinch.DataWrapper<Int32> orderId;
        private Cinch.DataWrapper<Int32> customerId;
        private Cinch.DataWrapper<Int32> productId;
        private Cinch.DataWrapper<Int32> quantity;
        private Cinch.DataWrapper<DateTime> deliveryDate;
        private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;
        #endregion

        #region Ctor
        public OrderModel()
        {
            #region Create DataWrappers

            OrderId = new DataWrapper<Int32>(this, orderIdChangeArgs);
            CustomerId = new DataWrapper<Int32>(this, customerIdChangeArgs);
            ProductId = new DataWrapper<Int32>(this, productIdChangeArgs);
            Quantity = new DataWrapper<Int32>(this, quantityChangeArgs);
            DeliveryDate = new DataWrapper<DateTime>(this, deliveryDateChangeArgs);

            //fetch list of all DataWrappers, so they can be used again later without the
            //need for reflection
            cachedListOfDataWrappers =
                DataWrapperHelper.GetWrapperProperties<OrderModel>(this);

            #endregion
        }
        #endregion
    }
}

然后,每当我们处理 DataWrapper<T> 属性时,我们都可以使用缓存的列表。

那么,回到我们在视图中如何使用它们,我只需像这样绑定到这些 DataWrapper<T> 属性:

<TextBox FontWeight="Normal" FontSize="11" Width="200"
    Cinch:NumericTextBoxBehavior.IsEnabled="True"            
    Text="{Binding Path=CurrentCustomerOrder.Quantity.DataValue,
    UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=True,
    ValidatesOnExceptions=True}"

    Style="{StaticResource ValidatingTextBox}"
    IsEnabled="{Binding Path=CurrentCustomerOrder.Quantity.IsEditable}"/>

所以这一切都很酷,但是这些 DataWrapper<T> 对象如何响应模式状态的变化呢?嗯,这很简单,我们确实有一个 ViewModel 中的 Cinch.ViewMode,每当它改变状态时,我们就需要更新我们正在尝试改变状态的任何对象中所有嵌套的 DataWrapper<T> 对象的状态(对我来说,这总是一个 UI 模型,对其他人来说,这可能是 ViewModel 本身)。

这是一个示例 AddEditOrderViewModel,它对我来说包含一个类型为 OrderModel 的单个 UI 模型。正如我所说,其他人可能不喜欢这样,并且会让 ViewModel 公开 OrderModel 类型 UI 模型中所有可用的属性。MVVM 的事情是你按照自己的方式去做,而这是我的方式。我不在乎 InValid 数据是否进入模型,只要该模型无法保存到数据库即可。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Data;
using System.Linq;

using Cinch;
using MVVM.Models;
using MVVM.DataAccess;

namespace MVVM.ViewModels
{
    /// <summary>
    /// Provides ALL logic for the AddEditOrderView
    /// </summary>
    public class AddEditOrderViewModel : Cinch.WorkspaceViewModel
    {
        private ViewMode currentViewMode = ViewMode.AddMode;
        private OrderModel currentCustomerOrder;

        public AddEditOrderViewModel()
        {

        }

        /// <summary>
        /// The current ViewMode, when changed will loop
        /// through all nested DataWrapper objects and change
        /// their state also
        /// </summary>

        static PropertyChangedEventArgs currentViewModeChangeArgs =
            ObservableHelper.CreateArgs<AddEditOrderViewModel>(x => x.CurrentViewMode);

        public ViewMode CurrentViewMode
        {
            get { return currentViewMode; }
            set
            {
                currentViewMode = value;

                switch (currentViewMode)
                {
                    case ViewMode.AddMode:
                        CurrentCustomerOrder = new OrderModel();
                        this.DisplayName = "Add Order";
                        break;
                    case ViewMode.EditMode:
                        CurrentCustomerOrder.BeginEdit();
                        this.DisplayName = "Edit Order";
                        break;
                    case ViewMode.ViewOnlyMode:
                        this.DisplayName = "View Order";
                        break;
                }

                //Now change all the CurrentCustomer.CachedListOfDataWrappers
                //Which sets all the Cinch.DataWrapper<T>s to the correct IsEditable
                //state based on the new ViewMode applied to the ViewModel
                //we can use the Cinch.DataWrapperHelper class for this
                DataWrapperHelper.SetMode(
                    CurrentCustomer.CachedListOfDataWrappers,
                    currentViewMode);

                NotifyPropertyChanged(currentViewModeChangeArgs);
            }
        }

        /// <summary>
        /// Current Customer OrderModel
        /// </summary>
        static PropertyChangedEventArgs currentCustomerOrderChangeArgs =
            ObservableHelper.CreateArgs<AddEditOrderViewModel>(x => x.CurrentCustomerOrder);

        public OrderModel CurrentCustomerOrder
        {
            get { return currentCustomerOrder; }
            set
            {
                currentCustomerOrder = value;
                if (currentCustomerOrder != null)
                {
                    if (currentCustomerOrder.ProductId.DataValue > 0)
                    {
                        ProductModel prod = this.Products.Where(p => p.ProductId ==
                            currentCustomerOrder.ProductId.DataValue).Single();
                        productsCV.MoveCurrentTo(prod);
                    }
                }
                NotifyPropertyChanged(currentCustomerOrderChangeArgs);
            }
        }

        ....
        ....
        ....
    }
}

这里值得一提的是,当 CurrentViewMode 属性更改时,会使用一个 DataWrapperHelper 类将特定对象的所有缓存的 DataWrapper<T> 对象设置为相同的请求状态。这是执行此操作的代码:

// The following functions may be used when dealing with model/viewmodel objects
// whose entire set of DataWrapper properties are immutable (only have a getter
// for the property). They avoid having to do reflection to retrieve the list
// of wrapper properties every time a mode change, edit state change

/// <summary>
/// Set all Cinch.DataWrapper properties to have the correct Cinch.DataWrapper.IsEditable 
/// to the correct state based on the current ViewMode 
/// </summary>
/// <param name="wrapperProperties">The properties on which to change the mode</param>
/// <param name="currentViewMode">The current ViewMode</param>
public static void SetMode(IEnumerable<DataWrapperBase> wrapperProperties,
    ViewMode currentViewMode)
{
    bool isEditable = currentViewMode ==
            ViewMode.EditMode || currentViewMode == ViewMode.AddMode;

    foreach (var wrapperProperty in wrapperProperties)
    {
        try
        {
            wrapperProperty.IsEditable = isEditable;
        }
        catch (Exception)
        {
            Debug.WriteLine("There was a problem setting the currentViewMode");
        }
    }
}

验证规则/IDataErrorInfo 集成

我记得很久以前 Paul Stovell 发表了一篇很棒的文章 Delegates and Business Objects,我非常喜欢它,因为它对我来说非常有意义。为此,Cinch 利用了 Paul 的绝妙想法,使用委托为业务对象提供验证。

这个想法很简单,业务对象有一个 AddRule(Rule newRule) 方法,用于添加规则;业务对象还实现了 IDataErrorInfo,这是 WPF 首选的验证技术。然后,基本上会发生的是,当针对特定业务对象调用 IDataErrorInfo.IsValid 属性时,将检查所有验证规则(委托),并将违反规则的列表(由添加到对象上的委托规则决定)作为 IDataErrorInfo.Error 字符串呈现。

我强烈建议您先阅读 Paul Stovell 的出色文章 Delegates and Business Objects,但基本上 Cinch 利用了这一点。

Cinch 提供的内容是:

  • 一个 ValidatingObject 基类,可以用来接受任何基于 Rule 的类进行添加。
  • SimpleRule,一个简单的委托规则。
  • RegexRule,一个正则表达式规则。
  • 相当不错的是,每个 Type 只声明一次规则(因为它们是静态字段),这节省了业务对象验证所需的内存。

这是如何使用 Cinch 验证属性为简单类型(如 String/Int32 等)的示例:

public class OrderModel : Cinch.ValidatingObject
{
    private Int32 quantity;

    //rules
    private static SimpleRule quantityRule;

    public OrderModel()
    {
        #region Create Validation Rules
        quantity.AddRule(quantityRule);
        #endregion
    }

    static OrderModel()
    {
        quantityRule = new SimpleRule("Quantity", "Quantity can not be < 0",
                  (Object domainObject)=>
                  {
                      OrderModel obj = (OrderModel)domainObject;
                      return obj.Quantity <= 0;
                  });
    }
}

然而,回想一下我提到的一个特殊的 Cinch 类,它允许 ViewModel 将单个模型字段设置为编辑模式,使用 Cinch.DataWrapper<T>。嗯,对于这些,我们需要做一些稍微不同的事情,我们需要这样做:

public class OrderModel : Cinch.ValidatingObject
{
    #region Data
    //Any data item is declared as a Cinch.DataWrapper, to allow the ViewModel
    //to decide what state the data is in, and the View just renders 
    //the data state accordingly
    private Cinch.DataWrapper<Int32> customerId;
    
    //rules
    private static SimpleRule quantityRule;    
    #endregion

    #region Ctor
    public OrderModel()
    {
        //setup DataWrappers prior to setting up rules
        ....
        ....

        #region Create Validation Rules

        quantity.AddRule(quantityRule);

        #endregion
    }
    
    static OrderModel()
    {
        quantityRule = new SimpleRule("DataValue", "Quantity can not be < 0",
                  (Object domainObject)=>
                  {
                      DataWrapper<Int32> obj = (DataWrapper<Int32>)domainObject;
                      return obj.DataValue <= 0;
                  });
    }    
    #endregion

    #region Public Properties

    static PropertyChangedEventArgs quantityChangeArgs =
            ObservableHelper.CreateArgs<OrderModel>(x => x.Quantity);

    public Cinch.DataWrapper<Int32> Quantity
    {
        get { return quantity; }
        private set
        {
            quantity = value;
            NotifyPropertyChanged(quantityChangeArgs);
        }
    }
    
    #endregion
}

我们需要像这样声明 Cinch.DataWrapper<T> 对象的验证规则,因为它们不仅仅是属性,而是实际的类,所以我们需要为单个 Cinch.DataWrapper<T> 对象的 DataValue 属性指定要验证的规则。

这也体现在继承自 Cinch.ValidatingObjectIsValid 方法中。假设您有一个 UI 模型对象如下:

using System;

using Cinch;
using MVVM.DataAccess;
using System.ComponentModel;
using System.Collections.Generic;

namespace MVVM.Models
{
    /// <summary>
    /// Respresents a UI Order Model, which has all the
    /// good stuff like Validation/INotifyPropertyChanged/IEditableObject
    /// which are all ready to use within the base class.
    /// 
    /// This class also makes use of <see cref="Cinch.DataWrapper">
    /// Cinch.DataWrapper</see>s. Where the idea is that the ViewModel
    /// is able to control the mode for the data, and as such the View
    /// simply binds to a instance of a <see cref="Cinch.DataWrapper">
    /// Cinch.DataWrapper</see> for both its data and its editable state.
    /// Where the View can disable a control based on the 
    /// <see cref="Cinch.DataWrapper">Cinch.DataWrapper</see> editable state.
    /// </summary>
    public class OrderModel : Cinch.ValidatingObject
    {
        #region Data
        //Any data item is declared as a Cinch.DataWrapper, to allow the ViewModel
        //to decide what state the data is in, and the View just renders 
        //the data state accordingly
        private Cinch.DataWrapper<Int32> orderId;
        private Cinch.DataWrapper<Int32> customerId;
        private Cinch.DataWrapper<Int32> productId;
        private Cinch.DataWrapper<Int32> quantity;
        private Cinch.DataWrapper<DateTime> deliveryDate;
        private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;

        //rules
        private static SimpleRule quantityRule;

        #endregion

        #region Ctor
        public OrderModel()
        {
            #region Create DataWrappers

            OrderId = new DataWrapper<Int32>(this, orderIdChangeArgs);
            CustomerId = new DataWrapper<Int32>(this, customerIdChangeArgs);
            ProductId = new DataWrapper<Int32>(this, productIdChangeArgs);
            Quantity = new DataWrapper<Int32>(this, quantityChangeArgs);
            DeliveryDate = new DataWrapper<DateTime>(this, deliveryDateChangeArgs);

            //fetch list of all DataWrappers, so they can be used again later without the
            //need for reflection
            cachedListOfDataWrappers =
                DataWrapperHelper.GetWrapperProperties<OrderModel>(this);

            #endregion

            #region Create Validation Rules

            quantity.AddRule(quantityRule);

            #endregion


            //I could not be bothered to write a full DateTime picker in
            //WPF, so for the purpose of this demo, DeliveryDate is
            //fixed to DateTime.Now
            DeliveryDate.DataValue = DateTime.Now;
        }

        static OrderModel()
        {
            quantityRule = new SimpleRule("DataValue", "Quantity can not be < 0",
                      (Object domainObject)=>
                      {
                          DataWrapper<Int32> obj = (DataWrapper<Int32>)domainObject;
                          return obj.DataValue <= 0;
                      });
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// OrderId
        /// </summary>
        static PropertyChangedEventArgs orderIdChangeArgs =
            ObservableHelper.CreateArgs<OrderModel>(x => x.OrderId);

        public Cinch.DataWrapper<Int32> OrderId
        {
            get { return orderId; }
            private set
            {
                orderId = value;
                NotifyPropertyChanged(orderIdChangeArgs);
            }
        }

        /// <summary>
        /// CustomerId
        /// </summary>
        static PropertyChangedEventArgs customerIdChangeArgs =
            ObservableHelper.CreateArgs<OrderModel>(x => x.CustomerId);

        public Cinch.DataWrapper<Int32> CustomerId
        {
            get { return customerId; }
            private set
            {
                customerId = value;
                NotifyPropertyChanged(customerIdChangeArgs);
            }
        }

        /// <summary>
        /// ProductId
        /// </summary>
        static PropertyChangedEventArgs productIdChangeArgs =
            ObservableHelper.CreateArgs<OrderModel>(x => x.ProductId);

        public Cinch.DataWrapper<Int32> ProductId
        {
            get { return productId; }
            private set
            {
                productId = value;
                NotifyPropertyChanged(productIdChangeArgs);
            }
        }

        /// <summary>
        /// Quantity
        /// </summary>
        static PropertyChangedEventArgs quantityChangeArgs =
            ObservableHelper.CreateArgs<OrderModel>(x => x.Quantity);

        public Cinch.DataWrapper<Int32> Quantity
        {
            get { return quantity; }
            private set
            {
                quantity = value;
                NotifyPropertyChanged(quantityChangeArgs);
            }
        }

        /// <summary>
        /// DeliveryDate
        /// </summary>
        static PropertyChangedEventArgs deliveryDateChangeArgs =
            ObservableHelper.CreateArgs<OrderModel>(x => x.DeliveryDate);

        public Cinch.DataWrapper<DateTime> DeliveryDate
        {
            get { return deliveryDate; }
            private set
            {
                deliveryDate = value;
                NotifyPropertyChanged(deliveryDateChangeArgs);
            }
        }

        /// <summary>
        /// Returns cached collection of DataWrapperBase
        /// </summary>
        public IEnumerable<DataWrapperBase> CachedListOfDataWrappers
        {
            get { return cachedListOfDataWrappers; }
        }
        #endregion

        #region Overrides
        /// <summary>
        /// Is the Model Valid
        /// </summary>
        public override bool IsValid
        {
            get
            {
                //return base.IsValid and use DataWrapperHelper, if you are
                //using DataWrappers
                return base.IsValid &&
                    DataWrapperHelper.AllValid(cachedListOfDataWrappers);

            }
        }
        #endregion
    }
}

然后您需要像这样覆盖 IsValid 属性,其中我们为整个对象得出一个组合的 IsValid,它不仅基于其自身的 IsValid,还基于任何嵌套的 Cinch.DataWrapper<T> 对象的 IsValid 状态,这很容易,因为它们也继承自 Cinch.EditableValidatingObject,而后者又继承自 Cinch.ValidatingObject,因此它们已经有了 IDataErrorInfo 实现,所以处理起来并不难。

我知道这似乎是额外的工作,但 ViewModel 能够设置模型单个字段的可编辑状态,并让视图通过绑定无缝反映这一点所带来的好处是不可忽视的。

/// <summary>
/// Override hook which allows us to also put any child 
/// EditableValidatingObject objects IsValid state into
/// a combined IsValid state for the whole Model
/// </summary>
public override bool IsValid
{
    get
    {
       //return base.IsValid and use DataWrapperHelper, if you are
       //using DataWrappers
       return base.IsValid &&
          DataWrapperHelper.AllValid(cachedListOfDataWrappers);
    }
}

通常,我们需要为需要为 IDataErrorInfo 提供验证支持的 TextBox 使用的 WPF 样式如下所示,其中我们使用 Validation.HasError 属性来更改 TextBox 边框的颜色,当存在验证错误时。

<Style x:Key="ValidatingTextBox" TargetType="{x:Type TextBoxBase}">
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="OverridesDefaultStyle" Value="True"/>

    <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>

    <Setter Property="MinWidth" Value="120"/>

    <Setter Property="MinHeight" Value="20"/>

    <Setter Property="AllowDrop" Value="true"/>
    <Setter Property="Validation.ErrorTemplate" Value="{x:Null}"/>

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBoxBase}">
                <Border 
                      Name="Border"
                      CornerRadius="5" 
                      Padding="2"

                      Background="White"
                      BorderBrush="Black"
                      BorderThickness="2" >
                    <ScrollViewer Margin="0" x:Name="PART_ContentHost"/>

                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter TargetName="Border" 
                           Property="Background" Value="LightGray"/>

                        <Setter TargetName="Border" 
                           Property="BorderBrush" Value="Black"/>
                        <Setter Property="Foreground" Value="Gray"/>

                    </Trigger>
                    <Trigger Property="Validation.HasError" Value="true">
                        <Setter TargetName="Border" Property="BorderBrush" 
                                Value="Red"/>

                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>

        <Trigger Property="Validation.HasError" Value="true">
            <Setter Property="ToolTip"
                        Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                        Path=(Validation.Errors).CurrentItem.ErrorContent}"/>

        </Trigger>

    </Style.Triggers>
</Style>

IEditableObject 支持

我过去曾使用过一个称为 Memento 的模式,它基本上是一个用于支持业务对象撤销的出色模式。基本上,它允许将对象的状态存储到一个 Memento 后备对象中,该对象与它正在存储状态的业务对象具有完全相同的属性。因此,当您开始编辑业务对象时,您会将当前状态存储在 Memento 中,然后进行编辑。如果您取消编辑,业务对象的状态将从 Memento 恢复。这确实效果很好,但 Microsoft 也通过一个名为 IEditableObject 的接口来支持这一点,该接口如下所示:

  • BeginEdit()
  • CancelEdit()
  • EndEdit()

因此,通过使用此接口,我们可以让我们的业务对象自行存储其状态。现在,我不能为此代码归功于我,它来自 Mark Smith 出色的作品。实际上,Cinch 的相当一部分都归功于 Mark Smith 的作品。同样,我曾问 Mark 是否可以抄袭他的代码,他说可以,太好了,谢谢 Mark。

Cinch 提供了一个基类,可以用于继承业务对象;此基类还支持通过我们上面讨论过的 IDataErrorInfo 接口进行验证。它的工作原理如下。在 BeginEdit() 时,会使用一些反射/LINQ 将当前对象的状态存储到内部的 Dictionary 中。在 CancelEdit() 时,内部的 Dictionary 值将使用属性名作为存储的 Dictionary 状态的键来恢复到当前对象的属性。

此图显示了这一点:

这是执行所有这些操作的 Cinch 基类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Reflection;
using System.Diagnostics;

namespace Cinch
{
    /// <summary>
    /// Provides a IDataErrorInfo validating object that is also
    /// editable by implementing the IEditableObject interface
    /// </summary>
    public abstract partial class EditableValidatingObject :
        ValidatingObject, IEditableObject
    {
        #region Data
        /// <summary>
        /// This stores the current "copy" of the object. 
        /// If it is non-null, then we are in the middle of an 
        /// editable operation.
        /// </summary>
        private Dictionary<string, object> _savedState;
        #endregion

        #region Public/Protected Methods
        /// <summary>
        /// Begins an edit on an object.
        /// </summary>

        public void BeginEdit()
        {
            OnBeginEdit();
            _savedState = GetFieldValues();
        }

        /// <summary>
        /// Interception point for derived logic to do work when beginning edit.
        /// </summary>
        protected virtual void OnBeginEdit()
        {
        }

        /// <summary>
        /// Discards changes since the last 
        /// <see cref="M:System.ComponentModel.IEditableObject.BeginEdit"/> call.
        /// </summary>
        public void CancelEdit()
        {
            OnCancelEdit();
            RestoreFieldValues(_savedState);
            _savedState = null;
        }

        /// <summary>
        /// This is called in response CancelEdit and provides an interception point.
        /// </summary>
        protected virtual void OnCancelEdit()
        {
        }

        /// <summary>
        /// Pushes changes since the last 
        /// <see cref="M:System.ComponentModel.IEditableObject.BeginEdit"/> 
        /// or <see cref="M:System.ComponentModel.IBindingList.AddNew"/> 
        /// call into the underlying object.
        /// </summary>
        public void EndEdit()
        {
            OnEndEdit();
            _savedState = null;
        }

        /// <summary>
        /// This is called in response EndEdit and provides an interception point.
        /// </summary>
        protected virtual void OnEndEdit()
        {
        }

        /// <summary>
        /// This is used to clone the object. 
        /// Override the method to provide a more efficient clone. 
        /// The default implementation simply reflects across 
        /// the object copying every field.
        /// </summary>
        /// <returns>Clone of current object</returns>
        protected virtual Dictionary<string, object> GetFieldValues()
        {
            return GetType().GetProperties(BindingFlags.Public |
                BindingFlags.NonPublic | BindingFlags.Instance)
                .Where(pi => pi.CanRead && pi.GetIndexParameters().Length == 0)
                .Select(pi => new { Key = pi.Name, Value = pi.GetValue(this, null) })
                .ToDictionary(k => k.Key, k => k.Value);

        }

        /// <summary>
        /// This restores the state of the current object from the passed clone object.
        /// </summary>
        /// <param name="fieldValues">Object to restore state from</param>
        protected virtual void RestoreFieldValues(Dictionary<string, object> fieldValues)
        {
            foreach (PropertyInfo pi in GetType().GetProperties(
                BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                .Where(pi => pi.CanWrite && pi.GetIndexParameters().Length == 0) )
            {
                object value;
                if (fieldValues.TryGetValue(pi.Name, out value))
                    pi.SetValue(this, value, null);
                else
                {
                    Debug.WriteLine("Failed to restore property " +
                    pi.Name + " from cloned values, property not found in Dictionary.");
                }
            }
        }
        #endregion
    }
}

所以,要获得可编辑性支持,您所要做的就是让您的 UI 模型对象继承自 Cinch.EditableValidatingObject。工作完成。

让我们看看您可能如何将继承自 Cinch.EditableValidatingObject 的对象置于编辑模式。

嗯,从 ViewModel,我们可以简单地执行 this.CurrentCustomer.BeginEdit();就是这么简单。但是,您还**必须**做的是,如果您有任何嵌套的 Cinch.DataWrapper<T> 对象,请确保它们被置于正确的状态。您应该在 UI 模型类中这样做,其中我们只需覆盖从 Cinch.EditableValidatingObject 继承而来的 protected virtual void OnBeginEdit()

其中 UI 模型对象可能如下所示:

using System;

using Cinch;
using MVVM.DataAccess;
using System.ComponentModel;
using System.Collections.Generic;

namespace MVVM.Models
{
    /// <summary>
    /// Respresents a UI Order Model, which has all the
    /// good stuff like Validation/INotifyPropertyChanged/IEditableObject
    /// which are all ready to use within the base class.
    /// 
    /// This class also makes use of <see cref="Cinch.DataWrapper">
    /// Cinch.DataWrapper</see>s. Where the idea is that the ViewModel
    /// is able to control the mode for the data, and as such the View
    /// simply binds to a instance of a <see cref="Cinch.DataWrapper">
    /// Cinch.DataWrapper</see> for both its data and its editable state.
    /// Where the View can disable a control based on the 
    /// <see cref="Cinch.DataWrapper">Cinch.DataWrapper</see> editable state.
    /// </summary>
    public class OrderModel : Cinch.EditableValidatingObject
    {
        #region Data
        //Any data item is declared as a Cinch.DataWrapper, to allow the ViewModel
        //to decide what state the data is in, and the View just renders 
        //the data state accordingly
        private Cinch.DataWrapper<Int32> orderId;
        private Cinch.DataWrapper<Int32> customerId;
        private Cinch.DataWrapper<Int32> productId;
        private Cinch.DataWrapper<Int32> quantity;
        private Cinch.DataWrapper<DateTime> deliveryDate;
        private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;

        //rules
        private static SimpleRule quantityRule;

        #endregion

        #region Ctor
        public OrderModel()
        {
            #region Create DataWrappers

            OrderId = new DataWrapper<Int32>(this, orderIdChangeArgs);
            CustomerId = new DataWrapper<Int32>(this, customerIdChangeArgs);
            ProductId = new DataWrapper<Int32>(this, productIdChangeArgs);
            Quantity = new DataWrapper<Int32>(this, quantityChangeArgs);
            DeliveryDate = new DataWrapper<DateTime>(this, deliveryDateChangeArgs);

            //fetch list of all DataWrappers, so they can be used again later without the
            //need for reflection
            cachedListOfDataWrappers =
                DataWrapperHelper.GetWrapperProperties<OrderModel>(this);

            #endregion

            #region Create Validation Rules

            quantity.AddRule(quantityRule);

            #endregion


            //I could not be bothered to write a full DateTime picker in
            //WPF, so for the purpose of this demo, DeliveryDate is
            //fixed to DateTime.Now
            DeliveryDate.DataValue = DateTime.Now;
        }

        static OrderModel()
        {

            quantityRule = new SimpleRule("DataValue", "Quantity can not be < 0",
                      (Object domainObject)=>
                      {
                          DataWrapper<Int32> obj = (DataWrapper<Int32>)domainObject;
                          return obj.DataValue <= 0;
                      });
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// OrderId
        /// </summary>
        static PropertyChangedEventArgs orderIdChangeArgs =
            ObservableHelper.CreateArgs<OrderModel>(x => x.OrderId);

        public Cinch.DataWrapper<Int32> OrderId
        {
            get { return orderId; }
            private set
            {
                orderId = value;
                NotifyPropertyChanged(orderIdChangeArgs);
            }
        }

        /// <summary>
        /// CustomerId
        /// </summary>
        static PropertyChangedEventArgs customerIdChangeArgs =
            ObservableHelper.CreateArgs<OrderModel>(x => x.CustomerId);

        public Cinch.DataWrapper<Int32> CustomerId
        {
            get { return customerId; }
            private set
            {
                customerId = value;
                NotifyPropertyChanged(customerIdChangeArgs);
            }
        }

        /// <summary>
        /// ProductId
        /// </summary>
        static PropertyChangedEventArgs productIdChangeArgs =
            ObservableHelper.CreateArgs<OrderModel>(x => x.ProductId);

        public Cinch.DataWrapper<Int32> ProductId
        {
            get { return productId; }
            private set
            {
                productId = value;
                NotifyPropertyChanged(productIdChangeArgs);
            }
        }

        /// <summary>
        /// Quantity
        /// </summary>
        static PropertyChangedEventArgs quantityChangeArgs =
            ObservableHelper.CreateArgs<OrderModel>(x => x.Quantity);

        public Cinch.DataWrapper<Int32> Quantity
        {
            get { return quantity; }
            private set
            {
                quantity = value;
                NotifyPropertyChanged(quantityChangeArgs);
            }
        }

        /// <summary>
        /// DeliveryDate
        /// </summary>
        static PropertyChangedEventArgs deliveryDateChangeArgs =
            ObservableHelper.CreateArgs<OrderModel>(x => x.DeliveryDate);

        public Cinch.DataWrapper<DateTime> DeliveryDate
        {
            get { return deliveryDate; }
            private set
            {
                deliveryDate = value;
                NotifyPropertyChanged(deliveryDateChangeArgs);
            }
        }

        /// <summary>
        /// Returns cached collection of DataWrapperBase
        /// </summary>
        public IEnumerable<DataWrapperBase> CachedListOfDataWrappers
        {
            get { return cachedListOfDataWrappers; }
        }
        #endregion

        #region Overrides
        /// <summary>
        /// Is the Model Valid
        /// </summary>
        public override bool IsValid
        {
            get
            {
                //return base.IsValid and use DataWrapperHelper, if you are
                //using DataWrappers
                return base.IsValid &&
                    DataWrapperHelper.AllValid(cachedListOfDataWrappers);

            }
        }
        #endregion

        #region Static Methods
        /// <summary>
        /// Allows Service layer objects to be translated into
        /// UI objects
        /// </summary>
        /// <param name="cust">Service layer object</param>
        /// <returns>UI layer object</returns>
        public static OrderModel OrderToOrderModel(Order order)
        {
            OrderModel orderModel = new OrderModel();
            orderModel.OrderId.DataValue = order.OrderId;
            orderModel.CustomerId.DataValue = order.CustomerId;
            orderModel.Quantity.DataValue = order.Quantity;
            orderModel.ProductId.DataValue = order.ProductId;
            orderModel.DeliveryDate.DataValue = order.DeliveryDate;
            return orderModel;

        }
        #endregion

        #region EditableValidatingObject overrides

        /// <summary>
        /// Override hook which allows us to also put any child 
        /// EditableValidatingObject objects into the BeginEdit state
        /// </summary>
        protected override void OnBeginEdit()
        {
            base.OnBeginEdit();
            //Now walk the list of properties in the OrderModel
            //and call BeginEdit() on all Cinch.DataWrapper<T>s.
            //we can use the Cinch.DataWrapperHelper class for this
            DataWrapperHelper.SetBeginEdit(cachedListOfDataWrappers);
        }

        /// <summary>
        /// Override hook which allows us to also put any child 
        /// EditableValidatingObject objects into the EndEdit state
        /// </summary>
        protected override void OnEndEdit()
        {
            base.OnEndEdit();
            //Now walk the list of properties in the CustomerModel
            //and call CancelEdit() on all Cinch.DataWrapper<T>s.
            //we can use the Cinch.DataWrapperHelper class for this
            DataWrapperHelper.SetEndEdit(cachedListOfDataWrappers);
        }

        /// <summary>
        /// Override hook which allows us to also put any child 
        /// EditableValidatingObject objects into the CancelEdit state
        /// </summary>
        protected override void OnCancelEdit()
        {
            base.OnCancelEdit();
            //Now walk the list of properties in the CustomerModel
            //and call CancelEdit() on all Cinch.DataWrapper<T>s.
            //we can use the Cinch.DataWrapperHelper class for this
            DataWrapperHelper.SetCancelEdit(cachedListOfDataWrappers);

        }
        #endregion
    }
}

我们需要像这样覆盖 Cinch.EditableValidatingObject 的虚拟方法:

#region EditableValidatingObject overrides

/// <summary>
/// Override hook which allows us to also put any child 
/// EditableValidatingObject objects into the BeginEdit state
/// </summary>
protected override void OnBeginEdit()
{
    base.OnBeginEdit();
    //Now walk the list of properties in the CustomerModel
    //and call BeginEdit() on all Cinch.DataWrapper<T>s.
    //we can use the Cinch.DataWrapperHelper class for this
    DataWrapperHelper.SetBeginEdit(cachedListOfDataWrappers);
}

/// <summary>
/// Override hook which allows us to also put any child 
/// EditableValidatingObject objects into the EndEdit state
/// </summary>
protected override void OnEndEdit()
{
    base.OnEndEdit();
    //Now walk the list of properties in the CustomerModel
    //and call CancelEdit() on all Cinch.DataWrapper<T>s.
    //we can use the Cinch.DataWrapperHelper class for this
    DataWrapperHelper.SetEndEdit(cachedListOfDataWrappers);
}

/// <summary>
/// Override hook which allows us to also put any child 
/// EditableValidatingObject objects into the CancelEdit state
/// </summary>
protected override void OnCancelEdit()
{
    base.OnCancelEdit();
    //Now walk the list of properties in the CustomerModel
    //and call CancelEdit() on all Cinch.DataWrapper<T>s.
    //we can use the Cinch.DataWrapperHelper class for this
    DataWrapperHelper.SetCancelEdit(cachedListOfDataWrappers);

}
#endregion

其中 Cinch 框架提供了一个名为 DataWrapperHelper 的静态助手,您**必须**使用它来设置嵌套的 DataWrapper<T> 对象的正确编辑状态;您可以使用这些助手方法:

  • DataWrapperHelper.SetBeginEdit(IEnumerable<DataWrapperBase> wrapperProperties)
  • DataWrapperHelper.SetEndEdit(IEnumerable<DataWrapperBase> wrapperProperties)
  • DataWrapperHelper.SetCancelEdit(IEnumerable<DataWrapperBase> wrapperProperties)

其中 IEnumerable<DataWrapperBase> wrapperProperties 实际上是在对象构造期间获得的 cachedListOfDataWrappers

您不必担心这一点,Cinch 会为您处理,前提是您在 UI 模型类中正确操作。如果您对此感到有些迷茫,请不要担心:后续文章中的一篇将介绍如何使用 Cinch 创建 UI 模型。本文更多的是关于 Cinch 内部机制,以供有兴趣的读者阅读。

弱事件创建

在我开始讨论如何创建 WeakEvent 之前,我认为这是一个很好的起点,可以进行一些小小的讨论。我想有很多读者/。NET 开发者认为 .NET 中的事件很棒。嗯,我也是,我喜欢事件。问题是,有多少人认为他们需要过多地担心垃圾回收,并且在处理事件时,.NET 通过 GC 管理自己的内存,对吗?是的,它确实如此,但事件是一个领域,可以说是 .NET 中有点灰色地带。

在上图所示的示例中,有一个对象(eventExposer)声明了一个事件(SpecialEvent)。然后,创建了一个窗体(myForm),该窗体向该事件添加了一个处理程序。窗体关闭,预期是窗体将被垃圾回收,但它没有。不幸的是,事件的底层委托仍然对窗体保持强引用,因为窗体的处理程序未被移除。

图片和文字摘自 http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx

在典型应用程序中,附加到事件源的处理程序可能不会与附加处理程序的侦听器对象同步销毁。这种情况可能导致内存泄漏。Windows Presentation Foundation (WPF) 引入了一种可以解决此问题的设计模式,它通过为特定事件提供专门的管理器类,并在事件的侦听器上实现接口来实现。这种设计模式称为弱事件模式。

MSDN: http://msdn.microsoft.com/en-us/library/aa970850.aspx

现在,如果您曾经研究过弱事件管理器/接口实现,您会意识到这需要大量工作,并且您必须为每种 Event 类型都有一个新的 WeakEventManager。在我看来,这似乎工作量太大了,所以我宁愿采用其他机制,例如一开始就有一个 WeakEvent。更好的是,也许有一个弱侦听器,只有当事件源仍然存活且未被 GC 回收时,它才会对源事件做出反应。

引发一个 WeakEvent<T>

那么,事不宜迟,让我向您展示一些 Cinch 在处理事件时可用的方便的小助手,以及可能使它们变弱。在可能的情况下,手动添加/删除事件的委托仍然更好,但有时您就是不知道对象的生命周期,因此最好选择 WeakEvent 策略。

首先,让我们使用绝对出色的 WeakEvent<T>,它来自非常有才华的 Daniel Grunwald,他很久以前就发布了一篇关于 WeakEvents精彩文章。Daniel 的 WeakEvent<T> 展示了如何以弱引用的方式引发事件。

我不会用 WeakEvent<T> 的所有代码来让您感到厌烦,但有一件事您应该熟悉,如果您还不熟悉的话,那就是 WeakReference 类。这是一个标准的 .NET 类,它引用一个对象,同时仍然允许该对象被垃圾回收。

几乎所有的 WeakEvent 订阅/事件引发都会使用内部的 WeakReference 类来允许事件源或订阅者被 GC 回收。

总之,要使用 Daniel Grunwald 的 WeakEvent<T>,我们可以这样做:

声明 WeakEvent<T>

private readonly WeakEvent<EventHandler<EventArgs>> 
     dependencyChangedEvent =
         new WeakEvent<EventHandler<EventArgs>>();

public event EventHandler<EventArgs> DependencyChanged
{
  add { dependencyChangedEvent.Add(value); }
  remove { dependencyChangedEvent.Remove(value); }
}

引发 WeakEvent<T>

dependencyChangedEvent.Raise(this, new EventArgs());

监听 WeakEvent<T>

SourceDependency.DependencyChanged += OnSourceChanged;
...
private void OnSourceChanged(object sender, EventArgs e)
{

}

这就是如何创建 WeakEvent<T> 的方法,但有时不是您自己的代码,您也不负责代码中包含的事件。也许您正在使用第三方控件集。在这种情况下,您可能需要使用 WeakEvent 订阅。Cinch 提供了两种方法来实现这一点。

弱事件订阅

上面我们看到了如何使用 Daniel Grunwald 的 WeakEvent<T> 来引发 WeakEvent,那么当我们想订阅现有事件时该怎么办?同样,这通常使用 WeakReference 类来检查 WeakReference.Target 是否为 null。如果值为 null,则事件源已被垃圾回收,因此不要引发调用列表委托。如果它不为 null,则事件源已存活,因此调用已订阅事件的调用列表委托。

Cinch 提供了两种方法来做到这一点。

WeakEventProxy

这是一个很棒的小类,Paul Stovell 在很久以前就写了。整个类如下所示:

using System;

namespace Cinch
{
    public class WeakEventProxy<TEventArgs> : IDisposable
        where TEventArgs : EventArgs
    {
        #region Data
        private WeakReference callbackReference;
        private readonly object syncRoot = new object();
        #endregion

        #region Ctor
        /// <summary>
        /// Initializes a new instance of the <see 
          /// cref="WeakEventProxy<TEventArgs>"/> class.
        /// </summary>

        /// <param name="callback">The callback.</param>
        public WeakEventProxy(EventHandler<TEventArgs> callback)
        {
            callbackReference = new WeakReference(callback, true);
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Used as the event handler which should be subscribed to source collections.
        /// </summary>

        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void Handler(object sender, TEventArgs e)
        {
            //acquire callback, if any
            EventHandler<TEventArgs> callback;
            lock (syncRoot)
            {
                callback = callbackReference == null ? null : 
            callbackReference.Target as EventHandler<TEventArgs>;
            }

            if (callback != null)
            {
                callback(sender, e);
            }
        }

        /// <summary>

        /// Performs application-defined tasks associated with freeing, 
          /// releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            lock (syncRoot)
            {
                GC.SuppressFinalize(this);

                if (callbackReference != null)
                {
                    //test for null in case the reference was already cleared
                    callbackReference.Target = null;
                }

                callbackReference = null;
            }
        }
        #endregion
    }
}

使用它,我们可以简单地这样做:

声明事件处理程序,如下所示:

private EventHandler<NotifyCollectionChangedEventArgs> 
     collectionChangeHandler;
private WeakEventProxy<NotifyCollectionChangedEventArgs> 
     weakCollectionChangeListener;

像这样连接事件订阅委托:

if (weakCollectionChangeListener == null)
{
  collectionChangeHandler = OnCollectionChanged;
  weakCollectionChangeListener = 
     new WeakEventProxy<NotifyCollectionChangedEventArgs>(
         collectionChangeHandler);
}
ncc.CollectionChanged += weakCollectionChangeListener.Handler;


private void OnCollectionChanged(object sender, 
 NotifyCollectionChangedEventArgs e)
{

}

带自动取消订阅的弱事件订阅程序

有一天我在网上浏览,找到了这篇关于弱事件的精彩文章:http://diditwith.net/PermaLink,guid,aacdb8ae-7baa-4423-a953-c18c1c7940ab.aspx。这些链接包含了一些我在 Cinch 中使用的很棒的代码,它不仅允许用户创建 WeakEvent 订阅,还允许用户指定一个自动取消订阅回调委托。此外,使用此代码的一个小变体,可以使所有订阅的事件处理程序都成为弱引用。让我们快速看一下这两种操作的语法:

指定带取消订阅的弱事件订阅

我们只需这样做:

workspace.CloseWorkSpace +=
    new EventHandler<EventArgs>(OnCloseWorkSpace).
       MakeWeak(eh => workspace.CloseWorkSpace -= eh);
       
private void OnCloseWorkSpace(object sender, EventArgs e)
{
}

这一行创建了一个带有自动取消订阅的弱侦听器。很方便,不是吗?

我提到您也可以使用此代码创建 WeakEvent,使得对特定事件的所有订阅者都成为弱引用。使用此代码可以做到这一点:

public class EventProvider
{
    private EventHandler<EventArgs> closeWorkSpace;
    public event EventHandler<EventArgs> CloseWorkSpace
    {
        add
        {
            closeWorkSpace += value.MakeWeak(eh => closeWorkSpace -= eh);
        }
        remove
        {
        }
    }
}

正如我所说,我不能为此代码承担太多功劳,它来自指定的链接,但我确实认为它非常方便。我们实际上在生产代码中使用它,而且问题不大。我唯一注意到的一点是,它与 ObservableCollection<T>CollectionChanged 不太兼容,但那时我只使用我上面提到的 Cinch 中的 WeakEventProxy,它工作正常。

中介者消息传递

现在,我不知道您的情况,但一般来说,当我和 MVVM 框架一起工作时,我不会有一个 ViewModel 来管理整个应用程序。我实际上有许多 ViewModel(事实上,我们有很多)。在使用标准 MVVM 模式时的一个问题是跨 ViewModel 的通信。毕竟,构成应用程序的 ViewModel 可能都是独立的、未连接的对象,它们彼此一无所知。然而,它们需要了解用户执行的某些操作。这里有一个具体的例子。

假设您有两个视图,一个用于客户,一个用于客户的订单。让我们说订单视图正在使用 OrdersViewModel,而客户视图正在使用 CustomersViewModel,并且当客户的订单被更新、删除或添加时,客户视图应该显示某种视觉触发器,以提醒用户某个客户的订单详细信息已更改。

听起来很简单,对吧?然而,我们有两个独立的视图由两个独立的 ViewModel 运行,没有链接,但显然,需要某种从 OrdersViewModelCustomersViewModel 的连接,某种消息传递。

这正是中介者模式的意义所在,它是一个简单轻量级的消息传递系统。我很久以前在 我的博客 上写过关于这个的问题,而 Josh Smith / Marlon Grech(作为一个整体)把它做得更好,他们想出了您将在 Cinch 中看到的 Mediator 实现。

那么中介者是如何工作的呢?

这张图可能有所帮助:

这个想法很简单,中介者监听传入的消息,查看谁对某个特定消息感兴趣,然后调用所有已订阅该消息的接收者。消息通常是字符串。

基本上,发生的情况是,有一个 Mediator 实例(通常作为 ViewModelBase 类的静态属性暴露)在那里等待对象订阅它,使用:

  • 一个完整的对象引用。然后,使用反射会找到在注册对象上标记有 MediatorMessageSinkAttribute 属性的所有 Mediator 消息方法,并自动创建一个回调委托。
  • 一个实际的 Lambda 回调委托。

无论哪种情况,Mediator 都维护着一个 WeakAction 回调委托列表。其中每个 WeakAction 都是一个委托,它使用内部的 WeakReference 类来检查 WeakReference.Target 是否为 null,然后再回调委托。这考虑到了回调委托的目标可能不再存活的事实,因为它可能已被垃圾回收。指向不再存活的对象的 WeakAction 回调委托的任何实例都会从 Mediator WeakAction 回调委托列表中移除。

当获得回调委托时,将调用原始回调委托,或者将调用标记有 MediatorMessageSinkAttribute 属性的 Mediator 消息方法。

以下是如何以所有可能的方式使用中介者的示例:

注册消息

使用显式回调委托(这不是我的首选选项)

我们只需创建正确类型的委托,并使用 Mediator 注册一个回调以接收消息通知。

public delegate void DummyDelegate(Boolean dummy);
...

Mediator.Register("AddCustomerMessage", new DummyDelegate((x) =>
{
    AddCustomerCommand.Execute(null);
}));

注册整个对象,并使用 MediatorMessageSinkAttribute 属性

这是我最喜欢的方法,也是我最简单的方法。您只需使用 Mediator 注册整个对象,并为一些消息钩子方法打上属性标记。

因此,注册操作在 Cinch 中已经为您完成,您无需执行任何操作。只需继承自 ViewModelBase,工作就完成了。如果您想知道这是如何完成的,Cinch 中的 ViewModelBase 类会像这样使用 Mediator 注册它自己:

//Register all decorated methods to the Mediator
Mediator.Register(this);

因此,任何标记有 MediatorMessageSinkAttribute 属性的方法都将在注册对象上找到(使用反射),并且会自动创建一个回调委托。这里有一个例子:

/// <summary>
/// Mediator callback from StartPageViewModel
/// </summary>
/// <param name="dummy">Dummy not needed</param>

[MediatorMessageSink("AddCustomerMessage"))]
private void AddCustomerMessageSink(Boolean dummy)
{
    AddCustomerCommand.Execute(null);
}

那么消息通知如何呢?

消息通知

这很容易做到,我们只需使用 Mediator.NotifyCollegues() 方法,如下所示:

//Use the Mediator to send a Message to MainWindowViewModel to add a new 
//Workspace item
Mediator.NotifyColleagues<Boolean>("AddCustomerMessage", true);

您也可以像这样异步使用中介者:

Mediator.NotifyColleaguesAsync<Boolean>("AddCustomerMessage", true);

目前,Cinch ViewModelBase 类在 ViewModel 构造函数中将 ViewModel 实例注册到 Mediator,并在 Dispose() 方法中将 ViewModel 实例从 Mediator 注销。

因此,任何订阅此消息的对象现在都将通过 Mediator WeakAction 回调委托列表进行回调。

即将推出什么?

在后续文章中,我将大致如下展示:

  1. Cinch 及其内部机制的演练:II
  2. 如何使用Cinch开发ViewModels
  3. 如何使用 Cinch 应用对 ViewModel 进行单元测试,包括如何测试可能在 Cinch ViewModel 中运行的后台工作线程。
  4. 使用Cinch的演示应用程序

就是这些了,希望您喜欢

这就是我目前想说的全部内容,但我希望通过这篇文章您可以看到 Cinch 的发展方向以及它如何帮助您使用 MVVM。在我们继续我们的旅程时,我们将涵盖 Cinch 中剩余的项目,然后我们将继续了解如何使用 Cinch 开发应用程序。

谢谢

一如既往,欢迎投票/评论。

历史

  • 2009/07/19:初始发布
  • 25/07/2009:
    • 修改了 NumericTextBoxBehavior 以考虑粘贴的数据
    • INotifyPropertyChanged 更改为使用静态 LINQ 表达式树
  • 2009/08/02:将 INotifyPropertyChanged 更改为使用 Phil(一位读者)的想法,您可以在此处阅读更多内容:CinchII.aspx?msg=3141144#xx3141144xx
  • 2009/08/15:为 DataWrapper 添加了通知父级何时更改内部 DataWrapper 属性的支持
  • 2009/09/12:为 DataWrapper 添加了缓存支持并修复了 RegExRule
  • 2009/12/05:添加了新的代码段,向用户展示如何使用 Cinch 中的新验证方法添加验证规则
  • 2009/12/24:更新了 NumericTextBoxBehavior 的代码片段
  • 2009/05/07:更新了文章以显示新的 ViewModelBase 视图生命周期属性并更新了 DataWrapper 代码,还更新了 Mediator 的用法
© . All rights reserved.