Browse Source

Implemented Menus a bit better.

Now appear correctly in logical tree and use general-purpose mechanisms
rather than direct communication between MenuItems.
pull/58/head
Steven Kirk 12 years ago
parent
commit
fef04a5f5d
  1. 35
      Perspex.Controls/ContentControl.cs
  2. 4
      Perspex.Controls/ControlExtensions.cs
  3. 14
      Perspex.Controls/GlobalSuppressions.cs
  4. 17
      Perspex.Controls/IMenu.cs
  5. 30
      Perspex.Controls/ItemsControl.cs
  6. 135
      Perspex.Controls/Menu.cs
  7. 247
      Perspex.Controls/MenuItem.cs
  8. 12
      Perspex.Controls/Panel.cs
  9. 2
      Perspex.Controls/Perspex.Controls.csproj
  10. 12
      Perspex.Controls/Popup.cs
  11. 8
      Perspex.Controls/PopupRoot.cs
  12. 2
      Perspex.Controls/Presenters/ContentPresenter.cs
  13. 2
      Perspex.Input/IInputElement.cs
  14. 9
      Perspex.Interactive.UnitTests/GlobalSuppressions.cs
  15. 13
      Perspex.Interactive.UnitTests/InteractiveTests.cs
  16. 1
      Perspex.Interactive.UnitTests/Perspex.Interactive.UnitTests.csproj
  17. 14
      Perspex.Interactivity/GlobalSuppressions.cs
  18. 42
      Perspex.Interactivity/IInteractive.cs
  19. 82
      Perspex.Interactivity/Interactive.cs
  20. 41
      Perspex.Interactivity/InteractiveExtensions.cs
  21. 2
      Perspex.Interactivity/Perspex.Interactivity.csproj
  22. 5
      Perspex.Interactivity/RoutedEventArgs.cs
  23. 5
      Perspex.Styling/LogicalTree/LogicalExtensions.cs
  24. 10
      TestApplication/Program.cs
  25. 43
      Tests/Perspex.Controls.UnitTests/ContentControlTests.cs
  26. 9
      Tests/Perspex.Controls.UnitTests/GlobalSuppressions.cs
  27. 38
      Tests/Perspex.Controls.UnitTests/ItemsControlTests.cs
  28. 1
      Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

35
Perspex.Controls/ContentControl.cs

@ -28,7 +28,6 @@ namespace Perspex.Controls
public ContentControl() public ContentControl()
{ {
this.GetObservableWithHistory(ContentProperty).Subscribe(this.SetContentParent);
} }
public object Content public object Content
@ -37,6 +36,12 @@ namespace Perspex.Controls
set { this.SetValue(ContentProperty, value); } set { this.SetValue(ContentProperty, value); }
} }
public ContentPresenter Presenter
{
get;
private set;
}
public HorizontalAlignment HorizontalContentAlignment public HorizontalAlignment HorizontalContentAlignment
{ {
get { return this.GetValue(HorizontalContentAlignmentProperty); } get { return this.GetValue(HorizontalContentAlignmentProperty); }
@ -59,32 +64,8 @@ namespace Perspex.Controls
// We allow ContentControls without ContentPresenters in the template. This can be // 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 // 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. // have a ContentPresenter in the visual tree for that.
var presenter = this.FindTemplateChild<ContentPresenter>("contentPresenter"); this.Presenter = this.FindTemplateChild<ContentPresenter>("contentPresenter");
this.logicalChildren.Source = ((ILogical)this.Presenter)?.LogicalChildren;
if (presenter != null)
{
this.logicalChildren.Source = ((ILogical)presenter).LogicalChildren;
}
else
{
this.logicalChildren.Source = null;
}
}
private void SetContentParent(Tuple<object, object> change)
{
var control1 = change.Item1 as Control;
var control2 = change.Item2 as Control;
if (control1 != null)
{
control1.Parent = null;
}
if (control2 != null)
{
control2.Parent = this;
}
} }
} }
} }

4
Perspex.Controls/ControlExtensions.cs

@ -17,11 +17,11 @@ namespace Perspex.Controls
public static class ControlExtensions public static class ControlExtensions
{ {
public static T FindControl<T>(this Control control, string id) where T : Control public static T FindControl<T>(this Control control, string name) where T : Control
{ {
return control.GetLogicalDescendents() return control.GetLogicalDescendents()
.OfType<T>() .OfType<T>()
.FirstOrDefault(x => x.Name == id); .FirstOrDefault(x => x.Name == name);
} }
} }
} }

14
Perspex.Controls/GlobalSuppressions.cs

@ -0,0 +1,14 @@
// -----------------------------------------------------------------------
// <copyright file="GlobalSuppressions.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(
"StyleCop.CSharp.MaintainabilityRules",
"SA1401:Fields must be private",
Justification = "PerspexProperty fields should not be private.")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(
"StyleCop.CSharp.DocumentationRules",
"SA1609:Property documentation must have value",
Justification = "This rule is fscking pointless")]

17
Perspex.Controls/IMenu.cs

@ -1,17 +0,0 @@
// -----------------------------------------------------------------------
// <copyright file="IMenu.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Controls
{
internal interface IMenu
{
void ChildPointerEnter(MenuItem item);
void ChildSubMenuOpened(MenuItem item);
void CloseMenu();
}
}

30
Perspex.Controls/ItemsControl.cs

@ -6,17 +6,18 @@
namespace Perspex.Controls namespace Perspex.Controls
{ {
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Perspex.Collections; using Perspex.Collections;
using Perspex.Controls.Generators; using Perspex.Controls.Generators;
using Perspex.Controls.Presenters; using Perspex.Controls.Presenters;
using Perspex.Controls.Primitives; using Perspex.Controls.Primitives;
using Perspex.Controls.Templates; using Perspex.Controls.Templates;
using Perspex.Controls.Utils; using Perspex.Controls.Utils;
using System; using Perspex.Styling;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
public class ItemsControl : TemplatedControl, ILogical public class ItemsControl : TemplatedControl, ILogical
{ {
@ -35,6 +36,8 @@ namespace Perspex.Controls
private PerspexReadOnlyListView<IVisual, ILogical> logicalChildren = private PerspexReadOnlyListView<IVisual, ILogical> logicalChildren =
new PerspexReadOnlyListView<IVisual, ILogical>(x => (ILogical)x); new PerspexReadOnlyListView<IVisual, ILogical>(x => (ILogical)x);
private IItemsPresenter presenter;
static ItemsControl() static ItemsControl()
{ {
ItemsProperty.Changed.Subscribe(e => ItemsProperty.Changed.Subscribe(e =>
@ -85,8 +88,16 @@ namespace Perspex.Controls
protected IItemsPresenter Presenter protected IItemsPresenter Presenter
{ {
get; get
private set; {
return this.presenter;
}
set
{
this.presenter = value;
this.logicalChildren.Source = ((IVisual)value?.Panel)?.VisualChildren;
}
} }
protected virtual ItemContainerGenerator CreateItemContainerGenerator() protected virtual ItemContainerGenerator CreateItemContainerGenerator()
@ -97,11 +108,6 @@ namespace Perspex.Controls
protected override void OnTemplateApplied() protected override void OnTemplateApplied()
{ {
this.Presenter = this.FindTemplateChild<IItemsPresenter>("itemsPresenter"); this.Presenter = this.FindTemplateChild<IItemsPresenter>("itemsPresenter");
if (this.Presenter != null)
{
this.logicalChildren.Source = ((IVisual)this.Presenter.Panel).VisualChildren;
}
} }
protected virtual void ItemsChanged(IEnumerable oldValue, IEnumerable newValue) protected virtual void ItemsChanged(IEnumerable oldValue, IEnumerable newValue)

135
Perspex.Controls/Menu.cs

@ -8,52 +8,57 @@ namespace Perspex.Controls
{ {
using System; using System;
using System.Linq; using System.Linq;
using System.Reactive.Disposables;
using Perspex.Input; using Perspex.Input;
using Perspex.LogicalTree; using Perspex.LogicalTree;
using Perspex.Rendering; using Perspex.Rendering;
using System.Reactive.Disposables; using Perspex.Interactivity;
public class Menu : ItemsControl, IMenu /// <summary>
/// A top-level menu control.
/// </summary>
public class Menu : ItemsControl
{ {
/// <summary>
/// Defines the default items panel used by a <see cref="Menu"/>.
/// </summary>
private static readonly ItemsPanelTemplate DefaultPanel = private static readonly ItemsPanelTemplate DefaultPanel =
new ItemsPanelTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); new ItemsPanelTemplate(() => new StackPanel { Orientation = Orientation.Horizontal });
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsOpenProperty =
PerspexProperty.Register<Menu, bool>(nameof(IsOpen));
/// <summary>
/// Tracks event handlers added to the root of the visual tree.
/// </summary>
private IDisposable subscription; private IDisposable subscription;
/// <summary>
/// Initializes static members of the <see cref="Menu"/> class.
/// </summary>
static Menu() static Menu()
{ {
ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel);
MenuItem.ClickEvent.AddClassHandler<Menu>(x => x.OnMenuClick);
MenuItem.SubmenuOpenedEvent.AddClassHandler<Menu>(x => x.OnSubmenuOpened);
} }
void IMenu.ChildPointerEnter(MenuItem item) /// <summary>
{ /// Gets a value indicating whether the menu is open.
var children = this.GetLogicalChildren().Cast<MenuItem>(); /// </summary>
public bool IsOpen
if (children.Any(x => x.IsSubMenuOpen))
{
foreach (MenuItem i in this.GetLogicalChildren())
{
i.IsSubMenuOpen = i == item;
}
}
}
void IMenu.ChildSubMenuOpened(MenuItem item)
{
foreach (MenuItem i in this.GetLogicalChildren())
{
i.IsSubMenuOpen = i == item;
}
}
void IMenu.CloseMenu()
{ {
foreach (MenuItem i in this.GetLogicalChildren()) get { return this.GetValue(IsOpenProperty); }
{ private set { this.SetValue(IsOpenProperty, value); }
i.IsSubMenuOpen = false;
}
} }
/// <summary>
/// Called when the <see cref="MenuItem"/> is attached to the visual tree.
/// </summary>
/// <param name="root">The root of the visual tree.</param>
protected override void OnAttachedToVisualTree(IRenderRoot root) protected override void OnAttachedToVisualTree(IRenderRoot root)
{ {
base.OnAttachedToVisualTree(root); base.OnAttachedToVisualTree(root);
@ -65,22 +70,90 @@ namespace Perspex.Controls
this.subscription = new CompositeDisposable( this.subscription = new CompositeDisposable(
topLevel.AddHandler( topLevel.AddHandler(
InputElement.PointerPressedEvent, InputElement.PointerPressedEvent,
this.Deactivated, this.TopLevelPointerPress,
Interactivity.RoutingStrategies.Tunnel), Interactivity.RoutingStrategies.Tunnel),
Disposable.Create(() => topLevel.Deactivated -= this.Deactivated)); Disposable.Create(() => topLevel.Deactivated -= this.Deactivated));
} }
/// <summary>
/// Called when the <see cref="MenuItem"/> is detached from the visual tree.
/// </summary>
/// <param name="oldRoot">The root of the visual tree being detached from.</param>
protected override void OnDetachedFromVisualTree(IRenderRoot oldRoot) protected override void OnDetachedFromVisualTree(IRenderRoot oldRoot)
{ {
base.OnDetachedFromVisualTree(oldRoot); base.OnDetachedFromVisualTree(oldRoot);
this.subscription.Dispose(); this.subscription.Dispose();
} }
/// <summary>
/// Called when a submenu opens somewhere in the menu.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnSubmenuOpened(RoutedEventArgs e)
{
var menuItem = e.Source as MenuItem;
if (menuItem != null && menuItem.Parent == this)
{
foreach (var child in this.Items.OfType<MenuItem>())
{
if (child != menuItem && child.IsSubMenuOpen)
{
child.IsSubMenuOpen = false;
}
}
}
this.IsOpen = true;
}
/// <summary>
/// Closes the menu.
/// </summary>
private void CloseMenu()
{
foreach (MenuItem i in this.GetLogicalChildren())
{
i.IsSubMenuOpen = false;
}
this.IsOpen = false;
}
/// <summary>
/// Called when the top-level window is deactivated.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void Deactivated(object sender, EventArgs e) private void Deactivated(object sender, EventArgs e)
{ {
foreach (var i in this.GetLogicalChildren().Cast<MenuItem>()) this.CloseMenu();
}
/// <summary>
/// Called when a submenu is clicked somewhere in the menu.
/// </summary>
/// <param name="e">The event args.</param>
private void OnMenuClick(RoutedEventArgs e)
{
this.CloseMenu();
}
/// <summary>
/// Called when the pointer is pressed anywhere on the window.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void TopLevelPointerPress(object sender, PointerPressEventArgs e)
{
if (this.IsOpen)
{ {
i.IsSubMenuOpen = false; var control = e.Source as ILogical;
if (!this.IsLogicalParentOf(control))
{
this.CloseMenu();
}
} }
} }
} }

247
Perspex.Controls/MenuItem.cs

@ -11,81 +11,154 @@ namespace Perspex.Controls
using System.Windows.Input; using System.Windows.Input;
using Perspex.Controls.Primitives; using Perspex.Controls.Primitives;
using Perspex.Input; using Perspex.Input;
using Perspex.LogicalTree;
using Perspex.VisualTree;
using Perspex.Interactivity; using Perspex.Interactivity;
using Perspex.Rendering;
using Perspex.Controls.Templates;
using Perspex.Controls.Presenters;
public class MenuItem : HeaderedItemsControl, IMenu /// <summary>
/// A menu item control.
/// </summary>
public class MenuItem : HeaderedItemsControl
{ {
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
public static readonly PerspexProperty<ICommand> CommandProperty = public static readonly PerspexProperty<ICommand> CommandProperty =
Button.CommandProperty.AddOwner<MenuItem>(); Button.CommandProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="CommandParameter"/> property.
/// </summary>
public static readonly PerspexProperty<object> CommandParameterProperty = public static readonly PerspexProperty<object> CommandParameterProperty =
Button.CommandParameterProperty.AddOwner<MenuItem>(); Button.CommandParameterProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="Icon"/> property.
/// </summary>
public static readonly PerspexProperty<object> IconProperty = public static readonly PerspexProperty<object> IconProperty =
PerspexProperty.Register<MenuItem, object>("Icon"); PerspexProperty.Register<MenuItem, object>(nameof(Icon));
/// <summary>
/// Defines the <see cref="IsSubMenuOpen"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsSubMenuOpenProperty = public static readonly PerspexProperty<bool> IsSubMenuOpenProperty =
PerspexProperty.Register<MenuItem, bool>("IsSubMenuOpen"); PerspexProperty.Register<MenuItem, bool>(nameof(IsSubMenuOpen));
/// <summary>
/// Defines the <see cref="Click"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> ClickEvent = public static readonly RoutedEvent<RoutedEventArgs> ClickEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>("Click", RoutingStrategies.Bubble); RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(Click), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="SubmenuOpened"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> SubmenuOpenedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(SubmenuOpened), RoutingStrategies.Bubble);
/// <summary>
/// The timer used to display submenus.
/// </summary>
private IDisposable submenuTimer;
/// <summary>
/// Initializes static members of the <see cref="MenuItem"/> class.
/// </summary>
static MenuItem() static MenuItem()
{ {
ClickEvent.AddClassHandler<MenuItem>(x => x.OnClick); ClickEvent.AddClassHandler<MenuItem>(x => x.OnClick);
SubmenuOpenedEvent.AddClassHandler<MenuItem>(x => x.OnSubmenuOpened);
IsSubMenuOpenProperty.Changed.Subscribe(SubMenuOpenChanged); IsSubMenuOpenProperty.Changed.Subscribe(SubMenuOpenChanged);
} }
/// <summary>
/// Occurs when a <see cref="MenuItem"/> without a submenu is clicked.
/// </summary>
public event EventHandler<RoutedEventArgs> Click public event EventHandler<RoutedEventArgs> Click
{ {
add { this.AddHandler(ClickEvent, value); } add { this.AddHandler(ClickEvent, value); }
remove { this.RemoveHandler(ClickEvent, value); } remove { this.RemoveHandler(ClickEvent, value); }
} }
/// <summary>
/// Occurs when a <see cref="MenuItem"/>'s submenu is opened.
/// </summary>
public event EventHandler<RoutedEventArgs> SubmenuOpened
{
add { this.AddHandler(SubmenuOpenedEvent, value); }
remove { this.RemoveHandler(SubmenuOpenedEvent, value); }
}
/// <summary>
/// Gets or sets the command associated with the menu item.
/// </summary>
public ICommand Command public ICommand Command
{ {
get { return this.GetValue(CommandProperty); } get { return this.GetValue(CommandProperty); }
set { this.SetValue(CommandProperty, value); } set { this.SetValue(CommandProperty, value); }
} }
/// <summary>
/// Gets or sets the parameter to pass to the <see cref="Command"/> property of a
/// <see cref="MenuItem"/>.
/// </summary>
public object CommandParameter public object CommandParameter
{ {
get { return this.GetValue(CommandParameterProperty); } get { return this.GetValue(CommandParameterProperty); }
set { this.SetValue(CommandParameterProperty, value); } set { this.SetValue(CommandParameterProperty, value); }
} }
/// <summary>
/// Gets or sets the icon that appears in a <see cref="MenuItem"/>.
/// </summary>
public object Icon public object Icon
{ {
get { return this.GetValue(IconProperty); } get { return this.GetValue(IconProperty); }
set { this.SetValue(IconProperty, value); } set { this.SetValue(IconProperty, value); }
} }
/// <summary>
/// Gets or sets a value that indicates whether the submenu of the <see cref="MenuItem"/> is
/// open.
/// </summary>
public bool IsSubMenuOpen public bool IsSubMenuOpen
{ {
get { return this.GetValue(IsSubMenuOpenProperty); } get { return this.GetValue(IsSubMenuOpenProperty); }
set { this.SetValue(IsSubMenuOpenProperty, value); } set { this.SetValue(IsSubMenuOpenProperty, value); }
} }
void IMenu.ChildPointerEnter(MenuItem item) /// <summary>
/// Gets or sets a value that indicates whether the <see cref="MenuItem"/> has a submenu.
/// </summary>
public bool HasSubMenu
{ {
get { return !this.Classes.Contains(":empty"); }
} }
void IMenu.ChildSubMenuOpened(MenuItem item) /// <summary>
/// Gets a value that indicates whether the <see cref="MenuItem"/> is a top-level menu item.
/// </summary>
public bool IsTopLevel
{ {
foreach (var i in this.Items.Cast<object>().OfType<MenuItem>()) get;
{ private set;
i.IsSubMenuOpen = i == item;
}
} }
void IMenu.CloseMenu() /// <summary>
/// Called when the <see cref="MenuItem"/> is attached to the visual tree.
/// </summary>
/// <param name="root">The root of the visual tree.</param>
protected override void OnAttachedToVisualTree(IRenderRoot root)
{ {
this.IsSubMenuOpen = false; base.OnAttachedToVisualTree(root);
this.GetParentMenu().CloseMenu(); this.IsTopLevel = this.Parent is Menu;
} }
/// <summary>
/// Called when the <see cref="MenuItem"/> is clicked.
/// </summary>
/// <param name="e">The click event args.</param>
protected virtual void OnClick(RoutedEventArgs e) protected virtual void OnClick(RoutedEventArgs e)
{ {
if (this.Command != null) if (this.Command != null)
@ -94,71 +167,157 @@ namespace Perspex.Controls
} }
} }
/// <summary>
/// Called when the pointer enters the <see cref="MenuItem"/>.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPointerEnter(PointerEventArgs e) protected override void OnPointerEnter(PointerEventArgs e)
{ {
base.OnPointerEnter(e); base.OnPointerEnter(e);
this.GetLogicalParent<IMenu>()?.ChildPointerEnter(this);
}
protected override void OnPointerPressed(PointerPressEventArgs e) var menu = this.Parent as Menu;
{
base.OnPointerPressed(e);
if (this.Classes.Contains(":empty")) if (menu != null && menu.IsOpen)
{ {
RoutedEventArgs click = new RoutedEventArgs this.IsSubMenuOpen = true;
{
RoutedEvent = ClickEvent,
};
this.RaiseEvent(click);
this.GetParentMenu().CloseMenu();
} }
else }
/// <summary>
/// Called when the pointer leaves the <see cref="MenuItem"/>.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPointerLeave(PointerEventArgs e)
{
base.OnPointerLeave(e);
if (this.submenuTimer != null)
{ {
this.IsSubMenuOpen = !this.IsSubMenuOpen; this.submenuTimer.Dispose();
this.submenuTimer = null;
} }
} }
private IMenu GetParentMenu() /// <summary>
/// Called when the pointer is pressed over the <see cref="MenuItem"/>.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPointerPressed(PointerPressEventArgs e)
{ {
var parent = this.GetLogicalParent<IMenu>(); base.OnPointerPressed(e);
if (parent != null) if (!this.HasSubMenu)
{ {
return parent; this.RaiseEvent(new RoutedEventArgs(ClickEvent));
}
else if (this.IsTopLevel)
{
this.IsSubMenuOpen = !this.IsSubMenuOpen;
} }
else else
{ {
var popupRoot = this.GetVisualAncestors().OfType<PopupRoot>().FirstOrDefault(); this.IsSubMenuOpen = true;
var parentItem = ((ILogical)popupRoot).GetLogicalParent<Popup>().TemplatedParent;
return (IMenu)parentItem;
} }
e.Handled = true;
} }
private void OnSubMenuOpenChanged(bool open) /// <summary>
/// Called when a submenu is opened on this MenuItem or a child MenuItem.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnSubmenuOpened(RoutedEventArgs e)
{ {
if (!open && this.Items != null) var menuItem = e.Source as MenuItem;
if (menuItem != null && menuItem.Parent == this)
{ {
foreach (var item in this.Items.Cast<object>().OfType<MenuItem>()) foreach (var child in this.Items.OfType<MenuItem>())
{ {
item.IsSubMenuOpen = false; if (child != menuItem && child.IsSubMenuOpen)
{
child.IsSubMenuOpen = false;
}
} }
} }
else if (open) }
/// <summary>
/// Called when the MenuItem's template has been applied.
/// </summary>
protected override void OnTemplateApplied()
{
base.OnTemplateApplied();
var popup = this.FindTemplateChild<Popup>("popup");
if (popup != null)
{
popup.Opened += this.PopupFirstOpened;
}
}
/// <summary>
/// Closes all submenus of the menu item.
/// </summary>
private void CloseSubmenus()
{
foreach (var child in this.Items.OfType<MenuItem>())
{ {
this.GetParentMenu().ChildSubMenuOpened(this); child.IsSubMenuOpen = false;
} }
} }
/// <summary>
/// Called when the <see cref="IsSubMenuOpen"/> property changes.
/// </summary>
/// <param name="e">The property change event.</param>
private static void SubMenuOpenChanged(PerspexPropertyChangedEventArgs e) private static void SubMenuOpenChanged(PerspexPropertyChangedEventArgs e)
{ {
var sender = e.Sender as MenuItem; var sender = e.Sender as MenuItem;
var value = (bool)e.NewValue;
if (sender != null) if (sender != null)
{ {
sender.OnSubMenuOpenChanged((bool)e.NewValue); if (value)
{
sender.RaiseEvent(new RoutedEventArgs(SubmenuOpenedEvent));
}
else
{
sender.CloseSubmenus();
}
}
}
/// <summary>
/// Called the first time the MenuItem's popup is opened.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void PopupFirstOpened(object sender, EventArgs e)
{
var popup = (Popup)sender;
// Our ItemsPresenter is in a Popup which means that it's only created when the
// Popup is opened, therefore it wasn't found by ItemsControl.OnTemplateApplied.
// Now the Popup has been opened for the first time it should exist, so make sure
// the PopupRoot's template is applied and look for the ItemsPresenter.
popup.PopupRoot.ApplyTemplate();
var presenter = popup.PopupRoot.FindControl<ItemsPresenter>("itemsPresenter");
if (presenter != null)
{
// The presenter was found. First make its Panel's ChildLogicalParent point to
// this so that the child MenuItems will be logically parented by the parent
// MenuItem and then assign it to our Presenter property.
presenter.ApplyTemplate();
((IItemsPanel)presenter.Panel).ChildLogicalParent = this;
this.Presenter = presenter;
} }
// Don't call this event handler again.
popup.Opened -= this.PopupFirstOpened;
} }
} }
} }

12
Perspex.Controls/Panel.cs

@ -72,8 +72,16 @@ namespace Perspex.Controls
ILogical IItemsPanel.ChildLogicalParent ILogical IItemsPanel.ChildLogicalParent
{ {
get { return this.childLogicalParent; } get
set { this.childLogicalParent = value; } {
return this.childLogicalParent;
}
set
{
this.childLogicalParent = value;
this.SetLogicalParent(this.Children);
}
} }
protected virtual void OnChildrenAdded(IEnumerable<Control> child) protected virtual void OnChildrenAdded(IEnumerable<Control> child)

2
Perspex.Controls/Perspex.Controls.csproj

@ -37,7 +37,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Border.cs" /> <Compile Include="Border.cs" />
<Compile Include="IMenu.cs" /> <Compile Include="GlobalSuppressions.cs" />
<Compile Include="MenuItem.cs" /> <Compile Include="MenuItem.cs" />
<Compile Include="Menu.cs" /> <Compile Include="Menu.cs" />
<Compile Include="Button.cs" /> <Compile Include="Button.cs" />

12
Perspex.Controls/Popup.cs

@ -45,6 +45,10 @@ namespace Perspex.Controls
this.GetObservableWithHistory(ChildProperty).Subscribe(ChildChanged); this.GetObservableWithHistory(ChildProperty).Subscribe(ChildChanged);
} }
public event EventHandler Closed;
public event EventHandler Opened;
public Control Child public Control Child
{ {
get { return this.GetValue(ChildProperty); } get { return this.GetValue(ChildProperty); }
@ -69,6 +73,11 @@ namespace Perspex.Controls
set { this.SetValue(PlacementTargetProperty, value); } set { this.SetValue(PlacementTargetProperty, value); }
} }
public PopupRoot PopupRoot
{
get { return this.popupRoot; }
}
public bool StaysOpen public bool StaysOpen
{ {
get { return this.GetValue(StaysOpenProperty); } get { return this.GetValue(StaysOpenProperty); }
@ -108,6 +117,8 @@ namespace Perspex.Controls
this.topLevel.AddHandler(TopLevel.PointerPressedEvent, this.MaybeClose, RoutingStrategies.Tunnel); this.topLevel.AddHandler(TopLevel.PointerPressedEvent, this.MaybeClose, RoutingStrategies.Tunnel);
this.popupRoot.Show(); this.popupRoot.Show();
this.IsOpen = true;
this.Opened?.Invoke(this, EventArgs.Empty);
} }
public void Close() public void Close()
@ -121,6 +132,7 @@ namespace Perspex.Controls
} }
this.IsOpen = false; this.IsOpen = false;
this.Closed?.Invoke(this, EventArgs.Empty);
} }
protected override Size MeasureCore(Size availableSize) protected override Size MeasureCore(Size availableSize)

8
Perspex.Controls/PopupRoot.cs

@ -6,11 +6,12 @@
namespace Perspex.Controls namespace Perspex.Controls
{ {
using Perspex.Interactivity;
using Perspex.Media; using Perspex.Media;
using Perspex.Platform; using Perspex.Platform;
using Splat; using Splat;
public class PopupRoot : TopLevel public class PopupRoot : TopLevel, IInteractive
{ {
static PopupRoot() static PopupRoot()
{ {
@ -27,6 +28,11 @@ namespace Perspex.Controls
get { return (IPopupImpl)base.PlatformImpl; } get { return (IPopupImpl)base.PlatformImpl; }
} }
IInteractive IInteractive.InteractiveParent
{
get { return this.Parent; }
}
public void SetPosition(Point p) public void SetPosition(Point p)
{ {
this.PlatformImpl.SetPosition(p); this.PlatformImpl.SetPosition(p);

2
Perspex.Controls/Presenters/ContentPresenter.cs

@ -87,6 +87,8 @@ namespace Perspex.Controls.Presenters
if (content != null) if (content != null)
{ {
result = this.MaterializeDataTemplate(content); result = this.MaterializeDataTemplate(content);
result.Parent = this.TemplatedParent as Control;
var templatedParent = this.TemplatedParent as TemplatedControl; var templatedParent = this.TemplatedParent as TemplatedControl;
if (templatedParent != null) if (templatedParent != null)

2
Perspex.Input/IInputElement.cs

@ -9,7 +9,7 @@ namespace Perspex.Input
using System; using System;
using Perspex.Interactivity; using Perspex.Interactivity;
public interface IInputElement : IInteractive public interface IInputElement : IInteractive, IVisual
{ {
event EventHandler<RoutedEventArgs> GotFocus; event EventHandler<RoutedEventArgs> GotFocus;

9
Perspex.Interactive.UnitTests/GlobalSuppressions.cs

@ -0,0 +1,9 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(
"StyleCop.CSharp.DocumentationRules",
"SA1600:Elements must be documented",
Justification = "Tests should be self-documenting")]

13
Perspex.Interactive.UnitTests/InteractiveTests.cs

@ -9,7 +9,6 @@ namespace Perspex.Interactive.UnitTests
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Perspex.Collections;
using Perspex.Interactivity; using Perspex.Interactivity;
using Perspex.VisualTree; using Perspex.VisualTree;
using Xunit; using Xunit;
@ -82,9 +81,9 @@ namespace Perspex.Interactive.UnitTests
public void Tunneling_Bubbling_Event_Should_Tunnel_Then_Bubble_Up() public void Tunneling_Bubbling_Event_Should_Tunnel_Then_Bubble_Up()
{ {
var ev = new RoutedEvent( var ev = new RoutedEvent(
"test", "test",
RoutingStrategies.Bubble | RoutingStrategies.Tunnel, RoutingStrategies.Bubble | RoutingStrategies.Tunnel,
typeof(RoutedEventArgs), typeof(RoutedEventArgs),
typeof(TestInteractive)); typeof(TestInteractive));
var invoked = new List<string>(); var invoked = new List<string>();
EventHandler<RoutedEventArgs> handler = (s, e) => invoked.Add(((TestInteractive)s).Name); EventHandler<RoutedEventArgs> handler = (s, e) => invoked.Add(((TestInteractive)s).Name);
@ -117,7 +116,7 @@ namespace Perspex.Interactive.UnitTests
RoutingStrategies.Tunnel, RoutingStrategies.Tunnel,
RoutingStrategies.Bubble, RoutingStrategies.Bubble,
RoutingStrategies.Bubble, RoutingStrategies.Bubble,
}, },
invoked); invoked);
} }
@ -299,7 +298,7 @@ namespace Perspex.Interactive.UnitTests
} }
private TestInteractive CreateTree( private TestInteractive CreateTree(
RoutedEvent ev, RoutedEvent ev,
EventHandler<RoutedEventArgs> handler, EventHandler<RoutedEventArgs> handler,
RoutingStrategies handlerRoutes, RoutingStrategies handlerRoutes,
bool handledEventsToo = false) bool handledEventsToo = false)
@ -336,7 +335,7 @@ namespace Perspex.Interactive.UnitTests
i.AddHandler(ev, handler, handlerRoutes, handledEventsToo); i.AddHandler(ev, handler, handlerRoutes, handledEventsToo);
} }
} }
return target; return target;
} }

1
Perspex.Interactive.UnitTests/Perspex.Interactive.UnitTests.csproj

@ -55,6 +55,7 @@
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="InteractiveTests.cs" /> <Compile Include="InteractiveTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>

14
Perspex.Interactivity/GlobalSuppressions.cs

@ -0,0 +1,14 @@
// -----------------------------------------------------------------------
// <copyright file="GlobalSuppressions.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(
"StyleCop.CSharp.MaintainabilityRules",
"SA1401:Fields must be private",
Justification = "Routed event fields should not be private.")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(
"StyleCop.CSharp.DocumentationRules",
"SA1609:Property documentation must have value",
Justification = "This rule is fscking pointless")]

42
Perspex.Interactivity/IInteractive.cs

@ -8,25 +8,65 @@ namespace Perspex.Interactivity
{ {
using System; using System;
public interface IInteractive : IVisual /// <summary>
/// Interface for objects that raise routed events.
/// </summary>
public interface IInteractive
{ {
/// <summary>
/// Gets the interactive parent of the object for bubbling and tunnelling events.
/// </summary>
IInteractive InteractiveParent { get; }
/// <summary>
/// Adds a handler for the specified routed event.
/// </summary>
/// <param name="routedEvent">The routed event.</param>
/// <param name="handler">The handler.</param>
/// <param name="routes">The routing strategies to listen to.</param>
/// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
/// <returns>A disposable that terminates the event subscription.</returns>
IDisposable AddHandler( IDisposable AddHandler(
RoutedEvent routedEvent, RoutedEvent routedEvent,
Delegate handler, Delegate handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false); bool handledEventsToo = false);
/// <summary>
/// Adds a handler for the specified routed event.
/// </summary>
/// <typeparam name="TEventArgs">The type of the event's args.</typeparam>
/// <param name="routedEvent">The routed event.</param>
/// <param name="handler">The handler.</param>
/// <param name="routes">The routing strategies to listen to.</param>
/// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
/// <returns>A disposable that terminates the event subscription.</returns>
IDisposable AddHandler<TEventArgs>( IDisposable AddHandler<TEventArgs>(
RoutedEvent<TEventArgs> routedEvent, RoutedEvent<TEventArgs> routedEvent,
EventHandler<TEventArgs> handler, EventHandler<TEventArgs> handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false) where TEventArgs : RoutedEventArgs; bool handledEventsToo = false) where TEventArgs : RoutedEventArgs;
/// <summary>
/// Removes a handler for the specified routed event.
/// </summary>
/// <param name="routedEvent">The routed event.</param>
/// <param name="handler">The handler.</param>
void RemoveHandler(RoutedEvent routedEvent, Delegate handler); void RemoveHandler(RoutedEvent routedEvent, Delegate handler);
/// <summary>
/// Removes a handler for the specified routed event.
/// </summary>
/// <typeparam name="TEventArgs">The type of the event's args.</typeparam>
/// <param name="routedEvent">The routed event.</param>
/// <param name="handler">The handler.</param>
void RemoveHandler<TEventArgs>(RoutedEvent<TEventArgs> routedEvent, EventHandler<TEventArgs> handler) void RemoveHandler<TEventArgs>(RoutedEvent<TEventArgs> routedEvent, EventHandler<TEventArgs> handler)
where TEventArgs : RoutedEventArgs; where TEventArgs : RoutedEventArgs;
/// <summary>
/// Raises a routed event.
/// </summary>
/// <param name="e">The event args.</param>
void RaiseEvent(RoutedEventArgs e); void RaiseEvent(RoutedEventArgs e);
} }
} }

82
Perspex.Interactivity/Interactive.cs

@ -15,13 +15,32 @@ namespace Perspex.Interactivity
using Perspex.Layout; using Perspex.Layout;
using Perspex.VisualTree; using Perspex.VisualTree;
/// <summary>
/// Base class for objects that raise routed events.
/// </summary>
public class Interactive : Layoutable, IInteractive public class Interactive : Layoutable, IInteractive
{ {
private Dictionary<RoutedEvent, List<EventSubscription>> eventHandlers = private Dictionary<RoutedEvent, List<EventSubscription>> eventHandlers =
new Dictionary<RoutedEvent, List<EventSubscription>>(); new Dictionary<RoutedEvent, List<EventSubscription>>();
/// <summary>
/// Gets the interactive parent of the object for bubbling and tunnelling events.
/// </summary>
IInteractive IInteractive.InteractiveParent
{
get { return ((IVisual)this).VisualParent as IInteractive; }
}
/// <summary>
/// Adds a handler for the specified routed event.
/// </summary>
/// <param name="routedEvent">The routed event.</param>
/// <param name="handler">The handler.</param>
/// <param name="routes">The routing strategies to listen to.</param>
/// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
/// <returns>A disposable that terminates the event subscription.</returns>
public IDisposable AddHandler( public IDisposable AddHandler(
RoutedEvent routedEvent, RoutedEvent routedEvent,
Delegate handler, Delegate handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false) bool handledEventsToo = false)
@ -49,6 +68,15 @@ namespace Perspex.Interactivity
return Disposable.Create(() => subscriptions.Remove(sub)); return Disposable.Create(() => subscriptions.Remove(sub));
} }
/// <summary>
/// Adds a handler for the specified routed event.
/// </summary>
/// <typeparam name="TEventArgs">The type of the event's args.</typeparam>
/// <param name="routedEvent">The routed event.</param>
/// <param name="handler">The handler.</param>
/// <param name="routes">The routing strategies to listen to.</param>
/// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
/// <returns>A disposable that terminates the event subscription.</returns>
public IDisposable AddHandler<TEventArgs>( public IDisposable AddHandler<TEventArgs>(
RoutedEvent<TEventArgs> routedEvent, RoutedEvent<TEventArgs> routedEvent,
EventHandler<TEventArgs> handler, EventHandler<TEventArgs> handler,
@ -58,15 +86,11 @@ namespace Perspex.Interactivity
return this.AddHandler(routedEvent, (Delegate)handler, routes, handledEventsToo); return this.AddHandler(routedEvent, (Delegate)handler, routes, handledEventsToo);
} }
public IObservable<EventPattern<T>> GetObservable<T>(RoutedEvent<T> routedEvent) where T : RoutedEventArgs /// <summary>
{ /// Removes a handler for the specified routed event.
Contract.Requires<NullReferenceException>(routedEvent != null); /// </summary>
/// <param name="routedEvent">The routed event.</param>
return Observable.FromEventPattern<T>( /// <param name="handler">The handler.</param>
handler => this.AddHandler(routedEvent, handler),
handler => this.RemoveHandler(routedEvent, handler));
}
public void RemoveHandler(RoutedEvent routedEvent, Delegate handler) public void RemoveHandler(RoutedEvent routedEvent, Delegate handler)
{ {
Contract.Requires<NullReferenceException>(routedEvent != null); Contract.Requires<NullReferenceException>(routedEvent != null);
@ -80,12 +104,22 @@ namespace Perspex.Interactivity
} }
} }
public void RemoveHandler<TEventArgs>(RoutedEvent<TEventArgs> routedEvent, EventHandler<TEventArgs> handler) /// <summary>
/// Removes a handler for the specified routed event.
/// </summary>
/// <typeparam name="TEventArgs">The type of the event's args.</typeparam>
/// <param name="routedEvent">The routed event.</param>
/// <param name="handler">The handler.</param>
public void RemoveHandler<TEventArgs>(RoutedEvent<TEventArgs> routedEvent, EventHandler<TEventArgs> handler)
where TEventArgs : RoutedEventArgs where TEventArgs : RoutedEventArgs
{ {
this.RemoveHandler(routedEvent, (Delegate)handler); this.RemoveHandler(routedEvent, (Delegate)handler);
} }
/// <summary>
/// Raises a routed event.
/// </summary>
/// <param name="e">The event args.</param>
public void RaiseEvent(RoutedEventArgs e) public void RaiseEvent(RoutedEventArgs e)
{ {
Contract.Requires<NullReferenceException>(e != null); Contract.Requires<NullReferenceException>(e != null);
@ -110,30 +144,42 @@ namespace Perspex.Interactivity
} }
} }
/// <summary>
/// Bubbles an event.
/// </summary>
/// <param name="e">The event args.</param>
private void BubbleEvent(RoutedEventArgs e) private void BubbleEvent(RoutedEventArgs e)
{ {
Contract.Requires<NullReferenceException>(e != null); Contract.Requires<NullReferenceException>(e != null);
e.Route = RoutingStrategies.Bubble; e.Route = RoutingStrategies.Bubble;
foreach (var target in this.GetSelfAndVisualAncestors().OfType<Interactive>()) foreach (var target in this.GetBubbleEventRoute())
{ {
target.RaiseEventImpl(e); ((Interactive)target).RaiseEventImpl(e);
} }
} }
/// <summary>
/// Tunnels an event.
/// </summary>
/// <param name="e">The event args.</param>
private void TunnelEvent(RoutedEventArgs e) private void TunnelEvent(RoutedEventArgs e)
{ {
Contract.Requires<NullReferenceException>(e != null); Contract.Requires<NullReferenceException>(e != null);
e.Route = RoutingStrategies.Tunnel; e.Route = RoutingStrategies.Tunnel;
foreach (var target in this.GetSelfAndVisualAncestors().OfType<Interactive>().Reverse()) foreach (var target in this.GetTunnelEventRoute())
{ {
target.RaiseEventImpl(e); ((Interactive)target).RaiseEventImpl(e);
} }
} }
/// <summary>
/// Carries out the actual invocation of an event on this object.
/// </summary>
/// <param name="e">The event args.</param>
private void RaiseEventImpl(RoutedEventArgs e) private void RaiseEventImpl(RoutedEventArgs e)
{ {
Contract.Requires<NullReferenceException>(e != null); Contract.Requires<NullReferenceException>(e != null);
@ -146,9 +192,7 @@ namespace Perspex.Interactivity
{ {
foreach (var sub in subscriptions.ToList()) foreach (var sub in subscriptions.ToList())
{ {
bool correctRoute = bool correctRoute = (e.Route & sub.Routes) != 0;
(e.Route == RoutingStrategies.Direct && (sub.Routes & RoutingStrategies.Direct) != 0) ||
(e.Route != RoutingStrategies.Direct && (e.Route & sub.Routes) != 0);
bool notFinished = !e.Handled || sub.AlsoIfHandled; bool notFinished = !e.Handled || sub.AlsoIfHandled;
if (correctRoute && notFinished) if (correctRoute && notFinished)

41
Perspex.Interactivity/InteractiveExtensions.cs

@ -0,0 +1,41 @@
// -----------------------------------------------------------------------
// <copyright file="InteractiveExtensions.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Interactivity
{
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Provides extension methods for the <see cref="IInteractive"/> interface.
/// </summary>
public static class InteractiveExtensions
{
/// <summary>
/// Gets the route for bubbling events from the specified interactive.
/// </summary>
/// <param name="interactive">The interactive.</param>
/// <returns>The event route.</returns>
public static IEnumerable<IInteractive> GetBubbleEventRoute(this IInteractive interactive)
{
while (interactive != null)
{
yield return interactive;
interactive = interactive.InteractiveParent;
}
}
/// <summary>
/// Gets the route for tunneling events from the specified interactive.
/// </summary>
/// <param name="interactive">The interactive.</param>
/// <returns>The event route.</returns>
public static IEnumerable<IInteractive> GetTunnelEventRoute(this IInteractive interactive)
{
return interactive.GetBubbleEventRoute().Reverse();
}
}
}

2
Perspex.Interactivity/Perspex.Interactivity.csproj

@ -55,7 +55,9 @@
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="IInteractive.cs" /> <Compile Include="IInteractive.cs" />
<Compile Include="InteractiveExtensions.cs" />
<Compile Include="Interactive.cs" /> <Compile Include="Interactive.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="RoutedEvent.cs" /> <Compile Include="RoutedEvent.cs" />

5
Perspex.Interactivity/RoutedEventArgs.cs

@ -14,6 +14,11 @@ namespace Perspex.Interactivity
{ {
} }
public RoutedEventArgs(RoutedEvent routedEvent)
{
this.RoutedEvent = routedEvent;
}
public RoutedEventArgs(RoutedEvent routedEvent, IInteractive source) public RoutedEventArgs(RoutedEvent routedEvent, IInteractive source)
{ {
this.RoutedEvent = routedEvent; this.RoutedEvent = routedEvent;

5
Perspex.Styling/LogicalTree/LogicalExtensions.cs

@ -75,5 +75,10 @@ namespace Perspex.LogicalTree
} }
} }
} }
public static bool IsLogicalParentOf(this ILogical logical, ILogical target)
{
return target.GetLogicalAncestors().Any(x => x == logical);
}
} }
} }

10
TestApplication/Program.cs

@ -94,11 +94,11 @@ namespace TestApplication
static void Main(string[] args) static void Main(string[] args)
{ {
Log.Logger = new LoggerConfiguration() //Log.Logger = new LoggerConfiguration()
.Filter.ByIncludingOnly(Matching.WithProperty("Area", "Layout")) // .Filter.ByIncludingOnly(Matching.WithProperty("Area", "Layout"))
//.MinimumLevel.Verbose() // .MinimumLevel.Verbose()
.WriteTo.Trace(outputTemplate: "[{Id:X8}] [{SourceContext}] {Message}") // .WriteTo.Trace(outputTemplate: "[{Id:X8}] [{SourceContext}] {Message}")
.CreateLogger(); // .CreateLogger();
// The version of ReactiveUI currently included is for WPF and so expects a WPF // The version of ReactiveUI currently included is for WPF and so expects a WPF
// dispatcher. This makes sure it's initialized. // dispatcher. This makes sure it's initialized.

43
Tests/Perspex.Controls.UnitTests/ContentControlTests.cs

@ -95,12 +95,33 @@ namespace Perspex.Controls.UnitTests
} }
[Fact] [Fact]
public void Setting_Content_Should_Set_Child_Controls_Parent() public void Setting_Content_To_Control_Should_Set_Child_Controls_Parent()
{ {
var target = new ContentControl(); var target = new ContentControl
var child = new Control(); {
Template = this.GetTemplate(),
};
var child = new Control();
target.Content = child; target.Content = child;
target.ApplyTemplate();
Assert.Equal(child.Parent, target);
Assert.Equal(((ILogical)child).LogicalParent, target);
}
[Fact]
public void Setting_Content_To_String_Should_Set_Child_Controls_Parent()
{
var target = new ContentControl
{
Template = this.GetTemplate(),
};
target.Content = "Foo";
target.ApplyTemplate();
var child = target.Presenter.Child;
Assert.Equal(child.Parent, target); Assert.Equal(child.Parent, target);
Assert.Equal(((ILogical)child).LogicalParent, target); Assert.Equal(((ILogical)child).LogicalParent, target);
@ -234,6 +255,22 @@ namespace Perspex.Controls.UnitTests
Assert.True(called); Assert.True(called);
} }
[Fact]
public void Changing_Content_Should_Update_Presenter()
{
var target = new ContentControl();
target.Template = this.GetTemplate();
target.ApplyTemplate();
target.Content = "Foo";
target.Presenter.ApplyTemplate();
Assert.Equal("Foo", ((TextBlock)target.Presenter.Child).Text);
target.Content = "Bar";
target.Presenter.ApplyTemplate();
Assert.Equal("Bar", ((TextBlock)target.Presenter.Child).Text);
}
private ControlTemplate GetTemplate() private ControlTemplate GetTemplate()
{ {
return ControlTemplate.Create<ContentControl>(parent => return ControlTemplate.Create<ContentControl>(parent =>

9
Tests/Perspex.Controls.UnitTests/GlobalSuppressions.cs

@ -0,0 +1,9 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(
"StyleCop.CSharp.DocumentationRules",
"SA1600:Elements must be documented",
Justification = "Tests should be self-documenting")]

38
Tests/Perspex.Controls.UnitTests/ItemsControlTests.cs

@ -6,19 +6,14 @@
namespace Perspex.Controls.UnitTests namespace Perspex.Controls.UnitTests
{ {
using System;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using Perspex.Collections; using Perspex.Collections;
using Perspex.Controls; using Perspex.Controls;
using Perspex.Controls.Presenters; using Perspex.Controls.Presenters;
using Perspex.Controls.Templates; using Perspex.Controls.Templates;
using Perspex.Platform;
using Perspex.Styling; using Perspex.Styling;
using Perspex.VisualTree; using Perspex.VisualTree;
using Ploeh.AutoFixture;
using Ploeh.AutoFixture.AutoMoq;
using Splat;
using Xunit; using Xunit;
public class ItemsControlTests public class ItemsControlTests
@ -275,6 +270,27 @@ namespace Perspex.Controls.UnitTests
Assert.True(target.Classes.Contains(":empty")); Assert.True(target.Classes.Contains(":empty"));
} }
[Fact]
public void Setting_Presenter_Explicitly_Should_Set_Item_Parent()
{
var target = new TestItemsControl();
var child = new Control();
var presenter = new ItemsPresenter
{
TemplatedParent = target,
[~ItemsPresenter.ItemsProperty] = target[~ItemsControl.ItemsProperty],
};
presenter.ApplyTemplate();
target.Presenter = presenter;
target.Items = new[] { child };
target.ApplyTemplate();
Assert.Equal(target, child.Parent);
Assert.Equal(target, ((ILogical)child).LogicalParent);
}
private ControlTemplate GetTemplate() private ControlTemplate GetTemplate()
{ {
return ControlTemplate.Create<ItemsControl>(parent => return ControlTemplate.Create<ItemsControl>(parent =>
@ -291,13 +307,13 @@ namespace Perspex.Controls.UnitTests
}); });
} }
private IDisposable RegisterServices() private class TestItemsControl : ItemsControl
{ {
var result = Locator.CurrentMutable.WithResolver(); public new IItemsPresenter Presenter
var fixture = new Fixture().Customize(new AutoMoqCustomization()); {
var renderInterface = fixture.Create<IPlatformRenderInterface>(); get { return base.Presenter; }
Locator.CurrentMutable.RegisterConstant(renderInterface, typeof(IPlatformRenderInterface)); set { base.Presenter = value; }
return result; }
} }
} }
} }

1
Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@ -90,6 +90,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="ContentPresenterTests.cs" /> <Compile Include="ContentPresenterTests.cs" />
<Compile Include="BorderTests.cs" /> <Compile Include="BorderTests.cs" />
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="PopupTests.cs" /> <Compile Include="PopupTests.cs" />
<Compile Include="DropDownTests.cs" /> <Compile Include="DropDownTests.cs" />
<Compile Include="Presenters\ItemsPresenterTests.cs" /> <Compile Include="Presenters\ItemsPresenterTests.cs" />

Loading…
Cancel
Save