Поделиться через



May 2019

Volume 34 Number 5

[XAML]

Custom XAML Controls

By Jerry Nixon

As an enterprise developer, you know your way around SQL Server. You know .NET Web services. And, for you, designing beautiful XAML interfaces (probably with Windows Presentation Foundation [WPF]) is child’s play. Like thousands of other career developers, Microsoft technologies pad your resumé and you clip MSDN Magazine articles like this one and pin them to your Kanban board. Get your scissors, this article is table stakes.

It’s time to boost your expertise with XAML controls. The XAML framework offers an extensive library of controls for UI development, but to accomplish what you want, you need more. In this article I’ll show you how to get the results you want using XAML custom controls.

Custom Controls

There are two approaches to creating custom controls in XAML: user controls and templated controls. User controls are an easy, designer-­friendly approach to creating a reusable layout. Templated controls offer a flexible layout with a customizable API for developers. As is the case in any language, sophisticated layouts can produce thousands of lines of XAML that can be difficult to navigate productively. Custom controls are an effective strategy for reducing layout code.

Choosing the correct approach will impact how successfully you can use and reuse the controls in your application. Here are some considerations to help you get started.

Simplicity. Easy is not always simple, but simple is always easy. User controls are simple and easy. Developers on any level can deliver them with little reach for documentation.

Design experience. Many developers love the XAML designer. Templated control layouts can be built in the designer, but it’s the user control that embraces the design-time experience.

API surface. Building an intuitive API surface lets developers easily consume it. User controls support custom properties, events and methods, but templated controls are the most flexible.

Flexible visuals. Providing a great default experience lets devel­opers consume controls with ease. But flexible controls support re-templated visuals. Only templated controls support re-templating.

To sum, user controls are optimal for simplicity and design experience, while templated controls give you the best API surface and the most flexible visuals.

If you decide to start with a user control and migrate to a templated control, you have some work ahead of you. But, it’s not terminal. This article starts with a user control and moves to a templated control. It’s important to recognize that many reusable layouts only require user controls. It’s also reasonable to open a line-of-business solution and find both user controls and templated controls.

A New User Story

You need to get new users to consent to the end-user license agreement (EULA) in your new application. As you and I both know, no user wants to consent to a EULA. Still, legal needs to ensure users check “I agree” before continuing. So even if the EULA is confusing, you’re going to make sure the XAML interface is clean and intuitive. You start prototyping; you add a TextBlock, a CheckBox, a Button, as shown in Figure 1, and then you start thinking.

Prototyped UI
Figure 1 Prototyped UI

Prototyping in the XAML designer is fast. And it’s easy because you took the time to learn the tooling. But what about other forms in your application? You might need this functionality elsewhere. Encapsulation is a design pattern used to hide complex logic from the consumer. Don’t repeat yourself (DRY) is another, focusing on code reuse.  XAML offers both through user controls and custom controls. As a XAML developer you know custom controls are more powerful than user controls, but they’re not as simple. You decide to start with a user control. Perhaps it can get the job done. Spoiler alert: It can’t.

User Controls

User controls are easy. They provide consistent, reusable interfaces and a custom, encapsulated codebehind. To create a user control, select User Control from the Add New Item dialog, as shown in Figure 2.

Add New Item Dialog
Figure 2 Add New Item Dialog

User controls are usually a child of another control. However, their lifecycle is so similar to that of windows and pages that a user control can be the value set to the Window.Current.Content property. User controls are full-featured, supporting Visual State Management, internal resources, and every other staple of the XAML framework. Building them isn’t a compromise of available functionality. My goal is to reuse them on a page, taking advantage of their support for Visual State Management, resources, styles and data binding. Their XAML implementation is simple and designer-friendly:

<UserControl>
  <StackPanel Padding="20">
    <TextBlock>Lorem ipsum.</TextBlock>
    <CheckBox>I agree!</CheckBox>
    <Button>Submit</Button>
  </StackPanel>
</UserControl>

This XAML renders my earlier prototype and shows just how simple a user control can be. Of course, there’s no custom behavior yet, only the built-in behavior of the controls I declare.

The Text Fast Path EULAs are long, so let’s address text performance. The TextBlock (and only the TextBlock) has been optimized to use the fast path, low memory and CPU rendering. It’s built to be fast—but I can spoil that:

<TextBlock Text="Optimized" />
<TextBlock>Not optimized</TextBlock>

Using TextBlock inline controls like <Run/> and <LineBreak /> breaks optimization. Properties like CharacterSpacing, Line­StackingStrategy and TextTrimming can do the same. Confused? There’s an easy test:

Application.Current.DebugSettings
  .IsTextPerformanceVisualizationEnabled = true;

IsTextPerformanceVisualizationEnabled is a little-known debug setting that allows you to see what text in your application is optimized as you debug. If the text isn’t green, it’s time to investigate.

With every release of Windows, fewer properties impact the fast path; however, there are still several that negatively and unexpectedly impact performance. With a little intentional debugging, this isn’t a problem.

Immutable Rules There are as many opinions as there are options for where business logic should reside. A general rule puts less-mutable rules closer to the control. This is generally easier and faster and it optimizes maintainability.

Business rules, by the way, are different from data validation. Responding to data entry and checking for text length and numeric ranges is simply validation. Rules govern types of user behavior.

For example, a bank has a business rule not to give a loan to customers with a credit score below a particular value. A plumber has a rule not to travel to a customer outside a certain ZIP code. Rules are about behavior. In some cases, rules change every single day, like what credit scores influence new loans. In other cases, rules never change, such as how a mechanic won’t ever work on a Subaru older than 2014.

Now, consider this acceptance criteria: A user can’t click the Button until the CheckBox is checked. This is a rule and it’s as close to immutable as you can get. I’m going to implement it close to my controls:

<StackPanel Padding="20">
  <TextBlock>Lorem ipsum.</TextBlock>
  <CheckBox x:Name="AgreeCheckBox">I agree!</CheckBox>
  <Button IsEnabled="{Binding Path=IsChecked,
    ElementName=AgreeCheckBox}">Submit1</Button>
  <Button IsEnabled="{x:Bind Path=AgreeCheckBox.IsChecked.Value,
    Mode=OneWay}">Submit2</Button>
</StackPanel>

In this code, data binding perfectly satisfies my requirement. The Submit1 Button uses classic WPF (and UWP) data binding. The Submit2 Button uses modern UWP data binding.

Notice in Figure 3 that Submit2 is enabled. Is this right? Well, in the Visual Studio Designer, classic data binding has the advantage of rendering at design time. For now, compiled data binding (x:Bind) only occurs at run time. Choosing between classic and compiled data binding is the most difficult easy decision you’re going to make. On the one hand, compiled binding is fast. But, on the other, classic binding is simple. Compiled binding exists to address XAML’s difficult performance problem: data binding. Because classic binding requires runtime reflection, it’s inherently slower, struggling to scale.

Implementing a Business Rule with Data Binding
Figure 3 Implementing a Business Rule with Data Binding

Many new features have been added to classic binding, such as asynchronous binding; and several patterns have emerged to help developers. Yet, as UWP postured to succeed WPF, it suffered from the same dragging issue. Here’s something to think about: The ability to use classic binding in an asynchronous mode was not ported to UWP from WPF. Read into that what you want, but it does encourage enterprise developers to invest in compiled binding. Compiled binding leverages the XAML code generator, creating the codebehind automatically and coupling the binding statements with real properties and datatypes expected at run time.

Because of this coupling, mismatched types can create errors, as can attempting to bind to anonymous objects or dynamic JSON objects. These edge cases aren’t missed by many developers, but they’re gone:

  • Compiled binding resolves the performance issues of data binding while introducing certain constraints.
  • Backward compatibility maintains classic binding support while giving UWP developers a better option.
  • Innovation and improvements to data binding are invested into compiled binding, not classic binding.
  • Features like function binding are available only with compiled binding where Microsoft’s binding strategy is clearly focused.

Yet the simplicity and the design-time support of classic binding keeps the argument alive, pressing the Microsoft developer tooling team to continue to improve compiled binding and its developer experience. Note that in this article, choosing one or the other will have a near-immeasurable impact. Some of the samples will demonstrate classic binding while others show compiled binding. It’s up to you to decide. The decision, of course, is most meaningful in large apps.

Custom Events Custom events can’t be declared in XAML, so you handle them in your codebehind. For example, I can forward the click event of the submit button to a custom click event on my user control:

public event RoutedEventHandler Click;
public MyUserControl1()
{
  InitializeComponent();
  SubmitButton.Click += (s, e)
    => Click?.Invoke(this, e);
}

Here, the code raises the custom events, forwarding the Routed­EventArgs from the button. Consuming developers can handle these events declaratively, like every other event in XAML:

<controls:MyUserControl1 Click="MyUserControl1_Click" />

The value of this is that consuming developers don’t need to learn a new paradigm; custom controls and out-of-the box first-party controls behave functionally the same.

Custom Properties To let consuming developers supply their own EULAs, I can set the x:FieldModifier attribute on the TextBlock. This modifies XAML compilation behavior from the default private value:

<TextBlock x:Name="EulaTextBlock" x:FieldModifier="public" />

But easy doesn’t mean good. This method offers little abstraction and requires developers to understand the internal structure. It also requires codebehind. So, I’ll avoid using the attribute approach in this case:

public string Text
{
  get => (string)GetValue(TextProperty);
  set => SetValue(TextProperty, value);
}
public static readonly DependencyProperty TextProperty =
  DependencyProperty.Register(nameof(Text), typeof(string),
    typeof(MyUserControl1), new PropertyMetadata(string.Empty));

Equally easy—and without the caveats—is a dependency property data-bound to the TextBlock’s Text property. This allows the consuming developer to read, write or bind to the custom Text property:

<StackPanel Padding="20">
  <TextBlock Text="{x:Bind Text, Mode=OneWay}" />
  <CheckBox>I agree!</CheckBox>
  <Button>Submit</Button>
</StackPanel>

The dependency property is necessary to support data binding. Robust controls support basic use cases like data binding. Plus, the dependency property adds only a single line to my code base:

<TextBox Text="{x:Bind Text, Mode=TwoWay,
  UpdateSourceTrigger=PropertyChanged}" />

Two-way data binding for custom properties in user controls is supported without INotifyPropertyChanged. This is because dependency properties raise internal changed events that the binding framework monitors. It’s its own kind of INotifyPropertyChanged.

The preceding code reminds us that UpdateSourceTrigger determines when changes are registered. Possible values are Explicit, LostFocus and PropertyChanged. The latter occurs as changes are made.

Roadblocks The consuming developer may want to set the content property of the user control. This is an intuitive approach, but it’s not supported by user controls. The property is already set to the XAML I declared:

<Controls:MyUserControl>
  Lorem Ipsum
</Controls:MyUserControl>

This syntax overwrites the content property: the TextBlock, CheckBox and Button. If I consider re-templating a basic XAML use case, my user control fails to deliver a complete, robust experience. User controls are easy, but offer little control or extensibility. An intuitive syntax and re-templating support are part of a common experience. It’s time to consider a templated control.

Templated Controls

XAML has seen massive improvements in memory consumption, performance, accessibility and visual consistency. Developers love XAML because it’s flexible. Templated controls are a case in point.

Templated controls can define something completely new, but are typically a composite of several existing controls. The example here of a TextBlock, CheckBox and Button together is a classic scenario.

By the way, don’t confuse templated controls with custom controls. A templated control is a custom layout. A custom control is just a class inheriting an existing control without any custom styling. Sometimes, if all you need is an extra method or property on an existing control, custom controls are a great option; their visuals and logic are already in place and you’re simply extending them.

Control Template A control’s layout is defined by a ControlTemplate. This special resource is applied at run time. Every box and button resides in the ControlTemplate. A control’s template can be easily accessed by the Template property. That Template property isn’t read-only. Developers can set it to a custom ControlTemplate, transforming a control’s visuals and behavior to meet their particular needs. That’s the power of re-templating:

<ControlTemplate>
  <StackPanel Padding="20">
    <ContentControl Content="{TemplateBinding Content}" />
    <CheckBox>I agree!</CheckBox>
    <Button>Submit1</Button>
  </StackPanel>
</ControlTemplate>

ControlTemplate XAML looks like any other layout declaration. In the preceding code, notice the special TemplateBinding markup extension. This special binding is tuned for one-way template operations. Since Windows 10 version 1809, x:Bind syntax is supported in UWP ControlTemplate definitions. This allows for performant, compiled, two-way and function bindings in templates. TemplateBinding works great in most cases.

Generic.xaml To create a templated control, select Templated Control in the Add New Item dialog. This introduces three files: the XAML file; its codebehind; and themes/generic.xaml, which holds the ControlTemplate. The themes/generic.xaml file is identical to WPF. It’s special. The framework merges it into your app’s resources automatically. Resources defined here are scoped at the application level:

<Style TargetType="controls:MyControl">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="controls:MyControl" />
    </Setter.Value>
  </Setter>
</Style>

ControlTemplates are applied using implicit styles: styles without a key. Explicit styles have a key used to apply them to controls; implicit styles are applied based on TargetType. So, you must set DefaultStyleKey:

public sealed class MyControl : Control
{
  public MyControl() => DefaultStyleKey = typeof(MyControl);
}

This code sets the DefaultStyleKey, determining which style is implicitly applied to your control. I’m setting it to the corresponding value of the TargetType in the ControlTemplate:

<ControlTemplate TargetType="controls:MyControl">
  <StackPanel Padding="20">
    <TextBlock Text="{TemplateBinding Text}" />
    <CheckBox Name="AgreeCheckbox">I agree!</CheckBox>
    <Button IsEnabled="{Binding IsChecked,
      ElementName=AgreeCheckbox}">Submit</Button>
  </StackPanel>
</ControlTemplate>

TemplateBinding binds the TextBlock’s Text property to the custom dependency property copied from the user control to the templated control. TemplateBinding is one way, very efficient and generally the best option.

Figure 4 shows the result of my work in the designer. The custom layout declared in the ControlTemplate is applied to my custom control, and the binding is executed and rendered at design time:

 

<controls:MyControl Text="My outside value." />

Preview Transparently Using Internal Properties
Figure 4 Preview Transparently Using Internal Properties

The syntax to use my custom control is simple. I’ll make it better by allowing the developer to use inline text. This is the most intuitive syntax to set an element’s content. XAML provides a class attribute to help me do that, as Figure 5 shows.

Figure 5 Using the Class Attribute to Set the Content Property

[ContentProperty(Name = "Text")]
public sealed class MyControl : Control
{
  public MyControl() => DefaultStyleKey = typeof(MyControl);
  public string Text
  {
    get => (string)GetValue(TextProperty);
    set => SetValue(TextProperty, value);
  }
  public static readonly DependencyProperty TextProperty =
    DependencyProperty.Register(nameof(Text), typeof(string),
      typeof(MyControl), new PropertyMetadata(default(string)));
}

Notice the ContentProperty attribute, which comes from the Windows.UI.Xaml.Markup namespace. It indicates to which property direct, inline content declared in XAML should be written. So, now, I can declare my content like this:

<controls:MyControl>
  My inline value!
</controls:MyControl>

It’s beautiful. Templated controls give you the flexibility to design control interaction and syntax in whatever way seems most intuitive to you. Figure 6 shows the result of introducing ContentProperty to the control.

Design Preview
Figure 6 Design Preview

The ContentControl Where previously I had to create a custom property and map that property to the content of the control, XAML provides a control already built for that. It’s known as ContentControl and its property is called Content. ContentControl also provides the ContentTemplate and ContentTransition properties to handle visualization and transitions. Button, CheckBox, Frame and many standard XAML controls inherit ContentControl. Mine could have, too; I’d just use Content instead of Text:

public sealed class MyControl2 : ContentControl
{
  // Empty
}

In this code, notice the terse syntax to create a custom control with a Content property. ContentControl auto-renders as a ContentPresenter when declared. It’s a fast, easy solution. There’s a caveat, however: ContentControl doesn’t support literal strings in XAML. Because it violates my goal to make my control support literal strings, I’ll stick to Control, considering ContentControl another time.

Accessing Internal Controls The x:Name directive declares the field name auto-generated in a codebehind. Give a checkbox an x:Name of MyCheckBox and the generator will create a field in your class called MyCheckBox.

By contrast, x:Key (for resources only) doesn’t create a field. It adds resources to a dictionary of unresolved types until they’re first used. For resources, x:Key offers performance improvements.

Because x:Name creates a backing field, it can’t be used in a ControlTemplate; templates are decoupled from any backing class. Instead, you use the Name property.

Name is a dependency property on FrameworkElement, an ancestor of Control. You use it when you can’t use x:Name. Name and x:Name are mutually exclusive in a given scope because of FindName.

FindName is a XAML method used to locate objects by name; it works with Name or x:Name. It’s reliable in the codebehind, but not in templated controls where you must use GetTemplateChild:

protected override void OnApplyTemplate()
{
  if (GetTemplateChild("AgreeCheckbox") is CheckBox c)
  {
    c.Content = "I really agree!";
  }
}

GetTemplateChild is a helper method used in the OnApply­Template override to find controls created by a ControlTemplate. Use it to find references to internal controls.

Changing the Template Re-templating the control is simple, but I’ve built the class to expect controls with certain names. I must ensure the new template maintains this dependency. Let’s build a new ControlTemplate:

<ControlTemplate
  TargetType="controls:MyControl"
  x:Key="MyNewStyle">

Small changes to a ControlTemplate are normal. You don’t need to start from scratch. In the Visual Studio Document Outline pane, right-click any control and extract a copy of its current template (see Figure 7).

Extracting a Control Template
Figure 7 Extracting a Control Template

If I maintain its dependencies, my new ControlTemplate can totally change the visuals and behaviors of a control. Declaring an explicit style on the control tells the framework to ignore the default implicit style:

<controls:MyControl Style="{StaticResource MyControlNewStyle}">

But this rewriting of the ControlTemplate comes with a warning. Control designers and developers must be careful to support accessibility and localization capabilities. It’s easy to remove these by mistake.

TemplatePartAttribute It would be handy if a custom control could communicate the named elements it expects. Some named elements may be required only during edge cases. In WPF, you have TemplatePartAttribute:

[TemplatePart (
  Name = "EulaTextBlock",
  Type = typeof(TextBlock))]
public sealed class MyControl : Control { }

This syntax shows how the control can communicate internal dependencies to external developers. In this case, I expect a TextBlock with the name EulaTextBlock in my ControlTemplate. I can also specify the visual states I expect in my custom control:

[TemplateVisualState(
  GroupName = "Visual",
  Name = "Mouseover")]
public sealed class MyControl : Control { }

TemplatePart is used by Blend with TemplateVisualState to guide developers against expectations while creating custom templates. A ControlTemplate can be validated against these attributions. Since 10240, WinRT has included these attributes. UWP can use them, but Blend for Visual Studio doesn’t. These remain a good, forward-­looking practice, but documentation is still the best approach.

Accessibility First-party XAML controls are meticulously designed and tested to be beautiful, compatible and accessible. Accessibility requirements are now first-class citizens and are release requirements for every control.

When you re-template a first-party control, you put at risk the accessibility features thoughtfully added by the development teams. These are difficult to get right, and simple to get wrong. When choosing to re-template a control, you should be well-versed in the accessibility capabilities of the framework and in the techniques to implement them. Otherwise, you lose a considerable portion of their value.

Adding accessibility as a release requirement helps not only those with permanent disabilities, but also those who are temporarily incapacitated. It also reduces risk when re-templating first-party controls.

You Did It!

After upgrading from a user control to a templated control, I intro­duced very little new code. But I’ve added a lot of functionality. Let’s consider what’s been accomplished overall.

Encapsulation. The control is a collection of several controls, bundled with custom visuals and behaviors that consuming devel­opers can reuse with ease across an application.

Business logic. The control incorporates business rules that fulfill the acceptance criteria of the user story. I’ve put immutable rules close to the control and supported a rich design-time experience, too.

Custom events. The control exposes control-specific custom events like “click” that help developers interoperate with the control, without requiring them to understand the internal structure of the layout.

Custom properties. The control exposes properties to allow the consuming developer to influence the content of the layout. This has been done in a way to fully support XAML data binding.

API syntax. The control supports an intuitive approach that allows developers to declare their content with literal strings and in a straightforward way. I leveraged the ContentProperty attribute to do this.

Templates. The control ships with a default ControlTemplate that lays out an intuitive interface. However, XAML re-templating is supported to allow consumer developers to customize the visuals as needed.

There’s More to Do

My control needs more, but not much more— just a little attention to layout (like the need to scroll large text) and a few properties (like the content of the checkbox). I’m amazingly close.

Controls can support Visual State Management, a native feature of XAML that allows properties to change based on sizing events or framework events (like mouseover). Mature controls have visual states.

Controls can support localization; the native capability in UWP uses the x:Uid directive associating controls with RESW strings that are filtered by the active locale. Mature controls support localization.

Controls can support external-style definitions to help update their visuals without requiring a new template; this can involve shared visuals and leverage themes and BasedOn styles. Mature controls share and reuse styles.

Wrapping Up

Prototyping a UI in XAML is fast. User controls create simple, reusable layouts easily. Templated controls require a bit more work to create simple, reusable layouts with more sophisticated capabilities. The right approach is up to the developer, based on a little knowledge and a lot of experience. Experiment. The more you learn the tooling, the more productive you’ll become.

Windows Forms succeeded Visual Basic 6 in 2002, just as WPF succeeded Windows Forms in 2006. WPF brought with it XAML: a declarative language for UI. Microsoft developers had never seen anything like XAML. Today, Xamarin and UWP bring XAML to iOS, Android, HoloLens, Surface Hub, Xbox, IoT and the modern desktop. In fact, XAML is now the technology building the Windows OS itself.

Developers around the world love XAML because it’s so productive and flexible. Microsoft engineers feel the same; we’re building our own apps and even Windows 10 with XAML. The future is bright, the tooling is powerful and the technology is more approachable than ever.


Jerry Nixon is a senior software engineer & lead architect in Commercial Software Engineering at Microsoft. He has developed and designed software for two decades. Speaker, organizer, teacher and author, Nixon is also the host of DevRadio. Most of his days are spent teaching his three daughters “Star Trek” backstories and episode plots.

Thanks to the following Microsoft technical experts for reviewing this article: Daniel Jacobson, Dmitry Lyalin, Daren May, Ricardo Minguez Pablos


Discuss this article in the MSDN Magazine forum