using System; using System.ComponentModel; using System.Collections.Generic; using System.Runtime.Serialization; using Csla.Properties; namespace Csla { /// /// This is the base class from which most business collections /// or lists will be derived. /// /// Type of the business object being defined. /// Type of the child objects contained in the list. [System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] [Serializable()] public abstract class BusinessListBase : Core.ExtendedBindingList, Core.IEditableCollection, ICloneable, Core.ISavable, Core.IParent where T : BusinessListBase where C : Core.IEditableBusinessObject { #region Constructors /// /// Creates an instance of the object. /// protected BusinessListBase() { Initialize(); } #endregion #region Initialize /// /// Override this method to set up event handlers so user /// code in a partial class can respond to events raised by /// generated code. /// protected virtual void Initialize() { /* allows subclass to initialize events before any other activity occurs */ } #endregion #region IsDirty, IsValid /// /// Gets a value indicating whether this object's data has been changed. /// 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; } } /// /// Gets a value indicating whether this object is currently in /// a valid state (has no broken validation rules). /// 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 /// /// Starts a nested edit on the object. /// /// /// /// 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 /// or committed by calling . /// /// 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. /// /// See Chapters 2 and 3 for details on n-level undo and state stacking. /// /// This method triggers the copying of all child object states. /// /// public void BeginEdit() { if (this.IsChild) throw new NotSupportedException(Resources.NoBeginEditChildException); CopyState(); } /// /// Cancels the current edit process, restoring the object's state to /// its previous values. /// /// /// 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 /// call. /// /// This method triggers an undo in all child objects. /// /// public void CancelEdit() { if (this.IsChild) throw new NotSupportedException(Resources.NoCancelEditChildException); UndoChanges(); } /// /// Commits the current edit process. /// /// /// 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 /// call. /// /// This method triggers an /// in all child objects. /// /// public void ApplyEdit() { if (this.IsChild) throw new NotSupportedException(Resources.NoApplyEditChildException); AcceptChanges(); } void Core.IParent.ApplyEditChild(Core.IEditableBusinessObject child) { EditChildComplete(child); } /// /// Override this method to be notified when a child object's /// method has /// completed. /// /// The child object that was edited. 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 _deletedList; /// /// A collection containing all child objects marked /// for deletion. /// [System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Design", "CA1002:DoNotExposeGenericLists")] [EditorBrowsable(EditorBrowsableState.Advanced)] protected List DeletedList { get { if (_deletedList == null) _deletedList = new List(); 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); } /// /// Returns if the internal deleted list /// contains the specified child object. /// /// Child object to check. [EditorBrowsable(EditorBrowsableState.Advanced)] public bool ContainsDeleted(C item) { return DeletedList.Contains(item); } #endregion #region Insert, Remove, Clear /// /// This method is called by a child object when it /// wants to be removed from the collection. /// /// The child object to remove. void Core.IEditableCollection.RemoveChild(Csla.Core.IEditableBusinessObject child) { Remove((C)child); } /// /// This method is called by a child object when it /// wants to be removed from the collection. /// /// The child object to remove. void Core.IParent.RemoveChild(Csla.Core.IEditableBusinessObject child) { Remove((C)child); } /// /// Sets the edit level of the child object as it is added. /// /// Index of the item to insert. /// Item to insert. 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); } /// /// Marks the child object for deletion and moves it to /// the collection of deleted objects. /// /// Index of the item to remove. 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); } /// /// Clears the collection, moving all active /// items to the deleted list. /// protected override void ClearItems() { while (base.Count > 0) RemoveItem(0); base.ClearItems(); } /// /// Replaces the item at the specified index with /// the specified item, first moving the original /// item to the deleted list. /// /// The zero-based index of the item to replace. /// /// The new value for the item at the specified index. /// The value can be null for reference types. /// /// 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; /// /// Returns the current edit level of the object. /// [EditorBrowsable(EditorBrowsableState.Never)] protected int EditLevel { get { return _editLevel; } } #endregion #region IsChild [NotUndoable()] private bool _isChild = false; /// /// Indicates whether this collection object is a child object. /// /// True if this is a child object. protected bool IsChild { get { return _isChild; } } /// /// Marks the object as being a child object. /// /// /// /// By default all business objects are 'parent' objects. This means /// that they can be directly retrieved and updated into the database. /// /// 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. /// /// 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. /// /// protected void MarkAsChild() { _isChild = true; } #endregion #region ICloneable object ICloneable.Clone() { return GetClone(); } /// /// Creates a clone of the object. /// /// A new object containing the exact data of the original object. [EditorBrowsable(EditorBrowsableState.Advanced)] protected virtual object GetClone() { return Core.ObjectCloner.Clone(this); } /// /// Creates a clone of the object. /// /// A new object containing the exact data of the original object. 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); } /// /// This method is called on a newly deserialized object /// after deserialization is complete. /// [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 /// /// Saves the object to the database. /// /// /// /// 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. /// /// All this is contingent on . If /// this value is , no data operation occurs. /// It is also contingent on . If this value is /// an exception will be thrown to /// indicate that the UI attempted to save an invalid object. /// /// 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. /// /// 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 MyBase.Save(). /// /// /// A new object containing the saved values. 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; } /// /// Override this method to load a new business object with default /// values from the database. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")] protected virtual void DataPortal_Create() { throw new NotSupportedException(Resources.CreateNotSupportedException); } /// /// Override this method to allow retrieval of an existing business /// object based on data in the database. /// /// An object containing criteria values to identify the object. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")] protected virtual void DataPortal_Fetch(object criteria) { throw new NotSupportedException(Resources.FetchNotSupportedException); } /// /// Override this method to allow update of a business /// object. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")] protected virtual void DataPortal_Update() { throw new NotSupportedException(Resources.UpdateNotSupportedException); } /// /// Override this method to allow immediate deletion of a business object. /// /// An object containing criteria values to identify the object. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")] protected virtual void DataPortal_Delete(object criteria) { throw new NotSupportedException(Resources.DeleteNotSupportedException); } /// /// Called by the server-side DataPortal prior to calling the /// requested DataPortal_xyz method. /// /// The DataPortalContext object passed to the DataPortal. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")] [EditorBrowsable(EditorBrowsableState.Advanced)] protected virtual void DataPortal_OnDataPortalInvoke(DataPortalEventArgs e) { } /// /// Called by the server-side DataPortal after calling the /// requested DataPortal_xyz method. /// /// The DataPortalContext object passed to the DataPortal. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", MessageId = "Member")] [EditorBrowsable(EditorBrowsableState.Advanced)] protected virtual void DataPortal_OnDataPortalInvokeComplete(DataPortalEventArgs e) { } /// /// Called by the server-side DataPortal if an exception /// occurs during data access. /// /// The DataPortalContext object passed to the DataPortal. /// The Exception thrown during data access. [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 _nonSerializableSavedHandlers; [NotUndoable] private EventHandler _serializableSavedHandlers; /// /// Event raised when an object has been saved. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")] public event EventHandler Saved { add { if (value.Method.IsPublic && (value.Method.DeclaringType.IsSerializable || value.Method.IsStatic)) _serializableSavedHandlers = (EventHandler) System.Delegate.Combine(_serializableSavedHandlers, value); else _nonSerializableSavedHandlers = (EventHandler) System.Delegate.Combine(_nonSerializableSavedHandlers, value); } remove { if (value.Method.IsPublic && (value.Method.DeclaringType.IsSerializable || value.Method.IsStatic)) _serializableSavedHandlers = (EventHandler) System.Delegate.Remove(_serializableSavedHandlers, value); else _nonSerializableSavedHandlers = (EventHandler) System.Delegate.Remove(_nonSerializableSavedHandlers, value); } } /// /// Raises the event, indicating that the /// object has been saved, and providing a reference /// to the new object instance. /// /// The new object instance. [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 } }