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
This commit is contained in:
Antony Male 2018-09-30 17:38:20 +01:00
parent 61fcf4617c
commit 2027fad730
5 changed files with 76 additions and 36 deletions

1
.gitignore vendored
View File

@ -25,6 +25,7 @@ obj/
[Bb]in
[Dd]ebug*/
[Rr]elease*/
*.vs
#Project files
[Bb]uild/

View File

@ -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

View File

@ -24,7 +24,7 @@
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>Stylet.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>bin\Debug\Stylet.xml</DocumentationFile>
<LangVersion>5</LangVersion>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
@ -35,6 +35,7 @@
<WarningLevel>4</WarningLevel>
<DocumentationFile>bin\Release\Stylet.xml</DocumentationFile>
<CodeAnalysisRuleSet>Stylet.ruleset</CodeAnalysisRuleSet>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="PresentationCore" />

View File

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

View File

@ -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<IProvideValueTarget> provideValueTarget;
private Mock<IRootObjectProvider> rootObjectProvider;
private Mock<IServiceProvider> 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<IProvideValueTarget>();
this.provideValueTarget.Setup(x => x.TargetObject).Returns(new FrameworkElement());
this.rootObjectProvider = new Mock<IRootObjectProvider>();
this.serviceProvider = new Mock<IServiceProvider>();
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<ArgumentException>(() => 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<ExecutedRoutedEventHandler>(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<InvalidOperationException>(() => this.actionExtension.ProvideValue(this.serviceProvider.Object));
}
}
}