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
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
{
///
/// An ObservableCollection for Entity Framework 4 entity collections.
///
/// The type of EF4 entity served.
public class FsObservableCollection : ObservableCollection where T:class
{
#region Fields
// Member variables
private readonly IRepository m_Repository;
#endregion
#region Constructors
///
/// Creates a new FS Observable Collection and populates it with a list of items.
///
/// The items to be inserted into the collection.
/// The Repository for type T.
public FsObservableCollection(IEnumerable items, IRepository 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;
}
///
/// Creates an empty FS Observable Collection, with a repository.
///
/// The Repository for type T.
public FsObservableCollection(IRepository repository) : base()
{
m_Repository = repository;
}
#endregion
#region Events
///
/// Occurs before the collection changes, providing the opportunity to cancel the change.
///
public event CollectionChangingEventHandler CollectionChanging;
#endregion
#region Protected Method Overrides
///
/// Inserts an element into the Collection at the specified index.
///
/// The zero-based index at which item should be inserted.
/// The object to insert.
protected override void InsertItem(int index, T item)
{
// Raise CollectionChanging event; exit if change cancelled
var newItems = new List(new[] {item});
var cancelled = this.RaiseCollectionChangingEvent(NotifyCollectionChangingAction.Add, null, newItems);
if (cancelled) return;
// Insert new item
base.InsertItem(index, item);
m_Repository.Add(item);
}
///
/// Removes the item at the specified index of the collection.
///
/// The zero-based index of the element to remove.
protected override void RemoveItem(int index)
{
// Initialize
var itemToRemove = this[index];
// Raise CollectionChanging event; exit if change cancelled
var oldItems = new List(new[] { itemToRemove });
var cancelled = this.RaiseCollectionChangingEvent(NotifyCollectionChangingAction.Remove, oldItems, null);
if (cancelled) return;
// Remove new item
base.RemoveItem(index);
m_Repository.Delete(itemToRemove);
}
///
/// Removes all items from the collection.
///
protected override void ClearItems()
{
// Initialize
var itemsToDelete = this.ToArray();
// Raise CollectionChanging event; exit if change cancelled
var oldItems = new List(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);
}
}
///
/// Replaces the element at the specified index.
///
/// The zero-based index of the element to replace.
/// The new value for the element at the specified index.
protected override void SetItem(int index, T newItem)
{
// Initialize
var itemToReplace = this[index];
// Raise CollectionChanging event; exit if change cancelled
var oldItems = new List(new[] { itemToReplace });
var newItems = new List(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
///
/// Adds an object to the end of the collection.
///
/// The object to be added to the end of the collection.
public new void Add(T item)
{
// Raise CollectionChanging event; exit if change cancelled
var newItems = new List(new[] { item });
var cancelled = this.RaiseCollectionChangingEvent(NotifyCollectionChangingAction.Add, null, newItems);
if (cancelled) return;
// Add new item
base.Add(item);
m_Repository.Add(item);
}
///
/// Removes all elements from the collection and from the data store.
///
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);
}
///
/// Removes all elements from the collection.
///
/// Whether the items should also be deleted from the data store.
public void Clear(bool clearFromDataStore)
{
// Initialize
var itemsToDelete = this.ToArray();
// Raise CollectionChanging event; exit if change cancelled
var oldItems = new List(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);
}
}
///
/// Inserts an element into the collection at the specified index.
///
/// The zero-based index at which item should be inserted.
/// The object to insert.
public new void Insert(int index, T item)
{
// Raise CollectionChanging event; exit if change cancelled
var newItems = new List(new[] { item });
var cancelled = this.RaiseCollectionChangingEvent(NotifyCollectionChangingAction.Add, null, newItems);
if (cancelled) return;
// Insert new item
base.Insert(index, item);
m_Repository.Add(item);
}
///
/// Persists changes to the collection to the data store.
///
public void PersistToDataStore()
{
m_Repository.SaveChanges();
}
///
/// Removes the first occurrence of a specific object from the collection.
///
/// The object to remove from the collection.
public new void Remove(T itemToRemove)
{
// Raise CollectionChanging event; exit if change cancelled
var oldItems = new List(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
///
/// Raises the CollectionChanging event.
///
/// True if a subscriber cancelled the change, false otherwise.
private bool RaiseCollectionChangingEvent(NotifyCollectionChangingAction action, IList oldItems, IList newItems)
{
// Exit if no subscribers
if (CollectionChanging == null) return false;
// Create event args
var e = new NotifyCollectionChangingEventArgs(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
///
/// Describes the action that caused a CollectionChanging event.
///
public enum NotifyCollectionChangingAction { Add, Remove, Replace, Move, Reset }
#endregion
#region Delegates
///
/// Occurs before an item is added, removed, changed, moved, or the entire list is refreshed.
///
/// The type of elements in the collection.
/// The object that raised the event.
/// Information about the event.
public delegate void CollectionChangingEventHandler(object sender, NotifyCollectionChangingEventArgs e);
#endregion
#region Event Args
public class NotifyCollectionChangingEventArgs : EventArgs
{
#region Constructors
///
/// Constructor with all arguments.
///
/// The action that caused the event.
/// The list of items affected by a Replace, Remove, or Move action.
/// The list of new items involved in the change.
/// The index at which a Move, Remove, or Replace action is occurring.
/// The index at which the change is occurring.
public NotifyCollectionChangingEventArgs(NotifyCollectionChangingAction action, IList oldItems, IList newItems, int oldStartingIndex, int newStartingIndex)
{
this.Action = action;
this.OldItems = oldItems;
this.NewItems = newItems;
this.OldStartingIndex = oldStartingIndex;
this.NewStartingIndex = newStartingIndex;
this.Cancel = false;
}
///
/// Constructor that omits 'starting index' arguments.
///
/// The action that caused the event.
/// The list of items affected by a Replace, Remove, or Move action.
/// The list of new items involved in the change.
public NotifyCollectionChangingEventArgs(NotifyCollectionChangingAction action, IList oldItems, IList newItems)
{
this.Action = action;
this.OldItems = oldItems;
this.NewItems = newItems;
this.OldStartingIndex = -1;
this.NewStartingIndex = -1;
this.Cancel = false;
}
#endregion
#region Properties
///
/// Gets the action that caused the event.
///
public NotifyCollectionChangingAction Action { get; private set; }
///
/// Whether to cancel the pending change.
///
/// This property is set by an event subscriber. It enables
/// the subscriber to cancel the pending change.
public bool Cancel { get; set; }
///
/// Gets the list of new items involved in the change.
///
public IList NewItems { get; private set; }
///
/// Gets the index at which the change is occurring.
///
public int NewStartingIndex { get; set; }
///
/// Gets the list of items affected by a Replace, Remove, or Move action.
///
public IList OldItems { get; private set; }
///
/// Gets the index at which a Move, Remove, or Replace action is occurring.
///
public int OldStartingIndex { get; set; }
#endregion
}
#endregion
}