Browse Source

Merge branch 'devtools'

Steven Kirk 9 years ago
parent
commit
369313da00
27 changed files with 549 additions and 506 deletions
  1. 12 0
      samples/ControlCatalog/App.paml.cs
  2. 9 0
      samples/ControlCatalog/ControlCatalog.csproj
  3. 4 0
      samples/ControlCatalog/packages.config
  4. 10 7
      src/Perspex.Base/PriorityValue.cs
  5. 27 0
      src/Perspex.Base/Threading/DispatcherTimer.cs
  6. 13 0
      src/Perspex.Controls/Generators/ItemContainerEventArgs.cs
  7. 19 0
      src/Perspex.Controls/Generators/TreeContainerIndex.cs
  8. 1 1
      src/Perspex.Controls/Primitives/HeaderedItemsControl.cs
  9. 56 1
      src/Perspex.Controls/TreeView.cs
  10. 1 21
      src/Perspex.Controls/TreeViewItem.cs
  11. 0 120
      src/Perspex.Diagnostics/DevTools.cs
  12. 18 0
      src/Perspex.Diagnostics/DevTools.paml
  13. 92 0
      src/Perspex.Diagnostics/DevTools.paml.cs
  14. 21 6
      src/Perspex.Diagnostics/Perspex.Diagnostics.csproj
  15. 1 1
      src/Perspex.Diagnostics/ViewLocator.cs
  16. 44 25
      src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs
  17. 4 4
      src/Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs
  18. 0 34
      src/Perspex.Diagnostics/ViewModels/LogicalTreeViewModel.cs
  19. 16 4
      src/Perspex.Diagnostics/ViewModels/TreeNode.cs
  20. 102 0
      src/Perspex.Diagnostics/ViewModels/TreePageViewModel.cs
  21. 5 5
      src/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs
  22. 0 34
      src/Perspex.Diagnostics/ViewModels/VisualTreeViewModel.cs
  23. 0 99
      src/Perspex.Diagnostics/Views/LogicalTreeView.cs
  24. 0 43
      src/Perspex.Diagnostics/Views/TreePage.cs
  25. 70 0
      src/Perspex.Diagnostics/Views/TreePage.paml.cs
  26. 24 0
      src/Perspex.Diagnostics/Views/TreePageView.paml
  27. 0 101
      src/Perspex.Diagnostics/Views/VisualTreeView.cs

+ 12 - 0
samples/ControlCatalog/App.paml.cs

@@ -5,6 +5,7 @@ using Perspex.Controls;
 using Perspex.Diagnostics;
 using Perspex.Markup.Xaml;
 using Perspex.Themes.Default;
+using Serilog;
 
 namespace ControlCatalog
 {
@@ -14,6 +15,7 @@ namespace ControlCatalog
         {
             RegisterServices();
             InitializeSubsystems(GetPlatformId());
+            InitializeLogging();
             Styles = new DefaultTheme();
             InitializeComponent();
         }
@@ -38,6 +40,16 @@ namespace ControlCatalog
             PerspexXamlLoader.Load(this);
         }
 
+        private void InitializeLogging()
+        {
+#if DEBUG
+            Log.Logger = new LoggerConfiguration()
+                .MinimumLevel.Error()
+                .WriteTo.Trace(outputTemplate: "{Message}")
+                .CreateLogger();
+#endif
+        }
+
         private int GetPlatformId()
         {
             var args = Environment.GetCommandLineArgs();

+ 9 - 0
samples/ControlCatalog/ControlCatalog.csproj

@@ -36,6 +36,14 @@
     <StartupObject />
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="Serilog, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
+      <HintPath>..\..\packages\Serilog.1.5.9\lib\net45\Serilog.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
+    <Reference Include="Serilog.FullNetFx, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
+      <HintPath>..\..\packages\Serilog.1.5.9\lib\net45\Serilog.FullNetFx.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
     <Reference Include="System.Xml.Linq" />
@@ -93,6 +101,7 @@
       <SubType>Designer</SubType>
     </EmbeddedResource>
     <EmbeddedResource Include="Pages\DropDownPage.paml" />
+    <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Gtk\Perspex.Cairo\Perspex.Cairo.csproj">

+ 4 - 0
samples/ControlCatalog/packages.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="Serilog" version="1.5.9" targetFramework="net46" />
+</packages>

+ 10 - 7
src/Perspex.Base/PriorityValue.cs

@@ -224,26 +224,29 @@ namespace Perspex
         /// <param name="priority">The priority level that the value came from.</param>
         private void UpdateValue(object value, int priority)
         {
-            if (TypeUtilities.TryCast(_valueType, value, out value))
+            object castValue;
+
+            if (TypeUtilities.TryCast(_valueType, value, out castValue))
             {
                 var old = _value;
 
-                if (_validate != null && value != PerspexProperty.UnsetValue)
+                if (_validate != null && castValue != PerspexProperty.UnsetValue)
                 {
-                    value = _validate(value);
+                    castValue = _validate(castValue);
                 }
 
                 ValuePriority = priority;
-                _value = value;
+                _value = castValue;
                 _changed.OnNext(Tuple.Create(old, _value));
             }
             else if (_logger != null)
             {
                 _logger.Error(
-                    "Binding produced invalid value for {$Type} {$Property}: {$Value}",
-                    _valueType,
+                    "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
                     _name,
-                    value);
+                    _valueType,
+                    value,
+                    value.GetType());
             }
         }
 

+ 27 - 0
src/Perspex.Base/Threading/DispatcherTimer.cs

@@ -146,6 +146,33 @@ namespace Perspex.Threading
             return Disposable.Create(() => timer.Stop());
         }
 
+        /// <summary>
+        /// Runs a method once, after the specified interval.
+        /// </summary>
+        /// <param name="action">
+        /// The method to call after the interval has elapsed.
+        /// </param>
+        /// <param name="interval">The interval after which to call the method.</param>
+        /// <param name="priority">The priority to use.</param>
+        /// <returns>An <see cref="IDisposable"/> used to cancel the timer.</returns>
+        public static IDisposable RunOnce(
+            Action action,
+            TimeSpan interval,
+            DispatcherPriority priority = DispatcherPriority.Normal)
+        {
+            var timer = new DispatcherTimer(priority) { Interval = interval };
+
+            timer.Tick += (s, e) =>
+            {
+                action();
+                timer.Stop();
+            };
+
+            timer.Start();
+
+            return Disposable.Create(() => timer.Stop());
+        }
+
         /// <summary>
         /// Starts the timer.
         /// </summary>

+ 13 - 0
src/Perspex.Controls/Generators/ItemContainerEventArgs.cs

@@ -12,6 +12,19 @@ namespace Perspex.Controls.Generators
     /// </summary>
     public class ItemContainerEventArgs : EventArgs
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
+        /// </summary>
+        /// <param name="startingIndex">The index of the first container in the source items.</param>
+        /// <param name="container">The container.</param>
+        public ItemContainerEventArgs(
+            int startingIndex,
+            ItemContainer container)
+        {
+            StartingIndex = startingIndex;
+            Containers = new[] { container };
+        }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
         /// </summary>

+ 19 - 0
src/Perspex.Controls/Generators/TreeContainerIndex.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;
 using System.Collections.Generic;
 
 namespace Perspex.Controls.Generators
@@ -19,6 +20,16 @@ namespace Perspex.Controls.Generators
         private readonly Dictionary<object, IControl> _itemToContainer = new Dictionary<object, IControl>();
         private readonly Dictionary<IControl, object> _containerToItem = new Dictionary<IControl, object>();
 
+        /// <summary>
+        /// Signalled whenever new containers are materialized.
+        /// </summary>
+        public event EventHandler<ItemContainerEventArgs> Materialized;
+
+        /// <summary>
+        /// Event raised whenever containers are dematerialized.
+        /// </summary>
+        public event EventHandler<ItemContainerEventArgs> Dematerialized;
+
         /// <summary>
         /// Gets the currently materialized containers.
         /// </summary>
@@ -33,6 +44,10 @@ namespace Perspex.Controls.Generators
         {
             _itemToContainer.Add(item, container);
             _containerToItem.Add(container, item);
+
+            Materialized?.Invoke(
+                this, 
+                new ItemContainerEventArgs(0, new ItemContainer(container, item, 0)));
         }
 
         /// <summary>
@@ -44,6 +59,10 @@ namespace Perspex.Controls.Generators
             var item = _containerToItem[container];
             _containerToItem.Remove(container);
             _itemToContainer.Remove(item);
+
+            Dematerialized?.Invoke(
+                this, 
+                new ItemContainerEventArgs(0, new ItemContainer(container, item, 0)));
         }
 
         /// <summary>

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

@@ -51,8 +51,8 @@ namespace Perspex.Controls.Primitives
         /// <inheritdoc/>
         protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
         {
-            base.OnTemplateApplied(e);
             HeaderPresenter = e.NameScope.Find<ContentPresenter>("PART_HeaderPresenter");
+            base.OnTemplateApplied(e);
         }
     }
 }

+ 56 - 1
src/Perspex.Controls/TreeView.cs

@@ -8,6 +8,7 @@ using Perspex.Controls.Primitives;
 using Perspex.Input;
 using Perspex.Interactivity;
 using Perspex.Styling;
+using Perspex.Threading;
 using Perspex.VisualTree;
 
 namespace Perspex.Controls
@@ -17,6 +18,14 @@ namespace Perspex.Controls
     /// </summary>
     public class TreeView : ItemsControl
     {
+        /// <summary>
+        /// Defines the <see cref="AutoScrollToSelectedItem"/> property.
+        /// </summary>
+        public static readonly PerspexProperty<bool> AutoScrollToSelectedItemProperty =
+            PerspexProperty.Register<TreeView, bool>(
+                nameof(AutoScrollToSelectedItem),
+                defaultValue: true);
+
         /// <summary>
         /// Defines the <see cref="SelectedItem"/> property.
         /// </summary>
@@ -41,6 +50,15 @@ namespace Perspex.Controls
         public new ITreeItemContainerGenerator ItemContainerGenerator => 
             (ITreeItemContainerGenerator)base.ItemContainerGenerator;
 
+        /// <summary>
+        /// Gets or sets a value indicating whether to automatically scroll to newly selected items.
+        /// </summary>
+        public bool AutoScrollToSelectedItem
+        {
+            get { return GetValue(AutoScrollToSelectedItemProperty); }
+            set { SetValue(AutoScrollToSelectedItemProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets the selected item.
         /// </summary>
@@ -65,6 +83,11 @@ namespace Perspex.Controls
                 {
                     var container = ItemContainerGenerator.Index.ContainerFromItem(_selectedItem);
                     MarkContainerSelected(container, true);
+
+                    if (AutoScrollToSelectedItem && container != null)
+                    {
+                        container.BringIntoView();
+                    }
                 }
             }
         }
@@ -72,12 +95,14 @@ namespace Perspex.Controls
         /// <inheritdoc/>
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
-            return new TreeItemContainerGenerator<TreeViewItem>(
+            var result = new TreeItemContainerGenerator<TreeViewItem>(
                 this,
                 TreeViewItem.HeaderProperty,
                 TreeViewItem.ItemsProperty,
                 TreeViewItem.IsExpandedProperty,
                 new TreeContainerIndex());
+            result.Index.Materialized += ContainerMaterialized;
+            return result;
         }
 
         /// <inheritdoc/>
@@ -190,6 +215,36 @@ namespace Perspex.Controls
             return null;
         }
 
+        /// <summary>
+        /// Called when a new item container is materialized, to set its selected state.
+        /// </summary>
+        /// <param name="sender">The event sender.</param>
+        /// <param name="e">The event args.</param>
+        private void ContainerMaterialized(object sender, ItemContainerEventArgs e)
+        {
+            var selectedItem = SelectedItem;
+
+            if (selectedItem != null)
+            {
+                foreach (var container in e.Containers)
+                {
+                    if (container.Item == selectedItem)
+                    {
+                        ((TreeViewItem)container.ContainerControl).IsSelected = true;
+
+                        if (AutoScrollToSelectedItem)
+                        {
+                            DispatcherTimer.RunOnce(
+                                container.ContainerControl.BringIntoView,
+                                TimeSpan.Zero);
+                        }
+
+                        break;
+                    }
+                }
+            }
+        }
+
         /// <summary>
         /// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
         /// </summary>

+ 1 - 21
src/Perspex.Controls/TreeViewItem.cs

@@ -73,16 +73,12 @@ namespace Perspex.Controls
         /// <inheritdoc/>
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
-            var result =  new TreeItemContainerGenerator<TreeViewItem>(
+            return new TreeItemContainerGenerator<TreeViewItem>(
                 this,
                 TreeViewItem.HeaderProperty,
                 TreeViewItem.ItemsProperty,
                 TreeViewItem.IsExpandedProperty,
                 _treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex());
-
-            result.Materialized += ItemMaterialized;
-
-            return result;
         }
 
         /// <inheritdoc/>
@@ -123,21 +119,5 @@ namespace Perspex.Controls
 
             base.OnKeyDown(e);
         }
-
-        private void ItemMaterialized(object sender, ItemContainerEventArgs e)
-        {
-            var selectedItem = _treeView?.SelectedItem;
-
-            if (selectedItem != null)
-            {
-                foreach (var container in e.Containers)
-                {
-                    if (container.Item == selectedItem)
-                    {
-                        ((TreeViewItem)container.ContainerControl).IsSelected = true;
-                    }
-                }
-            }
-        }
     }
 }

+ 0 - 120
src/Perspex.Diagnostics/DevTools.cs

@@ -1,120 +0,0 @@
-// 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.Reactive.Linq;
-using Perspex.Controls;
-using Perspex.Diagnostics.ViewModels;
-using Perspex.Input;
-using Perspex.Themes.Default;
-using ReactiveUI;
-
-namespace Perspex.Diagnostics
-{
-    public class DevTools : Decorator
-    {
-        public static readonly PerspexProperty<Control> RootProperty =
-            PerspexProperty.Register<DevTools, Control>("Root");
-
-        private readonly DevToolsViewModel _viewModel;
-
-        public DevTools()
-        {
-            _viewModel = new DevToolsViewModel();
-            this.GetObservable(RootProperty).Subscribe(x => _viewModel.Root = x);
-
-            InitializeComponent();
-        }
-
-        public Control Root
-        {
-            get { return GetValue(RootProperty); }
-            set { SetValue(RootProperty, value); }
-        }
-
-        public static IDisposable Attach(Window window)
-        {
-            return window.AddHandler(
-                KeyDownEvent,
-                WindowPreviewKeyDown,
-                Interactivity.RoutingStrategies.Tunnel);
-        }
-
-        private static void WindowPreviewKeyDown(object sender, KeyEventArgs e)
-        {
-            if (e.Key == Key.F12)
-            {
-                Window window = new Window
-                {
-                    Width = 1024,
-                    Height = 512,
-                    Content = new DevTools
-                    {
-                        Root = (Window)sender,
-                    },
-                };
-
-                window.Show();
-            }
-        }
-
-        private void InitializeComponent()
-        {
-            DataTemplates.Add(new ViewLocator<ReactiveObject>());
-            Styles.Add(new DefaultTheme());
-
-            Child = new Grid
-            {
-                RowDefinitions = new RowDefinitions("*,Auto"),
-                Children = new Controls.Controls
-                {
-                    new TabControl
-                    {
-                        Items = new[]
-                        {
-                            new TabItem
-                            {
-                                Header = "Logical Tree",
-                                [!ContentControl.ContentProperty] = _viewModel.WhenAnyValue(x => x.LogicalTree),
-                            },
-                            new TabItem
-                            {
-                                Header = "Visual Tree",
-                                [!ContentControl.ContentProperty] = _viewModel.WhenAnyValue(x => x.VisualTree),
-                            }
-                        },
-                    },
-                    new StackPanel
-                    {
-                        Orientation = Orientation.Horizontal,
-                        Gap = 4,
-                        [Grid.RowProperty] = 1,
-                        Children = new Controls.Controls
-                        {
-                            new TextBlock
-                            {
-                                Text = "Focused: "
-                            },
-                            new TextBlock
-                            {
-                                [!TextBlock.TextProperty] = _viewModel
-                                    .WhenAnyValue(x => x.FocusedControl)
-                                    .Select(x => x?.GetType().Name ?? "(null)")
-                            },
-                            new TextBlock
-                            {
-                                Text = "Pointer Over: "
-                            },
-                            new TextBlock
-                            {
-                                [!TextBlock.TextProperty] = _viewModel
-                                    .WhenAnyValue(x => x.PointerOverElement)
-                                    .Select(x => x?.GetType().Name ?? "(null)")
-                            }
-                        }
-                    }
-                }
-            };
-        }
-    }
-}

+ 18 - 0
src/Perspex.Diagnostics/DevTools.paml

@@ -0,0 +1,18 @@
+<UserControl xmlns="https://github.com/perspex">
+  <Grid RowDefinitions="Auto,*,Auto">
+    <TabStrip SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
+      <TabStripItem Content="Logical Tree"/>
+      <TabStripItem Content="Visual Tree"/>
+    </TabStrip>
+
+    <ContentControl Content="{Binding Content}" Grid.Row="1"/> 
+    
+    <StackPanel Gap="4" Orientation="Horizontal" Grid.Row="2">
+      <TextBlock>Focused:</TextBlock>
+      <TextBlock Text="{Binding FocusedControl}"/>
+      <Separator/>
+      <TextBlock>Pointer Over:</TextBlock>
+      <TextBlock Text="{Binding PointerOverElement}"/>
+    </StackPanel>
+  </Grid>
+</UserControl>

+ 92 - 0
src/Perspex.Diagnostics/DevTools.paml.cs

@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using Perspex.Controls;
+using Perspex.Controls.Templates;
+using Perspex.Diagnostics.ViewModels;
+using Perspex.Input;
+using Perspex.Interactivity;
+using Perspex.Markup.Xaml;
+using ReactiveUI;
+
+namespace Perspex.Diagnostics
+{
+    public class DevTools : UserControl
+    {
+        private static Dictionary<Window, Window> s_open = new Dictionary<Window, Window>();
+
+        public DevTools(IControl root)
+        {
+            InitializeComponent();
+            Root = root;
+            DataContext = new DevToolsViewModel(root);
+            Root.PointerMoved += RootPointerMoved;
+        }
+
+        public IControl Root { get; }
+
+        public static IDisposable Attach(Window window)
+        {
+            return window.AddHandler(
+                KeyDownEvent,
+                WindowPreviewKeyDown,
+                RoutingStrategies.Tunnel);
+        }
+
+        private static void WindowPreviewKeyDown(object sender, KeyEventArgs e)
+        {
+            if (e.Key == Key.F12)
+            {
+                var window = (Window)sender;
+                var devToolsWindow = default(Window);
+
+                if (s_open.TryGetValue(window, out devToolsWindow))
+                {
+                    devToolsWindow.Activate();
+                }
+                else
+                {
+                    devToolsWindow = new Window
+                    {
+                        Width = 1024,
+                        Height = 512,
+                        Content = new DevTools(window),
+                        DataTemplates = new DataTemplates
+                        {
+                            new ViewLocator<ReactiveObject>(),
+                        }
+                    };
+
+                    devToolsWindow.Closed += DevToolsClosed;
+                    s_open.Add((Window)sender, devToolsWindow);
+                    devToolsWindow.Show();
+                }
+            }
+        }
+
+        private static void DevToolsClosed(object sender, EventArgs e)
+        {
+            var devToolsWindow = (Window)sender;
+            var devTools = (DevTools)devToolsWindow.Content;
+            var window = (Window)devTools.Root;
+
+            s_open.Remove(window);
+            devToolsWindow.Closed -= DevToolsClosed;
+        }
+
+        private void InitializeComponent()
+        {
+            PerspexXamlLoader.Load(this);
+        }
+
+        private void RootPointerMoved(object sender, PointerEventArgs e)
+        {
+            var modifiers = InputModifiers.Control | InputModifiers.Shift;
+
+            if ((e.InputModifiers & modifiers) == modifiers)
+            {
+                var vm = (DevToolsViewModel)DataContext;
+                vm.SelectControl((IControl)e.Source);
+            }
+        }
+    }
+}

+ 21 - 6
src/Perspex.Diagnostics/Perspex.Diagnostics.csproj

@@ -40,6 +40,14 @@
   </PropertyGroup>
   <ItemGroup>
     <!-- A reference to the entire .NET Framework is automatically included -->
+    <ProjectReference Include="..\Markup\Perspex.Markup.Xaml\Perspex.Markup.Xaml.csproj">
+      <Project>{3e53a01a-b331-47f3-b828-4a5717e77a24}</Project>
+      <Name>Perspex.Markup.Xaml</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\Markup\Perspex.Markup\Perspex.Markup.csproj">
+      <Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project>
+      <Name>Perspex.Markup</Name>
+    </ProjectReference>
     <ProjectReference Include="..\Perspex.Animation\Perspex.Animation.csproj">
       <Project>{D211E587-D8BC-45B9-95A4-F297C8FA5200}</Project>
       <Name>Perspex.Animation</Name>
@@ -86,23 +94,24 @@
       <Link>Properties\SharedAssemblyInfo.cs</Link>
     </Compile>
     <Compile Include="LogManager.cs" />
-    <Compile Include="DevTools.cs" />
     <Compile Include="Debug.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="ViewLocator.cs" />
     <Compile Include="ViewModels\DevToolsViewModel.cs" />
-    <Compile Include="ViewModels\VisualTreeViewModel.cs" />
-    <Compile Include="ViewModels\LogicalTreeViewModel.cs" />
+    <Compile Include="ViewModels\TreePageViewModel.cs" />
     <Compile Include="ViewModels\PropertyDetails.cs" />
     <Compile Include="ViewModels\ControlDetailsViewModel.cs" />
     <Compile Include="ViewModels\LogicalTreeNode.cs" />
     <Compile Include="ViewModels\TreeNode.cs" />
     <Compile Include="ViewModels\VisualTreeNode.cs" />
+    <Compile Include="Views\TreePage.paml.cs">
+      <DependentUpon>TreePageView.paml</DependentUpon>
+    </Compile>
+    <Compile Include="DevTools.paml.cs">
+      <DependentUpon>DevTools.paml</DependentUpon>
+    </Compile>
     <Compile Include="Views\ControlDetailsView.cs" />
     <Compile Include="Views\GridRepeater.cs" />
-    <Compile Include="Views\VisualTreeView.cs" />
-    <Compile Include="Views\LogicalTreeView.cs" />
-    <Compile Include="Views\TreePage.cs" />
   </ItemGroup>
   <ItemGroup>
     <Reference Include="Splat">
@@ -124,6 +133,12 @@
   <ItemGroup>
     <None Include="app.config" />
     <None Include="packages.config" />
+    <EmbeddedResource Include="DevTools.paml">
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
+    <EmbeddedResource Include="Views\TreePageView.paml">
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

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

@@ -7,7 +7,7 @@ using Perspex.Controls.Templates;
 
 namespace Perspex.Diagnostics
 {
-    internal class ViewLocator<TViewModel> : IDataTemplate
+    public class ViewLocator<TViewModel> : IDataTemplate
     {
         public IControl Build(object data)
         {

+ 44 - 25
src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs

@@ -11,54 +11,73 @@ namespace Perspex.Diagnostics.ViewModels
 {
     internal class DevToolsViewModel : ReactiveObject
     {
-        private Control _root;
+        private IControl _root;
 
-        private LogicalTreeViewModel _logicalTree;
+        private ReactiveObject _content;
 
-        private VisualTreeViewModel _visualTree;
+        private int _selectedTab;
 
-        private readonly ObservableAsPropertyHelper<IInputElement> _focusedControl;
+        private TreePageViewModel _logicalTree;
 
-        private readonly ObservableAsPropertyHelper<IInputElement> _pointerOverElement;
+        private TreePageViewModel _visualTree;
 
-        public DevToolsViewModel()
+        private readonly ObservableAsPropertyHelper<string> _focusedControl;
+
+        private readonly ObservableAsPropertyHelper<string> _pointerOverElement;
+
+        public DevToolsViewModel(IControl root)
         {
-            this.WhenAnyValue(x => x.Root).Subscribe(x =>
+            _root = root;
+            _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
+            _visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
+
+            this.WhenAnyValue(x => x.SelectedTab).Subscribe(index =>
             {
-                LogicalTree = new LogicalTreeViewModel(_root);
-                VisualTree = new VisualTreeViewModel(_root);
+                switch (index)
+                {
+                    case 0:
+                        Content = _logicalTree;
+                        break;
+                    case 1:
+                        Content = _visualTree;
+                        break;
+                }
             });
 
             _focusedControl = KeyboardDevice.Instance
                 .WhenAnyValue(x => x.FocusedElement)
+                .Select(x => x?.GetType().Name)
                 .ToProperty(this, x => x.FocusedControl);
 
-            _pointerOverElement = this.WhenAnyValue(x => x.Root, x => x as TopLevel)
-                .Select(x => x?.GetObservable(TopLevel.PointerOverElementProperty) ?? Observable.Empty<IInputElement>())
-                .Switch()
+            _pointerOverElement = root.GetObservable(TopLevel.PointerOverElementProperty)
+                .Select(x => x?.GetType().Name)
                 .ToProperty(this, x => x.PointerOverElement);
         }
 
-        public Control Root
+        public ReactiveObject Content
         {
-            get { return _root; }
-            set { this.RaiseAndSetIfChanged(ref _root, value); }
+            get { return _content; }
+            private set { this.RaiseAndSetIfChanged(ref _content, value); }
         }
 
-        public LogicalTreeViewModel LogicalTree
+        public int SelectedTab
         {
-            get { return _logicalTree; }
-            private set { this.RaiseAndSetIfChanged(ref _logicalTree, value); }
+            get { return _selectedTab; }
+            set { this.RaiseAndSetIfChanged(ref _selectedTab, value); }
         }
 
-        public VisualTreeViewModel VisualTree
-        {
-            get { return _visualTree; }
-            private set { this.RaiseAndSetIfChanged(ref _visualTree, value); }
-        }
+        public string FocusedControl => _focusedControl.Value;
 
-        public IInputElement FocusedControl => _focusedControl.Value;
+        public string PointerOverElement => _pointerOverElement.Value;
 
-        public IInputElement PointerOverElement => _pointerOverElement.Value;
+        public void SelectControl(IControl control)
+        {
+            var tree = Content as TreePageViewModel;
+
+            if (tree != null)
+            {
+                tree.SelectControl(control);
+            }
+        }
     }
 }

+ 4 - 4
src/Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs

@@ -9,16 +9,16 @@ namespace Perspex.Diagnostics.ViewModels
 {
     internal class LogicalTreeNode : TreeNode
     {
-        public LogicalTreeNode(ILogical logical)
-            : base((Control)logical)
+        public LogicalTreeNode(ILogical logical, TreeNode parent)
+            : base((Control)logical, parent)
         {
-            Children = logical.LogicalChildren.CreateDerivedCollection(x => new LogicalTreeNode(x));
+            Children = logical.LogicalChildren.CreateDerivedCollection(x => new LogicalTreeNode(x, this));
         }
 
         public static LogicalTreeNode[] Create(object control)
         {
             var logical = control as ILogical;
-            return logical != null ? new[] { new LogicalTreeNode(logical) } : null;
+            return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null;
         }
     }
 }

+ 0 - 34
src/Perspex.Diagnostics/ViewModels/LogicalTreeViewModel.cs

@@ -1,34 +0,0 @@
-// 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.Reactive.Linq;
-using Perspex.Controls;
-using ReactiveUI;
-
-namespace Perspex.Diagnostics.ViewModels
-{
-    internal class LogicalTreeViewModel : ReactiveObject
-    {
-        private LogicalTreeNode _selected;
-
-        private readonly ObservableAsPropertyHelper<ControlDetailsViewModel> _details;
-
-        public LogicalTreeViewModel(Control root)
-        {
-            Nodes = LogicalTreeNode.Create(root);
-            _details = this.WhenAnyValue(x => x.SelectedNode)
-                .Select(x => x != null ? new ControlDetailsViewModel(x.Control) : null)
-                .ToProperty(this, x => x.Details);
-        }
-
-        public LogicalTreeNode[] Nodes { get; }
-
-        public LogicalTreeNode SelectedNode
-        {
-            get { return _selected; }
-            set { this.RaiseAndSetIfChanged(ref _selected, value); }
-        }
-
-        public ControlDetailsViewModel Details => _details.Value;
-    }
-}

+ 16 - 4
src/Perspex.Diagnostics/ViewModels/TreeNode.cs

@@ -13,10 +13,12 @@ namespace Perspex.Diagnostics.ViewModels
     internal class TreeNode : ReactiveObject
     {
         private string _classes;
+        private bool _isExpanded;
 
-        public TreeNode(Control control)
+        public TreeNode(Control control, TreeNode parent)
         {
             Control = control;
+            Parent = parent;
             Type = control.GetType().Name;
 
             var classesChanged = Observable.FromEventPattern<
@@ -52,13 +54,23 @@ namespace Perspex.Diagnostics.ViewModels
             private set { this.RaiseAndSetIfChanged(ref _classes, value); }
         }
 
-        public string Type
+        public Control Control
         {
             get;
-            private set;
         }
 
-        public Control Control
+        public bool IsExpanded
+        {
+            get { return _isExpanded; }
+            set { this.RaiseAndSetIfChanged(ref _isExpanded, value); }
+        }
+
+        public TreeNode Parent
+        {
+            get;
+        }
+
+        public string Type
         {
             get;
             private set;

+ 102 - 0
src/Perspex.Diagnostics/ViewModels/TreePageViewModel.cs

@@ -0,0 +1,102 @@
+// 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.Reactive.Linq;
+using Perspex.Controls;
+using Perspex.VisualTree;
+using ReactiveUI;
+
+namespace Perspex.Diagnostics.ViewModels
+{
+    internal class TreePageViewModel : ReactiveObject
+    {
+        private TreeNode _selected;
+
+        private readonly ObservableAsPropertyHelper<ControlDetailsViewModel> _details;
+
+        public TreePageViewModel(TreeNode[] nodes)
+        {
+            Nodes = nodes;
+            _details = this.WhenAnyValue(x => x.SelectedNode)
+                .Select(x => x != null ? new ControlDetailsViewModel(x.Control) : null)
+                .ToProperty(this, x => x.Details);
+        }
+
+        public TreeNode[] Nodes { get; protected set; }
+
+        public TreeNode SelectedNode
+        {
+            get { return _selected; }
+            set { this.RaiseAndSetIfChanged(ref _selected, value); }
+        }
+
+        public ControlDetailsViewModel Details => _details.Value;
+
+        public TreeNode FindNode(IControl control)
+        {
+            foreach (var node in Nodes)
+            {
+                var result = FindNode(node, control);
+
+                if (result != null)
+                {
+                    return result;
+                }
+            }
+
+            return null;
+        }
+
+        public void SelectControl(IControl control)
+        {
+            var node = default(TreeNode);
+
+            while (node == null && control != null)
+            {
+                node = FindNode(control);
+
+                if (node == null)
+                {
+                    control = control.GetVisualParent<IControl>();
+                }
+            }            
+
+            if (node != null)
+            {
+                SelectedNode = node;
+                ExpandNode(node.Parent);
+            }
+        }
+
+        private void ExpandNode(TreeNode node)
+        {
+            if (node != null)
+            {
+                node.IsExpanded = true;
+                ExpandNode(node.Parent);
+            }
+        }
+
+        private TreeNode FindNode(TreeNode node, IControl control)
+        {
+            if (node.Control == control)
+            {
+                return node;
+            }
+            else
+            {
+                foreach (var child in node.Children)
+                {
+                    var result = FindNode(child, control);
+
+                    if (result != null)
+                    {
+                        return result;
+                    }
+                }
+            }
+
+            return null;
+        }
+    }
+}

+ 5 - 5
src/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs

@@ -9,18 +9,18 @@ namespace Perspex.Diagnostics.ViewModels
 {
     internal class VisualTreeNode : TreeNode
     {
-        public VisualTreeNode(IVisual visual)
-            : base((Control)visual)
+        public VisualTreeNode(IVisual visual, TreeNode parent)
+            : base((Control)visual, parent)
         {
             var host = visual as IVisualTreeHost;
 
             if (host?.Root == null)
             {
-                Children = visual.VisualChildren.CreateDerivedCollection(x => new VisualTreeNode(x));
+                Children = visual.VisualChildren.CreateDerivedCollection(x => new VisualTreeNode(x, this));
             }
             else
             {
-                Children = new ReactiveList<VisualTreeNode>(new[] { new VisualTreeNode(host.Root) });
+                Children = new ReactiveList<VisualTreeNode>(new[] { new VisualTreeNode(host.Root, this) });
             }
 
             if (Control != null)
@@ -34,7 +34,7 @@ namespace Perspex.Diagnostics.ViewModels
         public static VisualTreeNode[] Create(object control)
         {
             var visual = control as IVisual;
-            return visual != null ? new[] { new VisualTreeNode(visual) } : null;
+            return visual != null ? new[] { new VisualTreeNode(visual, null) } : null;
         }
     }
 }

+ 0 - 34
src/Perspex.Diagnostics/ViewModels/VisualTreeViewModel.cs

@@ -1,34 +0,0 @@
-// 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.Reactive.Linq;
-using Perspex.Controls;
-using ReactiveUI;
-
-namespace Perspex.Diagnostics.ViewModels
-{
-    internal class VisualTreeViewModel : ReactiveObject
-    {
-        private VisualTreeNode _selected;
-
-        private readonly ObservableAsPropertyHelper<ControlDetailsViewModel> _details;
-
-        public VisualTreeViewModel(Control root)
-        {
-            Nodes = VisualTreeNode.Create(root);
-            _details = this.WhenAnyValue(x => x.SelectedNode)
-                .Select(x => x != null ? new ControlDetailsViewModel(x.Control) : null)
-                .ToProperty(this, x => x.Details);
-        }
-
-        public VisualTreeNode[] Nodes { get; }
-
-        public VisualTreeNode SelectedNode
-        {
-            get { return _selected; }
-            set { this.RaiseAndSetIfChanged(ref _selected, value); }
-        }
-
-        public ControlDetailsViewModel Details => _details.Value;
-    }
-}

+ 0 - 99
src/Perspex.Diagnostics/Views/LogicalTreeView.cs

@@ -1,99 +0,0 @@
-// 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.Reactive.Linq;
-using Perspex.Controls;
-using Perspex.Controls.Templates;
-using Perspex.Diagnostics.ViewModels;
-using ReactiveUI;
-
-namespace Perspex.Diagnostics.Views
-{
-    using Controls = Controls.Controls;
-
-    internal class LogicalTreeView : TreePage
-    {
-        private static readonly PerspexProperty<LogicalTreeViewModel> ViewModelProperty =
-            PerspexProperty.Register<LogicalTreeView, LogicalTreeViewModel>("ViewModel");
-
-        public LogicalTreeView()
-        {
-            InitializeComponent();
-            this.GetObservable(DataContextProperty)
-                .Subscribe(x => ViewModel = (LogicalTreeViewModel)x);
-        }
-
-        public LogicalTreeViewModel ViewModel
-        {
-            get { return GetValue(ViewModelProperty); }
-            private set { SetValue(ViewModelProperty, value); }
-        }
-
-        private void InitializeComponent()
-        {
-            TreeView tree;
-
-            Content = new Grid
-            {
-                ColumnDefinitions = new ColumnDefinitions
-                {
-                    new ColumnDefinition(1, GridUnitType.Star),
-                    new ColumnDefinition(4, GridUnitType.Pixel),
-                    new ColumnDefinition(3, GridUnitType.Star),
-                },
-                Children = new Controls
-                {
-                    (tree = new TreeView
-                    {
-                        DataTemplates = new DataTemplates
-                        {
-                            new FuncTreeDataTemplate<LogicalTreeNode>(GetHeader, x => x.Children),
-                        },
-                        [!ItemsControl.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Nodes),
-                    }),
-                    new GridSplitter
-                    {
-                        Width = 4,
-                        Orientation = Orientation.Vertical,
-                        [Grid.ColumnProperty] = 1,
-                    },
-                    new ContentControl
-                    {
-                        [!ContentProperty] = this.WhenAnyValue(x => x.ViewModel.Details),
-                        [Grid.ColumnProperty] = 2,
-                    }
-                }
-            };
-
-            tree.GetObservable(TreeView.SelectedItemProperty)
-                .OfType<LogicalTreeNode>()
-                .Subscribe(x => ViewModel.SelectedNode = x);
-        }
-
-        private Control GetHeader(LogicalTreeNode node)
-        {
-            var result = new StackPanel
-            {
-                Orientation = Orientation.Horizontal,
-                Gap = 8,
-                Children = new Controls
-                {
-                    new TextBlock
-                    {
-                        Text = node.Type,
-                    },
-                    new TextBlock
-                    {
-                        [!TextBlock.TextProperty] = node.WhenAnyValue(x => x.Classes),
-                    }
-                }
-            };
-
-            result.PointerEnter += AddAdorner;
-            result.PointerLeave += RemoveAdorner;
-
-            return result;
-        }
-    }
-}

+ 0 - 43
src/Perspex.Diagnostics/Views/TreePage.cs

@@ -1,43 +0,0 @@
-// 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 Perspex.Controls;
-using Perspex.Controls.Primitives;
-using Perspex.Controls.Shapes;
-using Perspex.Diagnostics.ViewModels;
-using Perspex.Input;
-using Perspex.Media;
-
-namespace Perspex.Diagnostics.Views
-{
-    internal class TreePage : UserControl
-    {
-        private Control _adorner;
-
-        protected void AddAdorner(object sender, PointerEventArgs e)
-        {
-            var node = (TreeNode)((Control)sender).DataContext;
-            var layer = AdornerLayer.GetAdornerLayer(node.Control);
-
-            if (layer != null)
-            {
-                _adorner = new Rectangle
-                {
-                    Fill = new SolidColorBrush(0x80a0c5e8),
-                    [AdornerLayer.AdornedElementProperty] = node.Control,
-                };
-
-                layer.Children.Add(_adorner);
-            }
-        }
-
-        protected void RemoveAdorner(object sender, PointerEventArgs e)
-        {
-            if (_adorner != null)
-            {
-                ((Panel)_adorner.Parent).Children.Remove(_adorner);
-                _adorner = null;
-            }
-        }
-    }
-}

+ 70 - 0
src/Perspex.Diagnostics/Views/TreePage.paml.cs

@@ -0,0 +1,70 @@
+using Perspex.Controls;
+using Perspex.Controls.Generators;
+using Perspex.Controls.Primitives;
+using Perspex.Controls.Shapes;
+using Perspex.Diagnostics.ViewModels;
+using Perspex.Input;
+using Perspex.Markup.Xaml;
+using Perspex.Media;
+
+namespace Perspex.Diagnostics.Views
+{
+    public class TreePageView : UserControl
+    {
+        private Control _adorner;
+        private TreeView _tree;
+
+        public TreePageView()
+        {
+            this.InitializeComponent();
+            _tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
+        }
+
+        protected void AddAdorner(object sender, PointerEventArgs e)
+        {
+            var node = (TreeNode)((Control)sender).DataContext;
+            var layer = AdornerLayer.GetAdornerLayer(node.Control);
+
+            if (layer != null)
+            {
+                _adorner = new Rectangle
+                {
+                    Fill = new SolidColorBrush(0x80a0c5e8),
+                    [AdornerLayer.AdornedElementProperty] = node.Control,
+                };
+
+                layer.Children.Add(_adorner);
+            }
+        }
+
+        protected void RemoveAdorner(object sender, PointerEventArgs e)
+        {
+            if (_adorner != null)
+            {
+                ((Panel)_adorner.Parent).Children.Remove(_adorner);
+                _adorner = null;
+            }
+        }
+
+        private void InitializeComponent()
+        {
+            PerspexXamlLoader.Load(this);
+            _tree = this.FindControl<TreeView>("tree");
+        }
+
+        private void TreeViewItemMaterialized(object sender, ItemContainerEventArgs e)
+        {
+            var item = (TreeViewItem)e.Containers[0].ContainerControl;
+            item.TemplateApplied += TreeViewItemTemplateApplied;
+        }
+
+        private void TreeViewItemTemplateApplied(object sender, TemplateAppliedEventArgs e)
+        {
+            var item = (TreeViewItem)sender;
+            var header = item.HeaderPresenter.Child;
+            header.PointerEnter += AddAdorner;
+            header.PointerLeave += RemoveAdorner;
+            item.TemplateApplied -= TreeViewItemTemplateApplied;
+        }
+    }
+}

+ 24 - 0
src/Perspex.Diagnostics/Views/TreePageView.paml

@@ -0,0 +1,24 @@
+<UserControl xmlns="https://github.com/perspex"
+             xmlns:vm="clr-namespace:Perspex.Diagnostics.ViewModels;assembly=Perspex.Diagnostics">
+  <Grid ColumnDefinitions="*,4,3*">    
+    <TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
+      <TreeView.DataTemplates>
+        <TreeDataTemplate DataType="vm:TreeNode"
+                          ItemsSource="{Binding Children}">
+          <StackPanel Orientation="Horizontal" Gap="8">
+            <TextBlock Text="{Binding Type}"/>
+            <TextBlock Text="{Binding Classes}"/>
+          </StackPanel>
+        </TreeDataTemplate>
+      </TreeView.DataTemplates>
+      <TreeView.Styles>
+        <Style Selector="TreeViewItem">
+          <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
+        </Style>
+      </TreeView.Styles>
+    </TreeView>
+
+    <GridSplitter Width="4" Orientation="Vertical" Grid.Column="1"/>
+    <ContentControl Content="{Binding Details}" Grid.Column="2"/>
+  </Grid>
+</UserControl>

+ 0 - 101
src/Perspex.Diagnostics/Views/VisualTreeView.cs

@@ -1,101 +0,0 @@
-// 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.Reactive.Linq;
-using Perspex.Controls;
-using Perspex.Controls.Templates;
-using Perspex.Diagnostics.ViewModels;
-using Perspex.Media;
-using ReactiveUI;
-
-namespace Perspex.Diagnostics.Views
-{
-    using Controls = Controls.Controls;
-
-    internal class VisualTreeView : TreePage
-    {
-        private static readonly PerspexProperty<VisualTreeViewModel> ViewModelProperty =
-            PerspexProperty.Register<VisualTreeView, VisualTreeViewModel>("ViewModel");
-
-        public VisualTreeView()
-        {
-            InitializeComponent();
-            this.GetObservable(DataContextProperty)
-                .Subscribe(x => ViewModel = (VisualTreeViewModel)x);
-        }
-
-        public VisualTreeViewModel ViewModel
-        {
-            get { return GetValue(ViewModelProperty); }
-            private set { SetValue(ViewModelProperty, value); }
-        }
-
-        private void InitializeComponent()
-        {
-            TreeView tree;
-
-            Content = new Grid
-            {
-                ColumnDefinitions = new ColumnDefinitions
-                {
-                    new ColumnDefinition(1, GridUnitType.Star),
-                    new ColumnDefinition(4, GridUnitType.Pixel),
-                    new ColumnDefinition(3, GridUnitType.Star),
-                },
-                Children = new Controls
-                {
-                    (tree = new TreeView
-                    {
-                        DataTemplates = new DataTemplates
-                        {
-                            new FuncTreeDataTemplate<VisualTreeNode>(GetHeader, x => x.Children),
-                        },
-                        [!ItemsControl.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Nodes),
-                    }),
-                    new GridSplitter
-                    {
-                        Width = 4,
-                        Orientation = Orientation.Vertical,
-                        [Grid.ColumnProperty] = 1,
-                    },
-                    new ContentControl
-                    {
-                        [!ContentProperty] = this.WhenAnyValue(x => x.ViewModel.Details),
-                        [Grid.ColumnProperty] = 2,
-                    }
-                }
-            };
-
-            tree.GetObservable(TreeView.SelectedItemProperty)
-                .OfType<VisualTreeNode>()
-                .Subscribe(x => ViewModel.SelectedNode = x);
-        }
-
-        private Control GetHeader(VisualTreeNode node)
-        {
-            var result = new StackPanel
-            {
-                Orientation = Orientation.Horizontal,
-                Gap = 8,
-                Children = new Controls
-                {
-                    new TextBlock
-                    {
-                        FontStyle = node.IsInTemplate ? FontStyle.Italic : FontStyle.Normal,
-                        Text = node.Type,
-                    },
-                    new TextBlock
-                    {
-                        [!TextBlock.TextProperty] = node.WhenAnyValue(x => x.Classes),
-                    }
-                }
-            };
-
-            result.PointerEnter += AddAdorner;
-            result.PointerLeave += RemoveAdorner;
-
-            return result;
-        }
-    }
-}