1) In Use…
On the one hand, I find the FluidMoveBehavior in Blend very simple. When an item’s layout changes I can use the behavior to have the item animate from its previous layout position to its new layout position rather than having the change be instantaneous.
In truth, I think the change is still instantaneous but the Behavior does clever things to apply transformations to the item from its new position to make it look like it’s travelling to that position from its previous position.
I can make a WrapPanel like this one with lots of rectangles in it;
and I can drag-and-drop a FluidMoveBehavior onto that WrapPanel from the Assets tab;
and I need to make sure that the properties on the behavior are set up correctly;
and as I resize the UI or as items are added/removed from the panel they will animate into their new positions.
Easy. Done. You can see them in mid-transition here;
Similarly, I can use the same behavior to cope with items moving from one ListBox to another. For example, if I was to create some sample data in Blend here just using some images with names (I’ll come back to data and sample data in Blend in a follow on post) and then drag that onto the design surface in order to create a ListBox bound to that sample data;
then I could easily add a Button and another ListBox and have the Button run code to move items from the sample data collection that it is bound to and insert them directly into the items of the second ListBox (it’s a bit unusual);
SampleDataSource src = (SampleDataSource)App.Current.Resources["SampleDataSource"]; Item item = src.Collection[0]; src.Collection.RemoveAt(0); this.listbox2.Items.Add(item);
and that gives the effect of;
and then I could again use the FluidMoveBehavior here by applying it to the Panels being used by both of my ListBoxes;
and making sure that the behavior is configured correctly;
and with that set on both of my ListBoxes, the behavior works fine although in this particular case because I haven’t set up an item template for the right hand ListBox the effect appears a little odd by revealing a little of the magic that’s going on underneath as one template gets swapped for another;
The other place where I can use this is in the “master/details” kind of scenario where if I start from scratch again by removing all my controls and behaviors and then adding a similar ListBox on the left-hand side of the screen but a Grid on the right hand side where the DataContext of that Grid is bound to the SelectedValue in the ListBox and then I drop an Image into the Grid and bind its Source leaving a working UI;
then I can add a FluidMoveBehavior to my Grid on the right hand side of the screen as in;
and make sure that’s configured correctly;
and I can then edit the ListBox ItemTemplate on the left-hand side of the screen in order to use a FluidMoveSetTagBehavior;
and if I have that correctly configured;
then changing selection in my ListBox will make it seem that the newly select image zooms out from its position in the ListBox to its position in the Grid on the right hand side of the screen;
Now, that all works and I see examples of these 3 scenarios repeated all over the web but despite having read this and this and various other articles, I still find that I’m mostly taking the approach of copying what I saw someone else do rather than fully understanding what it is I’m doing with this behavior.
I’ve never been happy with copying what I saw someone else do without understanding it since I was a small child who broke lots of toys trying to understand
how they worked
Nothing’s changed…
Digging In A Little…
On the other hand, then, I find this behavior to be complicated and I think the main reason for that is that I’ve never really seen an explanation that got through to me in terms of understanding how it actually works although I’ve seen plenty that told me how to make effective use of it.
So I asked about how it works.. There are 2 behaviors here.
- FluidMoveBehavior. I think this runs something like this;
- Syncs the LayoutUpdated event on the item itself if applied to Self or on all the children if applied to Children.
- On LayoutUpdated, lookup the previous position of the element( s )
- Run animations from previous position( s ) to new position( s )
- Store new position( s ) of the element( s ) as previous position( s ) for next time around.
- FluidMoveSetTagBehavior.
- Sync LayoutUpdated event.
- Store new position of the element( s ) as previous position( s )
Now the “previous positions” are stored in a static dictionary and that dictionary can be keyed in multiple ways;
- Tag = Element
- You store the .NET object reference of the UI element itself (i.e. the associated object of the Behavior) into the dictionary to record its previous position and to look it up later on.
- Tag = DataContext
- You store some other value ( either the DataContext itself or perhaps some part of it that you refer to using the TagPath value ) to record its previous position and look it up again.
The first one makes sense if you are moving a UI element from A to B directly from A to B as it will be the same UI object in both places so using the object reference of the UI element as a key seems fine.
The second one makes sense if you are moving the representation of a piece of data from A to B as the UI objects may well change but the animation still needs to go from A to B so a different way of identifying the 2 pieces of UI as “related” is needed because the exact same UI elements are not moving from A to B directly.
In the simplest case, if I have a UI such as;
which is just 2 WrapPanels with some rectangles in the one on the left;
<UserControl x:Class="SilverlightApplication2.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:int="clr-namespace:Microsoft.Expression.Interactivity.Layout;assembly=Microsoft.Expression.Interactions" d:DesignHeight="300" d:DesignWidth="400" xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <toolkit:WrapPanel x:Name="panel1"> <i:Interaction.Behaviors> <int:FluidMoveBehavior AppliesTo="Children" /> </i:Interaction.Behaviors> <Rectangle Width="96" Stroke="Black" RadiusX="3" RadiusY="3" StrokeThickness="2" Height="96" Fill="Red" Margin="6" /> <Rectangle Width="96" Stroke="Black" RadiusX="3" RadiusY="3" StrokeThickness="2" Height="96" Fill="Yellow" Margin="6" /> <Rectangle Width="96" Stroke="Black" RadiusX="3" RadiusY="3" StrokeThickness="2" Height="96" Fill="Lime" Margin="6" /> </toolkit:WrapPanel> <toolkit:WrapPanel x:Name="panel2" Grid.Column="1"> <i:Interaction.Behaviors> <int:FluidMoveBehavior AppliesTo="Children" /> </i:Interaction.Behaviors> </toolkit:WrapPanel> <Button Grid.Row="1" Grid.ColumnSpan="2" Content="Move" Click="Button_Click" /> </Grid> </UserControl>
and if the code behind the button just moves the rectangles to the other wrap panel;
if (this.panel1.Children.Count > 0) { UIElement element = this.panel1.Children[0]; this.panel1.Children.RemoveAt(0); this.panel2.Children.Add(element); }
then I’ll get my fluid move behaviour because what’s happening is;
- As items are initially laid out, they will store their position using their own object reference as a key because of the FluidMoveBehavior in the left hand WrapPanel.
- When an item moves to the right hand WrapPanel, it arrives in that panel and has its layout updated.
- The FluidMoveBehavior in the right hand WrapPanel will look up the previous position of the element using the object reference as a key and will animate from one position to the other.
- The items within the 2 WrapPanels will also move fluidly if they need to have their layout adjusted within their respective WrapPanels (e.g. because the Window size shrinks).
If I was to swap the FluidMoveBehavior on the left hand WrapPanel for a FluidMoveSetTagBehavior then I would still see 1-3 above but I would not see the items in the left hand WrapPanel move fluidly within that panel because the behavior is only setting the position. It is not using previous positions for animations.
But what if I was doing something a little more data-bound. I made a little class MyItem as below;
public class MyItem { public int FirstValue { get; set; } }
and a little class that I can use as a DataContext with 2 lists of these MyItem types available;
public class MyData { public MyData() { this.LeftHandList = new ObservableCollection<MyItem>( from i in Enumerable.Range(1, 10) select new MyItem() { FirstValue = i } ); this.RightHandList = new ObservableCollection<MyItem>(); this.MoveAcrossCommand = new ActionCommand(() => { if (this.LeftHandList.Count > 0) { MyItem item = this.LeftHandList[this.LeftHandList.Count - 1]; this.LeftHandList.Remove(item); this.RightHandList.Add(item); } }); } public ICommand MoveAcrossCommand { get; private set; } public ObservableCollection<MyItem> LeftHandList { get; set; } public ObservableCollection<MyItem> RightHandList { get; set; } }
so – very simple stuff. Just 2 lists for the LeftHandList and the RightHandList and a ICommand which takes items out of the left hand list and drops them into the right hand list.
With a little UI for this;
<UserControl x:Class="SilverlightApplication2.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:SilverlightApplication2" mc:Ignorable="d" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:int="clr-namespace:Microsoft.Expression.Interactivity.Layout;assembly=Microsoft.Expression.Interactions" d:DesignHeight="300" d:DesignWidth="400" xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit"> <UserControl.Resources> <local:MyData x:Key="myData" /> <DataTemplate x:Key="myTemplate"> <Border Width="96" Height="96" CornerRadius="2" Margin="6" BorderBrush="Black" BorderThickness="1"> <Grid> <Grid.RowDefinitions> <RowDefinition /> </Grid.RowDefinitions> <Viewbox> <TextBlock Text="{Binding FirstValue}" /> </Viewbox> </Grid> </Border> </DataTemplate> <ItemsPanelTemplate x:Key="myPanelTemplate"> <toolkit:WrapPanel> <i:Interaction.Behaviors> <int:FluidMoveBehavior AppliesTo="Children" Tag="DataContext" /> </i:Interaction.Behaviors> </toolkit:WrapPanel> </ItemsPanelTemplate> </UserControl.Resources> <Grid DataContext="{StaticResource myData}"> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled" ItemsSource="{Binding LeftHandList}" ItemTemplate="{StaticResource myTemplate}" ItemsPanel="{StaticResource myPanelTemplate}" /> <ListBox ItemTemplate="{StaticResource myTemplate}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" Grid.Column="1" ItemsSource="{Binding RightHandList}" ItemsPanel="{StaticResource myPanelTemplate}"> </ListBox> <Button Grid.Row="1" Grid.ColumnSpan="2" Content="Move" Command="{Binding MoveAcrossCommand}"/> </Grid> </UserControl>
Then as I use my UI and click the Button to move the items from one data list to another, I see the right fluid movement happening;
The difference between this scenario and the previous scenario is that it is not the same UI element that leaves the ListBox on the left hand side and arrives in the ListBox on the right hand side.
However, it is a representation of the same data and the trick is the way in which we signify to the behavior that this is what we want to happen. So, what goes on here is something like;
- As items are created in the left hand side ListBox, they have the FluidMoveBehavior applied to them and so they store their positions but they use the value of the DataContext as the key of that dictionary entry rather than using the object reference of the UI. This is because of line 42 above where the Tag is set to DataContext.
- When an item moves to the right hand side ListBox, a new set of UI elements is created in that ListBox. Because the parent container has a FluidMoveBehavior applied to it, the item will use its DataContext to look for a previous position that’s stored in the dictionary and it will find it and apply the right animations/transformations to make it look like the item has moved from the left hand ListBox to the right hand one.
Now, I could use some portion of the data present in the DataContext in order to identify an object as it moves from point A to point B. On my particular object model I only have the FirstValue property so I could have selected that and if I alter the FluidMoveBehavior that is being applied to the 2 WrapPanels within the 2 ListBoxes to do just that;
<ItemsPanelTemplate x:Key="myPanelTemplate"> <toolkit:WrapPanel> <i:Interaction.Behaviors> <int:FluidMoveBehavior AppliesTo="Children" Tag="DataContext" TagPath="FirstValue"/> </i:Interaction.Behaviors> </toolkit:WrapPanel> </ItemsPanelTemplate>
then the functionality is completely unchanged but items are being keyed now off the value of FirstValue rather than off the value of DataContext although it makes no material difference.
I could illustrate that difference by setting the InitialTag and InitialTagPath properties to show how these keys are being used.
If I change my data a little;
public class MyItem { public int FirstValue { get; set; } public int SecondValue { get; set; } }
and change the way that the data is created such that I make a set of 10 items where the FirstValue ranges from 1 to 10 and the SecondValue ranges from 1 to 5 for the last 5 items as in;
this.LeftHandList = new ObservableCollection<MyItem>( from i in Enumerable.Range(1, 10) select new MyItem() { FirstValue = i, SecondValue = (i > 5) ? i - 5 : 0 } );
and then I could change my command that moves items from one list to another so that it only attempts to move the last 5 items;
this.MoveAcrossCommand = new ActionCommand(() => { if (this.LeftHandList.Count > 5) { MyItem item = this.LeftHandList[this.LeftHandList.Count - 1]; this.LeftHandList.Remove(item); this.RightHandList.Add(item); } });
and change my UI such that on the right hand ListBox I will have a FluidMoveBehavior that not only sets the Tag and the TagPath but also sets the InitialTag and InitialTagPath. Here’s that 2nd ListBox;
<ItemsPanelTemplate> <toolkit:WrapPanel> <i:Interaction.Behaviors> <int:FluidMoveBehavior AppliesTo="Children" Tag="DataContext" TagPath="FirstValue" InitialTag="DataContext" InitialTagPath="SecondValue"/> </i:Interaction.Behaviors> </toolkit:WrapPanel> </ItemsPanelTemplate>
and so what this is trying to say is that the item should locate its previous position based on the FirstValue property in the DataContext.
However, when it is initially animated into position (using InitialTag and InitialTagPath) we’re saying that we want to use the SecondValue from the DataContext to lookup the starting point for the animation. That means that I’ll see;
- Item10 { FirstValue=10, SecondValue=5 } appear to animate from Item5 { FirstValue=5,SecondValue=0 }
- Item9 { FirstValue=9, SecondValue=4 } appear to animate from Item4 { FirstValue=4,SecondValue=0 }
- etc.
as in the picture below;
and those items will still lay themselves out “fluidly” within their individual WrapPanels based on the values of the FirstValue property.
I think that’s pretty much it – I’m feeling better about this Behavior having prodded around under the hood a little on it