Browse Source

Replaced delegates with new interface ITreeBuilder

tznind 4 years ago
parent
commit
8fe8128b0b
3 changed files with 163 additions and 56 deletions
  1. 147 40
      Terminal.Gui/Views/TreeView.cs
  2. 9 8
      UICatalog/Scenarios/TreeViewFileSystem.cs
  3. 7 8
      UnitTests/TreeViewTests.cs

+ 147 - 40
Terminal.Gui/Views/TreeView.cs

@@ -64,30 +64,146 @@ namespace Terminal.Gui {
 		{
 			Text = text;
 		}
+	}
+
+	/// <summary>
+	/// Interface for supplying data to a <see cref="TreeView"/> on demand as root level nodes are expanded by the user
+	/// </summary>
+	public interface ITreeBuilder
+	{
+		/// <summary>
+		/// Returns true if <see cref="CanExpand"/> is implemented by this class
+		/// </summary>
+		/// <value></value>
+		bool SupportsCanExpand {get;}
 
+		/// <summary>
+		/// Returns true/false for whether a model has children.  This method should be implemented when <see cref="GetChildren"/> is an expensive operation otherwise <see cref="SupportsCanExpand"/> should return false (in which case this method will not be called)
+		/// </summary>
+		/// <param name="model"></param>
+		/// <returns></returns>
+		bool CanExpand(object model);
 
+		/// <summary>
+		/// Returns all children of a given <paramref name="model"/> which should be added to the tree as new branches underneath it
+		/// </summary>
+		/// <param name="model"></param>
+		/// <returns></returns>
+		IEnumerable<object> GetChildren(object model);
 	}
 
 	/// <summary>
-	/// Hierarchical tree view with expandable branches.  Branch objects are dynamically determined when expanded using a user defined <see cref="ChildrenGetterDelegate"/>
+	/// Abstract implementation of <see cref="ITreeBuilder"/>
 	/// </summary>
-	public class TreeView : View
-	{   
+	public abstract class TreeBuilder : ITreeBuilder {
+
+		/// <inheritdoc/>
+		public bool SupportsCanExpand { get; protected set;} = false;
+
 		/// <summary>
-		/// Default implementation of a <see cref="ChildrenGetterDelegate"/>.  Supports returning children of <see cref="ITreeNode"/> or otherwise returns an empty collection (i.e. no children)
+		/// Override this method to return a rapid answer as to whether <see cref="GetChildren(object)"/> returns results.
 		/// </summary>
-		static ChildrenGetterDelegate DefaultChildrenGetter = (s)=>{return s is ITreeNode n ? n.Children : Enumerable.Empty<object>();};
+		/// <param name="model"></param>
+		/// <returns></returns>
+		public virtual bool CanExpand (object model){
+			
+			return GetChildren(model).Any();
+		}
+
+		/// <inheritdoc/>
+		public abstract IEnumerable<object> GetChildren (object model);
 
 		/// <summary>
-		/// This is the delegate that will be used to fetch the children of a model object
+		/// Constructs base and initializes <see cref="SupportsCanExpand"/>
 		/// </summary>
-		public ChildrenGetterDelegate ChildrenGetter {
-			get { return childrenGetter ?? DefaultChildrenGetter; }
-			set { childrenGetter = value; }
+		/// <param name="supportsCanExpand">Pass true if you intend to implement <see cref="CanExpand(object)"/> otherwise false</param>
+		public TreeBuilder(bool supportsCanExpand)
+		{
+			SupportsCanExpand = supportsCanExpand;
 		}
-	
-		private ChildrenGetterDelegate childrenGetter;
-		private CanExpandGetterDelegate canExpandGetter;
+	}
+
+	/// <summary>
+	/// <see cref="ITreeBuilder"/> implementation for <see cref="TreeNode"/> objects
+	/// </summary>
+	public class TreeNodeBuilder : TreeBuilder {
+		
+		/// <summary>
+		/// Initialises a new instance of builder for any model objects of Type <see cref="ITreeNode"/>
+		/// </summary>
+		public TreeNodeBuilder():base(false)
+		{
+			
+		}
+
+		/// <summary>
+		/// Returns <see cref="ITreeNode.Children"/> from <paramref name="model"/>
+		/// </summary>
+		/// <param name="model"></param>
+		/// <returns></returns>
+		public override IEnumerable<object> GetChildren (object model)
+		{
+			return model is ITreeNode n ? n.Children : Enumerable.Empty<object>();
+		}
+	}
+
+	/// <summary>
+	/// Implementation of <see cref="ITreeBuilder"/> that uses user defined functions
+	/// </summary>
+	public class DelegateTreeBuilder : TreeBuilder
+	{
+		private Func<object,IEnumerable<object>> childGetter;
+		private Func<object,bool> canExpand;
+
+		/// <summary>
+		/// Constructs an implementation of <see cref="ITreeBuilder"/> that calls the user defined method <paramref name="childGetter"/> to determine children
+		/// </summary>
+		/// <param name="childGetter"></param>
+		/// <returns></returns>
+   		public DelegateTreeBuilder(Func<object,IEnumerable<object>> childGetter) : base(false)
+		{
+			this.childGetter = childGetter;
+		}
+
+		/// <summary>
+		/// Constructs an implementation of <see cref="ITreeBuilder"/> that calls the user defined method <paramref name="childGetter"/> to determine children and <paramref name="canExpand"/> to determine expandability
+		/// </summary>
+		/// <param name="childGetter"></param>
+		/// <param name="canExpand"></param>
+		/// <returns></returns>
+		public DelegateTreeBuilder(Func<object,IEnumerable<object>> childGetter, Func<object,bool> canExpand) : base(true)
+		{
+			this.childGetter = childGetter;
+			this.canExpand = canExpand;
+		}
+
+		/// <summary>
+		/// Returns whether a node can be expanded based on the delegate passed during construction
+		/// </summary>
+		/// <param name="model"></param>
+		/// <returns></returns>
+		public override bool CanExpand (object model)
+		{
+			return canExpand?.Invoke(model) ?? base.CanExpand (model);
+		}
+
+		/// <summary>
+		/// Returns children using the delegate method passed during construction
+		/// </summary>
+		/// <param name="model"></param>
+		/// <returns></returns>
+		public override IEnumerable<object> GetChildren (object model)
+		{
+			return childGetter.Invoke(model);
+		}
+	}
+
+
+	/// <summary>
+	/// Hierarchical tree view with expandable branches.  Branch objects are dynamically determined when expanded using a user defined <see cref="ITreeBuilder"/>
+	/// </summary>
+	public class TreeView : View
+	{   
 		private int scrollOffset;
 
 		/// <summary>
@@ -97,13 +213,10 @@ namespace Terminal.Gui {
 		public bool ShowBranchLines {get;set;} = true;
 
 		/// <summary>
-		/// Optional delegate where <see cref="ChildrenGetter"/> is expensive.  This should quickly return true/false for whether an object is expandable.  (e.g. indicating to a user that all folders can be expanded because they are folders without having to calculate contents)
+		/// Determines how sub branches of the tree are dynamically built at runtime as the user expands root nodes
 		/// </summary>
-		/// <remarks>When this is null <see cref="ChildrenGetter"/> is used directly to determine if a node should be expandable</remarks>
-		public CanExpandGetterDelegate CanExpandGetter {
-			get { return canExpandGetter; }
-			set { canExpandGetter = value; }
-		}
+		/// <value></value>
+		public ITreeBuilder TreeBuilder { get;set;}
 
 		/// <summary>
 		/// private variable for <see cref="SelectedObject"/>
@@ -179,11 +292,20 @@ namespace Terminal.Gui {
 
 
 		/// <summary>
-		/// Creates a new tree view with absolute positioning.  Use <see cref="AddObjects(IEnumerable{object})"/> to set set root objects for the tree
+		/// Creates a new tree view with absolute positioning.  Use <see cref="AddObjects(IEnumerable{object})"/> to set set root objects for the tree.  
 		/// </summary>
-		public TreeView ():base()
+		public TreeView():base()
 		{
 			CanFocus = true;
+			TreeBuilder = new TreeNodeBuilder();
+		}
+
+		/// <summary>
+		/// Initialises <see cref="TreeBuilder"/>.Creates a new tree view with absolute positioning.  Use <see cref="AddObjects(IEnumerable{object})"/> to set set root objects for the tree.
+		/// </summary>
+		public TreeView(ITreeBuilder builder) : this()
+		{
+			TreeBuilder = builder;
 		}
 
 		/// <summary>
@@ -580,10 +702,10 @@ namespace Terminal.Gui {
 		/// </summary>
 		public virtual void FetchChildren()
 		{
-			if (tree.ChildrenGetter == null)
+			if (tree.TreeBuilder == null)
 				return;
 
-			var children = tree.ChildrenGetter(this.Model) ?? new object[0];
+			var children = tree.TreeBuilder.GetChildren(this.Model) ?? Enumerable.Empty<object>();
 
 			this.ChildBranches = children.ToDictionary(k=>k,val=>new Branch(tree,this,val));
 		}
@@ -684,8 +806,8 @@ namespace Terminal.Gui {
 			if(ChildBranches == null) {
 			
 				//if there is a rapid method for determining whether there are children
-				if(tree.CanExpandGetter != null) {
-					return tree.CanExpandGetter(Model) ? tree.ExpandableSymbol : leafSymbol;
+				if(tree.TreeBuilder.SupportsCanExpand) {
+					return tree.TreeBuilder.CanExpand(Model) ? tree.ExpandableSymbol : leafSymbol;
 				}
 				
 				//there is no way of knowing whether we can expand without fetching the children
@@ -736,7 +858,7 @@ namespace Terminal.Gui {
 				// we already knew about some children so preserve the state of the old children
 
 				// first gather the new Children
-				var newChildren = tree.ChildrenGetter(this.Model) ?? new object[0];
+				var newChildren = tree.TreeBuilder?.GetChildren(this.Model) ?? Enumerable.Empty<object>();
 
 				// Children who no longer appear need to go
 				foreach(var toRemove in ChildBranches.Keys.Except(newChildren).ToArray())
@@ -800,13 +922,6 @@ namespace Terminal.Gui {
 			return Parent.ChildBranches.Values.LastOrDefault() == this;
 		}
 	}
-   
-	/// <summary>
-	/// Delegates of this type are used to fetch the children of the given model object
-	/// </summary>
-	/// <param name="model">The parent whose children should be fetched</param>
-	/// <returns>An enumerable over the children</returns>
-	public delegate IEnumerable<object> ChildrenGetterDelegate(object model);
 
 	/// <summary>
 	/// Delegates of this type are used to fetch string representations of user's model objects
@@ -814,14 +929,6 @@ namespace Terminal.Gui {
 	/// <param name="model"></param>
 	/// <returns></returns>
 	public delegate string AspectGetterDelegate(object model);
-
-	/// <summary>
-	/// Delegates of this type are used to quickly display to the user whether a given user object can be expanded when fetching it's children is expensive (e.g. indicating to a user that all 1000 folders can be expanded because they are folders without having to calculate contents)
-	/// </summary>
-	/// <param name="model"></param>
-	/// <returns></returns>
-	public delegate bool CanExpandGetterDelegate(object model);
-
 	
 	/// <summary>
 	/// Event arguments describing a change in selected object in a tree view

+ 9 - 8
UICatalog/Scenarios/TreeViewFileSystem.cs

@@ -73,12 +73,14 @@ namespace UICatalog.Scenarios {
 		/// </summary>
 		private void SetupFileSystemDelegates ()
 		{
-			
-			// As a shortcut to enumerating half the file system, tell tree that all directories are expandable (even if they turn out to be empty later on)
-			_treeView.CanExpandGetter = (o)=>o is DirectoryInfo;
+			_treeView.TreeBuilder = new DelegateTreeBuilder(
+
+				// Determines how to compute children of any given branch
+				GetChildren,
+				// As a shortcut to enumerating half the file system, tell tree that all directories are expandable (even if they turn out to be empty later on)				
+				(o)=>o is DirectoryInfo
 
-			// Determines how to compute children of any given branch
-			_treeView.ChildrenGetter = GetChildren;
+			);
 
 			// Determines how to represent objects as strings on the screen
 			_treeView.AspectGetter = AspectGetter;
@@ -88,9 +90,8 @@ namespace UICatalog.Scenarios {
 		{
 			ClearObjects();
 		
-			// Clear any previous delegates
-			_treeView.CanExpandGetter = null;
-			_treeView.ChildrenGetter = null;
+			// Set builder to serve children of ITreeNode objects
+			_treeView.TreeBuilder = new TreeNodeBuilder();
 
 			// Add 2 root nodes with simple set of subfolders
 			_treeView.AddObject(CreateSimpleRoot());

+ 7 - 8
UnitTests/TreeViewTests.cs

@@ -33,8 +33,7 @@ namespace UnitTests {
 				Cars = new []{car1 ,car2}
 			};
 			
-			var tree = new TreeView();
-			tree.ChildrenGetter = (s)=> s is Factory f ? f.Cars: null;
+			var tree = new TreeView(new DelegateTreeBuilder((s)=> s is Factory f ? f.Cars: null));
 			tree.AddObject(factory1);
 
 			return tree;
@@ -219,11 +218,11 @@ namespace UnitTests {
 			Assert.False(tree.IsExpanded(c1));
 
 			// change the children getter so that now cars can have wheels
-			tree.ChildrenGetter = (o)=>
+			tree.TreeBuilder = new DelegateTreeBuilder((o)=>
 				// factories have cars
 				o is Factory ? new object[]{c1,c2} 
 				// cars have wheels
-				: new object[]{wheel };
+				: new object[]{wheel });
 			
 			// still cannot expand
 			tree.Expand(c1);
@@ -256,11 +255,11 @@ namespace UnitTests {
 			Assert.False(tree.IsExpanded(c1));
 
 			// change the children getter so that now cars can have wheels
-			tree.ChildrenGetter = (o)=>
+			tree.TreeBuilder = new DelegateTreeBuilder((o)=>
 				// factories have cars
 				o is Factory ? new object[]{c1,c2} 
 				// cars have wheels
-				: new object[]{wheel };
+				: new object[]{wheel });
 			
 			// still cannot expand
 			tree.Expand(c1);
@@ -333,7 +332,7 @@ namespace UnitTests {
 			string root = "root";
 			
 			var tree = new TreeView();
-			tree.ChildrenGetter = (s)=>  ReferenceEquals(s , root) ? new object[]{obj1 } : null;
+			tree.TreeBuilder = new DelegateTreeBuilder((s)=>  ReferenceEquals(s , root) ? new object[]{obj1 } : null);
 			tree.AddObject(root);
 
 			// Tree is not expanded so the root has no children yet
@@ -346,7 +345,7 @@ namespace UnitTests {
 			Assert.Equal(1,tree.GetChildren(root).Count(child=>ReferenceEquals(obj1,child)));
 
 			// change the getter to return an Equal object (but not the same reference - obj2)
-			tree.ChildrenGetter = (s)=>  ReferenceEquals(s , root) ? new object[]{obj2 } : null;
+			tree.TreeBuilder = new DelegateTreeBuilder((s)=>  ReferenceEquals(s , root) ? new object[]{obj2 } : null);
 
 			// tree has cached the knowledge of what children the root has so won't know about the change (we still get obj1)
 			Assert.Equal(1,tree.GetChildren(root).Count(child=>ReferenceEquals(obj1,child)));