Another twitter question. As usual, I'm targeting WPF 4
here, but just about everything here works with Silverlight 4 as
well.
(read from bottom up)
Let's say you want to create a standardized "gadget" for your
application, from which anyone can derive and create their own
gadgets.
The desire:
- Provide a base gadget that has some sort of standardized chrome
and some behavior to be inherited by all derived controls.
The challenge:
- Regular derived controls won't be friendly for your users to
create controls
- Regular usercontrols don't enforce any control template
- Controls inherit functionality/behavior, but not template.
Well, technically you do inherit the style and template, but you
can't really override just a part of the template, you have to
modify the whole thing, which would let the users mess around with
the chrome you're trying to keep.
There are a bunch of ways to do that, depending on what part you
need to standardize, but one of the more interesting is to create
your own UserControl-like class deriving from ContentControl. Other
options include allowing other people to make their gadgets using
anything and you simply host them in a shim control that has the
behavior. In some cases, that's the better choice, but not in cases
where you want them to be able to call methods you provide.
The Base Gadget Control
Our base control is simple, but contained in two parts. The
first part is the code (the behavior and model of the control). The
second part is the visual representation, contained in its style
and template.
Control Behavior
Since we're not providing any other functionality at this point
(you can, but I didn't want to get into Dependency Properties
here), the code has only the static constructor required for
styling. The constructor says the default style is the one that
targets the Gadget type.
public class Gadget : ContentControl
{
static Gadget()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Gadget),
new FrameworkPropertyMetadata(typeof(Gadget)));
}
}
All the other behavior is inherited from ContentControl.
ContentControl is a specialization of Control which allows for a
single element to be inserted as content. In a typical UserControl,
this single element is the root Grid.
Control Template and generic.xaml
So from where does the Gadget pick up its style? In WPF and Silverlight,
there exists the concept of a generic or default style for a
control. That style is contained in the themes\generic.xaml
resource dictionary for the library inside which the control
exists. The name and location are special.
The contents of generic.xaml include styles for one or more
controls. The styles contain the control templates which define
what the UI of the control is made up of. Since this is a content
control, we need to have a ContentPresenter (or another
ContentControl) in there, bound to the Content property.
Truthfully, you can have anything bound to that property, but if
the types don't match, you'll get an exception at runtime.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:local="clr-namespace:WpfApplication22"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type local:Gadget}">
<Setter Property="Padding"
Value="10" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Gadget}">
<Border BorderBrush="Red"
BorderThickness="5"
CornerRadius="10">
<ContentPresenter Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
The TemplateBinding syntax is what you use to bind to properties
of the control that the template is applied to.
So, our custom gadget simply enforces a red 5px border around
all elements. You could also enforce a background, drag handles,
close buttons etc.
The Custom UserControls (Gadgets)
Here are the custom UserControls. I simply added a UserControl
to the project, and then changed the markup and codebehind to
reference Gadget rather than UserControl. To make it friendlier to
your users, I'd recommend creating a custom Visual Studio template
for your Gadget. The template would set the base class and
namespaces automatically.
First Gadget
There's also a related code-behind, but it's empty. In a real
app, it'll likely have code. Here's the Xaml:
<local:Gadget x:Class="WpfApplication22.Gadgets.TestGadget"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication22"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300">
<Grid>
<TextBlock Text="My Custom Gadget Content"
FontSize="20"
TextWrapping="Wrap"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</local:Gadget>
Second Gadget
Similarly, here's the second gadget's markup. This control
contains an ellipse.
<local:Gadget x:Class="WpfApplication22.Gadgets.SecondGadget"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication22"
mc:Ignorable="d"
Padding="25"
d:DesignHeight="300"
d:DesignWidth="300">
<Grid>
<Ellipse x:Name="MyEllipse" Fill="Blue"/>
</Grid>
</local:Gadget>
The Main Window
The main Window contains instances of both gadgets. The
highlighted areas of the Window xaml are what are needed to load in
our own gadgets.
<Window x:Class="WpfApplication22.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:gadgets="clr-namespace:WpfApplication22.Gadgets"
Title="MainWindow" Height="450" Width="450">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<gadgets:TestGadget Grid.Row="0" Grid.Column="0" />
<gadgets:SecondGadget Grid.Row="1" Grid.Column="1" />
</Grid>
</Window>
Here's how it looks at runtime:
That's all there is to it for the basics of creating your own
usercontrol stand-ins.
If you're building add-ins or runtime-resolved Gadgets for your
own app, take a look at MEF (Managed Extensibility Framework).