Patrones de diseño

Problemas y soluciones con Model-View-ViewModel

Robert McCarter

Descargar el código de muestra

Windows Presentation Foundation (WPF) y Silverlight ofrecen API enriquecidas para crear aplicaciones modernas, sin embargo, comprender y aplicar todas las características de WPF con armonía entre sí para crear aplicaciones bien diseñadas y de fácil mantenimiento puede ser una tarea difícil. ¿Por dónde se empieza? Y ¿cuál es la manera correcta de crear su aplicación?

El patrón de diseño Model-View-ViewModel (MVVM) describe un enfoque popular para crear aplicaciones de WPF y Silverlight. Es una herramienta eficaz para crear aplicaciones y un lenguaje común para analizar el diseño de la aplicación con los desarrolladores. A pesar de que MVVM es un patrón muy útil, aún es relativamente nuevo y no se comprende bien.

¿Cuándo es aplicable y cuándo es innecesario el patrón de diseño MVVM? ¿Cómo debe estructurarse la aplicación? ¿Cuánto trabajo implica escribir y mantener la capa de ViewModel y qué alternativas existen para reducir la cantidad de código en la capa de ViewModel? ¿Cómo se administran de modo elegante las propiedades relacionadas dentro de Model? ¿Cómo debe exponer las colecciones dentro de Model para View? ¿Dónde deben crear instancias y enlazarse los objetos de ViewModel a los objetos de Model?

En este artículo, explicaré cómo funciona ViewModel, y comentaré algunos beneficios y problemas al implementar un ViewModel en su código. También mostraré algunos ejemplos concretos del uso de ViewModel como administrador de documentos para exponer los objetos de Model en la capa View.

Model, ViewModel y View

Todas las aplicaciones de WPF y Silverlight en las que he trabajado hasta ahora tenían el mismo diseño de componente de alto nivel. Model era la característica principal de la aplicación, y se destinó mucho esfuerzo a diseñarla en función de las prácticas recomendadas de diseño y análisis orientados a objetos (OOAD).

Para mí, Model es el núcleo de la aplicación, que representa el activo empresarial esencial y más importante porque captura todas las entidades empresariales complejas y sus relaciones y su funcionalidad.

Model se basa en ViewModel. Los dos objetivos principales de ViewModel son: hacer que Model sea fácil de adoptar por WPF/XAML View, y separar y encapsular Model desde View. Estos son objetivos excelentes, aunque a veces no se cumplen por motivos pragmáticos.

Usted crea ViewModel sabiendo cómo interactuará el usuario con la aplicación a un alto nivel. Sin embargo, es una parte importante del patrón de diseño MVVM que ViewModel no tenga ningún conocimiento acerca de View. Esto les permite a los diseñadores de interacción y artistas gráficos crear interfaces de usuario bonitas y funcionales sobre ViewModel mientras trabajan en estrecha colaboración con programadores para diseñar un ViewModel adecuado para respaldar sus esfuerzos. Además, el desacoplamiento entre View y ViewModel también le permite a ViewModel tener unidades más reutilizables y que se puedan probar.

Para aplicar una separación estricta entre las capas de Model, View y ViewMode, deseo crear cada capa como un proyecto de Visual Studio por separado. En combinación con las utilidades reutilizables, el principal ensamblado ejecutable y algunos proyectos de pruebas unitarias (tienen muchos de estos, ¿cierto?), esto puede generar mucho proyectos y ensamblados, como se ilustra en la Figura 1.

Figure 1 The Components of an MVVM Application

Figura 1 Componentes de una aplicación de MVVM

Dada la gran cantidad de proyectos, este enfoque de estricta separación es obviamente el más útil en proyectos de gran tamaño. Para aplicaciones pequeñas que tienen sólo uno o dos programadores, es posible que los beneficios de esta separación estricta no compensen el inconveniente de crear, configurar y mantener múltiples proyectos. Por lo tanto, el simple hecho de separar su código en otros espacios de nombres dentro del mismo proyecto puede ofrecer un aislamiento más que suficiente.

La escritura y el mantenimiento de un ViewModel no son tareas triviales y no deberían emprenderse a la ligera. No obstante, la respuesta a la mayoría de las preguntas básicas, cuándo debe considerar el patrón de diseño MVVM y cuándo es innecesario, se encuentra con frecuencia en su modelo de dominio.

En proyectos grandes, el modelo de dominio puede ser muy complejo y contar con cientos de clases diseñadas minuciosamente para trabajar de manera elegante en cualquier tipo de aplicación, incluidos los servicios web, WPF o las aplicaciones ASP.NET. Model puede formar varios ensamblados que trabajan en conjunto. En organizaciones muy grandes, el modelo de dominio a veces es creado y mantenido por un equipo de desarrollo especializado.

Cuando tenga un modelo de dominio grande y complejo, casi siempre resulta favorable introducir una capa de ViewModel.

Por otro lado, a veces el modelo de dominio es simple, quizás no tiene más que una capa delgada sobre la base de datos. Las clases pueden generarse automáticamente e implementan InotifyPropertyChanged con frecuencia. La interfaz de usuario es comúnmente una colección de listas o cuadrículas con formas de edición que le permiten al usuario manipular los datos subyacentes. El conjunto de herramientas de Microsoft siempre ha sido muy bueno para crear estos tipos de aplicaciones rápida y fácilmente.

Si su modelo o aplicación se encuentran en esta categoría, es probable que ViewModel imponga una sobrecarga inaceptablemente enorme sin beneficiar lo suficiente su diseño de la aplicación.

Dicho esto, incluso en estos casos ViewModel puede proporcionar valor. Por ejemplo, ViewModel es un lugar excelente para implementar la funcionalidad de deshacer. De forma alternativa, puede optar por usar MVVM para una parte de la aplicación (por ejemplo, la administración de documentos, que analizaré más adelante) y exponer pragmáticamente su Model directamente a View.

¿Por qué utilizar ViewModel?

Si ViewModel parece adecuado para su aplicación, aún existen preguntas que deben responderse antes de iniciar la codificación. Una de las primeras es cómo reducir el número de propiedades de proxy.

La separación de View desde Model impulsada por el patrón de diseño MVVM es un aspecto importante y valioso del patrón. Como resultado, si una clase de Model tiene 10 propiedades que deben ser expuestas en View, ViewModel normalmente termina teniendo 10 propiedades idénticas que simplemente llaman con un proxy a la instancia de modelo subyacente. Estas propiedades de proxy normalmente generan un evento de cambio de propiedad cuando se establecen para indicarle a View que cambió la propiedad.

No toda propiedad de Model necesita una propiedad de proxy de ViewModel, pero cada propiedad de Model que necesite exponerse en View normalmente tendrá una propiedad de proxy. Generalmente, las propiedades de proxy tienen este aspecto:

public string Description {
  get { 
    return this.UnderlyingModelInstance.Description; 
  }
  set {
    this.UnderlyingModelInstance.Description = value;
    this.RaisePropertyChangedEvent("Description");
  }
}

Cualquier aplicación no trivial tendrá decenas o centenares de clases de Model que deben exponerse al usuario a través de ViewModel de esta forma. Esto es simplemente intrínseco a la separación proporcionada por MVVM.

La escritura de estas propiedades de proxy es aburrida y, por lo tanto, propensa a generar errores, especialmente porque la generación de evento de cambio de propiedad requiere una cadena que debe coincidir con el nombre de la propiedad (y no se incluirá en ninguna refactorización automática de código). Para eliminar estos eventos de proxy, la solución habitual es exponer la instancia de modelo desde el contenedor de ViewModel directamente y, luego, hacer que el modelo de dominio implemente la interfaz INotifyPropertyChanged:

public class SomeViewModel {
  public SomeViewModel( DomainObject domainObject ) {
    Contract.Requires(domainObject!=null, 
      "The domain object to wrap must not be null");
    this.WrappedDomainObject = domainObject;
  }
  public DomainObject WrappedDomainObject { 
    get; private set; 
  }
...

De este modo, ViewModel aun puede exponer los comandos y las propiedades adicionales requeridos por la vista sin duplicar las propiedades de Model ni crear una gran cantidad de propiedades de proxy. Este enfoque sin duda tiene su atractivo, especialmente si las clases de Model ya implementan la interfaz INotifyPropertyChanged. Hacer que el modelo implemente esta interfaz no es necesariamente algo malo y era incluso habitual con aplicaciones Microsoft .NET Framework 2.0 y Windows Forms. Sin embargo, abarrota el modelo de dominio y no sería útil en los servicios de dominio o las aplicaciones de ASP.NET.

Con este enfoque, View depende de Model, pero sólo se trata de una dependencia indirecta a través del enlace de datos, que no requiere una referencia de proyecto desde el proyecto View al proyecto Model. Entonces, por motivos exclusivamente pragmáticos, este enfoque a veces resulta útil.

Sin embargo, este enfoque viola el espíritu del patrón de diseño MVVM y reduce su capacidad de introducir una nueva funcionalidad específica de ViewModel posteriormente (como las capacidades de deshacer). He detectado escenarios con este enfoque que ocasionaban bastante rediseño. Imagine la situación habitual en que existe un enlace de datos en una propiedad de anidamiento profundo. Si Person ViewModel (ViewModel de Persona) es el contexto de datos actual y la persona tiene una dirección, el enlace de datos podría presentar este aspecto:

{Binding WrappedDomainObject.Address.Country}

Si alguna vez debe introducir la funcionalidad adicional de ViewModel en el objeto Address (Dirección), deberá quitar las referencias de enlace de datos WrappedDomainObject.Address y, en su lugar, utilizar las nuevas propiedades de ViewModel. Esto es problemático porque las actualizaciones del enlace de datos XAML (y posiblemente el contexto de datos también) son difíciles de probar. View es el único componente que no tiene pruebas de regresión automatizadas y exhaustivas.

Propiedades dinámicas

Mi solución a la proliferación de propiedades de proxy es usar el nuevo soporte .NET Framework 4 y WPF para objetos dinámicos y distribución dinámica de métodos. Esta última le permite determinar en tiempo de ejecución cómo administrar la lectura o escritura en una propiedad que, de hecho, no existe en la clase. Esto significa que puede eliminar todas las propiedades de proxy escritas a mano en ViewModel al mismo tiempo que encapsula el modelo subyacente. Observe, sin embargo, que Silverlight 4 no es compatible con el enlace a las propiedades dinámicas.

La manera más sencilla de implementar esta capacidad es hacer que la clase básica ViewModel amplíe la nueva clase de System.Dynamic.DynamicObject y reemplace los métodos TryGetMember y TrySetMember. El tiempo de ejecución de lenguaje dinámico (DLR) llama a estos dos métodos cuando la propiedad a la que hace referencia no existe en la clase y le permite a la clase determinar en tiempo de ejecución cómo implementar las propiedades faltantes. En combinación con una pequeña cantidad de reflexión, la clase de ViewModel puede llamar con un proxy dinámicamente al acceso a la propiedad en la instancia de modelo subyacente sólo en unas pocas líneas de código:

public override bool TryGetMember(
  GetMemberBinder binder, out object result) {

  string propertyName = binder.Name;
  PropertyInfo property = 
    this.WrappedDomainObject.GetType().GetProperty(propertyName);

  if( property==null || property.CanRead==false ) {
    result = null;
    return false;
  }

  result = property.GetValue(this.WrappedDomainObject, null);
  return true;
}

El método comienza mediante el uso de reflexión para encontrar la propiedad en la instancia de Model subyacente. (Para obtener más información, consulte la columna de junio 2007 “Todo sobre CLR” “Reflexiones sobre la reflexión”). Si el modelo no tiene esta propiedad, el método presenta error y devuelve false (error), y el enlace de datos presenta error. Si existe la propiedad, el método utiliza la información de la propiedad para recuperar y devolver el valor de propiedad de Model. Esto supone más trabajo que el método get de propiedad de proxy tradicional, pero esta es la única implementación que necesita para obtener todos los modelos y todas las propiedades.

La verdadera eficacia del enfoque de propiedad dinámica de proxy está en los establecedores de propiedad. En TrySetMember puede incluir la lógica común como generar eventos de cambio de propiedad. El código presenta un aspecto similar al siguiente:

public override bool TrySetMember(
  SetMemberBinder binder, object value) {

  string propertyName = binder.Name;
  PropertyInfo property = 
    this.WrappedDomainObject.GetType().GetProperty(propertyName);

  if( property==null || property.CanWrite==false )
    return false;

  property.SetValue(this.WrappedDomainObject, value, null);

  this.RaisePropertyChanged(propertyName);
  return true;
}

Nuevamente, el método comienza mediante la reflexión para obtener la propiedad desde la instancia de Model subyacente. Si la propiedad no existe o la propiedad es de sólo lectura, el método presenta error y devuelve false. Si la propiedad existe en el objeto de dominio, la información de propiedad se utiliza para establecer la propiedad de Model. A continuación, puede incluir cualquier lógica común a todos los establecedores de propiedad. En este código de muestra simplemente genero el evento de cambio de propiedad para la propiedad recién establecida, pero fácilmente puede hacer más.

Uno de los desafíos que supone la encapsulación de un Model es que Model con frecuencia tiene lo que el lenguaje de modelado unificado denomina propiedades derivadas. Por ejemplo: una clase de Person (Persona) tiene una propiedad BirthDate (Fecha de nacimiento) y una propiedad derivada Age (Edad). La propiedad Age (Edad) es de sólo lectura y calcula la edad automáticamente basándose en la fecha de nacimiento y la fecha actual:

public class Person : DomainObject {
  public DateTime BirthDate { 
    get; set; 
  }

  public int Age {
    get {
      var today = DateTime.Now;
      // Simplified demo code!
      int age = today.Year - this.BirthDate.Year;
      return age;
    }
  }
...

Cuando la propiedad BirthDate (Fecha de nacimiento) cambia, la propiedad Age (Edad) cambia porque la edad deriva matemáticamente de la fecha de nacimiento. Por lo tanto, cuando la propiedad BirthDate (Fecha de nacimiento) está establecida, la clase de ViewModel debe generar un evento de cambio de propiedad para la propiedad BirthDate (Fecha de nacimiento) y la propiedad Age (Edad). Con el enfoque dinámico de ViewModel, puede hacer esto automáticamente al explicitar esta relación entre propiedades dentro del modelo.

En primer lugar, necesita que un atributo personalizado capture la relación de propiedad:

[AttributeUsage(AttributeTargets.Property, AllowMultiple=true)]
public sealed class AffectsOtherPropertyAttribute : Attribute {
  public AffectsOtherPropertyAttribute(
    string otherPropertyName) {
    this.AffectsProperty = otherPropertyName;
  }

  public string AffectsProperty { 
    get; 
    private set; 
  }
}

Establezco AllowMultiple en true (verdadero) para que sea compatible con escenarios en los que una propiedad puede afectar a varias otras propiedades. La aplicación de este atributo para codificar la relación entre BirthDate (Fecha de nacimiento) y Age (Edad) directamente en el modelo es sencilla:

[AffectsOtherProperty("Age")]
public DateTime BirthDate { get; set; }

Para usar estos nuevos metadatos de modelo dentro de la clase dinámica de ViewModel, ahora podremos actualizar el método TrySetMember con tres líneas adicionales de código. Se presenta del siguiente modo:

public override bool TrySetMember(
  SetMemberBinder binder, object value) {
...
  var affectsProps = property.GetCustomAttributes(
    typeof(AffectsOtherPropertyAttribute), true);
  foreach(AffectsOtherPropertyAttribute otherPropertyAttr 
    in affectsProps)
    this.RaisePropertyChanged(
      otherPropertyAttr.AffectsProperty);
}

Con la información de propiedad reflejada ya en mano, el método GetCustomAttributes puede devolver cualquier atributo de AffectsOtherProperty en la propiedad de modelo. A continuación, el código simplemente realiza un bucle sobre los atributos y genera eventos de cambio de propiedad para cada uno. Por lo tanto, los cambios en la propiedad BirthDate (Fecha de nacimiento) mediante ViewModel ahora generan automáticamente los eventos de cambio de propiedad BirthDate (Fecha de nacimiento) y Age (Edad).

Es importante tener en cuenta que si programa una propiedad explícitamente en la clase dinámica de ViewModel (o, más probablemente, en las clases de ViewModel derivadas específicas de modelo), DLR no llamará a los métodos TryGetMember y TrySetMember y, en su lugar, llamará a las propiedades directamente. En ese caso, pierde este comportamiento automático. No obstante, el código puede refactorizarse fácilmente para que las propiedades personalizadas también puedan utilizar esta funcionalidad.

Regresemos al problema del enlace de datos en una propiedad de anidamiento profundo (donde ViewModel es el contexto de datos WPF actual) que presenta el siguiente aspecto:

{Binding WrappedDomainObject.Address.Country}

El uso de propiedades dinámicas de proxy significa que el objeto de dominio contenido subyacente ya no está expuesto. El enlace de datos de hecho se presentaría de la siguiente manera:

{Binding Address.Country}

En este caso, la propiedad Address (Dirección) aún sigue teniendo acceso directo a la instancia Address (Dirección) de modelo subyacente. Sin embargo, ahora cuando desee introducir ViewModel en torno a Address (Dirección), simplemente agregue una nueva propiedad en la clase Person ViewModel (ViewModel de Persona). La nueva propiedad de Address (Dirección) es muy sencilla:

public DynamicViewModel Address {
  get {
    if( addressViewModel==null )
      addressViewModel = 
        new DynamicViewModel(this.Person.Address);
    return addressViewModel;
  }
}

private DynamicViewModel addressViewModel;

No debe cambiar ninguno de los enlaces de datos XAML porque la propiedad aún se llama Address (Dirección), pero ahora DLR llama a una nueva propiedad concreta en lugar del método dinámico TryGetMember. (Observe que la creación diferida de instancias dentro de esta propiedad Address [Dirección] no es segura. Sin embargo, sólo View debe tener acceso a ViewModel y la vista de WPF o Silverlight es única, por lo tanto, esto no es un problema).

Este enfoque puede utilizarse incluso cuando el modelo implementa INotifyPropertyChanged. ViewModel puede advertir esto y optar por no llamar con un proxy a eventos de cambio de propiedad. En este caso, los escucha desde la instancia de modelo subyacente y, luego, regenera los eventos como propios. En el constructor de la clase dinámica de ViewModel, realizo la comprobación y recuerdo el resultado:

public DynamicViewModel(DomainObject model) {
  Contract.Requires(model != null, 
    "Cannot encapsulate a null model");
  this.ModelInstance = model;

  // Raises its own property changed events
  if( model is INotifyPropertyChanged ) {
    this.ModelRaisesPropertyChangedEvents = true;
    var raisesPropChangedEvents = 
      model as INotifyPropertyChanged;
    raisesPropChangedEvents.PropertyChanged +=
      (sender,args) => 
      this.RaisePropertyChanged(args.PropertyName);
  }
}

Para evitar eventos de cambio de propiedad duplicados, también debo realizar una pequeña modificación al método TrySetMember.

if( this.ModelRaisesPropertyChangedEvents==false )
  this.RaisePropertyChanged(property.Name);

Entonces, puede utilizar una propiedad dinámica de proxy para simplificar significativamente la capa de ViewModel y eliminar las propiedades estándar de proxy. Esto reduce de manera significativa la codificación, las pruebas, la documentación y el mantenimiento a largo plazo. La adición de nuevas propiedades al modelo ya no requiere la actualización de la capa de ViewModel a menos que exista una lógica de View muy especial para la nueva propiedad. Además, este enfoque puede resolver problemas difíciles como las propiedades relacionadas. El método común TrySetMember también podría ayudar a implementar una capacidad de deshacer porque la propiedad controlada por el usuario cambia todo el flujo a través del método TrySetMember.

Ventajas y desventajas

Muchos programadores desconfían de la reflexión (y DLR) ante el riesgo de que el rendimiento disminuya. En mi propio trabajo, no creo que esto sea un problema. Es poco probable que se detecte la penalización de rendimiento del usuario al establecer una propiedad única en la interfaz de usuario. Es posible que ese no sea el caso en las interfaces de usuario sumamente interactivas, como las superficies de diseño multitoque.

El único problema importante de rendimiento está en el llenado inicial de la vista cuando existe una gran cantidad de campos. Los problemas de uso deben limitar naturalmente la cantidad de campos que expone en una pantalla para que el rendimiento de los enlaces de datos iniciales mediante este enfoque de DLR pase desapercibido.

No obstante, el rendimiento siempre debe comprenderse y supervisarse cuidadosamente en lo referente a la experiencia del usuario. El enfoque simple descrito anteriormente se podría reescribir con el almacenamiento en caché de reflexión. Para obtener detalles adicionales, consulte el artículo de Joel Pobar en el número de julio de 2005 de MSDN Magazine.

Hay algo de válido en el argumento de que la legibilidad y el mantenimiento del código se ven afectados negativamente por el uso de este enfoque porque la capa de View parece estar haciendo referencia a propiedades en ViewModel que, de hecho, no existen. Sin embargo, creo que los beneficios de eliminar la mayoría de las propiedades de proxy de forma manual superan de lejos los problemas, especialmente con la documentación adecuada sobre ViewModel.

El enfoque de propiedad dinámica de proxy reduce o elimina la capacidad de ofuscar la capa de Model porque ahora se hace referencia a las propiedades en Model según el nombre en XAML. El uso de propiedades tradicionales de proxy no limita su capacidad de ofuscar a Model porque se hace referencia directa a las propiedades y podrían ofuscarse con el resto de la aplicación. Sin embargo, debido a que la mayoría de las herramientas de ofuscación aún no funcionan con XAML/BAML, esto resulta irrelevante. Un buscador de código puede comenzar desde XAML/BAML y trabajar en la capa de Model en cualquiera de los casos.

Por último, este enfoque podría sufrir abusos al atribuirse propiedades de modelo con metadatos relacionados con la seguridad y esperar que ViewModel sea responsable de aplicar la seguridad. La seguridad no parece ser responsabilidad específica de View, y creo que esto significa darle demasiadas responsabilidades a ViewModel. En este caso, un enfoque orientado a aspectos y aplicado dentro de Model sería más adecuado.

Colecciones

Las colecciones son uno de los aspectos más difíciles y menos satisfactorios del patrón de diseño MVVM. Si una colección de Model subyacente cambia por Model, ViewModel es responsable de exponer de algún modo el cambio para que View pueda actualizarse adecuadamente.

Desafortunadamente, lo más probable es que Model no exponga las colecciones que implementan la interfaz INotifyCollectionChanged. En .NET Framework 3.5, esta interfaz está en System.Windows.dll, que hace todo lo posible por evitar su uso en Model. Afortunadamente, en .NET Framework 4, esta interfaz ha migrado a System.dll, y hace que resulte mucho más natural el uso de colecciones observables desde el interior de Model.

Las colecciones observables en Model abren nuevas posibilidades para el desarrollo de Model y podrían utilizarse en las aplicaciones de Windows Forms y Silverlight. Actualmente, este es mi enfoque preferido porque es mucho más simple, y me agrada que la interfaz INotifyCollectionChanged cambie a un ensamblado más común.

Sin las colecciones observables en Model, lo mejor que puede hacer es exponer otro mecanismo, probablemente eventos personalizados, en Model para indicar cuándo se cambió la colección. Esto debe realizarse en una manera específica de Model. Por ejemplo, si la clase Person (Persona) tuviera una colección de direcciones podría exponer eventos como:

public event EventHandler<AddressesChangedEventArgs> 
  NewAddressAdded;
public event EventHandler<AddressesChangedEventArgs> 
  AddressRemoved;

Esto es preferible para generar un evento de colección personalizado diseñado específicamente para WPF ViewModel. Sin embargo, aún resulta difícil exponer los cambios de colección en ViewModel. Probablemente, el único recurso es generar un evento de cambio de propiedad en toda la propiedad de colección de ViewModel. Esta es una solución poco satisfactoria, en el mejor de los casos.

Otro problema que existe con las colecciones es determinar cuándo o si se debe incluir cada instancia de Model en la colección dentro de una instancia de ViewModel. Para las colecciones más pequeñas, ViewModel puede exponer una nueva colección observable y copiar todo lo que haya en la colección de Model subyacente en la colección observable de ViewModel, e incluir cada elemento de Model en la colección en una instancia de ViewModel correspondiente a medida que la recorre. Es posible que ViewModel necesite escuchar los eventos de cambio de colección para devolver los cambios del usuario al Model subyacente.

No obstante, para las colecciones muy grandes que se expondrán en alguna forma de panel de virtualización, el enfoque más sencillo y más pragmático es exponer directamente los objetos de Model.

Creación de instancias de ViewModel

Otro problema que se presenta con el patrón de diseño MVVM que rara vez se analiza es dónde y cuándo deben crearse las instancias de ViewModel. Con frecuencia este problema también se pasa por alto en los análisis de patrones de diseño similares, como MVC.

Mi preferencia es escribir un singleton de ViewModel que ofrezca los principales objetos ViewModel desde los cuales View pueda recuperar fácilmente todos los demás objetos de ViewModel, según sea necesario. Con frecuencia, este objeto maestro de ViewModel ofrece las implementaciones de comando para que View pueda admitir la apertura de documentos.

Sin embargo, la mayoría de las aplicaciones con las que he trabajado ofrecen una interfaz centrada en documentos, y por lo general, utilizan un espacio de trabajo por fichas similar a Visual Studio. En la capa de ViewModel deseo pensar en términos de documentos, y los documentos exponen uno o más objetos de ViewModel que incluyen determinados objetos de Model. Los comandos de WPF estándar en la capa de ViewModel pueden utilizar la capa de persistencia para recuperar los objetos necesarios, incluirlos en las instancias de ViewModel y crear administradores de documentos de ViewModel para mostrarlos.

En la aplicación de ejemplo incluida en este artículo, el comando de ViewModel para crear una nueva Person (Persona) es:

internal class OpenNewPersonCommand : ICommand {
...
  // Open a new person in a new window.
  public void Execute(object parameter) {
    var person = new MvvmDemo.Model.Person();
    var document = new PersonDocument(person);
    DocumentManager.Instance.ActiveDocument = document;
  }
}

El administrador de documentos de ViewModel al que se hizo referencia en la última línea es un singleton que administra todos los documentos abiertos de ViewModel. La pregunta que se plantea es, ¿cómo se expone la colección de documentos de ViewModel en View?

El control de ficha de WPF integrado no ofrece el tipo de interfaz eficaz de múltiples documentos que los usuarios esperan. Afortunadamente, están disponibles los productos de acoplamiento de terceros y espacio de trabajo por fichas. La mayoría se esfuerza por emular el aspecto de Visual Studio del documento por fichas, incluidas las ventanas de herramienta acoplable, las vistas divididas, las ventanas emergentes Ctrl+Tab (con vistas de mini documento) y más.

Desafortunadamente, la mayoría de estos componentes no ofrece soporte integrado para el patrón de diseño MVVM. Pero no importa, porque puede aplicar fácilmente el patrón de diseño del adaptador para vincular el administrador de documentos de ViewModel al componente de vista de terceros.

Adaptador del administrador de documentos

El diseño de adaptador que se muestra en la Figura 2 garantiza que ViewModel no requiera ninguna referencia a View, para que respete los objetivos principales del patrón de diseño MVVM. (Sin embargo, en este caso, el concepto de un documento se define en la capa de ViewModel en lugar de la capa de Model debido a que es puramente un concepto de interfaz de usuario).

Figure 2 Document Manager View Adapter

Figura 2 Adaptador de View de administrador de documentos

El administrador de documentos de ViewModel es responsable de mantener la colección de documentos abiertos de ViewModel y saber qué documento está activo. Este diseño le permite a la capa de ViewModel abrir o cerrar documentos mediante el administrador de documentos, y cambiar el documento activo sin conocimiento de View. El lado de ViewModel de este enfoque es considerablemente sencillo. Las clases de ViewModel en la aplicación de ejemplo se muestran en la Figura 3.

Figure 3 The ViewModel Layer’s Document Manager and Document Classes

Figura 3 Las clases de documento y el administrador de documentos de la capa de ViewModel

La clase de base de documento expone varios métodos de ciclo de vida internos (Activado, Activación Perdida y Documento Cerrado) a los cuales llama el administrador de documentos para mantener el documento al día sobre lo que está pasando. El documento también implementa una interfaz INotifyPropertyChanged para que sea compatible con el enlace de datos. Por ejemplo, los datos del adaptador enlazan la propiedad Title del documento de vista a la propiedad DocumentTitle de ViewModel.

El elemento más complejo de este enfoque es la clase de adaptador, y he proporcionado una copia de trabajo en el proyecto que acompaña a este artículo. El adaptador se suscribe a los eventos del administrador de documentos y utiliza esos eventos para mantener actualizado el control de espacio de trabajo por fichas. Por ejemplo, cuando el administrador de documentos indica que se abrió un nuevo documento, el adaptador recibe un evento, incluye el documento de ViewModel en cualquier control de WPF que sea requerido y luego expone ese control en el espacio de trabajo por fichas.

El adaptador tiene otra responsabilidad: mantener el administrador de documentos de ViewModel sincronizado con las acciones del usuario. Por ello, el adaptador también debe escuchar los eventos desde el control del espacio de trabajo por fichas para que cuando el usuario cambie el documento activo o cierre el documento, el adaptador pueda informar al administrador de documentos.

Aunque ninguna de estas lógicas es muy compleja, hay algunas advertencias. Existen varios escenarios donde el código se convierte en varios participantes, y este debe administrarse correctamente. Por ejemplo, si ViewModel utiliza el administrador de documentos para cerrar un documento, el adaptador recibirá el evento desde el administrador de documentos y cerrará la ventana del documento físico en la vista. Esto hace que el control del espacio de trabajo por fichas también genere un evento de cierre de documento, que el adaptador también recibirá, y el controlador de eventos del adaptador, por supuesto, informará al administrador de documentos que el documento debe cerrarse. Ya se cerró el documento, por lo tanto, el administrador de documentos debe ser lo suficientemente compasivo para permitirlo.

La otra dificultad es que el adaptador de View debe ser capaz de vincular un control de documento por fichas de View con un objeto de documento de ViewModel. La solución más sólida es utilizar una propiedad de dependencia adjunta de WPF. El adaptador declara una propiedad de dependencia adjunta privada que se utiliza para vincular el control de la ventana View con su instancia de documento de ViewModel.

En el proyecto de ejemplo de este artículo, utilizo un componente gratuito de espacio de trabajo por fichas llamado AvalonDock, así que mi propiedad de dependencia adjunta es similar al código que se muestra en la Figura 4.

Figura 4 Vinculación de control de View y documento de ViewModel

private static readonly DependencyProperty 
  ViewModelDocumentProperty =
  DependencyProperty.RegisterAttached(
  "ViewModelDocument", typeof(Document),
  typeof(DocumentManagerAdapter), null);

private static Document GetViewModelDocument(
  AvalonDock.ManagedContent viewDoc) {

  return viewDoc.GetValue(ViewModelDocumentProperty) 
    as Document;
}

private static void SetViewModelDocument(
  AvalonDock.ManagedContent viewDoc, Document document) {

  viewDoc.SetValue(ViewModelDocumentProperty, document);
}

Cuando el adaptador crea un nuevo control de ventana de View, establece la propiedad adjunta en el nuevo control de ventana al documento subyacente de ViewModel (consulte la Figura 5). También puede consultar el enlace de datos de título que se configura aquí, y consulte cómo el adaptador configura el contexto de datos y el contenido del control de documento de View.

Figura 5 Establecimiento de la propiedad adjunta

private AvalonDock.DocumentContent CreateNewViewDocument(
  Document viewModelDocument) {

  var viewDoc = new AvalonDock.DocumentContent();
  viewDoc.DataContext = viewModelDocument;
  viewDoc.Content = viewModelDocument;

  Binding titleBinding = new Binding("DocumentTitle") { 
    Source = viewModelDocument };

  viewDoc.SetBinding(AvalonDock.ManagedContent.TitleProperty, 
    titleBinding);
  viewDoc.Closing += OnUserClosingDocument;
  DocumentManagerAdapter.SetViewModelDocument(viewDoc, 
    viewModelDocument);

  return viewDoc;
}

Al establecer el contenido del control de documento de View, hice que WPF se encargara del trabajo pesado de averiguar cómo mostrar este tipo particular de documento de ViewModel. Las plantillas de datos reales para los documentos de ViewModel se encuentran en un diccionario de recursos incluido en la ventana principal de XAML.

He utilizado este enfoque de administrador de documentos de ViewModel con WPF y Silverlight correctamente. El único código de capa de View es el adaptador, y este puede ser fácil de probar y luego dejado de lado. Este enfoque mantiene a ViewModel completamente independiente de View, y en una sola ocasión cambié de proveedores para mi componente de espacio de trabajo por fichas sólo con cambios mínimos en la clase del adaptador y sin ningún cambio en absoluto en ViewModel ni Model.

La capacidad de trabajar con documentos en la capa de ViewModel parece elegante, e implementar comandos de ViewModel como el que mostré aquí es sencillo. Las clases de documento de ViewModel también se convierten en lugares obvios para exponer instancias de ICommand relacionadas con el documento.

View se enlaza con estos comandos y se trasluce la belleza del patrón de diseño MVVM. Además, el enfoque del administrador de documentos de ViewModel también funciona con el enfoque de singleton si necesita exponer los datos antes de que el usuario haya creado documentos (posiblemente en una ventana de herramienta contraíble).

Conclusión

El patrón de diseño MVVM es eficaz y útil, aunque ningún patrón de diseño puede resolver todos los problemas. Como mostré aquí, al combinar el patrón y los objetivos de MVVM con otros patrones, como adaptadores y singletons, mientras también aprovecha las nuevas características de .NET Framework 4, como la distribución dinámica, puede resolver cuestiones comunes sobre la implementación del patrón de diseño MVVM. Emplear MVVM de manera correcta contribuye a que las aplicaciones de WPF y Silverlight sean más elegantes y más fáciles de mantener. Para obtener más información acerca de MVVM, consulte el artículo de Josh Smith en el número de febrero de 2009 de MSDN Magazine.

Robert McCarteres un desarrollador, arquitecto y empresario software canadiense independiente. Puede leer su blog en robertmccarter.wordpress.com.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo:  Josh Smith