From 2027fad7308980bc98053d4db32cf513ebe064e6 Mon Sep 17 00:00:00 2001 From: Antony Male Date: Sun, 30 Sep 2018 17:38:20 +0100 Subject: [PATCH] Add support for CommandBinding in Actions CommandBinding isn't a DependencyObject, so we can't get its DataContext or View.ActionTarget -- we can only use the IRootObjectProvider.RootObject. This should be good enough for most cases, as these tend to get installed at the root of a window. Fixes #50 --- .gitignore | 1 + Stylet.sln | 7 ++- Stylet/Stylet.csproj | 3 +- Stylet/Xaml/ActionExtension.cs | 68 +++++++++++++++---------- StyletUnitTests/ActionExtensionTests.cs | 33 ++++++++++-- 5 files changed, 76 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index 003b806..0573f54 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ obj/ [Bb]in [Dd]ebug*/ [Rr]elease*/ +*.vs #Project files [Bb]uild/ diff --git a/Stylet.sln b/Stylet.sln index c7839e9..ad5ad5d 100644 --- a/Stylet.sln +++ b/Stylet.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.30110.0 +# Visual Studio 15 +VisualStudioVersion = 15.0.27703.2026 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet", "Stylet\Stylet.csproj", "{2435BD00-AC12-48B0-AD36-9BAB2FDEC3F5}" EndProject @@ -31,4 +31,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6A75A07E-E87F-4A90-BA14-D1237C7A3C67} + EndGlobalSection EndGlobal diff --git a/Stylet/Stylet.csproj b/Stylet/Stylet.csproj index 652f3e8..ef516c0 100644 --- a/Stylet/Stylet.csproj +++ b/Stylet/Stylet.csproj @@ -24,7 +24,7 @@ 4 Stylet.ruleset bin\Debug\Stylet.xml - 5 + latest pdbonly @@ -35,6 +35,7 @@ 4 bin\Release\Stylet.xml Stylet.ruleset + latest diff --git a/Stylet/Xaml/ActionExtension.cs b/Stylet/Xaml/ActionExtension.cs index 9e75d67..16ac98e 100644 --- a/Stylet/Xaml/ActionExtension.cs +++ b/Stylet/Xaml/ActionExtension.cs @@ -101,44 +101,56 @@ namespace Stylet.Xaml throw new InvalidOperationException("Method has not been set"); var valueService = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget)); - - // Seems this is the case when we're in a template. We'll get called again properly in a second. - // http://social.msdn.microsoft.com/Forums/vstudio/en-US/a9ead3d5-a4e4-4f9c-b507-b7a7d530c6a9/gaining-access-to-target-object-instead-of-shareddp-in-custom-markupextensions-providevalue-method?forum=wpf - if (!(valueService.TargetObject is DependencyObject)) - return this; - - var targetObject = (DependencyObject)valueService.TargetObject; - var rootObjectProvider = (IRootObjectProvider)serviceProvider.GetService(typeof(IRootObjectProvider)); - var rootObject = rootObjectProvider == null ? null : rootObjectProvider.RootObject as DependencyObject; + var rootObject = rootObjectProvider?.RootObject as DependencyObject; - var propertyAsDependencyProperty = valueService.TargetProperty as DependencyProperty; - if (propertyAsDependencyProperty != null && propertyAsDependencyProperty.PropertyType == typeof(ICommand)) + switch (valueService.TargetObject) { - // If they're in design mode and haven't set View.ActionTarget, default to looking sensible - return new CommandAction(targetObject, rootObject, this.Method, this.CommandNullTargetBehaviour, this.CommandActionNotFoundBehaviour); + case DependencyObject targetObject: + return this.HandleDependencyObject(valueService, targetObject, rootObject); + case CommandBinding commandBinding: + return this.HandleCommandBinding(rootObject, ((EventInfo)valueService.TargetProperty).EventHandlerType); + default: + // Seems this is the case when we're in a template. We'll get called again properly in a second. + // http://social.msdn.microsoft.com/Forums/vstudio/en-US/a9ead3d5-a4e4-4f9c-b507-b7a7d530c6a9/gaining-access-to-target-object-instead-of-shareddp-in-custom-markupextensions-providevalue-method?forum=wpf + return this; } + } - var propertyAsEventInfo = valueService.TargetProperty as EventInfo; - if (propertyAsEventInfo != null) + private object HandleDependencyObject(IProvideValueTarget valueService, DependencyObject targetObject, DependencyObject rootObject) + { + switch (valueService.TargetProperty) { - var ec = new EventAction(targetObject, rootObject, propertyAsEventInfo.EventHandlerType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour); - return ec.GetDelegate(); - } - - // For attached events - var propertyAsMethodInfo = valueService.TargetProperty as MethodInfo; - if (propertyAsMethodInfo != null) - { - var parameters = propertyAsMethodInfo.GetParameters(); - if (parameters.Length == 2 && typeof(Delegate).IsAssignableFrom(parameters[1].ParameterType)) + case DependencyProperty dependencyProperty when dependencyProperty.PropertyType == typeof(ICommand): + // If they're in design mode and haven't set View.ActionTarget, default to looking sensible + return new CommandAction(targetObject, rootObject, this.Method, this.CommandNullTargetBehaviour, this.CommandActionNotFoundBehaviour); + case EventInfo eventInfo: { - var ec = new EventAction(targetObject, rootObject, parameters[1].ParameterType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour); + var ec = new EventAction(targetObject, rootObject, eventInfo.EventHandlerType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour); return ec.GetDelegate(); } + case MethodInfo methodInfo: // For attached events + { + var parameters = methodInfo.GetParameters(); + if (parameters.Length == 2 && typeof(Delegate).IsAssignableFrom(parameters[1].ParameterType)) + { + var ec = new EventAction(targetObject, rootObject, parameters[1].ParameterType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour); + return ec.GetDelegate(); + } + throw new ArgumentException("Action used with an attached event (or something similar) which didn't follow the normal pattern"); + } + default: + throw new ArgumentException("Can only use ActionExtension with a Command property or an event handler"); } - - throw new ArgumentException("Can only use ActionExtension with a Command property or an event handler"); + } + + private object HandleCommandBinding(DependencyObject rootObject, Type propertyType) + { + if (rootObject == null) + throw new InvalidOperationException("Action may only be used with CommandBinding from a XAML view (unable to retrieve IRootObjectProvider.RootObject)"); + + var ec = new EventAction(rootObject, null, propertyType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour); + return ec.GetDelegate(); } } diff --git a/StyletUnitTests/ActionExtensionTests.cs b/StyletUnitTests/ActionExtensionTests.cs index e247ffc..1794a4b 100644 --- a/StyletUnitTests/ActionExtensionTests.cs +++ b/StyletUnitTests/ActionExtensionTests.cs @@ -4,7 +4,9 @@ using Stylet.Xaml; using System; using System.Windows; using System.Windows.Controls; +using System.Windows.Input; using System.Windows.Markup; +using System.Xaml; namespace StyletUnitTests { @@ -13,6 +15,7 @@ namespace StyletUnitTests { private ActionExtension actionExtension; private Mock provideValueTarget; + private Mock rootObjectProvider; private Mock serviceProvider; private class TestExtensions @@ -20,14 +23,12 @@ namespace StyletUnitTests public static readonly RoutedEvent TestEvent = EventManager.RegisterRoutedEvent("Test", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TestExtensions)); public static void AddTestHandler(DependencyObject d, RoutedEventHandler handler) { - UIElement uie = d as UIElement; - if (uie != null) + if (d is UIElement uie) uie.AddHandler(TestExtensions.TestEvent, handler); } public static void RemoveTestHandler(DependencyObject d, RoutedEventHandler handler) { - UIElement uie = d as UIElement; - if (uie != null) + if (d is UIElement uie) uie.RemoveHandler(TestExtensions.TestEvent, handler); } @@ -44,8 +45,11 @@ namespace StyletUnitTests this.provideValueTarget = new Mock(); this.provideValueTarget.Setup(x => x.TargetObject).Returns(new FrameworkElement()); + this.rootObjectProvider = new Mock(); + this.serviceProvider = new Mock(); - serviceProvider.Setup(x => x.GetService(typeof(IProvideValueTarget))).Returns(provideValueTarget.Object); + this.serviceProvider.Setup(x => x.GetService(typeof(IProvideValueTarget))).Returns(this.provideValueTarget.Object); + this.serviceProvider.Setup(x => x.GetService(typeof(IRootObjectProvider))).Returns(this.rootObjectProvider.Object); } [Test] @@ -100,5 +104,24 @@ namespace StyletUnitTests Assert.Throws(() => this.actionExtension.ProvideValue(this.serviceProvider.Object)); } + + [Test] + public void ReturnsEventActionIfTargetIsCommandBinding() + { + this.provideValueTarget.Setup(x => x.TargetObject).Returns(new CommandBinding()); + this.provideValueTarget.Setup(x => x.TargetProperty).Returns(typeof(CommandBinding).GetEvent("Executed")); + this.rootObjectProvider.Setup(x => x.RootObject).Returns(new DependencyObject()); + + Assert.IsInstanceOf(this.actionExtension.ProvideValue(this.serviceProvider.Object)); + } + + [Test] + public void ThrowsIfTargetIsCommandBindingAndRootObjectNotSet() + { + this.provideValueTarget.Setup(x => x.TargetObject).Returns(new CommandBinding()); + this.provideValueTarget.Setup(x => x.TargetProperty).Returns(typeof(CommandBinding).GetEvent("Executed")); + + Assert.Throws(() => this.actionExtension.ProvideValue(this.serviceProvider.Object)); + } } }