Entity Framework 4 and WPF

后端 未结 3 2043
我寻月下人不归
我寻月下人不归 2021-02-02 01:54

I am writing a WPF application, using an MVVM design with Entity Framework 4 as the ORM. I have collection properties in my view model that will contain collections of entities

相关标签:
3条回答
  • 2021-02-02 02:19

    I would probably use a factory pattern to achieve a level of abstraction in your viewModel if you want it.

    The downside is that you will have to limit yourself to calling the factory every time you create one of your collections.

    so if your factory had an API like this(which you could switch with whatever you wanted):

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

    so now you would:

    • just implement IFactory to return your EdmObservableCollection whenever TCollection is ObservableCollection
    • whenever you app initializes call ObjectBuilder.SetFactory()
    • and now in your viewmodels wherever you want this you simply call ObjectBuilder.Create<ObservableCollection,MyEntity>();

    also if/whenever you would need to change your ORM backend you simply implement a new IFactory and call ObjectBuilder.SetFactory(factory)

    0 讨论(0)
  • 2021-02-02 02:22

    I will throw in some thoughts but without having a final answer.

    The basic question is in my opinion: Are operations a user can do on a UI always in a unique way related to database operations? Or more specific: If a user can remove an item from a list on the UI or insert a new item into a list, does that necessarily mean that a record has to be deleted from or inserted into the database?

    I think, the answer is: No.

    At first I can see a good use case to work with the EdmObservableCollection<T>. That is for example a view on the WPF UI with only a DataGrid which is bound to a collection of customers. A list of customers will be fetched by a query specification. Now the user can edit in this DataGrid: He can change rows (single customers), he can insert a new row and he can delete a row. The DataGrid supports these operations quite easily and the databinding engine writes those "CUD" operations directly to the bound EdmObservableCollection. In this situation deleting a row or inserting a new row is actually supposed to be directly reflected on the database, so the EdmObservableCollection might be quite useful as it handles Inserts and Deletes in the ObjectContext internally.

    But even in this simple situation there are a few points to take into account:

    • You probably need to inject the ObjectContext/Repository into your ViewModel anyway (to query for the objects you want to put into the collection) - and it must be the same context as injected into the EdmObservableCollection to handle object updates (editing a customer row) properly. You also must work with change tracking objects/proxies if you don't want to do manual "late" change tracking before you call SaveChanges.

    • This kind a "generic" delete operation the EdmObservableCollection<T> provides doesn't consider database or business constraints. What happens for instance if a user tries to delete a row for a customer who is assigned to various orders? If there is a foreign key relationship in the database SaveChanges will fail and throw an exception. Well, you might catch this exception, evaluate it and give a message to the user. But perhaps he has done a lot of other changes (edited many other rows and inserted new customers) but due to this violated FK constraint the whole transaction failed. OK, also this you could handle (remove this deleted customer from the ObjectContext and try again to save the changes) or even give the customer a choice what to do. And up to here we have only considered database constraints. There can be additional business rules which are not reflected in the database model (a customer can't be deleted before he hasn't paid all invoices, deletion must be approved by the sales department boss, customer must not be deleted before 6 month after his last order, and so on and so on...). So, there can be much more involved than a simple "ObjectContext.DeleteObject" to execute deletion in a safe and user friendly way.

    Now let's consider another example: Imagine there is a view to assign contact persons to an order (well, unusual probably but let's say these are large, complex, very individual orders which include a lot of customer services and every order needs different contact persons at the customer site for various aspects of the order). This view may contain a small readonly view of the order, a readonly list of a pool of contact persons which are already in the customer's master data and then an editable list of contact persons which are assigned to the order. Now, like in the first example, the user can do similar things: He can delete a contact person from the list and he can maybe drag and drop a contact person from the master list to insert it into that list of order contact persons. If we had bound this list again to a EdmObservableCollection<T> nonsense would happen: New contact persons would be inserted into the database and contact persons would be deleted from the database. We don't want that, we actually only want to assign or unassign references to existing records (the customer's contact person master data) but never delete or insert records.

    So we have two examples of similar operations on the UI (rows are deleted from and inserted into a list) but with quite different business rules behind them and also different operations in the data store. For WPF the same happens (which can be handled with an ObservableCollection in both cases), but different things must be done in the business and database layer.

    I would draw a few conclusions from this:

    • EdmObservableCollection<T> can be useful in special situations when you have to deal with collections on the UI and you don't have to consider difficult business rules or database constraints. But it many situations it isn't applicable. Of course you could possibly create derived collections for other situations which overload and implement for instance Remove(T item) in another way (for example don't delete from the ObjectContext but set a reference to null or something instead). But this strategy would move responsibilities of repositories or a service layer more and more into those specialized ObservableCollections. If your application does basically CRUD-like operations in DataGrid/List views then EdmObservableCollection might be well suited. For anything else, I doubt.

    • As described there are in my opinion more arguments against coupling database/repository operations with Insert/Remove operations of ObservableCollections and therefore against using a construct like EdmObservableCollection. I believe that in many cases your ViewModels will need a repository or service injected to fulfill the specific needs in your business and database layer. For instance for delete operations you could have a Command in the ViewModel and in the command handler do something like:

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

      Complex stuff like this doesn't belong into a derived ObservableCollection in my opinion.

    • Generally I tend to keep units of work as small as possible - not for technical but for usability reasons. If a user does a lot of stuff in a view (edit something, delete something, insert and so on) and clicks on a "Save" button late after a lot of work, also a lot of things can go wrong, he might get a long list of validation errors and be forced to correct a lot of things. Of course basic validation should have been done in the UI before he can press the "Save" button at all, but the more complex validation will happen later in the business layer. For example if he deletes a row, I delete through the service at once (after confirmation message box perhaps) like in the example above. The same for Inserts. Updates can become more complicated (especially when many navigation properties in an entity are involved) since I don't work with change tracking proxies. (I am not sure if I shouldn't better do.)

    • I have no big hope to make different things look like they were the same. To separate concerns it makes sense to have a CustomerService.Delete and a OrderContactPersonsService.Delete which the ViewModels don't care about what happens behind. But somewhere (business layer, repository, ...) those operations will be different and the hard work has to be done. EdmObservableCollection with an intrinsic IRepository is over-generic the whole chain from the presentation layer down to the database and tries to hide these differences which is unrealistic in any other than the simplest CRUD applications.

    • Having an ObjectContext/DbContext versus an IRepository in the EdmObservableCollection is in my opinion the least problem. The EF context or ObjectSets/DbSets are almost UnitOfWork/Repositories anyway and it is questionable if you don't need to change the interface contracts when you ever should change the database access technology. Personally I have things like "Attach" or "LoadNavigationCollection" on my generic repository and it's not clear for me if these methods with their parameters would make sense at all with another persistance layer. But making the repository even more abstract (in a hope to have a real Add-Update-Delete-Super-Persistance-Ignorant-Interface-Marvel<T>) would only move it more towards uselessness. Abstracting EF away into an IRepository does not solve the concerns I've described.

    Last note as a disclaimer: Read my words with scepticism. I am not an experienced WPF/EF developer, I am just working on my first somewhat bigger application (since around 2 months) which combines these two technologies. But my experience so far is that I have trashed a lot of over-abstracting code reduction attempts. I'd be happy - for maintenance reasons and for the sake of simplicity - if I could get along with an EdmObservableCollection and only a generic repository but finally there are application and customer demands which unfortunately require a lot of differently working code.

    0 讨论(0)
  • 2021-02-02 02:43

    I think I have worked out the answer. The problem isn't with the collection, it's with what is being passed to the collection. The collection shouldn't be working directly with the ObjectContext; instead, it should work with the Repository for the type of entity that it collects. So, a Repository class should be passed to the collection's constructor, and all the persistence code in the collection should be replaced by simple calls to Repository methods. The revised collection class appears below:


    EDIT: Slauma asked about data validation (see his response), so I have added a CollectionChanging event to the collection class I originally posted in my answer. Thanks, Slauma, for the catch! Client code should subscribe to the event and use it to perform validation. Set the EventArgs.Cancel property to true to cancel a change.

    The Collection Class

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

    The Event Args Class

    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 讨论(0)
提交回复
热议问题