Wednesday, February 9, 2011

Silverlight 4 Property Triggers

I spent a little time this week messing around with the newly added Triggers and TriggerActions available through the new Expression Blend 4 SDK.

Triggers and Behaviors are really just ways to attach functionality to an existing element, and the base classes that are included in the newer version of Silverlight 4 really make the job easier.  I'm going to walk through adding a trigger that fires when one of the properties on my ViewModel changes to true.  Now allegedly there is an existing trigger (DataStoreChangedTrigger) that will fire actions based on when a bound property changes, but I want to only fire my actions when my bound property becomes a specific value.

Our Goal

<ItemsControl
    Margin="0 130 0 0"
    HorizontalAlignment="Left"
    VerticalAlignment="Top"
    Opacity="0.0"
    ItemsSource="{Binding Items}">
    <i:Interaction.Triggers>
        <local:BooleanPropertyTrigger
            Binding="{Binding FinishedLoading}"
            TriggerValue="True">
            <local:StoryboardAction>
                <Storyboard>
                    <DoubleAnimation
                        To="1.0"
                        Duration="00:00:0.7"
                        Storyboard.TargetProperty="Opacity" />
                </Storyboard>
            </local:StoryboardAction>
        </local:BooleanPropertyTrigger>
    </i:Interaction.Triggers>
</ItemsControl>

The Codez

[Download the PropertyTrigger Example Source Project and play along at home]

To start out with, I create a base PropertyChangedTrigger class that will do most of the heavy lifting for us.  Essentially, we want to inherit from the TriggerBase<...> generic base class and specify that we want our Trigger to attach to a FrameworkElement (I suppose you could use another type of control class, but FrameworkElement will encompass just about any element with a DataContext, which I find useful).  Our PropertyChangedTrigger will expose a Binding property that will allow us to attach an event handler when our bound property changes so we can invoke our TriggerActions.

/// <summary>
/// A base property changed trigger that
/// fires whenever the bound property changes.
/// </summary>
public class PropertyChangedTrigger : TriggerBase<FrameworkElement>
{
    /// <summary>
    /// The <see cref="Binding" /> dependency property's name.
    /// </summary>
    public const string BindingPropertyName = "Binding";

    /// <summary>
    /// Gets or sets the value of the <see cref="Binding" />
    /// property. This is a dependency property.
    /// </summary>
    public object Binding
    {
        get
        {
            return (object)GetValue(BindingProperty);
        }
        set
        {
            SetValue(BindingProperty, value);
        }
    }

    /// <summary>
    /// Identifies the <see cref="Binding" /> dependency property.
    /// </summary>
    public static readonly DependencyProperty BindingProperty =
        DependencyProperty.Register(
        BindingPropertyName,
        typeof(object),
        typeof(PropertyChangedTrigger),
        new PropertyMetadata(null,
            new PropertyChangedCallback(Binding_ValueChanged)));

    /// <summary>
    /// Called after the trigger is attached to an AssociatedObject.
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();
    }

    /// <summary>
    /// Called when the trigger is being detached
    /// from its AssociatedObject,
    /// but before it has actually occurred.
    /// </summary>
    protected override void OnDetaching()
    {
        base.OnDetaching();
    }

    /// <summary>
    /// Occurs when Binding's value changes.
    /// </summary>
    /// <param name="obj">The obj on which the binding changed.</param>
    /// <param name="args">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
    private static void Binding_ValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        var trig = obj as PropertyChangedTrigger;
        if (trig != null && trig.ShouldTriggerFire(args.NewValue))
        {
            trig.OnPropertyTrigger(args.NewValue);
        }
    }

    /// <summary>
    /// Does the change logic test. By default, it will always fire on value change.
    /// </summary>
    /// <param name="newValue">The new value.</param>
    /// <returns>True if the trigger should fire, otherwise false.</returns>
    protected virtual bool ShouldTriggerFire(object newValue)
    {
        return true;
    }

    /// <summary>
    /// Called when [property trigger].
    /// </summary>
    /// <param name="value">The value of the property.</param>
    protected virtual void OnPropertyTrigger(object value)
    {
        base.InvokeActions(value);
    }
}

As you can tell, our PropertyChangedTrigger makes use of a virtual method ShouldTriggerFire(…) that will default to just fire everytime a property changes value. Next, we will override our base class to create an EqualsPropertyTrigger that only fires when the value changes to a specific one that we want.

/// <summary>
/// A base class for property triggers that must be equal to fire.
/// </summary>
/// <typeparam name="TValue">The type of the trigger value.</typeparam>
/// <summary>
/// A base class for property triggers that must be equal to fire.
/// </summary>
/// <typeparam name="TValue">The type of the trigger value.</typeparam>
public class EqualsPropertyTrigger<TValue> : PropertyChangedTrigger
{
    /// <summary>
    /// Gets or sets the trigger value to match the property value for.
    /// </summary>
    /// <value>The trigger value.</value>
    public TValue TriggerValue { get; set; }

    /// <summary>
    /// Logic to check whether the trigger should fire.
    /// </summary>
    /// <param name="newValue">The new value.</param>
    /// <returns>True if the trigger should fire, otherwise false.</returns>
    protected override bool ShouldTriggerFire(object newValue)
    {
        if (newValue == null)
            return this.TriggerValue == null;

        return newValue.Equals(this.TriggerValue);
    }
}

So now we have a nice base class for our BooleanPropertyTrigger that makes it's implementation really nice and clean.

/// <summary>
/// A generic object property trigger
/// </summary>
public class PropertyTrigger : EqualsPropertyTrigger<object>
{ }

/// <summary>
/// A Boolean value property trigger
/// </summary>
public class BooleanPropertyTrigger : EqualsPropertyTrigger<bool>
{ }

/// <summary>
/// A string property value trigger
/// </summary>
public class StringPropertyTrigger : EqualsPropertyTrigger<string>
{ }

Now, all that's left to do is hook it up in our XAML by adding the namespace to our trigger and making sure we have a reference to System.Windows.Interactivity (version 4.0.5.0).

Next Steps

Next, we could make a NotEqualsPropertyTrigger that fires when a value is not a certain value. It's implementation would be as easy as inheriting from the EqualsPropertyTrigger and negating the base ShouldFireTrigger(...) method.

Hopefully, like me, you've learned a little about triggers and how they can be useful. For my next blog post I'm going to incorporate the visual state manager and make a GoToStateAction along with talking a little bit about creating the StoryBoardAction you see in the example.

Now Playing - Pretty Lights - Hot Like Sauce

No comments:

Post a Comment