views:

558

answers:

3

I have created an attached dependency property for Storyboards, with the intention of enabling me to call a method on my ViewModel when a Storyboard Completed event fires:

public static class StoryboardExtensions
{
    public static ICommand GetCompletedCommand(DependencyObject target)
    {
        return (ICommand)target.GetValue(CompletedCommandProperty);
    }

    public static void SetCompletedCommand(DependencyObject target, ICommand value)
    {
        target.SetValue(CompletedCommandProperty, value);
    }

    public static readonly DependencyProperty CompletedCommandProperty =
        DependencyProperty.RegisterAttached(
            "CompletedCommand",
            typeof(ICommand),
            typeof(StoryboardExtensions),
            new FrameworkPropertyMetadata(null, OnCompletedCommandChanged));

    static void OnCompletedCommandChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        Storyboard storyboard = target as Storyboard;
        if (storyboard == null) throw new InvalidOperationException("This behavior can be attached to Storyboard item only.");
        storyboard.Completed += new EventHandler(OnStoryboardCompleted);
    }

    static void OnStoryboardCompleted(object sender, EventArgs e)
    {                        
        Storyboard item = ... // snip
        ICommand command = GetCompletedCommand(item);
        command.Execute(null);
    }
}

then I try to use it in XAML, with a Binding syntax:

<Grid>
    <Grid.Resources>
        <Storyboard x:Key="myStoryboard" my:StoryboardExtensions.CompletedCommand="{Binding AnimationCompleted}">
            <DoubleAnimation Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:5" />
        </Storyboard>

        <Style x:Key="myStyle" TargetType="{x:Type Label}">
            <Style.Triggers>
                <DataTrigger 
                 Binding="{Binding Path=QuestionState}" Value="Correct">
                    <DataTrigger.EnterActions>
                        <BeginStoryboard Storyboard="{StaticResource myStoryboard}" />
                    </DataTrigger.EnterActions>
                </DataTrigger>
            </Style.Triggers>
        </Style>

    </Grid.Resources>
    <Label x:Name="labelHello" Grid.Row="0" Style="{StaticResource myStyle}">Hello</Label>
</Grid>

This fails with the following exception:

System.Windows.Markup.XamlParseException occurred Message="Cannot convert the value in attribute 'Style' to object of type 'System.Windows.Style'. Cannot freeze this Storyboard timeline tree for use across threads. Error at object 'labelHello' in markup file 'TestWpfApp;component/window1.xaml'

Is there any way to get the Binding syntax working with an attached ICommand property for a Storyboard?

+1  A: 

This is something by design. If you have a freezable object that is put into a style, the style will be frozen to allow cross-thread access. But you binding is essentially an expression which means it cannot be frozen as data binding is single threaded.

If you need to do this, put the trigger outside the style under a framework element instead of in a style. You can do this in your Grid.Triggers section. This does suck a little as your style is no longer complete and you have to duplicate the triggers but it is a "by design" feature in WPF.

The full answer on MSDN Social forums is here.

Ray Booysen
but Grid.Triggers will not accept DataTrigger, only EventTrigger
Mark Heath
Hmm that makes it more complicated. Not quite sure then.
Ray Booysen
The idea still stands regarding the freezable objects but a solution? not sure.
Ray Booysen
A: 

You could create a new Freezable-derived class to launch a storyboard as a shim. Bind a property on that shim object to the storyboard name. That way, you won't have to duplicate triggers or store them outside the style.

Dan Vanderboom
A: 

To get around this problem, I created a bunch of Attached Properties, called Storyboard Helpers (source code here). I gave up trying to attach them to the Storyboard itself, and now attach to any (arbitrary) framework element to call an ICommand on my ViewModel when the storyboard is completed, as well as binding to a particular event on my ViewModel to launch the Storyboard. A third attached property specifies the Storyboard we are dealing with:

<FrameworkElement
   my:StoryboardHelpers.Storyboard="{StaticResource rightAnswerAnimation}"
   my:StoryboardHelpers.Completed="{Binding CompletedCommand}"
   my:StoryboardHelpers.BeginEvent="{Binding StartCorrectAnswer}" />
Mark Heath