WPF Data Triggers and Story Boards

2019-01-13 22:15发布

问题:

I'm trying to trigger a progress animation when ever the ViewModel/Presentation Model is Busy. I have a IsBusy Property, and the ViewModel is set as the DataContext of the UserControl. What is the best way to trigger a "progressAnimation" story board when the IsBusy property is true? Blend only let med add Event-Triggers on a UserControl level, and I can only create property triggers in my data templates.

The "progressAnimation" is defined as a resource in the user control.

I tried adding the DataTriggers as a Style on the UserControl, but when I try to start the StoryBoard I get the following error:

'System.Windows.Style' value cannot be assigned to property 'Style' 
of object'Colorful.Control.SearchPanel'. A Storyboard tree in a Style 
cannot specify a TargetName. Remove TargetName 'progressWheel'.

ProgressWheel is the name of the object I'm trying to animate, so removing target name is obvisouly NOT what I want.

I was hoping to solve this in XAML using data binding techniques, in stead of having to expose events and start/stop the animation through code.

回答1:

What you want is possible by declaring the animation on the progressWheel itself: The XAML:

<UserControl x:Class="TriggerSpike.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300">
<UserControl.Resources>
    <DoubleAnimation x:Key="SearchAnimation" Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:4"/>
    <DoubleAnimation x:Key="StopSearchAnimation" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:4"/>
</UserControl.Resources>
<StackPanel>
    <TextBlock Name="progressWheel" TextAlignment="Center" Opacity="0">
        <TextBlock.Style>
            <Style>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding IsBusy}" Value="True">
                        <DataTrigger.EnterActions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <StaticResource ResourceKey="SearchAnimation"/>
                                </Storyboard>
                            </BeginStoryboard>
                        </DataTrigger.EnterActions>
                        <DataTrigger.ExitActions>
                            <BeginStoryboard>
                                <Storyboard>
                                   <StaticResource ResourceKey="StopSearchAnimation"/> 
                                </Storyboard>
                            </BeginStoryboard>
                        </DataTrigger.ExitActions>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </TextBlock.Style>
        Searching
    </TextBlock>
    <Label Content="Here your search query"/>
    <TextBox Text="{Binding SearchClause}"/>
    <Button Click="Button_Click">Search!</Button>
    <TextBlock Text="{Binding Result}"/>
</StackPanel>

Code behind:

    using System.Windows;
using System.Windows.Controls;

namespace TriggerSpike
{
    public partial class UserControl1 : UserControl
    {
        private MyViewModel myModel;

        public UserControl1()
        {
            myModel=new MyViewModel();
            DataContext = myModel;
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            myModel.Search(myModel.SearchClause);
        }
    }
}

The viewmodel:

    using System.ComponentModel;
using System.Threading;
using System.Windows;

namespace TriggerSpike
{
    class MyViewModel:DependencyObject
    {

        public string SearchClause{ get;set;}

        public bool IsBusy
        {
            get { return (bool)GetValue(IsBusyProperty); }
            set { SetValue(IsBusyProperty, value); }
        }

        public static readonly DependencyProperty IsBusyProperty =
            DependencyProperty.Register("IsBusy", typeof(bool), typeof(MyViewModel), new UIPropertyMetadata(false));



        public string Result
        {
            get { return (string)GetValue(ResultProperty); }
            set { SetValue(ResultProperty, value); }
        }

        public static readonly DependencyProperty ResultProperty =
            DependencyProperty.Register("Result", typeof(string), typeof(MyViewModel), new UIPropertyMetadata(string.Empty));

        public void Search(string search_clause)
        {
            Result = string.Empty;
            SearchClause = search_clause;
            var worker = new BackgroundWorker();
            worker.DoWork += worker_DoWork;
            worker.RunWorkerCompleted += worker_RunWorkerCompleted;
            IsBusy = true;
            worker.RunWorkerAsync();
        }

        void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            IsBusy=false;
            Result = "Sorry, no results found for: " + SearchClause;
        }

        void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            Thread.Sleep(5000);
        }
    }
}

Hope this helps!



回答2:

Although the answer that proposes attaching the animation directly to the element to be animated solves this problem in simple cases, this isn't really workable when you have a complex animation that needs to target multiple elements. (You can attach an animation to each element of course, but it gets pretty horrible to manage.)

So there's an alternative way to solve this that lets you use a DataTrigger to run an animation that targets named elements.

There are three places you can attach triggers in WPF: elements, styles, and templates. However, the first two options don't work here. The first is ruled out because WPF doesn't support the use of a DataTrigger directly on an element. (There's no particularly good reason for this, as far as I know. As far as I remember, when I asked people on the WPF team about this many years ago, they said they'd have liked to have supported it but didn't have time to make it work.) And styles are out because, as the error message you've reported says, you can't target named elements in an animation associated with a style.

So that leaves templates. And you can use either control or data templates for this.

<ContentControl>
    <ContentControl.Template>
        <ControlTemplate TargetType="ContentControl">
            <ControlTemplate.Resources>
                <Storyboard x:Key="myAnimation">

                    <!-- Your animation goes here... -->

                </Storyboard>
            </ControlTemplate.Resources>
            <ControlTemplate.Triggers>
                <DataTrigger
                    Binding="{Binding MyProperty}"
                    Value="DesiredValue">
                    <DataTrigger.EnterActions>
                        <BeginStoryboard
                            x:Name="beginAnimation"
                            Storyboard="{StaticResource myAnimation}" />
                    </DataTrigger.EnterActions>
                    <DataTrigger.ExitActions>
                        <StopStoryboard
                            BeginStoryboardName="beginAnimation" />
                    </DataTrigger.ExitActions>
                </DataTrigger>
            </ControlTemplate.Triggers>

            <!-- Content to be animated goes here -->

        </ControlTemplate>
    </ContentControl.Template>
<ContentControl>

With this construction, WPF is happy to let the animation refer to named elements inside the template. (I've left both the animation and the template content empty here - obviously you'd populate that with your actual animation nd content.)

The reason this works in a template but not a style is that when you apply a template, the named elements it defines will always be present, and so it's safe for animations defined within that template's scope to refer to those elements. This is not generally the case with a style, because styles can be applied to multiple different elements, each of which may have quite different visual trees. (It's a little frustrating that it prevents you from doing this even in scenarios when you can be certain that the required elements will be there, but perhaps there's something that makes it very difficult for the animation to be bound to the named elements at the right time. I know there are quite a lot of optimizations in WPF to enable elements of a style to be reused efficiently, so perhaps one of those is what makes this difficult to support.)



回答3:

I would recommend to use RoutedEvent instead of your IsBusy property. Just fire OnBusyStarted and OnBusyStopped event and use Event trigger on the appropriate elements.



回答4:

You can subscribe to the PropertyChanged event of the DataObject class and make a RoutedEvent fire from Usercontrol level.

For RoutedEvent to work we need to have the class derived from DependancyObject



回答5:

You can use Trigger.EnterAction to start an animation when a property is changed.

<Trigger Property="IsBusy" Value="true">
    <Trigger.EnterActions>
        <BeginStoryboard x:Name="BeginBusy" Storyboard="{StaticResource MyStoryboard}" />
    </Trigger.EnterActions>
    <Trigger.ExitActions>
        <StopStoryboard BeginStoryboardName="BeginBusy" />
    </Trigger.ExitActions>
</Trigger>