ObservableObject

El ObservableObject es una clase base para los objetos que son observables mediante la implementación de las interfaces INotifyPropertyChanged y INotifyPropertyChanging. Se puede usar como punto de partida para todos los tipos de objetos que necesitan admitir notificaciones de cambio de propiedad.

API de la plataforma:ObservableObject, TaskNotifier, TaskNotifier<T>

Funcionamiento

ObservableObject tiene las siguientes características principales:

  • Proporciona una implementación base para INotifyPropertyChanged y INotifyPropertyChanging, que expone los eventos PropertyChanged y PropertyChanging.
  • Proporciona una serie de métodos de SetProperty que se pueden usar para establecer fácilmente valores de propiedad de los tipos que heredan de ObservableObject, y para generar automáticamente los eventos adecuados.
  • Proporciona el método SetPropertyAndNotifyOnCompletion, que es análogo a SetProperty pero con la capacidad de establecer propiedades Task y generar automáticamente los eventos de notificación cuando se completan las tareas asignadas.
  • Expone los métodos OnPropertyChanged y OnPropertyChanging, que se pueden invalidar en tipos derivados para personalizar cómo se generan los eventos de notificación.

Propiedad simple

Este es un ejemplo de cómo implementar la compatibilidad con notificaciones en una propiedad personalizada:

public class User : ObservableObject
{
    private string name;

    public string Name
    {
        get => name;
        set => SetProperty(ref name, value);
    }
}

El método SetProperty<T>(ref T, T, string) proporcionado comprueba el valor actual de la propiedad y lo actualiza si es diferente y, a continuación, también genera automáticamente los eventos pertinentes. El nombre de la propiedad se captura automáticamente mediante el uso del atributo [CallerMemberName], por lo que no es necesario especificar manualmente qué propiedad se está actualizando.

Ajuste de un modelo no observable

Un escenario común, por ejemplo, cuando se trabaja con elementos de base de datos, consiste en crear un modelo "enlazable" de ajuste que retransmite las propiedades del modelo de base de datos y genera las notificaciones modificadas de la propiedad cuando es necesario. Esto también es necesario cuando se desea insertar compatibilidad con notificaciones en los modelos, que no implementan la interfazINotifyPropertyChanged. ObservableObject proporciona un método dedicado para simplificar este proceso. En el ejemplo siguiente, User es un modelo que asigna directamente una tabla de base de datos, sin heredar de ObservableObject:

public class ObservableUser : ObservableObject
{
    private readonly User user;

    public ObservableUser(User user) => this.user = user;

    public string Name
    {
        get => user.Name;
        set => SetProperty(user.Name, value, user, (u, n) => u.Name = n);
    }
}

En este caso, se usa la sobrecarga SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string). La firma es ligeramente más compleja que la anterior: esto es necesario para permitir que el código siga siendo extremadamente eficaz aunque no tengamos acceso a un campo de respaldo como en el escenario anterior. Podemos pasar por cada parte de esta firma de método en detalle para comprender el rol de los distintos componentes:

  • TModel es un argumento de tipo, que indica el tipo del modelo que estamos ajustando. En este caso, será nuestra clase User. Tenga en cuenta que no es necesario especificar esto explícitamente: el compilador de C# lo deducirá automáticamente mediante la invocación del método SetProperty.
  • T es el tipo de la propiedad que queremos establecer. De forma similar a TModel, esto se deduce automáticamente.
  • T oldValue es el primer parámetro y, en este caso, usamos user.Name para pasar el valor actual de esa propiedad que estamos encapsulando.
  • T newValue es el nuevo valor que se va a establecer en la propiedad y aquí se pasa value, que es el valor de entrada dentro del establecedor de propiedades.
  • TModel model es el modelo de destino que estamos encapsulando, en este caso se pasa la instancia almacenada en el campo user.
  • Action<TModel, T> callback es una función que se invocará si el nuevo valor de la propiedad es diferente al actual y la propiedad debe establecerse. Esto lo hará esta función de devolución de llamada, que recibe como entrada el modelo de destino y el nuevo valor de propiedad que se va a establecer. En este caso, simplemente asignamos el valor de entrada (que llamamos n) a laName propiedad (haciendo u.Name = n). Aquí es importante evitar capturar valores del ámbito actual e interactuar solo con los proporcionados como entrada a la devolución de llamada, ya que esto permite al compilador de C# almacenar en caché la función de devolución de llamada y realizar una serie de mejoras de rendimiento. Esto se debe a que no solo estamos accediendo directamente al campo user aquí o al parámetro value en el establecedor, sino que solo usamos los parámetros de entrada para la expresión lambda.

El método SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string) hace que la creación de estas propiedades de ajuste sea extremadamente sencilla, ya que se encarga de recuperar y establecer las propiedades de destino al tiempo que proporciona una API extremadamente compacta.

Nota:

En comparación con la implementación de este método mediante expresiones LINQ, específicamente a través de un parámetro de tipo Expression<Func<T>> en lugar de los parámetros de estado y devolución de llamada, las mejoras de rendimiento que se pueden lograr de esta manera son realmente significativas. En concreto, esta versión es ~200x más rápida que la que usa expresiones LINQ y no realiza asignaciones de memoria en absoluto.

Control de propiedades Task<T>

Si una propiedad es un Task también es necesario generar el evento de notificación una vez completada la tarea, de modo que los enlaces se actualicen en el momento adecuado. Ejemplo, para mostrar un indicador de carga u otra información de estado en la operación representada por la tarea. ObservableObject tiene una API para este escenario:

public class MyModel : ObservableObject
{
    private TaskNotifier<int>? requestTask;

    public Task<int>? RequestTask
    {
        get => requestTask;
        set => SetPropertyAndNotifyOnCompletion(ref requestTask, value);
    }

    public void RequestValue()
    {
        RequestTask = WebService.LoadMyValueAsync();
    }
}

Aquí el método SetPropertyAndNotifyOnCompletion<T>(ref TaskNotifier<T>, Task<T>, string) se encargará de actualizar el campo de destino, supervisar la nueva tarea, si está presente y generar el evento de notificación cuando se complete esa tarea. De este modo, es posible enlazar solo a una propiedad de tarea y recibir una notificación cuando cambie su estado. El TaskNotifier<T> es un tipo especial expuesto por ObservableObject que encapsula una instancia Task<T> de destino y habilita la lógica de notificación necesaria para este método. El tipo TaskNotifier también está disponible para usarlo directamente si solo tiene una Task general.

Nota:

El método SetPropertyAndNotifyOnCompletion está diseñado para reemplazar el uso del tipo NotifyTaskCompletion<T> del paquete de Microsoft.Toolkit. Si se usaba este tipo, se puede reemplazar solo por la propiedad Task interna (o Task<TResult>) y, a continuación, se puede usar el método SetPropertyAndNotifyOnCompletion para establecer su valor y generar cambios de notificación. Todas las propiedades expuestas por el tipo de NotifyTaskCompletion<T> están disponibles directamente en Task instancias.

Ejemplos

  • Consulte la aplicación de ejemplo (para varios marcos de interfaz de usuario) para ver el kit de herramientas de MVVM en acción.
  • También puede encontrar más ejemplos en las pruebas unitarias.