Setting a property with an EventTrigger

WpfXamlTriggersEventtrigger

Wpf Problem Overview


I want to be able to set a property with an EventTrigger, there's a number of problems with this.

  1. EventTriggers only support Actions, so I must use a storyBoard to set my properties.

  2. Once I use a storyboard, I have two options:

  • Stop: Once the animation has stopped the value reverts back to before the animation started
  • HoldEnd: This locks the property, so that neither code, nor user interaction can change the property that the animation is holding.

In the below example, I want to set the IsChecked property to False when the button is clicked and I want the user to be able to change the IsChecked and/or I want to be able to change the property in code.

Example:

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

I realize that I can use the "Completed" event after the storyboard completes to set the value to False. However, in this instance I want to contain the logic within the XAML, as this logic will be used on a custom control and is only specific to the UI.

Wpf Solutions


Solution 1 - Wpf

Just create your own action.

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

In this case I'm binding to a property called DialogResult on my viewmodel.

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

Solution 2 - Wpf

As much as I love XAML, for this kinds of tasks I switch to code behind. Attached behaviors are a good pattern for this. Keep in mind, Expression Blend 3 provides a standard way to program and use behaviors. There are a few existing ones on the Expression Community Site.

Solution 3 - Wpf

I modified Neutrino's solution to make the xaml look less verbose when specifying the value:

Sorry for no pictures of the rendered xaml, just imagine a [=] hamburger button that you click and it turns into [<-] a back button and also toggles the visibility of a Grid.

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>

You can also specify values this way like in Neutrino's solution:

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

And here's the code.

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

EDIT: The Interactivity dll is no longer part of Blend and is now the "Microsoft.Xaml.Behaviors.Wpf" NuGet package. Code listed here: https://github.com/microsoft/XamlBehaviorsWpf

See: https://devblogs.microsoft.com/dotnet/open-sourcing-xaml-behaviors-for-wpf/

Steps to migrate from old Blend Microsoft.Expression.Interactions.dll to new opensource Interactivity dll (hopefully my old notes are correct ;p):

1. Install the "Microsoft.Xaml.Behaviors.Wpf" NuGet package.

2. Edit xaml files:
       Replace 'http://schemas.microsoft.com/expression/2010/interactivity' and
               'http://schemas.microsoft.com/expression/2010/interactions'

               with 'http://schemas.microsoft.com/xaml/behaviors'.

       Replace 'xmlns:ei="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions"' and
               'xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"'

               with 'xmlns:i="http://schemas.microsoft.com/xaml/behaviors"'.

3. Edit C# files:
       Replace usings in c# files 'Microsoft.Xaml.Interactivity' and
                                  'Microsoft.Xaml.Interactions'

                                  with 'Microsoft.Xaml.Behaviors'.

       Remove references to 'Microsoft.Expression.Interactions' and
                            'System.Windows.Interactivity'.

Solution 4 - Wpf

Stopping the Storyboard can be done in the code behind, or the xaml, depending on where the need comes from.

If the EventTrigger is moved outside of the button, then we can go ahead and target it with another EventTrigger that will tell the storyboard to stop. When the storyboard is stopped in this manner it will not revert to the previous value.

Here I've moved the Button.Click EventTrigger to a surrounding StackPanel and added a new EventTrigger on the the CheckBox.Click to stop the Button's storyboard when the CheckBox is clicked. This lets us check and uncheck the CheckBox when it is clicked on and gives us the desired unchecking behavior from the button as well.

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

To stop the storyboard in the code behind, we will have to do something slightly different. The third button provides the method where we will stop the storyboard and set the IsChecked property back to true through code.

We can't call myStoryboard.Stop() because we did not begin the Storyboard through the code setting the isControllable parameter. Instead, we can remove the Storyboard. To do this we need the FrameworkElement that the storyboard exists on, in this case our StackPanel. Once the storyboard is removed, we can once again set the IsChecked property with it persisting to the UI.

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionChris NicolView Question on Stackoverflow
Solution 1 - WpfNeutrinoView Answer on Stackoverflow
Solution 2 - WpfSergey AldoukhovView Answer on Stackoverflow
Solution 3 - WpfFocusedWolfView Answer on Stackoverflow
Solution 4 - WpfrmooreView Answer on Stackoverflow