The Ins and Outs of Dependency Properties and Routed Events in WPF

XAML, one of the most important pieces in the Windows Presentation Foundation (WPF) puzzle, is merely another way to describe your intent to the computer. In fact, the XAML you write can almost always be written in VB.NET or C#. For instance, you could create a button using the following XAML code:

<Grid Name="myGrid">
   <Button Width="200" Height="50">Sample Button</Button>
</Grid>

Alternatively, you could achieve the same effect using the following C# code:

Button btn  = new Button();
btn.Content = "Sample Button";
btn.Width   = 200;
btn.Height  = 50;

myGrid.Children.Add(btn);

Either of the above will produce an output similar to Figure 1.

Figure 1. Output from XAML and C# Code for Creating a Button

As you can see, the C# code seems to equate XAML's element names to classes, and its attributes to properties of those classes. Anywhere it encounters nested elements, some mechanism in C# adds them either as children or as content of the outer element. This seems like a very linear transformation of the code from XAML to C#.

Now, to complicate this a little bit. Suppose you have some nesting of elements. Consider the following XAML:

<Grid Name="myGrid" Margin="20">
   <TextBlock>
      Sample Text outside the ItemsControl block
   </TextBlock>
   <ItemsControl Foreground="Red" VerticalAlignment="Center">
      <TextBlock>Sample Text without foreground</TextBlock>
      <TextBlock Foreground="Blue">
         Sample Text with foreground
      </TextBlock>
   </ItemsControl>
</Grid>

As you can see, there are three TextBlocks. Two of them have no ForeGround specified, but one of those two is nested inside an ItemsControl element that does have a ForeGround specified. If you run this application, it produces the output in Figure 2.

Figure 2. Application with Nested Elements

The first TextBlock is black because it has no ForeGround set on it. But by that argument, the second TextBlock should also be black. Curiously enough, it is getting its ForeGround from the element in which it is nested. There is something behind the scenes that is setting the second TextBlock's ForeGround to red. That same mechanism is probably also trying to set the ForeGround of the third TextBlock to red, but the third TextBlock has chosen to ignore that and instead specify its own ForeGround to blue. Clearly, this isn't a simple linear transformation to C#.

However, before you accept that conclusion, try writing this code in C#. If you had to write a simple linear transformation of the XAML to C#, the code would look a bit like this:

TextBlock firstTextBlock = new TextBlock();
firstTextBlock.Text      = "Sample Text outside the ItemsControl
                            block";

ItemsControl itc      = new ItemsControl();
itc.Foreground        = Brushes.Red;
itc.VerticalAlignment = VerticalAlignment.Center;

TextBlock secondTextBlock = new TextBlock();
secondTextBlock.Text      = "Sample text without foreground";
itc.Items.Add(secondTextBlock);

TextBlock thirdTextBlock  = new TextBlock();
thirdTextBlock.Foreground = Brushes.Blue;
thirdTextBlock.Text       = "Sample Text with foreground";
itc.Items.Add(thirdTextBlock);

myGrid.Children.Add(firstTextBlock);
myGrid.Children.Add(itc);

Now, when you run the application, you'd probably be shocked to see that the output looks like Figure 3.

Figure 3. Linear Transformation of the XAML to C#

So the linear transformation of XAML to C# seems to produce the same results? Not one line of code above seems to set secondTextBlock.ForeGround to its parent's ForeGround. Yet the ForeGround of the second TextBlock is red, not the default black. Who is setting the ForeGround of the second TextBlock? The answer is the WPF runtime is setting ForeGround, because ForeGround is a dependency property.

In fact, the above example is just one of the ramifications of not fully understanding dependency properties (in this case, an attached property).

Dependency Properties Explained

A dependency property is a property of an element that depends on a number of things outside the element. In WPF, if you want properties on your custom elements (visual or otherwise) to support things such as value expressions, property invalidation, per-type default values, inheritance, data binding, animation, and styling, you need to use dependency properties. Thus, dependency properties are one of the most important concepts within WPF.

So, how can you author your own dependency property? When writing a Windows or ASP.NET control, you would typically write a property to represent ForeGround, in a manner similar to the following:

public class MyCustomControl : {..some base class..}
{
   private Brush _foreGround;

   public Brush ForeGround
   {
      get { return _foreGround; }
      set { _foreGround = value; }
   }
}

Writing a dependency property for a WPF element is a bit different. If you had to author a dependency property to represent foreground color on a custom element, it would look like this:

public class MyCustomElement : Control
{
   public MyCustomElement() : base() { }
   public Brush ForeGround
   {
      get { return (Brush)this.GetValue(ForeGroundProperty); }
      set { this.SetValue(ForeGroundProperty, value); }
   }
   public static readonly DependencyProperty ForeGroundProperty =
      DependencyProperty.Register(
         "ForeGround", typeof(Brush), typeof(MyCustomElement));
}

As you can see, there are two main steps involved in creating a dependency property:

  1. Create a property called ForeGround, just as you would in a Windows or ASP.NET control.
  2. Call two curious methods, SetValue and GetValue, in which you pass in a public static readonly variable called ForeGroundProperty. This public variable is of type DependencyProperty, and the base class Control inherits from DependencyObject, which includes the methods SetValue and GetValue.

The Ins and Outs of Dependency Properties and Routed Events in WPF

If you follow these rules, you will create a dependency property that the WPF runtime will understand.

Suppose that the text you intended to show in various TextBlocks was so large that your window needed to scroll. You could use a ScrollViewer to scroll the various TextBlocks, kind of like this:

<Window ...
      Title="MyWPFApp" Height="189" Width="300"
      Name="myWindow"
      >
   <Window.Resources>
      <Style TargetType="{x:Type TextBlock}">
         <Setter Property="Margin" Value="10"/>
      </Style>
   </Window.Resources>
   <ScrollViewer Name="myScroll">
      <StackPanel Name="myStackPanel">
         <TextBlock TextWrapping="Wrap" Name="myTextBlock1">
            Sample Text outside the ItemsControl block.
            Some more text. Even more Text. A lot more text.
         </TextBlock>
         <ItemsControl Foreground="Red">
            <TextBlock TextWrapping="Wrap" Name="myTextBlock2">
               Sample Text without foreground.
               More Red text, even more red text.
               A lot more red text.
            </TextBlock>
            <TextBlock Foreground="Blue" TextWrapping="Wrap" 
                       Name="myTextBlock3">
               Sample Text with foreground. A terrific amount of
               blue text. A lot more amount of blue text. We need
               to make sure that this scrolls.
            </TextBlock>
         </ItemsControl>
      </StackPanel>
   </ScrollViewer>
</Window>

This XAML snippet ends up producing a window that looks like Figure 4.

[Ele04.jpg]

Figure 4. Using a ScrollViewer to scroll TextBlocks

Click the title bar where it says MyWPFApp to make sure that the window frame has the right focus. Next, press the "Down" arrow key on your keyboard. What do you notice? You probably found that the text doesn't scroll downwards.

Note: Due to a bug in .NET 3.0 extensions CTP for Visual Studio 2005, you may need to try this scrolling by directly running the EXE. Doing so in debug mode may result in an inexplicable exception.

Now, click on the thumb of the scroll bar, and then try pressing the down arrow key on your keyboard. The text still won't scroll. Not until you explicitly click on the text itself and then hit the down arrow key will the text finally scroll.

This problem is not unusual in, say, a NotePad.exe written with the Win32 API. However, it is accentuated in WPF. In the Win32 API, you could reasonably assume that the window you see representing NotePad.exe contains a bunch of sub-windows, and the huge multi-line textbox you type in is just another window. Each window receives messages, so when you create a WM_KEYDOWN message using the keyboard and press "A," the textbox responds by displaying "A" within its client area.

However, because the window has focus, what happens when you press CTRL_O to open a file? The notepad window needs to somehow intercept that message and show the File Open dialog. I don't have access to the source code for notepad.exe, but I assume that its top level is using a Win32 API method such as PeekMessage to hear messages before the child textbox does and, if appropriate, to act upon them before the TextBox does.

Routed Events Explained

This problem is to some extent accentuated in WPF as well, because the control tree tends to get a little bit more complex when the framework is almost infinitely flexible and lets you do crazy things such as throw a TextBox on a button as the button's content. Thus, the solution to this problem in WPF is routed events—events that traverse up or down the control hierarchy. Thus, if you press the down arrow key, every relevant control in the control hierarchy somehow is informed that a key was pressed—unless, of course, one of the links in this chain decides to break the communication.

In the example application, a rather incomplete control hierarchy would look a bit like Figure 5 (incomplete because you don't see some controls).

[Ele05.jpg]

Figure 5. Incomplete Control Hierarchy

To try and see who gets which events, modify the code of your Window as follows:

public Window1()
{
   InitializeComponent();

   myWindow.KeyDown     += new KeyEventHandler(GenericKeyDownHandler);
   myScroll.KeyDown     += new KeyEventHandler(GenericKeyDownHandler);
   myStackPanel.KeyDown += new KeyEventHandler(GenericKeyDownHandler);

   myWindow.PreviewKeyDown +=
       new KeyEventHandler(GenericKeyDownHandler);
   myScroll.PreviewKeyDown +=
       new KeyEventHandler(GenericKeyDownHandler);
   myStackPanel.PreviewKeyDown += 
      new KeyEventHandler(GenericKeyDownHandler);
}


void GenericKeyDownHandler(object sender, KeyEventArgs e)
{
   myTextBlock1.Text += 
      "\nSender: " + (sender as Control).Name + 
      "\t RoutedEvent:" + e.RoutedEvent.Name;
}

Now run the application, click the title bar to give the window focus, and press the down arrow key. You will see the following event sequence:

Sender: myWindow   RoutedEvent:PreviewKeyDown
Sender: myWindow   RoutedEvent:KeyDown

So, the myWindow gets the KeyDown message first and then eats the message so the underlying controls never get that message. Well, that certainly explains the mystery of the text not scrolling.

Now, click on the TextBlock itself, and press the down arrow key once again. You would see the following event sequence:

Sender: myWindow    RoutedEvent:PreviewKeyDown
Sender: myScroll   RoutedEvent:PreviewKeyDown

In this case, the window does get the PreviewKeyDown message first, but it sends it along to myScroll, which then dutifully acts on the message by scrolling the text. Thus, the event is being routed from top to bottom in the control hierarchy. In certain instances, you may want to bubble the event up the chain instead of tunneling it down the chain, or simply send the event directly.

You can specify this behavior on a custom element when you register your event. The following is a very simple implementation of a custom Pop event on a MyCustomElement:

public class MyCustomElement : UIElement
{
   public static readonly RoutedEvent PopEvent ;

   public event RoutedEventHandler Pop
   {
      add {AddHandler(PopEvent, value);} 
      remove{RemoveHandler(PopEvent, value);}
   }

   public static MyCustomElement() 
   {
      PopEvent = 
         EventManager.RegisterRoutedEvent(
            "Pop", RoutingStrategy.Bubble, 
            typeof(RoutedEventHandler), typeof(MyCustomElement)) ;

   }
}

The next question obviously is how can you fix your code so the text will indeed scroll with the window in focus and the down arrow key being pressed?

This example involves four major visual elements:

  • The window
  • The TextBlock
  • The Scrollviewer
  • The Stack Panel

If you observe the class hierarchy of these, it looks like Figure 6.

[Ele06.jpg]

Figure 6. The Class Hierarchy of the Four Major Visual Elements

As you can see, all of these end up inheriting from UIElement. If you run reflector and decompile the code for UIElement.OnKeyDown, you will find that it is implemented as a protected virtual void method that accepts a single parameter of type KeyEventArgs. Thus, controls further down in the hierarchy can choose to give an implementation to this method.

As it turns out, this event is simply ignored all the way down to the window. After all, why should a generic window with no scroll bars need to bother about the key down event? However, if you look into the implementation of OnKeyDown for ScrollViewer, you will note that ScrollViewer verifies whether the event has already been handled by checking the KeyEventArgs.Handled property. If the event isn't handled yet and the appropriate cursor key is pressed, it responds to the event by scrolling in the appropriate direction.

For the scrolling to work with the window in focus, the myWindow element will need to handle the OnKeyDown event and simply pass that event to the myScroll element. To ensure this happens, add the following code to Window1's code:

protected override void OnKeyDown(KeyEventArgs e)
{
   base.OnKeyDown(e);
   if (e.Key == Key.Down)
   {
      myScroll.RaiseEvent(e);
   }
}

The text will now scroll with just the window in focus.

Be very careful of connecting events in this manner, though. For instance, if I forgot to check for Key.Down, ALT_F4 would also be routed to the scroll viewer. Thus, pressing ALT_F4 on the window would not close the window properly. As a rule, I always try to call the base class's implementation for key handling, just to be safe.

About the Author

Sahil Malik (www.winsmarts.com) has worked for a number of top-notch clients in Microsoft technologies ranging from DOS to .NET. He is the author of Pro ADO.NET 2.0 and co-author of Pro ADO.NET with VB.NET 1.1. Sahil is currently also working on a multimedia series on ADO.NET 2.0 for Keystone Learning. For his community involvement, contributions, and speaking, he also has been awarded the Microsoft MVP award.



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

  • "Security" is the number one issue holding business leaders back from the cloud. But does the reality match the perception? Keeping data close to home, on premises, makes business and IT leaders feel inherently more secure. But the truth is, cloud solutions can offer companies real, tangible security advantages. Before you assume that on-site is the only way to keep data safe, it's worth taking a comprehensive approach to evaluating risks. Doing so can lead to big benefits.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds