Click here to Skip to main content
15,886,137 members
Articles / Desktop Programming / WPF
Article

Template-aware control authoring example: HexViewer

Rate me:
Please Sign up or sign in to vote.
4.79/5 (11 votes)
25 Jun 20077 min read 44K   677   29   1
Control smart behaviour sample in case the control template does not strictly follow the control contract

Screenshot - HexViewerControl.gif

Introduction

HexViewer control is designed for viewing any collection of bytes in the customary hexadecimal form: each line of text displays 16 bytes and has an address pane at the left, hexadecimal pane in the middle and text pane at the right. It's just the viewer – it hasn't any editing or even selecting/copying capabilities.

Although simple, HexViewer is the full-fledged WPF control supporting templates and themes. It relies on template (customer provided or default theme one) to plug-in its core functionality into the template element tree and uses named template part (sort of PART_) to fulfil this task, like many built-in WPF controls do.

The primary motivation that pushed me to place this article at the CodeProject is to give one answer to the question: what should happen if the control template misses required named element or it has an inappropriate type? Other reasons: this control can be useful (1) by itself and (2) as the sample on elements composition and simple element tree manipulations. In my opinion, neither printed WPF books nor Internet are yet overcrowded with these sort of samples.

Background

WPF controls and control templating is the great approach allowing us completely change control look and feel without any need to touch control internals. There are some drawbacks, however.

Some built-in WPF controls (i.e. provided by Microsoft) rely on templates to plug-in their core functionality into a template-provided element tree. If the template isn't constructed properly from templated control point of view, then an attempt to bring in that core functionality fails and the control becomes unusable.

Let's take a close look at the TextBox control which seems to be the closest to HexViewer among other built-in controls. TextBox control is derived from TextBoxBase. The latter is marked with TemplatePartAttribute(Name="PART_ContentHost", Type=typeof(FrameworkElement)), i.e. TextBox control expects to find an element named as PART_ContentHost in its template element tree. This element type can't be any type derived from FrameworkElement, however: MSDN topics on TextBox define more exactly that it should be either ScrollViewer or AdornerDecorator (there is a little contradiction in MSDN documentation: in one place it's stated that Content Host may be of any type derived from Decorator, not just the AdornerDecorator; experiments show that this statement is right: e.g. we can use Border in place of AdornerDecorator). That's how TextBox default template under Windows Vista Aero theme (I got it with the help of Charles Petzold DumpControlTemplate tool from his Applications = Code + Markup book) looks like:

XML
<ControlTemplate TargetType="TextBoxBase
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:mwt="clr-namespace:Microsoft.Windows.Themes;assembly=
                        PresentationFramework.Aero"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:s="clr-namespace:System;assembly=mscorlib">

    <mwt:ListBoxChrome . . . >
         <ScrollViewer Name="PART_ContentHost" . . . />
    </mwt:ListBoxChrome>
    <ControlTemplate.Triggers>
        ...
    </ControlTemplate.Triggers>
</ControlTemplate>
(details not pertaining to our discussion are stripped out)

And that's TextBox default Aero visual tree:

plain
:TextBox
  Bd:ListBoxChrome
    PART_ContentHost:ScrollViewer
      :Grid
        :Rectangle
        :ScrollContentPresenter
          :TextBoxView
            :DrawingVisual
          :AdornerLayer
        :ScrollBar
        :ScrollBar

If we replace ScrollViewer in the template above with AdornerDecorator we'll get the visual tree:

plain
:TextBox
  Bd:ListBoxChrome
    PART_ContentHost:AdornerDecorator
      :TextBoxView
        :DrawingVisual
      :AdornerLayer

Notice the TextBoxView element which appears under PART_ContentHost element in both visual trees above. It's derived form FrameworkElement, isn't publicly accessible and isn't documented. If we remove Name="PART_ContentHost" attribute from the template or just change its value to something else than "PART_ContentHost", TextBoxView disappears from the Visual Tree and all TextBox text displaying/editing functionality goes out (no text, no caret, no text input …). It seems to be legal to suppose that this TextBoxView element plays an important role in TextBox implementation and that TextBox control uses its template PART_ContentHost named part to plug in TextBoxView into the Visual Tree to content host provided.

One more thing: as soon as we replace Content Host with something other than ScrollViewer or a derivative from Decorator then the TextBox creation process will fail with NotSupportedException raised by TextBoxBase.SetRenderScopeToContentHost() method with the message like "Only Decorator or ScrollViewer elements can be used as PART_ContentHost" (it's the back translation from Russian so the original message can differ).

Well, Microsoft WPF control design guidelines are expounded in the Guidelines for Designing Stylable Controls document. That's an excerpt from it:

1. Do not strictly enforce template contracts. The template contract of a control might consist of elements, commands, bindings, triggers or even property settings that are required or expected for a control to function properly.
· ...
· Do not throw exceptions when any aspect of a template contract is not followed. …

Is there some inconsistency between Microsoft intentions and the practice?

To me as a control author it seems reasonable to provide some visual cue in case the control template supplied doesn't match the control requirements and strips out its core functionality. This visual cue should be a part of the control and shows up above the template provided visuals. That's how HexViewer control is designed.

Solution

HexViewer consists of two parts

  1. HexView is derived from FrameworkElement, is internal and is rather trivial. It's responsible for data formatting into text lines and for their rendering.
  2. HexViewer is public and is responsible for plugging internal HexView part into a template provided by control customer (or into a default theme template). This goal is controlled by the use of template PART_ContentHost named part, the same way as TextBox does.

HexViewer allows for the same Content Host element types, as TextBox does: Content Host could be either of the ScrollViewer type (the default) or could be any derivative from Decorator type (like AdornerDecorator, Border, etc.)

First, HexViewer has to implement this behaviour is to override OnApplyTemplate:

C#
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    object contentHost = Template.FindName("PART_ContentHost", this);
    if (contentHost != null)
    {
        if (contentHost.GetType() == typeof(ScrollViewer))
        {
            m_hv = new HexView();
            (contentHost as ScrollViewer).Content = m_hv;
        }
        else if (contentHost is Decorator)
        {
            m_hv = new HexView();
            (contentHost as Decorator).Child = m_hv;
        }
    }

    if (m_hv != null)
    {
        // We have to connect HexView PaddingProperty to its 
        // HexViewer counterpart
        // because it isn't inheritable.
        Binding paddingBinding = new Binding("Padding");
        paddingBinding.Source = this;
        m_hv.SetBinding(HexView.PaddingProperty, paddingBinding);
    }
    else
        ApplyUglyTemplateCue();
}
(Note: m_hv private field is of HexView type)

In the first part of this code, after the template is created and applied, HexViewer tries to find PART_ContentHost in it. If find succeeds and if the contentHost is the ScrollViewer or a derivative from Decorator then new HexView object is created and added to the contentHost as its content. Otherwise HexView object isn't created and HexViewer control doesn't show up its core functionality. This short code implements the same templating behavior as TextBox has except it doesn't throw an Exception when contentHost has an inappropriate type.

The second block of the code, if HexView object isn't created, calls ApplyUglyTemplateCue() function (and this behavior is different from that of TextBox).

C#
private void ApplyUglyTemplateCue()
{
    // Try to Get Ugly Template Cue from resource
    try
    {// Intercept ResourceReferenceKeyNotFoundException
        m_UglyTemplateCue = FindResource("uglyTemplateCue") as UIElement;
    }
    catch
    {
    }

    // Create default Ugly Template Cue object
    if (m_UglyTemplateCue == null)
    {
        Border cue = new Border();
        cue.Margin = new Thickness(5);
        cue.VerticalAlignment = VerticalAlignment.Top;
        cue.BorderBrush = Brushes.Red;
        cue.BorderThickness = new Thickness(2);
        cue.Background = SystemColors.WindowBrush;
        cue.Opacity = 0.5;
        cue.Padding = new Thickness(2);
        TextBlock txt = new TextBlock();
        txt.Foreground = Brushes.Red;
        txt.Text = "Ugly Template warning: template provided hides 
            HexViewer control (missed PART_ContentHost 
            element of type ScrollViewer or AdornerDecorator)";
        txt.TextWrapping = TextWrapping.Wrap;
        cue.Child = txt;
        m_UglyTemplateCue = cue;
    }

    AddVisualChild(m_UglyTemplateCue);
    AddLogicalChild(m_UglyTemplateCue);
}
(Note: m_UglyTemplateCue private field is of UIElement type) 

ApplyUglyTemplateCue() function creates a visual Ugly Template Cue element used to notify the HexViewer user that the control template is inappropriate and stores it in a private field. To make this HexViewer aspect stylable HexViewer has to allow the customer to provide its own Ugly Template Cue element. I decided to use a resource for that end: a customer can define any visual element (derived from UIElement) somewhere in an application resource tree and tag it with "uglyTemplateCue" key (don't forget to apply x:Shared="True" attribute to it if it could happen to be used by multiple HexViewer instances). If "uglyTemplateCue" resource isn't found then ApplyUglyTemplateCue() function creates default Ugly Template Cue element. Then the element is registered with Visual and Logical Trees.

Now we have to plug Ugly Template Cue element (if there is one) into HexViewer visual tree. To do that we have to override VisualChildrenCount property and GetVisualChild, MeasureOverride and ArrangeOverride methods (I would like to emphasize: we shouldn't override these methods if we wouldn't implement Ugly Template Cue notification). One thing to remember: don't omit call to base versions of these methods!

C#
protected override int VisualChildrenCount
{
    get
    {
        return base.VisualChildrenCount + (m_UglyTemplateCue == null ? 0 : 1);
    }
}

Notice that base.VisualChildrenCount will return 0 before the template is applied and 1 after that.

C#
protected override Visual GetVisualChild(int index)
{
    return index < base.VisualChildrenCount ?
        base.GetVisualChild(index) : m_UglyTemplateCue;
}

It's important that GetVisualChild returns Ugly Template Cue (if any) as the last element of this Visual Tree level. Otherwise it could be hidden by template-provided elements.

The last thing to do are layout methods overrides:

C#
protected override Size MeasureOverride(Size sizeAvailable)
{
    Size sizeDesired = base.MeasureOverride(sizeAvailable);
    if (m_UglyTemplateCue != null)
        m_UglyTemplateCue.Measure(sizeAvailable);
    return sizeDesired;
}
protected override Size ArrangeOverride(Size sizeFinal)
{
    base.ArrangeOverride(sizeFinal);

    if (m_UglyTemplateCue != null)
        m_UglyTemplateCue.Arrange(new Rect(0, 0, sizeFinal.Width, 
                            sizeFinal.Height));
    return sizeFinal;
}

Using the code

The source code for this article consists of VS 2005 SP1 solution with two projects: one for HexViewer control library and the other for the test application. It is compiled and tested under Windows Vista with .NET 3.0 preinstalled. Frankly, I haven't compiled it in any other environment.

HexViewer control accepts input data (to visualize in hexadecimal) through Data dependency property of IEnumerable<byte> type.

Notice that HexViewer isn't optimized to work with large data: it doesn't implement any virtualization, etc. This implementation aspect is intentionally left as simple as possible.

Theming support includes only generic and Aero dictionaries.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Team Leader
Russian Federation Russian Federation
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralInteresting and Original!! Pin
Super Lloyd7-Jul-07 0:08
Super Lloyd7-Jul-07 0:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.