Silverlight 4 Rough Notes: Animating Items Into/Out Of ItemsControls

Note – these posts are put together after a short time with Silverlight 4 as a way of providing pointers to some of the new features that Silverlight 4 has to offer. I’m posting these from the PDC as Silverlight 4 is announced for the first time so please bear that in mind when working through these posts.

If you’re very sharp-eyed you might spot that the default ControlTemplate for a ListBoxItem has changed to include a new VSM state group called LayoutStates and that group includes states called BeforeLoaded, Loaded and Unloaded.

These allow you to apply transitions to items as they are added and removed from a ListBox ( or, perhaps more generally an ItemsControl but I haven’t quite figured that out at the time of writing ).

So, for instance – if I take the standard ListBoxItem template from Silverlight 3;

<Style TargetType="ListBoxItem">
      <Setter Property="Padding" Value="3" />
      <Setter Property="HorizontalContentAlignment" Value="Left" />
      <Setter Property="VerticalContentAlignment" Value="Top" />
      <Setter Property="Background" Value="Transparent" />
      <Setter Property="BorderThickness" Value="1"/>
      <Setter Property="TabNavigation" Value="Local" />
      <Setter Property="Template">
          <Setter.Value>
              <ControlTemplate TargetType="ListBoxItem">
                  <Grid Background="{TemplateBinding Background}">
                      <vsm:VisualStateManager.VisualStateGroups>
                          <vsm:VisualStateGroup x:Name="CommonStates">
                              <vsm:VisualState x:Name="Normal" />
                              <vsm:VisualState x:Name="MouseOver">
                                  <Storyboard>
                                      <DoubleAnimation Storyboard.TargetName="fillColor" Storyboard.TargetProperty="Opacity" Duration="0" To=".35"/>
                                  </Storyboard>
                              </vsm:VisualState>
                              <vsm:VisualState x:Name="Disabled">
                                  <Storyboard>
                                      <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="Opacity" Duration="0" To=".55" />
                                  </Storyboard>
                              </vsm:VisualState>
                          </vsm:VisualStateGroup>
                          <vsm:VisualStateGroup x:Name="SelectionStates">
                              <vsm:VisualState x:Name="Unselected" />
                              <vsm:VisualState x:Name="Selected">
                                  <Storyboard>
                                      <DoubleAnimation Storyboard.TargetName="fillColor2" Storyboard.TargetProperty="Opacity" Duration="0" To=".75"/>
                                  </Storyboard>
                              </vsm:VisualState>
                          </vsm:VisualStateGroup>
                          <vsm:VisualStateGroup x:Name="FocusStates">
                              <vsm:VisualState x:Name="Focused">
                                  <Storyboard>
                                      <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisualElement" Storyboard.TargetProperty="Visibility" Duration="0">
                                          <DiscreteObjectKeyFrame KeyTime="0">
                                              <DiscreteObjectKeyFrame.Value>
                                                  <Visibility>Visible</Visibility>
                                              </DiscreteObjectKeyFrame.Value>
                                          </DiscreteObjectKeyFrame>
                                      </ObjectAnimationUsingKeyFrames>
                                  </Storyboard>
                              </vsm:VisualState>
                              <vsm:VisualState x:Name="Unfocused"/>
                          </vsm:VisualStateGroup>
                      </vsm:VisualStateManager.VisualStateGroups>
                      <Rectangle x:Name="fillColor" Opacity="0" Fill="#FFBADDE9" IsHitTestVisible="False" RadiusX="1" RadiusY="1"/>
                      <Rectangle x:Name="fillColor2" Opacity="0" Fill="#FFBADDE9" IsHitTestVisible="False" RadiusX="1" RadiusY="1"/>
                      <ContentPresenter
                              x:Name="contentPresenter"
                              Content="{TemplateBinding Content}"
                              ContentTemplate="{TemplateBinding ContentTemplate}"
                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                              Margin="{TemplateBinding Padding}"/>
                      <Rectangle x:Name="FocusVisualElement" Stroke="#FF6DBDD1" StrokeThickness="1" Visibility="Collapsed" RadiusX="1" RadiusY="1" />
                  </Grid>
              </ControlTemplate>
          </Setter.Value>
      </Setter>
  </Style>

Then I can add a new VisualStateGroup to it as in;

                                <VisualStateGroup
                                    x:Name="LayoutStates">
                                    <VisualState
                                        x:Name="BeforeLoaded">          
                                    </VisualState>
                                    <VisualState
                                        x:Name="Loaded">
                                    </VisualState>
                                    <VisualState
                                        x:Name="Unloaded" />
                                </VisualStateGroup>

and then if I want my ListBoxItems to perhaps twirl in from “off-screen” I can do something like add a little PlaneProjection to the contentPresenter in the template as in;

<ContentPresenter
                                x:Name="contentPresenter"
                                Content="{TemplateBinding Content}"
                                ContentTemplate="{TemplateBinding ContentTemplate}"
                                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                Margin="{TemplateBinding Padding}">
                                <ContentPresenter.Projection>
                                    <PlaneProjection
                                        x:Name="contentProjection">

                                    </PlaneProjection>
                                </ContentPresenter.Projection>
                            </ContentPresenter>

and then manipulate that from the transitions into those new states;

     <VisualStateGroup
                                    x:Name="LayoutStates">
                                    <VisualState
                                        x:Name="BeforeLoaded">
                                        <Storyboard>
                                            <DoubleAnimation
                                                Duration="00:00:00"
                                                By="-196"
                                                Storyboard.TargetName="contentProjection"
                                                Storyboard.TargetProperty="GlobalOffsetX" />
                                            <DoubleAnimation
                                                Duration="00:00:00"
                                                By="-180"
                                                Storyboard.TargetName="contentProjection"
                                                Storyboard.TargetProperty="RotationY" />
                                            <DoubleAnimation
                                                Duration="00:00:00"
                                                By="270"
                                                Storyboard.TargetName="contentProjection"
                                                Storyboard.TargetProperty="RotationX" />
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState
                                        x:Name="Loaded">
                                        <Storyboard>
                                            <DoubleAnimation
                                                Duration="00:00:01"
                                                To="0"
                                                Storyboard.TargetName="contentProjection"
                                                Storyboard.TargetProperty="GlobalOffsetX" />
                                            <DoubleAnimation
                                                Duration="00:00:01"
                                                To="0"
                                                Storyboard.TargetName="contentProjection"
                                                Storyboard.TargetProperty="RotationY" />
                                            <DoubleAnimation
                                                Duration="00:00:01"
                                                To="0"
                                                Storyboard.TargetName="contentProjection"
                                                Storyboard.TargetProperty="RotationX" />
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState 
                                        x:Name="Unloaded">
                                    </VisualState>
                                </VisualStateGroup>

Adding a little UI and code around that – here’s some XAML that has a ListBox using that ItemContainerStyle and is set up to display some Rectangles from a data-bound Rectangles property and a couple of buttons to Add/Remove rectangles;

    <Grid
        x:Name="LayoutRoot"
        Background="White">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ListBox
            x:Name="listRectangles"
            ItemsSource="{Binding Rectangles}"
            ItemContainerStyle="{StaticResource myStyle}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Rectangle
                        Width="{Binding Width}"
                        Height="{Binding Height}"
                        Fill="{Binding Fill}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <StackPanel
            Orientation="Horizontal"
            Grid.Row="1"
            Margin="10">

            <Button
                Content="Add"
                Margin="10"
                Click="OnAdd" />
            <Button
                Content="Remove"
                Margin="10"
                IsEnabled="{Binding CanRemove}"
                Click="OnRemove" />
        </StackPanel>
    </Grid>

and here’s some code-behind to make that work;

  public partial class MainPage : UserControl, INotifyPropertyChanged
  {
    public ObservableCollection<RectangleData> Rectangles { get; set; }

    public MainPage()
    {
      InitializeComponent();

      Rectangles = new ObservableCollection<RectangleData>();

      Loaded += (s, e) =>
        {
          DataContext = this;
        };
    }
    public bool CanRemove
    {
      get
      {
        return (Rectangles.Count > 0);
      }
    }
    void OnAdd(object sender, RoutedEventArgs args)
    {
      Rectangles.Add(RectangleData.MakeRandom());
      FireCanRemoveChanged();
    }
    void OnRemove(object sender, RoutedEventArgs args)
    {
      if (listRectangles.SelectedItem != null)
      {
        Rectangles.Remove((RectangleData)listRectangles.SelectedItem);
        FireCanRemoveChanged();
      }
    }
    void FireCanRemoveChanged()
    {
      if (PropertyChanged != null)
      {
        PropertyChanged(this, new PropertyChangedEventArgs("CanRemove"));
      }
    }
    public event PropertyChangedEventHandler PropertyChanged;
  }

where RectangleData is just a little class;

  public class RectangleData
  {
    public double Width { get; set; }
    public double Height { get; set; }
    public Brush Fill { get; set; }

    public static RectangleData MakeRandom()
    {
      Random r = new Random((int)DateTime.Now.Ticks);
      RectangleData data = new RectangleData()
      {
        Width = r.Next(24, 192),
        Height = r.Next(24, 192),      
        Fill = new SolidColorBrush(Color.FromArgb(255, (byte)r.Next(0,255), (byte)r.Next(0,255),
          (byte)r.Next(0,255)))
      };
      return (data);
    }
  }

and so with that in place I get ListBoxItems that animate themselves onto the screen from the left-hand side ( kind of difficult to illustrate here so I changed the animation time and captured a few screenshots of this purple rectangle spinning in );

image image image

I think this’ll work great in Blend and provide people with a bunch of simple opportunities for designers to take control and use this kind of functionality along with easing animations and other kinds of panels to provide richer UI. Quick note – I didn’t have much success with the Unloaded state so far – not sure whether I was doing something wrong…