Julio de 2016

Volumen 31, número 7

Enlace de datos: Una mejor manera de implementar enlace de datos en .NET

Por Mark Sowul

El enlace de datos es una técnica eficaz para desarrollar interfaces de usuario: hace que sea más fácil separar la lógica de vista de la lógica de negocios y simplifica la ejecución de pruebas del código resultante. Aunque se encuentra en Microsoft .NET Framework desde el comienzo, el enlace de datos adquirió mayor relevancia con la aparición de Windows Presentation Foundation (WPF) y XAML, ya que es el "pegamento" entre View y ViewModel en el patrón Model-View-ViewModel (MVVM).

El inconveniente de implementar el enlace de datos ha sido siempre la necesidad de cadenas mágicas y código reutilizable, para difundir los cambios en las propiedades y enlazarles los elementos de la interfaz de usuario. A lo largo de los años, han aparecido distintos kits de herramientas y técnicas para facilitar la tarea; el objetivo de este artículo es simplificar el proceso aún más.

En primer lugar, exploraré los conceptos básicos de la implementación del enlace de datos, así como técnicas habituales para simplificarla (si ya está familiarizado con este tema, puede omitir estas secciones). A continuación, desarrollaré una técnica que quizá no haya considerado ("Una tercera manera") y presentaré soluciones para dificultades de diseño relacionadas que hayan surgido durante el desarrollo de aplicaciones con MVVM. Puede conseguir la versión finalizada del marco que se desarrolla aquí en la descarga de código que se incluye con el artículo o agregar el paquete NuGet SolSoft.DataBinding a sus propios proyectos.

Conceptos básicos: INotifyPropertyChanged

La implementación de INotifyPropertyChanged es la manera preferida para habilitar que un objeto se enlace a una interfaz de usuario. Es muy sencillo, tan solo contiene un miembro: el evento PropertyChanged. El objeto debe generar este evento cuando una propiedad enlazable cambie, de forma que notifique a la vista que debe actualizar la representación del valor de la propiedad.

La interfaz es simple, pero su implementación no lo es. La generación manual del evento con nombres de propiedades textuales codificadas de forma rígida no es una solución que se escale adecuadamente, ni tampoco funciona bien con la refactorización: debe tener cuidado para garantizar que el nombre textual se mantiene sincronizado con el nombre de la propiedad en el código. Esto no le permitirá precisamente ganarse el cariño de sus sucesores. Aquí se muestra un ejemplo:

public int UnreadItemCount
{
  get
  {
    return m_unreadItemCount;
  }
  set
  {
    m_unreadItemCount = value;
    OnNotifyPropertyChanged(
      new PropertyChangedEventArgs("UnreadItemCount")); // Yuck
  }
}

Existen varias técnicas que la gente ha desarrollado como respuesta, a fin de mantener la cordura (por ejemplo, consulte la pregunta en Stack Overflow que puede encontrar en bit.ly/24ZQ7CY); la mayoría entran en uno de dos tipos.

Técnica habitual número 1: clase base

Una forma de simplificar la situación es con una clase base, a fin de reutilizar parte de la lógica reutilizable. Esto también ofrece unas cuantas maneras de obtener el nombre de la propiedad mediante programación, en lugar de tener que codificarlo de forma rígida.

Obtención del nombre de la propiedad con expresiones: .NET Framework 3.5 introdujo las expresiones, que permiten una inspección en tiempo de ejecución de la estructura del código. LINQ usa esta API de forma muy eficaz, por ejemplo, para traducir consultas .NET LINQ en instrucciones SQL. Los desarrolladores empresariales también han aprovechado esta API para inspeccionar nombres de propiedades. Si se utiliza una clase base para realizar esta inspección, el establecedor anterior se puede reescribir como sigue:

public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged(() => UnreadItemCount);
}

De esta forma, al cambiar el nombre de UnreadItemCount también se cambiará el nombre de la referencia a la expresión, por lo que el código seguirá funcionando. La signatura de RaiseNotifyPropertyChanged sería esta:

void RaiseNotifyPropertyChanged<T>(Expression<Func<T>> memberExpression)

Existen diversas técnicas para recuperar el nombre de la propiedad del elemento memberExpression. En el blog de MSDN de C#, que puede encontrar en bit.ly/25baMHM, se ofrece un sencillo ejemplo:

public static string GetName<T>(Expression<Func<T>> e)
{
  var member = (MemberExpression)e.Body;
  return member.Member.Name;
}

StackOverflow ofrece un listado más exhaustivo en bit.ly/23Xczu2. En cualquier caso, esta técnica tiene un aspecto negativo: en la recuperación del nombre de la expresión se usa reflexión, que es lenta. La sobrecarga de rendimiento puede ser significativa, en función del número de notificaciones de cambios de propiedades que haya.

Obtención del nombre de la propiedad con CallerMemberName: C# 5.0 y .NET Framework 4.5 proporcionaron una forma adicional de recuperar el nombre de la propiedad, mediante el atributo CallerMemberName (puede usarlo con versiones más antiguas de .NET Framework mediante el paquete NuGet Microsoft.Bcl). En esta ocasión, el compilador realiza todo el trabajo, por lo que no hay sobrecarga en tiempo de ejecución. Con este enfoque, el método se convierte en lo siguiente:

void RaiseNotifyPropertyChanged<T>([CallerMemberName] string propertyName = "")
And the call to it is:
public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged();
}

Este atributo indica al compilador que rellene el nombre del llamador, UnreadItemCount, con el valor del parámetro opcional propertyName.

Obtención del nombre de la propiedad con nameof: Probablemente, el atributo CallerMemberName se hizo a medida para este caso de uso (generando PropertyChanged en una clase base), pero en C# 6, el equipo del compilador proporcionó algo que era útil de una forma mucho más amplia: la palabra clave nameof. Nameof resulta de utilidad para muchos fines; en este caso, si reemplazo el código basado en expresiones por nameof, de nuevo el compilador realizará todo el trabajo (sin ninguna sobrecarga en tiempo de ejecución). Merece la pena destacar que se trata estrictamente de una característica de versión del compilador y no de una característica de versión de .NET: Sería posible utilizar esta técnica y seguir teniendo como destino .NET Framework 2.0. No obstante, es necesario que tanto usted como el resto de miembros del equipo usen como mínimo Visual Studio 2015. Cuando se utiliza nameof, este es el aspecto del código:

public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged(nameof(UnreadItemCount));
}

Sin embargo, existe un problema general con cualquier técnica de clase base: la clase base "se quema", como se suele decir. Si quiere que el modelo de vista extienda una clase distinta, no podrá hacerlo. Tampoco hace nada para controlar las propiedades "dependientes" (por ejemplo, una propiedad FullName que concatene FirstName y LastName: cualquier cambio en FirstName o LastName también debe desencadenar un cambio en FullName).

Técnica habitual número 2: programación orientada a aspectos

La programación orientada a aspectos (AOP) es una técnica que básicamente "posprocesa" el código compilado, ya sea en tiempo de ejecución o con un paso de poscompilación, a fin de agregar comportamientos determinados (conocidos como un "aspecto"). Normalmente, el objetivo es reemplazar el código reutilizable repetitivo, como el registro o el control de excepciones (conocidos como "asuntos transversales"). No es sorprendente que la implementación de INotifyPropertyChanged sea un buen candidato.

Existen varios kits de herramientas disponibles para este enfoque. PostSharp es uno (bit.ly/1Xmq4n2). Me sorprendió gratamente conocer que controla adecuadamente las propiedades dependientes (por ejemplo, la propiedad FullName descrita anteriormente). Un marco de código abierto denominado "Fody" es similar (bit.ly/1wXR2VA).

Se trata de un enfoque atractivo; sus inconvenientes pueden no ser significativos. Algunas implementaciones interceptan el comportamiento en tiempo de ejecución, lo que provoca un costo en rendimiento. En cambio, los marcos de poscompilación no deberían introducir nada de sobrecarga en tiempo de ejecución, pero es posible que requieran algún tipo de instalación o configuración. PostSharp se ofrece actualmente como una extensión para Visual Studio. Su edición gratuita "Express" limita el uso del aspecto INotifyPropertyChanged a 10 clases, por lo que probablemente esto implica un costo monetario. Fody, por el contrario, es un paquete NuGet gratuito, que lo convierte en una opción convincente. En cualquier caso, tenga en cuenta que con cualquier marco AOP el código que escribe no es exactamente el mismo código que ejecutará... y depurará.

Una tercera manera

Una forma alternativa de controlar esto es aprovechar el diseño orientado a objetos: hacer que las mismas propiedades sean responsables de generar los eventos. No es una idea especialmente revolucionaria, pero no la he encontrado fuera de mis propios proyectos. En su forma más básica, podría tener un aspecto similar a lo siguiente:

public class NotifyProperty<T>
{
  public NotifyProperty(INotifyPropertyChanged owner, string name, T initialValue);
  public string Name { get; }
  public T Value { get; }
  public void SetValue(T newValue);
}

La idea es que proporcione la propiedad con su nombre y una referencia a su propietario, y dejar que haga el trabajo y genere el evento PropertyChanged, como se indica a continuación:

public void SetValue(T newValue)
{
  if(newValue != m_value)
  {
    m_value = newValue;
    m_owner.PropertyChanged(m_owner, new PropertyChangedEventArgs(Name));
  }
}

El problema es que esto en realidad no funcionará: no es posible generar un evento desde otra clase de esa manera. Es necesario algún tipo de contrato con la clase propietaria para que me permita generar su evento PropertyChanged; y ese es exactamente el trabajo de una interfaz, por lo que crearé una:

public interface IRaisePropertyChanged
{
  void RaisePropertyChanged(string propertyName)
}

Cuando disponga de esta interfaz, podré implementar definitivamente Notify­Property.SetValue:

public void SetValue(T newValue)
{
  if(newValue != m_value)
  {
    m_value = newValue;
    m_owner.RaisePropertyChanged(this.Name);
  }
}

Implementación de IRaisePropertyChanged: al requerir que el propietario de la propiedad implemente una interfaz, cada clase de modelo de vista requerirá algo de código reutilizable, como se ilustró en la Figura 1. La primera parte es necesaria para que cualquier clase implemente INotifyPropertyChanged; la segunda parte es específica del nuevo elemento IRaisePropertyChanged. Tenga en cuenta que como el método RaisePropertyChanged no está pensado para un uso general, prefiero implementarlo explícitamente.

Figura 1. Código necesario para implementar IRaisePropertyChanged

// PART 1: required for any class that implements INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
  // In C# 6, you can use PropertyChanged?.Invoke.
  // Otherwise I'd suggest an extension method.
  var toRaise = PropertyChanged;
  if (toRaise != null)
    toRaise(this, args);
}
// PART 2: IRaisePropertyChanged-specific
protected virtual void RaisePropertyChanged(string propertyName)
{
  OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
// This method is only really for the sake of the interface,
// not for general usage, so I implement it explicitly.
void IRaisePropertyChanged.RaisePropertyChanged(string propertyName)
{
  this.RaisePropertyChanged(propertyName);
}

Podría poner este código reutilizable en una clase base y extenderla, lo que me llevaría de nuevo al debate anterior. Después de todo, si aplico CallerMemberName al método RaisePropertyChanged, básicamente habré reinventado la primera técnica; por tanto, ¿qué sentido tiene? En ambos casos, podría simplemente copiar el código reutilizable en otras clases si no pueden derivarse de una clase base.

Una diferencia clave en comparación con la técnica de clase base anterior es que en este caso no hay lógica real en el código reutilizable; toda la lógica está encapsulada en la clase NotifyProperty. La comprobación de si el valor de propiedad ha cambiado antes de generar el evento es una lógica simple, pero sigue siendo mejor no duplicarla. Considere lo que sucedería si quisiera usar un elemento IEqualityComparer distinto para hacer la comprobación. Con este modelo, solo sería necesario modificar la clase NotifyProperty. Incluso aunque tuviera varias clases con el mismo código reutilizable de IRaisePropertyChanged, cada una de las implementaciones podría beneficiarse de los cambios en NotifyProperty sin tener que cambiar nada del código en sí. Independientemente de los cambios de comportamiento que quiera introducir, no es muy probable que el código de IRaisePropertyChanged cambie.

Juntar las piezas: ahora tengo la interfaz que el modelo de vista debe implementar y la clase NotifyProperty que se usa para las propiedades que se enlazarán a datos. El último paso es construir el elemento NotifyProperty; para ello, será necesario pasar de alguna forma un nombre de propiedad. Si es tan afortunado que utiliza C# 6, esto se realiza de manera sencilla con el operador nameof. En caso contrario, puede en su lugar crear el elemento NotifyProperty con ayuda de expresiones, como por ejemplo mediante un método de extensión (lamentablemente, en este caso Caller­MemberName no puede ayudar en ningún sitio):

public static NotifyProperty<T> CreateNotifyProperty<T>(
  this IRaisePropertyChanged owner,
  Expression<Func<T>> nameExpression, T initialValue)
{
  return new NotifyProperty<T>(owner,
    ObjectNamingExtensions.GetName(nameExpression),
    initialValue);
}
// Listing of GetName provided earlier

Con este enfoque, seguirá pagando un costo de reflexión, pero solo cuando se cree un objeto, en lugar de cada vez que cambie una propiedad. Si esto sigue siendo demasiado costoso (está creando demasiados objetos), siempre puede almacenar en caché una llamada a GetName y mantenerla como un valor de solo lectura estático en la clase modelo de vista. En cualquier caso, en la Figura 2 se muestra un ejemplo de un modelo de vista simple.

Figura 2. Un elemento ViewModel básico con un elemento NotifyProperty

public class LogInViewModel : IRaisePropertyChanged
{
  public LogInViewModel()
  {
    // C# 6
    this.m_userNameProperty = new NotifyProperty<string>(
      this, nameof(UserName), null);
    // Extension method using expressions
    this.m_userNameProperty = this.CreateNotifyProperty(() => UserName, null);
  }
  private readonly NotifyProperty<string> m_userNameProperty;
  public string UserName
  {
    get
    {
      return m_userNameProperty.Value;
    }
    set
    {
      m_userNameProperty.SetValue(value);
    }
  }
  // Plus the IRaisePropertyChanged code in Figure 1 (otherwise, use a base class)
}

Enlace y cambio de nombre: aunque estoy hablando sobre nombres, es un buen momento para comentar otro asunto del enlace de datos. La generación de forma segura del evento PropertyChanged sin una cadena codificada de forma rígida es la mitad de la batalla para sobrevivir a la refactorización; el enlace de datos en sí es la otra mitad. Si cambia el nombre de una propiedad que se usa para un enlace en XAML, debo decir que no estará garantizado que se realice correctamente (consulte como ejemplo bit.ly/1WCWE5m).

La alternativa es codificar los enlaces de datos manualmente en el archivo de código subyacente. Por ejemplo:

// Constructor
public LogInDialog()
{
  InitializeComponent();
  LogInViewModel forNaming = null;
  m_textBoxUserName.SetBinding(TextBox.TextProperty,
    ObjectNamingExtensions.GetName(() => forNaming.UserName);
  // Or with C# 6, just nameof(LogInViewModel.UserName)
}

Es un poco extraño que el objeto null solo esté para aprovechar la funcionalidad de las expresiones, pero funciona (no es necesario si dispone de acceso a nameof).

Considero que esta técnica es valiosa, pero reconozco sus implicaciones. En el lado positivo, si cambio el nombre de la propiedad UserName, tendré la garantía de que la refactorización funcionará. Otra ventaja significativa es que "Encontrar todas las referencias" funciona exactamente como se espera.

La parte negativa es que no es necesariamente tan simple y natural como hacer el enlace en XAML, y me impide que pueda mantener el diseño de la interfaz de usuario "independiente". Por ejemplo, no puedo simplemente rediseñar la apariencia en la herramienta Blend sin cambiar código. Además, esta técnica no funciona con plantillas de datos; puede extraer la plantilla en un control personalizado, pero eso requiere más trabajo.

En definitiva, gano flexibilidad para cambiar el lado del "modelo de datos" a cambio de sacrificar flexibilidad en el lado de la "vista". En términos generales, es su decisión juzgar si las ventajas justifican la declaración de los enlaces de esta manera.

Propiedades "derivadas"

Anteriormente, describí un escenario en el que es especialmente inapropiado generar el evento PropertyChanged, en concreto para aquellas propiedades cuyos valores dependan de otras propiedades. Mencioné el ejemplo sencillo de una propiedad FullName que dependa de FirstName y LastName. Mi objetivo para implementar este escenario es tomar esos objetos NotifyProperty base (FirstName y LastName), además de la función para calcular el valor derivado a partir de ellos (por ejemplo, FirstName.Value + " " + LastName.Value), y con ello, producir un objeto de propiedad que controlará automáticamente el resto. Para habilitarlo, hay un par de modificaciones que realizaré en el código original de NotifyProperty.

La primera tarea es exponer un evento ValueChanged independiente en NotifyProperty. La propiedad derivada escuchará este evento en sus propiedades subyacentes y responderá calculando un nuevo valor (y generando el evento PropertyChanged adecuado para sí misma). La segunda tarea es extraer una interfaz, IProperty<T>, para encapsular la funcionalidad general de NotifyProperty. Entre otras cosas, esto me permite disponer propiedades derivadas a partir de otras propiedades derivadas. Aquí se muestra la sencilla interfaz resultante (los cambios correspondientes a NotifyProperty son muy sencillos, por lo que no los enumeraré aquí):

public interface IProperty<TValue>
{
  string Name { get; }
  event EventHandler<ValueChangedEventArgs> ValueChanged;
  TValue Value { get; }
}

La creación de la clase DerivedNotifyProperty también parece sencilla, hasta que comienza a intentar juntar las piezas. La idea básica era aceptar las propiedades subyacentes y una función para calcular algún valor nuevo a partir de ellas, pero enseguida comienzan a surgir problemas por los genéricos. No hay una forma práctica de aceptar varios tipos de propiedades distintos:

// Attempted constructor
public DerivedNotifyProperty(IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)

Puedo solucionar la primera mitad del problema (aceptar varios tipos genéricos) mediante el uso de métodos estáticos Create en su lugar:

static DerivedNotifyProperty<TDerived> CreateDerivedNotifyProperty
  <T1, T2, TDerived>(this IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)

Pero sigue siendo necesario que la propiedad derivada escuche el evento ValueChanged en cada propiedad base. Para resolverlo, son necesarios dos pasos. En primer lugar, extraeré el evento ValueChanged a una interfaz aparte:

public interface INotifyValueChanged // No generic type!
{
  event EventHandler<ValueChangedEventArgs> ValueChanged;
}
public interface IProperty<TValue> : INotifyValueChanged
{
  string Name { get; }
  TValue Value { get; }
}

Esto permite que DerivedNotifyProperty acepte el elemento no genérico INotifyValueChanged, en lugar del genérico IProperty<T>. En segundo lugar, debo calcular el nuevo valor sin genéricos: tomaré el elemento derivedValueFunction original que acepta los dos parámetros genéricos y, a partir de él, crearé una nueva función anónima que no requiera ningún parámetro (en su lugar haré referencia a los valores de las dos propiedades pasadas). En otras palabras, crearé una clausura. Puede ver este proceso en el código siguiente:

static DerivedNotifyProperty<TDerived> CreateDerivedNotifyProperty
  <T1, T2, TDerived>(this IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)
{
  // Closure
  Func<TDerived> newDerivedValueFunction =
    () => derivedValueFunction (property1.Value, property2.Value);
  return new DerivedNotifyProperty<TValue>(owner, propertyName,
    newDerivedValueFunction, property1, property2);
}

La nueva función de "valor derivado" es simplemente Func<TDerived> sin parámetros; ahora, DerivedNotifyProperty no requiere conocimiento sobre los tipos de propiedad subyacentes, de forma que puedo crear sin problemas uno a partir de varias propiedades de tipos distintos.

El otro matiz es cuándo llamar realmente a esa función de valor derivado. Una implementación evidente sería escuchar el evento ValueChanged de cada una de las propiedades subyacentes y llamar a la función cuando cambie una propiedad, pero eso no es eficaz cuando cambian varias propiedades subyacentes en la misma operación (imagine un botón de restablecimiento que borra un formulario). Una mejor idea sería producir el valor a petición (y almacenarlo en caché) e invalidarlo si alguna de las propiedades subyacentes cambia. Lazy<T> es una forma perfecta de implementarlo.

Puede ver una lista abreviada de la clase DerivedNotifyProperty en la Figura 3. Observe que la clase acepta un número arbitrario de propiedades que escuchará (aunque solo enumero el método Create para dos propiedades subyacentes, creo sobrecargas adicionales que aceptan una propiedad subyacente, tres propiedades subyacentes y así sucesivamente).

Figura 3. Implementación del núcleo de DerivedNotifyProperty

public class DerivedNotifyProperty<TValue> : IProperty<TValue>
{
  private readonly IRaisePropertyChanged m_owner;
  private readonly Func<TValue> m_getValueProperty;
  public DerivedNotifyProperty(IRaisePropertyChanged owner,
    string derivedPropertyName, Func<TValue> getDerivedPropertyValue,
    params INotifyValueChanged[] valueChangesToListenFor)
  {
    this.m_owner = owner;
    this.Name = derivedPropertyName;
    this.m_getValueProperty = getDerivedPropertyValue;
    this.m_value = new Lazy<TValue>(m_getValueProperty);
    foreach (INotifyValueChanged valueChangeToListenFor in valueChangesToListenFor)
      valueChangeToListenFor.ValueChanged += (sender, e) => RefreshProperty();
  }
  // Name property and ValueChanged event omitted for brevity 
  private Lazy<TValue> m_value;
  public TValue Value
  {
    get
    {
      return m_value.Value;
    }
  }
  public void RefreshProperty()
  {
    // Ensure we retrieve the value anew the next time it is requested
    this.m_value = new Lazy<TValue>(m_getValueProperty);
    OnValueChanged(new ValueChangedEventArgs());
    m_owner.RaisePropertyChanged(Name);
  }
}

Tenga en cuenta que las propiedades subyacentes podrían proceder de distintos propietarios. Por ejemplo, suponga que tiene un modelo de vista Address con una propiedad IsAddressValid. También tiene un modelo de vista Order que contiene dos modelos de vista Address, para las direcciones de facturación y envío. Sería razonable crear una propiedad IsOrderValid en el modelo de vista principal Order que combine las propiedades IsAddressValid de los modelos de vista secundarios Address, de forma que pueda enviar el pedido si las dos direcciones son válidas. Para hacerlo, el modelo de vista Address expondría tanto bool IsAddressValid { get; } como IProperty<bool> IsAddressValidProperty { get; }, para que el modelo de vista Order pueda crear un elemento DerivedNotifyProperty que haga referencia a los objetos secundarios IsAddressValidProperty.

La utilidad de DerivedNotifyProperty

El ejemplo de FullName que indiqué anteriormente para una propiedad derivada es bastante forzado, pero quiero comentar algunos casos de uso reales y relacionarlos con algunos principios de diseño. Justo acabo de comentar un ejemplo: IsValid. Se trata de una forma bastante simple y eficaz de deshabilitar el botón "Guardar" en un formulario, por ejemplo. Tenga en cuenta que no hay nada que le obligue a usar esta técnica solo en el contexto de un modelo de vista de interfaz de usuario. Puede usarlo también para validar objetos de negocio; solo es necesario que implementen IRaisePropertyChanged.

Una segunda situación en la que las propiedades derivados son extremadamente útiles es en los escenarios de "exploración en profundidad". Como un ejemplo sencillo, piense en un cuadro combinado para seleccionar un país, en el que al seleccionar un país se rellene una lista de ciudades. Puede optar por que SelectedCountry sea un objeto NotifyProperty y, dado un método GetCitiesForCountry, se cree AvailableCities como una propiedad DerivedNotifyProperty que se mantendrá sincronizada automáticamente cuando el país seleccionado cambie.

Una tercera área donde he usado objetos NotifyProperty es para indicar si un objeto está "ocupado". Aunque se considere que el objeto está ocupado, algunas características de la interfaz de usuario deben deshabilitarse con la posibilidad de que el usuario vea un indicador de progreso. Aparentemente, este es un escenario simple, pero hay muchos aspectos sutiles que deben considerarse.

La primera parte es realizar un seguimiento de si el objeto está ocupado; en el caso simple, puedo hacerlo con una propiedad booleana NotifyProperty. No obstante, lo que a menudo sucede es que un objeto puede estar "ocupado" por una o varias razones: digamos que estoy cargando varias áreas de datos, posiblemente en paralelo. El estado general "ocupado" debería depender de si alguno de esos elementos está aún en curso. Eso suena casi como un trabajo para propiedades derivadas, pero sería inadecuado (si no imposible): necesitaría una propiedad por cada operación posible a fin de realizar un seguimiento sobre si está en curso. En su lugar, quiero hacer algo como lo siguiente para cada operación, con una propiedad IsBusy única:

try
{
  IsBusy.SetValue(true);
  await LongRunningOperation();
}
finally
{
  IsBusy.SetValue(false);
}

Para habilitarlo, creo una clase IsBusyNotifyProperty que extiende NotifyProperty<bool> y, en ella, mantengo un "contador de ocupado". Reemplazo SetValue de forma que SetValue(true) incremente ese contador y SetValue(false) lo decremente. Solo cuando el contador pase de 0 a 1 llamaré a base.SetValue(true), y cuando pase de 1 a 0 a base.SetValue(false). De esta forma, cuando se inician varias operaciones pendientes, el valor de IsBusy pasa a ser verdadero solo una vez y, a partir de entonces, solo pasa a ser falso de nuevo cuando todas hayan terminado. Puede ver la implementación en la descarga de código.

Eso se ocupa del aspecto "ocupado" de las cosas: puedo enlazar "está ocupado" a la visibilidad de un indicador de progreso. Sin embargo, para deshabilitar la interfaz de usuario, necesito lo opuesto. Cuando "está ocupado" sea verdadero, "UI habilitada" debe ser falso.

XAML tiene el concepto de un elemento IValueConverter, que convierte un valor a (o desde) una representación por pantalla. Un ejemplo omnipresente es BooleanToVisibilityConverter; en XAML, la "visibilidad" de un elemento no se describe mediante un valor booleano, sino mediante un valor de enumeración. Esto significa que no es posible enlazar la visibilidad de un elemento directamente a una propiedad booleana (como IsBusy); es necesario enlazar el valor y además usar un convertidor. Por ejemplo:

<StackPanel Visibility="{Binding IsBusy,
  Converter={StaticResource BooleanToVisibilityConverter}}" />

He mencionado que "habilitar la UI" es lo opuesto de "está ocupado"; podría ser tentador crear un convertidor de valores para invertir una propiedad booleana y usarlo para hacer la tarea:

<Grid IsEnabled="{Binding IsBusy,
   Converter={StaticResource BooleanToInverseConverter}}" />

En efecto, antes de que creara una clase DerivedNotifyProperty, esa era la forma más sencilla. Era bastante tedioso crear una propiedad separada, conectarla para que sea la inversa de IsBusy y generar el evento PropertyChanged adecuado. Sin embargo, ahora es trivial, y sin esa barrera artificial (que es la pereza) tengo una mejor percepción de dónde tiene sentido usar IValueConverter.

En el fondo, la vista, independientemente de cómo esté implementada (por ejemplo, WPF, Windows Forms o incluso una aplicación de consola es un tipo de vista), debe ser una visualización (o "proyección") de lo que sucede en la aplicación subyacente, y no es responsable de determinar el mecanismo y las reglas de negocio de lo que está ocurriendo. En este caso, el hecho de que IsBusy e IsEnabled estén tan íntimamente relacionados entre ellos es un detalle de implementación; no es inherente que deshabilitar la interfaz de usuario deba relacionarse específicamente con si la aplicación está ocupada.

Como está establecido, lo considero un terreno dudoso y no le discutiría que quisiera usar un convertidor de valores para implementar esto. No obstante, puedo reforzar mi argumento si añado otra pieza al ejemplo. Supongamos que si se pierde el acceso a la red, la aplicación debería poder deshabilitar la interfaz de usuario (y mostrar un panel que indique la situación). Eso daría lugar a que existiesen tres situaciones: si la aplicación está ocupada, debería deshabilitar la interfaz de usuario (y mostrar un panel de progreso); si la aplicación pierde el acceso a la red, también debería deshabilitar la interfaz de usuario (y mostrar un panel de "conexión perdida"); la tercera situación se produciría cuando la aplicación está conectada y no está ocupada, y por tanto está lista para aceptar entradas.

Intentar implementar esto sin una propiedad IsEnabled independiente es cuando menos poco práctico; podría usar un elemento MultiBinding, pero sigue siendo algo poco elegante y no se admite en todos los entornos. Al fin y al cabo, ese tipo de dificultades normalmente implican que existe una forma mejor de hacerlo, y ahora sabemos que la hay: esta lógica se controla mejor dentro del modelo de vista. Ahora es trivial exponer dos elementos NotifyProperties (IsBusy e IsDisconnected) y después crear una propiedad DerivedNotifyProperty (IsEnabled) que solo será verdadera si las otras dos son falsas.

Si se decidiese por usar IValueConverter y enlazar el estado Enabled de la interfaz de usuario directamente a IsBusy (con un convertidor para invertirlo), tendría bastante trabajo que debería realizar. Si en su lugar expusiera una propiedad derivada independiente, IsEnabled, agregar esta pequeña lógica implicaría mucho menos trabajo y el propio enlace a IsEnabled no tendría que cambiar. Es una buena señal de que está haciendo las cosas bien.

Resumen

El diseño de este marco ha sido toda una aventura, pero la recompensa es que ahora puedo implementar notificaciones de cambio de propiedades sin el repetitivo código reutilizable, sin cadenas mágicas y con compatibilidad con la refactorización. Mis modelos de vista no requieren lógica de una clase base concreta. Puedo crear propiedades derivadas y además generar las notificaciones de cambios adecuadas sin mucho esfuerzo adicional. Por último, el código que veo es el código que se está ejecutando. Y consigo todo eso desarrollando un marco bastante sencillo con un diseño orientado a objetos. Espero que le resulte útil para sus propios proyectos.


Mark Sowulha sido un desarrollador de .NET comprometido desde el principio y comparte sus amplios conocimientos sobre arquitectura y rendimiento en Microsoft .NET Framework y SQL Server a través de su empresa de consultoría SolSoft Solutions, con sede en Nueva York. Puede ponerse en contacto con él en mark@solsoftsolutions.com. Si sus ideas le parecen interesantes y quiere suscribirse a su boletín, puede hacerlo en eepurl.com/_K7YD.

Gracias a los siguientes expertos técnicos por revisar este artículo: Francis Cheung (Microsoft) y Charles Malm (Zebra Technologies)
Francis Cheung es desarrollador jefe de Microsoft Patterns & Practices Group. Francis ha estado involucrado en una amplia gama de proyectos, incluido Prism. Actualmente está centrado en las guías relacionadas con Azure.

Charles Malm es un ingeniero de software de juegos, .NET y web, y es cofundador de RealmSource, LLC.