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 11 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()
{
this.GetObservableWithHistory(ContentProperty).Subscribe(this.SetContentParent);
}
public object Content
@ -37,6 +36,12 @@ namespace Perspex.Controls
set { this.SetValue(ContentProperty, value); }
}
public ContentPresenter Presenter
{
get;
private set;
}
public HorizontalAlignment HorizontalContentAlignment
{
get { return this.GetValue(HorizontalContentAlignmentProperty); }
@ -59,32 +64,8 @@ namespace Perspex.Controls
// 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.
var presenter = this.FindTemplateChild<ContentPresenter>("contentPresenter");
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;
}
this.Presenter = this.FindTemplateChild<ContentPresenter>("contentPresenter");
this.logicalChildren.Source = ((ILogical)this.Presenter)?.LogicalChildren;
}
}
}

4
Perspex.Controls/ControlExtensions.cs

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

135
Perspex.Controls/Menu.cs

@ -8,52 +8,57 @@ namespace Perspex.Controls
{
using System;
using System.Linq;
using System.Reactive.Disposables;
using Perspex.Input;
using Perspex.LogicalTree;
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 =
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;
/// <summary>
/// Initializes static members of the <see cref="Menu"/> class.
/// </summary>
static Menu()
{
ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel);
MenuItem.ClickEvent.AddClassHandler<Menu>(x => x.OnMenuClick);
MenuItem.SubmenuOpenedEvent.AddClassHandler<Menu>(x => x.OnSubmenuOpened);
}
void IMenu.ChildPointerEnter(MenuItem item)
{
var children = this.GetLogicalChildren().Cast<MenuItem>();
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()
/// <summary>
/// Gets a value indicating whether the menu is open.
/// </summary>
public bool IsOpen
{
foreach (MenuItem i in this.GetLogicalChildren())
{
i.IsSubMenuOpen = false;
}
get { return this.GetValue(IsOpenProperty); }
private set { this.SetValue(IsOpenProperty, value); }
}
/// <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)
{
base.OnAttachedToVisualTree(root);
@ -65,22 +70,90 @@ namespace Perspex.Controls
this.subscription = new CompositeDisposable(
topLevel.AddHandler(
InputElement.PointerPressedEvent,
this.Deactivated,
this.TopLevelPointerPress,
Interactivity.RoutingStrategies.Tunnel),
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)
{
base.OnDetachedFromVisualTree(oldRoot);
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)
{
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 Perspex.Controls.Primitives;
using Perspex.Input;
using Perspex.LogicalTree;
using Perspex.VisualTree;
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 =
Button.CommandProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="CommandParameter"/> property.
/// </summary>
public static readonly PerspexProperty<object> CommandParameterProperty =
Button.CommandParameterProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="Icon"/> property.
/// </summary>
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 =
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 =
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()
{
ClickEvent.AddClassHandler<MenuItem>(x => x.OnClick);
SubmenuOpenedEvent.AddClassHandler<MenuItem>(x => x.OnSubmenuOpened);
IsSubMenuOpenProperty.Changed.Subscribe(SubMenuOpenChanged);
}
/// <summary>
/// Occurs when a <see cref="MenuItem"/> without a submenu is clicked.
/// </summary>
public event EventHandler<RoutedEventArgs> Click
{
add { this.AddHandler(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
{
get { return this.GetValue(CommandProperty); }
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
{
get { return this.GetValue(CommandParameterProperty); }
set { this.SetValue(CommandParameterProperty, value); }
}
/// <summary>
/// Gets or sets the icon that appears in a <see cref="MenuItem"/>.
/// </summary>
public object Icon
{
get { return this.GetValue(IconProperty); }
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
{
get { return this.GetValue(IsSubMenuOpenProperty); }
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>())
{
i.IsSubMenuOpen = i == item;
}
get;
private set;
}
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;
this.GetParentMenu().CloseMenu();
base.OnAttachedToVisualTree(root);
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)
{
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)
{
base.OnPointerEnter(e);
this.GetLogicalParent<IMenu>()?.ChildPointerEnter(this);
}
protected override void OnPointerPressed(PointerPressEventArgs e)
{
base.OnPointerPressed(e);
var menu = this.Parent as Menu;
if (this.Classes.Contains(":empty"))
if (menu != null && menu.IsOpen)
{
RoutedEventArgs click = new RoutedEventArgs
{
RoutedEvent = ClickEvent,
};
this.RaiseEvent(click);
this.GetParentMenu().CloseMenu();
this.IsSubMenuOpen = true;
}
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
{
var popupRoot = this.GetVisualAncestors().OfType<PopupRoot>().FirstOrDefault();
var parentItem = ((ILogical)popupRoot).GetLogicalParent<Popup>().TemplatedParent;
return (IMenu)parentItem;
this.IsSubMenuOpen = true;
}
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)
{
var sender = e.Sender as MenuItem;
var value = (bool)e.NewValue;
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
{
get { return this.childLogicalParent; }
set { this.childLogicalParent = value; }
get
{
return this.childLogicalParent;
}
set
{
this.childLogicalParent = value;
this.SetLogicalParent(this.Children);
}
}
protected virtual void OnChildrenAdded(IEnumerable<Control> child)

2
Perspex.Controls/Perspex.Controls.csproj

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

12
Perspex.Controls/Popup.cs

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

8
Perspex.Controls/PopupRoot.cs

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

2
Perspex.Controls/Presenters/ContentPresenter.cs

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

2
Perspex.Input/IInputElement.cs

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

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

@ -55,6 +55,7 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="InteractiveTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</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;
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(
RoutedEvent routedEvent,
Delegate handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
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>(
RoutedEvent<TEventArgs> routedEvent,
EventHandler<TEventArgs> handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
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);
/// <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)
where TEventArgs : RoutedEventArgs;
/// <summary>
/// Raises a routed event.
/// </summary>
/// <param name="e">The event args.</param>
void RaiseEvent(RoutedEventArgs e);
}
}

82
Perspex.Interactivity/Interactive.cs

@ -15,13 +15,32 @@ namespace Perspex.Interactivity
using Perspex.Layout;
using Perspex.VisualTree;
/// <summary>
/// Base class for objects that raise routed events.
/// </summary>
public class Interactive : Layoutable, IInteractive
{
private Dictionary<RoutedEvent, List<EventSubscription>> eventHandlers =
private Dictionary<RoutedEvent, List<EventSubscription>> eventHandlers =
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(
RoutedEvent routedEvent,
RoutedEvent routedEvent,
Delegate handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false)
@ -49,6 +68,15 @@ namespace Perspex.Interactivity
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>(
RoutedEvent<TEventArgs> routedEvent,
EventHandler<TEventArgs> handler,
@ -58,15 +86,11 @@ namespace Perspex.Interactivity
return this.AddHandler(routedEvent, (Delegate)handler, routes, handledEventsToo);
}
public IObservable<EventPattern<T>> GetObservable<T>(RoutedEvent<T> routedEvent) where T : RoutedEventArgs
{
Contract.Requires<NullReferenceException>(routedEvent != null);
return Observable.FromEventPattern<T>(
handler => this.AddHandler(routedEvent, handler),
handler => this.RemoveHandler(routedEvent, handler));
}
/// <summary>
/// Removes a handler for the specified routed event.
/// </summary>
/// <param name="routedEvent">The routed event.</param>
/// <param name="handler">The handler.</param>
public void RemoveHandler(RoutedEvent routedEvent, Delegate handler)
{
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
{
this.RemoveHandler(routedEvent, (Delegate)handler);
}
/// <summary>
/// Raises a routed event.
/// </summary>
/// <param name="e">The event args.</param>
public void RaiseEvent(RoutedEventArgs e)
{
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)
{
Contract.Requires<NullReferenceException>(e != null);
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)
{
Contract.Requires<NullReferenceException>(e != null);
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)
{
Contract.Requires<NullReferenceException>(e != null);
@ -146,9 +192,7 @@ namespace Perspex.Interactivity
{
foreach (var sub in subscriptions.ToList())
{
bool correctRoute =
(e.Route == RoutingStrategies.Direct && (sub.Routes & RoutingStrategies.Direct) != 0) ||
(e.Route != RoutingStrategies.Direct && (e.Route & sub.Routes) != 0);
bool correctRoute = (e.Route & sub.Routes) != 0;
bool notFinished = !e.Handled || sub.AlsoIfHandled;
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>
</ItemGroup>
<ItemGroup>
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="IInteractive.cs" />
<Compile Include="InteractiveExtensions.cs" />
<Compile Include="Interactive.cs" />
<Compile Include="Properties\AssemblyInfo.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)
{
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)
{
Log.Logger = new LoggerConfiguration()
.Filter.ByIncludingOnly(Matching.WithProperty("Area", "Layout"))
//.MinimumLevel.Verbose()
.WriteTo.Trace(outputTemplate: "[{Id:X8}] [{SourceContext}] {Message}")
.CreateLogger();
//Log.Logger = new LoggerConfiguration()
// .Filter.ByIncludingOnly(Matching.WithProperty("Area", "Layout"))
// .MinimumLevel.Verbose()
// .WriteTo.Trace(outputTemplate: "[{Id:X8}] [{SourceContext}] {Message}")
// .CreateLogger();
// The version of ReactiveUI currently included is for WPF and so expects a WPF
// dispatcher. This makes sure it's initialized.

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

@ -95,12 +95,33 @@ namespace Perspex.Controls.UnitTests
}
[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 child = new Control();
var target = new ContentControl
{
Template = this.GetTemplate(),
};
var child = new Control();
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(((ILogical)child).LogicalParent, target);
@ -234,6 +255,22 @@ namespace Perspex.Controls.UnitTests
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()
{
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
{
using System;
using System.Collections.Specialized;
using System.Linq;
using Perspex.Collections;
using Perspex.Controls;
using Perspex.Controls.Presenters;
using Perspex.Controls.Templates;
using Perspex.Platform;
using Perspex.Styling;
using Perspex.VisualTree;
using Ploeh.AutoFixture;
using Ploeh.AutoFixture.AutoMoq;
using Splat;
using Xunit;
public class ItemsControlTests
@ -275,6 +270,27 @@ namespace Perspex.Controls.UnitTests
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()
{
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();
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var renderInterface = fixture.Create<IPlatformRenderInterface>();
Locator.CurrentMutable.RegisterConstant(renderInterface, typeof(IPlatformRenderInterface));
return result;
public new IItemsPresenter Presenter
{
get { return base.Presenter; }
set { base.Presenter = value; }
}
}
}
}

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

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

Loading…
Cancel
Save