From 4fa3c98ca381f1f47706bf9ec3743c18a7f7637a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 30 Nov 2015 21:06:24 +0100 Subject: [PATCH 01/36] Detach styles when control removed from visual tree. --- src/Perspex.Controls/ItemsControl.cs | 7 ++ .../Primitives/TemplatedControl.cs | 17 ++-- src/Perspex.Layout/LayoutManager.cs | 4 +- .../Properties/AssemblyInfo.cs | 4 +- src/Perspex.Styling/Styling/Style.cs | 14 +++- src/Perspex.Styling/Styling/StyleBinding.cs | 13 +-- .../Perspex.Styling.UnitTests.csproj | 2 + .../StyleBindingTests.cs | 79 +++++++++++++++++++ tests/Perspex.Styling.UnitTests/StyleTests.cs | 27 ++++++- tests/Perspex.Styling.UnitTests/TestRoot.cs | 34 ++++++++ 10 files changed, 184 insertions(+), 17 deletions(-) create mode 100644 tests/Perspex.Styling.UnitTests/StyleBindingTests.cs create mode 100644 tests/Perspex.Styling.UnitTests/TestRoot.cs diff --git a/src/Perspex.Controls/ItemsControl.cs b/src/Perspex.Controls/ItemsControl.cs index 5c06dbc04e..02357c6f3f 100644 --- a/src/Perspex.Controls/ItemsControl.cs +++ b/src/Perspex.Controls/ItemsControl.cs @@ -152,6 +152,13 @@ namespace Perspex.Controls Presenter = nameScope.Find("PART_ItemsPresenter"); } + /// + protected override void OnTemplateChanged(PerspexPropertyChangedEventArgs e) + { + base.OnTemplateChanged(e); + ItemContainerGenerator.Clear(); + } + /// /// Caled when the property changes. /// diff --git a/src/Perspex.Controls/Primitives/TemplatedControl.cs b/src/Perspex.Controls/Primitives/TemplatedControl.cs index 9e98ad154c..92e1c4af88 100644 --- a/src/Perspex.Controls/Primitives/TemplatedControl.cs +++ b/src/Perspex.Controls/Primitives/TemplatedControl.cs @@ -81,12 +81,7 @@ namespace Perspex.Controls.Primitives /// static TemplatedControl() { - TemplateProperty.Changed.Subscribe(e => - { - var templatedControl = (TemplatedControl)e.Sender; - templatedControl._templateApplied = false; - templatedControl.InvalidateMeasure(); - }); + TemplateProperty.Changed.AddClassHandler(x => x.OnTemplateChanged); } /// @@ -224,6 +219,16 @@ namespace Perspex.Controls.Primitives { } + /// + /// Called when the property changes. + /// + /// The event args. + protected virtual void OnTemplateChanged(PerspexPropertyChangedEventArgs e) + { + _templateApplied = false; + InvalidateMeasure(); + } + /// /// Sets the TemplatedParent property for a control created from the control template and /// applies the templates of nested templated controls. Also adds each control to its name diff --git a/src/Perspex.Layout/LayoutManager.cs b/src/Perspex.Layout/LayoutManager.cs index f1c45693ab..326549ccae 100644 --- a/src/Perspex.Layout/LayoutManager.cs +++ b/src/Perspex.Layout/LayoutManager.cs @@ -226,12 +226,12 @@ namespace Perspex.Layout { var parent = item.Control.GetVisualParent(); - while (parent.PreviousMeasure == null) + while (parent != null && parent.PreviousMeasure == null) { parent = parent.GetVisualParent(); } - if (parent.GetVisualRoot() == Root) + if (parent != null && parent.GetVisualRoot() == Root) { parent.Measure(parent.PreviousMeasure.Value, true); } diff --git a/src/Perspex.Styling/Properties/AssemblyInfo.cs b/src/Perspex.Styling/Properties/AssemblyInfo.cs index f06c17710e..21034a0753 100644 --- a/src/Perspex.Styling/Properties/AssemblyInfo.cs +++ b/src/Perspex.Styling/Properties/AssemblyInfo.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Reflection; +using System.Runtime.CompilerServices; using Perspex.Metadata; [assembly: AssemblyTitle("Perspex.Styling")] -[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Styling")] \ No newline at end of file +[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Styling")] +[assembly: InternalsVisibleTo("Perspex.Styling.UnitTests")] \ No newline at end of file diff --git a/src/Perspex.Styling/Styling/Style.cs b/src/Perspex.Styling/Styling/Style.cs index 61b05d1412..112bc817a0 100644 --- a/src/Perspex.Styling/Styling/Style.cs +++ b/src/Perspex.Styling/Styling/Style.cs @@ -56,9 +56,21 @@ namespace Perspex.Styling if (match.ImmediateResult != false) { + var visual = control as IVisual; + var activator = match.ObservableResult ?? + Observable.Never().StartWith(true); + + if (visual != null) + { + var detached = Observable.FromEventPattern( + x => visual.DetachedFromVisualTree += x, + x => visual.DetachedFromVisualTree -= x); + activator = activator.TakeUntil(detached); + } + foreach (var setter in Setters) { - setter.Apply(this, control, match.ObservableResult); + setter.Apply(this, control, activator); } } } diff --git a/src/Perspex.Styling/Styling/StyleBinding.cs b/src/Perspex.Styling/Styling/StyleBinding.cs index 9eb6f00235..b2ddbeee1b 100644 --- a/src/Perspex.Styling/Styling/StyleBinding.cs +++ b/src/Perspex.Styling/Styling/StyleBinding.cs @@ -61,7 +61,8 @@ namespace Perspex.Styling /// public object ActivatedValue { - get; } + get; + } /// /// Gets a description of the binding. @@ -90,16 +91,16 @@ namespace Perspex.Styling if (Source == null) { - return _activator.Subscribe( - active => observer.OnNext(active ? ActivatedValue : PerspexProperty.UnsetValue), - observer.OnError, - observer.OnCompleted); + return _activator + .Select(active => active ? ActivatedValue : PerspexProperty.UnsetValue) + .Subscribe(observer); } else { return _activator .CombineLatest(Source, (x, y) => new { Active = x, Value = y }) - .Subscribe(x => observer.OnNext(x.Active ? x.Value : PerspexProperty.UnsetValue)); + .Select(x => x.Active ? x.Value : PerspexProperty.UnsetValue) + .Subscribe(observer); } } } diff --git a/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj b/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj index 4099319b5a..95cb831480 100644 --- a/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj +++ b/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj @@ -85,10 +85,12 @@ + + diff --git a/tests/Perspex.Styling.UnitTests/StyleBindingTests.cs b/tests/Perspex.Styling.UnitTests/StyleBindingTests.cs new file mode 100644 index 0000000000..82a7f66d07 --- /dev/null +++ b/tests/Perspex.Styling.UnitTests/StyleBindingTests.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; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Xunit; + +namespace Perspex.Styling.UnitTests +{ + public class StyleBindingTests + { + [Fact] + public async void Should_Produce_UnsetValue_On_Activator_False() + { + var activator = new BehaviorSubject(false); + var target = new StyleBinding(activator, 1, string.Empty); + var result = await target.Take(1); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + + [Fact] + public async void Should_Produce_Value_On_Activator_True() + { + var activator = new BehaviorSubject(true); + var target = new StyleBinding(activator, 1, string.Empty); + var result = await target.Take(1); + + Assert.Equal(1, result); + } + + [Fact] + public void Should_Change_Value_On_Activator_Change() + { + var activator = new BehaviorSubject(false); + var target = new StyleBinding(activator, 1, string.Empty); + var result = new List(); + + target.Subscribe(x => result.Add(x)); + + activator.OnNext(true); + activator.OnNext(false); + + Assert.Equal(new[] { PerspexProperty.UnsetValue, 1, PerspexProperty.UnsetValue }, result); + } + + [Fact] + public void Should_Change_Value_With_Source_Observable() + { + var activator = new BehaviorSubject(false); + var source = new BehaviorSubject(1); + var target = new StyleBinding(activator, source, string.Empty); + var result = new List(); + + target.Subscribe(x => result.Add(x)); + + activator.OnNext(true); + source.OnNext(2); + activator.OnNext(false); + + Assert.Equal(new[] { PerspexProperty.UnsetValue, 1, 2, PerspexProperty.UnsetValue }, result); + } + + [Fact] + public void Should_Complete_When_Activator_Completes() + { + var activator = new BehaviorSubject(false); + var target = new StyleBinding(activator, 1, string.Empty); + var completed = false; + + target.Subscribe(_ => { }, () => completed = true); + activator.OnCompleted(); + + Assert.True(completed); + } + } +} diff --git a/tests/Perspex.Styling.UnitTests/StyleTests.cs b/tests/Perspex.Styling.UnitTests/StyleTests.cs index cda975b597..c484389cbf 100644 --- a/tests/Perspex.Styling.UnitTests/StyleTests.cs +++ b/tests/Perspex.Styling.UnitTests/StyleTests.cs @@ -166,7 +166,7 @@ namespace Perspex.Styling.UnitTests { var source = new BehaviorSubject("Foo"); - Style style = new Style(x => x.OfType().Class("foo")) + var style = new Style(x => x.OfType().Class("foo")) { Setters = new[] { @@ -187,6 +187,31 @@ namespace Perspex.Styling.UnitTests Assert.Equal("foodefault", target.Foo); } + [Fact] + public void Style_Should_Detach_When_Removed_From_Visual_Tree() + { + Border border; + + var style = new Style(x => x.OfType()) + { + Setters = new[] + { + new Setter(Border.BorderThicknessProperty, 4), + } + }; + + var root = new TestRoot + { + Child = border = new Border(), + }; + + style.Attach(border, null); + + Assert.Equal(4, border.BorderThickness); + root.Child = null; + Assert.Equal(0, border.BorderThickness); + } + private class Class1 : Control { public static readonly PerspexProperty FooProperty = diff --git a/tests/Perspex.Styling.UnitTests/TestRoot.cs b/tests/Perspex.Styling.UnitTests/TestRoot.cs new file mode 100644 index 0000000000..2f613e4e51 --- /dev/null +++ b/tests/Perspex.Styling.UnitTests/TestRoot.cs @@ -0,0 +1,34 @@ +// 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 Moq; +using Perspex.Controls; +using Perspex.Layout; +using Perspex.Platform; +using Perspex.Rendering; + +namespace Perspex.Styling.UnitTests +{ + internal class TestRoot : Decorator, ILayoutRoot, IRenderRoot + { + public Size ClientSize => new Size(100, 100); + + public ILayoutManager LayoutManager => new Mock().Object; + + public IRenderTarget RenderTarget + { + get { throw new NotImplementedException(); } + } + + public IRenderQueueManager RenderQueueManager + { + get { throw new NotImplementedException(); } + } + + public Point TranslatePointToScreen(Point p) + { + return new Point(); + } + } +} From 2bcaef3c7ecb02ceb6bdca2aa82c8372515e1e37 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 30 Nov 2015 21:32:50 +0100 Subject: [PATCH 02/36] Fixed failing TreeView tests. --- src/Perspex.Controls/ItemsControl.cs | 6 +++++- tests/Perspex.Controls.UnitTests/TreeViewTests.cs | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Perspex.Controls/ItemsControl.cs b/src/Perspex.Controls/ItemsControl.cs index 02357c6f3f..1fc232870e 100644 --- a/src/Perspex.Controls/ItemsControl.cs +++ b/src/Perspex.Controls/ItemsControl.cs @@ -156,7 +156,11 @@ namespace Perspex.Controls protected override void OnTemplateChanged(PerspexPropertyChangedEventArgs e) { base.OnTemplateChanged(e); - ItemContainerGenerator.Clear(); + + if (e.NewValue == null) + { + ItemContainerGenerator.Clear(); + } } /// diff --git a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs index 2db62dd619..1e8ffc7a48 100644 --- a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs @@ -67,6 +67,9 @@ namespace Perspex.Controls.UnitTests var container = target.ItemContainerGenerator.TreeContainerFromItem( tree[0].Children[1].Children[0]); + + Assert.NotNull(container); + var header = ((TreeViewItem)container).Header; var headerContent = ((TextBlock)header).Text; @@ -91,6 +94,8 @@ namespace Perspex.Controls.UnitTests var item = tree[0].Children[1].Children[0]; var container = (TreeViewItem)target.ItemContainerGenerator.TreeContainerFromItem(item); + Assert.NotNull(container); + container.RaiseEvent(new PointerPressEventArgs { RoutedEvent = InputElement.PointerPressedEvent, From 260f2a4cc4b7221cee0060dab167179b639151dd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 30 Nov 2015 22:25:04 +0100 Subject: [PATCH 03/36] Added some more leak tests. All passing. --- tests/Perspex.LeakTests/ControlTests.cs | 107 ++++++++++++++++++++++++ tests/Perspex.LeakTests/TestApp.cs | 4 + 2 files changed, 111 insertions(+) diff --git a/tests/Perspex.LeakTests/ControlTests.cs b/tests/Perspex.LeakTests/ControlTests.cs index a44532dc5c..abc4280357 100644 --- a/tests/Perspex.LeakTests/ControlTests.cs +++ b/tests/Perspex.LeakTests/ControlTests.cs @@ -7,6 +7,7 @@ using System.Linq; using JetBrains.dotMemoryUnit; using Perspex.Controls; using Perspex.Controls.Templates; +using Perspex.VisualTree; using Xunit; using Xunit.Abstractions; @@ -81,6 +82,112 @@ namespace Perspex.LeakTests Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } + [Fact] + public void ScrollViewer_With_Content_Is_Freed() + { + Func run = () => + { + var window = new Window + { + Content = new ScrollViewer + { + Content = new Canvas() + } + }; + + // Do a layout and make sure that ScrollViewer gets added to visual tree and its + // template applied. + window.LayoutManager.ExecuteLayoutPass(); + Assert.IsType(window.Presenter.Child); + Assert.IsType(((ScrollViewer)window.Presenter.Child).Presenter.Child); + + // Clear the content and ensure the ScrollViewer is removed. + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + + [Fact] + public void TextBox_Is_Freed() + { + Func run = () => + { + var window = new Window + { + Content = new TextBox() + }; + + // Do a layout and make sure that TextBox gets added to visual tree and its + // template applied. + window.LayoutManager.ExecuteLayoutPass(); + Assert.IsType(window.Presenter.Child); + Assert.NotEqual(0, window.Presenter.Child.GetVisualChildren().Count()); + + // Clear the content and ensure the TextBox is removed. + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + + [Fact] + public void TextBox_With_Xaml_Binding_Is_Freed() + { + Func run = () => + { + var window = new Window + { + DataContext = new Node { Name = "foo" }, + Content = new TextBox() + }; + + var binding = new Perspex.Markup.Xaml.Data.Binding + { + Path = "Name" + }; + + binding.Bind((TextBox)window.Content, TextBox.TextProperty); + + // Do a layout and make sure that TextBox gets added to visual tree and its + // Text property set. + window.LayoutManager.ExecuteLayoutPass(); + Assert.IsType(window.Presenter.Child); + Assert.Equal("foo", ((TextBox)window.Presenter.Child).Text); + + // Clear the content and DataContext and ensure the TextBox is removed. + window.Content = null; + window.DataContext = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + [Fact] public void TreeView_Is_Freed() { diff --git a/tests/Perspex.LeakTests/TestApp.cs b/tests/Perspex.LeakTests/TestApp.cs index d0df7c18fc..8c08c57fc3 100644 --- a/tests/Perspex.LeakTests/TestApp.cs +++ b/tests/Perspex.LeakTests/TestApp.cs @@ -20,11 +20,15 @@ namespace Perspex.LeakTests var fixture = new Fixture().Customize(new AutoMoqCustomization()); var windowImpl = new Mock(); var renderInterface = fixture.Create(); + var threadingInterface = Mock.Of(x => + x.CurrentThreadIsLoopThread == true); PerspexLocator.CurrentMutable .Bind().ToConstant(new AssetLoader()) .Bind().ToConstant(new PclPlatformWrapper()) .Bind().ToConstant(renderInterface) + .Bind().ToConstant(threadingInterface) + .Bind().ToConstant(new Mock().Object) .Bind().ToConstant(new WindowingPlatformMock(() => windowImpl.Object)); Styles = new DefaultTheme(); From f20e26ea38282ab9713d6bb47be7cbb9c09819ab Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 30 Nov 2015 22:47:23 +0100 Subject: [PATCH 04/36] TextBox ScrollViewer doesn't get released When TextBox.Template is cleared. This is showing up as a leak when a TextBox is contained in a TabControl and tabs get changed. --- tests/Perspex.LeakTests/ControlTests.cs | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/Perspex.LeakTests/ControlTests.cs b/tests/Perspex.LeakTests/ControlTests.cs index abc4280357..c770fb1c16 100644 --- a/tests/Perspex.LeakTests/ControlTests.cs +++ b/tests/Perspex.LeakTests/ControlTests.cs @@ -188,6 +188,36 @@ namespace Perspex.LeakTests Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } + [Fact] + public void TextBox_ScrollViewer_Is_Freed_When_Template_Cleared() + { + Func run = () => + { + var window = new Window + { + Content = new TextBox() + }; + + // Do a layout and make sure that TextBox gets added to visual tree and its + // template applied. + window.LayoutManager.ExecuteLayoutPass(); + Assert.IsType(window.Presenter.Child); + Assert.NotEqual(0, window.Presenter.Child.GetVisualChildren().Count()); + + // Clear the template and ensure the TextBox template gets removed + ((TextBox)window.Content).Template = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Equal(0, window.Presenter.Child.GetVisualChildren().Count()); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + [Fact] public void TreeView_Is_Freed() { From aa6b9045bcb5a2a0259f14caae8c3133a153b599 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 3 Dec 2015 21:34:07 +0100 Subject: [PATCH 05/36] Added new leak test. --- tests/Perspex.LeakTests/ControlTests.cs | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/Perspex.LeakTests/ControlTests.cs b/tests/Perspex.LeakTests/ControlTests.cs index c770fb1c16..3a68e55f5c 100644 --- a/tests/Perspex.LeakTests/ControlTests.cs +++ b/tests/Perspex.LeakTests/ControlTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using JetBrains.dotMemoryUnit; using Perspex.Controls; +using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.VisualTree; using Xunit; @@ -82,6 +83,36 @@ namespace Perspex.LeakTests Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } + [Fact] + public void Templated_Child_Is_Freed_When_Template_Cleared() + { + Func run = () => + { + var window = new Window + { + Content = new TestTemplatedControl() + }; + + // Do a layout and make sure that the control gets added to visual tree and its + // template applied. + window.LayoutManager.ExecuteLayoutPass(); + Assert.IsType(window.Presenter.Child); + Assert.IsType(window.Presenter.Child.GetVisualChildren().SingleOrDefault()); + + // Clear the template and ensure the control template gets removed + ((TestTemplatedControl)window.Content).Template = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Equal(0, window.Presenter.Child.GetVisualChildren().Count()); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + [Fact] public void ScrollViewer_With_Content_Is_Freed() { @@ -266,6 +297,14 @@ namespace Perspex.LeakTests Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } + private class TestTemplatedControl : TemplatedControl + { + public TestTemplatedControl() + { + Template = new FuncControlTemplate(_ => new Canvas()); + } + } + private class Node { public string Name { get; set; } From 21bd841258d680bed335d0236611226be9c41949 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 3 Dec 2015 21:57:42 +0100 Subject: [PATCH 06/36] Made templated child leak test fail. --- tests/Perspex.LeakTests/ControlTests.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/Perspex.LeakTests/ControlTests.cs b/tests/Perspex.LeakTests/ControlTests.cs index 3a68e55f5c..76351ac635 100644 --- a/tests/Perspex.LeakTests/ControlTests.cs +++ b/tests/Perspex.LeakTests/ControlTests.cs @@ -299,9 +299,16 @@ namespace Perspex.LeakTests private class TestTemplatedControl : TemplatedControl { + public static readonly PerspexProperty IsCanvasVisibleProperty = + PerspexProperty.Register("IsCanvasVisible"); + public TestTemplatedControl() { - Template = new FuncControlTemplate(_ => new Canvas()); + Template = new FuncControlTemplate(parent => + new Canvas + { + [~IsVisibleProperty] = parent[~IsCanvasVisibleProperty] + }); } } From e402eb8cdd3f7748e52005f395f0ecb8a3c73bdc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 3 Dec 2015 22:36:27 +0100 Subject: [PATCH 07/36] Make template bindings complete on Template change. This fixes a leak - needs to be done in XAML as well though. --- src/Perspex.Base/BindingDescriptor.cs | 14 +++++++++++++- src/Perspex.Base/PerspexObject.cs | 19 ++++++++++++------- .../Primitives/TemplatedControl.cs | 17 +++++++++++++++++ 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/Perspex.Base/BindingDescriptor.cs b/src/Perspex.Base/BindingDescriptor.cs index 4ee4966a98..c0c8f3406f 100644 --- a/src/Perspex.Base/BindingDescriptor.cs +++ b/src/Perspex.Base/BindingDescriptor.cs @@ -78,6 +78,18 @@ namespace Perspex set; } + /// + /// Gets or sets the source observable. + /// + /// + /// If null, then . will be used. + /// + public IObservable SourceObservable + { + get; + set; + } + /// /// Gets a description of the binding. /// @@ -128,7 +140,7 @@ namespace Perspex /// protected override IDisposable SubscribeCore(IObserver observer) { - return Source.GetObservable(Property).Subscribe(observer); + return (SourceObservable ?? Source.GetObservable(Property)).Subscribe(observer); } } } diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 70dff41dda..25e0ff48be 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -163,13 +163,7 @@ namespace Perspex { get { - return new BindingDescriptor - { - Mode = binding.Mode, - Priority = binding.Priority, - Property = binding.Property, - Source = this, - }; + return CreateBindingDescriptor(binding); } set @@ -203,6 +197,17 @@ namespace Perspex } } + protected virtual BindingDescriptor CreateBindingDescriptor(BindingDescriptor source) + { + return new BindingDescriptor + { + Mode = source.Mode, + Priority = source.Priority, + Property = source.Property, + Source = this, + }; + } + public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); diff --git a/src/Perspex.Controls/Primitives/TemplatedControl.cs b/src/Perspex.Controls/Primitives/TemplatedControl.cs index 92e1c4af88..2955c733b8 100644 --- a/src/Perspex.Controls/Primitives/TemplatedControl.cs +++ b/src/Perspex.Controls/Primitives/TemplatedControl.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Reactive.Linq; using Perspex.Controls.Presenters; using Perspex.Controls.Templates; using Perspex.Media; @@ -211,6 +212,22 @@ namespace Perspex.Controls.Primitives } } + protected sealed override BindingDescriptor CreateBindingDescriptor(BindingDescriptor source) + { + var result = base.CreateBindingDescriptor(source); + + // If the binding is a template binding, then complete when the Template changes. + if (source.Priority == BindingPriority.TemplatedParent) + { + var templateChanged = GetObservable(TemplateProperty).Skip(1); + + result.SourceObservable = result.Source.GetObservable(result.Property) + .TakeUntil(templateChanged); + } + + return result; + } + /// /// Called when the control's template is applied. /// From 318be5196d31922611b5ec9ca02a345efd64f8df Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 4 Dec 2015 00:24:56 +0100 Subject: [PATCH 08/36] Added some ExpressionObserver lifetime tests. --- .../Perspex.Markup/Data/ExpressionObserver.cs | 5 ++ .../Data/ExpressionObserverTests_Lifetime.cs | 70 +++++++++++++++++++ .../Data/ExpressionObserverTests_Property.cs | 2 +- .../Perspex.Markup.UnitTests.csproj | 1 + 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs diff --git a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs index e8df013d50..791a788df5 100644 --- a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs @@ -156,6 +156,11 @@ namespace Perspex.Markup.Data /// string IDescription.Description => Expression; + /// + /// Gets the root expression node. Used for testing. + /// + internal ExpressionNode Node => _node; + /// /// Gets the leaf node. /// diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs new file mode 100644 index 0000000000..22373193c3 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs @@ -0,0 +1,70 @@ +// 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.Reactive; +using System.Reactive.Subjects; +using Microsoft.Reactive.Testing; +using Perspex.Markup.Data; +using Xunit; + +namespace Perspex.Markup.UnitTests.Data +{ + public class ExpressionObserverTests_Lifetime + { + [Fact(Skip = "Not working yet")] + public void Should_Complete_When_Source_Observable_Completes() + { + var source = new BehaviorSubject(1); + var target = new ExpressionObserver(source, "Foo"); + var completed = false; + + target.Subscribe(_ => { }, () => completed = true); + source.OnCompleted(); + + Assert.True(completed); + } + + [Fact] + public void Should_Unsubscribe_From_Source_Observable() + { + var scheduler = new TestScheduler(); + var source = scheduler.CreateColdObservable( + OnNext(1, new { Foo = "foo" })); + var target = new ExpressionObserver(source, "Foo"); + var result = new List(); + + using (target.Subscribe(x => result.Add(x))) + using (target.Subscribe(_ => { })) + { + scheduler.Start(); + } + + Assert.Equal(new[] { PerspexProperty.UnsetValue, "foo" }, result); + Assert.Equal(1, source.Subscriptions.Count); + Assert.NotEqual(Subscription.Infinite, source.Subscriptions[0].Unsubscribe); + } + + [Fact] + public void Should_Set_Node_Target_To_Null_On_Unsubscribe() + { + var target = new ExpressionObserver(new { Foo = "foo" }, "Foo"); + var result = new List(); + + using (target.Subscribe(x => result.Add(x))) + using (target.Subscribe(_ => { })) + { + Assert.NotNull(target.Node.Target); + } + + Assert.Equal(new[] { "foo" }, result); + Assert.Null(target.Node.Target); + } + + private Recorded> OnNext(long time, object value) + { + return new Recorded>(time, Notification.CreateOnNext(value)); + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index fd1583106c..0d917b15b4 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -355,7 +355,7 @@ namespace Perspex.Markup.UnitTests.Data { } - public Recorded> OnNext(long time, object value) + private Recorded> OnNext(long time, object value) { return new Recorded>(time, Notification.CreateOnNext(value)); } diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index ad9843a103..639142b00e 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -84,6 +84,7 @@ + From 5f0a6c9d72bec413f1d0b3964632deadf77e604a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 4 Dec 2015 00:54:06 +0100 Subject: [PATCH 09/36] Use observable to signal update... ...in ExpressionObserver, instead of calling UpdateRoot. This should avoid a leak. --- .../Perspex.Markup.Xaml/Data/Binding.cs | 32 +++++----- .../Perspex.Markup/Data/ExpressionObserver.cs | 62 +++++++++++-------- .../Data/ExpressionObserverTests_Lifetime.cs | 19 ++++++ .../Data/ExpressionObserverTests_Property.cs | 13 ++-- .../Perspex.Markup.UnitTests.csproj | 2 +- 5 files changed, 77 insertions(+), 51 deletions(-) diff --git a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs index b220fb4e46..20cab81984 100644 --- a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using Perspex.Controls; @@ -216,15 +217,13 @@ namespace Perspex.Markup.Xaml.Data if (!targetIsDataContext) { + var update = target.GetObservable(Control.DataContextProperty) + .Skip(1) + .Select(_ => Unit.Default); var result = new ExpressionObserver( () => target.GetValue(Control.DataContextProperty), - path); - - /// TODO: Instead of doing this, make the ExpressionObserver accept an "update" - /// observable as doing it this way can will cause a leak in Binding as this - /// observable is never unsubscribed. - target.GetObservable(Control.DataContextProperty).Subscribe(x => - result.UpdateRoot()); + path, + update); return result; } @@ -245,19 +244,16 @@ namespace Perspex.Markup.Xaml.Data { Contract.Requires(target != null); + var update = target.GetObservable(Control.TemplatedParentProperty) + .Skip(1) + .Where(x => x != null) + .Take(1) + .Select(_ => Unit.Default); + var result = new ExpressionObserver( () => target.GetValue(Control.TemplatedParentProperty), - path); - - if (target.GetValue(Control.TemplatedParentProperty) == null) - { - // TemplatedParent should only be set once, so only listen for the first non-null - // value. - target.GetObservable(Control.TemplatedParentProperty) - .Where(x => x != null) - .Take(1) - .Subscribe(x => result.UpdateRoot()); - } + path, + update); return result; } diff --git a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs index 791a788df5..d3c2146bf2 100644 --- a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Reactive; using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Reactive.Subjects; using Perspex.Markup.Data.Plugins; @@ -29,10 +30,11 @@ namespace Perspex.Markup.Data private readonly object _root; private readonly Func _rootGetter; private readonly IObservable _rootObservable; + private readonly IObservable _update; private IDisposable _rootObserverSubscription; + private IDisposable _updateSubscription; private int _count; private readonly ExpressionNode _node; - private ISubject _empty; /// /// Initializes a new instance of the class. @@ -78,12 +80,18 @@ namespace Perspex.Markup.Data /// /// A function which gets the root object. /// The expression. - public ExpressionObserver(Func rootGetter, string expression) + /// An observable which triggers a re-read of the getter. + public ExpressionObserver( + Func rootGetter, + string expression, + IObservable update) { Contract.Requires(rootGetter != null); Contract.Requires(expression != null); + Contract.Requires(update != null); _rootGetter = rootGetter; + _update = update; if (!string.IsNullOrWhiteSpace(expression)) { @@ -104,7 +112,11 @@ namespace Perspex.Markup.Data public bool SetValue(object value) { IncrementCount(); - UpdateRoot(); + + if (_rootGetter != null && _node != null) + { + _node.Target = _rootGetter(); + } try { @@ -174,26 +186,6 @@ namespace Perspex.Markup.Data } } - /// - /// Causes the root object to be re-read from the root getter. - /// - /// TODO: Instead of doing this, make the object accept an "update" observable - /// as doing it this way can cause a leak in Binding. - public void UpdateRoot() - { - if (_count > 0 && _rootGetter != null) - { - if (_node != null) - { - _node.Target = _rootGetter(); - } - else - { - _empty?.OnNext(_rootGetter()); - } - } - } - /// protected override IDisposable SubscribeCore(IObserver observer) { @@ -211,12 +203,17 @@ namespace Perspex.Markup.Data } else { - if (_empty == null) + if (_update == null) { - _empty = new BehaviorSubject(_rootGetter()); + return Observable.Never().StartWith(_root).Subscribe(observer); + } + else + { + return _update + .Select(_ => _rootGetter()) + .StartWith(_rootGetter()) + .Subscribe(observer); } - - return _empty.Subscribe(observer); } } @@ -227,6 +224,11 @@ namespace Perspex.Markup.Data if (_rootGetter != null) { _node.Target = _rootGetter(); + + if (_update != null) + { + _updateSubscription = _update.Subscribe(x => _node.Target = _rootGetter()); + } } else if (_rootObservable != null) { @@ -249,6 +251,12 @@ namespace Perspex.Markup.Data _rootObserverSubscription = null; } + if (_updateSubscription != null) + { + _updateSubscription.Dispose(); + _updateSubscription = null; + } + _node.Target = null; } } diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs index 22373193c3..cfbc4bc29f 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs @@ -46,6 +46,25 @@ namespace Perspex.Markup.UnitTests.Data Assert.NotEqual(Subscription.Infinite, source.Subscriptions[0].Unsubscribe); } + [Fact] + public void Should_Unsubscribe_From_Update_Observable() + { + var scheduler = new TestScheduler(); + var update = scheduler.CreateColdObservable(); + var target = new ExpressionObserver(() => new { Foo = "foo" }, "Foo", update); + var result = new List(); + + using (target.Subscribe(x => result.Add(x))) + using (target.Subscribe(_ => { })) + { + scheduler.Start(); + } + + Assert.Equal(new[] { "foo" }, result); + Assert.Equal(1, update.Subscriptions.Count); + Assert.NotEqual(Subscription.Infinite, update.Subscriptions[0].Unsubscribe); + } + [Fact] public void Should_Set_Node_Target_To_Null_On_Unsubscribe() { diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index 0d917b15b4..fe9336ebe6 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Reactive; using System.Reactive.Linq; +using System.Reactive.Subjects; using Microsoft.Reactive.Testing; using Perspex.Markup.Data; using Xunit; @@ -193,13 +194,14 @@ namespace Perspex.Markup.UnitTests.Data public void Empty_Expression_Should_Track_Root() { var data = new Class1 { Foo = "foo" }; - var target = new ExpressionObserver(() => data.Foo, ""); + var update = new Subject(); + var target = new ExpressionObserver(() => data.Foo, "", update); var result = new List(); target.Subscribe(x => result.Add(x)); data.Foo = "bar"; - target.UpdateRoot(); + update.OnNext(Unit.Default); Assert.Equal(new[] { "foo", "bar" }, result); } @@ -286,14 +288,15 @@ namespace Perspex.Markup.UnitTests.Data var first = new Class1 { Foo = "foo" }; var second = new Class1 { Foo = "bar" }; var root = first; - var target = new ExpressionObserver(() => root, "Foo"); + var update = new Subject(); + var target = new ExpressionObserver(() => root, "Foo", update); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); root = second; - target.UpdateRoot(); + update.OnNext(Unit.Default); root = null; - target.UpdateRoot(); + update.OnNext(Unit.Default); Assert.Equal(new[] { "foo", "bar", PerspexProperty.UnsetValue }, result); diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index 639142b00e..161e79f6f6 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -84,7 +84,7 @@ - + From 4d6572b74e14025c5d0bdf0ba1f6d1f437f76ada Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 7 Dec 2015 20:45:36 +0100 Subject: [PATCH 10/36] Added StyleActivator leak test. Trying to locate source of StyleActivator leaks - this test doesn't find it. --- src/Perspex.Styling/Styling/Style.cs | 14 ++-- .../Perspex.LeakTests.csproj | 1 + tests/Perspex.LeakTests/StyleTests.cs | 69 +++++++++++++++++++ 3 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 tests/Perspex.LeakTests/StyleTests.cs diff --git a/src/Perspex.Styling/Styling/Style.cs b/src/Perspex.Styling/Styling/Style.cs index 112bc817a0..5c5f97417d 100644 --- a/src/Perspex.Styling/Styling/Style.cs +++ b/src/Perspex.Styling/Styling/Style.cs @@ -60,13 +60,13 @@ namespace Perspex.Styling var activator = match.ObservableResult ?? Observable.Never().StartWith(true); - if (visual != null) - { - var detached = Observable.FromEventPattern( - x => visual.DetachedFromVisualTree += x, - x => visual.DetachedFromVisualTree -= x); - activator = activator.TakeUntil(detached); - } + //if (visual != null) + //{ + // var detached = Observable.FromEventPattern( + // x => visual.DetachedFromVisualTree += x, + // x => visual.DetachedFromVisualTree -= x); + // activator = activator.TakeUntil(detached); + //} foreach (var setter in Setters) { diff --git a/tests/Perspex.LeakTests/Perspex.LeakTests.csproj b/tests/Perspex.LeakTests/Perspex.LeakTests.csproj index b2d67554c3..ba2450f8f9 100644 --- a/tests/Perspex.LeakTests/Perspex.LeakTests.csproj +++ b/tests/Perspex.LeakTests/Perspex.LeakTests.csproj @@ -88,6 +88,7 @@ + diff --git a/tests/Perspex.LeakTests/StyleTests.cs b/tests/Perspex.LeakTests/StyleTests.cs new file mode 100644 index 0000000000..82f8ebf1d5 --- /dev/null +++ b/tests/Perspex.LeakTests/StyleTests.cs @@ -0,0 +1,69 @@ +// 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 JetBrains.dotMemoryUnit; +using Perspex.Controls; +using Perspex.Controls.Primitives; +using Perspex.Controls.Templates; +using Perspex.Styling; +using Perspex.VisualTree; +using Xunit; +using Xunit.Abstractions; + +namespace Perspex.LeakTests +{ + [DotMemoryUnit(FailIfRunWithoutSupport = false)] + public class StyleTests + { + public StyleTests(ITestOutputHelper atr) + { + TestApp.Initialize(); + DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); + } + + [Fact] + public void StyleActivator_Should_Be_Released() + { + Func run = () => + { + var window = new Window + { + Styles = new Styles + { + new Style(x => x.OfType().Class("foo")) + { + Setters = new[] + { + new Setter(Canvas.WidthProperty, 100), + } + } + }, + Content = new Canvas + { + Classes = new Classes("foo"), + } + }; + + // Do a layout and make sure that styled Canvas gets added to visual tree. + window.LayoutManager.ExecuteLayoutPass(); + Assert.IsType(window.Presenter.Child); + Assert.Equal(100, (window.Presenter.Child).Width); + + // Clear the content and ensure the Canvas is removed. + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + } +} From da39751faf43419d694a32a9bad54942cb04487c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 7 Dec 2015 21:02:59 +0100 Subject: [PATCH 11/36] Undo commented out code. --- src/Perspex.Styling/Styling/Style.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Perspex.Styling/Styling/Style.cs b/src/Perspex.Styling/Styling/Style.cs index 5c5f97417d..112bc817a0 100644 --- a/src/Perspex.Styling/Styling/Style.cs +++ b/src/Perspex.Styling/Styling/Style.cs @@ -60,13 +60,13 @@ namespace Perspex.Styling var activator = match.ObservableResult ?? Observable.Never().StartWith(true); - //if (visual != null) - //{ - // var detached = Observable.FromEventPattern( - // x => visual.DetachedFromVisualTree += x, - // x => visual.DetachedFromVisualTree -= x); - // activator = activator.TakeUntil(detached); - //} + if (visual != null) + { + var detached = Observable.FromEventPattern( + x => visual.DetachedFromVisualTree += x, + x => visual.DetachedFromVisualTree -= x); + activator = activator.TakeUntil(detached); + } foreach (var setter in Setters) { From dc757851c11bcf9b3e3befafc90b7a4c85886fa1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Dec 2015 00:05:28 +0000 Subject: [PATCH 12/36] Added failing Carousel leak test. As found by @donandren. --- tests/Perspex.LeakTests/ControlTests.cs | 12 ++--- tests/Perspex.LeakTests/StyleTests.cs | 59 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/tests/Perspex.LeakTests/ControlTests.cs b/tests/Perspex.LeakTests/ControlTests.cs index 76351ac635..78dc8d764e 100644 --- a/tests/Perspex.LeakTests/ControlTests.cs +++ b/tests/Perspex.LeakTests/ControlTests.cs @@ -269,12 +269,12 @@ namespace Perspex.LeakTests Content = target = new TreeView { DataTemplates = new DataTemplates - { - new FuncTreeDataTemplate( - x => new TextBlock { Text = x.Name }, - x => x.Children, - x => true) - }, + { + new FuncTreeDataTemplate( + x => new TextBlock { Text = x.Name }, + x => x.Children, + x => true) + }, Items = nodes } }; diff --git a/tests/Perspex.LeakTests/StyleTests.cs b/tests/Perspex.LeakTests/StyleTests.cs index 82f8ebf1d5..30a4aef525 100644 --- a/tests/Perspex.LeakTests/StyleTests.cs +++ b/tests/Perspex.LeakTests/StyleTests.cs @@ -65,5 +65,64 @@ namespace Perspex.LeakTests dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } + + [Fact] + public void Changing_Carousel_SelectedIndex_Should_Not_Leak_StyleActivators() + { + Func run = () => + { + Carousel target; + + var window = new Window + { + Styles = new Styles + { + new Style(x => x.OfType().Class("foo")) + { + Setters = new[] + { + new Setter(Visual.OpacityProperty, 0.5), + } + } + }, + Content = target = new Carousel + { + Items = new[] + { + new ContentControl + { + Name = "item1", + Classes = new Classes("foo"), + Content = "item1", + }, + new ContentControl + { + Name = "item2", + Classes = new Classes("foo"), + Content = "item2", + }, + } + } + }; + + // Do a layout and make sure that Carousel gets added to visual tree. + window.LayoutManager.ExecuteLayoutPass(); + Assert.IsType(window.Presenter.Child); + + target.SelectedIndex = 1; + window.LayoutManager.ExecuteLayoutPass(); + target.SelectedIndex = 0; + window.LayoutManager.ExecuteLayoutPass(); + target.SelectedIndex = 1; + window.LayoutManager.ExecuteLayoutPass(); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(1, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } } } From 5d2658d85619d30ea6aa2c73e570ea5d04ed5395 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Dec 2015 14:49:08 +0000 Subject: [PATCH 13/36] Make IStyleable.Classes an IPerspexReadOnlyList. So we can mock it. --- .../e0ab357f917440988efaba9c0cd4a9c0 | Bin 0 -> 16766 bytes .../Converters/ClassesTypeConverter.cs | 1 + src/Perspex.Controls/Classes.cs | 25 +++ src/Perspex.Controls/Control.cs | 5 +- src/Perspex.Controls/IControl.cs | 5 + src/Perspex.Controls/Perspex.Controls.csproj | 1 + .../Primitives/SelectingItemsControl.cs | 7 +- src/Perspex.Controls/TreeView.cs | 7 +- .../ViewModels/TreeNode.cs | 9 +- src/Perspex.Styling/Perspex.Styling.csproj | 1 - src/Perspex.Styling/Styling/Classes.cs | 155 ------------------ src/Perspex.Styling/Styling/IStyleable.cs | 3 +- src/Perspex.Styling/Styling/Selectors.cs | 10 +- tests/Perspex.LeakTests/StyleTests.cs | 1 + .../PerspexPropertyConverterTest.cs | 4 +- .../SelectorTests_Child.cs | 7 +- .../SelectorTests_Class.cs | 1 + .../SelectorTests_Descendent.cs | 3 + .../SelectorTests_Multiple.cs | 2 +- .../TestControlBase.cs | 4 + .../TestTemplatedControl.cs | 4 + 21 files changed, 84 insertions(+), 171 deletions(-) create mode 100644 _NCrunch_Perspex/StoredText/e0ab357f917440988efaba9c0cd4a9c0 create mode 100644 src/Perspex.Controls/Classes.cs delete mode 100644 src/Perspex.Styling/Styling/Classes.cs diff --git a/_NCrunch_Perspex/StoredText/e0ab357f917440988efaba9c0cd4a9c0 b/_NCrunch_Perspex/StoredText/e0ab357f917440988efaba9c0cd4a9c0 new file mode 100644 index 0000000000000000000000000000000000000000..ce9d389343b7751146df5daab975a29ff5983857 GIT binary patch literal 16766 zcmeHO+in{-5GC5T0{I6$6%J4c@g-Ln0URem{g5Vz-8Mi}sIsLfYGlQgHmUvhJ%{5F zxg__NDCrhi2)0&S-sa5ka5yAym>EZhg{@*dTu0K7zyEGTb zwZzldmSMSLnWaf$a8#zX1Zu4B3L$q>^zKv|(mgY~hjS_wIB*UAf*)ost{T}%z$g@B@ zE7Undi4${${}a^k7XK;5Pk4J&*+LJc7x=J0Ve2=oOhxkea`e4+UZXF6pr>qm0qLj< zqIqh2O^H+k>}Q6z<8@z)@>3VtHBvWGo_+2i?SUDfHDYm&-V^65q?{t>46TiU^%QTZ zwSoD^BKQtZcg>!;XKuqLcJOo?_HheoBXb|$d>#PRC2AkQPMrQmp56vIEv}H0*v+kk z?CA`@7C|0rafKdJ*TlOAB<`bq+MbW`3%pw*C$)KL?S@iGsVRQ{iqe#nacZCk7g{7r#4RuF0I4cf_>Lo}3W)*o)I%cF#T{rP)Wk7x zC&Ck0SKqv}wxFZ_8B#MK^d2(*40$O#M`a&4EzC!xzr!y-*B0a>q|WQVd&pj*o0 ztho#U%KQd3i*>Hhw>K#3Bf)1PxwJi>!!~?*$F;Qy(4YRASD`VtOPJ2`+{)R1UUN>I)oP;tM~G8dt92z}2{tJug9+NXxd zntpnWPioFO0qV3N)}npZ+3674Ci7{Ya+{ZW97nK5*31#a`S$bsvu_@vrXfC$fgO32 ze8jm`d1(*KKeRLJzS+;ol#+q-+?DO!LuiBbZli@UZ2cMV{D?mPg1<}1K)!3jA>?D3 z@5#^9H`!~6I%gIOGP`U21X6Ie_wjuMT?)6eoG>KEKxQ}#A6r~GV#xJmdfL*&&d$Ye ze}s*QM&gpOd`+#TnRa}8tE-!o7E--@0-cfH=q1X0eS&Xt43XRi@>6`suT!8$ESxT= ze_Fy`X9Yl}XPV<%hVB-4l782j^?x}J>IghQnrf^4wsLmL!}s;woWF(0S{-cYIpuxW^xEI`%ta|(n)W5% z+w{80^|8$!a+|EY3g^_k7xJLV1J6VCqJ*ssSf z8V_{VxJl;q*+#7`WM0QHyPao@RXC#*>9u(A7!EQ0^x2GYK<;b#no}Fm_MNTNZAVL{ zYOk^j?ys~jO4p{@ryAYO+lShOZwp5@s>wYgdMpz}E@xO{aVM6MbEYk@_U4+4kx_a@ zab8Bp!+IDWWvrc1Jy$|f)6w+we#rr%FY!)HJ)TT^VQif?Eh|XI&pCQE4k^1w3*f+A zZ0<#Jm*=ce{(4ag$Ny>zzRy`UmUTqt^%&Qu`MBeyD`{bhdOfV_SDpnlx3;=UL~VDm z@7wW0o%y2376fy#jxxlXvV3`a#VhJvyPMOkrmH=Ga)dX@_PX0v#vy)h@;ste<>zn= z;-?DlzXZ30JJx~tkI0|T*T4GFwJ+qtZB}> z*Kr=^(CQIVhI*dyiZE}=dSxw^ao*idqZ`tw&pq|VXv)utPk{O3R{dUOHm7yJ^`rGU z3isaeo=YocJw1`-81HO-@}W83)kDL{RE|NKyPQo1zTd%Vz0O~&$6a;s&$Cz& z`psFaPTR$Nwu3X6ot8JPWjC7Y>N93G*1^cmInF=LaTcna)nY5~`_OcS?(<<>v(Jq2 z+vaS?5?nd{*Q;{n(=z0XxHQHg#i@JU8W_5+Guoe{$|O@ZAi%7k-SOou{>9KB@`Y-Rl)brdsYs(#?xTSoKX6xYc9?)uKM#-c2 zEupu7pS`?3<^A@0-8aT>xkL48eanqIHH>7?a(Eiw?UMCz>Rj)_?X06FG4ecnQgtKb zHd@h{L9J+oQhEY2#b2QWTF&Puv9#h7--jpnEWebrL zcRJp!jN&%^e6d}Ja1V z@l(sozd(7Nd03h^GJB{oPOV0;xR=f{1PM6{c!HZcymvAJ?tSxp@)kj;rO#{QyJQ>r zJAr<$xI53gsD9ed= + { + public Classes() + { + } + + public Classes(IEnumerable items) + : base(items) + { + } + + public Classes(params string[] items) + : base(items) + { + } + } +} diff --git a/src/Perspex.Controls/Control.cs b/src/Perspex.Controls/Control.cs index ad959d7f8e..e76be17149 100644 --- a/src/Perspex.Controls/Control.cs +++ b/src/Perspex.Controls/Control.cs @@ -129,7 +129,7 @@ namespace Perspex.Controls if (_classes != value) { _classes.Clear(); - _classes.Add(value); + _classes.AddRange(value); } } } @@ -216,6 +216,9 @@ namespace Perspex.Controls /// IPerspexReadOnlyList ILogical.LogicalChildren => LogicalChildren; + /// + IPerspexReadOnlyList IStyleable.Classes => Classes; + /// /// Gets the type by which the control is styled. /// diff --git a/src/Perspex.Controls/IControl.cs b/src/Perspex.Controls/IControl.cs index 21cb044d0a..0e65c599be 100644 --- a/src/Perspex.Controls/IControl.cs +++ b/src/Perspex.Controls/IControl.cs @@ -13,6 +13,11 @@ namespace Perspex.Controls /// public interface IControl : IVisual, ILogical, ILayoutable, IInputElement, INamed, IStyleable, IStyleHost { + /// + /// Gets or sets the control's styling classes. + /// + new Classes Classes { get; set; } + /// /// Gets or sets the control's data context. /// diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index f9a4a3ad84..31b23702a4 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -42,6 +42,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index b60af28fda..142da517a8 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -593,7 +593,6 @@ namespace Perspex.Controls.Primitives try { var selectable = container as ISelectable; - var styleable = container as IStyleable; _ignoreContainerSelectionChanged = true; @@ -601,15 +600,15 @@ namespace Perspex.Controls.Primitives { selectable.IsSelected = selected; } - else if (styleable != null) + else { if (selected) { - styleable.Classes.Add(":selected"); + container.Classes.Add(":selected"); } else { - styleable.Classes.Remove(":selected"); + container.Classes.Remove(":selected"); } } } diff --git a/src/Perspex.Controls/TreeView.cs b/src/Perspex.Controls/TreeView.cs index a1d14d5441..f479f01238 100644 --- a/src/Perspex.Controls/TreeView.cs +++ b/src/Perspex.Controls/TreeView.cs @@ -179,21 +179,20 @@ namespace Perspex.Controls private void MarkContainerSelected(IControl container, bool selected) { var selectable = container as ISelectable; - var styleable = container as IStyleable; if (selectable != null) { selectable.IsSelected = selected; } - else if (styleable != null) + else { if (selected) { - styleable.Classes.Add(":selected"); + container.Classes.Add(":selected"); } else { - styleable.Classes.Remove(":selected"); + container.Classes.Remove(":selected"); } } } diff --git a/src/Perspex.Diagnostics/ViewModels/TreeNode.cs b/src/Perspex.Diagnostics/ViewModels/TreeNode.cs index 837930aadb..7caf3ac075 100644 --- a/src/Perspex.Diagnostics/ViewModels/TreeNode.cs +++ b/src/Perspex.Diagnostics/ViewModels/TreeNode.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Perspex.Controls; @@ -18,7 +19,13 @@ namespace Perspex.Diagnostics.ViewModels Control = control; Type = control.GetType().Name; - control.Classes.Changed.Select(_ => Unit.Default) + var classesChanged = Observable.FromEventPattern< + NotifyCollectionChangedEventHandler, + NotifyCollectionChangedEventHandler>( + x => control.Classes.CollectionChanged += x, + x => control.Classes.CollectionChanged -= x); + + classesChanged.Select(_ => Unit.Default) .StartWith(Unit.Default) .Subscribe(_ => { diff --git a/src/Perspex.Styling/Perspex.Styling.csproj b/src/Perspex.Styling/Perspex.Styling.csproj index c741543430..cb5367bc9a 100644 --- a/src/Perspex.Styling/Perspex.Styling.csproj +++ b/src/Perspex.Styling/Perspex.Styling.csproj @@ -45,7 +45,6 @@ - diff --git a/src/Perspex.Styling/Styling/Classes.cs b/src/Perspex.Styling/Styling/Classes.cs deleted file mode 100644 index 1517b06018..0000000000 --- a/src/Perspex.Styling/Styling/Classes.cs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Reactive; -using System.Reactive.Subjects; - -namespace Perspex.Styling -{ - public class Classes : ICollection, INotifyCollectionChanged - { - private readonly List _inner; - - private readonly Subject _beforeChanged - = new Subject(); - - private readonly Subject _changed - = new Subject(); - - private readonly Subject _afterChanged - = new Subject(); - - public Classes() - { - _inner = new List(); - } - - public Classes(params string[] classes) - { - _inner = new List(classes); - } - - public Classes(IEnumerable classes) - { - _inner = new List(classes); - } - - public event NotifyCollectionChangedEventHandler CollectionChanged; - - public int Count => _inner.Count; - - public bool IsReadOnly => false; - - public IObservable BeforeChanged => _beforeChanged; - - public IObservable Changed => _changed; - - public IObservable AfterChanged => _afterChanged; - - public void Add(string item) - { - Add(Enumerable.Repeat(item, 1)); - } - - public void Add(params string[] items) - { - Add((IEnumerable)items); - } - - public void Add(IEnumerable items) - { - items = items.Except(_inner); - - NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - items); - - _beforeChanged.OnNext(e); - _inner.AddRange(items); - RaiseChanged(e); - } - - public void Clear() - { - NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Reset); - - _beforeChanged.OnNext(e); - _inner.Clear(); - RaiseChanged(e); - } - - public bool Contains(string item) - { - return _inner.Contains(item); - } - - public void CopyTo(string[] array, int arrayIndex) - { - _inner.CopyTo(array, arrayIndex); - } - - public IEnumerator GetEnumerator() - { - return _inner.GetEnumerator(); - } - - public override string ToString() - { - return string.Join(" ", this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _inner.GetEnumerator(); - } - - public bool Remove(string item) - { - return Remove(Enumerable.Repeat(item, 1)); - } - - public bool Remove(params string[] items) - { - return Remove((IEnumerable)items); - } - - public bool Remove(IEnumerable items) - { - items = items.Intersect(_inner); - - if (items.Any()) - { - NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Remove, - items); - - _beforeChanged.OnNext(e); - - foreach (string item in items) - { - _inner.Remove(item); - } - - RaiseChanged(e); - return true; - } - else - { - return false; - } - } - - private void RaiseChanged(NotifyCollectionChangedEventArgs e) - { - CollectionChanged?.Invoke(this, e); - _changed.OnNext(e); - _afterChanged.OnNext(e); - } - } -} diff --git a/src/Perspex.Styling/Styling/IStyleable.cs b/src/Perspex.Styling/Styling/IStyleable.cs index c28e96f184..c6ebacaea7 100644 --- a/src/Perspex.Styling/Styling/IStyleable.cs +++ b/src/Perspex.Styling/Styling/IStyleable.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Perspex.Collections; namespace Perspex.Styling { @@ -13,7 +14,7 @@ namespace Perspex.Styling /// /// Gets the list of classes for the control. /// - Classes Classes { get; } + IPerspexReadOnlyList Classes { get; } /// /// Gets the type by which the control is styled. diff --git a/src/Perspex.Styling/Styling/Selectors.cs b/src/Perspex.Styling/Styling/Selectors.cs index 65f0d6dee0..312f7e76e1 100644 --- a/src/Perspex.Styling/Styling/Selectors.cs +++ b/src/Perspex.Styling/Styling/Selectors.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; using System.Reactive.Linq; using System.Reflection; @@ -176,10 +178,16 @@ namespace Perspex.Styling private static SelectorMatch MatchClass(IStyleable control, string name) { + var changed = Observable.FromEventPattern< + NotifyCollectionChangedEventHandler, + NotifyCollectionChangedEventHandler>( + x => control.Classes.CollectionChanged += x, + x => control.Classes.CollectionChanged -= x); + return new SelectorMatch( Observable .Return(control.Classes.Contains(name)) - .Concat(control.Classes.Changed.Select(e => control.Classes.Contains(name)))); + .Concat(changed.Select(e => control.Classes.Contains(name)))); } private static SelectorMatch MatchDescendent(IStyleable control, Selector previous) diff --git a/tests/Perspex.LeakTests/StyleTests.cs b/tests/Perspex.LeakTests/StyleTests.cs index 30a4aef525..bbaade7250 100644 --- a/tests/Perspex.LeakTests/StyleTests.cs +++ b/tests/Perspex.LeakTests/StyleTests.cs @@ -123,6 +123,7 @@ namespace Perspex.LeakTests dotMemory.Check(memory => Assert.Equal(1, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(true); } } } diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Converters/PerspexPropertyConverterTest.cs b/tests/Perspex.Markup.Xaml.UnitTests/Converters/PerspexPropertyConverterTest.cs index 9b6230df17..f1565fb457 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Converters/PerspexPropertyConverterTest.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Converters/PerspexPropertyConverterTest.cs @@ -7,6 +7,8 @@ using OmniXaml; using OmniXaml.ObjectAssembler.Commands; using OmniXaml.TypeConversion; using OmniXaml.Typing; +using Perspex.Collections; +using Perspex.Controls; using Perspex.Markup.Xaml.Converters; using Perspex.Styling; using Xunit; @@ -74,7 +76,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Converters public static readonly PerspexProperty FooProperty = PerspexProperty.Register("Foo"); - public Classes Classes + public IPerspexReadOnlyList Classes { get { throw new NotImplementedException(); } } diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs index 0df2e061a3..89419b0b17 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs @@ -2,11 +2,13 @@ // 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.Reactive.Linq; using System.Reactive.Subjects; using System.Threading.Tasks; using Perspex.Collections; +using Perspex.Controls; using Perspex.Styling; using Xunit; @@ -43,7 +45,7 @@ namespace Perspex.Styling.UnitTests } [Fact] - public async Task Child_Matches_Control_When_It_Is_Child_OfType_And_Class() + public async void Child_Matches_Control_When_It_Is_Child_OfType_And_Class() { var parent = new TestLogical1(); var child = new TestLogical2(); @@ -52,6 +54,7 @@ namespace Perspex.Styling.UnitTests var selector = new Selector().OfType().Class("foo").Child().OfType(); var activator = selector.Match(child).ObservableResult; + var result = new List(); Assert.False(await activator.Take(1)); parent.Classes.Add("foo"); @@ -96,6 +99,8 @@ namespace Perspex.Styling.UnitTests } } + IPerspexReadOnlyList IStyleable.Classes => Classes; + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Class.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Class.cs index 9d98bd9a12..3232279e5e 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Class.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Class.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using Moq; +using Perspex.Controls; using Perspex.Styling; using Xunit; diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs index 7b5f78e6bb..80c3e4f370 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs @@ -7,6 +7,7 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading.Tasks; using Perspex.Collections; +using Perspex.Controls; using Perspex.Styling; using Xunit; @@ -128,6 +129,8 @@ namespace Perspex.Styling.UnitTests } } + IPerspexReadOnlyList IStyleable.Classes => Classes; + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Multiple.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Multiple.cs index 0f14820f59..89cd535338 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Multiple.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Multiple.cs @@ -44,7 +44,7 @@ namespace Perspex.Styling.UnitTests activator.Subscribe(x => values.Add(x)); Assert.Equal(new[] { false }, values); - control.Classes.Add("foo", "bar"); + control.Classes.AddRange(new[] { "foo", "bar" }); Assert.Equal(new[] { false, true }, values); control.Classes.Remove("foo"); Assert.Equal(new[] { false, true, false }, values); diff --git a/tests/Perspex.Styling.UnitTests/TestControlBase.cs b/tests/Perspex.Styling.UnitTests/TestControlBase.cs index ad47f1eb27..64a0b67b2c 100644 --- a/tests/Perspex.Styling.UnitTests/TestControlBase.cs +++ b/tests/Perspex.Styling.UnitTests/TestControlBase.cs @@ -3,6 +3,8 @@ using System; using System.Reactive.Subjects; +using Perspex.Collections; +using Perspex.Controls; namespace Perspex.Styling.UnitTests { @@ -36,6 +38,8 @@ namespace Perspex.Styling.UnitTests } } + IPerspexReadOnlyList IStyleable.Classes => Classes; + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); diff --git a/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs b/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs index 764c402138..f8d576d4de 100644 --- a/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs +++ b/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Reactive.Subjects; +using Perspex.Collections; +using Perspex.Controls; namespace Perspex.Styling.UnitTests { @@ -42,6 +44,8 @@ namespace Perspex.Styling.UnitTests } } + IPerspexReadOnlyList IStyleable.Classes => Classes; + public IObservable GetObservable(PerspexProperty property) { throw new NotImplementedException(); From 23cc9a2ff44b0ef92d2b68c8de7b9dea3dcbf3d2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Dec 2015 19:01:55 +0000 Subject: [PATCH 14/36] Allow binding to element without path. Closes #365. --- .../Perspex.Markup/Data/ExpressionObserver.cs | 4 + .../Data/BindingTests_ElementName.cs | 77 ++++++++++++++++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs index e8df013d50..3f529c11cb 100644 --- a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs @@ -204,6 +204,10 @@ namespace Perspex.Markup.Data subscription.Dispose(); }); } + else if (_rootObservable != null) + { + return _rootObservable.Subscribe(observer); + } else { if (_empty == null) diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs index bde7297d47..c47d53d0a3 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs @@ -10,7 +10,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data public class BindingTests_ElementName { [Fact] - public void Should_Bind_To_Element() + public void Should_Bind_To_Element_Path() { TextBlock target; var root = new TestRoot @@ -44,7 +44,42 @@ namespace Perspex.Markup.Xaml.UnitTests.Data } [Fact] - public void Should_Bind_To_Later_Added_Element() + public void Should_Bind_To_Element() + { + TextBlock source; + ContentControl target; + + var root = new TestRoot + { + Child = new StackPanel + { + Children = new Controls.Controls + { + (source = new TextBlock + { + Name = "source", + Text = "foo", + }), + (target = new ContentControl + { + Name = "target", + }) + } + } + }; + + var binding = new Binding + { + ElementName = "source", + }; + + binding.Bind(target, ContentControl.ContentProperty); + + Assert.Same(source, target.Content); + } + + [Fact] + public void Should_Bind_To_Later_Added_Element_Path() { TextBlock target; StackPanel stackPanel; @@ -79,5 +114,43 @@ namespace Perspex.Markup.Xaml.UnitTests.Data Assert.Equal("foo", target.Text); } + + [Fact] + public void Should_Bind_To_Later_Added_Element() + { + ContentControl target; + StackPanel stackPanel; + + var root = new TestRoot + { + Child = stackPanel = new StackPanel + { + Children = new Controls.Controls + { + (target = new ContentControl + { + Name = "target", + }), + } + } + }; + + var binding = new Binding + { + ElementName = "source", + }; + + binding.Bind(target, ContentControl.ContentProperty); + + var source = new TextBlock + { + Name = "source", + Text = "foo", + }; + + stackPanel.Children.Add(source); + + Assert.Same(source, target.Content); + } } } From 8172d8214f345a75a006c9887881a15996acbda2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Dec 2015 22:07:33 +0000 Subject: [PATCH 15/36] Use a naive implementation of StyleActivator. As previous one was buggy and leaked. --- src/Perspex.Base/Collections/PerspexList.cs | 16 +-- src/Perspex.Controls/Classes.cs | 14 ++ .../Primitives/SelectingItemsControl.cs | 5 +- src/Perspex.Styling/Styling/Selectors.cs | 15 ++- src/Perspex.Styling/Styling/StyleActivator.cs | 127 ++---------------- tests/Perspex.LeakTests/StyleTests.cs | 5 - .../Perspex.Styling.UnitTests.csproj | 5 + .../StyleActivatorTests.cs | 101 +++++++------- .../Perspex.Styling.UnitTests/packages.config | 1 + 9 files changed, 99 insertions(+), 190 deletions(-) diff --git a/src/Perspex.Base/Collections/PerspexList.cs b/src/Perspex.Base/Collections/PerspexList.cs index 25424461db..e2d829afe5 100644 --- a/src/Perspex.Base/Collections/PerspexList.cs +++ b/src/Perspex.Base/Collections/PerspexList.cs @@ -171,7 +171,7 @@ namespace Perspex.Collections /// Adds an item to the collection. /// /// The item. - public void Add(T item) + public virtual void Add(T item) { Validate?.Invoke(item); int index = _inner.Count; @@ -183,7 +183,7 @@ namespace Perspex.Collections /// Adds multiple items to the collection. /// /// The items. - public void AddRange(IEnumerable items) + public virtual void AddRange(IEnumerable items) { Contract.Requires(items != null); @@ -264,7 +264,7 @@ namespace Perspex.Collections /// /// The index. /// The item. - public void Insert(int index, T item) + public virtual void Insert(int index, T item) { Validate?.Invoke(item); _inner.Insert(index, item); @@ -276,7 +276,7 @@ namespace Perspex.Collections /// /// The index. /// The items. - public void InsertRange(int index, IEnumerable items) + public virtual void InsertRange(int index, IEnumerable items) { Contract.Requires(items != null); @@ -302,7 +302,7 @@ namespace Perspex.Collections /// /// The item. /// True if the item was found and removed, otherwise false. - public bool Remove(T item) + public virtual bool Remove(T item) { int index = _inner.IndexOf(item); @@ -320,7 +320,7 @@ namespace Perspex.Collections /// Removes multiple items from the collection. /// /// The items. - public void RemoveAll(IEnumerable items) + public virtual void RemoveAll(IEnumerable items) { Contract.Requires(items != null); @@ -337,7 +337,7 @@ namespace Perspex.Collections /// Removes the item at the specified index. /// /// The index. - public void RemoveAt(int index) + public virtual void RemoveAt(int index) { T item = _inner[index]; _inner.RemoveAt(index); @@ -349,7 +349,7 @@ namespace Perspex.Collections /// /// The first index to remove. /// The number of items to remove. - public void RemoveRange(int index, int count) + public virtual void RemoveRange(int index, int count) { if (count > 0) { diff --git a/src/Perspex.Controls/Classes.cs b/src/Perspex.Controls/Classes.cs index 9e12871dc9..7a0d79de2b 100644 --- a/src/Perspex.Controls/Classes.cs +++ b/src/Perspex.Controls/Classes.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections.Generic; +using System.Linq; using Perspex.Collections; namespace Perspex.Controls @@ -21,5 +22,18 @@ namespace Perspex.Controls : base(items) { } + + public override void Add(string item) + { + if (!Contains(item)) + { + base.Add(item); + } + } + + public override void AddRange(IEnumerable items) + { + base.AddRange(items.Where(x => !Contains(x))); + } } } diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 142da517a8..df9f5b08d2 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -682,7 +682,10 @@ namespace Perspex.Controls.Primitives case NotifyCollectionChangedAction.Reset: foreach (var item in ItemContainerGenerator.Containers) { - MarkContainerSelected(item, false); + if (item != null) + { + MarkContainerSelected(item, false); + } } if (!_syncingSelectedItems) diff --git a/src/Perspex.Styling/Styling/Selectors.cs b/src/Perspex.Styling/Styling/Selectors.cs index 312f7e76e1..67da8fc8ca 100644 --- a/src/Perspex.Styling/Styling/Selectors.cs +++ b/src/Perspex.Styling/Styling/Selectors.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using System.Reactive; using System.Reactive.Linq; using System.Reflection; @@ -178,16 +179,16 @@ namespace Perspex.Styling private static SelectorMatch MatchClass(IStyleable control, string name) { - var changed = Observable.FromEventPattern< + var observable = Observable.FromEventPattern< NotifyCollectionChangedEventHandler, - NotifyCollectionChangedEventHandler>( + NotifyCollectionChangedEventArgs>( x => control.Classes.CollectionChanged += x, - x => control.Classes.CollectionChanged -= x); + x => control.Classes.CollectionChanged -= x) + .Select(_ => Unit.Default) + .StartWith(Unit.Default) + .Select(_ => control.Classes.Contains(name)); - return new SelectorMatch( - Observable - .Return(control.Classes.Contains(name)) - .Concat(changed.Select(e => control.Classes.Contains(name)))); + return new SelectorMatch(observable); } private static SelectorMatch MatchDescendent(IStyleable control, Selector previous) diff --git a/src/Perspex.Styling/Styling/StyleActivator.cs b/src/Perspex.Styling/Styling/StyleActivator.cs index c6b92a4e39..aa71884530 100644 --- a/src/Perspex.Styling/Styling/StyleActivator.cs +++ b/src/Perspex.Styling/Styling/StyleActivator.cs @@ -4,7 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; +using System.Reactive; +using System.Reactive.Linq; namespace Perspex.Styling { @@ -14,134 +15,30 @@ namespace Perspex.Styling Or, } - public class StyleActivator : IObservable, IDisposable + public class StyleActivator : ObservableBase { + private readonly IObservable[] _inputs; private readonly ActivatorMode _mode; - private readonly bool[] _values; - - private readonly List _subscriptions = new List(); - - private readonly List> _observers = new List>(); - public StyleActivator( IList> inputs, ActivatorMode mode = ActivatorMode.And) { - int i = 0; - + _inputs = inputs.ToArray(); _mode = mode; - _values = new bool[inputs.Count]; - - foreach (IObservable input in inputs) - { - int capturedIndex = i; - - IDisposable subscription = input.Subscribe( - x => Update(capturedIndex, x), - x => Finish(capturedIndex), - () => Finish(capturedIndex)); - _subscriptions.Add(subscription); - ++i; - } } - public bool CurrentValue + protected override IDisposable SubscribeCore(IObserver observer) { - get; - private set; - } - - public bool HasCompleted - { - get; - private set; - } - - public void Dispose() - { - foreach (IObserver observer in _observers) - { - observer.OnCompleted(); - } - - foreach (IDisposable subscription in _subscriptions) - { - subscription.Dispose(); - } - } - - public IDisposable Subscribe(IObserver observer) - { - Contract.Requires(observer != null); - - observer.OnNext(CurrentValue); - - if (HasCompleted) - { - observer.OnCompleted(); - return Disposable.Empty; - } - else - { - _observers.Add(observer); - return Disposable.Create(() => _observers.Remove(observer)); - } - } - - private void Update(int index, bool value) - { - _values[index] = value; - - bool current; - - switch (_mode) - { - case ActivatorMode.And: - current = _values.All(x => x); - break; - case ActivatorMode.Or: - current = _values.Any(x => x); - break; - default: - throw new InvalidOperationException("Invalid Activator mode."); - } - - if (current != CurrentValue) - { - Push(current); - CurrentValue = current; - } - } - - private void Finish(int i) - { - // We can unsubscribe from everything if the completed observable: - // - Is the only subscription. - // - Has finished on 'false' and we're in And mode - // - Has finished on 'true' and we're in Or mode - var value = _values[i]; - var unsubscribe = - (_values.Length == 1) || - (_mode == ActivatorMode.And ? !value : value); - - if (unsubscribe) - { - foreach (IDisposable subscription in _subscriptions) - { - subscription.Dispose(); - } - - HasCompleted = true; - } + return _inputs.CombineLatest() + .Select(Calculate) + .DistinctUntilChanged() + .Subscribe(observer); } - private void Push(bool value) + private bool Calculate(IList values) { - foreach (IObserver observer in _observers) - { - observer.OnNext(value); - } + return _mode == ActivatorMode.And ? values.All(x => x) : values.Any(x => x); } } } diff --git a/tests/Perspex.LeakTests/StyleTests.cs b/tests/Perspex.LeakTests/StyleTests.cs index bbaade7250..ae74048ae9 100644 --- a/tests/Perspex.LeakTests/StyleTests.cs +++ b/tests/Perspex.LeakTests/StyleTests.cs @@ -2,14 +2,10 @@ // 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 JetBrains.dotMemoryUnit; using Perspex.Controls; -using Perspex.Controls.Primitives; -using Perspex.Controls.Templates; using Perspex.Styling; -using Perspex.VisualTree; using Xunit; using Xunit.Abstractions; @@ -123,7 +119,6 @@ namespace Perspex.LeakTests dotMemory.Check(memory => Assert.Equal(1, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); - Assert.False(true); } } } diff --git a/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj b/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj index 95cb831480..cc22e7f257 100644 --- a/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj +++ b/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj @@ -40,6 +40,11 @@ 4 + + ..\..\packages\Rx-Testing.2.2.5\lib\net45\Microsoft.Reactive.Testing.dll + True + + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll diff --git a/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs b/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs index 2172bc39f7..3b444ec77f 100644 --- a/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs +++ b/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs @@ -2,13 +2,44 @@ // 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.Reactive; using System.Reactive.Linq; +using Microsoft.Reactive.Testing; using Xunit; namespace Perspex.Styling.UnitTests { - public class StyleActivatorTests + public class StyleActivatorTests : ReactiveTest { + [Fact] + public void Activator_Should_Subscribe_To_Inputs_On_First_Subscription() + { + var scheduler = new TestScheduler(); + var source = scheduler.CreateColdObservable(); + var target = new StyleActivator(new[] { source }, ActivatorMode.And); + + Assert.Equal(0, source.Subscriptions.Count); + target.Subscribe(_ => { }); + Assert.Equal(1, source.Subscriptions.Count); + } + + [Fact] + public void Activator_Should_Unsubscribe_From_Inputs_After_Last_Subscriber_Completes() + { + var scheduler = new TestScheduler(); + var source = scheduler.CreateColdObservable(); + var target = new StyleActivator(new[] { source }, ActivatorMode.And); + + var dispose = target.Subscribe(_ => { }); + Assert.Equal(1, source.Subscriptions.Count); + Assert.Equal(Subscription.Infinite, source.Subscriptions[0].Unsubscribe); + + dispose.Dispose(); + Assert.Equal(1, source.Subscriptions.Count); + Assert.Equal(0, source.Subscriptions[0].Unsubscribe); + } + [Fact] public void Activator_And_Should_Follow_Single_Input() { @@ -53,32 +84,6 @@ namespace Perspex.Styling.UnitTests Assert.Equal(1, inputs[2].SubscriberCount); } - [Fact] - public void Activator_And_Should_Unsubscribe_All_When_Input_Completes_On_False() - { - var inputs = new[] - { - new TestSubject(false), - new TestSubject(false), - new TestSubject(true), - }; - var target = new StyleActivator(inputs, ActivatorMode.And); - var result = new TestObserver(); - - target.Subscribe(result); - Assert.False(result.GetValue()); - inputs[0].OnNext(true); - inputs[1].OnNext(true); - Assert.True(result.GetValue()); - inputs[0].OnNext(false); - Assert.False(result.GetValue()); - inputs[0].OnCompleted(); - - Assert.Equal(0, inputs[0].SubscriberCount); - Assert.Equal(0, inputs[1].SubscriberCount); - Assert.Equal(0, inputs[2].SubscriberCount); - } - [Fact] public void Activator_And_Should_Not_Unsubscribe_All_When_Input_Completes_On_True() { @@ -96,7 +101,7 @@ namespace Perspex.Styling.UnitTests inputs[0].OnNext(true); inputs[0].OnCompleted(); - Assert.Equal(1, inputs[0].SubscriberCount); + Assert.Equal(0, inputs[0].SubscriberCount); Assert.Equal(1, inputs[1].SubscriberCount); Assert.Equal(1, inputs[2].SubscriberCount); } @@ -144,31 +149,6 @@ namespace Perspex.Styling.UnitTests Assert.Equal(1, inputs[2].SubscriberCount); } - [Fact] - public void Activator_Or_Should_Unsubscribe_All_When_Input_Completes_On_True() - { - var inputs = new[] - { - new TestSubject(false), - new TestSubject(false), - new TestSubject(true), - }; - var target = new StyleActivator(inputs, ActivatorMode.Or); - var result = new TestObserver(); - - target.Subscribe(result); - Assert.True(result.GetValue()); - inputs[2].OnNext(false); - Assert.False(result.GetValue()); - inputs[0].OnNext(true); - Assert.True(result.GetValue()); - inputs[0].OnCompleted(); - - Assert.Equal(0, inputs[0].SubscriberCount); - Assert.Equal(0, inputs[1].SubscriberCount); - Assert.Equal(0, inputs[2].SubscriberCount); - } - [Fact] public void Activator_Or_Should_Not_Unsubscribe_All_When_Input_Completes_On_False() { @@ -189,7 +169,7 @@ namespace Perspex.Styling.UnitTests Assert.Equal(1, inputs[0].SubscriberCount); Assert.Equal(1, inputs[1].SubscriberCount); - Assert.Equal(1, inputs[2].SubscriberCount); + Assert.Equal(0, inputs[2].SubscriberCount); } [Fact] @@ -207,5 +187,18 @@ namespace Perspex.Styling.UnitTests Assert.True(completed); } + + private Recorded>[] OnNextValues(params bool[] values) + { + var result = new List>>(); + var time = 1; + + foreach (var value in values) + { + result.Add(new Recorded>(time, Notification.CreateOnNext(value))); + } + + return result.ToArray(); + } } } diff --git a/tests/Perspex.Styling.UnitTests/packages.config b/tests/Perspex.Styling.UnitTests/packages.config index 3cf2b984d5..bacf5a37aa 100644 --- a/tests/Perspex.Styling.UnitTests/packages.config +++ b/tests/Perspex.Styling.UnitTests/packages.config @@ -6,6 +6,7 @@ + From a6b74db847506fd4f4d04aebe43d968b111a4acc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Dec 2015 22:40:24 +0000 Subject: [PATCH 16/36] Merge branch 'logical-tree' into leaks --- samples/TestApplicationShared/GalleryStyle.cs | 21 +- samples/TestApplicationShared/MainWindow.cs | 1 - .../Perspex.Markup.Xaml/Data/Binding.cs | 2 - src/Perspex.Application/Application.cs | 7 +- src/Perspex.Base/Collections/PerspexList.cs | 5 +- src/Perspex.Controls/ContentControl.cs | 42 +--- src/Perspex.Controls/Control.cs | 223 +++++++++++++++--- src/Perspex.Controls/Decorator.cs | 4 +- src/Perspex.Controls/DropDown.cs | 4 +- .../Generators/IItemContainerGenerator.cs | 19 +- .../Generators/ItemContainer.cs | 45 ++++ ...ontainers.cs => ItemContainerEventArgs.cs} | 22 +- .../Generators/ItemContainerGenerator.cs | 77 +++--- .../Generators/ItemContainerGenerator`1.cs | 5 +- .../Generators/TreeItemContainerGenerator.cs | 10 +- src/Perspex.Controls/IReparentingControl.cs | 25 -- src/Perspex.Controls/IReparentingHost.cs | 28 --- src/Perspex.Controls/ItemsControl.cs | 193 +++++++++++++-- .../LogicalTreeAttachmentEventArgs.cs | 31 +++ src/Perspex.Controls/MenuItem.cs | 28 +-- .../Mixins/ContentControlMixin.cs | 136 +++++++++++ src/Perspex.Controls/Panel.cs | 79 ++----- src/Perspex.Controls/Perspex.Controls.csproj | 10 +- .../Presenters/CarouselPresenter.cs | 18 +- .../Presenters/ContentPresenter.cs | 30 +-- .../Presenters/ItemsPresenter.cs | 58 +++-- .../Primitives/AdornerDecorator.cs | 2 +- .../Primitives/HeaderedItemsControl.cs | 41 ++++ .../Primitives/HeaderedSelectingControl.cs | 58 +++++ src/Perspex.Controls/Primitives/PopupRoot.cs | 5 +- .../Primitives/SelectingItemsControl.cs | 113 +++------ src/Perspex.Controls/Primitives/TabStrip.cs | 40 +--- .../Primitives/TabStripItem.cs | 12 + .../Primitives/TemplateAppliedEventArgs.cs | 28 +++ .../Primitives/TemplatedControl.cs | 46 +++- src/Perspex.Controls/Primitives/Track.cs | 4 +- src/Perspex.Controls/ProgressBar.cs | 4 +- src/Perspex.Controls/TabControl.cs | 93 +++++--- .../Templates/TemplateExtensions.cs | 29 --- src/Perspex.Controls/TextBlock.cs | 6 +- src/Perspex.Controls/TextBox.cs | 4 +- src/Perspex.Controls/TopLevel.cs | 7 +- src/Perspex.SceneGraph/Visual.cs | 130 +++------- .../VisualTreeAttachmentEventArgs.cs | 2 + src/Perspex.Styling/ILogical.cs | 5 + src/Perspex.Styling/Perspex.Styling.csproj | 1 + src/Perspex.Styling/Styling/IGlobalStyles.cs | 6 +- src/Perspex.Styling/Styling/IStyleHost.cs | 2 +- src/Perspex.Styling/Styling/IStyleRoot.cs | 12 + src/Perspex.Styling/Styling/IStyleable.cs | 6 + src/Perspex.Styling/Styling/Selector.cs | 2 +- src/Perspex.Styling/Styling/Selectors.cs | 6 +- src/Perspex.Styling/Styling/Style.cs | 16 +- src/Perspex.Styling/Styling/StyleActivator.cs | 32 ++- src/Perspex.Styling/Styling/Styler.cs | 28 +-- src/Perspex.Themes.Default/Button.paml | 3 +- src/Perspex.Themes.Default/DefaultTheme.paml | 2 +- src/Perspex.Themes.Default/MenuItem.paml | 6 +- .../Perspex.Themes.Default.csproj | 2 +- src/Perspex.Themes.Default/TabControl.paml | 36 ++- src/Perspex.Themes.Default/TabStrip.paml | 6 +- .../{TabItem.paml => TabStripItem.paml} | 8 +- src/Perspex.Themes.Default/ToggleButton.paml | 3 +- src/Perspex.Themes.Default/TreeViewItem.paml | 2 +- .../ContentControlTests.cs | 73 +----- .../ContentPresenterTests.cs | 89 ------- .../ControlTests.cs | 110 ++++++++- .../ControlTests_NameScope.cs | 3 +- .../EnumerableExtensions.cs | 20 ++ .../Generators/ItemContainerGeneratorTests.cs | 26 +- .../ItemContainerGeneratorTypedTests.cs | 4 +- .../HeaderedItemsControlTests .cs | 78 ++++++ .../ItemsControlTests.cs | 58 +++-- .../ListBoxTests.cs | 35 ++- .../Perspex.Controls.UnitTests/PanelTests.cs | 17 -- .../Perspex.Controls.UnitTests.csproj | 4 +- .../Presenters/ContentPresenterTests.cs | 68 ++++++ .../Presenters/ItemsPresenterTests.cs | 3 +- .../Primitives/PopupTests.cs | 16 -- .../Primitives/TabStripTests.cs | 159 ++++++++----- .../Primitives/TemplatedControlTests.cs | 99 ++++++-- .../TabControlTests.cs | 174 +++++++++----- tests/Perspex.Controls.UnitTests/TestRoot.cs | 3 +- .../TestTemplatedControl.cs | 7 +- .../TreeViewTests.cs | 2 +- .../InteractiveTests.cs | 2 +- .../Perspex.LeakTests.csproj | 1 - tests/Perspex.LeakTests/StyleTests.cs | 124 ---------- tests/Perspex.Markup.UnitTests/TestRoot.cs | 3 +- .../PerspexPropertyConverterTest.cs | 3 + .../Perspex.Markup.Xaml.UnitTests/TestRoot.cs | 3 +- .../TestVisual.cs | 14 +- .../VisualTests.cs | 23 -- .../SelectorTests_Child.cs | 5 + .../SelectorTests_Descendent.cs | 5 + .../StyleActivatorTests.cs | 30 ++- tests/Perspex.Styling.UnitTests/StyleTests.cs | 2 +- .../TestControlBase.cs | 3 + tests/Perspex.Styling.UnitTests/TestRoot.cs | 2 +- .../TestTemplatedControl.cs | 3 + 100 files changed, 1925 insertions(+), 1277 deletions(-) create mode 100644 src/Perspex.Controls/Generators/ItemContainer.cs rename src/Perspex.Controls/Generators/{ItemContainers.cs => ItemContainerEventArgs.cs} (62%) delete mode 100644 src/Perspex.Controls/IReparentingControl.cs delete mode 100644 src/Perspex.Controls/IReparentingHost.cs create mode 100644 src/Perspex.Controls/LogicalTreeAttachmentEventArgs.cs create mode 100644 src/Perspex.Controls/Mixins/ContentControlMixin.cs create mode 100644 src/Perspex.Controls/Primitives/HeaderedSelectingControl.cs create mode 100644 src/Perspex.Controls/Primitives/TabStripItem.cs create mode 100644 src/Perspex.Controls/Primitives/TemplateAppliedEventArgs.cs create mode 100644 src/Perspex.Styling/Styling/IStyleRoot.cs rename src/Perspex.Themes.Default/{TabItem.paml => TabStripItem.paml} (53%) delete mode 100644 tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs create mode 100644 tests/Perspex.Controls.UnitTests/EnumerableExtensions.cs create mode 100644 tests/Perspex.Controls.UnitTests/HeaderedItemsControlTests .cs create mode 100644 tests/Perspex.Controls.UnitTests/Presenters/ContentPresenterTests.cs delete mode 100644 tests/Perspex.LeakTests/StyleTests.cs diff --git a/samples/TestApplicationShared/GalleryStyle.cs b/samples/TestApplicationShared/GalleryStyle.cs index a0e32b7740..ecac60c33a 100644 --- a/samples/TestApplicationShared/GalleryStyle.cs +++ b/samples/TestApplicationShared/GalleryStyle.cs @@ -27,15 +27,15 @@ namespace TestApplication } }, - new Style(s => s.Class(":container").OfType().Child().Child().Child().Child().Child().OfType()) + new Style(s => s.Class(":container").OfType().Child().Child().Child().Child().Child().OfType()) { Setters = new[] { - new Setter (TemplatedControl.TemplateProperty, new FuncControlTemplate (TabItemTemplate)), + new Setter (TemplatedControl.TemplateProperty, new FuncControlTemplate(TabStripItemTemplate)), } }, - new Style(s => s.Name("internalStrip").OfType().Child().OfType()) + new Style(s => s.Name("PART_TabStrip").OfType().Child().OfType()) { Setters = new[] { @@ -44,7 +44,7 @@ namespace TestApplication } }, - new Style(s => s.Name("internalStrip").OfType().Child().OfType().Class(":selected")) + new Style(s => s.Name("PART_TabStrip").OfType().Child().OfType().Class(":selected")) { Setters = new[] { @@ -55,7 +55,7 @@ namespace TestApplication }); } - public static Control TabItemTemplate(TabItem control) + public static Control TabStripItemTemplate(TabStripItem control) { return new ContentPresenter { @@ -72,8 +72,8 @@ namespace TestApplication } }) }, - Name = "headerPresenter", - [~ContentPresenter.ContentProperty] = control[~HeaderedContentControl.HeaderProperty], + Name = "PART_ContentPresenter", + [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], }; } @@ -96,9 +96,10 @@ namespace TestApplication { Content = new TabStrip { + Name = "PART_TabStrip", ItemsPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical, Gap = 4 }), Margin = new Thickness(0, 10, 0, 0), - Name = "internalStrip", + MemberSelector = TabControl.HeaderSelector, [!ItemsControl.ItemsProperty] = control[!ItemsControl.ItemsProperty], [!!SelectingItemsControl.SelectedItemProperty] = control[!!SelectingItemsControl.SelectedItemProperty], } @@ -106,8 +107,8 @@ namespace TestApplication }, new Carousel { - Name = "carousel", - MemberSelector = control.ContentSelector, + Name = "PART_Content", + MemberSelector = TabControl.ContentSelector, [~Carousel.TransitionProperty] = control[~TabControl.TransitionProperty], [!Carousel.ItemsProperty] = control[!ItemsControl.ItemsProperty], [!Carousel.SelectedItemProperty] = control[!SelectingItemsControl.SelectedItemProperty], diff --git a/samples/TestApplicationShared/MainWindow.cs b/samples/TestApplicationShared/MainWindow.cs index 2a0c736984..7e05c01a35 100644 --- a/samples/TestApplicationShared/MainWindow.cs +++ b/samples/TestApplicationShared/MainWindow.cs @@ -105,7 +105,6 @@ namespace TestApplication }; container.Classes.Add(":container"); - window.Show(); return window; diff --git a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs index 20cab81984..00ed242855 100644 --- a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs @@ -246,8 +246,6 @@ namespace Perspex.Markup.Xaml.Data var update = target.GetObservable(Control.TemplatedParentProperty) .Skip(1) - .Where(x => x != null) - .Take(1) .Select(_ => Unit.Default); var result = new ExpressionObserver( diff --git a/src/Perspex.Application/Application.cs b/src/Perspex.Application/Application.cs index 52d2574c70..177c337d04 100644 --- a/src/Perspex.Application/Application.cs +++ b/src/Perspex.Application/Application.cs @@ -32,7 +32,7 @@ namespace Perspex /// method. /// - Tracks the lifetime of the application. /// - public class Application : IGlobalDataTemplates, IGlobalStyles + public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot { static Action _platformInitializationCallback; @@ -135,6 +135,11 @@ namespace Perspex protected set; } + /// + /// Gets the styling parent of the application, which is null. + /// + IStyleHost IStyleHost.StylingParent => null; + /// /// Runs the application's main loop until the is closed. /// diff --git a/src/Perspex.Base/Collections/PerspexList.cs b/src/Perspex.Base/Collections/PerspexList.cs index e2d829afe5..120f62d82f 100644 --- a/src/Perspex.Base/Collections/PerspexList.cs +++ b/src/Perspex.Base/Collections/PerspexList.cs @@ -52,7 +52,7 @@ namespace Perspex.Collections /// /// /// - public class PerspexList : IPerspexList, IList, INotifyCollectionChanged, INotifyPropertyChanged + public class PerspexList : IPerspexList, IList { private List _inner; @@ -150,7 +150,8 @@ namespace Perspex.Collections var e = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Replace, value, - old); + old, + index); CollectionChanged(this, e); } } diff --git a/src/Perspex.Controls/ContentControl.cs b/src/Perspex.Controls/ContentControl.cs index c4c1c94e31..2899b58bcd 100644 --- a/src/Perspex.Controls/ContentControl.cs +++ b/src/Perspex.Controls/ContentControl.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Perspex.Collections; +using System.Linq; +using System.Reactive.Linq; +using Perspex.Controls.Mixins; using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; @@ -14,7 +16,7 @@ namespace Perspex.Controls /// /// Displays according to a . /// - public class ContentControl : TemplatedControl, IContentControl, IReparentingHost + public class ContentControl : TemplatedControl, IContentControl { /// /// Defines the property. @@ -34,18 +36,14 @@ namespace Perspex.Controls public static readonly PerspexProperty VerticalContentAlignmentProperty = PerspexProperty.Register(nameof(VerticalContentAlignment)); - /// - /// Initializes static members of the class. - /// - static ContentControl() - { - } + private IDisposable _presenterSubscription; /// - /// Initializes a new instance of the class. + /// Initializes static members of the class. /// - public ContentControl() + static ContentControl() { + ContentControlMixin.Attach(ContentProperty, x => x.LogicalChildren); } /// @@ -85,31 +83,15 @@ namespace Perspex.Controls set { SetValue(VerticalContentAlignmentProperty, value); } } - /// - /// Gets a writeable logical children collection from the host. - /// - IPerspexList IReparentingHost.LogicalChildren => LogicalChildren; - - /// - /// Asks the control whether it wants to reparent the logical children of the specified - /// control. - /// - /// The control. - /// - /// True if the control wants to reparent its logical children otherwise false. - /// - bool IReparentingHost.WillReparentChildrenOf(IControl control) - { - return control is IContentPresenter && control.TemplatedParent == this; - } - /// - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { + base.OnTemplateApplied(e); + // We allow ContentControls without ContentPresenters in the template. This can be // useful for e.g. a simple ToggleButton that displays an image. There's no need to // have a ContentPresenter in the visual tree for that. - Presenter = nameScope.Find("PART_ContentPresenter"); + Presenter = e.NameScope.Find("PART_ContentPresenter"); } } } diff --git a/src/Perspex.Controls/Control.cs b/src/Perspex.Controls/Control.cs index e76be17149..ea20401142 100644 --- a/src/Perspex.Controls/Control.cs +++ b/src/Perspex.Controls/Control.cs @@ -2,12 +2,18 @@ // 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.Collections.Specialized; +using System.Linq; +using System.Reactive; using System.Reactive.Linq; +using System.Reactive.Subjects; using Perspex.Collections; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.Input; using Perspex.Interactivity; +using Perspex.LogicalTree; using Perspex.Rendering; using Perspex.Styling; @@ -70,9 +76,11 @@ namespace Perspex.Controls private readonly Classes _classes = new Classes(); private DataTemplates _dataTemplates; private IControl _focusAdorner; + private bool _isAttachedToLogicalTree; private IPerspexList _logicalChildren; private INameScope _nameScope; private Styles _styles; + private Subject _styleDetach = new Subject(); /// /// Initializes static members of the class. @@ -93,6 +101,16 @@ namespace Perspex.Controls _nameScope = this as INameScope; } + /// + /// Raised when the control is attached to a rooted logical tree. + /// + public event EventHandler AttachedToLogicalTree; + + /// + /// Raised when the control is detached from a rooted logical tree. + /// + public event EventHandler DetachedFromLogicalTree; + /// /// Occurs when the property changes. /// @@ -206,6 +224,11 @@ namespace Perspex.Controls internal set { SetValue(TemplatedParentProperty, value); } } + /// + /// Gets a value indicating whether the element is attached to a rooted logical tree. + /// + bool ILogical.IsAttachedToLogicalTree => _isAttachedToLogicalTree; + /// /// Gets the control's logical parent. /// @@ -230,9 +253,10 @@ namespace Perspex.Controls /// Type IStyleable.StyleKey => GetType(); - /// - /// Gets the parent style host element. - /// + /// + IObservable IStyleable.StyleDetach => _styleDetach; + + /// IStyleHost IStyleHost.StylingParent => (IStyleHost)InheritanceParent; /// @@ -255,12 +279,32 @@ namespace Perspex.Controls if (_logicalChildren == null) { var list = new PerspexList(); - list.ResetBehavior = ResetBehavior.Remove; - _logicalChildren = list; + LogicalChildren = list; } return _logicalChildren; } + + set + { + Contract.Requires(value != null); + + if (_logicalChildren != value) + { + if (_logicalChildren != null) + { + _logicalChildren.CollectionChanged -= LogicalChildrenCollectionChanged; + } + } + + if (value is PerspexList) + { + ((PerspexList)value).ResetBehavior = ResetBehavior.Remove; + } + + _logicalChildren = value; + _logicalChildren.CollectionChanged += LogicalChildrenCollectionChanged; + } } /// @@ -278,7 +322,26 @@ namespace Perspex.Controls throw new InvalidOperationException("The Control already has a parent."); } - SetAndRaise(ParentProperty, ref _parent, (IControl)parent); + InheritanceParent = parent as PerspexObject; + _parent = (IControl)parent; + + var root = FindStyleRoot(old); + + if (root != null) + { + var e = new LogicalTreeAttachmentEventArgs(root); + OnDetachedFromLogicalTree(e); + } + + root = FindStyleRoot(this); + + if (root != null) + { + var e = new LogicalTreeAttachmentEventArgs(root); + OnAttachedToLogicalTree(e); + } + + RaisePropertyChanged(ParentProperty, old, _parent, BindingPriority.LocalValue); } } @@ -328,6 +391,62 @@ namespace Perspex.Controls }); } + /// + /// Called when the control is added to a logical tree. + /// + /// The event args. + /// + /// It is vital that if you override this method you call the base implementation; + /// failing to do so will cause numerous features to not work as expected. + /// + protected virtual void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (_nameScope == null) + { + _nameScope = NameScope.GetNameScope(this) ?? ((Control)Parent)?._nameScope; + } + + if (Name != null) + { + _nameScope?.Register(Name, this); + } + + _isAttachedToLogicalTree = true; + PerspexLocator.Current.GetService()?.ApplyStyles(this); + AttachedToLogicalTree?.Invoke(this, e); + + foreach (var child in LogicalChildren.OfType()) + { + child.OnAttachedToLogicalTree(e); + } + } + + /// + /// Called when the control is removed from a logical tree. + /// + /// The event args. + /// + /// It is vital that if you override this method you call the base implementation; + /// failing to do so will cause numerous features to not work as expected. + /// + protected virtual void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (Name != null) + { + _nameScope?.Unregister(Name); + } + + _isAttachedToLogicalTree = false; + _styleDetach.OnNext(Unit.Default); + this.TemplatedParent = null; + DetachedFromLogicalTree?.Invoke(this, e); + + foreach (var child in LogicalChildren.OfType()) + { + child.OnDetachedFromLogicalTree(e); + } + } + /// protected override void OnGotFocus(GotFocusEventArgs e) { @@ -373,35 +492,6 @@ namespace Perspex.Controls } } - /// - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - - if (_nameScope == null) - { - _nameScope = NameScope.GetNameScope(this) ?? ((Control)Parent)?._nameScope; - } - - if (Name != null) - { - _nameScope?.Register(Name, this); - } - - PerspexLocator.Current.GetService()?.ApplyStyles(this); - } - - /// - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - - if (Name != null) - { - _nameScope?.Unregister(Name); - } - } - /// /// Called when the is changed and all subscribers to that change /// have been notified. @@ -417,7 +507,7 @@ namespace Perspex.Controls /// The logical children to use. protected void RedirectLogicalChildren(IPerspexList collection) { - _logicalChildren = collection; + LogicalChildren = collection; } /// @@ -439,5 +529,66 @@ namespace Perspex.Controls } } } + + private static IStyleRoot FindStyleRoot(IStyleHost e) + { + while (e != null) + { + var root = e as IStyleRoot; + + if (root != null && root.StylingParent == null) + { + return root; + } + + e = e.StylingParent; + } + + return null; + } + + private void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + SetLogicalParent(e.NewItems.Cast()); + break; + + case NotifyCollectionChangedAction.Remove: + ClearLogicalParent(e.OldItems.Cast()); + break; + + case NotifyCollectionChangedAction.Replace: + ClearLogicalParent(e.OldItems.Cast()); + SetLogicalParent(e.NewItems.Cast()); + break; + + case NotifyCollectionChangedAction.Reset: + throw new NotSupportedException("Reset should not be signalled on LogicalChildren collection"); + } + } + + private void SetLogicalParent(IEnumerable children) + { + foreach (var i in children) + { + if (i.LogicalParent == null) + { + ((ISetLogicalParent)i).SetParent(this); + } + } + } + + private void ClearLogicalParent(IEnumerable children) + { + foreach (var i in children) + { + if (i.LogicalParent == this) + { + ((ISetLogicalParent)i).SetParent(null); + } + } + } } } diff --git a/src/Perspex.Controls/Decorator.cs b/src/Perspex.Controls/Decorator.cs index f5da5a4114..3440845c46 100644 --- a/src/Perspex.Controls/Decorator.cs +++ b/src/Perspex.Controls/Decorator.cs @@ -88,13 +88,13 @@ namespace Perspex.Controls { ((ISetLogicalParent)oldChild).SetParent(null); LogicalChildren.Clear(); - RemoveVisualChild(oldChild); + VisualChildren.Remove(oldChild); } if (newChild != null) { ((ISetLogicalParent)newChild).SetParent(this); - AddVisualChild(newChild); + VisualChildren.Add(newChild); LogicalChildren.Add(newChild); } } diff --git a/src/Perspex.Controls/DropDown.cs b/src/Perspex.Controls/DropDown.cs index 31a44ad7d0..7995d48105 100644 --- a/src/Perspex.Controls/DropDown.cs +++ b/src/Perspex.Controls/DropDown.cs @@ -125,14 +125,14 @@ namespace Perspex.Controls base.OnPointerPressed(e); } - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { if (_popup != null) { _popup.Opened -= PopupOpened; } - _popup = nameScope.Get("PART_Popup"); + _popup = e.NameScope.Get("PART_Popup"); _popup.Opened += PopupOpened; } diff --git a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs index 7189d7a0c9..85628e3374 100644 --- a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs @@ -16,12 +16,17 @@ namespace Perspex.Controls.Generators /// /// Gets the currently realized containers. /// - IEnumerable Containers { get; } + IEnumerable Containers { get; } /// - /// Signalled whenever new containers are initialized. + /// Signalled whenever new containers are materialized. /// - IObservable ContainersInitialized { get; } + event EventHandler Materialized; + + /// + /// Event raised whenever containers are dematerialized. + /// + event EventHandler Dematerialized; /// /// Creates container controls for a collection of items. @@ -32,7 +37,7 @@ namespace Perspex.Controls.Generators /// The items. /// An optional member selector. /// The created controls. - IEnumerable Materialize( + IEnumerable Materialize( int startingIndex, IEnumerable items, IMemberSelector selector); @@ -45,7 +50,7 @@ namespace Perspex.Controls.Generators /// /// The the number of items to remove. /// The removed containers. - IEnumerable Dematerialize(int startingIndex, int count); + IEnumerable Dematerialize(int startingIndex, int count); /// /// Removes a set of created containers and updates the index of later containers to fill @@ -56,13 +61,13 @@ namespace Perspex.Controls.Generators /// /// The the number of items to remove. /// The removed containers. - IEnumerable RemoveRange(int startingIndex, int count); + IEnumerable RemoveRange(int startingIndex, int count); /// /// Clears all created containers and returns the removed controls. /// /// The removed controls. - IEnumerable Clear(); + IEnumerable Clear(); /// /// Gets the container control representing the item with the specified index. diff --git a/src/Perspex.Controls/Generators/ItemContainer.cs b/src/Perspex.Controls/Generators/ItemContainer.cs new file mode 100644 index 0000000000..86f3795106 --- /dev/null +++ b/src/Perspex.Controls/Generators/ItemContainer.cs @@ -0,0 +1,45 @@ +// 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. + +namespace Perspex.Controls.Generators +{ + /// + /// Holds information about an item container generated by an + /// . + /// + public class ItemContainer + { + /// + /// Initializes a new instance of the class. + /// + /// The container control. + /// The item that the container represents. + /// + /// The index of the item in the collection. + /// + public ItemContainer(IControl container, object item, int index) + { + ContainerControl = container; + Item = item; + Index = index; + } + + /// + /// Gets the container control. + /// + /// + /// This will be null if is null. + /// + public IControl ContainerControl { get; } + + /// + /// Gets the item that the container represents. + /// + public object Item { get; } + + /// + /// Gets the index of the item in the collection. + /// + public int Index { get; } + } +} \ No newline at end of file diff --git a/src/Perspex.Controls/Generators/ItemContainers.cs b/src/Perspex.Controls/Generators/ItemContainerEventArgs.cs similarity index 62% rename from src/Perspex.Controls/Generators/ItemContainers.cs rename to src/Perspex.Controls/Generators/ItemContainerEventArgs.cs index 9eec0f1147..86e2b890d1 100644 --- a/src/Perspex.Controls/Generators/ItemContainers.cs +++ b/src/Perspex.Controls/Generators/ItemContainerEventArgs.cs @@ -1,34 +1,38 @@ // Copyright (c) The Perspex Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.Collections.Generic; namespace Perspex.Controls.Generators { /// - /// Holds details about a set of item containers in an . + /// Provides details for the + /// and events. /// - public class ItemContainers + public class ItemContainerEventArgs : EventArgs { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The index of the first container in the source items. /// The containers. - public ItemContainers(int startingIndex, IList containers) + public ItemContainerEventArgs( + int startingIndex, + IList containers) { StartingIndex = startingIndex; - Items = containers; + Containers = containers; } /// - /// Gets the index of the first container in the source items. + /// Gets the containers. /// - public int StartingIndex { get; } + public IList Containers { get; } /// - /// Gets the containers. May contain null entries. + /// Gets the index of the first container in the source items. /// - public IList Items { get; } + public int StartingIndex { get; } } } diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs index b8b57ddd59..2733de5770 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs @@ -15,9 +15,7 @@ namespace Perspex.Controls.Generators /// public class ItemContainerGenerator : IItemContainerGenerator { - private List _containers = new List(); - - private readonly Subject _containersInitialized = new Subject(); + private List _containers = new List(); /// /// Initializes a new instance of the class. @@ -31,10 +29,13 @@ namespace Perspex.Controls.Generators } /// - public IEnumerable Containers => _containers; + public IEnumerable Containers => _containers; + + /// + public event EventHandler Materialized; /// - public IObservable ContainersInitialized => _containersInitialized; + public event EventHandler Dematerialized; /// /// Gets the owner control. @@ -42,7 +43,7 @@ namespace Perspex.Controls.Generators public IControl Owner { get; } /// - public IEnumerable Materialize( + public IEnumerable Materialize( int startingIndex, IEnumerable items, IMemberSelector selector) @@ -50,25 +51,25 @@ namespace Perspex.Controls.Generators Contract.Requires(items != null); int index = startingIndex; - var result = new List(); + var result = new List(); foreach (var item in items) { var i = selector != null ? selector.Select(item) : item; - var container = CreateContainer(i); + var container = new ItemContainer(CreateContainer(i), item, index++); result.Add(container); } - AddContainers(startingIndex, result); - _containersInitialized.OnNext(new ItemContainers(startingIndex, result)); + AddContainers(result); + Materialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); return result.Where(x => x != null).ToList(); } /// - public virtual IEnumerable Dematerialize(int startingIndex, int count) + public virtual IEnumerable Dematerialize(int startingIndex, int count) { - var result = new List(); + var result = new List(); for (int i = startingIndex; i < startingIndex + count; ++i) { @@ -79,22 +80,31 @@ namespace Perspex.Controls.Generators } } + Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); + return result; } /// - public virtual IEnumerable RemoveRange(int startingIndex, int count) + public virtual IEnumerable RemoveRange(int startingIndex, int count) { var result = _containers.GetRange(startingIndex, count); _containers.RemoveRange(startingIndex, count); + Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); return result; } /// - public virtual IEnumerable Clear() + public virtual IEnumerable Clear() { var result = _containers; - _containers = new List(); + _containers = new List(); + + if (result.Count > 0) + { + Dematerialized?.Invoke(this, new ItemContainerEventArgs(0, result)); + } + return result; } @@ -103,7 +113,7 @@ namespace Perspex.Controls.Generators { if (index < _containers.Count) { - return _containers[index]; + return _containers[index]?.ContainerControl; } return null; @@ -112,7 +122,19 @@ namespace Perspex.Controls.Generators /// public int IndexFromContainer(IControl container) { - return _containers.IndexOf(container); + var index = 0; + + foreach (var i in _containers) + { + if (i?.ContainerControl == container) + { + return index; + } + + ++index; + } + + return -1; } /// @@ -135,33 +157,30 @@ namespace Perspex.Controls.Generators /// /// Adds a collection of containers to the index. /// - /// The starting index. - /// The container. - protected void AddContainers(int index, IList container) + /// The containers. + protected void AddContainers(IList containers) { - Contract.Requires(container != null); + Contract.Requires(containers != null); - foreach (var c in container) + foreach (var c in containers) { - while (_containers.Count < index) + while (_containers.Count < c.Index) { _containers.Add(null); } - if (_containers.Count == index) + if (_containers.Count == c.Index) { _containers.Add(c); } - else if (_containers[index] == null) + else if (_containers[c.Index] == null) { - _containers[index] = c; + _containers[c.Index] = c; } else { throw new InvalidOperationException("Container already created."); } - - ++index; } } @@ -171,7 +190,7 @@ namespace Perspex.Controls.Generators /// The first index. /// The number of elements in the range. /// The containers. - protected IEnumerable GetContainerRange(int index, int count) + protected IEnumerable GetContainerRange(int index, int count) { return _containers.GetRange(index, count); } diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs index dcb365b120..1a18c2d154 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs @@ -24,6 +24,9 @@ namespace Perspex.Controls.Generators PerspexProperty contentProperty) : base(owner) { + Contract.Requires(owner != null); + Contract.Requires(contentProperty != null); + ContentProperty = contentProperty; } @@ -48,7 +51,7 @@ namespace Perspex.Controls.Generators else { var result = new T(); - result.SetValue(ContentProperty, Owner.MaterializeDataTemplate(item)); + result.SetValue(ContentProperty, item); if (!(item is IControl)) { diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index 802cd3866b..4caa53e3c1 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -120,13 +120,13 @@ namespace Perspex.Controls.Generators } } - public override IEnumerable Clear() + public override IEnumerable Clear() { ClearIndex(); return base.Clear(); } - public override IEnumerable Dematerialize(int startingIndex, int count) + public override IEnumerable Dematerialize(int startingIndex, int count) { RemoveFromIndex(GetContainerRange(startingIndex, count)); return base.Dematerialize(startingIndex, count); @@ -145,7 +145,7 @@ namespace Perspex.Controls.Generators } } - private void RemoveFromIndex(IEnumerable containers) + private void RemoveFromIndex(IEnumerable containers) { if (RootGenerator != null) { @@ -155,8 +155,8 @@ namespace Perspex.Controls.Generators { foreach (var container in containers) { - var item = _containerToItem[container]; - _containerToItem.Remove(container); + var item = _containerToItem[container.ContainerControl]; + _containerToItem.Remove(container.ContainerControl); _itemToContainer.Remove(item); } } diff --git a/src/Perspex.Controls/IReparentingControl.cs b/src/Perspex.Controls/IReparentingControl.cs deleted file mode 100644 index a7d4b46e64..0000000000 --- a/src/Perspex.Controls/IReparentingControl.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Perspex.Collections; - -namespace Perspex.Controls -{ - /// - /// A control that can make its visual children the logical children of another control. - /// - public interface IReparentingControl : IControl - { - /// - /// Requests that the visual children of the control use another control as their logical - /// parent. - /// - /// - /// The logical parent for the visual children of the control. - /// - /// - /// The collection to modify. - /// - void ReparentLogicalChildren(ILogical logicalParent, IPerspexList children); - } -} \ No newline at end of file diff --git a/src/Perspex.Controls/IReparentingHost.cs b/src/Perspex.Controls/IReparentingHost.cs deleted file mode 100644 index 99dcf7d4f1..0000000000 --- a/src/Perspex.Controls/IReparentingHost.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Perspex.Collections; - -namespace Perspex.Controls -{ - /// - /// A control that can use the visual children of another control as its logical children. - /// - public interface IReparentingHost : ILogical - { - /// - /// Gets a writeable logical children collection from the host. - /// - new IPerspexList LogicalChildren { get; } - - /// - /// Asks the control whether it wants to reparent the logical children of the specified - /// control. - /// - /// The control. - /// - /// True if the control wants to reparent its logical children otherwise false. - /// - bool WillReparentChildrenOf(IControl control); - } -} \ No newline at end of file diff --git a/src/Perspex.Controls/ItemsControl.cs b/src/Perspex.Controls/ItemsControl.cs index 1fc232870e..017434bfac 100644 --- a/src/Perspex.Controls/ItemsControl.cs +++ b/src/Perspex.Controls/ItemsControl.cs @@ -3,7 +3,7 @@ using System; using System.Collections; -using System.Collections.ObjectModel; +using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -21,7 +21,7 @@ namespace Perspex.Controls /// /// Displays a collection of items. /// - public class ItemsControl : TemplatedControl, IReparentingHost + public class ItemsControl : TemplatedControl { /// /// The default value for the property. @@ -78,6 +78,12 @@ namespace Perspex.Controls if (_itemContainerGenerator == null) { _itemContainerGenerator = CreateItemContainerGenerator(); + + if (_itemContainerGenerator != null) + { + _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); + _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); + } } return _itemContainerGenerator; @@ -118,38 +124,130 @@ namespace Perspex.Controls public IItemsPresenter Presenter { get; - set; + protected set; } - /// - IPerspexList IReparentingHost.LogicalChildren => LogicalChildren; + /// + /// Gets the item at the specified index in a collection. + /// + /// The collection. + /// The index. + /// The index of the item or -1 if the item was not found. + protected static object ElementAt(IEnumerable items, int index) + { + var typedItems = items?.Cast(); + + if (index != -1 && typedItems != null && index < typedItems.Count()) + { + return typedItems.ElementAt(index) ?? null; + } + else + { + return null; + } + } /// - /// Asks the control whether it wants to reparent the logical children of the specified - /// control. + /// Gets the index of an item in a collection. /// - /// The control. - /// - /// True if the control wants to reparent its logical children otherwise false. - /// - bool IReparentingHost.WillReparentChildrenOf(IControl control) + /// The collection. + /// The item. + /// The index of the item or -1 if the item was not found. + protected static int IndexOf(IEnumerable items, object item) { - return control is IItemsPresenter && control.TemplatedParent == this; + if (items != null && item != null) + { + var list = items as IList; + + if (list != null) + { + return list.IndexOf(item); + } + else + { + int index = 0; + + foreach (var i in items) + { + if (Equals(i, item)) + { + return index; + } + + ++index; + } + } + } + + return -1; } /// /// Creates the for the control. /// - /// An . + /// + /// An or null. + /// + /// + /// Certain controls such as don't actually create item + /// containers; however they want it to be ItemsControls so that they have an Items + /// property etc. In this case, a derived class can override this method to return null + /// in order to disable the creation of item containers. + /// protected virtual IItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator(this); } + /// + /// Called when new containers are materialized for the by its + /// . + /// + /// The details of the containers. + protected virtual void OnContainersMaterialized(ItemContainerEventArgs e) + { + var toAdd = new List(); + + foreach (var container in e.Containers) + { + // If the item is its own container, then it will be added to the logical tree when + // it was added to the Items collection. + if (container.ContainerControl != container.Item) + { + toAdd.Add(container.ContainerControl); + } + } + + LogicalChildren.AddRange(toAdd); + } + + /// + /// Called when containers are dematerialized for the by its + /// . + /// + /// The details of the containers. + protected virtual void OnContainersDematerialized(ItemContainerEventArgs e) + { + var toRemove = new List(); + + foreach (var container in e.Containers) + { + // If the item is its own container, then it will be removed from the logical tree + // when it is removed from the Items collection. + if (container.ContainerControl != container.Item) + { + toRemove.Add(container.ContainerControl); + } + } + + LogicalChildren.RemoveAll(toRemove); + } + /// - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { - Presenter = nameScope.Find("PART_ItemsPresenter"); + base.OnTemplateApplied(e); + Presenter = e.NameScope.Find("PART_ItemsPresenter"); } /// @@ -176,7 +274,11 @@ namespace Perspex.Controls incc.CollectionChanged -= ItemsCollectionChanged; } + var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; + + RemoveControlItemsFromLogicalChildren(oldValue); + AddControlItemsToLogicalChildren(newValue); SubscribeToItems(newValue); } @@ -188,6 +290,17 @@ namespace Perspex.Controls /// The event args. protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + AddControlItemsToLogicalChildren(e.NewItems); + break; + + case NotifyCollectionChangedAction.Remove: + RemoveControlItemsFromLogicalChildren(e.OldItems); + break; + } + var collection = sender as ICollection; if (collection.Count == 0) @@ -200,6 +313,54 @@ namespace Perspex.Controls } } + /// + /// Given a collection of items, adds those that are controls to the logical children. + /// + /// The items. + private void AddControlItemsToLogicalChildren(IEnumerable items) + { + var toAdd = new List(); + + if (items != null) + { + foreach (var i in items) + { + var control = i as IControl; + + if (control != null && !LogicalChildren.Contains(control)) + { + toAdd.Add(control); + } + } + } + + LogicalChildren.AddRange(toAdd); + } + + /// + /// Given a collection of items, removes those that are controls to from logical children. + /// + /// The items. + private void RemoveControlItemsFromLogicalChildren(IEnumerable items) + { + var toRemove = new List(); + + if (items != null) + { + foreach (var i in items) + { + var control = i as IControl; + + if (control != null) + { + toRemove.Add(control); + } + } + } + + LogicalChildren.RemoveAll(toRemove); + } + /// /// Subscribes to an collection. /// diff --git a/src/Perspex.Controls/LogicalTreeAttachmentEventArgs.cs b/src/Perspex.Controls/LogicalTreeAttachmentEventArgs.cs new file mode 100644 index 0000000000..024942cf71 --- /dev/null +++ b/src/Perspex.Controls/LogicalTreeAttachmentEventArgs.cs @@ -0,0 +1,31 @@ +// 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 Perspex.Styling; + +namespace Perspex.Controls +{ + /// + /// Holds the event arguments for the and + /// events. + /// + public class LogicalTreeAttachmentEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The root of the logical tree. + public LogicalTreeAttachmentEventArgs(IStyleHost root) + { + Contract.Requires(root != null); + + Root = root; + } + + /// + /// Gets the root of the logical tree that the control is being attached to or detached from. + /// + public IStyleHost Root { get; } + } +} diff --git a/src/Perspex.Controls/MenuItem.cs b/src/Perspex.Controls/MenuItem.cs index 7207bfa808..1a95b48a41 100644 --- a/src/Perspex.Controls/MenuItem.cs +++ b/src/Perspex.Controls/MenuItem.cs @@ -20,7 +20,7 @@ namespace Perspex.Controls /// /// A menu item control. /// - public class MenuItem : SelectingItemsControl, ISelectable + public class MenuItem : HeaderedSelectingItemsControl, ISelectable { /// /// Defines the property. @@ -40,12 +40,6 @@ namespace Perspex.Controls public static readonly PerspexProperty CommandParameterProperty = Button.CommandParameterProperty.AddOwner(); - /// - /// Defines the property. - /// - public static readonly PerspexProperty HeaderProperty = - HeaderedItemsControl.HeaderProperty.AddOwner(); - /// /// Defines the property. /// @@ -136,7 +130,6 @@ namespace Perspex.Controls set { SetValue(CommandProperty, value); } } - /// /// Gets or sets an associated with this control /// @@ -156,15 +149,6 @@ namespace Perspex.Controls set { SetValue(CommandParameterProperty, value); } } - /// - /// Gets or sets the 's header. - /// - public object Header - { - get { return GetValue(HeaderProperty); } - set { SetValue(HeaderProperty, value); } - } - /// /// Gets or sets the icon that appears in a . /// @@ -370,14 +354,12 @@ namespace Perspex.Controls } } - /// - /// Called when the MenuItem's template has been applied. - /// - protected override void OnTemplateApplied(INameScope nameScope) + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(nameScope); + base.OnTemplateApplied(e); - _popup = nameScope.Get("PART_Popup"); + _popup = e.NameScope.Get("PART_Popup"); _popup.DependencyResolver = DependencyResolver.Instance; _popup.PopupRootCreated += PopupRootCreated; _popup.Opened += PopupOpened; diff --git a/src/Perspex.Controls/Mixins/ContentControlMixin.cs b/src/Perspex.Controls/Mixins/ContentControlMixin.cs new file mode 100644 index 0000000000..65153ed0af --- /dev/null +++ b/src/Perspex.Controls/Mixins/ContentControlMixin.cs @@ -0,0 +1,136 @@ +// 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 System.Runtime.CompilerServices; +using Perspex.Collections; +using Perspex.Controls.Presenters; +using Perspex.Controls.Primitives; +using Perspex.Interactivity; + +namespace Perspex.Controls.Mixins +{ + /// + /// Adds content control functionality to control classes. + /// + /// + /// The adds behavior to a control which acts as a content + /// control such as and . It + /// updates keeps the control's logical children in sync with the content being displayed by + /// the control. + /// + public class ContentControlMixin + { + private static Lazy> subscriptions = + new Lazy>(() => + new ConditionalWeakTable()); + + /// + /// Initializes a new instance of the class. + /// + /// The control type. + /// The content property. + /// + /// Given an control of should return the control's + /// logical children collection. + /// + /// + /// The name of the content presenter in the control's template. + /// + public static void Attach( + PerspexProperty content, + Func> logicalChildrenSelector, + string presenterName = "PART_ContentPresenter") + where TControl : TemplatedControl + { + Contract.Requires(content != null); + Contract.Requires(logicalChildrenSelector != null); + + EventHandler templateApplied = (s, ev) => + { + var sender = s as TControl; + + if (sender != null) + { + var e = (TemplateAppliedEventArgs)ev; + var presenter = (IControl)e.NameScope.Find(presenterName); + + if (presenter != null) + { + var logicalChildren = logicalChildrenSelector(sender); + var subscription = presenter + .GetObservable(ContentPresenter.ChildProperty) + .Subscribe(child => UpdateLogicalChild( + logicalChildren, + logicalChildren.FirstOrDefault(), + child)); + subscriptions.Value.Add(sender, subscription); + } + } + }; + + TemplatedControl.TemplateAppliedEvent.AddClassHandler( + typeof(TControl), + templateApplied, + RoutingStrategies.Direct); + + content.Changed.Subscribe(e => + { + var sender = e.Sender as TControl; + + if (sender != null) + { + var logicalChildren = logicalChildrenSelector(sender); + UpdateLogicalChild(logicalChildren, e.OldValue, e.NewValue); + } + }); + + TemplatedControl.TemplateProperty.Changed.Subscribe(e => + { + var sender = e.Sender as TControl; + + if (sender != null) + { + IDisposable subscription; + + if (subscriptions.Value.TryGetValue(sender, out subscription)) + { + subscription.Dispose(); + subscriptions.Value.Remove(sender); + } + } + }); + } + + private static event EventHandler TemplateApplied; + + private static void OnTemplateApplied(object sender, RoutedEventArgs e) + { + TemplateApplied?.Invoke(sender, (TemplateAppliedEventArgs)e); + } + + private static void UpdateLogicalChild( + IPerspexList logicalChildren, + object oldValue, + object newValue) + { + if (oldValue != newValue) + { + var logical = oldValue as ILogical; + + if (logical != null) + { + logicalChildren.Remove(logical); + } + + logical = newValue as ILogical; + + if (logical != null) + { + logicalChildren.Add(logical); + } + } + } + } +} diff --git a/src/Perspex.Controls/Panel.cs b/src/Perspex.Controls/Panel.cs index 9adda8bb16..edb110f22d 100644 --- a/src/Perspex.Controls/Panel.cs +++ b/src/Perspex.Controls/Panel.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; -using Perspex.Collections; using Perspex.Media; using Perspex.Metadata; @@ -18,7 +17,7 @@ namespace Perspex.Controls /// Controls can be added to a by adding them to its /// collection. All children are layed out to fill the panel. /// - public class Panel : Control, IReparentingControl, IPanel + public class Panel : Control, IPanel { /// /// Defines the property. @@ -68,7 +67,7 @@ namespace Perspex.Controls { Contract.Requires(value != null); - ClearVisualChildren(); + VisualChildren.Clear(); _children.Clear(); _children.AddRange(value); } @@ -83,58 +82,6 @@ namespace Perspex.Controls set { SetValue(BackgroundProperty, value); } } - /// - /// Requests that the visual children of the panel use another control as their logical - /// parent. - /// - /// - /// The logical parent for the visual children of the panel. - /// - /// - /// The collection to modify. - /// - void IReparentingControl.ReparentLogicalChildren(ILogical logicalParent, IPerspexList children) - { - Contract.Requires(logicalParent != null); - Contract.Requires(children != null); - - _childLogicalParent = logicalParent; - RedirectLogicalChildren(children); - - foreach (var control in Children) - { - ((ISetLogicalParent)control).SetParent(null); - ((ISetLogicalParent)control).SetParent((IControl)logicalParent); - children.Add(control); - } - } - - /// - /// Clears for the specified controls. - /// - /// The controls. - private void ClearLogicalParent(IEnumerable controls) - { - foreach (var control in controls) - { - ((ISetLogicalParent)control).SetParent(null); - } - } - - /// - /// Sets for the specified controls. - /// - /// The controls. - private void SetLogicalParent(IEnumerable controls) - { - var parent = _childLogicalParent as Control; - - foreach (var control in controls) - { - ((ISetLogicalParent)control).SetParent(parent); - } - } - /// /// Called when the collection changes. /// @@ -144,29 +91,35 @@ namespace Perspex.Controls { List controls; - // TODO: Handle Replace. switch (e.Action) { case NotifyCollectionChangedAction.Add: controls = e.NewItems.OfType().ToList(); - SetLogicalParent(controls); - AddVisualChildren(e.NewItems.OfType()); LogicalChildren.InsertRange(e.NewStartingIndex, controls); + VisualChildren.AddRange(e.NewItems.OfType()); break; case NotifyCollectionChangedAction.Remove: controls = e.OldItems.OfType().ToList(); - ClearLogicalParent(e.OldItems.OfType()); LogicalChildren.RemoveAll(controls); - RemoveVisualChildren(e.OldItems.OfType()); + VisualChildren.RemoveAll(e.OldItems.OfType()); + break; + + case NotifyCollectionChangedAction.Replace: + for (var i = 0; i < e.OldItems.Count; ++i) + { + var index = i + e.OldStartingIndex; + var child = (IControl)e.NewItems[i]; + LogicalChildren[index] = child; + VisualChildren[index] = child; + } break; case NotifyCollectionChangedAction.Reset: controls = e.OldItems.OfType().ToList(); - ClearLogicalParent(controls); LogicalChildren.Clear(); - ClearVisualChildren(); - AddVisualChildren(_children); + VisualChildren.Clear(); + VisualChildren.AddRange(_children); break; } diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index 31b23702a4..10215b7e87 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -44,23 +44,27 @@ + + + + + - + - @@ -72,7 +76,6 @@ - @@ -150,6 +153,7 @@ + diff --git a/src/Perspex.Controls/Presenters/CarouselPresenter.cs b/src/Perspex.Controls/Presenters/CarouselPresenter.cs index e130936d29..cee9a053db 100644 --- a/src/Perspex.Controls/Presenters/CarouselPresenter.cs +++ b/src/Perspex.Controls/Presenters/CarouselPresenter.cs @@ -162,20 +162,13 @@ namespace Perspex.Controls.Presenters /// private void CreatePanel() { - var logicalHost = this.FindReparentingHost(); - - ClearVisualChildren(); Panel = ItemsPanel.Build(); Panel.SetValue(TemplatedParentProperty, TemplatedParent); - AddVisualChild(Panel); - - if (logicalHost != null) - { - ((IReparentingControl)Panel).ReparentLogicalChildren( - logicalHost, - logicalHost.LogicalChildren); - } + LogicalChildren.Clear(); + VisualChildren.Clear(); + LogicalChildren.Add(Panel); + VisualChildren.Add(Panel); _createdPanel = true; var task = MoveToPage(-1, SelectedIndex); @@ -204,7 +197,8 @@ namespace Perspex.Controls.Presenters { var item = Items.Cast().ElementAt(toIndex); to = generator.ContainerFromIndex(toIndex) ?? - generator.Materialize(toIndex, new[] { item }, MemberSelector).FirstOrDefault(); + generator.Materialize(toIndex, new[] { item }, MemberSelector) + .FirstOrDefault()?.ContainerControl; if (to != null) { diff --git a/src/Perspex.Controls/Presenters/ContentPresenter.cs b/src/Perspex.Controls/Presenters/ContentPresenter.cs index 46a57dd613..0faa7fd783 100644 --- a/src/Perspex.Controls/Presenters/ContentPresenter.cs +++ b/src/Perspex.Controls/Presenters/ContentPresenter.cs @@ -63,6 +63,14 @@ namespace Perspex.Controls.Presenters } } + /// + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + _createdChild = false; + InvalidateMeasure(); + } + /// protected override Size MeasureCore(Size availableSize) { @@ -101,18 +109,12 @@ namespace Perspex.Controls.Presenters var old = Child; var content = Content; var result = this.MaterializeDataTemplate(content); - var logicalHost = this.FindReparentingHost(); - var logicalChildren = logicalHost?.LogicalChildren ?? LogicalChildren; if (old != null) { - ((ISetLogicalParent)old).SetParent(null); - logicalChildren.Remove(old); - ClearVisualChildren(); + VisualChildren.Remove(old); } - Child = result; - if (result != null) { if (!(content is IControl)) @@ -120,14 +122,12 @@ namespace Perspex.Controls.Presenters result.DataContext = content; } - if (result.Parent == null) - { - ((ISetLogicalParent)result).SetParent((ILogical)logicalHost ?? this); - } - - AddVisualChild(result); - logicalChildren.Remove(old); - logicalChildren.Add(result); + Child = result; + VisualChildren.Add(result); + } + else + { + Child = null; } _createdChild = true; diff --git a/src/Perspex.Controls/Presenters/ItemsPresenter.cs b/src/Perspex.Controls/Presenters/ItemsPresenter.cs index ecd74e7bdb..6f052af7f7 100644 --- a/src/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/src/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Perspex.Controls.Generators; using Perspex.Controls.Templates; using Perspex.Input; @@ -150,7 +151,6 @@ namespace Perspex.Controls.Presenters /// private void CreatePanel() { - ClearVisualChildren(); Panel = ItemsPanel.Build(); Panel.SetValue(TemplatedParentProperty, TemplatedParent); @@ -161,16 +161,10 @@ namespace Perspex.Controls.Presenters KeyboardNavigationMode.Contained); } - AddVisualChild(Panel); - - var logicalHost = this.FindReparentingHost(); - - if (logicalHost != null) - { - ((IReparentingControl)Panel).ReparentLogicalChildren( - logicalHost, - logicalHost.LogicalChildren); - } + LogicalChildren.Clear(); + VisualChildren.Clear(); + LogicalChildren.Add(Panel); + VisualChildren.Add(Panel); KeyboardNavigation.SetTabNavigation( (InputElement)Panel, @@ -187,7 +181,7 @@ namespace Perspex.Controls.Presenters { if (items != null) { - Panel.Children.AddRange(ItemContainerGenerator.Materialize(0, Items, MemberSelector)); + AddContainers(ItemContainerGenerator.Materialize(0, Items, MemberSelector)); INotifyCollectionChanged incc = items as INotifyCollectionChanged; @@ -238,30 +232,28 @@ namespace Perspex.Controls.Presenters if (_createdPanel) { var generator = ItemContainerGenerator; - IEnumerable containers; // TODO: Handle Move and Replace etc. switch (e.Action) { case NotifyCollectionChangedAction.Add: - containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector); - Panel.Children.AddRange(containers); + AddContainers(generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector)); break; case NotifyCollectionChangedAction.Remove: - containers = generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count); - Panel.Children.RemoveAll(containers); + RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count)); break; case NotifyCollectionChangedAction.Replace: - generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count); - containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector); + RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count)); + var containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector); + AddContainers(containers); var i = e.NewStartingIndex; foreach (var container in containers) { - Panel.Children[i++] = container; + Panel.Children[i++] = container.ContainerControl; } break; @@ -269,13 +261,35 @@ namespace Perspex.Controls.Presenters 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)); + RemoveContainers(generator.Clear()); + AddContainers(generator.Materialize(0, Items, MemberSelector)); break; } InvalidateMeasure(); } } + + private void AddContainers(IEnumerable items) + { + foreach (var i in items) + { + if (i.ContainerControl != null) + { + this.Panel.Children.Add(i.ContainerControl); + } + } + } + + private void RemoveContainers(IEnumerable items) + { + foreach (var i in items) + { + if (i.ContainerControl != null) + { + this.Panel.Children.Remove(i.ContainerControl); + } + } + } } } diff --git a/src/Perspex.Controls/Primitives/AdornerDecorator.cs b/src/Perspex.Controls/Primitives/AdornerDecorator.cs index 6fa8ec81b3..218e7b8360 100644 --- a/src/Perspex.Controls/Primitives/AdornerDecorator.cs +++ b/src/Perspex.Controls/Primitives/AdornerDecorator.cs @@ -10,7 +10,7 @@ namespace Perspex.Controls.Primitives AdornerLayer = new AdornerLayer(); ((ISetLogicalParent)AdornerLayer).SetParent(this); AdornerLayer.ZIndex = int.MaxValue; - AddVisualChild(AdornerLayer); + VisualChildren.Add(AdornerLayer); } public AdornerLayer AdornerLayer diff --git a/src/Perspex.Controls/Primitives/HeaderedItemsControl.cs b/src/Perspex.Controls/Primitives/HeaderedItemsControl.cs index cf58646d4b..5f543cae17 100644 --- a/src/Perspex.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Perspex.Controls/Primitives/HeaderedItemsControl.cs @@ -1,17 +1,58 @@ // 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.Presenters; + namespace Perspex.Controls.Primitives { + /// + /// Represents an with a related header. + /// public class HeaderedItemsControl : ItemsControl { + /// + /// Defines the property. + /// public static readonly PerspexProperty HeaderProperty = HeaderedContentControl.HeaderProperty.AddOwner(); + /// + /// Initializes static members of the class. + /// + static HeaderedItemsControl() + { + ContentControlMixin.Attach( + HeaderProperty, + x => x.LogicalChildren, + "PART_HeaderPresenter"); + } + + /// + /// Gets or sets the content of the control's header. + /// public object Header { get { return GetValue(HeaderProperty); } set { SetValue(HeaderProperty, value); } } + + /// + /// Gets the header presenter from the control's template. + /// + public ContentPresenter HeaderPresenter + { + get; + private set; + } + + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + HeaderPresenter = e.NameScope.Find("PART_HeaderPresenter"); + } } } diff --git a/src/Perspex.Controls/Primitives/HeaderedSelectingControl.cs b/src/Perspex.Controls/Primitives/HeaderedSelectingControl.cs new file mode 100644 index 0000000000..9a6431c3e5 --- /dev/null +++ b/src/Perspex.Controls/Primitives/HeaderedSelectingControl.cs @@ -0,0 +1,58 @@ +// 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.Presenters; + +namespace Perspex.Controls.Primitives +{ + /// + /// Represents a with a related header. + /// + public class HeaderedSelectingItemsControl : SelectingItemsControl + { + /// + /// Defines the property. + /// + public static readonly PerspexProperty HeaderProperty = + HeaderedContentControl.HeaderProperty.AddOwner(); + + /// + /// Initializes static members of the class. + /// + static HeaderedSelectingItemsControl() + { + ContentControlMixin.Attach( + HeaderProperty, + x => x.LogicalChildren, + "PART_HeaderPresenter"); + } + + /// + /// Gets or sets the content of the control's header. + /// + public object Header + { + get { return GetValue(HeaderProperty); } + set { SetValue(HeaderProperty, value); } + } + + /// + /// Gets the header presenter from the control's template. + /// + public ContentPresenter HeaderPresenter + { + get; + private set; + } + + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + HeaderPresenter = e.NameScope.Find("PART_HeaderPresenter"); + } + } +} diff --git a/src/Perspex.Controls/Primitives/PopupRoot.cs b/src/Perspex.Controls/Primitives/PopupRoot.cs index b69af20c3f..8b659d6e91 100644 --- a/src/Perspex.Controls/Primitives/PopupRoot.cs +++ b/src/Perspex.Controls/Primitives/PopupRoot.cs @@ -44,7 +44,6 @@ namespace Perspex.Controls.Primitives public PopupRoot(IPerspexDependencyResolver dependencyResolver) : base(PlatformManager.CreatePopup(), dependencyResolver) { - GetObservable(ParentProperty).Subscribe(x => InheritanceParent = (PerspexObject)x); } /// @@ -94,9 +93,9 @@ namespace Perspex.Controls.Primitives } /// - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(nameScope); + base.OnTemplateApplied(e); if (Parent.TemplatedParent != null) { diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index df9f5b08d2..999766f8af 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -98,7 +98,6 @@ namespace Perspex.Controls.Primitives /// public SelectingItemsControl() { - ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized); } /// @@ -286,6 +285,27 @@ namespace Perspex.Controls.Primitives } } + /// + protected override void OnContainersMaterialized(ItemContainerEventArgs e) + { + base.OnContainersMaterialized(e); + + var selectedIndex = SelectedIndex; + var selectedContainer = e.Containers + .FirstOrDefault(x => (x.ContainerControl as ISelectable)?.IsSelected == true); + + if (selectedContainer != null) + { + SelectedIndex = selectedContainer.Index; + } + else if (selectedIndex >= e.StartingIndex && + selectedIndex < e.StartingIndex + e.Containers.Count) + { + var container = e.Containers[selectedIndex - e.StartingIndex]; + MarkContainerSelected(container.ContainerControl, true); + } + } + /// protected override void OnDataContextChanged() { @@ -379,7 +399,7 @@ namespace Perspex.Controls.Primitives bool rangeModifier = false, bool toggleModifier = false) { - var index = ItemContainerGenerator.IndexFromContainer(container); + var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1; if (index != -1) { @@ -416,61 +436,6 @@ namespace Perspex.Controls.Primitives return false; } - /// - /// Gets the item at the specified index in a collection. - /// - /// The collection. - /// The index. - /// The index of the item or -1 if the item was not found. - private static object ElementAt(IEnumerable items, int index) - { - var typedItems = items?.Cast(); - - if (index != -1 && typedItems != null && index < typedItems.Count()) - { - return typedItems.ElementAt(index) ?? null; - } - else - { - return null; - } - } - - /// - /// Gets the index of an item in a collection. - /// - /// The collection. - /// The item. - /// The index of the item or -1 if the item was not found. - private static int IndexOf(IEnumerable items, object item) - { - if (items != null && item != null) - { - var list = items as IList; - - if (list != null) - { - return list.IndexOf(item); - } - else - { - int index = 0; - - foreach (var i in items) - { - if (Equals(i, item)) - { - return index; - } - - ++index; - } - } - } - - return -1; - } - /// /// Gets a range of items from an IEnumerable. /// @@ -523,27 +488,6 @@ namespace Perspex.Controls.Primitives } } - /// - /// Called when new containers are initialized by the . - /// - /// The containers. - private void ContainersInitialized(ItemContainers containers) - { - var selectedIndex = SelectedIndex; - var selectedContainer = containers.Items.OfType().FirstOrDefault(x => x.IsSelected); - - if (selectedContainer != null) - { - SelectedIndex = containers.Items.IndexOf((IControl)selectedContainer) + containers.StartingIndex; - } - else if (selectedIndex >= containers.StartingIndex && - selectedIndex < containers.StartingIndex + containers.Items.Count) - { - var container = containers.Items[selectedIndex - containers.StartingIndex]; - MarkContainerSelected(container, true); - } - } - /// /// Called when a container raises the . /// @@ -625,7 +569,7 @@ namespace Perspex.Controls.Primitives /// Whether the item should be selected or deselected. private void MarkItemSelected(int index, bool selected) { - var container = ItemContainerGenerator.ContainerFromIndex(index); + var container = ItemContainerGenerator?.ContainerFromIndex(index); if (container != null) { @@ -655,6 +599,8 @@ namespace Perspex.Controls.Primitives /// The event args. private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { + var generator = ItemContainerGenerator; + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -680,11 +626,14 @@ namespace Perspex.Controls.Primitives break; case NotifyCollectionChangedAction.Reset: - foreach (var item in ItemContainerGenerator.Containers) + if (generator != null) { - if (item != null) + foreach (var item in generator.Containers) { - MarkContainerSelected(item, false); + if (item != null) + { + MarkContainerSelected(item.ContainerControl, false); + } } } diff --git a/src/Perspex.Controls/Primitives/TabStrip.cs b/src/Perspex.Controls/Primitives/TabStrip.cs index 5fc305c73a..1cfb4e0515 100644 --- a/src/Perspex.Controls/Primitives/TabStrip.cs +++ b/src/Perspex.Controls/Primitives/TabStrip.cs @@ -1,52 +1,26 @@ // 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 System.Reactive.Linq; using Perspex.Controls.Generators; +using Perspex.Controls.Templates; using Perspex.Input; namespace Perspex.Controls.Primitives { public class TabStrip : SelectingItemsControl { - public static readonly PerspexProperty SelectedTabProperty = - TabControl.SelectedTabProperty.AddOwner(); + private static IMemberSelector s_MemberSelector = new FuncMemberSelector(SelectHeader); static TabStrip() { + MemberSelectorProperty.OverrideDefaultValue(s_MemberSelector); SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false); } - public TabStrip() - { - GetObservable(SelectedItemProperty).Subscribe(x => SelectedTab = x as TabItem); - GetObservable(SelectedTabProperty).Subscribe(x => SelectedItem = x as TabItem); - } - - public TabItem SelectedTab - { - get { return GetValue(SelectedTabProperty); } - set { SetValue(SelectedTabProperty, value); } - } - protected override IItemContainerGenerator CreateItemContainerGenerator() { - TabControl tabControl = TemplatedParent as TabControl; - IItemContainerGenerator result; - - if (tabControl != null) - { - result = tabControl.ItemContainerGenerator; - } - else - { - result = new ItemContainerGenerator(this, TabItem.ContentProperty); - } - - return result; + return new ItemContainerGenerator(this, ContentControl.ContentProperty); } /// @@ -70,5 +44,11 @@ namespace Perspex.Controls.Primitives e.Handled = UpdateSelectionFromEventSource(e.Source); } } + + private static object SelectHeader(object o) + { + var headered = o as IHeadered; + return (headered != null) ? (headered.Header ?? string.Empty) : o; + } } } diff --git a/src/Perspex.Controls/Primitives/TabStripItem.cs b/src/Perspex.Controls/Primitives/TabStripItem.cs new file mode 100644 index 0000000000..5e6787c167 --- /dev/null +++ b/src/Perspex.Controls/Primitives/TabStripItem.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Perspex.Controls.Primitives +{ + /// + /// Represents a tab in a . + /// + public class TabStripItem : ListBoxItem + { + } +} diff --git a/src/Perspex.Controls/Primitives/TemplateAppliedEventArgs.cs b/src/Perspex.Controls/Primitives/TemplateAppliedEventArgs.cs new file mode 100644 index 0000000000..e921f40785 --- /dev/null +++ b/src/Perspex.Controls/Primitives/TemplateAppliedEventArgs.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 Perspex.Interactivity; + +namespace Perspex.Controls.Primitives +{ + /// + /// Holds the details of the event. + /// + public class TemplateAppliedEventArgs : RoutedEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The applied template's name scope. + public TemplateAppliedEventArgs(INameScope nameScope) + : base(TemplatedControl.TemplateAppliedEvent) + { + NameScope = nameScope; + } + + /// + /// Gets the name scope of the applied template. + /// + public INameScope NameScope { get; } + } +} diff --git a/src/Perspex.Controls/Primitives/TemplatedControl.cs b/src/Perspex.Controls/Primitives/TemplatedControl.cs index 2955c733b8..87e2f03eeb 100644 --- a/src/Perspex.Controls/Primitives/TemplatedControl.cs +++ b/src/Perspex.Controls/Primitives/TemplatedControl.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reactive.Linq; using Perspex.Controls.Presenters; using Perspex.Controls.Templates; +using Perspex.Interactivity; using Perspex.Media; using Perspex.Styling; using Perspex.VisualTree; @@ -73,6 +74,14 @@ namespace Perspex.Controls.Primitives public static readonly PerspexProperty TemplateProperty = PerspexProperty.Register("Template"); + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent TemplateAppliedEvent = + RoutedEvent.Register( + "TemplateApplied", + RoutingStrategies.Direct); + private bool _templateApplied; private readonly ILogger _templateLog; @@ -98,6 +107,15 @@ namespace Perspex.Controls.Primitives }); } + /// + /// Raised when the control's template is applied. + /// + public event EventHandler TemplateApplied + { + add { AddHandler(TemplateAppliedEvent, value); } + remove { RemoveHandler(TemplateAppliedEvent, value); } + } + /// /// Gets or sets the brush used to draw the control's background. /// @@ -184,7 +202,7 @@ namespace Perspex.Controls.Primitives { if (!_templateApplied) { - ClearVisualChildren(); + VisualChildren.Clear(); if (Template != null) { @@ -194,18 +212,21 @@ namespace Perspex.Controls.Primitives var nameScope = new NameScope(); NameScope.SetNameScope((Control)child, nameScope); - // We need to call SetTemplatedParentAndApplyChildTemplates twice - once - // before the controls are added to the visual tree so that the logical - // tree can be set up before styling is applied. - ((ISetLogicalParent)child).SetParent(this); + // We need to call SetupTemplateControls twice: + // - Once before the controls are added to the visual/logical trees so that the + // TemplatedParent property is set and names are registered; if + // TemplatedParent is not set when the control is added to the logical tree, + // then styles with the /template/ selector won't match. + // - Once after the controls are added to the logical tree (and thus styled) to + // call ApplyTemplate on nested templated controls and register any of our + // templated children that appear as children of presenters in these nested + // templated child controls. SetupTemplateControls(child, nameScope); - - // And again after the controls are added to the visual tree, and have their - // styling and thus Template property set. - AddVisualChild((Visual)child); + VisualChildren.Add(child); + ((ISetLogicalParent)child).SetParent(this); SetupTemplateControls(child, nameScope); - OnTemplateApplied(nameScope); + OnTemplateApplied(new TemplateAppliedEventArgs(nameScope)); } _templateApplied = true; @@ -231,9 +252,10 @@ namespace Perspex.Controls.Primitives /// /// Called when the control's template is applied. /// - /// The template name scope. - protected virtual void OnTemplateApplied(INameScope nameScope) + /// The event args. + protected virtual void OnTemplateApplied(TemplateAppliedEventArgs e) { + RaiseEvent(e); } /// diff --git a/src/Perspex.Controls/Primitives/Track.cs b/src/Perspex.Controls/Primitives/Track.cs index ae9a8d1c4a..4e343b65b3 100644 --- a/src/Perspex.Controls/Primitives/Track.cs +++ b/src/Perspex.Controls/Primitives/Track.cs @@ -45,12 +45,12 @@ namespace Perspex.Controls.Primitives val.Item1.DragDelta -= ThumbDragged; } - ClearVisualChildren(); + VisualChildren.Clear(); if (val.Item2 != null) { val.Item2.DragDelta += ThumbDragged; - AddVisualChild(val.Item2); + VisualChildren.Add(val.Item2); } }); } diff --git a/src/Perspex.Controls/ProgressBar.cs b/src/Perspex.Controls/ProgressBar.cs index 4bc1fdb9d2..0cb512d50e 100644 --- a/src/Perspex.Controls/ProgressBar.cs +++ b/src/Perspex.Controls/ProgressBar.cs @@ -26,9 +26,9 @@ namespace Perspex.Controls } /// - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { - _indicator = nameScope.Get("PART_Indicator"); + _indicator = e.NameScope.Get("PART_Indicator"); UpdateIndicator(Bounds.Size); } diff --git a/src/Perspex.Controls/TabControl.cs b/src/Perspex.Controls/TabControl.cs index bff505497f..86ca837a4e 100644 --- a/src/Perspex.Controls/TabControl.cs +++ b/src/Perspex.Controls/TabControl.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Perspex.Animation; -using Perspex.Controls.Presenters; +using Perspex.Controls.Generators; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; @@ -11,23 +11,26 @@ namespace Perspex.Controls /// /// A tab control that displays a tab strip along with the content of the selected tab. /// - public class TabControl : SelectingItemsControl, IReparentingHost + public class TabControl : SelectingItemsControl { - /// - /// Defines the property. - /// - public static readonly PerspexProperty SelectedTabProperty = - PerspexProperty.Register("SelectedTab"); - /// /// Defines the property. /// public static readonly PerspexProperty TransitionProperty = - Carousel.TransitionProperty.AddOwner(); + Perspex.Controls.Carousel.TransitionProperty.AddOwner(); - private static readonly IMemberSelector s_contentSelector = + /// + /// Defines an that selects the content of a . + /// + public static readonly IMemberSelector ContentSelector = new FuncMemberSelector(SelectContent); + /// + /// Defines an that selects the header of a . + /// + public static readonly IMemberSelector HeaderSelector = + new FuncMemberSelector(SelectHeader); + /// /// Defines the property. /// @@ -41,22 +44,25 @@ namespace Perspex.Controls { SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(false); - SelectedItemProperty.Changed.AddClassHandler(x => x.SelectedItemChanged); AffectsMeasure(TabStripPlacementProperty); } /// - /// Gets an that selects the content of a . + /// Gets the pages portion of the 's template. /// - public IMemberSelector ContentSelector => s_contentSelector; + public IControl Pages + { + get; + private set; + } /// - /// Gets the as a . + /// Gets the tab strip portion of the 's template. /// - public TabItem SelectedTab + public IControl TabStrip { - get { return GetValue(SelectedTabProperty); } - private set { SetValue(SelectedTabProperty, value); } + get; + private set; } /// @@ -77,17 +83,21 @@ namespace Perspex.Controls set { SetValue(TabStripPlacementProperty, value); } } - /// - /// Asks the control whether it wants to reparent the logical children of the specified - /// control. - /// - /// The control. - /// - /// True if the control wants to reparent its logical children otherwise false. - /// - bool IReparentingHost.WillReparentChildrenOf(IControl control) + protected override IItemContainerGenerator CreateItemContainerGenerator() { - return control is CarouselPresenter; + // TabControl doesn't actually create items - instead its TabStrip and Carousel + // children create the items. However we want it to be a SelectingItemsControl + // so that it has the Items/SelectedItem etc properties. In this case, we can + // return a null ItemContainerGenerator to disable the creation of item containers. + return null; + } + + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + + TabStrip = e.NameScope.Find("PART_TabStrip"); + Pages = e.NameScope.Find("PART_Content"); } /// @@ -110,14 +120,31 @@ namespace Perspex.Controls } /// - /// Called when the property changes. + /// Selects the header of a tab item. /// - /// The event args. - private void SelectedItemChanged(PerspexPropertyChangedEventArgs e) + /// The tab item. + /// The content. + private static object SelectHeader(object o) { - var item = e.NewValue as IContentControl; - var content = item?.Content ?? item; - SelectedTab = item as TabItem; + var headered = o as IHeadered; + var control = o as IControl; + + if (headered != null) + { + return headered.Header ?? string.Empty; + } + else if (control != null) + { + // Non-headered control items should result in TabStripItems with empty content. + // If a TabStrip is created with non IHeadered controls as its items, don't try to + // display the control in the TabStripItem: the content portion will also try to + // display this control, resulting in dual-parentage breakage. + return string.Empty; + } + else + { + return o; + } } } } diff --git a/src/Perspex.Controls/Templates/TemplateExtensions.cs b/src/Perspex.Controls/Templates/TemplateExtensions.cs index b8cd9e25c2..40672f873e 100644 --- a/src/Perspex.Controls/Templates/TemplateExtensions.cs +++ b/src/Perspex.Controls/Templates/TemplateExtensions.cs @@ -12,35 +12,6 @@ namespace Perspex.Controls.Templates { public static class TemplateExtensions { - public static IReparentingHost FindReparentingHost(this IControl control) - { - var tp = control.TemplatedParent; - var chain = new List(); - - while (tp != null) - { - var reparentingHost = tp as IReparentingHost; - var styleable = tp as IStyleable; - - if (reparentingHost != null) - { - chain.Add(reparentingHost); - } - - tp = styleable?.TemplatedParent ?? null; - } - - foreach (var reparenting in chain.AsEnumerable().Reverse()) - { - if (reparenting.WillReparentChildrenOf(control)) - { - return reparenting; - } - } - - return null; - } - public static IEnumerable GetTemplateChildren(this ITemplatedControl control) { var visual = control as IVisual; diff --git a/src/Perspex.Controls/TextBlock.cs b/src/Perspex.Controls/TextBlock.cs index 1442bae1f2..ea749be84e 100644 --- a/src/Perspex.Controls/TextBlock.cs +++ b/src/Perspex.Controls/TextBlock.cs @@ -26,6 +26,7 @@ namespace Perspex.Controls public static readonly PerspexProperty FontFamilyProperty = PerspexProperty.RegisterAttached( nameof(FontFamily), + defaultValue: "Courier New", inherits: true); /// @@ -34,6 +35,7 @@ namespace Perspex.Controls public static readonly PerspexProperty FontSizeProperty = PerspexProperty.RegisterAttached( nameof(FontSize), + defaultValue: 12, inherits: true); /// @@ -343,8 +345,8 @@ namespace Perspex.Controls { var result = new FormattedText( Text ?? string.Empty, - FontFamily ?? "Arial", - FontSize > 0 ? FontSize : 12, + FontFamily, + FontSize, FontStyle, TextAlignment, FontWeight); diff --git a/src/Perspex.Controls/TextBox.cs b/src/Perspex.Controls/TextBox.cs index 7eddecf0f7..0bfc8d2eeb 100644 --- a/src/Perspex.Controls/TextBox.cs +++ b/src/Perspex.Controls/TextBox.cs @@ -157,9 +157,9 @@ namespace Perspex.Controls set { SetValue(TextWrappingProperty, value); } } - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { - _presenter = nameScope.Get("PART_TextPresenter"); + _presenter = e.NameScope.Get("PART_TextPresenter"); _presenter.Cursor = new Cursor(StandardCursorType.Ibeam); } diff --git a/src/Perspex.Controls/TopLevel.cs b/src/Perspex.Controls/TopLevel.cs index ca84f67de1..2f3799751c 100644 --- a/src/Perspex.Controls/TopLevel.cs +++ b/src/Perspex.Controls/TopLevel.cs @@ -24,7 +24,7 @@ namespace Perspex.Controls /// . It handles scheduling layout, styling and rendering as well as /// tracking the window and state. /// - public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable + public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable, IStyleRoot { /// /// Defines the property. @@ -204,6 +204,11 @@ namespace Perspex.Controls set { SetValue(AccessText.ShowAccessKeyProperty, value); } } + IStyleHost IStyleHost.StylingParent + { + get { return PerspexLocator.Current.GetService(); } + } + /// /// Whether an auto-size operation is in progress. /// diff --git a/src/Perspex.SceneGraph/Visual.cs b/src/Perspex.SceneGraph/Visual.cs index 597528d0a7..e8fa579960 100644 --- a/src/Perspex.SceneGraph/Visual.cs +++ b/src/Perspex.SceneGraph/Visual.cs @@ -81,11 +81,6 @@ namespace Perspex /// private string _name; - /// - /// Holds the children of the visual. - /// - private readonly PerspexList _visualChildren; - /// /// The visual's bounds relative to its parent. /// @@ -128,9 +123,10 @@ namespace Perspex new PropertyEnricher("Id", GetHashCode()), }); - _visualChildren = new PerspexList(); - _visualChildren.ResetBehavior = ResetBehavior.Remove; - _visualChildren.CollectionChanged += VisualChildrenChanged; + var visualChildren = new PerspexList(); + visualChildren.ResetBehavior = ResetBehavior.Remove; + visualChildren.CollectionChanged += VisualChildrenChanged; + VisualChildren = visualChildren; } /// @@ -249,6 +245,15 @@ namespace Perspex set { SetValue(ZIndexProperty, value); } } + /// + /// Gets the control's visual children. + /// + protected IPerspexList VisualChildren + { + get; + private set; + } + /// /// Gets a value indicating whether this scene graph node is attached to a visual root. /// @@ -257,7 +262,7 @@ namespace Perspex /// /// Gets the scene graph node's child nodes. /// - IPerspexReadOnlyList IVisual.VisualChildren => _visualChildren; + IPerspexReadOnlyList IVisual.VisualChildren => VisualChildren; /// /// Gets the scene graph node's parent node. @@ -333,61 +338,6 @@ namespace Perspex property.Changed.Subscribe(AffectsRenderInvalidate); } - /// - /// Adds a visual child to the control. - /// - /// The child to add. - protected void AddVisualChild(IVisual visual) - { - Contract.Requires(visual != null); - - _visualChildren.Add(visual); - } - - /// - /// Adds visual children to the control. - /// - /// The children to add. - protected void AddVisualChildren(IEnumerable visuals) - { - Contract.Requires(visuals != null); - - _visualChildren.AddRange(visuals); - } - - /// - /// Removes all visual children from the control. - /// - protected void ClearVisualChildren() - { - _visualChildren.Clear(); - } - - /// - /// Removes a visual child from the control; - /// - /// The child to remove. - protected void RemoveVisualChild(IVisual visual) - { - Contract.Requires(visual != null); - - _visualChildren.Remove(visual); - } - - /// - /// Removes a visual children from the control; - /// - /// The children to remove. - protected void RemoveVisualChildren(IEnumerable visuals) - { - Contract.Requires(visuals != null); - - foreach (var v in visuals) - { - _visualChildren.Remove(v); - } - } - /// /// Called when the control is added to a visual tree. /// @@ -433,20 +383,6 @@ namespace Perspex (e.Sender as Visual)?.InvalidateVisual(); } - /// - /// Gets the event args for an or - /// event. - /// - /// - /// A if the visual currently has a root; - /// otherwise null. - /// - private VisualTreeAttachmentEventArgs GetAttachmentEventArgs() - { - var root = this.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); - return root != null ? new VisualTreeAttachmentEventArgs(root) : null; - } - /// /// Gets the visual offset from the specified ancestor. /// @@ -544,38 +480,28 @@ namespace Perspex } var old = _visualParent; + _visualParent = value; if (_isAttachedToVisualTree) { - var oldArgs = GetAttachmentEventArgs(); - - _visualParent = value; - - if (oldArgs != null) - { - NotifyDetachedFromVisualTree(oldArgs); - } - } - else - { - _visualParent = value; + var root = (this as IRenderRoot) ?? + old.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); + var e = new VisualTreeAttachmentEventArgs(root); + NotifyDetachedFromVisualTree(e); } if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true) { - var newArgs = GetAttachmentEventArgs(); - - if (newArgs != null) - { - NotifyAttachedToVisualTree(newArgs); - } + var root = this.GetVisualAncestors().OfType().FirstOrDefault(); + var e = new VisualTreeAttachmentEventArgs(root); + NotifyAttachedToVisualTree(e); } RaisePropertyChanged(VisualParentProperty, old, value, BindingPriority.LocalValue); } /// - /// Called when the collection changes. + /// Called when the collection changes. /// /// The sender. /// The event args. @@ -586,7 +512,6 @@ namespace Perspex case NotifyCollectionChangedAction.Add: foreach (Visual v in e.NewItems) { - v.InheritanceParent = this; v.SetVisualParent(this); } @@ -595,7 +520,6 @@ namespace Perspex case NotifyCollectionChangedAction.Remove: foreach (Visual v in e.OldItems) { - v.InheritanceParent = null; v.SetVisualParent(null); } @@ -616,9 +540,9 @@ namespace Perspex OnAttachedToVisualTree(e); - if (_visualChildren != null) + if (VisualChildren != null) { - foreach (Visual child in _visualChildren.OfType()) + foreach (Visual child in VisualChildren.OfType()) { child.NotifyAttachedToVisualTree(e); } @@ -637,9 +561,9 @@ namespace Perspex _isAttachedToVisualTree = false; OnDetachedFromVisualTree(e); - if (_visualChildren != null) + if (VisualChildren != null) { - foreach (Visual child in _visualChildren.OfType()) + foreach (Visual child in VisualChildren.OfType()) { child.NotifyDetachedFromVisualTree(e); } diff --git a/src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs b/src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs index b2df5a9a34..a6f1081ed3 100644 --- a/src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs +++ b/src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs @@ -18,6 +18,8 @@ namespace Perspex /// The root visual. public VisualTreeAttachmentEventArgs(IRenderRoot root) { + Contract.Requires(root != null); + Root = root; } diff --git a/src/Perspex.Styling/ILogical.cs b/src/Perspex.Styling/ILogical.cs index a62e309021..b1f5ded5b6 100644 --- a/src/Perspex.Styling/ILogical.cs +++ b/src/Perspex.Styling/ILogical.cs @@ -10,6 +10,11 @@ namespace Perspex /// public interface ILogical { + /// + /// Gets a value indicating whether the element is attached to a rooted logical tree. + /// + bool IsAttachedToLogicalTree { get; } + /// /// Gets the logical parent. /// diff --git a/src/Perspex.Styling/Perspex.Styling.csproj b/src/Perspex.Styling/Perspex.Styling.csproj index cb5367bc9a..2ad2057aa5 100644 --- a/src/Perspex.Styling/Perspex.Styling.csproj +++ b/src/Perspex.Styling/Perspex.Styling.csproj @@ -49,6 +49,7 @@ + diff --git a/src/Perspex.Styling/Styling/IGlobalStyles.cs b/src/Perspex.Styling/Styling/IGlobalStyles.cs index ef246f9b84..85cc9abaa7 100644 --- a/src/Perspex.Styling/Styling/IGlobalStyles.cs +++ b/src/Perspex.Styling/Styling/IGlobalStyles.cs @@ -3,8 +3,10 @@ namespace Perspex.Styling { - public interface IGlobalStyles + /// + /// Defines the style host that provides styles global to the application. + /// + public interface IGlobalStyles : IStyleRoot { - Styles Styles { get; } } } diff --git a/src/Perspex.Styling/Styling/IStyleHost.cs b/src/Perspex.Styling/Styling/IStyleHost.cs index 40cad25f63..9f434e1231 100644 --- a/src/Perspex.Styling/Styling/IStyleHost.cs +++ b/src/Perspex.Styling/Styling/IStyleHost.cs @@ -6,7 +6,7 @@ namespace Perspex.Styling /// /// Defines an element that has a collection. /// - public interface IStyleHost : IVisual + public interface IStyleHost { /// /// Gets the styles for the element. diff --git a/src/Perspex.Styling/Styling/IStyleRoot.cs b/src/Perspex.Styling/Styling/IStyleRoot.cs new file mode 100644 index 0000000000..9b3e1dcc70 --- /dev/null +++ b/src/Perspex.Styling/Styling/IStyleRoot.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Perspex.Styling +{ + /// + /// Denotes the root in a tree. + /// + public interface IStyleRoot : IStyleHost + { + } +} diff --git a/src/Perspex.Styling/Styling/IStyleable.cs b/src/Perspex.Styling/Styling/IStyleable.cs index c6ebacaea7..e5a3f9db81 100644 --- a/src/Perspex.Styling/Styling/IStyleable.cs +++ b/src/Perspex.Styling/Styling/IStyleable.cs @@ -3,6 +3,7 @@ using System; using Perspex.Collections; +using System.Reactive; namespace Perspex.Styling { @@ -11,6 +12,11 @@ namespace Perspex.Styling /// public interface IStyleable : IObservablePropertyBag, INamed { + /// + /// Raised when the control's style should be removed. + /// + IObservable StyleDetach { get; } + /// /// Gets the list of classes for the control. /// diff --git a/src/Perspex.Styling/Styling/Selector.cs b/src/Perspex.Styling/Styling/Selector.cs index 06999744c1..b47a3e28fd 100644 --- a/src/Perspex.Styling/Styling/Selector.cs +++ b/src/Perspex.Styling/Styling/Selector.cs @@ -141,7 +141,7 @@ namespace Perspex.Styling if (inputs.Count > 0) { - return new SelectorMatch(new StyleActivator(inputs)); + return new SelectorMatch(StyleActivator.And(inputs)); } else { diff --git a/src/Perspex.Styling/Styling/Selectors.cs b/src/Perspex.Styling/Styling/Selectors.cs index 67da8fc8ca..7ed066f293 100644 --- a/src/Perspex.Styling/Styling/Selectors.cs +++ b/src/Perspex.Styling/Styling/Selectors.cs @@ -64,7 +64,7 @@ namespace Perspex.Styling { Contract.Requires(previous != null); - return new Selector(previous, x => MatchIs(x, type), type.Name, type); + return new Selector(previous, x => MatchIs(x, type), $":is({type.Name})", type); } /// @@ -218,9 +218,7 @@ namespace Perspex.Styling } } - return new SelectorMatch(new StyleActivator( - descendentMatches, - ActivatorMode.Or)); + return new SelectorMatch(StyleActivator.Or(descendentMatches)); } private static SelectorMatch MatchIs(IStyleable control, Type type) diff --git a/src/Perspex.Styling/Styling/Style.cs b/src/Perspex.Styling/Styling/Style.cs index 112bc817a0..b1337b0233 100644 --- a/src/Perspex.Styling/Styling/Style.cs +++ b/src/Perspex.Styling/Styling/Style.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Linq; using Perspex.Metadata; @@ -13,6 +14,8 @@ namespace Perspex.Styling /// public class Style : IStyle { + private static readonly IObservable True = Observable.Never().StartWith(true); + /// /// Initializes a new instance of the class. /// @@ -56,17 +59,8 @@ namespace Perspex.Styling if (match.ImmediateResult != false) { - var visual = control as IVisual; - var activator = match.ObservableResult ?? - Observable.Never().StartWith(true); - - if (visual != null) - { - var detached = Observable.FromEventPattern( - x => visual.DetachedFromVisualTree += x, - x => visual.DetachedFromVisualTree -= x); - activator = activator.TakeUntil(detached); - } + var activator = (match.ObservableResult ?? True) + .TakeUntil(control.StyleDetach); foreach (var setter in Setters) { diff --git a/src/Perspex.Styling/Styling/StyleActivator.cs b/src/Perspex.Styling/Styling/StyleActivator.cs index aa71884530..463fc5bb78 100644 --- a/src/Perspex.Styling/Styling/StyleActivator.cs +++ b/src/Perspex.Styling/Styling/StyleActivator.cs @@ -15,30 +15,28 @@ namespace Perspex.Styling Or, } - public class StyleActivator : ObservableBase + public static class StyleActivator { - private readonly IObservable[] _inputs; - private readonly ActivatorMode _mode; - - public StyleActivator( - IList> inputs, - ActivatorMode mode = ActivatorMode.And) + public static IObservable And(IEnumerable> inputs) { - _inputs = inputs.ToArray(); - _mode = mode; - } + var sourceArray = inputs.Select(s => s.Publish().RefCount()).ToArray(); - protected override IDisposable SubscribeCore(IObserver observer) - { - return _inputs.CombineLatest() - .Select(Calculate) + var terminate = sourceArray + .ToObservable() + .SelectMany(x => x.LastAsync() + .Where(y => y == false)); + + return sourceArray + .CombineLatest(values => values.All(x => x)) .DistinctUntilChanged() - .Subscribe(observer); + .TakeUntil(terminate); } - private bool Calculate(IList values) + public static IObservable Or(IEnumerable> inputs) { - return _mode == ActivatorMode.And ? values.All(x => x) : values.Any(x => x); + return inputs.CombineLatest() + .Select(values => values.Any(x => x)) + .DistinctUntilChanged(); } } } diff --git a/src/Perspex.Styling/Styling/Styler.cs b/src/Perspex.Styling/Styling/Styler.cs index f2f8f56380..400af18252 100644 --- a/src/Perspex.Styling/Styling/Styler.cs +++ b/src/Perspex.Styling/Styling/Styler.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; -using Perspex.VisualTree; namespace Perspex.Styling { @@ -11,39 +9,27 @@ namespace Perspex.Styling { public void ApplyStyles(IStyleable control) { - IVisual visual = control as IVisual; - IStyleHost styleContainer = visual - .GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(); - IGlobalStyles global = PerspexLocator.Current.GetService(); + var styleHost = control as IStyleHost; - global?.Styles.Attach(control, null); - - if (styleContainer != null) + if (styleHost != null) { - ApplyStyles(control, styleContainer); + ApplyStyles(control, styleHost); } } - private void ApplyStyles(IStyleable control, IStyleHost container) + private void ApplyStyles(IStyleable control, IStyleHost styleHost) { Contract.Requires(control != null); - Contract.Requires(container != null); + Contract.Requires(styleHost != null); - var parentContainer = container.StylingParent; + var parentContainer = styleHost.StylingParent; if (parentContainer != null) { ApplyStyles(control, parentContainer); } - container.Styles.Attach(control, container); - } - - private IStyleHost GetParentContainer(IStyleHost container) - { - return container.GetVisualAncestors().OfType().FirstOrDefault(); + styleHost.Styles.Attach(control, styleHost); } } } diff --git a/src/Perspex.Themes.Default/Button.paml b/src/Perspex.Themes.Default/Button.paml index 256ac94df9..7cfd1e7f35 100644 --- a/src/Perspex.Themes.Default/Button.paml +++ b/src/Perspex.Themes.Default/Button.paml @@ -13,7 +13,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - - + diff --git a/src/Perspex.Themes.Default/MenuItem.paml b/src/Perspex.Themes.Default/MenuItem.paml index 68d4602a92..26034cacbd 100644 --- a/src/Perspex.Themes.Default/MenuItem.paml +++ b/src/Perspex.Themes.Default/MenuItem.paml @@ -24,7 +24,8 @@ IsVisible="False" Margin="3" VerticalAlignment="Center"/> - @@ -78,7 +79,8 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - diff --git a/src/Perspex.Themes.Default/Perspex.Themes.Default.csproj b/src/Perspex.Themes.Default/Perspex.Themes.Default.csproj index 2ec676592d..f17c80fe52 100644 --- a/src/Perspex.Themes.Default/Perspex.Themes.Default.csproj +++ b/src/Perspex.Themes.Default/Perspex.Themes.Default.csproj @@ -159,7 +159,7 @@ Designer - + Designer diff --git a/src/Perspex.Themes.Default/TabControl.paml b/src/Perspex.Themes.Default/TabControl.paml index 0809be9077..b678304a87 100644 --- a/src/Perspex.Themes.Default/TabControl.paml +++ b/src/Perspex.Themes.Default/TabControl.paml @@ -3,10 +3,13 @@ - - @@ -18,11 +21,14 @@ - - @@ -35,7 +41,9 @@ - @@ -45,8 +53,9 @@ - @@ -58,13 +67,16 @@ - - diff --git a/src/Perspex.Themes.Default/TabStrip.paml b/src/Perspex.Themes.Default/TabStrip.paml index a9df7c0ca7..fbc8aefd8d 100644 --- a/src/Perspex.Themes.Default/TabStrip.paml +++ b/src/Perspex.Themes.Default/TabStrip.paml @@ -2,7 +2,9 @@ - \ No newline at end of file diff --git a/src/Perspex.Themes.Default/TabItem.paml b/src/Perspex.Themes.Default/TabStripItem.paml similarity index 53% rename from src/Perspex.Themes.Default/TabItem.paml rename to src/Perspex.Themes.Default/TabStripItem.paml index 3781d32de7..08dd0bb932 100644 --- a/src/Perspex.Themes.Default/TabItem.paml +++ b/src/Perspex.Themes.Default/TabStripItem.paml @@ -1,14 +1,16 @@  - - \ No newline at end of file diff --git a/src/Perspex.Themes.Default/ToggleButton.paml b/src/Perspex.Themes.Default/ToggleButton.paml index cbb5341450..0311294db9 100644 --- a/src/Perspex.Themes.Default/ToggleButton.paml +++ b/src/Perspex.Themes.Default/ToggleButton.paml @@ -13,7 +13,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - - diff --git a/tests/Perspex.Controls.UnitTests/ContentControlTests.cs b/tests/Perspex.Controls.UnitTests/ContentControlTests.cs index ca47bf8571..ff0c993af6 100644 --- a/tests/Perspex.Controls.UnitTests/ContentControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/ContentControlTests.cs @@ -90,7 +90,7 @@ namespace Perspex.Controls.UnitTests } [Fact] - public void Setting_Content_To_Control_Should_Set_Child_Controls_Parent() + public void Control_Content_Should_Be_Logical_Child_Before_ApplyTemplate() { var target = new ContentControl { @@ -99,14 +99,14 @@ namespace Perspex.Controls.UnitTests var child = new Control(); target.Content = child; - target.ApplyTemplate(); Assert.Equal(child.Parent, target); - Assert.Equal(((ILogical)child).LogicalParent, target); + Assert.Equal(child.GetLogicalParent(), target); + Assert.Equal(new[] { child }, target.GetLogicalChildren()); } [Fact] - public void Setting_Content_To_String_Should_Set_Child_Controls_Parent() + public void DataTemplate_Created_Control_Should_Be_Logical_Child_After_ApplyTemplate() { var target = new ContentControl { @@ -118,12 +118,14 @@ namespace Perspex.Controls.UnitTests var child = target.Presenter.Child; - Assert.Equal(child.Parent, target); - Assert.Equal(((ILogical)child).LogicalParent, target); + Assert.NotNull(child); + Assert.Equal(target, child.Parent); + Assert.Equal(target, child.GetLogicalParent()); + Assert.Equal(new[] { child }, target.GetLogicalChildren()); } [Fact] - public void Clearing_Content_Should_Clear_Child_Controls_Parent() + public void Clearing_Content_Should_Clear_Logical_Child() { var target = new ContentControl(); var child = new Control(); @@ -132,52 +134,7 @@ namespace Perspex.Controls.UnitTests target.Content = null; Assert.Null(child.Parent); - Assert.Null(((ILogical)child).LogicalParent); - } - - [Fact] - public void Setting_Content_To_Control_Should_Make_Control_Appear_In_LogicalChildren() - { - var target = new ContentControl(); - var child = new Control(); - - target.Template = GetTemplate(); - target.Content = child; - target.ApplyTemplate(); - - Assert.Equal(new[] { child }, ((ILogical)target).LogicalChildren.ToList()); - } - - [Fact] - public void Setting_Content_To_String_Should_Make_TextBlock_Appear_In_LogicalChildren() - { - var target = new ContentControl(); - var child = new Control(); - - target.Template = GetTemplate(); - target.Content = "Foo"; - target.ApplyTemplate(); - - var logical = (ILogical)target; - Assert.Equal(1, logical.LogicalChildren.Count); - Assert.IsType(logical.LogicalChildren[0]); - } - - [Fact] - public void Clearing_Content_Should_Remove_From_LogicalChildren() - { - var target = new ContentControl(); - var child = new Control(); - - target.Template = GetTemplate(); - target.Content = child; - target.ApplyTemplate(); - - target.Content = null; - - // Need to call ApplyTemplate on presenter for LogicalChildren to be updated. - target.Presenter.ApplyTemplate(); - + Assert.Null(child.GetLogicalParent()); Assert.Empty(target.GetLogicalChildren()); } @@ -194,8 +151,6 @@ namespace Perspex.Controls.UnitTests target.Template = GetTemplate(); target.Content = child; target.ApplyTemplate(); - - // Need to call ApplyTemplate on presenter for LogicalChildren to be updated. target.Presenter.ApplyTemplate(); Assert.True(called); @@ -215,8 +170,6 @@ namespace Perspex.Controls.UnitTests ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true; target.Content = null; - - // Need to call ApplyTemplate on presenter for CollectionChanged to be called. target.Presenter.ApplyTemplate(); Assert.True(called); @@ -237,8 +190,6 @@ namespace Perspex.Controls.UnitTests ((ILogical)contentControl).LogicalChildren.CollectionChanged += (s, e) => called = true; contentControl.Content = child2; - - // Need to call ApplyTemplate on presenter for CollectionChanged to be called. contentControl.Presenter.ApplyTemplate(); Assert.True(called); @@ -261,7 +212,7 @@ namespace Perspex.Controls.UnitTests } [Fact] - public void DataContext_Should_Be_Set_For_Templated_Data() + public void DataContext_Should_Be_Set_For_DataTemplate_Created_Content() { var target = new ContentControl(); @@ -273,7 +224,7 @@ namespace Perspex.Controls.UnitTests } [Fact] - public void DataContext_Should_Not_Be_Set_For_Control_Data() + public void DataContext_Should_Not_Be_Set_For_Control_Content() { var target = new ContentControl(); diff --git a/tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs b/tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs deleted file mode 100644 index 05b330872f..0000000000 --- a/tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Specialized; -using System.Linq; -using Perspex.Controls.Presenters; -using Perspex.LogicalTree; -using Xunit; - -namespace Perspex.Controls.UnitTests -{ - public class ContentPresenterTests - { - [Fact] - public void Setting_Content_Should_Make_Control_Appear_In_LogicalChildren() - { - var target = new ContentPresenter(); - var child = new Control(); - - target.Content = child; - target.ApplyTemplate(); - - Assert.Equal(new[] { child }, ((ILogical)target).LogicalChildren.ToList()); - } - - [Fact] - public void Clearing_Content_Should_Remove_From_LogicalChildren() - { - var target = new ContentPresenter(); - var child = new Control(); - - target.Content = child; - target.ApplyTemplate(); - target.Content = null; - target.ApplyTemplate(); - - Assert.Equal(new ILogical[0], ((ILogical)target).LogicalChildren.ToList()); - } - - [Fact] - public void Clearing_Content_Clear_Childs_Parent() - { - var target = new ContentPresenter(); - var child = new Control(); - - target.Content = child; - target.ApplyTemplate(); - target.Content = null; - target.ApplyTemplate(); - - Assert.Null(child.Parent); - Assert.Null(child.GetLogicalParent()); - } - - [Fact] - public void Changing_Content_Should_Fire_LogicalChildren_CollectionChanged() - { - var target = new ContentPresenter(); - var child = new Control(); - var called = false; - - ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => - called = e.Action == NotifyCollectionChangedAction.Add; - - target.Content = child; - target.ApplyTemplate(); - - Assert.True(called); - } - - [Fact] - public void Clearing_Content_Should_Fire_LogicalChildren_CollectionChanged() - { - var target = new ContentPresenter(); - var child = new Control(); - var called = false; - - target.Content = child; - target.ApplyTemplate(); - - ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true; - - target.Content = null; - target.ApplyTemplate(); - - Assert.True(called); - } - } -} diff --git a/tests/Perspex.Controls.UnitTests/ControlTests.cs b/tests/Perspex.Controls.UnitTests/ControlTests.cs index d86733de3d..152c175d20 100644 --- a/tests/Perspex.Controls.UnitTests/ControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/ControlTests.cs @@ -2,6 +2,8 @@ // 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.Reactive.Linq; using Moq; using Perspex.Layout; using Perspex.Platform; @@ -22,24 +24,105 @@ namespace Perspex.Controls.UnitTests } [Fact] - public void Adding_Control_To_IRenderRoot_Should_Style_Control() + public void LogicalParent_Should_Be_Set_To_Parent() { - using (PerspexLocator.EnterScope()) + var parent = new Decorator(); + var target = new TestControl(); + + parent.Child = target; + + Assert.Equal(parent, target.InheritanceParent); + } + + [Fact] + public void LogicalParent_Should_Be_Cleared_When_Removed_From_Parent() + { + var parent = new Decorator(); + var target = new TestControl(); + + parent.Child = target; + parent.Child = null; + + Assert.Null(target.InheritanceParent); + } + + [Fact] + public void AttachedToLogicalParent_Should_Be_Called_When_Added_To_Tree() + { + var root = new TestRoot(); + var parent = new Border(); + var child = new Border(); + var grandchild = new Border(); + var parentRaised = false; + var childRaised = false; + var grandchildRaised = false; + + parent.AttachedToLogicalTree += (s, e) => parentRaised = true; + child.AttachedToLogicalTree += (s, e) => childRaised = true; + grandchild.AttachedToLogicalTree += (s, e) => grandchildRaised = true; + + parent.Child = child; + child.Child = grandchild; + + Assert.False(parentRaised); + Assert.False(childRaised); + Assert.False(grandchildRaised); + + root.Child = parent; + + Assert.True(parentRaised); + Assert.True(childRaised); + Assert.True(grandchildRaised); + } + + [Fact] + public void AttachedToLogicalParent_Should_Be_Called_Before_Parent_Change_Signalled() + { + var root = new TestRoot(); + var child = new Border(); + var raised = new List(); + + child.AttachedToLogicalTree += (s, e) => { - var root = new TestRoot(); - var target = new Control(); - var styler = new Mock(); + Assert.Equal(root, child.Parent); + raised.Add("attached"); + }; - PerspexLocator.CurrentMutable.Bind().ToConstant(styler.Object); + child.GetObservable(Control.ParentProperty).Skip(1).Subscribe(_ => raised.Add("parent")); - root.Child = target; + root.Child = child; - styler.Verify(x => x.ApplyStyles(target), Times.Once()); - } + Assert.Equal(new[] { "attached", "parent" }, raised); } [Fact] - public void Adding_Tree_To_ILayoutRoot_Should_Style_Controls() + public void DetachedToLogicalParent_Should_Be_Called_When_Removed_From_Tree() + { + var root = new TestRoot(); + var parent = new Border(); + var child = new Border(); + var grandchild = new Border(); + var parentRaised = false; + var childRaised = false; + var grandchildRaised = false; + + parent.Child = child; + child.Child = grandchild; + root.Child = parent; + + parent.DetachedFromLogicalTree += (s, e) => parentRaised = true; + child.DetachedFromLogicalTree += (s, e) => childRaised = true; + grandchild.DetachedFromLogicalTree += (s, e) => grandchildRaised = true; + + root.Child = null; + + Assert.True(parentRaised); + Assert.True(childRaised); + Assert.True(grandchildRaised); + } + + [Fact] + public void Adding_Tree_To_IStyleRoot_Should_Style_Controls() { using (PerspexLocator.EnterScope()) { @@ -64,7 +147,7 @@ namespace Perspex.Controls.UnitTests } } - private class TestRoot : Decorator, ILayoutRoot, IRenderRoot + private class TestRoot : Decorator, ILayoutRoot, IRenderRoot, IStyleRoot { public Size ClientSize { @@ -91,5 +174,10 @@ namespace Perspex.Controls.UnitTests throw new NotImplementedException(); } } + + private class TestControl : Control + { + public new PerspexObject InheritanceParent => base.InheritanceParent; + } } } diff --git a/tests/Perspex.Controls.UnitTests/ControlTests_NameScope.cs b/tests/Perspex.Controls.UnitTests/ControlTests_NameScope.cs index 2aa911b61a..ec5f657877 100644 --- a/tests/Perspex.Controls.UnitTests/ControlTests_NameScope.cs +++ b/tests/Perspex.Controls.UnitTests/ControlTests_NameScope.cs @@ -5,6 +5,7 @@ using System; using Perspex.Controls.Presenters; using Perspex.Controls.Templates; using Perspex.Rendering; +using Perspex.Styling; using Xunit; namespace Perspex.Controls.UnitTests @@ -71,7 +72,7 @@ namespace Perspex.Controls.UnitTests Assert.Null(NameScope.GetNameScope(root.Presenter).Find("foo")); } - private class TestRoot : ContentControl, IRenderRoot, INameScope + private class TestRoot : ContentControl, IRenderRoot, INameScope, IStyleRoot { private readonly NameScope _nameScope = new NameScope(); diff --git a/tests/Perspex.Controls.UnitTests/EnumerableExtensions.cs b/tests/Perspex.Controls.UnitTests/EnumerableExtensions.cs new file mode 100644 index 0000000000..92b7f9c815 --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/EnumerableExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Perspex.Controls.UnitTests +{ + internal static class EnumerableExtensions + { + public static IEnumerable Do(this IEnumerable items, Action action) + { + foreach (var i in items) + { + action(i); + yield return i; + } + } + } +} diff --git a/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs index d8d2c6ba1f..7731155e71 100644 --- a/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs +++ b/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs @@ -16,7 +16,11 @@ namespace Perspex.Controls.UnitTests.Generators 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(); + var result = containers + .Select(x => x.ContainerControl) + .OfType() + .Select(x => x.Text) + .ToList(); Assert.Equal(items, result); } @@ -29,9 +33,9 @@ namespace Perspex.Controls.UnitTests.Generators 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)); + Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0)); + Assert.Equal(containers[1].ContainerControl, target.ContainerFromIndex(1)); + Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2)); } [Fact] @@ -42,9 +46,9 @@ namespace Perspex.Controls.UnitTests.Generators 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])); + Assert.Equal(0, target.IndexFromContainer(containers[0].ContainerControl)); + Assert.Equal(1, target.IndexFromContainer(containers[1].ContainerControl)); + Assert.Equal(2, target.IndexFromContainer(containers[2].ContainerControl)); } [Fact] @@ -57,9 +61,9 @@ namespace Perspex.Controls.UnitTests.Generators target.Dematerialize(1, 1); - Assert.Equal(containers[0], target.ContainerFromIndex(0)); + Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0)); Assert.Equal(null, target.ContainerFromIndex(1)); - Assert.Equal(containers[2], target.ContainerFromIndex(2)); + Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2)); } [Fact] @@ -85,8 +89,8 @@ namespace Perspex.Controls.UnitTests.Generators var removed = target.RemoveRange(1, 1).Single(); - Assert.Equal(containers[0], target.ContainerFromIndex(0)); - Assert.Equal(containers[2], target.ContainerFromIndex(1)); + Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0)); + Assert.Equal(containers[2].ContainerControl, 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 index f6b0ff2eae..e59a1e0256 100644 --- a/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs +++ b/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs @@ -17,10 +17,10 @@ namespace Perspex.Controls.UnitTests.Generators var target = new ItemContainerGenerator(owner, ListBoxItem.ContentProperty); var containers = target.Materialize(0, items, null); var result = containers + .Select(x => x.ContainerControl) .OfType() .Select(x => x.Content) - .OfType() - .Select(x => x.Text).ToList(); + .ToList(); Assert.Equal(items, result); } diff --git a/tests/Perspex.Controls.UnitTests/HeaderedItemsControlTests .cs b/tests/Perspex.Controls.UnitTests/HeaderedItemsControlTests .cs new file mode 100644 index 0000000000..3eb4db00de --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/HeaderedItemsControlTests .cs @@ -0,0 +1,78 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Controls.Presenters; +using Perspex.Controls.Primitives; +using Perspex.Controls.Templates; +using Perspex.LogicalTree; +using Xunit; + +namespace Perspex.Controls.UnitTests +{ + public class HeaderedItemsControlTests + { + [Fact] + public void Control_Header_Should_Be_Logical_Child_Before_ApplyTemplate() + { + var target = new HeaderedItemsControl + { + Template = GetTemplate(), + }; + + var child = new Control(); + target.Header = child; + + Assert.Equal(child.Parent, target); + Assert.Equal(child.GetLogicalParent(), target); + Assert.Equal(new[] { child }, target.GetLogicalChildren()); + } + + [Fact] + public void DataTemplate_Created_Control_Should_Be_Logical_Child_After_ApplyTemplate() + { + var target = new HeaderedItemsControl + { + Template = GetTemplate(), + }; + + target.Header = "Foo"; + target.ApplyTemplate(); + + var child = target.HeaderPresenter.Child; + + Assert.NotNull(child); + Assert.Equal(target, child.Parent); + Assert.Equal(target, child.GetLogicalParent()); + Assert.Equal(new[] { child }, target.GetLogicalChildren()); + } + + [Fact] + public void Clearing_Content_Should_Clear_Logical_Child() + { + var target = new HeaderedItemsControl(); + var child = new Control(); + + target.Header = child; + target.Header = null; + + Assert.Null(child.Parent); + Assert.Null(child.GetLogicalParent()); + Assert.Empty(target.GetLogicalChildren()); + } + + private FuncControlTemplate GetTemplate() + { + return new FuncControlTemplate(parent => + { + return new Border + { + Child = new ContentPresenter + { + Name = "PART_HeaderPresenter", + [~ContentPresenter.ContentProperty] = parent[~HeaderedItemsControl.HeaderProperty], + } + }; + }); + } + } +} diff --git a/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs b/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs index e066866a05..9cc960a5c2 100644 --- a/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs @@ -7,7 +7,6 @@ using Perspex.Collections; using Perspex.Controls.Presenters; using Perspex.Controls.Templates; using Perspex.LogicalTree; -using Perspex.Styling; using Perspex.VisualTree; using Xunit; @@ -24,10 +23,7 @@ namespace Perspex.Controls.UnitTests target.Items = new[] { "Foo" }; target.ApplyTemplate(); - var presenter = target.GetTemplateChildren().OfType().Single(); - var panel = target.GetTemplateChildren().OfType().Single(); - - Assert.Equal(target, panel.TemplatedParent); + Assert.Equal(target, target.Presenter.Panel.TemplatedParent); } [Fact] @@ -39,29 +35,57 @@ namespace Perspex.Controls.UnitTests target.Items = new[] { "Foo" }; target.ApplyTemplate(); - var presenter = target.GetTemplateChildren().OfType().Single(); - var panel = target.GetTemplateChildren().OfType().Single(); - var item = (TextBlock)panel.GetVisualChildren().First(); + var item = (TextBlock)target.Presenter.Panel.GetVisualChildren().First(); Assert.Null(item.TemplatedParent); } [Fact] - public void Control_Item_Should_Have_Parent_Set() + public void Control_Item_Should_Be_Logical_Child_Before_ApplyTemplate() { var target = new ItemsControl(); var child = new Control(); target.Template = GetTemplate(); target.Items = new[] { child }; - target.ApplyTemplate(); - Assert.Equal(target, child.Parent); - Assert.Equal(target, ((ILogical)child).LogicalParent); + Assert.Equal(child.Parent, target); + Assert.Equal(child.GetLogicalParent(), target); + Assert.Equal(new[] { child }, target.GetLogicalChildren()); + } + + [Fact] + public void Control_Item_Should_Be_Removed_From_Logical_Children_Before_ApplyTemplate() + { + var target = new ItemsControl(); + var child = new Control(); + var items = new PerspexList(child); + + target.Template = GetTemplate(); + target.Items = items; + items.RemoveAt(0); + + Assert.Null(child.Parent); + Assert.Null(child.GetLogicalParent()); + Assert.Empty(target.GetLogicalChildren()); } [Fact] - public void Clearing_Control_Item_Should_Clear_Child_Controls_Parent() + public void Clearing_Items_Should_Clear_Child_Controls_Parent_Before_ApplyTemplate() + { + var target = new ItemsControl(); + var child = new Control(); + + target.Template = GetTemplate(); + target.Items = new[] { child }; + target.Items = null; + + Assert.Null(child.Parent); + Assert.Null(((ILogical)child).LogicalParent); + } + + [Fact] + public void Clearing_Items_Should_Clear_Child_Controls_Parent() { var target = new ItemsControl(); var child = new Control(); @@ -83,9 +107,13 @@ namespace Perspex.Controls.UnitTests target.Template = GetTemplate(); target.Items = new[] { child }; + + // Should appear both before and after applying template. + Assert.Equal(new ILogical[] { child }, target.GetLogicalChildren()); + target.ApplyTemplate(); - Assert.Equal(new[] { child }, ((ILogical)target).LogicalChildren.ToList()); + Assert.Equal(new ILogical[] { child }, target.GetLogicalChildren()); } [Fact] @@ -114,7 +142,7 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); target.Items = null; - Assert.Equal(new ILogical[0], ((ILogical)target).LogicalChildren.ToList()); + Assert.Equal(new ILogical[0], target.GetLogicalChildren()); } [Fact] diff --git a/tests/Perspex.Controls.UnitTests/ListBoxTests.cs b/tests/Perspex.Controls.UnitTests/ListBoxTests.cs index 0f1a60b902..08c445e8df 100644 --- a/tests/Perspex.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Perspex.Controls.UnitTests/ListBoxTests.cs @@ -14,7 +14,31 @@ namespace Perspex.Controls.UnitTests public class ListBoxTests { [Fact] - public void LogicalChildren_Should_Be_Set() + public void ListBoxItem_Containers_Should_Be_Generated() + { + var items = new[] { "Foo", "Bar", "Baz " }; + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = items, + }; + + target.ApplyTemplate(); + + var text = target.Presenter.Panel.Children + .OfType() + .Do(x => x.Template = ListBoxItemTemplate()) + .Do(x => x.ApplyTemplate()) + .Select(x => x.Presenter.Child) + .OfType() + .Select(x => x.Text) + .ToList(); + + Assert.Equal(items, text); + } + + [Fact] + public void LogicalChildren_Should_Be_Set_For_DataTemplate_Generated_Items() { var target = new ListBox { @@ -103,6 +127,15 @@ namespace Perspex.Controls.UnitTests }; } + private FuncControlTemplate ListBoxItemTemplate() + { + return new FuncControlTemplate(parent => new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty], + }); + } + private Control CreateScrollViewerTemplate(ITemplatedControl parent) { return new ScrollContentPresenter diff --git a/tests/Perspex.Controls.UnitTests/PanelTests.cs b/tests/Perspex.Controls.UnitTests/PanelTests.cs index 877f7039fc..68f6075e2e 100644 --- a/tests/Perspex.Controls.UnitTests/PanelTests.cs +++ b/tests/Perspex.Controls.UnitTests/PanelTests.cs @@ -118,23 +118,6 @@ namespace Perspex.Controls.UnitTests Assert.Equal(new ILogical[0], panel.GetLogicalChildren()); } - [Fact] - public void Should_Be_Able_To_Reparent_Child_Controls() - { - var target = new Panel(); - var parent = new TestReparent(); - var control1 = new Control(); - var control2 = new Control(); - - target.Children.Add(control1); - ((IReparentingControl)target).ReparentLogicalChildren(parent, parent.LogicalChildren); - target.Children.Add(control2); - - Assert.Equal(new[] { control1, control2 }, parent.LogicalChildren); - Assert.Equal(parent, target.Children[0].Parent); - Assert.Equal(parent, target.Children[1].Parent); - } - private class TestReparent : Panel { public new IPerspexList LogicalChildren => base.LogicalChildren; diff --git a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index 745556a791..49ca24dba4 100644 --- a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -81,11 +81,13 @@ + + - + diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ContentPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ContentPresenterTests.cs new file mode 100644 index 0000000000..3bc0465dc4 --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/Presenters/ContentPresenterTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Controls.Presenters; +using Perspex.Controls.Templates; +using Xunit; + +namespace Perspex.Controls.UnitTests.Presenters +{ + public class ContentPresenterTests + { + [Fact] + public void Setting_Content_To_Control_Should_Set_Child() + { + var target = new ContentPresenter(); + var child = new Border(); + + target.Content = child; + + // Child should not update until ApplyTemplate called. + Assert.Null(target.Child); + + target.ApplyTemplate(); + + Assert.Equal(child, target.Child); + } + + [Fact] + public void Setting_Content_To_String_Should_Create_TextBlock() + { + var target = new ContentPresenter(); + + target.Content = "Foo"; + + // Child should not update until ApplyTemplate called. + Assert.Null(target.Child); + + target.ApplyTemplate(); + + Assert.IsType(target.Child); + Assert.Equal("Foo", ((TextBlock)target.Child).Text); + } + + [Fact] + public void Adding_To_Logical_Tree_Should_Reevaluate_DataTemplates() + { + var target = new ContentPresenter + { + Content = "Foo", + }; + + target.ApplyTemplate(); + Assert.IsType(target.Child); + + var root = new TestRoot + { + DataTemplates = new DataTemplates + { + new FuncDataTemplate(x => new Decorator()), + }, + }; + + root.Child = target; + target.ApplyTemplate(); + Assert.IsType(target.Child); + } + } +} diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index db9e6d3aec..b5fc0a0869 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -206,7 +206,8 @@ namespace Perspex.Controls.UnitTests.Presenters target.ApplyTemplate(); - Assert.Equal(panel, target.Panel); + Assert.Same(panel, target.Panel); + Assert.Same(target, target.Panel.Parent); } [Fact] diff --git a/tests/Perspex.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/PopupTests.cs index 172d2320f1..4751ffbfde 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/PopupTests.cs @@ -183,21 +183,6 @@ namespace Perspex.Controls.UnitTests.Primitives } } - [Fact] - public void PopupRoot_Should_Have_Child_As_LogicalChild() - { - using (CreateServices()) - { - var target = new Popup(); - var child = new Control(); - - target.Child = child; - target.Open(); - - Assert.Equal(new[] { child }, target.PopupRoot.GetLogicalChildren()); - } - } - [Fact] public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent() { @@ -263,7 +248,6 @@ namespace Perspex.Controls.UnitTests.Primitives var globalStyles = new Mock(); globalStyles.Setup(x => x.Styles).Returns(styles); - PerspexLocator.CurrentMutable .Bind().ToTransient() .Bind().ToFunc(() => globalStyles.Object) diff --git a/tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs index d20b15be23..6ae55f73c2 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs @@ -3,9 +3,11 @@ using System.Collections.ObjectModel; using System.Linq; +using Moq; using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; +using Perspex.LogicalTree; using Xunit; namespace Perspex.Controls.UnitTests.Primitives @@ -13,114 +15,144 @@ namespace Perspex.Controls.UnitTests.Primitives public class TabStripTests { [Fact] - public void First_Tab_Should_Be_Selected_By_Default() + public void Header_Of_IHeadered_Items_Should_Be_Used() { + var items = new[] + { + Mock.Of(x => x.Header == "foo"), + Mock.Of(x => x.Header == "bar"), + }; + var target = new TabStrip { Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = new[] - { - new TabItem - { - Name = "first" - }, - new TabItem - { - Name = "second" - }, - } + Items = items, }; target.ApplyTemplate(); - Assert.Equal(0, target.SelectedIndex); - Assert.Equal(target.Items.Cast().First(), target.SelectedItem); - Assert.Equal(target.Items.Cast().First(), target.SelectedTab); + var result = target.GetLogicalChildren() + .OfType() + .Select(x => x.Content) + .ToList(); + + Assert.Equal(new[] { "foo", "bar" }, result); } [Fact] - public void Setting_SelectedItem_Should_Set_SelectedTab() + public void Data_Of_Non_IHeadered_Items_Should_Be_Used() { + var items = new[] + { + "foo", + "bar" + }; + var target = new TabStrip { Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = new[] - { - new TabItem - { - Name = "first" - }, - new TabItem - { - Name = "second" - }, - } + Items = items, }; target.ApplyTemplate(); - target.SelectedItem = target.Items.Cast().ElementAt(1); - Assert.Same(target.SelectedTab, target.SelectedItem); + var result = target.GetLogicalChildren() + .OfType() + .Select(x => x.Content) + .ToList(); + + Assert.Equal(new[] { "foo", "bar" }, result); } [Fact] - public void Setting_SelectedTab_Should_Set_SelectedItem() + public void First_Tab_Should_Be_Selected_By_Default() { + var items = new[] + { + new TabItem + { + Name = "first" + }, + new TabItem + { + Name = "second" + }, + }; + var target = new TabStrip { Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = new[] + Items = items, + }; + + target.ApplyTemplate(); + + Assert.Equal(0, target.SelectedIndex); + Assert.Same(items[0], target.SelectedItem); + } + + [Fact] + public void Setting_SelectedItem_Should_Set_Selection() + { + var items = new[] + { + new TabItem + { + Name = "first" + }, + new TabItem { - new TabItem - { - Name = "first" - }, - new TabItem - { - Name = "second" - }, - } + Name = "second" + }, + }; + + var target = new TabStrip + { + Template = new FuncControlTemplate(CreateTabStripTemplate), + Items = items, + SelectedItem = items[1], }; target.ApplyTemplate(); - target.SelectedTab = target.Items.Cast().ElementAt(1); - Assert.Same(target.SelectedItem, target.SelectedTab); + Assert.Equal(1, target.SelectedIndex); + Assert.Same(items[1], target.SelectedItem); } [Fact] public void Removing_Selected_Should_Select_Next() { - var list = new ObservableCollection() + var items = new ObservableCollection() + { + new TabItem + { + Name = "first" + }, + new TabItem { - new TabItem - { - Name = "first" - }, - new TabItem - { - Name = "second" - }, - new TabItem - { - Name = "3rd" - }, - }; + Name = "second" + }, + new TabItem + { + Name = "3rd" + }, + }; var target = new TabStrip { Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = list + Items = items }; target.ApplyTemplate(); - target.SelectedTab = list[1]; - Assert.Same(list[1], target.SelectedTab); - list.RemoveAt(1); + target.SelectedItem = items[1]; + Assert.Same(items[1], target.SelectedItem); + items.RemoveAt(1); // Assert for former element [2] now [1] == "3rd" - Assert.Same(list[1], target.SelectedTab); - Assert.Same("3rd", target.SelectedTab.Name); + Assert.Equal(1, target.SelectedIndex); + Assert.Same(items[1], target.SelectedItem); + Assert.Same("3rd", ((TabItem)target.SelectedItem).Name); } private Control CreateTabStripTemplate(TabStrip parent) @@ -128,7 +160,8 @@ namespace Perspex.Controls.UnitTests.Primitives return new ItemsPresenter { Name = "itemsPresenter", - [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + [!ItemsPresenter.ItemsProperty] = parent[!ItemsControl.ItemsProperty], + [!ItemsPresenter.MemberSelectorProperty] = parent[!ItemsControl.MemberSelectorProperty], }; } } diff --git a/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs index d0f0547854..40d8e1c0c0 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -118,27 +118,6 @@ namespace Perspex.Controls.UnitTests.Primitives Assert.Equal(target, child.GetLogicalParent()); } - [Fact] - public void Templated_Child_Should_Have_ApplyTemplate_Called_With_Logical_Then_Visual_Parent() - { - var target = new TemplatedControl - { - Template = new FuncControlTemplate(_ => new ApplyTemplateTracker()) - }; - - target.ApplyTemplate(); - - var child = (ApplyTemplateTracker)target.GetVisualChildren().Single(); - - Assert.Equal( - new[] - { - new Tuple(null, target), - new Tuple(target, target), - }, - child.Invocations); - } - [Fact] public void Nested_TemplatedControls_Should_Be_Expanded_And_Have_Correct_TemplatedParent() { @@ -186,6 +165,76 @@ namespace Perspex.Controls.UnitTests.Primitives templatedParents); } + + + [Fact] + public void Nested_TemplatedControls_Should_Register_With_Correct_NameScope() + { + var target = new ContentControl + { + Template = new FuncControlTemplate(ScrollingContentControlTemplate), + Content = "foo" + }; + + target.ApplyTemplate(); + + var border = target.GetVisualChildren().FirstOrDefault(); + Assert.IsType(border); + var scrollViewer = border.GetVisualChildren().FirstOrDefault(); + Assert.IsType(scrollViewer); + var scrollContentPresenter = scrollViewer.GetVisualChildren().FirstOrDefault(); + Assert.IsType(scrollContentPresenter); + var contentPresenter = scrollContentPresenter.GetVisualChildren().FirstOrDefault(); + Assert.IsType(contentPresenter); + + var borderNs = NameScope.GetNameScope((Control)border); + var scrollContentPresenterNs = NameScope.GetNameScope((Control)scrollContentPresenter); + Assert.NotNull(borderNs); + Assert.Same(scrollViewer, borderNs.Find("ScrollViewer")); + Assert.Same(contentPresenter, borderNs.Find("PART_ContentPresenter")); + Assert.Same(scrollContentPresenter, scrollContentPresenterNs.Find("PART_ContentPresenter")); + } + + [Fact] + public void ApplyTemplate_Should_Raise_TemplateApplied() + { + var target = new TestTemplatedControl + { + Template = new FuncControlTemplate(_ => new Decorator()) + }; + + var raised = false; + + target.TemplateApplied += (s, e) => + { + Assert.Equal(TemplatedControl.TemplateAppliedEvent, e.RoutedEvent); + Assert.Same(target, e.Source); + Assert.NotNull(e.NameScope); + raised = true; + }; + + target.ApplyTemplate(); + + Assert.True(raised); + } + + private static IControl ScrollingContentControlTemplate(ContentControl control) + { + return new Border + { + Child = new ScrollViewer + { + Template = new FuncControlTemplate(ScrollViewerTemplate), + Name = "ScrollViewer", + Content = new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = control[!ContentControl.ContentProperty], + } + } + }; + } + private static IControl ItemsControlTemplate(ItemsControl control) { return new Border @@ -195,9 +244,9 @@ namespace Perspex.Controls.UnitTests.Primitives Template = new FuncControlTemplate(ScrollViewerTemplate), Content = new ItemsPresenter { - Name = "itemsPresenter", - [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], - [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], + Name = "PART_ItemsPresenter", + [!ItemsPresenter.ItemsProperty] = control[!ItemsControl.ItemsProperty], + [!ItemsPresenter.ItemsPanelProperty] = control[!ItemsControl.ItemsPanelProperty], } } }; @@ -207,7 +256,7 @@ namespace Perspex.Controls.UnitTests.Primitives { var result = new ScrollContentPresenter { - Name = "contentPresenter", + Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], }; diff --git a/tests/Perspex.Controls.UnitTests/TabControlTests.cs b/tests/Perspex.Controls.UnitTests/TabControlTests.cs index 541c1c8cd8..0f3172e715 100644 --- a/tests/Perspex.Controls.UnitTests/TabControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/TabControlTests.cs @@ -7,6 +7,7 @@ using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.LogicalTree; +using Perspex.Styling; using Xunit; namespace Perspex.Controls.UnitTests @@ -37,60 +38,34 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); + Assert.Equal(0, target.SelectedIndex); Assert.Equal(selected, target.SelectedItem); - Assert.Equal(selected, target.SelectedTab); } [Fact] - public void Setting_SelectedItem_Should_Set_SelectedTab() + public void Logical_Children_Should_Be_TabItems() { - var target = new TabControl + var items = new[] { - Template = new FuncControlTemplate(CreateTabControlTemplate), - Items = new[] + new TabItem { - new TabItem - { - Name = "first", - Content = "foo", - }, - new TabItem - { - Name = "second", - Content = "bar", - }, - } + Content = "foo" + }, + new TabItem + { + Content = "bar" + }, }; - target.ApplyTemplate(); - target.SelectedItem = target.Items.Cast().ElementAt(1); - - Assert.Same(target.SelectedTab, target.SelectedItem); - } - - [Fact] - public void Logical_Child_Should_Be_Selected_Tab_Content() - { var target = new TabControl { Template = new FuncControlTemplate(CreateTabControlTemplate), - Items = new[] - { - new TabItem - { - Content = "foo" - }, - new TabItem - { - Content = "bar" - }, - }, + Items = items, }; + Assert.Equal(items, target.GetLogicalChildren()); target.ApplyTemplate(); - - Assert.Equal(1, target.GetLogicalChildren().Count()); - Assert.Equal("foo", ((TextBlock)target.GetLogicalChildren().First()).Text); + Assert.Equal(items, target.GetLogicalChildren()); } [Fact] @@ -127,7 +102,60 @@ namespace Perspex.Controls.UnitTests // compare with former [2] now [1] == "3rd" Assert.Same(collection[1], target.SelectedItem); - Assert.Same(target.SelectedTab, target.SelectedItem); + } + + + [Fact] + public void TabItem_Templates_Should_Be_Set_Before_TabItem_ApplyTemplate() + { + var collection = new[] + { + new TabItem + { + Name = "first", + Content = "foo", + }, + new TabItem + { + Name = "second", + Content = "bar", + }, + new TabItem + { + Name = "3rd", + Content = "barf", + }, + }; + + var template = new FuncControlTemplate(x => new Decorator()); + + using (PerspexLocator.EnterScope()) + { + PerspexLocator.CurrentMutable.Bind().ToConstant(new Styler()); + + var root = new TestRoot + { + Styles = new Styles + { + new Style(x => x.OfType()) + { + Setters = new[] + { + new Setter(TemplatedControl.TemplateProperty, template) + } + } + }, + Child = new TabControl + { + Template = new FuncControlTemplate(CreateTabControlTemplate), + Items = collection, + } + }; + } + + Assert.Same(collection[0].Template, template); + Assert.Same(collection[1].Template, template); + Assert.Same(collection[2].Template, template); } [Fact] @@ -155,26 +183,62 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); - var dataContext = ((TextBlock)target.GetLogicalChildren().Single()).DataContext; + var carousel = (Carousel)target.Pages; + + var dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; Assert.Equal(items[0], dataContext); target.SelectedIndex = 1; - dataContext = ((Button)target.GetLogicalChildren().Single()).DataContext; + dataContext = ((Button)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; Assert.Equal(items[1], dataContext); target.SelectedIndex = 2; - dataContext = ((TextBlock)target.GetLogicalChildren().Single()).DataContext; + dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; Assert.Equal("Base", dataContext); target.SelectedIndex = 3; - dataContext = ((TextBlock)target.GetLogicalChildren().Single()).DataContext; + dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; Assert.Equal("Qux", dataContext); target.SelectedIndex = 4; - dataContext = ((TextBlock)target.GetLogicalChildren().Single()).DataContext; + dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; Assert.Equal("Base", dataContext); } + /// + /// Non-headered control items should result in TabStripItems with empty content. + /// + /// + /// If a TabStrip is created with non IHeadered controls as its items, don't try to + /// display the control in the TabStripItem: if the TabStrip is part of a TabControl + /// then *that* will also try to display the control, resulting in dual-parentage + /// breakage. + /// + [Fact] + public void Non_IHeadered_Control_Items_Should_Be_Ignored() + { + var items = new[] + { + new TextBlock { Text = "foo" }, + new TextBlock { Text = "bar" }, + }; + + var target = new TabControl + { + Template = new FuncControlTemplate(CreateTabControlTemplate), + Items = items, + }; + + target.ApplyTemplate(); + + var result = target.TabStrip.GetLogicalChildren() + .OfType() + .Select(x => x.Content) + .ToList(); + + Assert.Equal(new object[] { string.Empty, string.Empty }, result); + } + private Control CreateTabControlTemplate(TabControl parent) { return new StackPanel @@ -183,18 +247,19 @@ namespace Perspex.Controls.UnitTests { new TabStrip { - Name = "tabStrip", + Name = "PART_TabStrip", Template = new FuncControlTemplate(CreateTabStripTemplate), - [!ItemsControl.ItemsProperty] = parent[!ItemsControl.ItemsProperty], - [!!TabStrip.SelectedTabProperty] = parent[!!TabControl.SelectedTabProperty] + MemberSelector = TabControl.HeaderSelector, + [!TabStrip.ItemsProperty] = parent[!TabControl.ItemsProperty], + [!!TabStrip.SelectedIndexProperty] = parent[!!TabControl.SelectedIndexProperty] }, new Carousel { - Name = "carousel", + Name = "PART_Content", Template = new FuncControlTemplate(CreateCarouselTemplate), - MemberSelector = parent.ContentSelector, - [!ItemsControl.ItemsProperty] = parent[!ItemsControl.ItemsProperty], - [!SelectingItemsControl.SelectedItemProperty] = parent[!SelectingItemsControl.SelectedItemProperty], + MemberSelector = TabControl.ContentSelector, + [!Carousel.ItemsProperty] = parent[!TabControl.ItemsProperty], + [!Carousel.SelectedItemProperty] = parent[!TabControl.SelectedItemProperty], } } }; @@ -204,8 +269,9 @@ namespace Perspex.Controls.UnitTests { return new ItemsPresenter { - Name = "itemsPresenter", + Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + [!CarouselPresenter.MemberSelectorProperty] = parent[!ItemsControl.MemberSelectorProperty], }; } @@ -213,7 +279,7 @@ namespace Perspex.Controls.UnitTests { return new CarouselPresenter { - Name = "itemsPresenter", + Name = "PART_ItemsPresenter", [!CarouselPresenter.ItemsProperty] = control[!ItemsControl.ItemsProperty], [!CarouselPresenter.ItemsPanelProperty] = control[!ItemsControl.ItemsPanelProperty], [!CarouselPresenter.MemberSelectorProperty] = control[!ItemsControl.MemberSelectorProperty], diff --git a/tests/Perspex.Controls.UnitTests/TestRoot.cs b/tests/Perspex.Controls.UnitTests/TestRoot.cs index 10973f7c9c..ea72005719 100644 --- a/tests/Perspex.Controls.UnitTests/TestRoot.cs +++ b/tests/Perspex.Controls.UnitTests/TestRoot.cs @@ -6,10 +6,11 @@ using Moq; using Perspex.Layout; using Perspex.Platform; using Perspex.Rendering; +using Perspex.Styling; namespace Perspex.Controls.UnitTests { - internal class TestRoot : Decorator, ILayoutRoot, IRenderRoot + internal class TestRoot : Decorator, ILayoutRoot, IRenderRoot, IStyleRoot { public Size ClientSize => new Size(100, 100); diff --git a/tests/Perspex.Controls.UnitTests/TestTemplatedControl.cs b/tests/Perspex.Controls.UnitTests/TestTemplatedControl.cs index 8cbf1dca3f..56f2ca8b3b 100644 --- a/tests/Perspex.Controls.UnitTests/TestTemplatedControl.cs +++ b/tests/Perspex.Controls.UnitTests/TestTemplatedControl.cs @@ -9,13 +9,14 @@ namespace Perspex.Controls.UnitTests { public bool OnTemplateAppliedCalled { get; private set; } - public new void AddVisualChild(IVisual visual) + public void AddVisualChild(IVisual visual) { - base.AddVisualChild(visual); + VisualChildren.Add(visual); } - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { + base.OnTemplateApplied(e); OnTemplateAppliedCalled = true; } } diff --git a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs index 1e8ffc7a48..06337d9412 100644 --- a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs @@ -42,7 +42,7 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); - var container = (TreeViewItem)target.ItemContainerGenerator.Containers.Single(); + var container = (TreeViewItem)target.ItemContainerGenerator.Containers.Single().ContainerControl; var header = (TextBlock)container.Header; Assert.Equal("Root", header.Text); } diff --git a/tests/Perspex.Interactivity.UnitTests/InteractiveTests.cs b/tests/Perspex.Interactivity.UnitTests/InteractiveTests.cs index 3a3d47947a..d7c937f597 100644 --- a/tests/Perspex.Interactivity.UnitTests/InteractiveTests.cs +++ b/tests/Perspex.Interactivity.UnitTests/InteractiveTests.cs @@ -349,7 +349,7 @@ namespace Perspex.Interactivity.UnitTests set { - AddVisualChildren(value.Cast()); + VisualChildren.AddRange(value.Cast()); } } diff --git a/tests/Perspex.LeakTests/Perspex.LeakTests.csproj b/tests/Perspex.LeakTests/Perspex.LeakTests.csproj index ba2450f8f9..b2d67554c3 100644 --- a/tests/Perspex.LeakTests/Perspex.LeakTests.csproj +++ b/tests/Perspex.LeakTests/Perspex.LeakTests.csproj @@ -88,7 +88,6 @@ - diff --git a/tests/Perspex.LeakTests/StyleTests.cs b/tests/Perspex.LeakTests/StyleTests.cs deleted file mode 100644 index ae74048ae9..0000000000 --- a/tests/Perspex.LeakTests/StyleTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Linq; -using JetBrains.dotMemoryUnit; -using Perspex.Controls; -using Perspex.Styling; -using Xunit; -using Xunit.Abstractions; - -namespace Perspex.LeakTests -{ - [DotMemoryUnit(FailIfRunWithoutSupport = false)] - public class StyleTests - { - public StyleTests(ITestOutputHelper atr) - { - TestApp.Initialize(); - DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); - } - - [Fact] - public void StyleActivator_Should_Be_Released() - { - Func run = () => - { - var window = new Window - { - Styles = new Styles - { - new Style(x => x.OfType().Class("foo")) - { - Setters = new[] - { - new Setter(Canvas.WidthProperty, 100), - } - } - }, - Content = new Canvas - { - Classes = new Classes("foo"), - } - }; - - // Do a layout and make sure that styled Canvas gets added to visual tree. - window.LayoutManager.ExecuteLayoutPass(); - Assert.IsType(window.Presenter.Child); - Assert.Equal(100, (window.Presenter.Child).Width); - - // Clear the content and ensure the Canvas is removed. - window.Content = null; - window.LayoutManager.ExecuteLayoutPass(); - Assert.Null(window.Presenter.Child); - - return window; - }; - - var result = run(); - - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); - } - - [Fact] - public void Changing_Carousel_SelectedIndex_Should_Not_Leak_StyleActivators() - { - Func run = () => - { - Carousel target; - - var window = new Window - { - Styles = new Styles - { - new Style(x => x.OfType().Class("foo")) - { - Setters = new[] - { - new Setter(Visual.OpacityProperty, 0.5), - } - } - }, - Content = target = new Carousel - { - Items = new[] - { - new ContentControl - { - Name = "item1", - Classes = new Classes("foo"), - Content = "item1", - }, - new ContentControl - { - Name = "item2", - Classes = new Classes("foo"), - Content = "item2", - }, - } - } - }; - - // Do a layout and make sure that Carousel gets added to visual tree. - window.LayoutManager.ExecuteLayoutPass(); - Assert.IsType(window.Presenter.Child); - - target.SelectedIndex = 1; - window.LayoutManager.ExecuteLayoutPass(); - target.SelectedIndex = 0; - window.LayoutManager.ExecuteLayoutPass(); - target.SelectedIndex = 1; - window.LayoutManager.ExecuteLayoutPass(); - - return window; - }; - - var result = run(); - - dotMemory.Check(memory => - Assert.Equal(1, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); - } - } -} diff --git a/tests/Perspex.Markup.UnitTests/TestRoot.cs b/tests/Perspex.Markup.UnitTests/TestRoot.cs index 3649493f0c..26ac0ed0da 100644 --- a/tests/Perspex.Markup.UnitTests/TestRoot.cs +++ b/tests/Perspex.Markup.UnitTests/TestRoot.cs @@ -5,10 +5,11 @@ using System; using Perspex.Controls; using Perspex.Platform; using Perspex.Rendering; +using Perspex.Styling; namespace Perspex.Markup.UnitTests { - public class TestRoot : Decorator, IRenderRoot, INameScope + public class TestRoot : Decorator, IRenderRoot, INameScope, IStyleRoot { private readonly NameScope _nameScope = new NameScope(); diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Converters/PerspexPropertyConverterTest.cs b/tests/Perspex.Markup.Xaml.UnitTests/Converters/PerspexPropertyConverterTest.cs index f1565fb457..270b6d3d9b 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Converters/PerspexPropertyConverterTest.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Converters/PerspexPropertyConverterTest.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive; using Moq; using OmniXaml; using OmniXaml.ObjectAssembler.Commands; @@ -95,6 +96,8 @@ namespace Perspex.Markup.Xaml.UnitTests.Converters { get { throw new NotImplementedException(); } } + + IObservable IStyleable.StyleDetach { get; } } private class AttachedOwner diff --git a/tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs b/tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs index b9db948265..e172e2ccb7 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs @@ -5,10 +5,11 @@ using System; using Perspex.Controls; using Perspex.Platform; using Perspex.Rendering; +using Perspex.Styling; namespace Perspex.Markup.Xaml.UnitTests { - public class TestRoot : Decorator, IRenderRoot, INameScope + public class TestRoot : Decorator, IRenderRoot, INameScope, IStyleRoot { private readonly NameScope _nameScope = new NameScope(); diff --git a/tests/Perspex.SceneGraph.UnitTests/TestVisual.cs b/tests/Perspex.SceneGraph.UnitTests/TestVisual.cs index 88f47d7876..21a4427fbc 100644 --- a/tests/Perspex.SceneGraph.UnitTests/TestVisual.cs +++ b/tests/Perspex.SceneGraph.UnitTests/TestVisual.cs @@ -20,8 +20,6 @@ namespace Perspex.SceneGraph.UnitTests public class TestVisual : Visual { - public new PerspexObject InheritanceParent => base.InheritanceParent; - public IVisual Child { get @@ -33,34 +31,34 @@ namespace Perspex.SceneGraph.UnitTests { if (Child != null) { - RemoveVisualChild(Child); + VisualChildren.Remove(Child); } if (value != null) { - AddVisualChild(value); + VisualChildren.Add(value); } } } public void AddChild(Visual v) { - AddVisualChild(v); + VisualChildren.Add(v); } public void AddChildren(IEnumerable v) { - AddVisualChildren(v); + VisualChildren.AddRange(v); } public void RemoveChild(Visual v) { - RemoveVisualChild(v); + VisualChildren.Remove(v); } public void ClearChildren() { - ClearVisualChildren(); + VisualChildren.Clear(); } } } diff --git a/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs b/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs index f4785b66c1..00d8c01d0d 100644 --- a/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs +++ b/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs @@ -23,17 +23,6 @@ namespace Perspex.SceneGraph.UnitTests Assert.Equal(target, child.GetVisualParent()); } - [Fact] - public void Added_Child_Should_Have_InheritanceParent_Set() - { - var target = new TestVisual(); - var child = new TestVisual(); - - target.AddChild(child); - - Assert.Equal(target, child.InheritanceParent); - } - [Fact] public void Added_Child_Should_Notify_VisualParent_Changed() { @@ -60,18 +49,6 @@ namespace Perspex.SceneGraph.UnitTests Assert.Null(child.GetVisualParent()); } - [Fact] - public void Removed_Child_Should_Have_InheritanceParent_Cleared() - { - var target = new TestVisual(); - var child = new TestVisual(); - - target.AddChild(child); - target.RemoveChild(child); - - Assert.Null(child.InheritanceParent); - } - [Fact] public void Clearing_Children_Should_Clear_VisualParent() { diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs index 89419b0b17..8a579223b3 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading.Tasks; @@ -83,6 +84,8 @@ namespace Perspex.Styling.UnitTests public string Name { get; set; } + public bool IsAttachedToLogicalTree { get; } + public IPerspexReadOnlyList LogicalChildren { get; set; } public ILogical LogicalParent { get; set; } @@ -91,6 +94,8 @@ namespace Perspex.Styling.UnitTests public ITemplatedControl TemplatedParent { get; } + IObservable IStyleable.StyleDetach { get; } + public IPropertyBag InheritanceParent { get diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs index 80c3e4f370..f7ce2beaa9 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading.Tasks; @@ -113,6 +114,8 @@ namespace Perspex.Styling.UnitTests public string Name { get; set; } + public bool IsAttachedToLogicalTree { get; } + public IPerspexReadOnlyList LogicalChildren { get; set; } public ILogical LogicalParent { get; set; } @@ -131,6 +134,8 @@ namespace Perspex.Styling.UnitTests IPerspexReadOnlyList IStyleable.Classes => Classes; + IObservable IStyleable.StyleDetach { get; } + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); diff --git a/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs b/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs index 3b444ec77f..9669904af6 100644 --- a/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs +++ b/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs @@ -17,7 +17,7 @@ namespace Perspex.Styling.UnitTests { var scheduler = new TestScheduler(); var source = scheduler.CreateColdObservable(); - var target = new StyleActivator(new[] { source }, ActivatorMode.And); + var target = StyleActivator.And(new[] { source }); Assert.Equal(0, source.Subscriptions.Count); target.Subscribe(_ => { }); @@ -29,7 +29,7 @@ namespace Perspex.Styling.UnitTests { var scheduler = new TestScheduler(); var source = scheduler.CreateColdObservable(); - var target = new StyleActivator(new[] { source }, ActivatorMode.And); + var target = StyleActivator.And(new[] { source }); var dispose = target.Subscribe(_ => { }); Assert.Equal(1, source.Subscriptions.Count); @@ -44,7 +44,7 @@ namespace Perspex.Styling.UnitTests public void Activator_And_Should_Follow_Single_Input() { var inputs = new[] { new TestSubject(false) }; - var target = new StyleActivator(inputs, ActivatorMode.And); + var target = StyleActivator.And(inputs); var result = new TestObserver(); target.Subscribe(result); @@ -68,7 +68,7 @@ namespace Perspex.Styling.UnitTests new TestSubject(false), new TestSubject(true), }; - var target = new StyleActivator(inputs, ActivatorMode.And); + var target = StyleActivator.And(inputs); var result = new TestObserver(); target.Subscribe(result); @@ -85,7 +85,7 @@ namespace Perspex.Styling.UnitTests } [Fact] - public void Activator_And_Should_Not_Unsubscribe_All_When_Input_Completes_On_True() + public void Activator_And_Should_Complete_When_Input_Completes_On_False() { var inputs = new[] { @@ -93,24 +93,22 @@ namespace Perspex.Styling.UnitTests new TestSubject(false), new TestSubject(true), }; - var target = new StyleActivator(inputs, ActivatorMode.And); + var target = StyleActivator.And(inputs); var result = new TestObserver(); + var completed = false; - target.Subscribe(result); - Assert.False(result.GetValue()); - inputs[0].OnNext(true); + target.Subscribe(_ => { }, () => completed = true); + inputs[0].OnNext(false); inputs[0].OnCompleted(); - Assert.Equal(0, inputs[0].SubscriberCount); - Assert.Equal(1, inputs[1].SubscriberCount); - Assert.Equal(1, inputs[2].SubscriberCount); + Assert.True(completed); } [Fact] public void Activator_Or_Should_Follow_Single_Input() { var inputs = new[] { new TestSubject(false) }; - var target = new StyleActivator(inputs, ActivatorMode.Or); + var target = StyleActivator.Or(inputs); var result = new TestObserver(); target.Subscribe(result); @@ -134,7 +132,7 @@ namespace Perspex.Styling.UnitTests new TestSubject(false), new TestSubject(true), }; - var target = new StyleActivator(inputs, ActivatorMode.Or); + var target = StyleActivator.Or(inputs); var result = new TestObserver(); target.Subscribe(result); @@ -158,7 +156,7 @@ namespace Perspex.Styling.UnitTests new TestSubject(false), new TestSubject(true), }; - var target = new StyleActivator(inputs, ActivatorMode.Or); + var target = StyleActivator.Or(inputs); var result = new TestObserver(); target.Subscribe(result); @@ -180,7 +178,7 @@ namespace Perspex.Styling.UnitTests Observable.Return(false), }; - var target = new StyleActivator(inputs, ActivatorMode.Or); + var target = StyleActivator.Or(inputs); var completed = false; target.Subscribe(_ => { }, () => completed = true); diff --git a/tests/Perspex.Styling.UnitTests/StyleTests.cs b/tests/Perspex.Styling.UnitTests/StyleTests.cs index c484389cbf..f915f4169c 100644 --- a/tests/Perspex.Styling.UnitTests/StyleTests.cs +++ b/tests/Perspex.Styling.UnitTests/StyleTests.cs @@ -188,7 +188,7 @@ namespace Perspex.Styling.UnitTests } [Fact] - public void Style_Should_Detach_When_Removed_From_Visual_Tree() + public void Style_Should_Detach_When_Removed_From_Logical_Tree() { Border border; diff --git a/tests/Perspex.Styling.UnitTests/TestControlBase.cs b/tests/Perspex.Styling.UnitTests/TestControlBase.cs index 64a0b67b2c..82ab942a09 100644 --- a/tests/Perspex.Styling.UnitTests/TestControlBase.cs +++ b/tests/Perspex.Styling.UnitTests/TestControlBase.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive; using System.Reactive.Subjects; using Perspex.Collections; using Perspex.Controls; @@ -40,6 +41,8 @@ namespace Perspex.Styling.UnitTests IPerspexReadOnlyList IStyleable.Classes => Classes; + IObservable IStyleable.StyleDetach { get; } + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); diff --git a/tests/Perspex.Styling.UnitTests/TestRoot.cs b/tests/Perspex.Styling.UnitTests/TestRoot.cs index 2f613e4e51..3223a033c6 100644 --- a/tests/Perspex.Styling.UnitTests/TestRoot.cs +++ b/tests/Perspex.Styling.UnitTests/TestRoot.cs @@ -10,7 +10,7 @@ using Perspex.Rendering; namespace Perspex.Styling.UnitTests { - internal class TestRoot : Decorator, ILayoutRoot, IRenderRoot + internal class TestRoot : Decorator, ILayoutRoot, IRenderRoot, IStyleRoot { public Size ClientSize => new Size(100, 100); diff --git a/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs b/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs index f8d576d4de..1348b1d662 100644 --- a/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs +++ b/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Reactive; using System.Reactive.Subjects; using Perspex.Collections; using Perspex.Controls; @@ -46,6 +47,8 @@ namespace Perspex.Styling.UnitTests IPerspexReadOnlyList IStyleable.Classes => Classes; + IObservable IStyleable.StyleDetach { get; } + public IObservable GetObservable(PerspexProperty property) { throw new NotImplementedException(); From b858d01c6195cf10637ec089e2a5b5611a98fa5d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Dec 2015 23:42:27 +0000 Subject: [PATCH 17/36] Fix DevTools bug. --- src/Perspex.Diagnostics/ViewModels/TreeNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Perspex.Diagnostics/ViewModels/TreeNode.cs b/src/Perspex.Diagnostics/ViewModels/TreeNode.cs index 7caf3ac075..2ecb1939b7 100644 --- a/src/Perspex.Diagnostics/ViewModels/TreeNode.cs +++ b/src/Perspex.Diagnostics/ViewModels/TreeNode.cs @@ -21,7 +21,7 @@ namespace Perspex.Diagnostics.ViewModels var classesChanged = Observable.FromEventPattern< NotifyCollectionChangedEventHandler, - NotifyCollectionChangedEventHandler>( + NotifyCollectionChangedEventArgs>( x => control.Classes.CollectionChanged += x, x => control.Classes.CollectionChanged -= x); From db1e20acc865264760ffc4a4eb61323c3377700f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Dec 2015 14:27:39 +0000 Subject: [PATCH 18/36] Only allow pseudoclasses to be set by control. Limit adding/removing pseudoclasses to IPseudoClasses interface. --- samples/TestApplicationShared/GalleryStyle.cs | 4 +- samples/TestApplicationShared/MainWindow.cs | 2 +- .../Views/MainWindow.paml | 4 +- src/Perspex.Base/Collections/PerspexList.cs | 10 + src/Perspex.Controls/Button.cs | 4 +- src/Perspex.Controls/Classes.cs | 228 +++++++++++++++++- src/Perspex.Controls/Control.cs | 13 +- src/Perspex.Controls/ControlExtensions.cs | 30 +++ src/Perspex.Controls/IPseudoClasses.cs | 25 ++ src/Perspex.Controls/ItemsControl.cs | 21 +- .../Mixins/SelectableMixin.cs | 4 +- src/Perspex.Controls/Perspex.Controls.csproj | 1 + .../Primitives/SelectingItemsControl.cs | 9 +- src/Perspex.Controls/TreeView.cs | 9 +- .../ClassesTests.cs | 153 ++++++++++++ .../Perspex.Controls.UnitTests.csproj | 1 + 16 files changed, 464 insertions(+), 54 deletions(-) create mode 100644 src/Perspex.Controls/IPseudoClasses.cs create mode 100644 tests/Perspex.Controls.UnitTests/ClassesTests.cs diff --git a/samples/TestApplicationShared/GalleryStyle.cs b/samples/TestApplicationShared/GalleryStyle.cs index ecac60c33a..b29e22d2ec 100644 --- a/samples/TestApplicationShared/GalleryStyle.cs +++ b/samples/TestApplicationShared/GalleryStyle.cs @@ -19,7 +19,7 @@ namespace TestApplication { this.AddRange(new[] { - new Style (s => s.Class(":container").OfType ()) + new Style (s => s.Class("container").OfType ()) { Setters = new[] { @@ -27,7 +27,7 @@ namespace TestApplication } }, - new Style(s => s.Class(":container").OfType().Child().Child().Child().Child().Child().OfType()) + new Style(s => s.Class("container").OfType().Child().Child().Child().Child().Child().OfType()) { Setters = new[] { diff --git a/samples/TestApplicationShared/MainWindow.cs b/samples/TestApplicationShared/MainWindow.cs index 7e05c01a35..e702b6f4a5 100644 --- a/samples/TestApplicationShared/MainWindow.cs +++ b/samples/TestApplicationShared/MainWindow.cs @@ -104,7 +104,7 @@ namespace TestApplication }; - container.Classes.Add(":container"); + container.Classes.Add("container"); window.Show(); return window; diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml index c7680382d4..c19220deb7 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml @@ -5,11 +5,11 @@ Title="Perspex Test Application" Width="800" Height="600"> - + - + diff --git a/src/Perspex.Base/Collections/PerspexList.cs b/src/Perspex.Base/Collections/PerspexList.cs index 120f62d82f..3cb3b34ad0 100644 --- a/src/Perspex.Base/Collections/PerspexList.cs +++ b/src/Perspex.Base/Collections/PerspexList.cs @@ -248,6 +248,16 @@ namespace Perspex.Collections return _inner.GetEnumerator(); } + /// + /// Gets a range of items from the collection. + /// + /// The first index to remove. + /// The number of items to remove. + public IEnumerable GetRange(int index, int count) + { + return _inner.GetRange(index, count); + } + /// /// Gets the index of the specified item in the collection. /// diff --git a/src/Perspex.Controls/Button.cs b/src/Perspex.Controls/Button.cs index c334007b70..71dbbf22e0 100644 --- a/src/Perspex.Controls/Button.cs +++ b/src/Perspex.Controls/Button.cs @@ -215,7 +215,7 @@ namespace Perspex.Controls { base.OnPointerPressed(e); - Classes.Add(":pressed"); + PseudoClasses.Add(":pressed"); e.Device.Capture(this); e.Handled = true; @@ -231,7 +231,7 @@ namespace Perspex.Controls base.OnPointerReleased(e); e.Device.Capture(null); - Classes.Remove(":pressed"); + PseudoClasses.Remove(":pressed"); e.Handled = true; if (ClickMode == ClickMode.Release && Classes.Contains(":pointerover")) diff --git a/src/Perspex.Controls/Classes.cs b/src/Perspex.Controls/Classes.cs index 7a0d79de2b..61f4e8368d 100644 --- a/src/Perspex.Controls/Classes.cs +++ b/src/Perspex.Controls/Classes.cs @@ -1,39 +1,253 @@ // 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.Collections; namespace Perspex.Controls { - public class Classes : PerspexList + /// + /// Holds a collection of style classes for an . + /// + /// + /// Similar to CSS, each control may have any number of styling classes applied. + /// + public class Classes : PerspexList, IPseudoClasses { + /// + /// Initializes a new instance of the class. + /// public Classes() - { + { } + /// + /// Initializes a new instance of the class. + /// + /// The initial items. public Classes(IEnumerable items) : base(items) { } + /// + /// Initializes a new instance of the class. + /// + /// The initial items. public Classes(params string[] items) : base(items) { } - public override void Add(string item) + /// + /// Adds a style class to the collection. + /// + /// The class name. + /// + /// Only standard classes may be added via this method. To add pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override void Add(string name) + { + ThrowIfPseudoclass(name, "added"); + + if (!Contains(name)) + { + base.Add(name); + } + } + + /// + /// Adds a style classes to the collection. + /// + /// The class names. + /// + /// Only standard classes may be added via this method. To add pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override void AddRange(IEnumerable names) + { + var c = new List(); + + foreach (var name in names) + { + ThrowIfPseudoclass(name, "added"); + + if (!Contains(name)) + { + c.Add(name); + } + } + + base.AddRange(c); + } + + /// + /// Inserts a style class into the collection. + /// + /// The index to insert the class at. + /// The class name. + /// + /// Only standard classes may be added via this method. To add pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override void Insert(int index, string name) + { + ThrowIfPseudoclass(name, "added"); + + if (!Contains(name)) + { + base.Insert(index, name); + } + } + + /// + /// Inserts style classes into the collection. + /// + /// The index to insert the class at. + /// The class names. + /// + /// Only standard classes may be added via this method. To add pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override void InsertRange(int index, IEnumerable names) + { + var c = new List(); + + foreach (var name in names) + { + ThrowIfPseudoclass(name, "added"); + + if (!Contains(name)) + { + c.Add(name); + } + } + + base.InsertRange(index, c); + } + + /// + /// Removes a style class from the collection. + /// + /// The class name. + /// + /// Only standard classes may be removed via this method. To remove pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override bool Remove(string name) + { + ThrowIfPseudoclass(name, "removed"); + return base.Remove(name); + } + + /// + /// Removes style classes from the collection. + /// + /// The class name. + /// + /// Only standard classes may be removed via this method. To remove pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override void RemoveAll(IEnumerable names) { - if (!Contains(item)) + var c = new List(); + + foreach (var name in names) { - base.Add(item); + ThrowIfPseudoclass(name, "removed"); + + if (!Contains(name)) + { + c.Add(name); + } } + + base.RemoveAll(c); + } + + /// + /// Removes a style class from the collection. + /// + /// The index of the class in the collection. + /// + /// Only standard classes may be removed via this method. To remove pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override void RemoveAt(int index) + { + var name = this[index]; + ThrowIfPseudoclass(name, "removed"); + base.RemoveAt(index); } - public override void AddRange(IEnumerable items) + /// + /// Removes style classes from the collection. + /// + /// The first index to remove. + /// The number of items to remove. + public override void RemoveRange(int index, int count) { - base.AddRange(items.Where(x => !Contains(x))); + var names = GetRange(index, count); + base.RemoveRange(index, count); + } + + /// + /// Removes all non-pseudoclasses in the collection and adds a new set. + /// + /// The new contents of the collection. + public void Replace(IList source) + { + var toRemove = new List(); + + foreach (var name in source) + { + ThrowIfPseudoclass(name, "added"); + } + + foreach (var name in this) + { + if (!name.StartsWith(":")) + { + toRemove.Add(name); + } + } + + base.RemoveAll(toRemove); + base.AddRange(source); + } + + /// + void IPseudoClasses.Add(string name) + { + if (!Contains(name)) + { + base.Add(name); + } + } + + /// + bool IPseudoClasses.Remove(string name) + { + return base.Remove(name); + } + + private void ThrowIfPseudoclass(string name, string operation) + { + if (name.StartsWith(":")) + { + throw new ArgumentException( + $"The pseudoclass '{name}' may only be {operation} by the control itself."); + } } } } diff --git a/src/Perspex.Controls/Control.cs b/src/Perspex.Controls/Control.cs index ea20401142..e568b37244 100644 --- a/src/Perspex.Controls/Control.cs +++ b/src/Perspex.Controls/Control.cs @@ -146,8 +146,7 @@ namespace Perspex.Controls { if (_classes != value) { - _classes.Clear(); - _classes.AddRange(value); + _classes.Replace(value); } } } @@ -307,6 +306,12 @@ namespace Perspex.Controls } } + /// + /// Gets the collection in a form that allows adding and removing + /// pseudoclasses. + /// + protected IPseudoClasses PseudoClasses => Classes; + /// /// Sets the control's logical parent. /// @@ -382,11 +387,11 @@ namespace Perspex.Controls { if (selector((T)e.NewValue)) { - ((Control)e.Sender).Classes.Add(className); + ((Control)e.Sender).PseudoClasses.Add(className); } else { - ((Control)e.Sender).Classes.Remove(className); + ((Control)e.Sender).PseudoClasses.Remove(className); } }); } diff --git a/src/Perspex.Controls/ControlExtensions.cs b/src/Perspex.Controls/ControlExtensions.cs index 6667810f24..5948331652 100644 --- a/src/Perspex.Controls/ControlExtensions.cs +++ b/src/Perspex.Controls/ControlExtensions.cs @@ -70,5 +70,35 @@ namespace Perspex.Controls .Select(x => (x as INameScope) ?? NameScope.GetNameScope(x)) .FirstOrDefault(x => x != null); } + + /// + /// Adds or removes a pseudoclass depending on a boolean value. + /// + /// The pseudoclasses collection. + /// The name of the pseudoclass to set. + /// True to add the pseudoclass or false to remove. + public static void Set(this IPseudoClasses classes, string name, bool value) + { + if (value) + { + classes.Add(name); + } + else + { + classes.Remove(name); + } + } + + /// + /// Sets a pseudoclass depending on an observable trigger. + /// + /// The pseudoclasses collection. + /// The name of the pseudoclass to set. + /// The trigger: true adds the pseudoclass, false removes. + /// A disposable used to cancel the subscription. + public static IDisposable Set(this IPseudoClasses classes, string name, IObservable trigger) + { + return trigger.Subscribe(x => classes.Set(name, x)); + } } } diff --git a/src/Perspex.Controls/IPseudoClasses.cs b/src/Perspex.Controls/IPseudoClasses.cs new file mode 100644 index 0000000000..87e5a9259a --- /dev/null +++ b/src/Perspex.Controls/IPseudoClasses.cs @@ -0,0 +1,25 @@ +// 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; + +namespace Perspex.Controls +{ + /// + /// Exposes an interface for setting pseudoclasses on a collection. + /// + public interface IPseudoClasses + { + /// + /// Adds a pseudoclass to the collection. + /// + /// The pseudoclass name. + void Add(string name); + + /// + /// Removes a pseudoclass from the collection. + /// + /// The pseudoclass name. + bool Remove(string name); + } +} diff --git a/src/Perspex.Controls/ItemsControl.cs b/src/Perspex.Controls/ItemsControl.cs index 017434bfac..9a427ae59b 100644 --- a/src/Perspex.Controls/ItemsControl.cs +++ b/src/Perspex.Controls/ItemsControl.cs @@ -64,7 +64,7 @@ namespace Perspex.Controls /// public ItemsControl() { - Classes.Add(":empty"); + PseudoClasses.Add(":empty"); SubscribeToItems(_items); } @@ -302,15 +302,7 @@ namespace Perspex.Controls } var collection = sender as ICollection; - - if (collection.Count == 0) - { - Classes.Add(":empty"); - } - else - { - Classes.Remove(":empty"); - } + PseudoClasses.Set(":empty", collection.Count == 0); } /// @@ -367,14 +359,7 @@ namespace Perspex.Controls /// private void SubscribeToItems(IEnumerable items) { - if (items == null || items.Count() == 0) - { - Classes.Add(":empty"); - } - else - { - Classes.Remove(":empty"); - } + PseudoClasses.Set(":empty", items == null || items.Count() == 0); var incc = items as INotifyCollectionChanged; diff --git a/src/Perspex.Controls/Mixins/SelectableMixin.cs b/src/Perspex.Controls/Mixins/SelectableMixin.cs index 75abe9a849..21b04f9702 100644 --- a/src/Perspex.Controls/Mixins/SelectableMixin.cs +++ b/src/Perspex.Controls/Mixins/SelectableMixin.cs @@ -52,7 +52,7 @@ namespace Perspex.Controls.Mixins { if ((bool)x.NewValue) { - sender.Classes.Add(":selected"); + ((IPseudoClasses)sender.Classes).Add(":selected"); if (((IVisual)sender).IsAttachedToVisualTree) { @@ -61,7 +61,7 @@ namespace Perspex.Controls.Mixins } else { - sender.Classes.Remove(":selected"); + ((IPseudoClasses)sender.Classes).Remove(":selected"); } sender.RaiseEvent(new RoutedEventArgs diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index b554a5a663..b85fe8461b 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -47,6 +47,7 @@ + diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 999766f8af..20181a90fc 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -546,14 +546,7 @@ namespace Perspex.Controls.Primitives } else { - if (selected) - { - container.Classes.Add(":selected"); - } - else - { - container.Classes.Remove(":selected"); - } + ((IPseudoClasses)container.Classes).Set(":selected", selected); } } finally diff --git a/src/Perspex.Controls/TreeView.cs b/src/Perspex.Controls/TreeView.cs index f479f01238..2dcb136526 100644 --- a/src/Perspex.Controls/TreeView.cs +++ b/src/Perspex.Controls/TreeView.cs @@ -186,14 +186,7 @@ namespace Perspex.Controls } else { - if (selected) - { - container.Classes.Add(":selected"); - } - else - { - container.Classes.Remove(":selected"); - } + ((IPseudoClasses)container.Classes).Set(":selected", selected); } } } diff --git a/tests/Perspex.Controls.UnitTests/ClassesTests.cs b/tests/Perspex.Controls.UnitTests/ClassesTests.cs new file mode 100644 index 0000000000..9669f4ea9c --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/ClassesTests.cs @@ -0,0 +1,153 @@ +// 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 Xunit; + +namespace Perspex.Controls.UnitTests +{ + public class ClassesTests + { + [Fact] + public void Duplicates_Should_Not_Be_Added() + { + var target = new Classes(); + + target.Add("foo"); + target.Add("foo"); + + Assert.Equal(new[] { "foo" }, target); + } + + [Fact] + public void Duplicates_Should_Not_Be_Added_Via_AddRange() + { + var target = new Classes(); + + target.Add("foo"); + target.AddRange(new[] { "foo", "bar" }); + + Assert.Equal(new[] { "foo", "bar" }, target); + } + + [Fact] + public void Duplicates_Should_Not_Be_Added_Via_Pseudoclasses() + { + var target = new Classes(); + var ps = (IPseudoClasses)target; + + ps.Add(":foo"); + ps.Add(":foo"); + + Assert.Equal(new[] { ":foo" }, target); + } + + [Fact] + public void Duplicates_Should_Not_Be_Inserted() + { + var target = new Classes(); + + target.Add("foo"); + target.Insert(0, "foo"); + + Assert.Equal(new[] { "foo" }, target); + } + + [Fact] + public void Duplicates_Should_Not_Be_Inserted_Via_InsertRange() + { + var target = new Classes(); + + target.Add("foo"); + target.InsertRange(1, new[] { "foo", "bar" }); + + Assert.Equal(new[] { "foo", "bar" }, target); + } + + [Fact] + public void Should_Not_Be_Able_To_Add_Pseudoclass() + { + var target = new Classes(); + + Assert.Throws(() => target.Add(":foo")); + } + + [Fact] + public void Should_Not_Be_Able_To_Add_Pseudoclasses_Via_AddRange() + { + var target = new Classes(); + + Assert.Throws(() => target.AddRange(new[] { "foo", ":bar" })); + } + + [Fact] + public void Should_Not_Be_Able_To_Insert_Pseudoclass() + { + var target = new Classes(); + + Assert.Throws(() => target.Insert(0, ":foo")); + } + + [Fact] + public void Should_Not_Be_Able_To_Insert_Pseudoclasses_Via_InsertRange() + { + var target = new Classes(); + + Assert.Throws(() => target.InsertRange(0, new[] { "foo", ":bar" })); + } + + [Fact] + public void Should_Not_Be_Able_To_Remove_Pseudoclass() + { + var target = new Classes(); + + Assert.Throws(() => target.Remove(":foo")); + } + + [Fact] + public void Should_Not_Be_Able_To_Remove_Pseudoclasses_Via_RemoveAll() + { + var target = new Classes(); + + Assert.Throws(() => target.RemoveAll(new[] { "foo", ":bar" })); + } + + [Fact] + public void Should_Not_Be_Able_To_Remove_Pseudoclasses_Via_RemoveRange() + { + var target = new Classes(); + + Assert.Throws(() => target.RemoveRange(0, 1)); + } + + [Fact] + public void Should_Not_Be_Able_To_Remove_Pseudoclass_Via_RemoveAt() + { + var target = new Classes(); + + ((IPseudoClasses)target).Add(":foo"); + + Assert.Throws(() => target.RemoveAt(0)); + } + + [Fact] + public void Replace_Should_Not_Replace_Pseudoclasses() + { + var target = new Classes("foo", "bar"); + + ((IPseudoClasses)target).Add(":baz"); + + target.Replace(new[] { "qux" }); + + Assert.Equal(new[] { ":baz", "qux" }, target); + } + + [Fact] + public void Replace_Should_Not_Accept_Pseudoclasses() + { + var target = new Classes(); + + Assert.Throws(() => target.Replace(new[] { ":qux" })); + } + } +} diff --git a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index 49ca24dba4..19078b5b2e 100644 --- a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -81,6 +81,7 @@ + From 75dcd93bf1fa586085704adbb59aab8aa119e2a8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Dec 2015 16:04:02 +0000 Subject: [PATCH 19/36] Allow changing color of MenuItem via styles. --- src/Perspex.Themes.Default/MenuItem.paml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Perspex.Themes.Default/MenuItem.paml b/src/Perspex.Themes.Default/MenuItem.paml index 26034cacbd..b9ba42eac5 100644 --- a/src/Perspex.Themes.Default/MenuItem.paml +++ b/src/Perspex.Themes.Default/MenuItem.paml @@ -45,22 +45,22 @@ PlacementMode="Right" StaysOpen="True" IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}"> - + - @@ -91,22 +91,22 @@ - + - From bfc010f757afe1f404b4d297c3cbce03215509bf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Dec 2015 20:18:28 +0000 Subject: [PATCH 20/36] Initial implementation of logical scrolling. aka IScrollInfo in WPF. --- .../XamlTestApplicationPcl/TestScrollable.cs | 92 ++++++++ .../Views/MainWindow.paml | 6 + .../XamlTestApplicationPcl.csproj | 1 + src/Perspex.Controls/ContentControl.cs | 2 - src/Perspex.Controls/Perspex.Controls.csproj | 3 +- .../Presenters/ScrollContentPresenter.cs | 60 ++++- .../Primitives/IScrollInfo.cs | 97 -------- .../Primitives/IScrollable.cs | 39 ++++ .../Primitives/ScrollInfoAdapter.cs | 126 ---------- src/Perspex.Controls/ScrollViewer.cs | 5 + .../Perspex.Controls.UnitTests.csproj | 1 + .../Presenters/ScrollContentPresenterTests.cs | 32 +++ ...ScrollContentPresenterTests_IScrollable.cs | 216 ++++++++++++++++++ 13 files changed, 443 insertions(+), 237 deletions(-) create mode 100644 samples/XamlTestApplicationPcl/TestScrollable.cs delete mode 100644 src/Perspex.Controls/Primitives/IScrollInfo.cs create mode 100644 src/Perspex.Controls/Primitives/IScrollable.cs delete mode 100644 src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs create mode 100644 tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs new file mode 100644 index 0000000000..9a0bc8de73 --- /dev/null +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -0,0 +1,92 @@ +using System; +using Perspex; +using Perspex.Controls; +using Perspex.Controls.Primitives; +using Perspex.Media; + +namespace XamlTestApplication +{ + public class TestScrollable : Control, IScrollable + { + private int itemCount = 100; + private Action _invalidateScroll; + private Size _extent; + private Vector _offset; + private Size _viewport; + private Size _lineSize; + + public Action InvalidateScroll + { + get { return _invalidateScroll; } + set { _invalidateScroll = value; } + } + + Size IScrollable.Extent + { + get { return _extent; } + } + + Vector IScrollable.Offset + { + get + { + return _offset; + } + + set + { + _offset = value; + InvalidateVisual(); + } + } + + Size IScrollable.Viewport + { + get { return _viewport; } + } + + protected override Size MeasureOverride(Size availableSize) + { + using (var line = new FormattedText( + "Item 100", + TextBlock.GetFontFamily(this), + TextBlock.GetFontSize(this), + TextBlock.GetFontStyle(this), + TextAlignment.Left, + TextBlock.GetFontWeight(this))) + { + line.Constraint = availableSize; + _lineSize = line.Measure(); + return new Size(_lineSize.Width, _lineSize.Height * itemCount); + } + } + + protected override Size ArrangeOverride(Size finalSize) + { + _viewport = new Size(finalSize.Width, finalSize.Height / _lineSize.Height); + _extent = new Size(_lineSize.Width, itemCount); + InvalidateScroll(); + return finalSize; + } + + public override void Render(DrawingContext context) + { + var y = 0.0; + + for (var i = (int)_offset.Y; i < itemCount; ++i) + { + using (var line = new FormattedText( + "Item " + (i + 1), + TextBlock.GetFontFamily(this), + TextBlock.GetFontSize(this), + TextBlock.GetFontStyle(this), + TextAlignment.Left, + TextBlock.GetFontWeight(this))) + { + context.DrawText(Brushes.Black, new Point(-_offset.X, y), line); + y += _lineSize.Height; + } + } + } + } +} \ No newline at end of file diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml index c19220deb7..12440b4106 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml @@ -1,6 +1,7 @@  @@ -272,6 +273,11 @@ + + + + + \ No newline at end of file diff --git a/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj b/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj index 46358e5836..a32ae5eec6 100644 --- a/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj +++ b/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj @@ -41,6 +41,7 @@ + diff --git a/src/Perspex.Controls/ContentControl.cs b/src/Perspex.Controls/ContentControl.cs index 2899b58bcd..e8b52a2948 100644 --- a/src/Perspex.Controls/ContentControl.cs +++ b/src/Perspex.Controls/ContentControl.cs @@ -36,8 +36,6 @@ namespace Perspex.Controls public static readonly PerspexProperty VerticalContentAlignmentProperty = PerspexProperty.Register(nameof(VerticalContentAlignment)); - private IDisposable _presenterSubscription; - /// /// Initializes static members of the class. /// diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index b85fe8461b..6bd1dd9162 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -56,6 +56,7 @@ + @@ -81,9 +82,7 @@ - - diff --git a/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs b/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs index 65aa4ae4f9..1ea8b3ef41 100644 --- a/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs @@ -3,6 +3,9 @@ using System; using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Perspex.Controls.Primitives; using Perspex.Input; using Perspex.Layout; using Perspex.VisualTree; @@ -39,6 +42,7 @@ namespace Perspex.Controls.Presenters PerspexProperty.Register("CanScrollHorizontally", true); private Size _measuredExtent; + private IDisposable _scrollableSubscription; /// /// Initializes static members of the class. @@ -56,6 +60,8 @@ namespace Perspex.Controls.Presenters public ScrollContentPresenter() { AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); + + GetObservable(ChildProperty).Subscribe(ChildChanged); } /// @@ -149,19 +155,24 @@ namespace Perspex.Controls.Presenters /// protected override Size MeasureOverride(Size availableSize) { - var content = Content as ILayoutable; + var child = Child; - if (content != null) + if (child != null) { - var measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity); + var measureSize = availableSize; - if (!CanScrollHorizontally) + if (_scrollableSubscription == null) { - measureSize = measureSize.WithWidth(availableSize.Width); + measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity); + + if (!CanScrollHorizontally) + { + measureSize = measureSize.WithWidth(availableSize.Width); + } } - content.Measure(measureSize); - var size = content.DesiredSize; + child.Measure(measureSize); + var size = child.DesiredSize; _measuredExtent = size; return size.Constrain(availableSize); } @@ -175,16 +186,21 @@ namespace Perspex.Controls.Presenters protected override Size ArrangeOverride(Size finalSize) { var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable; + var offset = default(Vector); - Viewport = finalSize; - Extent = _measuredExtent; + if (_scrollableSubscription == null) + { + Viewport = finalSize; + Extent = _measuredExtent; + offset = Offset; + } if (child != null) { var size = new Size( Math.Max(finalSize.Width, child.DesiredSize.Width), Math.Max(finalSize.Height, child.DesiredSize.Height)); - child.Arrange(new Rect((Point)(-Offset), size)); + child.Arrange(new Rect((Point)(-offset), size)); return finalSize; } @@ -209,6 +225,30 @@ namespace Perspex.Controls.Presenters e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect); } + private void ChildChanged(IControl child) + { + var scrollable = child as IScrollable; + + _scrollableSubscription?.Dispose(); + _scrollableSubscription = null; + + if (scrollable != null) + { + scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable); + _scrollableSubscription = new CompositeDisposable( + GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x), + Disposable.Create(() => scrollable.InvalidateScroll = null)); + UpdateFromScrollable(scrollable); + } + } + + private void UpdateFromScrollable(IScrollable scrollable) + { + Viewport = scrollable.Viewport; + Extent = scrollable.Extent; + Offset = scrollable.Offset; + } + private static Vector ValidateOffset(ScrollContentPresenter o, Vector value) { return ScrollViewer.CoerceOffset( diff --git a/src/Perspex.Controls/Primitives/IScrollInfo.cs b/src/Perspex.Controls/Primitives/IScrollInfo.cs deleted file mode 100644 index 59dbc085e6..0000000000 --- a/src/Perspex.Controls/Primitives/IScrollInfo.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Perspex.Controls.Primitives -{ - public interface IScrollInfoBase - { - /// - /// ScrollOwner is the container that controls any scrollbars, headers, etc... that are dependant - /// on this IScrollInfo's properties. Implementers of IScrollInfo should call InvalidateScrollInfo() - /// on this object when properties change. - /// - ScrollViewer ScrollOwner { get; set; } - - Rect MakeVisible(Visual visual, Rect rectangle); - } - - public interface IVerticalScrollInfo : IScrollInfoBase - { - /// - /// VerticalOffset is the vertical offset into the scrolled content that represents the first unit visible. - /// - double VerticalOffset { get; set; } - - /// - /// ExtentHeight contains the full vertical range of the scrolled content. - /// - double ExtentHeight { get; } - - /// - /// ViewportHeight contains the currently visible vertical range of the scrolled content. - /// - double ViewportHeight { get; } - - /// - /// This property indicates to the IScrollInfo whether or not it can scroll in the vertical given dimension. - /// - bool CanVerticallyScroll { get; set; } - - void LineDown(); - - void LineUp(); - - void MouseWheelDown(); - - void MouseWheelUp(); - - void PageDown(); - - void PageUp(); - } - - public interface IHorizontalScrollInfo : IScrollInfoBase - { - /// - /// ExtentWidth contains the full horizontal range of the scrolled content. - /// - double ExtentWidth { get; } - - /// - /// ViewportWidth contains the currently visible horizontal range of the scrolled content. - /// - double ViewportWidth { get; } - - /// - /// HorizontalOffset is the horizontal offset into the scrolled content that represents the first unit visible. - /// - double HorizontalOffset { get; set; } - - /// - /// This property indicates to the IScrollInfo whether or not it can scroll in the horizontal given dimension. - /// - bool CanHorizontallyScroll { get; set; } - - void LineLeft(); - - void LineRight(); - - void MouseWheelLeft(); - - void MouseWheelRight(); - - void PageLeft(); - - void PageRight(); - } - - public interface IScrollInfo : IHorizontalScrollInfo, IVerticalScrollInfo - { - } -} diff --git a/src/Perspex.Controls/Primitives/IScrollable.cs b/src/Perspex.Controls/Primitives/IScrollable.cs new file mode 100644 index 0000000000..65adadb15e --- /dev/null +++ b/src/Perspex.Controls/Primitives/IScrollable.cs @@ -0,0 +1,39 @@ +// 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; + +namespace Perspex.Controls.Primitives +{ + /// + /// Interface implemented by controls that handle their own scrolling when placed inside a + /// . + /// + public interface IScrollable + { + /// + /// Gets or sets the invalidation method which notifies the attached + /// of a change in or . + /// + /// + /// This property is set by the parent when the + /// is placed inside it. + /// + Action InvalidateScroll { get; set; } + + /// + /// Gets the extent of the scrollable content, in logical units + /// + Size Extent { get; } + + /// + /// Gets or sets the current scroll offset, in logical units. + /// + Vector Offset { get; set; } + + /// + /// Gets the size of the viewport, in logical units. + /// + Size Viewport { get; } + } +} diff --git a/src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs b/src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs deleted file mode 100644 index 8ea94f6fb6..0000000000 --- a/src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -namespace Perspex.Controls.Primitives -{ - public class ScrollInfoAdapter : IScrollInfo - { - private readonly IScrollInfoBase _nfo; - public ScrollInfoAdapter(IScrollInfoBase nfo) - { - _nfo = nfo; - } - - public ScrollViewer ScrollOwner - { - get { return _nfo.ScrollOwner; } - set { _nfo.ScrollOwner = value; } - } - - public double ExtentWidth => (_nfo as IHorizontalScrollInfo)?.ExtentWidth ?? 0; - - public double ViewportWidth => (_nfo as IHorizontalScrollInfo)?.ViewportWidth ?? 0; - - public double ExtentHeight => (_nfo as IVerticalScrollInfo)?.ExtentHeight ?? 0; - - public double ViewportHeight => (_nfo as IVerticalScrollInfo)?.ViewportHeight ?? 0; - - private double _horizontalOffset; - public double HorizontalOffset - { - get - { - return (_nfo as IHorizontalScrollInfo)?.HorizontalOffset ?? _horizontalOffset; - } - - set - { - var info = (_nfo as IHorizontalScrollInfo); - if (info == null) - _horizontalOffset = value; - else - info.HorizontalOffset = value; - } - } - - private double _verticalOffset; - public double VerticalOffset - { - get - { - return (_nfo as IVerticalScrollInfo)?.VerticalOffset ?? _verticalOffset; - } - - set - { - var info = (_nfo as IVerticalScrollInfo); - if (info == null) - _verticalOffset = value; - else - info.VerticalOffset = value; - } - } - - public void LineLeft() => (_nfo as IHorizontalScrollInfo)?.LineLeft(); - - public void LineRight() => (_nfo as IHorizontalScrollInfo)?.LineRight(); - - public void MouseWheelLeft() => (_nfo as IHorizontalScrollInfo)?.MouseWheelLeft(); - - public void MouseWheelRight() => (_nfo as IHorizontalScrollInfo)?.MouseWheelRight(); - - public void PageLeft() => (_nfo as IHorizontalScrollInfo)?.PageLeft(); - - public Rect MakeVisible(Visual visual, Rect rectangle) => _nfo.MakeVisible(visual, rectangle); - - public void PageRight() => (_nfo as IHorizontalScrollInfo)?.PageRight(); - - public void LineDown() => (_nfo as IVerticalScrollInfo)?.LineDown(); - - public void LineUp() => (_nfo as IVerticalScrollInfo)?.LineUp(); - - public void MouseWheelDown() => (_nfo as IVerticalScrollInfo)?.MouseWheelDown(); - - public void MouseWheelUp() => (_nfo as IVerticalScrollInfo)?.MouseWheelUp(); - - public void PageDown() => (_nfo as IVerticalScrollInfo)?.PageDown(); - - public void PageUp() => (_nfo as IVerticalScrollInfo)?.PageUp(); - - private bool _canVerticallyScroll; - public bool CanVerticallyScroll - { - get - { - return (_nfo as IVerticalScrollInfo)?.CanVerticallyScroll ?? _canVerticallyScroll; - } - - set - { - var info = (_nfo as IVerticalScrollInfo); - if (info == null) - _canVerticallyScroll = value; - else - info.CanVerticallyScroll = value; - } - } - - private bool _canHorizontallyScroll; - public bool CanHorizontallyScroll - { - get - { - return (_nfo as IHorizontalScrollInfo)?.CanHorizontallyScroll ?? _canHorizontallyScroll; - } - - set - { - var info = (_nfo as IHorizontalScrollInfo); - if (info == null) - _canHorizontallyScroll = value; - else - info.CanHorizontallyScroll = value; - } - } - } -} diff --git a/src/Perspex.Controls/ScrollViewer.cs b/src/Perspex.Controls/ScrollViewer.cs index 5af724a614..b454bc6d5e 100644 --- a/src/Perspex.Controls/ScrollViewer.cs +++ b/src/Perspex.Controls/ScrollViewer.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Specialized; +using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; @@ -117,6 +120,8 @@ namespace Perspex.Controls nameof(VerticalScrollBarVisibility), ScrollBarVisibility.Auto); + private IDisposable _scrollableSubscription; + /// /// Initializes static members of the class. /// diff --git a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index 19078b5b2e..2ec1569e11 100644 --- a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -93,6 +93,7 @@ + diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs index eb913b3410..adb4bfd0fa 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs @@ -142,6 +142,35 @@ namespace Perspex.Controls.UnitTests.Presenters Assert.Equal(new Rect(-25, -25, 150, 150), content.Bounds); } + [Fact] + public void Measure_Should_Pass_Bounded_X_If_CannotScrollHorizontally() + { + var child = new TestControl(); + var target = new ScrollContentPresenter + { + Content = child, + [ScrollContentPresenter.CanScrollHorizontallyProperty] = false, + }; + + target.Measure(new Size(100, 100)); + + Assert.Equal(new Size(100, double.PositiveInfinity), child.AvailableSize); + } + + [Fact] + public void Measure_Should_Pass_Unbounded_X_If_CanScrollHorizontally() + { + var child = new TestControl(); + var target = new ScrollContentPresenter + { + Content = child, + }; + + target.Measure(new Size(100, 100)); + + Assert.Equal(Size.Infinity, child.AvailableSize); + } + [Fact] public void Arrange_Should_Set_Viewport_And_Extent_In_That_Order() { @@ -240,8 +269,11 @@ namespace Perspex.Controls.UnitTests.Presenters private class TestControl : Control { + public Size AvailableSize { get; private set; } + protected override Size MeasureOverride(Size availableSize) { + AvailableSize = availableSize; return new Size(150, 150); } } diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs new file mode 100644 index 0000000000..0dc1c31462 --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs @@ -0,0 +1,216 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Linq; +using Perspex.Controls.Presenters; +using Perspex.Controls.Primitives; +using Xunit; + +namespace Perspex.Controls.UnitTests +{ + public class ScrollContentPresenterTests_IScrollable + { + [Fact] + public void Measure_Should_Pass_Unchanged_Bounds_To_IScrollable() + { + var scrollable = new TestScrollable(); + var target = new ScrollContentPresenter + { + Content = scrollable, + }; + + target.Measure(new Size(100, 100)); + + Assert.Equal(new Size(100, 100), scrollable.AvailableSize); + } + + [Fact] + public void Arrange_Should_Not_Offset_IScrollable_Bounds() + { + var scrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + Viewport = new Size(25, 25), + }; + + var target = new ScrollContentPresenter + { + Content = scrollable, + }; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); + } + + [Fact] + public void Arrange_Should_Not_Set_Viewport_And_Extent_With_IScrollable() + { + var target = new ScrollContentPresenter + { + Content = new TestScrollable() + }; + + var changed = false; + + target.Measure(new Size(100, 100)); + + target.GetObservable(ScrollViewer.ViewportProperty).Skip(1).Subscribe(_ => changed = true); + target.GetObservable(ScrollViewer.ExtentProperty).Skip(1).Subscribe(_ => changed = true); + + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.False(changed); + } + + [Fact] + public void InvalidateScroll_Should_Be_Set_When_Set_As_Content() + { + var scrollable = new TestScrollable(); + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.ApplyTemplate(); + + Assert.NotNull(scrollable.InvalidateScroll); + } + + [Fact] + public void InvalidateScroll_Should_Be_Cleared_When_Removed_From_Content() + { + var scrollable = new TestScrollable(); + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.ApplyTemplate(); + target.Content = null; + target.ApplyTemplate(); + + Assert.Null(scrollable.InvalidateScroll); + } + + [Fact] + public void Extent_Offset_And_Viewport_Should_Be_Read_From_IScrollable() + { + var scrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + Viewport = new Size(25, 25), + }; + + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.ApplyTemplate(); + + Assert.Equal(scrollable.Extent, target.Extent); + Assert.Equal(scrollable.Offset, target.Offset); + Assert.Equal(scrollable.Viewport, target.Viewport); + + scrollable.Extent = new Size(200, 200); + scrollable.Offset = new Vector(100, 100); + scrollable.Viewport = new Size(50, 50); + + Assert.Equal(scrollable.Extent, target.Extent); + Assert.Equal(scrollable.Offset, target.Offset); + Assert.Equal(scrollable.Viewport, target.Viewport); + } + + [Fact] + public void Offset_Should_Be_Written_To_IScrollable() + { + var scrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + }; + + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.ApplyTemplate(); + + target.Offset = new Vector(25, 25); + + Assert.Equal(target.Offset, scrollable.Offset); + } + + [Fact] + public void Offset_Should_Not_Be_Written_To_IScrollable_After_Removal() + { + var scrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + }; + + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.Content = null; + target.Offset = new Vector(25, 25); + + Assert.Equal(new Vector(50, 50), scrollable.Offset); + } + + private class TestScrollable : Control, IScrollable + { + private Size _extent; + private Vector _offset; + private Size _viewport; + + public Size AvailableSize { get; private set; } + public Action InvalidateScroll { get; set; } + + public Size Extent + { + get { return _extent; } + set + { + _extent = value; + InvalidateScroll?.Invoke(); + } + } + + public Vector Offset + { + get { return _offset; } + set + { + _offset = value; + InvalidateScroll?.Invoke(); + } + } + + public Size Viewport + { + get { return _viewport; } + set + { + _viewport = value; + InvalidateScroll?.Invoke(); + } + } + + protected override Size MeasureOverride(Size availableSize) + { + AvailableSize = availableSize; + return new Size(150, 150); + } + } + } +} \ No newline at end of file From 0e3768d2b4ed6ef6634931a00b12306ea7c12030 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Dec 2015 20:34:33 +0000 Subject: [PATCH 21/36] Tweaked documentation and TestScrollable. --- samples/XamlTestApplicationPcl/TestScrollable.cs | 16 ++++------------ src/Perspex.Controls/Primitives/IScrollable.cs | 9 +++++++-- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs index 9a0bc8de73..cc5552375b 100644 --- a/samples/XamlTestApplicationPcl/TestScrollable.cs +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -9,17 +9,12 @@ namespace XamlTestApplication public class TestScrollable : Control, IScrollable { private int itemCount = 100; - private Action _invalidateScroll; private Size _extent; private Vector _offset; private Size _viewport; private Size _lineSize; - public Action InvalidateScroll - { - get { return _invalidateScroll; } - set { _invalidateScroll = value; } - } + public Action InvalidateScroll { get; set; } Size IScrollable.Extent { @@ -28,10 +23,7 @@ namespace XamlTestApplication Vector IScrollable.Offset { - get - { - return _offset; - } + get { return _offset; } set { @@ -64,8 +56,8 @@ namespace XamlTestApplication protected override Size ArrangeOverride(Size finalSize) { _viewport = new Size(finalSize.Width, finalSize.Height / _lineSize.Height); - _extent = new Size(_lineSize.Width, itemCount); - InvalidateScroll(); + _extent = new Size(_lineSize.Width, itemCount + 1); + InvalidateScroll?.Invoke(); return finalSize; } diff --git a/src/Perspex.Controls/Primitives/IScrollable.cs b/src/Perspex.Controls/Primitives/IScrollable.cs index 65adadb15e..752c02685d 100644 --- a/src/Perspex.Controls/Primitives/IScrollable.cs +++ b/src/Perspex.Controls/Primitives/IScrollable.cs @@ -12,12 +12,17 @@ namespace Perspex.Controls.Primitives public interface IScrollable { /// - /// Gets or sets the invalidation method which notifies the attached - /// of a change in or . + /// Gets or sets the scroll invalidation method. /// /// + /// + /// This method notifies the attached of a change in + /// the , or properties. + /// + /// /// This property is set by the parent when the /// is placed inside it. + /// /// Action InvalidateScroll { get; set; } From 34f0e18ee495055b584a2ce0602f5ec64c64a1e1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Dec 2015 20:59:48 +0000 Subject: [PATCH 22/36] Removed accidentally committed file. --- .../StoredText/e0ab357f917440988efaba9c0cd4a9c0 | Bin 16766 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 _NCrunch_Perspex/StoredText/e0ab357f917440988efaba9c0cd4a9c0 diff --git a/_NCrunch_Perspex/StoredText/e0ab357f917440988efaba9c0cd4a9c0 b/_NCrunch_Perspex/StoredText/e0ab357f917440988efaba9c0cd4a9c0 deleted file mode 100644 index ce9d389343b7751146df5daab975a29ff5983857..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16766 zcmeHO+in{-5GC5T0{I6$6%J4c@g-Ln0URem{g5Vz-8Mi}sIsLfYGlQgHmUvhJ%{5F zxg__NDCrhi2)0&S-sa5ka5yAym>EZhg{@*dTu0K7zyEGTb zwZzldmSMSLnWaf$a8#zX1Zu4B3L$q>^zKv|(mgY~hjS_wIB*UAf*)ost{T}%z$g@B@ zE7Undi4${${}a^k7XK;5Pk4J&*+LJc7x=J0Ve2=oOhxkea`e4+UZXF6pr>qm0qLj< zqIqh2O^H+k>}Q6z<8@z)@>3VtHBvWGo_+2i?SUDfHDYm&-V^65q?{t>46TiU^%QTZ zwSoD^BKQtZcg>!;XKuqLcJOo?_HheoBXb|$d>#PRC2AkQPMrQmp56vIEv}H0*v+kk z?CA`@7C|0rafKdJ*TlOAB<`bq+MbW`3%pw*C$)KL?S@iGsVRQ{iqe#nacZCk7g{7r#4RuF0I4cf_>Lo}3W)*o)I%cF#T{rP)Wk7x zC&Ck0SKqv}wxFZ_8B#MK^d2(*40$O#M`a&4EzC!xzr!y-*B0a>q|WQVd&pj*o0 ztho#U%KQd3i*>Hhw>K#3Bf)1PxwJi>!!~?*$F;Qy(4YRASD`VtOPJ2`+{)R1UUN>I)oP;tM~G8dt92z}2{tJug9+NXxd zntpnWPioFO0qV3N)}npZ+3674Ci7{Ya+{ZW97nK5*31#a`S$bsvu_@vrXfC$fgO32 ze8jm`d1(*KKeRLJzS+;ol#+q-+?DO!LuiBbZli@UZ2cMV{D?mPg1<}1K)!3jA>?D3 z@5#^9H`!~6I%gIOGP`U21X6Ie_wjuMT?)6eoG>KEKxQ}#A6r~GV#xJmdfL*&&d$Ye ze}s*QM&gpOd`+#TnRa}8tE-!o7E--@0-cfH=q1X0eS&Xt43XRi@>6`suT!8$ESxT= ze_Fy`X9Yl}XPV<%hVB-4l782j^?x}J>IghQnrf^4wsLmL!}s;woWF(0S{-cYIpuxW^xEI`%ta|(n)W5% z+w{80^|8$!a+|EY3g^_k7xJLV1J6VCqJ*ssSf z8V_{VxJl;q*+#7`WM0QHyPao@RXC#*>9u(A7!EQ0^x2GYK<;b#no}Fm_MNTNZAVL{ zYOk^j?ys~jO4p{@ryAYO+lShOZwp5@s>wYgdMpz}E@xO{aVM6MbEYk@_U4+4kx_a@ zab8Bp!+IDWWvrc1Jy$|f)6w+we#rr%FY!)HJ)TT^VQif?Eh|XI&pCQE4k^1w3*f+A zZ0<#Jm*=ce{(4ag$Ny>zzRy`UmUTqt^%&Qu`MBeyD`{bhdOfV_SDpnlx3;=UL~VDm z@7wW0o%y2376fy#jxxlXvV3`a#VhJvyPMOkrmH=Ga)dX@_PX0v#vy)h@;ste<>zn= z;-?DlzXZ30JJx~tkI0|T*T4GFwJ+qtZB}> z*Kr=^(CQIVhI*dyiZE}=dSxw^ao*idqZ`tw&pq|VXv)utPk{O3R{dUOHm7yJ^`rGU z3isaeo=YocJw1`-81HO-@}W83)kDL{RE|NKyPQo1zTd%Vz0O~&$6a;s&$Cz& z`psFaPTR$Nwu3X6ot8JPWjC7Y>N93G*1^cmInF=LaTcna)nY5~`_OcS?(<<>v(Jq2 z+vaS?5?nd{*Q;{n(=z0XxHQHg#i@JU8W_5+Guoe{$|O@ZAi%7k-SOou{>9KB@`Y-Rl)brdsYs(#?xTSoKX6xYc9?)uKM#-c2 zEupu7pS`?3<^A@0-8aT>xkL48eanqIHH>7?a(Eiw?UMCz>Rj)_?X06FG4ecnQgtKb zHd@h{L9J+oQhEY2#b2QWTF&Puv9#h7--jpnEWebrL zcRJp!jN&%^e6d}Ja1V z@l(sozd(7Nd03h^GJB{oPOV0;xR=f{1PM6{c!HZcymvAJ?tSxp@)kj;rO#{QyJQ>r zJAr<$xI53gsD9ed= Date: Thu, 17 Dec 2015 22:34:41 +0100 Subject: [PATCH 23/36] Reduce TabStripItem Margin value --- src/Perspex.Themes.Default/TabStrip.paml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Perspex.Themes.Default/TabStrip.paml b/src/Perspex.Themes.Default/TabStrip.paml index fbc8aefd8d..2cc73f6945 100644 --- a/src/Perspex.Themes.Default/TabStrip.paml +++ b/src/Perspex.Themes.Default/TabStrip.paml @@ -15,6 +15,6 @@ \ No newline at end of file From 44408cfc5caeaf22230d17ea2c4673d2f3841d87 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Dec 2015 23:24:35 +0000 Subject: [PATCH 24/36] ItemContainerGenerator can be null. --- src/Perspex.Controls/ItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Perspex.Controls/ItemsControl.cs b/src/Perspex.Controls/ItemsControl.cs index 9a427ae59b..16873b7f05 100644 --- a/src/Perspex.Controls/ItemsControl.cs +++ b/src/Perspex.Controls/ItemsControl.cs @@ -257,7 +257,7 @@ namespace Perspex.Controls if (e.NewValue == null) { - ItemContainerGenerator.Clear(); + ItemContainerGenerator?.Clear(); } } From ea9ba2930ae0245111ff0743dea5cc49c9a7acd3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 18 Dec 2015 00:04:53 +0000 Subject: [PATCH 25/36] Revert "Merge pull request #366 from wieslawsoltes/TabStripItem" This reverts commit 46003152f4b5c5570d99e76edbed8012cb3ecc44, reversing changes made to 44408cfc5caeaf22230d17ea2c4673d2f3841d87. --- src/Perspex.Themes.Default/TabStrip.paml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Perspex.Themes.Default/TabStrip.paml b/src/Perspex.Themes.Default/TabStrip.paml index 2cc73f6945..fbc8aefd8d 100644 --- a/src/Perspex.Themes.Default/TabStrip.paml +++ b/src/Perspex.Themes.Default/TabStrip.paml @@ -15,6 +15,6 @@ \ No newline at end of file From 0f0ae1fb7b7390d7ebf676173d8721e18f746d87 Mon Sep 17 00:00:00 2001 From: donandren Date: Fri, 18 Dec 2015 19:13:29 +0200 Subject: [PATCH 26/36] new expander control --- src/Perspex.Controls/Expander.cs | 69 ++++++++++ src/Perspex.Controls/Perspex.Controls.csproj | 1 + src/Perspex.Themes.Default/DefaultTheme.paml | 1 + src/Perspex.Themes.Default/Expander.paml | 123 ++++++++++++++++++ .../Perspex.Themes.Default.csproj | 1 + 5 files changed, 195 insertions(+) create mode 100644 src/Perspex.Controls/Expander.cs create mode 100644 src/Perspex.Themes.Default/Expander.paml diff --git a/src/Perspex.Controls/Expander.cs b/src/Perspex.Controls/Expander.cs new file mode 100644 index 0000000000..5d96107ac8 --- /dev/null +++ b/src/Perspex.Controls/Expander.cs @@ -0,0 +1,69 @@ +using Perspex.Animation; +using Perspex.Controls.Primitives; + +namespace Perspex.Controls +{ + public enum ExpandDirection + { + Down, + Up, + Left, + Right + } + + public class Expander : HeaderedContentControl + { + static Expander() + { + PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Down, ":ExpandDirectionDown"); + PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Up, ":ExpandDirectionUp"); + PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Left, ":ExpandDirectionLeft"); + PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Right, ":ExpandDirectionRight"); + + PseudoClass(IsExpandedProperty, ":expanded"); + + IsExpandedProperty.Changed.AddClassHandler(x => x.OnIsExpandedChanged); + } + + protected virtual void OnIsExpandedChanged(PerspexPropertyChangedEventArgs e) + { + IVisual visualContent = Presenter; + if (Content != null && ContentTransition != null && visualContent != null) + { + bool forward = ExpandDirection == ExpandDirection.Left || ExpandDirection == ExpandDirection.Up; + if (IsExpanded) + { + ContentTransition.Start(null, visualContent, forward); + } + else + { + ContentTransition.Start(visualContent, null, !forward); + } + } + } + + public static readonly PerspexProperty IsExpandedProperty = PerspexProperty.Register(nameof(IsExpanded), true); + + public bool IsExpanded + { + get { return GetValue(IsExpandedProperty); } + set { SetValue(IsExpandedProperty, value); } + } + + public static readonly PerspexProperty ExpandDirectionProperty = PerspexProperty.Register(nameof(ExpandDirection), ExpandDirection.Down); + + public ExpandDirection ExpandDirection + { + get { return GetValue(ExpandDirectionProperty); } + set { SetValue(ExpandDirectionProperty, value); } + } + + public static readonly PerspexProperty ContentTransitionProperty = PerspexProperty.Register(nameof(ContentTransition)); + + public IPageTransition ContentTransition + { + get { return GetValue(ContentTransitionProperty); } + set { SetValue(ContentTransitionProperty, value); } + } + } +} \ No newline at end of file diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index 6bd1dd9162..0e8f07767e 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -44,6 +44,7 @@ + diff --git a/src/Perspex.Themes.Default/DefaultTheme.paml b/src/Perspex.Themes.Default/DefaultTheme.paml index 5d8ea78ef1..e3bcf623f1 100644 --- a/src/Perspex.Themes.Default/DefaultTheme.paml +++ b/src/Perspex.Themes.Default/DefaultTheme.paml @@ -23,6 +23,7 @@ + diff --git a/src/Perspex.Themes.Default/Expander.paml b/src/Perspex.Themes.Default/Expander.paml new file mode 100644 index 0000000000..c1cb99248d --- /dev/null +++ b/src/Perspex.Themes.Default/Expander.paml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Perspex.Themes.Default/Perspex.Themes.Default.csproj b/src/Perspex.Themes.Default/Perspex.Themes.Default.csproj index f17c80fe52..cfc8544d16 100644 --- a/src/Perspex.Themes.Default/Perspex.Themes.Default.csproj +++ b/src/Perspex.Themes.Default/Perspex.Themes.Default.csproj @@ -143,6 +143,7 @@ Designer + Designer From 440fbf44fc38346dbdbc14e096af0e9a16854b4a Mon Sep 17 00:00:00 2001 From: donandren Date: Fri, 18 Dec 2015 19:42:25 +0200 Subject: [PATCH 27/36] expander some code refactoring --- src/Perspex.Controls/Expander.cs | 59 +++++++++++++----------- src/Perspex.Themes.Default/Expander.paml | 10 ++-- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/Perspex.Controls/Expander.cs b/src/Perspex.Controls/Expander.cs index 5d96107ac8..5ac926af47 100644 --- a/src/Perspex.Controls/Expander.cs +++ b/src/Perspex.Controls/Expander.cs @@ -13,57 +13,62 @@ namespace Perspex.Controls public class Expander : HeaderedContentControl { + public static readonly PerspexProperty IsExpandedProperty = + PerspexProperty.Register(nameof(IsExpanded), true); + + public static readonly PerspexProperty ExpandDirectionProperty = + PerspexProperty.Register(nameof(ExpandDirection), ExpandDirection.Down); + + public static readonly PerspexProperty ContentTransitionProperty = + PerspexProperty.Register(nameof(ContentTransition)); + static Expander() { - PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Down, ":ExpandDirectionDown"); - PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Up, ":ExpandDirectionUp"); - PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Left, ":ExpandDirectionLeft"); - PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Right, ":ExpandDirectionRight"); + PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Down, ":down"); + PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Up, ":up"); + PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Left, ":left"); + PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Right, ":right"); PseudoClass(IsExpandedProperty, ":expanded"); IsExpandedProperty.Changed.AddClassHandler(x => x.OnIsExpandedChanged); } - protected virtual void OnIsExpandedChanged(PerspexPropertyChangedEventArgs e) - { - IVisual visualContent = Presenter; - if (Content != null && ContentTransition != null && visualContent != null) - { - bool forward = ExpandDirection == ExpandDirection.Left || ExpandDirection == ExpandDirection.Up; - if (IsExpanded) - { - ContentTransition.Start(null, visualContent, forward); - } - else - { - ContentTransition.Start(visualContent, null, !forward); - } - } - } - - public static readonly PerspexProperty IsExpandedProperty = PerspexProperty.Register(nameof(IsExpanded), true); - public bool IsExpanded { get { return GetValue(IsExpandedProperty); } set { SetValue(IsExpandedProperty, value); } } - public static readonly PerspexProperty ExpandDirectionProperty = PerspexProperty.Register(nameof(ExpandDirection), ExpandDirection.Down); - public ExpandDirection ExpandDirection { get { return GetValue(ExpandDirectionProperty); } set { SetValue(ExpandDirectionProperty, value); } } - public static readonly PerspexProperty ContentTransitionProperty = PerspexProperty.Register(nameof(ContentTransition)); - public IPageTransition ContentTransition { get { return GetValue(ContentTransitionProperty); } set { SetValue(ContentTransitionProperty, value); } } + + protected virtual void OnIsExpandedChanged(PerspexPropertyChangedEventArgs e) + { + IVisual visualContent = Presenter; + + if (Content != null && ContentTransition != null && visualContent != null) + { + bool forward = ExpandDirection == ExpandDirection.Left || + ExpandDirection == ExpandDirection.Up; + if (IsExpanded) + { + ContentTransition.Start(null, visualContent, forward); + } + else + { + ContentTransition.Start(visualContent, null, !forward); + } + } + } } } \ No newline at end of file diff --git a/src/Perspex.Themes.Default/Expander.paml b/src/Perspex.Themes.Default/Expander.paml index c1cb99248d..158f6514fd 100644 --- a/src/Perspex.Themes.Default/Expander.paml +++ b/src/Perspex.Themes.Default/Expander.paml @@ -95,27 +95,27 @@ - - - - -