Browse Source

Merge branch 'logical-tree' into leaks

pull/366/head
Steven Kirk 10 years ago
parent
commit
a6b74db847
  1. 21
      samples/TestApplicationShared/GalleryStyle.cs
  2. 1
      samples/TestApplicationShared/MainWindow.cs
  3. 2
      src/Markup/Perspex.Markup.Xaml/Data/Binding.cs
  4. 7
      src/Perspex.Application/Application.cs
  5. 5
      src/Perspex.Base/Collections/PerspexList.cs
  6. 42
      src/Perspex.Controls/ContentControl.cs
  7. 223
      src/Perspex.Controls/Control.cs
  8. 4
      src/Perspex.Controls/Decorator.cs
  9. 4
      src/Perspex.Controls/DropDown.cs
  10. 19
      src/Perspex.Controls/Generators/IItemContainerGenerator.cs
  11. 45
      src/Perspex.Controls/Generators/ItemContainer.cs
  12. 22
      src/Perspex.Controls/Generators/ItemContainerEventArgs.cs
  13. 77
      src/Perspex.Controls/Generators/ItemContainerGenerator.cs
  14. 5
      src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs
  15. 10
      src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs
  16. 25
      src/Perspex.Controls/IReparentingControl.cs
  17. 28
      src/Perspex.Controls/IReparentingHost.cs
  18. 193
      src/Perspex.Controls/ItemsControl.cs
  19. 31
      src/Perspex.Controls/LogicalTreeAttachmentEventArgs.cs
  20. 28
      src/Perspex.Controls/MenuItem.cs
  21. 136
      src/Perspex.Controls/Mixins/ContentControlMixin.cs
  22. 79
      src/Perspex.Controls/Panel.cs
  23. 10
      src/Perspex.Controls/Perspex.Controls.csproj
  24. 18
      src/Perspex.Controls/Presenters/CarouselPresenter.cs
  25. 30
      src/Perspex.Controls/Presenters/ContentPresenter.cs
  26. 58
      src/Perspex.Controls/Presenters/ItemsPresenter.cs
  27. 2
      src/Perspex.Controls/Primitives/AdornerDecorator.cs
  28. 41
      src/Perspex.Controls/Primitives/HeaderedItemsControl.cs
  29. 58
      src/Perspex.Controls/Primitives/HeaderedSelectingControl.cs
  30. 5
      src/Perspex.Controls/Primitives/PopupRoot.cs
  31. 113
      src/Perspex.Controls/Primitives/SelectingItemsControl.cs
  32. 40
      src/Perspex.Controls/Primitives/TabStrip.cs
  33. 12
      src/Perspex.Controls/Primitives/TabStripItem.cs
  34. 28
      src/Perspex.Controls/Primitives/TemplateAppliedEventArgs.cs
  35. 46
      src/Perspex.Controls/Primitives/TemplatedControl.cs
  36. 4
      src/Perspex.Controls/Primitives/Track.cs
  37. 4
      src/Perspex.Controls/ProgressBar.cs
  38. 93
      src/Perspex.Controls/TabControl.cs
  39. 29
      src/Perspex.Controls/Templates/TemplateExtensions.cs
  40. 6
      src/Perspex.Controls/TextBlock.cs
  41. 4
      src/Perspex.Controls/TextBox.cs
  42. 7
      src/Perspex.Controls/TopLevel.cs
  43. 130
      src/Perspex.SceneGraph/Visual.cs
  44. 2
      src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs
  45. 5
      src/Perspex.Styling/ILogical.cs
  46. 1
      src/Perspex.Styling/Perspex.Styling.csproj
  47. 6
      src/Perspex.Styling/Styling/IGlobalStyles.cs
  48. 2
      src/Perspex.Styling/Styling/IStyleHost.cs
  49. 12
      src/Perspex.Styling/Styling/IStyleRoot.cs
  50. 6
      src/Perspex.Styling/Styling/IStyleable.cs
  51. 2
      src/Perspex.Styling/Styling/Selector.cs
  52. 6
      src/Perspex.Styling/Styling/Selectors.cs
  53. 16
      src/Perspex.Styling/Styling/Style.cs
  54. 32
      src/Perspex.Styling/Styling/StyleActivator.cs
  55. 28
      src/Perspex.Styling/Styling/Styler.cs
  56. 3
      src/Perspex.Themes.Default/Button.paml
  57. 2
      src/Perspex.Themes.Default/DefaultTheme.paml
  58. 6
      src/Perspex.Themes.Default/MenuItem.paml
  59. 2
      src/Perspex.Themes.Default/Perspex.Themes.Default.csproj
  60. 36
      src/Perspex.Themes.Default/TabControl.paml
  61. 6
      src/Perspex.Themes.Default/TabStrip.paml
  62. 8
      src/Perspex.Themes.Default/TabStripItem.paml
  63. 3
      src/Perspex.Themes.Default/ToggleButton.paml
  64. 2
      src/Perspex.Themes.Default/TreeViewItem.paml
  65. 73
      tests/Perspex.Controls.UnitTests/ContentControlTests.cs
  66. 89
      tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs
  67. 110
      tests/Perspex.Controls.UnitTests/ControlTests.cs
  68. 3
      tests/Perspex.Controls.UnitTests/ControlTests_NameScope.cs
  69. 20
      tests/Perspex.Controls.UnitTests/EnumerableExtensions.cs
  70. 26
      tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs
  71. 4
      tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs
  72. 78
      tests/Perspex.Controls.UnitTests/HeaderedItemsControlTests .cs
  73. 58
      tests/Perspex.Controls.UnitTests/ItemsControlTests.cs
  74. 35
      tests/Perspex.Controls.UnitTests/ListBoxTests.cs
  75. 17
      tests/Perspex.Controls.UnitTests/PanelTests.cs
  76. 4
      tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
  77. 68
      tests/Perspex.Controls.UnitTests/Presenters/ContentPresenterTests.cs
  78. 3
      tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
  79. 16
      tests/Perspex.Controls.UnitTests/Primitives/PopupTests.cs
  80. 159
      tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs
  81. 99
      tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs
  82. 174
      tests/Perspex.Controls.UnitTests/TabControlTests.cs
  83. 3
      tests/Perspex.Controls.UnitTests/TestRoot.cs
  84. 7
      tests/Perspex.Controls.UnitTests/TestTemplatedControl.cs
  85. 2
      tests/Perspex.Controls.UnitTests/TreeViewTests.cs
  86. 2
      tests/Perspex.Interactivity.UnitTests/InteractiveTests.cs
  87. 1
      tests/Perspex.LeakTests/Perspex.LeakTests.csproj
  88. 124
      tests/Perspex.LeakTests/StyleTests.cs
  89. 3
      tests/Perspex.Markup.UnitTests/TestRoot.cs
  90. 3
      tests/Perspex.Markup.Xaml.UnitTests/Converters/PerspexPropertyConverterTest.cs
  91. 3
      tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs
  92. 14
      tests/Perspex.SceneGraph.UnitTests/TestVisual.cs
  93. 23
      tests/Perspex.SceneGraph.UnitTests/VisualTests.cs
  94. 5
      tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs
  95. 5
      tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs
  96. 30
      tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs
  97. 2
      tests/Perspex.Styling.UnitTests/StyleTests.cs
  98. 3
      tests/Perspex.Styling.UnitTests/TestControlBase.cs
  99. 2
      tests/Perspex.Styling.UnitTests/TestRoot.cs
  100. 3
      tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs

21
samples/TestApplicationShared/GalleryStyle.cs

@ -27,15 +27,15 @@ namespace TestApplication
}
},
new Style(s => s.Class(":container").OfType<TabControl>().Child().Child().Child().Child().Child().OfType<TabItem>())
new Style(s => s.Class(":container").OfType<TabControl>().Child().Child().Child().Child().Child().OfType<TabStripItem>())
{
Setters = new[]
{
new Setter (TemplatedControl.TemplateProperty, new FuncControlTemplate<TabItem> (TabItemTemplate)),
new Setter (TemplatedControl.TemplateProperty, new FuncControlTemplate<TabStripItem>(TabStripItemTemplate)),
}
},
new Style(s => s.Name("internalStrip").OfType<TabStrip>().Child().OfType<TabItem>())
new Style(s => s.Name("PART_TabStrip").OfType<TabStrip>().Child().OfType<TabStripItem>())
{
Setters = new[]
{
@ -44,7 +44,7 @@ namespace TestApplication
}
},
new Style(s => s.Name("internalStrip").OfType<TabStrip>().Child().OfType<TabItem>().Class(":selected"))
new Style(s => s.Name("PART_TabStrip").OfType<TabStrip>().Child().OfType<TabStripItem>().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<IPanel>(() => 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],

1
samples/TestApplicationShared/MainWindow.cs

@ -105,7 +105,6 @@ namespace TestApplication
};
container.Classes.Add(":container");
window.Show();
return window;

2
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(

7
src/Perspex.Application/Application.cs

@ -32,7 +32,7 @@ namespace Perspex
/// method.
/// - Tracks the lifetime of the application.
/// </remarks>
public class Application : IGlobalDataTemplates, IGlobalStyles
public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot
{
static Action _platformInitializationCallback;
@ -135,6 +135,11 @@ namespace Perspex
protected set;
}
/// <summary>
/// Gets the styling parent of the application, which is null.
/// </summary>
IStyleHost IStyleHost.StylingParent => null;
/// <summary>
/// Runs the application's main loop until the <see cref="ICloseable"/> is closed.
/// </summary>

5
src/Perspex.Base/Collections/PerspexList.cs

@ -52,7 +52,7 @@ namespace Perspex.Collections
/// </item>
/// </list>
/// </remarks>
public class PerspexList<T> : IPerspexList<T>, IList, INotifyCollectionChanged, INotifyPropertyChanged
public class PerspexList<T> : IPerspexList<T>, IList
{
private List<T> _inner;
@ -150,7 +150,8 @@ namespace Perspex.Collections
var e = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace,
value,
old);
old,
index);
CollectionChanged(this, e);
}
}

42
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
/// <summary>
/// Displays <see cref="Content"/> according to a <see cref="FuncDataTemplate"/>.
/// </summary>
public class ContentControl : TemplatedControl, IContentControl, IReparentingHost
public class ContentControl : TemplatedControl, IContentControl
{
/// <summary>
/// Defines the <see cref="Content"/> property.
@ -34,18 +36,14 @@ namespace Perspex.Controls
public static readonly PerspexProperty<VerticalAlignment> VerticalContentAlignmentProperty =
PerspexProperty.Register<ContentControl, VerticalAlignment>(nameof(VerticalContentAlignment));
/// <summary>
/// Initializes static members of the <see cref="Button"/> class.
/// </summary>
static ContentControl()
{
}
private IDisposable _presenterSubscription;
/// <summary>
/// Initializes a new instance of the <see cref="ContentControl"/> class.
/// Initializes static members of the <see cref="ContentControl"/> class.
/// </summary>
public ContentControl()
static ContentControl()
{
ContentControlMixin.Attach<ContentControl>(ContentProperty, x => x.LogicalChildren);
}
/// <summary>
@ -85,31 +83,15 @@ namespace Perspex.Controls
set { SetValue(VerticalContentAlignmentProperty, value); }
}
/// <summary>
/// Gets a writeable logical children collection from the host.
/// </summary>
IPerspexList<ILogical> IReparentingHost.LogicalChildren => LogicalChildren;
/// <summary>
/// Asks the control whether it wants to reparent the logical children of the specified
/// control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>
/// True if the control wants to reparent its logical children otherwise false.
/// </returns>
bool IReparentingHost.WillReparentChildrenOf(IControl control)
{
return control is IContentPresenter && control.TemplatedParent == this;
}
/// <inheritdoc/>
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<ContentPresenter>("PART_ContentPresenter");
Presenter = e.NameScope.Find<ContentPresenter>("PART_ContentPresenter");
}
}
}

223
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<ILogical> _logicalChildren;
private INameScope _nameScope;
private Styles _styles;
private Subject<Unit> _styleDetach = new Subject<Unit>();
/// <summary>
/// Initializes static members of the <see cref="Control"/> class.
@ -93,6 +101,16 @@ namespace Perspex.Controls
_nameScope = this as INameScope;
}
/// <summary>
/// Raised when the control is attached to a rooted logical tree.
/// </summary>
public event EventHandler<LogicalTreeAttachmentEventArgs> AttachedToLogicalTree;
/// <summary>
/// Raised when the control is detached from a rooted logical tree.
/// </summary>
public event EventHandler<LogicalTreeAttachmentEventArgs> DetachedFromLogicalTree;
/// <summary>
/// Occurs when the <see cref="DataContext"/> property changes.
/// </summary>
@ -206,6 +224,11 @@ namespace Perspex.Controls
internal set { SetValue(TemplatedParentProperty, value); }
}
/// <summary>
/// Gets a value indicating whether the element is attached to a rooted logical tree.
/// </summary>
bool ILogical.IsAttachedToLogicalTree => _isAttachedToLogicalTree;
/// <summary>
/// Gets the control's logical parent.
/// </summary>
@ -230,9 +253,10 @@ namespace Perspex.Controls
/// </remarks>
Type IStyleable.StyleKey => GetType();
/// <summary>
/// Gets the parent style host element.
/// </summary>
/// <inheritdoc/>
IObservable<Unit> IStyleable.StyleDetach => _styleDetach;
/// <inheritdoc/>
IStyleHost IStyleHost.StylingParent => (IStyleHost)InheritanceParent;
/// <summary>
@ -255,12 +279,32 @@ namespace Perspex.Controls
if (_logicalChildren == null)
{
var list = new PerspexList<ILogical>();
list.ResetBehavior = ResetBehavior.Remove;
_logicalChildren = list;
LogicalChildren = list;
}
return _logicalChildren;
}
set
{
Contract.Requires<ArgumentNullException>(value != null);
if (_logicalChildren != value)
{
if (_logicalChildren != null)
{
_logicalChildren.CollectionChanged -= LogicalChildrenCollectionChanged;
}
}
if (value is PerspexList<ILogical>)
{
((PerspexList<ILogical>)value).ResetBehavior = ResetBehavior.Remove;
}
_logicalChildren = value;
_logicalChildren.CollectionChanged += LogicalChildrenCollectionChanged;
}
}
/// <summary>
@ -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
});
}
/// <summary>
/// Called when the control is added to a logical tree.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// 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.
/// </remarks>
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<IStyler>()?.ApplyStyles(this);
AttachedToLogicalTree?.Invoke(this, e);
foreach (var child in LogicalChildren.OfType<Control>())
{
child.OnAttachedToLogicalTree(e);
}
}
/// <summary>
/// Called when the control is removed from a logical tree.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// 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.
/// </remarks>
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<Control>())
{
child.OnDetachedFromLogicalTree(e);
}
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
{
@ -373,35 +492,6 @@ namespace Perspex.Controls
}
}
/// <inheritdoc/>
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<IStyler>()?.ApplyStyles(this);
}
/// <inheritdoc/>
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
if (Name != null)
{
_nameScope?.Unregister(Name);
}
}
/// <summary>
/// Called when the <see cref="DataContext"/> is changed and all subscribers to that change
/// have been notified.
@ -417,7 +507,7 @@ namespace Perspex.Controls
/// <param name="collection">The logical children to use.</param>
protected void RedirectLogicalChildren(IPerspexList<ILogical> collection)
{
_logicalChildren = collection;
LogicalChildren = collection;
}
/// <summary>
@ -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<ILogical>());
break;
case NotifyCollectionChangedAction.Remove:
ClearLogicalParent(e.OldItems.Cast<ILogical>());
break;
case NotifyCollectionChangedAction.Replace:
ClearLogicalParent(e.OldItems.Cast<ILogical>());
SetLogicalParent(e.NewItems.Cast<ILogical>());
break;
case NotifyCollectionChangedAction.Reset:
throw new NotSupportedException("Reset should not be signalled on LogicalChildren collection");
}
}
private void SetLogicalParent(IEnumerable<ILogical> children)
{
foreach (var i in children)
{
if (i.LogicalParent == null)
{
((ISetLogicalParent)i).SetParent(this);
}
}
}
private void ClearLogicalParent(IEnumerable<ILogical> children)
{
foreach (var i in children)
{
if (i.LogicalParent == this)
{
((ISetLogicalParent)i).SetParent(null);
}
}
}
}
}

4
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);
}
}

4
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<Popup>("PART_Popup");
_popup = e.NameScope.Get<Popup>("PART_Popup");
_popup.Opened += PopupOpened;
}

19
src/Perspex.Controls/Generators/IItemContainerGenerator.cs

@ -16,12 +16,17 @@ namespace Perspex.Controls.Generators
/// <summary>
/// Gets the currently realized containers.
/// </summary>
IEnumerable<IControl> Containers { get; }
IEnumerable<ItemContainer> Containers { get; }
/// <summary>
/// Signalled whenever new containers are initialized.
/// Signalled whenever new containers are materialized.
/// </summary>
IObservable<ItemContainers> ContainersInitialized { get; }
event EventHandler<ItemContainerEventArgs> Materialized;
/// <summary>
/// Event raised whenever containers are dematerialized.
/// </summary>
event EventHandler<ItemContainerEventArgs> Dematerialized;
/// <summary>
/// Creates container controls for a collection of items.
@ -32,7 +37,7 @@ namespace Perspex.Controls.Generators
/// <param name="items">The items.</param>
/// <param name="selector">An optional member selector.</param>
/// <returns>The created controls.</returns>
IEnumerable<IControl> Materialize(
IEnumerable<ItemContainer> Materialize(
int startingIndex,
IEnumerable items,
IMemberSelector selector);
@ -45,7 +50,7 @@ namespace Perspex.Controls.Generators
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<IControl> Dematerialize(int startingIndex, int count);
IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count);
/// <summary>
/// Removes a set of created containers and updates the index of later containers to fill
@ -56,13 +61,13 @@ namespace Perspex.Controls.Generators
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<IControl> RemoveRange(int startingIndex, int count);
IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count);
/// <summary>
/// Clears all created containers and returns the removed controls.
/// </summary>
/// <returns>The removed controls.</returns>
IEnumerable<IControl> Clear();
IEnumerable<ItemContainer> Clear();
/// <summary>
/// Gets the container control representing the item with the specified index.

45
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
{
/// <summary>
/// Holds information about an item container generated by an
/// <see cref="IItemContainerGenerator"/>.
/// </summary>
public class ItemContainer
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainer"/> class.
/// </summary>
/// <param name="container">The container control.</param>
/// <param name="item">The item that the container represents.</param>
/// <param name="index">
/// The index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </param>
public ItemContainer(IControl container, object item, int index)
{
ContainerControl = container;
Item = item;
Index = index;
}
/// <summary>
/// Gets the container control.
/// </summary>
/// <remarks>
/// This will be null if <see cref="Item"/> is null.
/// </remarks>
public IControl ContainerControl { get; }
/// <summary>
/// Gets the item that the container represents.
/// </summary>
public object Item { get; }
/// <summary>
/// Gets the index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </summary>
public int Index { get; }
}
}

22
src/Perspex.Controls/Generators/ItemContainers.cs → 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
{
/// <summary>
/// Holds details about a set of item containers in an <see cref="IItemContainerGenerator"/>.
/// Provides details for the <see cref="IItemContainerGenerator.Materialized"/>
/// and <see cref="IItemContainerGenerator.Dematerialized"/> events.
/// </summary>
public class ItemContainers
public class ItemContainerEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainers"/> class.
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
/// </summary>
/// <param name="startingIndex">The index of the first container in the source items.</param>
/// <param name="containers">The containers.</param>
public ItemContainers(int startingIndex, IList<IControl> containers)
public ItemContainerEventArgs(
int startingIndex,
IList<ItemContainer> containers)
{
StartingIndex = startingIndex;
Items = containers;
Containers = containers;
}
/// <summary>
/// Gets the index of the first container in the source items.
/// Gets the containers.
/// </summary>
public int StartingIndex { get; }
public IList<ItemContainer> Containers { get; }
/// <summary>
/// Gets the containers. May contain null entries.
/// Gets the index of the first container in the source items.
/// </summary>
public IList<IControl> Items { get; }
public int StartingIndex { get; }
}
}

77
src/Perspex.Controls/Generators/ItemContainerGenerator.cs

@ -15,9 +15,7 @@ namespace Perspex.Controls.Generators
/// </summary>
public class ItemContainerGenerator : IItemContainerGenerator
{
private List<IControl> _containers = new List<IControl>();
private readonly Subject<ItemContainers> _containersInitialized = new Subject<ItemContainers>();
private List<ItemContainer> _containers = new List<ItemContainer>();
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator"/> class.
@ -31,10 +29,13 @@ namespace Perspex.Controls.Generators
}
/// <inheritdoc/>
public IEnumerable<IControl> Containers => _containers;
public IEnumerable<ItemContainer> Containers => _containers;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs> Materialized;
/// <inheritdoc/>
public IObservable<ItemContainers> ContainersInitialized => _containersInitialized;
public event EventHandler<ItemContainerEventArgs> Dematerialized;
/// <summary>
/// Gets the owner control.
@ -42,7 +43,7 @@ namespace Perspex.Controls.Generators
public IControl Owner { get; }
/// <inheritdoc/>
public IEnumerable<IControl> Materialize(
public IEnumerable<ItemContainer> Materialize(
int startingIndex,
IEnumerable items,
IMemberSelector selector)
@ -50,25 +51,25 @@ namespace Perspex.Controls.Generators
Contract.Requires<ArgumentNullException>(items != null);
int index = startingIndex;
var result = new List<IControl>();
var result = new List<ItemContainer>();
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();
}
/// <inheritdoc/>
public virtual IEnumerable<IControl> Dematerialize(int startingIndex, int count)
public virtual IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count)
{
var result = new List<IControl>();
var result = new List<ItemContainer>();
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;
}
/// <inheritdoc/>
public virtual IEnumerable<IControl> RemoveRange(int startingIndex, int count)
public virtual IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count)
{
var result = _containers.GetRange(startingIndex, count);
_containers.RemoveRange(startingIndex, count);
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
return result;
}
/// <inheritdoc/>
public virtual IEnumerable<IControl> Clear()
public virtual IEnumerable<ItemContainer> Clear()
{
var result = _containers;
_containers = new List<IControl>();
_containers = new List<ItemContainer>();
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
/// <inheritdoc/>
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;
}
/// <summary>
@ -135,33 +157,30 @@ namespace Perspex.Controls.Generators
/// <summary>
/// Adds a collection of containers to the index.
/// </summary>
/// <param name="index">The starting index.</param>
/// <param name="container">The container.</param>
protected void AddContainers(int index, IList<IControl> container)
/// <param name="containers">The containers.</param>
protected void AddContainers(IList<ItemContainer> containers)
{
Contract.Requires<ArgumentNullException>(container != null);
Contract.Requires<ArgumentNullException>(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
/// <param name="index">The first index.</param>
/// <param name="count">The number of elements in the range.</param>
/// <returns>The containers.</returns>
protected IEnumerable<IControl> GetContainerRange(int index, int count)
protected IEnumerable<ItemContainer> GetContainerRange(int index, int count)
{
return _containers.GetRange(index, count);
}

5
src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs

@ -24,6 +24,9 @@ namespace Perspex.Controls.Generators
PerspexProperty contentProperty)
: base(owner)
{
Contract.Requires<ArgumentNullException>(owner != null);
Contract.Requires<ArgumentNullException>(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))
{

10
src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs

@ -120,13 +120,13 @@ namespace Perspex.Controls.Generators
}
}
public override IEnumerable<IControl> Clear()
public override IEnumerable<ItemContainer> Clear()
{
ClearIndex();
return base.Clear();
}
public override IEnumerable<IControl> Dematerialize(int startingIndex, int count)
public override IEnumerable<ItemContainer> 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<IControl> containers)
private void RemoveFromIndex(IEnumerable<ItemContainer> 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);
}
}

25
src/Perspex.Controls/IReparentingControl.cs

@ -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
{
/// <summary>
/// A control that can make its visual children the logical children of another control.
/// </summary>
public interface IReparentingControl : IControl
{
/// <summary>
/// Requests that the visual children of the control use another control as their logical
/// parent.
/// </summary>
/// <param name="logicalParent">
/// The logical parent for the visual children of the control.
/// </param>
/// <param name="children">
/// The <see cref="ILogical.LogicalChildren"/> collection to modify.
/// </param>
void ReparentLogicalChildren(ILogical logicalParent, IPerspexList<ILogical> children);
}
}

28
src/Perspex.Controls/IReparentingHost.cs

@ -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
{
/// <summary>
/// A control that can use the visual children of another control as its logical children.
/// </summary>
public interface IReparentingHost : ILogical
{
/// <summary>
/// Gets a writeable logical children collection from the host.
/// </summary>
new IPerspexList<ILogical> LogicalChildren { get; }
/// <summary>
/// Asks the control whether it wants to reparent the logical children of the specified
/// control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>
/// True if the control wants to reparent its logical children otherwise false.
/// </returns>
bool WillReparentChildrenOf(IControl control);
}
}

193
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
/// <summary>
/// Displays a collection of items.
/// </summary>
public class ItemsControl : TemplatedControl, IReparentingHost
public class ItemsControl : TemplatedControl
{
/// <summary>
/// The default value for the <see cref="ItemsPanel"/> 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;
}
/// <inheritdoc/>
IPerspexList<ILogical> IReparentingHost.LogicalChildren => LogicalChildren;
/// <summary>
/// Gets the item at the specified index in a collection.
/// </summary>
/// <param name="items">The collection.</param>
/// <param name="index">The index.</param>
/// <returns>The index of the item or -1 if the item was not found.</returns>
protected static object ElementAt(IEnumerable items, int index)
{
var typedItems = items?.Cast<object>();
if (index != -1 && typedItems != null && index < typedItems.Count())
{
return typedItems.ElementAt(index) ?? null;
}
else
{
return null;
}
}
/// <summary>
/// Asks the control whether it wants to reparent the logical children of the specified
/// control.
/// Gets the index of an item in a collection.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>
/// True if the control wants to reparent its logical children otherwise false.
/// </returns>
bool IReparentingHost.WillReparentChildrenOf(IControl control)
/// <param name="items">The collection.</param>
/// <param name="item">The item.</param>
/// <returns>The index of the item or -1 if the item was not found.</returns>
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;
}
/// <summary>
/// Creates the <see cref="ItemContainerGenerator"/> for the control.
/// </summary>
/// <returns>An <see cref="IItemContainerGenerator"/>.</returns>
/// <returns>
/// An <see cref="IItemContainerGenerator"/> or null.
/// </returns>
/// <remarks>
/// Certain controls such as <see cref="TabControl"/> 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.
/// </remarks>
protected virtual IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator(this);
}
/// <summary>
/// Called when new containers are materialized for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersMaterialized(ItemContainerEventArgs e)
{
var toAdd = new List<ILogical>();
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);
}
/// <summary>
/// Called when containers are dematerialized for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersDematerialized(ItemContainerEventArgs e)
{
var toRemove = new List<ILogical>();
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);
}
/// <inheritdoc/>
protected override void OnTemplateApplied(INameScope nameScope)
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
Presenter = nameScope.Find<IItemsPresenter>("PART_ItemsPresenter");
base.OnTemplateApplied(e);
Presenter = e.NameScope.Find<IItemsPresenter>("PART_ItemsPresenter");
}
/// <inheritdoc/>
@ -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
/// <param name="e">The event args.</param>
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
}
}
/// <summary>
/// Given a collection of items, adds those that are controls to the logical children.
/// </summary>
/// <param name="items">The items.</param>
private void AddControlItemsToLogicalChildren(IEnumerable items)
{
var toAdd = new List<ILogical>();
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);
}
/// <summary>
/// Given a collection of items, removes those that are controls to from logical children.
/// </summary>
/// <param name="items">The items.</param>
private void RemoveControlItemsFromLogicalChildren(IEnumerable items)
{
var toRemove = new List<ILogical>();
if (items != null)
{
foreach (var i in items)
{
var control = i as IControl;
if (control != null)
{
toRemove.Add(control);
}
}
}
LogicalChildren.RemoveAll(toRemove);
}
/// <summary>
/// Subscribes to an <see cref="Items"/> collection.
/// </summary>

31
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
{
/// <summary>
/// Holds the event arguments for the <see cref="Control.AttachedToLogicalTree"/> and
/// <see cref="Control.DetachedFromLogicalTree"/> events.
/// </summary>
public class LogicalTreeAttachmentEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="LogicalTreeAttachmentEventArgs"/> class.
/// </summary>
/// <param name="root">The root of the logical tree.</param>
public LogicalTreeAttachmentEventArgs(IStyleHost root)
{
Contract.Requires<ArgumentNullException>(root != null);
Root = root;
}
/// <summary>
/// Gets the root of the logical tree that the control is being attached to or detached from.
/// </summary>
public IStyleHost Root { get; }
}
}

28
src/Perspex.Controls/MenuItem.cs

@ -20,7 +20,7 @@ namespace Perspex.Controls
/// <summary>
/// A menu item control.
/// </summary>
public class MenuItem : SelectingItemsControl, ISelectable
public class MenuItem : HeaderedSelectingItemsControl, ISelectable
{
/// <summary>
/// Defines the <see cref="Command"/> property.
@ -40,12 +40,6 @@ namespace Perspex.Controls
public static readonly PerspexProperty<object> CommandParameterProperty =
Button.CommandParameterProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="Header"/> property.
/// </summary>
public static readonly PerspexProperty<object> HeaderProperty =
HeaderedItemsControl.HeaderProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="Icon"/> property.
/// </summary>
@ -136,7 +130,6 @@ namespace Perspex.Controls
set { SetValue(CommandProperty, value); }
}
/// <summary>
/// Gets or sets an <see cref="KeyGesture"/> associated with this control
/// </summary>
@ -156,15 +149,6 @@ namespace Perspex.Controls
set { SetValue(CommandParameterProperty, value); }
}
/// <summary>
/// Gets or sets the <see cref="MenuItem"/>'s header.
/// </summary>
public object Header
{
get { return GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
/// <summary>
/// Gets or sets the icon that appears in a <see cref="MenuItem"/>.
/// </summary>
@ -370,14 +354,12 @@ namespace Perspex.Controls
}
}
/// <summary>
/// Called when the MenuItem's template has been applied.
/// </summary>
protected override void OnTemplateApplied(INameScope nameScope)
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(nameScope);
base.OnTemplateApplied(e);
_popup = nameScope.Get<Popup>("PART_Popup");
_popup = e.NameScope.Get<Popup>("PART_Popup");
_popup.DependencyResolver = DependencyResolver.Instance;
_popup.PopupRootCreated += PopupRootCreated;
_popup.Opened += PopupOpened;

136
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
{
/// <summary>
/// Adds content control functionality to control classes.
/// </summary>
/// <para>
/// The <see cref="ContentControlMixin"/> adds behavior to a control which acts as a content
/// control such as <see cref="ContentControl"/> and <see cref="HeaderedItemsControl"/>. It
/// updates keeps the control's logical children in sync with the content being displayed by
/// the control.
/// </para>
public class ContentControlMixin
{
private static Lazy<ConditionalWeakTable<TemplatedControl, IDisposable>> subscriptions =
new Lazy<ConditionalWeakTable<TemplatedControl, IDisposable>>(() =>
new ConditionalWeakTable<TemplatedControl, IDisposable>());
/// <summary>
/// Initializes a new instance of the <see cref="SelectableMixin"/> class.
/// </summary>
/// <typeparam name="TControl">The control type.</typeparam>
/// <param name="content">The content property.</param>
/// <param name="logicalChildrenSelector">
/// Given an control of <typeparamref name="TControl"/> should return the control's
/// logical children collection.
/// </param>
/// <param name="presenterName">
/// The name of the content presenter in the control's template.
/// </param>
public static void Attach<TControl>(
PerspexProperty content,
Func<TControl, IPerspexList<ILogical>> logicalChildrenSelector,
string presenterName = "PART_ContentPresenter")
where TControl : TemplatedControl
{
Contract.Requires<ArgumentNullException>(content != null);
Contract.Requires<ArgumentNullException>(logicalChildrenSelector != null);
EventHandler<RoutedEventArgs> 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<TemplateAppliedEventArgs> TemplateApplied;
private static void OnTemplateApplied(object sender, RoutedEventArgs e)
{
TemplateApplied?.Invoke(sender, (TemplateAppliedEventArgs)e);
}
private static void UpdateLogicalChild(
IPerspexList<ILogical> 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);
}
}
}
}
}

79
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 <see cref="Panel"/> by adding them to its <see cref="Children"/>
/// collection. All children are layed out to fill the panel.
/// </remarks>
public class Panel : Control, IReparentingControl, IPanel
public class Panel : Control, IPanel
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -68,7 +67,7 @@ namespace Perspex.Controls
{
Contract.Requires<ArgumentNullException>(value != null);
ClearVisualChildren();
VisualChildren.Clear();
_children.Clear();
_children.AddRange(value);
}
@ -83,58 +82,6 @@ namespace Perspex.Controls
set { SetValue(BackgroundProperty, value); }
}
/// <summary>
/// Requests that the visual children of the panel use another control as their logical
/// parent.
/// </summary>
/// <param name="logicalParent">
/// The logical parent for the visual children of the panel.
/// </param>
/// <param name="children">
/// The <see cref="ILogical.LogicalChildren"/> collection to modify.
/// </param>
void IReparentingControl.ReparentLogicalChildren(ILogical logicalParent, IPerspexList<ILogical> children)
{
Contract.Requires<ArgumentNullException>(logicalParent != null);
Contract.Requires<ArgumentNullException>(children != null);
_childLogicalParent = logicalParent;
RedirectLogicalChildren(children);
foreach (var control in Children)
{
((ISetLogicalParent)control).SetParent(null);
((ISetLogicalParent)control).SetParent((IControl)logicalParent);
children.Add(control);
}
}
/// <summary>
/// Clears <see cref="IControl.Parent"/> for the specified controls.
/// </summary>
/// <param name="controls">The controls.</param>
private void ClearLogicalParent(IEnumerable<IControl> controls)
{
foreach (var control in controls)
{
((ISetLogicalParent)control).SetParent(null);
}
}
/// <summary>
/// Sets <see cref="IControl.Parent"/> for the specified controls.
/// </summary>
/// <param name="controls">The controls.</param>
private void SetLogicalParent(IEnumerable<IControl> controls)
{
var parent = _childLogicalParent as Control;
foreach (var control in controls)
{
((ISetLogicalParent)control).SetParent(parent);
}
}
/// <summary>
/// Called when the <see cref="Children"/> collection changes.
/// </summary>
@ -144,29 +91,35 @@ namespace Perspex.Controls
{
List<Control> controls;
// TODO: Handle Replace.
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
controls = e.NewItems.OfType<Control>().ToList();
SetLogicalParent(controls);
AddVisualChildren(e.NewItems.OfType<Visual>());
LogicalChildren.InsertRange(e.NewStartingIndex, controls);
VisualChildren.AddRange(e.NewItems.OfType<Visual>());
break;
case NotifyCollectionChangedAction.Remove:
controls = e.OldItems.OfType<Control>().ToList();
ClearLogicalParent(e.OldItems.OfType<Control>());
LogicalChildren.RemoveAll(controls);
RemoveVisualChildren(e.OldItems.OfType<Visual>());
VisualChildren.RemoveAll(e.OldItems.OfType<Visual>());
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<Control>().ToList();
ClearLogicalParent(controls);
LogicalChildren.Clear();
ClearVisualChildren();
AddVisualChildren(_children);
VisualChildren.Clear();
VisualChildren.AddRange(_children);
break;
}

10
src/Perspex.Controls/Perspex.Controls.csproj

@ -44,23 +44,27 @@
</Compile>
<Compile Include="Classes.cs" />
<Compile Include="DockPanel.cs" />
<Compile Include="Generators\ItemContainer.cs" />
<Compile Include="HotkeyManager.cs" />
<Compile Include="INameScope.cs" />
<Compile Include="Mixins\ContentControlMixin.cs" />
<Compile Include="NameScope.cs" />
<Compile Include="NameScopeEventArgs.cs" />
<Compile Include="NameScopeExtensions.cs" />
<Compile Include="Platform\ITopLevelRenderer.cs" />
<Compile Include="Platform\IWindowingPlatform.cs" />
<Compile Include="Platform\PlatformManager.cs" />
<Compile Include="Primitives\HeaderedSelectingControl.cs" />
<Compile Include="Primitives\TabStripItem.cs" />
<Compile Include="Primitives\TemplateAppliedEventArgs.cs" />
<Compile Include="SelectionMode.cs" />
<Compile Include="Separator.cs" />
<Compile Include="SystemDialog.cs" />
<Compile Include="Generators\ITreeItemContainerGenerator.cs" />
<Compile Include="Generators\ItemContainers.cs" />
<Compile Include="Generators\ItemContainerEventArgs.cs" />
<Compile Include="IContentControl.cs" />
<Compile Include="IControl.cs" />
<Compile Include="IPanel.cs" />
<Compile Include="IReparentingHost.cs" />
<Compile Include="ISetLogicalParent.cs" />
<Compile Include="MenuItemAccessKeyHandler.cs" />
<Compile Include="Mixins\SelectableMixin.cs" />
@ -72,7 +76,6 @@
<Compile Include="Menu.cs" />
<Compile Include="Button.cs" />
<Compile Include="DropDown.cs" />
<Compile Include="IReparentingControl.cs" />
<Compile Include="Carousel.cs" />
<Compile Include="Platform\IPopupImpl.cs" />
<Compile Include="Platform\ITopLevelImpl.cs" />
@ -150,6 +153,7 @@
<Compile Include="TopLevel.cs" />
<Compile Include="Primitives\PopupRoot.cs" />
<Compile Include="Utils\UndoRedoHelper.cs" />
<Compile Include="LogicalTreeAttachmentEventArgs.cs" />
<Compile Include="Window.cs" />
<Compile Include="RowDefinition.cs" />
<Compile Include="RowDefinitions.cs" />

18
src/Perspex.Controls/Presenters/CarouselPresenter.cs

@ -162,20 +162,13 @@ namespace Perspex.Controls.Presenters
/// </summary>
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<object>().ElementAt(toIndex);
to = generator.ContainerFromIndex(toIndex) ??
generator.Materialize(toIndex, new[] { item }, MemberSelector).FirstOrDefault();
generator.Materialize(toIndex, new[] { item }, MemberSelector)
.FirstOrDefault()?.ContainerControl;
if (to != null)
{

30
src/Perspex.Controls/Presenters/ContentPresenter.cs

@ -63,6 +63,14 @@ namespace Perspex.Controls.Presenters
}
}
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
_createdChild = false;
InvalidateMeasure();
}
/// <inheritdoc/>
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;

58
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
/// </summary>
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<IControl> 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<ItemContainer> items)
{
foreach (var i in items)
{
if (i.ContainerControl != null)
{
this.Panel.Children.Add(i.ContainerControl);
}
}
}
private void RemoveContainers(IEnumerable<ItemContainer> items)
{
foreach (var i in items)
{
if (i.ContainerControl != null)
{
this.Panel.Children.Remove(i.ContainerControl);
}
}
}
}
}

2
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

41
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
{
/// <summary>
/// Represents an <see cref="ItemsControl"/> with a related header.
/// </summary>
public class HeaderedItemsControl : ItemsControl
{
/// <summary>
/// Defines the <see cref="Header"/> property.
/// </summary>
public static readonly PerspexProperty<object> HeaderProperty =
HeaderedContentControl.HeaderProperty.AddOwner<HeaderedItemsControl>();
/// <summary>
/// Initializes static members of the <see cref="ContentControl"/> class.
/// </summary>
static HeaderedItemsControl()
{
ContentControlMixin.Attach<HeaderedItemsControl>(
HeaderProperty,
x => x.LogicalChildren,
"PART_HeaderPresenter");
}
/// <summary>
/// Gets or sets the content of the control's header.
/// </summary>
public object Header
{
get { return GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
/// <summary>
/// Gets the header presenter from the control's template.
/// </summary>
public ContentPresenter HeaderPresenter
{
get;
private set;
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
HeaderPresenter = e.NameScope.Find<ContentPresenter>("PART_HeaderPresenter");
}
}
}

58
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
{
/// <summary>
/// Represents a <see cref="SelectingItemsControl"/> with a related header.
/// </summary>
public class HeaderedSelectingItemsControl : SelectingItemsControl
{
/// <summary>
/// Defines the <see cref="Header"/> property.
/// </summary>
public static readonly PerspexProperty<object> HeaderProperty =
HeaderedContentControl.HeaderProperty.AddOwner<HeaderedSelectingItemsControl>();
/// <summary>
/// Initializes static members of the <see cref="ContentControl"/> class.
/// </summary>
static HeaderedSelectingItemsControl()
{
ContentControlMixin.Attach<HeaderedSelectingItemsControl>(
HeaderProperty,
x => x.LogicalChildren,
"PART_HeaderPresenter");
}
/// <summary>
/// Gets or sets the content of the control's header.
/// </summary>
public object Header
{
get { return GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
/// <summary>
/// Gets the header presenter from the control's template.
/// </summary>
public ContentPresenter HeaderPresenter
{
get;
private set;
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
HeaderPresenter = e.NameScope.Find<ContentPresenter>("PART_HeaderPresenter");
}
}
}

5
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);
}
/// <summary>
@ -94,9 +93,9 @@ namespace Perspex.Controls.Primitives
}
/// <inheritdoc/>
protected override void OnTemplateApplied(INameScope nameScope)
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(nameScope);
base.OnTemplateApplied(e);
if (Parent.TemplatedParent != null)
{

113
src/Perspex.Controls/Primitives/SelectingItemsControl.cs

@ -98,7 +98,6 @@ namespace Perspex.Controls.Primitives
/// </summary>
public SelectingItemsControl()
{
ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized);
}
/// <summary>
@ -286,6 +285,27 @@ namespace Perspex.Controls.Primitives
}
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
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;
}
/// <summary>
/// Gets the item at the specified index in a collection.
/// </summary>
/// <param name="items">The collection.</param>
/// <param name="index">The index.</param>
/// <returns>The index of the item or -1 if the item was not found.</returns>
private static object ElementAt(IEnumerable items, int index)
{
var typedItems = items?.Cast<object>();
if (index != -1 && typedItems != null && index < typedItems.Count())
{
return typedItems.ElementAt(index) ?? null;
}
else
{
return null;
}
}
/// <summary>
/// Gets the index of an item in a collection.
/// </summary>
/// <param name="items">The collection.</param>
/// <param name="item">The item.</param>
/// <returns>The index of the item or -1 if the item was not found.</returns>
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;
}
/// <summary>
/// Gets a range of items from an IEnumerable.
/// </summary>
@ -523,27 +488,6 @@ namespace Perspex.Controls.Primitives
}
}
/// <summary>
/// Called when new containers are initialized by the <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="containers">The containers.</param>
private void ContainersInitialized(ItemContainers containers)
{
var selectedIndex = SelectedIndex;
var selectedContainer = containers.Items.OfType<ISelectable>().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);
}
}
/// <summary>
/// Called when a container raises the <see cref="IsSelectedChangedEvent"/>.
/// </summary>
@ -625,7 +569,7 @@ namespace Perspex.Controls.Primitives
/// <param name="selected">Whether the item should be selected or deselected.</param>
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
/// <param name="e">The event args.</param>
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);
}
}
}

40
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<TabItem> SelectedTabProperty =
TabControl.SelectedTabProperty.AddOwner<TabStrip>();
private static IMemberSelector s_MemberSelector = new FuncMemberSelector<object, object>(SelectHeader);
static TabStrip()
{
MemberSelectorProperty.OverrideDefaultValue<TabStrip>(s_MemberSelector);
SelectionModeProperty.OverrideDefaultValue<TabStrip>(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<TabItem>(this, TabItem.ContentProperty);
}
return result;
return new ItemContainerGenerator<TabStripItem>(this, ContentControl.ContentProperty);
}
/// <inheritdoc/>
@ -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;
}
}
}

12
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
{
/// <summary>
/// Represents a tab in a <see cref="TabStrip"/>.
/// </summary>
public class TabStripItem : ListBoxItem
{
}
}

28
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
{
/// <summary>
/// Holds the details of the <see cref="TemplatedControl.TemplateApplied"/> event.
/// </summary>
public class TemplateAppliedEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="TemplateAppliedEventArgs"/> class.
/// </summary>
/// <param name="nameScope">The applied template's name scope.</param>
public TemplateAppliedEventArgs(INameScope nameScope)
: base(TemplatedControl.TemplateAppliedEvent)
{
NameScope = nameScope;
}
/// <summary>
/// Gets the name scope of the applied template.
/// </summary>
public INameScope NameScope { get; }
}
}

46
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<IControlTemplate> TemplateProperty =
PerspexProperty.Register<TemplatedControl, IControlTemplate>("Template");
/// <summary>
/// Defines the <see cref="TemplateApplied"/> routed event.
/// </summary>
public static readonly RoutedEvent<TemplateAppliedEventArgs> TemplateAppliedEvent =
RoutedEvent.Register<TemplatedControl, TemplateAppliedEventArgs>(
"TemplateApplied",
RoutingStrategies.Direct);
private bool _templateApplied;
private readonly ILogger _templateLog;
@ -98,6 +107,15 @@ namespace Perspex.Controls.Primitives
});
}
/// <summary>
/// Raised when the control's template is applied.
/// </summary>
public event EventHandler<TemplateAppliedEventArgs> TemplateApplied
{
add { AddHandler(TemplateAppliedEvent, value); }
remove { RemoveHandler(TemplateAppliedEvent, value); }
}
/// <summary>
/// Gets or sets the brush used to draw the control's background.
/// </summary>
@ -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
/// <summary>
/// Called when the control's template is applied.
/// </summary>
/// <param name="nameScope">The template name scope.</param>
protected virtual void OnTemplateApplied(INameScope nameScope)
/// <param name="e">The event args.</param>
protected virtual void OnTemplateApplied(TemplateAppliedEventArgs e)
{
RaiseEvent(e);
}
/// <summary>

4
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);
}
});
}

4
src/Perspex.Controls/ProgressBar.cs

@ -26,9 +26,9 @@ namespace Perspex.Controls
}
/// <inheritdoc/>
protected override void OnTemplateApplied(INameScope nameScope)
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
_indicator = nameScope.Get<Border>("PART_Indicator");
_indicator = e.NameScope.Get<Border>("PART_Indicator");
UpdateIndicator(Bounds.Size);
}

93
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
/// <summary>
/// A tab control that displays a tab strip along with the content of the selected tab.
/// </summary>
public class TabControl : SelectingItemsControl, IReparentingHost
public class TabControl : SelectingItemsControl
{
/// <summary>
/// Defines the <see cref="SelectedTab"/> property.
/// </summary>
public static readonly PerspexProperty<TabItem> SelectedTabProperty =
PerspexProperty.Register<TabControl, TabItem>("SelectedTab");
/// <summary>
/// Defines the <see cref="Transition"/> property.
/// </summary>
public static readonly PerspexProperty<IPageTransition> TransitionProperty =
Carousel.TransitionProperty.AddOwner<TabControl>();
Perspex.Controls.Carousel.TransitionProperty.AddOwner<TabControl>();
private static readonly IMemberSelector s_contentSelector =
/// <summary>
/// Defines an <see cref="IMemberSelector"/> that selects the content of a <see cref="TabItem"/>.
/// </summary>
public static readonly IMemberSelector ContentSelector =
new FuncMemberSelector<object, object>(SelectContent);
/// <summary>
/// Defines an <see cref="IMemberSelector"/> that selects the header of a <see cref="TabItem"/>.
/// </summary>
public static readonly IMemberSelector HeaderSelector =
new FuncMemberSelector<object, object>(SelectHeader);
/// <summary>
/// Defines the <see cref="TabStripPlacement"/> property.
/// </summary>
@ -41,22 +44,25 @@ namespace Perspex.Controls
{
SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue<TabControl>(false);
SelectedItemProperty.Changed.AddClassHandler<TabControl>(x => x.SelectedItemChanged);
AffectsMeasure(TabStripPlacementProperty);
}
/// <summary>
/// Gets an <see cref="IMemberSelector"/> that selects the content of a <see cref="TabItem"/>.
/// Gets the pages portion of the <see cref="TabControl"/>'s template.
/// </summary>
public IMemberSelector ContentSelector => s_contentSelector;
public IControl Pages
{
get;
private set;
}
/// <summary>
/// Gets the <see cref="SelectingItemsControl.SelectedItem"/> as a <see cref="TabItem"/>.
/// Gets the tab strip portion of the <see cref="TabControl"/>'s template.
/// </summary>
public TabItem SelectedTab
public IControl TabStrip
{
get { return GetValue(SelectedTabProperty); }
private set { SetValue(SelectedTabProperty, value); }
get;
private set;
}
/// <summary>
@ -77,17 +83,21 @@ namespace Perspex.Controls
set { SetValue(TabStripPlacementProperty, value); }
}
/// <summary>
/// Asks the control whether it wants to reparent the logical children of the specified
/// control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>
/// True if the control wants to reparent its logical children otherwise false.
/// </returns>
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<IControl>("PART_TabStrip");
Pages = e.NameScope.Find<IControl>("PART_Content");
}
/// <summary>
@ -110,14 +120,31 @@ namespace Perspex.Controls
}
/// <summary>
/// Called when the <see cref="SelectingItemsControl.SelectedIndex"/> property changes.
/// Selects the header of a tab item.
/// </summary>
/// <param name="e">The event args.</param>
private void SelectedItemChanged(PerspexPropertyChangedEventArgs e)
/// <param name="o">The tab item.</param>
/// <returns>The content.</returns>
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;
}
}
}
}

29
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<IReparentingHost>();
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<Control> GetTemplateChildren(this ITemplatedControl control)
{
var visual = control as IVisual;

6
src/Perspex.Controls/TextBlock.cs

@ -26,6 +26,7 @@ namespace Perspex.Controls
public static readonly PerspexProperty<string> FontFamilyProperty =
PerspexProperty.RegisterAttached<TextBlock, Control, string>(
nameof(FontFamily),
defaultValue: "Courier New",
inherits: true);
/// <summary>
@ -34,6 +35,7 @@ namespace Perspex.Controls
public static readonly PerspexProperty<double> FontSizeProperty =
PerspexProperty.RegisterAttached<TextBlock, Control, double>(
nameof(FontSize),
defaultValue: 12,
inherits: true);
/// <summary>
@ -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);

4
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<TextPresenter>("PART_TextPresenter");
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
_presenter.Cursor = new Cursor(StandardCursorType.Ibeam);
}

7
src/Perspex.Controls/TopLevel.cs

@ -24,7 +24,7 @@ namespace Perspex.Controls
/// <see cref="PopupRoot"/>. It handles scheduling layout, styling and rendering as well as
/// tracking the window <see cref="ClientSize"/> and <see cref="IsActive"/> state.
/// </remarks>
public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable
public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable, IStyleRoot
{
/// <summary>
/// Defines the <see cref="ClientSize"/> property.
@ -204,6 +204,11 @@ namespace Perspex.Controls
set { SetValue(AccessText.ShowAccessKeyProperty, value); }
}
IStyleHost IStyleHost.StylingParent
{
get { return PerspexLocator.Current.GetService<IGlobalStyles>(); }
}
/// <summary>
/// Whether an auto-size operation is in progress.
/// </summary>

130
src/Perspex.SceneGraph/Visual.cs

@ -81,11 +81,6 @@ namespace Perspex
/// </summary>
private string _name;
/// <summary>
/// Holds the children of the visual.
/// </summary>
private readonly PerspexList<IVisual> _visualChildren;
/// <summary>
/// The visual's bounds relative to its parent.
/// </summary>
@ -128,9 +123,10 @@ namespace Perspex
new PropertyEnricher("Id", GetHashCode()),
});
_visualChildren = new PerspexList<IVisual>();
_visualChildren.ResetBehavior = ResetBehavior.Remove;
_visualChildren.CollectionChanged += VisualChildrenChanged;
var visualChildren = new PerspexList<IVisual>();
visualChildren.ResetBehavior = ResetBehavior.Remove;
visualChildren.CollectionChanged += VisualChildrenChanged;
VisualChildren = visualChildren;
}
/// <summary>
@ -249,6 +245,15 @@ namespace Perspex
set { SetValue(ZIndexProperty, value); }
}
/// <summary>
/// Gets the control's visual children.
/// </summary>
protected IPerspexList<IVisual> VisualChildren
{
get;
private set;
}
/// <summary>
/// Gets a value indicating whether this scene graph node is attached to a visual root.
/// </summary>
@ -257,7 +262,7 @@ namespace Perspex
/// <summary>
/// Gets the scene graph node's child nodes.
/// </summary>
IPerspexReadOnlyList<IVisual> IVisual.VisualChildren => _visualChildren;
IPerspexReadOnlyList<IVisual> IVisual.VisualChildren => VisualChildren;
/// <summary>
/// Gets the scene graph node's parent node.
@ -333,61 +338,6 @@ namespace Perspex
property.Changed.Subscribe(AffectsRenderInvalidate);
}
/// <summary>
/// Adds a visual child to the control.
/// </summary>
/// <param name="visual">The child to add.</param>
protected void AddVisualChild(IVisual visual)
{
Contract.Requires<ArgumentNullException>(visual != null);
_visualChildren.Add(visual);
}
/// <summary>
/// Adds visual children to the control.
/// </summary>
/// <param name="visuals">The children to add.</param>
protected void AddVisualChildren(IEnumerable<IVisual> visuals)
{
Contract.Requires<ArgumentNullException>(visuals != null);
_visualChildren.AddRange(visuals);
}
/// <summary>
/// Removes all visual children from the control.
/// </summary>
protected void ClearVisualChildren()
{
_visualChildren.Clear();
}
/// <summary>
/// Removes a visual child from the control;
/// </summary>
/// <param name="visual">The child to remove.</param>
protected void RemoveVisualChild(IVisual visual)
{
Contract.Requires<ArgumentNullException>(visual != null);
_visualChildren.Remove(visual);
}
/// <summary>
/// Removes a visual children from the control;
/// </summary>
/// <param name="visuals">The children to remove.</param>
protected void RemoveVisualChildren(IEnumerable<IVisual> visuals)
{
Contract.Requires<ArgumentNullException>(visuals != null);
foreach (var v in visuals)
{
_visualChildren.Remove(v);
}
}
/// <summary>
/// Called when the control is added to a visual tree.
/// </summary>
@ -433,20 +383,6 @@ namespace Perspex
(e.Sender as Visual)?.InvalidateVisual();
}
/// <summary>
/// Gets the event args for an <see cref="AttachedToVisualTree"/> or
/// <see cref="DetachedFromVisualTree"/> event.
/// </summary>
/// <returns>
/// A <see cref="VisualTreeAttachmentEventArgs"/> if the visual currently has a root;
/// otherwise null.
/// </returns>
private VisualTreeAttachmentEventArgs GetAttachmentEventArgs()
{
var root = this.GetSelfAndVisualAncestors().OfType<IRenderRoot>().FirstOrDefault();
return root != null ? new VisualTreeAttachmentEventArgs(root) : null;
}
/// <summary>
/// Gets the visual offset from the specified ancestor.
/// </summary>
@ -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<IRenderRoot>().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<IRenderRoot>().FirstOrDefault();
var e = new VisualTreeAttachmentEventArgs(root);
NotifyAttachedToVisualTree(e);
}
RaisePropertyChanged(VisualParentProperty, old, value, BindingPriority.LocalValue);
}
/// <summary>
/// Called when the <see cref="_visualChildren"/> collection changes.
/// Called when the <see cref="VisualChildren"/> collection changes.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
@ -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<Visual>())
foreach (Visual child in VisualChildren.OfType<Visual>())
{
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<Visual>())
foreach (Visual child in VisualChildren.OfType<Visual>())
{
child.NotifyDetachedFromVisualTree(e);
}

2
src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs

@ -18,6 +18,8 @@ namespace Perspex
/// <param name="root">The root visual.</param>
public VisualTreeAttachmentEventArgs(IRenderRoot root)
{
Contract.Requires<ArgumentNullException>(root != null);
Root = root;
}

5
src/Perspex.Styling/ILogical.cs

@ -10,6 +10,11 @@ namespace Perspex
/// </summary>
public interface ILogical
{
/// <summary>
/// Gets a value indicating whether the element is attached to a rooted logical tree.
/// </summary>
bool IsAttachedToLogicalTree { get; }
/// <summary>
/// Gets the logical parent.
/// </summary>

1
src/Perspex.Styling/Perspex.Styling.csproj

@ -49,6 +49,7 @@
<Compile Include="Styling\ISetter.cs" />
<Compile Include="Styling\IStyle.cs" />
<Compile Include="Styling\IStyleable.cs" />
<Compile Include="Styling\IStyleRoot.cs" />
<Compile Include="Styling\IStyleHost.cs" />
<Compile Include="Styling\IStyler.cs" />
<Compile Include="Styling\ITemplatedControl.cs" />

6
src/Perspex.Styling/Styling/IGlobalStyles.cs

@ -3,8 +3,10 @@
namespace Perspex.Styling
{
public interface IGlobalStyles
/// <summary>
/// Defines the style host that provides styles global to the application.
/// </summary>
public interface IGlobalStyles : IStyleRoot
{
Styles Styles { get; }
}
}

2
src/Perspex.Styling/Styling/IStyleHost.cs

@ -6,7 +6,7 @@ namespace Perspex.Styling
/// <summary>
/// Defines an element that has a <see cref="Styles"/> collection.
/// </summary>
public interface IStyleHost : IVisual
public interface IStyleHost
{
/// <summary>
/// Gets the styles for the element.

12
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
{
/// <summary>
/// Denotes the root <see cref="IStyleHost"/> in a tree.
/// </summary>
public interface IStyleRoot : IStyleHost
{
}
}

6
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
/// </summary>
public interface IStyleable : IObservablePropertyBag, INamed
{
/// <summary>
/// Raised when the control's style should be removed.
/// </summary>
IObservable<Unit> StyleDetach { get; }
/// <summary>
/// Gets the list of classes for the control.
/// </summary>

2
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
{

6
src/Perspex.Styling/Styling/Selectors.cs

@ -64,7 +64,7 @@ namespace Perspex.Styling
{
Contract.Requires<ArgumentNullException>(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);
}
/// <summary>
@ -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)

16
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
/// </summary>
public class Style : IStyle
{
private static readonly IObservable<bool> True = Observable.Never<bool>().StartWith(true);
/// <summary>
/// Initializes a new instance of the <see cref="Style"/> class.
/// </summary>
@ -56,17 +59,8 @@ namespace Perspex.Styling
if (match.ImmediateResult != false)
{
var visual = control as IVisual;
var activator = match.ObservableResult ??
Observable.Never<bool>().StartWith(true);
if (visual != null)
{
var detached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
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)
{

32
src/Perspex.Styling/Styling/StyleActivator.cs

@ -15,30 +15,28 @@ namespace Perspex.Styling
Or,
}
public class StyleActivator : ObservableBase<bool>
public static class StyleActivator
{
private readonly IObservable<bool>[] _inputs;
private readonly ActivatorMode _mode;
public StyleActivator(
IList<IObservable<bool>> inputs,
ActivatorMode mode = ActivatorMode.And)
public static IObservable<bool> And(IEnumerable<IObservable<bool>> inputs)
{
_inputs = inputs.ToArray();
_mode = mode;
}
var sourceArray = inputs.Select(s => s.Publish().RefCount()).ToArray();
protected override IDisposable SubscribeCore(IObserver<bool> 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<bool> values)
public static IObservable<bool> Or(IEnumerable<IObservable<bool>> inputs)
{
return _mode == ActivatorMode.And ? values.All(x => x) : values.Any(x => x);
return inputs.CombineLatest()
.Select(values => values.Any(x => x))
.DistinctUntilChanged();
}
}
}

28
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<IStyleHost>()
.FirstOrDefault();
IGlobalStyles global = PerspexLocator.Current.GetService<IGlobalStyles>();
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<ArgumentNullException>(control != null);
Contract.Requires<ArgumentNullException>(container != null);
Contract.Requires<ArgumentNullException>(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<IStyleHost>().FirstOrDefault();
styleHost.Styles.Attach(control, styleHost);
}
}
}

3
src/Perspex.Themes.Default/Button.paml

@ -13,7 +13,8 @@
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter Content="{TemplateBinding Content}"
<ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
TextBlock.Foreground="{TemplateBinding Foreground}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

2
src/Perspex.Themes.Default/DefaultTheme.paml

@ -19,8 +19,8 @@
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.ScrollBar.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.ScrollViewer.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.TabControl.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.TabItem.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.TabStrip.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.TabStripItem.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.TextBox.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.ToggleButton.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.ToolTip.paml"/>

6
src/Perspex.Themes.Default/MenuItem.paml

@ -24,7 +24,8 @@
IsVisible="False"
Margin="3"
VerticalAlignment="Center"/>
<ContentPresenter Content="{TemplateBinding Header}"
<ContentPresenter Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
Grid.Column="2">
@ -78,7 +79,8 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Panel>
<ContentPresenter Content="{TemplateBinding Header}"
<ContentPresenter Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
Margin="{TemplateBinding Padding}">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="sys:String">

2
src/Perspex.Themes.Default/Perspex.Themes.Default.csproj

@ -159,7 +159,7 @@
<EmbeddedResource Include="TabControl.paml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="TabItem.paml">
<EmbeddedResource Include="TabStripItem.paml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="TabStrip.paml">

36
src/Perspex.Themes.Default/TabControl.paml

@ -3,10 +3,13 @@
<Setter Property="Template">
<ControlTemplate>
<Grid RowDefinitions="Auto,*">
<TabStrip Items="{TemplateBinding Items}"
<TabStrip Name="PART_TabStrip"
MemberSelector="{Static TabControl.HeaderSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex, Mode=TwoWay}"/>
<Carousel Items="{TemplateBinding Items}"
MemberSelector="Content"
<Carousel Name="PART_Content"
MemberSelector="{Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
Transition="{TemplateBinding Transition}"
Grid.Row="1"/>
@ -18,11 +21,14 @@
<Setter Property="Template">
<ControlTemplate>
<Grid RowDefinitions="*,Auto">
<Carousel Items="{TemplateBinding Items}"
MemberSelector="Content"
<Carousel Name="PART_Content"
MemberSelector="{Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
Transition="{TemplateBinding Transition}"/>
<TabStrip Items="{TemplateBinding Items}"
<TabStrip Name="PART_TabStrip"
MemberSelector="{Static TabControl.HeaderSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex, Mode=TwoWay}"
Grid.Row="1"/>
</Grid>
@ -35,7 +41,9 @@
<Grid ColumnDefinitions="Auto,*">
<Border>
<ScrollViewer>
<TabStrip Items="{TemplateBinding Items}"
<TabStrip Name="PART_TabStrip"
MemberSelector="{Static TabControl.HeaderSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex, Mode=TwoWay}">
<TabStrip.ItemsPanel>
<ItemsPanelTemplate>
@ -45,8 +53,9 @@
</TabStrip>
</ScrollViewer>
</Border>
<Carousel Items="{TemplateBinding Items}"
MemberSelector="Content"
<Carousel Name="PART_Content"
MemberSelector="{Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
Transition="{TemplateBinding Transition}"
Grid.Column="1"/>
@ -58,13 +67,16 @@
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="*,Auto">
<Carousel Items="{TemplateBinding Items}"
MemberSelector="Content"
<Carousel Name="PART_Content"
MemberSelector="{Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
Transition="{TemplateBinding Transition}"/>
<Border Grid.Column="1">
<ScrollViewer>
<TabStrip Items="{TemplateBinding Items}"
<TabStrip Name="PART_TabStrip"
MemberSelector="{Static TabControl.HeaderSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex, Mode=TwoWay}">
<TabStrip.ItemsPanel>
<ItemsPanelTemplate>

6
src/Perspex.Themes.Default/TabStrip.paml

@ -2,7 +2,9 @@
<Style Selector="TabStrip">
<Setter Property="Template">
<ControlTemplate>
<ItemsPresenter Items="{TemplateBinding Items}"
<ItemsPresenter Name="PART_ItemsPresenter"
MemberSelector="{TemplateBinding MemberSelector}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"/>
</ControlTemplate>
</Setter>
@ -12,7 +14,7 @@
</ItemsPanelTemplate>
</Setter>
</Style>
<Style Selector="TabStrip > TabItem">
<Style Selector="TabStrip > TabStripItem">
<Setter Property="Margin" Value="16"/>
</Style>
</Styles>

8
src/Perspex.Themes.Default/TabItem.paml → src/Perspex.Themes.Default/TabStripItem.paml

@ -1,14 +1,16 @@
<Styles xmlns="https://github.com/perspex">
<Style Selector="TabItem">
<Style Selector="TabStripItem">
<Setter Property="FontSize" Value="16"/>
<Setter Property="Foreground" Value="Gray"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_HeaderPresenter" Content="{TemplateBinding Header}"/>
<ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"/>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="TabItem:selected">
<Style Selector="TabStripItem:selected">
<Setter Property="Foreground" Value="Black"/>
</Style>
</Styles>

3
src/Perspex.Themes.Default/ToggleButton.paml

@ -13,7 +13,8 @@
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter Content="{TemplateBinding Content}"
<ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
TextBlock.Foreground="{TemplateBinding Foreground}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

2
src/Perspex.Themes.Default/TreeViewItem.paml

@ -12,7 +12,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Grid.Column="1">
<ContentPresenter Name="PART_ContentPresenter"
<ContentPresenter Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
Margin="{TemplateBinding Padding}"/>
</Border>

73
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<TextBlock>(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();

89
tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs

@ -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);
}
}
}

110
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<string>();
child.AttachedToLogicalTree += (s, e) =>
{
var root = new TestRoot();
var target = new Control();
var styler = new Mock<IStyler>();
Assert.Equal(root, child.Parent);
raised.Add("attached");
};
PerspexLocator.CurrentMutable.Bind<IStyler>().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;
}
}
}

3
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();

20
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<T> Do<T>(this IEnumerable<T> items, Action<T> action)
{
foreach (var i in items)
{
action(i);
yield return i;
}
}
}
}

26
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<TextBlock>().Select(x => x.Text).ToList();
var result = containers
.Select(x => x.ContainerControl)
.OfType<TextBlock>()
.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);
}
}

4
tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs

@ -17,10 +17,10 @@ namespace Perspex.Controls.UnitTests.Generators
var target = new ItemContainerGenerator<ListBoxItem>(owner, ListBoxItem.ContentProperty);
var containers = target.Materialize(0, items, null);
var result = containers
.Select(x => x.ContainerControl)
.OfType<ListBoxItem>()
.Select(x => x.Content)
.OfType<TextBlock>()
.Select(x => x.Text).ToList();
.ToList();
Assert.Equal(items, result);
}

78
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<HeaderedItemsControl>(parent =>
{
return new Border
{
Child = new ContentPresenter
{
Name = "PART_HeaderPresenter",
[~ContentPresenter.ContentProperty] = parent[~HeaderedItemsControl.HeaderProperty],
}
};
});
}
}
}

58
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<ItemsPresenter>().Single();
var panel = target.GetTemplateChildren().OfType<StackPanel>().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<ItemsPresenter>().Single();
var panel = target.GetTemplateChildren().OfType<StackPanel>().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<Control>(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]

35
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<ListBoxItem>()
.Do(x => x.Template = ListBoxItemTemplate())
.Do(x => x.ApplyTemplate())
.Select(x => x.Presenter.Child)
.OfType<TextBlock>()
.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<ListBoxItem>(parent => new ContentPresenter
{
Name = "PART_ContentPresenter",
[!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty],
});
}
private Control CreateScrollViewerTemplate(ITemplatedControl parent)
{
return new ScrollContentPresenter

17
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<ILogical> LogicalChildren => base.LogicalChildren;

4
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@ -81,11 +81,13 @@
<Otherwise />
</Choose>
<ItemGroup>
<Compile Include="EnumerableExtensions.cs" />
<Compile Include="HeaderedItemsControlTests .cs" />
<Compile Include="ControlTests_NameScope.cs" />
<Compile Include="Generators\ItemContainerGeneratorTests.cs" />
<Compile Include="Generators\ItemContainerGeneratorTypedTests.cs" />
<Compile Include="GridLengthTests.cs" />
<Compile Include="ContentPresenterTests.cs" />
<Compile Include="Presenters\ContentPresenterTests.cs" />
<Compile Include="BorderTests.cs" />
<Compile Include="ListBoxTests_Single.cs" />
<Compile Include="NameScopeTests.cs" />

68
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<TextBlock>(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<TextBlock>(target.Child);
var root = new TestRoot
{
DataTemplates = new DataTemplates
{
new FuncDataTemplate<string>(x => new Decorator()),
},
};
root.Child = target;
target.ApplyTemplate();
Assert.IsType<Decorator>(target.Child);
}
}
}

3
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]

16
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<IGlobalStyles>();
globalStyles.Setup(x => x.Styles).Returns(styles);
PerspexLocator.CurrentMutable
.Bind<ILayoutManager>().ToTransient<LayoutManager>()
.Bind<IGlobalStyles>().ToFunc(() => globalStyles.Object)

159
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<IHeadered>(x => x.Header == "foo"),
Mock.Of<IHeadered>(x => x.Header == "bar"),
};
var target = new TabStrip
{
Template = new FuncControlTemplate<TabStrip>(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<TabItem>().First(), target.SelectedItem);
Assert.Equal(target.Items.Cast<TabItem>().First(), target.SelectedTab);
var result = target.GetLogicalChildren()
.OfType<TabStripItem>()
.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<TabStrip>(CreateTabStripTemplate),
Items = new[]
{
new TabItem
{
Name = "first"
},
new TabItem
{
Name = "second"
},
}
Items = items,
};
target.ApplyTemplate();
target.SelectedItem = target.Items.Cast<TabItem>().ElementAt(1);
Assert.Same(target.SelectedTab, target.SelectedItem);
var result = target.GetLogicalChildren()
.OfType<TabStripItem>()
.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<TabStrip>(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<TabStrip>(CreateTabStripTemplate),
Items = items,
SelectedItem = items[1],
};
target.ApplyTemplate();
target.SelectedTab = target.Items.Cast<TabItem>().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<TabItem>()
var items = new ObservableCollection<TabItem>()
{
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<TabStrip>(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],
};
}
}

99
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<IVisual, ILogical>(null, target),
new Tuple<IVisual, ILogical>(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<ContentControl>(ScrollingContentControlTemplate),
Content = "foo"
};
target.ApplyTemplate();
var border = target.GetVisualChildren().FirstOrDefault();
Assert.IsType<Border>(border);
var scrollViewer = border.GetVisualChildren().FirstOrDefault();
Assert.IsType<ScrollViewer>(scrollViewer);
var scrollContentPresenter = scrollViewer.GetVisualChildren().FirstOrDefault();
Assert.IsType<ScrollContentPresenter>(scrollContentPresenter);
var contentPresenter = scrollContentPresenter.GetVisualChildren().FirstOrDefault();
Assert.IsType<ContentPresenter>(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<ScrollViewer>(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<ScrollViewer>(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],
};

174
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<TabControl>(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<TabItem>().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<TabControl>(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<TabItem>(x => new Decorator());
using (PerspexLocator.EnterScope())
{
PerspexLocator.CurrentMutable.Bind<IStyler>().ToConstant(new Styler());
var root = new TestRoot
{
Styles = new Styles
{
new Style(x => x.OfType<TabItem>())
{
Setters = new[]
{
new Setter(TemplatedControl.TemplateProperty, template)
}
}
},
Child = new TabControl
{
Template = new FuncControlTemplate<TabControl>(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);
}
/// <summary>
/// Non-headered control items should result in TabStripItems with empty content.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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<TabControl>(CreateTabControlTemplate),
Items = items,
};
target.ApplyTemplate();
var result = target.TabStrip.GetLogicalChildren()
.OfType<TabStripItem>()
.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<TabStrip>(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<Carousel>(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],

3
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);

7
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;
}
}

2
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);
}

2
tests/Perspex.Interactivity.UnitTests/InteractiveTests.cs

@ -349,7 +349,7 @@ namespace Perspex.Interactivity.UnitTests
set
{
AddVisualChildren(value.Cast<Visual>());
VisualChildren.AddRange(value.Cast<Visual>());
}
}

1
tests/Perspex.LeakTests/Perspex.LeakTests.csproj

@ -88,7 +88,6 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="StyleTests.cs" />
<Compile Include="ControlTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TestApp.cs" />

124
tests/Perspex.LeakTests/StyleTests.cs

@ -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<Window> run = () =>
{
var window = new Window
{
Styles = new Styles
{
new Style(x => x.OfType<Canvas>().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<Canvas>(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<StyleActivator>()).ObjectsCount));
}
[Fact]
public void Changing_Carousel_SelectedIndex_Should_Not_Leak_StyleActivators()
{
Func<Window> run = () =>
{
Carousel target;
var window = new Window
{
Styles = new Styles
{
new Style(x => x.OfType<ContentControl>().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<Carousel>(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<StyleActivator>()).ObjectsCount));
}
}
}

3
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();

3
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<Unit> IStyleable.StyleDetach { get; }
}
private class AttachedOwner

3
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();

14
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<Visual> v)
{
AddVisualChildren(v);
VisualChildren.AddRange(v);
}
public void RemoveChild(Visual v)
{
RemoveVisualChild(v);
VisualChildren.Remove(v);
}
public void ClearChildren()
{
ClearVisualChildren();
VisualChildren.Clear();
}
}
}

23
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()
{

5
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<ILogical> LogicalChildren { get; set; }
public ILogical LogicalParent { get; set; }
@ -91,6 +94,8 @@ namespace Perspex.Styling.UnitTests
public ITemplatedControl TemplatedParent { get; }
IObservable<Unit> IStyleable.StyleDetach { get; }
public IPropertyBag InheritanceParent
{
get

5
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<ILogical> LogicalChildren { get; set; }
public ILogical LogicalParent { get; set; }
@ -131,6 +134,8 @@ namespace Perspex.Styling.UnitTests
IPerspexReadOnlyList<string> IStyleable.Classes => Classes;
IObservable<Unit> IStyleable.StyleDetach { get; }
public IDisposable Bind(PerspexProperty property, IObservable<object> source, BindingPriority priority)
{
throw new NotImplementedException();

30
tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs

@ -17,7 +17,7 @@ namespace Perspex.Styling.UnitTests
{
var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable<bool>();
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<bool>();
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<bool>(false) };
var target = new StyleActivator(inputs, ActivatorMode.And);
var target = StyleActivator.And(inputs);
var result = new TestObserver<bool>();
target.Subscribe(result);
@ -68,7 +68,7 @@ namespace Perspex.Styling.UnitTests
new TestSubject<bool>(false),
new TestSubject<bool>(true),
};
var target = new StyleActivator(inputs, ActivatorMode.And);
var target = StyleActivator.And(inputs);
var result = new TestObserver<bool>();
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<bool>(false),
new TestSubject<bool>(true),
};
var target = new StyleActivator(inputs, ActivatorMode.And);
var target = StyleActivator.And(inputs);
var result = new TestObserver<bool>();
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<bool>(false) };
var target = new StyleActivator(inputs, ActivatorMode.Or);
var target = StyleActivator.Or(inputs);
var result = new TestObserver<bool>();
target.Subscribe(result);
@ -134,7 +132,7 @@ namespace Perspex.Styling.UnitTests
new TestSubject<bool>(false),
new TestSubject<bool>(true),
};
var target = new StyleActivator(inputs, ActivatorMode.Or);
var target = StyleActivator.Or(inputs);
var result = new TestObserver<bool>();
target.Subscribe(result);
@ -158,7 +156,7 @@ namespace Perspex.Styling.UnitTests
new TestSubject<bool>(false),
new TestSubject<bool>(true),
};
var target = new StyleActivator(inputs, ActivatorMode.Or);
var target = StyleActivator.Or(inputs);
var result = new TestObserver<bool>();
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);

2
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;

3
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<string> IStyleable.Classes => Classes;
IObservable<Unit> IStyleable.StyleDetach { get; }
public IDisposable Bind(PerspexProperty property, IObservable<object> source, BindingPriority priority)
{
throw new NotImplementedException();

2
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);

3
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<string> IStyleable.Classes => Classes;
IObservable<Unit> IStyleable.StyleDetach { get; }
public IObservable<T> GetObservable<T>(PerspexProperty<T> property)
{
throw new NotImplementedException();

Loading…
Cancel
Save