Online training from DevelopMentor
Labels: .NET, DevelopMentor, Training, WPF
a mash of all things technical.
Labels: .NET, DevelopMentor, Training, WPF
“Oh my, starry eyed surprise. Sun down to sun rise. Dance all night. We gonna dance all night…”
Feel free to stab your eyes out at this point (Or drink a Diet Coke)
Ok so WTF am I talking about? Custom Ink Canvas rendering in WPF of course… I ran across a problem recently that I thought was blog worthy. The objective was to allow a user to draw on an InkCanvas with a custom “stencil.” The “stencil” is really just some custom shape that should be used as the stroke for the InkCanvas. This is kind of hard to explain so a picture may help
In this picture the stars (hence the terrible Oakenfold reference) would be the stencil. When the user drags the mouse around the canvas we want to draw the stars like shown above. This isn’t quite as obvious to implement as most things in WPF. My hope was that I could just set the Stroke to some DrawingBrush and be done. However, it isn’t that easy.
The main reason for the complication is the way that the InkCanvas collects the strokes from the user. The strokes are are collected on a background thread to ensure that all strokes will be collected, even if the UI is blocking. This is done through a DynamicRenderer object. This object collects the user input and renders all points in the stroke on a separate thread. Once the entire stroke is collected The InkCanvas raises the OnStrokeCollected method.
The first step to solving my problem is to implement a custom DynamicRenderer that will track the users movements on the canvas and render our custom shape along the path that the user has drawn.
public class CustomRenderer : DynamicRenderer
{
private Point prevPoint;
public DrawingGroup Stencil { get; set; }
protected override void OnStylusDown(RawStylusInput rawStylusInput)
{
// Allocate memory to store the previous point to draw from.
prevPoint = new Point(double.NegativeInfinity, double.NegativeInfinity);
base.OnStylusDown(rawStylusInput);
}
protected override void OnDraw(DrawingContext drawingContext,StylusPointCollection stylusPoints,Geometry geometry, Brush fillBrush)
{
for (int i = 0; i < stylusPoints.Count; i++)
{
var pt = (Point)stylusPoints[i];
Vector v = Point.Subtract(prevPoint, pt);
// Only draw if we are at least 4 units away
// from the end of the last ellipse. Otherwise,
// we're just redrawing and wasting cycles.
if (v.Length > 4)
{
var clone = Stencil.Clone();
clone.Transform = new TranslateTransform(pt.X, pt.Y);
drawingContext.DrawDrawing(clone);
prevPoint = pt;
}
}
}
}
The key to this snippet is in the OnDraw method override. I am iterating through the points along the path that the user has drawn then rendering a new instance of our custom drawing to the DrawingContext pipeline. Notice the TranslateTransform, I am using the translate to make sure the drawing I add to the DrawingContext pipeline follows the points in the path that the user created.
Once the stylus points are converted to strokes then the stroke is told to render itself. This is the second step in the process. We need to create a custom stroke class that renders our custom shape when it is asked to render itself.
class CustomStroke : Stroke
{
public DrawingGroup Stencil { get; set; }
public CustomStroke(StylusPointCollection stylusPoints) : base(stylusPoints)
{
}
protected override void DrawCore(DrawingContext drawingContext,DrawingAttributes drawingAttributes)
{
// Allocate memory to store the previous point to draw from.
var prevPoint = new Point(double.NegativeInfinity,
double.NegativeInfinity);
// Draw linear gradient ellipses between
// all the StylusPoints in the Stroke.
for (int i = 0; i < StylusPoints.Count; i++)
{
var pt = (Point)StylusPoints[i];
Vector v = Point.Subtract(prevPoint, pt);
// Only draw if we are at least 4 units away
// from the end of the last ellipse. Otherwise,
// we're just redrawing and wasting cycles.
if (v.Length > 4)
{
var clone = Stencil.Clone();
clone.Transform = new TranslateTransform(pt.X, pt.Y);
drawingContext.DrawDrawing(clone);
prevPoint = pt;
}
}
}
}
You can see the DrawCore method looks very similar to the OnDraw method in the custom renderer. We are doing the same rendering in both places so the code will be very similar.
The final step in this process is to create a class that inherits from InkCanvas and wire up the dynamic renderer and the custom stroke.
public class CustomRenderingInkCanvas : InkCanvas
{
readonly CustomRenderer customRenderer = new CustomRenderer();
public CustomRenderingInkCanvas(): base()
{
// Use the custom dynamic renderer on the
// custom InkCanvas.
DynamicRenderer = customRenderer;
}
protected override void OnStrokeCollected(InkCanvasStrokeCollectedEventArgs e)
{
// Remove the original stroke and add a custom stroke.
Strokes.Remove(e.Stroke);
var customStroke = new CustomStroke(e.Stroke.StylusPoints)
{
Stencil = Stencil
};
Strokes.Add(customStroke);
// Pass the custom stroke to base class' OnStrokeCollected method.
var args = new InkCanvasStrokeCollectedEventArgs(customStroke);
base.OnStrokeCollected(args);
}
}
I have cut out some code from the snippet above to focus on the important parts. The two keys are wiring up the DynamicRenderer in the public constructor and removing the default stroke and adding in our custom stroke in the StrokeCollected override
Once we put all these steps together we end up with an ink canvas that we can apply a custom stencil to and use that stencil to draw with.
The result is shown in the video below ( Turn on your sound ;)
Custom InkCanvas in WPF from Brad Cunningham on Vimeo.
You can download the full sample project from here. I followed this MSDN article in solving this problem
If you have done any with WPF radio buttons you know that WPF has given us greater flexibility to define radio groups. In WinForms radio button groups were constrained to the parent panel. This meant if you wanted radio buttons to be mutually exclusive, that is you can only have one selected at a time, they all had to live within the same parent layout panel.
In WPF you don’t have this limitation, when you specify a group name for a radio button it will be mutually exclusive to any other radio button that has the same group name within the visual tree. This gives you greater flexibility with how you layout your radio groups.
Recently, I had the need to show a popup menu within a complex layout scenario and I needed there to be only one popup open at a time. So I originally went down the route of using radio buttons. I figured I could layout radio buttons anywhere in the visual tree, give them all the same group name and then bind the IsOpen property of popup to the IsChecked property of my radio button.
This worked fine at first, however I quickly found out that once a popup was open, i.e. one of the radio buttons in the group was checked, from that point on you always had one popup open. The problem is you can’t “un-select” a radio button once it has been checked. This was not the desired behavior for my situation so I set out to find a solution that would give me mutual exclusivity and the ability to have an unchecked state for my entire group, that is nothing in the group is selected.
Enter the grouping checkboxes. What I wanted was the check / uncheck behavior of checkbox (really of the base toggle button) with the functionality of a radio button. So I decided to do this through a simple attached property.
First I created a class called ToggleButtonExtensions. I decided to pull the functionality up to the base ToggleButton class since there is nothing in the checkbox class that I need and I wanted to give the flexibility. This is where I am going to define my custom attached property and handle the changed event when the ToggleButton checked event fires.
So first I define my custom attached property called GroupName (I wanted the usage to feel as much like the radio button grouping as possible)
public static readonly DependencyProperty GroupNameProperty = DependencyProperty.RegisterAttached("GroupName",typeof(String),typeof(ToggleButtonExtensions),new PropertyMetadata(String.Empty, OnGroupNameChanged));The key to hooking into the ToggleButton checked event is in the property change handler on this custom attached property. At the end of the code snippet above you see the method name OnGroupNameChanged. In that method we can get a hold of the element that is using this attached property and hook into it’s events. The body of that method looks like this:
private static void OnGroupNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//Add an entry to the group name collection
var toggleButton = d as ToggleButton;
if (toggleButton != null)
{
String newGroupName = e.NewValue.ToString();
String oldGroupName = e.OldValue.ToString();
if (String.IsNullOrEmpty(newGroupName))
{
//Removing the toggle button from grouping
RemoveCheckboxFromGrouping(toggleButton);
}
else
{
//Switching to a new group
if (newGroupName != oldGroupName)
{
if (!String.IsNullOrEmpty(oldGroupName))
{
//Remove the old group mapping
RemoveCheckboxFromGrouping(toggleButton);
}
ElementToGroupNames.Add(toggleButton, e.NewValue.ToString());
toggleButton.Checked += ToggleButtonChecked;
}
}
}
}
There are a few things going on here, first you see the last line in the method is hooking into the Checked event of the ToggleButton. This gives us a way to go through the other ToggleButtons that are a part of this group and “un-check” them. This will give us the mutual exclusivity we want.
The other thing in the method to note is how I am managing the groups of ToggleButtons. I have a simple dictionary that stores a GroupName to ToggleButton mapping. This gives me a handle to the ToggleButtons so I can uncheck them if another element in the same group gets checked.
The last thing we need to do is implement the Checked handler where we can iterate over the dictionary and uncheck any other elements in the same group as the element that is being checked. That handler method looks like this:
static void ToggleButtonChecked(object sender, RoutedEventArgs e)
{
var toggleButton = e.OriginalSource as ToggleButton;
foreach(var item in ElementToGroupNames)
{
if (item.Key != toggleButton && item.Value == GetGroupName(toggleButton))
{
item.Key.IsChecked = false;
}
}
}
And that’s it. Now we have mutually exclusive toggle buttons that allow us to have an unchecked state for the whole group.
UPDATE: Fixed Link. You can check out the sample project here.
UPDATE: I found and issue with placing the default content elements in the Resources for the Page. When you place the elements directly in the resources, without wrapping them in a template, there is only one instance of the elements created. This means that after the MasterPage control is created the first time the elements will never get re-initialized. This causes problems when you have buttons or other interactions that bind to properties specific to current instance of the master page.
For example : If you had a ButtonContentArea defined and you placed a save button in that area with a Command "SaveCommand" that was bound to a command on the consumer of the MasterPage control, that command would be initialized the first time the MasterPage control is created. Then any subsequent consumers of the master page would use the Binding from the first instance of the master page. This means your save command would be routed to the wrong handler. If you make the Dependency Properties of Type DataTemplate and you wrap the elements in a DataTemplate then bind to the ContentTemplate property of the controls you can resolve this issue. The code below has been updated to reflect this change.
Now on with your regularly scheduled programming.
Coming from an ASP.NET background one of the things I really like are Master Pages. If you aren't familiar with master pages read this first.
In WPF there are several different ways to share styles and templates across pages. Recently I had a need to share a common layout across multiple pages while still allowing for different content and behaviors on each page. The first thing I thought was Master Page. However this feature doesn't come out of the box with WPF.
However it is easy enough to implement your own version. A quick search provided me with Karin Huber's code project article about this very subject. This approach uses a user control as the master page. After playing around with this example one problem I found is that you are unable to name elements that are inside of the master page user control (others have discovered this as well if you view the comments on the codeplex project or you view Rob Relyea's blog post) This isn't a deal breaker by any means but I wanted to give a go at implementing it myself. It was surprisingly easy when I got down to it.
First I created a simple custom control named Master Page and a few dependency properties for the different content zones I am going to expose. Here is what that looks like
public class MasterPage : Control
{
public static DependencyProperty MainContentAreaProperty =
DependencyProperty.Register("MainContentArea", typeof (DataTemplate), typeof (MasterPage));
public static DependencyProperty HeaderContentAreaProperty =
DependencyProperty.Register("HeaderContentArea", typeof(DataTemplate), typeof(MasterPage));
public static DependencyProperty RightContentAreaProperty =
DependencyProperty.Register("RightContentArea", typeof(DataTemplate), typeof(MasterPage));
public static DependencyProperty FooterContentAreaProperty =
DependencyProperty.Register("FooterContentArea", typeof(DataTemplate), typeof(MasterPage));
static MasterPage()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MasterPage), new FrameworkPropertyMetadata(typeof(MasterPage)));
}
public DataTemplate MainContentArea
{
get { return (DataTemplate)GetValue(MainContentAreaProperty); }
set { SetValue(MainContentAreaProperty, value); }
}
public DataTemplate HeaderContentArea
{
get { return (DataTemplate)GetValue(HeaderContentAreaProperty); }
set { SetValue(HeaderContentAreaProperty, value); }
}
public DataTemplate RightContentArea
{
get { return (DataTemplate)GetValue(RightContentAreaProperty); }
set { SetValue(RightContentAreaProperty, value); }
}
public DataTemplate FooterContentArea
{
get { return (DataTemplate)GetValue(FooterContentAreaProperty); }
set { SetValue(FooterContentAreaProperty, value); }
}
}
The generic.xaml is straightforward. I placed ContentControls where I wanted "content zones." I named them accordingly and I bound the ContentTemplate property to the appropriate dependency property
<Style TargetType="{x:Type local:MasterPage}">
<Setter Property="HeaderContentArea" Value="{StaticResource HeaderContent}"/>
<Setter Property="MainContentArea" Value="{StaticResource MainContent}"/>
<Setter Property="RightContentArea" Value="{StaticResource RightContent}"/>
<Setter Property="FooterContentArea" Value="{StaticResource FooterContent}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MasterPage}">
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="225"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="25"></RowDefinition>
<RowDefinition Height="25"></RowDefinition>
</Grid.RowDefinitions>
<ContentControl Grid.Column="0" Grid.Row="0" x:Name="HeaderContentArea" ContentTemplate="{TemplateBinding HeaderContentArea}"/>
<ContentControl Grid.Column="0" Grid.Row="1" x:Name="MainContentArea" ContentTemplate="{TemplateBinding MainContentArea}"/>
<ContentControl Grid.Column="0" Grid.Row="2" x:Name="FooterContentArea" ContentTemplate="{TemplateBinding FooterContentArea}"/>
<ContentControl Grid.Column="1" Grid.RowSpan="3" x:Name="RightContentArea" ContentTemplate="{TemplateBinding RightContentArea}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
This allows a consumer of this control to bind any piece of content they want to the dependency property of their choice and that content will be displayed in the "content zone" defined in the generic.xaml.
For example your Window1.xaml would look something like this
<Window.Resources>
<DataTemplate x:Key="HeaderContent">
<StackPanel>
<TextBlock Text="Header Text"/>
<Rectangle Fill="Black" Width="25" Height="25"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="RightContent">
<StackPanel Grid.RowSpan="3">
<Rectangle Width="100" Height="50" Fill="DarkBlue"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<local:MasterPage HeaderContentArea="{StaticResource HeaderContent}" RightContentArea="{StaticResource RightContent}" />
Pretty simple right?
Well what if we don't bind to all the available content zones like in the example above? With the current setup the layout elements (in this case grid rows / columns) will still be rendered but they will be empty. This may or may not be the desired behavior for your situation.
Furthermore you don't want to build this nice layout template and still force your consumers to bind content that will be static across multiple consumers. So we want to provide default content for the content zones from within the generic.xaml.
Easy enough we just need to bind the content property of our zone to a static resource defined in generic.xaml right?
Something like this :
<DataTemplate x:Key="FooterContent">
<StackPanel>
<TextBlock Text="Copyright 2007. All rights reserved"/>
</StackPanel>
</DataTemplate>
<ContentControl Grid.Column="0" Grid.Row="2" x:Name="FooterContentArea" Content="{StaticResource FooterContent}"/>
This will work and will give you a distinct behavior. By doing this you are no longer binding to the dependency property defined in the control and therefore you are forcing the content zone to use the content defined in the generic.xaml. Your consumer will not be able override the content for this zone. This may or may not be the desired behavior.
For my situation I wanted to provide default content for the different zones but I also wanted to let the consumer override the content if they wanted to. I also wanted to provide specific defaults for zones that give the user a visual cue they have not bound to a "required" content zone.
First we defined default content for each of our zones in the generic.xaml file like this.
<DataTemplate x:Key="HeaderContent">Then we modified our control template to use Setters to set the dependency properties to the Static Resources in our generic.xaml.
<StackPanel>
<TextBlock Text="Header" FontSize="15" FontWeight="Bold"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="MainContent">
<StackPanel>
<TextBlock Opacity="0.25" FontSize="18" Text="CONTENT ERROR! Bind content to the MainContentArea dependency property" TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="RightContent">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Right Text"/>
<Rectangle Fill="AliceBlue" Width="25" Height="25">
<Rectangle.BitmapEffect>
<DropShadowBitmapEffect/>
</Rectangle.BitmapEffect>
</Rectangle>
<Ellipse Fill="YellowGreen" Width="25" Height="25"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="FooterContent">
<StackPanel>
<TextBlock Text="Copyright 2007. All rights reserved"/>
</StackPanel>
</DataTemplate>
<Style TargetType="{x:Type local:MasterPage}">
<Setter Property="HeaderContentArea" Value="{StaticResource HeaderContent}"/>
<Setter Property="MainContentArea" Value="{StaticResource MainContent}"/>
<Setter Property="RightContentArea" Value="{StaticResource RightContent}"/>
<Setter Property="FooterContentArea" Value="{StaticResource FooterContent}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MasterPage}">
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="225"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="25"></RowDefinition>
<RowDefinition Height="25"></RowDefinition>
</Grid.RowDefinitions>
<ContentControl Grid.Column="0" Grid.Row="0" x:Name="HeaderContentArea" ContentTemplate="{TemplateBinding HeaderContentArea}"/>
<ContentControl Grid.Column="0" Grid.Row="1" x:Name="MainContentArea" ContentTemplate="{TemplateBinding MainContentArea}"/>
<ContentControl Grid.Column="0" Grid.Row="2" x:Name="FooterContentArea" ContentTemplate="{TemplateBinding FooterContentArea}"/>
<ContentControl Grid.Column="1" Grid.RowSpan="3" x:Name="RightContentArea" ContentTemplate="{TemplateBinding RightContentArea}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Notice that we can now keep the content properties for each of our zones bound to the dependency properties we defined in our class. We have also set these properties with default values so if the consumer doesn't set them they will get the content we defined. This is nice because it allows us to move static content in to the custom control's generic.xaml and leaves less for the consumer to worry about.
Another nice feature is we are able to give the user a visual error if they do not bind to a zone that we feel they should bind to. In this case I have defined a main content area and defaulted the content to a textblock that shows an error:
<DataTemplate x:Key="MainContent">
<StackPanel>
<TextBlock Opacity="0.25" FontSize="18" Text="CONTENT ERROR! Bind content to the MainContentArea dependency property" TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
Right away the consumer will see the message that they forgot to bind to the content area and can quickly fix the error.
Overall this is a pretty simple solution that gives you a nice way to share a layout across multiple pages while still allowing the individual pages to control the content. This also makes it easy to change the layout of your pages in one place. You just modify the layout of the "content zones" in the master page and all your consumer using the control will see the change.
You can find the sample code here
Labels: WPF