Click here to Skip to main content
15,887,214 members
Articles / Desktop Programming / WPF

Item-Level Presentation Models for WPF

Rate me:
Please Sign up or sign in to vote.
4.65/5 (18 votes)
2 Apr 2011GPL39 min read 80K   6   74  
Make your life easier by inserting a Presentation Model layer (aka ViewModel) between your domain-model collection contents and template-generated WPF objects.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;

using HappyNomad.BidirectionalAssociations;
using HappyNomad.Outlining;

namespace HappyNomad.PresentationModel {

	/// <summary>Presentation-tailored hierarchical representation of a domain-model type</summary>
	/// <typeparam name="DMType">The domain-model type to be represented</typeparam>
	/// <typeparam name="PMType">The presentation-model type (this) doing the representing</typeparam>
	/// <author>Adrian Alexander</author>
	public abstract class HierarchicalPresentationModel<DMType, PMType> : ItemPresentationModel<DMType>
	  where DMType : class
	  where PMType : HierarchicalPresentationModel<DMType, PMType> {

		#region Object Creation

		private ICollection<DMType> subItemsSource;
		private bool useSourceIndex, cascadeDeletes;
		protected bool debug;

		/// <summary>
		/// Initializes this presentation-model node with the domain items to be
		/// represented, then subscribes for notification of future changes.
		/// </summary>
		/// <param name="domainItem">The domain item to be represented by this node</param>
		/// <param name="subItemsSource">The domain items to be represented as this node's sub-items.
		///   Null if root wrapper will be responsible for all add operations.</param>
		/// <param name="useSourceIndex">Specifies whether presentation-model subitems' positions here,
		///   are the same as in their corresponding subItemsSource</param>
		/// <param name="cascadeDeletes">Specifies whether subitems also get deleted once their parent is deleted.
		///   If false, then a deleted item's subitems are re-added at their parent's former location.</param>
		protected HierarchicalPresentationModel( DMType domainItem, ICollection<DMType> subItemsSource,
		  bool useSourceIndex, bool cascadeDeletes, bool debug ) : base( domainItem ) {

			this.useSourceIndex = useSourceIndex; this.cascadeDeletes = cascadeDeletes; this.debug = debug;
			
			this.subItemsSource = subItemsSource;
			if ( subItemsSource == null ) return; //If no recursion desired, then done.
			IList<DMType> sourceAsList = subItemsSource as IList<DMType>;

			if ( sourceAsList != null ) { //If the source is a list...
				for ( int i = 0; i < sourceAsList.Count; i++ )
					AddSubItem( CreateInstance(sourceAsList[i]), i ); //then iterate using its index. //-1
			} else { //If the source isn't a list...
				foreach ( DMType itemToAdd in subItemsSource )
					AddSubItem( CreateInstance(itemToAdd), -1 ); //then iterate w/o using an index.
			}
			SubscribeToSource(); //subscribe to source's change events
		}

		/// <summary>
		/// Creates a new presentation-model item that does not (yet) have any sub-items.
		/// </summary>
		/// <param name="domainItem">The domain item to be represented by the new instance</param>
		/// <returns>The newly-created instance</returns>
		protected abstract PMType CreateInstance( DMType domainItem );

		#endregion

		#region SubItems/Parent Properties

		private ObservableCollection<PMType> _subItems;
		private PMType parent;

		protected ObservableCollection<PMType> subItems {
			get {
				if ( _subItems == null ) {
					_subItems = new ObservableCollection<PMType>();
					( (INotifyCollectionChanged)_subItems ).CollectionChanged +=
						new OneToManyAssocSync( this, "Parent" ).UpdateManySide;
				}
				return _subItems;
			}
		}

		public PMType Parent {
			get { return parent; }
			private set {
				PMType oldParent = parent;
				parent = value;
				OneToManyAssocSync.UpdateOneSide<PMType>( (PMType)this, oldParent, parent, "subItems" );
			}
		}

		/// <summary>The sub-items of this presentation-model node, as a read-only collection</summary>
		/// <remarks>
		/// It is the responsibility of this object to ensure that this sub-items collection reflects 
		/// the contents of the corresponding domain-model collection.  It does this by listening for 
		/// changes to the source collection and updating itself accordingly.  It does not, however, 
		/// propagate changes in the other direction. This sub-items collection is therefore available 
		/// to the outside only in read-only form.
		/// </remarks>
		public ReadOnlyObservableCollection<PMType> SubItems {
			get { return new ReadOnlyObservableCollection<PMType>( subItems ); }
		}

		public bool HasSubItems {
			get { return _subItems != null && subItems.Count > 0; }
		}

		#endregion

		#region 'AddSubItem' Method

		/// <summary>
		/// Adds the presentation-model sub-item.
		/// </summary>
		/// <param name="wrapperToAdd">
		///   The presentation-model item to be added to the sub-items collection</param>
		/// <param name="sourceIndex">The position at which to add the child item, if applicable</param>
		private void AddSubItem( PMType wrapperToAdd, int sourceIndex ) {
			RelativePosition<PMType> desiredPos = DesiredPosition( wrapperToAdd.DomainItem );
			desiredPos.subItemsPropName = "subItems";
			if ( useSourceIndex && sourceIndex >= 0 ) desiredPos.childIndex = sourceIndex;
			if ( debug ) Console.WriteLine( "Adding {" + wrapperToAdd.DomainItem + "}, Positioning {" + desiredPos + "}" );
			OutliningUtil.Insert<PMType>( wrapperToAdd, desiredPos );
		}

		/// <summary>
		/// Determine the desired position for adding the sub-item.
		/// </summary>
		/// <param name="itemToAdd">
		///   The item to be added to the sub-items collection</param>
		protected abstract RelativePosition<PMType> DesiredPosition( DMType itemToAdd );

		#endregion

		#region 'SubItems' Change Notification

		/// <summary>
		/// The last sub-item removed; remains available until another sub-item is added.
		/// Useful when the item is being moved from one location (where it's just been
		/// deleted) in the hierarchy, to another (where it will next be re-added.
		/// </summary>
		private static PMType lastRemoved;

		/// <summary>
		/// Responds to the represented domain item's sub-items collection changing.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="args"></param>
		private void SubItemsSourceChanged( object sender, NotifyCollectionChangedEventArgs args ) {
			if ( args.Action == NotifyCollectionChangedAction.Add ) {
				//foreach ( DMType addingToCollection in e.NewItems )
				for ( int i = 0; i < args.NewItems.Count; i++ ) {
					DMType addingToCollection = (DMType)args.NewItems[i];
					if ( lastRemoved != null && lastRemoved.DomainItem == addingToCollection ) {
						AddSubItem( lastRemoved, args.NewStartingIndex + i );
						lastRemoved.SubscribeToSource();
					} else
						AddSubItem( CreateInstance(addingToCollection), args.NewStartingIndex + i ); //-1
					lastRemoved = null;
				}
			} else if ( args.Action == NotifyCollectionChangedAction.Remove ) {
				foreach ( DMType removingFromCollection in args.OldItems ) {
					PMType pm = FindDescendant( removingFromCollection );
					int lastRemovedIndex = pm.Parent.SubItems.IndexOf( pm );
					pm.Parent = null;
					pm.UnsubscribeFromSource();
					lastRemoved = pm;

					//When deletes don't cascade, re-add a deleted item's subitems:
					if ( !cascadeDeletes ) {
						IList<PMType> toReAdd = new List<PMType>( pm.SubItems );
						for ( int i = 0; i < toReAdd.Count; i++ )
							AddSubItem( toReAdd[i], lastRemovedIndex + i );
					}
				}
			}
		}

		/// <summary>
		/// Subscribe to the sub-items source's change events.
		/// </summary>
		private void SubscribeToSource() {
			if ( subItemsSource != null )
				( (INotifyCollectionChanged)subItemsSource ).CollectionChanged += SubItemsSourceChanged;
		}

		/// <summary>
		/// Unsubscribe from the sub-items source's change events.
		/// </summary>
		public void UnsubscribeFromSource() {
			if ( subItemsSource != null )
				( (INotifyCollectionChanged)subItemsSource ).CollectionChanged -= SubItemsSourceChanged;
		}

		#endregion

		#region 'IsExpanded' Property

		private bool isExpanded;

		/// <summary>
		/// Is the TreeViewItem associated with this object expanded?
		/// [Source: http://www.codeproject.com/KB/WPF/TreeViewWithViewModel.aspx ]
		/// </summary>
		public bool IsExpanded {
			get { return isExpanded; }
			set {
				if ( value != isExpanded ) {
					isExpanded = value;
					this.OnPropertyChanged( "IsExpanded" );
				}

				// Expand all the way up to the root:
				if ( isExpanded && Parent != null )
					Parent.IsExpanded = true;
			}
		}

		#endregion // IsExpanded

		#region Search/Traversal Methods

		/// <summary>
		/// Search within this presentation-model node, and its sub-nodes, using the supplied criteria.
		/// </summary>
		/// <typeparam name="T">Only check nodes of this type</typeparam>
		/// <param name="testNode">Criteria by which to choose which node to return</param>
		/// <returns>First node found that matches the given criteria</returns>
		public PMType FindDescendant<T>( Predicate<T> testNode ) where T : class, DMType {
			foreach ( PMType node in BreadthFirstNodeEnumerator ) {
				T domainItemAsT = node.DomainItem as T;
				if ( domainItemAsT != null && testNode(domainItemAsT) ) return node;
			}
			return null;
		}

		/// <summary>
		/// Search within this presentation-model node, and its sub-nodes, for the specified domain item.
		/// </summary>
		/// <param name="forDomainItem">
		///   Domain item for which to find a corresponding presentation-model</param>
		/// <returns>The presentation-model that represents the specified domain item</returns>
		public PMType FindDescendant( DMType forDomainItem ) {
			Predicate<DMType> testNode = delegate( DMType nodeToTest ) {
				return nodeToTest == forDomainItem;
			};
			return FindDescendant( testNode );
		}

		/// <summary>
		/// Search within this presentation-model node, and its sub-nodes, using the supplied criteria.
		/// </summary>
		/// <typeparam name="T">Only check nodes of this type</typeparam>
		/// <param name="testNode">Criteria by which to choose which nodes to return</param>
		/// <returns>All nodes found that match the given criteria</returns>
		public IList<PMType> FindDescendants<T>( Predicate<T> testNode ) where T : class, DMType {
			IList<PMType> result = new List<PMType>();
			foreach ( PMType node in BreadthFirstNodeEnumerator ) {
				T domainItemAsT = node.DomainItem as T;
				if ( domainItemAsT != null && testNode(domainItemAsT) )
					result.Add( node );
			}
			return result;
		}

		/// <summary>
		/// Returns an iterator that performs a breadth-first traversal of nodes.
		/// [Source: http://www.codeproject.com/KB/recipes/phSharpTree.aspx ]
		/// </summary>
		private IEnumerable<PMType> BreadthFirstNodeEnumerator {
			get {
				Queue<PMType> todo = new Queue<PMType>();
				todo.Enqueue( (PMType)this );
				while ( todo.Count > 0 ) {
					PMType node = todo.Dequeue();
					if ( node.HasSubItems ) {
						foreach ( PMType child in node.SubItems )
							todo.Enqueue( child );
					}
					yield return node;
				}
			}
		}

		#endregion

		#region 'IsAncestorOf' Method

		/// <summary>
		/// Is this an ancestor of the given node?
		/// </summary>
		/// <param name="maybeDescendant">A possible descendant of this node</param>
		/// <returns>Returns true if maybeDescendant, or any of its ancestors, are equal to this node</returns>
		public bool IsAncestorOf( PMType maybeDescendant ) {
			if ( maybeDescendant == this )
				return true; //target found!
			else if ( maybeDescendant.Parent == null )
				return false; //target not found & nowhere left to look
			else //target not found, so progress towards tree root:
				return IsAncestorOf( maybeDescendant.Parent );
		}

		#endregion

		#region 'UpdatePosition' Method

		public void UpdatePosition( DMType itemToUpdate ) {
			PMType wrapperToUpdate = FindDescendant( itemToUpdate );
			RelativePosition<PMType> desiredPos = DesiredPosition( itemToUpdate );

			if ( desiredPos.command == OutliningCommands.NewChild && wrapperToUpdate.subItems.Count > 0 ) {
				IList<PMType> wrapperToUpdate_SubItems = new List<PMType>( wrapperToUpdate.subItems );
				foreach ( PMType pm in wrapperToUpdate_SubItems ) {
					wrapperToUpdate.subItems.Remove( pm );
					wrapperToUpdate.Parent.subItems.Add( pm );
				}
			}
			if ( wrapperToUpdate.Parent != desiredPos.parent ) {
				wrapperToUpdate.Parent = null;
				AddSubItem( wrapperToUpdate, -1 );
			} else if ( desiredPos.command == OutliningCommands.NewParent &&
			  !CollectionsUtil.Equals(wrapperToUpdate.subItems, desiredPos.insertRelativeTo) ) {
				IList<PMType> wrapperToUpdate_SubItems = new List<PMType>( wrapperToUpdate.subItems );
				foreach ( PMType pm in wrapperToUpdate_SubItems ) {
					if ( !desiredPos.insertRelativeTo.Contains(pm) ) {
						wrapperToUpdate.subItems.Remove( pm );
						wrapperToUpdate.Parent.subItems.Add( pm );
					}
				}
				foreach ( PMType pm in desiredPos.insertRelativeTo ) {
					wrapperToUpdate.Parent.subItems.Remove( pm );
					wrapperToUpdate.subItems.Add( pm );
				}
			}
		}

		#endregion
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
United States United States
Adrian loves facilitating suave user experiences via the latest and greatest GUI technologies such as Windows 8 Metro-style apps as well as WPF. More generally, he finds joy in architecting software that is easy to comprehend and maintain. He does so by applying design patterns at the top-level, and by incessantly refactoring code at lower levels. He's always interested in hearing about opportunities for full or part-time development work. He resides in Pennsylvania but can potentially travel anywhere in the country. (Writing about himself in the third-person is Adrian's new hobby.)

Comments and Discussions