2011-03-23 15 views
10

Estoy escribiendo una aplicación WPF, utilizando un diseño MVVM con Entity Framework 4 como ORM. Tengo propiedades de recopilación en mi modelo de vista que contendrán colecciones de entidades devueltas de EF4 como colecciones IEnumerable<T> en respuesta a consultas enviadas desde la capa empresarial.Entity Framework 4 y WPF

Esperaba simplemente envolver el conjunto de resultados IEnumerable<T> en un ObservableCollection<T>. Sin embargo, me encontré escribiendo código de seguimiento de cambios en mi repositorio, o manteniendo colecciones ocultas de objetos cambiados, solo para mantener el modelo de vista y la capa de persistencia sincronizados. Cada vez que se agrega una entidad a la colección en el modelo de vista, tuve que ir a mi repositorio para agregarlo al conjunto de objetos EF4. Tenía que hacer el mismo tipo de cosas con las actualizaciones y eliminaciones.

Para simplificar las cosas, me pidió prestado una clase EdmObservableCollection<T> del marco de aplicaciones WPF proyecto en CodePlex (http://waf.codeplex.com/). La clase ajusta un ObservableCollection<T> con una referencia a un EF4 ObjectContext, de modo que el OC se puede actualizar a medida que se actualiza la colección. He reimpreso la clase EdmObservableCollection a continuación. La clase funciona bastante bien, pero tiene un poco de olor a código, porque termino con una referencia a EF4 en mi modelo de vista.

Aquí está mi pregunta: En una aplicación WPF, ¿cuál es la forma habitual de mantener una colección de entidades EF4 sincronizada con el contexto de su objeto? ¿Es EdmObservableCollection un enfoque adecuado, o hay una mejor manera? ¿Me estoy perdiendo algo fundamental al trabajar con EF4? Gracias por tu ayuda.


using System; 
using System.Collections.Generic; 
using System.Collections.ObjectModel; 
using System.Data.Objects; 
using System.Linq; 

namespace Ef4Sqlce4Demo.ViewModel.BaseClasses 
{ 
    /// <summary> 
    /// An ObservableCollection for Entity Framework 4 entity collections. 
    /// </summary> 
    /// <typeparam name="T">The type of EF4 entity served.</typeparam> 
    /// <remarks>Developed from WPF Application Framework (WAF) http://waf.codeplex.com/</remarks> 
    public class EdmObservableCollection<T> : ObservableCollection<T> 
    { 
      #region Fields 

      // Member variables 
      private readonly string m_EntitySetName; 
      private readonly ObjectContext m_ObjectContext; 

      #endregion 

      #region Constructors 

      /// <summary> 
      /// Creates a new EDM Observable Collection and populates it with a list of items. 
      /// </summary> 
      /// <param name="objectContext">The EF4 ObjectContext that will manage the collection.</param> 
      /// <param name="entitySetName">The name of the entity set in the EDM.</param> 
      /// <param name="items">The items to be inserted into the collection.</param> 
      public EdmObservableCollection(ObjectContext objectContext, string entitySetName, IEnumerable<T> items) 
       : base(items ?? new T[] {}) 
      { 
       if (objectContext == null) 
       { 
        throw new ArgumentNullException("objectContext"); 
       } 
       if (entitySetName == null) 
       { 
        throw new ArgumentNullException("entitySetName"); 
       } 

       m_ObjectContext = objectContext; 
       m_EntitySetName = entitySetName; 
      } 

      /// <summary> 
      /// Creates an empty EDM Observable Collection that has an ObjectContext. 
      /// </summary> 
      /// <param name="objectContext">The EF4 ObjectContext that will manage the collection.</param> 
      /// <param name="entitySetName">The name of the entity set in the EDM.</param> 
      public EdmObservableCollection(ObjectContext objectContext, string entitySetName) 
       : this(objectContext, entitySetName, null) 
      { 
      } 

      /// <summary> 
      /// Creates an empty EDM Observable Collection, with no ObjectContext. 
      /// </summary> 
      /// <remarks> 
      /// We use this constructor to create a placeholder collection before we have an 
      /// ObjectContext to work with. This state occurs when the program is first launched, 
      /// before a file is open. We need to initialize collections in the application's 
      /// ViewModels, so that the MainWindow can get Note and Tag counts, which are zero. 
      /// </remarks> 
      public EdmObservableCollection() 
      { 
      } 

      #endregion 

      #region Method Overrides 

      protected override void InsertItem(int index, T item) 
      { 
       base.InsertItem(index, item); 
       m_ObjectContext.AddObject(m_EntitySetName, item); 
      } 

      protected override void RemoveItem(int index) 
      { 
       T itemToDelete = this[index]; 
       base.RemoveItem(index); 
       m_ObjectContext.DeleteObject(itemToDelete); 
      } 

      protected override void ClearItems() 
      { 
       T[] itemsToDelete = this.ToArray(); 
       base.ClearItems(); 

       foreach (T item in itemsToDelete) 
       { 
        m_ObjectContext.DeleteObject(item); 
       } 
      } 

      protected override void SetItem(int index, T item) 
      { 
       T itemToReplace = this[index]; 
       base.SetItem(index, item); 

       m_ObjectContext.DeleteObject(itemToReplace); 
       m_ObjectContext.AddObject(m_EntitySetName, item); 
      } 

      #endregion 

      #region Public Methods 

      /// <summary> 
      /// Adds an object to the end of the collection. 
      /// </summary> 
      /// <param name="item">The object to be added to the end of the collection.</param> 
      public new void Add(T item) 
      { 
       InsertItem(Count, item); 
      } 

      /// <summary> 
      /// Removes all elements from the collection. 
      /// </summary> 
      /// <param name="clearFromContext">Whether the items should also be deleted from the ObjectContext.</param> 
      public void Clear(bool clearFromContext) 
      { 
       if (clearFromContext) 
       { 
        foreach (T item in Items) 
        { 
         m_ObjectContext.DeleteObject(item); 
        } 
       } 

       base.Clear(); 
      } 

      /// <summary> 
      /// Inserts an element into the collection at the specified index. 
      /// </summary> 
      /// <param name="index">The zero-based index at which item should be inserted.</param> 
      /// <param name="item">The object to insert.</param> 
      public new void Insert(int index, T item) 
      { 
       base.Insert(index, item); 
       m_ObjectContext.AddObject(m_EntitySetName, item); 
      } 

      /// <summary> 
      /// Updates the ObjectContext for changes to the collection. 
      /// </summary> 
      public void Refresh() 
      { 
       m_ObjectContext.SaveChanges(); 
      } 

      /// <summary> 
      /// Removes the first occurrence of a specific object from the collection. 
      /// </summary> 
      /// <param name="item">The object to remove from the collection.</param> 
      public new void Remove(T item) 
      { 
       base.Remove(item); 
       m_ObjectContext.DeleteObject(item); 
      } 

      #endregion 
    } 
} 

Respuesta

5

Creo que he trabajado a cabo la respuesta. El problema no está en la colección, sino en lo que se está pasando a la colección. La colección no debería estar trabajando directamente con ObjectContext; en su lugar, debería funcionar con el Repositorio para el tipo de entidad que recopila. Por lo tanto, una clase Repository se debe pasar al constructor de la colección, y todo el código de persistencia en la colección se debe reemplazar por simples llamadas a los métodos de Repository.La clase de colección revisada aparece a continuación:


EDIT: Slauma preguntó acerca de validación de datos (véase su respuesta), por lo que he añadido un evento CollectionChanging a la clase de colección que Iniciado en mi respuesta. Gracias, Slauma, por la captura! El código del cliente debe suscribirse al evento y usarlo para realizar la validación. Establezca la propiedad EventArgs.Cancel en true para cancelar un cambio.

El Class Collection

using System; 
using System.Collections.Generic; 
using System.Collections.ObjectModel; 
using System.Linq; 
using Ef4Sqlce4Demo.Persistence.Interfaces; 
using Ef4Sqlce4Demo.ViewModel.Utility; 

namespace Ef4Sqlce4Demo.ViewModel.BaseClasses 
{ 
    /// <summary> 
    /// An ObservableCollection for Entity Framework 4 entity collections. 
    /// </summary> 
    /// <typeparam name="T">The type of EF4 entity served.</typeparam> 
    public class FsObservableCollection<T> : ObservableCollection<T> where T:class 
    { 
     #region Fields 

     // Member variables 
     private readonly IRepository<T> m_Repository; 

     #endregion 

     #region Constructors 

     /// <summary> 
     /// Creates a new FS Observable Collection and populates it with a list of items. 
     /// </summary> 
     /// <param name="items">The items to be inserted into the collection.</param> 
     /// <param name="repository">The Repository for type T.</param> 
     public FsObservableCollection(IEnumerable<T> items, IRepository<T> repository) : base(items ?? new T[] {}) 
     { 
      /* The base class constructor call above uses the null-coalescing operator (the 
      * double-question mark) which specifies a default value if the value passed in 
      * is null. The base class constructor call passes a new empty array of type t, 
      * which has the same effect as calling the constructor with no parameters-- 
      * a new, empty collection is created. */ 

      if (repository == null) throw new ArgumentNullException("repository"); 
      m_Repository = repository; 
     } 

     /// <summary> 
     /// Creates an empty FS Observable Collection, with a repository. 
     /// </summary> 
     /// <param name="repository">The Repository for type T.</param> 
     public FsObservableCollection(IRepository<T> repository) : base() 
     { 
      m_Repository = repository; 
     } 

     #endregion 

     #region Events 

     /// <summary> 
     /// Occurs before the collection changes, providing the opportunity to cancel the change. 
     /// </summary> 
     public event CollectionChangingEventHandler<T> CollectionChanging; 

     #endregion 

     #region Protected Method Overrides 

     /// <summary> 
     /// Inserts an element into the Collection at the specified index. 
     /// </summary> 
     /// <param name="index">The zero-based index at which item should be inserted.</param> 
     /// <param name="item">The object to insert.</param> 
     protected override void InsertItem(int index, T item) 
     { 
      // Raise CollectionChanging event; exit if change cancelled 
      var newItems = new List<T>(new[] {item}); 
      var cancelled = this.RaiseCollectionChangingEvent(NotifyCollectionChangingAction.Add, null, newItems); 
      if (cancelled) return; 

      // Insert new item 
      base.InsertItem(index, item); 
      m_Repository.Add(item); 
     } 

     /// <summary> 
     /// Removes the item at the specified index of the collection. 
     /// </summary> 
     /// <param name="index">The zero-based index of the element to remove.</param> 
     protected override void RemoveItem(int index) 
     { 
      // Initialize 
      var itemToRemove = this[index]; 

      // Raise CollectionChanging event; exit if change cancelled 
      var oldItems = new List<T>(new[] { itemToRemove }); 
      var cancelled = this.RaiseCollectionChangingEvent(NotifyCollectionChangingAction.Remove, oldItems, null); 
      if (cancelled) return; 

      // Remove new item 
      base.RemoveItem(index); 
      m_Repository.Delete(itemToRemove); 
     } 

     /// <summary> 
     /// Removes all items from the collection. 
     /// </summary> 
     protected override void ClearItems() 
     { 
      // Initialize 
      var itemsToDelete = this.ToArray(); 

      // Raise CollectionChanging event; exit if change cancelled 
      var oldItems = new List<T>(itemsToDelete); 
      var cancelled = this.RaiseCollectionChangingEvent(NotifyCollectionChangingAction.Remove, oldItems, null); 
      if (cancelled) return; 

      // Removes all items from the collection. 
      base.ClearItems(); 
      foreach (var item in itemsToDelete) 
      { 
       m_Repository.Delete(item); 
      } 
     } 

     /// <summary> 
     /// Replaces the element at the specified index. 
     /// </summary> 
     /// <param name="index">The zero-based index of the element to replace.</param> 
     /// <param name="newItem">The new value for the element at the specified index.</param> 
     protected override void SetItem(int index, T newItem) 
     { 
      // Initialize 
      var itemToReplace = this[index]; 

      // Raise CollectionChanging event; exit if change cancelled 
      var oldItems = new List<T>(new[] { itemToReplace }); 
      var newItems = new List<T>(new[] { newItem }); 
      var cancelled = this.RaiseCollectionChangingEvent(NotifyCollectionChangingAction.Replace, oldItems, newItems); 
      if (cancelled) return; 

      // Rereplace item 
      base.SetItem(index, newItem); 

      m_Repository.Delete(itemToReplace); 
      m_Repository.Add(newItem); 
     } 

     #endregion 

     #region Public Method Overrides 

     /// <summary> 
     /// Adds an object to the end of the collection. 
     /// </summary> 
     /// <param name="item">The object to be added to the end of the collection.</param> 
     public new void Add(T item) 
     { 
      // Raise CollectionChanging event; exit if change cancelled 
      var newItems = new List<T>(new[] { item }); 
      var cancelled = this.RaiseCollectionChangingEvent(NotifyCollectionChangingAction.Add, null, newItems); 
      if (cancelled) return; 

      // Add new item 
      base.Add(item); 
      m_Repository.Add(item); 
     } 

     /// <summary> 
     /// Removes all elements from the collection and from the data store. 
     /// </summary> 
     public new void Clear() 
     { 
      /* We call the overload of this method with the 'clearFromDataStore' 
      * parameter, hard-coding its value as true. */ 

      // Call overload with parameter 
      this.Clear(true); 
     } 

     /// <summary> 
     /// Removes all elements from the collection. 
     /// </summary> 
     /// <param name="clearFromDataStore">Whether the items should also be deleted from the data store.</param> 
     public void Clear(bool clearFromDataStore) 
     { 
      // Initialize 
      var itemsToDelete = this.ToArray(); 

      // Raise CollectionChanging event; exit if change cancelled 
      var oldItems = new List<T>(itemsToDelete); 
      var cancelled = this.RaiseCollectionChangingEvent(NotifyCollectionChangingAction.Remove, oldItems, null); 
      if (cancelled) return; 

      // Remove all items from the collection. 
      base.Clear(); 

      // Exit if not removing from data store 
      if (!clearFromDataStore) return; 

      // Remove all items from the data store 
      foreach (var item in itemsToDelete) 
      { 
       m_Repository.Delete(item); 
      } 
     } 

     /// <summary> 
     /// Inserts an element into the collection at the specified index. 
     /// </summary> 
     /// <param name="index">The zero-based index at which item should be inserted.</param> 
     /// <param name="item">The object to insert.</param> 
     public new void Insert(int index, T item) 
     { 
      // Raise CollectionChanging event; exit if change cancelled 
      var newItems = new List<T>(new[] { item }); 
      var cancelled = this.RaiseCollectionChangingEvent(NotifyCollectionChangingAction.Add, null, newItems); 
      if (cancelled) return; 

      // Insert new item 
      base.Insert(index, item); 
      m_Repository.Add(item); 
     } 

     /// <summary> 
     /// Persists changes to the collection to the data store. 
     /// </summary> 
     public void PersistToDataStore() 
     { 
      m_Repository.SaveChanges(); 
     } 

     /// <summary> 
     /// Removes the first occurrence of a specific object from the collection. 
     /// </summary> 
     /// <param name="itemToRemove">The object to remove from the collection.</param> 
     public new void Remove(T itemToRemove) 
     { 
      // Raise CollectionChanging event; exit if change cancelled 
      var oldItems = new List<T>(new[] { itemToRemove }); 
      var cancelled = this.RaiseCollectionChangingEvent(NotifyCollectionChangingAction.Remove, oldItems, null); 
      if (cancelled) return; 

      // Remove target item 
      base.Remove(itemToRemove); 
      m_Repository.Delete(itemToRemove); 
     } 

     #endregion 

     #region Private Methods 

     /// <summary> 
     /// Raises the CollectionChanging event. 
     /// </summary> 
     /// <returns>True if a subscriber cancelled the change, false otherwise.</returns> 
     private bool RaiseCollectionChangingEvent(NotifyCollectionChangingAction action, IList<T> oldItems, IList<T> newItems) 
     { 
      // Exit if no subscribers 
      if (CollectionChanging == null) return false; 

      // Create event args 
      var e = new NotifyCollectionChangingEventArgs<T>(action, oldItems, newItems); 

      // Raise event 
      this.CollectionChanging(this, e); 

      /* Subscribers can set the Cancel property on the event args; the 
      * event args will reflect that change after the event is raised. */ 

      // Set return value 
      return e.Cancel; 
     } 

     #endregion 
    } 
} 

El evento args Clase

using System; 
using System.Collections.Generic; 

namespace Ef4Sqlce4Demo.ViewModel.Utility 
{ 

    #region Enums 

    /// <summary> 
    /// Describes the action that caused a CollectionChanging event. 
    /// </summary> 
    public enum NotifyCollectionChangingAction { Add, Remove, Replace, Move, Reset } 

    #endregion 

    #region Delegates 

    /// <summary> 
    /// Occurs before an item is added, removed, changed, moved, or the entire list is refreshed. 
    /// </summary> 
    /// <typeparam name="T">The type of elements in the collection.</typeparam> 
    /// <param name="sender">The object that raised the event.</param> 
    /// <param name="e">Information about the event.</param> 
    public delegate void CollectionChangingEventHandler<T>(object sender, NotifyCollectionChangingEventArgs<T> e); 

    #endregion 

    #region Event Args 

    public class NotifyCollectionChangingEventArgs<T> : EventArgs 
    { 
     #region Constructors 

     /// <summary> 
     /// Constructor with all arguments. 
     /// </summary> 
     /// <param name="action">The action that caused the event. </param> 
     /// <param name="oldItems">The list of items affected by a Replace, Remove, or Move action.</param> 
     /// <param name="newItems">The list of new items involved in the change.</param> 
     /// <param name="oldStartingIndex">The index at which a Move, Remove, or Replace action is occurring.</param> 
     /// <param name="newStartingIndex">The index at which the change is occurring.</param> 
     public NotifyCollectionChangingEventArgs(NotifyCollectionChangingAction action, IList<T> oldItems, IList<T> newItems, int oldStartingIndex, int newStartingIndex) 
     { 
      this.Action = action; 
      this.OldItems = oldItems; 
      this.NewItems = newItems; 
      this.OldStartingIndex = oldStartingIndex; 
      this.NewStartingIndex = newStartingIndex; 
      this.Cancel = false; 
     } 

     /// <summary> 
     /// Constructor that omits 'starting index' arguments. 
     /// </summary> 
     /// <param name="action">The action that caused the event. </param> 
     /// <param name="oldItems">The list of items affected by a Replace, Remove, or Move action.</param> 
     /// <param name="newItems">The list of new items involved in the change.</param> 
     public NotifyCollectionChangingEventArgs(NotifyCollectionChangingAction action, IList<T> oldItems, IList<T> newItems) 
     { 
      this.Action = action; 
      this.OldItems = oldItems; 
      this.NewItems = newItems; 
      this.OldStartingIndex = -1; 
      this.NewStartingIndex = -1; 
      this.Cancel = false; 
     } 

     #endregion 

     #region Properties 

     /// <summary> 
     /// Gets the action that caused the event. 
     /// </summary> 
     public NotifyCollectionChangingAction Action { get; private set; } 

     /// <summary> 
     /// Whether to cancel the pending change. 
     /// </summary> 
     /// <remarks>This property is set by an event subscriber. It enables 
     /// the subscriber to cancel the pending change.</remarks> 
     public bool Cancel { get; set; } 

     /// <summary> 
     /// Gets the list of new items involved in the change. 
     /// </summary> 
     public IList<T> NewItems { get; private set; } 

     /// <summary> 
     /// Gets the index at which the change is occurring. 
     /// </summary> 
     public int NewStartingIndex { get; set; } 

     /// <summary> 
     /// Gets the list of items affected by a Replace, Remove, or Move action. 
     /// </summary> 
     public IList<T> OldItems { get; private set; } 

     /// <summary> 
     /// Gets the index at which a Move, Remove, or Replace action is occurring. 
     /// </summary> 
     public int OldStartingIndex { get; set; } 

     #endregion 

    } 

    #endregion 
} 
0

probablemente me gustaría utilizar un factory pattern para lograr un nivel de abstracción en su modelo de vista si lo desea.

Lo malo es que tendrá que limitarse a llamar a la fábrica cada vez que cree una de sus colecciones.

así que si su fábrica tenía una API como esta (que usted podría cambiar con lo que quería):

public static class ObjectBuilder 
{ 
    static Factory; 
    SetFactory(IFactory factory) { Factory = factory; }; 
    T CreateObject<T>() { return factory.Create<T>();}; 
    TCollection<T> CreateObject<TCollection,T>>() 
    { 
    return Factory.Create<TCollection,T>(); 
    }  
    TCollection<T> CreateObject<TCollection,T>>(TCollection<T> items) 
    { 
    return Factory.Create<TCollection,T>(TCollection<T> items); 
    } 
} 

por lo que ahora usted:

  • acaba de poner en práctica IFactory para devolver su EdmObservableCollection siempre TCollection is ObservableCollection
  • aplicación cada vez que se inicializa llamar ObjectBuilder.SetFactory()
  • y ahora en sus ViewModels siempre que lo desee esto le llaman simplemente ObjectBuilder.Create<ObservableCollection,MyEntity>();

también si/cuando lo que se necesita para cambiar su base de ORM sólo tiene que implementar un nuevo IFactory y llamar ObjectBuilder.SetFactory(factory)

1

voy a tirar en algunos pensamientos, pero sin tener una respuesta final.

La pregunta básica es en mi opinión: ¿Las operaciones que un usuario puede hacer en una interfaz de usuario siempre de una manera única relacionada con las operaciones de la base de datos? O más específico: si un usuario puede eliminar un elemento de una lista en la interfaz de usuario o insertar un nuevo elemento en una lista, ¿significa necesariamente que se debe eliminar o insertar un registro en la base de datos?

creo, la respuesta es: No.

Al principio puedo ver un buen caso de uso para trabajar con el EdmObservableCollection<T>. Esa es, por ejemplo, una vista en la interfaz de usuario de WPF con solo un DataGrid que está vinculado a una colección de clientes. Se buscará una lista de clientes mediante una especificación de consulta. Ahora el usuario puede editar en este DataGrid: puede cambiar filas (clientes individuales), puede insertar una nueva fila y puede eliminar una fila. DataGrid admite estas operaciones con bastante facilidad y el motor de enlace de datos escribe esas operaciones "CUD" directamente en la EdmObservableCollection encuadernada. En esta situación, se supone que eliminar una fila o insertar una nueva fila se refleja directamente en la base de datos, por lo que EdmObservableCollection podría ser bastante útil, ya que maneja las Inserciones y Eliminaciones en el ObjectContext internamente.

Pero incluso en esta situación simple hay algunos puntos a tener en cuenta:

  • Es probable que tenga que inyectar ObjectContext/repositorio en su modelo de vista de todos modos (para consultar los objetos que desea poner en la colección) - y debe ser el mismo contexto que se inyectó en EdmObservableCollection para manejar las actualizaciones de objetos (edición de una fila de cliente) correctamente. También debe trabajar con objetos/proxies de seguimiento de cambios si no desea realizar el seguimiento de cambios manual "tardío" antes de llamar a SaveChanges.

  • Este tipo una operación de eliminación "genérica" ​​que el EdmObservableCollection<T> proporciona no tiene en cuenta las limitaciones de la base de datos o de la empresa. ¿Qué sucede, por ejemplo, si un usuario intenta eliminar una fila para un cliente que está asignado a varios pedidos? Si hay una relación de clave externa en la base de datos, SaveChanges fallará y emitirá una excepción. Bueno, es posible que detecte esta excepción, la evalúe y le dé un mensaje al usuario. Pero tal vez ha hecho muchos otros cambios (editó muchas otras filas e insertó nuevos clientes), pero debido a esta restricción violada de FK, la transacción fracasó. OK, también esto podría manejarlo (eliminar este cliente eliminado de ObjectContext e intentar guardar los cambios nuevamente) o incluso darle al cliente una opción de qué hacer. Y hasta ahora, solo hemos considerado las restricciones de la base de datos.Puede haber reglas comerciales adicionales que no están reflejadas en el modelo de base de datos (un cliente no puede eliminarse antes de que no haya pagado todas las facturas, la eliminación debe ser aprobada por el jefe del departamento de ventas, el cliente no debe eliminarse antes de 6 meses después su último pedido, y así sucesivamente ...). Por lo tanto, puede haber mucho más en juego que un simple "ObjectContext.DeleteObject" para ejecutar la eliminación de una manera segura y fácil de usar.

Ahora vamos a considerar otro ejemplo: Imagine que hay una vista para asignar personas de contacto a una orden (bueno, inusual, probablemente, pero digamos que estos son pedidos grandes y complejos, muy individuales que incluyen una gran cantidad de servicios al cliente y cada pedido necesita diferentes personas de contacto en el sitio del cliente para diversos aspectos del pedido). Esta vista puede contener una pequeña vista de solo lectura del pedido, una lista de solo lectura de un grupo de personas de contacto que ya están en los datos maestros del cliente y luego una lista editable de personas de contacto que están asignadas al pedido. Ahora, como en el primer ejemplo, el usuario puede hacer cosas similares: puede eliminar a una persona de contacto de la lista y puede arrastrar y soltar a una persona de contacto de la lista maestra para insertarla en esa lista de personas de contacto. Si volviéramos a encuadernar esta lista a un EdmObservableCollection<T>, pasarían tonterías: se insertarían nuevas personas de contacto en la base de datos y las personas de contacto se borrarían de la base de datos. No queremos eso, en realidad solo queremos asignar o anular la asignación de referencias a registros existentes (los datos maestros de la persona de contacto del cliente) pero nunca eliminar o insertar registros.

Tenemos dos ejemplos de operaciones similares en la interfaz de usuario (las filas se eliminan e insertan en una lista) pero con reglas comerciales bastante diferentes detrás de ellas y también diferentes operaciones en el almacén de datos. Para WPF sucede lo mismo (que se puede manejar con un ObservableCollection en ambos casos), pero se deben hacer cosas diferentes en la capa de negocios y de la base de datos.

Me gustaría sacar algunas conclusiones de esta:

  • EdmObservableCollection<T> puede ser útil en situaciones especiales cuando se tiene que tratar con colecciones en la interfaz de usuario y usted no tiene que tener en cuenta las reglas de negocio difíciles o de bases de datos restricciones Pero en muchas situaciones no es aplicable. Por supuesto, podría crear colecciones derivadas para otras situaciones que sobrecarguen e implementen por ejemplo Remove(T item) de otra manera (por ejemplo, no elimine del ObjectContext sino que establezca una referencia a null o algo en su lugar). Pero esta estrategia movería las responsabilidades de los repositorios o una capa de servicio cada vez más a esos ObservableCollections especializados. Si su aplicación básicamente realiza operaciones similares a CRUD en vistas DataGrid/List, entonces EdmObservableCollection podría ser adecuado. Por cualquier otra cosa, lo dudo.

  • Como se describe, en mi opinión hay más argumentos contra el acoplamiento de operaciones de base de datos/repositorio con Insertar/Eliminar operaciones de ObservableCollections y por lo tanto contra el uso de una construcción como EdmObservableCollection. Creo que en muchos casos sus ViewModels necesitarán un repositorio o servicio inyectado para satisfacer las necesidades específicas de su empresa y la capa de la base de datos. Por ejemplo, para operaciones de borrado que podría haber un comando en el modelo de vista y en el controlador de comandos hacer algo como:

    private void DeleteCustomer(Customer customer) 
    { 
        Validator validator = customerService.Delete(customer); 
        // customerService.Delete checks business rules, has access to repository 
        // and checks also FK constraints before trying to delete 
        if (validator.IsValid) 
         observableCustomerCollection.RemoveItem(customer); 
        else 
         messageService.ShowMessage(
          "Dear User, you can't delete this customer because: " 
          + validator.ReasonOfFailedValidation); 
    } 
    

    cosas complejas como éste no pertenece a un ObservableCollection derivada en mi opinión.

  • Generalmente tiendo a mantener las unidades de trabajo lo más pequeñas posible, no por razones técnicas sino de usabilidad. Si un usuario hace muchas cosas en una vista (edita algo, borra algo, inserta, etc.) y hace clic en el botón "Guardar" tarde después de mucho trabajo, también pueden salir muchas cosas, puede que consiga una larga lista de errores de validación y verse obligado a corregir muchas cosas.Por supuesto, la validación básica debería haberse hecho en la interfaz de usuario antes de que pueda presionar el botón "Guardar", pero la validación más compleja ocurrirá más adelante en la capa de negocios. Por ejemplo, si elimina una fila, la borro a través del servicio de inmediato (después del cuadro de mensaje de confirmación) como en el ejemplo anterior. Lo mismo para Inserts. Las actualizaciones pueden ser más complicadas (especialmente cuando están involucradas muchas propiedades de navegación en una entidad) ya que no trabajo con proxies de seguimiento de cambios. (No estoy seguro de si debería hacerlo mejor)

  • No tengo muchas esperanzas de hacer que las cosas parezcan iguales. Para separar las preocupaciones, tiene sentido tener un CustomerService.Delete y un OrderContactPersonsService.Delete que a los ViewModels no les importa lo que ocurra detrás. Pero en algún lugar (capa de negocios, repositorio, ...) esas operaciones serán diferentes y el trabajo arduo tiene que hacerse. EdmObservableCollection con un IRepository intrínseco es genérico en toda la cadena desde la capa de presentación hasta la base de datos e intenta ocultar estas diferencias, lo que no es realista en ninguna otra que no sea las aplicaciones CRUD más simples.

  • Tener un ObjectContext/DbContext frente a un IRepository en el EdmObservableCollection es en mi opinión el menor problema. El contexto EF o los ObjectSets/DbSets son casi de UnitOfWork/Repositories de todos modos y es cuestionable si no necesita cambiar los contratos de interfaz cuando alguna vez deba cambiar la tecnología de acceso a la base de datos. Personalmente tengo cosas como "Adjuntar" o "LoadNavigationCollection" en mi repositorio genérico y no está claro para mí si estos métodos con sus parámetros tendrían algún sentido con otra capa de persistencia. Pero hacer el repositorio aún más abstracto (con la esperanza de tener un verdadero Add-Update-Delete-Super-Persistance-Ignorant-Interface-Marvel<T>) lo movería más hacia la inutilidad. Resumir EF en un IRepository no resuelve las preocupaciones que he descrito.

Última nota como descargo de responsabilidad: Lea mis palabras con escepticismo. No soy un desarrollador experimentado de WPF/EF, solo estoy trabajando en mi primera aplicación algo más grande (desde hace aproximadamente 2 meses) que combina estas dos tecnologías. Pero mi experiencia hasta ahora es que he destruido un montón de intentos de reducción de código de abstracción. Me alegraría, por razones de mantenimiento y en aras de la simplicidad, si pudiera llevarme bien con EdmObservableCollection y solo con un repositorio genérico, pero finalmente hay demandas de aplicaciones y clientes que lamentablemente requieren una gran cantidad de códigos que funcionan de manera diferente.

+0

Puntos interesantes y atentos; +1 de mi parte Veo algunos de los problemas de manera diferente. Específicamente, realizo mucha validación de reglas de datos y negocios. Uso el evento CollectionChanging en el modelo de vista para invocar un método de servicio para hacer la validación. Si la validación falla, el método de servicio cancela la operación y devuelve la llamada a la vista para mostrar un mensaje de error apropiado. –

+0

@David Veeneman: ¡No sabía este evento, interesante! ¿Está disponible en WPF? Solo estaba buscando y solo pude encontrarlo en los espacios de nombres de Windows Forms. – Slauma

+0

Vaya, ese es un evento que había implementado en otra versión de EdmObservableCollection. Bastante fácil de hacer: declare los argumentos del evento para pasar la operación (agregar, actualizar, eliminar), el objeto afectado y un indicador de Cancelar (leer/escribir). Levante el evento en cada método y afecte a la colección, antes de llamar a la clase base, y salga si Cancel se establece en verdadero. Voy a hacer un artículo de CodeProject sobre este tema, e incluiré el evento en ese código. Gracias por la captura! –

Cuestiones relacionadas