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

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

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.31/5 (13投票s)

2009年12月27日

CPOL

7分钟阅读

viewsIcon

41549

downloadIcon

573

在本系列文章中,我将讨论为什么我认为 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 核心库。

© . All rights reserved.