From d25c4e0b4c4a7596f3cfefb1eb4638dd44e1d34a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 30 Jan 2015 00:57:58 +0100 Subject: [PATCH] Some work on TabControl/TabStrip. --- Perspex.Controls/ControlExtensions.cs | 2 +- Perspex.Controls/ItemsControl.cs | 19 +-- Perspex.Controls/Presenters/ItemsPresenter.cs | 32 ++-- .../Primitives/SelectingItemsControl.cs | 2 + Perspex.Controls/Primitives/TabStrip.cs | 6 +- .../Primitives/TemplatedControl.cs | 35 +++- Perspex.Controls/TabControl.cs | 39 +---- Perspex.Themes.Default/ItemsControlStyle.cs | 1 + Perspex.Themes.Default/TabControlStyle.cs | 2 + Perspex.Themes.Default/TreeViewStyle.cs | 1 + .../ListBoxTests.cs | 4 +- .../Perspex.Controls.UnitTests.csproj | 3 + .../SelectingItemsControlTests.cs | 31 ++++ .../TabControlTests.cs | 161 ++++++++++++++++++ .../TabStripTests.cs | 102 +++++++++++ 15 files changed, 366 insertions(+), 74 deletions(-) create mode 100644 Tests/Perspex.Controls.UnitTests/SelectingItemsControlTests.cs create mode 100644 Tests/Perspex.Controls.UnitTests/TabControlTests.cs create mode 100644 Tests/Perspex.Controls.UnitTests/TabStripTests.cs diff --git a/Perspex.Controls/ControlExtensions.cs b/Perspex.Controls/ControlExtensions.cs index 56cc5e86cc..c1211e1c5b 100644 --- a/Perspex.Controls/ControlExtensions.cs +++ b/Perspex.Controls/ControlExtensions.cs @@ -31,7 +31,7 @@ namespace Perspex.Controls { return visual.GetVisualDescendents() .OfType() - .Where(x => x.TemplatedParent == control); + .Where(x => x.TemplatedParent != null); } else { diff --git a/Perspex.Controls/ItemsControl.cs b/Perspex.Controls/ItemsControl.cs index 08a4f2ba27..9893472505 100644 --- a/Perspex.Controls/ItemsControl.cs +++ b/Perspex.Controls/ItemsControl.cs @@ -87,21 +87,10 @@ namespace Perspex.Controls this.logicalChildren = null; } - this.presenter = this.FindTemplateChild("itemsPresenter"); - - if (this.presenter != null) - { - var panel = (IVisual)this.presenter.GetVisualChildren().Single(); - this.logicalChildren = new PerspexReadOnlyListView( - panel.VisualChildren, - x => (ILogical)x); - } - else - { - this.logicalChildren = new PerspexReadOnlyListView( - new PerspexList(), - x => (ILogical)x); - } + this.presenter = this.GetTemplateChild("itemsPresenter"); + this.logicalChildren = new PerspexReadOnlyListView( + ((IVisual)presenter.Panel).VisualChildren, + x => (ILogical)x); } private void ItemsChanged(Tuple value) diff --git a/Perspex.Controls/Presenters/ItemsPresenter.cs b/Perspex.Controls/Presenters/ItemsPresenter.cs index e39f6e3ff7..aabf4e16ad 100644 --- a/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -21,8 +21,6 @@ namespace Perspex.Controls.Presenters public static readonly PerspexProperty ItemsPanelProperty = ItemsControl.ItemsPanelProperty.AddOwner(); - private Panel panel; - private bool createdPanel; public ItemsPresenter() @@ -42,6 +40,12 @@ namespace Perspex.Controls.Presenters set { this.SetValue(ItemsPanelProperty, value); } } + public Panel Panel + { + get; + private set; + } + public override sealed void ApplyTemplate() { if (!this.createdPanel) @@ -52,23 +56,23 @@ namespace Perspex.Controls.Presenters protected override Size MeasureOverride(Size availableSize) { - this.panel.Measure(availableSize); - return this.panel.DesiredSize.Value; + this.Panel.Measure(availableSize); + return this.Panel.DesiredSize.Value; } protected override Size ArrangeOverride(Size finalSize) { - this.panel.Arrange(new Rect(finalSize)); + this.Panel.Arrange(new Rect(finalSize)); return finalSize; } private void CreatePanel() { this.ClearVisualChildren(); - this.panel = this.ItemsPanel.Build(); - this.panel.TemplatedParent = this; - ((IItemsPanel)this.panel).ChildLogicalParent = this.TemplatedParent as ILogical; - this.AddVisualChild(this.panel); + this.Panel = this.ItemsPanel.Build(); + this.Panel.TemplatedParent = this; + ((IItemsPanel)this.Panel).ChildLogicalParent = this.TemplatedParent as ILogical; + this.AddVisualChild(this.Panel); this.createdPanel = true; this.ItemsChanged(Tuple.Create(default(IEnumerable), this.Items)); } @@ -93,7 +97,7 @@ namespace Perspex.Controls.Presenters if (value.Item1 != null) { - this.panel.Children.RemoveAll(generator.Remove(value.Item1)); + this.Panel.Children.RemoveAll(generator.Remove(value.Item1)); INotifyCollectionChanged incc = value.Item1 as INotifyCollectionChanged; @@ -103,11 +107,11 @@ namespace Perspex.Controls.Presenters } } - if (this.panel != null) + if (this.Panel != null) { if (value.Item2 != null) { - this.panel.Children.AddRange(generator.Generate(this.Items)); + this.Panel.Children.AddRange(generator.Generate(this.Items)); INotifyCollectionChanged incc = value.Item2 as INotifyCollectionChanged; @@ -130,11 +134,11 @@ namespace Perspex.Controls.Presenters switch (e.Action) { case NotifyCollectionChangedAction.Add: - this.panel.Children.AddRange(generator.Generate(e.NewItems)); + this.Panel.Children.AddRange(generator.Generate(e.NewItems)); break; case NotifyCollectionChangedAction.Remove: - this.panel.Children.RemoveAll(generator.Remove(e.OldItems)); + this.Panel.Children.RemoveAll(generator.Remove(e.OldItems)); break; } diff --git a/Perspex.Controls/Primitives/SelectingItemsControl.cs b/Perspex.Controls/Primitives/SelectingItemsControl.cs index 74ce1b4bf5..c18e9cffcf 100644 --- a/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -130,6 +130,8 @@ namespace Perspex.Controls.Primitives this.SelectedItem = item; } } + + e.Handled = true; } private void SelectedItemChanged(object selected) diff --git a/Perspex.Controls/Primitives/TabStrip.cs b/Perspex.Controls/Primitives/TabStrip.cs index 91b13da1c6..8ce0c73f9c 100644 --- a/Perspex.Controls/Primitives/TabStrip.cs +++ b/Perspex.Controls/Primitives/TabStrip.cs @@ -26,15 +26,13 @@ namespace Perspex.Controls.Primitives public TabStrip() { - this.Bind( - SelectedTabProperty, - this.GetObservable(SelectedItemProperty).Select(x => x as TabItem)); + this.BindTwoWay(SelectedTabProperty, this, SelectingItemsControl.SelectedItemProperty); } public TabItem SelectedTab { get { return this.GetValue(SelectedTabProperty); } - private set { this.SetValue(SelectedTabProperty, value); } + set { this.SetValue(SelectedTabProperty, value); } } protected override ItemContainerGenerator CreateItemContainerGenerator() diff --git a/Perspex.Controls/Primitives/TemplatedControl.cs b/Perspex.Controls/Primitives/TemplatedControl.cs index d0e73ed9ff..5eb78dbeb7 100644 --- a/Perspex.Controls/Primitives/TemplatedControl.cs +++ b/Perspex.Controls/Primitives/TemplatedControl.cs @@ -164,7 +164,37 @@ namespace Perspex.Controls.Primitives protected T FindTemplateChild(string id) where T : Control { - return (T)this.GetTemplateControls().SingleOrDefault(x => x.Id == id); + var matches = this.GetTemplateControls() + .OfType() + .Where(x => x.Id == id) + .ToList(); + + if (matches.Count == 1) + { + return matches[0]; + } + else if (matches.Count == 0) + { + return null; + } + else + { + // If there are multiple matches, try filtering out nested matches. + matches = matches.Where(x => x.TemplatedParent == this).ToList(); + + if (matches.Count > 1) + { + throw new InvalidOperationException(string.Format( + "Found multiple template children '{0}' of type '{1}' in template for '{2}'.", + id, + typeof(T).FullName, + this.GetType().FullName)); + } + else + { + return matches.FirstOrDefault(); + } + } } protected T GetTemplateChild(string id) where T : Control @@ -174,8 +204,9 @@ namespace Perspex.Controls.Primitives if (result == null) { throw new InvalidOperationException(string.Format( - "Could not find template child '{0}' in template for '{1}'.", + "Could not find template child '{0}' of type '{1}' in template for '{2}'.", id, + typeof(T).FullName, this.GetType().FullName)); } diff --git a/Perspex.Controls/TabControl.cs b/Perspex.Controls/TabControl.cs index eb9cb0eb11..5f7416c29d 100644 --- a/Perspex.Controls/TabControl.cs +++ b/Perspex.Controls/TabControl.cs @@ -14,7 +14,7 @@ namespace Perspex.Controls using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; - public class TabControl : SelectingItemsControl, ILogical + public class TabControl : SelectingItemsControl { public static readonly PerspexProperty SelectedContentProperty = PerspexProperty.Register("SelectedContent"); @@ -22,12 +22,6 @@ namespace Perspex.Controls public static readonly PerspexProperty SelectedTabProperty = PerspexProperty.Register("SelectedTab"); - private TabStrip tabStrip; - - private ContentPresenter presenter; - - private IDisposable presenterSubscription; - private SingleItemPerspexList logicalChild = new SingleItemPerspexList(); public TabControl() @@ -39,9 +33,7 @@ namespace Perspex.Controls this.SetValue(SelectedContentProperty, content); }); - this.Bind( - SelectedTabProperty, - this.GetObservable(SelectedItemProperty).Select(x => x as TabItem)); + this.BindTwoWay(SelectedTabProperty, this, SelectingItemsControl.SelectedItemProperty); } public object SelectedContent @@ -53,37 +45,12 @@ namespace Perspex.Controls public TabItem SelectedTab { get { return this.GetValue(SelectedTabProperty); } - private set { this.SetValue(SelectedTabProperty, value); } - } - - IReadOnlyPerspexList ILogical.LogicalChildren - { - get { return this.logicalChild; } + set { this.SetValue(SelectedTabProperty, value); } } protected override ItemContainerGenerator CreateItemContainerGenerator() { return new TypedItemContainerGenerator(this); } - - protected override void OnTemplateApplied() - { - if (this.presenterSubscription != null) - { - this.presenterSubscription.Dispose(); - this.presenterSubscription = null; - } - - this.presenter = this.FindTemplateChild("contentPresenter"); - - if (this.presenter != null) - { - this.presenterSubscription = this.presenter.ChildObservable - .Subscribe(x => this.logicalChild.SingleItem = x); - } - - this.tabStrip = this.GetTemplateControls().OfType().FirstOrDefault(); - this.BindTwoWay(TabControl.SelectedItemProperty, this.tabStrip, TabControl.SelectedItemProperty); - } } } diff --git a/Perspex.Themes.Default/ItemsControlStyle.cs b/Perspex.Themes.Default/ItemsControlStyle.cs index bd0766e66b..d9c1862862 100644 --- a/Perspex.Themes.Default/ItemsControlStyle.cs +++ b/Perspex.Themes.Default/ItemsControlStyle.cs @@ -31,6 +31,7 @@ namespace Perspex.Themes.Default { return new ItemsPresenter { + Id = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], }; diff --git a/Perspex.Themes.Default/TabControlStyle.cs b/Perspex.Themes.Default/TabControlStyle.cs index b3b919374a..efd112128b 100644 --- a/Perspex.Themes.Default/TabControlStyle.cs +++ b/Perspex.Themes.Default/TabControlStyle.cs @@ -42,7 +42,9 @@ namespace Perspex.Themes.Default { new TabStrip { + Id = "tabStrip", [~TabStrip.ItemsProperty] = control[~TabControl.ItemsProperty], + [~~TabStrip.SelectedTabProperty] = control[~~TabControl.SelectedTabProperty], }, new ContentPresenter { diff --git a/Perspex.Themes.Default/TreeViewStyle.cs b/Perspex.Themes.Default/TreeViewStyle.cs index 76f6a32c15..23f0d51c3d 100644 --- a/Perspex.Themes.Default/TreeViewStyle.cs +++ b/Perspex.Themes.Default/TreeViewStyle.cs @@ -42,6 +42,7 @@ namespace Perspex.Themes.Default { Content = new ItemsPresenter { + Id = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~TreeView.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~TreeView.ItemsPanelProperty], } diff --git a/Tests/Perspex.Controls.UnitTests/ListBoxTests.cs b/Tests/Perspex.Controls.UnitTests/ListBoxTests.cs index fd32940a08..36d98a2fe5 100644 --- a/Tests/Perspex.Controls.UnitTests/ListBoxTests.cs +++ b/Tests/Perspex.Controls.UnitTests/ListBoxTests.cs @@ -21,7 +21,7 @@ namespace Perspex.Controls.UnitTests { var target = new ListBox { - Template = new ControlTemplate(x => this.CreateListBoxTemplate(x)), + Template = new ControlTemplate(this.CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, }; @@ -39,7 +39,7 @@ namespace Perspex.Controls.UnitTests { return new ScrollViewer { - Template = new ControlTemplate(x => this.CreateScrollViewerTemplate(x)), + Template = new ControlTemplate(this.CreateScrollViewerTemplate), Content = new ItemsPresenter { Id = "itemsPresenter", diff --git a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index 676b73224b..7016fd417f 100644 --- a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -65,10 +65,13 @@ + + + diff --git a/Tests/Perspex.Controls.UnitTests/SelectingItemsControlTests.cs b/Tests/Perspex.Controls.UnitTests/SelectingItemsControlTests.cs new file mode 100644 index 0000000000..8257db9ee7 --- /dev/null +++ b/Tests/Perspex.Controls.UnitTests/SelectingItemsControlTests.cs @@ -0,0 +1,31 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls.UnitTests +{ + using Perspex.Controls.Primitives; + using Perspex.Input; + using Perspex.Interactivity; + using Xunit; + + public class SelectingItemsControlTests + { + [Fact] + public void PointerPressed_Event_Should_Be_Handled() + { + var target = new SelectingItemsControl(); + + var e = new PointerPressEventArgs + { + RoutedEvent = InputElement.PointerPressedEvent + }; + + target.RaiseEvent(e); + + Assert.True(e.Handled); + } + } +} diff --git a/Tests/Perspex.Controls.UnitTests/TabControlTests.cs b/Tests/Perspex.Controls.UnitTests/TabControlTests.cs new file mode 100644 index 0000000000..fc799a93bd --- /dev/null +++ b/Tests/Perspex.Controls.UnitTests/TabControlTests.cs @@ -0,0 +1,161 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls.UnitTests +{ + using System.Linq; + using Perspex.Controls.Presenters; + using Perspex.Controls.Primitives; + using Perspex.LogicalTree; + using Perspex.Styling; + using Xunit; + + public class TabControlTests + { + [Fact] + public void First_Tab_Should_Be_Selected_By_Default() + { + var target = new TabControl + { + Template = ControlTemplate.Create(this.CreateTabControlTemplate), + Items = new[] + { + new TabItem + { + Id = "first" + }, + new TabItem + { + Id = "second" + }, + } + }; + + target.ApplyTemplate(); + + Assert.NotNull(target.SelectedTab); + Assert.Equal(target.SelectedTab, target.SelectedItem); + } + + [Fact] + public void First_Tab_Content_Should_Be_Displayed_By_Default() + { + var target = new TabControl + { + Template = ControlTemplate.Create(this.CreateTabControlTemplate), + Items = new[] + { + new TabItem + { + Content = new TextBlock(), + }, + new TabItem + { + Content = new Border(), + }, + } + }; + + target.ApplyTemplate(); + + Assert.IsType(target.SelectedContent); + } + + [Fact] + public void Setting_SelectedItem_Should_Set_SelectedTab() + { + var target = new TabControl + { + Template = ControlTemplate.Create(this.CreateTabControlTemplate), + Items = new[] + { + new TabItem + { + Id = "first" + }, + new TabItem + { + Id = "second" + }, + } + }; + + target.ApplyTemplate(); + target.SelectedItem = target.Items.Cast().ElementAt(1); + + Assert.Same(target.SelectedTab, target.SelectedItem); + } + + [Fact] + public void Setting_SelectedTab_Should_Set_SelectedItem() + { + var target = new TabControl + { + Template = ControlTemplate.Create(this.CreateTabControlTemplate), + Items = new[] + { + new TabItem + { + Id = "first" + }, + new TabItem + { + Id = "second" + }, + } + }; + + target.ApplyTemplate(); + target.SelectedTab = target.Items.Cast().ElementAt(1); + + Assert.Same(target.SelectedItem, target.SelectedTab); + } + + [Fact] + public void Logical_Children_Should_Be_TabItems() + { + var target = new TabControl + { + Template = ControlTemplate.Create(this.CreateTabControlTemplate), + Items = new[] { "Foo", "Bar" }, + }; + + target.ApplyTemplate(); + + Assert.Equal(2, target.GetLogicalChildren().Count()); + } + + private Control CreateTabControlTemplate(TabControl parent) + { + return new StackPanel + { + Children = new Controls + { + new TabStrip + { + Id = "tabStrip", + Template = ControlTemplate.Create(this.CreateTabStripTemplate), + [~TabStrip.ItemsProperty] = parent[~TabControl.ItemsProperty], + [~~TabStrip.SelectedTabProperty] = parent[~~TabControl.SelectedTabProperty] + }, + new ContentPresenter + { + Id = "contentPresenter", + } + } + }; + } + + private Control CreateTabStripTemplate(TabStrip parent) + { + return new ItemsPresenter + { + Id = "itemsPresenter", + [~ItemsPresenter.ItemsProperty] = parent[~TabStrip.ItemsProperty], + }; + } + } +} diff --git a/Tests/Perspex.Controls.UnitTests/TabStripTests.cs b/Tests/Perspex.Controls.UnitTests/TabStripTests.cs new file mode 100644 index 0000000000..910ebb7ab6 --- /dev/null +++ b/Tests/Perspex.Controls.UnitTests/TabStripTests.cs @@ -0,0 +1,102 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls.UnitTests +{ + using System.Linq; + using Perspex.Controls.Presenters; + using Perspex.Controls.Primitives; + using Perspex.LogicalTree; + using Perspex.Styling; + using Xunit; + + public class TabStripTests + { + [Fact] + public void First_Tab_Should_Be_Selected_By_Default() + { + var target = new TabStrip + { + Template = ControlTemplate.Create(this.CreateTabStripTemplate), + Items = new[] + { + new TabItem + { + Id = "first" + }, + new TabItem + { + Id = "second" + }, + } + }; + + target.ApplyTemplate(); + + Assert.Equal(target.Items.Cast().First(), target.SelectedItem); + Assert.Equal(target.Items.Cast().First(), target.SelectedTab); + } + + [Fact] + public void Setting_SelectedItem_Should_Set_SelectedTab() + { + var target = new TabStrip + { + Template = ControlTemplate.Create(this.CreateTabStripTemplate), + Items = new[] + { + new TabItem + { + Id = "first" + }, + new TabItem + { + Id = "second" + }, + } + }; + + target.ApplyTemplate(); + target.SelectedItem = target.Items.Cast().ElementAt(1); + + Assert.Same(target.SelectedTab, target.SelectedItem); + } + + [Fact] + public void Setting_SelectedTab_Should_Set_SelectedItem() + { + var target = new TabStrip + { + Template = ControlTemplate.Create(this.CreateTabStripTemplate), + Items = new[] + { + new TabItem + { + Id = "first" + }, + new TabItem + { + Id = "second" + }, + } + }; + + target.ApplyTemplate(); + target.SelectedTab = target.Items.Cast().ElementAt(1); + + Assert.Same(target.SelectedItem, target.SelectedTab); + } + + private Control CreateTabStripTemplate(TabStrip parent) + { + return new ItemsPresenter + { + Id = "itemsPresenter", + [~ItemsPresenter.ItemsProperty] = parent[~TabStrip.ItemsProperty], + }; + } + } +}