From c5566a19d3cac16da315205da566ef8a07c80222 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 14 Sep 2014 23:14:40 +0200 Subject: [PATCH] Track changes to collections in ItemsControl. And add a simple UI to test adding nodes to tree view. --- Perspex/Controls/ItemsControl.cs | 71 ++++++++++++++++++++++++--- Perspex/Controls/ItemsPresenter.cs | 77 ++++++++++++++++++++++++++++-- Perspex/Controls/Panel.cs | 7 ++- Perspex/Controls/TabControl.cs | 8 +++- Perspex/Controls/TreeView.cs | 2 + Perspex/LogicalExtensions.cs | 37 ++++++++++++++ Perspex/Perspex.csproj | 1 + TestApplication/Program.cs | 50 ++++++++++++++++--- 8 files changed, 234 insertions(+), 19 deletions(-) create mode 100644 Perspex/LogicalExtensions.cs diff --git a/Perspex/Controls/ItemsControl.cs b/Perspex/Controls/ItemsControl.cs index 3de66cd865..7b26728be9 100644 --- a/Perspex/Controls/ItemsControl.cs +++ b/Perspex/Controls/ItemsControl.cs @@ -9,6 +9,8 @@ namespace Perspex.Controls using System; using System.Collections; using System.Collections.Generic; + using System.Collections.Specialized; + using System.ComponentModel; using System.Linq; public class ItemsControl : TemplatedControl @@ -22,11 +24,13 @@ namespace Perspex.Controls public static readonly PerspexProperty ItemsPanelProperty = PerspexProperty.Register("ItemsPanel", defaultValue: DefaultPanel); - private Dictionary itemControls = new Dictionary(); + private Dictionary controlsByItem = new Dictionary(); + + private Dictionary itemsByControl = new Dictionary(); public ItemsControl() { - this.GetObservable(ItemsProperty).Subscribe(this.ItemsChanged); + this.GetObservableWithHistory(ItemsProperty).Subscribe(this.ItemsChanged); } public IEnumerable Items @@ -44,30 +48,61 @@ namespace Perspex.Controls public Control GetControlForItem(object item) { Control result; - this.itemControls.TryGetValue(item, out result); + this.controlsByItem.TryGetValue(item, out result); + return result; + } + + public object GetItemForControl(Control control) + { + object result; + this.itemsByControl.TryGetValue(control, out result); return result; } public IEnumerable GetAllItemControls() { - return this.itemControls.Values; + return this.controlsByItem.Values; } internal Control CreateItemControl(object item) { Control control = this.CreateItemControlOverride(item); - this.itemControls.Add(item, control); + this.itemsByControl.Add(control, item); + this.controlsByItem.Add(item, control); return control; } + internal void RemoveItemControls(IEnumerable items) + { + foreach (object i in items) + { + Control control = this.GetControlForItem(i); + this.controlsByItem.Remove(i); + this.itemsByControl.Remove(control); + } + } + + internal void ClearItemControls() + { + this.controlsByItem.Clear(); + this.itemsByControl.Clear(); + } + protected virtual Control CreateItemControlOverride(object item) { return (item as Control) ?? this.GetDataTemplate(item).Build(item); } - private void ItemsChanged(IEnumerable items) + private void ItemsChanged(Tuple value) { - if (items == null || !items.OfType().Any()) + INotifyPropertyChanged inpc = value.Item1 as INotifyPropertyChanged; + + if (inpc != null) + { + inpc.PropertyChanged -= ItemsPropertyChanged; + } + + if (value.Item2 == null || !value.Item2.OfType().Any()) { this.Classes.Add(":empty"); } @@ -75,6 +110,28 @@ namespace Perspex.Controls { this.Classes.Remove(":empty"); } + + inpc = value.Item2 as INotifyPropertyChanged; + + if (inpc != null) + { + inpc.PropertyChanged += ItemsPropertyChanged; + } + } + + private void ItemsPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "Count") + { + if (((IList)sender).Count == 0) + { + this.Classes.Add(":empty"); + } + else + { + this.Classes.Remove(":empty"); + } + } } } } diff --git a/Perspex/Controls/ItemsPresenter.cs b/Perspex/Controls/ItemsPresenter.cs index f01a21d2c5..ac0cbe728e 100644 --- a/Perspex/Controls/ItemsPresenter.cs +++ b/Perspex/Controls/ItemsPresenter.cs @@ -9,6 +9,7 @@ namespace Perspex.Controls using System; using System.Collections; using System.Collections.Generic; + using System.Collections.Specialized; using System.Linq; using Perspex.Layout; @@ -24,7 +25,7 @@ namespace Perspex.Controls public ItemsPresenter() { - this.GetObservable(ItemsProperty).Subscribe(this.ItemsChanged); + this.GetObservableWithHistory(ItemsProperty).Subscribe(this.ItemsChanged); } public IEnumerable Items @@ -91,23 +92,55 @@ namespace Perspex.Controls } } + private void ClearItemControls() + { + ItemsControl i = this.TemplatedParent as ItemsControl; + + if (i != null) + { + i.ClearItemControls(); + } + } + + private void RemoveItemControls(IEnumerable items) + { + ItemsControl i = this.TemplatedParent as ItemsControl; + + if (i != null) + { + i.RemoveItemControls(items); + } + } + private Panel GetPanel() { if (this.panel == null && this.ItemsPanel != null) { this.panel = this.ItemsPanel.Build(); ((IVisual)this.panel).VisualParent = this; - this.ItemsChanged(this.Items); + this.ItemsChanged(Tuple.Create(default(IEnumerable), this.Items)); } return this.panel; } - private void ItemsChanged(IEnumerable items) + private void ItemsChanged(Tuple value) { + if (value.Item1 != null) + { + INotifyCollectionChanged incc = value.Item1 as INotifyCollectionChanged; + + if (incc != null) + { + incc.CollectionChanged -= this.ItemsCollectionChanged; + } + } + + this.ClearItemControls(); + if (this.panel != null) { - var controls = this.CreateItemControls(items).ToList(); + var controls = this.CreateItemControls(value.Item2).ToList(); foreach (var control in controls) { @@ -115,6 +148,42 @@ namespace Perspex.Controls } this.panel.Children = new Controls(controls); + + INotifyCollectionChanged incc = value.Item2 as INotifyCollectionChanged; + + if (incc != null) + { + incc.CollectionChanged += this.ItemsCollectionChanged; + } + } + } + + private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (this.panel != null) + { + // TODO: Handle Move and Replace. + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + var controls = this.CreateItemControls(e.NewItems).ToList(); + + foreach (var control in controls) + { + control.TemplatedParent = null; + } + + this.panel.Children.AddRange(controls); + break; + + case NotifyCollectionChangedAction.Remove: + this.RemoveItemControls(e.OldItems); + break; + + case NotifyCollectionChangedAction.Reset: + this.ItemsChanged(Tuple.Create(this.Items, this.Items)); + break; + } } } } diff --git a/Perspex/Controls/Panel.cs b/Perspex/Controls/Panel.cs index 901d30ea44..4c216b5baf 100644 --- a/Perspex/Controls/Panel.cs +++ b/Perspex/Controls/Panel.cs @@ -17,7 +17,7 @@ namespace Perspex.Controls /// /// Base class for controls that can contain multiple children. /// - public class Panel : Control, IVisual + public class Panel : Control, ILogical, IVisual { private Controls children; @@ -51,6 +51,11 @@ namespace Perspex.Controls } } + IEnumerable ILogical.LogicalChildren + { + get { return this.children ?? Enumerable.Empty(); } + } + IEnumerable IVisual.VisualChildren { get { return this.children ?? Enumerable.Empty(); } diff --git a/Perspex/Controls/TabControl.cs b/Perspex/Controls/TabControl.cs index 6828466a31..0f0ee31607 100644 --- a/Perspex/Controls/TabControl.cs +++ b/Perspex/Controls/TabControl.cs @@ -7,10 +7,11 @@ namespace Perspex.Controls { using System; + using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; - public class TabControl : SelectingItemsControl + public class TabControl : SelectingItemsControl, ILogical { public static readonly PerspexProperty SelectedContentProperty = PerspexProperty.Register("SelectedContent"); @@ -22,6 +23,11 @@ namespace Perspex.Controls this.GetObservable(SelectedItemProperty).Skip(1).Subscribe(this.SelectedItemChanged); } + IEnumerable ILogical.LogicalChildren + { + get { return this.Items.OfType(); } + } + protected override void OnTemplateApplied() { this.tabStrip = this.GetTemplateControls() diff --git a/Perspex/Controls/TreeView.cs b/Perspex/Controls/TreeView.cs index bcd07c8c21..6e2c8446c2 100644 --- a/Perspex/Controls/TreeView.cs +++ b/Perspex/Controls/TreeView.cs @@ -66,6 +66,8 @@ namespace Perspex.Controls { i.IsSelected = i == item; } + + this.SelectedItem = this.GetItemForControl(item); } } diff --git a/Perspex/LogicalExtensions.cs b/Perspex/LogicalExtensions.cs new file mode 100644 index 0000000000..f2ff0953f7 --- /dev/null +++ b/Perspex/LogicalExtensions.cs @@ -0,0 +1,37 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Perspex.Controls; + using Perspex.Styling; + + public static class LogicalExtensions + { + public static T FindControl(this ILogical control, string id) where T : Control + { + return control.GetLogicalDescendents() + .OfType() + .FirstOrDefault(x => x.Id == id); + } + + public static IEnumerable GetLogicalDescendents(this ILogical control) + { + foreach (ILogical child in control.LogicalChildren) + { + yield return child; + + foreach (ILogical descendent in child.GetLogicalDescendents()) + { + yield return descendent; + } + } + } + } +} diff --git a/Perspex/Perspex.csproj b/Perspex/Perspex.csproj index a35b5aa8e6..a045aeac5a 100644 --- a/Perspex/Perspex.csproj +++ b/Perspex/Perspex.csproj @@ -218,6 +218,7 @@ + diff --git a/TestApplication/Program.cs b/TestApplication/Program.cs index 1df8fa0828..e01e484606 100644 --- a/TestApplication/Program.cs +++ b/TestApplication/Program.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using System.Linq; using Perspex; using Perspex.Controls; using Perspex.Layout; @@ -34,18 +35,23 @@ namespace TestApplication class Node { + public Node() + { + this.Children = new PerspexList(); + } + public string Name { get; set; } - public IEnumerable Children { get; set; } + public PerspexList Children { get; set; } } class Program { - private static Node[] treeData = new[] + private static PerspexList treeData = new PerspexList { new Node { Name = "Root 1", - Children = new[] + Children = new PerspexList { new Node { @@ -54,7 +60,7 @@ namespace TestApplication new Node { Name = "Child 2", - Children = new[] + Children = new PerspexList { new Node { @@ -176,7 +182,7 @@ namespace TestApplication IsSelected = true, Content = new StackPanel { - Orientation = Orientation.Vertical, + Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Gap = 8, @@ -184,8 +190,27 @@ namespace TestApplication { new TreeView { + Id = "treeView", Items = treeData, }, + new StackPanel + { + Orientation = Orientation.Vertical, + Gap = 2.0, + Children = new Controls + { + new TextBox + { + Id = "newTreeViewItemText", + Text = "New Item" + }, + new Button + { + Id = "addTreeViewItem", + Content = "Add", + }, + } + }, } }, }, @@ -193,7 +218,20 @@ namespace TestApplication } }; - //System.Console.WriteLine(Perspex.Diagnostics.Debug.PrintVisualTree(window)); + var treeView = window.FindControl("treeView"); + var newTreeViewItemText = window.FindControl("newTreeViewItemText"); + var addTreeViewItem = window.FindControl