2
0
Эх сурвалжийг харах

Fixes #2944. `TreeView` unit tests fail intermittently: Ensures `Attribute` and `Color` are read only value types (#3163)

* Removed resharper settings from editorconfig

* Initial work in progress

* API doc

* API doc

* initial commit

* Fixed color and attribute issues

* Made Color a value type (readonly struct) etc...

* Removed stuff from other PR

* Ensured Colors.ColorScheme has private setter

* oops

* oops 2

* Code cleanup

* Code cleanup

* oops 4

* Reverted Attrivte.Default to be readonly static

* Fixed [Fact] [AutoInitShutdown]
Tig 1 жил өмнө
parent
commit
6ad36b7ac9

+ 79 - 81
Terminal.Gui/Configuration/AttributeJsonConverter.cs

@@ -1,99 +1,97 @@
 using System;
 using System.Text.Json;
 using System.Text.Json.Serialization;
-using Terminal.Gui;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Json converter fro the <see cref="Attribute"/> class.
-	/// </summary>
-	class AttributeJsonConverter : JsonConverter<Attribute> {
-		private static AttributeJsonConverter instance;
+namespace Terminal.Gui; 
 
-		/// <summary>
-		/// 
-		/// </summary>
-		public static AttributeJsonConverter Instance {
-			get {
-				if (instance == null) {
-					instance = new AttributeJsonConverter ();
-				}
+/// <summary>
+/// Json converter fro the <see cref="Attribute"/> class.
+/// </summary>
+class AttributeJsonConverter : JsonConverter<Attribute> {
+	static AttributeJsonConverter _instance;
 
-				return instance;
+	/// <summary>
+	/// 
+	/// </summary>
+	public static AttributeJsonConverter Instance {
+		get {
+			if (_instance == null) {
+				_instance = new AttributeJsonConverter ();
 			}
+
+			return _instance;
 		}
+	}
 
-		public override Attribute Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-		{
-			if (reader.TokenType != JsonTokenType.StartObject) {
-				throw new JsonException ($"Unexpected StartObject token when parsing Attribute: {reader.TokenType}.");
-			}
+	public override Attribute Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+	{
+		if (reader.TokenType != JsonTokenType.StartObject) {
+			throw new JsonException ($"Unexpected StartObject token when parsing Attribute: {reader.TokenType}.");
+		}
 
-			Attribute attribute = new Attribute ();
-			Color foreground = null;
-			Color background = null;
-			while (reader.Read ()) {
-				if (reader.TokenType == JsonTokenType.EndObject) {
-					if (foreground == null || background == null) {
-						throw new JsonException ($"Both Foreground and Background colors must be provided.");
-					} 
-					return new Attribute (foreground, background);
+		var attribute = new Attribute ();
+		Color? foreground = null;
+		Color? background = null;
+		while (reader.Read ()) {
+			if (reader.TokenType == JsonTokenType.EndObject) {
+				if (foreground == null || background == null) {
+					throw new JsonException ("Both Foreground and Background colors must be provided.");
 				}
+				return new Attribute (foreground.Value, background.Value);
+			}
 
-				if (reader.TokenType != JsonTokenType.PropertyName) {
-					throw new JsonException ($"Unexpected token when parsing Attribute: {reader.TokenType}.");
-				}
+			if (reader.TokenType != JsonTokenType.PropertyName) {
+				throw new JsonException ($"Unexpected token when parsing Attribute: {reader.TokenType}.");
+			}
 
-				string propertyName = reader.GetString ();
-				reader.Read ();
-				string color = $"\"{reader.GetString ()}\"";
+			var propertyName = reader.GetString ();
+			reader.Read ();
+			var color = $"\"{reader.GetString ()}\"";
 
-				switch (propertyName.ToLower ()) {
-				case "foreground":
-					foreground = JsonSerializer.Deserialize<Color> (color, options);
-					break;
-				case "background":
-					background = JsonSerializer.Deserialize<Color> (color, options);
-					break;
-				//case "bright":
-				//case "bold":
-				//	attribute.Bright = reader.GetBoolean ();
-				//	break;
-				//case "dim":
-				//	attribute.Dim = reader.GetBoolean ();
-				//	break;
-				//case "underline":
-				//	attribute.Underline = reader.GetBoolean ();
-				//	break;
-				//case "blink":
-				//	attribute.Blink = reader.GetBoolean ();
-				//	break;
-				//case "reverse":
-				//	attribute.Reverse = reader.GetBoolean ();
-				//	break;
-				//case "hidden":
-				//	attribute.Hidden = reader.GetBoolean ();
-				//	break;
-				//case "strike-through":
-				//	attribute.StrikeThrough = reader.GetBoolean ();
-				//	break;				
-				default:
-					throw new JsonException ($"Unknown Attribute property {propertyName}.");
-				}
+			switch (propertyName?.ToLower ()) {
+			case "foreground":
+				foreground = JsonSerializer.Deserialize<Color> (color, options);
+				break;
+			case "background":
+				background = JsonSerializer.Deserialize<Color> (color, options);
+				break;
+			//case "bright":
+			//case "bold":
+			//	attribute.Bright = reader.GetBoolean ();
+			//	break;
+			//case "dim":
+			//	attribute.Dim = reader.GetBoolean ();
+			//	break;
+			//case "underline":
+			//	attribute.Underline = reader.GetBoolean ();
+			//	break;
+			//case "blink":
+			//	attribute.Blink = reader.GetBoolean ();
+			//	break;
+			//case "reverse":
+			//	attribute.Reverse = reader.GetBoolean ();
+			//	break;
+			//case "hidden":
+			//	attribute.Hidden = reader.GetBoolean ();
+			//	break;
+			//case "strike-through":
+			//	attribute.StrikeThrough = reader.GetBoolean ();
+			//	break;				
+			default:
+				throw new JsonException ($"Unknown Attribute property {propertyName}.");
 			}
-			throw new JsonException ();
-		}
-
-		public override void Write (Utf8JsonWriter writer, Attribute value, JsonSerializerOptions options)
-		{
-			writer.WriteStartObject ();
-			writer.WritePropertyName (nameof(Attribute.Foreground));
-			ColorJsonConverter.Instance.Write (writer, value.Foreground, options);
-			writer.WritePropertyName (nameof (Attribute.Background));
-			ColorJsonConverter.Instance.Write (writer, value.Background, options);
-			
-			writer.WriteEndObject ();
 		}
+		throw new JsonException ();
 	}
-}
 
+	public override void Write (Utf8JsonWriter writer, Attribute value, JsonSerializerOptions options)
+	{
+		writer.WriteStartObject ();
+		writer.WritePropertyName (nameof (Attribute.Foreground));
+		ColorJsonConverter.Instance.Write (writer, value.Foreground, options);
+		writer.WritePropertyName (nameof (Attribute.Background));
+		ColorJsonConverter.Instance.Write (writer, value.Background, options);
+
+		writer.WriteEndObject ();
+	}
+}

+ 27 - 35
Terminal.Gui/Configuration/ConfigProperty.cs

@@ -1,31 +1,41 @@
-using System;
+#nullable enable
+
+using System;
 using System.Reflection;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 
-#nullable enable
-
 namespace Terminal.Gui;
 
 /// <summary>
-/// Holds a property's value and the <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/> 
+/// Holds a property's value and the <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/>
 /// to get and set the property's value.
 /// </summary>
 /// <remarks>
-/// Configuration properties must be <see langword="public"/> and <see langword="static"/> 
+/// Configuration properties must be <see langword="public"/> and <see langword="static"/>
 /// and have the <see cref="SerializableConfigurationProperty"/>
-/// attribute. If the type of the property requires specialized JSON serialization, 
-/// a <see cref="JsonConverter"/> must be provided using 
+/// attribute. If the type of the property requires specialized JSON serialization,
+/// a <see cref="JsonConverter"/> must be provided using
 /// the <see cref="JsonConverterAttribute"/> attribute.
 /// </remarks>
 public class ConfigProperty {
-	private object? propertyValue;
 
 	/// <summary>
 	/// Describes the property.
 	/// </summary>
 	public PropertyInfo? PropertyInfo { get; set; }
 
+	/// <summary>
+	/// Holds the property's value as it was either read from the class's implementation or from a config file.
+	/// If the property has not been set (e.g. because no configuration file specified a value),
+	/// this will be <see langword="null"/>.
+	/// </summary>
+	/// <remarks>
+	/// On <see langword="set"/>, performs a sparse-copy of the new value to the existing value (only copies elements of
+	/// the object that are non-null).
+	/// </remarks>
+	public object? PropertyValue { get; set; }
+
 	/// <summary>
 	/// Helper to get either the Json property named (specified by [JsonPropertyName(name)]
 	/// or the actual property name.
@@ -38,22 +48,6 @@ public class ConfigProperty {
 		return jpna?.Name ?? pi.Name;
 	}
 
-	/// <summary>
-	/// Holds the property's value as it was either read from the class's implementation or from a config file. 
-	/// If the property has not been set (e.g. because no configuration file specified a value), 
-	/// this will be <see langword="null"/>.
-	/// </summary>
-	/// <remarks>
-	/// On <see langword="set"/>, performs a sparse-copy of the new value to the existing value (only copies elements of 
-	/// the object that are non-null).
-	/// </remarks>
-	public object? PropertyValue {
-		get => propertyValue;
-		set {
-			propertyValue = value;
-		}
-	}
-
 	internal object? UpdateValueFrom (object source)
 	{
 		if (source == null) {
@@ -61,11 +55,11 @@ public class ConfigProperty {
 		}
 
 		var ut = Nullable.GetUnderlyingType (PropertyInfo!.PropertyType);
-		if (source.GetType () != PropertyInfo!.PropertyType && (ut != null && source.GetType () != ut)) {
+		if (source.GetType () != PropertyInfo!.PropertyType && ut != null && source.GetType () != ut) {
 			throw new ArgumentException ($"The source object ({PropertyInfo!.DeclaringType}.{PropertyInfo!.Name}) is not of type {PropertyInfo!.PropertyType}.");
 		}
-		if (PropertyValue != null && source != null) {
-			PropertyValue = ConfigurationManager.DeepMemberwiseCopy (source, PropertyValue);
+		if (PropertyValue != null) {
+			PropertyValue = DeepMemberwiseCopy (source, PropertyValue);
 		} else {
 			PropertyValue = source;
 		}
@@ -78,10 +72,7 @@ public class ConfigProperty {
 	/// into <see cref="PropertyValue"/>.
 	/// </summary>
 	/// <returns></returns>
-	public object? RetrieveValue ()
-	{
-		return PropertyValue = PropertyInfo!.GetValue (null);
-	}
+	public object? RetrieveValue () => PropertyValue = PropertyInfo!.GetValue (null);
 
 	/// <summary>
 	/// Applies the <see cref="PropertyValue"/> to the property described by <see cref="PropertyInfo"/>.
@@ -91,12 +82,12 @@ public class ConfigProperty {
 	{
 		if (PropertyValue != null) {
 			try {
-				PropertyInfo?.SetValue (null, ConfigurationManager.DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null)));
+				PropertyInfo?.SetValue (null, DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null)));
 			} catch (TargetInvocationException tie) {
 				// Check if there is an inner exception
 				if (tie.InnerException != null) {
 					// Handle the inner exception separately without catching the outer exception
-					Exception innerException = tie.InnerException;
+					var innerException = tie.InnerException;
 
 					// Handle the inner exception here
 					throw new JsonException ($"Error Applying Configuration Change: {innerException.Message}", innerException);
@@ -104,9 +95,10 @@ public class ConfigProperty {
 
 				// Handle the outer exception or rethrow it if needed
 				throw new JsonException ($"Error Applying Configuration Change: {tie.Message}", tie);
+			} catch (ArgumentException ae) {
+				throw new JsonException ($"Error Applying Configuration Change ({PropertyInfo?.Name}): {ae.Message}", ae);
 			}
 		}
 		return PropertyValue != null;
 	}
-
-}
+}

+ 146 - 191
Terminal.Gui/Configuration/ConfigurationManager.cs

@@ -1,6 +1,5 @@
 global using static Terminal.Gui.ConfigurationManager;
 global using CM = Terminal.Gui.ConfigurationManager;
-
 using System;
 using System.Collections;
 using System.Collections.Generic;
@@ -12,7 +11,6 @@ using System.Text;
 using System.Text.Encodings.Web;
 using System.Text.Json;
 using System.Text.Json.Serialization;
-using static Terminal.Gui.SpinnerStyle;
 
 
 #nullable enable
@@ -20,87 +18,124 @@ using static Terminal.Gui.SpinnerStyle;
 namespace Terminal.Gui;
 
 /// <summary>
-/// Provides settings and configuration management for Terminal.Gui applications. 
+/// Provides settings and configuration management for Terminal.Gui applications.
 /// <para>
-/// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted configuration files.
-/// The configuration files can be placed in at <c>.tui</c> folder in the user's home directory (e.g. <c>C:/Users/username/.tui</c>, 
+/// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted configuration
+/// files.
+/// The configuration files can be placed in at <c>.tui</c> folder in the user's home directory (e.g.
+/// <c>C:/Users/username/.tui</c>,
 /// or <c>/usr/username/.tui</c>),
 /// the folder where the Terminal.Gui application was launched from (e.g. <c>./.tui</c>), or as a resource
-/// within the Terminal.Gui application's main assembly. 
+/// within the Terminal.Gui application's main assembly.
 /// </para>
 /// <para>
-/// Settings are defined in JSON format, according to this schema: 
-///	https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
+/// Settings are defined in JSON format, according to this schema:
+/// https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
 /// </para>
 /// <para>
-/// Settings that will apply to all applications (global settings) reside in files named <c>config.json</c>. Settings 
+/// Settings that will apply to all applications (global settings) reside in files named <c>config.json</c>. Settings
 /// that will apply to a specific Terminal.Gui application reside in files named <c>appname.config.json</c>,
 /// where <c>appname</c> is the assembly name of the application (e.g. <c>UICatalog.config.json</c>).
 /// </para>
 /// Settings are applied using the following precedence (higher precedence settings
 /// overwrite lower precedence settings):
 /// <para>
-///	1. Application configuration found in the users's home directory (<c>~/.tui/appname.config.json</c>) -- Highest precedence 
+/// 1. Application configuration found in the users's home directory (<c>~/.tui/appname.config.json</c>) -- Highest
+/// precedence
 /// </para>
 /// <para>
-///	2. Application configuration found in the directory the app was launched from (<c>./.tui/appname.config.json</c>).
+/// 2. Application configuration found in the directory the app was launched from (<c>./.tui/appname.config.json</c>).
 /// </para>
 /// <para>
-///	3. Application configuration found in the applications's resources (<c>Resources/config.json</c>). 
+/// 3. Application configuration found in the applications's resources (<c>Resources/config.json</c>).
 /// </para>
 /// <para>
-///	4. Global configuration found in the user's home directory (<c>~/.tui/config.json</c>).
+/// 4. Global configuration found in the user's home directory (<c>~/.tui/config.json</c>).
 /// </para>
 /// <para>
-///	5. Global configuration found in the directory the app was launched from (<c>./.tui/config.json</c>).
+/// 5. Global configuration found in the directory the app was launched from (<c>./.tui/config.json</c>).
 /// </para>
 /// <para>
-///     6. Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest Precidence.
+/// 6. Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest
+/// Precidence.
 /// </para>
 /// </summary>
-public static partial class ConfigurationManager {
+public static class ConfigurationManager {
+
+	/// <summary>
+	/// Describes the location of the configuration files. The constants can be
+	/// combined (bitwise) to specify multiple locations.
+	/// </summary>
+	[Flags]
+	public enum ConfigLocations {
+		/// <summary>
+		/// No configuration will be loaded.
+		/// </summary>
+		/// <remarks>
+		/// Used for development and testing only. For Terminal,Gui to function properly, at least
+		/// <see cref="DefaultOnly"/> should be set.
+		/// </remarks>
+		None = 0,
+
+		/// <summary>
+		/// Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest
+		/// Precedence.
+		/// </summary>
+		DefaultOnly,
+
+		/// <summary>
+		/// This constant is a combination of all locations
+		/// </summary>
+		All = -1
 
-	private static readonly string _configFilename = "config.json";
+	}
+
+	static readonly string _configFilename = "config.json";
 
-	internal static readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions {
+	internal static readonly JsonSerializerOptions _serializerOptions = new() {
 		ReadCommentHandling = JsonCommentHandling.Skip,
 		PropertyNameCaseInsensitive = true,
 		DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 		WriteIndented = true,
 		Converters = {
-				// We override the standard Rune converter to support specifying Glyphs in
-				// a flexible way
-				new RuneJsonConverter(),
-				// Override Key to support "Ctrl+Q" format.
-				new KeyJsonConverter()
-			},
+			// We override the standard Rune converter to support specifying Glyphs in
+			// a flexible way
+			new RuneJsonConverter (),
+			// Override Key to support "Ctrl+Q" format.
+			new KeyJsonConverter ()
+		},
 		// Enables Key to be "Ctrl+Q" vs "Ctrl\u002BQ"
 		Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
 
-};
+	};
 
 	/// <summary>
-	/// A dictionary of all properties in the Terminal.Gui project that are decorated with the <see cref="SerializableConfigurationProperty"/> attribute.
-	/// The keys are the property names pre-pended with the class that implements the property (e.g. <c>Application.UseSystemConsole</c>).
+	/// A dictionary of all properties in the Terminal.Gui project that are decorated with the
+	/// <see cref="SerializableConfigurationProperty"/> attribute.
+	/// The keys are the property names pre-pended with the class that implements the property (e.g.
+	/// <c>Application.UseSystemConsole</c>).
 	/// The values are instances of <see cref="ConfigProperty"/> which hold the property's value and the
 	/// <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/> to get and set the property's value.
 	/// </summary>
 	/// <remarks>
-	/// Is <see langword="null"/> until <see cref="Initialize"/> is called. 
+	/// Is <see langword="null"/> until <see cref="Initialize"/> is called.
 	/// </remarks>
 	internal static Dictionary<string, ConfigProperty>? _allConfigProperties;
 
 	/// <summary>
-	/// The backing property for <see cref="Settings"/>. 
+	/// The backing property for <see cref="Settings"/>.
 	/// </summary>
 	/// <remarks>
 	/// Is <see langword="null"/> until <see cref="Reset"/> is called. Gets set to a new instance by
 	/// deserialization (see <see cref="Load"/>).
 	/// </remarks>
-	private static SettingsScope? _settings;
+	static SettingsScope? _settings;
+
+	internal static StringBuilder jsonErrors = new ();
 
 	/// <summary>
-	/// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties with the <see cref="SettingsScope"/>
+	/// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties with the
+	/// <see cref="SettingsScope"/>
 	/// attribute value.
 	/// </summary>
 	public static SettingsScope? Settings {
@@ -110,9 +145,7 @@ public static partial class ConfigurationManager {
 			}
 			return _settings;
 		}
-		set {
-			_settings = value!;
-		}
+		set => _settings = value!;
 	}
 
 	/// <summary>
@@ -124,15 +157,34 @@ public static partial class ConfigurationManager {
 	/// <summary>
 	/// Application-specific configuration settings scope.
 	/// </summary>
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true), JsonPropertyName ("AppSettings")]
+	[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)] [JsonPropertyName ("AppSettings")]
 	public static AppScope? AppSettings { get; set; }
 
 	/// <summary>
-	/// The set of glyphs used to draw checkboxes, lines, borders, etc...See also <seealso cref="Terminal.Gui.GlyphDefinitions"/>.
+	/// The set of glyphs used to draw checkboxes, lines, borders, etc...See also
+	/// <seealso cref="Terminal.Gui.GlyphDefinitions"/>.
+	/// </summary>
+	[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)] [JsonPropertyName ("Glyphs")]
+	public static GlyphDefinitions Glyphs { get; set; } = new ();
+
+	/// <summary>
+	/// Gets or sets whether the <see cref="ConfigurationManager"/> should throw an exception if it encounters
+	/// an error on deserialization. If <see langword="false"/> (the default), the error is logged and printed to the
+	/// console when <see cref="Application.Shutdown"/> is called.
 	/// </summary>
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true),
-		JsonPropertyName ("Glyphs")]
-	public static GlyphDefinitions Glyphs { get; set; } = new GlyphDefinitions ();
+	[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+	public static bool? ThrowOnJsonErrors { get; set; } = false;
+
+	/// <summary>
+	/// Name of the running application. By default this property is set to the application's assembly name.
+	/// </summary>
+	public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!;
+
+	/// <summary>
+	/// Gets and sets the locations where <see cref="ConfigurationManager"/> will look for config files.
+	/// The value is <see cref="ConfigLocations.All"/>.
+	/// </summary>
+	public static ConfigLocations Locations { get; set; } = ConfigLocations.All;
 
 	/// <summary>
 	/// Initializes the internal state of ConfigurationManager. Nominally called once as part of application
@@ -143,13 +195,13 @@ public static partial class ConfigurationManager {
 		_allConfigProperties = new Dictionary<string, ConfigProperty> ();
 		_settings = null;
 
-		Dictionary<string, Type> classesWithConfigProps = new Dictionary<string, Type> (StringComparer.InvariantCultureIgnoreCase);
+		var classesWithConfigProps = new Dictionary<string, Type> (StringComparer.InvariantCultureIgnoreCase);
 		// Get Terminal.Gui.dll classes
 
 		var types = from assembly in AppDomain.CurrentDomain.GetAssemblies ()
-			    from type in assembly.GetTypes ()
-			    where type.GetProperties ().Any (prop => prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) != null)
-			    select type;
+			from type in assembly.GetTypes ()
+			where type.GetProperties ().Any (prop => prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) != null)
+			select type;
 
 		foreach (var classWithConfig in types) {
 			classesWithConfigProps.Add (classWithConfig.Name, classWithConfig);
@@ -159,11 +211,11 @@ public static partial class ConfigurationManager {
 		classesWithConfigProps.ToList ().ForEach (x => Debug.WriteLine ($"  Class: {x.Key}"));
 
 		foreach (var p in from c in classesWithConfigProps
-				  let props = c.Value.GetProperties (BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).Where (prop =>
-					prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty)
-				  let enumerable = props
-				  from p in enumerable
-				  select p) {
+			let props = c.Value.GetProperties (BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).Where (prop =>
+				prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty)
+			let enumerable = props
+			from p in enumerable
+			select p) {
 			if (p.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty scp) {
 				if (p.GetGetMethod (true)!.IsStatic) {
 					// If the class name is omitted, JsonPropertyName is allowed. 
@@ -186,18 +238,18 @@ public static partial class ConfigurationManager {
 	}
 
 	/// <summary>
-	/// Creates a JSON document with the configuration specified. 
+	/// Creates a JSON document with the configuration specified.
 	/// </summary>
 	/// <returns></returns>
 	internal static string ToJson ()
 	{
-		Debug.WriteLine ($"ConfigurationManager.ToJson()");
-		return JsonSerializer.Serialize<SettingsScope> (Settings!, _serializerOptions);
+		Debug.WriteLine ("ConfigurationManager.ToJson()");
+		return JsonSerializer.Serialize (Settings!, _serializerOptions);
 	}
 
 	internal static Stream ToStream ()
 	{
-		var json = JsonSerializer.Serialize<SettingsScope> (Settings!, _serializerOptions);
+		var json = JsonSerializer.Serialize (Settings!, _serializerOptions);
 		// turn it into a stream
 		var stream = new MemoryStream ();
 		var writer = new StreamWriter (stream);
@@ -207,16 +259,6 @@ public static partial class ConfigurationManager {
 		return stream;
 	}
 
-	/// <summary>
-	/// Gets or sets whether the <see cref="ConfigurationManager"/> should throw an exception if it encounters 
-	/// an error on deserialization. If <see langword="false"/> (the default), the error is logged and printed to the 
-	/// console when <see cref="Application.Shutdown"/> is called. 
-	/// </summary>
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-	public static bool? ThrowOnJsonErrors { get; set; } = false;
-
-	internal static StringBuilder jsonErrors = new StringBuilder ();
-
 	internal static void AddJsonError (string error)
 	{
 		Debug.WriteLine ($"ConfigurationManager: {error}");
@@ -229,15 +271,12 @@ public static partial class ConfigurationManager {
 	public static void PrintJsonErrors ()
 	{
 		if (jsonErrors.Length > 0) {
-			Console.WriteLine ($"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:");
+			Console.WriteLine (@"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:");
 			Console.WriteLine (jsonErrors.ToString ());
 		}
 	}
 
-	private static void ClearJsonErrors ()
-	{
-		jsonErrors.Clear ();
-	}
+	static void ClearJsonErrors () => jsonErrors.Clear ();
 
 	/// <summary>
 	/// Called when the configuration has been updated from a configuration file. Invokes the <see cref="Updated"/>
@@ -245,12 +284,12 @@ public static partial class ConfigurationManager {
 	/// </summary>
 	public static void OnUpdated ()
 	{
-		Debug.WriteLine ($"ConfigurationManager.OnApplied()");
+		Debug.WriteLine (@"ConfigurationManager.OnApplied()");
 		Updated?.Invoke (null, new ConfigurationManagerEventArgs ());
 	}
 
 	/// <summary>
-	/// Event fired when the configuration has been updated from a configuration source.  
+	/// Event fired when the configuration has been updated from a configuration source.
 	/// application.
 	/// </summary>
 	public static event EventHandler<ConfigurationManagerEventArgs>? Updated;
@@ -265,9 +304,9 @@ public static partial class ConfigurationManager {
 	/// </remarks>
 	public static void Reset ()
 	{
-		Debug.WriteLine ($"ConfigurationManager.Reset()");
+		Debug.WriteLine (@"ConfigurationManager.Reset()");
 		if (_allConfigProperties == null) {
-			ConfigurationManager.Initialize ();
+			Initialize ();
 		}
 
 		ClearJsonErrors ();
@@ -291,15 +330,16 @@ public static partial class ConfigurationManager {
 	/// <see cref="Locations"/> is set to <see cref="ConfigLocations.None"/>.
 	/// </summary>
 	/// <remarks>
-	/// <para>
-	/// This method is only really useful when using ConfigurationManagerTests
-	/// to generate the JSON doc that is embedded into Terminal.Gui (during development). 
-	/// </para>
-	/// <para>
-	/// WARNING: The <c>Terminal.Gui.Resources.config.json</c> resource has setting definitions (Themes)
-	/// that are NOT generated by this function. If you use this function to regenerate <c>Terminal.Gui.Resources.config.json</c>,
-	/// make sure you copy the Theme definitions from the existing <c>Terminal.Gui.Resources.config.json</c> file.
-	/// </para>		
+	///         <para>
+	///         This method is only really useful when using ConfigurationManagerTests
+	///         to generate the JSON doc that is embedded into Terminal.Gui (during development).
+	///         </para>
+	///         <para>
+	///         WARNING: The <c>Terminal.Gui.Resources.config.json</c> resource has setting definitions (Themes)
+	///         that are NOT generated by this function. If you use this function to regenerate
+	///         <c>Terminal.Gui.Resources.config.json</c>,
+	///         make sure you copy the Theme definitions from the existing <c>Terminal.Gui.Resources.config.json</c> file.
+	///         </para>
 	/// </remarks>
 	internal static void GetHardCodedDefaults ()
 	{
@@ -341,12 +381,12 @@ public static partial class ConfigurationManager {
 	}
 
 	/// <summary>
-	/// Called when an updated configuration has been applied to the  
+	/// Called when an updated configuration has been applied to the
 	/// application. Fires the <see cref="Applied"/> event.
 	/// </summary>
 	public static void OnApplied ()
 	{
-		Debug.WriteLine ($"ConfigurationManager.OnApplied()");
+		Debug.WriteLine ("ConfigurationManager.OnApplied()");
 		Applied?.Invoke (null, new ConfigurationManagerEventArgs ());
 
 		// TODO: Refactor ConfigurationManager to not use an event handler for this.
@@ -355,62 +395,26 @@ public static partial class ConfigurationManager {
 	}
 
 	/// <summary>
-	/// Event fired when an updated configuration has been applied to the  
+	/// Event fired when an updated configuration has been applied to the
 	/// application.
 	/// </summary>
 	public static event EventHandler<ConfigurationManagerEventArgs>? Applied;
 
 	/// <summary>
-	/// Name of the running application. By default this property is set to the application's assembly name.
-	/// </summary>
-	public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!;
-
-	/// <summary>
-	/// Describes the location of the configuration files. The constants can be
-	/// combined (bitwise) to specify multiple locations.
-	/// </summary>
-	[Flags]
-	public enum ConfigLocations {
-		/// <summary>
-		/// No configuration will be loaded.
-		/// </summary>
-		/// <remarks>
-		///  Used for development and testing only. For Terminal,Gui to function properly, at least
-		///  <see cref="DefaultOnly"/> should be set.
-		/// </remarks>
-		None = 0,
-
-		/// <summary>
-		/// Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest Precidence.
-		/// </summary>
-		DefaultOnly,
-
-		/// <summary>
-		/// This constant is a combination of all locations
-		/// </summary>
-		All = -1
-
-	}
-
-	/// <summary>
-	/// Gets and sets the locations where <see cref="ConfigurationManager"/> will look for config files.
-	/// The value is <see cref="ConfigLocations.All"/>.
-	/// </summary>
-	public static ConfigLocations Locations { get; set; } = ConfigLocations.All;
-
-	/// <summary>
-	/// Loads all settings found in the various configuration storage locations to 
+	/// Loads all settings found in the various configuration storage locations to
 	/// the <see cref="ConfigurationManager"/>. Optionally,
 	/// resets all settings attributed with <see cref="SerializableConfigurationProperty"/> to the defaults.
 	/// </summary>
 	/// <remarks>
 	/// Use <see cref="Apply"/> to cause the loaded settings to be applied to the running application.
 	/// </remarks>
-	/// <param name="reset">If <see langword="true"/> the state of <see cref="ConfigurationManager"/> will
-	/// be reset to the defaults.</param>
+	/// <param name="reset">
+	/// If <see langword="true"/> the state of <see cref="ConfigurationManager"/> will
+	/// be reset to the defaults.
+	/// </param>
 	public static void Load (bool reset = false)
 	{
-		Debug.WriteLine ($"ConfigurationManager.Load()");
+		Debug.WriteLine ("ConfigurationManager.Load()");
 
 		if (reset) {
 			Reset ();
@@ -446,16 +450,16 @@ public static partial class ConfigurationManager {
 	{
 		var emptyScope = new SettingsScope ();
 		emptyScope.Clear ();
-		return JsonSerializer.Serialize<SettingsScope> (emptyScope, _serializerOptions);
+		return JsonSerializer.Serialize (emptyScope, _serializerOptions);
 	}
 
 	/// <summary>
 	/// System.Text.Json does not support copying a deserialized object to an existing instance.
-	/// To work around this, we implement a 'deep, memberwise copy' method. 
+	/// To work around this, we implement a 'deep, memberwise copy' method.
 	/// </summary>
 	/// <remarks>
 	/// TOOD: When System.Text.Json implements `PopulateObject` revisit
-	///	https://github.com/dotnet/corefx/issues/37627
+	/// https://github.com/dotnet/corefx/issues/37627
 	/// </remarks>
 	/// <param name="source"></param>
 	/// <param name="destination"></param>
@@ -488,12 +492,10 @@ public static partial class ConfigurationManager {
 		// Dictionary
 		if (source.GetType ().IsGenericType && source.GetType ().GetGenericTypeDefinition ().IsAssignableFrom (typeof (Dictionary<,>))) {
 			foreach (var srcKey in ((IDictionary)source).Keys) {
-				if (srcKey is string) {
-
-				}
-				if (((IDictionary)destination).Contains (srcKey))
+				if (srcKey is string) { }
+				if (((IDictionary)destination).Contains (srcKey)) {
 					((IDictionary)destination) [srcKey] = DeepMemberwiseCopy (((IDictionary)source) [srcKey], ((IDictionary)destination) [srcKey]);
-				else {
+				} else {
 					((IDictionary)destination).Add (srcKey, ((IDictionary)source) [srcKey]);
 				}
 			}
@@ -503,7 +505,7 @@ public static partial class ConfigurationManager {
 		// ALl other object types
 		var sourceProps = source?.GetType ().GetProperties ().Where (x => x.CanRead).ToList ();
 		var destProps = destination?.GetType ().GetProperties ().Where (x => x.CanWrite).ToList ()!;
-		foreach (var (sourceProp, destProp) in
+		foreach ((var sourceProp, var destProp) in
 			from sourceProp in sourceProps
 			where destProps.Any (x => x.Name == sourceProp.Name)
 			let destProp = destProps.First (x => x.Name == sourceProp.Name)
@@ -513,65 +515,18 @@ public static partial class ConfigurationManager {
 			var sourceVal = sourceProp.GetValue (source);
 			var destVal = destProp.GetValue (destination);
 			if (sourceVal != null) {
-				if (destVal != null) {
-					// Recurse
-					destProp.SetValue (destination, DeepMemberwiseCopy (sourceVal, destVal));
-				} else {
-					destProp.SetValue (destination, sourceVal);
+				try {
+					if (destVal != null) {
+						// Recurse
+						destProp.SetValue (destination, DeepMemberwiseCopy (sourceVal, destVal));
+					} else {
+						destProp.SetValue (destination, sourceVal);
+					}
+				} catch (ArgumentException e) {
+					throw new JsonException ($"Error Applying Configuration Change: {e.Message}", e);
 				}
 			}
 		}
 		return destination!;
 	}
-
-	//public class ConfiguraitonLocation
-	//{
-	//	public string Name { get; set; } = string.Empty;
-
-	//	public string? Path { get; set; }
-
-	//	public async Task<SettingsScope> UpdateAsync (Stream stream)
-	//	{
-	//		var scope = await JsonSerializer.DeserializeAsync<SettingsScope> (stream, serializerOptions);
-	//		if (scope != null) {
-	//			ConfigurationManager.Settings?.UpdateFrom (scope);
-	//			return scope;
-	//		}
-	//		return new SettingsScope ();
-	//	}
-
-	//}
-
-	//public class StreamConfiguration {
-	//	private bool _reset;
-
-	//	public StreamConfiguration (bool reset)
-	//	{
-	//		_reset = reset;
-	//	}
-
-	//	public StreamConfiguration UpdateAppResources ()
-	//	{
-	//		if (Locations.HasFlag (ConfigLocations.AppResources)) LoadAppResources ();
-	//		return this;
-	//	}
-
-	//	public StreamConfiguration UpdateAppDirectory ()
-	//	{
-	//		if (Locations.HasFlag (ConfigLocations.AppDirectory)) LoadAppDirectory ();
-	//		return this;
-	//	}
-
-	//	// Additional update methods for each location here
-
-	//	private void LoadAppResources ()
-	//	{
-	//		// Load AppResources logic here
-	//	}
-
-	//	private void LoadAppDirectory ()
-	//	{
-	//		// Load AppDirectory logic here
-	//	}
-	//}
-}
+}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 312 - 275
Terminal.Gui/Drawing/Color.cs


+ 5 - 3
Terminal.Gui/Views/FileSystemColorProvider.cs

@@ -14,7 +14,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="file"></param>
 		/// <returns></returns>
-		public Color GetColor (IFileSystemInfo file)
+		public Color? GetColor (IFileSystemInfo file)
 		{
 			if (FilenameToColor.ContainsKey (file.Name)) {
 				return FilenameToColor [file.Name];
@@ -443,8 +443,10 @@ namespace Terminal.Gui {
 
 		private static Color StringToColor (string str)
 		{
-			Color.TryParse (str, out var c);
-			return c ?? throw new System.Exception ("Failed to parse Color from " + str);
+			if (!Color.TryParse (str, out var c)) {
+				throw new System.Exception ("Failed to parse Color from " + str);
+			}
+			return c;
 		}
 	}
 }

+ 50 - 58
Terminal.Gui/Views/TreeView/TreeNode.cs

@@ -1,73 +1,65 @@
 using System.Collections.Generic;
 
-namespace Terminal.Gui {
-		
+namespace Terminal.Gui; 
+
+/// <summary>
+/// Interface to implement when you want the regular (non generic) <see cref="TreeView"/>
+/// to automatically determine children for your class (without having to specify
+/// an <see cref="ITreeBuilder{T}"/>)
+/// </summary>
+public interface ITreeNode {
+	/// <summary>
+	/// Text to display when rendering the node
+	/// </summary>
+	string Text { get; set; }
+
 	/// <summary>
-	/// Interface to implement when you want the regular (non generic) <see cref="TreeView"/>
-	/// to automatically determine children for your class (without having to specify 
-	/// an <see cref="ITreeBuilder{T}"/>)
+	/// The children of your class which should be rendered underneath it when expanded
 	/// </summary>
-	public interface ITreeNode {
-		/// <summary>
-		/// Text to display when rendering the node
-		/// </summary>
-		string Text { get; set; }
+	/// <value></value>
+	IList<ITreeNode> Children { get; }
 
-		/// <summary>
-		/// The children of your class which should be rendered underneath it when expanded
-		/// </summary>
-		/// <value></value>
-		IList<ITreeNode> Children { get; }
+	/// <summary>
+	/// Optionally allows you to store some custom data/class here.
+	/// </summary>
+	object Tag { get; set; }
+}
 
-		/// <summary>
-		/// Optionally allows you to store some custom data/class here.
-		/// </summary>
-		object Tag { get; set; }
-	}
+/// <summary>
+/// Simple class for representing nodes, use with regular (non generic) <see cref="TreeView"/>.
+/// </summary>
+public class TreeNode : ITreeNode {
 
 	/// <summary>
-	/// Simple class for representing nodes, use with regular (non generic) <see cref="TreeView"/>.
+	/// Initialises a new instance with no <see cref="Text"/>
 	/// </summary>
-	public class TreeNode : ITreeNode {
-		/// <summary>
-		/// Children of the current node
-		/// </summary>
-		/// <returns></returns>
-		public virtual IList<ITreeNode> Children { get; set; } = new List<ITreeNode> ();
+	public TreeNode () { }
 
-		/// <summary>
-		/// Text to display in tree node for current entry
-		/// </summary>
-		/// <value></value>
-		public virtual string Text { get; set; }
+	/// <summary>
+	/// Initialises a new instance and sets starting <see cref="Text"/>
+	/// </summary>
+	public TreeNode (string text) => Text = text;
 
-		/// <summary>
-		/// Optionally allows you to store some custom data/class here.
-		/// </summary>
-		public object Tag { get; set; }
+	/// <summary>
+	/// Children of the current node
+	/// </summary>
+	/// <returns></returns>
+	public virtual IList<ITreeNode> Children { get; set; } = new List<ITreeNode> ();
 
-		/// <summary>
-		/// returns <see cref="Text"/>
-		/// </summary>
-		/// <returns></returns>
-		public override string ToString ()
-		{
-			return Text ?? "Unamed Node";
-		}
+	/// <summary>
+	/// Text to display in tree node for current entry
+	/// </summary>
+	/// <value></value>
+	public virtual string Text { get; set; }
 
-		/// <summary>
-		/// Initialises a new instance with no <see cref="Text"/>
-		/// </summary>
-		public TreeNode ()
-		{
+	/// <summary>
+	/// Optionally allows you to store some custom data/class here.
+	/// </summary>
+	public object Tag { get; set; }
 
-		}
-		/// <summary>
-		/// Initialises a new instance and sets starting <see cref="Text"/>
-		/// </summary>
-		public TreeNode (string text)
-		{
-			Text = text;
-		}
-	}
+	/// <summary>
+	/// returns <see cref="Text"/>
+	/// </summary>
+	/// <returns></returns>
+	public override string ToString () => Text ?? "Unamed Node";
 }

+ 1242 - 1221
Terminal.Gui/Views/TreeView/TreeView.cs

@@ -2,1474 +2,1495 @@
 // by [email protected]). Phillip has explicitly granted permission for his design
 // and code to be used in this library under the MIT license.
 
-using System.Text;
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Linq;
-using Terminal.Gui;
 
-namespace Terminal.Gui {
+namespace Terminal.Gui; 
 
+/// <summary>
+/// Interface for all non generic members of <see cref="TreeView{T}"/>.
+/// <a href="../docs/treeview.md">See TreeView Deep Dive for more information</a>.
+/// </summary>
+public interface ITreeView {
 	/// <summary>
-	/// Interface for all non generic members of <see cref="TreeView{T}"/>.
-	/// 
-	/// <a href="../docs/treeview.md">See TreeView Deep Dive for more information</a>.
-	/// </summary>
-	public interface ITreeView {
-		/// <summary>
-		/// Contains options for changing how the tree is rendered.
-		/// </summary>
-		TreeStyle Style { get; set; }
-
-		/// <summary>
-		/// Removes all objects from the tree and clears selection.
-		/// </summary>
-		void ClearObjects ();
-
-		/// <summary>
-		/// Sets a flag indicating this view needs to be redisplayed because its state has changed.
-		/// </summary>
-		void SetNeedsDisplay ();
+	/// Contains options for changing how the tree is rendered.
+	/// </summary>
+	TreeStyle Style { get; set; }
+
+	/// <summary>
+	/// Removes all objects from the tree and clears selection.
+	/// </summary>
+	void ClearObjects ();
+
+	/// <summary>
+	/// Sets a flag indicating this view needs to be redisplayed because its state has changed.
+	/// </summary>
+	void SetNeedsDisplay ();
+}
+
+/// <summary>
+/// Convenience implementation of generic <see cref="TreeView{T}"/> for any tree were all nodes
+/// implement <see cref="ITreeNode"/>.
+/// <a href="../docs/treeview.md">See TreeView Deep Dive for more information</a>.
+/// </summary>
+public class TreeView : TreeView<ITreeNode> {
+
+	/// <summary>
+	/// Creates a new instance of the tree control with absolute positioning and initialises
+	/// <see cref="TreeBuilder{T}"/> with default <see cref="ITreeNode"/> based builder.
+	/// </summary>
+	public TreeView ()
+	{
+		TreeBuilder = new TreeNodeBuilder ();
+		AspectGetter = o => o == null ? "Null" : o.Text ?? o?.ToString () ?? "Unamed Node";
 	}
+}
+
+/// <summary>
+/// Hierarchical tree view with expandable branches. Branch objects are dynamically determined
+/// when expanded using a user defined <see cref="ITreeBuilder{T}"/>.
+/// <a href="../docs/treeview.md">See TreeView Deep Dive for more information</a>.
+/// </summary>
+public class TreeView<T> : View, ITreeView where T : class {
 
 	/// <summary>
-	/// Convenience implementation of generic <see cref="TreeView{T}"/> for any tree were all nodes
-	/// implement <see cref="ITreeNode"/>.
-	/// 
-	/// <a href="../docs/treeview.md">See TreeView Deep Dive for more information</a>.
+	/// Error message to display when the control is not properly initialized at draw time
+	/// (nodes added but no tree builder set).
 	/// </summary>
-	public class TreeView : TreeView<ITreeNode> {
+	public static string NoBuilderError = "ERROR: TreeBuilder Not Set";
 
-		/// <summary>
-		/// Creates a new instance of the tree control with absolute positioning and initialises
-		/// <see cref="TreeBuilder{T}"/> with default <see cref="ITreeNode"/> based builder.
-		/// </summary>
-		public TreeView ()
-		{
-			TreeBuilder = new TreeNodeBuilder ();
-			AspectGetter = o => o == null ? "Null" : (o.Text ?? o?.ToString () ?? "Unamed Node");
-		}
+	/// <summary>
+	/// Cached result of <see cref="BuildLineMap"/>
+	/// </summary>
+	IReadOnlyCollection<Branch<T>> cachedLineMap;
+
+	CursorVisibility desiredCursorVisibility = CursorVisibility.Invisible;
+
+	/// <summary>
+	/// Interface for filtering which lines of the tree are displayed
+	/// e.g. to provide text searching.  Defaults to <see langword="null"/>
+	/// (no filtering).
+	/// </summary>
+	public ITreeViewFilter<T> Filter = null;
+
+	/// <summary>
+	/// Secondary selected regions of tree when <see cref="MultiSelect"/> is true.
+	/// </summary>
+	readonly Stack<TreeSelection<T>> multiSelectedRegions = new ();
+
+	KeyCode objectActivationKey = KeyCode.Enter;
+	int scrollOffsetHorizontal;
+	int scrollOffsetVertical;
+
+	/// <summary>
+	/// private variable for <see cref="SelectedObject"/>
+	/// </summary>
+	T selectedObject;
+
+	/// <summary>
+	/// Creates a new tree view with absolute positioning.
+	/// Use <see cref="AddObjects(IEnumerable{T})"/> to set set root objects for the tree.
+	/// Children will not be rendered until you set <see cref="TreeBuilder"/>.
+	/// </summary>
+	public TreeView ()
+	{
+		CanFocus = true;
+
+		// Things this view knows how to do
+		AddCommand (Command.PageUp, () => {
+			MovePageUp ();
+			return true;
+		});
+		AddCommand (Command.PageDown, () => {
+			MovePageDown ();
+			return true;
+		});
+		AddCommand (Command.PageUpExtend, () => {
+			MovePageUp (true);
+			return true;
+		});
+		AddCommand (Command.PageDownExtend, () => {
+			MovePageDown (true);
+			return true;
+		});
+		AddCommand (Command.Expand, () => {
+			Expand ();
+			return true;
+		});
+		AddCommand (Command.ExpandAll, () => {
+			ExpandAll (SelectedObject);
+			return true;
+		});
+		AddCommand (Command.Collapse, () => {
+			CursorLeft (false);
+			return true;
+		});
+		AddCommand (Command.CollapseAll, () => {
+			CursorLeft (true);
+			return true;
+		});
+		AddCommand (Command.LineUp, () => {
+			AdjustSelection (-1);
+			return true;
+		});
+		AddCommand (Command.LineUpExtend, () => {
+			AdjustSelection (-1, true);
+			return true;
+		});
+		AddCommand (Command.LineUpToFirstBranch, () => {
+			AdjustSelectionToBranchStart ();
+			return true;
+		});
+
+		AddCommand (Command.LineDown, () => {
+			AdjustSelection (1);
+			return true;
+		});
+		AddCommand (Command.LineDownExtend, () => {
+			AdjustSelection (1, true);
+			return true;
+		});
+		AddCommand (Command.LineDownToLastBranch, () => {
+			AdjustSelectionToBranchEnd ();
+			return true;
+		});
+
+		AddCommand (Command.TopHome, () => {
+			GoToFirst ();
+			return true;
+		});
+		AddCommand (Command.BottomEnd, () => {
+			GoToEnd ();
+			return true;
+		});
+		AddCommand (Command.SelectAll, () => {
+			SelectAll ();
+			return true;
+		});
+
+		AddCommand (Command.ScrollUp, () => {
+			ScrollUp ();
+			return true;
+		});
+		AddCommand (Command.ScrollDown, () => {
+			ScrollDown ();
+			return true;
+		});
+		AddCommand (Command.Accept, () => {
+			ActivateSelectedObjectIfAny ();
+			return true;
+		});
+
+		// Default keybindings for this view
+		KeyBindings.Add (KeyCode.PageUp, Command.PageUp);
+		KeyBindings.Add (KeyCode.PageDown, Command.PageDown);
+		KeyBindings.Add (KeyCode.PageUp | KeyCode.ShiftMask, Command.PageUpExtend);
+		KeyBindings.Add (KeyCode.PageDown | KeyCode.ShiftMask, Command.PageDownExtend);
+		KeyBindings.Add (KeyCode.CursorRight, Command.Expand);
+		KeyBindings.Add (KeyCode.CursorRight | KeyCode.CtrlMask, Command.ExpandAll);
+		KeyBindings.Add (KeyCode.CursorLeft, Command.Collapse);
+		KeyBindings.Add (KeyCode.CursorLeft | KeyCode.CtrlMask, Command.CollapseAll);
+
+		KeyBindings.Add (KeyCode.CursorUp, Command.LineUp);
+		KeyBindings.Add (KeyCode.CursorUp | KeyCode.ShiftMask, Command.LineUpExtend);
+		KeyBindings.Add (KeyCode.CursorUp | KeyCode.CtrlMask, Command.LineUpToFirstBranch);
+
+		KeyBindings.Add (KeyCode.CursorDown, Command.LineDown);
+		KeyBindings.Add (KeyCode.CursorDown | KeyCode.ShiftMask, Command.LineDownExtend);
+		KeyBindings.Add (KeyCode.CursorDown | KeyCode.CtrlMask, Command.LineDownToLastBranch);
+
+		KeyBindings.Add (KeyCode.Home, Command.TopHome);
+		KeyBindings.Add (KeyCode.End, Command.BottomEnd);
+		KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, Command.SelectAll);
+		KeyBindings.Add (ObjectActivationKey, Command.Accept);
 	}
 
 	/// <summary>
-	/// Hierarchical tree view with expandable branches. Branch objects are dynamically determined
-	/// when expanded using a user defined <see cref="ITreeBuilder{T}"/>.
-	/// 
-	/// <a href="../docs/treeview.md">See TreeView Deep Dive for more information</a>.
-	/// </summary>
-	public class TreeView<T> : View, ITreeView where T : class {
-		private int scrollOffsetVertical;
-		private int scrollOffsetHorizontal;
-
-		/// <summary>
-		/// Determines how sub branches of the tree are dynamically built at runtime as the user
-		/// expands root nodes.
-		/// </summary>
-		/// <value></value>
-		public ITreeBuilder<T> TreeBuilder { get; set; }
-
-		/// <summary>
-		/// private variable for <see cref="SelectedObject"/>
-		/// </summary>
-		T selectedObject;
-
-		/// <summary>
-		/// Contains options for changing how the tree is rendered.
-		/// </summary>
-		public TreeStyle Style { get; set; } = new TreeStyle ();
-
-		/// <summary>
-		/// True to allow multiple objects to be selected at once.
-		/// </summary>
-		/// <value></value>
-		public bool MultiSelect { get; set; } = true;
-
-		/// <summary>
-		/// Maximum number of nodes that can be expanded in any given branch.
-		/// </summary>
-		public int MaxDepth { get; set; } = 100;
-
-		/// <summary>
-		/// True makes a letter key press navigate to the next visible branch that begins with
-		/// that letter/digit.
-		/// </summary>
-		/// <value></value>
-		public bool AllowLetterBasedNavigation { get; set; } = true;
-
-		/// <summary>
-		/// The currently selected object in the tree. When <see cref="MultiSelect"/> is true this
-		/// is the object at which the cursor is at.
-		/// </summary>
-		public T SelectedObject {
-			get => selectedObject;
-			set {
-				var oldValue = selectedObject;
-				selectedObject = value;
-
-				if (!ReferenceEquals (oldValue, value)) {
-					OnSelectionChanged (new SelectionChangedEventArgs<T> (this, oldValue, value));
-				}
-			}
-		}
+	/// Initialises <see cref="TreeBuilder"/>.Creates a new tree view with absolute
+	/// positioning. Use <see cref="AddObjects(IEnumerable{T})"/> to set set root
+	/// objects for the tree.
+	/// </summary>
+	public TreeView (ITreeBuilder<T> builder) : this () => TreeBuilder = builder;
 
-		/// <summary>
-		/// This event is raised when an object is activated e.g. by double clicking or 
-		/// pressing <see cref="ObjectActivationKey"/>.
-		/// </summary>
-		public event EventHandler<ObjectActivatedEventArgs<T>> ObjectActivated;
-
-		// TODO: Update to use Key instead of KeyCode
-		/// <summary>
-		/// Key which when pressed triggers <see cref="TreeView{T}.ObjectActivated"/>.
-		/// Defaults to Enter.
-		/// </summary>
-		public KeyCode ObjectActivationKey {
-			get => objectActivationKey;
-			set {
-				if (objectActivationKey != value) {
-					KeyBindings.Replace (ObjectActivationKey, value);
-					objectActivationKey = value;
-				}
-			}
-		}
+	/// <summary>
+	/// Determines how sub branches of the tree are dynamically built at runtime as the user
+	/// expands root nodes.
+	/// </summary>
+	/// <value></value>
+	public ITreeBuilder<T> TreeBuilder { get; set; }
 
-		/// <summary>
-		/// Mouse event to trigger <see cref="TreeView{T}.ObjectActivated"/>.
-		/// Defaults to double click (<see cref="MouseFlags.Button1DoubleClicked"/>).
-		/// Set to null to disable this feature.
-		/// </summary>
-		/// <value></value>
-		public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked;
-
-		/// <summary>
-		/// Delegate for multi colored tree views. Return the <see cref="ColorScheme"/> to use
-		/// for each passed object or null to use the default.
-		/// </summary>
-		public Func<T, ColorScheme> ColorGetter { get; set; }
-
-		/// <summary>
-		/// Secondary selected regions of tree when <see cref="MultiSelect"/> is true.
-		/// </summary>
-		private Stack<TreeSelection<T>> multiSelectedRegions = new Stack<TreeSelection<T>> ();
-
-		/// <summary>
-		/// Cached result of <see cref="BuildLineMap"/>
-		/// </summary>
-		private IReadOnlyCollection<Branch<T>> cachedLineMap;
-
-		/// <summary>
-		/// Error message to display when the control is not properly initialized at draw time 
-		/// (nodes added but no tree builder set).
-		/// </summary>
-		public static string NoBuilderError = "ERROR: TreeBuilder Not Set";
-		private KeyCode objectActivationKey = KeyCode.Enter;
-
-		/// <summary>
-		/// Called when the <see cref="SelectedObject"/> changes.
-		/// </summary>
-		public event EventHandler<SelectionChangedEventArgs<T>> SelectionChanged;
-
-		/// <summary>
-		/// Called once for each visible row during rendering.  Can be used
-		/// to make last minute changes to color or text rendered
-		/// </summary>
-		public event EventHandler<DrawTreeViewLineEventArgs<T>> DrawLine;
-
-		/// <summary>
-		/// The root objects in the tree, note that this collection is of root objects only.
-		/// </summary>
-		public IEnumerable<T> Objects { get => roots.Keys; }
-
-		/// <summary>
-		/// Map of root objects to the branches under them. All objects have 
-		/// a <see cref="Branch{T}"/> even if that branch has no children.
-		/// </summary>
-		internal Dictionary<T, Branch<T>> roots { get; set; } = new Dictionary<T, Branch<T>> ();
-
-		/// <summary>
-		/// The amount of tree view that has been scrolled off the top of the screen (by the user 
-		/// scrolling down).
-		/// </summary>
-		/// <remarks>Setting a value of less than 0 will result in a offset of 0. To see changes 
-		/// in the UI call <see cref="View.SetNeedsDisplay()"/>.</remarks>
-		public int ScrollOffsetVertical {
-			get => scrollOffsetVertical;
-			set {
-				scrollOffsetVertical = Math.Max (0, value);
-			}
-		}
+	/// <summary>
+	/// True to allow multiple objects to be selected at once.
+	/// </summary>
+	/// <value></value>
+	public bool MultiSelect { get; set; } = true;
+
+	/// <summary>
+	/// Maximum number of nodes that can be expanded in any given branch.
+	/// </summary>
+	public int MaxDepth { get; set; } = 100;
+
+	/// <summary>
+	/// True makes a letter key press navigate to the next visible branch that begins with
+	/// that letter/digit.
+	/// </summary>
+	/// <value></value>
+	public bool AllowLetterBasedNavigation { get; set; } = true;
+
+	/// <summary>
+	/// The currently selected object in the tree. When <see cref="MultiSelect"/> is true this
+	/// is the object at which the cursor is at.
+	/// </summary>
+	public T SelectedObject {
+		get => selectedObject;
+		set {
+			var oldValue = selectedObject;
+			selectedObject = value;
 
-		/// <summary>
-		/// The amount of tree view that has been scrolled to the right (horizontally).
-		/// </summary>
-		/// <remarks>Setting a value of less than 0 will result in a offset of 0. To see changes 
-		/// in the UI call <see cref="View.SetNeedsDisplay()"/>.</remarks>
-		public int ScrollOffsetHorizontal {
-			get => scrollOffsetHorizontal;
-			set {
-				scrollOffsetHorizontal = Math.Max (0, value);
+			if (!ReferenceEquals (oldValue, value)) {
+				OnSelectionChanged (new SelectionChangedEventArgs<T> (this, oldValue, value));
 			}
 		}
+	}
 
-		/// <summary>
-		/// The current number of rows in the tree (ignoring the controls bounds).
-		/// </summary>
-		public int ContentHeight => BuildLineMap ().Count ();
-
-		/// <summary>
-		/// Returns the string representation of model objects hosted in the tree. Default 
-		/// implementation is to call <see cref="object.ToString"/>.
-		/// </summary>
-		/// <value></value>
-		public AspectGetterDelegate<T> AspectGetter { get; set; } = (o) => o.ToString () ?? "";
-
-		CursorVisibility desiredCursorVisibility = CursorVisibility.Invisible;
-
-		/// <summary>
-		/// Interface for filtering which lines of the tree are displayed
-		///  e.g. to provide text searching.  Defaults to <see langword="null"/>
-		/// (no filtering).
-		/// </summary>
-		public ITreeViewFilter<T> Filter = null;
-
-		/// <summary>
-		/// Get / Set the wished cursor when the tree is focused.
-		/// Only applies when <see cref="MultiSelect"/> is true.
-		/// Defaults to <see cref="CursorVisibility.Invisible"/>.
-		/// </summary>
-		public CursorVisibility DesiredCursorVisibility {
-			get {
-				return MultiSelect ? desiredCursorVisibility : CursorVisibility.Invisible;
-			}
-			set {
-				if (desiredCursorVisibility != value) {
-					desiredCursorVisibility = value;
-					if (HasFocus) {
-						Application.Driver.SetCursorVisibility (DesiredCursorVisibility);
-					}
-				}
+	// TODO: Update to use Key instead of KeyCode
+	/// <summary>
+	/// Key which when pressed triggers <see cref="TreeView{T}.ObjectActivated"/>.
+	/// Defaults to Enter.
+	/// </summary>
+	public KeyCode ObjectActivationKey {
+		get => objectActivationKey;
+		set {
+			if (objectActivationKey != value) {
+				KeyBindings.Replace (ObjectActivationKey, value);
+				objectActivationKey = value;
 			}
 		}
+	}
 
-		/// <summary>
-		/// Creates a new tree view with absolute positioning. 
-		/// Use <see cref="AddObjects(IEnumerable{T})"/> to set set root objects for the tree.
-		/// Children will not be rendered until you set <see cref="TreeBuilder"/>.
-		/// </summary>
-		public TreeView () : base ()
-		{
-			CanFocus = true;
-
-			// Things this view knows how to do
-			AddCommand (Command.PageUp, () => { MovePageUp (false); return true; });
-			AddCommand (Command.PageDown, () => { MovePageDown (false); return true; });
-			AddCommand (Command.PageUpExtend, () => { MovePageUp (true); return true; });
-			AddCommand (Command.PageDownExtend, () => { MovePageDown (true); return true; });
-			AddCommand (Command.Expand, () => { Expand (); return true; });
-			AddCommand (Command.ExpandAll, () => { ExpandAll (SelectedObject); return true; });
-			AddCommand (Command.Collapse, () => { CursorLeft (false); return true; });
-			AddCommand (Command.CollapseAll, () => { CursorLeft (true); return true; });
-			AddCommand (Command.LineUp, () => { AdjustSelection (-1, false); return true; });
-			AddCommand (Command.LineUpExtend, () => { AdjustSelection (-1, true); return true; });
-			AddCommand (Command.LineUpToFirstBranch, () => { AdjustSelectionToBranchStart (); return true; });
-
-			AddCommand (Command.LineDown, () => { AdjustSelection (1, false); return true; });
-			AddCommand (Command.LineDownExtend, () => { AdjustSelection (1, true); return true; });
-			AddCommand (Command.LineDownToLastBranch, () => { AdjustSelectionToBranchEnd (); return true; });
-
-			AddCommand (Command.TopHome, () => { GoToFirst (); return true; });
-			AddCommand (Command.BottomEnd, () => { GoToEnd (); return true; });
-			AddCommand (Command.SelectAll, () => { SelectAll (); return true; });
-
-			AddCommand (Command.ScrollUp, () => { ScrollUp (); return true; });
-			AddCommand (Command.ScrollDown, () => { ScrollDown (); return true; });
-			AddCommand (Command.Accept, () => { ActivateSelectedObjectIfAny (); return true; });
-
-			// Default keybindings for this view
-			KeyBindings.Add (KeyCode.PageUp, Command.PageUp);
-			KeyBindings.Add (KeyCode.PageDown, Command.PageDown);
-			KeyBindings.Add (KeyCode.PageUp | KeyCode.ShiftMask, Command.PageUpExtend);
-			KeyBindings.Add (KeyCode.PageDown | KeyCode.ShiftMask, Command.PageDownExtend);
-			KeyBindings.Add (KeyCode.CursorRight, Command.Expand);
-			KeyBindings.Add (KeyCode.CursorRight | KeyCode.CtrlMask, Command.ExpandAll);
-			KeyBindings.Add (KeyCode.CursorLeft, Command.Collapse);
-			KeyBindings.Add (KeyCode.CursorLeft | KeyCode.CtrlMask, Command.CollapseAll);
-
-			KeyBindings.Add (KeyCode.CursorUp, Command.LineUp);
-			KeyBindings.Add (KeyCode.CursorUp | KeyCode.ShiftMask, Command.LineUpExtend);
-			KeyBindings.Add (KeyCode.CursorUp | KeyCode.CtrlMask, Command.LineUpToFirstBranch);
-
-			KeyBindings.Add (KeyCode.CursorDown, Command.LineDown);
-			KeyBindings.Add (KeyCode.CursorDown | KeyCode.ShiftMask, Command.LineDownExtend);
-			KeyBindings.Add (KeyCode.CursorDown | KeyCode.CtrlMask, Command.LineDownToLastBranch);
-
-			KeyBindings.Add (KeyCode.Home, Command.TopHome);
-			KeyBindings.Add (KeyCode.End, Command.BottomEnd);
-			KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, Command.SelectAll);
-			KeyBindings.Add (ObjectActivationKey, Command.Accept);
-		}
-
-		/// <summary>
-		/// Initialises <see cref="TreeBuilder"/>.Creates a new tree view with absolute 
-		/// positioning. Use <see cref="AddObjects(IEnumerable{T})"/> to set set root 
-		/// objects for the tree.
-		/// </summary>
-		public TreeView (ITreeBuilder<T> builder) : this ()
-		{
-			TreeBuilder = builder;
-		}
-
-		///<inheritdoc/>
-		public override bool OnEnter (View view)
-		{
-			Application.Driver.SetCursorVisibility (DesiredCursorVisibility);
-
-			if (SelectedObject == null && Objects.Any ()) {
-				SelectedObject = Objects.First ();
-			}
+	/// <summary>
+	/// Mouse event to trigger <see cref="TreeView{T}.ObjectActivated"/>.
+	/// Defaults to double click (<see cref="MouseFlags.Button1DoubleClicked"/>).
+	/// Set to null to disable this feature.
+	/// </summary>
+	/// <value></value>
+	public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked;
 
-			return base.OnEnter (view);
-		}
+	/// <summary>
+	/// Delegate for multi colored tree views. Return the <see cref="ColorScheme"/> to use
+	/// for each passed object or null to use the default.
+	/// </summary>
+	public Func<T, ColorScheme> ColorGetter { get; set; }
 
-		/// <summary>
-		/// Adds a new root level object unless it is already a root of the tree.
-		/// </summary>
-		/// <param name="o"></param>
-		public void AddObject (T o)
-		{
-			if (!roots.ContainsKey (o)) {
-				roots.Add (o, new Branch<T> (this, null, o));
-				InvalidateLineMap ();
-				SetNeedsDisplay ();
-			}
-		}
+	/// <summary>
+	/// The root objects in the tree, note that this collection is of root objects only.
+	/// </summary>
+	public IEnumerable<T> Objects => roots.Keys;
 
-		/// <summary>
-		/// Removes all objects from the tree and clears <see cref="SelectedObject"/>.
-		/// </summary>
-		public void ClearObjects ()
-		{
-			SelectedObject = default (T);
-			multiSelectedRegions.Clear ();
-			roots = new Dictionary<T, Branch<T>> ();
-			InvalidateLineMap ();
-			SetNeedsDisplay ();
-		}
+	/// <summary>
+	/// Map of root objects to the branches under them. All objects have
+	/// a <see cref="Branch{T}"/> even if that branch has no children.
+	/// </summary>
+	internal Dictionary<T, Branch<T>> roots { get; set; } = new ();
 
-		/// <summary>
-		/// Removes the given root object from the tree
-		/// </summary>
-		/// <remarks>If <paramref name="o"/> is the currently <see cref="SelectedObject"/> then the
-		/// selection is cleared</remarks>.
-		/// <param name="o"></param>
-		public void Remove (T o)
-		{
-			if (roots.ContainsKey (o)) {
-				roots.Remove (o);
-				InvalidateLineMap ();
-				SetNeedsDisplay ();
+	/// <summary>
+	/// The amount of tree view that has been scrolled off the top of the screen (by the user
+	/// scrolling down).
+	/// </summary>
+	/// <remarks>
+	/// Setting a value of less than 0 will result in a offset of 0. To see changes
+	/// in the UI call <see cref="View.SetNeedsDisplay()"/>.
+	/// </remarks>
+	public int ScrollOffsetVertical {
+		get => scrollOffsetVertical;
+		set => scrollOffsetVertical = Math.Max (0, value);
+	}
+
+	/// <summary>
+	/// The amount of tree view that has been scrolled to the right (horizontally).
+	/// </summary>
+	/// <remarks>
+	/// Setting a value of less than 0 will result in a offset of 0. To see changes
+	/// in the UI call <see cref="View.SetNeedsDisplay()"/>.
+	/// </remarks>
+	public int ScrollOffsetHorizontal {
+		get => scrollOffsetHorizontal;
+		set => scrollOffsetHorizontal = Math.Max (0, value);
+	}
+
+	/// <summary>
+	/// The current number of rows in the tree (ignoring the controls bounds).
+	/// </summary>
+	public int ContentHeight => BuildLineMap ().Count ();
+
+	/// <summary>
+	/// Returns the string representation of model objects hosted in the tree. Default
+	/// implementation is to call <see cref="object.ToString"/>.
+	/// </summary>
+	/// <value></value>
+	public AspectGetterDelegate<T> AspectGetter { get; set; } = o => o.ToString () ?? "";
 
-				if (Equals (SelectedObject, o)) {
-					SelectedObject = default (T);
+	/// <summary>
+	/// Get / Set the wished cursor when the tree is focused.
+	/// Only applies when <see cref="MultiSelect"/> is true.
+	/// Defaults to <see cref="CursorVisibility.Invisible"/>.
+	/// </summary>
+	public CursorVisibility DesiredCursorVisibility {
+		get => MultiSelect ? desiredCursorVisibility : CursorVisibility.Invisible;
+		set {
+			if (desiredCursorVisibility != value) {
+				desiredCursorVisibility = value;
+				if (HasFocus) {
+					Application.Driver.SetCursorVisibility (DesiredCursorVisibility);
 				}
 			}
 		}
+	}
 
-		/// <summary>
-		/// Adds many new root level objects. Objects that are already root objects are ignored.
-		/// </summary>
-		/// <param name="collection">Objects to add as new root level objects.</param>.\
-		public void AddObjects (IEnumerable<T> collection)
-		{
-			bool objectsAdded = false;
+	/// <summary>
+	/// Gets the <see cref="CollectionNavigator"/> that searches the <see cref="Objects"/> collection as
+	/// the user types.
+	/// </summary>
+	public CollectionNavigator KeystrokeNavigator { get; } = new ();
 
-			foreach (var o in collection) {
-				if (!roots.ContainsKey (o)) {
-					roots.Add (o, new Branch<T> (this, null, o));
-					objectsAdded = true;
-				}
-			}
+	/// <summary>
+	/// Contains options for changing how the tree is rendered.
+	/// </summary>
+	public TreeStyle Style { get; set; } = new ();
 
-			if (objectsAdded) {
-				InvalidateLineMap ();
-				SetNeedsDisplay ();
-			}
-		}
+	/// <summary>
+	/// Removes all objects from the tree and clears <see cref="SelectedObject"/>.
+	/// </summary>
+	public void ClearObjects ()
+	{
+		SelectedObject = default;
+		multiSelectedRegions.Clear ();
+		roots = new Dictionary<T, Branch<T>> ();
+		InvalidateLineMap ();
+		SetNeedsDisplay ();
+	}
 
-		/// <summary>
-		/// Refreshes the state of the object <paramref name="o"/> in the tree. This will 
-		/// recompute children, string representation etc.
-		/// </summary>
-		/// <remarks>This has no effect if the object is not exposed in the tree.</remarks>
-		/// <param name="o"></param>
-		/// <param name="startAtTop">True to also refresh all ancestors of the objects branch 
-		/// (starting with the root). False to refresh only the passed node.</param>
-		public void RefreshObject (T o, bool startAtTop = false)
-		{
-			var branch = ObjectToBranch (o);
-			if (branch != null) {
-				branch.Refresh (startAtTop);
-				InvalidateLineMap ();
-				SetNeedsDisplay ();
-			}
+	/// <summary>
+	/// This event is raised when an object is activated e.g. by double clicking or
+	/// pressing <see cref="ObjectActivationKey"/>.
+	/// </summary>
+	public event EventHandler<ObjectActivatedEventArgs<T>> ObjectActivated;
+
+	/// <summary>
+	/// Called when the <see cref="SelectedObject"/> changes.
+	/// </summary>
+	public event EventHandler<SelectionChangedEventArgs<T>> SelectionChanged;
+
+	/// <summary>
+	/// Called once for each visible row during rendering.  Can be used
+	/// to make last minute changes to color or text rendered
+	/// </summary>
+	public event EventHandler<DrawTreeViewLineEventArgs<T>> DrawLine;
+
+	///<inheritdoc/>
+	public override bool OnEnter (View view)
+	{
+		Application.Driver.SetCursorVisibility (DesiredCursorVisibility);
 
+		if (SelectedObject == null && Objects.Any ()) {
+			SelectedObject = Objects.First ();
 		}
 
-		/// <summary>
-		/// Rebuilds the tree structure for all exposed objects starting with the root objects.
-		/// Call this method when you know there are changes to the tree but don't know which 
-		/// objects have changed (otherwise use <see cref="RefreshObject(T, bool)"/>).
-		/// </summary>
-		public void RebuildTree ()
-		{
-			foreach (var branch in roots.Values) {
-				branch.Rebuild ();
-			}
+		return base.OnEnter (view);
+	}
 
+	/// <summary>
+	/// Adds a new root level object unless it is already a root of the tree.
+	/// </summary>
+	/// <param name="o"></param>
+	public void AddObject (T o)
+	{
+		if (!roots.ContainsKey (o)) {
+			roots.Add (o, new Branch<T> (this, null, o));
 			InvalidateLineMap ();
 			SetNeedsDisplay ();
 		}
+	}
 
-		/// <summary>
-		/// Returns the currently expanded children of the passed object. Returns an empty
-		/// collection if the branch is not exposed or not expanded.
-		/// </summary>
-		/// <param name="o">An object in the tree.</param>
-		/// <returns></returns>
-		public IEnumerable<T> GetChildren (T o)
-		{
-			var branch = ObjectToBranch (o);
+	/// <summary>
+	/// Removes the given root object from the tree
+	/// </summary>
+	/// <remarks>
+	/// If <paramref name="o"/> is the currently <see cref="SelectedObject"/> then the
+	/// selection is cleared
+	/// </remarks>
+	/// .
+	/// <param name="o"></param>
+	public void Remove (T o)
+	{
+		if (roots.ContainsKey (o)) {
+			roots.Remove (o);
+			InvalidateLineMap ();
+			SetNeedsDisplay ();
 
-			if (branch == null || !branch.IsExpanded) {
-				return new T [0];
+			if (Equals (SelectedObject, o)) {
+				SelectedObject = default;
 			}
-
-			return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0];
-		}
-		/// <summary>
-		/// Returns the parent object of <paramref name="o"/> in the tree. Returns null if 
-		/// the object is not exposed in the tree.
-		/// </summary>
-		/// <param name="o">An object in the tree.</param>
-		/// <returns></returns>
-		public T GetParent (T o)
-		{
-			return ObjectToBranch (o)?.Parent?.Model;
 		}
+	}
 
-		///<inheritdoc/>
-		public override void OnDrawContent (Rect contentArea)
-		{
-			if (roots == null) {
-				return;
-			}
+	/// <summary>
+	/// Adds many new root level objects. Objects that are already root objects are ignored.
+	/// </summary>
+	/// <param name="collection">Objects to add as new root level objects.</param>
+	/// .\
+	public void AddObjects (IEnumerable<T> collection)
+	{
+		var objectsAdded = false;
 
-			if (TreeBuilder == null) {
-				Move (0, 0);
-				Driver.AddStr (NoBuilderError);
-				return;
+		foreach (var o in collection) {
+			if (!roots.ContainsKey (o)) {
+				roots.Add (o, new Branch<T> (this, null, o));
+				objectsAdded = true;
 			}
+		}
 
-			var map = BuildLineMap ();
-
-			for (int line = 0; line < Bounds.Height; line++) {
+		if (objectsAdded) {
+			InvalidateLineMap ();
+			SetNeedsDisplay ();
+		}
+	}
 
-				var idxToRender = ScrollOffsetVertical + line;
+	/// <summary>
+	/// Refreshes the state of the object <paramref name="o"/> in the tree. This will
+	/// recompute children, string representation etc.
+	/// </summary>
+	/// <remarks>This has no effect if the object is not exposed in the tree.</remarks>
+	/// <param name="o"></param>
+	/// <param name="startAtTop">
+	/// True to also refresh all ancestors of the objects branch
+	/// (starting with the root). False to refresh only the passed node.
+	/// </param>
+	public void RefreshObject (T o, bool startAtTop = false)
+	{
+		var branch = ObjectToBranch (o);
+		if (branch != null) {
+			branch.Refresh (startAtTop);
+			InvalidateLineMap ();
+			SetNeedsDisplay ();
+		}
 
-				// Is there part of the tree view to render?
-				if (idxToRender < map.Count) {
-					// Render the line
-					map.ElementAt (idxToRender).Draw (Driver, ColorScheme, line, Bounds.Width);
-				} else {
+	}
 
-					// Else clear the line to prevent stale symbols due to scrolling etc
-					Move (0, line);
-					Driver.SetAttribute (GetNormalColor ());
-					Driver.AddStr (new string (' ', Bounds.Width));
-				}
-			}
+	/// <summary>
+	/// Rebuilds the tree structure for all exposed objects starting with the root objects.
+	/// Call this method when you know there are changes to the tree but don't know which
+	/// objects have changed (otherwise use <see cref="RefreshObject(T, bool)"/>).
+	/// </summary>
+	public void RebuildTree ()
+	{
+		foreach (var branch in roots.Values) {
+			branch.Rebuild ();
 		}
 
-		/// <summary>
-		/// Returns the index of the object <paramref name="o"/> if it is currently exposed (it's 
-		/// parent(s) have been expanded). This can be used with <see cref="ScrollOffsetVertical"/>
-		/// and <see cref="View.SetNeedsDisplay()"/> to scroll to a specific object.
-		/// </summary>
-		/// <remarks>Uses the Equals method and returns the first index at which the object is found
-		/// or -1 if it is not found.</remarks>
-		/// <param name="o">An object that appears in your tree and is currently exposed.</param>
-		/// <returns>The index the object was found at or -1 if it is not currently revealed or
-		/// not in the tree at all.</returns>
-		public int GetScrollOffsetOf (T o)
-		{
-			var map = BuildLineMap ();
-			for (int i = 0; i < map.Count; i++) {
-				if (map.ElementAt (i).Model.Equals (o)) {
-					return i;
-				}
-			}
+		InvalidateLineMap ();
+		SetNeedsDisplay ();
+	}
+
+	/// <summary>
+	/// Returns the currently expanded children of the passed object. Returns an empty
+	/// collection if the branch is not exposed or not expanded.
+	/// </summary>
+	/// <param name="o">An object in the tree.</param>
+	/// <returns></returns>
+	public IEnumerable<T> GetChildren (T o)
+	{
+		var branch = ObjectToBranch (o);
 
-			//object not found
-			return -1;
+		if (branch == null || !branch.IsExpanded) {
+			return new T [0];
 		}
 
-		/// <summary>
-		/// Returns the maximum width line in the tree including prefix and expansion symbols.
-		/// </summary>
-		/// <param name="visible">True to consider only rows currently visible (based on window
-		/// bounds and <see cref="ScrollOffsetVertical"/>. False to calculate the width of 
-		/// every exposed branch in the tree.</param>
-		/// <returns></returns>
-		public int GetContentWidth (bool visible)
-		{
-			var map = BuildLineMap ();
+		return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0];
+	}
 
-			if (map.Count == 0) {
-				return 0;
-			}
+	/// <summary>
+	/// Returns the parent object of <paramref name="o"/> in the tree. Returns null if
+	/// the object is not exposed in the tree.
+	/// </summary>
+	/// <param name="o">An object in the tree.</param>
+	/// <returns></returns>
+	public T GetParent (T o) => ObjectToBranch (o)?.Parent?.Model;
 
-			if (visible) {
+	///<inheritdoc/>
+	public override void OnDrawContent (Rect contentArea)
+	{
+		if (roots == null) {
+			return;
+		}
 
-				//Somehow we managed to scroll off the end of the control
-				if (ScrollOffsetVertical >= map.Count) {
-					return 0;
-				}
+		if (TreeBuilder == null) {
+			Move (0, 0);
+			Driver.AddStr (NoBuilderError);
+			return;
+		}
 
-				// If control has no height to it then there is no visible area for content
-				if (Bounds.Height == 0) {
-					return 0;
-				}
+		var map = BuildLineMap ();
 
-				return map.Skip (ScrollOffsetVertical).Take (Bounds.Height).Max (b => b.GetWidth (Driver));
+		for (var line = 0; line < Bounds.Height; line++) {
+
+			var idxToRender = ScrollOffsetVertical + line;
+
+			// Is there part of the tree view to render?
+			if (idxToRender < map.Count) {
+				// Render the line
+				map.ElementAt (idxToRender).Draw (Driver, ColorScheme, line, Bounds.Width);
 			} else {
 
-				return map.Max (b => b.GetWidth (Driver));
+				// Else clear the line to prevent stale symbols due to scrolling etc
+				Move (0, line);
+				Driver.SetAttribute (GetNormalColor ());
+				Driver.AddStr (new string (' ', Bounds.Width));
 			}
 		}
+	}
 
-		/// <summary>
-		/// Calculates all currently visible/expanded branches (including leafs) and outputs them 
-		/// by index from the top of the screen.
-		/// </summary>
-		/// <remarks>Index 0 of the returned array is the first item that should be visible in the
-		/// top of the control, index 1 is the next etc.</remarks>
-		/// <returns></returns>
-		internal IReadOnlyCollection<Branch<T>> BuildLineMap ()
-		{
-			if (cachedLineMap != null) {
-				return cachedLineMap;
-			}
+	/// <summary>
+	/// Returns the index of the object <paramref name="o"/> if it is currently exposed (it's
+	/// parent(s) have been expanded). This can be used with <see cref="ScrollOffsetVertical"/>
+	/// and <see cref="View.SetNeedsDisplay()"/> to scroll to a specific object.
+	/// </summary>
+	/// <remarks>
+	/// Uses the Equals method and returns the first index at which the object is found
+	/// or -1 if it is not found.
+	/// </remarks>
+	/// <param name="o">An object that appears in your tree and is currently exposed.</param>
+	/// <returns>
+	/// The index the object was found at or -1 if it is not currently revealed or
+	/// not in the tree at all.
+	/// </returns>
+	public int GetScrollOffsetOf (T o)
+	{
+		var map = BuildLineMap ();
+		for (var i = 0; i < map.Count; i++) {
+			if (map.ElementAt (i).Model.Equals (o)) {
+				return i;
+			}
+		}
+
+		//object not found
+		return -1;
+	}
+
+	/// <summary>
+	/// Returns the maximum width line in the tree including prefix and expansion symbols.
+	/// </summary>
+	/// <param name="visible">
+	/// True to consider only rows currently visible (based on window
+	/// bounds and <see cref="ScrollOffsetVertical"/>. False to calculate the width of
+	/// every exposed branch in the tree.
+	/// </param>
+	/// <returns></returns>
+	public int GetContentWidth (bool visible)
+	{
+		var map = BuildLineMap ();
 
-			List<Branch<T>> toReturn = new List<Branch<T>> ();
+		if (map.Count == 0) {
+			return 0;
+		}
 
-			foreach (var root in roots.Values) {
+		if (visible) {
 
-				var toAdd = AddToLineMap (root, false, out var isMatch);
-				if (isMatch) {
-					toReturn.AddRange (toAdd);
-				}
+			//Somehow we managed to scroll off the end of the control
+			if (ScrollOffsetVertical >= map.Count) {
+				return 0;
 			}
 
-			cachedLineMap = new ReadOnlyCollection<Branch<T>> (toReturn);
+			// If control has no height to it then there is no visible area for content
+			if (Bounds.Height == 0) {
+				return 0;
+			}
 
-			// Update the collection used for search-typing
-			KeystrokeNavigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray ();
-			return cachedLineMap;
+			return map.Skip (ScrollOffsetVertical).Take (Bounds.Height).Max (b => b.GetWidth (Driver));
 		}
+		return map.Max (b => b.GetWidth (Driver));
+	}
 
-		private bool IsFilterMatch (Branch<T> branch)
-		{
-			return Filter?.IsMatch (branch.Model) ?? true;
+	/// <summary>
+	/// Calculates all currently visible/expanded branches (including leafs) and outputs them
+	/// by index from the top of the screen.
+	/// </summary>
+	/// <remarks>
+	/// Index 0 of the returned array is the first item that should be visible in the
+	/// top of the control, index 1 is the next etc.
+	/// </remarks>
+	/// <returns></returns>
+	internal IReadOnlyCollection<Branch<T>> BuildLineMap ()
+	{
+		if (cachedLineMap != null) {
+			return cachedLineMap;
 		}
 
-		private IEnumerable<Branch<T>> AddToLineMap (Branch<T> currentBranch, bool parentMatches, out bool match)
-		{
-			bool weMatch = IsFilterMatch (currentBranch);
-			bool anyChildMatches = false;
+		var toReturn = new List<Branch<T>> ();
 
-			var toReturn = new List<Branch<T>> ();
-			var children = new List<Branch<T>> ();
+		foreach (var root in roots.Values) {
 
-			if (currentBranch.IsExpanded) {
-				foreach (var subBranch in currentBranch.ChildBranches.Values) {
+			var toAdd = AddToLineMap (root, false, out var isMatch);
+			if (isMatch) {
+				toReturn.AddRange (toAdd);
+			}
+		}
 
-					foreach (var sub in AddToLineMap (subBranch, weMatch, out var childMatch)) {
+		cachedLineMap = new ReadOnlyCollection<Branch<T>> (toReturn);
 
-						if (childMatch) {
-							children.Add (sub);
-							anyChildMatches = true;
-						}
-					}
-				}
-			}
+		// Update the collection used for search-typing
+		KeystrokeNavigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray ();
+		return cachedLineMap;
+	}
 
-			if (parentMatches || weMatch || anyChildMatches) {
-				match = true;
-				toReturn.Add (currentBranch);
-			} else {
-				match = false;
-			}
+	bool IsFilterMatch (Branch<T> branch) => Filter?.IsMatch (branch.Model) ?? true;
 
-			toReturn.AddRange (children);
-			return toReturn;
-		}
+	IEnumerable<Branch<T>> AddToLineMap (Branch<T> currentBranch, bool parentMatches, out bool match)
+	{
+		var weMatch = IsFilterMatch (currentBranch);
+		var anyChildMatches = false;
 
-		/// <summary>
-		/// Gets the <see cref="CollectionNavigator"/> that searches the <see cref="Objects"/> collection as
-		/// the user types.
-		/// </summary>
-		public CollectionNavigator KeystrokeNavigator { get; private set; } = new CollectionNavigator ();
+		var toReturn = new List<Branch<T>> ();
+		var children = new List<Branch<T>> ();
 
-		/// <inheritdoc/>
-		public override bool OnProcessKeyDown (Key keyEvent)
-		{
-			if (!Enabled) {
-				return false;
-			}
+		if (currentBranch.IsExpanded) {
+			foreach (var subBranch in currentBranch.ChildBranches.Values) {
+
+				foreach (var sub in AddToLineMap (subBranch, weMatch, out var childMatch)) {
 
-			try {
-				// BUGBUG: this should move to OnInvokingKeyBindings
-				// If not a keybinding, is the key a searchable key press?
-				if (CollectionNavigator.IsCompatibleKey (keyEvent) && AllowLetterBasedNavigation) {
-					IReadOnlyCollection<Branch<T>> map;
-
-					// If there has been a call to InvalidateMap since the last time
-					// we need a new one to reflect the new exposed tree state
-					map = BuildLineMap ();
-
-					// Find the current selected object within the tree
-					var current = map.IndexOf (b => b.Model == SelectedObject);
-					var newIndex = KeystrokeNavigator?.GetNextMatchingItem (current, (char)keyEvent);
-
-					if (newIndex is int && newIndex != -1) {
-						SelectedObject = map.ElementAt ((int)newIndex).Model;
-						EnsureVisible (selectedObject);
-						SetNeedsDisplay ();
-						return true;
+					if (childMatch) {
+						children.Add (sub);
+						anyChildMatches = true;
 					}
 				}
-			} finally {
-				if (IsInitialized) {
-					PositionCursor ();
-				}
 			}
+		}
+
+		if (parentMatches || weMatch || anyChildMatches) {
+			match = true;
+			toReturn.Add (currentBranch);
+		} else {
+			match = false;
+		}
 
+		toReturn.AddRange (children);
+		return toReturn;
+	}
+
+	/// <inheritdoc/>
+	public override bool OnProcessKeyDown (Key keyEvent)
+	{
+		if (!Enabled) {
 			return false;
 		}
 
-		/// <summary>
-		/// <para>Triggers the <see cref="ObjectActivated"/> event with the <see cref="SelectedObject"/>.</para>
-		/// 
-		/// <para>This method also ensures that the selected object is visible.</para>
-		/// </summary>
-		public void ActivateSelectedObjectIfAny ()
-		{
-			var o = SelectedObject;
+		try {
+			// BUGBUG: this should move to OnInvokingKeyBindings
+			// If not a keybinding, is the key a searchable key press?
+			if (CollectionNavigatorBase.IsCompatibleKey (keyEvent) && AllowLetterBasedNavigation) {
+				IReadOnlyCollection<Branch<T>> map;
+
+				// If there has been a call to InvalidateMap since the last time
+				// we need a new one to reflect the new exposed tree state
+				map = BuildLineMap ();
+
+				// Find the current selected object within the tree
+				var current = map.IndexOf (b => b.Model == SelectedObject);
+				var newIndex = KeystrokeNavigator?.GetNextMatchingItem (current, (char)keyEvent);
 
-			if (o != null) {
-				OnObjectActivated (new ObjectActivatedEventArgs<T> (this, o));
+				if (newIndex is int && newIndex != -1) {
+					SelectedObject = map.ElementAt ((int)newIndex).Model;
+					EnsureVisible (selectedObject);
+					SetNeedsDisplay ();
+					return true;
+				}
+			}
+		} finally {
+			if (IsInitialized) {
 				PositionCursor ();
 			}
 		}
 
-		/// <summary>
-		/// <para>
-		/// Returns the Y coordinate within the <see cref="View.Bounds"/> of the
-		/// tree at which <paramref name="toFind"/> would be displayed or null if
-		/// it is not currently exposed (e.g. its parent is collapsed).
-		/// </para>
-		/// <para>
-		/// Note that the returned value can be negative if the TreeView is scrolled
-		/// down and the <paramref name="toFind"/> object is off the top of the view.
-		/// </para>
-		/// </summary>
-		/// <param name="toFind"></param>
-		/// <returns></returns>
-		public int? GetObjectRow (T toFind)
-		{
-			var idx = BuildLineMap ().IndexOf (o => o.Model.Equals (toFind));
-
-			if (idx == -1)
-				return null;
-
-			return idx - ScrollOffsetVertical;
-		}
-
-		/// <summary>
-		/// <para>Moves the <see cref="SelectedObject"/> to the next item that begins with <paramref name="character"/>.</para>
-		/// <para>This method will loop back to the start of the tree if reaching the end without finding a match.</para>
-		/// </summary>
-		/// <param name="character">The first character of the next item you want selected.</param>
-		/// <param name="caseSensitivity">Case sensitivity of the search.</param>
-		public void AdjustSelectionToNextItemBeginningWith (char character, StringComparison caseSensitivity = StringComparison.CurrentCultureIgnoreCase)
-		{
-			// search for next branch that begins with that letter
-			var characterAsStr = character.ToString ();
-			AdjustSelectionToNext (b => AspectGetter (b.Model).StartsWith (characterAsStr, caseSensitivity));
+		return false;
+	}
+
+	/// <summary>
+	///         <para>Triggers the <see cref="ObjectActivated"/> event with the <see cref="SelectedObject"/>.</para>
+	/// 
+	///         <para>This method also ensures that the selected object is visible.</para>
+	/// </summary>
+	public void ActivateSelectedObjectIfAny ()
+	{
+		var o = SelectedObject;
 
+		if (o != null) {
+			OnObjectActivated (new ObjectActivatedEventArgs<T> (this, o));
 			PositionCursor ();
 		}
+	}
 
-		/// <summary>
-		/// Moves the selection up by the height of the control (1 page).
-		/// </summary>
-		/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection.</param>
-		/// <exception cref="NotImplementedException"></exception>
-		public void MovePageUp (bool expandSelection = false)
-		{
-			AdjustSelection (-Bounds.Height, expandSelection);
-		}
-
-		/// <summary>
-		/// Moves the selection down by the height of the control (1 page).
-		/// </summary>
-		/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection.</param>
-		/// <exception cref="NotImplementedException"></exception>
-		public void MovePageDown (bool expandSelection = false)
-		{
-			AdjustSelection (Bounds.Height, expandSelection);
-		}
-
-		/// <summary>
-		/// Scrolls the view area down a single line without changing the current selection.
-		/// </summary>
-		public void ScrollDown ()
-		{
-			if (ScrollOffsetVertical <= ContentHeight - 2) {
-				ScrollOffsetVertical++;
-				SetNeedsDisplay ();
-			}
+	/// <summary>
+	///         <para>
+	///         Returns the Y coordinate within the <see cref="View.Bounds"/> of the
+	///         tree at which <paramref name="toFind"/> would be displayed or null if
+	///         it is not currently exposed (e.g. its parent is collapsed).
+	///         </para>
+	///         <para>
+	///         Note that the returned value can be negative if the TreeView is scrolled
+	///         down and the <paramref name="toFind"/> object is off the top of the view.
+	///         </para>
+	/// </summary>
+	/// <param name="toFind"></param>
+	/// <returns></returns>
+	public int? GetObjectRow (T toFind)
+	{
+		var idx = BuildLineMap ().IndexOf (o => o.Model.Equals (toFind));
+
+		if (idx == -1) {
+			return null;
 		}
 
-		/// <summary>
-		/// Scrolls the view area up a single line without changing the current selection.
-		/// </summary>
-		public void ScrollUp ()
-		{
-			if (scrollOffsetVertical > 0) {
-				ScrollOffsetVertical--;
-				SetNeedsDisplay ();
-			}
+		return idx - ScrollOffsetVertical;
+	}
+
+	/// <summary>
+	///         <para>Moves the <see cref="SelectedObject"/> to the next item that begins with <paramref name="character"/>.</para>
+	///         <para>This method will loop back to the start of the tree if reaching the end without finding a match.</para>
+	/// </summary>
+	/// <param name="character">The first character of the next item you want selected.</param>
+	/// <param name="caseSensitivity">Case sensitivity of the search.</param>
+	public void AdjustSelectionToNextItemBeginningWith (char character, StringComparison caseSensitivity = StringComparison.CurrentCultureIgnoreCase)
+	{
+		// search for next branch that begins with that letter
+		var characterAsStr = character.ToString ();
+		AdjustSelectionToNext (b => AspectGetter (b.Model).StartsWith (characterAsStr, caseSensitivity));
+
+		PositionCursor ();
+	}
+
+	/// <summary>
+	/// Moves the selection up by the height of the control (1 page).
+	/// </summary>
+	/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection.</param>
+	/// <exception cref="NotImplementedException"></exception>
+	public void MovePageUp (bool expandSelection = false) => AdjustSelection (-Bounds.Height, expandSelection);
+
+	/// <summary>
+	/// Moves the selection down by the height of the control (1 page).
+	/// </summary>
+	/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection.</param>
+	/// <exception cref="NotImplementedException"></exception>
+	public void MovePageDown (bool expandSelection = false) => AdjustSelection (Bounds.Height, expandSelection);
+
+	/// <summary>
+	/// Scrolls the view area down a single line without changing the current selection.
+	/// </summary>
+	public void ScrollDown ()
+	{
+		if (ScrollOffsetVertical <= ContentHeight - 2) {
+			ScrollOffsetVertical++;
+			SetNeedsDisplay ();
 		}
+	}
 
-		/// <summary>
-		/// Raises the <see cref="ObjectActivated"/> event.
-		/// </summary>
-		/// <param name="e"></param>
-		protected virtual void OnObjectActivated (ObjectActivatedEventArgs<T> e)
-		{
-			ObjectActivated?.Invoke (this, e);
-		}
-
-		/// <summary>
-		/// Returns the object in the tree list that is currently visible.
-		/// at the provided row. Returns null if no object is at that location.
-		/// <remarks>
-		/// </remarks>
-		/// If you have screen coordinates then use <see cref="View.ScreenToFrame"/>
-		/// to translate these into the client area of the <see cref="TreeView{T}"/>.
-		/// </summary>
-		/// <param name="row">The row of the <see cref="View.Bounds"/> of the <see cref="TreeView{T}"/>.</param>
-		/// <returns>The object currently displayed on this row or null.</returns>
-		public T GetObjectOnRow (int row)
-		{
-			return HitTest (row)?.Model;
-		}
-
-		///<inheritdoc/>
-		public override bool MouseEvent (MouseEvent me)
-		{
-			// If it is not an event we care about
-			if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) &&
-				!me.Flags.HasFlag (ObjectActivationButton ?? MouseFlags.Button1DoubleClicked) &&
-				!me.Flags.HasFlag (MouseFlags.WheeledDown) &&
-				!me.Flags.HasFlag (MouseFlags.WheeledUp) &&
-				!me.Flags.HasFlag (MouseFlags.WheeledRight) &&
-				!me.Flags.HasFlag (MouseFlags.WheeledLeft)) {
-
-				// do nothing
-				return false;
-			}
+	/// <summary>
+	/// Scrolls the view area up a single line without changing the current selection.
+	/// </summary>
+	public void ScrollUp ()
+	{
+		if (scrollOffsetVertical > 0) {
+			ScrollOffsetVertical--;
+			SetNeedsDisplay ();
+		}
+	}
 
-			if (!HasFocus && CanFocus) {
-				SetFocus ();
-			}
+	/// <summary>
+	/// Raises the <see cref="ObjectActivated"/> event.
+	/// </summary>
+	/// <param name="e"></param>
+	protected virtual void OnObjectActivated (ObjectActivatedEventArgs<T> e) => ObjectActivated?.Invoke (this, e);
 
-			if (me.Flags == MouseFlags.WheeledDown) {
+	/// <summary>
+	/// Returns the object in the tree list that is currently visible.
+	/// at the provided row. Returns null if no object is at that location.
+	/// <remarks>
+	/// </remarks>
+	/// If you have screen coordinates then use <see cref="View.ScreenToFrame"/>
+	/// to translate these into the client area of the <see cref="TreeView{T}"/>.
+	/// </summary>
+	/// <param name="row">The row of the <see cref="View.Bounds"/> of the <see cref="TreeView{T}"/>.</param>
+	/// <returns>The object currently displayed on this row or null.</returns>
+	public T GetObjectOnRow (int row) => HitTest (row)?.Model;
+
+	///<inheritdoc/>
+	public override bool MouseEvent (MouseEvent me)
+	{
+		// If it is not an event we care about
+		if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) &&
+		    !me.Flags.HasFlag (ObjectActivationButton ?? MouseFlags.Button1DoubleClicked) &&
+		    !me.Flags.HasFlag (MouseFlags.WheeledDown) &&
+		    !me.Flags.HasFlag (MouseFlags.WheeledUp) &&
+		    !me.Flags.HasFlag (MouseFlags.WheeledRight) &&
+		    !me.Flags.HasFlag (MouseFlags.WheeledLeft)) {
+
+			// do nothing
+			return false;
+		}
 
-				ScrollDown ();
+		if (!HasFocus && CanFocus) {
+			SetFocus ();
+		}
 
-				return true;
-			} else if (me.Flags == MouseFlags.WheeledUp) {
-				ScrollUp ();
+		if (me.Flags == MouseFlags.WheeledDown) {
 
-				return true;
-			}
+			ScrollDown ();
 
-			if (me.Flags == MouseFlags.WheeledRight) {
+			return true;
+		}
+		if (me.Flags == MouseFlags.WheeledUp) {
+			ScrollUp ();
 
-				ScrollOffsetHorizontal++;
-				SetNeedsDisplay ();
+			return true;
+		}
 
-				return true;
-			} else if (me.Flags == MouseFlags.WheeledLeft) {
-				ScrollOffsetHorizontal--;
-				SetNeedsDisplay ();
+		if (me.Flags == MouseFlags.WheeledRight) {
 
-				return true;
-			}
+			ScrollOffsetHorizontal++;
+			SetNeedsDisplay ();
+
+			return true;
+		}
+		if (me.Flags == MouseFlags.WheeledLeft) {
+			ScrollOffsetHorizontal--;
+			SetNeedsDisplay ();
 
-			if (me.Flags.HasFlag (MouseFlags.Button1Clicked)) {
+			return true;
+		}
 
-				// The line they clicked on a branch
-				var clickedBranch = HitTest (me.Y);
+		if (me.Flags.HasFlag (MouseFlags.Button1Clicked)) {
 
-				if (clickedBranch == null) {
-					return false;
-				}
+			// The line they clicked on a branch
+			var clickedBranch = HitTest (me.Y);
 
-				bool isExpandToggleAttempt = clickedBranch.IsHitOnExpandableSymbol (Driver, me.X);
+			if (clickedBranch == null) {
+				return false;
+			}
 
-				// If we are already selected (double click)
-				if (Equals (SelectedObject, clickedBranch.Model)) {
-					isExpandToggleAttempt = true;
-				}
+			var isExpandToggleAttempt = clickedBranch.IsHitOnExpandableSymbol (Driver, me.X);
 
-				// if they clicked on the +/- expansion symbol
-				if (isExpandToggleAttempt) {
+			// If we are already selected (double click)
+			if (Equals (SelectedObject, clickedBranch.Model)) {
+				isExpandToggleAttempt = true;
+			}
 
-					if (clickedBranch.IsExpanded) {
-						clickedBranch.Collapse ();
-						InvalidateLineMap ();
-					} else
-					if (clickedBranch.CanExpand ()) {
-						clickedBranch.Expand ();
-						InvalidateLineMap ();
-					} else {
-						SelectedObject = clickedBranch.Model; // It is a leaf node
-						multiSelectedRegions.Clear ();
-					}
+			// if they clicked on the +/- expansion symbol
+			if (isExpandToggleAttempt) {
+
+				if (clickedBranch.IsExpanded) {
+					clickedBranch.Collapse ();
+					InvalidateLineMap ();
+				} else if (clickedBranch.CanExpand ()) {
+					clickedBranch.Expand ();
+					InvalidateLineMap ();
 				} else {
-					// It is a first click somewhere in the current line that doesn't look like an expansion/collapse attempt
-					SelectedObject = clickedBranch.Model;
+					SelectedObject = clickedBranch.Model; // It is a leaf node
 					multiSelectedRegions.Clear ();
 				}
-
-				SetNeedsDisplay ();
-				return true;
+			} else {
+				// It is a first click somewhere in the current line that doesn't look like an expansion/collapse attempt
+				SelectedObject = clickedBranch.Model;
+				multiSelectedRegions.Clear ();
 			}
 
-			// If it is activation via mouse (e.g. double click)
-			if (ObjectActivationButton.HasValue && me.Flags.HasFlag (ObjectActivationButton.Value)) {
-				// The line they clicked on a branch
-				var clickedBranch = HitTest (me.Y);
+			SetNeedsDisplay ();
+			return true;
+		}
+
+		// If it is activation via mouse (e.g. double click)
+		if (ObjectActivationButton.HasValue && me.Flags.HasFlag (ObjectActivationButton.Value)) {
+			// The line they clicked on a branch
+			var clickedBranch = HitTest (me.Y);
 
-				if (clickedBranch == null) {
-					return false;
-				}
+			if (clickedBranch == null) {
+				return false;
+			}
 
-				// Double click changes the selection to the clicked node as well as triggering
-				// activation otherwise it feels wierd
-				SelectedObject = clickedBranch.Model;
-				SetNeedsDisplay ();
+			// Double click changes the selection to the clicked node as well as triggering
+			// activation otherwise it feels wierd
+			SelectedObject = clickedBranch.Model;
+			SetNeedsDisplay ();
 
-				// trigger activation event				
-				OnObjectActivated (new ObjectActivatedEventArgs<T> (this, clickedBranch.Model));
+			// trigger activation event				
+			OnObjectActivated (new ObjectActivatedEventArgs<T> (this, clickedBranch.Model));
 
-				// mouse event is handled.
-				return true;
-			}
-			return false;
+			// mouse event is handled.
+			return true;
 		}
+		return false;
+	}
 
-		/// <summary>
-		/// Returns the branch at the given <paramref name="y"/> client
-		/// coordinate e.g. following a click event.
-		/// </summary>
-		/// <param name="y">Client Y position in the controls bounds.</param>
-		/// <returns>The clicked branch or null if outside of tree region.</returns>
-		private Branch<T> HitTest (int y)
-		{
-			var map = BuildLineMap ();
-
-			var idx = y + ScrollOffsetVertical;
+	/// <summary>
+	/// Returns the branch at the given <paramref name="y"/> client
+	/// coordinate e.g. following a click event.
+	/// </summary>
+	/// <param name="y">Client Y position in the controls bounds.</param>
+	/// <returns>The clicked branch or null if outside of tree region.</returns>
+	Branch<T> HitTest (int y)
+	{
+		var map = BuildLineMap ();
 
-			// click is outside any visible nodes
-			if (idx < 0 || idx >= map.Count) {
-				return null;
-			}
+		var idx = y + ScrollOffsetVertical;
 
-			// The line they clicked on
-			return map.ElementAt (idx);
+		// click is outside any visible nodes
+		if (idx < 0 || idx >= map.Count) {
+			return null;
 		}
 
-		/// <summary>
-		/// Positions the cursor at the start of the selected objects line (if visible).
-		/// </summary>
-		public override void PositionCursor ()
-		{
-			if (CanFocus && HasFocus && Visible && SelectedObject != null) {
+		// The line they clicked on
+		return map.ElementAt (idx);
+	}
 
-				var map = BuildLineMap ();
-				var idx = map.IndexOf (b => b.Model.Equals (SelectedObject));
+	/// <summary>
+	/// Positions the cursor at the start of the selected objects line (if visible).
+	/// </summary>
+	public override void PositionCursor ()
+	{
+		if (CanFocus && HasFocus && Visible && SelectedObject != null) {
 
-				// if currently selected line is visible
-				if (idx - ScrollOffsetVertical >= 0 && idx - ScrollOffsetVertical < Bounds.Height) {
-					Move (0, idx - ScrollOffsetVertical);
-				} else {
-					base.PositionCursor ();
-				}
+			var map = BuildLineMap ();
+			var idx = map.IndexOf (b => b.Model.Equals (SelectedObject));
 
+			// if currently selected line is visible
+			if (idx - ScrollOffsetVertical >= 0 && idx - ScrollOffsetVertical < Bounds.Height) {
+				Move (0, idx - ScrollOffsetVertical);
 			} else {
 				base.PositionCursor ();
 			}
+
+		} else {
+			base.PositionCursor ();
 		}
+	}
 
-		/// <summary>
-		/// Determines systems behaviour when the left arrow key is pressed. Default behaviour is
-		/// to collapse the current tree node if possible otherwise changes selection to current 
-		/// branches parent.
-		/// </summary>
-		protected virtual void CursorLeft (bool ctrl)
-		{
-			if (IsExpanded (SelectedObject)) {
+	/// <summary>
+	/// Determines systems behaviour when the left arrow key is pressed. Default behaviour is
+	/// to collapse the current tree node if possible otherwise changes selection to current
+	/// branches parent.
+	/// </summary>
+	protected virtual void CursorLeft (bool ctrl)
+	{
+		if (IsExpanded (SelectedObject)) {
 
-				if (ctrl) {
-					CollapseAll (SelectedObject);
-				} else {
-					Collapse (SelectedObject);
-				}
+			if (ctrl) {
+				CollapseAll (SelectedObject);
 			} else {
-				var parent = GetParent (SelectedObject);
+				Collapse (SelectedObject);
+			}
+		} else {
+			var parent = GetParent (SelectedObject);
 
-				if (parent != null) {
-					SelectedObject = parent;
-					AdjustSelection (0);
-					SetNeedsDisplay ();
-				}
+			if (parent != null) {
+				SelectedObject = parent;
+				AdjustSelection (0);
+				SetNeedsDisplay ();
 			}
 		}
+	}
 
-		/// <summary>
-		/// Changes the <see cref="SelectedObject"/> to the first root object and resets 
-		/// the <see cref="ScrollOffsetVertical"/> to 0.
-		/// </summary>
-		public void GoToFirst ()
-		{
-			ScrollOffsetVertical = 0;
-			SelectedObject = roots.Keys.FirstOrDefault ();
+	/// <summary>
+	/// Changes the <see cref="SelectedObject"/> to the first root object and resets
+	/// the <see cref="ScrollOffsetVertical"/> to 0.
+	/// </summary>
+	public void GoToFirst ()
+	{
+		ScrollOffsetVertical = 0;
+		SelectedObject = roots.Keys.FirstOrDefault ();
 
-			SetNeedsDisplay ();
-		}
+		SetNeedsDisplay ();
+	}
 
-		/// <summary>
-		/// Changes the <see cref="SelectedObject"/> to the last object in the tree and scrolls so
-		/// that it is visible.
-		/// </summary>
-		public void GoToEnd ()
-		{
-			var map = BuildLineMap ();
-			ScrollOffsetVertical = Math.Max (0, map.Count - Bounds.Height + 1);
-			SelectedObject = map.LastOrDefault ()?.Model;
+	/// <summary>
+	/// Changes the <see cref="SelectedObject"/> to the last object in the tree and scrolls so
+	/// that it is visible.
+	/// </summary>
+	public void GoToEnd ()
+	{
+		var map = BuildLineMap ();
+		ScrollOffsetVertical = Math.Max (0, map.Count - Bounds.Height + 1);
+		SelectedObject = map.LastOrDefault ()?.Model;
 
-			SetNeedsDisplay ();
+		SetNeedsDisplay ();
+	}
+
+	/// <summary>
+	/// Changes the <see cref="SelectedObject"/> to <paramref name="toSelect"/> and scrolls to ensure
+	/// it is visible. Has no effect if <paramref name="toSelect"/> is not exposed in the tree (e.g.
+	/// its parents are collapsed).
+	/// </summary>
+	/// <param name="toSelect"></param>
+	public void GoTo (T toSelect)
+	{
+		if (ObjectToBranch (toSelect) == null) {
+			return;
 		}
 
-		/// <summary>
-		/// Changes the <see cref="SelectedObject"/> to <paramref name="toSelect"/> and scrolls to ensure
-		/// it is visible. Has no effect if <paramref name="toSelect"/> is not exposed in the tree (e.g. 
-		/// its parents are collapsed).
-		/// </summary>
-		/// <param name="toSelect"></param>
-		public void GoTo (T toSelect)
-		{
-			if (ObjectToBranch (toSelect) == null) {
-				return;
-			}
+		SelectedObject = toSelect;
+		EnsureVisible (toSelect);
+		SetNeedsDisplay ();
+	}
 
-			SelectedObject = toSelect;
-			EnsureVisible (toSelect);
-			SetNeedsDisplay ();
+	/// <summary>
+	/// The number of screen lines to move the currently selected object by. Supports negative values.
+	/// <paramref name="offset"/>. Each branch occupies 1 line on screen.
+	/// </summary>
+	/// <remarks>
+	/// If nothing is currently selected or the selected object is no longer in the tree
+	/// then the first object in the tree is selected instead.
+	/// </remarks>
+	/// <param name="offset">Positive to move the selection down the screen, negative to move it up</param>
+	/// <param name="expandSelection">
+	/// True to expand the selection (assuming
+	/// <see cref="MultiSelect"/> is enabled). False to replace.
+	/// </param>
+	public void AdjustSelection (int offset, bool expandSelection = false)
+	{
+		// if it is not a shift click or we don't allow multi select
+		if (!expandSelection || !MultiSelect) {
+			multiSelectedRegions.Clear ();
 		}
 
-		/// <summary>
-		/// The number of screen lines to move the currently selected object by. Supports negative values.
-		/// <paramref name="offset"/>. Each branch occupies 1 line on screen.
-		/// </summary>
-		/// <remarks>If nothing is currently selected or the selected object is no longer in the tree
-		/// then the first object in the tree is selected instead.</remarks>
-		/// <param name="offset">Positive to move the selection down the screen, negative to move it up</param>
-		/// <param name="expandSelection">True to expand the selection (assuming 
-		/// <see cref="MultiSelect"/> is enabled). False to replace.</param>
-		public void AdjustSelection (int offset, bool expandSelection = false)
-		{
-			// if it is not a shift click or we don't allow multi select
-			if (!expandSelection || !MultiSelect) {
-				multiSelectedRegions.Clear ();
-			}
+		if (SelectedObject == null) {
+			SelectedObject = roots.Keys.FirstOrDefault ();
+		} else {
+			var map = BuildLineMap ();
 
-			if (SelectedObject == null) {
+			var idx = map.IndexOf (b => b.Model.Equals (SelectedObject));
+
+			if (idx == -1) {
+				// The current selection has disapeared!
 				SelectedObject = roots.Keys.FirstOrDefault ();
 			} else {
-				var map = BuildLineMap ();
+				var newIdx = Math.Min (Math.Max (0, idx + offset), map.Count - 1);
 
-				var idx = map.IndexOf (b => b.Model.Equals (SelectedObject));
+				var newBranch = map.ElementAt (newIdx);
 
-				if (idx == -1) {
-					// The current selection has disapeared!
-					SelectedObject = roots.Keys.FirstOrDefault ();
-				} else {
-					var newIdx = Math.Min (Math.Max (0, idx + offset), map.Count - 1);
-
-					var newBranch = map.ElementAt (newIdx);
-
-					// If it is a multi selection
-					if (expandSelection && MultiSelect) {
-						if (multiSelectedRegions.Any ()) {
-							// expand the existing head selection
-							var head = multiSelectedRegions.Pop ();
-							multiSelectedRegions.Push (new TreeSelection<T> (head.Origin, newIdx, map));
-						} else {
-							// or start a new multi selection region
-							multiSelectedRegions.Push (new TreeSelection<T> (map.ElementAt (idx), newIdx, map));
-						}
+				// If it is a multi selection
+				if (expandSelection && MultiSelect) {
+					if (multiSelectedRegions.Any ()) {
+						// expand the existing head selection
+						var head = multiSelectedRegions.Pop ();
+						multiSelectedRegions.Push (new TreeSelection<T> (head.Origin, newIdx, map));
+					} else {
+						// or start a new multi selection region
+						multiSelectedRegions.Push (new TreeSelection<T> (map.ElementAt (idx), newIdx, map));
 					}
+				}
 
-					SelectedObject = newBranch.Model;
+				SelectedObject = newBranch.Model;
 
-					EnsureVisible (SelectedObject);
-				}
+				EnsureVisible (SelectedObject);
 			}
-			SetNeedsDisplay ();
 		}
+		SetNeedsDisplay ();
+	}
 
-		/// <summary>
-		/// Moves the selection to the first child in the currently selected level.
-		/// </summary>
-		public void AdjustSelectionToBranchStart ()
-		{
-			var o = SelectedObject;
-			if (o == null) {
-				return;
-			}
-
-			var map = BuildLineMap ();
+	/// <summary>
+	/// Moves the selection to the first child in the currently selected level.
+	/// </summary>
+	public void AdjustSelectionToBranchStart ()
+	{
+		var o = SelectedObject;
+		if (o == null) {
+			return;
+		}
 
-			int currentIdx = map.IndexOf (b => Equals (b.Model, o));
+		var map = BuildLineMap ();
 
-			if (currentIdx == -1) {
-				return;
-			}
+		var currentIdx = map.IndexOf (b => Equals (b.Model, o));
 
-			var currentBranch = map.ElementAt (currentIdx);
-			var next = currentBranch;
+		if (currentIdx == -1) {
+			return;
+		}
 
-			for (; currentIdx >= 0; currentIdx--) {
-				//if it is the beginning of the current depth of branch
-				if (currentBranch.Depth != next.Depth) {
+		var currentBranch = map.ElementAt (currentIdx);
+		var next = currentBranch;
 
-					SelectedObject = currentBranch.Model;
-					EnsureVisible (currentBranch.Model);
-					SetNeedsDisplay ();
-					return;
-				}
+		for (; currentIdx >= 0; currentIdx--) {
+			//if it is the beginning of the current depth of branch
+			if (currentBranch.Depth != next.Depth) {
 
-				// look at next branch up for consideration
-				currentBranch = next;
-				next = map.ElementAt (currentIdx);
+				SelectedObject = currentBranch.Model;
+				EnsureVisible (currentBranch.Model);
+				SetNeedsDisplay ();
+				return;
 			}
 
-			// We ran all the way to top of tree
-			GoToFirst ();
+			// look at next branch up for consideration
+			currentBranch = next;
+			next = map.ElementAt (currentIdx);
 		}
 
-		/// <summary>
-		/// Moves the selection to the last child in the currently selected level.
-		/// </summary>
-		public void AdjustSelectionToBranchEnd ()
-		{
-			var o = SelectedObject;
-			if (o == null) {
-				return;
-			}
+		// We ran all the way to top of tree
+		GoToFirst ();
+	}
 
-			var map = BuildLineMap ();
+	/// <summary>
+	/// Moves the selection to the last child in the currently selected level.
+	/// </summary>
+	public void AdjustSelectionToBranchEnd ()
+	{
+		var o = SelectedObject;
+		if (o == null) {
+			return;
+		}
 
-			int currentIdx = map.IndexOf (b => Equals (b.Model, o));
+		var map = BuildLineMap ();
 
-			if (currentIdx == -1) {
-				return;
-			}
+		var currentIdx = map.IndexOf (b => Equals (b.Model, o));
 
-			var currentBranch = map.ElementAt (currentIdx);
-			var next = currentBranch;
+		if (currentIdx == -1) {
+			return;
+		}
 
-			for (; currentIdx < map.Count; currentIdx++) {
-				//if it is the end of the current depth of branch
-				if (currentBranch.Depth != next.Depth) {
+		var currentBranch = map.ElementAt (currentIdx);
+		var next = currentBranch;
 
-					SelectedObject = currentBranch.Model;
-					EnsureVisible (currentBranch.Model);
-					SetNeedsDisplay ();
-					return;
-				}
+		for (; currentIdx < map.Count; currentIdx++) {
+			//if it is the end of the current depth of branch
+			if (currentBranch.Depth != next.Depth) {
 
-				// look at next branch for consideration
-				currentBranch = next;
-				next = map.ElementAt (currentIdx);
+				SelectedObject = currentBranch.Model;
+				EnsureVisible (currentBranch.Model);
+				SetNeedsDisplay ();
+				return;
 			}
-			GoToEnd ();
+
+			// look at next branch for consideration
+			currentBranch = next;
+			next = map.ElementAt (currentIdx);
 		}
+		GoToEnd ();
+	}
 
-		/// <summary>
-		/// Sets the selection to the next branch that matches the <paramref name="predicate"/>.
-		/// </summary>
-		/// <param name="predicate"></param>
-		private void AdjustSelectionToNext (Func<Branch<T>, bool> predicate)
-		{
-			var map = BuildLineMap ();
+	/// <summary>
+	/// Sets the selection to the next branch that matches the <paramref name="predicate"/>.
+	/// </summary>
+	/// <param name="predicate"></param>
+	void AdjustSelectionToNext (Func<Branch<T>, bool> predicate)
+	{
+		var map = BuildLineMap ();
 
-			// empty map means we can't select anything anyway
-			if (map.Count == 0) {
-				return;
-			}
+		// empty map means we can't select anything anyway
+		if (map.Count == 0) {
+			return;
+		}
 
-			// Start searching from the first element in the map
-			var idxStart = 0;
+		// Start searching from the first element in the map
+		var idxStart = 0;
 
-			// or the current selected branch
-			if (SelectedObject != null) {
-				idxStart = map.IndexOf (b => Equals (b.Model, SelectedObject));
-			}
+		// or the current selected branch
+		if (SelectedObject != null) {
+			idxStart = map.IndexOf (b => Equals (b.Model, SelectedObject));
+		}
 
-			// if currently selected object mysteriously vanished, search from beginning
-			if (idxStart == -1) {
-				idxStart = 0;
-			}
+		// if currently selected object mysteriously vanished, search from beginning
+		if (idxStart == -1) {
+			idxStart = 0;
+		}
 
-			// loop around all indexes and back to first index
-			for (int idxCur = (idxStart + 1) % map.Count; idxCur != idxStart; idxCur = (idxCur + 1) % map.Count) {
-				if (predicate (map.ElementAt (idxCur))) {
-					SelectedObject = map.ElementAt (idxCur).Model;
-					EnsureVisible (map.ElementAt (idxCur).Model);
-					SetNeedsDisplay ();
-					return;
-				}
+		// loop around all indexes and back to first index
+		for (var idxCur = (idxStart + 1) % map.Count; idxCur != idxStart; idxCur = (idxCur + 1) % map.Count) {
+			if (predicate (map.ElementAt (idxCur))) {
+				SelectedObject = map.ElementAt (idxCur).Model;
+				EnsureVisible (map.ElementAt (idxCur).Model);
+				SetNeedsDisplay ();
+				return;
 			}
 		}
+	}
 
-		/// <summary>
-		/// Adjusts the <see cref="ScrollOffsetVertical"/> to ensure the given
-		/// <paramref name="model"/> is visible. Has no effect if already visible.
-		/// </summary>
-		public void EnsureVisible (T model)
-		{
-			var map = BuildLineMap ();
+	/// <summary>
+	/// Adjusts the <see cref="ScrollOffsetVertical"/> to ensure the given
+	/// <paramref name="model"/> is visible. Has no effect if already visible.
+	/// </summary>
+	public void EnsureVisible (T model)
+	{
+		var map = BuildLineMap ();
 
-			var idx = map.IndexOf (b => Equals (b.Model, model));
+		var idx = map.IndexOf (b => Equals (b.Model, model));
 
-			if (idx == -1) {
-				return;
-			}
+		if (idx == -1) {
+			return;
+		}
 
-			/*this -1 allows for possible horizontal scroll bar in the last row of the control*/
-			int leaveSpace = Style.LeaveLastRow ? 1 : 0;
+		/*this -1 allows for possible horizontal scroll bar in the last row of the control*/
+		var leaveSpace = Style.LeaveLastRow ? 1 : 0;
 
-			if (idx < ScrollOffsetVertical) {
-				//if user has scrolled up too far to see their selection
-				ScrollOffsetVertical = idx;
-			} else if (idx >= ScrollOffsetVertical + Bounds.Height - leaveSpace) {
+		if (idx < ScrollOffsetVertical) {
+			//if user has scrolled up too far to see their selection
+			ScrollOffsetVertical = idx;
+		} else if (idx >= ScrollOffsetVertical + Bounds.Height - leaveSpace) {
 
-				//if user has scrolled off bottom of visible tree
-				ScrollOffsetVertical = Math.Max (0, (idx + 1) - (Bounds.Height - leaveSpace));
-			}
+			//if user has scrolled off bottom of visible tree
+			ScrollOffsetVertical = Math.Max (0, idx + 1 - (Bounds.Height - leaveSpace));
 		}
+	}
+
+	/// <summary>
+	/// Expands the currently <see cref="SelectedObject"/>.
+	/// </summary>
+	public void Expand () => Expand (SelectedObject);
 
-		/// <summary>
-		/// Expands the currently <see cref="SelectedObject"/>.
-		/// </summary>
-		public void Expand ()
-		{
-			Expand (SelectedObject);
+	/// <summary>
+	/// Expands the supplied object if it is contained in the tree (either as a root object or
+	/// as an exposed branch object).
+	/// </summary>
+	/// <param name="toExpand">The object to expand.</param>
+	public void Expand (T toExpand)
+	{
+		if (toExpand == null) {
+			return;
 		}
 
-		/// <summary>
-		/// Expands the supplied object if it is contained in the tree (either as a root object or 
-		/// as an exposed branch object).
-		/// </summary>
-		/// <param name="toExpand">The object to expand.</param>
-		public void Expand (T toExpand)
-		{
-			if (toExpand == null) {
-				return;
-			}
+		ObjectToBranch (toExpand)?.Expand ();
+		InvalidateLineMap ();
+		SetNeedsDisplay ();
+	}
 
-			ObjectToBranch (toExpand)?.Expand ();
-			InvalidateLineMap ();
-			SetNeedsDisplay ();
+	/// <summary>
+	/// Expands the supplied object and all child objects.
+	/// </summary>
+	/// <param name="toExpand">The object to expand.</param>
+	public void ExpandAll (T toExpand)
+	{
+		if (toExpand == null) {
+			return;
 		}
 
-		/// <summary>
-		/// Expands the supplied object and all child objects.
-		/// </summary>
-		/// <param name="toExpand">The object to expand.</param>
-		public void ExpandAll (T toExpand)
-		{
-			if (toExpand == null) {
-				return;
-			}
+		ObjectToBranch (toExpand)?.ExpandAll ();
+		InvalidateLineMap ();
+		SetNeedsDisplay ();
+	}
 
-			ObjectToBranch (toExpand)?.ExpandAll ();
-			InvalidateLineMap ();
-			SetNeedsDisplay ();
+	/// <summary>
+	/// Fully expands all nodes in the tree, if the tree is very big and built dynamically this
+	/// may take a while (e.g. for file system).
+	/// </summary>
+	public void ExpandAll ()
+	{
+		foreach (var item in roots) {
+			item.Value.ExpandAll ();
 		}
-		/// <summary>
-		/// Fully expands all nodes in the tree, if the tree is very big and built dynamically this
-		/// may take a while (e.g. for file system).
-		/// </summary>
-		public void ExpandAll ()
-		{
-			foreach (var item in roots) {
-				item.Value.ExpandAll ();
-			}
 
-			InvalidateLineMap ();
-			SetNeedsDisplay ();
-		}
-		/// <summary>
-		/// Returns true if the given object <paramref name="o"/> is exposed in the tree and can be
-		/// expanded otherwise false.
-		/// </summary>
-		/// <param name="o"></param>
-		/// <returns></returns>
-		public bool CanExpand (T o)
-		{
-			return ObjectToBranch (o)?.CanExpand () ?? false;
-		}
-
-		/// <summary>
-		/// Returns true if the given object <paramref name="o"/> is exposed in the tree and 
-		/// expanded otherwise false.
-		/// </summary>
-		/// <param name="o"></param>
-		/// <returns></returns>
-		public bool IsExpanded (T o)
-		{
-			return ObjectToBranch (o)?.IsExpanded ?? false;
-		}
-
-		/// <summary>
-		/// Collapses the <see cref="SelectedObject"/>
-		/// </summary>
-		public void Collapse ()
-		{
-			Collapse (selectedObject);
-		}
-
-		/// <summary>
-		/// Collapses the supplied object if it is currently expanded .
-		/// </summary>
-		/// <param name="toCollapse">The object to collapse.</param>
-		public void Collapse (T toCollapse)
-		{
-			CollapseImpl (toCollapse, false);
-		}
-
-		/// <summary>
-		/// Collapses the supplied object if it is currently expanded. Also collapses all children
-		/// branches (this will only become apparent when/if the user expands it again).
-		/// </summary>
-		/// <param name="toCollapse">The object to collapse.</param>
-		public void CollapseAll (T toCollapse)
-		{
-			CollapseImpl (toCollapse, true);
-		}
-
-		/// <summary>
-		/// Collapses all root nodes in the tree.
-		/// </summary>
-		public void CollapseAll ()
-		{
-			foreach (var item in roots) {
-				item.Value.Collapse ();
-			}
+		InvalidateLineMap ();
+		SetNeedsDisplay ();
+	}
 
-			InvalidateLineMap ();
-			SetNeedsDisplay ();
-		}
+	/// <summary>
+	/// Returns true if the given object <paramref name="o"/> is exposed in the tree and can be
+	/// expanded otherwise false.
+	/// </summary>
+	/// <param name="o"></param>
+	/// <returns></returns>
+	public bool CanExpand (T o) => ObjectToBranch (o)?.CanExpand () ?? false;
 
-		/// <summary>
-		/// Implementation of <see cref="Collapse(T)"/> and <see cref="CollapseAll(T)"/>. Performs
-		/// operation and updates selection if disapeared.
-		/// </summary>
-		/// <param name="toCollapse"></param>
-		/// <param name="all"></param>
-		protected void CollapseImpl (T toCollapse, bool all)
-		{
-			if (toCollapse == null) {
-				return;
-			}
+	/// <summary>
+	/// Returns true if the given object <paramref name="o"/> is exposed in the tree and
+	/// expanded otherwise false.
+	/// </summary>
+	/// <param name="o"></param>
+	/// <returns></returns>
+	public bool IsExpanded (T o) => ObjectToBranch (o)?.IsExpanded ?? false;
 
-			var branch = ObjectToBranch (toCollapse);
+	/// <summary>
+	/// Collapses the <see cref="SelectedObject"/>
+	/// </summary>
+	public void Collapse () => Collapse (selectedObject);
 
-			// Nothing to collapse
-			if (branch == null) {
-				return;
-			}
+	/// <summary>
+	/// Collapses the supplied object if it is currently expanded .
+	/// </summary>
+	/// <param name="toCollapse">The object to collapse.</param>
+	public void Collapse (T toCollapse) => CollapseImpl (toCollapse, false);
 
-			if (all) {
-				branch.CollapseAll ();
-			} else {
-				branch.Collapse ();
-			}
+	/// <summary>
+	/// Collapses the supplied object if it is currently expanded. Also collapses all children
+	/// branches (this will only become apparent when/if the user expands it again).
+	/// </summary>
+	/// <param name="toCollapse">The object to collapse.</param>
+	public void CollapseAll (T toCollapse) => CollapseImpl (toCollapse, true);
 
-			if (SelectedObject != null && ObjectToBranch (SelectedObject) == null) {
-				// If the old selection suddenly became invalid then clear it
-				SelectedObject = null;
-			}
+	/// <summary>
+	/// Collapses all root nodes in the tree.
+	/// </summary>
+	public void CollapseAll ()
+	{
+		foreach (var item in roots) {
+			item.Value.Collapse ();
+		}
 
-			InvalidateLineMap ();
-			SetNeedsDisplay ();
+		InvalidateLineMap ();
+		SetNeedsDisplay ();
+	}
+
+	/// <summary>
+	/// Implementation of <see cref="Collapse(T)"/> and <see cref="CollapseAll(T)"/>. Performs
+	/// operation and updates selection if disapeared.
+	/// </summary>
+	/// <param name="toCollapse"></param>
+	/// <param name="all"></param>
+	protected void CollapseImpl (T toCollapse, bool all)
+	{
+		if (toCollapse == null) {
+			return;
 		}
 
-		/// <summary>
-		/// Clears any cached results of the tree state.
-		/// </summary>
-		public void InvalidateLineMap ()
-		{
-			cachedLineMap = null;
-		}
-
-		/// <summary>
-		/// Returns the corresponding <see cref="Branch{T}"/> in the tree for
-		/// <paramref name="toFind"/>. This will not work for objects hidden
-		/// by their parent being collapsed.
-		/// </summary>
-		/// <param name="toFind"></param>
-		/// <returns>The branch for <paramref name="toFind"/> or null if it is not currently 
-		/// exposed in the tree.</returns>
-		private Branch<T> ObjectToBranch (T toFind)
-		{
-			return BuildLineMap ().FirstOrDefault (o => o.Model.Equals (toFind));
-		}
-
-		/// <summary>
-		/// Returns true if the <paramref name="model"/> is either the 
-		/// <see cref="SelectedObject"/> or part of a <see cref="MultiSelect"/>.
-		/// </summary>
-		/// <param name="model"></param>
-		/// <returns></returns>
-		public bool IsSelected (T model)
-		{
-			return Equals (SelectedObject, model) ||
-				(MultiSelect && multiSelectedRegions.Any (s => s.Contains (model)));
-		}
-
-		/// <summary>
-		/// Returns <see cref="SelectedObject"/> (if not null) and all multi selected objects if 
-		/// <see cref="MultiSelect"/> is true
-		/// </summary>
-		/// <returns></returns>
-		public IEnumerable<T> GetAllSelectedObjects ()
-		{
-			var map = BuildLineMap ();
+		var branch = ObjectToBranch (toCollapse);
 
-			// To determine multi selected objects, start with the line map, that avoids yielding 
-			// hidden nodes that were selected then the parent collapsed e.g. programmatically or
-			// with mouse click
-			if (MultiSelect) {
-				foreach (var m in map.Select (b => b.Model).Where (IsSelected)) {
-					yield return m;
-				}
-			} else {
-				if (SelectedObject != null) {
-					yield return SelectedObject;
-				}
-			}
+		// Nothing to collapse
+		if (branch == null) {
+			return;
 		}
 
-		/// <summary>
-		/// Selects all objects in the tree when <see cref="MultiSelect"/> is enabled otherwise 
-		/// does nothing.
-		/// </summary>
-		public void SelectAll ()
-		{
-			if (!MultiSelect) {
-				return;
-			}
+		if (all) {
+			branch.CollapseAll ();
+		} else {
+			branch.Collapse ();
+		}
 
-			multiSelectedRegions.Clear ();
+		if (SelectedObject != null && ObjectToBranch (SelectedObject) == null) {
+			// If the old selection suddenly became invalid then clear it
+			SelectedObject = null;
+		}
 
-			var map = BuildLineMap ();
+		InvalidateLineMap ();
+		SetNeedsDisplay ();
+	}
 
-			if (map.Count == 0) {
-				return;
-			}
+	/// <summary>
+	/// Clears any cached results of the tree state.
+	/// </summary>
+	public void InvalidateLineMap () => cachedLineMap = null;
 
-			multiSelectedRegions.Push (new TreeSelection<T> (map.ElementAt (0), map.Count, map));
-			SetNeedsDisplay ();
+	/// <summary>
+	/// Returns the corresponding <see cref="Branch{T}"/> in the tree for
+	/// <paramref name="toFind"/>. This will not work for objects hidden
+	/// by their parent being collapsed.
+	/// </summary>
+	/// <param name="toFind"></param>
+	/// <returns>
+	/// The branch for <paramref name="toFind"/> or null if it is not currently
+	/// exposed in the tree.
+	/// </returns>
+	Branch<T> ObjectToBranch (T toFind) => BuildLineMap ().FirstOrDefault (o => o.Model.Equals (toFind));
 
-			OnSelectionChanged (new SelectionChangedEventArgs<T> (this, SelectedObject, SelectedObject));
-		}
+	/// <summary>
+	/// Returns true if the <paramref name="model"/> is either the
+	/// <see cref="SelectedObject"/> or part of a <see cref="MultiSelect"/>.
+	/// </summary>
+	/// <param name="model"></param>
+	/// <returns></returns>
+	public bool IsSelected (T model) => Equals (SelectedObject, model) ||
+	                                    MultiSelect && multiSelectedRegions.Any (s => s.Contains (model));
 
-		/// <summary>
-		/// Raises the SelectionChanged event.
-		/// </summary>
-		/// <param name="e"></param>
-		protected virtual void OnSelectionChanged (SelectionChangedEventArgs<T> e)
-		{
-			SelectionChanged?.Invoke (this, e);
+	/// <summary>
+	/// Returns <see cref="SelectedObject"/> (if not null) and all multi selected objects if
+	/// <see cref="MultiSelect"/> is true
+	/// </summary>
+	/// <returns></returns>
+	public IEnumerable<T> GetAllSelectedObjects ()
+	{
+		var map = BuildLineMap ();
+
+		// To determine multi selected objects, start with the line map, that avoids yielding 
+		// hidden nodes that were selected then the parent collapsed e.g. programmatically or
+		// with mouse click
+		if (MultiSelect) {
+			foreach (var m in map.Select (b => b.Model).Where (IsSelected)) {
+				yield return m;
+			}
+		} else {
+			if (SelectedObject != null) {
+				yield return SelectedObject;
+			}
 		}
+	}
 
-		/// <summary>
-		/// Raises the DrawLine event
-		/// </summary>
-		/// <param name="e"></param>
-		internal void OnDrawLine (DrawTreeViewLineEventArgs<T> e)
-		{
-			DrawLine?.Invoke (this, e);
+	/// <summary>
+	/// Selects all objects in the tree when <see cref="MultiSelect"/> is enabled otherwise
+	/// does nothing.
+	/// </summary>
+	public void SelectAll ()
+	{
+		if (!MultiSelect) {
+			return;
 		}
 
-		/// <inheritdoc/>
-		protected override void Dispose (bool disposing)
-		{
-			base.Dispose (disposing);
+		multiSelectedRegions.Clear ();
 
-			ColorGetter = null;
+		var map = BuildLineMap ();
+
+		if (map.Count == 0) {
+			return;
 		}
+
+		multiSelectedRegions.Push (new TreeSelection<T> (map.ElementAt (0), map.Count, map));
+		SetNeedsDisplay ();
+
+		OnSelectionChanged (new SelectionChangedEventArgs<T> (this, SelectedObject, SelectedObject));
 	}
-	class TreeSelection<T> where T : class {
 
-		public Branch<T> Origin { get; }
+	/// <summary>
+	/// Raises the SelectionChanged event.
+	/// </summary>
+	/// <param name="e"></param>
+	protected virtual void OnSelectionChanged (SelectionChangedEventArgs<T> e) => SelectionChanged?.Invoke (this, e);
 
-		private HashSet<T> included = new HashSet<T> ();
+	/// <summary>
+	/// Raises the DrawLine event
+	/// </summary>
+	/// <param name="e"></param>
+	internal void OnDrawLine (DrawTreeViewLineEventArgs<T> e) => DrawLine?.Invoke (this, e);
 
-		/// <summary>
-		/// Creates a new selection between two branches in the tree
-		/// </summary>
-		/// <param name="from"></param>
-		/// <param name="toIndex"></param>
-		/// <param name="map"></param>
-		public TreeSelection (Branch<T> from, int toIndex, IReadOnlyCollection<Branch<T>> map)
-		{
-			Origin = from;
-			included.Add (Origin.Model);
+	/// <inheritdoc/>
+	protected override void Dispose (bool disposing)
+	{
+		base.Dispose (disposing);
 
-			var oldIdx = map.IndexOf (from);
+		ColorGetter = null;
+	}
+}
 
-			var lowIndex = Math.Min (oldIdx, toIndex);
-			var highIndex = Math.Max (oldIdx, toIndex);
+class TreeSelection<T> where T : class {
 
-			// Select everything between the old and new indexes
-			foreach (var alsoInclude in map.Skip (lowIndex).Take (highIndex - lowIndex)) {
-				included.Add (alsoInclude.Model);
-			}
+	readonly HashSet<T> included = new ();
 
+	/// <summary>
+	/// Creates a new selection between two branches in the tree
+	/// </summary>
+	/// <param name="from"></param>
+	/// <param name="toIndex"></param>
+	/// <param name="map"></param>
+	public TreeSelection (Branch<T> from, int toIndex, IReadOnlyCollection<Branch<T>> map)
+	{
+		Origin = from;
+		included.Add (Origin.Model);
+
+		var oldIdx = map.IndexOf (from);
+
+		var lowIndex = Math.Min (oldIdx, toIndex);
+		var highIndex = Math.Max (oldIdx, toIndex);
+
+		// Select everything between the old and new indexes
+		foreach (var alsoInclude in map.Skip (lowIndex).Take (highIndex - lowIndex)) {
+			included.Add (alsoInclude.Model);
 		}
-		public bool Contains (T model)
-		{
-			return included.Contains (model);
-		}
+
 	}
+
+	public Branch<T> Origin { get; }
+
+	public bool Contains (T model) => included.Contains (model);
 }

+ 8 - 7
UICatalog/Scenarios/TreeViewFileSystem.cs

@@ -364,20 +364,21 @@ public class TreeViewFileSystem : Scenario {
 
 	void SetCustomColors ()
 	{
-		var hidden = new ColorScheme {
-			Focus = new Attribute (Color.BrightRed, _treeViewFiles.ColorScheme.Focus.Background),
-			Normal = new Attribute (Color.BrightYellow, _treeViewFiles.ColorScheme.Normal.Background)
-		};
-
 		_miCustomColors.Checked = !_miCustomColors.Checked;
 
 		if (_miCustomColors.Checked == true) {
 			_treeViewFiles.ColorGetter = (m) => {
 				if (m is IDirectoryInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) {
-					return hidden;
+					return new ColorScheme {
+						Focus = new Attribute (Color.BrightRed, _treeViewFiles.ColorScheme.Focus.Background),
+						Normal = new Attribute (Color.BrightYellow, _treeViewFiles.ColorScheme.Normal.Background)
+					}; ;
 				}
 				if (m is IFileInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) {
-					return hidden;
+					return new ColorScheme {
+						Focus = new Attribute (Color.BrightRed, _treeViewFiles.ColorScheme.Focus.Background),
+						Normal = new Attribute (Color.BrightYellow, _treeViewFiles.ColorScheme.Normal.Background)
+					}; ;
 				}
 				return null;
 			};

+ 1 - 1
UnitTests/Application/ApplicationTests.cs

@@ -566,7 +566,7 @@ public class ApplicationTests {
 	}
 	#endregion
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Begin_Sets_Application_Top_To_Console_Size ()
 	{
 		Assert.Equal (new Rect (0, 0, 80, 25), Application.Top.Frame);

+ 3 - 3
UnitTests/Configuration/ConfigurationMangerTests.cs

@@ -297,7 +297,7 @@ public class ConfigurationManagerTests {
 		Assert.Equal (pi, Themes ["Default"] ["ColorSchemes"].PropertyInfo);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void TestConfigurationManagerToJson ()
 	{
 		Reset ();
@@ -518,7 +518,7 @@ public class ConfigurationManagerTests {
 		Assert.Equal (new Color (Color.Blue), Colors.ColorSchemes ["Base"].Normal.Background);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void TestConfigurationManagerInvalidJsonThrows ()
 	{
 		ThrowOnJsonErrors = true;
@@ -690,7 +690,7 @@ public class ConfigurationManagerTests {
 		ThrowOnJsonErrors = false;
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void LoadConfigurationFromAllSources_ShouldLoadSettingsFromAllSources ()
 	{
 		//var _configFilename = "config.json";

+ 2 - 2
UnitTests/Configuration/JsonConverterTests.cs

@@ -191,7 +191,7 @@ public class AttributeJsonConverterTests {
 		Assert.Equal (Color.BrightGreen, attribute.Background.ColorName);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void TestSerialize ()
 	{
 		// Test serializing to human-readable color names
@@ -229,7 +229,7 @@ public class ColorSchemeJsonConverterTests {
 	//		}
 	//		}
 	//	}";
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void TestColorSchemesSerialization ()
 	{
 		// Arrange

+ 2 - 2
UnitTests/Configuration/SettingsScopeTests.cs

@@ -34,7 +34,7 @@ public class SettingsScopeTests {
 
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Apply_ShouldApplyProperties ()
 	{
 		// arrange
@@ -58,7 +58,7 @@ public class SettingsScopeTests {
 		Assert.True (Application.IsMouseDisabled);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void CopyUpdatedPropertiesFrom_ShouldCopyChangedPropertiesOnly ()
 	{
 		Settings ["Application.QuitKey"].PropertyValue = new Key (KeyCode.End);

+ 14 - 88
UnitTests/Drawing/AttributeTests.cs

@@ -1,14 +1,17 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Terminal.Gui;
 using Xunit;
-
 // Alias Console to MockConsole so we don't accidentally use Console
 using Console = Terminal.Gui.FakeConsole;
 
 namespace Terminal.Gui.DrawingTests;
+
 public class AttributeTests {
+
+	[Fact]
+	public void Attribute_Is_Value_Type () =>
+		// prove that Color is a value type
+		Assert.True (typeof (Attribute).IsValueType);
+
+
 	[Fact]
 	public void DefaultConstructor ()
 	{
@@ -47,7 +50,8 @@ public class AttributeTests {
 		Assert.Equal (new Color (Color.Blue), attribute.Background);
 	}
 
-	[Fact, AutoInitShutdown]
+	[Fact]
+	[AutoInitShutdown]
 	public void ColorConstructor ()
 	{
 		// Arrange & Act
@@ -60,7 +64,8 @@ public class AttributeTests {
 		Assert.Equal (backgroundColor, attribute.Background);
 	}
 
-	[Fact, AutoInitShutdown]
+	[Fact]
+	[AutoInitShutdown]
 	public void ColorAndColorNamesConstructor ()
 	{
 		// Arrange & Act
@@ -207,7 +212,7 @@ public class AttributeTests {
 
 		// Test conversion to int
 		attr = new Attribute (value, fg, bg);
-		int value_implicit = attr.PlatformColor;
+		var value_implicit = attr.PlatformColor;
 		Assert.Equal (value, value_implicit);
 
 		Assert.Equal (value, attr.PlatformColor);
@@ -368,83 +373,4 @@ public class AttributeTests {
 		// Assert
 		Assert.Equal (expectedString, attributeString);
 	}
-
-	[Fact]
-	public void Changing_One_Default_Reference_Also_Change_All_References_But_Not_A_Instance_Reference ()
-	{
-		// Make two local attributes, and grab Attribute.Default, which is a reference to a static.
-		Attribute attr1 = Attribute.Default;
-		Attribute attr2 = Attribute.Default;
-		// Make one local attributes, and grab Attribute(), which is a reference to a singleton.
-		Attribute attr3 = new Attribute (); // instance
-
-		// Assert the starting state that is expected
-		Assert.Equal (ColorName.White, attr1.Foreground.ColorName);
-		Assert.Equal (ColorName.White, attr2.Foreground.ColorName);
-		Assert.Equal (ColorName.White, Attribute.Default.Foreground.ColorName);
-		Assert.Equal (ColorName.White, attr3.Foreground.ColorName);
-
-		// Now set Foreground.ColorName to ColorName.Blue on one of our local attributes
-		attr1.Foreground.ColorName = ColorName.Blue;
-
-		// Assert the newly-expected case
-		// The last two assertions will fail, because we have actually modified a singleton
-		Assert.Equal (ColorName.Blue, attr1.Foreground.ColorName);
-		Assert.Equal (ColorName.Blue, attr2.Foreground.ColorName);
-		Assert.Equal (ColorName.Blue, Attribute.Default.Foreground.ColorName);
-		Assert.Equal (ColorName.White, attr3.Foreground.ColorName);
-
-		// Now set Foreground.ColorName to ColorName.Red on the singleton of our local attributes
-		attr3.Foreground.ColorName = ColorName.Red;
-
-		// Assert the newly-expected case
-		// The assertions will not fail, because we have actually modified a singleton
-		Assert.Equal (ColorName.Blue, attr1.Foreground.ColorName);
-		Assert.Equal (ColorName.Blue, attr2.Foreground.ColorName);
-		Assert.Equal (ColorName.Blue, Attribute.Default.Foreground.ColorName);
-		Assert.Equal (ColorName.Red, attr3.Foreground.ColorName);
-
-		// Now set Foreground.ColorName to ColorName.White on the static of our local attributes
-		// This also avoids errors on others unit test when the default is changed
-		Attribute.Default.Foreground.ColorName = ColorName.White;
-
-		// Assert the newly-expected case
-		// The assertions will not fail, because we have actually modified the static default reference
-		Assert.Equal (ColorName.White, attr1.Foreground.ColorName);
-		Assert.Equal (ColorName.White, attr2.Foreground.ColorName);
-		Assert.Equal (ColorName.White, Attribute.Default.Foreground.ColorName);
-		Assert.Equal (ColorName.Red, attr3.Foreground.ColorName);
-	}
-
-	[Fact]
-	public void Changing_One_Instance_Reference_Does_Not_Change_All_Instance_References ()
-	{
-		// Make two local attributes, and grab Attribute (), which are a reference to a singleton.
-		Attribute attr1 = new Attribute ();
-		// Make two local attributes, and grab Attribute (Int), which are a reference to a singleton.
-		Attribute attr2 = new Attribute (-1);
-
-		// Assert the starting state that is expected
-		Assert.Equal (ColorName.White, attr1.Foreground.ColorName);
-		Assert.Equal (ColorName.White, attr2.Foreground.ColorName);
-		Assert.Equal (ColorName.White, Attribute.Default.Foreground.ColorName);
-
-		// Now set Foreground.ColorName to ColorName.Blue on one of our local attributes
-		attr1.Foreground.ColorName = ColorName.Blue;
-
-		// Assert the newly-expected case
-		// The assertions will not fail, because we have actually modified a singleton
-		Assert.Equal (ColorName.Blue, attr1.Foreground.ColorName);
-		Assert.Equal (ColorName.White, attr2.Foreground.ColorName);
-		Assert.Equal (ColorName.White, Attribute.Default.Foreground.ColorName);
-
-		// Now set Foreground.ColorName to ColorName.Red on the other singleton of our local attributes
-		attr2.Foreground.ColorName = ColorName.Red;
-
-		// Assert the newly-expected case
-		// The assertions will not fail, because we have actually modified a singleton
-		Assert.Equal (ColorName.Blue, attr1.Foreground.ColorName);
-		Assert.Equal (ColorName.Red, attr2.Foreground.ColorName);
-		Assert.Equal (ColorName.White, Attribute.Default.Foreground.ColorName);
-	}
-}
+}

+ 53 - 64
UnitTests/Drawing/ColorTests.cs

@@ -1,18 +1,27 @@
-using Terminal.Gui;
-using System;
-using System.Buffers;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Data;
-using System.Globalization;
+using System;
 using System.Linq;
-using System.Text;
-using System.Text.Json;
 using Xunit;
-using static Unix.Terminal.Curses;
+
 namespace Terminal.Gui.DrawingTests;
 
 public class ColorTests {
+	[Fact]
+	public void Color_Is_Value_Type () =>
+		// prove that Color is a value type
+		Assert.True (typeof (Color).IsValueType);
+
+	[Fact]
+	public void Colors_ColorSchemes_Property_Has_Private_Setter ()
+	{
+		// Resharper Code Cleanup likes to remove the `private set; `
+		// from the ColorSchemes property.  This test will fail if
+		// that happens.
+		var property = typeof (Colors).GetProperty ("ColorSchemes");
+		Assert.NotNull (property);
+		Assert.NotNull (property.SetMethod);
+		Assert.True (property.GetSetMethod (true).IsPrivate);
+
+	}
 
 	[Fact, AutoInitShutdown]
 	public void ColorScheme_New ()
@@ -26,10 +35,10 @@ public class ColorTests {
 	[Fact]
 	public void TestAllColors ()
 	{
-		var colorNames = Enum.GetValues (typeof (ColorName)).Cast<int> ().Distinct ().ToList();
-		Attribute [] attrs = new Attribute [colorNames.Count];
+		var colorNames = Enum.GetValues (typeof (ColorName)).Cast<int> ().Distinct ().ToList ();
+		var attrs = new Attribute [colorNames.Count];
 
-		int idx = 0;
+		var idx = 0;
 		foreach (ColorName bg in colorNames) {
 			attrs [idx] = new Attribute (bg, colorNames.Count - 1 - bg);
 			idx++;
@@ -52,12 +61,9 @@ public class ColorTests {
 		Assert.Equal (new Attribute (Color.BrightYellow, Color.Blue), attrs [14]);
 		Assert.Equal (new Attribute (Color.White, Color.Black), attrs [^1]);
 	}
-	
+
 	[Fact]
-	public void ColorNames_HasOnly16DistinctElements ()
-	{
-		Assert.Equal (16, Enum.GetValues (typeof (ColorName)).Cast<int> ().Distinct ().Count ());
-	}
+	public void ColorNames_HasOnly16DistinctElements () => Assert.Equal (16, Enum.GetValues (typeof (ColorName)).Cast<int> ().Distinct ().Count ());
 
 	[Fact]
 	public void ColorNames_HaveCorrectOrdinals ()
@@ -84,9 +90,9 @@ public class ColorTests {
 	public void Color_Constructor_WithRGBValues ()
 	{
 		// Arrange
-		int expectedR = 255;
-		int expectedG = 0;
-		int expectedB = 128;
+		var expectedR = 255;
+		var expectedG = 0;
+		var expectedB = 128;
 
 		// Act
 		var color = new Color (expectedR, expectedG, expectedB);
@@ -102,10 +108,10 @@ public class ColorTests {
 	public void Color_Constructor_WithAlphaAndRGBValues ()
 	{
 		// Arrange
-		int expectedA = 128;
-		int expectedR = 255;
-		int expectedG = 0;
-		int expectedB = 128;
+		var expectedA = 128;
+		var expectedR = 255;
+		var expectedG = 0;
+		var expectedB = 128;
 
 		// Act
 		var color = new Color (expectedR, expectedG, expectedB, expectedA);
@@ -121,7 +127,7 @@ public class ColorTests {
 	public void Color_Constructor_WithRgbaValue ()
 	{
 		// Arrange
-		int expectedRgba = unchecked((int)0xFF804040); // R: 128, G: 64, B: 64, Alpha: 255
+		var expectedRgba = unchecked((int)0xFF804040); // R: 128, G: 64, B: 64, Alpha: 255
 
 		// Act
 		var color = new Color (expectedRgba);
@@ -137,7 +143,7 @@ public class ColorTests {
 	public void Color_Constructor_WithColorName ()
 	{
 		// Arrange
-		ColorName colorName = ColorName.Blue;
+		var colorName = ColorName.Blue;
 		var expectedColor = new Color (0, 55, 218); // Blue
 
 		// Act
@@ -151,10 +157,10 @@ public class ColorTests {
 	public void Color_ToString_WithNamedColor ()
 	{
 		// Arrange
-		Color color = new Color (0, 55, 218); // Blue
+		var color = new Color (0, 55, 218); // Blue
 
 		// Act
-		string colorString = color.ToString ();
+		var colorString = color.ToString ();
 
 		// Assert
 		Assert.Equal ("Blue", colorString);
@@ -164,10 +170,10 @@ public class ColorTests {
 	public void Color_ToString_WithRGBColor ()
 	{
 		// Arrange
-		Color color = new Color (1, 64, 32); // Custom RGB color
+		var color = new Color (1, 64, 32); // Custom RGB color
 
 		// Act
-		string colorString = color.ToString ();
+		var colorString = color.ToString ();
 
 		// Assert
 		Assert.Equal ("#014020", colorString);
@@ -177,7 +183,7 @@ public class ColorTests {
 	public void Color_ImplicitOperator_FromInt ()
 	{
 		// Arrange
-		int Rgba = unchecked((int)0xFF804020); // R: 128, G: 64, B: 32, Alpha: 255
+		var Rgba = unchecked((int)0xFF804020); // R: 128, G: 64, B: 32, Alpha: 255
 		var expectedColor = new Color (128, 64, 32);
 
 		// Act
@@ -192,10 +198,10 @@ public class ColorTests {
 	{
 		// Arrange
 		var color = new Color (128, 64, 32);
-		int expectedRgba = unchecked((int)0xFF804020); // R: 128, G: 64, B: 32, Alpha: 255
+		var expectedRgba = unchecked((int)0xFF804020); // R: 128, G: 64, B: 32, Alpha: 255
 
 		// Act
-		int Rgba = (int)color;
+		var Rgba = (int)color;
 
 		// Assert
 		Assert.Equal (expectedRgba, Rgba);
@@ -206,11 +212,11 @@ public class ColorTests {
 	public void Color_ImplicitOperator_FromColorNames ()
 	{
 		// Arrange
-		ColorName colorName = ColorName.Blue;
+		var colorName = ColorName.Blue;
 		var expectedColor = new Color (0, 55, 218); // Blue
 
 		// Act
-		Color color = new Color (colorName);
+		var color = new Color (colorName);
 
 		// Assert
 		Assert.Equal (expectedColor, color);
@@ -221,10 +227,10 @@ public class ColorTests {
 	{
 		// Arrange
 		var color = new Color (0, 0, 0x80); // Blue
-		ColorName expectedColorName = ColorName.Blue;
+		var expectedColorName = ColorName.Blue;
 
 		// Act
-		ColorName colorName = (ColorName)color;
+		var colorName = (ColorName)color;
 
 		// Assert
 		Assert.Equal (expectedColorName, colorName);
@@ -321,15 +327,14 @@ public class ColorTests {
 	public void FindClosestColor_ReturnsClosestColor ()
 	{
 		// Test cases with RGB values and expected closest color names
-		var testCases = new []
-		{
-			(new Color(0, 0, 0), ColorName.Black),
-			(new Color(255, 255, 255), ColorName.White),
-			(new Color(5, 100, 255), ColorName.BrightBlue),
-			(new Color(0, 255, 0), ColorName.BrightGreen),
-			(new Color(255, 70, 8), ColorName.BrightRed),
-			(new Color(0, 128, 128), ColorName.Cyan),
-			(new Color(128, 64, 32), ColorName.Yellow),
+		var testCases = new [] {
+			(new Color (0, 0, 0), ColorName.Black),
+			(new Color (255, 255, 255), ColorName.White),
+			(new Color (5, 100, 255), ColorName.BrightBlue),
+			(new Color (0, 255, 0), ColorName.BrightGreen),
+			(new Color (255, 70, 8), ColorName.BrightRed),
+			(new Color (0, 128, 128), ColorName.Cyan),
+			(new Color (128, 64, 32), ColorName.Yellow)
 		};
 
 		foreach (var testCase in testCases) {
@@ -341,20 +346,4 @@ public class ColorTests {
 			Assert.Equal (expectedColorName, actualColorName);
 		}
 	}
-
-	[Fact]
-	public void Color_ColorName_Set_SetsColorBasedOnColorName ()
-	{
-		// Arrange
-		var color = new Color (0, 0, 0); // Black
-		var expectedColor = new Color (ColorName.Magenta);
-
-		// Act
-		color.ColorName = ColorName.Magenta;
-
-		// Assert
-		Assert.Equal (expectedColor, color);
-	}
-}
-
-
+}

+ 19 - 19
UnitTests/View/ViewTests.cs

@@ -479,7 +479,7 @@ public class ViewTests {
 		}
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Internal_Tests ()
 	{
 		Assert.Equal (new [] { View.Direction.Forward, View.Direction.Backward },
@@ -569,7 +569,7 @@ public class ViewTests {
 		Application.End (runState);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Visible_Sets_Also_Sets_Subviews ()
 	{
 		var button = new Button ("Click Me");
@@ -637,7 +637,7 @@ public class ViewTests {
 		}
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void GetTopSuperView_Test ()
 	{
 		var v1 = new View ();
@@ -673,7 +673,7 @@ public class ViewTests {
 		top2.Dispose ();
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Clear_Can_Use_Driver_AddRune_Or_AddStr_Methods ()
 	{
 		var view = new FrameView {
@@ -721,7 +721,7 @@ public class ViewTests {
 		Assert.Equal (Rect.Empty, pos);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Clear_Bounds_Can_Use_Driver_AddRune_Or_AddStr_Methods ()
 	{
 		var view = new FrameView {
@@ -784,7 +784,7 @@ public class ViewTests {
 		view.Dispose ();
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Visible_Clear_The_View_Output ()
 	{
 		var view = new View ("Testing visibility."); // use View, not Label to avoid AutoSize == true
@@ -820,7 +820,7 @@ public class ViewTests {
 		Application.End (rs);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void DrawContentComplete_Event_Is_Always_Called ()
 	{
 		var viewCalled = false;
@@ -838,7 +838,7 @@ public class ViewTests {
 		Assert.True (tvCalled);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void GetNormalColor_ColorScheme ()
 	{
 		var view = new View { ColorScheme = Colors.Base };
@@ -850,7 +850,7 @@ public class ViewTests {
 		view.Dispose ();
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void GetHotNormalColor_ColorScheme ()
 	{
 		var view = new View { ColorScheme = Colors.Base };
@@ -926,7 +926,7 @@ cccccccccccccccccccc", output);
 		Application.End (runState);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Correct_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Up_Left_Using_Frame ()
 	{
 		var label = new Label ("At 0,0");
@@ -960,7 +960,7 @@ At 0,0
 		Application.End (runState);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Correct_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Up_Left_Using_Pos_Dim ()
 	{
 		var label = new Label ("At 0,0");
@@ -996,7 +996,7 @@ At 0,0
 		Application.End (runState);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Incorrect_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Up_Left_Using_Frame ()
 	{
 		var label = new Label ("At 0,0");
@@ -1032,7 +1032,7 @@ At 0,0
 		Application.End (runState);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Incorrect_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Up_Left_Using_Pos_Dim ()
 	{
 		var label = new Label ("At 0,0");
@@ -1070,7 +1070,7 @@ At 0,0
 		Application.End (runState);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Correct_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Down_Right_Using_Frame ()
 	{
 		var label = new Label ("At 0,0");
@@ -1105,7 +1105,7 @@ At 0,0
 		Application.End (runState);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Correct_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Down_Right_Using_Pos_Dim ()
 	{
 		var label = new Label ("At 0,0");
@@ -1143,7 +1143,7 @@ At 0,0
 		Application.End (runState);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Incorrect_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Down_Right_Using_Frame ()
 	{
 		var label = new Label ("At 0,0");
@@ -1177,7 +1177,7 @@ At 0,0
 		Application.End (runState);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Incorrect_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Down_Right_Using_Pos_Dim ()
 	{
 		var label = new Label ("At 0,0");
@@ -1215,7 +1215,7 @@ At 0,0
 		Application.End (runState);
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Test_Nested_Views_With_Height_Equal_To_One ()
 	{
 		var v = new View { Width = 11, Height = 3, ColorScheme = new ColorScheme () };
@@ -1245,7 +1245,7 @@ At 0,0
 		bottom.Dispose ();
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Frame_Set_After_Initialize_Update_NeededDisplay ()
 	{
 		var frame = new FrameView ();

+ 2 - 2
UnitTests/Views/OverlappedTests.cs

@@ -47,7 +47,7 @@ public class OverlappedTests {
 		Application.Shutdown ();
 	}
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Application_RequestStop_With_Params_On_A_Not_OverlappedContainer_Always_Use_Application_Current ()
 	{
 		var top1 = new Toplevel ();
@@ -683,7 +683,7 @@ public class OverlappedTests {
 	[Fact]
 	public void MoveToOverlappedChild_Throw_NullReferenceException_Passing_Null_Parameter () => Assert.Throws<NullReferenceException> (delegate { Application.MoveToOverlappedChild (null); });
 
-	[Fact] [AutoInitShutdown]
+	[Fact, AutoInitShutdown]
 	public void Visible_False_Does_Not_Clear ()
 	{
 		var overlapped = new Overlapped ();

+ 1074 - 1074
UnitTests/Views/TreeViewTests.cs

@@ -1,1286 +1,1286 @@
 using System.Collections.Generic;
 using System.Linq;
+using System.Text;
 using Xunit;
 using Xunit.Abstractions;
 
-namespace Terminal.Gui.ViewsTests {
+namespace Terminal.Gui.ViewsTests;
 
-	public class TreeViewTests {
+public class TreeViewTests {
 
-		readonly ITestOutputHelper output;
+	readonly ITestOutputHelper _output;
 
-		public TreeViewTests (ITestOutputHelper output)
-		{
-			this.output = output;
-		}
+	public TreeViewTests (ITestOutputHelper output) => _output = output;
 
-		#region Test Setup Methods
-		class Factory {
-			public Car [] Cars { get; set; }
-			public override string ToString ()
-			{
-				return "Factory";
-			}
-		};
-		class Car {
-			public string Name { get; set; }
-			public override string ToString ()
-			{
-				return Name;
-			}
-		};
+	/// <summary>
+	/// Tests that <see cref="TreeView.Expand(object)"/> and <see cref="TreeView.IsExpanded(object)"/> are consistent
+	/// </summary>
+	[Fact]
+	public void IsExpanded_TrueAfterExpand ()
+	{
+		var tree = CreateTree (out var f, out _, out _);
+		Assert.False (tree.IsExpanded (f));
+
+		tree.Expand (f);
+		Assert.True (tree.IsExpanded (f));
+
+		tree.Collapse (f);
+		Assert.False (tree.IsExpanded (f));
+	}
+
+	[Fact]
+	public void EmptyTreeView_ContentSizes ()
+	{
+		var emptyTree = new TreeView ();
+		Assert.Equal (0, emptyTree.ContentHeight);
+		Assert.Equal (0, emptyTree.GetContentWidth (true));
+		Assert.Equal (0, emptyTree.GetContentWidth (false));
+	}
+
+	[Fact]
+	public void EmptyTreeViewGeneric_ContentSizes ()
+	{
+		var emptyTree = new TreeView<string> ();
+		Assert.Equal (0, emptyTree.ContentHeight);
+		Assert.Equal (0, emptyTree.GetContentWidth (true));
+		Assert.Equal (0, emptyTree.GetContentWidth (false));
+	}
+
+	/// <summary>
+	/// Tests that <see cref="TreeView.Expand(object)"/> results in a correct content height
+	/// </summary>
+	[Fact]
+	public void ContentHeight_BiggerAfterExpand ()
+	{
+		var tree = CreateTree (out var f, out _, out _);
+		Assert.Equal (1, tree.ContentHeight);
+
+		tree.Expand (f);
+		Assert.Equal (3, tree.ContentHeight);
+
+		tree.Collapse (f);
+		Assert.Equal (1, tree.ContentHeight);
+	}
+
+	[Fact]
+	public void ContentWidth_BiggerAfterExpand ()
+	{
+		var tree = CreateTree (out var f, out var car1, out _);
+		tree.BeginInit ();
+		tree.EndInit ();
+
+		tree.Bounds = new Rect (0, 0, 10, 10);
+
+		InitFakeDriver ();
+
+		//-+Factory
+		Assert.Equal (9, tree.GetContentWidth (true));
+
+		car1.Name = "123456789";
+
+		tree.Expand (f);
+
+		//..├-123456789
+		Assert.Equal (13, tree.GetContentWidth (true));
+
+		tree.Collapse (f);
+		//-+Factory
+		Assert.Equal (9, tree.GetContentWidth (true));
+
+		Application.Shutdown ();
+	}
+
+	[Fact]
+	public void ContentWidth_VisibleVsAll ()
+	{
+		var tree = CreateTree (out var f, out var car1, out var car2);
+		tree.BeginInit ();
+		tree.EndInit ();
+
+		// control only allows 1 row to be viewed at once
+		tree.Bounds = new Rect (0, 0, 20, 1);
+
+		InitFakeDriver ();
+
+		//-+Factory
+		Assert.Equal (9, tree.GetContentWidth (true));
+		Assert.Equal (9, tree.GetContentWidth (false));
+
+		car1.Name = "123456789";
+		car2.Name = "12345678";
+
+		tree.Expand (f);
+
+		// Although expanded the bigger (longer) child node is not in the rendered area of the control
+		Assert.Equal (9, tree.GetContentWidth (true));
+		Assert.Equal (13, tree.GetContentWidth (false)); // If you ask for the global max width it includes the longer child
+
+		// Now that we have scrolled down 1 row we should see the big child
+		tree.ScrollOffsetVertical = 1;
+		Assert.Equal (13, tree.GetContentWidth (true));
+		Assert.Equal (13, tree.GetContentWidth (false));
+
+		// Scroll down so only car2 is visible
+		tree.ScrollOffsetVertical = 2;
+		Assert.Equal (12, tree.GetContentWidth (true));
+		Assert.Equal (13, tree.GetContentWidth (false));
+
+		// Scroll way down (off bottom of control even)
+		tree.ScrollOffsetVertical = 5;
+		Assert.Equal (0, tree.GetContentWidth (true));
+		Assert.Equal (13, tree.GetContentWidth (false));
+
+		Application.Shutdown ();
+	}
+
+	/// <summary>
+	/// Tests that <see cref="TreeView.IsExpanded(object)"/> and <see cref="TreeView.Expand(object)"/> behaves correctly when
+	/// an object cannot be expanded (because it has no children)
+	/// </summary>
+	[Fact]
+	public void IsExpanded_FalseIfCannotExpand ()
+	{
+		var tree = CreateTree (out var f, out var c, out _);
+
+		// expose the car by expanding the factory
+		tree.Expand (f);
+
+		// car is not expanded
+		Assert.False (tree.IsExpanded (c));
+
+		//try to expand the car (should have no effect because cars have no children)
+		tree.Expand (c);
+
+		Assert.False (tree.IsExpanded (c));
 
-		private TreeView<object> CreateTree ()
-		{
-			return CreateTree (out _, out _, out _);
-		}
-
-		private TreeView<object> CreateTree (out Factory factory1, out Car car1, out Car car2)
-		{
-			car1 = new Car ();
-			car2 = new Car ();
-
-			factory1 = new Factory () {
-				Cars = new [] { car1, car2 }
-			};
-
-			var tree = new TreeView<object> (new DelegateTreeBuilder<object> ((s) => s is Factory f ? f.Cars : null));
-			tree.AddObject (factory1);
-
-			return tree;
-		}
-		#endregion
-
-		/// <summary>
-		/// Tests that <see cref="TreeView.Expand(object)"/> and <see cref="TreeView.IsExpanded(object)"/> are consistent
-		/// </summary>
-		[Fact]
-		public void IsExpanded_TrueAfterExpand ()
-		{
-			var tree = CreateTree (out Factory f, out _, out _);
-			Assert.False (tree.IsExpanded (f));
-
-			tree.Expand (f);
-			Assert.True (tree.IsExpanded (f));
-
-			tree.Collapse (f);
-			Assert.False (tree.IsExpanded (f));
-		}
-
-		[Fact]
-		public void EmptyTreeView_ContentSizes ()
-		{
-			var emptyTree = new TreeView ();
-			Assert.Equal (0, emptyTree.ContentHeight);
-			Assert.Equal (0, emptyTree.GetContentWidth (true));
-			Assert.Equal (0, emptyTree.GetContentWidth (false));
-		}
-		[Fact]
-		public void EmptyTreeViewGeneric_ContentSizes ()
-		{
-			var emptyTree = new TreeView<string> ();
-			Assert.Equal (0, emptyTree.ContentHeight);
-			Assert.Equal (0, emptyTree.GetContentWidth (true));
-			Assert.Equal (0, emptyTree.GetContentWidth (false));
-		}
-
-		/// <summary>
-		/// Tests that <see cref="TreeView.Expand(object)"/> results in a correct content height
-		/// </summary>
-		[Fact]
-		public void ContentHeight_BiggerAfterExpand ()
-		{
-			var tree = CreateTree (out Factory f, out _, out _);
-			Assert.Equal (1, tree.ContentHeight);
-
-			tree.Expand (f);
-			Assert.Equal (3, tree.ContentHeight);
-
-			tree.Collapse (f);
-			Assert.Equal (1, tree.ContentHeight);
-		}
-
-		[Fact]
-		public void ContentWidth_BiggerAfterExpand ()
-		{
-			var tree = CreateTree (out Factory f, out Car car1, out _);
-			tree.BeginInit (); tree.EndInit ();
-
-			tree.Bounds = new Rect (0, 0, 10, 10);
-
-			InitFakeDriver ();
-
-			//-+Factory
-			Assert.Equal (9, tree.GetContentWidth (true));
-
-			car1.Name = "123456789";
-
-			tree.Expand (f);
-
-			//..├-123456789
-			Assert.Equal (13, tree.GetContentWidth (true));
-
-			tree.Collapse (f);
-			//-+Factory
-			Assert.Equal (9, tree.GetContentWidth (true));
-
-			Application.Shutdown ();
-		}
-
-		[Fact]
-		public void ContentWidth_VisibleVsAll ()
-		{
-			var tree = CreateTree (out Factory f, out Car car1, out Car car2);
-			tree.BeginInit (); tree.EndInit ();
-
-			// control only allows 1 row to be viewed at once
-			tree.Bounds = new Rect (0, 0, 20, 1);
-
-			InitFakeDriver ();
-
-			//-+Factory
-			Assert.Equal (9, tree.GetContentWidth (true));
-			Assert.Equal (9, tree.GetContentWidth (false));
-
-			car1.Name = "123456789";
-			car2.Name = "12345678";
-
-			tree.Expand (f);
-
-			// Although expanded the bigger (longer) child node is not in the rendered area of the control
-			Assert.Equal (9, tree.GetContentWidth (true));
-			Assert.Equal (13, tree.GetContentWidth (false)); // If you ask for the global max width it includes the longer child
-
-			// Now that we have scrolled down 1 row we should see the big child
-			tree.ScrollOffsetVertical = 1;
-			Assert.Equal (13, tree.GetContentWidth (true));
-			Assert.Equal (13, tree.GetContentWidth (false));
-
-			// Scroll down so only car2 is visible
-			tree.ScrollOffsetVertical = 2;
-			Assert.Equal (12, tree.GetContentWidth (true));
-			Assert.Equal (13, tree.GetContentWidth (false));
-
-			// Scroll way down (off bottom of control even)
-			tree.ScrollOffsetVertical = 5;
-			Assert.Equal (0, tree.GetContentWidth (true));
-			Assert.Equal (13, tree.GetContentWidth (false));
-
-			Application.Shutdown ();
-		}
-		/// <summary>
-		/// Tests that <see cref="TreeView.IsExpanded(object)"/> and <see cref="TreeView.Expand(object)"/> behaves correctly when an object cannot be expanded (because it has no children)
-		/// </summary>
-		[Fact]
-		public void IsExpanded_FalseIfCannotExpand ()
-		{
-			var tree = CreateTree (out Factory f, out Car c, out _);
-
-			// expose the car by expanding the factory
-			tree.Expand (f);
-
-			// car is not expanded
-			Assert.False (tree.IsExpanded (c));
+		// should also be ignored
+		tree.Collapse (c);
 
-			//try to expand the car (should have no effect because cars have no children)
-			tree.Expand (c);
-
-			Assert.False (tree.IsExpanded (c));
-
-			// should also be ignored
-			tree.Collapse (c);
+		Assert.False (tree.IsExpanded (c));
 
-			Assert.False (tree.IsExpanded (c));
+		Application.Shutdown ();
+	}
+
+	/// <summary>
+	/// Tests illegal ranges for <see cref="TreeView.ScrollOffset"/>
+	/// </summary>
+	[Fact]
+	public void ScrollOffset_CannotBeNegative ()
+	{
+		var tree = CreateTree ();
+
+		Assert.Equal (0, tree.ScrollOffsetVertical);
+
+		tree.ScrollOffsetVertical = -100;
+		Assert.Equal (0, tree.ScrollOffsetVertical);
+
+		tree.ScrollOffsetVertical = 10;
+		Assert.Equal (10, tree.ScrollOffsetVertical);
+	}
+
+	/// <summary>
+	/// Tests <see cref="TreeView.GetScrollOffsetOf(object)"/> for objects that are as yet undiscovered by the tree
+	/// </summary>
+	[Fact]
+	public void GetScrollOffsetOf_MinusOneForUnRevealed ()
+	{
+		var tree = CreateTree (out var f, out var c1, out var c2);
+
+		// to start with the tree is collapsed and only knows about the root object
+		Assert.Equal (0, tree.GetScrollOffsetOf (f));
+		Assert.Equal (-1, tree.GetScrollOffsetOf (c1));
+		Assert.Equal (-1, tree.GetScrollOffsetOf (c2));
+
+		// reveal it by expanding the root object
+		tree.Expand (f);
+
+		// tree now knows about children
+		Assert.Equal (0, tree.GetScrollOffsetOf (f));
+		Assert.Equal (1, tree.GetScrollOffsetOf (c1));
+		Assert.Equal (2, tree.GetScrollOffsetOf (c2));
+
+		// after collapsing the root node again
+		tree.Collapse (f);
+
+		// tree no longer knows about the locations of these objects
+		Assert.Equal (0, tree.GetScrollOffsetOf (f));
+		Assert.Equal (-1, tree.GetScrollOffsetOf (c1));
+		Assert.Equal (-1, tree.GetScrollOffsetOf (c2));
+	}
+
+	/// <summary>
+	/// Simulates behind the scenes changes to an object (which children it has) and how to sync that into the tree using
+	/// <see cref="TreeView.RefreshObject(object, bool)"/>
+	/// </summary>
+	[Fact]
+	public void RefreshObject_ChildRemoved ()
+	{
+		var tree = CreateTree (out var f, out var c1, out var c2);
+
+		//reveal it by expanding the root object
+		tree.Expand (f);
+
+		Assert.Equal (0, tree.GetScrollOffsetOf (f));
+		Assert.Equal (1, tree.GetScrollOffsetOf (c1));
+		Assert.Equal (2, tree.GetScrollOffsetOf (c2));
+
+		// Factory now no longer makes Car c1 (only c2)
+		f.Cars = new [] { c2 };
+
+		// Tree does not know this yet
+		Assert.Equal (0, tree.GetScrollOffsetOf (f));
+		Assert.Equal (1, tree.GetScrollOffsetOf (c1));
+		Assert.Equal (2, tree.GetScrollOffsetOf (c2));
+
+		// If the user has selected the node c1
+		tree.SelectedObject = c1;
+
+		// When we refresh the tree
+		tree.RefreshObject (f);
+
+		// Now tree knows that factory has only one child node c2
+		Assert.Equal (0, tree.GetScrollOffsetOf (f));
+		Assert.Equal (-1, tree.GetScrollOffsetOf (c1));
+		Assert.Equal (1, tree.GetScrollOffsetOf (c2));
+
+		// The old selection was c1 which is now gone so selection should default to the parent of that branch (the factory)
+		Assert.Equal (f, tree.SelectedObject);
+	}
+
+	/// <summary>
+	/// Tests that <see cref="TreeView.GetParent(object)"/> returns the parent object for
+	/// Cars (Factories).  Note that the method only works once the parent branch (Factory)
+	/// is expanded to expose the child (Car)
+	/// </summary>
+	[Fact]
+	public void GetParent_ReturnsParentOnlyWhenExpanded ()
+	{
+		var tree = CreateTree (out var f, out var c1, out var c2);
+
+		Assert.Null (tree.GetParent (f));
+		Assert.Null (tree.GetParent (c1));
+		Assert.Null (tree.GetParent (c2));
+
+		// now when we expand the factory we discover the cars
+		tree.Expand (f);
 
-			Application.Shutdown ();
-		}
+		Assert.Null (tree.GetParent (f));
+		Assert.Equal (f, tree.GetParent (c1));
+		Assert.Equal (f, tree.GetParent (c2));
 
-		/// <summary>
-		/// Tests illegal ranges for <see cref="TreeView.ScrollOffset"/>
-		/// </summary>
-		[Fact]
-		public void ScrollOffset_CannotBeNegative ()
-		{
-			var tree = CreateTree ();
+		tree.Collapse (f);
 
-			Assert.Equal (0, tree.ScrollOffsetVertical);
+		Assert.Null (tree.GetParent (f));
+		Assert.Null (tree.GetParent (c1));
+		Assert.Null (tree.GetParent (c2));
+	}
 
-			tree.ScrollOffsetVertical = -100;
-			Assert.Equal (0, tree.ScrollOffsetVertical);
+	/// <summary>
+	/// Tests how the tree adapts to changes in the ChildrenGetter delegate during runtime
+	/// when some branches are expanded and the new delegate returns children for a node that
+	/// previously didn't have any children
+	/// </summary>
+	[Fact]
+	public void RefreshObject_AfterChangingChildrenGetterDuringRuntime ()
+	{
+		var tree = CreateTree (out var f, out var c1, out var c2);
+
+		var wheel = "Shiny Wheel";
+
+		// Expand the Factory
+		tree.Expand (f);
+
+		// c1 cannot have children
+		Assert.Equal (f, tree.GetParent (c1));
+
+		// expanding it does nothing
+		tree.Expand (c1);
+		Assert.False (tree.IsExpanded (c1));
+
+		// change the children getter so that now cars can have wheels
+		tree.TreeBuilder = new DelegateTreeBuilder<object> (o =>
+			// factories have cars
+			o is Factory ? new object [] { c1, c2 }
+				// cars have wheels
+				: new object [] { wheel });
+
+		// still cannot expand
+		tree.Expand (c1);
+		Assert.False (tree.IsExpanded (c1));
+
+		tree.RefreshObject (c1);
+		tree.Expand (c1);
+		Assert.True (tree.IsExpanded (c1));
+		Assert.Equal (wheel, tree.GetChildren (c1).FirstOrDefault ());
+	}
 
-			tree.ScrollOffsetVertical = 10;
-			Assert.Equal (10, tree.ScrollOffsetVertical);
-		}
-
-		/// <summary>
-		/// Tests <see cref="TreeView.GetScrollOffsetOf(object)"/> for objects that are as yet undiscovered by the tree
-		/// </summary>
-		[Fact]
-		public void GetScrollOffsetOf_MinusOneForUnRevealed ()
-		{
-			var tree = CreateTree (out Factory f, out Car c1, out Car c2);
-
-			// to start with the tree is collapsed and only knows about the root object
-			Assert.Equal (0, tree.GetScrollOffsetOf (f));
-			Assert.Equal (-1, tree.GetScrollOffsetOf (c1));
-			Assert.Equal (-1, tree.GetScrollOffsetOf (c2));
-
-			// reveal it by expanding the root object
-			tree.Expand (f);
-
-			// tree now knows about children
-			Assert.Equal (0, tree.GetScrollOffsetOf (f));
-			Assert.Equal (1, tree.GetScrollOffsetOf (c1));
-			Assert.Equal (2, tree.GetScrollOffsetOf (c2));
-
-			// after collapsing the root node again
-			tree.Collapse (f);
-
-			// tree no longer knows about the locations of these objects
-			Assert.Equal (0, tree.GetScrollOffsetOf (f));
-			Assert.Equal (-1, tree.GetScrollOffsetOf (c1));
-			Assert.Equal (-1, tree.GetScrollOffsetOf (c2));
-		}
-
-		/// <summary>
-		/// Simulates behind the scenes changes to an object (which children it has) and how to sync that into the tree using <see cref="TreeView.RefreshObject(object, bool)"/>
-		/// </summary>
-		[Fact]
-		public void RefreshObject_ChildRemoved ()
-		{
-			var tree = CreateTree (out Factory f, out Car c1, out Car c2);
-
-			//reveal it by expanding the root object
-			tree.Expand (f);
-
-			Assert.Equal (0, tree.GetScrollOffsetOf (f));
-			Assert.Equal (1, tree.GetScrollOffsetOf (c1));
-			Assert.Equal (2, tree.GetScrollOffsetOf (c2));
-
-			// Factory now no longer makes Car c1 (only c2)
-			f.Cars = new Car [] { c2 };
-
-			// Tree does not know this yet
-			Assert.Equal (0, tree.GetScrollOffsetOf (f));
-			Assert.Equal (1, tree.GetScrollOffsetOf (c1));
-			Assert.Equal (2, tree.GetScrollOffsetOf (c2));
-
-			// If the user has selected the node c1
-			tree.SelectedObject = c1;
-
-			// When we refresh the tree
-			tree.RefreshObject (f);
-
-			// Now tree knows that factory has only one child node c2
-			Assert.Equal (0, tree.GetScrollOffsetOf (f));
-			Assert.Equal (-1, tree.GetScrollOffsetOf (c1));
-			Assert.Equal (1, tree.GetScrollOffsetOf (c2));
-
-			// The old selection was c1 which is now gone so selection should default to the parent of that branch (the factory)
-			Assert.Equal (f, tree.SelectedObject);
-		}
-
-		/// <summary>
-		/// Tests that <see cref="TreeView.GetParent(object)"/> returns the parent object for
-		/// Cars (Factories).  Note that the method only works once the parent branch (Factory)
-		/// is expanded to expose the child (Car)
-		/// </summary>
-		[Fact]
-		public void GetParent_ReturnsParentOnlyWhenExpanded ()
-		{
-			var tree = CreateTree (out Factory f, out Car c1, out Car c2);
-
-			Assert.Null (tree.GetParent (f));
-			Assert.Null (tree.GetParent (c1));
-			Assert.Null (tree.GetParent (c2));
-
-			// now when we expand the factory we discover the cars
-			tree.Expand (f);
-
-			Assert.Null (tree.GetParent (f));
-			Assert.Equal (f, tree.GetParent (c1));
-			Assert.Equal (f, tree.GetParent (c2));
-
-			tree.Collapse (f);
-
-			Assert.Null (tree.GetParent (f));
-			Assert.Null (tree.GetParent (c1));
-			Assert.Null (tree.GetParent (c2));
-		}
-
-		/// <summary>
-		/// Tests how the tree adapts to changes in the ChildrenGetter delegate during runtime
-		/// when some branches are expanded and the new delegate returns children for a node that
-		/// previously didn't have any children
-		/// </summary>
-		[Fact]
-		public void RefreshObject_AfterChangingChildrenGetterDuringRuntime ()
-		{
-			var tree = CreateTree (out Factory f, out Car c1, out Car c2);
-
-			string wheel = "Shiny Wheel";
-
-			// Expand the Factory
-			tree.Expand (f);
-
-			// c1 cannot have children
-			Assert.Equal (f, tree.GetParent (c1));
-
-			// expanding it does nothing
-			tree.Expand (c1);
-			Assert.False (tree.IsExpanded (c1));
-
-			// change the children getter so that now cars can have wheels
-			tree.TreeBuilder = new DelegateTreeBuilder<object> ((o) =>
-				 // factories have cars
-				 o is Factory ? new object [] { c1, c2 }
-				 // cars have wheels
-				 : new object [] { wheel });
-
-			// still cannot expand
-			tree.Expand (c1);
-			Assert.False (tree.IsExpanded (c1));
-
-			tree.RefreshObject (c1);
-			tree.Expand (c1);
-			Assert.True (tree.IsExpanded (c1));
-			Assert.Equal (wheel, tree.GetChildren (c1).FirstOrDefault ());
-		}
-		/// <summary>
-		/// Same as <see cref="RefreshObject_AfterChangingChildrenGetterDuringRuntime"/> but
-		/// uses <see cref="TreeView.RebuildTree()"/> instead of <see cref="TreeView.RefreshObject(object, bool)"/>
-		/// </summary>
-		[Fact]
-		public void RebuildTree_AfterChangingChildrenGetterDuringRuntime ()
-		{
-			var tree = CreateTree (out Factory f, out Car c1, out Car c2);
-
-			string wheel = "Shiny Wheel";
-
-			// Expand the Factory
-			tree.Expand (f);
-
-			// c1 cannot have children
-			Assert.Equal (f, tree.GetParent (c1));
-
-			// expanding it does nothing
-			tree.Expand (c1);
-			Assert.False (tree.IsExpanded (c1));
-
-			// change the children getter so that now cars can have wheels
-			tree.TreeBuilder = new DelegateTreeBuilder<object> ((o) =>
-				 // factories have cars
-				 o is Factory ? new object [] { c1, c2 }
-				 // cars have wheels
-				 : new object [] { wheel });
-
-			// still cannot expand
-			tree.Expand (c1);
-			Assert.False (tree.IsExpanded (c1));
-
-			// Rebuild the tree
-			tree.RebuildTree ();
-
-			// Rebuild should not have collapsed any branches or done anything wierd
-			Assert.True (tree.IsExpanded (f));
-
-			tree.Expand (c1);
-			Assert.True (tree.IsExpanded (c1));
-			Assert.Equal (wheel, tree.GetChildren (c1).FirstOrDefault ());
-		}
-		/// <summary>
-		/// Tests that <see cref="TreeView.GetChildren(object)"/> returns the child objects for
-		/// the factory.  Note that the method only works once the parent branch (Factory)
-		/// is expanded to expose the child (Car)
-		/// </summary>
-		[Fact]
-		public void GetChildren_ReturnsChildrenOnlyWhenExpanded ()
-		{
-			var tree = CreateTree (out Factory f, out Car c1, out Car c2);
-
-			Assert.Empty (tree.GetChildren (f));
-			Assert.Empty (tree.GetChildren (c1));
-			Assert.Empty (tree.GetChildren (c2));
-
-			// now when we expand the factory we discover the cars
-			tree.Expand (f);
-
-			Assert.Contains (c1, tree.GetChildren (f));
-			Assert.Contains (c2, tree.GetChildren (f));
-			Assert.Empty (tree.GetChildren (c1));
-			Assert.Empty (tree.GetChildren (c2));
-
-			tree.Collapse (f);
-
-			Assert.Empty (tree.GetChildren (f));
-			Assert.Empty (tree.GetChildren (c1));
-			Assert.Empty (tree.GetChildren (c2));
-		}
+	/// <summary>
+	/// Same as <see cref="RefreshObject_AfterChangingChildrenGetterDuringRuntime"/> but
+	/// uses <see cref="TreeView.RebuildTree()"/> instead of <see cref="TreeView.RefreshObject(object, bool)"/>
+	/// </summary>
+	[Fact]
+	public void RebuildTree_AfterChangingChildrenGetterDuringRuntime ()
+	{
+		var tree = CreateTree (out var f, out var c1, out var c2);
 
-		[Fact]
-		public void TreeNode_WorksWithoutDelegate ()
-		{
-			var tree = new TreeView ();
+		var wheel = "Shiny Wheel";
 
-			var root = new TreeNode ("Root");
-			root.Children.Add (new TreeNode ("Leaf1"));
-			root.Children.Add (new TreeNode ("Leaf2"));
+		// Expand the Factory
+		tree.Expand (f);
 
-			tree.AddObject (root);
+		// c1 cannot have children
+		Assert.Equal (f, tree.GetParent (c1));
 
-			tree.Expand (root);
-			Assert.Equal (2, tree.GetChildren (root).Count ());
-		}
+		// expanding it does nothing
+		tree.Expand (c1);
+		Assert.False (tree.IsExpanded (c1));
 
-		[Fact]
-		public void MultiSelect_GetAllSelectedObjects ()
-		{
-			var tree = new TreeView ();
+		// change the children getter so that now cars can have wheels
+		tree.TreeBuilder = new DelegateTreeBuilder<object> (o =>
+			// factories have cars
+			o is Factory ? new object [] { c1, c2 }
+				// cars have wheels
+				: new object [] { wheel });
 
-			TreeNode l1;
-			TreeNode l2;
-			TreeNode l3;
-			TreeNode l4;
+		// still cannot expand
+		tree.Expand (c1);
+		Assert.False (tree.IsExpanded (c1));
 
-			var root = new TreeNode ("Root");
-			root.Children.Add (l1 = new TreeNode ("Leaf1"));
-			root.Children.Add (l2 = new TreeNode ("Leaf2"));
-			root.Children.Add (l3 = new TreeNode ("Leaf3"));
-			root.Children.Add (l4 = new TreeNode ("Leaf4"));
+		// Rebuild the tree
+		tree.RebuildTree ();
 
-			tree.AddObject (root);
-			tree.MultiSelect = true;
+		// Rebuild should not have collapsed any branches or done anything wierd
+		Assert.True (tree.IsExpanded (f));
 
-			tree.Expand (root);
-			Assert.Empty (tree.GetAllSelectedObjects ());
+		tree.Expand (c1);
+		Assert.True (tree.IsExpanded (c1));
+		Assert.Equal (wheel, tree.GetChildren (c1).FirstOrDefault ());
+	}
 
-			tree.SelectedObject = root;
+	/// <summary>
+	/// Tests that <see cref="TreeView.GetChildren(object)"/> returns the child objects for
+	/// the factory.  Note that the method only works once the parent branch (Factory)
+	/// is expanded to expose the child (Car)
+	/// </summary>
+	[Fact]
+	public void GetChildren_ReturnsChildrenOnlyWhenExpanded ()
+	{
+		var tree = CreateTree (out var f, out var c1, out var c2);
+
+		Assert.Empty (tree.GetChildren (f));
+		Assert.Empty (tree.GetChildren (c1));
+		Assert.Empty (tree.GetChildren (c2));
+
+		// now when we expand the factory we discover the cars
+		tree.Expand (f);
+
+		Assert.Contains (c1, tree.GetChildren (f));
+		Assert.Contains (c2, tree.GetChildren (f));
+		Assert.Empty (tree.GetChildren (c1));
+		Assert.Empty (tree.GetChildren (c2));
+
+		tree.Collapse (f);
+
+		Assert.Empty (tree.GetChildren (f));
+		Assert.Empty (tree.GetChildren (c1));
+		Assert.Empty (tree.GetChildren (c2));
+	}
 
-			Assert.Single (tree.GetAllSelectedObjects (), root);
+	[Fact]
+	public void TreeNode_WorksWithoutDelegate ()
+	{
+		var tree = new TreeView ();
 
-			// move selection down 1
-			tree.AdjustSelection (1, false);
+		var root = new TreeNode ("Root");
+		root.Children.Add (new TreeNode ("Leaf1"));
+		root.Children.Add (new TreeNode ("Leaf2"));
 
-			Assert.Single (tree.GetAllSelectedObjects (), l1);
+		tree.AddObject (root);
 
-			// expand selection down 2 (e.g. shift down twice)
-			tree.AdjustSelection (1, true);
-			tree.AdjustSelection (1, true);
+		tree.Expand (root);
+		Assert.Equal (2, tree.GetChildren (root).Count ());
+	}
 
-			Assert.Equal (3, tree.GetAllSelectedObjects ().Count ());
-			Assert.Contains (l1, tree.GetAllSelectedObjects ());
-			Assert.Contains (l2, tree.GetAllSelectedObjects ());
-			Assert.Contains (l3, tree.GetAllSelectedObjects ());
+	[Fact]
+	public void MultiSelect_GetAllSelectedObjects ()
+	{
+		var tree = new TreeView ();
 
-			tree.Collapse (root);
+		TreeNode l1;
+		TreeNode l2;
+		TreeNode l3;
+		TreeNode l4;
 
-			// No selected objects since the root was collapsed
-			Assert.Empty (tree.GetAllSelectedObjects ());
-		}
+		var root = new TreeNode ("Root");
+		root.Children.Add (l1 = new TreeNode ("Leaf1"));
+		root.Children.Add (l2 = new TreeNode ("Leaf2"));
+		root.Children.Add (l3 = new TreeNode ("Leaf3"));
+		root.Children.Add (l4 = new TreeNode ("Leaf4"));
 
-		[Fact]
-		public void ObjectActivated_Called ()
-		{
-			var tree = CreateTree (out Factory f, out Car car1, out _);
+		tree.AddObject (root);
+		tree.MultiSelect = true;
 
-			InitFakeDriver ();
+		tree.Expand (root);
+		Assert.Empty (tree.GetAllSelectedObjects ());
 
-			object activated = null;
-			bool called = false;
+		tree.SelectedObject = root;
 
-			// register for the event
-			tree.ObjectActivated += (s, e) => {
-				activated = e.ActivatedObject;
-				called = true;
-			};
+		Assert.Single (tree.GetAllSelectedObjects (), root);
 
-			Assert.False (called);
+		// move selection down 1
+		tree.AdjustSelection (1);
 
-			// no object is selected yet so no event should happen
-			tree.NewKeyDownEvent (new (KeyCode.Enter));
+		Assert.Single (tree.GetAllSelectedObjects (), l1);
 
-			Assert.Null (activated);
-			Assert.False (called);
+		// expand selection down 2 (e.g. shift down twice)
+		tree.AdjustSelection (1, true);
+		tree.AdjustSelection (1, true);
 
-			// down to select factory
-			tree.NewKeyDownEvent (new (KeyCode.CursorDown));
+		Assert.Equal (3, tree.GetAllSelectedObjects ().Count ());
+		Assert.Contains (l1, tree.GetAllSelectedObjects ());
+		Assert.Contains (l2, tree.GetAllSelectedObjects ());
+		Assert.Contains (l3, tree.GetAllSelectedObjects ());
 
-			tree.NewKeyDownEvent (new (KeyCode.Enter));
+		tree.Collapse (root);
 
-			Assert.True (called);
-			Assert.Same (f, activated);
+		// No selected objects since the root was collapsed
+		Assert.Empty (tree.GetAllSelectedObjects ());
+	}
 
-			Application.Shutdown ();
-		}
+	[Fact]
+	public void ObjectActivated_Called ()
+	{
+		var tree = CreateTree (out var f, out var car1, out _);
 
-		[Fact]
-		public void GoTo_OnlyAppliesToExposedObjects ()
-		{
-			var tree = CreateTree (out Factory f, out Car car1, out _);
-			tree.BeginInit (); tree.EndInit ();
+		InitFakeDriver ();
 
-			// Make tree bounds 1 in height so that EnsureVisible always requires updating scroll offset
-			tree.Bounds = new Rect (0, 0, 50, 1);
+		object activated = null;
+		var called = false;
 
-			Assert.Null (tree.SelectedObject);
-			Assert.Equal (0, tree.ScrollOffsetVertical);
+		// register for the event
+		tree.ObjectActivated += (s, e) => {
+			activated = e.ActivatedObject;
+			called = true;
+		};
 
-			// car 1 is not yet exposed
-			tree.GoTo (car1);
+		Assert.False (called);
 
-			Assert.Null (tree.SelectedObject);
-			Assert.Equal (0, tree.ScrollOffsetVertical);
+		// no object is selected yet so no event should happen
+		tree.NewKeyDownEvent (new Key (KeyCode.Enter));
 
-			tree.Expand (f);
+		Assert.Null (activated);
+		Assert.False (called);
 
-			// Car1 is now exposed by expanding the factory
-			tree.GoTo (car1);
+		// down to select factory
+		tree.NewKeyDownEvent (new Key (KeyCode.CursorDown));
 
-			Assert.Equal (car1, tree.SelectedObject);
-			Assert.Equal (1, tree.ScrollOffsetVertical);
-		}
+		tree.NewKeyDownEvent (new Key (KeyCode.Enter));
 
-		[Fact]
-		public void GoToEnd_ShouldNotFailOnEmptyTreeView ()
-		{
-			var tree = new TreeView ();
+		Assert.True (called);
+		Assert.Same (f, activated);
 
-			var exception = Record.Exception (() => tree.GoToEnd ());
+		Application.Shutdown ();
+	}
 
-			Assert.Null (exception);
-		}
+	[Fact]
+	public void GoTo_OnlyAppliesToExposedObjects ()
+	{
+		var tree = CreateTree (out var f, out var car1, out _);
+		tree.BeginInit ();
+		tree.EndInit ();
 
-		[Fact]
-		public void ObjectActivated_CustomKey ()
-		{
-			var tree = CreateTree (out Factory f, out Car car1, out _);
+		// Make tree bounds 1 in height so that EnsureVisible always requires updating scroll offset
+		tree.Bounds = new Rect (0, 0, 50, 1);
 
-			InitFakeDriver ();
+		Assert.Null (tree.SelectedObject);
+		Assert.Equal (0, tree.ScrollOffsetVertical);
 
-			tree.ObjectActivationKey = KeyCode.Delete;
-			object activated = null;
-			bool called = false;
+		// car 1 is not yet exposed
+		tree.GoTo (car1);
 
-			// register for the event
-			tree.ObjectActivated += (s, e) => {
-				activated = e.ActivatedObject;
-				called = true;
-			};
+		Assert.Null (tree.SelectedObject);
+		Assert.Equal (0, tree.ScrollOffsetVertical);
 
-			Assert.False (called);
+		tree.Expand (f);
 
-			// no object is selected yet so no event should happen
-			tree.NewKeyDownEvent (new (KeyCode.Enter));
+		// Car1 is now exposed by expanding the factory
+		tree.GoTo (car1);
 
-			Assert.Null (activated);
-			Assert.False (called);
+		Assert.Equal (car1, tree.SelectedObject);
+		Assert.Equal (1, tree.ScrollOffsetVertical);
+	}
 
-			// down to select factory
-			tree.NewKeyDownEvent (new (KeyCode.CursorDown));
+	[Fact]
+	public void GoToEnd_ShouldNotFailOnEmptyTreeView ()
+	{
+		var tree = new TreeView ();
 
-			tree.NewKeyDownEvent (new (KeyCode.Enter));
+		var exception = Record.Exception (() => tree.GoToEnd ());
 
-			// Enter is not the activation key in this unit test
-			Assert.Null (activated);
-			Assert.False (called);
+		Assert.Null (exception);
+	}
 
-			// Delete is the activation key in this test so should result in activation occurring
-			tree.NewKeyDownEvent (new (KeyCode.Delete));
+	[Fact]
+	public void ObjectActivated_CustomKey ()
+	{
+		var tree = CreateTree (out var f, out var car1, out _);
 
-			Assert.True (called);
-			Assert.Same (f, activated);
+		InitFakeDriver ();
 
-			Application.Shutdown ();
-		}
+		tree.ObjectActivationKey = KeyCode.Delete;
+		object activated = null;
+		var called = false;
 
-		[Fact]
-		public void ObjectActivationButton_DoubleClick ()
-		{
-			var tree = CreateTree (out Factory f, out Car car1, out _);
+		// register for the event
+		tree.ObjectActivated += (s, e) => {
+			activated = e.ActivatedObject;
+			called = true;
+		};
 
-			InitFakeDriver ();
+		Assert.False (called);
 
-			object activated = null;
-			bool called = false;
+		// no object is selected yet so no event should happen
+		tree.NewKeyDownEvent (new Key (KeyCode.Enter));
 
-			// register for the event
-			tree.ObjectActivated += (s, e) => {
-				activated = e.ActivatedObject;
-				called = true;
-			};
+		Assert.Null (activated);
+		Assert.False (called);
 
-			Assert.False (called);
+		// down to select factory
+		tree.NewKeyDownEvent (new Key (KeyCode.CursorDown));
 
-			// double click triggers activation
-			tree.MouseEvent (new MouseEvent () { Y = 0, Flags = MouseFlags.Button1DoubleClicked });
+		tree.NewKeyDownEvent (new Key (KeyCode.Enter));
 
-			Assert.True (called);
-			Assert.Same (f, activated);
-			Assert.Same (f, tree.SelectedObject);
+		// Enter is not the activation key in this unit test
+		Assert.Null (activated);
+		Assert.False (called);
 
-			Application.Shutdown ();
-		}
+		// Delete is the activation key in this test so should result in activation occurring
+		tree.NewKeyDownEvent (new Key (KeyCode.Delete));
 
-		[Fact]
-		public void ObjectActivationButton_SetToNull ()
-		{
-			var tree = CreateTree (out Factory f, out Car car1, out _);
+		Assert.True (called);
+		Assert.Same (f, activated);
 
-			InitFakeDriver ();
+		Application.Shutdown ();
+	}
+
+	[Fact]
+	public void ObjectActivationButton_DoubleClick ()
+	{
+		var tree = CreateTree (out var f, out var car1, out _);
+
+		InitFakeDriver ();
 
-			// disable activation
-			tree.ObjectActivationButton = null;
+		object activated = null;
+		var called = false;
+
+		// register for the event
+		tree.ObjectActivated += (s, e) => {
+			activated = e.ActivatedObject;
+			called = true;
+		};
 
-			object activated = null;
-			bool called = false;
+		Assert.False (called);
 
-			// register for the event
-			tree.ObjectActivated += (s, e) => {
-				activated = e.ActivatedObject;
-				called = true;
-			};
+		// double click triggers activation
+		tree.MouseEvent (new MouseEvent { Y = 0, Flags = MouseFlags.Button1DoubleClicked });
 
-			Assert.False (called);
+		Assert.True (called);
+		Assert.Same (f, activated);
+		Assert.Same (f, tree.SelectedObject);
 
-			// double click does nothing because we changed button to null
-			tree.MouseEvent (new MouseEvent () { Y = 0, Flags = MouseFlags.Button1DoubleClicked });
+		Application.Shutdown ();
+	}
 
-			Assert.False (called);
-			Assert.Null (activated);
-			Assert.Null (tree.SelectedObject);
+	[Fact]
+	public void ObjectActivationButton_SetToNull ()
+	{
+		var tree = CreateTree (out var f, out var car1, out _);
 
-			Application.Shutdown ();
-		}
+		InitFakeDriver ();
 
-		[Fact]
-		public void ObjectActivationButton_RightClick ()
-		{
-			var tree = CreateTree (out Factory f, out Car car1, out _);
+		// disable activation
+		tree.ObjectActivationButton = null;
 
-			InitFakeDriver ();
+		object activated = null;
+		var called = false;
 
-			tree.ObjectActivationButton = MouseFlags.Button2Clicked;
-			tree.ExpandAll ();
+		// register for the event
+		tree.ObjectActivated += (s, e) => {
+			activated = e.ActivatedObject;
+			called = true;
+		};
 
-			object activated = null;
-			bool called = false;
+		Assert.False (called);
 
-			// register for the event
-			tree.ObjectActivated += (s, e) => {
-				activated = e.ActivatedObject;
-				called = true;
-			};
+		// double click does nothing because we changed button to null
+		tree.MouseEvent (new MouseEvent { Y = 0, Flags = MouseFlags.Button1DoubleClicked });
 
-			Assert.False (called);
+		Assert.False (called);
+		Assert.Null (activated);
+		Assert.Null (tree.SelectedObject);
 
-			// double click does nothing because we changed button binding to right click
-			tree.MouseEvent (new MouseEvent () { Y = 1, Flags = MouseFlags.Button1DoubleClicked });
+		Application.Shutdown ();
+	}
 
-			Assert.Null (activated);
-			Assert.False (called);
+	[Fact]
+	public void ObjectActivationButton_RightClick ()
+	{
+		var tree = CreateTree (out var f, out var car1, out _);
 
-			tree.MouseEvent (new MouseEvent () { Y = 1, Flags = MouseFlags.Button2Clicked });
+		InitFakeDriver ();
 
-			Assert.True (called);
-			Assert.Same (car1, activated);
-			Assert.Same (car1, tree.SelectedObject);
+		tree.ObjectActivationButton = MouseFlags.Button2Clicked;
+		tree.ExpandAll ();
 
-			Application.Shutdown ();
-		}
+		object activated = null;
+		var called = false;
 
+		// register for the event
+		tree.ObjectActivated += (s, e) => {
+			activated = e.ActivatedObject;
+			called = true;
+		};
 
-		/// <summary>
-		/// Simulates behind the scenes changes to an object (which children it has) and how to sync that into the tree using <see cref="TreeView.RefreshObject(object, bool)"/>
-		/// </summary>
-		[Fact]
-		public void RefreshObject_EqualityTest ()
-		{
-			var obj1 = new EqualityTestObject () { Name = "Bob", Age = 1 };
-			var obj2 = new EqualityTestObject () { Name = "Bob", Age = 2 }; ;
+		Assert.False (called);
 
-			string root = "root";
+		// double click does nothing because we changed button binding to right click
+		tree.MouseEvent (new MouseEvent { Y = 1, Flags = MouseFlags.Button1DoubleClicked });
 
-			var tree = new TreeView<object> ();
-			tree.TreeBuilder = new DelegateTreeBuilder<object> ((s) => ReferenceEquals (s, root) ? new object [] { obj1 } : null);
-			tree.AddObject (root);
+		Assert.Null (activated);
+		Assert.False (called);
 
-			// Tree is not expanded so the root has no children yet
-			Assert.Empty (tree.GetChildren (root));
+		tree.MouseEvent (new MouseEvent { Y = 1, Flags = MouseFlags.Button2Clicked });
 
-			tree.Expand (root);
+		Assert.True (called);
+		Assert.Same (car1, activated);
+		Assert.Same (car1, tree.SelectedObject);
 
-			// now that the tree is expanded we should get our child returned
-			Assert.Equal (1, tree.GetChildren (root).Count (child => ReferenceEquals (obj1, child)));
+		Application.Shutdown ();
+	}
 
-			// change the getter to return an Equal object (but not the same reference - obj2)
-			tree.TreeBuilder = new DelegateTreeBuilder<object> ((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)));
+	/// <summary>
+	/// Simulates behind the scenes changes to an object (which children it has) and how to sync that into the tree using
+	/// <see cref="TreeView.RefreshObject(object, bool)"/>
+	/// </summary>
+	[Fact]
+	public void RefreshObject_EqualityTest ()
+	{
+		var obj1 = new EqualityTestObject { Name = "Bob", Age = 1 };
+		var obj2 = new EqualityTestObject { Name = "Bob", Age = 2 };
+		;
 
-			// now that we refresh the root we should get the new child reference (obj2)
-			tree.RefreshObject (root);
-			Assert.Equal (1, tree.GetChildren (root).Count (child => ReferenceEquals (obj2, child)));
+		var root = "root";
 
-		}
-		[Fact, AutoInitShutdown]
-		public void TestGetObjectOnRow ()
-		{
-			var tv = new TreeView { Width = 20, Height = 10 };
-			tv.BeginInit (); tv.EndInit ();
-			var n1 = new TreeNode ("normal");
-			var n1_1 = new TreeNode ("pink");
-			var n1_2 = new TreeNode ("normal");
-			n1.Children.Add (n1_1);
-			n1.Children.Add (n1_2);
+		var tree = new TreeView<object> ();
+		tree.TreeBuilder = new DelegateTreeBuilder<object> (s => ReferenceEquals (s, root) ? new object [] { obj1 } : null);
+		tree.AddObject (root);
 
-			var n2 = new TreeNode ("pink");
-			tv.AddObject (n1);
-			tv.AddObject (n2);
-			tv.Expand (n1);
+		// Tree is not expanded so the root has no children yet
+		Assert.Empty (tree.GetChildren (root));
 
-			tv.ColorScheme = new ColorScheme ();
-			tv.LayoutSubviews ();
-			tv.Draw ();
+		tree.Expand (root);
 
-			TestHelpers.AssertDriverContentsAre (
-@"├-normal
+		// now that the tree is expanded we should get our child returned
+		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.TreeBuilder = new DelegateTreeBuilder<object> (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)));
+
+		// now that we refresh the root we should get the new child reference (obj2)
+		tree.RefreshObject (root);
+		Assert.Equal (1, tree.GetChildren (root).Count (child => ReferenceEquals (obj2, child)));
+
+	}
+
+	[Fact, AutoInitShutdown]
+	public void TestGetObjectOnRow ()
+	{
+		var tv = new TreeView { Width = 20, Height = 10 };
+		tv.BeginInit ();
+		tv.EndInit ();
+		var n1 = new TreeNode ("normal");
+		var n1_1 = new TreeNode ("pink");
+		var n1_2 = new TreeNode ("normal");
+		n1.Children.Add (n1_1);
+		n1.Children.Add (n1_2);
+
+		var n2 = new TreeNode ("pink");
+		tv.AddObject (n1);
+		tv.AddObject (n2);
+		tv.Expand (n1);
+
+		tv.ColorScheme = new ColorScheme ();
+		tv.LayoutSubviews ();
+		tv.Draw ();
+
+		TestHelpers.AssertDriverContentsAre (
+			@"├-normal
 │ ├─pink
 │ └─normal
 └─pink
-", output);
+", _output);
 
-			Assert.Same (n1, tv.GetObjectOnRow (0));
-			Assert.Same (n1_1, tv.GetObjectOnRow (1));
-			Assert.Same (n1_2, tv.GetObjectOnRow (2));
-			Assert.Same (n2, tv.GetObjectOnRow (3));
-			Assert.Null (tv.GetObjectOnRow (4));
+		Assert.Same (n1, tv.GetObjectOnRow (0));
+		Assert.Same (n1_1, tv.GetObjectOnRow (1));
+		Assert.Same (n1_2, tv.GetObjectOnRow (2));
+		Assert.Same (n2, tv.GetObjectOnRow (3));
+		Assert.Null (tv.GetObjectOnRow (4));
 
-			tv.Collapse (n1);
+		tv.Collapse (n1);
 
-			tv.Draw ();
+		tv.Draw ();
 
-			TestHelpers.AssertDriverContentsAre (
-@"├+normal
+		TestHelpers.AssertDriverContentsAre (
+			@"├+normal
 └─pink
-", output);
-
-			Assert.Same (n1, tv.GetObjectOnRow (0));
-			Assert.Same (n2, tv.GetObjectOnRow (1));
-			Assert.Null (tv.GetObjectOnRow (2));
-			Assert.Null (tv.GetObjectOnRow (3));
-			Assert.Null (tv.GetObjectOnRow (4));
-		}
-
-		[Fact, AutoInitShutdown]
-		public void TestGetObjectRow ()
-		{
-			var tv = new TreeView { Width = 20, Height = 10 };
-
-			var n1 = new TreeNode ("normal");
-			var n1_1 = new TreeNode ("pink");
-			var n1_2 = new TreeNode ("normal");
-			n1.Children.Add (n1_1);
-			n1.Children.Add (n1_2);
-
-			var n2 = new TreeNode ("pink");
-			tv.AddObject (n1);
-			tv.AddObject (n2);
-			tv.Expand (n1);
-
-			tv.ColorScheme = new ColorScheme ();
-			tv.LayoutSubviews ();
-			tv.Draw ();
-
-			TestHelpers.AssertDriverContentsAre (
-@"├-normal
+", _output);
+
+		Assert.Same (n1, tv.GetObjectOnRow (0));
+		Assert.Same (n2, tv.GetObjectOnRow (1));
+		Assert.Null (tv.GetObjectOnRow (2));
+		Assert.Null (tv.GetObjectOnRow (3));
+		Assert.Null (tv.GetObjectOnRow (4));
+	}
+
+	[Fact, AutoInitShutdown]
+	public void TestGetObjectRow ()
+	{
+		var tv = new TreeView { Width = 20, Height = 10 };
+
+		var n1 = new TreeNode ("normal");
+		var n1_1 = new TreeNode ("pink");
+		var n1_2 = new TreeNode ("normal");
+		n1.Children.Add (n1_1);
+		n1.Children.Add (n1_2);
+
+		var n2 = new TreeNode ("pink");
+		tv.AddObject (n1);
+		tv.AddObject (n2);
+		tv.Expand (n1);
+
+		tv.ColorScheme = new ColorScheme ();
+		tv.LayoutSubviews ();
+		tv.Draw ();
+
+		TestHelpers.AssertDriverContentsAre (
+			@"├-normal
 │ ├─pink
 │ └─normal
 └─pink
-", output);
+", _output);
 
-			Assert.Equal (0, tv.GetObjectRow (n1));
-			Assert.Equal (1, tv.GetObjectRow (n1_1));
-			Assert.Equal (2, tv.GetObjectRow (n1_2));
-			Assert.Equal (3, tv.GetObjectRow (n2));
+		Assert.Equal (0, tv.GetObjectRow (n1));
+		Assert.Equal (1, tv.GetObjectRow (n1_1));
+		Assert.Equal (2, tv.GetObjectRow (n1_2));
+		Assert.Equal (3, tv.GetObjectRow (n2));
 
-			tv.Collapse (n1);
+		tv.Collapse (n1);
 
-			tv.LayoutSubviews ();
-			tv.Draw ();
+		tv.LayoutSubviews ();
+		tv.Draw ();
 
-			TestHelpers.AssertDriverContentsAre (
-@"├+normal
+		TestHelpers.AssertDriverContentsAre (
+			@"├+normal
 └─pink
-", output);
-			Assert.Equal (0, tv.GetObjectRow (n1));
-			Assert.Null (tv.GetObjectRow (n1_1));
-			Assert.Null (tv.GetObjectRow (n1_2));
-			Assert.Equal (1, tv.GetObjectRow (n2));
-
-			// scroll down 1
-			tv.ScrollOffsetVertical = 1;
-
-			tv.LayoutSubviews ();
-			tv.Draw ();
-
-			TestHelpers.AssertDriverContentsAre (
-@"└─pink
-", output);
-			Assert.Equal (-1, tv.GetObjectRow (n1));
-			Assert.Null (tv.GetObjectRow (n1_1));
-			Assert.Null (tv.GetObjectRow (n1_2));
-			Assert.Equal (0, tv.GetObjectRow (n2));
-		}
-		[Fact, AutoInitShutdown]
-		public void TestTreeViewColor ()
-		{
-			var tv = new TreeView { Width = 20, Height = 10 };
-			tv.BeginInit ();
-			tv.EndInit ();
-			var n1 = new TreeNode ("normal");
-			var n1_1 = new TreeNode ("pink");
-			var n1_2 = new TreeNode ("normal");
-			n1.Children.Add (n1_1);
-			n1.Children.Add (n1_2);
-
-			var n2 = new TreeNode ("pink");
-			tv.AddObject (n1);
-			tv.AddObject (n2);
-			tv.Expand (n1);
-
-			tv.ColorScheme = new ColorScheme ();
-			tv.LayoutSubviews ();
-			tv.Draw ();
-
-			// create a new color scheme
-			var pink = new Attribute (Color.Magenta, Color.Black);
-			var hotpink = new Attribute (Color.BrightMagenta, Color.Black);
-
-			// Normal drawing of the tree view
-			TestHelpers.AssertDriverContentsAre (@"
+", _output);
+		Assert.Equal (0, tv.GetObjectRow (n1));
+		Assert.Null (tv.GetObjectRow (n1_1));
+		Assert.Null (tv.GetObjectRow (n1_2));
+		Assert.Equal (1, tv.GetObjectRow (n2));
+
+		// scroll down 1
+		tv.ScrollOffsetVertical = 1;
+
+		tv.LayoutSubviews ();
+		tv.Draw ();
+
+		TestHelpers.AssertDriverContentsAre (
+			@"└─pink
+", _output);
+		Assert.Equal (-1, tv.GetObjectRow (n1));
+		Assert.Null (tv.GetObjectRow (n1_1));
+		Assert.Null (tv.GetObjectRow (n1_2));
+		Assert.Equal (0, tv.GetObjectRow (n2));
+	}
+
+	[Fact, AutoInitShutdown]
+	public void TestTreeViewColor ()
+	{
+		var tv = new TreeView { Width = 20, Height = 10 };
+		tv.BeginInit ();
+		tv.EndInit ();
+		var n1 = new TreeNode ("normal");
+		var n1_1 = new TreeNode ("pink");
+		var n1_2 = new TreeNode ("normal");
+		n1.Children.Add (n1_1);
+		n1.Children.Add (n1_2);
+
+		var n2 = new TreeNode ("pink");
+		tv.AddObject (n1);
+		tv.AddObject (n2);
+		tv.Expand (n1);
+
+		tv.ColorScheme = new ColorScheme ();
+		tv.LayoutSubviews ();
+		tv.Draw ();
+
+		// create a new color scheme
+		var pink = new Attribute (Color.Magenta, Color.Black);
+		var hotpink = new Attribute (Color.BrightMagenta, Color.Black);
+
+		// Normal drawing of the tree view
+		TestHelpers.AssertDriverContentsAre (@"
 ├-normal
 │ ├─pink
 │ └─normal
 └─pink
-", output);
-			// Should all be the same color
-			TestHelpers.AssertDriverColorsAre (@"
+", _output);
+		// Should all be the same color
+		TestHelpers.AssertDriverColorsAre (@"
 0000000000
 0000000000
 0000000000
 0000000000
-", driver: Application.Driver,
-				new [] { tv.ColorScheme.Normal, pink });
+", Application.Driver, tv.ColorScheme.Normal, pink);
 
-			var pinkScheme = new ColorScheme {
-				Normal = pink,
-				Focus = hotpink
-			};
+		var pinkScheme = new ColorScheme {
+			Normal = pink,
+			Focus = hotpink
+		};
 
-			// and a delegate that uses the pink color scheme 
-			// for nodes "pink"
-			tv.ColorGetter = (n) => n.Text.Equals ("pink") ? pinkScheme : null;
+		// and a delegate that uses the pink color scheme 
+		// for nodes "pink"
+		tv.ColorGetter = n => n.Text.Equals ("pink") ? pinkScheme : null;
 
-			// redraw now that the custom color
-			// delegate is registered
-			tv.Draw ();
+		// redraw now that the custom color
+		// delegate is registered
+		tv.Draw ();
 
-			// Same text
-			TestHelpers.AssertDriverContentsAre (@"
+		// Same text
+		TestHelpers.AssertDriverContentsAre (@"
 ├-normal
 │ ├─pink
 │ └─normal
 └─pink
-", output);
-			// but now the item (only not lines) appear
-			// in pink when they are the word "pink"
-			TestHelpers.AssertDriverColorsAre (@"
+", _output);
+		// but now the item (only not lines) appear
+		// in pink when they are the word "pink"
+		TestHelpers.AssertDriverColorsAre (@"
 00000000
 00001111
 0000000000
 001111
-", driver: Application.Driver,
-				new [] { tv.ColorScheme.Normal, pink });
-		}
+", Application.Driver, tv.ColorScheme.Normal, pink);
+	}
 
-		[Fact, AutoInitShutdown]
-		public void TestBottomlessTreeView_MaxDepth_5 ()
-		{
-			var tv = new TreeView<string> () { Width = 20, Height = 10 };
+	[Fact, AutoInitShutdown]
+	public void TestBottomlessTreeView_MaxDepth_5 ()
+	{
+		var tv = new TreeView<string> { Width = 20, Height = 10 };
 
-			tv.TreeBuilder = new DelegateTreeBuilder<string> (
-				(s) => new [] { (int.Parse (s) + 1).ToString () }
-				);
+		tv.TreeBuilder = new DelegateTreeBuilder<string> (
+			s => new [] { (int.Parse (s) + 1).ToString () }
+		);
 
-			tv.AddObject ("1");
-			tv.ColorScheme = new ColorScheme ();
+		tv.AddObject ("1");
+		tv.ColorScheme = new ColorScheme ();
 
-			tv.LayoutSubviews ();
-			tv.Draw ();
+		tv.LayoutSubviews ();
+		tv.Draw ();
 
-			// Nothing expanded
-			TestHelpers.AssertDriverContentsAre (
-@"└+1
-", output);
-			tv.MaxDepth = 5;
-			tv.ExpandAll ();
+		// Nothing expanded
+		TestHelpers.AssertDriverContentsAre (
+			@"└+1
+", _output);
+		tv.MaxDepth = 5;
+		tv.ExpandAll ();
 
-			tv.Draw ();
+		tv.Draw ();
 
-			// Normal drawing of the tree view
-			TestHelpers.AssertDriverContentsAre (
-@"    
+		// Normal drawing of the tree view
+		TestHelpers.AssertDriverContentsAre (
+			@"    
 └-1
   └-2
     └-3
       └-4
         └-5
           └─6
-", output);
-			Assert.False (tv.CanExpand ("6"));
-			Assert.False (tv.IsExpanded ("6"));
+", _output);
+		Assert.False (tv.CanExpand ("6"));
+		Assert.False (tv.IsExpanded ("6"));
 
-			tv.Collapse ("6");
+		tv.Collapse ("6");
 
-			Assert.False (tv.CanExpand ("6"));
-			Assert.False (tv.IsExpanded ("6"));
+		Assert.False (tv.CanExpand ("6"));
+		Assert.False (tv.IsExpanded ("6"));
 
-			tv.Collapse ("5");
+		tv.Collapse ("5");
 
-			Assert.True (tv.CanExpand ("5"));
-			Assert.False (tv.IsExpanded ("5"));
+		Assert.True (tv.CanExpand ("5"));
+		Assert.False (tv.IsExpanded ("5"));
 
-			tv.Draw ();
+		tv.Draw ();
 
-			// Normal drawing of the tree view
-			TestHelpers.AssertDriverContentsAre (
-@"    
+		// Normal drawing of the tree view
+		TestHelpers.AssertDriverContentsAre (
+			@"    
 └-1
   └-2
     └-3
       └-4
         └+5
-", output);
-		}
-
-		[Fact, AutoInitShutdown]
-		public void TestBottomlessTreeView_MaxDepth_3 ()
-		{
-			var tv = new TreeView<string> () { Width = 20, Height = 10 };
-
-			tv.TreeBuilder = new DelegateTreeBuilder<string> (
-				(s) => new [] { (int.Parse (s) + 1).ToString () }
-				);
-
-			tv.AddObject ("1");
-			tv.ColorScheme = new ColorScheme ();
-
-			tv.LayoutSubviews ();
-			tv.Draw ();
-
-			// Nothing expanded
-			TestHelpers.AssertDriverContentsAre (
-@"└+1
-", output);
-			tv.MaxDepth = 3;
-			tv.ExpandAll ();
-			tv.Draw ();
-
-			// Normal drawing of the tree view
-			TestHelpers.AssertDriverContentsAre (
-@"    
+", _output);
+	}
+
+	[Fact, AutoInitShutdown]
+	public void TestBottomlessTreeView_MaxDepth_3 ()
+	{
+		var tv = new TreeView<string> { Width = 20, Height = 10 };
+
+		tv.TreeBuilder = new DelegateTreeBuilder<string> (
+			s => new [] { (int.Parse (s) + 1).ToString () }
+		);
+
+		tv.AddObject ("1");
+		tv.ColorScheme = new ColorScheme ();
+
+		tv.LayoutSubviews ();
+		tv.Draw ();
+
+		// Nothing expanded
+		TestHelpers.AssertDriverContentsAre (
+			@"└+1
+", _output);
+		tv.MaxDepth = 3;
+		tv.ExpandAll ();
+		tv.Draw ();
+
+		// Normal drawing of the tree view
+		TestHelpers.AssertDriverContentsAre (
+			@"    
 └-1
   └-2
     └-3
       └─4
-", output);
-		}
-		[Fact, AutoInitShutdown]
-		public void TestTreeView_DrawLineEvent ()
-		{
-			var tv = new TreeView { Width = 20, Height = 10 };
-
-			var eventArgs = new List<DrawTreeViewLineEventArgs<ITreeNode>> ();
-
-			tv.DrawLine += (s, e) => {
-				eventArgs.Add (e);
-			};
-
-			var n1 = new TreeNode ("root one");
-			var n1_1 = new TreeNode ("leaf 1");
-			var n1_2 = new TreeNode ("leaf 2");
-			n1.Children.Add (n1_1);
-			n1.Children.Add (n1_2);
-
-			var n2 = new TreeNode ("root two");
-			tv.AddObject (n1);
-			tv.AddObject (n2);
-			tv.Expand (n1);
-
-			tv.ColorScheme = new ColorScheme ();
-			tv.LayoutSubviews ();
-			tv.Draw ();
-
-			// Normal drawing of the tree view
-			TestHelpers.AssertDriverContentsAre (
-@"
+", _output);
+	}
+
+	[Fact, AutoInitShutdown]
+	public void TestTreeView_DrawLineEvent ()
+	{
+		var tv = new TreeView { Width = 20, Height = 10 };
+
+		var eventArgs = new List<DrawTreeViewLineEventArgs<ITreeNode>> ();
+
+		tv.DrawLine += (s, e) => {
+			eventArgs.Add (e);
+		};
+
+		var n1 = new TreeNode ("root one");
+		var n1_1 = new TreeNode ("leaf 1");
+		var n1_2 = new TreeNode ("leaf 2");
+		n1.Children.Add (n1_1);
+		n1.Children.Add (n1_2);
+
+		var n2 = new TreeNode ("root two");
+		tv.AddObject (n1);
+		tv.AddObject (n2);
+		tv.Expand (n1);
+
+		tv.ColorScheme = new ColorScheme ();
+		tv.LayoutSubviews ();
+		tv.Draw ();
+
+		// Normal drawing of the tree view
+		TestHelpers.AssertDriverContentsAre (
+			@"
 ├-root one
 │ ├─leaf 1
 │ └─leaf 2
 └─root two
-", output);
-			Assert.Equal (4, eventArgs.Count ());
-
-			Assert.Equal (0, eventArgs [0].Y);
-			Assert.Equal (1, eventArgs [1].Y);
-			Assert.Equal (2, eventArgs [2].Y);
-			Assert.Equal (3, eventArgs [3].Y);
-
-			Assert.All (eventArgs, ea => Assert.Equal (ea.Tree, tv));
-			Assert.All (eventArgs, ea => Assert.False (ea.Handled));
-
-			Assert.Equal ("├-root one", eventArgs [0].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
-			Assert.Equal ("│ ├─leaf 1", eventArgs [1].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
-			Assert.Equal ("│ └─leaf 2", eventArgs [2].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
-			Assert.Equal ("└─root two", eventArgs [3].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
-
-			Assert.Equal (1, eventArgs [0].IndexOfExpandCollapseSymbol);
-			Assert.Equal (3, eventArgs [1].IndexOfExpandCollapseSymbol);
-			Assert.Equal (3, eventArgs [2].IndexOfExpandCollapseSymbol);
-			Assert.Equal (1, eventArgs [3].IndexOfExpandCollapseSymbol);
-
-			Assert.Equal (2, eventArgs [0].IndexOfModelText);
-			Assert.Equal (4, eventArgs [1].IndexOfModelText);
-			Assert.Equal (4, eventArgs [2].IndexOfModelText);
-			Assert.Equal (2, eventArgs [3].IndexOfModelText);
-
-
-			Assert.Equal ("root one", eventArgs [0].Model.Text);
-			Assert.Equal ("leaf 1", eventArgs [1].Model.Text);
-			Assert.Equal ("leaf 2", eventArgs [2].Model.Text);
-			Assert.Equal ("root two", eventArgs [3].Model.Text);
-		}
-
-		[Fact, AutoInitShutdown]
-		public void TestTreeView_DrawLineEvent_WithScrolling ()
-		{
-			var tv = new TreeView { Width = 20, Height = 10 };
-
-			var eventArgs = new List<DrawTreeViewLineEventArgs<ITreeNode>> ();
-
-			tv.DrawLine += (s, e) => {
-				eventArgs.Add (e);
-			};
-
-			tv.ScrollOffsetHorizontal = 3;
-			tv.ScrollOffsetVertical = 1;
-
-			var n1 = new TreeNode ("root one");
-			var n1_1 = new TreeNode ("leaf 1");
-			var n1_2 = new TreeNode ("leaf 2");
-			n1.Children.Add (n1_1);
-			n1.Children.Add (n1_2);
-
-			var n2 = new TreeNode ("root two");
-			tv.AddObject (n1);
-			tv.AddObject (n2);
-			tv.Expand (n1);
-
-			tv.ColorScheme = new ColorScheme ();
-			tv.LayoutSubviews ();
-			tv.Draw ();
-
-			// Normal drawing of the tree view
-			TestHelpers.AssertDriverContentsAre (
-@"
+", _output);
+		Assert.Equal (4, eventArgs.Count ());
+
+		Assert.Equal (0, eventArgs [0].Y);
+		Assert.Equal (1, eventArgs [1].Y);
+		Assert.Equal (2, eventArgs [2].Y);
+		Assert.Equal (3, eventArgs [3].Y);
+
+		Assert.All (eventArgs, ea => Assert.Equal (ea.Tree, tv));
+		Assert.All (eventArgs, ea => Assert.False (ea.Handled));
+
+		Assert.Equal ("├-root one", eventArgs [0].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
+		Assert.Equal ("│ ├─leaf 1", eventArgs [1].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
+		Assert.Equal ("│ └─leaf 2", eventArgs [2].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
+		Assert.Equal ("└─root two", eventArgs [3].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
+
+		Assert.Equal (1, eventArgs [0].IndexOfExpandCollapseSymbol);
+		Assert.Equal (3, eventArgs [1].IndexOfExpandCollapseSymbol);
+		Assert.Equal (3, eventArgs [2].IndexOfExpandCollapseSymbol);
+		Assert.Equal (1, eventArgs [3].IndexOfExpandCollapseSymbol);
+
+		Assert.Equal (2, eventArgs [0].IndexOfModelText);
+		Assert.Equal (4, eventArgs [1].IndexOfModelText);
+		Assert.Equal (4, eventArgs [2].IndexOfModelText);
+		Assert.Equal (2, eventArgs [3].IndexOfModelText);
+
+
+		Assert.Equal ("root one", eventArgs [0].Model.Text);
+		Assert.Equal ("leaf 1", eventArgs [1].Model.Text);
+		Assert.Equal ("leaf 2", eventArgs [2].Model.Text);
+		Assert.Equal ("root two", eventArgs [3].Model.Text);
+	}
+
+	[Fact, AutoInitShutdown]
+	public void TestTreeView_DrawLineEvent_WithScrolling ()
+	{
+		var tv = new TreeView { Width = 20, Height = 10 };
+
+		var eventArgs = new List<DrawTreeViewLineEventArgs<ITreeNode>> ();
+
+		tv.DrawLine += (s, e) => {
+			eventArgs.Add (e);
+		};
+
+		tv.ScrollOffsetHorizontal = 3;
+		tv.ScrollOffsetVertical = 1;
+
+		var n1 = new TreeNode ("root one");
+		var n1_1 = new TreeNode ("leaf 1");
+		var n1_2 = new TreeNode ("leaf 2");
+		n1.Children.Add (n1_1);
+		n1.Children.Add (n1_2);
+
+		var n2 = new TreeNode ("root two");
+		tv.AddObject (n1);
+		tv.AddObject (n2);
+		tv.Expand (n1);
+
+		tv.ColorScheme = new ColorScheme ();
+		tv.LayoutSubviews ();
+		tv.Draw ();
+
+		// Normal drawing of the tree view
+		TestHelpers.AssertDriverContentsAre (
+			@"
 ─leaf 1
 ─leaf 2
 oot two
-", output);
-			Assert.Equal (3, eventArgs.Count ());
+", _output);
+		Assert.Equal (3, eventArgs.Count ());
 
-			Assert.Equal (0, eventArgs [0].Y);
-			Assert.Equal (1, eventArgs [1].Y);
-			Assert.Equal (2, eventArgs [2].Y);
+		Assert.Equal (0, eventArgs [0].Y);
+		Assert.Equal (1, eventArgs [1].Y);
+		Assert.Equal (2, eventArgs [2].Y);
 
-			Assert.All (eventArgs, ea => Assert.Equal (ea.Tree, tv));
-			Assert.All (eventArgs, ea => Assert.False (ea.Handled));
+		Assert.All (eventArgs, ea => Assert.Equal (ea.Tree, tv));
+		Assert.All (eventArgs, ea => Assert.False (ea.Handled));
 
-			Assert.Equal ("─leaf 1", eventArgs [0].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
-			Assert.Equal ("─leaf 2", eventArgs [1].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
-			Assert.Equal ("oot two", eventArgs [2].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
+		Assert.Equal ("─leaf 1", eventArgs [0].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
+		Assert.Equal ("─leaf 2", eventArgs [1].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
+		Assert.Equal ("oot two", eventArgs [2].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
 
-			Assert.Equal (0, eventArgs [0].IndexOfExpandCollapseSymbol);
-			Assert.Equal (0, eventArgs [1].IndexOfExpandCollapseSymbol);
-			Assert.Null (eventArgs [2].IndexOfExpandCollapseSymbol);
+		Assert.Equal (0, eventArgs [0].IndexOfExpandCollapseSymbol);
+		Assert.Equal (0, eventArgs [1].IndexOfExpandCollapseSymbol);
+		Assert.Null (eventArgs [2].IndexOfExpandCollapseSymbol);
 
-			Assert.Equal (1, eventArgs [0].IndexOfModelText);
-			Assert.Equal (1, eventArgs [1].IndexOfModelText);
-			Assert.Equal (-1, eventArgs [2].IndexOfModelText);
+		Assert.Equal (1, eventArgs [0].IndexOfModelText);
+		Assert.Equal (1, eventArgs [1].IndexOfModelText);
+		Assert.Equal (-1, eventArgs [2].IndexOfModelText);
 
-			Assert.Equal ("leaf 1", eventArgs [0].Model.Text);
-			Assert.Equal ("leaf 2", eventArgs [1].Model.Text);
-			Assert.Equal ("root two", eventArgs [2].Model.Text);
-		}
+		Assert.Equal ("leaf 1", eventArgs [0].Model.Text);
+		Assert.Equal ("leaf 2", eventArgs [1].Model.Text);
+		Assert.Equal ("root two", eventArgs [2].Model.Text);
+	}
 
-		[Fact, AutoInitShutdown]
-		public void TestTreeView_DrawLineEvent_Handled ()
-		{
-			var tv = new TreeView { Width = 20, Height = 10 };
+	[Fact, AutoInitShutdown]
+	public void TestTreeView_DrawLineEvent_Handled ()
+	{
+		var tv = new TreeView { Width = 20, Height = 10 };
 
-			tv.DrawLine += (s, e) => {
-				if(e.Model.Text.Equals("leaf 1")) {
-					e.Handled = true;
+		tv.DrawLine += (s, e) => {
+			if (e.Model.Text.Equals ("leaf 1")) {
+				e.Handled = true;
 
-					for (int i = 0; i < 10; i++) {
+				for (var i = 0; i < 10; i++) {
 
-						e.Tree.AddRune (i,e.Y,new System.Text.Rune('F'));
-					}
+					e.Tree.AddRune (i, e.Y, new Rune ('F'));
 				}
-			};
-
-			var n1 = new TreeNode ("root one");
-			var n1_1 = new TreeNode ("leaf 1");
-			var n1_2 = new TreeNode ("leaf 2");
-			n1.Children.Add (n1_1);
-			n1.Children.Add (n1_2);
-
-			var n2 = new TreeNode ("root two");
-			tv.AddObject (n1);
-			tv.AddObject (n2);
-			tv.Expand (n1);
-
-			tv.ColorScheme = new ColorScheme ();
-			tv.LayoutSubviews ();
-			tv.Draw ();
-
-			// Normal drawing of the tree view
-			TestHelpers.AssertDriverContentsAre (
-@"
+			}
+		};
+
+		var n1 = new TreeNode ("root one");
+		var n1_1 = new TreeNode ("leaf 1");
+		var n1_2 = new TreeNode ("leaf 2");
+		n1.Children.Add (n1_1);
+		n1.Children.Add (n1_2);
+
+		var n2 = new TreeNode ("root two");
+		tv.AddObject (n1);
+		tv.AddObject (n2);
+		tv.Expand (n1);
+
+		tv.ColorScheme = new ColorScheme ();
+		tv.LayoutSubviews ();
+		tv.Draw ();
+
+		// Normal drawing of the tree view
+		TestHelpers.AssertDriverContentsAre (
+			@"
 ├-root one
 FFFFFFFFFF
 │ └─leaf 2
 └─root two
-", output);
-		}
-
-		[Fact, AutoInitShutdown]
-		public void TestTreeView_Filter ()
-		{
-			var tv = new TreeView { Width = 20, Height = 10 };
-
-			var n1 = new TreeNode ("root one");
-			var n1_1 = new TreeNode ("leaf 1");
-			var n1_2 = new TreeNode ("leaf 2");
-			n1.Children.Add (n1_1);
-			n1.Children.Add (n1_2);
-
-			var n2 = new TreeNode ("root two");
-			tv.AddObject (n1);
-			tv.AddObject (n2);
-			tv.Expand (n1);
-
-			tv.ColorScheme = new ColorScheme ();
-			tv.LayoutSubviews ();
-			tv.Draw ();
-
-			// Normal drawing of the tree view
-			TestHelpers.AssertDriverContentsAre (
-@"
+", _output);
+	}
+
+	[Fact, AutoInitShutdown]
+	public void TestTreeView_Filter ()
+	{
+		var tv = new TreeView { Width = 20, Height = 10 };
+
+		var n1 = new TreeNode ("root one");
+		var n1_1 = new TreeNode ("leaf 1");
+		var n1_2 = new TreeNode ("leaf 2");
+		n1.Children.Add (n1_1);
+		n1.Children.Add (n1_2);
+
+		var n2 = new TreeNode ("root two");
+		tv.AddObject (n1);
+		tv.AddObject (n2);
+		tv.Expand (n1);
+
+		tv.ColorScheme = new ColorScheme ();
+		tv.LayoutSubviews ();
+		tv.Draw ();
+
+		// Normal drawing of the tree view
+		TestHelpers.AssertDriverContentsAre (
+			@"
 ├-root one
 │ ├─leaf 1
 │ └─leaf 2
 └─root two
-", output);
-			var filter = new TreeViewTextFilter<ITreeNode> (tv);
-			tv.Filter = filter;
-
-			// matches nothing
-			filter.Text = "asdfjhasdf";
-			tv.Draw ();
-			// Normal drawing of the tree view
-			TestHelpers.AssertDriverContentsAre (
-@"", output);
-
-
-			// Matches everything
-			filter.Text = "root";
-			tv.Draw ();
-			TestHelpers.AssertDriverContentsAre (
-@"
+", _output);
+		var filter = new TreeViewTextFilter<ITreeNode> (tv);
+		tv.Filter = filter;
+
+		// matches nothing
+		filter.Text = "asdfjhasdf";
+		tv.Draw ();
+		// Normal drawing of the tree view
+		TestHelpers.AssertDriverContentsAre (
+			@"", _output);
+
+
+		// Matches everything
+		filter.Text = "root";
+		tv.Draw ();
+		TestHelpers.AssertDriverContentsAre (
+			@"
 ├-root one
 │ ├─leaf 1
 │ └─leaf 2
 └─root two
-", output);
-			// Matches 2 leaf nodes
-			filter.Text = "leaf";
-			tv.Draw ();
-			TestHelpers.AssertDriverContentsAre (
-@"
+", _output);
+		// Matches 2 leaf nodes
+		filter.Text = "leaf";
+		tv.Draw ();
+		TestHelpers.AssertDriverContentsAre (
+			@"
 ├-root one
 │ ├─leaf 1
 │ └─leaf 2
-", output);
+", _output);
 
-			// Matches 1 leaf nodes
-			filter.Text = "leaf 1";
-			tv.Draw ();
-			TestHelpers.AssertDriverContentsAre (
-@"
+		// Matches 1 leaf nodes
+		filter.Text = "leaf 1";
+		tv.Draw ();
+		TestHelpers.AssertDriverContentsAre (
+			@"
 ├-root one
 │ ├─leaf 1
-", output);
-		}
-
-		[Fact, AutoInitShutdown]
-		public void DesiredCursorVisibility_MultiSelect ()
-		{
-			var tv = new TreeView { Width = 20, Height = 10 };
-
-			var n1 = new TreeNode ("normal");
-			var n2 = new TreeNode ("pink");
-			tv.AddObject (n1);
-			tv.AddObject (n2);
-
-			Application.Top.Add (tv);
-			Application.Begin (Application.Top);
-
-			Assert.True (tv.MultiSelect);
-			Assert.True (tv.HasFocus);
-			Assert.Equal (CursorVisibility.Invisible, tv.DesiredCursorVisibility);
-
-			tv.SelectAll ();
-			tv.DesiredCursorVisibility = CursorVisibility.Default;
-			Application.Refresh ();
-			Application.Driver.GetCursorVisibility (out CursorVisibility visibility);
-			Assert.Equal (CursorVisibility.Default, tv.DesiredCursorVisibility);
-			Assert.Equal (CursorVisibility.Default, visibility);
-		}
-
-		/// <summary>
-		/// Test object which considers for equality only <see cref="Name"/>
-		/// </summary>
-		private class EqualityTestObject {
-			public string Name { get; set; }
-			public int Age { get; set; }
-
-			public override int GetHashCode ()
-			{
-				return Name?.GetHashCode () ?? base.GetHashCode ();
-			}
-			public override bool Equals (object obj)
-			{
-				return obj is EqualityTestObject eto && Equals (Name, eto.Name);
-			}
-		}
-
-		private void InitFakeDriver ()
-		{
-			var driver = new FakeDriver ();
-			Application.Init (driver);
-			driver.Init ();
-		}
+", _output);
+	}
+
+	[Fact, AutoInitShutdown]
+	public void DesiredCursorVisibility_MultiSelect ()
+	{
+		var tv = new TreeView { Width = 20, Height = 10 };
+
+		var n1 = new TreeNode ("normal");
+		var n2 = new TreeNode ("pink");
+		tv.AddObject (n1);
+		tv.AddObject (n2);
+
+		Application.Top.Add (tv);
+		Application.Begin (Application.Top);
+
+		Assert.True (tv.MultiSelect);
+		Assert.True (tv.HasFocus);
+		Assert.Equal (CursorVisibility.Invisible, tv.DesiredCursorVisibility);
+
+		tv.SelectAll ();
+		tv.DesiredCursorVisibility = CursorVisibility.Default;
+		Application.Refresh ();
+		Application.Driver.GetCursorVisibility (out var visibility);
+		Assert.Equal (CursorVisibility.Default, tv.DesiredCursorVisibility);
+		Assert.Equal (CursorVisibility.Default, visibility);
+	}
+
+	void InitFakeDriver ()
+	{
+		var driver = new FakeDriver ();
+		Application.Init (driver);
+		driver.Init ();
+	}
+
+	/// <summary>
+	/// Test object which considers for equality only <see cref="Name"/>
+	/// </summary>
+	class EqualityTestObject {
+		public string Name { get; set; }
+
+		public int Age { get; set; }
+
+		public override int GetHashCode () => Name?.GetHashCode () ?? base.GetHashCode ();
+
+		public override bool Equals (object obj) => obj is EqualityTestObject eto && Equals (Name, eto.Name);
+	}
+
+	#region Test Setup Methods
+	class Factory {
+		public Car [] Cars { get; set; }
+
+		public override string ToString () => "Factory";
+	}
+
+	class Car {
+		public string Name { get; set; }
+
+		public override string ToString () => Name;
+	}
+
+	TreeView<object> CreateTree () => CreateTree (out _, out _, out _);
+
+	TreeView<object> CreateTree (out Factory factory1, out Car car1, out Car car2)
+	{
+		car1 = new Car ();
+		car2 = new Car ();
+
+		factory1 = new Factory {
+			Cars = new [] { car1, car2 }
+		};
+
+		var tree = new TreeView<object> (new DelegateTreeBuilder<object> (s => s is Factory f ? f.Cars : null));
+		tree.AddObject (factory1);
+
+		return tree;
 	}
-}
+	#endregion
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно