2011-03-23 2 views
10

J'écris une application WPF, en utilisant une conception MVVM avec Entity Framework 4 comme ORM. J'ai des propriétés de collection dans mon modèle de vue qui contiendra des collections d'entités renvoyées à partir de EF4 en tant que collections IEnumerable<T> en réponse à des requêtes soumises à partir de la couche de gestion.Entity Framework 4 et WPF

J'avais espéré emballer simplement le jeu de résultats IEnumerable<T> dans un ObservableCollection<T>. Cependant, je me suis retrouvé à écrire du code de suivi des modifications dans mon dépôt, ou à maintenir des collections fantômes d'objets modifiés, juste pour garder le modèle de vue et la couche de persistance synchronisés. Chaque fois qu'une entité est ajoutée à la collection dans le modèle de vue, je devais aller dans mon référentiel pour l'ajouter au jeu d'objets EF4. Je devais faire le même genre de chose avec des mises à jour et des suppressions.

Pour simplifier les choses, j'ai emprunté une classe EdmObservableCollection<T> à partir du projet WPF Application Framework sur CodePlex (http://waf.codeplex.com/). La classe enveloppe un ObservableCollection<T> avec une référence à un EF4 ObjectContext, de sorte que le CO peut être mis à jour lorsque la collection est mise à jour. J'ai réimprimé la classe EdmObservableCollection ci-dessous. La classe fonctionne plutôt bien, mais elle a une odeur de code, parce que je me retrouve avec une référence à EF4 dans mon modèle de vue.

Voici ma question: Dans une application WPF, quelle est la manière habituelle de garder une collection d'entités EF4 en phase avec son contexte d'objet? EdmObservableCollection est-il une approche appropriée ou existe-t-il un meilleur moyen? Est-ce que je manque quelque chose de fondamental dans le travail avec EF4? Merci de votre aide.


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 
    } 
} 

Répondre

5

Je pense que je travaille la réponse. Le problème n'est pas avec la collection, c'est avec ce qui est passé à la collection. La collection ne doit pas fonctionner directement avec ObjectContext; à la place, il devrait fonctionner avec le référentiel pour le type d'entité qu'il recueille. Ainsi, une classe Repository doit être transmise au constructeur de la collection, et tout le code de persistance de la collection doit être remplacé par de simples appels aux méthodes Repository.La classe de collection révisée apparaît ci-dessous:


EDIT: Slauma interrogé sur la validation des données (voir sa réponse), j'ai donc ajouté un événement CollectionChanging à la classe de collection I Envoyé dans ma réponse. Merci, Slauma, pour la prise! Le code client doit s'abonner à l'événement et l'utiliser pour effectuer la validation. Définissez la propriété EventArgs.Cancel sur true pour annuler une modification.

La collection Classe

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 
    } 
} 

args événement Classe

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

Je probablement utiliser un factory pattern pour atteindre un niveau d'abstraction dans votre viewmodel si vous le voulez. L'inconvénient est que vous devrez vous limiter à appeler l'usine chaque fois que vous créez l'une de vos collections.

donc si votre usine avait une API comme celui-ci (que vous pouvez passer avec tout ce que vous vouliez):

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); 
    } 
} 

maintenant vous:

  • mettre en œuvre juste IFactory retourner votre EdmObservableCollection chaque fois TCollection is ObservableCollection
  • chaque fois que vous app initialise appel ObjectBuilder.SetFactory()
  • et maintenant dans vos viewmodels où vous voulez ce que vous appelez simplement ObjectBuilder.Create<ObservableCollection,MyEntity>();

également si/quand vous devez changer votre backend ORM vous implémentez simplement une nouvelle IFactory et appelez ObjectBuilder.SetFactory(factory)

1

Je jetterai dans quelques pensées, mais sans avoir une réponse définitive.

La question fondamentale est à mon avis: Les opérations qu'un utilisateur peut effectuer sur une interface utilisateur sont-elles toujours liées de manière unique aux opérations de la base de données? Ou plus spécifique: Si un utilisateur peut supprimer un élément d'une liste sur l'interface utilisateur ou insérer un nouvel élément dans une liste, cela signifie-t-il nécessairement qu'un enregistrement doit être supprimé ou inséré dans la base de données?

Je pense, la réponse est: Non

Au début, je peux voir un bon cas d'utilisation pour travailler avec le EdmObservableCollection<T>. C'est par exemple une vue sur l'interface utilisateur WPF avec seulement un DataGrid qui est lié à une collection de clients. Une liste de clients sera récupérée par une spécification de requête. Maintenant, l'utilisateur peut modifier dans ce DataGrid: Il peut changer les lignes (clients uniques), il peut insérer une nouvelle ligne et il peut supprimer une ligne. Le DataGrid prend en charge ces opérations assez facilement et le moteur de liaison de données écrit ces opérations "CUD" directement à l'EdmObservableCollection lié. Dans cette situation, la suppression d'une ligne ou l'insertion d'une nouvelle ligne est censée être directement répercutée sur la base de données, donc EdmObservableCollection peut être très utile car elle gère les insertions et les suppressions dans ObjectContext en interne.

Mais même dans cette situation simple, il y a quelques points à prendre en compte:

  • Vous avez probablement besoin d'injecter ObjectContext/dépôt dans votre ViewModel de toute façon (pour interroger les objets que vous souhaitez mettre dans la collection) - et il doit être le même contexte que celui injecté dans EdmObservableCollection pour gérer les mises à jour des objets (éditer une ligne client) correctement. Vous devez également travailler avec les objets/proxys de suivi des modifications si vous ne souhaitez pas effectuer de suivi manuel des modifications «en retard» avant d'appeler SaveChanges.

  • Ce genre d'opération de suppression "générique" que le EdmObservableCollection<T> fournit ne tient pas compte des contraintes de base de données ou d'entreprise. Que se passe-t-il par exemple si un utilisateur tente de supprimer une ligne pour un client affecté à différentes commandes? S'il existe une relation de clé étrangère dans la base de données, SaveChanges échoue et génère une exception. Eh bien, vous pourriez attraper cette exception, l'évaluer et donner un message à l'utilisateur. Mais peut-être a-t-il fait beaucoup d'autres changements (édité beaucoup d'autres lignes et inséré de nouveaux clients) mais en raison de cette contrainte FK violée, la transaction entière a échoué. OK, vous pouvez aussi gérer ceci (supprimer ce client supprimé du ObjectContext et réessayer d'enregistrer les modifications) ou même donner au client le choix de ce qu'il doit faire. Et jusqu'à ici, nous avons seulement considéré les contraintes de base de données.Il peut y avoir des règles métier supplémentaires qui ne sont pas reflétées dans le modèle de base de données (un client ne peut pas être supprimé avant d'avoir payé toutes les factures, la suppression doit être approuvée par le responsable des ventes, le client ne doit pas être supprimé sa dernière commande, et ainsi de suite et ainsi de suite ...). Ainsi, il peut y avoir beaucoup plus impliqué qu'un simple "ObjectContext.DeleteObject" pour exécuter la suppression de manière sûre et conviviale.

Considérons maintenant un autre exemple: Imaginez qu'il y est une vue d'assigner des personnes de contact à un ordre (bien, inhabituel probablement, mais disons que ce sont des grands complexes, des commandes très individuelles qui comprennent un grand nombre de services à la clientèle et chaque commande nécessite des interlocuteurs différents sur le site du client pour différents aspects de la commande). Cette vue peut contenir une petite vue en lecture seule de la commande, une liste en lecture seule d'un pool de personnes de contact déjà présentes dans les données de base du client, puis une liste modifiable des personnes de contact affectées à la commande. Maintenant, comme dans le premier exemple, l'utilisateur peut faire des choses similaires: il peut supprimer un contact de la liste et il peut faire glisser et déposer une personne de contact de la liste principale pour l'insérer dans cette liste de contacts. Si nous avions de nouveau lié cette liste à un EdmObservableCollection<T>, il se produirait un non-sens: de nouvelles personnes de contact seraient insérées dans la base de données et les personnes de contact seraient supprimées de la base de données. Nous ne voulons pas cela, nous voulons seulement assigner ou désassigner des références à des enregistrements existants (les données de base du contact du client) mais ne jamais supprimer ou insérer des enregistrements. Nous avons donc deux exemples d'opérations similaires sur l'interface utilisateur (les lignes sont supprimées et insérées dans une liste) mais avec des règles métier très différentes et des opérations différentes dans le magasin de données. Pour WPF, la même chose se produit (ce qui peut être géré avec un ObservableCollection dans les deux cas), mais différentes choses doivent être faites dans la couche business et database.

je en tirer quelques conclusions:

  • EdmObservableCollection<T> peut être utile dans des situations particulières lorsque vous avez à traiter avec des collections sur l'interface utilisateur et vous ne devez pas tenir compte des règles commerciales difficiles ou base de données contraintes. Mais de nombreuses situations ne sont pas applicables. Bien sûr, vous pouvez éventuellement créer des collections dérivées pour d'autres situations qui surchargent et implémentent par exemple Remove(T item) d'une autre manière (par exemple, ne supprimez pas du ObjectContext mais définissez une référence à null ou quelque chose à la place). Mais cette stratégie déplacerait de plus en plus les responsabilités des référentiels ou une couche de service dans ces ObservableCollections spécialisées. Si votre application effectue essentiellement des opérations de type CRUD dans les vues DataGrid/List, alors EdmObservableCollection peut convenir. Pour toute autre chose, je doute. Comme cela a été décrit, il existe à mon avis plus d'arguments contre le couplage des opérations base de données/référentiel avec les opérations Insertion/Suppression de ObservableCollections et donc contre l'utilisation d'une construction comme EdmObservableCollection. Je crois que dans de nombreux cas, vos ViewModels auront besoin d'un référentiel ou d'un service injecté pour répondre aux besoins spécifiques de votre couche d'entreprise et de votre base de données. Par exemple, pour les opérations de suppression, vous pouvez avoir une commande dans le ViewModel et dans le gestionnaire de commandes faire quelque chose comme:

    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); 
    } 
    

    choses complexes comme celui-ci ne fait pas partie dans un ObservableCollection dérivé à mon avis.

  • En général, j'ai tendance à garder les unités de travail aussi petites que possible - pas pour des raisons techniques mais pour des raisons d'utilisabilité. Si un utilisateur fait beaucoup de choses dans une vue (modifier quelque chose, supprimer quelque chose, insérer et ainsi de suite) et clique sur un bouton "Enregistrer" tard après beaucoup de travail, beaucoup de choses peuvent mal se passer, il pourrait obtenir une longue liste d'erreurs de validation et être forcé de corriger beaucoup de choses.Bien sûr, la validation de base devrait avoir été faite dans l'interface utilisateur avant de pouvoir appuyer sur le bouton "Enregistrer", mais la validation plus complexe aura lieu plus tard dans la couche de gestion. Par exemple s'il supprime une ligne, je supprime à travers le service à la fois (après la boîte de message de confirmation peut-être) comme dans l'exemple ci-dessus. La même chose pour les inserts. Les mises à jour peuvent devenir plus compliquées (en particulier lorsque de nombreuses propriétés de navigation sont impliquées dans une entité) car je ne travaille pas avec les proxys de suivi des modifications. (Je ne suis pas sûr si je ne devrais pas mieux faire.)

  • J'ai pas grand espoir de faire des choses différentes ressemblent, ils étaient les mêmes. Pour dissocier les soucis, il est logique d'avoir un CustomerService.Delete et un OrderContactPersonsService.Delete dont ViewModels ne se soucie pas de ce qui se passe derrière. Mais quelque part (couche de gestion, référentiel, ...), ces opérations seront différentes et le travail acharné doit être fait. EdmObservableCollection avec un IRepository intrinsèque est sur-générique de la chaîne entière de la couche de présentation à la base de données et tente de cacher ces différences qui est irréaliste dans les autres applications CRUD les plus simples. Avoir un ObjectContext/DbContext par rapport à un IRepository dans EdmObservableCollection est selon moi le moindre problème. Le contexte EF ou ObjectSets/DbSets sont presque UnitOfWork/Repositories de toute façon et il est discutable si vous n'avez pas besoin de changer les contrats d'interface quand vous devriez changer la technologie d'accès à la base de données. Personnellement, j'ai des choses comme "Attach" ou "LoadNavigationCollection" sur mon dépôt générique et il n'est pas clair pour moi si ces méthodes avec leurs paramètres auraient du sens du tout avec une autre couche de persistance. Mais rendre le dépôt encore plus abstrait (dans l'espoir d'avoir un vrai Add-Update-Delete-Super-Persistance-Ignorant-Interface-Marvel<T>) ne ferait que le déplacer vers l'inutilité. Abstraction EF dans un IRepository ne résout pas les problèmes que j'ai décrits.

Dernière note comme un avertissement: Lisez mes mots avec scepticisme. Je ne suis pas un développeur WPF/EF expérimenté, je travaille juste sur ma première application un peu plus grande (depuis environ 2 mois) qui combine ces deux technologies. Mais mon expérience jusqu'à présent est que j'ai jeté beaucoup de tentatives de réduction de code trop abstraites. Je serais heureux - pour des raisons de maintenance et par souci de simplicité - de pouvoir m'entendre avec un EdmObservableCollection et seulement un référentiel générique mais enfin il y a des demandes d'applications et de clients qui nécessitent malheureusement beaucoup de code fonctionnant différemment.

+0

Points intéressants et réfléchis; +1 de moi. Je vois certains des problèmes différemment. Plus précisément, je fais beaucoup de validation de données et de règles métier. J'utilise l'événement CollectionChanging dans le modèle de vue pour appeler une méthode de service pour effectuer la validation. Si la validation échoue, la méthode de service annule l'opération et rappelle la vue pour afficher un message d'erreur approprié. –

+0

@David Veeneman: Je ne connaissais pas cet événement, intéressant! Est-il disponible dans WPF? Je faisais juste des recherches et je ne pouvais le trouver que dans les espaces de noms Windows Forms. – Slauma

+0

Oups - c'est un événement que j'avais implémenté sur une autre version de EdmObservableCollection. Assez facile à faire - déclarer les arguments de l'événement pour passer l'opération (ajouter, mettre à jour, supprimer), l'objet affecté, et un drapeau d'annulation (lire/écrire). Soulevez l'événement dans chaque méthode et affecte la collection, avant d'appeler la classe de base, et quittez si Cancel est défini sur true. Je vais faire un article CodeProject sur ce sujet, et j'inclurai l'événement dans ce code. Merci pour la capture! –