Properties, Commands, and Events

 

Chris Anderson
Microsoft Corporation

December 8, 2004

Summary: Chris Anderson shows you how to build an expand/collapse control using the properties, commands, and events available in the latest "Avalon" release. (18 printed pages)

Download the ControlBlocks source.msi file.

Much Has Changed

Ah, it has been a while since we last talked. In that time, there have been some big announcements regarding "Longhorn," WinFX, and "Avalon." I'm excited by these changes, mostly because the ability to have Avalon run on more versions of Windows lets developers take advantage of this new functionality sooner. I'm also happy to say that this article targets the just released Community Technical Preview of Avalon, which means that we will use the latest and greatest bits.

In the last article, Jeff talked about the Control Content Model. Today, I'd like to talk about more core concepts of the Avalon control model. While properties and events are probably familiar to most of you, there are powerful new services in Avalon that you will want to understand. Commands, which might be a new concept for many of you, provide the ability for a control to publish its actions, which other components can then bind. Combining the content model, with properties, events, and commands, you can create controls that are extremely flexible and require little or no code to author or customize.

As with any article, we need a problem to solve to demonstrate what we are doing. Let's try to build an expand/collapse control, similar to what you see in the left pane of a Windows XP Explorer window, as shown in Figure 1.

Figure 1. Expand/collapse control in Windows XP Explorer

There are three expanders here:

  • File and Folder Tasks (expanded)
  • Other Places (collapsed)
  • Details (expanded)

Instead of creating a control that can only look like this Windows XP-themed expander, we want to define a flexible control that can participate in the Avalon styling mechanism, and enables application developers to customize the look and behavior of the control.

Based upon Jeff's last article, we know that we can leverage some of the base classes in Avalon to automatically provide our content model. In this case, it looks like each expander has a header (red circle) and some content (green circle), as shown in Figure 2.

Figure 2. Header and content breakdown for expanders

In Avalon, there is a HeaderedContentControl that provides the right content model for this process. Eventually, we will need to add additional data, methods, and events, but for now we will define our control like this:

namespace InsideAvalon
{
    using System;
    using System.Windows;
    using System.Windows.Controls;
    public class Expander : HeaderedContentControl
    {
    }
}

In addition, we need to create a basic test harness for the control, so let's use some XAML:

<Window 
  xmlns="http://schemas.microsoft.com/2003/xaml"
  xmlns:x="Definition"
  xmlns:l="local"
  x:Class="InsideAvalon.ExpanderTest"
  Background="Beige"
  Text="Inside Avalon"
  >
</Window>

We will have a code-behind file for the Window, and define an application like so:

namespace InsideAvalon
{
    using System;
    using System.Windows;
    using System.Windows.Controls;
    public partial class ExpanderTest : Window
    {
    }
    #region Application
    class App : Application
    {
        [STAThread]
        static void Main()
        {
            new App().Run();
        }
        protected override void
            OnStartingUp(StartingUpCancelEventArgs e)
        {
            new ExpanderTest().Show();
        }
    }
    #endregion
}

The last step will be a simple MSBuild project file to compile the whole thing:

<Project 
  DefaultTargets="Build"
  xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <Configuration>Debug</Configuration>
    <TargetType>winexe</TargetType>
    <AssemblyName>expander</AssemblyName>
  </PropertyGroup>
  <Import Project="$(MSBuildBinPath)\Microsoft.CSHARP.targets" />
  <Import Project="$(MSBuildBinPath)\Microsoft.WinFX.targets" />
  <ItemGroup>
    <Compile Include="*.cs" />
    <Page Include="*.xaml" />
  </ItemGroup>
</Project>

These files can all be found in the source code for this article in a directory named Step1. Now that we have the housekeeping out of the way, we can get down to the good stuff—building the core of our control.

Properties

The first part of building a control should be thinking about the data associated with the control. In the case of the expander, we need a Boolean property to track if the panel is expanded. The additional state of the header and content is already managed for you by the base class.

You might be tempted to start building your control by declaring a simple CLR property. After all, this is how it works today:

public class Expander : HeaderedContentControl
{
    bool _isExpanded;
    public bool IsExpanded
    { 
        get { return _isExpanded; }
        set { _isExpanded = value; }
    }
}

This code will happily compile, and you can access the IsExpanded property from markup. However, if you remember from my previous article, properties in Avalon are expected to offer a wide variety of services—data binding, animation, styling, and so on. Imagine all the code you would need to write to support this, firing change notifications, querying for the state of the animation engine, looking up resources, and styled values. For example, here is some hypothetical code (not the real way styles work) that could be the needed code for getting a value from a style:

public class Expander : HeaderedContentControl
{
    bool _isExpanded;
    bool _isExpandedSet;
    public bool IsExpanded
    { 
        get 
        { 
            if (!_isExpandedSet)
            {
                Style style = LoadStyle();
                if (style.IsPropertySet("IsExpanded"))
                {
                    return (bool)style.GetProperty("IsExpanded")
                }
            }
            return _isExpanded; 
        }
        set 
        { 
            _isExpandedSet = true;
            if (_isExpanded != value)
            {
                _isExpanded = value;
                OnPropertyChanged("IsExpanded");
            }
        }
    }
}

We still haven't added support for animations, so let's do that now:

public class Expander : HeaderedContentControl
{
    IList<BooleanAnimations> _isExpandedAnimations;
    bool _isExpanded;
    bool _isExpandedSet;
    public bool IsExpanded
    { 
        get 
        { 
            if (_isExpandedAnimations != null &&
                   _isExpandedAnimations.Count > 0)
            {
              // lots of code for calculating a current value!
              // ... go find the active timeline
              // ... calculate the value at the current time
              // ... oh, and do this even in the case the value
              //     comes from the style!
            }
            if (!_isExpandedSet)
            {
                Style style = LoadStyle();
                if (style.IsPropertySet("IsExpanded"))
                {
                    return (bool)style.GetProperty("IsExpanded")
                }
            }
            return _isExpanded; 
        }
        set 
        { 
            _isExpandedSet = true;
            if (_isExpanded != value)
            {
                _isExpanded = value;
                OnPropertyChanged("IsExpanded");
            }
        }
    }
}

While animating a Boolean property seems odd, there are lots of other properties (like width, height, colors, and so forth) that you want to support animation. This code will continue to grow as you add services like data binding, and you will have to clone this code for every property you define. In addition, you may notice that the amount of storage on the object for a single Boolean property is growing rapidly.

To avoid having to write all this code for every property you define, as well as saving memory, there is a common set of code called the dependency property system, which enables you to easily define properties that support all of this code.

The property system is called a dependency property system because one of the major services that it offers is the tracking of dependencies between data and the property. For example, the property system knows that there is a dependency between the style and all the objects that are affected by that style. This enables the property system to invalidate all the needed objects when the style (or property that affects the style) changes.

To save space, the property system can store all the additional data for a property in a sparse store that is associated with any object deriving from DependencyObject. By having this sparse store, there is no additional overhead to declaring a property for an object, unless that property is set to some value other than the default. There are over 40 properties that affect the rendering of text in Avalon. If we didn't have this sparse storage, the overhead per element would be far too large. Most elements get their text properties set either as the default, inherited, or from a style, all of which introduce no additional per-instance cost:

public class Expander : HeaderedContentControl
{
    public static DependencyProperty IsExpandedProperty;
    static Expander()
    {
        IsExpandedProperty = DependencyProperty.Register(
            "IsExpanded", 
            typeof(bool), 
            typeof(Expander));
    }
    public bool IsExpanded
    { 
        get { return (bool)GetValueBase(IsExpandedProperty); }
        set { SetValueBase(IsExpandedProperty, value); }
    }
}

By declaring the DependecyProperty and using GetValueBase and SetValueBase as the implementation of the property, the dependency property system can offer all the common services I mentioned above. By default, this new property can be styled, data bound, and provide change notifications (later in the article) with minimal coding on your part.

To try out some of these services, we can use our new control inside of our test page. The first step is to import the CLR namespace into the XAML. To do this, we add a Mapping PI at the top of the XAML and an xmlns definition to the root tag:

<?Mapping XmlNamespace="local" ClrNamespace="InsideAvalon" ?>
<Window 
  xmlns="http://schemas.microsoft.com/2003/xaml"
  xmlns:x="Definition"
  xmlns:l="local"
  x:Class="InsideAvalon.ExpanderTest"
  Background="Beige"
  Text="Inside Avalon"
  >
...

I used the namespace local to refer to the namespace because these types are being defined in the build project, which is also the reason that I don't include the AssemblyName attribute on the mapping PI. The markup compilation process will lazily evaluate the tags to enable a project to have XAML refer to elements that haven't yet been compiled.

To try out our control, we need to add some markup that uses it:

<?Mapping XmlNamespace="local" ClrNamespace="InsideAvalon" ?>
<Window ... >
  <DockPanel>
    <l:Expander 
      DockPanel.Dock="Top"
      Header="Red Expander"
      IsExpanded="true">
      <Rectangle Height="75" Fill="Red" />
    </l:Expander>
    <l:Expander 
      DockPanel.Dock="Top" 
      Header="Blue Expander" 
      IsExpanded="false">
      <Rectangle Height="75" Fill="Blue" />
    </l:Expander>
  </DockPanel>
</Window>

If you run the application at this point, you won't see anything very exciting. The display will actually be a blank window. Why? Because we haven't defined any look for our control. At this point, there are at least two options for defining the display of the control. You could override the OnRender method on the control and use drawing commands to paint the display. Alternatively, you can leverage the styling system of Avalon and enable application developers to define the look of your control using markup. Because we want to create the most flexible control possible and empower developers to create interesting applications, we are going to use the second option.

If you were creating a reusable control, you would need to define a default style and compile it into the project. However, for the sake of simplicity we will define a local style:

<?Mapping XmlNamespace="local" ClrNamespace="InsideAvalon" ?>
<Window ...>
  <Window.Resources>
     <Style>
      <l:Expander />
      <Style.VisualTree>
        <Grid>
          <RowDefinition Height="Auto" />
          <RowDefinition Height="*" />
          <ContentPresenter 
            Content="*Alias(Target=Header)" 
            ContentStyle="*Alias(Target=HeaderStyle)" 
       ContentStyleSelector="*Alias(Target=HeaderStyleSelector)" 
            />
          <ContentPresenter 
             x:StyleID="expanderContent" 
             Grid.Row="1" />
        </Grid>
      </Style.VisualTree>
      <Style.VisualTriggers>
        <PropertyTrigger Property="IsExpanded" Value="False">
          <Set 
            Target="expanderContent" 
            PropertyPath="Visibility" 
            Value="Collapsed" />
        </PropertyTrigger>
      </Style.VisualTriggers>
    </Style>
  </Window.Resources>
  ...
</Window>

This looks foreboding, but when you break it down it becomes fairly simple. A style is a generic way of applying properties to a set of elements. The first tag under the style defines the target of the style. In this case, the expander control we just defined. If we wanted to apply additional properties to all the expanders (like background), you could include those on this tag.

The VisualTree section is a definition of the visual appearance of any control (Jeff talked about this in the last installment). In the example above, I define a Grid to contain two elements, and both are ContentPresenters. The first content presenter is bound to the header family of properties, the second content presenter is bound to the content family of properties (this is the default behavior for a content presenter).

The VisualTriggers section is a list of actions you want performed based upon some criteria. In this case, we use the PropertyTrigger element to specify when the IsExpanded property is set to false, we want the second content presenter to be collapsed. The StyleID property is a way of referring to a named element within a VisualTree.

When we run, we get the display shown in Figure 3.

Figure 3. Display of Step 2 as labeled in the sample code

The red expander had the IsExpanded property set to true, so its rectangle is displayed. The blue expander was not expanded, so its rectangle is collapsed. What we have now is the basic functionality of an expander control. The content can be displayed or hidden based upon the state of the IsExpanded property. Next step, allowing the user to expand and collapse the control.

Commands

Our control is fairly basic at this point. While you can programmatically expand and collapse the control, we don't have any default way of controlling this action.

Note If you want to expand and collapse the control at this point, you can add a button toggles the property. This is labeled as Step 2.5 in the sample code.

To make the programming simple, we can add a method to toggle the expansion of the control:

public class Expander : HeaderedContentControl
{
    ...
    public void ToggleExpanded()
    {
        IsExpanded = !IsExpanded;
    }
}

What we would like to do now is add a button to the default style for the expander that would call the ToggleExpanded method when the user clicks on it. We saw previously that you can use property triggers (a form of data binding) to wire up properties and actions in a style. Now we need to wire up the action of a control in a style (clicking the button) to an action on the styled control (calling the ToggleExpanded method).

One option would be to add code to Button to call the ToggleExpanded method in its Click event handler. This isn't a declarative option and it means that just to describe the look of the expander we need to write some code. In addition, we would need to create a new Button class that implemented the custom logic for Click to call the specific ToggleExpanded method.

Instead, we will use a Command. Commands provide an abstraction to performing an action, which enables this declarative binding of one control's action to another. Commands provide a way to bind to a method, and get additional metadata about the method (for example, you can determine if the command should be enabled).

To start, we need to define the command:

public class Expander : HeaderedContentControl
{
    public static Command ToggleExpandedCommand;
    public static DependencyProperty IsExpandedProperty;

    static Expander()
    {
        ToggleExpandedCommand = new Command(
            "ToggleExpanded",
            typeof(Expander), 
            null);
        IsExpandedProperty = DependencyProperty.Register(
            "IsExpanded", 
            typeof(bool), 
            typeof(Expander));
    }
    ...
}

Now that we have defined the command, we need to have the expander do something in response to that command. Remember, the command is an abstraction over the action that you want to perform. It provides a mechanism for some other control to basically get across the point that we want to do something to and it's called ToggleExpandedCommand. Now we need to decide how we want the expander to interpret the command.

using System.Windows.Commands;
public class Expander : HeaderedContentControl
{
    ...
    public Expander()
    {
        CommandBinding cb = 
          new CommandBinding(ToggleExpandedCommand);
        cb.Execute += 
          new ExecuteEventHandler(ToggleExpandedExecute);
        CommandBindings.Add(cb);
    }
    void ToggleExpandedExecute(object sender, ExecuteEventArgs e)
    {
        ToggleExpanded();
    }
    ...
}

To test this, we can modify the VisualTree of the style to include a button that binds to the command we just created.

<?Mapping XmlNamespace="local" ClrNamespace="InsideAvalon" ?>
<Window ...>
  <Window.Resources>
     <Style>
      <l:Expander />
      <Style.VisualTree>
        <Grid>
          <RowDefinition Height="Auto" />
          <RowDefinition Height="*" />
          
          <Grid>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
            
            <ContentPresenter 
              Content="*Alias(Target=Header)" 
              ContentStyle="*Alias(Target=HeaderStyle)" 
        ContentStyleSelector="*Alias(Target=HeaderStyleSelector)" 
              />
            <Button 
              Grid.Column="1"
              Command="l:Expander.ToggleExpandedCommand"
              Content="*" />
          </Grid>
  ...
</Window>

Running the code now gets the fully-functional, but limited expander control shown in Figure 4.

Figure 4. Fully-functioning expander control (Step 3 in the sample code)

In the same way that providing a clear data model using dynamic properties in step 2 of this article enabled us to use styles to define the look of the control, commands provide a way to enable us to use styles to define the interaction model of the control. Any control that supports binding to a command can be used to toggle the command state. You could use a hyperlink, button, or custom control that you define.

Events

We have a fairly functional control, and the last thing we need to do is add the ability for a developer to listen to some events so they can do cool stuff with the control. In this case, there is only interesting thing to listen to is the control expanding or collapsing.

Today you define events using a simple CLR event. This works great for many events (and in fact, would work fine for the event we are defining here), but there are often events that need more functionality. In the Avalon control composition-based world, you often end up with many controls that offer up the same event, and you only want to listen to that event in one place. You could, for example, add an event handler to the window in our sample application, and you would get notified when any button on that window is clicked:

<?Mapping XmlNamespace="local" ClrNamespace="InsideAvalon" ?>
<Window 
  xmlns="http://schemas.microsoft.com/2003/xaml"
  xmlns:x="Definition"
  xmlns:l="local"
  x:Class="InsideAvalon.ExpanderTest"
  Background="Beige"
  Text="Inside Avalon"
  Button.Click="AnyButtonClicked"
  >
  ...
</Window>

public partial class ExpanderTest : Window
{
    void AnyButtonClicked(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Clicked!");
    }
}

This is an odd example, but you get the picture. To define a new routed event, the name for an event that participates in the Avalon event system, you follow a similar pattern to properties.

public class Expander : HeaderedContentControl
{
    public static RoutedEventID ExpandingEvent;
    public static Command ToggleExpandedCommand;
    public static DependencyProperty IsExpandedProperty;
    static Expander()
    {
        ExpandingEvent = EventManager.RegisterRoutedEventID(
            "Expanding", 
            RoutingStrategy.Direct, 
            typeof(RoutedEventHandler), 
            typeof(Expander));
        ToggleExpandedCommand = new Command(
            "ToggleExpanded",
            typeof(Expander), 
            null);
        IsExpandedProperty = DependencyProperty.Register(
            "IsExpanded", 
            typeof(bool), 
            typeof(Expander));
    }
    ...
}

The RoutingStrategy determines how the event will be routed around the element tree. Direct events only fire on the originating element (in this case, the expander), while Bubble events travel up the element tree (like the click example above), and Tunnel events travel down the element tree (any of the preview events like PreviewKeyDown tunnel).

To raise the event at the right time, we need to determine when the property has been changed. For simplicity sake, we will raise the expanding event whenever the property has changed to true.

public class Expander : HeaderedContentControl
{
    ...
    protected override void OnPropertyInvalidated(
        DependencyProperty dp, 
        PropertyMetadata metadata)
    {
        if (dp == IsExpandedProperty)
        {
            if (IsExpanded)
            {
                RoutedEventArgs e = new RoutedEventArgs();
                e.SetRoutedEventID(ExpandingEvent);
                this.RaiseEvent(e);
            }
        }
        base.OnPropertyInvalidated(dp, metadata);
    }
    ...
}

In reality, this code fires when the value has changed because we are raising the event anytime we get an invalidated notification. Invalidation may occur for a number of reasons, and doesn't always mean that the property value has changed. An example of this would be when a style is changed for an element. The old and new values might be identical, but because the source of the property has been changed, the property system will flag the property as being invalidated. If you want to ensure that the event is only raised when the value actually changes, you will need to cache the old and new value and compare them inside of the invalidated callback.

Due to a bug in the CTP build of Avalon, you can't use an event in markup when it is being compiled in that same project (remember why we used the local namespace at the start of the article), as such, we will need to bind the event in code.

...
<DockPanel>
  <l:Expander
    ID="_redExpander" 
    DockPanel.Dock="Top" 
    Header="Red Expander" 
    IsExpanded="true">
    <Rectangle Height="75" Fill="Red" />
  </l:Expander>
  <l:Expander
    ID="_blueExpander" 
    DockPanel.Dock="Top" 
    Header="Blue Expander" 
    IsExpanded="false">
    <Rectangle Height="75" Fill="Blue" />
  </l:Expander>
</DockPanel>
...

public partial class ExpanderTest : Window
{
    protected override void OnLoading(EventArgs args)
    {
        _redExpander.Expanding += 
            new RoutedEventHandler(ExpanderExpanding);
        _blueExpander.Expanding += 
            new RoutedEventHandler(ExpanderExpanding);
        base.OnLoading(args);
    }


    void ExpanderExpanding(object sender, RoutedEventArgs e)
    {
        ((Expander)e.OriginalSource).Header = 
                     "Expanded at " + DateTime.Now;
    }
}

Running the code and expanding and collapsing the panels results in the display shown in Figure 5.

Figure 5. Display with expanding and collapsing panels (Step 4 in the sample code)

Once More, with Passion

After seeing the nuts and bolts of how it was put together, now we can appreciate a more complex project. By defining a more aesthetic style, including a tree view-like style, we can create a small file/folder explorer using nothing more than the expander control that we have written. This is shown in Figure 6 below.

Figure 6. A more complex and visual sample (Step5 in the sample code)

Summary

When building a custom control in Avalon, you will generally go down these steps if you want to create a flexible control that participates in styling.

  1. Decide the content model for the control (we chose the HeaderedContentControl to implement our header and content model).
  2. Build the data model for the control (we added a Boolean property to track the expanded state).
  3. Build the interaction model for the control (we defined the one command to toggle the expanded state).
  4. Build any needed programming model (we defined the expanding event to enable dynamic population of the control).
  5. Build a great look for the control using styling and visual trees.

In this article we've seen how properties, commands, and events allow us to define the shape of a control. Using these three concepts, combined with the control content model, you can build controls that are completely abstracted from the visualization. Using command binding, data binding, and attaching events you can create completely new visualization of controls using only markup.

Chris Anderson joined Microsoft in 1997 as a developer in Microsoft Visual Basic. Today he is an architect on the Windows Client Platform team working on Avalon. He is responsible for the design, developer experience, and architecture of the presentation components in Windows. Lately he is working on perfecting his rollerblading and Halo skills. Chris' wife puts up with his addictions, including digital photography, blogging, video games, and home theaters.