WPF - 如何根据绑定数据项的属性来条件执行动画

15

我有一个数据对象——一个名为Notification的自定义类——其中暴露了一个IsCritical属性。这意味着如果通知将要过期,它具有有效期,并且应该吸引用户的注意。

想象一个具有以下测试数据的场景:

_source = new[] {
    new Notification { Text = "Just thought you should know" },
    new Notification { Text = "Quick, run!", IsCritical = true },
  };
第二个项目应该在 ItemsControl 中以脉动背景出现。以下是一个简单的数据模板摘录,展示了我想到的在灰色和黄色之间动画化背景的方法。
<DataTemplate DataType="Notification">
  <Border CornerRadius="5" Background="#DDD">
    <Border.Triggers>
      <EventTrigger RoutedEvent="Border.Loaded">
        <BeginStoryboard>
          <Storyboard>
            <ColorAnimation 
              Storyboard.TargetProperty="Background.Color"
              From="#DDD" To="#FF0" Duration="0:0:0.7" 
              AutoReverse="True" RepeatBehavior="Forever" />
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger>
    </Border.Triggers>
    <ContentPresenter Content="{TemplateBinding Content}" />
  </Border>
</DataTemplate>

我不确定的是如何根据IsCritical的值来使此动画有条件地运行。如果绑定的值为false,则应保持默认的背景颜色为#DDD

5个回答

12
这个谜题的最后一部分是...DataTriggers。你只需要在DataTemplate中添加一个DataTrigger,将其绑定到IsCritical属性,并且每当它为true时,在它的EnterAction/ExitAction中启动和停止高亮Storyboard。这是一个完全工作的解决方案,其中包含一些硬编码的快捷方式(你肯定可以做得更好):

Xaml:

<Window x:Class="WpfTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Notification Sample" Height="300" Width="300">
  <Window.Resources>
    <DataTemplate x:Key="NotificationTemplate">
      <Border Name="brd" Background="Transparent">
        <TextBlock Text="{Binding Text}"/>
      </Border>
      <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding IsCritical}" Value="True">
          <DataTrigger.EnterActions>
            <BeginStoryboard Name="highlight">
              <Storyboard>
                <ColorAnimation 
                  Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"
                  Storyboard.TargetName="brd"
                  From="#DDD" To="#FF0" Duration="0:0:0.5" 
                  AutoReverse="True" RepeatBehavior="Forever" />
              </Storyboard>
            </BeginStoryboard>
          </DataTrigger.EnterActions>
          <DataTrigger.ExitActions>
            <StopStoryboard BeginStoryboardName="highlight"/>
          </DataTrigger.ExitActions>
        </DataTrigger>
      </DataTemplate.Triggers>
    </DataTemplate>
  </Window.Resources>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="*"/>
      <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <ItemsControl ItemsSource="{Binding Notifications}"
                  ItemTemplate="{StaticResource NotificationTemplate}"/>
    <Button Grid.Row="1"
            Click="ToggleImportance_Click"
            Content="Toggle importance"/>
  </Grid>
</Window>

后台代码:

using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;

namespace WpfTest
{
  public partial class Window1 : Window
  {
    public Window1()
    {
      InitializeComponent();
      DataContext = new NotificationViewModel();
    }

    private void ToggleImportance_Click(object sender, RoutedEventArgs e)
    {
      ((NotificationViewModel)DataContext).ToggleImportance();
    }
  }

  public class NotificationViewModel
  {
    public IList<Notification> Notifications
    {
      get;
      private set;
    }

    public NotificationViewModel()
    {
      Notifications = new List<Notification>
                        {
                          new Notification
                            {
                              Text = "Just thought you should know"
                            },
                          new Notification
                            {
                              Text = "Quick, run!",
                              IsCritical = true
                            },
                        };
    }

    public void ToggleImportance()
    {
      if (Notifications[0].IsCritical)
      {
        Notifications[0].IsCritical = false;
        Notifications[1].IsCritical = true;
      }
      else
      {
        Notifications[0].IsCritical = true;
        Notifications[1].IsCritical = false;
      }
    }
  }

  public class Notification : INotifyPropertyChanged
  {
    private bool _isCritical;

    public string Text { get; set; }

    public bool IsCritical
    {
      get { return _isCritical; }
      set
      {
        _isCritical = value;
        InvokePropertyChanged("IsCritical");
      }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void InvokePropertyChanged(string name)
    {
      var handler = PropertyChanged;
      if (handler != null)
      {
        handler(this, new PropertyChangedEventArgs(name));
      }
    }
  }
}
希望这可以帮到你:)。

1
@Anvanka - 謝謝您。我以前沒有使用過DataTrigger的EnterActions或ExitActions。同時,感謝您提供詳細的範例--這是一個非常出色且值得獲得懸賞的答案。 - Drew Noakes

2

我会创建两个DataTemplates并使用DataTemplateSelector。你的XAML代码应该是这样的:

<ItemsControl
ItemsSource="{Binding ElementName=Window, Path=Messages}">
<ItemsControl.Resources>
    <DataTemplate
        x:Key="CriticalTemplate">
        <Border
            CornerRadius="5"
            Background="#DDD">
            <Border.Triggers>
                <EventTrigger
                    RoutedEvent="Border.Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <ColorAnimation
                                Storyboard.TargetProperty="Background.Color"
                                From="#DDD"
                                To="#FF0"
                                Duration="0:0:0.7"
                                AutoReverse="True"
                                RepeatBehavior="Forever" />
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Border.Triggers>
            <TextBlock
                Text="{Binding Path=Text}" />
        </Border>
    </DataTemplate>
    <DataTemplate
        x:Key="NonCriticalTemplate">
        <Border
            CornerRadius="5"
            Background="#DDD">
            <TextBlock
                Text="{Binding Path=Text}" />
        </Border>
    </DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <StackPanel />
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplateSelector>
    <this:CriticalItemSelector
        Critical="{StaticResource CriticalTemplate}"
        NonCritical="{StaticResource NonCriticalTemplate}" />
</ItemsControl.ItemTemplateSelector>

数据模板选择器类似于以下内容:

class CriticalItemSelector : DataTemplateSelector
{
    public DataTemplate Critical
    {
        get;
        set;
    }

    public DataTemplate NonCritical
    {
        get;
        set;
    }

    public override DataTemplate SelectTemplate(object item, 
            DependencyObject container)
    {
        Message message = item as Message;
        if(item != null)
        {
            if(message.IsCritical)
            {
                return Critical;
            }
            else
            {
                return NonCritical;
            }
        }
        else
        {
            return null;
        }
    }
}

这样,WPF将自动将与动画相关的一切设置为模板,其他所有内容都是另一个模板。这也是通用的,因为以后你可以使用不同的属性来切换模板和/或添加更多模板(低/正常/高重要性方案)。

这是一个有趣的答案,但它不够灵活。例如,如果数据模板中有多个元素需要根据不同属性的状态进行动画处理怎么办?在我的情况下,实际的数据模板比仅有的<TextBlock Text="{Binding Path=Text}" />要复杂得多,因此通过这种方式会在我的XAML中引入很多重复内容。虽然可能适合某些人。对于详细的解释给予+1! - Drew Noakes

2
似乎ColorAnimation存在一些问题,但DoubleAnimation没有这个问题。使用ColorAnimation时,需要显式指定storyboard的“TargetName”属性才能正常工作。
    <Window.Resources>

    <DataTemplate x:Key="NotificationTemplate">

        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding Path=IsCritical}" Value="true">
                <DataTrigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <ColorAnimation 
                                Storyboard.TargetProperty="Background.Color"
                                Storyboard.TargetName="border"
                                From="#DDD" To="#FF0" Duration="0:0:0.7" 
                                AutoReverse="True" RepeatBehavior="Forever" />
                        </Storyboard>
                    </BeginStoryboard>
                </DataTrigger.EnterActions>
            </DataTrigger>
        </DataTemplate.Triggers>

        <Border x:Name="border" CornerRadius="5" Background="#DDD" >
            <TextBlock Text="{Binding Text}" />
        </Border>

    </DataTemplate>

</Window.Resources>

<Grid>
    <ItemsControl x:Name="NotificationItems" ItemsSource="{Binding}" ItemTemplate="{StaticResource NotificationTemplate}" />
</Grid>

@TFD - 感谢您的回答。通过您的编辑,它满足了我的需求,但是@Anvanka给出了一个正确的答案(基本相同),所以我把它给了他/她。还是+1。 - Drew Noakes

1

这里有一个解决方案,只有在传入的属性更新为特定值时才启动动画。如果您想通过动画吸引用户的注意力,但之后UI应返回到其默认状态,则此方法非常有用。

假设IsCritical绑定到控件(甚至是不可见控件),则将NotifyOnTargetUpdated添加到绑定,并将EventTrigger绑定到Binding.TargetUpdated事件。然后,您可以扩展控件,仅在传入值为您感兴趣的值时才触发TargetUpdated事件。所以...

public class CustomTextBlock : TextBlock
    {
        public CustomTextBlock()
        {
            base.TargetUpdated += new EventHandler<DataTransferEventArgs>(CustomTextBlock_TargetUpdated);
        }

        private void CustomTextBlock_TargetUpdated(object sender, DataTransferEventArgs e)
        {
            // don't fire the TargetUpdated event if the incoming value is false
            if (this.Text == "False") e.Handled = true;
        }
    }

而在 XAML 文件中...

<DataTemplate>
..
<Controls:CustomTextBlock x:Name="txtCustom" Text="{Binding Path=IsCritical, NotifyOnTargetUpdated=True}"/>
..
<DataTemplate.Triggers>
<EventTrigger SourceName="txtCustom" RoutedEvent="Binding.TargetUpdated">
  <BeginStoryboard>
    <Storyboard>..</Storyboard>
  </BeginStoryboard>
</EventTrigger>
</DataTemplate.Triggers>
</DataTemplate>

0
你可以在这种情况下使用样式触发器。(我是凭记忆做的,所以可能会有一些错误)
  <Style TargetType="Border">
    <Style.Triggers>
      <DataTrigger Binding="{Binding IsCritical}" Value="true">
        <Setter Property="Triggers">
         <Setter.Value>
            <EventTrigger RoutedEvent="Border.Loaded">
              <BeginStoryboard>
                <Storyboard>
                  <ColorAnimation 
                    Storyboard.TargetProperty="Background.Color"
                    From="#DDD" To="#FF0" Duration="0:0:0.7" 
                    AutoReverse="True" RepeatBehavior="Forever" />
                </Storyboard>
              </BeginStoryboard>
            </EventTrigger>
         </Setter.Value>
        </Setter>
      </DataTrigger>  
    </Style.Triggers>
  </Style>

看起来很有前途,谢谢。让我试一下,然后再回复你。 - Drew Noakes
1
不行,不起作用。出现了错误:无法设置属性设置器'Triggers',因为它没有可访问的设置访问器。 - Drew Noakes
嗯,这比我现在能处理的要复杂一些。我相信有一种方法可以做到这一点,但你可能需要完全不同的方式去解决它。这是一个很好的机会去了解触发器... - user1228
请注意 - 我已经在这个问题上开了一个悬赏,以防您想要重新访问它。 - Drew Noakes
我已经尝试了几次,但没有太大的成功。 - user1228
还是非常感谢。如果您有兴趣,已经给出了一个优雅的答案,展示了如何完成这个任务。 - Drew Noakes

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