Transacciones en MVVM

Traducción aproximada del artículo Transactions with MVVM publicado en inglés por Jeremy Likness el 22 de abril del 2010 en su blog C#er:Image.

 

Una objeción que escucho a menudo sobre MVVM es que no trabaja bien con transacciones. En este caso no me refiero a transacciones en bases de datos u operaciones atómicas, sino más bien al tipo de transacciones breves que a veces ocurren al nivel de interfase gráfica. Gracias a enlaces de datos, las actualizaciones son instantáneas, y si el nuevo dato pasa la validación, el campo subyacente es también actualizado.

No obstante, hay muchas aplicaciones que no funcionan de esa manera. Puede que los campos tengan editores y sean validados, pero muchas veces la lógica de la aplicación depende de varios otros datos en la pantalla. Si la validación compuesta falla, lo deseable es volver al estado inicial, deshaciendo los cambios. La pregunta es, ¿cómo podemos hacer esto en MVVM?

De nuevo, el problema no es con MVVM en sí, es con su implementación. Una práctica común es guardar una copia del contenido original y restaurarla según sea necesario. Aunque es buena idea, la cosa se puede poner difícil con grandes conjuntos de datos. ¿Qué hacer entonces?

Primero construyamos algunas de clases base simples que son comunes en la mayoría de soluciones de MVM. Para trabajar con  INotifyPropertyChanged vamos a tomar prestado el elegante código que Rob Eisenberg presentó en MIX 2010 en su charla Build your own MVVM Framework. También voy a insertar ciertos indicadores de datos alterados (la propiedad IsDirty será cierta si los datos han sido modificados).

 

public abstract class BaseModel : INotifyPropertyChanged
{
  public const string IS_DIRTY = "IsDirty";
  public const string IGNORE_DIRTY = "IgnoreDirty"; 

  private bool _isDirty = false; 

  protected BaseModel()
  {
    PropertyChanged += BaseModel_PropertyChanged;
  }

  void BaseModel_PropertyChanged(object sender,
         PropertyChangedEventArgs e)
  {
    if (!_isDirty && !e.PropertyName.Equals(IS_DIRTY))
    {
      IsDirty = true;
    }
  }

  public bool IsDirty
  {
    get { return _isDirty; }
    set
    {
      if (!_isDirty.Equals(value))
      {
        _isDirty = value;
        RaisePropertyChanged(() => IsDirty);
      }
    }
  }

  public void NotifyOfPropertyChange(string propertyName)
  {
    var handler = PropertyChanged;

    if (handler != null)
    {
      handler(this, new PropertyChangedEventArgs(propertyName)));
    }
  }

  public void RaisePropertyChanged<TProperty>(
                Expression<Func<TProperty>> property)
  {
    if (PropertyChanged == null)
    {
      return;
    }

    var lambda = (LambdaExpression)property;

    MemberExpression memberExpression;

    if (lambda.Body is UnaryExpression)
    {
      var unaryExpression = (UnaryExpression)lambda.Body;
      memberExpression =
        (MemberExpression)unaryExpression.Operand;
    }
    else memberExpression = (MemberExpression)lambda.Body;

    NotifyOfPropertyChange(memberExpression.Member.Name);
  }

  public event PropertyChangedEventHandler PropertyChanged;
}

 

Cualquier clase derivada de BaseModel tendrá eventos de tipo verificado por el compilador para cambios en propiedades. Lo que es más, cualquier propiedad que sea modificada activará el indicador de datos alterados.

Algo que quisiéramos evitar es tener hacer copias completas de todos datos cuando hay que guardarlos, ¿no es cierto? Estoy de acuerdo. En vez de eso vamos a usar un método de extensión que nos permita copiar clases que deriven de BaseModel. Si suponemos que sólo contiene tipos básicos, objetos simples, u otros objetos derivados de BaseModel, entonces podemos hacer la copia de modo recursivo:

 

public static class ModelExtensions
{
  public static T Copy<T>(this T source) where T : BaseModel
  {
    var clone = Activator.CreateInstance(source.GetType())
                as BaseModel;
    return (T)source.Copy(clone);
  }

  public static T Copy<T>(this T source, T clone)
                    where T : BaseModel
  {
    const string ITEM = "Item";
    const string COUNT = "Count";
    const string ADD = "Add";

    foreach (PropertyInfo curPropInfo in
             source.GetType().GetProperties())
    {
      if (curPropInfo.GetGetMethod() != null
          && (curPropInfo.GetSetMethod() != null))
      {
        if (!curPropInfo.Name.Equals(ITEM))
        {
          object getValue = curPropInfo.GetGetMethod().
                            Invoke(source, new object[] { });

          if (getValue != null && getValue is BaseModel)
          {
            var baseModel = getValue as BaseModel;
            getValue = baseModel.Copy();
          }

          curPropInfo.GetSetMethod().
            Invoke(clone, new[] { getValue });
        }
        else
        {
          var numberofItemsInCollection =
              (int) curPropInfo.ReflectedType.GetProperty(COUNT).
              GetGetMethod().Invoke(source, new object[] { });

          for (int i = 0; i < numberofItemsInCollection; i++)
          {
            object getValue = curPropInfo.GetGetMethod().
                              Invoke(source, new object[] { i });

            if (getValue != null && getValue is BaseModel)
              getValue = ((BaseModel)getValue).Copy();

            curPropInfo.ReflectedType.GetMethod(ADD).
              Invoke(clone, new[] { getValue });
          }
        }
      }
    }        

    return (T)clone;
  }
}

 

Dependiendo de cómo se maneje el modelo, puede que sea mejor desactivar el indicador de datos alterados luego de la copia.

Bien, pasemos ahora a las transacciones. Primero creamos una sencilla clase que sirva para envolver cualquier modelo en la transacción. Lo que hacemos es enlazarlo a la propiedad Value y luego, mediante comandos o mecanismos similares, invocamos el método Commit para aplicar los cambios o Rollback para revertirlos según sea necesario.

 

public class TransactionModel<T> where T: BaseModel
{
  private T _src;
  private T _editable;

  public TransactionModel(T src)
  {
    _editable = src;
    _src = _editable.Copy();
  }

  public T Value
  {
    get { return _editable; }
    set
    {
      _editable = value;
      _src = _editable.Copy();
    }
  }

  public void Commit()
  {
    _editable.Copy(_src);
  }

  public void Rollback()
  {
    _src.Copy(_editable);
  }

  public override bool Equals(object obj)
  {
    return obj is TransactionModel<T> &&
                  ((TransactionModel<T>)obj).Value.Equals(Value);
  }

  public override int GetHashCode()
  {
    return Value.GetHashCode();
  }
}

 

Observen cómo tomamos la propiedad inicial y la copiamos. Las operaciones de confirmar o revertir los cambios no crean nuevos objetos sino que transfieren las propiedades de un lugar a otro. Cuando un nuevo valor es asignado, generamos una copia automáticamente y así estamos listos para continuar con la transacción.

Con este escenario podemos simplemente envolver nuestro modelo en un TransactionMode y enlazarlo a la propiedad Value. Todas las otras operaciones pueden entonces hacer referencia al objeto original, que será actualizado dependiendo de si se desea confirmar o revertir los cambios.

Aunque el sistema es efectivo, ¿qué pasa si tenemos una lista extensa y queremos confirmar o revertir cambios en la lista entera? ¡No hay problema!

 

public class TransactionCollection<T> where T: BaseModel
{
  private List<TransactionModel<T>> _transactions; 

  public TransactionCollection(IEnumerable<T> source)
  {
    Collection = new ObservableCollection<T>(source);
    Collection.CollectionChanged += Collection_CollectionChanged;

    _transactions =
      new List<TransactionModel<T>>(Collection.Count);

    foreach(var t in source)
    {
      _transactions.Add(new TransactionModel<T>(t))
    }
  }

  void Collection_CollectionChanged(object sender,
         NotifyCollectionChangedEventArgs e)
  {
    if (e.OldItems != null)
    {
      foreach (var item in e.OldItems)
      {
        var transaction = (from t in _transactions
                          where t.Value.Equals(item)
                          select t).FirstOrDefault();
        if (transaction != null)
        {
          _transactions.Remove(transaction);
        }
      }
    }

    if (e.NewItems != null)
    {
      foreach (var item in e.NewItems)
      {
        _transactions.Add(new TransactionModel<T>((T)item));
      }
    }
  }

  public ObservableCollection<T> Collection { get; private set; }

  public void Commit()
  {
    foreach(var t in _transactions)
    {
     t.Commit();
    }
  }

  public void Rollback()
  {
    foreach(var t in _transactions)
    {
       t.Rollback();
    }
  }
}

 

Esta clase auxiliar nos permite enlazar la colección a una cuadrícula, lista, o cualquier otro tipo de control haciendo que sólo pueda “percibir” una colección de objetos. Aun así, la clase está al tanto de artículos que son añadidos o removidos de la lista y automáticamente los encierra en una transacción, de manera que se puede hacer que una acción de revertir o confirmar afecte la lista entera.

Como pueden ver, la habilidad de confirmar o revertir cambios no sólo es compatible con MVVM, sino que también es muy simple de implementar si se tiene una clase auxiliar apropiada.

 

Jeremy Likness

 

Etiquetas asignadas:
 

Responder



Licencia de uso

El contenido de las traducciones está sujeto a los términos de protección de derechos de uso de los autores originales quienes han autorizado su publicación en este blog. Asegúrese de entender los terminos de la licencia de cada autor antes de usar tal contenido.

Mis propios artículos son publicados bajo los términos de la Licencia Reconocimiento-Compartir bajo la misma licencia 3.0 Estados Unidos de Creative Commons:

Creative Commons License
Blog de Maromas Digitales by Maromas Digitales, LLC is licensed under a Creative Commons Reconocimiento-Compartir bajo la misma licencia 3.0 Estados Unidos License.

License

The contents of all translated articles is subject to the copyright and licensing terms of the original authors and has been published here with their express permission. Verify the original author's licensing terms before using the contents of these articles.

My own articles are licensed under the Creative Commons Attribution-Share Alike 3.0 United States License:

Creative Commons License
Blog de Maromas Digitales by Maromas Digitales, LLC is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.