Subverting XAML: How To Inherit From Silverlight User Controls

The Problem

Many Silverlighters use XAML to design the visual appearance of their applications.  A UserControl defined with XAML is a DependencyObject that has a complex lifecycle:  there’s typically a .xaml file,  a .xaml.cs file,  and a .xaml.g.cs file that is generated by visual studio. The .xaml.g.cs file is generated by Visual Studio,  and ensures that objects defined in the XAML file correspond to fields in the object (so they are seen in intellisense and available to your c# code.)  The XAML file is re-read at runtime,  and drives a process that instantiates the actual objects defined in the XAML file — a program can compile just fine,  but fail during initialization if the XAML file is invalid or if you break any of the assumptions of the system.

XAML is a pretty neat system because it’s not tied to WPF or WPF/E.  It can be used to initialize any kind of object:  for instance,  it can be used to design workflows in asynchronous server applications based on Windows Workflow Foundation.

One problem with XAML,  however,  is that you cannot write controls that inherit from a UserControl that defined in XAML.  Visual Studio might compile the classes for you,  but they will fail to initialize at at runtime.  This is serious because it makes it impossible to create subclasses that let you make small changes to the appearance or behavior of a control.

Should We Just Give Up?

One approach is to give up on XAML.  Although Visual Studio encourages you to create UserControls with XAML,  there’s nothing to stop you from creating a new class file and writing something like

class MyUserControl:UserControl {
      public MyUserControl {
      var innerPanel=new StackPanel();
      Content=innerPanel;
      innerPanel.Children.Add(new MyFirstVisualElement());
      innerPanel.Children.Add(new MySecondVisualElement());
      ...
}

UserControls defined like this have no (fundamental) problems with inheritance,  since you’ve got complete control of the initialization process.  If it were up to me,  I’d write a lot of controls like this,  but I work on a team with people who design controls in XAML,  so I needed a better solution.

Or Should We Just Cheat?

If we still want to use XAML to define the appearance of a control,  we’ve got another option.  We can move the XAML into a control that is outside the inheritance hierarchy:  the XAML control is then contained inside another control which doesn’t have restrictions as to how it is used:

The XamlPanelInnards.xaml.cs code-behind is almost completely empty:  it contains only the constructor created by Visual Studio’s XAML designer.  XamlPanel.cs contains a protected member called InnerControl which is of type XamlPanelInnardsInnerControl is initialized in the constructor of XamlPanel,  and is also assigned to the Content property of the XamlPanel,  to make it visible.  Everything that you’d ordinarily put in the code-behind XamlPanelInnards.xaml.cs goes into XamlPanel.cs,  which uses the InnerControl member to get access to the internal members defined by the XAML designer.

Step-by-Step Refactoring

Let’s imagine that we’ve got an existing UserControl implemented in XAML called the XamlPanel.  We’d like to subclass the XamlPanel so we can use it for multiple purposes.  We can do this by:

  1. Renaming XamlPanel to OldXamlPanel;  this renames both the *.xaml and *.xaml.cs files so we can have them to look at
  2. Use Visual Studio to create a new “Silverlight User Control” called XamlPanelInnards.  This will have both a *.xaml and *.xaml.xs file
  3. Copy the contents of OldXamlPanel.xaml to XamlPanelInnards.cs.  Edit the x:Class attribute of the <UserControl> element to reflect the new class name,  “XamlPanelInnards”
  4. Use Visual Studio to create a new class,  called XamlPanel.cs.  Do not create a XamlPanel.xaml.cs file!
  5. Copy the constructor,  methods,  fields and properties from the OldXamlPanel.xaml.cs file to the XamlPanel.cs file.
  6. Create a new private field in XamlPanel.cs like
    private XamlPanelInnards InnerControl;
  7. Now we modify the constructor,  so that it does something like this:
    public XamlPanel() {
       InnerControl = new XamlPanelInnards();
       Content = InnerControl;
       ... remainder of the constructor ...
    }
  8. Most likely you’ll have compilation errors in the XamlPanel.cs file because there are a lot of references to public fields that are now in the InnerControl.  You need to track these down,  and replace code that looks like
    TitleTextBlock.Text="Some Title";

    with

    InnerControl.TitleTextBlock.Text="SomeTitle";
  9. Any event handler attachments done from the XAML file will fail to work (they’ll trigger an error when you load the application.)  You’ll need to convert
    <Button x:PressMe ... Click="PressMe_OnClick">

    in the XAML file to

    InnerControl.PressMe.Click += PressMe_OnClick

    in the constructor of XamlPanel.

  10. UI Elements that are defined in XAML are public,  so there’s a good chance that other classes might expect UI Elements inside the XamlPanel to be accessable.  You’ve got some choices:  (a) make the InnerControl public and point those references to the InnerControl (yuck!),  (b) selectively add properties to XamlPanel to let outside classes access the elements that they need to access,  or (c) rethink the encapsulation so that other class don’t need direct access to the members of XamlPanelInnards.
  11. There are a few special methods that are defined in DependencyObject and FrameworkElement that you’ll need to pay attention to.  For instance,  if your class uses FindName to look up elements dynamically,  you need to replace
    FindName("Control"+controlNumber);

    with

    InnerControl.FindName("Control"+controlNumber);

So far this is a refactoring operation:  we’re left with a program that does what it already did,  but is organized differently.  Where can we go from here?

Extending The XamlPanel

At this point,  the XamlPanel is an ordinary UserControl class.  It’s initialization logic is self-sufficient,  so it can inherit (it doesn’t necessarily have to derive from UserControl) and be inherited from quite freely.

If we want to change the behavior of the XamlPanel,  for instance,  we could declare it abstract and leave certain methods (such as event handlers) abstract.  Alternately,  methods could be declared as virtual.

A number of methods exist to customize the appearance of XamlPanel:  since XamlPanel can see the objects inside XamlPanelInnards,  it can change colors,  image sources and text contents.  If you’re interested in adding additional graphical element to the Innards,  the innards can contain an empty StackPanel — children of XamlPanel can Add() something to the StackPanel in their constructors.

Note that you can still include a child XamlPanel inside a control defined in XAML by writing something like

<OURNAMESPACE:SecondXamlPanel x:Name="MyInstance">

you’re free to make XamlPanel and it’s children configurable via the DependencyObject mechanisms.  The one thing that you lose is public access to the graphical element inside the StackPanelInnards:  many developers would think that this increase in encapsulation is a good thing,  but it may involve a change in the way you do things.

Related articles

The community at silverlight.net has pointed me to a few other articles about XAML,  inheritance and configuring Custom XAML controls.

In a lavishly illustrated blog entry,  Amyo Kabir explains how to make a XAML-defined control inherit from a user-defined base class.  It’s the converse of what I’m doing in this article,  but it’s also a powerful technique:  I’m using it right now to implement a number of Steps in a Wizard.

Note that Silverlight has mechanisms for creating Custom Controls:  these are controls that use the DependencyObject mechanism to be configurable via XAML files that include them.  If you’re interested in customizing individual controls rather than customizing subclasses,  this is an option worth exploring.

Conclusion

XAML is a great system for configuring complex objects,  but you can’t inherit from a Silverlight class defined in XAML.  By using a containment relation instead of an inheritance relation,  we can push the XAML-configured class outside of our inheritance hierarchy,  allowing the container class to participate as we desire.  This way we can have both visual UI generation and the software engineering benefits of inheritance.

Reblog this post [with Zemanta]