Behaviors are not working properly inside Style Se

2019-02-19 08:19发布

问题:

In my Windows-Runtime app, I have a theme with a style that has a Behavior defined for the DoubleTapped action:

These are the XML Namespaces:

xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:core="using:Microsoft.Xaml.Interactions.Core"

And this is the style:

<Style x:Name="DisplayImage" TargetType="ScrollViewer">
    <Setter Property="VerticalScrollBarVisibility" Value="Hidden" />
    <Setter Property="HorizontalAlignment" Value="Left" />
    <Setter Property="ZoomMode" Value="Disabled" />
    <Setter Property="i:Interaction.Behaviors">
        <Setter.Value>
            <i:BehaviorCollection>
                <core:EventTriggerBehavior EventName="DoubleTapped">
                    <local:ScrollViewerDoubleTap />
                </core:EventTriggerBehavior>
            </i:BehaviorCollection>
        </Setter.Value>
    </Setter>
</Style>

This is my Behavior:

[DefaultEvent(typeof(ScrollViewer),"DoubleTapped")]
public class ScrollViewerDoubleTap : DependencyObject, IAction
{
    public object Execute(object sender, object parameter)
    {
        ScrollViewer sv = (ScrollViewer)sender;
        if (sv.HorizontalScrollBarVisibility == ScrollBarVisibility.Disabled)
        {
            sv.HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden;
        }
        else
        {
            sv.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
        }
        return sender;
    }
}

And this is how I am using it:

<ScrollViewer Style="{StaticResource Image}" MaxWidth="1067">
    <Border BorderBrush="Black" BorderThickness="1">
        <Image Source="MyImage.png"/>
    </Border>
</ScrollViewer>

When I double-tap the first image on a page that has this style, it works perfectly; however, when I double-tap the other images on the page, the behavior code is never run. I know it is never run because I ran it with breakpoints, and it would break when I double-tapped the first image, but not the second. I will appreciate any tips on why this is happening.

回答1:

This won't work because behaviors, actions or triggers are designed to be attached to a single element. When you define it inside a style's setter, it's like you are trying to associate it with multiple elements and as you have already seen, the trigger is only called when you interact with the first element with this style.

There's a simple way to fix this. Basically, you need to make sure each element that's associated with this style has a new instance of the trigger you have created. You can have all this logic wrapped inside an attached property and then your style will only need to reference this property.

<Style x:Name="DisplayImage" TargetType="ScrollViewer">
    <Setter Property="VerticalScrollBarVisibility" Value="Hidden" />
    <Setter Property="HorizontalAlignment" Value="Left" />
    <Setter Property="ZoomMode" Value="Disabled" />
    <Setter Property="local:FrameworkElementEx.AttachBehaviors" Value="True" />
</Style>

This is how this attached property is implemented.

public static class FrameworkElementEx
{
    public static bool GetAttachBehaviors(DependencyObject obj)
    {
        return (bool)obj.GetValue(AttachBehaviorsProperty);
    }

    public static void SetAttachBehaviors(DependencyObject obj, bool value)
    {
        obj.SetValue(AttachBehaviorsProperty, value);
    }

    public static readonly DependencyProperty AttachBehaviorsProperty =
        DependencyProperty.RegisterAttached("AttachBehaviors", typeof(bool), typeof(FrameworkElementEx), new PropertyMetadata(false, Callback));

    private static void Callback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behaviors = Interaction.GetBehaviors(d);

        var eventTriggerBehavior = new EventTriggerBehavior
        {
            EventName = "DoubleTapped"
        };
        eventTriggerBehavior.Actions.Add(new ScrollViewerDoubleTap());

        behaviors.Add(eventTriggerBehavior);
    }
}


回答2:

We can do this using pure XAML

...and a reusable attached property, that receives a DataTemplate with a BehaviorCollection (or a single Behavior).

You don't have to change your code-behind.

<Style x:Key="SomeImageStyle" 
       xmlns:Behaviors="using:MyBehaviors"
       xmlns:i="using:Microsoft.Xaml.Interactivity"
       xmlns:core="using:Microsoft.Xaml.Interactions.Core"
       TargetType="Image">
  <Setter Property="local:AttachedBehaviorsEx.AttachedBehaviors">
    <Setter.Value>
            <DataTemplate>
                    <core:EventTriggerBehavior EventName="Click">
                        <Behaviors:SomeAction SomeParameter="someValue" />
                    </core:EventTriggerBehavior>
            </DataTemplate>
    </Setter.Value>
  </Setter>
</Style>

Or if you need a few Behaviors at a time:

    <Setter.Value>
            <DataTemplate>
                <i:BehaviorCollection>
                    <core:EventTriggerBehavior EventName="Click">
                        <Behaviors:SomeAction SomeParameter="someValue" />
                    </core:EventTriggerBehavior>
                    <core:EventTriggerBehavior EventName="Tapped">
                        <Behaviors:SomeAction SomeParameter="someOtherValue" />
                    </core:EventTriggerBehavior>
                </i:BehaviorCollection>
            </DataTemplate>
    </Setter.Value>

The attached property

You don't even need to change it.

using Microsoft.Xaml.Interactivity;
using Windows.UI.Xaml;
using System;

namespace MyBehaviors
{
    public static class AttachedBehaviorsEx
    {
        public static DataTemplate GetAttachedBehaviors(DependencyObject obj)
        {
            return (DataTemplate)obj.GetValue(AttachedBehaviorsProperty);
        }

        public static void SetAttachedBehaviors(DependencyObject obj, DataTemplate value)
        {
            obj.SetValue(AttachedBehaviorsProperty, value);
        }

        public static readonly DependencyProperty AttachedBehaviorsProperty =
            DependencyProperty.RegisterAttached("AttachedBehaviors", typeof(DataTemplate), typeof(AttachedBehaviorsEx), new PropertyMetadata(null, Callback));

        private static void Callback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            BehaviorCollection collection = null;
            var template = (DataTemplate)e.NewValue;
            if (template != null)
            {
                var value = template.LoadContent();

                if (value is BehaviorCollection)
                    collection = (BehaviorCollection)value;
                else if (value is IBehavior)
                    collection = new BehaviorCollection { value };
                else throw new Exception($"AttachedBehaviors should be a BehaviorCollection or an IBehavior.");
            }
            // collection may be null here, if e.NewValue is null
            Interaction.SetBehaviors(d, collection);
        }
    }
}

Why is it better?

It's a more flexible way to achieve this than Justin XL suggested.

His code requires modifying code-behind in order to re-use it. Each time when you want to re-use it, you will need to set the EventName (in this line EventName = "DoubleTapped") and the Behavior (this line eventTriggerBehavior.Actions.Add(new ScrollViewerDoubleTap());).