Template-aware control authoring example: HexViewer






4.79/5 (10 votes)
Jun 26, 2007
7 min read

44395

678
Control smart behaviour sample in case the control template does not strictly follow the control contract
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:
<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:
: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:
: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
HexView
is derived fromFrameworkElement
, is internal and is rather trivial. It's responsible for data formatting into text lines and for their rendering.HexViewer
is public and is responsible for plugging internalHexView
part into a template provided by control customer (or into a default theme template). This goal is controlled by the use of templatePART_ContentHost
named part, the same way asTextBox
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
:
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
).
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!
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.
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:
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
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.