Friday, August 21, 2009

[2009.08.21] XAML NameScopes and WPF Code Behind Animation

I wanted to touch on this briefly as I ran into a namescoping issue reading on animations and thought I'd share a little bit of what I learned.

A namescope is the relationship between the name of an object defined in XAML and its instance. This is particularly relevant when interacting with the object in the code behind. Framework level items (FrameworkElement / FrameworkContentElement can be named using the Name property in code or XAML attribute. However, some some animatable objects such as those that derive from the Freezable class requires using the x:Name XAML attribute value in order for code behind interaction with that instance of the object. The name assigned to an object is unique and this is enforced by the namescope of the containing object.

The INameScope interface provides the contract for enforcing name uniqueness and how names are accessed in a XAML namescope. The NameScope class implements base support for the INameScope methods that store or retrieve name-object mappings into a particular namescope, and adds attached property support to make it simpler to get or set identifier namescope information dynamically at the element level (from documentation).

Some things have been added and shifted around from .NET 3.5 to 4.0 Beta 1 for NameScope.

.NET 3.5

  • NameScope only implements INameScope interface
  • NameScope is under the System.Windows namespace in the PresentationFramework.dll assembly.

.NET 4.0 Beta 1

  • NameScope implements several interfaces and provides a host of additional members
  • moved to the System.Windows namespace in the WindowsBase.dll assembly.
[2009.08.20].Namescope.hierarchy.01  
Fig. 1. NameScope hierarchy in .NET 3.5

[2009.08.20].Namescope.hierarchy.02
Fig. 2. NameScope hierarchy in .NET 4.0 B1

While namescopes can be nested, because of the enforcement of uniqueness, two elements that falls within the same namescope cannot have the same name. For example, when you fire up Visual Studio and create a new WPF project (page or window), a namescope is automatically created for you since these are root elements of an application. Even if you do not assign names to any of the elements in XAML, the namescope is still there and waiting for you. But if you try to give two buttons on the same window the same name, an exception will be thrown. However, things may get a little weird when you jump to C# and try to work with objects from the code behind, especially when dealing with animation. Thus far, you can pretty much do a whole lot in XAML to satisfy your animation needs without sweating over the whole namescope issue.

This is where I ran into my first namescoping problems - writing animation code in C#. Here, I will look at simple snippets of code in two scenarios:
[1] A empty WPF project was created with Visual Studio
[2] A WPF app is written without any initial XAML at all - like in Notepad

Case 1:

As soon as you create a new WPF project in VS, remember that some initial housekeeping is automatically taken care for you, such as creating a root element, be is a Page or a Window. At this point, since this is root element, a namescope is created for it. Now if you add elements using VS either using XAML or the designer, then add the Name or x:Name attribute, all is well in the world and you can freely use the named elements in code behind with no issues.

But here, let's forget the XAML part and do it all in C# in the code behind. Therefore, when you created your WPF project (let's say window), all you have is the basic structure of a window and a grid as its content. Remove the grid and go to the code behind.

The code will create a rectangle object, name it, register its name in the containing namescope, create an animation to change the opacity, create a storyboard to run the animation.

    1 using System;

    2 using System.Windows;

    3 using System.Windows.Controls;

    4 using System.Windows.Media;

    5 using System.Windows.Media.Animation;

    6 using System.Windows.Shapes;

    7 

    8 namespace WpfApplication_Namescope

    9 {

   10     public partial class Window1 : Window

   11     {

   12         Storyboard mySB;

   13 

   14         public Window1()

   15         {

   16             InitializeComponent();

   17 

   18             // STACKPANEL

   19             // create a stack panel to put my

   20             //  objects in

   21             StackPanel sp = new StackPanel();

   22 

   23             // RECTANGLE

   24             // create rectangle and set some

   25             //  properties

   26             Rectangle myRect = new Rectangle();

   27             myRect.Width = 100;

   28             myRect.Height = 100;

   29             myRect.Fill = Brushes.Black;

   30 

   31             // name the rectangle

   32             myRect.Name = "myRect";

   33 

   34             // register the name of the rectangle

   35             //  with the namescope of the containing

   36             //  element

   37             // in this case this is the root element,

   38             //  the window

   39             NameScope.GetNameScope(this).

   40                 RegisterName(myRect.Name, myRect);

   41 

   42             // add the rectangle to the stack panel

   43             sp.Children.Add(myRect);

   44 

   45             // ANIMATION

   46             // create a DoubleAnimation to animate the

   47             //  opacity of the rectangle from 100%

   48             //  to 50% over 2 seconds

   49             DoubleAnimation da = new DoubleAnimation();

   50             da.From = 1.0;

   51             da.To = 0.5;

   52             da.Duration =

   53                 new Duration(TimeSpan.FromSeconds(2));

   54             da.AutoReverse = true;

   55 

   56             // STORYBOARD

   57             mySB = new Storyboard();

   58             mySB.Children.Add(da);

   59             Storyboard.SetTargetName(da, myRect.Name);

   60             Storyboard.SetTargetProperty(da,

   61               new PropertyPath(Rectangle.OpacityProperty));

   62 

   63             // make the stack panel the main content of

   64             //  the window

   65             this.Content = sp;

   66 

   67             // start the storyboard when the window

   68             //  is loaded

   69             this.Loaded +=

   70                new RoutedEventHandler(Window1_Loaded);

   71         }

   72 

   73         void Window1_Loaded(object sender,

   74             RoutedEventArgs e)

   75         {

   76             this.mySB.Begin(this);

   77         }

   78     }

   79 }

Lines 32, 39 and 40 is where the namescoping comes in. We needed to set the name of the rectangle and register it in the namescope of the window (since its the root element, but you could have registered it to another containing elements' namescope). This is necessary for Line 59, setting the Storyboard.SetTargetName attached property. When the storyboard starts, it will look of the element with that particular name. It will first search any locally defined namescopes to find the name-object mapping then walk up the tree all the way to the root. If you did not register the name of the rectangle (you can comment out Lines 39,40 to see) then when you run the storyboards, guess what will happen. Nothing! That was because the storyboard could not resolve the name of the element it is supposed to use to animate a property on.

Case 2:

This is an instance if you do not use any XAML at all, probably not even using VS to create an initial skeleton project. Let's say you want to do exactly the stuff in case 1, but all in notepad. The main changes will be as shown:

    1             // create a namescope for this

    2             //  window

    3             NameScope.SetNameScope(this, new NameScope());

    4 

    5             // RECTANGLE

    6             // create rectangle and set some

    7             //  properties

    8             Rectangle myRect = new Rectangle();

    9             myRect.Width = 100;

   10             myRect.Height = 100;

   11             myRect.Fill = Brushes.Black;

   12 

   13             // name the rectangle

   14             myRect.Name = "myRect";

   15 

   16             // after naming the rectangle,

   17             //  register it with the namescope

   18             //  created above

   19             this.RegisterName(myRect.Name, myRect);

The main points are Lines 3 and 19. Because we did not have an initial XAML file, no namescope was automatically created for us to use. However, in Line 3, we created our own and in Line 19 we registered the rectangle to be animated with the namescope. Make note of the slight syntax differences.

The final thing I want to mention is namescopes in styles and templates. The main issue is that styles and templates have their own namescopes, which is independent to that of the containing page or window. For instance, in a template, you may have an rectangle with a name, yet if you use that template multiple times in your WPF application, there is no error. That is because the name-object mapping is resolved within each instantiation of the template. So each template instance will have a rectangle that has the exact same name but no collision will occur and each is contained in its own template and by extension, its own namescope.

No comments: