The Avalon Control Content Model

 

Jeff Bogdan
Microsoft Corporation

July 7, 2004

Summary: Jeff Bogdan explores Avalon's control content model, a concept that can be nearly missed by the developer on first encounter, but one that provides extreme flexibility when meeting the demands of richer scenarios. (8 printed pages)

View and download the source code for this article.

There's an obvious property that UI developers expect to find on the controls they're using: Text. Win32 gave WM_GETTEXT and WM_SETTEXT messages for HWNDs, and most frameworks built on top of HWNDs exposed this functionality. Yet in Avalon, we intentionally left Text out of the Control API. This has raised enough questions to warrant an explanation into our motivations behind this design. Obviously, it goes much deeper than one property. In Avalon, we've used two principles to shape our control content model:

  1. The developer should experience a smooth progression when moving from simpler to richer content.
  2. Data is a first class citizen for control content.

In this article, we're going to dive into these two principles to give you a better understanding at how we arrived at this design.

Smooth Progression From Simple To Rich Control Content

Microsoft Windows developers are used to specifying text for the content of their control. For more advanced scenarios, Windows controls offered OwnerDraw as the only real way for showing something other than text. When it was time to render the control that was marked OwnerDraw, a WM_OWNERDRAW message was sent to the parent of the control, supplying the same info a control author would find in the WM_PAINT message were this the author's own control.

In the Windows 95 days, with the introduction of commctrl, Windows introduced new controls (like ListView and TreeView) that supported text and image content in their controls. These new controls supported OwnerDraw but also a more granular extensibility mechanism called CustomDraw. OwnerDraw was an all-or-nothing mechanism. If you want anything to show up in an OwnerDraw control, you had to draw it. CustomDraw allowed you to tweak aspects of the drawing (Foreground, Background, Font, and so on), but then still allow the control to do the rest of the rendering.

Certainly, there were extensibility mechanisms the Windows developer could use to get what they wanted, but the progression was not at all smooth. If you want a list that displays strings, then ListBox is fine. If you want a list that displays strings and images, then switch over to ListView. If you want to tweak the rendering properties, then use CustomDraw. And if you want a list that displays more than strings and images, or displays this content in a non-conventional way, dive into OwnerDraw. So, this progression involved switching types from ListBox to ListView, and then switching rendering from tweak to draw, with the final jump being a large one—you're given an empty rectangle and told to fill it up.

In Avalon, element composition is the central extensibility mechanism, and can be leveraged by the developer who before had to resort to OwnerDraw or CustomDraw for custom rendering. Let's take a look at how element composition can be used in place of OwnerDraw to draw a button that contains a "live" analog clock with the string "Set Time" next to it.

XAML:

<Button>
    <FlowPanel>
        <myLibrary:AnalogClock />
        <Text>Set Time</Text>
    </FlowPanel>
</Button>

C#:

Button button = new Button();
FlowPanel flow = new FlowPanel();
AnalogClock clock = new AnalogClock();
flow.Children.Add(clock);

Text text = new Text();
text.TextContent = "Set Time";
flow.Children.Add(text);

button.Content = flow;

The XAML version glossed over an important detail that the C# version shows: Button.Content. So, what happened to Button.Text? It's Button.Content, the center of Avalon's control content model. But what's the type of content? The Content property is of type Object. This seems odd, given that what we're showing here is adding elements for the content. Well, it's odd if you only consider these rich scenarios. Element composition addresses the "rich" end of the content spectrum, but at the "simple" end of the content spectrum, element composition would be considered overkill. So, for the simple scenario, you want to be able to set the Content to something simpler, like a String:

XAML:

<Button>Hello</Button>

C#:

Button button = new Button();
button.Content = "Hello";

There's Button.Content again, but this time it's being set to a String. So, in the absence of union types, our only way for Content to support both String and UIElements is to make it be of type Object. And while this may look like a big sacrifice to make (in terms of discoverability) for a property that wants only these two disparate types, there's far more justification for making Content of type Object. Read on ...

Data Is a First Class Citizen for Control Content

Like the guilty child caught red handed by mom and dad, when asked, "Why are you using Object for all of your control content?" Avalon responds, "Well Windows Forms started it." System.Windows.Forms.ListBox has an Items collection. This Items collection is not a collection of strings, which is how Win32 originally did things; rather, it's a collection of objects. We believe that this was a huge step in the right direction. Application developers could now be more productive by spending less time coercing their data into the form expected by the control. Avalon adopted this more deeply, such that all controls can support content of any type.

XAML:

<FlowPanel>
    <Button>
        <Employee Name="Chris" ID="698" Picture="ChrisFace.png" />
    </Button>
    
    <ListBox>
        <myData:Customer Name="Luke" Number="346" />
        <myData:Customer Name="Drew" Number="823" />
        <myData:Customer Name="Charu" Number="107" />
    </ListBox>
</FlowPanel>

C#:

FlowPanel flow = new FlowPanel();
Employee employee = new Employee();
employee.Name = "Chris";
employee.ID = "698";
employee.Salary = 54000;
employee.Picture = new ImageData("ChrisFace.png");
Button button = new Button();
button.Content = employee;
flow.Children.Add(button);

ListBox list = new ListBox();
Customer customer = new Customer();
customer.Name="Luke";
customer.Number=346;
list.Items.Add(customer);
customer.Name="Drew";
customer.Number=823;
list.Items.Add(customer);
customer.Name="Charu";
customer.Number=107;
list.Items.Add(customer);
flow.Children.Add(list);

Figure 1 show what this code looks like when it's run.

Figure 1. The Avalon control content model at work without any styles

Hmmm, not quite what we'd like to see... When the content was a string or a UIElement, it displayed as expected. But when we used arbitrary objects for content, all that gets presented for each object is a ToString of that Object. Windows Forms solved this problem using a simple but effective presentation model through the DisplayMember property on ListBox. If DisplayMember isn't specified, then you would do a ToString of the object, just as we've seen here. But if DisplayMember is specified, and if the item has a field by that name, then that field is displayed. In Avalon, we've moved away from DisplayMember in favor of a versatile mechanism: VisualTree.

VisualTree: the Element Composition Answer to Ownerdraw

Think of a VisualTree as a template. A VisualTree is a template describing a tree of elements that should be instantiated. VisualTree is a property on Style. I'm going to use Style, resources, and data binding here, but I'm not going to give you much detail on them, so I'd encourage you to go to the Longhorn SDK and read the good overviews of these three concepts. (Like the Styles and Data Binding coverage in the Longhorn SDK, and the recent article by Chris Sells on this topic.) Let's first look at a sample Style in XAML:

<?Mapping XmlNamespace="MyData" ClrNamespace="MyData" Assembly="MyData" ?>
<Window  
    xmlns="https://schemas.microsoft.com/2003/xaml"
    xmlns:def="Definition"
    xmlns:myData="MyData"
    def:Class="ContentModelSample.Window1"
    def:CodeBehind="Window1.xaml.cs" 
    Text="ContentModelSample"
    >
  <Window.Resources>
    <TransformerSource def:Name="SalaryToColorTransformer" 
             TypeName="ContentModelSample.SalarayToColorTransformer" />
    <Style def:Name="*typeof(myData:Employee)">
      <Style.VisualTree>
        <FlowPanel 
          Background=
           "*Bind(Path=Salary;Transformer={SalaryToColorTransformer})">
          <Image Source="*Bind(Path=Picture)" />
          <Text TextContent="*Bind(Path=Name)" />
        </FlowPanel>
      </Style.VisualTree>
    </Style>
    <Style def:Name="*typeof(myData:Customer)">
      <Style.VisualTree>
        <Text TextContent="*Bind(Path=Name)" />
      </Style.VisualTree>
    </Style>
  </Window.Resources>
  <FlowPanel>
    <Button>
      <myData:Employee Name="Chris" ID="698" Picture="ChrisFace.png" />
    </Button>
    <ListBox>
      <myData:Customer Name="Luke" Number="346" />
      <myData:Customer Name="Drew" Number="823" />
      <myData:Customer Name="Charu" Number="107" />
    </ListBox>
  </FlowPanel>
</Window>

The above XAML defines a ResourceDictionary for the window, which contains two Style resources. The first Style resource has the name "*typeof(Employee)" and the second has the name "*typeof(Customer)". When ListBox and Button want to render their content, they look up their parent chain, and check the resources defined on each parent, searching for a resource whose name matches the type of the content. So, in this case, Button would search for the "*typeof(Employee)" resource in its own Resources, then in its parent FlowPanel's resources, and then in FlowPanel's parent Window's resources. It will find a matching resource in Window's resources, at which point it will instantiate an element tree as described by that Style. This means that a FlowPanel with an image and a text in it would be created and attached to the button. This new element tree would refer back to the content of the button through bind expressions. This is essentially how the VisualTree supports parameterization.

I made this first style intentionally rich to give you an example of how far you can go. The first bind that you see, to FlowPanel's Background, will use a custom IDataTransformer to convert from Double to Brush (let's say Green for < 50,000, Yellow for < 100,000, and Red for all other values). Then, the two children of FlowPanel consume the Picture and Name properties from the Employee.

ListBox, following the same process as Button, will find and apply the much simpler "*typeof(Customer)" Style, resulting in the presentation shown in Figure 2.

Figure 2. The Avalon control content model at work with styles for Customer and Employee

Plural Content

Thus far, I've focused on Button and its Content property. This property (and its associated ContentStyle property) come from Button's base type of ContentControl. ContentControl is the base type for all controls that have singular content. Though Button's content can be a rich and deep tree, it's conceptually a single object. For those controls that have plural content (such as the ListBox in the above example), there is a different base type that Avalon provides: ItemsControl. ItemsControl has an Items property that is of type ItemsCollection (which is basically a generic object collection with a little extra smarts under the hood). Similar to ContentStyle, ItemsControl also provides an ItemStyle property. You can look at earlier XAML and C# samples to see how you would define an instance of a ListBox (ListBox derives from ItemsControl).

ContentControl and ItemsControl represent the two basic variations of the control content model: singular and plural content. But these singular and plural patterns can be combined in any way to create more advanced variations of this model. Here are the five variations that we've considered common enough to create base types for them:

  • Control. This is the base type for controls that have no content (for instance, ScrollBar).
  • ContentControl. This is the base type for controls that have singular content (for instance, Button). ContentControl exposes a Content property of type Object.
  • ItemsControl. This is the base type for controls that have plural content (for instance, ListBox). ItemsControl exposes an Items property of type ItemCollection.
  • HeaderedContentControl. This is the base type for controls that have two content spaces, each one singular (for instance, TabItem with the singular content for the tab header and the singular content for the tab body). HeaderedContentControl extends from ContentControl and exposes a Header property of type Object.
  • HeaderedItemsControl. This is the base type for controls that have two content spaces, one singular and one plural (for instance, MenuItem with the singular content for the item itself as well as the plural content for the submenu of that item). HeaderedItemsControl extends from ItemsControl and exposes a Header property of type Object.

So Why Do Panels have Children instead of Items?

Looking at the code examples above, you may have noticed that Panel, though it has plural content, doesn't have an Items property. Rather, it has a Children property. This was deliberate. Decorators and panels are what we call "direct elements." We use the term "direct" because these elements do not support having a Style.VisualTree applied to them. Instead, these elements focus on direct hosting of elements, not generic content. I sometimes call direct elements the "Knowing When To Stop" elements.

Avalon's styling model is powerful, but it is most beneficial when applied to controls, because it allows a control author to focus more on the behavior of the control and less on the appearance of the control. In the cases of decorators and panels, whose behavior is all appearance, there is less gained from the abstraction provided by the control content model.

Homework

Hopefully this gave you some good insight into how we arrived at Avalon's control content model. Now that we've talked about the concept of arbitrary content, it's time to observe how it's put to use in Avalon's class hierarchy. Explore the class hierarchy and see which controls derive from which of the preceding base types (HeaderedItemsControl, and so on). Play around with these controls and try putting arbitrary data in for content. Then try constructing some styles to make that arbitrary data more presentable. Then, let us know the good (so we know what we got right) as well as the bad (so we know what we should focus our energies on polishing up). This is a work in progress, and your input is vital to this product.

 

Inside Avalon

Jeff Bogdan joined Microsoft in 1991 as a developer in Windows. Today he is an architect on the Windows Client Platform team working on Avalon. He is responsible for the design and developer experience of the presentation components in Windows. When he's not playing with computers, he's playing with his two sons, relaxing with his wife, or inline skating as fast as his legs can propel him.