dcsimg
December 7, 2016
Hot Topics:

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

  • March 16, 2007
  • By Sahil Malik
  • Send Email »
  • More Articles »

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.

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).

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.



Click here for a larger image.

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.





Page 2 of 2



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date
Rocket Fuel