Установка свойства с помощью EventTrigger

Я хочу иметь возможность установить свойство с EventTrigger, там есть ряд проблем.

1) EventTriggers поддерживает только действия, поэтому я должен использовать storyBoard для установки моих свойств.

2) Как только я использую раскадровку, у меня есть два варианта:

  • Остановить: как только анимация остановилась, значение вернется обратно до начала анимации.
  • HoldEnd: это блокирует свойство, так что ни код, ни взаимодействие пользователя не могут изменять свойство, которое поддерживает анимация.

В приведенном ниже примере я хочу установить для свойства IsChecked значение False, когда нажимается кнопка, и я хочу, чтобы пользователь мог изменить IsChecked и/или я хочу изменить свойство в коде.

Пример:

<EventTrigger
    SourceName="myButton"
    RoutedEvent="Button.Click">
    <EventTrigger.Actions>
        <BeginStoryboard>
            <Storyboard>
                <BooleanAnimationUsingKeyFrames
                    Storyboard.TargetName="myCheckBox"
                    Storyboard.TargetProperty="IsChecked"
                    FillBehavior="Stop">
                    <DiscreteBooleanKeyFrame
                        KeyTime="00:00:00"
                        Value="False" />
                </BooleanAnimationUsingKeyFrames>
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger.Actions>
</EventTrigger>

Я понимаю, что после завершения раскадровки я могу использовать событие "Завершено", чтобы установить значение False. Однако в этом случае я хочу содержать логику в XAML, так как эта логика будет использоваться в пользовательском элементе управления и специфична только для пользовательского интерфейса.

Ответ 1

Насколько мне нравится XAML, для таких задач я переключаюсь на код позади. Приложенные варианты поведения являются хорошим примером для этого. Имейте в виду, Expression Blend 3 предоставляет стандартный способ для программирования и использования поведения. На сайте сообщества Expression есть несколько существующих.

Ответ 2

Просто создайте свое собственное действие.

namespace WpfUtil
{
    using System.Reflection;
    using System.Windows;
    using System.Windows.Interactivity;


    /// <summary>
    /// Sets the designated property to the supplied value. TargetObject
    /// optionally designates the object on which to set the property. If
    /// TargetObject is not supplied then the property is set on the object
    /// to which the trigger is attached.
    /// </summary>
    public class SetPropertyAction : TriggerAction<FrameworkElement>
    {
        // PropertyName DependencyProperty.

        /// <summary>
        /// The property to be executed in response to the trigger.
        /// </summary>
        public string PropertyName
        {
            get { return (string)GetValue(PropertyNameProperty); }
            set { SetValue(PropertyNameProperty, value); }
        }

        public static readonly DependencyProperty PropertyNameProperty
            = DependencyProperty.Register("PropertyName", typeof(string),
            typeof(SetPropertyAction));


        // PropertyValue DependencyProperty.

        /// <summary>
        /// The value to set the property to.
        /// </summary>
        public object PropertyValue
        {
            get { return GetValue(PropertyValueProperty); }
            set { SetValue(PropertyValueProperty, value); }
        }

        public static readonly DependencyProperty PropertyValueProperty
            = DependencyProperty.Register("PropertyValue", typeof(object),
            typeof(SetPropertyAction));


        // TargetObject DependencyProperty.

        /// <summary>
        /// Specifies the object upon which to set the property.
        /// </summary>
        public object TargetObject
        {
            get { return GetValue(TargetObjectProperty); }
            set { SetValue(TargetObjectProperty, value); }
        }

        public static readonly DependencyProperty TargetObjectProperty
            = DependencyProperty.Register("TargetObject", typeof(object),
            typeof(SetPropertyAction));


        // Private Implementation.

        protected override void Invoke(object parameter)
        {
            object target = TargetObject ?? AssociatedObject;
            PropertyInfo propertyInfo = target.GetType().GetProperty(
                PropertyName,
                BindingFlags.Instance|BindingFlags.Public
                |BindingFlags.NonPublic|BindingFlags.InvokeMethod);

            propertyInfo.SetValue(target, PropertyValue);
        }
    }
}

В этом случае я привязываюсь к свойству DialogResult на моей модели просмотра.

<Grid>

    <Button>
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="Click">
                <wpf:SetPropertyAction PropertyName="DialogResult" TargetObject="{Binding}"
                                       PropertyValue="{x:Static mvvm:DialogResult.Cancel}"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
        Cancel
    </Button>

</Grid>

Ответ 3

Остановка раскадровки может быть выполнена в коде позади или в xaml, в зависимости от того, откуда возникла необходимость.

Если EventTrigger перемещается за пределы кнопки, мы можем пойти дальше и нацелить его на другой EventTrigger, который скажет, что раскадровка остановится. Когда раскадровка остановлена ​​таким образом, она не вернется к предыдущему значению.

Здесь я переместил Button.Click EventTrigger в окружение StackPanel и добавил новый EventTrigger в CheckBox.Click, чтобы остановить раскладку Button, когда щелкнут CheckBox. Это позволяет нам проверять и снимать флажок с CheckBox, когда он нажат, и также дает нам желаемое снятие отметки с кнопки.

    <StackPanel x:Name="myStackPanel">

        <CheckBox x:Name="myCheckBox"
                  Content="My CheckBox" />

        <Button Content="Click to Uncheck"
                x:Name="myUncheckButton" />

        <Button Content="Click to check the box in code."
                Click="OnClick" />

        <StackPanel.Triggers>

            <EventTrigger RoutedEvent="Button.Click"
                          SourceName="myUncheckButton">
                <EventTrigger.Actions>
                    <BeginStoryboard x:Name="myBeginStoryboard">
                        <Storyboard x:Name="myStoryboard">
                            <BooleanAnimationUsingKeyFrames Storyboard.TargetName="myCheckBox"
                                                            Storyboard.TargetProperty="IsChecked">
                                <DiscreteBooleanKeyFrame KeyTime="00:00:00"
                                                         Value="False" />
                            </BooleanAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger.Actions>
            </EventTrigger>

            <EventTrigger RoutedEvent="CheckBox.Click"
                          SourceName="myCheckBox">
                <EventTrigger.Actions>
                    <StopStoryboard BeginStoryboardName="myBeginStoryboard" />
                </EventTrigger.Actions>
            </EventTrigger>

        </StackPanel.Triggers>
    </StackPanel>

Чтобы остановить раскадровку в коде позади, нам нужно будет сделать что-то немного другое. Третья кнопка предоставляет метод, в котором мы остановим раскадровку и вернем свойство IsChecked в true через код.

Мы не можем вызвать myStoryboard.Stop(), потому что мы не начали раскадровку через код, задав параметр isControllable. Вместо этого мы можем удалить раскадровку. Для этого нам нужен элемент FrameworkElement, в котором существует раскадровка, в данном случае наша StackPanel. Как только раскадровка будет удалена, мы сможем снова установить свойство IsChecked, сохраняя его в пользовательском интерфейсе.

    private void OnClick(object sender, RoutedEventArgs e)
    {
        myStoryboard.Remove(myStackPanel);
        myCheckBox.IsChecked = true;
    }

Ответ 4

Я изменил решение Neutrino, чтобы сделать xaml менее подробным при указании значения:

Извините за отсутствие изображений отображаемого xaml, просто представьте кнопку [=] гамбургера, которую вы нажимаете, и она превращается в кнопку [< -] назад, а также переключает видимость сетки.

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

...

<Grid>
    <Button x:Name="optionsButton">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="Click">
                <local:SetterAction PropertyName="Visibility" Value="Collapsed" />
                <local:SetterAction PropertyName="Visibility" TargetObject="{Binding ElementName=optionsBackButton}" Value="Visible" />
                <local:SetterAction PropertyName="Visibility" TargetObject="{Binding ElementName=optionsPanel}" Value="Visible" />
            </i:EventTrigger>
        </i:Interaction.Triggers>

        <glyphs:Hamburger Width="10" Height="10" />
    </Button>

    <Button x:Name="optionsBackButton" Visibility="Collapsed">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="Click">
                <local:SetterAction PropertyName="Visibility" Value="Collapsed" />
                <local:SetterAction PropertyName="Visibility" TargetObject="{Binding ElementName=optionsButton}" Value="Visible" />
                <local:SetterAction PropertyName="Visibility" TargetObject="{Binding ElementName=optionsPanel}" Value="Collapsed" />
            </i:EventTrigger>
        </i:Interaction.Triggers>

        <glyphs:Back Width="12" Height="11" />
    </Button>
</Grid>

...

<Grid Grid.RowSpan="2" x:Name="optionsPanel" Visibility="Collapsed">

</Grid>

Вы также можете указать значения таким образом, как в решении Neutrino:

<Button x:Name="optionsButton">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <local:SetterAction PropertyName="Visibility" Value="{x:Static Visibility.Collapsed}" />
            <local:SetterAction PropertyName="Visibility" TargetObject="{Binding ElementName=optionsBackButton}" Value="{x:Static Visibility.Visible}" />
            <local:SetterAction PropertyName="Visibility" TargetObject="{Binding ElementName=optionsPanel}" Value="{x:Static Visibility.Visible}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>

    <glyphs:Hamburger Width="10" Height="10" />
</Button>

И вот код.

using System;
using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Windows.Interactivity;

namespace Mvvm.Actions
{
    /// <summary>
    /// Sets a specified property to a value when invoked.
    /// </summary>
    public class SetterAction : TargetedTriggerAction<FrameworkElement>
    {
        #region Properties

        #region PropertyName

        /// <summary>
        /// Property that is being set by this setter.
        /// </summary>
        public string PropertyName
        {
            get { return (string)GetValue(PropertyNameProperty); }
            set { SetValue(PropertyNameProperty, value); }
        }

        public static readonly DependencyProperty PropertyNameProperty =
            DependencyProperty.Register("PropertyName", typeof(string), typeof(SetterAction),
            new PropertyMetadata(String.Empty));

        #endregion

        #region Value

        /// <summary>
        /// Property value that is being set by this setter.
        /// </summary>
        public object Value
        {
            get { return (object)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }

        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register("Value", typeof(object), typeof(SetterAction),
            new PropertyMetadata(null));

        #endregion

        #endregion

        #region Overrides

        protected override void Invoke(object parameter)
        {
            var target = TargetObject ?? AssociatedObject;

            var targetType = target.GetType();

            var property = targetType.GetProperty(PropertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
            if (property == null)
                throw new ArgumentException(String.Format("Property not found: {0}", PropertyName));

            if (property.CanWrite == false)
                throw new ArgumentException(String.Format("Property is not settable: {0}", PropertyName));

            object convertedValue;

            if (Value == null)
                convertedValue = null;

            else
            {
                var valueType = Value.GetType();
                var propertyType = property.PropertyType;

                if (valueType == propertyType)
                    convertedValue = Value;

                else
                {
                    var propertyConverter = TypeDescriptor.GetConverter(propertyType);

                    if (propertyConverter.CanConvertFrom(valueType))
                        convertedValue = propertyConverter.ConvertFrom(Value);

                    else if (valueType.IsSubclassOf(propertyType))
                        convertedValue = Value;

                    else
                        throw new ArgumentException(String.Format("Cannot convert type '{0}' to '{1}'.", valueType, propertyType));
                }
            }

            property.SetValue(target, convertedValue);
        }

        #endregion
    }
}