From 6c77c401dd6bd4a810f968ffe8cce5fca25d6d51 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 16 Oct 2015 17:00:35 +0200 Subject: [PATCH 01/18] Updated OmniXAML. --- src/Markup/Perspex.Markup.Xaml/OmniXAML | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Perspex.Markup.Xaml/OmniXAML b/src/Markup/Perspex.Markup.Xaml/OmniXAML index 2a6309a4d9..4b0aeccf9a 160000 --- a/src/Markup/Perspex.Markup.Xaml/OmniXAML +++ b/src/Markup/Perspex.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit 2a6309a4d9b60b848241a34bf2adfa16b52c7a85 +Subproject commit 4b0aeccf9ad094782973cd76f5319e8a2d600223 From 53926a47bf025d0660acce32f9da52e1bec74031 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 16 Oct 2015 18:36:17 +0200 Subject: [PATCH 02/18] Refactored ItemContainerGenerator. To allow TreeItemContainerGenerator to inherit from it. --- src/Perspex.Controls/DropDown.cs | 2 +- .../Generators/ITreeItemContainerGenerator.cs | 20 +- .../Generators/ItemContainerGenerator`1.cs | 18 +- .../Generators/TreeItemContainerGenerator.cs | 204 +++--------------- src/Perspex.Controls/ListBox.cs | 3 +- .../Primitives/SelectingItemsControl.cs | 10 +- src/Perspex.Controls/Primitives/TabStrip.cs | 2 +- src/Perspex.Controls/TreeView.cs | 154 ++++++++++--- src/Perspex.Controls/TreeViewItem.cs | 39 ++-- .../Presenters/CarouselPresenterTests.cs | 2 +- .../Presenters/ItemsPresenterTests.cs | 6 +- 11 files changed, 203 insertions(+), 257 deletions(-) diff --git a/src/Perspex.Controls/DropDown.cs b/src/Perspex.Controls/DropDown.cs index eff7d2c4fe..23aba16009 100644 --- a/src/Perspex.Controls/DropDown.cs +++ b/src/Perspex.Controls/DropDown.cs @@ -80,7 +80,7 @@ namespace Perspex.Controls protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new ItemContainerGenerator(this); + return new ItemContainerGenerator(this, ListBoxItem.ContentProperty); } protected override void OnKeyDown(KeyEventArgs e) diff --git a/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs index 0555fd436f..08cf643d4c 100644 --- a/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs @@ -11,23 +11,9 @@ namespace Perspex.Controls.Generators public interface ITreeItemContainerGenerator : IItemContainerGenerator { /// - /// 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. /// - /// The containers. - IEnumerable GetAllContainers(); - - /// - /// Gets the item that is contained by the specified container. - /// - /// The container. - /// The item. - object ItemFromContainer(IControl container); - - /// - /// Gets the container for the specified item - /// - /// The item. - /// The container. - IControl ContainerFromItem(object item); + ITreeItemContainerGenerator RootGenerator { get; } } } \ No newline at end of file diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs index fca5d1b4f8..dcb365b120 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs +++ b/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. /// /// The type of the container. - public class ItemContainerGenerator : ItemContainerGenerator where T : class, IContentControl, new() + public class ItemContainerGenerator : ItemContainerGenerator where T : class, IControl, new() { /// /// Initializes a new instance of the class. /// /// The owner control. - public ItemContainerGenerator(Control owner) + /// The container's Content property. + public ItemContainerGenerator( + IControl owner, + PerspexProperty contentProperty) : base(owner) { + ContentProperty = contentProperty; } + /// + /// Gets the container's Content property. + /// + protected PerspexProperty ContentProperty { get; } + /// 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)) { diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index fa8c699ec3..29cc5f2d32 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -1,11 +1,6 @@ // 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 +9,53 @@ namespace Perspex.Controls.Generators /// Creates containers for tree items and maintains a list of created containers. /// /// The type of the container. - public class TreeItemContainerGenerator : ITreeItemContainerGenerator where T : TreeViewItem, new() + public class TreeItemContainerGenerator : ItemContainerGenerator, ITreeItemContainerGenerator + where T : class, IControl, new() { - private Dictionary _containers = new Dictionary(); - - private readonly Subject _containersInitialized = new Subject(); + private ITreeItemContainerGenerator rootGenerator; /// /// Initializes a new instance of the class. /// /// The owner control. - public TreeItemContainerGenerator(IControl owner) - { - Owner = owner; - } - - /// - /// Gets the currently realized containers. - /// - public IEnumerable Containers => _containers.Values; - - /// - /// Signalled whenever new containers are initialized. - /// - public IObservable ContainersInitialized => _containersInitialized; - - /// - /// Gets the owner control. - /// - public IControl Owner { get; } - - /// - /// Creates container controls for a collection of items. - /// - /// - /// The index of the first item of the data in the containing collection. + /// The container's Content property. + /// The container's Items property. + /// The container's IsExpanded property. + /// + /// The item container for the root of the tree, or null if this generator is itself the + /// root of the tree. /// - /// The items. - /// An optional member selector. - /// The created container controls. - public IList CreateContainers( - int startingIndex, - IEnumerable items, - IMemberSelector selector) + public TreeItemContainerGenerator( + IControl owner, + PerspexProperty contentProperty, + PerspexProperty itemsProperty, + PerspexProperty isExpandedProperty, + ITreeItemContainerGenerator rootGenerator) + : base(owner, contentProperty) { - Contract.Requires(items != null); - - int index = startingIndex; - var result = new List(); - - 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(); + ItemsProperty = itemsProperty; + IsExpandedProperty = isExpandedProperty; + RootGenerator = rootGenerator; } /// - /// Removes a set of 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. /// - /// - /// The index of the first item of the data in the containing collection. - /// - /// The items. - /// The removed controls. - public IList RemoveContainers(int startingIndex, IEnumerable items) - { - var result = new List(); - - foreach (var item in items) - { - T container; - - if (_containers.TryGetValue(item, out container)) - { - Remove(container, result); - } - } - - return result; - } + public ITreeItemContainerGenerator RootGenerator { get; } /// - /// Clears the created containers from the index and returns the removed controls. + /// Gets the item container's Items property. /// - /// The removed controls. - public IList ClearContainers() - { - var result = _containers; - _containers = new Dictionary(); - return result.Values.Cast().ToList(); - } + protected PerspexProperty ItemsProperty { get; } /// - /// Gets the container control representing the item with the specified index. + /// Gets the item container's IsExpanded property. /// - /// The index. - /// The container or null if no container created. - public IControl ContainerFromIndex(int index) - { - throw new NotImplementedException(); - } + protected PerspexProperty IsExpandedProperty { get; } - /// - /// Gets the index of the specified container control. - /// - /// The container. - /// The index of the container or -1 if not found. - public int IndexFromContainer(IControl container) - { - throw new NotImplementedException(); - } - - /// - /// Gets all of the generated container controls. - /// - /// The containers. - public IEnumerable GetAllContainers() - { - return _containers.Values; - } - - /// - /// Gets the item that is contained by the specified container. - /// - /// The container. - /// The item. - public object ItemFromContainer(IControl container) - { - return container.DataContext; - } - - /// - /// Gets the container for the specified item - /// - /// The item. - /// The container. - public IControl ContainerFromItem(object item) - { - T result; - _containers.TryGetValue(item, out result); - return result; - } - - /// - /// Creates the container for an item. - /// - /// The item. - /// The created container control. - protected virtual T CreateContainer(object item) + /// + protected override IControl CreateContainer(object item) { var container = item as T; @@ -183,12 +70,11 @@ 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)) { @@ -222,29 +108,5 @@ namespace Perspex.Controls.Generators return treeTemplate; } - - private void Remove(T container, IList 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); - } - } } } diff --git a/src/Perspex.Controls/ListBox.cs b/src/Perspex.Controls/ListBox.cs index 1ecd086cae..7641e2e54a 100644 --- a/src/Perspex.Controls/ListBox.cs +++ b/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 /// protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new ItemContainerGenerator(this); + return new ItemContainerGenerator(this, ListBoxItem.ContentProperty); } /// diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index f1de2dea5f..6ff41fe8ab 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -388,8 +388,8 @@ namespace Perspex.Controls.Primitives } /// - /// 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. /// /// The control that raised the event. /// Whether the container should be selected or unselected. @@ -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; } diff --git a/src/Perspex.Controls/Primitives/TabStrip.cs b/src/Perspex.Controls/Primitives/TabStrip.cs index 54b0abbf4c..5fc305c73a 100644 --- a/src/Perspex.Controls/Primitives/TabStrip.cs +++ b/src/Perspex.Controls/Primitives/TabStrip.cs @@ -43,7 +43,7 @@ namespace Perspex.Controls.Primitives } else { - result = new ItemContainerGenerator(this); + result = new ItemContainerGenerator(this, TabItem.ContentProperty); } return result; diff --git a/src/Perspex.Controls/TreeView.cs b/src/Perspex.Controls/TreeView.cs index c077d42aee..ee6e7b9d84 100644 --- a/src/Perspex.Controls/TreeView.cs +++ b/src/Perspex.Controls/TreeView.cs @@ -6,11 +6,19 @@ using System.Linq; using Perspex.Controls.Generators; using Perspex.Controls.Primitives; using Perspex.Input; +using Perspex.Interactivity; +using Perspex.VisualTree; namespace Perspex.Controls { + /// + /// Displays a hierachical tree of data. + /// public class TreeView : ItemsControl { + /// + /// Defines the property. + /// public static readonly PerspexProperty SelectedItemProperty = SelectingItemsControl.SelectedItemProperty.AddOwner( o => o.SelectedItem, @@ -18,62 +26,146 @@ namespace Perspex.Controls private object _selectedItem; - static TreeView() - { - SelectedItemProperty.Changed.Subscribe(x => - { - var control = x.Sender as TreeView; - - if (control != null) - { - control.SelectedItemChanged(x.NewValue); - } - }); - } - - public new ITreeItemContainerGenerator ItemContainerGenerator => (ITreeItemContainerGenerator)base.ItemContainerGenerator; + /// + /// Gets the for the tree view. + /// + public new ITreeItemContainerGenerator ItemContainerGenerator => + (ITreeItemContainerGenerator)base.ItemContainerGenerator; + /// + /// Gets or sets the selected item. + /// public object SelectedItem { get { return _selectedItem; } set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); } } + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new TreeItemContainerGenerator(this); + return new TreeItemContainerGenerator( + this, + TreeViewItem.HeaderProperty, + TreeViewItem.ItemsProperty, + TreeViewItem.IsExpandedProperty, + null); } + /// 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); + } + } + + /// + protected override void OnPointerPressed(PointerPressEventArgs e) + { + base.OnPointerPressed(e); - if (item != null) + if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) { - SelectedItem = item; - e.Handled = true; + e.Handled = UpdateSelectionFromEventSource( + e.Source, + true, + (e.InputModifiers & InputModifiers.Shift) != 0, + (e.InputModifiers & InputModifiers.Control) != 0); } } - private void SelectedItemChanged(object selected) + /// + /// Updates the selection for an item based on user interaction. + /// + /// The container. + /// Whether the item should be selected or unselected. + /// Whether the range modifier is enabled (i.e. shift key). + /// Whether the toggle modifier is enabled (i.e. ctrl key). + protected void UpdateSelectionFromContainer( + IControl container, + bool select = true, + bool rangeModifier = false, + bool toggleModifier = false) + { + } + + /// + /// Updates the selection based on an event that may have originated in a container that + /// belongs to the control. + /// + /// The control that raised the event. + /// Whether the container should be selected or unselected. + /// Whether the range modifier is enabled (i.e. shift key). + /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// + /// True if the event originated from a container that belongs to the control; otherwise + /// false. + /// + protected bool UpdateSelectionFromEventSource( + IInteractive eventSource, + bool select = true, + bool rangeModifier = false, + bool toggleModifier = false) { - var containers = ItemContainerGenerator.GetAllContainers().OfType(); - 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; + } + + /// + /// Tries to get the container that was the source of an event. + /// + /// The control that raised the event. + /// The container or null if the event did not originate in a container. + protected IControl GetContainerFromEventSource(IInteractive eventSource) + { + var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() + .OfType() + .FirstOrDefault(x => x.LogicalParent is TreeViewItem); + + if (item != null) { - item.IsSelected = item == selectedContainer; + var treeViewItem = (TreeViewItem)item.LogicalParent; + + if (treeViewItem.ItemContainerGenerator.RootGenerator == this.ItemContainerGenerator) + { + return treeViewItem; + } } + + return null; + } + + /// + private void SelectedItemChanged(object selected) + { + //var containers = ItemContainerGenerator.GetAllContainers().OfType(); + //var selectedContainer = (selected != null) ? + // ItemContainerGenerator.ContainerFromItem(selected) : + // null; + + //if (Presenter != null && Presenter.Panel != null) + //{ + // KeyboardNavigation.SetTabOnceActiveElement( + // (InputElement)Presenter.Panel, + // selectedContainer); + //} + + //foreach (var item in containers) + //{ + // item.IsSelected = item == selectedContainer; + //} } } } diff --git a/src/Perspex.Controls/TreeViewItem.cs b/src/Perspex.Controls/TreeViewItem.cs index 19bbdb1e7b..bd499e4f10 100644 --- a/src/Perspex.Controls/TreeViewItem.cs +++ b/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); } } + /// + /// Gets the for the tree view. + /// + public new ITreeItemContainerGenerator ItemContainerGenerator => + (ITreeItemContainerGenerator)base.ItemContainerGenerator; + /// 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( + this, + TreeViewItem.HeaderProperty, + TreeViewItem.ItemsProperty, + TreeViewItem.IsExpandedProperty, + _treeView?.ItemContainerGenerator); } /// protected override void OnAttachedToVisualTree(IRenderRoot root) { base.OnAttachedToVisualTree(root); - - if (this.GetVisualParent() != null) - { - _treeView = this.GetVisualAncestors().OfType().FirstOrDefault(); - - if (_treeView == null) - { - throw new InvalidOperationException("TreeViewItems must be added to a TreeView."); - } - } - else - { - _treeView = null; - } + _treeView = this.GetVisualAncestors().OfType().FirstOrDefault(); } + /// protected override void OnKeyDown(KeyEventArgs e) { if (!e.Handled) diff --git a/tests/Perspex.Controls.UnitTests/Presenters/CarouselPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/CarouselPresenterTests.cs index 4ce53c6142..7997e0cb90 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/CarouselPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/CarouselPresenterTests.cs @@ -74,7 +74,7 @@ namespace Perspex.Controls.UnitTests.Presenters { protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new ItemContainerGenerator(this); + return new ItemContainerGenerator(this, TestItem.ContentProperty); } } } diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 50378047dc..fa5f59effd 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -39,7 +39,9 @@ namespace Perspex.Controls.UnitTests.Presenters Items = new[] { "foo", "bar" }, }; - target.ItemContainerGenerator = new ItemContainerGenerator(target); + target.ItemContainerGenerator = new ItemContainerGenerator( + target, + ListBoxItem.ContentProperty); target.ApplyTemplate(); Assert.Equal(2, target.Panel.Children.Count); @@ -231,7 +233,7 @@ namespace Perspex.Controls.UnitTests.Presenters { protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new ItemContainerGenerator(this); + return new ItemContainerGenerator(this, TestItem.ContentProperty); } } } From 2615c5373cb87bcdb525e05dda7e8bcfcfb14159 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 16 Oct 2015 18:49:22 +0200 Subject: [PATCH 03/18] Started adding TreeView tests. --- .../TreeViewTests.cs | 120 +++++++++++++++--- 1 file changed, 100 insertions(+), 20 deletions(-) diff --git a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs index b3cb7dbbe3..623e772e0a 100644 --- a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs @@ -1,25 +1,40 @@ // 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.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" }, ExtractItemContent(target, 0)); + Assert.Equal(new[] { "Child1", "Child2" }, ExtractItemContent(target, 1)); + Assert.Equal(new[] { "Grandchild2a" }, ExtractItemContent(target, 2)); + } + [Fact] public void LogicalChildren_Should_Be_Set() { var target = new TreeView { - Template = new ControlTemplate(CreateTreeViewTemplate), + Template = CreateTreeViewTemplate(), Items = new[] { "Foo", "Bar", "Baz " }, }; @@ -39,18 +54,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 ControlTemplate(CreateTreeViewTemplate), + Template = CreateTreeViewTemplate(), DataContext = "Base", DataTemplates = new DataTemplates { - new FuncDataTemplate(x => new Button { Content = x }) + new FuncDataTemplate(x => new Button { Content = x }) }, Items = items, }; @@ -67,35 +82,100 @@ namespace Perspex.Controls.UnitTests dataContexts); } - private Control CreateTreeViewTemplate(ITemplatedControl parent) + private IList CreateTestTreeData() { - return new ScrollViewer + return new[] { - Template = new ControlTemplate(CreateScrollViewerTemplate), - Content = new ItemsPresenter + 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( + x => new TextBlock { Text = x.Value }, + x => x.Children), }; } - private class Item + private ControlTemplate CreateTreeViewTemplate() + { + return new ControlTemplate(parent => new ItemsPresenter + { + Name = "itemsPresenter", + [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + }); + } + + private ControlTemplate CreateTreeViewItemTemplate() { - public Item(string value) + return new ControlTemplate(parent => new ItemsPresenter { - Value = value; + Name = "itemsPresenter", + [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + }); + } + + private List ExtractItemContent(TreeView tree, int level) + { + return ExtractItemContent(tree.Presenter.Panel, 0, level) + .Select(x => x.Header) + .OfType() + .Select(x => x.Text) + .ToList(); + } + + private IEnumerable 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 Children { get; set; } } } } From 6a005d35c83432b3e68638fa374c14e2f87bfaf6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 16 Oct 2015 20:58:16 +0200 Subject: [PATCH 04/18] Make TreeView selection work etc. Unfortunately stuff is still broken - I think ItemContainerGenerator needs a refactor. --- src/Perspex.Controls/Control.cs | 6 +- .../Generators/IItemContainerGenerator.cs | 6 +- .../Generators/ITreeItemContainerGenerator.cs | 14 +++ .../Generators/ItemContainerGenerator.cs | 65 +++++------ .../Generators/TreeItemContainerGenerator.cs | 92 ++++++++++++++- .../Presenters/CarouselPresenter.cs | 2 +- .../Presenters/ItemsPresenter.cs | 2 +- src/Perspex.Controls/TreeView.cs | 70 ++++++++---- .../TreeViewTests.cs | 108 ++++++++++++++++-- 9 files changed, 291 insertions(+), 74 deletions(-) diff --git a/src/Perspex.Controls/Control.cs b/src/Perspex.Controls/Control.cs index e41be6ccc3..62150c76b9 100644 --- a/src/Perspex.Controls/Control.cs +++ b/src/Perspex.Controls/Control.cs @@ -406,7 +406,11 @@ namespace Perspex.Controls base.OnAttachedToVisualTree(root); IStyler styler = PerspexLocator.Current.GetService(); - styler.ApplyStyles(this); + + if (styler != null) + { + styler.ApplyStyles(this); + } } /// diff --git a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs index 2009131bc5..ff1d57994a 100644 --- a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs @@ -43,9 +43,9 @@ namespace Perspex.Controls.Generators /// /// The index of the first item of the data in the containing collection. /// - /// The items. - /// The removed controls. - IList RemoveContainers(int startingIndex, IEnumerable items); + /// The the number of items to remove. + /// The removed containers. + IList RemoveContainers(int startingIndex, int count); /// /// Clears the created containers from the index and returns the removed controls. diff --git a/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs index 08cf643d4c..37f097618b 100644 --- a/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs @@ -15,5 +15,19 @@ namespace Perspex.Controls.Generators /// the root of the tree. /// ITreeItemContainerGenerator RootGenerator { get; } + + /// + /// Gets the item container for the specified item, anywhere in the tree. + /// + /// The item. + /// The container, or null if not found. + IControl TreeContainerFromItem(object item); + + /// + /// Gets the item for the specified item container, anywhere in the tree. + /// + /// The container. + /// The item, or null if not found. + object TreeItemFromContainer(IControl container); } } \ No newline at end of file diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs index 9e8183c322..db7d39250b 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs @@ -15,7 +15,7 @@ namespace Perspex.Controls.Generators /// public class ItemContainerGenerator : IItemContainerGenerator { - private Dictionary _containers = new Dictionary(); + private List _containers = new List(); private readonly Subject _containersInitialized = new Subject(); @@ -31,7 +31,7 @@ namespace Perspex.Controls.Generators /// /// Gets the currently realized containers. /// - public IEnumerable Containers => _containers.Values; + public IEnumerable Containers => _containers; /// /// Signalled whenever new containers are initialized. @@ -81,24 +81,12 @@ namespace Perspex.Controls.Generators /// /// The index of the first item of the data in the containing collection. /// - /// The items. + /// The the number of items to remove. /// The removed controls. - public IList RemoveContainers(int startingIndex, IEnumerable items) + public virtual IList RemoveContainers(int startingIndex, int count) { - var result = new List(); - var count = items.Cast().Count(); - - for (int i = startingIndex; i < startingIndex + count; ++i) - { - var container = _containers[i]; - - if (container != null) - { - result.Add(container); - _containers.Remove(i); - } - } - + var result = _containers.GetRange(startingIndex, count); + _containers.RemoveRange(startingIndex, count); return result; } @@ -106,11 +94,11 @@ namespace Perspex.Controls.Generators /// Clears the created containers from the index and returns the removed controls. /// /// The removed controls. - public IList ClearContainers() + public virtual IList ClearContainers() { var result = _containers; - _containers = new Dictionary(); - return result.Values.ToList(); + _containers = new List(); + return result; } /// @@ -120,9 +108,12 @@ namespace Perspex.Controls.Generators /// The container or null if no container created. public IControl ContainerFromIndex(int index) { - IControl result; - _containers.TryGetValue(index, out result); - return result; + if (index < _containers.Count) + { + return _containers[index]; + } + + return null; } /// @@ -132,15 +123,7 @@ namespace Perspex.Controls.Generators /// The index of the container or -1 if not found. public int IndexFromContainer(IControl container) { - foreach (var i in _containers) - { - if (i.Value == container) - { - return i.Key; - } - } - - return -1; + return _containers.IndexOf(container); } /// @@ -171,7 +154,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 +175,10 @@ namespace Perspex.Controls.Generators ++index; } } + + protected IEnumerable GetContainerRange(int index, int count) + { + return _containers.GetRange(index, count); + } } } \ No newline at end of file diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index 29cc5f2d32..a08ee9ebbd 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -1,6 +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.Collections; +using System.Collections.Generic; using Perspex.Controls.Templates; namespace Perspex.Controls.Generators @@ -12,7 +14,8 @@ namespace Perspex.Controls.Generators public class TreeItemContainerGenerator : ItemContainerGenerator, ITreeItemContainerGenerator where T : class, IControl, new() { - private ITreeItemContainerGenerator rootGenerator; + private Dictionary _itemToContainer; + private Dictionary _containerToItem; /// /// Initializes a new instance of the class. @@ -36,6 +39,12 @@ namespace Perspex.Controls.Generators ItemsProperty = itemsProperty; IsExpandedProperty = isExpandedProperty; RootGenerator = rootGenerator; + + if (rootGenerator == null) + { + _itemToContainer = new Dictionary(); + _containerToItem = new Dictionary(); + } } /// @@ -54,6 +63,30 @@ namespace Perspex.Controls.Generators /// protected PerspexProperty IsExpandedProperty { get; } + /// + /// Gets the item container for the specified item, anywhere in the tree. + /// + /// The item. + /// The container, or null if not found. + public IControl TreeContainerFromItem(object item) + { + T result; + _itemToContainer.TryGetValue(item, out result); + return result; + } + + /// + /// Gets the item for the specified item container, anywhere in the tree. + /// + /// The container. + /// The item, or null if not found. + public object TreeItemFromContainer(IControl container) + { + object result; + _containerToItem.TryGetValue(container, out result); + return result; + } + /// protected override IControl CreateContainer(object item) { @@ -81,10 +114,67 @@ namespace Perspex.Controls.Generators result.DataContext = item; } + AddToIndex(item, result); + return result; } } + public override IList ClearContainers() + { + ClearIndex(); + return base.ClearContainers(); + } + + public override IList RemoveContainers(int startingIndex, int count) + { + RemoveFromIndex(GetContainerRange(startingIndex, count)); + return base.RemoveContainers(startingIndex, count); + } + + private void AddToIndex(object item, T container) + { + if (RootGenerator != null) + { + ((TreeItemContainerGenerator)RootGenerator).AddToIndex(item, container); + } + else + { + _itemToContainer.Add(item, container); + _containerToItem.Add(container, item); + } + } + + private void RemoveFromIndex(IEnumerable containers) + { + if (RootGenerator != null) + { + ((TreeItemContainerGenerator)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)RootGenerator).ClearIndex(); + } + else + { + _containerToItem.Clear(); + _itemToContainer.Clear(); + } + } + /// /// Gets the data template for the specified item. /// diff --git a/src/Perspex.Controls/Presenters/CarouselPresenter.cs b/src/Perspex.Controls/Presenters/CarouselPresenter.cs index 4687740adb..8641202acf 100644 --- a/src/Perspex.Controls/Presenters/CarouselPresenter.cs +++ b/src/Perspex.Controls/Presenters/CarouselPresenter.cs @@ -215,7 +215,7 @@ namespace Perspex.Controls.Presenters if (from != null) { Panel.Children.Remove(from); - generator.RemoveContainers(fromIndex, new[] { from }); + generator.RemoveContainers(fromIndex, 1); } } diff --git a/src/Perspex.Controls/Presenters/ItemsPresenter.cs b/src/Perspex.Controls/Presenters/ItemsPresenter.cs index 5c52f70df4..09132cfced 100644 --- a/src/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/src/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -248,7 +248,7 @@ namespace Perspex.Controls.Presenters case NotifyCollectionChangedAction.Remove: Panel.Children.RemoveAll( - generator.RemoveContainers(e.OldStartingIndex, e.OldItems)); + generator.RemoveContainers(e.OldStartingIndex, e.OldItems.Count)); break; } diff --git a/src/Perspex.Controls/TreeView.cs b/src/Perspex.Controls/TreeView.cs index ee6e7b9d84..d9b9271904 100644 --- a/src/Perspex.Controls/TreeView.cs +++ b/src/Perspex.Controls/TreeView.cs @@ -7,6 +7,7 @@ using Perspex.Controls.Generators; using Perspex.Controls.Primitives; using Perspex.Input; using Perspex.Interactivity; +using Perspex.Styling; using Perspex.VisualTree; namespace Perspex.Controls @@ -92,6 +93,23 @@ namespace Perspex.Controls 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; + + if (SelectedItem != null) + { + MarkContainerSelected(container, true); + } + } } /// @@ -131,41 +149,45 @@ namespace Perspex.Controls protected IControl GetContainerFromEventSource(IInteractive eventSource) { var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(x => x.LogicalParent is TreeViewItem); + .OfType() + .FirstOrDefault(); if (item != null) { - var treeViewItem = (TreeViewItem)item.LogicalParent; - - if (treeViewItem.ItemContainerGenerator.RootGenerator == this.ItemContainerGenerator) + if (item.ItemContainerGenerator.RootGenerator == this.ItemContainerGenerator) { - return treeViewItem; + return item; } } return null; } - /// - private void SelectedItemChanged(object selected) + /// + /// Sets a container's 'selected' class or . + /// + /// The container. + /// Whether the control is selected + private void MarkContainerSelected(IControl container, bool selected) { - //var containers = ItemContainerGenerator.GetAllContainers().OfType(); - //var selectedContainer = (selected != null) ? - // ItemContainerGenerator.ContainerFromItem(selected) : - // null; - - //if (Presenter != null && Presenter.Panel != null) - //{ - // KeyboardNavigation.SetTabOnceActiveElement( - // (InputElement)Presenter.Panel, - // selectedContainer); - //} - - //foreach (var item in containers) - //{ - // item.IsSelected = item == selectedContainer; - //} + var selectable = container as ISelectable; + var styleable = container as IStyleable; + + if (selectable != null) + { + selectable.IsSelected = selected; + } + else if (styleable != null) + { + if (selected) + { + styleable.Classes.Add(":selected"); + } + else + { + styleable.Classes.Remove(":selected"); + } + } } } } diff --git a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs index 623e772e0a..ba1c3bb2de 100644 --- a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Perspex.Controls.Presenters; using Perspex.Controls.Templates; +using Perspex.Input; using Perspex.LogicalTree; using Xunit; @@ -24,9 +25,80 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); - Assert.Equal(new[] { "Root" }, ExtractItemContent(target, 0)); - Assert.Equal(new[] { "Child1", "Child2" }, ExtractItemContent(target, 1)); - Assert.Equal(new[] { "Grandchild2a" }, ExtractItemContent(target, 2)); + 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] @@ -40,12 +112,14 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); - Assert.Equal(3, target.GetLogicalChildren().Count()); + var result = target.GetLogicalChildren() + .OfType() + .Select(x => x.Header) + .OfType() + .Select(x => x.Text) + .ToList(); - foreach (var child in target.GetLogicalChildren()) - { - Assert.IsType(child); - } + Assert.Equal(new[] { "Foo", "Bar", "Baz " }, result); } [Fact] @@ -82,6 +156,22 @@ namespace Perspex.Controls.UnitTests dataContexts); } + private void ApplyTemplates(TreeView tree) + { + tree.ApplyTemplate(); + ApplyTemplates(tree.Presenter.Panel.Children); + } + + private void ApplyTemplates(IEnumerable controls) + { + foreach (TreeViewItem control in controls) + { + control.Template = CreateTreeViewItemTemplate(); + control.ApplyTemplate(); + ApplyTemplates(control.Presenter.Panel.Children); + } + } + private IList CreateTestTreeData() { return new[] @@ -139,7 +229,7 @@ namespace Perspex.Controls.UnitTests }); } - private List ExtractItemContent(TreeView tree, int level) + private List ExtractItemHeader(TreeView tree, int level) { return ExtractItemContent(tree.Presenter.Panel, 0, level) .Select(x => x.Header) From 96f1ee54ddc3911c4f2457c6f99de531efc680a9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2015 16:57:34 +0200 Subject: [PATCH 05/18] Fix ItemContainerGenerator faulty remove logic. --- .../Generators/ItemContainerGenerator.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs index db7d39250b..ac0f9b9227 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs @@ -85,8 +85,17 @@ namespace Perspex.Controls.Generators /// The removed controls. public virtual IList RemoveContainers(int startingIndex, int count) { - var result = _containers.GetRange(startingIndex, count); - _containers.RemoveRange(startingIndex, count); + var result = new List(); + + for (int i = startingIndex; i < startingIndex + count; ++i) + { + if (i < _containers.Count) + { + result.Add(_containers[i]); + _containers[i] = null; + } + } + return result; } From 71973cabb182a88d4d3ff83e76d58aaf65a68853 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2015 17:25:38 +0200 Subject: [PATCH 06/18] Fix removing items from ItemsControl. --- .../Generators/IItemContainerGenerator.cs | 21 ++++- .../Generators/ItemContainerGenerator.cs | 66 ++++++------- .../Generators/TreeItemContainerGenerator.cs | 8 +- .../Presenters/CarouselPresenter.cs | 4 +- .../Presenters/ItemsPresenter.cs | 8 +- .../Generators/ItemContainerGeneratorTests.cs | 93 +++++++++++++++++++ .../ItemContainerGeneratorTypedTests.cs | 28 ++++++ .../Perspex.Controls.UnitTests.csproj | 2 + .../Presenters/ItemsPresenterTests.cs | 9 ++ 9 files changed, 184 insertions(+), 55 deletions(-) create mode 100644 tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs create mode 100644 tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs diff --git a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs index ff1d57994a..7189d7a0c9 100644 --- a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs @@ -32,26 +32,37 @@ namespace Perspex.Controls.Generators /// The items. /// An optional member selector. /// The created controls. - IList CreateContainers( + IEnumerable Materialize( int startingIndex, IEnumerable items, IMemberSelector selector); /// - /// Removes a set of created containers from the index and returns the removed controls. + /// Removes a set of created containers. /// /// /// The index of the first item of the data in the containing collection. /// /// The the number of items to remove. /// The removed containers. - IList RemoveContainers(int startingIndex, int count); + IEnumerable Dematerialize(int startingIndex, int count); /// - /// Clears the created containers from the index and returns the removed controls. + /// Removes a set of created containers and updates the index of later containers to fill + /// the gap. + /// + /// + /// The index of the first item of the data in the containing collection. + /// + /// The the number of items to remove. + /// The removed containers. + IEnumerable RemoveRange(int startingIndex, int count); + + /// + /// Clears all created containers and returns the removed controls. /// /// The removed controls. - IList ClearContainers(); + IEnumerable Clear(); /// /// Gets the container control representing the item with the specified index. diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs index ac0f9b9227..b8b57ddd59 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs @@ -25,17 +25,15 @@ namespace Perspex.Controls.Generators /// The owner control. public ItemContainerGenerator(IControl owner) { + Contract.Requires(owner != null); + Owner = owner; } - /// - /// Gets the currently realized containers. - /// + /// public IEnumerable Containers => _containers; - /// - /// Signalled whenever new containers are initialized. - /// + /// public IObservable ContainersInitialized => _containersInitialized; /// @@ -43,16 +41,8 @@ namespace Perspex.Controls.Generators /// public IControl Owner { get; } - /// - /// Creates container controls for a collection of items. - /// - /// - /// The index of the first item of the data in the containing collection. - /// - /// The items. - /// An optional member selector. - /// The created container controls. - public IList CreateContainers( + /// + public IEnumerable Materialize( int startingIndex, IEnumerable items, IMemberSelector selector) @@ -75,15 +65,8 @@ namespace Perspex.Controls.Generators return result.Where(x => x != null).ToList(); } - /// - /// Removes a set of created containers from the index and returns the removed controls. - /// - /// - /// The index of the first item of the data in the containing collection. - /// - /// The the number of items to remove. - /// The removed controls. - public virtual IList RemoveContainers(int startingIndex, int count) + /// + public virtual IEnumerable Dematerialize(int startingIndex, int count) { var result = new List(); @@ -99,22 +82,23 @@ namespace Perspex.Controls.Generators return result; } - /// - /// Clears the created containers from the index and returns the removed controls. - /// - /// The removed controls. - public virtual IList ClearContainers() + /// + public virtual IEnumerable RemoveRange(int startingIndex, int count) + { + var result = _containers.GetRange(startingIndex, count); + _containers.RemoveRange(startingIndex, count); + return result; + } + + /// + public virtual IEnumerable Clear() { var result = _containers; _containers = new List(); return result; } - /// - /// Gets the container control representing the item with the specified index. - /// - /// The index. - /// The container or null if no container created. + /// public IControl ContainerFromIndex(int index) { if (index < _containers.Count) @@ -125,11 +109,7 @@ namespace Perspex.Controls.Generators return null; } - /// - /// Gets the index of the specified container control. - /// - /// The container. - /// The index of the container or -1 if not found. + /// public int IndexFromContainer(IControl container) { return _containers.IndexOf(container); @@ -185,6 +165,12 @@ namespace Perspex.Controls.Generators } } + /// + /// Gets all containers with an index that fall within a range. + /// + /// The first index. + /// The number of elements in the range. + /// The containers. protected IEnumerable GetContainerRange(int index, int count) { return _containers.GetRange(index, count); diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index a08ee9ebbd..18df4c4183 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -120,16 +120,16 @@ namespace Perspex.Controls.Generators } } - public override IList ClearContainers() + public override IEnumerable Clear() { ClearIndex(); - return base.ClearContainers(); + return base.Clear(); } - public override IList RemoveContainers(int startingIndex, int count) + public override IEnumerable Dematerialize(int startingIndex, int count) { RemoveFromIndex(GetContainerRange(startingIndex, count)); - return base.RemoveContainers(startingIndex, count); + return base.Dematerialize(startingIndex, count); } private void AddToIndex(object item, T container) diff --git a/src/Perspex.Controls/Presenters/CarouselPresenter.cs b/src/Perspex.Controls/Presenters/CarouselPresenter.cs index 8641202acf..7af2416b9a 100644 --- a/src/Perspex.Controls/Presenters/CarouselPresenter.cs +++ b/src/Perspex.Controls/Presenters/CarouselPresenter.cs @@ -199,7 +199,7 @@ namespace Perspex.Controls.Presenters if (toIndex != -1) { var item = Items.Cast().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, 1); + generator.Dematerialize(fromIndex, 1); } } diff --git a/src/Perspex.Controls/Presenters/ItemsPresenter.cs b/src/Perspex.Controls/Presenters/ItemsPresenter.cs index 09132cfced..12327bdb48 100644 --- a/src/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/src/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -186,7 +186,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 +209,7 @@ namespace Perspex.Controls.Presenters if (e.OldValue != null) { - generator.ClearContainers(); + generator.Clear(); Panel.Children.Clear(); INotifyCollectionChanged incc = e.OldValue as INotifyCollectionChanged; @@ -243,12 +243,12 @@ namespace Perspex.Controls.Presenters { case NotifyCollectionChangedAction.Add: Panel.Children.AddRange( - generator.CreateContainers(e.NewStartingIndex, e.NewItems, MemberSelector)); + generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector)); break; case NotifyCollectionChangedAction.Remove: Panel.Children.RemoveAll( - generator.RemoveContainers(e.OldStartingIndex, e.OldItems.Count)); + generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count)); break; } diff --git a/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs new file mode 100644 index 0000000000..d8d2c6ba1f --- /dev/null +++ b/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().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); + } + } +} diff --git a/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs b/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs new file mode 100644 index 0000000000..f6b0ff2eae --- /dev/null +++ b/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(owner, ListBoxItem.ContentProperty); + var containers = target.Materialize(0, items, null); + var result = containers + .OfType() + .Select(x => x.Content) + .OfType() + .Select(x => x.Text).ToList(); + + Assert.Equal(items, result); + } + } +} diff --git a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index a66afeedc2..912bcd534a 100644 --- a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -81,6 +81,8 @@ + + diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index fa5f59effd..a41d4e326d 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -75,6 +75,7 @@ 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] @@ -89,6 +90,7 @@ namespace Perspex.Controls.UnitTests.Presenters target.Items = null; Assert.Empty(target.Panel.Children); + Assert.Empty(target.ItemContainerGenerator.Containers); } [Fact] @@ -104,12 +106,19 @@ namespace Perspex.Controls.UnitTests.Presenters target.ApplyTemplate(); var text = target.Panel.Children.Cast().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().Select(x => x.Text).ToList(); + Assert.Equal(new[] { "foo", "bar" }, text); + Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(0)); + Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(1)); } [Fact] From 0617aa23a5e7253db016ebe5849f7917ce1b68aa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2015 18:05:09 +0200 Subject: [PATCH 07/18] Support Replace and Reset in ItemsPresenter. --- .../Presenters/ItemsPresenter.cs | 28 ++++++++++++-- .../Presenters/ItemsPresenterTests.cs | 37 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/Perspex.Controls/Presenters/ItemsPresenter.cs b/src/Perspex.Controls/Presenters/ItemsPresenter.cs index 12327bdb48..0a80376aa7 100644 --- a/src/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/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; @@ -237,18 +238,37 @@ namespace Perspex.Controls.Presenters if (_createdPanel) { var generator = ItemContainerGenerator; + IEnumerable containers; // TODO: Handle Move and Replace etc. switch (e.Action) { case NotifyCollectionChangedAction.Add: - Panel.Children.AddRange( - generator.Materialize(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.RemoveRange(e.OldStartingIndex, e.OldItems.Count)); + 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.Reset: + Panel.Children.RemoveAll(generator.Clear()); + Panel.Children.AddRange(generator.Materialize(0, Items, MemberSelector)); break; } diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index a41d4e326d..c6354cc417 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/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; @@ -80,6 +81,42 @@ namespace Perspex.Controls.UnitTests.Presenters [Fact] public void Clearing_Items_Should_Remove_Containers() + { + var items = new ObservableCollection { "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 { "foo", "bar", "baz" }; + var target = new ItemsPresenter + { + Items = items, + }; + + target.ApplyTemplate(); + items[1] = "baz"; + + var text = target.Panel.Children + .OfType() + .Select(x => x.Text) + .ToList(); + + Assert.Equal(new[] { "foo", "baz", "baz" }, text); + } + + [Fact] + public void Setting_Items_To_Null_Should_Remove_Containers() { var target = new ItemsPresenter { From 42ea736be2fadfb27bc4889162b65a614b44d46a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2015 18:12:39 +0200 Subject: [PATCH 08/18] Reset ItemsPresenter on Move for now. Need to implement this in a more efficient manner. --- .../Presenters/ItemsPresenter.cs | 2 ++ .../Presenters/ItemsPresenterTests.cs | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Perspex.Controls/Presenters/ItemsPresenter.cs b/src/Perspex.Controls/Presenters/ItemsPresenter.cs index 0a80376aa7..ecd74e7bdb 100644 --- a/src/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/src/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -266,6 +266,8 @@ namespace Perspex.Controls.Presenters 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)); diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index c6354cc417..db9e6d3aec 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -115,6 +115,26 @@ namespace Perspex.Controls.UnitTests.Presenters Assert.Equal(new[] { "foo", "baz", "baz" }, text); } + [Fact] + public void Moving_Items_Should_Update_Containers() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + var target = new ItemsPresenter + { + Items = items, + }; + + target.ApplyTemplate(); + items.Move(2, 1); + + var text = target.Panel.Children + .OfType() + .Select(x => x.Text) + .ToList(); + + Assert.Equal(new[] { "foo", "baz", "bar" }, text); + } + [Fact] public void Setting_Items_To_Null_Should_Remove_Containers() { From a1e09628c1503720031093eb0534a32778b77bae Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2015 18:27:19 +0200 Subject: [PATCH 09/18] Added a focus adorner to TreeViewItem. --- src/Perspex.Themes.Default/TreeViewItemStyle.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Perspex.Themes.Default/TreeViewItemStyle.cs b/src/Perspex.Themes.Default/TreeViewItemStyle.cs index 3485041a62..ad6b2a8283 100644 --- a/src/Perspex.Themes.Default/TreeViewItemStyle.cs +++ b/src/Perspex.Themes.Default/TreeViewItemStyle.cs @@ -13,6 +13,7 @@ using Perspex.Styling; namespace Perspex.Themes.Default { + using Collections; using Controls = Controls.Controls; /// @@ -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(1, 2), + [Grid.ColumnProperty] = 1, + [!Rectangle.IsVisibleProperty] = control[!TreeViewItem.IsFocusedProperty], } } }, From 01a5b5a34500055903a8149558d086771b9831a5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2015 19:10:41 +0200 Subject: [PATCH 10/18] Fix clipping with UseLayoutRounding. --- src/Perspex.Layout/Layoutable.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Perspex.Layout/Layoutable.cs b/src/Perspex.Layout/Layoutable.cs index 09c4e1c103..ead1ef88bc 100644 --- a/src/Perspex.Layout/Layoutable.cs +++ b/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); } From 92f8ce6f477ca106aec804c5718e9c1dd323056a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2015 19:23:38 +0200 Subject: [PATCH 11/18] Added TreeView static ctor. To work around SelectedItem not being found in Release build. --- src/Perspex.Controls/TreeView.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Perspex.Controls/TreeView.cs b/src/Perspex.Controls/TreeView.cs index d9b9271904..a1d14d5441 100644 --- a/src/Perspex.Controls/TreeView.cs +++ b/src/Perspex.Controls/TreeView.cs @@ -27,6 +27,14 @@ namespace Perspex.Controls private object _selectedItem; + /// + /// Initializes static members of the class. + /// + static TreeView() + { + // HACK: Needed or SelectedItem property will not be found in Release build. + } + /// /// Gets the for the tree view. /// From 85d880e9feb179142be7a92bba57c1b12dc6174f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 17 Oct 2015 06:22:12 +0300 Subject: [PATCH 12/18] Extracted Perspex.Desktop to a separate package --- nuget/build-version.ps1 | 11 ++++---- nuget/template/Perspex.Desktop.nuspec | 29 ++++++++++++++++++++++ nuget/template/Perspex.nuspec | 5 ++-- nuget/template/build/net45/perspex.targets | 11 -------- 4 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 nuget/template/Perspex.Desktop.nuspec delete mode 100644 nuget/template/build/net45/perspex.targets diff --git a/nuget/build-version.ps1 b/nuget/build-version.ps1 index f458b30ffd..2c7e15683a 100644 --- a/nuget/build-version.ps1 +++ b/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 \ No newline at end of file +nuget.exe pack Perspex.Desktop\Perspex.Desktop.nuspec +rm -Force -Recurse .\Perspex +rm -Force -Recurse .\Perspex.Desktop \ No newline at end of file diff --git a/nuget/template/Perspex.Desktop.nuspec b/nuget/template/Perspex.Desktop.nuspec new file mode 100644 index 0000000000..b94e9c54f0 --- /dev/null +++ b/nuget/template/Perspex.Desktop.nuspec @@ -0,0 +1,29 @@ + + + + Perspex.Desktop + #VERSION# + Perspex Team + stevenk + http://opensource.org/licenses/MIT + https://github.com/Perspex/Perspex/ + false + The Perspex UI framework + + Copyright 2015 + Perspex + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nuget/template/Perspex.nuspec b/nuget/template/Perspex.nuspec index 45490d310e..7c55e7aaf6 100644 --- a/nuget/template/Perspex.nuspec +++ b/nuget/template/Perspex.nuspec @@ -3,10 +3,10 @@ Perspex #VERSION# - stevenk + Perspex Team stevenk http://opensource.org/licenses/MIT - https://github.com/grokys/Perspex/ + https://github.com/Perspex/Perspex/ false The Perspex UI framework Initial alpha release. @@ -21,6 +21,7 @@ + \ No newline at end of file diff --git a/nuget/template/build/net45/perspex.targets b/nuget/template/build/net45/perspex.targets deleted file mode 100644 index 70d36cf835..0000000000 --- a/nuget/template/build/net45/perspex.targets +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - %(RecursiveDir)%(FileName)%(Extension) - PreserveNewest - - - \ No newline at end of file From ed961d0819069d1a60b2d08f9d1bef9b99d5a300 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 17 Oct 2015 06:30:39 +0300 Subject: [PATCH 13/18] Fixed nuget push --- nuget/build-appveyor.ps1 | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/nuget/build-appveyor.ps1 b/nuget/build-appveyor.ps1 index d74216def0..74a6aa0285 100644 --- a/nuget/build-appveyor.ps1 +++ b/nuget/build-appveyor.ps1 @@ -9,10 +9,6 @@ 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 @@ -26,7 +22,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 } } From f08a8fdb5dfea32160d3ff1216b16e3c6fc1726c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 17 Oct 2015 06:39:45 +0300 Subject: [PATCH 14/18] Still trying to fix nuget --- nuget/build-appveyor.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/nuget/build-appveyor.ps1 b/nuget/build-appveyor.ps1 index 74a6aa0285..5d7f5f1628 100644 --- a/nuget/build-appveyor.ps1 +++ b/nuget/build-appveyor.ps1 @@ -9,6 +9,7 @@ sv version $env:APPVEYOR_BUILD_NUMBER sv version 9999.0.$version-nightly sv key $env:myget_key +.\build-version.ps1 $version sv reponame $env:APPVEYOR_REPO_NAME sv repobranch $env:APPVEYOR_REPO_BRANCH From d273958bd339c754f5d31ccbc72e94a9ed10df21 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2015 19:59:12 +0200 Subject: [PATCH 15/18] Correctly compare items. --- src/Perspex.Controls/Primitives/SelectingItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 6ff41fe8ab..f9c430d393 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/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); From 952e4a99f70d354fd45d9bf94b7e9ca82314ed7f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 19 Oct 2015 09:31:21 +0200 Subject: [PATCH 16/18] Created PerspexPropertyRegistry. And move registration of properties, finding properties etc here. --- .../Context/PerspexXamlMemberValuePlugin.cs | 4 +- .../PerspexPropertyTypeConverter.cs | 4 +- src/Perspex.Base/Perspex.Base.csproj | 1 + src/Perspex.Base/PerspexObject.cs | 172 +----------- src/Perspex.Base/PerspexProperty.cs | 9 +- src/Perspex.Base/PerspexPropertyRegistry.cs | 244 ++++++++++++++++++ src/Perspex.Base/PerspexProperty`1.cs | 10 +- src/Perspex.Diagnostics/Debug.cs | 2 +- .../ViewModels/ControlDetailsViewModel.cs | 2 +- .../Perspex.Base.UnitTests.csproj | 1 + .../PerspexObjectTests_Metadata.cs | 24 -- .../PerspexPropertyRegistryTests.cs | 79 ++++++ 12 files changed, 355 insertions(+), 197 deletions(-) create mode 100644 src/Perspex.Base/PerspexPropertyRegistry.cs create mode 100644 tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs index 36e1d03ea8..8dc4999a12 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs +++ b/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); } diff --git a/src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyTypeConverter.cs b/src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyTypeConverter.cs index f492e01111..1cdc3cc4f1 100644 --- a/src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyTypeConverter.cs +++ b/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); } diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj index b7227d831b..5b3a086f59 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -50,6 +50,7 @@ + diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 337fbf185d..a61e04a8ac 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -25,18 +25,6 @@ namespace Perspex /// public class PerspexObject : IObservablePropertyBag, INotifyPropertyChanged { - /// - /// The registered properties by type. - /// - private static readonly Dictionary> s_registered = - new Dictionary>(); - - /// - /// The registered attached properties by owner type. - /// - private static readonly Dictionary> s_attached = - new Dictionary>(); - /// /// The parent object that inherited values are inherited from. /// @@ -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 } } - /// - /// Gets all s registered on a type. - /// - /// The type. - /// A collection of definitions. - public static IEnumerable GetRegisteredProperties(Type type) - { - Contract.Requires(type != null); - - TypeInfo i = type.GetTypeInfo(); - - while (type != null) - { - List list; - - if (s_registered.TryGetValue(type, out list)) - { - foreach (PerspexProperty p in list) - { - yield return p; - } - } - - type = type.GetTypeInfo().BaseType; - } - } - - /// - /// Gets all attached s registered by an owner. - /// - /// The owner type. - /// A collection of definitions. - public static IEnumerable GetAttachedProperties(Type ownerType) - { - List list; - - if (s_attached.TryGetValue(ownerType, out list)) - { - return list; - } - - return Enumerable.Empty(); - } - - /// - /// Registers a on a type. - /// - /// The type. - /// The property. - /// - /// You won't usually want to call this method directly, instead use the - /// method. - /// - public static void Register(Type type, PerspexProperty property) - { - Contract.Requires(type != null); - Contract.Requires(property != null); - - List list; - - if (!s_registered.TryGetValue(type, out list)) - { - list = new List(); - 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(); - 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 } } - /// - /// Gets all properties that are registered on this object. - /// - /// - /// A collection of objects. - /// - public IEnumerable GetRegisteredProperties() - { - return GetRegisteredProperties(GetType()); - } - /// /// Checks whether a is set on this object. /// @@ -478,16 +369,6 @@ namespace Perspex return false; } - /// - /// Checks whether a is registered on this class. - /// - /// The property. - /// True if the property is registered, otherwise false. - public bool IsRegistered(PerspexProperty property) - { - return FindRegistered(property) != null; - } - /// /// Sets a value. /// @@ -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 } } + /// + bool IPropertyBag.IsRegistered(PerspexProperty property) + { + return PerspexPropertyRegistry.Instance.IsRegistered(this, property); + } + /// /// Gets all priority values set on the object. /// @@ -936,41 +823,6 @@ namespace Perspex } } - /// - /// Given a returns a registered perspex property that is - /// equal. - /// - /// The property. - /// The registered property or null if not found. - /// - /// 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 . - /// - public PerspexProperty FindRegistered(PerspexProperty property) - { - Type type = GetType(); - - while (type != null) - { - List 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; - } - /// /// Given a returns a registered perspex property that is /// equal or throws if not found. @@ -979,7 +831,7 @@ namespace Perspex /// The registered property. public PerspexProperty GetRegistered(PerspexProperty property) { - var result = FindRegistered(property); + var result = PerspexPropertyRegistry.Instance.FindRegistered(this, property); if (result == null) { diff --git a/src/Perspex.Base/PerspexProperty.cs b/src/Perspex.Base/PerspexProperty.cs index e44284b570..62a3d5cfe2 100644 --- a/src/Perspex.Base/PerspexProperty.cs +++ b/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; @@ -367,7 +368,7 @@ namespace Perspex notifying, false); - PerspexObject.Register(typeof(TOwner), result); + PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } @@ -395,7 +396,7 @@ namespace Perspex Cast(getter), Cast(setter)); - PerspexObject.Register(typeof(TOwner), result); + PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } @@ -431,7 +432,7 @@ namespace Perspex null, true); - PerspexObject.Register(typeof(THost), result); + PerspexPropertyRegistry.Instance.Register(typeof(THost), result); return result; } @@ -468,7 +469,7 @@ namespace Perspex null, true); - PerspexObject.Register(typeof(THost), result); + PerspexPropertyRegistry.Instance.Register(typeof(THost), result); return result; } diff --git a/src/Perspex.Base/PerspexPropertyRegistry.cs b/src/Perspex.Base/PerspexPropertyRegistry.cs new file mode 100644 index 0000000000..d5df44a66f --- /dev/null +++ b/src/Perspex.Base/PerspexPropertyRegistry.cs @@ -0,0 +1,244 @@ +// 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 +{ + /// + /// Tracks registered instances. + /// + public class PerspexPropertyRegistry + { + /// + /// The registered properties by type. + /// + private readonly Dictionary> _registered = + new Dictionary>(); + + /// + /// The registered attached properties by owner type. + /// + private readonly Dictionary> _attached = + new Dictionary>(); + + /// + /// Gets the instance + /// + public static PerspexPropertyRegistry Instance { get; } + = new PerspexPropertyRegistry(); + + /// + /// Gets all attached s registered by an owner. + /// + /// The owner type. + /// A collection of definitions. + public IEnumerable GetAttached(Type ownerType) + { + List list; + + if (_attached.TryGetValue(ownerType, out list)) + { + return list; + } + + return Enumerable.Empty(); + } + + /// + /// Gets all s registered on a type. + /// + /// The type. + /// A collection of definitions. + public IEnumerable GetRegistered(Type type) + { + Contract.Requires(type != null); + + var i = type.GetTypeInfo(); + + while (type != null) + { + List list; + + if (_registered.TryGetValue(type, out list)) + { + foreach (PerspexProperty p in list) + { + yield return p; + } + } + + type = type.GetTypeInfo().BaseType; + } + } + + /// + /// Gets all s registered on a object. + /// + /// The object. + /// A collection of definitions. + public IEnumerable GetRegistered(PerspexObject o) + { + Contract.Requires(o != null); + + return GetRegistered(o.GetType()); + } + + /// + /// Finds registered on a type. + /// + /// The type. + /// The property. + /// The registered property or null if not found. + /// + /// Calling AddOwner on a PerspexProperty creates a new PerspexProperty that is a + /// different object but is equal according to . + /// + public PerspexProperty FindRegistered(Type type, PerspexProperty property) + { + while (type != null) + { + List list; + + if (_registered.TryGetValue(type, out list)) + { + var index = list.IndexOf(property); + + if (index != -1) + { + return list[index]; + } + } + + type = type.GetTypeInfo().BaseType; + } + + return null; + } + + /// + /// Finds registered on an object. + /// + /// The object. + /// The property. + /// The registered property or null if not found. + /// + /// Calling AddOwner on a PerspexProperty creates a new PerspexProperty that is a + /// different object but is equal according to . + /// + public PerspexProperty FindRegistered(object o, PerspexProperty property) + { + return FindRegistered(o.GetType(), property); + } + + /// + /// Finds a registered property on a type by name. + /// + /// The type. + /// + /// The property name. If an attached property it should be in the form + /// "OwnerType.PropertyName". + /// + /// + /// The registered property or null if no matching property found. + /// + public PerspexProperty FindRegistered(Type type, string name) + { + Contract.Requires(type != null); + Contract.Requires(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 = GetRegistered(type) + .FirstOrDefault(x => !x.IsAttached && x.Name == parts[0]); + + if (result != null) + { + return result; + } + + // A type can .AddOwner an attached property. + return GetRegistered(type) + .FirstOrDefault(x => x.Name == parts[0]); + } + else + { + return GetRegistered(type) + .FirstOrDefault(x => x.IsAttached && x.OwnerType.Name == parts[0] && x.Name == parts[1]); + } + } + /// + /// Checks whether a is registered on a type. + /// + /// The type. + /// The property. + /// True if the property is registered, otherwise false. + public bool IsRegistered(Type type, PerspexProperty property) + { + return FindRegistered(type, property) != null; + } + + /// + /// Checks whether a is registered on a object. + /// + /// The object. + /// The property. + /// True if the property is registered, otherwise false. + public bool IsRegistered(object o, PerspexProperty property) + { + return IsRegistered(o.GetType(), property); + } + + /// + /// Registers a on a type. + /// + /// The type. + /// The property. + /// + /// You won't usually want to call this method directly, instead use the + /// method. + /// + public void Register(Type type, PerspexProperty property) + { + Contract.Requires(type != null); + Contract.Requires(property != null); + + List list; + + if (!_registered.TryGetValue(type, out list)) + { + list = new List(); + _registered.Add(type, list); + } + + if (!list.Contains(property)) + { + list.Add(property); + } + + if (property.IsAttached) + { + if (!_attached.TryGetValue(property.OwnerType, out list)) + { + list = new List(); + _attached.Add(property.OwnerType, list); + } + + if (!list.Contains(property)) + { + list.Add(property); + } + } + } + } +} diff --git a/src/Perspex.Base/PerspexProperty`1.cs b/src/Perspex.Base/PerspexProperty`1.cs index 73ac84e102..9a39b95c0c 100644 --- a/src/Perspex.Base/PerspexProperty`1.cs +++ b/src/Perspex.Base/PerspexProperty`1.cs @@ -105,7 +105,7 @@ namespace Perspex "You must provide a new getter and setter when calling AddOwner on a direct PerspexProperty."); } - PerspexObject.Register(typeof(TOwner), this); + PerspexPropertyRegistry.Instance.Register(typeof(TOwner), this); return this; } @@ -119,8 +119,12 @@ namespace Perspex Action setter = null) where TOwner : PerspexObject { - var result = new PerspexProperty(this, CastReturn(getter), CastParam1(setter)); - PerspexObject.Register(typeof(TOwner), result); + var result = new PerspexProperty( + this, + CastReturn(getter), + CastParam1(setter)); + + PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } diff --git a/src/Perspex.Diagnostics/Debug.cs b/src/Perspex.Diagnostics/Debug.cs index d5362c3257..1e07363a71 100644 --- a/src/Perspex.Diagnostics/Debug.cs +++ b/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); diff --git a/src/Perspex.Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Perspex.Diagnostics/ViewModels/ControlDetailsViewModel.cs index 8a45ec8244..73abcf4a04 100644 --- a/src/Perspex.Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/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); diff --git a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj index e343d3e512..398d073db5 100644 --- a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj +++ b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj @@ -75,6 +75,7 @@ + diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs index 7fa816cf82..4233806b65 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs +++ b/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() { diff --git a/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs b/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs new file mode 100644 index 0000000000..c0a9eb1f4f --- /dev/null +++ b/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs @@ -0,0 +1,79 @@ +// 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); + } + + private class Class1 : PerspexObject + { + public static readonly PerspexProperty FooProperty = + PerspexProperty.Register("Foo"); + + public static readonly PerspexProperty BazProperty = + PerspexProperty.Register("Baz"); + + public static readonly PerspexProperty QuxProperty = + PerspexProperty.Register("Qux"); + } + + private class Class2 : Class1 + { + public static readonly PerspexProperty BarProperty = + PerspexProperty.Register("Bar"); + + public static readonly PerspexProperty FlobProperty = + PerspexProperty.Register("Flob"); + + public static readonly PerspexProperty FredProperty = + PerspexProperty.Register("Fred"); + } + + private class AttachedOwner + { + public static readonly PerspexProperty AttachedProperty = + PerspexProperty.RegisterAttached("Attached"); + } + } +} From 6bd5f85d4d924caccda85ce8b3d757d57b2d3f32 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 19 Oct 2015 11:05:44 +0200 Subject: [PATCH 17/18] Update Owner when AddOwnering properties. --- src/Perspex.Base/PerspexProperty.cs | 56 +++++++++++++++++-- src/Perspex.Base/PerspexProperty`1.cs | 28 ++++++++-- .../PerspexPropertyTests.cs | 18 ++++++ 3 files changed, 94 insertions(+), 8 deletions(-) diff --git a/src/Perspex.Base/PerspexProperty.cs b/src/Perspex.Base/PerspexProperty.cs index 62a3d5cfe2..2dd679ce8d 100644 --- a/src/Perspex.Base/PerspexProperty.cs +++ b/src/Perspex.Base/PerspexProperty.cs @@ -29,7 +29,12 @@ namespace Perspex private static int s_nextId = 1; /// - /// The default values for the property, by type. + /// The default value provided when the property was first registered. + /// + private readonly object _defaultValue; + + /// + /// The overridden default values for the property, by type. /// private readonly Dictionary _defaultValues = new Dictionary(); @@ -93,7 +98,7 @@ namespace Perspex Name = name; PropertyType = valueType; OwnerType = ownerType; - _defaultValues.Add(ownerType, defaultValue); + _defaultValue = defaultValue; Inherits = inherits; DefaultBindingMode = defaultBindingMode; IsAttached = isAttached; @@ -144,14 +149,57 @@ namespace Perspex /// Initializes a new instance of the class. /// /// The direct property to copy. + /// The new owner type. + protected PerspexProperty(PerspexProperty source, Type ownerType) + { + Contract.Requires(source != null); + Contract.Requires(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; + } + + /// + /// Initializes a new instance of the class. + /// + /// The direct property to copy. + /// The new owner type. /// A new getter. /// A new setter. protected PerspexProperty( PerspexProperty source, + Type ownerType, Func getter, Action setter) { Contract.Requires(source != null); + Contract.Requires(ownerType != null); Contract.Requires(getter != null); if (!source.IsDirect) @@ -162,7 +210,7 @@ namespace Perspex Name = source.Name; PropertyType = source.PropertyType; - OwnerType = source.OwnerType; + OwnerType = ownerType; Getter = getter; Setter = setter; IsDirect = true; @@ -530,7 +578,7 @@ namespace Perspex type = type.GetTypeInfo().BaseType; } - return _defaultValues[OwnerType]; + return _defaultValue; } /// diff --git a/src/Perspex.Base/PerspexProperty`1.cs b/src/Perspex.Base/PerspexProperty`1.cs index 9a39b95c0c..6d97833f0a 100644 --- a/src/Perspex.Base/PerspexProperty`1.cs +++ b/src/Perspex.Base/PerspexProperty`1.cs @@ -66,17 +66,29 @@ namespace Perspex Setter = setter; } + /// + /// Initializes a new instance of the class. + /// + /// The property to copy. + /// The new owner type. + private PerspexProperty(PerspexProperty source, Type ownerType) + : base(source, ownerType) + { + } + /// /// Initializes a new instance of the class. /// /// The direct property to copy. + /// The new owner type. /// A new getter. /// A new setter. private PerspexProperty( PerspexProperty source, + Type ownerType, Func getter, Action 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."); } - PerspexPropertyRegistry.Instance.Register(typeof(TOwner), this); - return this; + var result = new PerspexProperty(this, typeof(TOwner)); + PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result); + return result; } /// @@ -119,8 +132,15 @@ namespace Perspex Action setter = null) where TOwner : PerspexObject { + if (!IsDirect) + { + throw new InvalidOperationException( + "This overload of AddOwner is for direct PerspexProperties."); + } + var result = new PerspexProperty( - this, + this, + typeof(TOwner), CastReturn(getter), CastParam1(setter)); diff --git a/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs b/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs index 6c90ac1039..c858152da5 100644 --- a/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs +++ b/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("p1", typeof(Class1)); + var p2 = p1.AddOwner(); + + 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("d1", typeof(Class1), o => null, (o, v) => { }); + var p2 = p1.AddOwner(o => null, (o, v) => { }); + + Assert.Equal(typeof(Class3), p2.OwnerType); + } + [Fact] public void AddOwner_With_Getter_And_Setter_On_Standard_Property_Should_Throw() { From 20436d95def298f6e5e33f38f70a08335578b3ae Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 19 Oct 2015 18:01:10 +0200 Subject: [PATCH 18/18] Correctly implement finding property by name. --- src/Perspex.Base/PerspexPropertyRegistry.cs | 31 ++++-- .../PerspexPropertyRegistryTests.cs | 98 +++++++++++++++++++ 2 files changed, 119 insertions(+), 10 deletions(-) diff --git a/src/Perspex.Base/PerspexPropertyRegistry.cs b/src/Perspex.Base/PerspexPropertyRegistry.cs index d5df44a66f..2c388131fa 100644 --- a/src/Perspex.Base/PerspexPropertyRegistry.cs +++ b/src/Perspex.Base/PerspexPropertyRegistry.cs @@ -157,26 +157,37 @@ namespace Perspex throw new ArgumentException("Invalid property name."); } + string propertyName; + var results = GetRegistered(type); + if (parts.Length == 1) { - var result = GetRegistered(type) - .FirstOrDefault(x => !x.IsAttached && x.Name == parts[0]); + propertyName = parts[0]; + } + else + { + var types = GetImplementedTypes(type); - if (result != null) + if (!types.Contains(parts[0])) { - return result; + results = results.Where(x => x.OwnerType.Name == parts[0]); } - // A type can .AddOwner an attached property. - return GetRegistered(type) - .FirstOrDefault(x => x.Name == parts[0]); + propertyName = parts[1]; } - else + + return results.FirstOrDefault(x => x.Name == propertyName); + } + + private IEnumerable GetImplementedTypes(Type type) + { + while (type != null) { - return GetRegistered(type) - .FirstOrDefault(x => x.IsAttached && x.OwnerType.Name == parts[0] && x.Name == parts[1]); + yield return type.Name; + type = type.GetTypeInfo().BaseType; } } + /// /// Checks whether a is registered on a type. /// diff --git a/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs b/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs index c0a9eb1f4f..e2e60c04dc 100644 --- a/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs +++ b/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs @@ -46,6 +46,94 @@ namespace Perspex.Base.UnitTests 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 FooProperty = @@ -70,6 +158,16 @@ namespace Perspex.Base.UnitTests PerspexProperty.Register("Fred"); } + private class Class3 : Class1 + { + public static readonly PerspexProperty AttachedProperty = + AttachedOwner.AttachedProperty.AddOwner(); + } + + public class Class4 : PerspexObject + { + } + private class AttachedOwner { public static readonly PerspexProperty AttachedProperty =