Tuesday 27 March 2012

WPF Custom Control with Image

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. 

image

Class Diagram

image

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

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

image

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

No comments:

Post a Comment