diff --git a/Perspex.Controls/ContentControl.cs b/Perspex.Controls/ContentControl.cs index aa0bfd803f..79ac1321c0 100644 --- a/Perspex.Controls/ContentControl.cs +++ b/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"); - - if (presenter != null) - { - this.logicalChildren.Source = ((ILogical)presenter).LogicalChildren; - } - else - { - this.logicalChildren.Source = null; - } - } - - private void SetContentParent(Tuple 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"); + this.logicalChildren.Source = ((ILogical)this.Presenter)?.LogicalChildren; } } } diff --git a/Perspex.Controls/ControlExtensions.cs b/Perspex.Controls/ControlExtensions.cs index af7c10403a..cb13407701 100644 --- a/Perspex.Controls/ControlExtensions.cs +++ b/Perspex.Controls/ControlExtensions.cs @@ -17,11 +17,11 @@ namespace Perspex.Controls public static class ControlExtensions { - public static T FindControl(this Control control, string id) where T : Control + public static T FindControl(this Control control, string name) where T : Control { return control.GetLogicalDescendents() .OfType() - .FirstOrDefault(x => x.Name == id); + .FirstOrDefault(x => x.Name == name); } } } diff --git a/Perspex.Controls/GlobalSuppressions.cs b/Perspex.Controls/GlobalSuppressions.cs new file mode 100644 index 0000000000..3d080850f1 --- /dev/null +++ b/Perspex.Controls/GlobalSuppressions.cs @@ -0,0 +1,14 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +[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")] \ No newline at end of file diff --git a/Perspex.Controls/IMenu.cs b/Perspex.Controls/IMenu.cs deleted file mode 100644 index 5ccc53902b..0000000000 --- a/Perspex.Controls/IMenu.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright 2015 MIT Licence. See licence.md for more information. -// -// ----------------------------------------------------------------------- - -namespace Perspex.Controls -{ - internal interface IMenu - { - void ChildPointerEnter(MenuItem item); - - void ChildSubMenuOpened(MenuItem item); - - void CloseMenu(); - } -} diff --git a/Perspex.Controls/ItemsControl.cs b/Perspex.Controls/ItemsControl.cs index 0c82b21cb7..ea73b048b0 100644 --- a/Perspex.Controls/ItemsControl.cs +++ b/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 logicalChildren = new PerspexReadOnlyListView(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("itemsPresenter"); - - if (this.Presenter != null) - { - this.logicalChildren.Source = ((IVisual)this.Presenter.Panel).VisualChildren; - } } protected virtual void ItemsChanged(IEnumerable oldValue, IEnumerable newValue) diff --git a/Perspex.Controls/Menu.cs b/Perspex.Controls/Menu.cs index ca0538640d..4efca58dd1 100644 --- a/Perspex.Controls/Menu.cs +++ b/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 + /// + /// A top-level menu control. + /// + public class Menu : ItemsControl { + /// + /// Defines the default items panel used by a . + /// private static readonly ItemsPanelTemplate DefaultPanel = new ItemsPanelTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); + /// + /// Defines the property. + /// + public static readonly PerspexProperty IsOpenProperty = + PerspexProperty.Register(nameof(IsOpen)); + + /// + /// Tracks event handlers added to the root of the visual tree. + /// private IDisposable subscription; + /// + /// Initializes static members of the class. + /// static Menu() { ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); + MenuItem.ClickEvent.AddClassHandler(x => x.OnMenuClick); + MenuItem.SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); } - void IMenu.ChildPointerEnter(MenuItem item) - { - var children = this.GetLogicalChildren().Cast(); - - 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() + /// + /// Gets a value indicating whether the menu is open. + /// + public bool IsOpen { - foreach (MenuItem i in this.GetLogicalChildren()) - { - i.IsSubMenuOpen = false; - } + get { return this.GetValue(IsOpenProperty); } + private set { this.SetValue(IsOpenProperty, value); } } + /// + /// Called when the is attached to the visual tree. + /// + /// The root of the visual tree. 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)); } + /// + /// Called when the is detached from the visual tree. + /// + /// The root of the visual tree being detached from. protected override void OnDetachedFromVisualTree(IRenderRoot oldRoot) { base.OnDetachedFromVisualTree(oldRoot); this.subscription.Dispose(); } + /// + /// Called when a submenu opens somewhere in the menu. + /// + /// The event args. + 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()) + { + if (child != menuItem && child.IsSubMenuOpen) + { + child.IsSubMenuOpen = false; + } + } + } + + this.IsOpen = true; + } + + /// + /// Closes the menu. + /// + private void CloseMenu() + { + foreach (MenuItem i in this.GetLogicalChildren()) + { + i.IsSubMenuOpen = false; + } + + this.IsOpen = false; + } + + /// + /// Called when the top-level window is deactivated. + /// + /// The sender. + /// The event args. private void Deactivated(object sender, EventArgs e) { - foreach (var i in this.GetLogicalChildren().Cast()) + this.CloseMenu(); + } + + /// + /// Called when a submenu is clicked somewhere in the menu. + /// + /// The event args. + private void OnMenuClick(RoutedEventArgs e) + { + this.CloseMenu(); + } + + /// + /// Called when the pointer is pressed anywhere on the window. + /// + /// The sender. + /// The event args. + 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(); + } } } } diff --git a/Perspex.Controls/MenuItem.cs b/Perspex.Controls/MenuItem.cs index a56f535f30..616d52b71e 100644 --- a/Perspex.Controls/MenuItem.cs +++ b/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 + /// + /// A menu item control. + /// + public class MenuItem : HeaderedItemsControl { + /// + /// Defines the property. + /// public static readonly PerspexProperty CommandProperty = Button.CommandProperty.AddOwner(); + /// + /// Defines the property. + /// public static readonly PerspexProperty CommandParameterProperty = Button.CommandParameterProperty.AddOwner(); + /// + /// Defines the property. + /// public static readonly PerspexProperty IconProperty = - PerspexProperty.Register("Icon"); + PerspexProperty.Register(nameof(Icon)); + /// + /// Defines the property. + /// public static readonly PerspexProperty IsSubMenuOpenProperty = - PerspexProperty.Register("IsSubMenuOpen"); + PerspexProperty.Register(nameof(IsSubMenuOpen)); + /// + /// Defines the event. + /// public static readonly RoutedEvent ClickEvent = - RoutedEvent.Register("Click", RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(Click), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// + public static readonly RoutedEvent SubmenuOpenedEvent = + RoutedEvent.Register(nameof(SubmenuOpened), RoutingStrategies.Bubble); + + /// + /// The timer used to display submenus. + /// + private IDisposable submenuTimer; + + /// + /// Initializes static members of the class. + /// static MenuItem() { ClickEvent.AddClassHandler(x => x.OnClick); + SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); IsSubMenuOpenProperty.Changed.Subscribe(SubMenuOpenChanged); } + /// + /// Occurs when a without a submenu is clicked. + /// public event EventHandler Click { add { this.AddHandler(ClickEvent, value); } remove { this.RemoveHandler(ClickEvent, value); } } + /// + /// Occurs when a 's submenu is opened. + /// + public event EventHandler SubmenuOpened + { + add { this.AddHandler(SubmenuOpenedEvent, value); } + remove { this.RemoveHandler(SubmenuOpenedEvent, value); } + } + + /// + /// Gets or sets the command associated with the menu item. + /// public ICommand Command { get { return this.GetValue(CommandProperty); } set { this.SetValue(CommandProperty, value); } } + /// + /// Gets or sets the parameter to pass to the property of a + /// . + /// public object CommandParameter { get { return this.GetValue(CommandParameterProperty); } set { this.SetValue(CommandParameterProperty, value); } } + /// + /// Gets or sets the icon that appears in a . + /// public object Icon { get { return this.GetValue(IconProperty); } set { this.SetValue(IconProperty, value); } } + /// + /// Gets or sets a value that indicates whether the submenu of the is + /// open. + /// public bool IsSubMenuOpen { get { return this.GetValue(IsSubMenuOpenProperty); } set { this.SetValue(IsSubMenuOpenProperty, value); } } - void IMenu.ChildPointerEnter(MenuItem item) + /// + /// Gets or sets a value that indicates whether the has a submenu. + /// + public bool HasSubMenu { + get { return !this.Classes.Contains(":empty"); } } - void IMenu.ChildSubMenuOpened(MenuItem item) + /// + /// Gets a value that indicates whether the is a top-level menu item. + /// + public bool IsTopLevel { - foreach (var i in this.Items.Cast().OfType()) - { - i.IsSubMenuOpen = i == item; - } + get; + private set; } - void IMenu.CloseMenu() + /// + /// Called when the is attached to the visual tree. + /// + /// The root of the visual tree. + protected override void OnAttachedToVisualTree(IRenderRoot root) { - this.IsSubMenuOpen = false; - this.GetParentMenu().CloseMenu(); + base.OnAttachedToVisualTree(root); + this.IsTopLevel = this.Parent is Menu; } + /// + /// Called when the is clicked. + /// + /// The click event args. protected virtual void OnClick(RoutedEventArgs e) { if (this.Command != null) @@ -94,71 +167,157 @@ namespace Perspex.Controls } } + /// + /// Called when the pointer enters the . + /// + /// The event args. protected override void OnPointerEnter(PointerEventArgs e) { base.OnPointerEnter(e); - this.GetLogicalParent()?.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 + } + + /// + /// Called when the pointer leaves the . + /// + /// The event args. + 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() + /// + /// Called when the pointer is pressed over the . + /// + /// The event args. + protected override void OnPointerPressed(PointerPressEventArgs e) { - var parent = this.GetLogicalParent(); + 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().FirstOrDefault(); - var parentItem = ((ILogical)popupRoot).GetLogicalParent().TemplatedParent; - return (IMenu)parentItem; + this.IsSubMenuOpen = true; } + + e.Handled = true; } - private void OnSubMenuOpenChanged(bool open) + /// + /// Called when a submenu is opened on this MenuItem or a child MenuItem. + /// + /// The event args. + 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().OfType()) + foreach (var child in this.Items.OfType()) { - item.IsSubMenuOpen = false; + if (child != menuItem && child.IsSubMenuOpen) + { + child.IsSubMenuOpen = false; + } } } - else if (open) + } + + /// + /// Called when the MenuItem's template has been applied. + /// + protected override void OnTemplateApplied() + { + base.OnTemplateApplied(); + + var popup = this.FindTemplateChild("popup"); + + if (popup != null) + { + popup.Opened += this.PopupFirstOpened; + } + } + + /// + /// Closes all submenus of the menu item. + /// + private void CloseSubmenus() + { + foreach (var child in this.Items.OfType()) { - this.GetParentMenu().ChildSubMenuOpened(this); + child.IsSubMenuOpen = false; } } + /// + /// Called when the property changes. + /// + /// The property change event. 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(); + } + } + } + + /// + /// Called the first time the MenuItem's popup is opened. + /// + /// The event sender. + /// The event args. + 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"); + + 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; } } } diff --git a/Perspex.Controls/Panel.cs b/Perspex.Controls/Panel.cs index faef20c742..e2c11ea408 100644 --- a/Perspex.Controls/Panel.cs +++ b/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 child) diff --git a/Perspex.Controls/Perspex.Controls.csproj b/Perspex.Controls/Perspex.Controls.csproj index 000aa578d6..2dcb64d1b5 100644 --- a/Perspex.Controls/Perspex.Controls.csproj +++ b/Perspex.Controls/Perspex.Controls.csproj @@ -37,7 +37,7 @@ - + diff --git a/Perspex.Controls/Popup.cs b/Perspex.Controls/Popup.cs index 9b71c7aa7a..eb9670cc46 100644 --- a/Perspex.Controls/Popup.cs +++ b/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) diff --git a/Perspex.Controls/PopupRoot.cs b/Perspex.Controls/PopupRoot.cs index ec2e3d0e57..7d7f6e9146 100644 --- a/Perspex.Controls/PopupRoot.cs +++ b/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); diff --git a/Perspex.Controls/Presenters/ContentPresenter.cs b/Perspex.Controls/Presenters/ContentPresenter.cs index 1ab7331f2a..5556be8a98 100644 --- a/Perspex.Controls/Presenters/ContentPresenter.cs +++ b/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) diff --git a/Perspex.Input/IInputElement.cs b/Perspex.Input/IInputElement.cs index bc66fa2990..6fab7c8e48 100644 --- a/Perspex.Input/IInputElement.cs +++ b/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 GotFocus; diff --git a/Perspex.Interactive.UnitTests/GlobalSuppressions.cs b/Perspex.Interactive.UnitTests/GlobalSuppressions.cs new file mode 100644 index 0000000000..a6e0896543 --- /dev/null +++ b/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")] \ No newline at end of file diff --git a/Perspex.Interactive.UnitTests/InteractiveTests.cs b/Perspex.Interactive.UnitTests/InteractiveTests.cs index c8eba683a3..fbf27bef12 100644 --- a/Perspex.Interactive.UnitTests/InteractiveTests.cs +++ b/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(); EventHandler 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 handler, RoutingStrategies handlerRoutes, bool handledEventsToo = false) @@ -336,7 +335,7 @@ namespace Perspex.Interactive.UnitTests i.AddHandler(ev, handler, handlerRoutes, handledEventsToo); } } - + return target; } diff --git a/Perspex.Interactive.UnitTests/Perspex.Interactive.UnitTests.csproj b/Perspex.Interactive.UnitTests/Perspex.Interactive.UnitTests.csproj index a3a31077c2..9e3b2fc176 100644 --- a/Perspex.Interactive.UnitTests/Perspex.Interactive.UnitTests.csproj +++ b/Perspex.Interactive.UnitTests/Perspex.Interactive.UnitTests.csproj @@ -55,6 +55,7 @@ + diff --git a/Perspex.Interactivity/GlobalSuppressions.cs b/Perspex.Interactivity/GlobalSuppressions.cs new file mode 100644 index 0000000000..4f293bba02 --- /dev/null +++ b/Perspex.Interactivity/GlobalSuppressions.cs @@ -0,0 +1,14 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +[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")] \ No newline at end of file diff --git a/Perspex.Interactivity/IInteractive.cs b/Perspex.Interactivity/IInteractive.cs index b18146d2ec..3d1bfc93ac 100644 --- a/Perspex.Interactivity/IInteractive.cs +++ b/Perspex.Interactivity/IInteractive.cs @@ -8,25 +8,65 @@ namespace Perspex.Interactivity { using System; - public interface IInteractive : IVisual + /// + /// Interface for objects that raise routed events. + /// + public interface IInteractive { + /// + /// Gets the interactive parent of the object for bubbling and tunnelling events. + /// + IInteractive InteractiveParent { get; } + + /// + /// Adds a handler for the specified routed event. + /// + /// The routed event. + /// The handler. + /// The routing strategies to listen to. + /// Whether handled events should also be listened for. + /// A disposable that terminates the event subscription. IDisposable AddHandler( RoutedEvent routedEvent, Delegate handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false); + /// + /// Adds a handler for the specified routed event. + /// + /// The type of the event's args. + /// The routed event. + /// The handler. + /// The routing strategies to listen to. + /// Whether handled events should also be listened for. + /// A disposable that terminates the event subscription. IDisposable AddHandler( RoutedEvent routedEvent, EventHandler handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) where TEventArgs : RoutedEventArgs; + /// + /// Removes a handler for the specified routed event. + /// + /// The routed event. + /// The handler. void RemoveHandler(RoutedEvent routedEvent, Delegate handler); + /// + /// Removes a handler for the specified routed event. + /// + /// The type of the event's args. + /// The routed event. + /// The handler. void RemoveHandler(RoutedEvent routedEvent, EventHandler handler) where TEventArgs : RoutedEventArgs; + /// + /// Raises a routed event. + /// + /// The event args. void RaiseEvent(RoutedEventArgs e); } } diff --git a/Perspex.Interactivity/Interactive.cs b/Perspex.Interactivity/Interactive.cs index 1924c9fde8..e5c94d01f1 100644 --- a/Perspex.Interactivity/Interactive.cs +++ b/Perspex.Interactivity/Interactive.cs @@ -15,13 +15,32 @@ namespace Perspex.Interactivity using Perspex.Layout; using Perspex.VisualTree; + /// + /// Base class for objects that raise routed events. + /// public class Interactive : Layoutable, IInteractive { - private Dictionary> eventHandlers = + private Dictionary> eventHandlers = new Dictionary>(); + /// + /// Gets the interactive parent of the object for bubbling and tunnelling events. + /// + IInteractive IInteractive.InteractiveParent + { + get { return ((IVisual)this).VisualParent as IInteractive; } + } + + /// + /// Adds a handler for the specified routed event. + /// + /// The routed event. + /// The handler. + /// The routing strategies to listen to. + /// Whether handled events should also be listened for. + /// A disposable that terminates the event subscription. 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)); } + /// + /// Adds a handler for the specified routed event. + /// + /// The type of the event's args. + /// The routed event. + /// The handler. + /// The routing strategies to listen to. + /// Whether handled events should also be listened for. + /// A disposable that terminates the event subscription. public IDisposable AddHandler( RoutedEvent routedEvent, EventHandler handler, @@ -58,15 +86,11 @@ namespace Perspex.Interactivity return this.AddHandler(routedEvent, (Delegate)handler, routes, handledEventsToo); } - public IObservable> GetObservable(RoutedEvent routedEvent) where T : RoutedEventArgs - { - Contract.Requires(routedEvent != null); - - return Observable.FromEventPattern( - handler => this.AddHandler(routedEvent, handler), - handler => this.RemoveHandler(routedEvent, handler)); - } - + /// + /// Removes a handler for the specified routed event. + /// + /// The routed event. + /// The handler. public void RemoveHandler(RoutedEvent routedEvent, Delegate handler) { Contract.Requires(routedEvent != null); @@ -80,12 +104,22 @@ namespace Perspex.Interactivity } } - public void RemoveHandler(RoutedEvent routedEvent, EventHandler handler) + /// + /// Removes a handler for the specified routed event. + /// + /// The type of the event's args. + /// The routed event. + /// The handler. + public void RemoveHandler(RoutedEvent routedEvent, EventHandler handler) where TEventArgs : RoutedEventArgs { this.RemoveHandler(routedEvent, (Delegate)handler); } + /// + /// Raises a routed event. + /// + /// The event args. public void RaiseEvent(RoutedEventArgs e) { Contract.Requires(e != null); @@ -110,30 +144,42 @@ namespace Perspex.Interactivity } } + /// + /// Bubbles an event. + /// + /// The event args. private void BubbleEvent(RoutedEventArgs e) { Contract.Requires(e != null); e.Route = RoutingStrategies.Bubble; - foreach (var target in this.GetSelfAndVisualAncestors().OfType()) + foreach (var target in this.GetBubbleEventRoute()) { - target.RaiseEventImpl(e); + ((Interactive)target).RaiseEventImpl(e); } } + /// + /// Tunnels an event. + /// + /// The event args. private void TunnelEvent(RoutedEventArgs e) { Contract.Requires(e != null); e.Route = RoutingStrategies.Tunnel; - foreach (var target in this.GetSelfAndVisualAncestors().OfType().Reverse()) + foreach (var target in this.GetTunnelEventRoute()) { - target.RaiseEventImpl(e); + ((Interactive)target).RaiseEventImpl(e); } } + /// + /// Carries out the actual invocation of an event on this object. + /// + /// The event args. private void RaiseEventImpl(RoutedEventArgs e) { Contract.Requires(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) diff --git a/Perspex.Interactivity/InteractiveExtensions.cs b/Perspex.Interactivity/InteractiveExtensions.cs new file mode 100644 index 0000000000..524a61dee9 --- /dev/null +++ b/Perspex.Interactivity/InteractiveExtensions.cs @@ -0,0 +1,41 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Interactivity +{ + using System.Collections.Generic; + using System.Linq; + + /// + /// Provides extension methods for the interface. + /// + public static class InteractiveExtensions + { + /// + /// Gets the route for bubbling events from the specified interactive. + /// + /// The interactive. + /// The event route. + public static IEnumerable GetBubbleEventRoute(this IInteractive interactive) + { + while (interactive != null) + { + yield return interactive; + interactive = interactive.InteractiveParent; + } + } + + /// + /// Gets the route for tunneling events from the specified interactive. + /// + /// The interactive. + /// The event route. + public static IEnumerable GetTunnelEventRoute(this IInteractive interactive) + { + return interactive.GetBubbleEventRoute().Reverse(); + } + } +} diff --git a/Perspex.Interactivity/Perspex.Interactivity.csproj b/Perspex.Interactivity/Perspex.Interactivity.csproj index 3ba34eea4c..ff2a017896 100644 --- a/Perspex.Interactivity/Perspex.Interactivity.csproj +++ b/Perspex.Interactivity/Perspex.Interactivity.csproj @@ -55,7 +55,9 @@ + + diff --git a/Perspex.Interactivity/RoutedEventArgs.cs b/Perspex.Interactivity/RoutedEventArgs.cs index a17c026e47..d050bffc9b 100644 --- a/Perspex.Interactivity/RoutedEventArgs.cs +++ b/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; diff --git a/Perspex.Styling/LogicalTree/LogicalExtensions.cs b/Perspex.Styling/LogicalTree/LogicalExtensions.cs index b362efb64c..7ae5de6ad0 100644 --- a/Perspex.Styling/LogicalTree/LogicalExtensions.cs +++ b/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); + } } } diff --git a/TestApplication/Program.cs b/TestApplication/Program.cs index 4146c6a7a3..e94dab71fb 100644 --- a/TestApplication/Program.cs +++ b/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. diff --git a/Tests/Perspex.Controls.UnitTests/ContentControlTests.cs b/Tests/Perspex.Controls.UnitTests/ContentControlTests.cs index 1c4be7badf..e02c2f21e6 100644 --- a/Tests/Perspex.Controls.UnitTests/ContentControlTests.cs +++ b/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(parent => diff --git a/Tests/Perspex.Controls.UnitTests/GlobalSuppressions.cs b/Tests/Perspex.Controls.UnitTests/GlobalSuppressions.cs new file mode 100644 index 0000000000..a6e0896543 --- /dev/null +++ b/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")] \ No newline at end of file diff --git a/Tests/Perspex.Controls.UnitTests/ItemsControlTests.cs b/Tests/Perspex.Controls.UnitTests/ItemsControlTests.cs index 71bf53c007..919533b255 100644 --- a/Tests/Perspex.Controls.UnitTests/ItemsControlTests.cs +++ b/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(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(); - Locator.CurrentMutable.RegisterConstant(renderInterface, typeof(IPlatformRenderInterface)); - return result; + public new IItemsPresenter Presenter + { + get { return base.Presenter; } + set { base.Presenter = value; } + } } } } diff --git a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index e2328c8eaf..6170b6a7ea 100644 --- a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -90,6 +90,7 @@ +