views:

1249

answers:

5

I have a data object -- a custom class called Notification -- that exposes a IsCritical property. The idea being that if a notification will expire, it has a period of validity and the user's attention should be drawn towards it.

Imagine a scenario with this test data:

_source = new[] {
    new Notification { Text = "Just thought you should know" },
    new Notification { Text = "Quick, run!", IsCritical = true },
  };

The second item should appear in the ItemsControl with a pulsing background. Here's a simple data template excerpt that shows the means by which I was thinking of animating the background between grey and yellow.

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

What I'm unsure about is how to make this animation conditional upon the value of IsCritical. If the bound value is false, then the default background colour of #DDD should be maintained.

A: 

You use style triggers in this case. (I'm doing this from memory so there might be some bugs)

  <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>
Will
Looks promising, thanks. Let me try it out and get back to you.
Drew Noakes
Nope, doesn't work. Get the error: `Property Setter 'Triggers' cannot be set because it does not have an accessible set accessor.`
Drew Noakes
Well, this is going to be a bit more complex than I can work out right now. I'm sure there's a way to do this, but you'll probably have to go about it a completely different way. Good opportunity to read up on triggers...
Will
FYI - I've opened a bounty on this question, in case you want to revisit it.
Drew Noakes
I've played around with it a couple more times without much success.
Will
Thanks all the same. If you're interested, an elegant answer has been given that shows how to do this.
Drew Noakes
+2  A: 

What I would do is create two DataTemplates and use a DataTemplateSelector. Your XAML would be something like:

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

And the DataTemplateSelector would be something similar to:

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

This way, WPF will automatically set anything that is critical to the template with the animation, and everything else will be the other template. This is also generic in that later on, you could use a different property to switch the templates and/or add more templates (A Low/Normal/High importance scheme).

Jake Basile
This is an interesting answer, but it's not as flexible as I'd like. For example, what if there are multiple elements within the data template that need to animate depending upon the state of different properties? In my case too the actual data template is much more complicated than just `<TextBlock Text="{Binding Path=Text}" />` so I'd introduce a lot of duplication to my XAML via this. Might suit some people though. +1 for the detailed explanation!
Drew Noakes
+2  A: 

It seems to be an odity with ColorAnimation, as it works fine with DoubleAnimation. You need to explicity specify the storyboards "TargetName" property to work with ColorAnimation

    <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
@TFD - thanks for your answer. With your edit, it suits my needs, but @Anvanka pipped you with a correct answer (basically the same), so I gave it to him/her. +1 all the same.
Drew Noakes
+4  A: 

Hey Drew,

The final part of this puzzle is... DataTriggers. All you have to do is add one DataTrigger to your DataTemplate, bind it to IsCritical property, and whenever it's true, in it's EnterAction/ExitAction you start and stop highlighting storyboard. Here is completely working solution with some hard-coded shortcuts (you can definitely do better):

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>

Code behind:

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));
      }
    }
  }
}

Hope this helps :).

Anvaka
@Anvanka - thanks for this. I hadn't used DataTrigger EnterActions or ExitActions before. Thanks also for the detailed example -- a great answer and worthy of the bounty.
Drew Noakes
You are welcome :). I'm glad I could help.
Anvaka
+1  A: 

Here's a solution that only starts the animation when the incoming property update is a certain value. Useful if you want to draw the user's attention to something with the animation, but afterwards the UI should return to it's default state.

Assuming IsCritical is bound to a control (or even an invisible control) you add NotifyOnTargetUpdated to the binding and tie an EventTrigger to the Binding.TargetUpdated event. Then you extend the control to only fire the TargetUpdated event when the incoming value is the one you are interested in. So...

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

and in the XAML file ..

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