MVVM # 第二集






4.96/5 (28投票s)
将扩展的 MVVM 模式用于实际的 LOB 应用程序:第二部分
本系列其他文章
第一部分
第二部分
第三部分
第四部分
引言
在本系列的上一篇文章中,我探讨了我在 WPF 应用程序中实现 MVVM 时遇到的一些问题,并提出了一些改进现有框架的建议。
在本文中,我将开始一个使用上次介绍的技术创建一个 MVVM 应用程序(尽管是一个小型的)的演练。
在第三篇文章中,我将为第二篇文章创建的骨架添加足够的实质内容,使其成为一个可运行的应用程序,尽管它功能不多。
在第四篇文章中,我将完成该应用程序,展示一个(小型的)但功能齐全的应用程序,演示一些可用的功能。
我们在这里只创建通用的先决条件,“框架”,如果你喜欢的话。下次,我们将继续实际项目。
只是因为这些东西必须有一个名字,我想我叫它 MVVM#。主要是因为 MVMVDCV 看起来太像罗马数字日期了 " src="https://codeproject.org.cn/script/Forums/Images/smiley_wink.gif" />
先决条件和注意事项
我使用 VS2010 和 C#,.NET 4.0 来构建。它应该可以翻译成 VB.NET。
我在这里不使用 TDD。尽管 MVVM 的优点之一是提高了应用程序的可测试性,但我之所以回避两者,是为了保持文章简短,并避免暴露我对 TDD 知识的不足!
我假设您熟悉 VS2010 和 C# - 因此没有“点击此处,下拉此处”的操作。但是,我确实会尽量避免做假设,因此即使是新手用户也应该能够跟上 - 并且可以下载已完成的应用程序的源代码。
Specification
我想让它在文章的限制范围内相当现实 - 所以这是规格:
- 我们希望用户能够看到客户列表。
- 他们需要能够按州过滤列表。
- 他们需要选择一个客户并编辑他们的详细信息。
- 他们需要能够保存他们的更改。
好了,这是我确定你们很多人都习惯的那种规格!显然,我们将不得不自己做出很多假设和设计决策 - 但这都很好;我们将保持敏捷,经常向用户展示我们的进展,并在必要时进行重新设计。
让我们开始吧!
创建一个新的 WPF C# 应用程序。我称我的为“CustomerMaintenance
”。我们不想要默认的 MainWindow
- 所以删除它。
现在,我们将创建所有需要的项目。因为我们处理的每个事物都应该彼此独立,所以我喜欢将每个事物放在自己的项目中 - 这样交叉污染就更难了,每个项目的“引用”可以轻松记录,如果需要,我可以更容易地在多个开发人员之间分配开发任务。所以,创建如下新的类库项目:
- 控制器
- 消息传递器
- 模型
- 服务
- ViewModels
我们还需要为我们的视图创建一个项目。这不应该是类库,而应该是 WPF 用户控件库。
您可以在创建它们时删除默认创建的类,或者直接重命名它们。
您的 VS 解决方案资源管理器应该如下所示。
解决方案资源管理器

基础类
我们将使用消息传递在我们的应用程序中进行通信。我们可以使用事件(只要我们非常小心地在类被处置时删除处理程序)。在此实现中,我使用的是一个派生自MVVM Foundation类的Messenger
类。Messenger
类的源代码是本文下载内容的一部分。它是一个实用类,在我所有的 MVVM 项目中都可以直接使用。
Messenger
类位于自己的项目(Messengers
)中,这样我就可以保持它的独立性。
某些Messenger
系统的实现存在一个真正的问题,可能导致意外行为,并且非常难以追踪。为了描述它,请想象以下场景:
ViewModels
VMA 和 VMB 都被实例化。VMA 发送消息给 VMB 订阅,但两者对彼此一无所知。当 VMB 处理消息时,它会执行某个函数(例如,写入数据库)。
VMA 发送消息,VMB 接收它,写入数据库,一切正常。现在用户关闭了 VMB(如果它在一个窗口中,也许他们关闭了窗口),所以系统会删除 VMB 的所有“强”引用。仍然有一个对它的弱引用 - 在Messenger
中 - 但这不会阻止它被垃圾回收,所以一切都很好。当然,因为 VMA 对 VMB 的情况一无所知,它会继续发送消息 - 毕竟,其他ViewModel
可能也会订阅该消息...
现在,如果您还记得垃圾回收 101,垃圾回收器并不一定在项目被所有强引用释放后立即将其从内存中移除 - 垃圾回收器会在它想运行时运行。所以如果你有充足的内存可用,特别是如果 VMB 很小,它仍然在那里。然后 VMA 发送消息。您可以看到这将走向何方,对吧?是的 - VMB 仍然接收消息,即使它没有任何引用!它仍然愉快地写入数据库!即使用户关闭了它!
解决此问题的办法是在 VMB 关闭之前取消订阅该消息。但是,等等,使用弱引用的一部分原因不就是我们不必担心忘记删除事件处理程序所带来的内存泄漏吗?(是的,确实如此!)但是现在我们仍然必须记住删除消息处理程序,否则我们将面临意外行为,而这种行为可能比内存泄漏更难追踪!
为了克服这个问题,我实现了一个Deregister
方法,它接受一个ViewModel
作为参数,并删除所有到它的消息订阅。然后我在我的基础ViewModel
类中的CloseViewModel
方法中调用这个方法。
我还向源代码添加了两个枚举:MessageHandledStatus
和NotificationResult
。
第一个,MessageHandledStatus
,用于允许消息处理程序(我们的 View Models)与 Messenger 系统进行通信。
默认值(NotHandled
)告诉系统ViewModel
尚未处理该消息(因此 Messenger 应该继续将其发送给任何其他注册为接收者的ViewModel
)。
如果ViewModel
将值设置为HandledContinue
,这告诉 Messenger 继续发送消息,但在完成后,它将知道有东西处理了消息。
HandledCompleted
值告诉 Messenger 不要将消息发送给任何进一步的接收者,因为它已经被处理了。
最后,NotHandledAbort
消息告诉 Messenger 尽管消息尚未处理,但它不应该将其发送给进一步的接收者。
NotificationResult
枚举用于将一些信息返回给发送消息的ViewModel
。例如,如果当前没有注册的处理程序来处理某个事件,这可以用于实例化一个新的ViewModel
来处理该事件。
我的信使类的版本还使用了一个名为Message
的类,该类随消息一起传递。该类看起来像这样...
Message.cs
namespace Messengers
{
public class Message
{
#region Public Properties
/// <summary>
/// Has the message been handled
/// </summary>
public MessageHandledStatus HandledStatus
{
get;
set;
}
/// <summary>
/// What type of message is this
/// </summary>
private MessageTypes messageType;
public MessageTypes MessageType
{
get
{
return messageType;
}
}
/// <summary>
/// The payload for the message
/// </summary>
public object Payload
{
get;
set;
}
#endregion
#region Constructor
public Message(MessageTypes messageType)
{
this.messageType = messageType;
}
#endregion
}
}
这个Message
对象与每条消息一起传递 - 因此每条消息处理程序都有机会查看或修改HandledStatus
并查看MessageType
。这例如允许单个消息处理程序处理多种不同的消息类型,并相应地设置HandledStatus
。
Message
对象还包含一个“Payload
”。这是一个您想随该消息一起传递的对象,因此当我们保存了一个Customer
后,例如,我们发送了一条消息,使用:
Messenger.NotifyColleagues(MessageTypes.MSG_CUSTOMER_SAVED, data);
其中传递的数据是CustomerEditViewData
,它包含所有刚刚更新的信息 - 所以如果某处的某个ViewModel
想要采取行动,它已经掌握了信息,可以说。
我们还将使用MVVMFoundation
项目中的另外两个类 - 即ObservableObject
和RelayCommand
。这两个类我都将在ViewModels
项目中的一个名为BaseClasses
的文件夹中创建,因为所有ViewModel
和ViewData
类都派生自ObservableObject
,并且ViewModels
是RelayCommand
的处理程序。
您还需要在ViewModels
项目中添加对PresentationCore
的引用。
为了完成这一部分,我们应该添加用于消息的枚举。所以向Messengers
项目添加一个新文件,名为Messages
...
MessageTypes.cs
namespace Messengers
{
/// <summary>
/// Use an enumeration for the messages to ensure consistency.
///
/// </summary>
public enum MessageTypes
{
MSG_CUSTOMER_SELECTED_FOR_EDIT,// Sent when a Customer is selected for editing
MSG_CUSTOMER_SAVED // Sent when a Customer is updated to the repository
};
}
这是我们应用程序将要处理的唯一两条消息 - 现在添加它们没问题。在更大的应用程序中,我们将根据功能规范添加新消息 - 这是一个确保所提供功能符合要求的好地方。
这,如果你愿意,就是我的 MVVM# 应用程序的基本“框架”。当然,您可以使用任何您喜欢的 Mediator 模式实现(我们的Messenger
类)。ObservableObject
和RelayCommand
类也可以用提供类似功能的其他版本替换。
ViewModel
我们场景中的其他配角现在也可以创建了。
我们正在使用一个 Controller 来管理应用程序 - 所以让我们创建它的接口。同样,这可以放在ViewModels
项目中的BaseClasses文件夹中。您可以看到基础控制器的接口只是指定它有一个Messenger
属性。
IController.cs
using Messengers;
namespace ViewModel
{
public interface IController
{
Messenger Messenger
{
get;
}
}
}
现在,我们还需要ViewData
和ViewModel
类的基类。在这里,一些争议 crept into my implementation。我们将声明一个IView
接口供我们的ViewModel
使用。
什么!!!我能听到惊呼声!我们的ViewModels
不应该知道任何关于我们的 Views!好吧,我生活在现实世界。我需要能够告诉 Views 激活自己并关闭自己。更准确地说,我需要能够告诉一个 View 它的ViewModel
正在关闭,或者它的ViewModel
正在激活 - 所以给 View 一个处理这些事件的选项。
您会看到,IView
接口就是这些 - 用于允许 Views 挂钩到ViewModel
引发的事件的几个方法的定义。它还指定了DataContext
属性 - 就像每个 View,作为UserControl
的后代,都会有的属性。您将在ViewModel
构造函数中看到这个属性。
IView.cs
namespace ViewModel
{
public interface IView
{
void ViewModelClosingHandler(bool? dialogResult);
void ViewModelActivatingHandler();
object DataContext{get;set;}
}
}
BaseViewdata
是我们能想要的尽可能简单的类...
BaseViewData.cs
namespace ViewModels
{
/// <summary>
/// The base class from which all View Data objects inherit.
/// Just an Observable Object right now -
/// but a separate abstract class in case we want to add
/// to it while not modifying ObservableObject itself.
/// </summary>
public abstract class BaseViewData : ObservableObject
{
}
}
BaseViewModel
源代码还声明了使用IView
接口中定义的两个方法。在BaseViewModel
中,我们还维护一个子BaseViewModels
的集合。保留此列表允许每个ViewModel
确保其每个子项在“父”被“关闭”时取消订阅所有消息(并释放任何其他资源)。顺便说一下,我使用“daddy”而不是“Parent
”来命名变量,以避免与“Parent”名称的其他用法产生任何可能的混淆。有女权主义倾向的人,可以随意将其重命名为“mummy”。
BaseViewModel
包含一个BaseViewData
属性。BaseViewData
是绑定到View
的业务数据,而ViewModel
中可能被View
绑定的任何其他属性都提供功能,而不仅仅是数据。
在构造函数中,ViewModel
被传递了一个IController
和IView
的引用。这是合乎逻辑的 - 每个ViewModel
都需要一个Controller
来服务它,以及一个View
来提供 GUI。您可以看到,构造函数是IView
中定义的方法与BaseViewModel
中定义的事件处理程序连接的地方 - 您可以注意到ViewModel
不保留对 View 的任何其他引用。
最后提供了两个方法:CloseViewModel
和ActivateViewModel
。
BaseViewModel.cs
using System.Collections.Generic;
namespace ViewModel
{
/// <summary>
/// When the VM is closed, the associated V needs to close too
/// </summary>
/// <param name="sender"></param>
public delegate void ViewModelClosingEventHandler(bool? dialogResult);
/// <summary>
/// When a pre-existing VM is activated the View needs to activate itself
/// </summary>
public delegate void ViewModelActivatingEventHandler();
/// <summary>
/// A base class for all view models
/// </summary>
public abstract class BaseViewModel : ObservableObject
{
public event ViewModelClosingEventHandler ViewModelClosing;
public event ViewModelActivatingEventHandler ViewModelActivating;
/// <summary>
/// Keep a list of any children ViewModels so we can safely
/// remove them when this ViewModel gets closed
/// </summary>
private List<BaseViewModel> childViewModels = new List<BaseViewModel>();
public List<BaseViewModel> ChildViewModels
{
get { return childViewModels; }
}
#region Bindable Properties
#region ViewData
private BaseViewData viewData;
public BaseViewData ViewData
{
get
{
return viewData;
}
set
{
if (value != viewData)
{
viewData = value;
base.RaisePropertyChanged("ViewData");
}
}
}
#endregion
#endregion
#region Controller
/// <summary>
/// If the ViewModel wants to do anything, it needs a controller
/// </summary>
protected IController Controller
{
get;
set;
}
#endregion
#region Constructor
/// <summary>
/// Parameterless Constructor required for support of DesignTime
/// versions of View Models
/// </summary>
public BaseViewModel()
{
}
/// <summary>
/// A view model needs a controller reference
/// </summary>
/// <param name="controller"></param>
public BaseViewModel(IController controller)
{
Controller = controller;
}
/// <summary>
/// Create the View Model with a Controller and a FrameworkElement (View) injected.
/// Note that we do not keep a reference to the View -
/// just set its data context and
/// subscribe it to our Activating and Closing events...
/// Of course, this means there are references -
/// that must be removed when the view closes,
/// which is handled in the BaseView
/// </summary>
/// <param name="controller"></param>
/// <param name="view"></param>
//public BaseViewModel(IController controller, FrameworkElement view)
public BaseViewModel(IController controller, IView view)
: this(controller)
{
if (view != null)
{
view.DataContext = this;
ViewModelClosing += view.ViewModelClosingHandler;
ViewModelActivating += view.ViewModelActivatingHandler;
}
}
#endregion
#region public methods
/// <summary>
/// De-Register the VM from the Messenger to avoid non-garbage
/// collected VMs receiving messages
/// Tell the View (via the ViewModelClosing event) that we are closing.
/// </summary>
public void CloseViewModel(bool? dialogResult)
{
Controller.Messenger.DeRegister(this);
if (ViewModelClosing != null)
{
ViewModelClosing(dialogResult);
}
foreach (var childViewModel in childViewModels)
{
childViewModel.CloseViewModel(dialogResult);
}
}
public void ActivateViewModel()
{
if (ViewModelActivating != null)
{
ViewModelActivating();
}
}
#endregion
}
}
控制器 (Controller)
因为我们的 Controllers 将有一些共同的功能,我们将使用一个BaseController
类,我们的 controller(s) 将从它继承。在这种情况下,实际上唯一的共同功能是到我们Messenger
类的单例实例的引用,如IController
接口中所定义。
所以我们只需要在Controllers
项目中创建一个名为BaseController
的新类。像往常一样,我将它创建一个名为Base Classes的文件夹中。
BaseController.cs
using Messengers;
using ViewModel;
namespace Controllers
{
/// <summary>
/// The base controller class.
/// </summary>
public abstract class BaseController : IController
{
/// <summary>
/// Retain a reference to the single instance of the
/// Messenger class for convenience
/// as it means we can use Controller.Messenger.blah rather than
/// Controller.Messenger.Instance.blah
/// In a large system this also allows us to use multiple Messengers
/// (e.g. for different parts of a system
/// that have no need to communicate between them)
/// by making a single change here to return a different Messenger
/// </summary>
public Messenger Messenger
{
get
{
return Messenger.Instance;
}
}
}
}
视图
在我们的 Views 项目中,我们需要创建两个项。首先,我们将创建一个 Window。当我们想在窗口中显示 View 时,我们将使用这个窗口 - 如下面所示。因为我们正在创建基类(嗯,Window
实际上不是基类,但它有点符合这个想法),所以我创建了一个名为Base Classes的文件夹,然后在其中创建一个名为ViewWindow
的新Window
。
因为当我们显示我们的 Views 时,我们需要将它们放在某个表面上,所以我们将一个DockPanel
添加到窗口中。这是所有 Views 在窗口中显示时将放置的表面。它必须命名为WindowDockPanel
。请注意,您可以根据喜好“美化”您的窗口 - 只要它有一个WindowDockPanel
即可。(您也可以更改此功能,通过更改将 Views 放置到窗口上的代码 - 所有这些都在 Base 类中定义,因此实现可以根据您的偏好进行更改。)
接下来,我们创建BaseView
类。
BaseView.cs
using System;
using System.Windows;
using System.Windows.Controls;
using ViewModels;
namespace Views
{
/// <summary>
/// A delegate to allow the window closed event to be handled (if required)
/// </summary>
/// <param name="o"></param>
/// <param name="e"></param>
public delegate
void OnWindowClose(
Object sender, EventArgs e);
/// <summary>
/// This is the basis of all views.
/// It cannot be Abstract because of design time issues when
/// it tries to instantiate this class.
/// Note that this 'view' doesn't have any XAML
/// (because you can't inherit XAML)
/// </summary>
public partial class BaseView : UserControl, IDisposable, IView
{
private ViewWindow viewWindow;
// If shown on a window, the window in question
private OnWindowClose onWindowClosed = null;
#region Closing
/// <summary>
/// The view is closing, so clean up references
/// </summary>
public void ViewClosed()
{
// In order to handle the case where the
// user closes the window
// (rather than us controlling the close via a ViewModel)
// we need to check that the DataContext is not null
// (which would mean this ViewClosed has already been done)
if (DataContext != null)
{
((BaseViewModel)DataContext).ViewModelClosing -=
ViewModelClosingHandler;
((BaseViewModel)DataContext).ViewModelActivating -=
ViewModelActivatingHandler;
this.DataContext = null; // Make sure we don't
// have a reference to the VM any more.
}
}
/// <summary>
/// Handle the Window Closed event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ViewsWindow_Closed(object sender, EventArgs e)
{
if (onWindowClosed != null)
{
onWindowClosed(sender, e);
}
((BaseViewModel)DataContext).CloseViewModel(false);
}
#endregion
#region IView implementations
/// <summary>
/// Tell the View to close itself. Handle the case
/// where we're in a window and the window needs closing.
/// </summary>
/// <param name="dialogResult"></param>
public void ViewModelClosingHandler(bool? dialogResult)
{
if (viewWindow == null)
{
System.Windows.Controls.Panel panel =
this.Parent as System.Windows.Controls.Panel;
if (panel != null)
{
panel.Children.Remove(this);
}
}
else
{
viewWindow.Closed -= ViewsWindow_Closed;
if (viewWindow.IsDialogWindow)
{
// If the window is a Dialog and is not
// active it must be in the process of
// being closed
if (viewWindow.IsActive)
{
viewWindow.DialogResult =
dialogResult;
}
}
else
{
viewWindow.Close();
}
viewWindow = null;
}
// Process the ViewClosed method to cater for if this has
// been instigated by the user closing a window,
// rather than by
// the close being instigated by a ViewModel
ViewClosed();
}
public void ViewModelActivatingHandler()
{
if (viewWindow != null)
{
viewWindow.Activate();
}
}
#endregion
#region Constructor
public BaseView()
{
}
#endregion
#region Window
/// <summary>
/// The Window on which the View is displayed
/// (if it is displayed on a Window)
/// The Window will be created by the View on demand
/// (if required) or may be
/// supplied by the application.
/// </summary>
private ViewWindow ViewWindow
{
get
{
if (viewWindow == null)
{
viewWindow = new ViewWindow();
viewWindow.Closed += ViewsWindow_Closed;
}
return viewWindow;
}
}
#endregion
#region Showing methods
/// <summary>
/// Show this control in a window, sized to fit, with this title
/// </summary>
/// <param name="windowTitle"></param>
public void ShowInWindow(bool modal, string windowTitle)
{
ShowInWindow(modal, windowTitle, 0, 0, Dock.Top, null);
}
/// <summary>
/// Show this control in an existing window, by default docked top.
/// </summary>
/// <param name="window"></param>
public void ShowInWindow(bool modal, ViewWindow window)
{
ShowInWindow(modal, window, window.Title,
window.Width, window.Height,
Dock.Top, null);
}
/// <summary>
/// Maximum Flexibility of Window Definition version of Show In Window
/// </summary>
/// <param name="window">The Window in which to show this View</param>
/// <param name="windowTitle"> A Title for the Window</param>
/// <param name="windowWidth">The Width of the Window</param>
/// <param name="windowHeight">The Height of the Window </param>
/// <param name="dock">How should the View be Docked </param>
/// <param name="onWindowClosed">Event handler for when the window
/// is closed </param>
public void ShowInWindow(
bool modal, ViewWindow window,
string windowTitle, double windowWidth,
double windowHeight,
Dock dock, OnWindowClose onWindowClose)
{
this.onWindowClosed = onWindowClose;
viewWindow = window;
viewWindow.Title = windowTitle;
DockPanel.SetDock(this, dock);
// The viewWindow must have a dockPanel
// called WindowDockPanel.
// If you want to change this to use some
// other container on the window, then
// the below code should be the only place
// it needs to be changed.
viewWindow.WindowDockPanel.Children.Add(this);
if (windowWidth == 0 && windowHeight == 0)
{
viewWindow.SizeToContent =
SizeToContent.WidthAndHeight;
}
else
{
viewWindow.SizeToContent = SizeToContent.Manual;
viewWindow.Width = windowWidth;
viewWindow.Height = windowHeight;
}
if (modal)
{
viewWindow.ShowDialog();
}
else
{
viewWindow.Show();
}
}
/// <summary>
/// Show the View in a New Window
/// </summary>
/// <param name="windowTitle">Give the Window a Title</param>
/// <param name="windowWidth">Set the Window's Width</param>
/// <param name="windowHeight">Set the Window's Height</param>
/// <param name="dock">How to Dock the View in the Window</param>
/// <param name="onWindowClosed">Event handler for
/// when the Window closes</param>
public void ShowInWindow(
bool modal, string windowTitle,
double windowWidth, double windowHeight,
Dock dock, OnWindowClose onWindowClose)
{
ShowInWindow(modal, ViewWindow, windowTitle,
windowWidth, windowHeight, dock, onWindowClose);
}
#endregion
#region IDisposable Members
void IDisposable.Dispose()
{
// Remove any events from our window to prevent any
// memory leakage.
if (viewWindow != null)
{
viewWindow.Closed -= this.ViewsWindow_Closed;
}
}
#endregion
}
}
您需要添加对ViewModel
项目的引用才能编译。
这是一个相当基本的BaseView
- 有几种不同的方法可以用来显示我们的 View,但列表绝不完整。我在这版本中尝试提供了基本要求。它肯定可以扩展。您还将看到我们的事件处理程序的实现,用于ViewModel
关闭和激活时。
第二部分结束
至此,我们已经完成了项目,达到了需要开始创建应用程序特定代码的地步。换句话说,我们已经创建了我们的框架 - 但我真的不想使用这个词,我不把它看作一个框架,而只是一堆类,组合在一起以允许我开发一个 WPF MVVM# 应用程序。
应用程序应该可以构建 - 如果您是手动输入而不是下载,请检查您的命名空间,因为 VS 倾向于添加文件夹名称到命名空间,只是为了惹恼我。
下次,我们将开始正式开发应用程序 - 并最终得到可以运行的东西。