Question

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?

Was it helpful?

Solution 3

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

OTHER TIPS

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.

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top