In this example we are going to create a custom control in WPF called MetroTile.
Our MetroTile control will expose three properties to let designers set an image icon, a display count and a text property. The usual core properties for controls are also settable such as background, foreground, margin, etc.
This screenshot shows three instances of the custom control in use, all with the generic template applied. The RabbitMQ and ZeroMq tiles override the default background.
Class Diagram
Key concepts
Three dependency properties that allow the Xaml data binding system to do its work.
I’ve also shown how to expose a bubbling RoutedEvent (travels up the visual tree until handled) through the DisplayCountChanged event. As is common, I’ve included a Preview RoutedEvent that tunnels down the visual tree, which fires just before the main event.
A public RoutedUICommand called ResetCountCommand is also included which you can bind to from Xaml. Commands are preferred to methods as you cannot bind directly to methods.
MetroTile Custom Control
Default Template
The display icon is located in top left hand corner when set by the developer using the custom control. When the developer first includes the control in their xaml, the following default values are visible on their design surface:
From Template | Purple background |
| White foreground |
From Custom Control | Empty display icon |
| “Not Set” display text |
| Zero display count |
Example of MetroTile in use
For information on the functionality of this example application, visit the following blog post: http://stevenhollidge.blogspot.co.uk/2012/03/metro-messaging.html
Custom Control: MetroTile
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace Common.Controls
{
[TemplatePart(Name="PART_DISPLAY_ICON", Type=typeof(Image))]
[TemplatePart(Name = "PART_DISPLAY_COUNT_CONTAINER", Type = typeof(TextBlock))]
[TemplatePart(Name = "PART_DISPLAY_TITLE_CONTAINER", Type = typeof(TextBlock))]
[Description("Represents a metro styled tile that displays an icon, a count and a title")]
public class MetroTile : Control
{
#region Constructor
static MetroTile()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MetroTile),
new FrameworkPropertyMetadata(typeof(MetroTile)));
CommandManager.RegisterClassCommandBinding(
typeof(MetroTile),
new CommandBinding(ResetCountCommand, OnResetCountCommand));
}
#endregion
#region Dependency Properties
#region DisplayIcon
public static readonly DependencyProperty DisplayIconProperty =
DependencyProperty.Register("DisplayIcon",
typeof (ImageSource),
typeof (MetroTile),
new PropertyMetadata(null));
[Description("The icon displayed in the tile."), Category("Common Properties")]
public ImageSource DisplayIcon
{
get { return (ImageSource)this.GetValue(DisplayIconProperty); }
set { this.SetValue(DisplayIconProperty, value); }
}
#endregion
#region DisplayCount
public static readonly DependencyProperty DisplayCountProperty =
DependencyProperty.Register("DisplayCount",
typeof(int),
typeof(MetroTile),
new UIPropertyMetadata(0));
[Description("The count displayed in the tile."), Category("Common Properties")]
public int DisplayCount
{
get { return (int)this.GetValue(DisplayCountProperty); }
set { this.SetValue(DisplayCountProperty, value); }
}
#endregion
#region DisplayText
[Description("The text displayed in the tile."), Category("Common Properties")]
public static readonly DependencyProperty DisplayTextProperty =
DependencyProperty.Register("DisplayText",
typeof(string),
typeof(MetroTile),
new PropertyMetadata("Not set"));
public string DisplayText
{
get { return (string)this.GetValue(DisplayTextProperty); }
set { this.SetValue(DisplayTextProperty, value); }
}
#endregion
#endregion
#region Events
public static readonly RoutedEvent DisplayCountChangedEvent =
EventManager.RegisterRoutedEvent("DisplayCountChanged",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(MetroTile));
public event RoutedEventHandler DisplayCountChanged
{
add { AddHandler(DisplayCountChangedEvent, value); }
remove { RemoveHandler(DisplayCountChangedEvent, value); }
}
protected virtual void OnDisplayCountChanged(int oldValue, int newValue)
{
// 1. Pair of events: A preview that tunnels and a main event that bubbles
// 2. We could also create our own RoutedEventArgs that includes oldValue & new Value
var previewEvent = new RoutedEventArgs(PreviewDisplayCountChangedEvent);
RaiseEvent(previewEvent);
var e = new RoutedEventArgs(DisplayCountChangedEvent);
RaiseEvent(e);
}
public static readonly RoutedEvent PreviewDisplayCountChangedEvent =
EventManager.RegisterRoutedEvent("PreviewDisplayCountChanged",
RoutingStrategy.Tunnel,
typeof(RoutedEventHandler),
typeof(MetroTile));
public event RoutedEventHandler PreviewDisplayCountChanged
{
add { AddHandler(PreviewDisplayCountChangedEvent, value); }
remove { RemoveHandler(PreviewDisplayCountChangedEvent, value); }
}
#endregion
#region Commands
public static readonly ICommand ResetCountCommand =
new RoutedUICommand("ResetCount", "ResetCount", typeof(MetroTile));
private static void OnResetCountCommand(object sender, ExecutedRoutedEventArgs e)
{
var target = (MetroTile)sender;
target.DisplayCount = 0;
}
#endregion
}
}
<Style TargetType="{x:Type c:MetroTile}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type c:MetroTile}">
<Border Background="{StaticResource MetroPurpleBrush}">
<Grid Background="{Binding Path=Background, RelativeSource={RelativeSource TemplatedParent}}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="1.25*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image x:Name="PART_DISPLAY_ICON"
Grid.Row="0"
Grid.Column="0"
Margin="10,10,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Source="{Binding Path=DisplayIcon,
RelativeSource={RelativeSource TemplatedParent},
UpdateSourceTrigger=PropertyChanged}"
Stretch="None" />
<TextBlock x:Name="PART_DISPLAY_COUNT_CONTAINER"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="0,0,10,0"
HorizontalAlignment="Right"
FontSize="48"
Foreground="White"
Text="{Binding Path=DisplayCount,
StringFormat=N0,
RelativeSource={RelativeSource TemplatedParent},
UpdateSourceTrigger=PropertyChanged}" />
<TextBlock x:Name="PART_DISPLAY_TITLE_CONTAINER"
Grid.Row="2"
Grid.RowSpan="2"
Grid.Column="0"
Grid.ColumnSpan="3"
Margin="10"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
FontSize="24"
Foreground="White"
Text="{Binding Path=DisplayText,
RelativeSource={RelativeSource TemplatedParent},
UpdateSourceTrigger=PropertyChanged}"
TextWrapping="Wrap" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Source code: http://stevenhollidge.com/blog-source-code/Messaging.zip