762 lines
23 KiB
C#

using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Csla.Properties;
namespace Csla
{
/// <summary>
/// This is the base class from which most business collections
/// or lists will be derived.
/// </summary>
/// <typeparam name="T">Type of the business object being defined.</typeparam>
/// <typeparam name="C">Type of the child objects contained in the list.</typeparam>
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
[Serializable()]
public abstract class BusinessListBase<T, C> :
Core.ExtendedBindingList<C>,
Core.IEditableCollection, ICloneable, Core.ISavable, Core.IParent
where T : BusinessListBase<T, C>
where C : Core.IEditableBusinessObject
{
#region Constructors
/// <summary>
/// Creates an instance of the object.
/// </summary>
protected BusinessListBase()
{
Initialize();
}
#endregion
#region Initialize
/// <summary>
/// Override this method to set up event handlers so user
/// code in a partial class can respond to events raised by
/// generated code.
/// </summary>
protected virtual void Initialize()
{ /* allows subclass to initialize events before any other activity occurs */ }
#endregion
#region IsDirty, IsValid
/// <summary>
/// Gets a value indicating whether this object's data has been changed.
/// </summary>
public bool IsDirty
{
get
{
// any non-new deletions make us dirty
foreach (C item in DeletedList)
if (!item.IsNew)
return true;
// run through all the child objects
// and if any are dirty then then
// collection is dirty
foreach (C child in this)
if (child.IsDirty)
return true;
return false;
}
}
/// <summary>
/// Gets a value indicating whether this object is currently in
/// a valid state (has no broken validation rules).
/// </summary>
public virtual bool IsValid
{
get
{
// run through all the child objects
// and if any are invalid then the
// collection is invalid
foreach (C child in this)
if (!child.IsValid)
return false;
return true;
}
}
#endregion
#region Begin/Cancel/ApplyEdit
/// <summary>
/// Starts a nested edit on the object.
/// </summary>
/// <remarks>
/// <para>
/// When this method is called the object takes a snapshot of
/// its current state (the values of its variables). This snapshot
/// can be restored by calling <see cref="CancelEdit" />
/// or committed by calling <see cref="ApplyEdit" />.
/// </para><para>
/// This is a nested operation. Each call to BeginEdit adds a new
/// snapshot of the object's state to a stack. You should ensure that
/// for each call to BeginEdit there is a corresponding call to either
/// CancelEdit or ApplyEdit to remove that snapshot from the stack.
/// </para><para>
/// See Chapters 2 and 3 for details on n-level undo and state stacking.
/// </para><para>
/// This method triggers the copying of all child object states.
/// </para>
/// </remarks>
public void BeginEdit()
{
if (this.IsChild)
throw new NotSupportedException(Resources.NoBeginEditChildException);
CopyState();
}
/// <summary>
/// Cancels the current edit process, restoring the object's state to
/// its previous values.
/// </summary>
/// <remarks>
/// Calling this method causes the most recently taken snapshot of the
/// object's state to be restored. This resets the object's values
/// to the point of the last <see cref="BeginEdit" />
/// call.
/// <para>
/// This method triggers an undo in all child objects.
/// </para>
/// </remarks>
public void CancelEdit()
{
if (this.IsChild)
throw new NotSupportedException(Resources.NoCancelEditChildException);
UndoChanges();
}
/// <summary>
/// Commits the current edit process.
/// </summary>
/// <remarks>
/// Calling this method causes the most recently taken snapshot of the
/// object's state to be discarded, thus committing any changes made
/// to the object's state since the last
/// <see cref="BeginEdit" /> call.
/// <para>
/// This method triggers an <see cref="Core.BusinessBase.ApplyEdit"/>
/// in all child objects.
/// </para>
/// </remarks>
public void ApplyEdit()
{
if (this.IsChild)
throw new NotSupportedException(Resources.NoApplyEditChildException);
AcceptChanges();
}
void Core.IParent.ApplyEditChild(Core.IEditableBusinessObject child)
{
EditChildComplete(child);
}
/// <summary>
/// Override this method to be notified when a child object's
/// <see cref="Core.BusinessBase.ApplyEdit" /> method has
/// completed.
/// </summary>
/// <param name="child">The child object that was edited.</param>
protected virtual void EditChildComplete(Core.IEditableBusinessObject child)
{
// do nothing, we don't really care
// when a child has its edits applied
}
#endregion
#region N-level undo
void Core.IUndoableObject.CopyState()
{
CopyState();
}
void Core.IUndoableObject.UndoChanges()
{
UndoChanges();
}
void Core.IUndoableObject.AcceptChanges()
{
AcceptChanges();
}
private void CopyState()
{
// we are going a level deeper in editing
_editLevel += 1;
// cascade the call to all child objects
foreach (C child in this)
child.CopyState();
// cascade the call to all deleted child objects
foreach (C child in DeletedList)
child.CopyState();
}
private void UndoChanges()
{
C child;
// we are coming up one edit level
_editLevel -= 1;
if (_editLevel < 0) _editLevel = 0;
// Cancel edit on all current items
for (int index = Count - 1; index >= 0; index--)
{
child = this[index];
child.UndoChanges();
// if item is below its point of addition, remove
if (child.EditLevelAdded > _editLevel)
{
bool oldAllowRemove = this.AllowRemove;
try
{
this.AllowRemove = true;
RemoveAt(index);
}
finally
{
this.AllowRemove = oldAllowRemove;
}
}
}
// cancel edit on all deleted items
for (int index = DeletedList.Count - 1; index >= 0; index--)
{
child = DeletedList[index];
child.UndoChanges();
if (child.EditLevelAdded > _editLevel)
{
// if item is below its point of addition, remove
DeletedList.RemoveAt(index);
}
else
{
// if item is no longer deleted move back to main list
if (!child.IsDeleted) UnDeleteChild(child);
}
}
}
private void AcceptChanges()
{
// we are coming up one edit level
_editLevel -= 1;
if (_editLevel < 0) _editLevel = 0;
// cascade the call to all child objects
foreach (C child in this)
{
child.AcceptChanges();
// if item is below its point of addition, lower point of addition
if (child.EditLevelAdded > _editLevel) child.EditLevelAdded = _editLevel;
}
// cascade the call to all deleted child objects
for (int index = DeletedList.Count - 1; index >= 0; index--)
{
C child = DeletedList[index];
child.AcceptChanges();
// if item is below its point of addition, remove
if (child.EditLevelAdded > _editLevel)
DeletedList.RemoveAt(index);
}
}
#endregion
#region Delete and Undelete child
private List<C> _deletedList;
/// <summary>
/// A collection containing all child objects marked
/// for deletion.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Microsoft.Design", "CA1002:DoNotExposeGenericLists")]
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected List<C> DeletedList
{
get
{
if (_deletedList == null)
_deletedList = new List<C>();
return _deletedList;
}
}
private void DeleteChild(C child)
{
// mark the object as deleted
child.DeleteChild();
// and add it to the deleted collection for storage
DeletedList.Add(child);
}
private void UnDeleteChild(C child)
{
// we are inserting an _existing_ object so
// we need to preserve the object's editleveladded value
// because it will be changed by the normal add process
int saveLevel = child.EditLevelAdded;
Add(child);
child.EditLevelAdded = saveLevel;
// since the object is no longer deleted, remove it from
// the deleted collection
DeletedList.Remove(child);
}
/// <summary>
/// Returns <see langword="true"/> if the internal deleted list
/// contains the specified child object.
/// </summary>
/// <param name="item">Child object to check.</param>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public bool ContainsDeleted(C item)
{
return DeletedList.Contains(item);
}
#endregion
#region Insert, Remove, Clear
/// <summary>
/// This method is called by a child object when it
/// wants to be removed from the collection.
/// </summary>
/// <param name="child">The child object to remove.</param>
void Core.IEditableCollection.RemoveChild(Csla.Core.IEditableBusinessObject child)
{
Remove((C)child);
}
/// <summary>
/// This method is called by a child object when it
/// wants to be removed from the collection.
/// </summary>
/// <param name="child">The child object to remove.</param>
void Core.IParent.RemoveChild(Csla.Core.IEditableBusinessObject child)
{
Remove((C)child);
}
/// <summary>
/// Sets the edit level of the child object as it is added.
/// </summary>
/// <param name="index">Index of the item to insert.</param>
/// <param name="item">Item to insert.</param>
protected override void InsertItem(int index, C item)
{
// when an object is inserted we assume it is
// a new object and so the edit level when it was
// added must be set
item.EditLevelAdded = _editLevel;
item.SetParent(this);
base.InsertItem(index, item);
}
/// <summary>
/// Marks the child object for deletion and moves it to
/// the collection of deleted objects.
/// </summary>
/// <param name="index">Index of the item to remove.</param>
protected override void RemoveItem(int index)
{
// when an object is 'removed' it is really
// being deleted, so do the deletion work
C child = this[index];
base.RemoveItem(index);
CopyToDeletedList(child);
}
private void CopyToDeletedList(C child)
{
DeleteChild(child);
INotifyPropertyChanged c = child as INotifyPropertyChanged;
if (c != null)
c.PropertyChanged -= new PropertyChangedEventHandler(Child_PropertyChanged);
}
/// <summary>
/// Clears the collection, moving all active
/// items to the deleted list.
/// </summary>
protected override void ClearItems()
{
while (base.Count > 0) RemoveItem(0);
base.ClearItems();
}
/// <summary>
/// Replaces the item at the specified index with
/// the specified item, first moving the original
/// item to the deleted list.
/// </summary>
/// <param name="index">The zero-based index of the item to replace.</param>
/// <param name="item">
/// The new value for the item at the specified index.
/// The value can be null for reference types.
/// </param>
/// <remarks></remarks>
protected override void SetItem(int index, C item)
{
// copy the original object to the deleted list,
// marking as deleted, etc.
C child = default(C);
if (!ReferenceEquals(this[index], item))
child = this[index];
// replace the original object with this new
// object
base.SetItem(index, item);
if (child != null)
CopyToDeletedList(child);
}
#endregion
#region Edit level tracking
// keep track of how many edit levels we have
private int _editLevel;
/// <summary>
/// Returns the current edit level of the object.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
protected int EditLevel
{
get { return _editLevel; }
}
#endregion
#region IsChild
[NotUndoable()]
private bool _isChild = false;
/// <summary>
/// Indicates whether this collection object is a child object.
/// </summary>
/// <returns>True if this is a child object.</returns>
protected bool IsChild
{
get { return _isChild; }
}
/// <summary>
/// Marks the object as being a child object.
/// </summary>
/// <remarks>
/// <para>
/// By default all business objects are 'parent' objects. This means
/// that they can be directly retrieved and updated into the database.
/// </para><para>
/// We often also need child objects. These are objects which are contained
/// within other objects. For instance, a parent Invoice object will contain
/// child LineItem objects.
/// </para><para>
/// To create a child object, the MarkAsChild method must be called as the
/// object is created. Please see Chapter 7 for details on the use of the
/// MarkAsChild method.
/// </para>
/// </remarks>
protected void MarkAsChild()
{
_isChild = true;
}
#endregion
#region ICloneable
object ICloneable.Clone()
{
return GetClone();
}
/// <summary>
/// Creates a clone of the object.
/// </summary>
/// <returns>A new object containing the exact data of the original object.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected virtual object GetClone()
{
return Core.ObjectCloner.Clone(this);
}
/// <summary>
/// Creates a clone of the object.
/// </summary>
/// <returns>A new object containing the exact data of the original object.</returns>
public T Clone()
{
return (T)GetClone();
}
#endregion
#region Cascade Child events
private void Child_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
for (int index = 0; index < Count; index++)
{
if (ReferenceEquals(this[index], sender))
{
OnListChanged(new ListChangedEventArgs(ListChangedType.ItemChanged, index));
return;
}
}
}
#endregion
#region Serialization Notification
[OnDeserialized()]
private void OnDeserializedHandler(StreamingContext context)
{
OnDeserialized(context);
foreach (Core.IEditableBusinessObject child in this)
{
child.SetParent(this);
INotifyPropertyChanged c = child as INotifyPropertyChanged;
if (c != null)
c.PropertyChanged += new PropertyChangedEventHandler(Child_PropertyChanged);
}
foreach (Core.IEditableBusinessObject child in DeletedList)
child.SetParent(this);
}
/// <summary>
/// This method is called on a newly deserialized object
/// after deserialization is complete.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected virtual void OnDeserialized(StreamingContext context)
{
// do nothing - this is here so a subclass
// could override if needed
}
#endregion
#region Data Access
/// <summary>
/// Saves the object to the database.
/// </summary>
/// <remarks>
/// <para>
/// Calling this method starts the save operation, causing the all child
/// objects to be inserted, updated or deleted within the database based on the
/// each object's current state.
/// </para><para>
/// All this is contingent on <see cref="IsDirty" />. If
/// this value is <see langword="false"/>, no data operation occurs.
/// It is also contingent on <see cref="IsValid" />. If this value is
/// <see langword="false"/> an exception will be thrown to
/// indicate that the UI attempted to save an invalid object.
/// </para><para>
/// It is important to note that this method returns a new version of the
/// business collection that contains any data updated during the save operation.
/// You MUST update all object references to use this new version of the
/// business collection in order to have access to the correct object data.
/// </para><para>
/// You can override this method to add your own custom behaviors to the save
/// operation. For instance, you may add some security checks to make sure
/// the user can save the object. If all security checks pass, you would then
/// invoke the base Save method via <c>MyBase.Save()</c>.
/// </para>
/// </remarks>
/// <returns>A new object containing the saved values.</returns>
public virtual T Save()
{
T result;
if (this.IsChild)
throw new NotSupportedException(Resources.NoSaveChildException);
if (_editLevel > 0)
throw new Validation.ValidationException(Resources.NoSaveEditingException);
if (!IsValid)
throw new Validation.ValidationException(Resources.NoSaveInvalidException);
if (IsDirty)
result = (T)DataPortal.Update(this);
else
result = (T)this;
OnSaved(result);
return result;
}
/// <summary>
/// Override this method to load a new business object with default
/// values from the database.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
protected virtual void DataPortal_Create()
{
throw new NotSupportedException(Resources.CreateNotSupportedException);
}
/// <summary>
/// Override this method to allow retrieval of an existing business
/// object based on data in the database.
/// </summary>
/// <param name="criteria">An object containing criteria values to identify the object.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
protected virtual void DataPortal_Fetch(object criteria)
{
throw new NotSupportedException(Resources.FetchNotSupportedException);
}
/// <summary>
/// Override this method to allow update of a business
/// object.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
protected virtual void DataPortal_Update()
{
throw new NotSupportedException(Resources.UpdateNotSupportedException);
}
/// <summary>
/// Override this method to allow immediate deletion of a business object.
/// </summary>
/// <param name="criteria">An object containing criteria values to identify the object.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
protected virtual void DataPortal_Delete(object criteria)
{
throw new NotSupportedException(Resources.DeleteNotSupportedException);
}
/// <summary>
/// Called by the server-side DataPortal prior to calling the
/// requested DataPortal_xyz method.
/// </summary>
/// <param name="e">The DataPortalContext object passed to the DataPortal.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected virtual void DataPortal_OnDataPortalInvoke(DataPortalEventArgs e)
{
}
/// <summary>
/// Called by the server-side DataPortal after calling the
/// requested DataPortal_xyz method.
/// </summary>
/// <param name="e">The DataPortalContext object passed to the DataPortal.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected virtual void DataPortal_OnDataPortalInvokeComplete(DataPortalEventArgs e)
{
}
/// <summary>
/// Called by the server-side DataPortal if an exception
/// occurs during data access.
/// </summary>
/// <param name="e">The DataPortalContext object passed to the DataPortal.</param>
/// <param name="ex">The Exception thrown during data access.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")]
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected virtual void DataPortal_OnDataPortalException(DataPortalEventArgs e, Exception ex)
{
}
#endregion
#region ISavable Members
object Csla.Core.ISavable.Save()
{
return Save();
}
[NonSerialized()]
[NotUndoable]
private EventHandler<Csla.Core.SavedEventArgs> _nonSerializableSavedHandlers;
[NotUndoable]
private EventHandler<Csla.Core.SavedEventArgs> _serializableSavedHandlers;
/// <summary>
/// Event raised when an object has been saved.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design",
"CA1062:ValidateArgumentsOfPublicMethods")]
public event EventHandler<Csla.Core.SavedEventArgs> Saved
{
add
{
if (value.Method.IsPublic &&
(value.Method.DeclaringType.IsSerializable ||
value.Method.IsStatic))
_serializableSavedHandlers = (EventHandler<Csla.Core.SavedEventArgs>)
System.Delegate.Combine(_serializableSavedHandlers, value);
else
_nonSerializableSavedHandlers = (EventHandler<Csla.Core.SavedEventArgs>)
System.Delegate.Combine(_nonSerializableSavedHandlers, value);
}
remove
{
if (value.Method.IsPublic &&
(value.Method.DeclaringType.IsSerializable ||
value.Method.IsStatic))
_serializableSavedHandlers = (EventHandler<Csla.Core.SavedEventArgs>)
System.Delegate.Remove(_serializableSavedHandlers, value);
else
_nonSerializableSavedHandlers = (EventHandler<Csla.Core.SavedEventArgs>)
System.Delegate.Remove(_nonSerializableSavedHandlers, value);
}
}
/// <summary>
/// Raises the <see cref="Saved"/> event, indicating that the
/// object has been saved, and providing a reference
/// to the new object instance.
/// </summary>
/// <param name="newObject">The new object instance.</param>
[System.ComponentModel.EditorBrowsable(EditorBrowsableState.Advanced)]
protected void OnSaved(T newObject)
{
Csla.Core.SavedEventArgs args = new Csla.Core.SavedEventArgs(newObject);
if (_nonSerializableSavedHandlers != null)
_nonSerializableSavedHandlers.Invoke(this, args);
if (_serializableSavedHandlers != null)
_serializableSavedHandlers.Invoke(this, args);
}
#endregion
}
}