Set hidden AttachedProperty through Style

2019-09-11 02:05发布

问题:

I've got a problem using System.Windows.Interactivity.Interaction attached behavior class (from Expression Blend SDK 4). I'd like to define a pair of triggers for System.Windows.Window class in XAML Style element. But as the TriggersProperty field of System.Windows.Interactivity.Interaction class is private and there is no SetTriggers method in this class, I've got an error 'Set property System.Windows.Setter.Property threw an exception. -> Value cannot be null. Parameter name: property' when running the following code.

I really want to use the triggers and actions in styles, because I'd like to use them for my window-descendant control. Of course I can use my custom behavior or directly code my window-descendant class with triggers-analogue logic, but I'd like to use already existent triggers and actions of the expression library and my own, not declining them, simply because the TriggersProperty of Interaction class is hidden and I can't set it through style.

Is any workaround for the problem? With Reflection or someway other?

PS. I already tried to declare custom static class with TriggersProperty attached dependency property, registered with the help of AddOwner method, but no help - at the end it still tries to access the same TriggersProperty in the same System.Windows.Interactivity.Interaction class.

<Window
    x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:windowsInteractivity="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity">
    <Window.Style>
        <Style TargetType="Window">
            <Setter Property="Title" Value="WindowStyleTest"/>
            <Setter Property="windowsInteractivity:Interaction.Triggers">
                <Setter.Value>
                    <windowsInteractivity:EventTrigger EventName="MouseDown"/>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Style>
</Window>

回答1:

!!!Update!!!

Okay I took it a bit further. I extended the extension to perform all the work including setting the Triggers collection.

TriggerCollectionExtension The Extension That does all the heavy lifting. Note: The first time ProvideValue is called it will be from loading the style so the TargetValue is a Setter.

[ContentProperty("Triggers")] 
public class TriggerCollectionExtension : MarkupExtension
{
    public string EventName { get; set; }

    public string CommandName { get; set; }

    public object CommandParameter { get; set; }


    public System.Windows.Interactivity.TriggerCollection Triggers { get; private set;}

    public TriggerCollectionExtension()
    {
        var trigCollectionType = 
            typeof(System.Windows.Interactivity.TriggerCollection);

        var triggers = (System.Windows.Interactivity.TriggerCollection)
                        trigCollectionType.GetConstructor( 
                        BindingFlags. NonPublic | BindingFlags. Instance, 
                        null, Type.EmptyTypes, null).Invoke (null);

        // Cheat to get around this problem.
        // must have IsFrozen set to false to modify
        var methCreateCore = trigCollectionType.GetMethod("CreateInstanceCore", 
            BindingFlags.NonPublic | BindingFlags.Instance);
        var cloneTriggers = 
            (System.Windows.Interactivity.TriggerCollection)
             methCreateCore.Invoke(triggers, null);

        this.Triggers = cloneTriggers;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var target = serviceProvider.GetService(typeof(IProvideValueTarget)) as         
            IProvideValueTarget;

        // The first time this is called is when loading the style.
        // At that point the TargetObject is of type Setter.
        // Return this (The MarkupExtension) and it will be reevaluated when the style 
        // is applied.

        var hostcontrol = target.TargetObject as Control;
        if (hostcontrol != null)
        {
            var cloneTriggers = this.Triggers;

            var eventTrigger = new EventTrigger(this.EventName);

            var trigbase = eventTrigger as TriggerBase;
            trigbase.Attach(hostcontrol);

            var commandAction = new CommandAction(hostcontrol, this.CommandName, 
                this.CommandParameter);
            eventTrigger.Actions.Add(commandAction);

            cloneTriggers.Add(eventTrigger);

            Interaction.SetShadowTriggers(hostcontrol, this.Triggers);

            return null;
        }
        else
        {
            return this;
        }

        return null;
    }
}

Interaction The re-ownership/exposure of the TriggersCollection.

<!-- language: c# -->
/// <summary>
/// Helps workaround the bug in the deployed interaction DLL.
/// The DependencyProperty is registered as ShadowTriggers and the Setter Getter is   
/// SetTriggers() GetTriggers().
/// The result is compile error for XAML if anything but Shadowtriggers is used and 
/// runtime error.
/// </summary>
public static class Interaction
{
    static Interaction()
    {
        var interActionType = typeof(System.Windows.Interactivity.Interaction);
        var triggersProperty = (DependencyProperty)interActionType.InvokeMember(
            "TriggersProperty", 
            BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.GetField, 
            null, null, null);

        ShadowTriggersProperty = triggersProperty.AddOwner(typeof(Interaction));
    }

    public static readonly DependencyProperty ShadowTriggersProperty;

    public static System.Windows.Interactivity.TriggerCollection 
       GetShadowTriggers(DependencyObject d)
    {
        return 
          (System.Windows.Interactivity.TriggerCollection)
          d.GetValue(ShadowTriggersProperty);
    }

    public static void 
       SetShadowTriggers(
         DependencyObject d, 
         System.Windows.Interactivity.TriggerCollection value)
    {
        d.SetValue(ShadowTriggersProperty, value);
    }
}

CommandAction A custom TriggerAction that looks up the Command on the DataContext.

<!-- language: c# -->
public class CommandAction : TriggerAction<FrameworkElement>
{
    FrameworkElement control;
    private string commandName;
    object commandParameter;

    private ICommand actualCommand;

    public CommandAction(FrameworkElement control, string commandName, 
            object commandParameter)
    {
        this.control = control;
        this.commandName = commandName;
        this.commandParameter = commandParameter;

        object datacontext;

        if (this.FindDataContext(this.control, out datacontext))
        {
            var datacontextType = datacontext.GetType();
            var propCommand = datacontextType.GetProperty(this.commandName);

            this.actualCommand = propCommand.GetValue(datacontext, null) as ICommand;
        }
    }

    private bool FindDataContext(FrameworkElement control, out object datacontext)
    {
        datacontext = default(object);

        var parent = VisualTreeHelper.GetParent(control);
        while (parent != null)
        {
            var parentFrame = parent as FrameworkElement;
            if (parentFrame != null)
            {
                datacontext = parentFrame.DataContext;
                if (datacontext != null)
                {
                    return true;
                }
            }

            var parentFrameContent = parent as FrameworkContentElement;
            if (parentFrameContent != null)
            {
                datacontext = parentFrameContent.DataContext;
                if (datacontext != null)
                {
                    return true;
                }
            }

            parent = VisualTreeHelper.GetParent(parent);
        }

        return false;
    }

    protected override void Invoke(object parameter)
    {
        if (this.actualCommand != null)
        {
            if (this.actualCommand.CanExecute(parameter))
            {
                this.actualCommand.Execute(parameter);
            }
        }
    }
}

Wow long time reader first time posting code. I finally learned why the code doesn't always cut and paste so well. It took so many tries to submit this update.

I am sure there are reasons like disk space, parsing, or rendering speed, and the editor maintains state on failure to submit excellently.



回答2:

I got it, why the error appears. That's because at runtime it's searching an Attached Dependency property by string name, that is "ShadowTriggers" (as it specified in System.Windows.Interactivity assembly, Interaction static constructor). So I created the own custom static class and inherit the Triggers Dependency Property from System.Windows.Interaction there (via Reflection and AddOwner, just exposed the property as ShadowTriggersProperty). It worked! But... Now I have to provide a TriggerCollection instance to the Style's Property Value Setter, and the constructor of the class is internal. Suppose it is a no way further.