Attach XAML Behavior to all controls of same type

2019-08-13 15:12发布

问题:

I have an InvokeCommandAction that I have that is attached to the GotFocus event of a TextBox like so:

<TextBox Grid.Row="0"
         Grid.Column="1"
         Width="40"
         HorizontalAlignment="Right">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="GotFocus">
            <i:InvokeCommandAction Command="{Binding GotFocusCommand}" CommandParameter="Enter data [message to be displayed]" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TextBox>

It works just fine this way, but I have dozens of TextBoxes with this same setup. Rather than repeating the code (as I am currently doing for every one), I am hoping to just attach that trigger to all controls of type {x:Type TextBox}.

Normally, I would set properties in the Resources section, like this:

<UserControl.Resources>
    <Style TargetType="TextBlock">
        <Setter Property="Padding" Value="5,0,0,0" />
        <Setter Property="VerticalAlignment" Value="Center" />
    </Style>
</UserControl.Resources>

Unfortunately, this will not work for Triggers:

The attached property "Triggers" can only be applied to types that are derived from "DependencyObject".

Ideally, I would like to do something like this:

<UserControl.Resources>
    <Style TargetType="{x:Type TextBox}">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="GotFocus">
                <i:InvokeCommandAction Command="{Binding GotFocusCommand}" CommandParameter="{Binding Tag}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </Style>
</UserControl.Resources>

Where I then just need to set the Tag property of each TextBox to specify the message to be displayed. Am I on the right track? Do I need to change it to use a ControlTemplate or something like that?

EDIT

I have seen a similar question here: Interaction Triggers in Style in ResourceDictionary WPF

After reading the answers for that question, I tried the following:

<UserControl.Resources>
    <TextBox x:Key="TextBoxWithTag">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="GotFocus">
                <i:InvokeCommandAction Command="{Binding GotFocusCommand}" CommandParameter="{Binding Path=Tag, RelativeSource={RelativeSource Self}}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </TextBox>
</UserControl.Resources>

Then assigning to a control like so:

<ContentControl Grid.Row="0"
                Grid.Column="1"
                Width="40"
                HorizontalAlignment="Right"
                Content="{StaticResource TextBoxWithTag}"
                Tag="test tag" />

This also does not work, still complaining about:

The attached property "Triggers" can only be applied to types that are derived from "DependencyObject".

EDIT 2

Here is the GotFocusCommand information. It sets the value of a string that has a TextBlock bound to it.

This is in my ViewModel:

private ICommand _gotFocusCommand;
public ICommand GotFocusCommand
{
    get
    {
        if (_gotFocusCommand == null)
        {
            _gotFocusCommand = new DelegateCommand<string>(TextBoxGotFocus);
        }
        return _gotFocusCommand;
    }
}

private void TextBoxGotFocus(string infoText)
{
    CardInfoText = infoText;
}

Then the XAML:

<TextBlock Grid.Row="2"
           Grid.Column="0"
           Grid.ColumnSpan="2"
           HorizontalAlignment="Center"
           Text="{Binding CardInfoText}" />

回答1:

There are several ways to do what you want. One example:

public static class UIBehaviors {
    public static readonly DependencyProperty AttachedTriggersProperty = DependencyProperty.RegisterAttached(
        "AttachedTriggers", typeof (EventTriggerCollection), typeof (UIBehaviors), new PropertyMetadata(null, OnAttachedTriggersChanged));

    private static void OnAttachedTriggersChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
        var triggers = Interaction.GetTriggers(d);
        if (e.OldValue != null) {
            foreach (var trigger in (EventTriggerCollection) e.OldValue) {
                triggers.Remove(trigger);
            }
        }
        if (e.NewValue != null) {
            foreach (var trigger in (EventTriggerCollection) e.NewValue) {
                triggers.Add(trigger);
            }
        }
    }

    public static void SetAttachedTriggers(DependencyObject element, EventTriggerCollection value) {
        element.SetValue(AttachedTriggersProperty, value);
    }

    public static EventTriggerCollection GetAttachedTriggers(DependencyObject element) {
        return (EventTriggerCollection) element.GetValue(AttachedTriggersProperty);            
    }
}

public class EventTriggerCollection : Collection<EventTrigger> {

}

Here we declare attached property which accepts set of EventTrigger (from Interactivity assembly). When this property is set, we just attach all those triggers, like i:Interaction.Triggers will do. Then use it like this:

<Window.Resources>
    <local:EventTriggerCollection x:Shared="False" x:Key="textBoxTriggers">
        <i:EventTrigger EventName="GotFocus">
            <i:InvokeCommandAction Command="{Binding GotFocusCommand}" CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=TextBox}, Path=Tag}" />
        </i:EventTrigger>
    </local:EventTriggerCollection>
    <Style TargetType="{x:Type TextBox}">
        <Setter Property="local:UIBehaviors.AttachedTriggers" Value="{StaticResource textBoxTriggers}"/>
    </Style>
</Window.Resources>

Note how I bind to TextBox.Tag property. You cannot just bind to it as in your question, because your data context will be view model (with GotFocusCommand). Also note how triggers collection is moved as a separate element in resource dictionary, and x:Shared="false" set for it. This will cause creating new set of triggers each time this property is accessed, so each TextBox will have it's own set of triggers.

Then any

<TextBox Text="Test" Tag="test message" />

Will call GotFocusCommand on data context of TextBox, with "test message" as parameter.