I have several times gotten in a discussion regarding the visual aspects around changing the DataContext. That sounds like a really interesting discussion, doesn’t it…? Well, why is it a discussion at all, why is it something that I even bother writing about (just wait until you see the amount of code I have written as well)? Well…as you might have noticed in my previous posts, data bindings are very central in Silverlight development. At least if you do it like I do it. And that means that we work quite a lot with the DataContext of different controls. And out of the box, changing the DataContext will update all the binding, which is exactly what we want. But what happens when we DON’T want the change to be instantaneous? What if we want to add some form of transition? Well…as far as I know, you are %$@ out of luck. That is why I decided to try and build something that can do it for us…in a nice re-usable package…
So once again I am back and “extending” Silverlight with the help of attached properties. And today’s exercise is to get the DataContext switching to be less instant and instead have the option to add some animations. In my very simple mind, adding an animation before the change and one after would suffice in most situations. That means that I can fade out the control, switch value and then fade back in. Or whatever type of animation you would like to use.
So let’s get started. The first thing we need is a brand new class responsible for registering, and handling, the attached properties. I’m calling it TDCExtension (Transitional Data Context Extension) and have declared is at follows
public static class TDCExtension
{
}
The two attached properties that I have declared are called PreChangeAnimation and PostChangeAnimation. They are both of type Storyboard and are interested in listening for any changes. The declaration will therefore look like this
public static DependencyProperty PreChangeAnimationProperty =
DependencyProperty.RegisterAttached("PreChangeAnimation", typeof(Storyboard),
typeof(TDCExtension), new PropertyMetadata(null, PreChangeAnimationChanged));
public static DependencyProperty PostChangeAnimationProperty =
DependencyProperty.RegisterAttached("PostChangeAnimation", typeof(Storyboard),
typeof(TDCExtension), new PropertyMetadata(null, PostChangeAnimationChanged));
To make them usable from Xaml, I also need to add getters and setters for these properties (as well as implement the change callbacks). But before I do this, we need to have a look at a couple of other things.
First off, how do I actually get this whole thing to work? Well, I need to listen to whenever the DataContext of our object changes and then handle that by starting the animations. This would have been very simple if there had been a DataContextChanging and DataContextChanged event. However, there isn’t. There is actually not a single public way to figure out when the DataContext has changed. At least not that I have found. So how do I solve this. Well, there is a hack workaround for this. Since I can add attached properties to any object, and I can listen to changes to that property, and I can bind that property to the DataContext, I can use this to get notified when the DataContext is changed.
Therefore, I declare another attached property. This property will however fly under the radar completely and because of this, I have decided to make it private. It also means that I don’t have to add getters and setters for it. The declaration looks like this
private static DependencyProperty PseudoContextProperty =
DependencyProperty.RegisterAttached("PseudoContext", typeof(object),
typeof(TDCExtension), new PropertyMetadata(null, PseudoContextChanged));
So, that’s my sneaky, hacky, workaround. It is very useful, not only in this scenario, but any time you need to get notified of changes to the DataContext.
So, can I get on with the getters and setters and callbacks now? No… There is still more plumbing that needs to be created. I have decided to create a “context” object that will hold the pieces together for each control, as well as be responsible for running the storyboards and so on. It just makes everything a bit clearer in the end.
Once again, it is a thing that is internal to this class, so I declare the new as private inside the TDCExtension class. I call it TDCContext. And here comes the twist. I have decided to limit this functionality to Panels. Why? Well, they can have children and thus animate larger pieces of a UI at the time. Generally you use the DataContext by setting it to a parent object and have it “inherited" by the controls inside the parent. For example a form with a bunch of fields. Anyhow…I have decided to do it like this. I might change my mind in the future… So, the TDCContext will revolve around a Panel, so I take a reference to a Panel as a parameter in the constructor and hold on to it.
private class TDCContext
{
public TDCContext(Panel pnl)
{
Panel = pnl;
}
public Panel Panel
{
get;
private set;
}
}
What else do I use in conjunction with the Panel? Well, the whole thing calls for at least 2 animations and a DataContext. So I will throw in those parts in this object as well
private class TDCContext
{
object _ctx;
Storyboard _preAnimation;
Storyboard _postAnimation;
public TDCContext(Panel pnl)
{
Panel = pnl;
}
public Panel Panel
{
get;
private set;
}
public Storyboard PreAnimation
{
get { return _preAnimation; }
set
{
if (_preAnimation != null)
{
_preAnimation.Completed -= OnPreAnimationComplete;
}
_preAnimation = value;
_preAnimation.Completed += OnPreAnimationComplete;
}
}
public Storyboard PostAnimation
{
get { return _postAnimation; }
set { _postAnimation = value; }
}
}
As you see, I added a couple of things in the previous snippet. As you might have noticed, I have also indicated that there is more to come. If you look at the PreAnimation property, you will see that I show interest in when that animation has completed. The reason for this is of course that I want to run that animation, then change the context and finally run any post animation that have defined. But there is actually more in there. There are three more methods to add. There is the OnPreAnimationComplete handler that will handle the previously mentioned parts, but there is also one called ChangeContext that will be called when the context should change and a final one called InternalChangeContext that will change the DataContext. They look like this
public void ChangeContext(object newContext)
{
_ctx = newContext;
if (PreAnimation != null)
{
PreAnimation.Begin();
}
else
{
Panel.Dispatcher.BeginInvoke(InternalChangeContext);
}
}
private void OnPreAnimationComplete(object sender, EventArgs e)
{
InternalChangeContext();
}
private void InternalChangeContext()
{
foreach (var item in Panel.Children)
{
FrameworkElement el = item as FrameworkElement;
if (el != null)
{
el.DataContext = _ctx;
}
}
if (PostAnimation != null)
{
PostAnimation.Begin();
}
}
As you can see, the first step is that the ChangeContext method is called. When that is called, I check to see if there is a pre animation. If there is one, I run it, otherwise I call InternalChangeContext, which otherwise will be called by the OnPreAnimationComplete method. The InternalChangeContext method is responsible for all the “heavy lifting” (not that there is any really heavy lifting going on). It basically just iterates through all the children of the Panel and set their DataContext. And then if there is a post change animation, this is started.
This is all good and would probably work fine. Except for one minor detail. When the properties are set up, the animations will run instantly. So I have decided not to run it the first time that the DataContext changes (There is probably room for some improvement and tweaking here…but this is nice and simple). So to get this functionality I have added a little check and a boolean in there as follows. I have also added a little helper called IsEmpty that will tell me if the object has no animations at all. The final class looks like this
private class TDCContext
{
object _ctx;
bool _first = true;
Storyboard _preAnimation;
Storyboard _postAnimation;
public TDCContext(Panel pnl)
{
Panel = pnl;
}
public void ChangeContext(object newContext)
{
_ctx = newContext;
if (PreAnimation != null && !_first)
{
PreAnimation.Begin();
}
else
{
Panel.Dispatcher.BeginInvoke(InternalChangeContext);
}
}
private void OnPreAnimationComplete(object sender, EventArgs e)
{
InternalChangeContext();
}
private void InternalChangeContext()
{
foreach (var item in Panel.Children)
{
FrameworkElement el = item as FrameworkElement;
if (el != null)
{
el.DataContext = _ctx;
}
}
if (PostAnimation != null && !_first)
{
PostAnimation.Begin();
}
_first = false;
}
public Panel Panel
{
get;
private set;
}
public Storyboard PreAnimation
{
get { return _preAnimation; }
set
{
if (_preAnimation != null)
{
_preAnimation.Completed -= OnPreAnimationComplete;
}
_preAnimation = value;
_preAnimation.Completed += OnPreAnimationComplete;
}
}
public Storyboard PostAnimation
{
get { return _postAnimation; }
set { _postAnimation = value; }
}
public bool IsEmpty
{
get
{
return PreAnimation == null && PostAnimation == null;
}
}
}
So…to the real stuff. That was just a helping class that makes a few things simpler. So lets get on with it and create the functionality, which is actually not that complex now that we have the TDCContext.
Just to get complete code in the post, I will show you the getters and setters for the animation properties. They look like this
public static Storyboard GetPreChangeAnimation(DependencyObject source)
{
return source.GetValue(PreChangeAnimationProperty) as Storyboard;
}
public static void SetPreChangeAnimation(DependencyObject source, Storyboard storyboard)
{
source.SetValue(PreChangeAnimationProperty,storyboard);
}
public static Storyboard GetPostChangeAnimation(DependencyObject source)
{
return source.GetValue(PostChangeAnimationProperty) as Storyboard;
}
public static void SetPostChangeAnimation(DependencyObject source, Storyboard storyboard)
{
source.SetValue(PostChangeAnimationProperty, storyboard);
}
Which is really basic boilerplate code. Unfortunately the “propdp” snippet in VS does not work that well for Silverlight, not in 2008 at least. Hope they fix it in 2010. Anyhow…the real code is in the callbacks for the attached properties. But once again, there is some other code that needs to be added first… To “link” a panel to its TDCContext I have added a Dictionary<>. I have also added a method called GetContext that gets the context for the passed in Panel, or creates a new if there isn’t one already. It does however set up the “magic” that makes the whole thing work as well. When a new TDCContext is created, a binding is set up as well. A binding that binds the Panels PseudoContext property to the DataContext property. This makes it possible for us to get the so important callback I talked about before. By setting the binding path to “”, I tell is that I want to bind to the DataContext object itself… It looks like this
private static TDCContext GetContext(Panel pnl)
{
if (_contexts.ContainsKey(pnl))
{
return _contexts[pnl];
}
else
{
TDCContext ctx = new TDCContext(pnl);
pnl.SetBinding(PseudoContextProperty, new Binding(""));
_contexts.Add(pnl, ctx);
return ctx;
}
}
So…finally we can get to the callbacks. The Pre- and PostChangeAnimationChanged callbacks look more or less the same since they perform more or less the same task. Probably should be refactored, but I didn’t have time. So I will only show you the Pre callback and explain the differences in the Post one.
The first thing is to verify that the object that has had the attached property set is a Panel. If not, I just ignore it and return. If it is, I get hold of a TDCContext by calling the GetContext method. As you might recall, this will either get me the corresponding context, or create a new for me to use. After that I set the corresponding animation property on the TDCContext object. I then verify if the TDCContext object “is empty” and if it is, I break the DataContext binding and remove the TDCContext from the Dictionary. It looks like this
public static void PreChangeAnimationChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
Panel pnl = source as Panel;
if (pnl == null)
{
return;
}
TDCContext ctx = GetContext(pnl);
ctx.PreAnimation = e.NewValue as Storyboard;
if (ctx.IsEmpty)
{
pnl.SetBinding(PseudoContextProperty, null);
_contexts.Remove(ctx.Panel);
}
}
So the differences are of course the one line of code that sets the PreAnimation property. It will be PostAnimation in the other callback.
The final callback is the one for the PseudoContext property. It is very simple. It verifies that the source is a Panel and then gets the Panel’s TDCContext and calls ChangeContext on it, passing in the new context.
private static void PseudoContextChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
Panel pnl = source as Panel;
if (pnl == null)
{
return;
}
_contexts[pnl].ChangeContext(e.NewValue);
}
That’s it. That’s all that is needed to get this going. But I guess I need to create a little test application as well. Very simply explained it looks like this (the project is downloadable at the bottom of the post…).
I created a simple model to bind to
public class Person
{
public string Firstname
{
get;
set;
}
public string Lastname
{
get;
set;
}
}
Hey…I did say it was simple…
Next on the list of things to do was a test page. So I created one that had a simple form (Grid), with some values that could be bound, two animations and finally two buttons to change context. The Xaml looks like this
<UserControl x:Class="TransitionalDataContext.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:tdc="clr-namespace:TransitionalDataContext"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<Grid x:Name="LayoutRoot">
<Grid.Resources>
<Storyboard x:Key="PreAnimation">
<DoubleAnimation Storyboard.TargetName="Form"
Storyboard.TargetProperty="Opacity"
Duration="0:0:0.2" To="0" />
</Storyboard>
<Storyboard x:Key="PostAnimation">
<DoubleAnimation Storyboard.TargetName="Form"
Storyboard.TargetProperty="Opacity"
Duration="0:0:0.2" To="1" />
</Storyboard>
</Grid.Resources>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center" x:Name="Form"
tdc:TDCExtension.PreChangeAnimation="{StaticResource PreAnimation}"
tdc:TDCExtension.PostChangeAnimation="{StaticResource PostAnimation}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Firstname:" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="Lastname:" />
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding Firstname}" />
<TextBlock Grid.Row="2" Grid.Column="2" Text="{Binding Lastname}" />
</Grid>
<Button Content="Scott" Margin="-70,-100,0,0" Width="50" Click="Button_Click"
HorizontalAlignment="Center" VerticalAlignment="Center" />
<Button Content="Tim" Margin="0,-100,-70,0" Width="50" Click="Button_Click"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</UserControl>
As you can see, I have set the Pre- and PostChangeAnimation properties on the form/Grid. The animations are pretty simple. The reduce the opacity to 0 before the DataContext is changed and then pulls it up to 1 again.
Finally I had to create the models and handle the button clicks. Like this
public partial class MainPage : UserControl
{
Person _person1;
Person _person2;
public MainPage()
{
InitializeComponent();
_person1 = new Person() { Firstname = "Scott", Lastname = "Guthrie" };
_person2 = new Person() { Firstname = "Tim", Lastname = "Heuer" };
this.DataContext = _person1;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
string str = ((Button)sender).Content as String;
switch (str)
{
case "Scott":
this.DataContext = _person1;
break;
case "Tim":
this.DataContext = _person2;
break;
}
}
}
That’s it! I hope you got the idea behind it, and that you might have use for it in the future. There are probably better ways, and if you have one or find one, please tell me! The code is available as a zip here