Browse Source

Merge branch 'refs/heads/property-registry' into xaml-control-themes

Conflicts:
	tests/Perspex.Controls.UnitTests/TreeViewTests.cs
Steven Kirk 10 years ago
parent
commit
88f155cdaa
42 changed files with 1413 additions and 602 deletions
  1. 2 4
      nuget/build-appveyor.ps1
  2. 6 5
      nuget/build-version.ps1
  3. 29 0
      nuget/template/Perspex.Desktop.nuspec
  4. 3 2
      nuget/template/Perspex.nuspec
  5. 0 11
      nuget/template/build/net45/perspex.targets
  6. 1 1
      src/Markup/Perspex.Markup.Xaml/Binding/XamlTemplateBinding.cs
  7. 2 2
      src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs
  8. 2 2
      src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyTypeConverter.cs
  9. 1 0
      src/Perspex.Base/Perspex.Base.csproj
  10. 12 160
      src/Perspex.Base/PerspexObject.cs
  11. 0 44
      src/Perspex.Base/PerspexObjectExtensions.cs
  12. 57 8
      src/Perspex.Base/PerspexProperty.cs
  13. 255 0
      src/Perspex.Base/PerspexPropertyRegistry.cs
  14. 29 5
      src/Perspex.Base/PerspexProperty`1.cs
  15. 5 1
      src/Perspex.Controls/Control.cs
  16. 1 1
      src/Perspex.Controls/DropDown.cs
  17. 18 7
      src/Perspex.Controls/Generators/IItemContainerGenerator.cs
  18. 11 11
      src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs
  19. 54 62
      src/Perspex.Controls/Generators/ItemContainerGenerator.cs
  20. 15 3
      src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs
  21. 106 154
      src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs
  22. 2 1
      src/Perspex.Controls/ListBox.cs
  23. 2 2
      src/Perspex.Controls/Presenters/CarouselPresenter.cs
  24. 28 6
      src/Perspex.Controls/Presenters/ItemsPresenter.cs
  25. 6 6
      src/Perspex.Controls/Primitives/SelectingItemsControl.cs
  26. 1 1
      src/Perspex.Controls/Primitives/TabStrip.cs
  27. 147 25
      src/Perspex.Controls/TreeView.cs
  28. 15 24
      src/Perspex.Controls/TreeViewItem.cs
  29. 1 1
      src/Perspex.Diagnostics/Debug.cs
  30. 1 1
      src/Perspex.Diagnostics/ViewModels/ControlDetailsViewModel.cs
  31. 6 0
      src/Perspex.Layout/Layoutable.cs
  32. 10 0
      src/Perspex.Themes.Default/TreeViewItemStyle.cs
  33. 1 0
      tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj
  34. 0 24
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs
  35. 177 0
      tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs
  36. 18 0
      tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs
  37. 93 0
      tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs
  38. 28 0
      tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs
  39. 2 0
      tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
  40. 1 1
      tests/Perspex.Controls.UnitTests/Presenters/CarouselPresenterTests.cs
  41. 70 2
      tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
  42. 195 25
      tests/Perspex.Controls.UnitTests/TreeViewTests.cs

+ 2 - 4
nuget/build-appveyor.ps1

@@ -9,11 +9,8 @@ sv version $env:APPVEYOR_BUILD_NUMBER
 sv version 9999.0.$version-nightly
 sv key $env:myget_key
 
-sv file Perspex.$version.nupkg
-
 .\build-version.ps1 $version
 
-
 sv reponame $env:APPVEYOR_REPO_NAME
 sv repobranch $env:APPVEYOR_REPO_BRANCH
 sv pullreq $env:APPVEYOR_PULL_REQUEST_NUMBER
@@ -26,7 +23,8 @@ if ($reponame -eq "Perspex/Perspex")
     if($repobranch -eq "master")
     {
         echo Repo branch matched
-        nuget.exe push $file $key -Source https://www.myget.org/F/perspex-nightly/api/v2/package
+        nuget.exe push Perspex.$version.nupkg $key -Source https://www.myget.org/F/perspex-nightly/api/v2/package
+		nuget.exe push Perspex.Desktop.$version.nupkg $key -Source https://www.myget.org/F/perspex-nightly/api/v2/package
     }
 }
 

+ 6 - 5
nuget/build-version.ps1

@@ -2,7 +2,8 @@ rm -Force -Recurse .\Perspex -ErrorAction SilentlyContinue
 rm -Force -Recurse *.nupkg -ErrorAction SilentlyContinue
 Copy-Item template Perspex -Recurse
 sv lib "Perspex\lib\portable-windows8+net45"
-sv build "Perspex\build\net45"
+sv build "Perspex.Desktop\lib\net45"
+
 mkdir $lib -ErrorAction SilentlyContinue
 mkdir $build -ErrorAction SilentlyContinue
 
@@ -36,14 +37,14 @@ Copy-Item ..\src\Perspex.HtmlRenderer\bin\Release\Perspex.HtmlRenderer.dll $lib
 Copy-Item ..\src\Perspex.ReactiveUI\bin\Release\Perspex.ReactiveUI.dll $lib
 
 Copy-Item ..\src\Windows\Perspex.Direct2D1\bin\Release\Perspex.Direct2D1.dll $build
-Copy-Item ..\src\Windows\Perspex.Direct2D1\bin\Release\SharpDX.dll $build
-Copy-Item ..\src\Windows\Perspex.Direct2D1\bin\Release\SharpDX.Direct2D1.dll $build
-Copy-Item ..\src\Windows\Perspex.Direct2D1\bin\Release\SharpDX.DXGI.dll $build
 Copy-Item ..\src\Windows\Perspex.Win32\bin\Release\Perspex.Win32.dll $build
 Copy-Item ..\src\Gtk\Perspex.Gtk\bin\Release\Perspex.Gtk.dll $build
 Copy-Item ..\src\Gtk\Perspex.Cairo\bin\Release\Perspex.Cairo.dll $build
 
 (gc Perspex\Perspex.nuspec).replace('#VERSION#', $args[0]) | sc Perspex\Perspex.nuspec
+(gc Perspex\Perspex.Desktop.nuspec).replace('#VERSION#', $args[0]) | sc Perspex.Desktop\Perspex.Desktop.nuspec
 
 nuget.exe pack Perspex\Perspex.nuspec
-rm -Force -Recurse .\Perspex
+nuget.exe pack Perspex.Desktop\Perspex.Desktop.nuspec
+rm -Force -Recurse .\Perspex
+rm -Force -Recurse .\Perspex.Desktop

+ 29 - 0
nuget/template/Perspex.Desktop.nuspec

@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<package>
+  <metadata>
+    <id>Perspex.Desktop</id>
+    <version>#VERSION#</version>
+    <authors>Perspex Team</authors>
+    <owners>stevenk</owners>
+    <licenseUrl>http://opensource.org/licenses/MIT</licenseUrl>
+    <projectUrl>https://github.com/Perspex/Perspex/</projectUrl>
+    <requireLicenseAcceptance>false</requireLicenseAcceptance>
+    <description>The Perspex UI framework</description>
+    <releaseNotes></releaseNotes>
+    <copyright>Copyright 2015</copyright>
+    <tags>Perspex</tags>
+    <dependencies>
+      <dependency id="Serilog" version="1.5.9" />
+      <dependency id="Splat" version="1.6.2" />
+      <dependency id="Sprache" version="2.0.0.47" />
+      <dependency id="Rx-Core" version="2.2.5" />
+      <dependency id="Rx-Interfaces" version="2.2.5" />
+      <dependency id="Rx-Linq" version="2.2.5" />
+      <dependency id="Rx-Main" version="2.2.5" />
+      <dependency id="Rx-PlatformServices" version="2.2.5" />
+      <dependency id="SharpDX" version="2.6.3"/>
+      <dependency id="SharpDX.Direct2D1" version="2.6.3"/>
+      <dependency id="SharpDX.DXGI" version="2.6.3"/>
+    </dependencies>
+  </metadata>
+</package>

+ 3 - 2
nuget/template/Perspex.nuspec

@@ -3,10 +3,10 @@
   <metadata>
     <id>Perspex</id>
     <version>#VERSION#</version>
-    <authors>stevenk</authors>
+    <authors>Perspex Team</authors>
     <owners>stevenk</owners>
     <licenseUrl>http://opensource.org/licenses/MIT</licenseUrl>
-    <projectUrl>https://github.com/grokys/Perspex/</projectUrl>
+    <projectUrl>https://github.com/Perspex/Perspex/</projectUrl>
     <requireLicenseAcceptance>false</requireLicenseAcceptance>
     <description>The Perspex UI framework</description>
     <releaseNotes>Initial alpha release.</releaseNotes>
@@ -21,6 +21,7 @@
       <dependency id="Rx-Linq" version="2.2.5" />
       <dependency id="Rx-Main" version="2.2.5" />
       <dependency id="Rx-PlatformServices" version="2.2.5" />
+	  <dependency id="Perspex" version="#VERSION#" />
     </dependencies>
   </metadata>
 </package>

+ 0 - 11
nuget/template/build/net45/perspex.targets

@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <ItemGroup Condition="'$(MSBuildThisFileDirectory)' != '' And HasTrailingSlash('$(MSBuildThisFileDirectory)')">
-    <PlatformLibs Include="$(MSBuildThisFileDirectory)**\*.dll" />
-    <Content Include="@(PlatformLibs)">
-      <Link>%(RecursiveDir)%(FileName)%(Extension)</Link>
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-</Project>

+ 1 - 1
src/Markup/Perspex.Markup.Xaml/Binding/XamlTemplateBinding.cs

@@ -40,7 +40,7 @@ namespace Perspex.Markup.Xaml.Binding
             PerspexProperty targetProperty,
             PerspexObject templatedParent)
         {
-            var sourceProperty = templatedParent.FindRegistered(SourcePropertyPath);
+            var sourceProperty = PerspexPropertyRegistry.Instance.FindRegistered(instance.GetType(), SourcePropertyPath);
 
             if (sourceProperty == null)
             {

+ 2 - 2
src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs

@@ -88,7 +88,7 @@ namespace Perspex.Markup.Xaml.Context
                 if (attached == null)
                 {
                     propertyName = _xamlMember.Name;
-                    property = perspexObject.GetRegisteredProperties()
+                    property = PerspexPropertyRegistry.Instance.GetRegistered(perspexObject)
                         .FirstOrDefault(x => x.Name == propertyName);
                 }
                 else
@@ -98,7 +98,7 @@ namespace Perspex.Markup.Xaml.Context
 
                     propertyName = attached.DeclaringType.UnderlyingType.Name + '.' + _xamlMember.Name;
 
-                    property = perspexObject.GetRegisteredProperties()
+                    property = PerspexPropertyRegistry.Instance.GetRegistered(perspexObject)
                         .Where(x => x.IsAttached && x.OwnerType == attached.DeclaringType.UnderlyingType)
                         .FirstOrDefault(x => x.Name == _xamlMember.Name);
                 }

+ 2 - 2
src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyTypeConverter.cs

@@ -46,12 +46,12 @@ namespace Perspex.Markup.Xaml.Converters
             }
 
             // First look for non-attached property on the type and then look for an attached property.
-            var property = PerspexObject.GetRegisteredProperties(type)
+            var property = PerspexPropertyRegistry.Instance.GetRegistered(type)
                 .FirstOrDefault(x => x.Name == propertyName);
 
             if (property == null)
             {
-                property = PerspexObject.GetAttachedProperties(type)
+                property = PerspexPropertyRegistry.Instance.GetAttached(type)
                     .FirstOrDefault(x => x.Name == propertyName);
             }
 

+ 1 - 0
src/Perspex.Base/Perspex.Base.csproj

@@ -50,6 +50,7 @@
     <Compile Include="PerspexLocator.cs" />
     <Compile Include="Metadata\XmlnsDefinitionAttribute.cs" />
     <Compile Include="PerspexObjectExtensions.cs" />
+    <Compile Include="PerspexPropertyRegistry.cs" />
     <Compile Include="PerspexProperty`1.cs" />
     <Compile Include="Platform\IPclPlatformWrapper.cs" />
     <Compile Include="BindingPriority.cs" />

+ 12 - 160
src/Perspex.Base/PerspexObject.cs

@@ -25,18 +25,6 @@ namespace Perspex
     /// </remarks>
     public class PerspexObject : IObservablePropertyBag, INotifyPropertyChanged
     {
-        /// <summary>
-        /// The registered properties by type.
-        /// </summary>
-        private static readonly Dictionary<Type, List<PerspexProperty>> s_registered =
-            new Dictionary<Type, List<PerspexProperty>>();
-
-        /// <summary>
-        /// The registered attached properties by owner type.
-        /// </summary>
-        private static readonly Dictionary<Type, List<PerspexProperty>> s_attached =
-            new Dictionary<Type, List<PerspexProperty>>();
-
         /// <summary>
         /// The parent object that inherited values are inherited from.
         /// </summary>
@@ -70,7 +58,7 @@ namespace Perspex
                 new PropertyEnricher("Id", GetHashCode()),
             });
 
-            foreach (var property in GetRegisteredProperties())
+            foreach (var property in PerspexPropertyRegistry.Instance.GetRegistered(this))
             {
                 object value = property.IsDirect ? 
                     property.Getter(this) : 
@@ -129,7 +117,7 @@ namespace Perspex
                         _inheritanceParent.PropertyChanged -= ParentPropertyChanged;
                     }
 
-                    var inherited = (from property in GetRegisteredProperties(GetType())
+                    var inherited = (from property in PerspexPropertyRegistry.Instance.GetRegistered(this)
                                      where property.Inherits
                                      select new
                                      {
@@ -215,92 +203,6 @@ namespace Perspex
             }
         }
 
-        /// <summary>
-        /// Gets all <see cref="PerspexProperty"/>s registered on a type.
-        /// </summary>
-        /// <param name="type">The type.</param>
-        /// <returns>A collection of <see cref="PerspexProperty"/> definitions.</returns>
-        public static IEnumerable<PerspexProperty> GetRegisteredProperties(Type type)
-        {
-            Contract.Requires<ArgumentNullException>(type != null);
-
-            TypeInfo i = type.GetTypeInfo();
-
-            while (type != null)
-            {
-                List<PerspexProperty> list;
-
-                if (s_registered.TryGetValue(type, out list))
-                {
-                    foreach (PerspexProperty p in list)
-                    {
-                        yield return p;
-                    }
-                }
-
-                type = type.GetTypeInfo().BaseType;
-            }
-        }
-
-        /// <summary>
-        /// Gets all attached <see cref="PerspexProperty"/>s registered by an owner.
-        /// </summary>
-        /// <param name="ownerType">The owner type.</param>
-        /// <returns>A collection of <see cref="PerspexProperty"/> definitions.</returns>
-        public static IEnumerable<PerspexProperty> GetAttachedProperties(Type ownerType)
-        {
-            List<PerspexProperty> list;
-
-            if (s_attached.TryGetValue(ownerType, out list))
-            {
-                return list;
-            }
-
-            return Enumerable.Empty<PerspexProperty>();
-        }
-
-        /// <summary>
-        /// Registers a <see cref="PerspexProperty"/> on a type.
-        /// </summary>
-        /// <param name="type">The type.</param>
-        /// <param name="property">The property.</param>
-        /// <remarks>
-        /// You won't usually want to call this method directly, instead use the
-        /// <see cref="PerspexProperty.Register"/> method.
-        /// </remarks>
-        public static void Register(Type type, PerspexProperty property)
-        {
-            Contract.Requires<ArgumentNullException>(type != null);
-            Contract.Requires<ArgumentNullException>(property != null);
-
-            List<PerspexProperty> list;
-
-            if (!s_registered.TryGetValue(type, out list))
-            {
-                list = new List<PerspexProperty>();
-                s_registered.Add(type, list);
-            }
-
-            if (!list.Contains(property))
-            {
-                list.Add(property);
-            }
-
-            if (property.IsAttached)
-            {
-                if (!s_attached.TryGetValue(property.OwnerType, out list))
-                {
-                    list = new List<PerspexProperty>();
-                    s_attached.Add(property.OwnerType, list);
-                }
-
-                if (!list.Contains(property))
-                {
-                    list.Add(property);
-                }
-            }
-        }
-
         public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();
 
         public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();
@@ -409,7 +311,7 @@ namespace Perspex
                 object result = PerspexProperty.UnsetValue;
                 PriorityValue value;
 
-                if (!IsRegistered(property))
+                if (!PerspexPropertyRegistry.Instance.IsRegistered(this, property))
                 {
                     ThrowNotRegistered(property);
                 }
@@ -448,17 +350,6 @@ namespace Perspex
             }
         }
 
-        /// <summary>
-        /// Gets all properties that are registered on this object.
-        /// </summary>
-        /// <returns>
-        /// A collection of <see cref="PerspexProperty"/> objects.
-        /// </returns>
-        public IEnumerable<PerspexProperty> GetRegisteredProperties()
-        {
-            return GetRegisteredProperties(GetType());
-        }
-
         /// <summary>
         /// Checks whether a <see cref="PerspexProperty"/> is set on this object.
         /// </summary>
@@ -478,16 +369,6 @@ namespace Perspex
             return false;
         }
 
-        /// <summary>
-        /// Checks whether a <see cref="PerspexProperty"/> is registered on this class.
-        /// </summary>
-        /// <param name="property">The property.</param>
-        /// <returns>True if the property is registered, otherwise false.</returns>
-        public bool IsRegistered(PerspexProperty property)
-        {
-            return FindRegistered(property) != null;
-        }
-
         /// <summary>
         /// Sets a <see cref="PerspexProperty"/> value.
         /// </summary>
@@ -519,7 +400,7 @@ namespace Perspex
                 PriorityValue v;
                 var originalValue = value;
 
-                if (!IsRegistered(property))
+                if (!PerspexPropertyRegistry.Instance.IsRegistered(this, property))
                 {
                     ThrowNotRegistered(property);
                 }
@@ -620,7 +501,7 @@ namespace Perspex
             {
                 PriorityValue v;
 
-                if (!IsRegistered(property))
+                if (!PerspexPropertyRegistry.Instance.IsRegistered(this, property))
                 {
                     ThrowNotRegistered(property);
                 }
@@ -751,6 +632,12 @@ namespace Perspex
             }
         }
 
+        /// <inheritdoc/>
+        bool IPropertyBag.IsRegistered(PerspexProperty property)
+        {
+            return PerspexPropertyRegistry.Instance.IsRegistered(this, property);
+        }
+
         /// <summary>
         /// Gets all priority values set on the object.
         /// </summary>
@@ -936,41 +823,6 @@ namespace Perspex
             }
         }
 
-        /// <summary>
-        /// Given a <see cref="PerspexProperty"/> returns a registered perspex property that is
-        /// equal.
-        /// </summary>
-        /// <param name="property">The property.</param>
-        /// <returns>The registered property or null if not found.</returns>
-        /// <remarks>
-        /// Calling AddOwner on a direct PerspexProperty creates new new PerspexProperty with
-        /// an overridden getter and setter. This property is a different object but is equal
-        /// according to <see cref="object.Equals(object)"/>.
-        /// </remarks>
-        public PerspexProperty FindRegistered(PerspexProperty property)
-        {
-            Type type = GetType();
-
-            while (type != null)
-            {
-                List<PerspexProperty> list;
-
-                if (s_registered.TryGetValue(type, out list))
-                {
-                    var index = list.IndexOf(property);
-
-                    if (index != -1)
-                    {
-                        return list[index];
-                    }
-                }
-
-                type = type.GetTypeInfo().BaseType;
-            }
-
-            return null;
-        }
-
         /// <summary>
         /// Given a <see cref="PerspexProperty"/> returns a registered perspex property that is
         /// equal or throws if not found.
@@ -979,7 +831,7 @@ namespace Perspex
         /// <returns>The registered property.</returns>
         public PerspexProperty GetRegistered(PerspexProperty property)
         {
-            var result = FindRegistered(property);
+            var result = PerspexPropertyRegistry.Instance.FindRegistered(this, property);
 
             if (result == null)
             {

+ 0 - 44
src/Perspex.Base/PerspexObjectExtensions.cs

@@ -52,50 +52,6 @@ namespace Perspex
             return observable.Subscribe(e => SubscribeAdapter(e, handler));
         }
 
-        /// <summary>
-        /// Finds a registered property on a <see cref="PerspexObject"/> by name.
-        /// </summary>
-        /// <param name="o">The object.</param>
-        /// <param name="name">
-        /// The property name. If an attached property it should be in the form 
-        /// "OwnerType.PropertyName".
-        /// </param>
-        /// <returns>
-        /// The registered property or null if no matching property found.
-        /// </returns>
-        public static PerspexProperty FindRegistered(this PerspexObject o, string name)
-        {
-            Contract.Requires<ArgumentNullException>(o != null);
-            Contract.Requires<ArgumentNullException>(name != null);
-
-            var parts = name.Split('.');
-
-            if (parts.Length < 1 || parts.Length > 2)
-            {
-                throw new ArgumentException("Invalid property name.");
-            }
-
-            if (parts.Length == 1)
-            {
-                var result =  o.GetRegisteredProperties()
-                    .FirstOrDefault(x => !x.IsAttached && x.Name == parts[0]);
-
-                if (result != null)
-                {
-                    return result;
-                }
-
-                // A type can .AddOwner an attached property.
-                return o.GetRegisteredProperties()
-                    .FirstOrDefault(x => x.Name == parts[0]);
-            }
-            else
-            {
-                return o.GetRegisteredProperties()
-                    .FirstOrDefault(x => x.IsAttached && x.OwnerType.Name == parts[0] && x.Name == parts[1]);
-            }
-        }
-
         /// <summary>
         /// Observer method for <see cref="AddClassHandler{TTarget}(IObservable{PerspexPropertyChangedEventArgs},
         /// Func{TTarget, Action{PerspexPropertyChangedEventArgs}})"/>.

+ 57 - 8
src/Perspex.Base/PerspexProperty.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Reactive.Subjects;
 using System.Reflection;
 using Perspex.Utilities;
@@ -28,7 +29,12 @@ namespace Perspex
         private static int s_nextId = 1;
 
         /// <summary>
-        /// The default values for the property, by type.
+        /// The default value provided when the property was first registered.
+        /// </summary>
+        private readonly object _defaultValue;
+
+        /// <summary>
+        /// The overridden default values for the property, by type.
         /// </summary>
         private readonly Dictionary<Type, object> _defaultValues = new Dictionary<Type, object>();
 
@@ -92,7 +98,7 @@ namespace Perspex
             Name = name;
             PropertyType = valueType;
             OwnerType = ownerType;
-            _defaultValues.Add(ownerType, defaultValue);
+            _defaultValue = defaultValue;
             Inherits = inherits;
             DefaultBindingMode = defaultBindingMode;
             IsAttached = isAttached;
@@ -143,14 +149,57 @@ namespace Perspex
         /// Initializes a new instance of the <see cref="PerspexProperty"/> class.
         /// </summary>
         /// <param name="source">The direct property to copy.</param>
+        /// <param name="ownerType">The new owner type.</param>
+        protected PerspexProperty(PerspexProperty source, Type ownerType)
+        {
+            Contract.Requires<ArgumentNullException>(source != null);
+            Contract.Requires<ArgumentNullException>(ownerType != null);
+
+            if (source.IsDirect)
+            {
+                throw new InvalidOperationException(
+                    "This method cannot be called on direct PerspexProperties.");
+            }
+
+            //Name = name;
+            //PropertyType = valueType;
+            //OwnerType = ownerType;
+            //_defaultValues.Add(ownerType, defaultValue);
+            //Inherits = inherits;
+            //DefaultBindingMode = defaultBindingMode;
+            //IsAttached = isAttached;
+            //Notifying = notifying;
+            //_id = s_nextId++;
+
+
+            Name = source.Name;
+            PropertyType = source.PropertyType;
+            OwnerType = ownerType;
+            _defaultValue = source._defaultValue;
+            _defaultValues = source._defaultValues;
+            Inherits = source.Inherits;
+            DefaultBindingMode = source.DefaultBindingMode;
+            IsAttached = false;
+            Notifying = Notifying;
+            _validation = source._validation;
+            _id = source._id;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PerspexProperty"/> class.
+        /// </summary>
+        /// <param name="source">The direct property to copy.</param>
+        /// <param name="ownerType">The new owner type.</param>
         /// <param name="getter">A new getter.</param>
         /// <param name="setter">A new setter.</param>
         protected PerspexProperty(
             PerspexProperty source,
+            Type ownerType,
             Func<PerspexObject, object> getter,
             Action<PerspexObject, object> setter)
         {
             Contract.Requires<ArgumentNullException>(source != null);
+            Contract.Requires<ArgumentNullException>(ownerType != null);
             Contract.Requires<ArgumentNullException>(getter != null);
 
             if (!source.IsDirect)
@@ -161,7 +210,7 @@ namespace Perspex
 
             Name = source.Name;
             PropertyType = source.PropertyType;
-            OwnerType = source.OwnerType;
+            OwnerType = ownerType;
             Getter = getter;
             Setter = setter;
             IsDirect = true;
@@ -367,7 +416,7 @@ namespace Perspex
                 notifying,
                 false);
 
-            PerspexObject.Register(typeof(TOwner), result);
+            PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result);
 
             return result;
         }
@@ -395,7 +444,7 @@ namespace Perspex
                 Cast(getter),
                 Cast(setter));
 
-            PerspexObject.Register(typeof(TOwner), result);
+            PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result);
 
             return result;
         }
@@ -431,7 +480,7 @@ namespace Perspex
                 null,
                 true);
 
-            PerspexObject.Register(typeof(THost), result);
+            PerspexPropertyRegistry.Instance.Register(typeof(THost), result);
 
             return result;
         }
@@ -468,7 +517,7 @@ namespace Perspex
                 null,
                 true);
 
-            PerspexObject.Register(typeof(THost), result);
+            PerspexPropertyRegistry.Instance.Register(typeof(THost), result);
 
             return result;
         }
@@ -529,7 +578,7 @@ namespace Perspex
                 type = type.GetTypeInfo().BaseType;
             }
 
-            return _defaultValues[OwnerType];
+            return _defaultValue;
         }
 
         /// <summary>

+ 255 - 0
src/Perspex.Base/PerspexPropertyRegistry.cs

@@ -0,0 +1,255 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace Perspex
+{
+    /// <summary>
+    /// Tracks registered <see cref="PerspexProperty"/> instances.
+    /// </summary>
+    public class PerspexPropertyRegistry
+    {
+        /// <summary>
+        /// The registered properties by type.
+        /// </summary>
+        private readonly Dictionary<Type, List<PerspexProperty>> _registered =
+            new Dictionary<Type, List<PerspexProperty>>();
+
+        /// <summary>
+        /// The registered attached properties by owner type.
+        /// </summary>
+        private readonly Dictionary<Type, List<PerspexProperty>> _attached =
+            new Dictionary<Type, List<PerspexProperty>>();
+
+        /// <summary>
+        /// Gets the <see cref="PerspexPropertyRegistry"/> instance
+        /// </summary>
+        public static PerspexPropertyRegistry Instance { get; }
+            = new PerspexPropertyRegistry();
+
+        /// <summary>
+        /// Gets all attached <see cref="PerspexProperty"/>s registered by an owner.
+        /// </summary>
+        /// <param name="ownerType">The owner type.</param>
+        /// <returns>A collection of <see cref="PerspexProperty"/> definitions.</returns>
+        public IEnumerable<PerspexProperty> GetAttached(Type ownerType)
+        {
+            List<PerspexProperty> list;
+
+            if (_attached.TryGetValue(ownerType, out list))
+            {
+                return list;
+            }
+
+            return Enumerable.Empty<PerspexProperty>();
+        }
+
+        /// <summary>
+        /// Gets all <see cref="PerspexProperty"/>s registered on a type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <returns>A collection of <see cref="PerspexProperty"/> definitions.</returns>
+        public IEnumerable<PerspexProperty> GetRegistered(Type type)
+        {
+            Contract.Requires<ArgumentNullException>(type != null);
+
+            var i = type.GetTypeInfo();
+
+            while (type != null)
+            {
+                List<PerspexProperty> list;
+
+                if (_registered.TryGetValue(type, out list))
+                {
+                    foreach (PerspexProperty p in list)
+                    {
+                        yield return p;
+                    }
+                }
+
+                type = type.GetTypeInfo().BaseType;
+            }
+        }
+
+        /// <summary>
+        /// Gets all <see cref="PerspexProperty"/>s registered on a object.
+        /// </summary>
+        /// <param name="o">The object.</param>
+        /// <returns>A collection of <see cref="PerspexProperty"/> definitions.</returns>
+        public IEnumerable<PerspexProperty> GetRegistered(PerspexObject o)
+        {
+            Contract.Requires<ArgumentNullException>(o != null);
+
+            return GetRegistered(o.GetType());
+        }
+
+        /// <summary>
+        /// Finds <see cref="PerspexProperty"/> registered on a type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <param name="property">The property.</param>
+        /// <returns>The registered property or null if not found.</returns>
+        /// <remarks>
+        /// Calling AddOwner on a PerspexProperty creates a new PerspexProperty that is a 
+        /// different object but is equal according to <see cref="object.Equals(object)"/>.
+        /// </remarks>
+        public PerspexProperty FindRegistered(Type type, PerspexProperty property)
+        {
+            while (type != null)
+            {
+                List<PerspexProperty> list;
+
+                if (_registered.TryGetValue(type, out list))
+                {
+                    var index = list.IndexOf(property);
+
+                    if (index != -1)
+                    {
+                        return list[index];
+                    }
+                }
+
+                type = type.GetTypeInfo().BaseType;
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Finds <see cref="PerspexProperty"/> registered on an object.
+        /// </summary>
+        /// <param name="o">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <returns>The registered property or null if not found.</returns>
+        /// <remarks>
+        /// Calling AddOwner on a PerspexProperty creates a new PerspexProperty that is a 
+        /// different object but is equal according to <see cref="object.Equals(object)"/>.
+        /// </remarks>
+        public PerspexProperty FindRegistered(object o, PerspexProperty property)
+        {
+            return FindRegistered(o.GetType(), property);
+        }
+
+        /// <summary>
+        /// Finds a registered property on a type by name.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <param name="name">
+        /// The property name. If an attached property it should be in the form 
+        /// "OwnerType.PropertyName".
+        /// </param>
+        /// <returns>
+        /// The registered property or null if no matching property found.
+        /// </returns>
+        public PerspexProperty FindRegistered(Type type, string name)
+        {
+            Contract.Requires<ArgumentNullException>(type != null);
+            Contract.Requires<ArgumentNullException>(name != null);
+
+            var parts = name.Split('.');
+
+            if (parts.Length < 1 || parts.Length > 2)
+            {
+                throw new ArgumentException("Invalid property name.");
+            }
+
+            string propertyName;
+            var results = GetRegistered(type);
+
+            if (parts.Length == 1)
+            {
+                propertyName = parts[0];
+            }
+            else
+            {
+                var types = GetImplementedTypes(type);
+
+                if (!types.Contains(parts[0]))
+                {
+                    results = results.Where(x => x.OwnerType.Name == parts[0]);
+                }
+
+                propertyName = parts[1];
+            }
+
+            return results.FirstOrDefault(x => x.Name == propertyName);
+        }
+
+        private IEnumerable<string> GetImplementedTypes(Type type)
+        {
+            while (type != null)
+            {
+                yield return type.Name;
+                type = type.GetTypeInfo().BaseType;
+            }
+        }
+
+        /// <summary>
+        /// Checks whether a <see cref="PerspexProperty"/> is registered on a type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <param name="property">The property.</param>
+        /// <returns>True if the property is registered, otherwise false.</returns>
+        public bool IsRegistered(Type type, PerspexProperty property)
+        {
+            return FindRegistered(type, property) != null;
+        }
+
+        /// <summary>
+        /// Checks whether a <see cref="PerspexProperty"/> is registered on a object.
+        /// </summary>
+        /// <param name="o">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <returns>True if the property is registered, otherwise false.</returns>
+        public bool IsRegistered(object o, PerspexProperty property)
+        {
+            return IsRegistered(o.GetType(), property);
+        }
+
+        /// <summary>
+        /// Registers a <see cref="PerspexProperty"/> on a type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <param name="property">The property.</param>
+        /// <remarks>
+        /// You won't usually want to call this method directly, instead use the
+        /// <see cref="PerspexProperty.Register"/> method.
+        /// </remarks>
+        public void Register(Type type, PerspexProperty property)
+        {
+            Contract.Requires<ArgumentNullException>(type != null);
+            Contract.Requires<ArgumentNullException>(property != null);
+
+            List<PerspexProperty> list;
+
+            if (!_registered.TryGetValue(type, out list))
+            {
+                list = new List<PerspexProperty>();
+                _registered.Add(type, list);
+            }
+
+            if (!list.Contains(property))
+            {
+                list.Add(property);
+            }
+
+            if (property.IsAttached)
+            {
+                if (!_attached.TryGetValue(property.OwnerType, out list))
+                {
+                    list = new List<PerspexProperty>();
+                    _attached.Add(property.OwnerType, list);
+                }
+
+                if (!list.Contains(property))
+                {
+                    list.Add(property);
+                }
+            }
+        }
+    }
+}

+ 29 - 5
src/Perspex.Base/PerspexProperty`1.cs

@@ -66,17 +66,29 @@ namespace Perspex
             Setter = setter;
         }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PerspexProperty"/> class.
+        /// </summary>
+        /// <param name="source">The property to copy.</param>
+        /// <param name="ownerType">The new owner type.</param>
+        private PerspexProperty(PerspexProperty source, Type ownerType)
+            : base(source, ownerType)
+        {
+        }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="PerspexProperty"/> class.
         /// </summary>
         /// <param name="source">The direct property to copy.</param>
+        /// <param name="ownerType">The new owner type.</param>
         /// <param name="getter">A new getter.</param>
         /// <param name="setter">A new setter.</param>
         private PerspexProperty(
             PerspexProperty source,
+            Type ownerType,
             Func<PerspexObject, TValue> getter,
             Action<PerspexObject, TValue> setter)
-            : base(source, CastParamReturn(getter), CastParams(setter))
+            : base(source, ownerType, CastParamReturn(getter), CastParams(setter))
         {
             Getter = getter;
             Setter = setter;
@@ -105,8 +117,9 @@ namespace Perspex
                     "You must provide a new getter and setter when calling AddOwner on a direct PerspexProperty.");
             }
 
-            PerspexObject.Register(typeof(TOwner), this);
-            return this;
+            var result = new PerspexProperty<TValue>(this, typeof(TOwner));
+            PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result);
+            return result;
         }
 
         /// <summary>
@@ -119,8 +132,19 @@ namespace Perspex
             Action<TOwner, TValue> setter = null)
                 where TOwner : PerspexObject
         {
-            var result = new PerspexProperty<TValue>(this, CastReturn(getter), CastParam1(setter));
-            PerspexObject.Register(typeof(TOwner), result);
+            if (!IsDirect)
+            {
+                throw new InvalidOperationException(
+                    "This overload of AddOwner is for direct PerspexProperties.");
+            }
+
+            var result = new PerspexProperty<TValue>(
+                this,
+                typeof(TOwner),
+                CastReturn(getter), 
+                CastParam1(setter));
+
+            PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result);
             return result;
         }
 

+ 5 - 1
src/Perspex.Controls/Control.cs

@@ -406,7 +406,11 @@ namespace Perspex.Controls
             base.OnAttachedToVisualTree(root);
 
             IStyler styler = PerspexLocator.Current.GetService<IStyler>();
-            styler.ApplyStyles(this);
+
+            if (styler != null)
+            {
+                styler.ApplyStyles(this);
+            }
         }
 
         /// <summary>

+ 1 - 1
src/Perspex.Controls/DropDown.cs

@@ -80,7 +80,7 @@ namespace Perspex.Controls
 
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
-            return new ItemContainerGenerator<ListBoxItem>(this);
+            return new ItemContainerGenerator<ListBoxItem>(this, ListBoxItem.ContentProperty);
         }
 
         protected override void OnKeyDown(KeyEventArgs e)

+ 18 - 7
src/Perspex.Controls/Generators/IItemContainerGenerator.cs

@@ -32,26 +32,37 @@ namespace Perspex.Controls.Generators
         /// <param name="items">The items.</param>
         /// <param name="selector">An optional member selector.</param>
         /// <returns>The created controls.</returns>
-        IList<IControl> CreateContainers(
+        IEnumerable<IControl> Materialize(
             int startingIndex,
             IEnumerable items,
             IMemberSelector selector);
 
         /// <summary>
-        /// Removes a set of created containers from the index and returns the removed controls.
+        /// Removes a set of created containers.
         /// </summary>
         /// <param name="startingIndex">
         /// The index of the first item of the data in the containing collection.
         /// </param>
-        /// <param name="items">The items.</param>
-        /// <returns>The removed controls.</returns>
-        IList<IControl> RemoveContainers(int startingIndex, IEnumerable items);
+        /// <param name="count">The the number of items to remove.</param>
+        /// <returns>The removed containers.</returns>
+        IEnumerable<IControl> Dematerialize(int startingIndex, int count);
+
+        /// <summary>
+        /// Removes a set of created containers and updates the index of later containers to fill
+        /// the gap.
+        /// </summary>
+        /// <param name="startingIndex">
+        /// The index of the first item of the data in the containing collection.
+        /// </param>
+        /// <param name="count">The the number of items to remove.</param>
+        /// <returns>The removed containers.</returns>
+        IEnumerable<IControl> RemoveRange(int startingIndex, int count);
 
         /// <summary>
-        /// Clears the created containers from the index and returns the removed controls.
+        /// Clears all created containers and returns the removed controls.
         /// </summary>
         /// <returns>The removed controls.</returns>
-        IList<IControl> ClearContainers();
+        IEnumerable<IControl> Clear();
 
         /// <summary>
         /// Gets the container control representing the item with the specified index.

+ 11 - 11
src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs

@@ -11,23 +11,23 @@ namespace Perspex.Controls.Generators
     public interface ITreeItemContainerGenerator : IItemContainerGenerator
     {
         /// <summary>
-        /// Gets all of the generated container controls.
+        /// Gets the item container for the root of the tree, or null if this generator is itself 
+        /// the root of the tree.
         /// </summary>
-        /// <returns>The containers.</returns>
-        IEnumerable<IControl> GetAllContainers();
+        ITreeItemContainerGenerator RootGenerator { get; }
 
         /// <summary>
-        /// Gets the item that is contained by the specified container.
+        /// Gets the item container for the specified item, anywhere in the tree.
         /// </summary>
-        /// <param name="container">The container.</param>
-        /// <returns>The item.</returns>
-        object ItemFromContainer(IControl container);
+        /// <param name="item">The item.</param>
+        /// <returns>The container, or null if not found.</returns>
+        IControl TreeContainerFromItem(object item);
 
         /// <summary>
-        /// Gets the container for the specified item
+        /// Gets the item for the specified item container, anywhere in the tree.
         /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns>The container.</returns>
-        IControl ContainerFromItem(object item);
+        /// <param name="container">The container.</param>
+        /// <returns>The item, or null if not found.</returns>
+        object TreeItemFromContainer(IControl container);
     }
 }

+ 54 - 62
src/Perspex.Controls/Generators/ItemContainerGenerator.cs

@@ -15,7 +15,7 @@ namespace Perspex.Controls.Generators
     /// </summary>
     public class ItemContainerGenerator : IItemContainerGenerator
     {
-        private Dictionary<int, IControl> _containers = new Dictionary<int, IControl>();
+        private List<IControl> _containers = new List<IControl>();
 
         private readonly Subject<ItemContainers> _containersInitialized = new Subject<ItemContainers>();
 
@@ -25,17 +25,15 @@ namespace Perspex.Controls.Generators
         /// <param name="owner">The owner control.</param>
         public ItemContainerGenerator(IControl owner)
         {
+            Contract.Requires<ArgumentNullException>(owner != null);
+
             Owner = owner;
         }
 
-        /// <summary>
-        /// Gets the currently realized containers.
-        /// </summary>
-        public IEnumerable<IControl> Containers => _containers.Values;
+        /// <inheritdoc/>
+        public IEnumerable<IControl> Containers => _containers;
 
-        /// <summary>
-        /// Signalled whenever new containers are initialized.
-        /// </summary>
+        /// <inheritdoc/>
         public IObservable<ItemContainers> ContainersInitialized => _containersInitialized;
 
         /// <summary>
@@ -43,16 +41,8 @@ namespace Perspex.Controls.Generators
         /// </summary>
         public IControl Owner { get; }
 
-        /// <summary>
-        /// Creates container controls for a collection of items.
-        /// </summary>
-        /// <param name="startingIndex">
-        /// The index of the first item of the data in the containing collection.
-        /// </param>
-        /// <param name="items">The items.</param>
-        /// <param name="selector">An optional member selector.</param>
-        /// <returns>The created container controls.</returns>
-        public IList<IControl> CreateContainers(
+        /// <inheritdoc/>
+        public IEnumerable<IControl> Materialize(
             int startingIndex,
             IEnumerable items,
             IMemberSelector selector)
@@ -75,72 +65,54 @@ namespace Perspex.Controls.Generators
             return result.Where(x => x != null).ToList();
         }
 
-        /// <summary>
-        /// Removes a set of created containers from the index and returns the removed controls.
-        /// </summary>
-        /// <param name="startingIndex">
-        /// The index of the first item of the data in the containing collection.
-        /// </param>
-        /// <param name="items">The items.</param>
-        /// <returns>The removed controls.</returns>
-        public IList<IControl> RemoveContainers(int startingIndex, IEnumerable items)
+        /// <inheritdoc/>
+        public virtual IEnumerable<IControl> Dematerialize(int startingIndex, int count)
         {
             var result = new List<IControl>();
-            var count = items.Cast<object>().Count();
 
             for (int i = startingIndex; i < startingIndex + count; ++i)
             {
-                var container = _containers[i];
-
-                if (container != null)
+                if (i < _containers.Count)
                 {
-                    result.Add(container);
-                    _containers.Remove(i);
+                    result.Add(_containers[i]);
+                    _containers[i] = null;
                 }
             }
 
             return result;
         }
 
-        /// <summary>
-        /// Clears the created containers from the index and returns the removed controls.
-        /// </summary>
-        /// <returns>The removed controls.</returns>
-        public IList<IControl> ClearContainers()
+        /// <inheritdoc/>
+        public virtual IEnumerable<IControl> RemoveRange(int startingIndex, int count)
         {
-            var result = _containers;
-            _containers = new Dictionary<int, IControl>();
-            return result.Values.ToList();
+            var result = _containers.GetRange(startingIndex, count);
+            _containers.RemoveRange(startingIndex, count);
+            return result;
         }
 
-        /// <summary>
-        /// Gets the container control representing the item with the specified index.
-        /// </summary>
-        /// <param name="index">The index.</param>
-        /// <returns>The container or null if no container created.</returns>
-        public IControl ContainerFromIndex(int index)
+        /// <inheritdoc/>
+        public virtual IEnumerable<IControl> Clear()
         {
-            IControl result;
-            _containers.TryGetValue(index, out result);
+            var result = _containers;
+            _containers = new List<IControl>();
             return result;
         }
 
-        /// <summary>
-        /// Gets the index of the specified container control.
-        /// </summary>
-        /// <param name="container">The container.</param>
-        /// <returns>The index of the container or -1 if not found.</returns>
-        public int IndexFromContainer(IControl container)
+        /// <inheritdoc/>
+        public IControl ContainerFromIndex(int index)
         {
-            foreach (var i in _containers)
+            if (index < _containers.Count)
             {
-                if (i.Value == container)
-                {
-                    return i.Key;
-                }
+                return _containers[index];
             }
 
-            return -1;
+            return null;
+        }
+
+        /// <inheritdoc/>
+        public int IndexFromContainer(IControl container)
+        {
+            return _containers.IndexOf(container);
         }
 
         /// <summary>
@@ -171,7 +143,16 @@ namespace Perspex.Controls.Generators
 
             foreach (var c in container)
             {
-                if (!_containers.ContainsKey(index))
+                while (_containers.Count < index)
+                {
+                    _containers.Add(null);
+                }
+
+                if (_containers.Count == index)
+                {
+                    _containers.Add(c);
+                }
+                else if (_containers[index] == null)
                 {
                     _containers[index] = c;
                 }
@@ -183,5 +164,16 @@ namespace Perspex.Controls.Generators
                 ++index;
             }
         }
+
+        /// <summary>
+        /// Gets all containers with an index that fall within a range.
+        /// </summary>
+        /// <param name="index">The first index.</param>
+        /// <param name="count">The number of elements in the range.</param>
+        /// <returns>The containers.</returns>
+        protected IEnumerable<IControl> GetContainerRange(int index, int count)
+        {
+            return _containers.GetRange(index, count);
+        }
     }
 }

+ 15 - 3
src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs

@@ -1,6 +1,9 @@
 // Copyright (c) The Perspex Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System;
+using System.Linq.Expressions;
+using System.Reflection;
 using Perspex.Controls.Templates;
 
 namespace Perspex.Controls.Generators
@@ -9,17 +12,26 @@ namespace Perspex.Controls.Generators
     /// Creates containers for items and maintains a list of created containers.
     /// </summary>
     /// <typeparam name="T">The type of the container.</typeparam>
-    public class ItemContainerGenerator<T> : ItemContainerGenerator where T : class, IContentControl, new()
+    public class ItemContainerGenerator<T> : ItemContainerGenerator where T : class, IControl, new()
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="ItemContainerGenerator{T}"/> class.
         /// </summary>
         /// <param name="owner">The owner control.</param>
-        public ItemContainerGenerator(Control owner)
+        /// <param name="contentProperty">The container's Content property.</param>
+        public ItemContainerGenerator(
+            IControl owner, 
+            PerspexProperty contentProperty)
             : base(owner)
         {
+            ContentProperty = contentProperty;
         }
 
+        /// <summary>
+        /// Gets the container's Content property.
+        /// </summary>
+        protected PerspexProperty ContentProperty { get; }
+
         /// <inheritdoc/>
         protected override IControl CreateContainer(object item)
         {
@@ -36,7 +48,7 @@ namespace Perspex.Controls.Generators
             else
             {
                 var result = new T();
-                result.Content = Owner.MaterializeDataTemplate(item);
+                result.SetValue(ContentProperty, Owner.MaterializeDataTemplate(item));
 
                 if (!(item is IControl))
                 {

+ 106 - 154
src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs

@@ -1,11 +1,8 @@
 // Copyright (c) The Perspex Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
-using System;
 using System.Collections;
 using System.Collections.Generic;
-using System.Linq;
-using System.Reactive.Subjects;
 using Perspex.Controls.Templates;
 
 namespace Perspex.Controls.Generators
@@ -14,161 +11,84 @@ namespace Perspex.Controls.Generators
     /// Creates containers for tree items and maintains a list of created containers.
     /// </summary>
     /// <typeparam name="T">The type of the container.</typeparam>
-    public class TreeItemContainerGenerator<T> : ITreeItemContainerGenerator where T : TreeViewItem, new()
+    public class TreeItemContainerGenerator<T> : ItemContainerGenerator<T>, ITreeItemContainerGenerator
+        where T : class, IControl, new()
     {
-        private Dictionary<object, T> _containers = new Dictionary<object, T>();
-
-        private readonly Subject<ItemContainers> _containersInitialized = new Subject<ItemContainers>();
+        private Dictionary<object, T> _itemToContainer;
+        private Dictionary<IControl, object> _containerToItem;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="TreeItemContainerGenerator{T}"/> class.
         /// </summary>
         /// <param name="owner">The owner control.</param>
-        public TreeItemContainerGenerator(IControl owner)
-        {
-            Owner = owner;
-        }
-
-        /// <summary>
-        /// Gets the currently realized containers.
-        /// </summary>
-        public IEnumerable<IControl> Containers => _containers.Values;
-
-        /// <summary>
-        /// Signalled whenever new containers are initialized.
-        /// </summary>
-        public IObservable<ItemContainers> ContainersInitialized => _containersInitialized;
-
-        /// <summary>
-        /// Gets the owner control.
-        /// </summary>
-        public IControl Owner { get; }
-
-        /// <summary>
-        /// Creates container controls for a collection of items.
-        /// </summary>
-        /// <param name="startingIndex">
-        /// The index of the first item of the data in the containing collection.
-        /// </param>
-        /// <param name="items">The items.</param>
-        /// <param name="selector">An optional member selector.</param>
-        /// <returns>The created container controls.</returns>
-        public IList<IControl> CreateContainers(
-            int startingIndex, 
-            IEnumerable items,
-            IMemberSelector selector)
-        {
-            Contract.Requires<ArgumentNullException>(items != null);
-
-            int index = startingIndex;
-            var result = new List<IControl>();
-
-            foreach (var item in items)
-            {
-                var i = selector != null ? selector.Select(item) : item;
-                var container = CreateContainer(i);
-                _containers.Add(i, container);
-                result.Add(container);
-            }
-
-            _containersInitialized.OnNext(new ItemContainers(startingIndex, result));
-
-            return result.Where(x => x != null).ToList();
-        }
-
-        /// <summary>
-        /// Removes a set of created containers from the index and returns the removed controls.
-        /// </summary>
-        /// <param name="startingIndex">
-        /// The index of the first item of the data in the containing collection.
+        /// <param name="contentProperty">The container's Content property.</param>
+        /// <param name="itemsProperty">The container's Items property.</param>
+        /// <param name="isExpandedProperty">The container's IsExpanded property.</param>
+        /// <param name="rootGenerator">
+        /// The item container for the root of the tree, or null if this generator is itself the
+        /// root of the tree.
         /// </param>
-        /// <param name="items">The items.</param>
-        /// <returns>The removed controls.</returns>
-        public IList<IControl> RemoveContainers(int startingIndex, IEnumerable items)
+        public TreeItemContainerGenerator(
+            IControl owner,
+            PerspexProperty contentProperty,
+            PerspexProperty itemsProperty,
+            PerspexProperty isExpandedProperty,
+            ITreeItemContainerGenerator rootGenerator)
+            : base(owner, contentProperty)
         {
-            var result = new List<IControl>();
+            ItemsProperty = itemsProperty;
+            IsExpandedProperty = isExpandedProperty;
+            RootGenerator = rootGenerator;
 
-            foreach (var item in items)
+            if (rootGenerator == null)
             {
-                T container;
-
-                if (_containers.TryGetValue(item, out container))
-                {
-                    Remove(container, result);
-                }
+                _itemToContainer = new Dictionary<object, T>();
+                _containerToItem = new Dictionary<IControl, object>();
             }
-
-            return result;
         }
 
         /// <summary>
-        /// Clears the created containers from the index and returns the removed controls.
+        /// Gets the item container for the root of the tree, or null if this generator is itself 
+        /// the root of the tree.
         /// </summary>
-        /// <returns>The removed controls.</returns>
-        public IList<IControl> ClearContainers()
-        {
-            var result = _containers;
-            _containers = new Dictionary<object, T>();
-            return result.Values.Cast<IControl>().ToList();
-        }
+        public ITreeItemContainerGenerator RootGenerator { get; }
 
         /// <summary>
-        /// Gets the container control representing the item with the specified index.
+        /// Gets the item container's Items property.
         /// </summary>
-        /// <param name="index">The index.</param>
-        /// <returns>The container or null if no container created.</returns>
-        public IControl ContainerFromIndex(int index)
-        {
-            throw new NotImplementedException();
-        }
+        protected PerspexProperty ItemsProperty { get; }
 
         /// <summary>
-        /// Gets the index of the specified container control.
+        /// Gets the item container's IsExpanded property.
         /// </summary>
-        /// <param name="container">The container.</param>
-        /// <returns>The index of the container or -1 if not found.</returns>
-        public int IndexFromContainer(IControl container)
-        {
-            throw new NotImplementedException();
-        }
+        protected PerspexProperty IsExpandedProperty { get; }
 
         /// <summary>
-        /// Gets all of the generated container controls.
+        /// Gets the item container for the specified item, anywhere in the tree.
         /// </summary>
-        /// <returns>The containers.</returns>
-        public IEnumerable<IControl> GetAllContainers()
+        /// <param name="item">The item.</param>
+        /// <returns>The container, or null if not found.</returns>
+        public IControl TreeContainerFromItem(object item)
         {
-            return _containers.Values;
+            T result;
+            _itemToContainer.TryGetValue(item, out result);
+            return result;
         }
 
         /// <summary>
-        /// Gets the item that is contained by the specified container.
+        /// Gets the item for the specified item container, anywhere in the tree.
         /// </summary>
         /// <param name="container">The container.</param>
-        /// <returns>The item.</returns>
-        public object ItemFromContainer(IControl container)
-        {
-            return container.DataContext;
-        }
-
-        /// <summary>
-        /// Gets the container for the specified item
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns>The container.</returns>
-        public IControl ContainerFromItem(object item)
+        /// <returns>The item, or null if not found.</returns>
+        public object TreeItemFromContainer(IControl container)
         {
-            T result;
-            _containers.TryGetValue(item, out result);
+            object result;
+            _containerToItem.TryGetValue(container, out result);
             return result;
         }
 
-        /// <summary>
-        /// Creates the container for an item.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns>The created container control.</returns>
-        protected virtual T CreateContainer(object item)
+        /// <inheritdoc/>
+        protected override IControl CreateContainer(object item)
         {
             var container = item as T;
 
@@ -183,22 +103,78 @@ namespace Perspex.Controls.Generators
             else
             {
                 var template = GetTreeDataTemplate(item);
-                var result = new T
-                {
-                    Header = template.Build(item),
-                    Items = template.ItemsSelector(item),
-                    IsExpanded = template.IsExpanded(item),
-                };
+                var result = new T();
+
+                result.SetValue(ContentProperty, template.Build(item));
+                result.SetValue(ItemsProperty, template.ItemsSelector(item));
+                result.SetValue(IsExpandedProperty, template.IsExpanded(item));
 
                 if (!(item is IControl))
                 {
                     result.DataContext = item;
                 }
 
+                AddToIndex(item, result);
+
                 return result;
             }
         }
 
+        public override IEnumerable<IControl> Clear()
+        {
+            ClearIndex();
+            return base.Clear();
+        }
+
+        public override IEnumerable<IControl> Dematerialize(int startingIndex, int count)
+        {
+            RemoveFromIndex(GetContainerRange(startingIndex, count));
+            return base.Dematerialize(startingIndex, count);
+        }
+
+        private void AddToIndex(object item, T container)
+        {
+            if (RootGenerator != null)
+            {
+                ((TreeItemContainerGenerator<T>)RootGenerator).AddToIndex(item, container);
+            }
+            else
+            {
+                _itemToContainer.Add(item, container);
+                _containerToItem.Add(container, item);
+            }
+        }
+
+        private void RemoveFromIndex(IEnumerable<IControl> containers)
+        {
+            if (RootGenerator != null)
+            {
+                ((TreeItemContainerGenerator<T>)RootGenerator).RemoveFromIndex(containers);
+            }
+            else
+            {
+                foreach (var container in containers)
+                {
+                    var item = _containerToItem[container];
+                    _containerToItem.Remove(container);
+                    _itemToContainer.Remove(item);
+                }
+            }
+        }
+
+        private void ClearIndex()
+        {
+            if (RootGenerator != null)
+            {
+                ((TreeItemContainerGenerator<T>)RootGenerator).ClearIndex();
+            }
+            else
+            {
+                _containerToItem.Clear();
+                _itemToContainer.Clear();
+            }
+        }
+
         /// <summary>
         /// Gets the data template for the specified item.
         /// </summary>
@@ -222,29 +198,5 @@ namespace Perspex.Controls.Generators
 
             return treeTemplate;
         }
-
-        private void Remove(T container, IList<IControl> removed)
-        {
-            if (container.Items != null)
-            {
-                foreach (var childItem in container.Items)
-                {
-                    T childContainer;
-
-                    if (_containers.TryGetValue(childItem, out childContainer))
-                    {
-                        Remove(childContainer, removed);
-                    }
-                }
-            }
-
-            // TODO: Dual index.
-            var i = _containers.FirstOrDefault(x => x.Value == container);
-
-            if (i.Key != null)
-            {
-                _containers.Remove(i.Key);
-            }
-        }
     }
 }

+ 2 - 1
src/Perspex.Controls/ListBox.cs

@@ -7,6 +7,7 @@ using Perspex.Collections;
 using Perspex.Controls.Generators;
 using Perspex.Controls.Primitives;
 using Perspex.Input;
+using Perspex.Interactivity;
 
 namespace Perspex.Controls
 {
@@ -43,7 +44,7 @@ namespace Perspex.Controls
         /// <inheritdoc/>
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
-            return new ItemContainerGenerator<ListBoxItem>(this);
+            return new ItemContainerGenerator<ListBoxItem>(this, ListBoxItem.ContentProperty);
         }
 
         /// <inheritdoc/>

+ 2 - 2
src/Perspex.Controls/Presenters/CarouselPresenter.cs

@@ -199,7 +199,7 @@ namespace Perspex.Controls.Presenters
             if (toIndex != -1)
             {
                 var item = Items.Cast<object>().ElementAt(toIndex);
-                to = generator.CreateContainers(toIndex, new[] { item }, MemberSelector).FirstOrDefault();
+                to = generator.Materialize(toIndex, new[] { item }, MemberSelector).FirstOrDefault();
 
                 if (to != null)
                 {
@@ -215,7 +215,7 @@ namespace Perspex.Controls.Presenters
             if (from != null)
             {
                 Panel.Children.Remove(from);
-                generator.RemoveContainers(fromIndex, new[] { from });
+                generator.Dematerialize(fromIndex, 1);
             }
         }
 

+ 28 - 6
src/Perspex.Controls/Presenters/ItemsPresenter.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections;
+using System.Collections.Generic;
 using System.Collections.Specialized;
 using Perspex.Controls.Generators;
 using Perspex.Controls.Templates;
@@ -186,7 +187,7 @@ namespace Perspex.Controls.Presenters
         {
             if (items != null)
             {
-                Panel.Children.AddRange(ItemContainerGenerator.CreateContainers(0, Items, MemberSelector));
+                Panel.Children.AddRange(ItemContainerGenerator.Materialize(0, Items, MemberSelector));
 
                 INotifyCollectionChanged incc = items as INotifyCollectionChanged;
 
@@ -209,7 +210,7 @@ namespace Perspex.Controls.Presenters
 
                 if (e.OldValue != null)
                 {
-                    generator.ClearContainers();
+                    generator.Clear();
                     Panel.Children.Clear();
 
                     INotifyCollectionChanged incc = e.OldValue as INotifyCollectionChanged;
@@ -237,18 +238,39 @@ namespace Perspex.Controls.Presenters
             if (_createdPanel)
             {
                 var generator = ItemContainerGenerator;
+                IEnumerable<IControl> containers;
 
                 // TODO: Handle Move and Replace etc.
                 switch (e.Action)
                 {
                     case NotifyCollectionChangedAction.Add:
-                        Panel.Children.AddRange(
-                            generator.CreateContainers(e.NewStartingIndex, e.NewItems, MemberSelector));
+                        containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector);
+                        Panel.Children.AddRange(containers);
                         break;
 
                     case NotifyCollectionChangedAction.Remove:
-                        Panel.Children.RemoveAll(
-                            generator.RemoveContainers(e.OldStartingIndex, e.OldItems));
+                        containers = generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count);
+                        Panel.Children.RemoveAll(containers);
+                        break;
+
+                    case NotifyCollectionChangedAction.Replace:
+                        generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count);
+                        containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector);
+
+                        var i = e.NewStartingIndex;
+
+                        foreach (var container in containers)
+                        {
+                            Panel.Children[i++] = container;
+                        }
+
+                        break;
+
+                    case NotifyCollectionChangedAction.Move:
+                        // TODO: Implement Move in a more efficient manner.
+                    case NotifyCollectionChangedAction.Reset:
+                        Panel.Children.RemoveAll(generator.Clear());
+                        Panel.Children.AddRange(generator.Materialize(0, Items, MemberSelector));
                         break;
                 }
 

+ 6 - 6
src/Perspex.Controls/Primitives/SelectingItemsControl.cs

@@ -141,7 +141,7 @@ namespace Perspex.Controls.Primitives
                 var index = IndexOf(Items, value);
                 var effective = index != -1 ? value : null;
 
-                if (effective != old)
+                if (!object.Equals(effective, old))
                 {
                     _selectedItem = effective;
                     RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue);
@@ -388,8 +388,8 @@ namespace Perspex.Controls.Primitives
         }
 
         /// <summary>
-        /// Updates the selection based on an event source that may have originated in a container
-        /// that belongs to the control.
+        /// Updates the selection based on an event that may have originated in a container that 
+        /// belongs to the control.
         /// </summary>
         /// <param name="eventSource">The control that raised the event.</param>
         /// <param name="select">Whether the container should be selected or unselected.</param>
@@ -405,11 +405,11 @@ namespace Perspex.Controls.Primitives
             bool rangeModifier = false,
             bool toggleModifier = false)
         {
-            var item = GetContainerFromEventSource(eventSource);
+            var container = GetContainerFromEventSource(eventSource);
 
-            if (item != null)
+            if (container != null)
             {
-                UpdateSelection(item, select, rangeModifier, toggleModifier);
+                UpdateSelection(container, select, rangeModifier, toggleModifier);
                 return true;
             }
 

+ 1 - 1
src/Perspex.Controls/Primitives/TabStrip.cs

@@ -43,7 +43,7 @@ namespace Perspex.Controls.Primitives
             }
             else
             {
-                result = new ItemContainerGenerator<TabItem>(this);
+                result = new ItemContainerGenerator<TabItem>(this, TabItem.ContentProperty);
             }
 
             return result;

+ 147 - 25
src/Perspex.Controls/TreeView.cs

@@ -6,11 +6,20 @@ using System.Linq;
 using Perspex.Controls.Generators;
 using Perspex.Controls.Primitives;
 using Perspex.Input;
+using Perspex.Interactivity;
+using Perspex.Styling;
+using Perspex.VisualTree;
 
 namespace Perspex.Controls
 {
+    /// <summary>
+    /// Displays a hierachical tree of data.
+    /// </summary>
     public class TreeView : ItemsControl
     {
+        /// <summary>
+        /// Defines the <see cref="SelectedItem"/> property.
+        /// </summary>
         public static readonly PerspexProperty<object> SelectedItemProperty =
             SelectingItemsControl.SelectedItemProperty.AddOwner<TreeView>(
                 o => o.SelectedItem,
@@ -18,61 +27,174 @@ namespace Perspex.Controls
 
         private object _selectedItem;
 
+        /// <summary>
+        /// Initializes static members of the <see cref="TreeView"/> class.
+        /// </summary>
         static TreeView()
         {
-            SelectedItemProperty.Changed.Subscribe(x =>
-            {
-                var control = x.Sender as TreeView;
-
-                if (control != null)
-                {
-                    control.SelectedItemChanged(x.NewValue);
-                }
-            });
+            // HACK: Needed or SelectedItem property will not be found in Release build.
         }
 
-        public new ITreeItemContainerGenerator ItemContainerGenerator => (ITreeItemContainerGenerator)base.ItemContainerGenerator;
+        /// <summary>
+        /// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view.
+        /// </summary>
+        public new ITreeItemContainerGenerator ItemContainerGenerator => 
+            (ITreeItemContainerGenerator)base.ItemContainerGenerator;
 
+        /// <summary>
+        /// Gets or sets the selected item.
+        /// </summary>
         public object SelectedItem
         {
             get { return _selectedItem; }
             set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); }
         }
 
+        /// <inheritdoc/>
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
-            return new TreeItemContainerGenerator<TreeViewItem>(this);
+            return new TreeItemContainerGenerator<TreeViewItem>(
+                this,
+                TreeViewItem.HeaderProperty,
+                TreeViewItem.ItemsProperty,
+                TreeViewItem.IsExpandedProperty,
+                null);
         }
 
+        /// <inheritdoc/>
         protected override void OnGotFocus(GotFocusEventArgs e)
         {
-            var control = (IControl)e.Source;
-            var item = ItemContainerGenerator.ItemFromContainer(control);
+            if (e.NavigationMethod == NavigationMethod.Directional)
+            {
+                e.Handled = UpdateSelectionFromEventSource(
+                    e.Source,
+                    true,
+                    (e.InputModifiers & InputModifiers.Shift) != 0);
+            }
+        }
+
+        /// <inheritdoc/>
+        protected override void OnPointerPressed(PointerPressEventArgs e)
+        {
+            base.OnPointerPressed(e);
+
+            if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right)
+            {
+                e.Handled = UpdateSelectionFromEventSource(
+                    e.Source,
+                    true,
+                    (e.InputModifiers & InputModifiers.Shift) != 0,
+                    (e.InputModifiers & InputModifiers.Control) != 0);
+            }
+        }
+
+        /// <summary>
+        /// Updates the selection for an item based on user interaction.
+        /// </summary>
+        /// <param name="container">The container.</param>
+        /// <param name="select">Whether the item should be selected or unselected.</param>
+        /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
+        /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
+        protected void UpdateSelectionFromContainer(
+            IControl container,
+            bool select = true,
+            bool rangeModifier = false,
+            bool toggleModifier = false)
+        {
+            var item = ItemContainerGenerator.TreeItemFromContainer(container);
 
             if (item != null)
             {
+                if (SelectedItem != null)
+                {
+                    var old = ItemContainerGenerator.TreeContainerFromItem(SelectedItem);
+                    MarkContainerSelected(old, false);
+                }
+
                 SelectedItem = item;
-                e.Handled = true;
+
+                if (SelectedItem != null)
+                {
+                    MarkContainerSelected(container, true);
+                }
             }
         }
 
-        private void SelectedItemChanged(object selected)
+        /// <summary>
+        /// Updates the selection based on an event that may have originated in a container that 
+        /// belongs to the control.
+        /// </summary>
+        /// <param name="eventSource">The control that raised the event.</param>
+        /// <param name="select">Whether the container should be selected or unselected.</param>
+        /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
+        /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
+        /// <returns>
+        /// True if the event originated from a container that belongs to the control; otherwise
+        /// false.
+        /// </returns>
+        protected bool UpdateSelectionFromEventSource(
+            IInteractive eventSource,
+            bool select = true,
+            bool rangeModifier = false,
+            bool toggleModifier = false)
         {
-            var containers = ItemContainerGenerator.GetAllContainers().OfType<ISelectable>();
-            var selectedContainer = (selected != null) ?
-                ItemContainerGenerator.ContainerFromItem(selected) :
-                null;
+            var container = GetContainerFromEventSource(eventSource);
 
-            if (Presenter != null && Presenter.Panel != null)
+            if (container != null)
             {
-                KeyboardNavigation.SetTabOnceActiveElement(
-                    (InputElement)Presenter.Panel,
-                    selectedContainer);
+                UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier);
+                return true;
             }
 
-            foreach (var item in containers)
+            return false;
+        }
+
+        /// <summary>
+        /// Tries to get the container that was the source of an event.
+        /// </summary>
+        /// <param name="eventSource">The control that raised the event.</param>
+        /// <returns>The container or null if the event did not originate in a container.</returns>
+        protected IControl GetContainerFromEventSource(IInteractive eventSource)
+        {
+            var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
+                .OfType<TreeViewItem>()
+                .FirstOrDefault();
+
+            if (item != null)
+            {
+                if (item.ItemContainerGenerator.RootGenerator == this.ItemContainerGenerator)
+                {
+                    return item;
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
+        /// </summary>
+        /// <param name="container">The container.</param>
+        /// <param name="selected">Whether the control is selected</param>
+        private void MarkContainerSelected(IControl container, bool selected)
+        {
+            var selectable = container as ISelectable;
+            var styleable = container as IStyleable;
+
+            if (selectable != null)
+            {
+                selectable.IsSelected = selected;
+            }
+            else if (styleable != null)
             {
-                item.IsSelected = item == selectedContainer;
+                if (selected)
+                {
+                    styleable.Classes.Add(":selected");
+                }
+                else
+                {
+                    styleable.Classes.Remove(":selected");
+                }
             }
         }
     }

+ 15 - 24
src/Perspex.Controls/TreeViewItem.cs

@@ -1,10 +1,9 @@
 // Copyright (c) The Perspex Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
-using System;
 using System.Linq;
-using Perspex.Controls.Mixins;
 using Perspex.Controls.Generators;
+using Perspex.Controls.Mixins;
 using Perspex.Controls.Primitives;
 using Perspex.Controls.Templates;
 using Perspex.Input;
@@ -66,39 +65,31 @@ namespace Perspex.Controls
             set { SetValue(IsSelectedProperty, value); }
         }
 
+        /// <summary>
+        /// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view.
+        /// </summary>
+        public new ITreeItemContainerGenerator ItemContainerGenerator =>
+            (ITreeItemContainerGenerator)base.ItemContainerGenerator;
+
         /// <inheritdoc/>
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
-            if (_treeView == null)
-            {
-                throw new InvalidOperationException(
-                    "Cannot get the ItemContainerGenerator for a TreeViewItem " +
-                    "before it is added to a TreeView.");
-            }
-
-            return _treeView.ItemContainerGenerator;
+            return new TreeItemContainerGenerator<TreeViewItem>(
+                this,
+                TreeViewItem.HeaderProperty,
+                TreeViewItem.ItemsProperty,
+                TreeViewItem.IsExpandedProperty,
+                _treeView?.ItemContainerGenerator);
         }
 
         /// <inheritdoc/>
         protected override void OnAttachedToVisualTree(IRenderRoot root)
         {
             base.OnAttachedToVisualTree(root);
-
-            if (this.GetVisualParent() != null)
-            {
-                _treeView = this.GetVisualAncestors().OfType<TreeView>().FirstOrDefault();
-
-                if (_treeView == null)
-                {
-                    throw new InvalidOperationException("TreeViewItems must be added to a TreeView.");
-                }
-            }
-            else
-            {
-                _treeView = null;
-            }
+            _treeView = this.GetVisualAncestors().OfType<TreeView>().FirstOrDefault();
         }
 
+        /// <inheritdoc/>
         protected override void OnKeyDown(KeyEventArgs e)
         {
             if (!e.Handled)

+ 1 - 1
src/Perspex.Diagnostics/Debug.cs

@@ -37,7 +37,7 @@ namespace Perspex.Diagnostics
                 builder.Append(" ");
                 builder.AppendLine(control.Classes.ToString());
 
-                foreach (var property in control.GetRegisteredProperties())
+                foreach (var property in PerspexPropertyRegistry.Instance.GetRegistered(control))
                 {
                     var value = control.GetDiagnostic(property);
 

+ 1 - 1
src/Perspex.Diagnostics/ViewModels/ControlDetailsViewModel.cs

@@ -14,7 +14,7 @@ namespace Perspex.Diagnostics.ViewModels
         {
             if (control != null)
             {
-                Properties = control.GetRegisteredProperties()
+                Properties = PerspexPropertyRegistry.Instance.GetRegistered(control)
                     .Select(x => new PropertyDetails(control, x))
                     .OrderBy(x => x.IsAttached)
                     .ThenBy(x => x.Name);

+ 6 - 0
src/Perspex.Layout/Layoutable.cs

@@ -505,6 +505,12 @@ namespace Perspex.Layout
                 height = Math.Max(height, child.DesiredSize.Height);
             }
 
+            if (UseLayoutRounding)
+            {
+                width = Math.Ceiling(width);
+                height = Math.Ceiling(height);
+            }
+
             return new Size(width, height);
         }
 

+ 10 - 0
src/Perspex.Themes.Default/TreeViewItemStyle.cs

@@ -13,6 +13,7 @@ using Perspex.Styling;
 
 namespace Perspex.Themes.Default
 {
+    using Collections;
     using Controls = Controls.Controls;
 
     /// <summary>
@@ -116,6 +117,15 @@ namespace Perspex.Themes.Default
                                 {
                                     [~ContentPresenter.ContentProperty] = control[~HeaderedItemsControl.HeaderProperty],
                                 },
+                            },
+                            new Rectangle
+                            {
+                                Name = "focus",
+                                Stroke = Brushes.Black,
+                                StrokeThickness = 1,
+                                StrokeDashArray = new PerspexList<double>(1, 2),
+                                [Grid.ColumnProperty] = 1,
+                                [!Rectangle.IsVisibleProperty] = control[!TreeViewItem.IsFocusedProperty],
                             }
                         }
                     },

+ 1 - 0
tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj

@@ -75,6 +75,7 @@
     <Compile Include="Collections\PropertyChangedTracker.cs" />
     <Compile Include="PerspexObjectTests_Direct.cs" />
     <Compile Include="PerspexObjectTests_GetObservable.cs" />
+    <Compile Include="PerspexPropertyRegistryTests.cs" />
     <Compile Include="PerspexObjectTests_Validation.cs" />
     <Compile Include="PerspexObjectTests_Binding.cs" />
     <Compile Include="PerspexObjectTests_Inheritance.cs" />

+ 0 - 24
tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs

@@ -18,30 +18,6 @@ namespace Perspex.Base.UnitTests
             p = AttachedOwner.AttachedProperty;
         }
 
-        [Fact]
-        public void GetRegisteredProperties_Returns_Registered_Properties()
-        {
-            string[] names = PerspexObject.GetRegisteredProperties(typeof(Class1)).Select(x => x.Name).ToArray();
-
-            Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached" }, names);
-        }
-
-        [Fact]
-        public void GetRegisteredProperties_Returns_Registered_Properties_For_Base_Types()
-        {
-            string[] names = PerspexObject.GetRegisteredProperties(typeof(Class2)).Select(x => x.Name).ToArray();
-
-            Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached" }, names);
-        }
-
-        [Fact]
-        public void GetAttachedProperties_Returns_Registered_Properties_For_Base_Types()
-        {
-            string[] names = PerspexObject.GetAttachedProperties(typeof(AttachedOwner)).Select(x => x.Name).ToArray();
-
-            Assert.Equal(new[] { "Attached" }, names);
-        }
-
         [Fact]
         public void IsSet_Returns_False_For_Unset_Property()
         {

+ 177 - 0
tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs

@@ -0,0 +1,177 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Linq;
+using System.Reactive.Linq;
+using Xunit;
+
+namespace Perspex.Base.UnitTests
+{
+    public class PerspexPropertyRegistryTests
+    {
+        public PerspexPropertyRegistryTests()
+        {
+            // Ensure properties are registered.
+            PerspexProperty p;
+            p = Class1.FooProperty;
+            p = Class2.BarProperty;
+            p = AttachedOwner.AttachedProperty;
+        }
+
+        [Fact]
+        public void GetRegistered_Returns_Registered_Properties()
+        {
+            string[] names = PerspexPropertyRegistry.Instance.GetRegistered(typeof(Class1))
+                .Select(x => x.Name)
+                .ToArray();
+
+            Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached" }, names);
+        }
+
+        [Fact]
+        public void GetRegistered_Returns_Registered_Properties_For_Base_Types()
+        {
+            string[] names = PerspexPropertyRegistry.Instance.GetRegistered(typeof(Class2))
+                .Select(x => x.Name)
+                .ToArray();
+
+            Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached" }, names);
+        }
+
+        [Fact]
+        public void GetAttached_Returns_Registered_Properties_For_Base_Types()
+        {
+            string[] names = PerspexPropertyRegistry.Instance.GetAttached(typeof(AttachedOwner)).Select(x => x.Name).ToArray();
+
+            Assert.Equal(new[] { "Attached" }, names);
+        }
+
+        [Fact]
+        public void FindRegistered_Finds_Untyped_Property()
+        {
+            var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Foo");
+
+            Assert.Equal(Class1.FooProperty, result);
+        }
+
+        [Fact]
+        public void FindRegistered_Finds_Typed_Property()
+        {
+            var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Class1.Foo");
+
+            Assert.Equal(Class1.FooProperty, result);
+        }
+
+        [Fact]
+        public void FindRegistered_Finds_Typed_Inherited_Property()
+        {
+            var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Class1.Foo");
+
+            Assert.Equal(Class2.FooProperty, result);
+        }
+
+        [Fact]
+        public void FindRegistered_Finds_Inherited_Property_With_Derived_Type_Name()
+        {
+            var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Class2.Foo");
+
+            Assert.Equal(Class2.FooProperty, result);
+        }
+
+        [Fact]
+        public void FindRegistered_Finds_Attached_Property()
+        {
+            var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class2), "AttachedOwner.Attached");
+
+            Assert.Equal(AttachedOwner.AttachedProperty, result);
+        }
+
+        [Fact]
+        public void FindRegistered_Finds_AddOwnered_Untyped_Attached_Property()
+        {
+            var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Attached");
+
+            Assert.Equal(AttachedOwner.AttachedProperty, result);
+        }
+
+        [Fact]
+        public void FindRegistered_Finds_AddOwnered_Typed_Attached_Property()
+        {
+            var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Class3.Attached");
+
+            Assert.Equal(AttachedOwner.AttachedProperty, result);
+        }
+
+        [Fact]
+        public void FindRegistered_Finds_AddOwnered_AttachedTyped_Attached_Property()
+        {
+            var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class3), "AttachedOwner.Attached");
+
+            Assert.Equal(AttachedOwner.AttachedProperty, result);
+        }
+
+        [Fact]
+        public void FindRegistered_Finds_AddOwnered_BaseTyped_Attached_Property()
+        {
+            var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Class1.Attached");
+
+            Assert.Equal(AttachedOwner.AttachedProperty, result);
+        }
+
+        [Fact]
+        public void FindRegistered_Doesnt_Find_Nonregistered_Property()
+        {
+            var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Bar");
+
+            Assert.Null(result);
+        }
+
+        [Fact]
+        public void FindRegistered_Doesnt_Find_Nonregistered_Attached_Property()
+        {
+            var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class4), "AttachedOwner.Attached");
+
+            Assert.Null(result);
+        }
+
+        private class Class1 : PerspexObject
+        {
+            public static readonly PerspexProperty<string> FooProperty =
+                PerspexProperty.Register<Class1, string>("Foo");
+
+            public static readonly PerspexProperty<string> BazProperty =
+                PerspexProperty.Register<Class1, string>("Baz");
+
+            public static readonly PerspexProperty<int> QuxProperty =
+                PerspexProperty.Register<Class1, int>("Qux");
+        }
+
+        private class Class2 : Class1
+        {
+            public static readonly PerspexProperty<string> BarProperty =
+                PerspexProperty.Register<Class2, string>("Bar");
+
+            public static readonly PerspexProperty<double> FlobProperty =
+                PerspexProperty.Register<Class2, double>("Flob");
+
+            public static readonly PerspexProperty<double?> FredProperty =
+                PerspexProperty.Register<Class2, double?>("Fred");
+        }
+
+        private class Class3 : Class1
+        {
+            public static readonly PerspexProperty<string> AttachedProperty =
+                AttachedOwner.AttachedProperty.AddOwner<Class3>();
+        }
+
+        public class Class4 : PerspexObject
+        {
+        }
+
+        private class AttachedOwner
+        {
+            public static readonly PerspexProperty<string> AttachedProperty =
+                PerspexProperty.RegisterAttached<AttachedOwner, Class1, string>("Attached");
+        }
+    }
+}

+ 18 - 0
tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs

@@ -161,6 +161,15 @@ namespace Perspex.Base.UnitTests
             Assert.True(p1 == p2);
         }
 
+        [Fact]
+        public void AddOwnered_Property_Should_Have_OwnerType_Set()
+        {
+            var p1 = new PerspexProperty<string>("p1", typeof(Class1));
+            var p2 = p1.AddOwner<Class3>();
+
+            Assert.Equal(typeof(Class3), p2.OwnerType);
+        }
+
         [Fact]
         public void AddOwnered_Direct_Property_Should_Equal_Original()
         {
@@ -172,6 +181,15 @@ namespace Perspex.Base.UnitTests
             Assert.True(p1 == p2);
         }
 
+        [Fact]
+        public void AddOwnered_Direct_Property_Should_Have_OwnerType_Set()
+        {
+            var p1 = new PerspexProperty<string>("d1", typeof(Class1), o => null, (o, v) => { });
+            var p2 = p1.AddOwner<Class3>(o => null, (o, v) => { });
+
+            Assert.Equal(typeof(Class3), p2.OwnerType);
+        }
+
         [Fact]
         public void AddOwner_With_Getter_And_Setter_On_Standard_Property_Should_Throw()
         {

+ 93 - 0
tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs

@@ -0,0 +1,93 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Linq;
+using Perspex.Controls.Generators;
+using Xunit;
+
+namespace Perspex.Controls.UnitTests.Generators
+{
+    public class ItemContainerGeneratorTests
+    {
+        [Fact]
+        public void Materialize_Should_Create_Containers()
+        {
+            var items = new[] { "foo", "bar", "baz" };
+            var owner = new Decorator();
+            var target = new ItemContainerGenerator(owner);
+            var containers = target.Materialize(0, items, null);
+            var result = containers.OfType<TextBlock>().Select(x => x.Text).ToList();
+
+            Assert.Equal(items, result);
+        }
+
+        [Fact]
+        public void ContainerFromIndex_Should_Return_Materialized_Containers()
+        {
+            var items = new[] { "foo", "bar", "baz" };
+            var owner = new Decorator();
+            var target = new ItemContainerGenerator(owner);
+            var containers = target.Materialize(0, items, null).ToList();
+
+            Assert.Equal(containers[0], target.ContainerFromIndex(0));
+            Assert.Equal(containers[1], target.ContainerFromIndex(1));
+            Assert.Equal(containers[2], target.ContainerFromIndex(2));
+        }
+
+        [Fact]
+        public void IndexFromContainer_Should_Return_Index()
+        {
+            var items = new[] { "foo", "bar", "baz" };
+            var owner = new Decorator();
+            var target = new ItemContainerGenerator(owner);
+            var containers = target.Materialize(0, items, null).ToList();
+
+            Assert.Equal(0, target.IndexFromContainer(containers[0]));
+            Assert.Equal(1, target.IndexFromContainer(containers[1]));
+            Assert.Equal(2, target.IndexFromContainer(containers[2]));
+        }
+
+        [Fact]
+        public void Dematerialize_Should_Remove_Container()
+        {
+            var items = new[] { "foo", "bar", "baz" };
+            var owner = new Decorator();
+            var target = new ItemContainerGenerator(owner);
+            var containers = target.Materialize(0, items, null).ToList();
+
+            target.Dematerialize(1, 1);
+
+            Assert.Equal(containers[0], target.ContainerFromIndex(0));
+            Assert.Equal(null, target.ContainerFromIndex(1));
+            Assert.Equal(containers[2], target.ContainerFromIndex(2));
+        }
+
+        [Fact]
+        public void Dematerialize_Should_Return_Removed_Containers()
+        {
+            var items = new[] { "foo", "bar", "baz" };
+            var owner = new Decorator();
+            var target = new ItemContainerGenerator(owner);
+            var containers = target.Materialize(0, items, null);
+            var expected = target.Containers.Take(2).ToList();
+            var result = target.Dematerialize(0, 2);
+
+            Assert.Equal(expected, result);
+        }
+
+        [Fact]
+        public void RemoveRange_Should_Alter_Successive_Container_Indexes()
+        {
+            var items = new[] { "foo", "bar", "baz" };
+            var owner = new Decorator();
+            var target = new ItemContainerGenerator(owner);
+            var containers = target.Materialize(0, items, null).ToList();
+
+            var removed = target.RemoveRange(1, 1).Single();
+
+            Assert.Equal(containers[0], target.ContainerFromIndex(0));
+            Assert.Equal(containers[2], target.ContainerFromIndex(1));
+            Assert.Equal(containers[1], removed);
+        }
+    }
+}

+ 28 - 0
tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs

@@ -0,0 +1,28 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Linq;
+using Perspex.Controls.Generators;
+using Xunit;
+
+namespace Perspex.Controls.UnitTests.Generators
+{
+    public class ItemContainerGeneratorTypedTests
+    {
+        [Fact]
+        public void Materialize_Should_Create_Containers()
+        {
+            var items = new[] { "foo", "bar", "baz" };
+            var owner = new Decorator();
+            var target = new ItemContainerGenerator<ListBoxItem>(owner, ListBoxItem.ContentProperty);
+            var containers = target.Materialize(0, items, null);
+            var result = containers
+                .OfType<ListBoxItem>()
+                .Select(x => x.Content)
+                .OfType<TextBlock>()
+                .Select(x => x.Text).ToList();
+
+            Assert.Equal(items, result);
+        }
+    }
+}

+ 2 - 0
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@@ -81,6 +81,8 @@
     <Otherwise />
   </Choose>
   <ItemGroup>
+    <Compile Include="Generators\ItemContainerGeneratorTests.cs" />
+    <Compile Include="Generators\ItemContainerGeneratorTypedTests.cs" />
     <Compile Include="GridLengthTests.cs" />
     <Compile Include="ContentPresenterTests.cs" />
     <Compile Include="BorderTests.cs" />

+ 1 - 1
tests/Perspex.Controls.UnitTests/Presenters/CarouselPresenterTests.cs

@@ -74,7 +74,7 @@ namespace Perspex.Controls.UnitTests.Presenters
         {
             protected override IItemContainerGenerator CreateItemContainerGenerator()
             {
-                return new ItemContainerGenerator<TestItem>(this);
+                return new ItemContainerGenerator<TestItem>(this, TestItem.ContentProperty);
             }
         }
     }

+ 70 - 2
tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Perspex Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System.Collections.ObjectModel;
 using System.Linq;
 using Perspex.Collections;
 using Perspex.Controls.Generators;
@@ -39,7 +40,9 @@ namespace Perspex.Controls.UnitTests.Presenters
                 Items = new[] { "foo", "bar" },
             };
 
-            target.ItemContainerGenerator = new ItemContainerGenerator<ListBoxItem>(target);
+            target.ItemContainerGenerator = new ItemContainerGenerator<ListBoxItem>(
+                target, 
+                ListBoxItem.ContentProperty);
             target.ApplyTemplate();
 
             Assert.Equal(2, target.Panel.Children.Count);
@@ -73,10 +76,67 @@ namespace Perspex.Controls.UnitTests.Presenters
 
             Assert.Equal(1, target.Panel.Children.Count);
             Assert.Equal("bar", ((TextBlock)target.Panel.Children[0]).Text);
+            Assert.Equal("bar", ((TextBlock)target.ItemContainerGenerator.ContainerFromIndex(0)).Text);
         }
 
         [Fact]
         public void Clearing_Items_Should_Remove_Containers()
+        {
+            var items = new ObservableCollection<string> { "foo", "bar" };
+            var target = new ItemsPresenter
+            {
+                Items = items,
+            };
+
+            target.ApplyTemplate();
+            items.Clear();
+
+            Assert.Empty(target.Panel.Children);
+            Assert.Empty(target.ItemContainerGenerator.Containers);
+        }
+
+        [Fact]
+        public void Replacing_Items_Should_Update_Containers()
+        {
+            var items = new ObservableCollection<string> { "foo", "bar", "baz" };
+            var target = new ItemsPresenter
+            {
+                Items = items,
+            };
+
+            target.ApplyTemplate();
+            items[1] = "baz";
+
+            var text = target.Panel.Children
+                .OfType<TextBlock>()
+                .Select(x => x.Text)
+                .ToList();
+
+            Assert.Equal(new[] { "foo", "baz", "baz" }, text);
+        }
+
+        [Fact]
+        public void Moving_Items_Should_Update_Containers()
+        {
+            var items = new ObservableCollection<string> { "foo", "bar", "baz" };
+            var target = new ItemsPresenter
+            {
+                Items = items,
+            };
+
+            target.ApplyTemplate();
+            items.Move(2, 1);
+
+            var text = target.Panel.Children
+                .OfType<TextBlock>()
+                .Select(x => x.Text)
+                .ToList();
+
+            Assert.Equal(new[] { "foo", "baz", "bar" }, text);
+        }
+
+        [Fact]
+        public void Setting_Items_To_Null_Should_Remove_Containers()
         {
             var target = new ItemsPresenter
             {
@@ -87,6 +147,7 @@ namespace Perspex.Controls.UnitTests.Presenters
             target.Items = null;
 
             Assert.Empty(target.Panel.Children);
+            Assert.Empty(target.ItemContainerGenerator.Containers);
         }
 
         [Fact]
@@ -102,12 +163,19 @@ namespace Perspex.Controls.UnitTests.Presenters
             target.ApplyTemplate();
 
             var text = target.Panel.Children.Cast<TextBlock>().Select(x => x.Text).ToList();
+
             Assert.Equal(new[] { "foo", "bar" }, text);
+            Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(0));
+            Assert.Null(target.ItemContainerGenerator.ContainerFromIndex(1));
+            Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(2));
 
             items.RemoveAt(1);
 
             text = target.Panel.Children.Cast<TextBlock>().Select(x => x.Text).ToList();
+
             Assert.Equal(new[] { "foo", "bar" }, text);
+            Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(0));
+            Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(1));
         }
 
         [Fact]
@@ -231,7 +299,7 @@ namespace Perspex.Controls.UnitTests.Presenters
         {
             protected override IItemContainerGenerator CreateItemContainerGenerator()
             {
-                return new ItemContainerGenerator<TestItem>(this);
+                return new ItemContainerGenerator<TestItem>(this, TestItem.ContentProperty);
             }
         }
     }

+ 195 - 25
tests/Perspex.Controls.UnitTests/TreeViewTests.cs

@@ -1,36 +1,125 @@
 // Copyright (c) The Perspex Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
-using System;
+using System.Collections.Generic;
 using System.Linq;
-using Perspex.Controls;
 using Perspex.Controls.Presenters;
 using Perspex.Controls.Templates;
+using Perspex.Input;
 using Perspex.LogicalTree;
-using Perspex.Styling;
 using Xunit;
 
 namespace Perspex.Controls.UnitTests
 {
     public class TreeViewTests
     {
+        [Fact]
+        public void Items_Should_Be_Created()
+        {
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = CreateTestTreeData(),
+                DataTemplates = CreateNodeDataTemplate(),
+            };
+
+            target.ApplyTemplate();
+
+            Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0));
+            Assert.Equal(new[] { "Child1", "Child2" }, ExtractItemHeader(target, 1));
+            Assert.Equal(new[] { "Grandchild2a" }, ExtractItemHeader(target, 2));
+        }
+
+        [Fact]
+        public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers()
+        {
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = CreateTestTreeData(),
+                DataTemplates = CreateNodeDataTemplate(),
+            };
+
+            target.ApplyTemplate();
+
+            var container = (TreeViewItem)target.ItemContainerGenerator.Containers.Single();
+            var header = (TextBlock)container.Header;
+            Assert.Equal("Root", header.Text);
+        }
+
+        [Fact]
+        public void Root_TreeContainerFromItem_Should_Return_Descendent_Item()
+        {
+            var tree = CreateTestTreeData();
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = tree,
+                DataTemplates = CreateNodeDataTemplate(),
+            };
+
+            // For TreeViewItem to find its parent TreeView, OnAttachedToVisualTree needs
+            // to be called, which requires an IRenderRoot.
+            var visualRoot = new TestRoot();
+            visualRoot.Child = target;
+
+            ApplyTemplates(target);
+
+            var container = target.ItemContainerGenerator.TreeContainerFromItem(
+                tree[0].Children[1].Children[0]);
+            var header = ((TreeViewItem)container).Header;
+            var headerContent = ((TextBlock)header).Text;
+
+            Assert.Equal("Grandchild2a", headerContent);
+        }
+
+        [Fact]
+        public void Clicking_Item_Should_Select_It()
+        {
+            var tree = CreateTestTreeData();
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = tree,
+                DataTemplates = CreateNodeDataTemplate(),
+            };
+
+            var visualRoot = new TestRoot();
+            visualRoot.Child = target;
+            ApplyTemplates(target);
+
+            var item = tree[0].Children[1].Children[0];
+            var container = (TreeViewItem)target.ItemContainerGenerator.TreeContainerFromItem(item);
+
+            container.RaiseEvent(new PointerPressEventArgs
+            {
+                RoutedEvent = InputElement.PointerPressedEvent,
+                MouseButton = MouseButton.Left,
+            });
+
+            Assert.Equal(item, target.SelectedItem);
+            Assert.True(container.IsSelected);
+        }
+
         [Fact]
         public void LogicalChildren_Should_Be_Set()
         {
             var target = new TreeView
             {
-                Template = new FuncControlTemplate(CreateTreeViewTemplate),
+                Template = CreateTreeViewTemplate(),
                 Items = new[] { "Foo", "Bar", "Baz " },
             };
 
             target.ApplyTemplate();
 
-            Assert.Equal(3, target.GetLogicalChildren().Count());
+            var result = target.GetLogicalChildren()
+                .OfType<TreeViewItem>()
+                .Select(x => x.Header)
+                .OfType<TextBlock>()
+                .Select(x => x.Text)
+                .ToList();
 
-            foreach (var child in target.GetLogicalChildren())
-            {
-                Assert.IsType<TreeViewItem>(child);
-            }
+            Assert.Equal(new[] { "Foo", "Bar", "Baz " }, result);
         }
 
         [Fact]
@@ -39,18 +128,18 @@ namespace Perspex.Controls.UnitTests
             var items = new object[]
             {
                 "Foo",
-                new Item("Bar"),
+                new Node { Value = "Bar" },
                 new TextBlock { Text = "Baz" },
                 new TreeViewItem { Header = "Qux" },
             };
 
             var target = new TreeView
             {
-                Template = new FuncControlTemplate(CreateTreeViewTemplate),
+                Template = CreateTreeViewTemplate(),
                 DataContext = "Base",
                 DataTemplates = new DataTemplates
                 {
-                    new FuncDataTemplate<Item>(x => new Button { Content = x })
+                    new FuncDataTemplate<Node>(x => new Button { Content = x })
                 },
                 Items = items,
             };
@@ -67,35 +156,116 @@ namespace Perspex.Controls.UnitTests
                 dataContexts);
         }
 
-        private Control CreateTreeViewTemplate(ITemplatedControl parent)
+        private void ApplyTemplates(TreeView tree)
         {
-            return new ScrollViewer
+            tree.ApplyTemplate();
+            ApplyTemplates(tree.Presenter.Panel.Children);
+        }
+
+        private void ApplyTemplates(IEnumerable<IControl> controls)
+        {
+            foreach (TreeViewItem control in controls)
             {
-                Template = new FuncControlTemplate(CreateScrollViewerTemplate),
-                Content = new ItemsPresenter
+                control.Template = CreateTreeViewItemTemplate();
+                control.ApplyTemplate();
+                ApplyTemplates(control.Presenter.Panel.Children);
+            }
+        }
+
+        private IList<Node> CreateTestTreeData()
+        {
+            return new[]
+            {
+                new Node
                 {
-                    Name = "itemsPresenter",
-                    [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty),
+                    Value = "Root",
+                    Children = new[]
+                    {
+                        new Node
+                        {
+                            Value = "Child1",
+                        },
+                        new Node
+                        {
+                            Value = "Child2",
+                            Children = new[]
+                            {
+                                new Node
+                                {
+                                    Value = "Grandchild2a",
+                                },
+                            },
+                        },
+                    }
                 }
             };
         }
 
-        private Control CreateScrollViewerTemplate(ITemplatedControl parent)
+        private DataTemplates CreateNodeDataTemplate()
         {
-            return new ScrollContentPresenter
+            return new DataTemplates
             {
-                [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty),
+                new FuncTreeDataTemplate<Node>(
+                    x => new TextBlock { Text = x.Value },
+                    x => x.Children),
             };
         }
 
-        private class Item
+        private IControlTemplate CreateTreeViewTemplate()
         {
-            public Item(string value)
+            return new FuncControlTemplate<TreeView>(parent => new ItemsPresenter
             {
-                Value = value;
+                Name = "itemsPresenter",
+                [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
+            });
+        }
+
+        private IControlTemplate CreateTreeViewItemTemplate()
+        {
+            return new FuncControlTemplate<TreeViewItem>(parent => new ItemsPresenter
+            {
+                Name = "itemsPresenter",
+                [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
+            });
+        }
+
+        private List<string> ExtractItemHeader(TreeView tree, int level)
+        {
+            return ExtractItemContent(tree.Presenter.Panel, 0, level)
+                .Select(x => x.Header)
+                .OfType<TextBlock>()
+                .Select(x => x.Text)
+                .ToList();
+        }
+
+        private IEnumerable<TreeViewItem> ExtractItemContent(IPanel panel, int currentLevel, int level)
+        {
+            foreach (TreeViewItem container in panel.Children)
+            {
+                if (container.Template == null)
+                {
+                    container.Template = CreateTreeViewItemTemplate();
+                    container.ApplyTemplate();
+                }
+
+                if (currentLevel == level)
+                {
+                    yield return container;
+                }
+                else
+                {
+                    foreach (var child in ExtractItemContent(container.Presenter.Panel, currentLevel + 1, level))
+                    {
+                        yield return child;
+                    }
+                }
             }
+        }
 
-            public string Value { get; }
+        private class Node
+        {
+            public string Value { get; set; }
+            public IList<Node> Children { get; set; }
         }
     }
 }