WPF数据触发器和故事板

29

我正在尝试在ViewModel/Presentation Model繁忙时触发进度动画。我有一个IsBusy属性,并且ViewModel被设置为UserControl的DataContext。最佳方法是什么,可以在IsBusy属性为true时触发“progressAnimation”故事板?Blend只允许我在UserControl级别添加事件触发器,并且我只能在数据模板中创建属性触发器。

“progressAnimation”在用户控件中被定义为资源。

我尝试将DataTriggers作为UserControl上的Style添加,但是当我尝试启动StoryBoard时,我会收到以下错误:

'Colorful.Control.SearchPanel'对象的“Style”属性不能分配“System.Windows.Style”值。样式中的Storyboard树不能指定TargetName。删除TargetName“progressWheel”。

ProgressWheel是我正在尝试执行动画的对象的名称,因此显然不想删除目标名称。

我希望使用数据绑定技术在XAML中解决此问题,而不是通过代码公开事件并启动/停止动画。

5个回答

48

你想要的可以通过在progressWheel本身上声明动画来实现: 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>

代码后端:

    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);
        }
    }
}
视图模型:
    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);
        }
    }
}
希望这能帮到你!

16

虽然直接将动画附加到要进行动画处理的元素上可以解决简单情况下的问题,但当您需要针对多个元素进行复杂动画时,这并不可行。(当然您可以为每个元素附加一个动画,但是很难管理。)

所以有一种另外的解决方法,让您可以使用 DataTrigger 来运行一个动画,以便针对命名元素进行处理。

在WPF中,有三个地方可以附加触发器: 元素、样式和模板。但是,前两个选项在这里都不起作用。第一个被排除在外,因为WPF不支持直接在元素上使用 DataTrigger。(据我所知,没有特别好的理由。记得很多年前我曾经询问过WPF团队的人员,他们说他们希望支持它,但是没有时间使其正常工作。)样式无法使用,因为正如您报告的错误消息所说,您不能在与样式关联的动画中针对命名元素进行处理。

因此,只能使用模板。您可以使用控件模板或数据模板。

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

使用这种结构,WPF可以轻松让动画引用模板中的命名元素。 (在这里我留空了动画和模板内容 - 当然,您需要使用实际的动画和内容填充它。)

这种方法可以在模板中运作而不能在样式中运作的原因是,当应用模板时,它定义的命名元素将始终存在,因此在该模板范围内定义的动画可以安全地引用那些元素。但是,在样式中通常不是这种情况,因为可以将样式应用于多个不同的元素,每个元素可能具有相当不同的可视树。(即使在您可以确保所需元素将出现的情况下,这也会有点令人沮丧,但也许有些东西使得在正确的时间将动画绑定到命名元素变得非常困难。我知道在 WPF 中有很多优化,以使样式的元素能够高效地重复使用,因此也许其中一个是使其难以支持的原因。)


1
我建议使用RoutedEvent而不是IsBusy属性。只需触发OnBusyStarted和OnBusyStopped事件,并在适当的元素上使用事件触发器即可。

1
好吧,那正是我想避免的...但是,如果我这样做:你有没有在不从UIElement派生的类中实现RoutedEvent的示例? - Jonas Follesø

1

您可以订阅DataObject类的PropertyChanged事件,并从Usercontrol级别触发RoutedEvent。

为了使RoutedEvent正常工作,我们需要将该类派生自DependancyObject。


我认为你是对的...从UserControl公开一个RoutedEvent似乎是最明显的解决方案...然而,我还没有放弃基于数据执行任意故事板的想法..但感谢您的建议! - Jonas Follesø

0

当属性改变时,您可以使用Trigger.EnterAction来启动动画。

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

就像我之前所说的,这是在用户控件级别上进行的,并且我只接受EventTriggers(而不是Property-或DataTriggers)。此外,IsBusy不是UserControl上的属性,而是设置为DataContext(ViewModel)的对象上的属性。 - Jonas Follesø

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接