CinchV2 :我的 Cinch MVVM 框架的第 2 版:第 4 部分






4.89/5 (32投票s)
如果 Jack Daniels 制作 MVVM 框架。
目录
引言
上次,我们讨论了 Cinch V2 的全新内容。在本文中,我们将比较 Cinch V2 和 Cinch V1,并讨论哪些内容发生了变化,哪些保持不变,并在适当的时候,我将提供指向 Cinch V1 代码和 Cinch V2 代码的链接,以便您自己查看发生了哪些变化。
正如我承诺过的,在每篇文章中,我都会展示 Cinch V2 兼容性矩阵。
兼容性矩阵列出了类及其一般工作区域,以及它们是否与WPF或SL或两者兼容。
工作区域 | 类名 | WPF | Silverlight(4或更高版本) | 两者 |
业务对象 | EditableValidatingObject.cs | 是 | ||
业务对象 | ValidatingObject.cs | 是 | ||
业务对象 | DataWrapper.cs | 是 | ||
Commands | EventToCommandArgs.cs | 是 | ||
Commands | SimpleCommand.cs | 是 | ||
Commands | WeakEventHandlerManager.cs | 是 | ||
事件 | CloseRequestEventArgs.cs | 是 | ||
事件 | UICompletedEventArgs.cs | 是 | ||
弱事件 | WeakEvent.cs | 是 | ||
弱事件 | WeakEventHelper.cs | 是 | ||
弱事件 | WeakEventProxy.cs | 是 | ||
扩展方法 | DispatcherExtensions.cs | 是 | ||
扩展方法 | GenericListExtensions.cs | 是 | ||
交互行为 | CommandDrivenGoToStateAction.cs | 是 | ||
交互行为 | FocusBehaviourBase.cs | 是 | ||
交互行为 | NumericTextBoxBehaviour.cs | 是 | ||
交互行为 | SelectorDoubleClickCommandBehavior.cs | 是 | ||
交互行为 | TextBoxFocusBehavior.cs | 是 | ||
交互触发器 | CompletedAwareCommandTrigger.cs | 是 | ||
交互触发器 | CompletedAwareGotoStateCommandTrigger.cs | 是 | ||
交互触发器 | EventToCommandTrigger.cs | 是 | ||
消息中介者 | MediatorMessageSinkAttribute.cs | 是 | ||
消息中介者 | MediatorSingleton.cs | 是 | ||
服务实现 | ChildWindowService.cs | 是 | ||
服务实现 | SLMessageBoxService.cs | 是 | ||
服务实现 | ViewAwareStatus.cs | 是 | ||
服务实现 | ViewAwareStatusWindow.cs | 是 | ||
服务实现 | VSMService.cs | 是 | ||
服务实现 | WPFMessageBoxService.cs | 是 | ||
服务实现 | WPFOpenFileService.cs | 是 | ||
服务实现 | WPFSaveFileService.cs | 是 | ||
服务实现 | WPFUIVisualizerService.cs | 是 | ||
服务接口 | IChildWindowService.cs | 是 | ||
服务接口 | IMessageBoxService.cs | 是 | ||
服务接口 | IViewAwareStatus.cs | 是 | ||
服务接口 | IViewAwareStatusWindow.cs | 是 | ||
服务接口 | IVSM.cs | 是 | ||
服务接口 | IMessageBoxService.cs | 是 | ||
服务接口 | IOpenFileService.cs | 是 | ||
服务接口 | ISaveFileService.cs | 是 | ||
服务接口 | IUIVisualizerService.cs | 是 | ||
服务测试实现 | TestChildWindowService.cs | 是 | ||
服务测试实现 | TestMessageBoxService.cs | 是 | ||
服务测试实现 | TestViewAwareStatus.cs | 是 | ||
服务测试实现 | TestViewAwareStatusWindow.cs | 是 | ||
服务测试实现 | TestVSMService.cs | 是 | ||
服务测试实现 | TestMessageBoxService.cs | 是 | ||
服务测试实现 | TestOpenFileService.cs | 是 | ||
服务测试实现 | TestSaveFileService.cs | 是 | ||
服务测试实现 | TestUIVisualizerService.cs | 是 | ||
多线程 | AddRangeObservableCollection.cs(这是特定于SL的实现) | 是 | ||
多线程 | AddRangeObservableCollection.cs(这是特定于WPF的实现) | 是 | ||
多线程 | BackgroundTaskManager.cs | 是 | ||
多线程 | ISynchronizationContext.cs | 是 | ||
多线程 | UISynchronizationContext.cs | 是 | ||
多线程 | ApplicationHelper.cs | 是 | ||
多线程 | DispatcherNotifiedObservableCollection.cs | 是 | ||
菜单 | CinchMenuItem.cs | 是 | ||
实用程序 | ArgumentValidator.cs | 是 | ||
实用程序 | IWeakEventListener.cs(这是 SL 中缺失的 System 类,所以我创建了一个) |
是 | ||
实用程序 | ObservableHelper.cs | 是 | ||
实用程序 | PropertyChangedEventManager.cs(这是 SL 中缺失的 System 类,所以我创建了一个) |
是 | ||
实用程序 | PropertyObserver.cs | 是 | ||
实用程序 | BindingEvaluator.cs | 是 | ||
实用程序 | ObservableDictionary.cs | 是 | ||
实用程序 | TreeHelper.cs | 是 | ||
验证 | RegexRule.cs | 是 | ||
验证 | Rule.cs | 是 | ||
验证 | SimpleRule.cs | 是 | ||
ViewModels | EditableValidatingViewModelBase.cs | 是 | ||
ViewModels | IViewStatusAwareInjectionAware.cs | 是 | ||
ViewModels | ValidatingViewModelBase.cs | 是 | ||
ViewModels | ViewMode.cs | 是 | ||
ViewModels | ViewModelBase.cs | 是 | ||
ViewModels | ViewModelBaseSLSpecific.cs | 是 | ||
ViewModels | ViewModelBaseWPFSpecific.cs | 是 | ||
Workspaces | ChildWindowResolver.cs | 是 | ||
Workspaces | CinchBootStrapper.cs(SL 版本) | 是 | ||
Workspaces | CinchBootStrapper.cs(WPF版本) | 是 | ||
Workspaces | PopupNameToViewLookupKeyMetadataAttribute.cs | 是 | ||
Workspaces | IWorkspaceAware.cs | 是 | ||
Workspaces | MockView.cs | 是 | ||
Workspaces | NavProps.cs | 是 | ||
Workspaces | PopupResolver.cs | 是 | ||
Workspaces | ViewnameToViewLookupKeyMetadataAttribute.cs | 是 | ||
Workspaces | ViewResolver.cs | 是 | ||
Workspaces | WorkspaceData.cs | 是 |
既然我已经向您展示了哪些类可以与 WPF/SL 兼容,那么让我们继续阅读本文的其余部分,好吗?但在此之前,这里是旧的 Cinch V1 文章的链接。
如果您错过了 Cinch V1,并且对 MVVM 感兴趣,我强烈建议您先阅读所有 Cinch V1 文章,这将帮助您更深入地理解这些 Cinch V2 文章中的内容。
CinchV1 文章链接
也许你们中的一些人从未见过旧的 Cinch V1 文章,所以我也会在这里列出这些文章,因为当 Cinch V2 仍然使用与 Cinch V1 相同的功能时,我会将人们重定向到这些文章。
- Cinch入门文章
- Cinch及其内部机制的演练 I
- Cinch及其内部机制的演练 II
- 如何使用Cinch开发ViewModels
- 如何使用 Cinch 进行 ViewModel 的单元测试,包括如何测试 Cinch ViewModel 中可能运行的后台工作线程。
- 使用Cinch的演示应用程序
CinchV2 文章链接
- CinchV2:简介和 MEFedMVVM 以及 ViewModel/Service 解析
- CinchV2:服务/UI 服务
- CinchV2:全新内容
- CinchV2:深入探讨内容的变化/不变之处(本文)
- CinchV2:剖析 WPF 演示应用程序
- CinchV2:剖析 SL4 演示应用程序
文章路线图就是这样。我想现在是时候深入探讨本文的内容了,我们开始吧。
有什么变化?有什么保持不变?
现在我们可以进入本文的核心内容,这 realmente 是要向 Cinch 的新老用户展示哪些内容发生了变化,哪些内容保持不变。
V1 到 V2 的变化
本小节仅讨论 Cinch V1 的哪些方面从 V1 变为 V2。
SimpleCommand
在 Cinch V1 中,我确实有一个基本的委托式命令,但我并没有让它变得非常易于使用,所以 **Cinch** V2 中,我更进一步,创建了一个更好的 SimpleCommand
,它允许您为 CanExecute
传递一个 Func<T,TResult>
,为 Execute
传递一个 Action<T>
。我还提供了一个 CommandCompleted
事件,该事件对于各种事情都很有用。点击此链接了解更多:CinchV2_3.aspx#SimpleCommand。
附加属性 / 事件到命令
在 Cinch V1 中,我提供了一些附加的 DP。这些大多数已被 Blend Interactivity Actions/Triggers/Behaviours 取代。让我们看看 Cinch V1 的每个产品,我将向您展示它们在 Cinch V2 中被什么取代。
视图生命周期事件
这些是简单的附加 DP,您可以使用它们在 ViewModel 中运行命令。有用于 Loaded/Unloaded/Activated/DeActivated 等的 DP。现在它们已被 Cinch V2 的 IViewAwareStatus
服务取代。
您可以通过此链接了解更多关于 Cinch V1 如何处理此问题的信息:CinchII.aspx#Lifecycle,以及 Cinch V2 的 IViewAwareStatus
服务如何工作的信息:CinchV2_2.aspx#CoreServices。
数字文本框附加行为
这是一个简单的附加 DP,它允许用户指定一个文本框只能接受数字字符。这已简单地转换为 Blend Behaviour。
您可以通过此链接了解更多关于 Cinch V1 如何处理此问题的信息:CinchII.aspx#NumericAtt,以及 Cinch V2 的 Blend Behaviour 如何工作的信息:CinchV2_3.aspx#Interactivity。
附加命令行为
这是一系列附加 DP,允许用户将某个 FrameworkElement
的事件连接到其 ViewModel 中的绑定 ICommand
。现在它已被一个名为 EventToCommandTrigger
的 Blend Trigger 取代。
您可以通过此链接了解更多关于 Cinch V1 如何处理此问题的信息:CinchII.aspx#CommandAtt,以及 Cinch V2 的 EventToCommandTrigger
如何工作的信息:CinchV2_3.aspx#Interactivity。
工作区
在 Cinch V1 中,我提供了一种 ViewModel 优先的方法,并建议使用 DataTemplate 样式的方法进行 View-ViewModel 解析。这在 Cinch V2 中仍然受支持,但设计时支持远不如管理 Cinch V2 中工作区的新方法。
您可以通过以下链接了解更多关于这两种方法的信息。
- 在 Cinch V1 中,您可以这样做:CinchIII.aspx#CloseVM。
- 在 Cinch V2 中,您应该这样做:CinchV2_3.aspx#Workspaces。
线程辅助
在 Cinch V1 中,我已经包含了一些与线程相关的助手,例如 Application.DoEvents
和 Dispatcher
扩展方法。对于 Cinch V2,我包含了更多的线程助手类,并且还包含了原始的 Cinch V1 类。
实用工具
在 Cinch V1 中,我已经包含了一些与实用工具相关的助手,例如 ObservableHelper
、PropertyObserver
。对于 Cinch V2,我包含了更多的实用工具助手类,并且还包含了原始的 Cinch V1 类。
- V1 包含:CinchII.aspx#Better_INPC。
- V2 包含:CinchV2_3.aspx#ExtraUtilities
V1 到 V2 保持不变的
本小节仅讨论 Cinch V1 的哪些方面在 V1 和 V2 中保持不变。
ViewModel 模式
在使用 MVVM 和生产 LOB 应用程序时,我一直苦苦挣扎的一件事就是视图模式。例如,拥有一个只读视图,然后用户点击编辑,所有视图字段都可编辑,这样会很好。现在,这可以通过在 ViewModel 中创建一个命令来实现,该命令从 ReadOnly
模式更改为 EditMode
,视图上的所有 UIElement
都可以绑定到 ViewModel 上某个 CurrentMode
属性。听起来可以做到,但正如我们所知,事情从来没有那么简单。在我工作的地方,我们在数据录入方面有复杂的要求,不可能在单个视图上的所有数据录入字段上应用单一模式,绝对不可能。我们需要非常细粒度的数据录入权限,直到单个字段级别。
因此,这让我思考。我们需要的是 ViewModel 中每个数据项的可编辑状态。我对此进行了更多思考,并提出了一个通用包装类,它包装单个属性,但也公开一个 IsEditable
属性。现在视图可以访问这些包装器,因为它们是 ViewModel 中的公共属性,所以它可以将其数据绑定到包装器的数据属性,并根据包装器的 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
}
}
然后,您可以像这样将其用作 ViewModel 上的属性(不用担心继承自 Cinch.EditableValidatingViewModelBase
,我们稍后会讨论这个问题)。
public class OrderViewModel : Cinch.EditableValidatingViewModelBase
{
//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 OrderViewModel()
{
Quantity = new DataWrapper<int32>(this, quantityChangeArgs);
....
....
//Setup rules etc etc
}
static PropertyChangedEventArgs quantityChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.Quantity);
public Cinch.DataWrapper<Int32> Quantity
{
get { return quantity; }
private set
{
quantity = value;
NotifyPropertyChanged(quantityChangeArgs);
}
}
}
请注意,setter 是 private
的,这是因为这些对象是不可变的,并且只能在构造函数中设置。但是,IsEditable
和 DataValue
可以随时更改。另一件值得注意的事情是,ViewModel 在构造时实际上使用了一些反射来获取一个 IEnumerable<DataWrapperBase>
,它被用作一个缓存,因此之后对任何缓存的 DataWrapper<T>
属性的设置都非常快。这是通过以下方式实现的:
在构造函数中,我们有类似这样的内容:
using System;
using Cinch;
using MVVM.DataAccess;
using System.ComponentModel;
using System.Collections.Generic;
namespace MVVM.ViewModels
{
/// <summary>
/// Respresents a UI OrderViewModel, 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 OrderViewModel : Cinch.EditableValidatingViewModelBase
{
#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 OrderViewModel()
{
#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<OrderViewModel>(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
,每当它更改状态时,我们就需要更新 ViewModel 本身(或其他任何需要更改状态的对象)中所有嵌套的 DataWrapper<T>
对象的状态。
这里有一个示例 AddEditOrderViewModel
,它为我保存了一个类型为 OrderModel
的单个 UI 模型。正如我所说,有些人不喜欢这样,他们会让他们 ViewModel 公开 OrderModel
类型的数据模型中所有可用的属性。MVVM 的关键在于您按照自己的方式去做,而这是我的方式。我不在乎是否会向 Model 传递 InValid
数据,只要该 Model 不能保存到数据库即可。
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.EditableValidatingViewModelBase
{
private ViewMode currentViewMode = ViewMode.AddMode;
private Cinch.DataWrapper<Int32> quantity;
#region Ctor
public AddEditOrderViewModel()
{
#region Create DataWrappers
Quantity= new DataWrapper<Int32>(this, quantityChangeArgs );
//fetch list of all DataWrappers, so they can be used again later without the
//need for reflection
cachedListOfDataWrappers =
DataWrapperHelper.GetWrapperProperties<AddEditOrderViewModel>(this);
#endregion
}
#endregion
static PropertyChangedEventArgs quantityChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.Quantity);
public Cinch.DataWrapper<Int32> Quantity
{
get { return quantity; }
private set
{
quantity = value;
NotifyPropertyChanged(quantityChangeArgs);
}
}
/// <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:
Quantity.DataValue= 0;
this.DisplayName = "Add Order"
break;
case ViewMode.EditMode:
this.DisplayName = "Edit Order";
break;
case ViewMode.ViewOnlyMode:
this.DisplayName = "View Order";
break;
}
//Now change all the 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(
CachedListOfDataWrappers,
currentViewMode);
NotifyPropertyChanged(currentViewModeChangeArgs);
}
}
....
....
}
}
这里值得一提的一点是,当 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
,一个正则表达式规则。- 相当巧妙地,每个类型只声明一次规则(因为它们是静态字段),这节省了业务对象验证所需的内存量。
以下是如何在 Cinch 中使用这些,其中属性是简单类型,如 String
/Int32
等。
public class OrderViewModel : Cinch.EditableValidatingViewModelBase
{
private Int32 quantity;
//rules
private static SimpleRule quantityRule;
public OrderViewModel()
{
#region Create Validation Rules
quantity.AddRule(quantityRule);
#endregion
}
static OrderViewModel()
{
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 OrderViewModel : Cinch.EditableValidatingViewModelBase
{
#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 OrderViewModel()
{
//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<OrderViewModel>(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.EditableValidatingViewModelBase
对象继承时获得的 IsValid
方法。假设您有一个 UI ViewModel 对象,如下所示:
using System;
using Cinch;
using MVVM.DataAccess;
using System.ComponentModel;
using System.Collections.Generic;
namespace MVVM.ViewModels
{
/// <summary>
/// Respresents a UI Order ViewModel, 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 OrderViewModel : Cinch.EditableValidatingViewModelBase
{
#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 OrderViewModel()
{
#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<OrderViewModel>(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<OrderViewModel>(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<OrderViewModel>(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<OrderViewModel>(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<OrderViewModel>(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<OrderViewModel>(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
以及任何嵌套的 Cinch.DataWrapper<T>
对象的 IsValid
状态,得出一个组合的 IsValid
。这非常简单,因为它们也继承自 Cinch.EditableValidatingViewModelBase
,而 Cinch.EditableValidatingViewModelBase
又继承自 Cinch.ValidatingViewModelBase
,因此它们已经实现了 IDataErrorInfo
,所以处理起来并不难。
我知道这似乎需要额外的工作,但 ViewModel 能够设置单个字段的可编辑状态,并且视图通过绑定无缝地反映这一点所带来的额外好处是不可忽视的。
/// <summary>
/// Override hook which allows us to also put any child
/// EditableValidatingViewModelBase 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 将当前对象的状态存储在内部字典中。在 CancelEdit()
时,使用属性名作为键,将内部字典值恢复到当前对象的属性中,恢复存储的字典状态。
这是执行所有这些操作的 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 EditableValidatingViewModelBase :
ValidatingViewModelBase, 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.EditableValidatingViewModelBase
。搞定。
让我们看看如何将继承自 Cinch.EditableValidatingViewModelBase
的对象置于编辑模式。
从 ViewModel,我们可以简单地执行 this.BeginEdit()
,就是这么简单。但是,如果您有嵌套的 Cinch.DataWrapper<T>
对象,您 **还必须** 确保它们也处于正确的状态。您将在 UI ViewModel 类中执行此操作,其中我们只需重写从 Cinch.EditableValidatingViewModelBase
继承的 protected virtual void OnBeginEdit()
。
这里我们可能有一个 UI ViewModel 对象,如下所示:
using System;
using Cinch;
using MVVM.DataAccess;
using System.ComponentModel;
using System.Collections.Generic;
namespace MVVM.ViewModels
{
/// <summary>
/// Respresents a UI Order ViewModel, 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 OrderViewModel : Cinch.EditableValidatingViewModelBase
{
#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 OrderViewModel()
{
#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<OrderViewModel>(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<OrderViewModel>(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<OrderViewModel>(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<OrderViewModel>(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<OrderViewModel>(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<OrderViewModel>(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 EditableValidatingViewModelBase overrides
/// <summary>
/// Override hook which allows us to also put any child
/// EditableValidatingViewModelBase objects into the BeginEdit state
/// </summary>
protected override void OnBeginEdit()
{
base.OnBeginEdit();
//Now walk the list of properties in the OrderViewModel
//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
/// EditableValidatingViewModelBase objects into the EndEdit state
/// </summary>
protected override void OnEndEdit()
{
base.OnEndEdit();
//Now walk the list of properties in the OrderViewModel
//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
/// EditableValidatingViewModelBase objects into the CancelEdit state
/// </summary>
protected override void OnCancelEdit()
{
base.OnCancelEdit();
//Now walk the list of properties in the OrderViewModel
//and call CancelEdit() on all Cinch.DataWrapper<T>s.
//we can use the Cinch.DataWrapperHelper class for this
DataWrapperHelper.SetCancelEdit(cachedListOfDataWrappers);
}
#endregion
}
}
我们需要做的就是重写 Cinch.EditableValidatingViewModelBase
虚拟方法,如下所示:
#region EditableValidatingViewModelBase overrides
/// <summary>
/// Override hook which allows us to also put any child
/// EditableValidatingViewModelBase objects into the BeginEdit state
/// </summary>
protected override void OnBeginEdit()
{
base.OnBeginEdit();
//Now walk the list of properties in the OrderViewModel
//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
/// EditableValidatingViewModelBase objects into the EndEdit state
/// </summary>
protected override void OnEndEdit()
{
base.OnEndEdit();
//Now walk the list of properties in the OrderViewModel
//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
/// EditableValidatingViewModelBase objects into the CancelEdit state
/// </summary>
protected override void OnCancelEdit()
{
base.OnCancelEdit();
//Now walk the list of properties in the OrderViewModel
//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 ViewModel 类中做对了。如果您对此感到有些迷茫,请不要担心,帮助就在眼前。您可以通过此文章链接阅读所有关于使用 Cinch 创建 ViewModel 的内容:CinchIV.aspx#DevelopingVMs。
弱事件创建
在开始讨论如何创建弱事件之前,我认为这是一个可以开始小讨论的好地方。我想有很多读者/.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
现在,如果您曾经研究过弱事件管理器/接口实现,您就会意识到这是一项相当大的工作,而且您必须为每种事件类型都有一个新的 WeakEventManager
。对我来说,这听起来工作量太大了,所以我宁愿选择其他机制,例如一开始就有一个 WeakEvent
。更好的是,也许有一个弱侦听器,只有当事件源仍然存在且未被 GC 回收时,它才会对源事件做出反应。
引发一个 WeakEvent<T>
所以,废话不多说,让我向您展示一些在处理事件时(可能使其成为弱事件)在 Cinch 中可用的非常方便的小助手。当然,尽可能手动添加/删除事件的委托仍然是更好的选择,但有时您就是不知道对象生命周期的相关信息,因此最好选择弱事件策略。
首先,让我们使用来自才华横溢的 Daniel Grunwald 的非常出色的 WeakEvent<T>
,他在不久前发布了一篇关于弱事件的 精彩文章。Daniel 的 WeakEvent<T>
展示了如何以弱方式引发事件。
我不会无聊地给您看 WeakEvent<T>
的所有代码,但有一件事您应该熟悉,如果您还不熟悉的话,那就是 WeakReference
类。这是一个标准的 .NET 类,它引用一个对象,同时仍然允许该对象被垃圾回收。
几乎任何弱事件订阅/事件引发都会使用内部的 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>
的方法,但有时这不是您自己的代码,您也不负责代码中包含的事件。也许您正在使用第三方控件集。在这种情况下,您可能需要使用弱事件订阅。Cinch 提供了两种方法来实现这一点。
弱事件订阅
上面我们看到了如何使用 Daniel Grunwald 的 WeakEvent<T>
引发弱事件。那么,在我们想订阅现有事件的情况下呢?同样,这通常是通过使用 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 的很棒的代码,它不仅允许用户创建弱事件订阅,还允许用户指定一个自动退订回调委托。此外,使用此代码的一个小变种,可以使所有订阅的事件处理程序都成为弱引用。让我们快速看一下这两种操作的语法。
指定带取消订阅的弱事件订阅
我们只需这样做:
workspace.CloseWorkSpace +=
new EventHandler<EventArgs>(OnCloseWorkSpace).
MakeWeak(eh => workspace.CloseWorkSpace -= eh);
private void OnCloseWorkSpace(object sender, EventArgs e)
{
}
这一行创建了一个弱侦听器,带有一个自动取消订阅。很方便,对吧?
我提到您也可以使用此代码来创建一个弱事件,使得特定事件的所有订阅者都成为弱引用。您可以使用此代码执行此操作:
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 来管理整个项目。实际上我有好几个(事实上,我们有很多)。使用标准的 MVVM 模式的一个问题是 ViewModel 之间的跨通信。毕竟,构成应用程序的 ViewModels 可能是不相关的独立对象,它们彼此一无所知。但是,它们需要了解用户执行的某些操作。这里有一个具体的例子。
假设您有两个视图,一个显示客户,一个显示客户的订单。假设订单视图使用的是 OrdersViewModel
,客户视图使用的是 CustomersViewModel
,当客户的订单更新、删除或添加时,客户视图应该显示某种视觉提示,以提醒用户某个客户的订单详细信息已更改。
听起来很简单,对吧?但是,我们有两个独立的视图由两个独立的 ViewModel 运行,没有任何联系,但显然,OrdersViewModel
和 CustomersViewModel
之间需要某种联系,某种消息传递。
这正是 Mediator 模式的意义所在,它是一个简单轻量级的消息系统。我曾在一篇 我的博客 上谈论过这个问题,后来 Josh Smith / Marlon Grech(作为一个原子对)对其进行了极大的改进,他们提出了您将在 Cinch 中看到的 Mediator 实现。
那么中介者是如何工作的呢?
这张图可能有所帮助:
这个想法很简单,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.Instance.Register("AddCustomerMessage", new DummyDelegate((x) =>
{
AddCustomerCommand.Execute(null);
}));
注册整个对象,并使用 MediatorMessageSinkAttribute 属性
这是我最喜欢的方法,也是我心目中最简单的方法。您只需要将整个对象注册到 Mediator
,并用属性标记一些消息挂钩方法。
在 Cinch V1 中,我会自动为您注册继承自 ViewModelBase
的任何 ViewModel。现在对于 Cinch V2,我决定不这样做,因为您可能有许多 ViewModel 根本不需要 Mediator
,所以您必须自己手动注册到 Mediator,如下所示:
//Register all decorated methods to the Mediator
Mediator.Instance.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.Instance.NotifyCollegues()
方法,如下所示:
//Use the Mediator to send a Message to MainWindowViewModel to add a new
//Workspace item
Mediator.Instance.NotifyColleagues<Boolean>("AddCustomerMessage", true);
您也可以像这样异步使用 Mediator:
Mediator.Instance.NotifyColleaguesAsync<Boolean>("AddCustomerMessage", true);
模型基类的选择
在 Cinch V1 中,我开始时建议人们从 ViewModel 公开一个 CurrentXXXModel
并将其绑定。有几个人开始抱怨他们无法编辑模型。对我来说,UI 堆栈总是如下面所示:
DB -> LINQ to SQL/EF -> 服务器端模型 -> WCF -> UI 模型 -> UI ViewModel -> View
其中 UI 始终负责自己的模型。事实上,Cinch V1 演示应用程序显示的就是这种情况,使用了 LINQ to EF。在服务器和客户端之间没有共享模型对象的情况下,每个端都有自己的类型。因此,人们可以公开一个 Cinch 模型(其中模型是 Cinch 基于的模型)。
Cinch 提供了几个您可以使用的模型基类,它们如下:
ValidatingObject
:提供对DataWrapper
s 和INotifyPropertyChanged
实现的支持,并通过IDataErrorInfo
实现支持验证规则。EditableValidatingObject
:通过IEditableObject
实现提供可编辑对象支持。
如果这描述了您的情况,您可能想阅读旧的 Cinch V1 文章的这一部分:CinchIV.aspx#DevelopingModels。
重要提示:由于我实际上改变了我的想法以匹配大多数人的期望(如下),Cinch 代码生成器将输出的代码,这些代码期望继承自 Cinch ViewModel,所以请注意,如果您采用了从 ViewModel 公开 CurrentXXXModel
的方法,代码生成器将完全无法帮助您。
抱怨 抱怨 抱怨
即便如此,许多人表示他们的堆栈看起来更像这样:
DB -> LINQ to SQL/EF(共享) -> 服务器端模型 -> WCF -> LINQ to SQL/EF(共享) -> UI ViewModel -> View
因此,他们**无法**修改他们的模型以使其继承自 Cinch V1 模型类。这是合理的;事实上,我对此进行了 180 度的转变,**不再**提倡从 ViewModel 公开 CurrentXXXModel
,而是将内容从 View 传递到 ViewModel,再传递到 Model。因此,所有验证规则(IDataErrorInfo
)/ ViewMode
更改 / IEditableObject
操作都应该针对支持这些操作的 ViewModel 进行。我在下一节讨论这些选项。
事实上,有一篇完整的 Cinch V1 文章专门讨论了公开 Model/ViewModel 的整个争论,您可以在以下位置找到:CinchIV.aspx。
ViewModel 基类的选择
正如我刚才所说,Cinch 实际上允许您从 ViewModel 公开一个 CurrentXXXModel
,该模型支持 DataWrapper
s/验证规则(IDataErrorInfo
)/ ViewMode 更改 / IEditableObject
操作,通过使用上面提到的两个 Cinch 模型类。
但正如我上面所说,我**不再**推荐这种方法,并且认为模型应该保持不变,您应该在 ViewModel 中执行所有 DataWrapper
s/验证规则(IDataErrorInfo
)/ ViewMode 更改 / IEditableObject
操作。因此,Cinch 提供了几个您可以使用的 ViewModel 基类;它们如下:
ViewModelBase
:提供对DataWrapper
s 和INotifyPropertyChanged
实现的支持。ValidatingViewModelBase
:通过IDataErrorInfo
实现提供对验证规则的支持。EditableValidatingViewModelBase
:通过IEditableObject
实现提供可编辑对象支持。
和以前一样,请参阅专门讨论公开 Model/ViewModel 争论的 Cinch V1 文章,您可以在以下位置找到:CinchIV.aspx。
而本节将特别有趣:CinchIV.aspx#DevelopingVMs;您当然需要**忽略服务是如何检索的**,因为现在它们是由 meffedMVVM 提供的。
单元测试
尽管一些服务发生了变化,并且增加了几个服务,但 Cinch 的单元测试从 Cinch V1 到 Cinch V2 基本保持不变。因此,最初的 Cinch V1 单元测试文章仍然完全准确,您可以通过以下链接阅读更多内容:CinchV.aspx。
至于 Cinch V1 中未包含的附加 IStatusAware
服务,我在 Cinch V2 中提供了一个名为 TestViewAwareStatus
的测试替身,应该很容易弄清楚如何使用它。只需查看各种测试方法的名称,例如 SimulateViewIsLoadedEvent()
,它将模拟实际视图的 Loaded
事件。
代码生成器
Cinch V1 提供了一个代码生成器,有助于开发 Cinch ViewModel。它生成一个部分类的两个部分。您可以使用此链接找到有关 Cinch 代码生成器的更多详细信息:CinchCodeGen.aspx。
生成器生成的代码对 Cinch V1 和 Cinch V2 完全有效。
我认为代码生成器的文章很有趣,因为它使用 Compiler Services/CodeDOM 来确保生成的代码确实有效。它甚至赢得了两个奖项,所以我认为值得一看。
暂时就到这里
如果您喜欢这篇文章,并且觉得它对您有帮助,能否请您通过留下投票/评论来表示支持?
与以前一样,如果您有任何深入的 MEF 相关问题,您应该通过他的博客 C# Disciples 或通过 MefedMVVM CodePlex 站点 将其发送给 Marlon Grech。任何其他 Cinch V2 问题将在下一篇 Cinch V2 文章中得到解答。