Entity Framework 4 и WPF

Я пишу приложение WPF, используя проект MVVM с Entity Framework 4 как ORM. У меня есть свойства коллекции в моей модели представления, которая будет содержать коллекции объектов, возвращаемых из EF4, в виде коллекций IEnumerable<T> в ответ на запросы, отправленные с бизнес-уровня.

Я надеялся просто обернуть набор результатов IEnumerable<T> в ObservableCollection<T>. Тем не менее, я обнаружил, что записываю код отслеживания изменений в своем репозитории или поддерживаю теневые коллекции измененных объектов, чтобы сохранить синхронизацию модели представления и уровня сохранения. Каждый раз, когда объект добавляется в коллекцию в модели представления, мне приходилось переходить в мой репозиторий, чтобы добавить его в ObjectSet EF4. Я должен был делать то же самое с обновлениями и удалениями.

Чтобы упростить ситуацию, я заимствовал класс EdmObservableCollection<T> из проекта WPF Application Framework на CodePlex (http://waf.codeplex.com/). Класс обертывает ObservableCollection<T> ссылкой на EF4 ObjectContext, поэтому OC может обновляться по мере обновления коллекции. Я перепечатал класс EdmObservableCollection ниже. Класс работает очень хорошо, но у него немного запах кода, потому что в итоге я получаю ссылку на EF4 в моей модели просмотра.

Здесь мой вопрос: в приложении WPF, какой обычный способ хранения коллекции сущностей EF4 синхронизируется с контекстом объекта? Является ли EdmObservableCollection подходящим подходом или есть лучший способ? Я что-то не замечаю в работе с EF4? Благодарим за помощь.


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

Ответ 1

Я думаю, что я отработал ответ. Проблема заключается не в коллекции, а в том, что передается в коллекцию. Сбор не должен работать непосредственно с ObjectContext; вместо этого он должен работать с репозиторием для типа объекта, который он собирает. Таким образом, класс репозитория должен быть передан конструктору коллекции, и весь код сохранения в коллекции должен быть заменен простыми вызовами методов репозитория. Пересмотренный класс коллекции отображается ниже:


РЕДАКТИРОВАТЬ: Slauma спросил о проверке данных (см. его ответ), поэтому я добавил событие CollectionChanging в класс коллекции, который я изначально опубликовал в своем ответе. Спасибо, Слаума, за улов! Клиентский код должен подписаться на это событие и использовать его для проверки. Установите для свойства EventArgs.Cancel значение true, чтобы отменить изменение.

Класс коллекции

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

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
 }

Ответ 2

Я выскажу некоторые мысли, но не получив окончательного ответа.

Основной вопрос, на мой взгляд: всегда ли действия пользователя, которые пользователь может делать в пользовательском интерфейсе, связанные с операциями базы данных? Или более конкретно: если пользователь может удалить элемент из списка в пользовательском интерфейсе или вставить новый элемент в список, обязательно ли это означает, что запись должна быть удалена или вставлена ​​в базу данных?

Думаю, ответ: No.

Сначала я вижу хороший прецедент для работы с EdmObservableCollection<T>. Это, например, представление об пользовательском интерфейсе WPF с помощью только DataGrid, связанного с коллекцией клиентов. Список клиентов будет получен по спецификации запроса. Теперь пользователь может редактировать этот DataGrid: он может изменять строки (отдельные клиенты), он может вставить новую строку, и он может удалить строку. DataGrid поддерживает эти операции довольно легко, и механизм привязки данных записывает эти операции "CUD" непосредственно в связанный EdmObservableCollection. В этой ситуации удаление строки или вставка новой строки на самом деле должно быть непосредственно отражено в базе данных, поэтому EdmObservableCollection может оказаться весьма полезным, поскольку он внутренне обрабатывает вставки и удаляет объект ObjectContext.

Но даже в этой простой ситуации нужно учитывать несколько моментов:

  • Вероятно, вам нужно в любом случае добавить ObjectContext/Repository в свою ViewModel (чтобы запросить объекты, которые вы хотите поместить в коллекцию) - и это должен быть тот же контекст, который был введен в EdmObservableCollection для обработки обновлений объектов (редактирование строки клиента). Вы также должны работать с объектами/прокси-сайтами отслеживания изменений, если вы не хотите выполнять ручное "опоздание" отслеживания изменений до вызова SaveChanges.

  • Этот вид "общей" операции удаления, предоставляемый EdmObservableCollection<T> не учитывает ограничения базы данных или бизнеса. Что происходит, например, если пользователь пытается удалить строку для клиента, которому назначены разные заказы? Если в базе данных есть отношения с внешним ключом, SaveChanges завершится с ошибкой и вызовет исключение. Ну, вы можете поймать это исключение, оценить его и дать сообщение пользователю. Но, возможно, он сделал много других изменений (отредактировал многие другие строки и вставил новых клиентов), но из-за этого нарушенного ограничения FK вся транзакция завершилась неудачно. OK, также вы можете обработать (удалить этого удаленного клиента из ObjectContext и повторить попытку сохранить изменения) или даже дать клиенту выбор, что делать. И здесь мы только рассмотрели ограничения базы данных. Могут быть дополнительные бизнес-правила, которые не отражаются в модели базы данных (клиент не может быть удален до того, как он не оплатил все счета-фактуры, удаление должно быть одобрено руководителем отдела продаж, клиент не должен быть удален до 6 месяцев после его последний заказ и т.д. и т.д.). Таким образом, может быть гораздо больше, чем простой "ObjectContext.DeleteObject", чтобы выполнять удаление безопасным и удобным для пользователя способом.

Теперь рассмотрим еще один пример: представьте, что есть возможность назначить контактных лиц для заказа (что, возможно, необычно, но пусть говорят, что это большие, сложные, очень индивидуальные заказы, которые включают в себя множество клиентских услуг, и каждый заказ нуждается в разных контактные лица на сайте заказчика для различных аспектов заказа). Это представление может содержать небольшое представление только для чтения порядка, список только для чтения пула контактных лиц, которые уже находятся в основных данных клиента, а затем редактируемый список контактных лиц, которые назначены для заказа. Теперь, как и в первом примере, пользователь может делать похожие вещи: он может удалить контактного лица из списка, и он может перетащить контактного лица из главного списка, чтобы вставить его в список контактов для контактов. Если бы мы снова связали этот список с ошибкой EdmObservableCollection<T>: новые контактные лица будут вставлены в базу данных, а контактные лица будут удалены из базы данных. Мы не хотим этого, мы на самом деле хотим только назначить или отменить назначение ссылок на существующие записи (основные данные контактного лица клиента), но никогда не удалять и не вставлять записи.

Итак, у нас есть два примера аналогичных операций с пользовательским интерфейсом (строки удаляются и вставляются в список), но с совершенно разными бизнес-правилами, а также различными операциями в хранилище данных. Для WPF то же самое происходит (в обоих случаях это может обрабатываться ObservableCollection), но разные вещи должны выполняться на уровне бизнеса и базы данных.

Я бы сделал несколько выводов из этого:

  • EdmObservableCollection<T> может быть полезен в особых ситуациях, когда вам приходится иметь дело с коллекциями в пользовательском интерфейсе, и вам не нужно учитывать сложные бизнес-правила или ограничения базы данных. Но это многие ситуации не применимы. Конечно, вы могли бы создать производные коллекции для других ситуаций, которые перегружают и реализуют, например, Remove(T item) по-другому (например, не удалять из ObjectContext, а устанавливать ссылку на нуль или что-то вместо этого). Но эта стратегия будет все больше перемещать обязанности репозиториев или уровня обслуживания в эти специализированные ObservableCollections. Если ваше приложение в основном выполняет операции типа CRUD в представлениях DataGrid/List, то EdmObservableCollection может быть хорошо подходит. Что-то еще, я сомневаюсь.

  • Как описано выше, на мой взгляд больше аргументов против связи операций с базами данных/хранилища с операциями Insert/Remove ObservableCollections и, следовательно, против использования такой конструкции, как EdmObservableCollection. Я считаю, что во многих случаях вашим ViewModels потребуется репозиторий или служба, которые будут внедрены для удовлетворения конкретных потребностей вашего бизнеса и уровня базы данных. Например, для операций удаления вы можете иметь команду в ViewModel и в обработчике команд сделать что-то вроде:

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

    Сложные вещи вроде этого не относятся к производному ObservableCollection, на мой взгляд.

  • Как правило, я стараюсь, чтобы единицы работы были как можно меньше - не для технических, а для удобства использования. Если пользователь делает много вещей в представлении (что-то редактирует, удаляет что-то, вставлять и т.д.) И нажимает кнопку "Сохранить" в конце после большой работы, также многое может пойти не так, он может получить длинный список ошибок проверки и быть вынужденным исправить множество вещей. Конечно, базовая проверка должна была быть выполнена в пользовательском интерфейсе, прежде чем он может нажать кнопку "Сохранить" вообще, но более сложная проверка будет происходить позже на бизнес-уровне. Например, если он удаляет строку, я удаляю ее через службу сразу (возможно, после окна подтверждения), как в приведенном выше примере. То же самое для вложений. Обновления могут усложняться (особенно когда задействованы многие свойства навигации в сущности), так как я не работаю с проксими отслеживания изменений. (Я не уверен, что мне лучше не делать этого.)

  • У меня нет большой надежды сделать разные вещи похожими на то, что они были одинаковыми. Чтобы разделить проблемы, имеет смысл иметь CustomerService.Delete и OrderContactPersonsService.Delete, которые ViewModels не заботятся о том, что происходит позади. Но где-то (бизнес-уровень, репозиторий,...) эти операции будут разными, и тяжелая работа должна быть выполнена. EdmObservableCollection с внутренним IRepository полностью генерирует всю цепочку от уровня представления до базы данных и пытается скрыть эти различия, нереалистичные в любых других, чем простейшие приложения CRUD.

  • Наличие ObjectContext/DbContext против IRepository в EdmObservableCollection - это, на мой взгляд, наименьшая проблема. Контекст EF или ObjectSets/DbSets в любом случае являются почти UnitOfWork/Repositories, и сомнительно, если вам не нужно менять контракты интерфейса, когда вы когда-либо должны менять технологию доступа к базе данных. Лично у меня есть такие вещи, как "Attach" или "LoadNavigationCollection" в моем общем хранилище, и мне не ясно, будут ли эти методы с их параметрами иметь смысл вообще с другим уровнем сопротивления. Но сделать репозиторий еще более абстрактным (в надежде получить реальный Add-Update-Delete-Super-Persistance-Ignorant-Interface-Marvel<T>) только продвинул бы его больше к бесполезности. Уточнение EF в IRepository не решает проблем, которые я описал.

Последнее примечание как отказ от ответственности: прочитайте мои слова скептицизмом. Я не опытный разработчик WPF/EF, я просто работаю над своим первым несколько большим приложением (начиная примерно с 2 месяцев), которое объединяет эти две технологии. Но мой опыт до сих пор заключается в том, что я повредил множество чрезмерно абстрагирующих попыток сокращения кода. Я был бы счастлив - по причинам обслуживания и ради простоты - если бы я мог согласиться с EdmObservableCollection и только с общим хранилищем, но, наконец, есть приложения и требования клиентов, которые, к сожалению, требуют много другого рабочего кода.

Ответ 3

Я бы, вероятно, использовал factory pattern, чтобы достичь уровня абстракции в вашей модели viewModel, если вы этого хотите.

Недостатком является то, что вам придётся ограничивать себя вызовом factory при создании одной из ваших коллекций.

так что если ваш factory имел API вроде этого (который вы могли бы переключаться с любым желаемым):

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

теперь вы должны:

  • просто реализуйте IFactory, чтобы вернуть свой EdmObservableCollection всякий раз, когда TCollection is ObservableCollection
  • всякий раз, когда вы выполняете инициализацию вызова ObjectBuilder.SetFactory()
  • и теперь в ваших моделях просмотра, где бы вы этого ни хотели, вы просто вызываете ObjectBuilder.Create<ObservableCollection,MyEntity>();

также, если/когда вам нужно будет изменить ваш сервер ORM, вы просто реализуете новый IFactory и вызываете ObjectBuilder.SetFactory(factory)