diff --git a/Perspex.UnitTests/Styling/SelectorTests.cs b/Perspex.UnitTests/Styling/SelectorTests.cs index 45ec6294a1..2ae75688c1 100644 --- a/Perspex.UnitTests/Styling/SelectorTests.cs +++ b/Perspex.UnitTests/Styling/SelectorTests.cs @@ -17,11 +17,106 @@ namespace Perspex.UnitTests.Styling [TestClass] public class SelectorTests { + [TestMethod] + public void Class_Matches_Control_With_Class() + { + var control = new Control1 + { + Classes = new Classes { "foo" }, + }; + + var target = control.Select().Class("foo"); + + CollectionAssert.AreEqual(new[] { true }, target.GetActivator().Take(1).ToEnumerable().ToArray()); + } + + [TestMethod] + public void Class_Doesnt_Match_Control_Without_Class() + { + var control = new Control1 + { + Classes = new Classes { "bar" }, + }; + + var target = control.Select().Class("foo"); + + CollectionAssert.AreEqual(new[] { false }, target.GetActivator().Take(1).ToEnumerable().ToArray()); + } + + [TestMethod] + public void Class_Tracks_Additions() + { + var control = new Control1(); + + var target = control.Select().Class("foo"); + var activator = target.GetActivator(); + + CollectionAssert.AreEqual(new[] { false }, activator.Take(1).ToEnumerable().ToArray()); + control.Classes.Add("foo"); + CollectionAssert.AreEqual(new[] { true }, activator.Take(1).ToEnumerable().ToArray()); + } + + [TestMethod] + public void Class_Tracks_Removals() + { + var control = new Control1 + { + Classes = new Classes { "foo" }, + }; + + var target = control.Select().Class("foo"); + var activator = target.GetActivator(); + + CollectionAssert.AreEqual(new[] { true }, activator.Take(1).ToEnumerable().ToArray()); + control.Classes.Remove("foo"); + CollectionAssert.AreEqual(new[] { false }, activator.Take(1).ToEnumerable().ToArray()); + } + + [TestMethod] + public void Id_Matches_Control_With_Correct_Id() + { + var control = new Control1 { Id = "foo" }; + var target = control.Select().Id("foo"); + + CollectionAssert.AreEqual(new[] { true }, target.GetActivator().Take(1).ToEnumerable().ToArray()); + } + + [TestMethod] + public void Id_Doesnt_Match_Control_Of_Wrong_Id() + { + var control = new Control1 { Id = "foo" }; + var target = control.Select().Id("bar"); + + CollectionAssert.AreEqual(new[] { false }, target.GetActivator().Take(1).ToEnumerable().ToArray()); + } + + [TestMethod] + public void When_Id_Matches_Control_Other_Selectors_Are_Subscribed() + { + var control = new Control1 { Id = "foo" }; + var target = control.Select().Id("foo").SubscribeCheck(); + + var result = target.GetActivator().ToEnumerable().Take(1).ToArray(); + + Assert.AreEqual(1, control.SubscribeCheckObservable.SubscribedCount); + } + + [TestMethod] + public void When_Id_Doesnt_Match_Control_Other_Selectors_Are_Not_Subscribed() + { + var control = new Control1 { Id = "foo" }; + var target = control.Select().Id("bar").SubscribeCheck(); + + var result = target.GetActivator().ToEnumerable().Take(1).ToArray(); + + Assert.AreEqual(0, control.SubscribeCheckObservable.SubscribedCount); + } + [TestMethod] public void OfType_Matches_Control_Of_Correct_Type() { - var control = new Class1(); - var target = control.Select().OfType(); + var control = new Control1(); + var target = control.Select().OfType(); CollectionAssert.AreEqual(new[] { true }, target.GetActivator().Take(1).ToEnumerable().ToArray()); } @@ -29,8 +124,8 @@ namespace Perspex.UnitTests.Styling [TestMethod] public void OfType_Doesnt_Match_Control_Of_Wrong_Type() { - var control = new Class2(); - var target = control.Select().OfType(); + var control = new Control2(); + var target = control.Select().OfType(); CollectionAssert.AreEqual(new[] { false }, target.GetActivator().Take(1).ToEnumerable().ToArray()); } @@ -38,8 +133,8 @@ namespace Perspex.UnitTests.Styling [TestMethod] public void When_OfType_Matches_Control_Other_Selectors_Are_Subscribed() { - var control = new Class1(); - var target = control.Select().OfType().SubscribeCheck(); + var control = new Control1(); + var target = control.Select().OfType().SubscribeCheck(); var result = target.GetActivator().ToEnumerable().Take(1).ToArray(); @@ -49,19 +144,19 @@ namespace Perspex.UnitTests.Styling [TestMethod] public void When_OfType_Doesnt_Match_Control_Other_Selectors_Are_Not_Subscribed() { - var control = new Class1(); - var target = control.Select().OfType().SubscribeCheck(); + var control = new Control1(); + var target = control.Select().OfType().SubscribeCheck(); var result = target.GetActivator().ToEnumerable().Take(1).ToArray(); Assert.AreEqual(0, control.SubscribeCheckObservable.SubscribedCount); } - public class Class1 : SubscribeCheck + public class Control1 : SubscribeCheck { } - public class Class2 : SubscribeCheck + public class Control2 : SubscribeCheck { } } diff --git a/Perspex.UnitTests/Styling/SubscribeCheck.cs b/Perspex.UnitTests/Styling/SubscribeCheck.cs index ed9ba74724..b9eb1ddf86 100644 --- a/Perspex.UnitTests/Styling/SubscribeCheck.cs +++ b/Perspex.UnitTests/Styling/SubscribeCheck.cs @@ -8,6 +8,7 @@ namespace Perspex.UnitTests.Styling { using System; using System.Reactive.Disposables; + using Perspex.Controls; using Perspex.Styling; using Match = Perspex.Styling.Match; @@ -27,20 +28,20 @@ namespace Perspex.UnitTests.Styling { public SubscribeCheck() { - this.Classes = Classes; + this.Classes = new Classes(); this.SubscribeCheckObservable = new TestObservable(); } - public Classes Classes - { - get; - private set; - } + public string Id { get; set; } + + public Classes Classes { get; set; } + + public TestObservable SubscribeCheckObservable { get; private set; } - public TestObservable SubscribeCheckObservable + public TemplatedControl TemplatedParent { get; - private set; + set; } public virtual void SetValue(PerspexProperty property, object value, IObservable activator) diff --git a/Perspex/ControlTemplate.cs b/Perspex/ControlTemplate.cs index 1e539cfdc2..904cfe376f 100644 --- a/Perspex/ControlTemplate.cs +++ b/Perspex/ControlTemplate.cs @@ -1,25 +1,55 @@ -namespace Perspex +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex { using System; + using System.Diagnostics.Contracts; + using System.Linq; using Perspex.Controls; public class ControlTemplate { + private Func build; + public ControlTemplate(Func build) { - this.Build = build; + Contract.Requires(build != null); + + this.build = build; } - public Func Build + public Control Build(TemplatedControl templatedParent) { - get; - private set; + Contract.Requires(templatedParent != null); + + Control root = this.build(templatedParent); + this.SetTemplatedParent(root, templatedParent); + return root; } public static ControlTemplate Create(Func build) where TControl : TemplatedControl { + Contract.Requires(build != null); + return new ControlTemplate(c => build((TControl)c)); } + + private void SetTemplatedParent(Control control, TemplatedControl templatedParent) + { + Contract.Requires(control != null); + Contract.Requires(templatedParent != null); + + control.TemplatedParent = templatedParent; + + foreach (Control child in control.VisualChildren.OfType()) + { + this.SetTemplatedParent(child, templatedParent); + } + } } } diff --git a/Perspex/Controls/Control.cs b/Perspex/Controls/Control.cs index eef91b0cbe..e5dcb712b9 100644 --- a/Perspex/Controls/Control.cs +++ b/Perspex/Controls/Control.cs @@ -70,13 +70,15 @@ namespace Perspex.Controls internal static readonly PerspexProperty ParentPropertyRW = PerspexProperty.Register("Parent"); + private Classes classes; + private Styles styles; public Control() { - this.Classes = new Classes(); - this.Classes.BeforeChanged.Subscribe(x => this.BeginDeferStyleChanges()); - this.Classes.AfterChanged.Subscribe(x => this.EndDeferStyleChanges()); + this.classes = new Classes(); + this.classes.BeforeChanged.Subscribe(x => this.BeginDeferStyleChanges()); + this.classes.AfterChanged.Subscribe(x => this.EndDeferStyleChanges()); this.GetObservableWithHistory(ParentPropertyRW).Subscribe(this.ParentChanged); @@ -145,8 +147,21 @@ namespace Perspex.Controls public Classes Classes { - get; - private set; + get + { + return this.classes; + } + + set + { + if (this.classes != value) + { + this.BeginDeferStyleChanges(); + this.classes.Clear(); + this.classes.Add(value); + this.EndDeferStyleChanges(); + } + } } public Brush Foreground @@ -203,6 +218,12 @@ namespace Perspex.Controls } } + public TemplatedControl TemplatedParent + { + get; + internal set; + } + public VerticalAlignment VerticalAlignment { get { return this.GetValue(VerticalAlignmentProperty); } diff --git a/Perspex/Styling/IStyleable.cs b/Perspex/Styling/IStyleable.cs index 988caa9f6c..3bd2eb1d33 100644 --- a/Perspex/Styling/IStyleable.cs +++ b/Perspex/Styling/IStyleable.cs @@ -19,6 +19,16 @@ namespace Perspex.Styling /// Classes Classes { get; } + /// + /// Gets the ID of the control. + /// + string Id { get; } + + /// + /// Gets the template parent of this element if the control comes from a template. + /// + TemplatedControl TemplatedParent { get; } + /// /// Binds a to a style. /// diff --git a/Perspex/Styling/Match.cs b/Perspex/Styling/Match.cs index a05cf66010..a9851c0c4c 100644 --- a/Perspex/Styling/Match.cs +++ b/Perspex/Styling/Match.cs @@ -26,6 +26,12 @@ namespace Perspex.Styling private set; } + public bool InTemplate + { + get; + set; + } + public List> Observables { get; diff --git a/Perspex/Styling/Selectors.cs b/Perspex/Styling/Selectors.cs index f7994dde95..51712705ca 100644 --- a/Perspex/Styling/Selectors.cs +++ b/Perspex/Styling/Selectors.cs @@ -38,6 +38,37 @@ namespace Perspex.Styling return match; } + public static Match Id(this Match match, string id) + { + Contract.Requires(match != null); + + if (!match.InTemplate) + { + match.Observables.Add(Observable.Return( + match.Control.TemplatedParent == null && + match.Control.Id == id)); + } + else + { + match.Observables.Add(Observable.Return( + match.Control.TemplatedParent != null && + match.Control.Id == id)); + } + + match.SelectorString += '#' + id; + return match; + } + + public static Match InTemplateOf(this Match match) + { + Contract.Requires(match != null); + + match.Observables.Add(Observable.Return(match.Control.TemplatedParent is T)); + match.InTemplate = true; + match.SelectorString += '%' + typeof(T).Name; + return match; + } + public static Match OfType(this Match match) { Contract.Requires(match != null); diff --git a/Perspex/Visual.cs b/Perspex/Visual.cs index 8099ac262f..85ddc0aa5e 100644 --- a/Perspex/Visual.cs +++ b/Perspex/Visual.cs @@ -14,6 +14,8 @@ namespace Perspex public abstract class Visual : PerspexObject { + private string id; + private Visual visualParent; public Rect Bounds @@ -22,6 +24,29 @@ namespace Perspex protected set; } + public string Id + { + get + { + return this.id; + } + + set + { + if (this.id != null) + { + throw new InvalidOperationException("ID already set."); + } + + if (this.visualParent != null) + { + throw new InvalidOperationException("Cannot set ID : control already added to tree."); + } + + this.id = value; + } + } + public virtual IEnumerable VisualChildren { get { return Enumerable.Empty(); }