多用途模型-视图(MMV)对象建模模式与 WPF 和 WCF:MVVM 是反基督者吗?






3.31/5 (13投票s)
在本系列文章中,我将讨论为什么我认为 MVVM 是面向对象编程的巨大弊端,并演示开发人员编写应用程序的不同方式。
引言
在当今 WPF 和 WCF 客户端-服务器应用程序的世界中,MVVM 在多层应用程序开发人员中越来越受欢迎。在本系列文章中,我将讨论为什么我认为 MVVM 是面向对象编程的巨大弊端,并演示开发人员编写应用程序的不同方式。
模型-视图-视图模型
(注意:如果您已经熟悉 MVVM 模式,请跳到多功能模型-视图部分)。
要理解为什么 MVVM 不是最佳选择,我们必须首先了解 MVVM 是什么以及它是如何工作的。有关 MVVM 的更详细描述,请阅读有关 WPF 的模型-视图-视图模型设计模式。
MVVM 主要由一组定义数据、一组定义行为以及一组定义外观的类组成。
让我们以一个简单的通讯录应用程序为例
假设应用程序将使用以下数据库表作为持久化存储
在一个典型的 WPF/WCF 应用程序中,您可能会有一个项目包含您的数据对象模型,另一个项目包含您的数据传输对象模型以及数据对象和 DTO 对象之间的转换器,一个项目包含您的视图模型以及 DTO 对象和 VM 对象之间的转换器,以及另一个项目包含您的视图。实现可能会因项目和开发人员而略有不同,但大多数情况下,一个典型的通讯录应用程序可能看起来像这样
请注意,我们有三个类表示联系人表中存储的数据(`Contact`、`ContactDTO` 和 `ContactViewModel`)。所有这些类都或多或少地暴露相同数量的属性,不同之处在于 `ContactDO` 类具有与数据库相关的逻辑(如果您使用 NHibernate,很可能是一个映射),`ContactDTO` 是一个(很可能是 SOAP)可序列化对象,而 `ContactVM` 包含 `ICommand` 和其他与视图相关的属性。
当我看到这个时,我立即想到我大学里的编程 201 课程:编程基础,当时我们讨论了面向对象编程的四个基本原则(对于那些不记得的人,它们是:封装、抽象、继承和多态)。我不知道你怎么想,但我肯定没有看到 MVVM 模式中应用了这些原则。我们有三个类做着几乎相同的事情(封装没了),它们之间没有任何联系(抽象运气不好),它们不共享彼此的属性(哎呀——继承太糟糕了),它们不能在不同的上下文中互换(多态)。所以,你可能会问,这有什么大不了的?如果你曾经在一个 MVVM 应用程序中工作过,我确信你已经注意到(就像我一样),向现有对象模型添加新的数据(属性)或功能(方法、类等)是一件非常痛苦的事情,耗时,麻烦,最重要的是,很有可能忘记一些东西并产生错误代码。想想看:如果你想向你的数据库表添加一个列,你至少需要更改 5 个类(有时更多,取决于项目)。忘记某些东西或搞砸某些东西的可能性比你只需要担心一个类高出 500%。更不用说完成更改所需的时间是 5 倍(不包括调试由于你忘记更改而导致的错误所花费的时间)。
总而言之,以下是 MVVM 的优缺点列表
优点
- 逻辑(视图模型)和显示(视图)之间的硬性分离。
- 易于单元测试 UI 逻辑。
- 利用 WPF 技术,例如绑定和命令。
缺点
- 不符合面向对象编程标准。
- 对于简单的 UI 操作来说过于复杂。
- 对于大型应用程序,需要生成大量元数据。
- 需要代码重复。
- 难以维护。
多功能模型-视图方法
所以我们知道 MVVM 与我们面向对象开发人员的需求恰恰相反,它难以维护,并且需要代码重复。但问题是:**什么是**一个可行的面向对象解决方案来解决这个问题?答案是 MMV:一种利用封装、抽象、继承和多态来将数据从数据库一直传输到视图的模式。
让我们再次以通讯录为例。给定相同的联系人表,让我们绘制一个新解决方案
哇,所有的项目都去哪了?现在,我们有 6 个项目而不是 7 个,但只有一个类表示联系人表。我们仍然有一个公共和私有 Web 服务器,我们拥有与 MVVM 模式中完全相同的 shell 应用程序和完全相同的视图,但我们现在有一个项目用于整个数据模型(加上一个稍后我们将介绍的*核心*项目)。所以,让我们仔细看看 `Contact` 类
[Table(Name="Contact_tbl")]
public class Contact : MultiuseObject<Contact>
{
public virtual int ContactId { get; set; }
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual string Email { get; set; }
public void SendEMail()
{
Process.Start(string.Format("mailto-{0}", Email));
}
}
这是一个简单的类,有四个属性和一个用于执行特定联系人操作的方法(在这种情况下,从对象模型的角度来看,我们希望能够向数据库中的联系人发送电子邮件)。仅仅看一眼,有几件事就会跳出来
- 首先,我们看到类的 `TableAttribute`。暂时忽略它,因为它特定于从数据库中检索数据。根据您最喜欢的持久化库类型,这可能不是必需的(例如,如果您使用 NHibernate,您将有一个映射类或一个 XML 文件)。
- 其次,我们注意到所有相关属性都被声明为 `
virtual
`。这与我们的第三点相关。 - 第三,这个类继承自一个通用的 `MultiuseObject` 类(所有魔法都在这里发生)。
请注意这个类(它封装了您需要的一切)是多么干净和简单易读。如果您想从数据库中添加或删除一列,您所要做的就是向这个类添加一个相应的属性,瞧,您就完成了!`MultiuseObject` 类负责从数据库中获取和保存对象,它负责将对象和对象集合从私有服务器发送到公共服务器,再从公共服务器发送到客户端,它甚至实现了 `INotifyPropertyChanged` 并为您创建了 `ICommand`。它是如何做到这一切的?那么,让我们看看我们的示例 `MultiuseObject
public abstract class MultiuseObject<T> : INotifyPropertyChanged
{
private const string cExtendedTypesAssemblyName = "MMV.ExtendedTypes";
private const string cExtendedTypesModuleName = "MMV.ExtendedTypes";
private const string cExtendedTypesNamePostfix= "<>Extended";
private const string cExtendedTypesCommandPostfix = "Command";
// every time the CLR loads a type derived from MultiuseObject,
// we'll create a new type that adds needed features for WPF
// (such as NotifyPropertyChanged and Commands)
static MultiuseObject()
{
CreateOverridenType(typeof(T));
// this is to ensure that the newly created assembly
// gets properly loaded by WCF's DataContractSerializer on deserialization.
AppDomain.CurrentDomain.AssemblyResolve +=
(sender, args) => { return(GetExtendedTypesAssembly()); };
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChange(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
/// <summary>
/// provides functionality to derived objects
/// to get all data related to the type from the database.
/// </summary>
public static List<T> GetAllFromDB()
{
List<T> returning = new List<T>();
// note: here you can use any persistance library
// to dynamically populate a list of all objects
using (SqlConnection connection = new SqlConnection(
ConfigurationManager.ConnectionStrings["MMVSample"].ConnectionString))
{
connection.Open();
using (SqlCommand command = new SqlCommand(
string.Format("select * from {0}",
((TableAttribute)typeof(T).GetCustomAttributes(
typeof(TableAttribute), true)[0]).Name), connection))
{
SqlDataReader reader = command.ExecuteReader();
while (reader.Read())
{
// the trick here is to return
// an instance of the dynamically derived type.
T c = (T)Activator.CreateInstance(CreateOverridenType(typeof(T)));
for (int i = 0; i < reader.FieldCount; i++)
{
object value = reader.GetValue(i);
if (!(value is DBNull))
typeof(T).GetProperty(reader.GetName(i)).SetValue(c, value, null);
}
returning.Add(c);
}
}
}
return (returning);
}
/// <summary>
/// provides functionality to derived objects
/// to call GetAllObjectsFromDB from a client app with
/// no access to the database. (for example, the public server)
/// </summary>
public static List<T> GetAllFromPrivateServer()
{
ServiceClient<IMultiuseObjectPrivateServiceContract> client =
new ServiceClient<IMultiuseObjectPrivateServiceContract>();
return (client.ContractChannel.GetAll(typeof(T)).Cast<T>().ToList());
}
/// <summary>
/// provides functionality to derived objects
/// to call GetAllObjectsFromDB from a client app with
/// no access to the database and no access
/// to the private server. (for example, the WPF client app)
/// </summary>
public static List<T> GetAllFromPublicServer()
{
ServiceClient<IMultiuseObjectPublicServiceContract> client =
new ServiceClient<IMultiuseObjectPublicServiceContract>();
return (client.ContractChannel.GetAll(typeof(T)).Cast<T>().ToList());
}
/// <summary>
/// Looks for or creates a new AssemblyBuilder
/// and a corresponding module to store our dynamically derived types.
/// </summary>
private static AssemblyBuilder GetExtendedTypesAssembly()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(
a => a.FullName.Contains(cExtendedTypesAssemblyName));
if (assemblies.Count() > 0)
return ((AssemblyBuilder)assemblies.First());
else
{
AssemblyBuilder assemblyBuilder =
AppDomain.CurrentDomain.DefineDynamicAssembly(
new AssemblyName() { Name = cExtendedTypesAssemblyName },
AssemblyBuilderAccess.RunAndSave);
assemblyBuilder.DefineDynamicModule(cExtendedTypesModuleName, true);
return (assemblyBuilder);
}
}
/// <summary>
/// This is the key method for encapsulating WPF functionality
/// without redundancy. This method overrides virtual properties to call
/// NotifyPropertyChanged, it creates ICommands for public methods,
/// and it shadows the GetType method for serialization compatibility.
/// </summary>
private static Type CreateOverridenType(Type parentType)
{
string childTypeName = parentType.Namespace + "." +
parentType.Name + cExtendedTypesNamePostfix;
AssemblyBuilder assemblyBuilder = GetExtendedTypesAssembly();
ModuleBuilder moduleBuilder =
assemblyBuilder.GetDynamicModule(cExtendedTypesModuleName);
Type childType = moduleBuilder.GetType(childTypeName);
if (childType == null)
{
TypeBuilder typeBuilder = moduleBuilder.DefineType(childTypeName,
parentType.Attributes, parentType);
// shadow GetType
// some serializers (DataContractSerializer for example) use
// GetType to validate if the deserialized type is the same as
// the alleged return type. If they are not equal they thrown an exception.
// to bypass this we override GetType to return the value of the base type.
MethodInfo method = typeof(object).GetMethod("GetType",
BindingFlags.Public | BindingFlags.Instance ,
null, new Type[] { }, null);
MethodBuilder methodBuilder = typeBuilder.DefineMethod(method.Name,
method.Attributes, typeof(Type),
method.GetParameters().Select(pi => pi.ParameterType).ToArray());
ILGenerator il = methodBuilder.GetILGenerator();
LocalBuilder locAi = il.DeclareLocal(typeof(ArgIterator));
il.Emit(OpCodes.Ldtoken, parentType);
il.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle",
BindingFlags.Public | BindingFlags.Static), null);
il.Emit(OpCodes.Stloc_0);
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ret);
// we create an ICommand property and a backing DelegateCommand field
// for each public method (we exclude
// backing methods for properties and events).
foreach (MethodInfo mi in parentType.GetMethods(BindingFlags.Instance |
BindingFlags.Public).Where(methodInfo =>
!methodInfo.Name.StartsWith("get_") &&
!methodInfo.Name.StartsWith("set_") &&
!methodInfo.Name.StartsWith("add_") &&
!methodInfo.Name.StartsWith("remove_")))
{
FieldBuilder commandField = typeBuilder.DefineField("_" +
mi.Name + cExtendedTypesCommandPostfix,
typeof(DelegateCommand), FieldAttributes.Private);
MethodBuilder commandGetMethod = typeBuilder.DefineMethod("get_" +
mi.Name + cExtendedTypesCommandPostfix, MethodAttributes.Public |
MethodAttributes.SpecialName | MethodAttributes.HideBySig,
typeof(ICommand), Type.EmptyTypes);
ILGenerator commandGetMethodIL = commandGetMethod.GetILGenerator();
var commandNullLabel = commandGetMethodIL.DefineLabel();
var defaultLabel = commandGetMethodIL.DefineLabel();
commandGetMethodIL.Emit(OpCodes.Nop);
commandGetMethodIL.Emit(OpCodes.Ldarg_0);
commandGetMethodIL.Emit(OpCodes.Ldfld, commandField);
commandGetMethodIL.Emit(OpCodes.Ldnull);
commandGetMethodIL.Emit(OpCodes.Ceq);
commandGetMethodIL.Emit(OpCodes.Brfalse, commandNullLabel);
commandGetMethodIL.Emit(OpCodes.Ldarg_0);
commandGetMethodIL.Emit(OpCodes.Ldarg_0);
commandGetMethodIL.Emit(OpCodes.Ldftn, mi);
commandGetMethodIL.Emit(OpCodes.Newobj,
typeof(Action).GetConstructor(
new Type[] { typeof(object), typeof(IntPtr) }));
commandGetMethodIL.Emit(OpCodes.Newobj,
typeof(DelegateCommand).GetConstructor(new Type[] { typeof(Action) }));
commandGetMethodIL.Emit(OpCodes.Stfld, commandField);
commandGetMethodIL.MarkLabel(commandNullLabel);
commandGetMethodIL.Emit(OpCodes.Ldarg_0);
commandGetMethodIL.Emit(OpCodes.Ldfld, commandField);
commandGetMethodIL.Emit(OpCodes.Ret);
PropertyBuilder commandProperty = typeBuilder.DefineProperty(mi.Name +
cExtendedTypesCommandPostfix, PropertyAttributes.HasDefault,
typeof(ICommand), null);
commandProperty.SetGetMethod(commandGetMethod);
}
// we override the virtual properties to call
// OnPropertyChanged after calling the base class implementation.
foreach (PropertyInfo property in parentType.GetProperties())
{
method = parentType.GetMethod("set_" + property.Name);
methodBuilder = typeBuilder.DefineMethod(method.Name,
method.Attributes, method.ReturnType,
method.GetParameters().Select(pi => pi.ParameterType).ToArray());
il = methodBuilder.GetILGenerator();
locAi = il.DeclareLocal(typeof(ArgIterator));
il.Emit(OpCodes.Nop);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.EmitCall(OpCodes.Call, method, null);
il.Emit(OpCodes.Nop);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldstr, property.Name);
il.EmitCall(OpCodes.Call, parentType.GetMethod(
"OnPropertyChange",
BindingFlags.NonPublic | BindingFlags.Instance), null);
il.Emit(OpCodes.Nop);
il.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(methodBuilder, method);
}
childType = typeBuilder.CreateType();
}
return (childType);
}
}
要使用多功能对象,我们只需从适当的应用程序级别调用其方法之一。例如,要绑定联系人应用程序的主视图,我们调用 `Contact` 类,如下所示
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
MainView mv = new MainView();
mv.DataContext = Contact.GetAllFromPublicServer();
mv.Show();
}
}
公共服务器将依次执行以下操作
public class AddressBookDataService : IMultiuseObjectPublicServiceContract
{
public List<object> GetAll(Type returnObjectType)
{
return ((IEnumerable)returnObjectType.GetMethod(
"GetAllFromPrivateServer", BindingFlags.Static |
BindingFlags.Public |
BindingFlags.FlattenHierarchy).Invoke(null, null)).Cast<object>().ToList();
}
}
私有服务器将像这样调用 `GetAllFromDB` 方法
public class AddressBookDataService : IMultiuseObjectPrivateServiceContract
{
public List<object> GetAll(Type returnObjectType)
{
return ((IEnumerable)returnObjectType.GetMethod("GetAllFromDB",
BindingFlags.Static BindingFlags.Public
BindingFlags.FlattenHierarchy).Invoke(null, null)).Cast<object>().ToList();
}
}
如果我们想为我们的多功能对象添加更多功能,我们只需在 `MultiuseObject` 类中实现所需的行为,并相应地修改 `IMultiuseObjectServiceContract`
[ServiceContract]
public interface IMultiuseObjectServiceContract
{
[OperationContract]
List<object> GetAll(Type returnObjectType);
}
如您所见,我们可以使用您通常在 MVVM 应用程序中使用的相同 WPF 视图,但其背后有一个更清晰、更美观的对象模型。
总结一下,MMV 的优缺点如下
优点
- 逻辑(视图模型)和显示(视图)之间的硬性分离。
- 易于单元测试 UI 逻辑 - 利用 WPF 技术,例如绑定和命令。
- 符合面向对象编程标准。
- 扩展所需的代码量最少。
- 易于维护。
缺点
- 需要一个复杂的核心库。
- 要求应用程序堆栈全部是基于 Microsoft 的产品(显然与 Java 或其他服务器端技术不兼容)。
查看我的博客 http://andresusandi.blogspot.com。在我的下一篇文章中,我将演示如何创建一个全面的 MultiuseObject 核心库。