I am trying to create a custom control called PieceImage
that contains one of several predefined Canvas
elements. I have defined these Canvases in a ResourceDictionary
called PieceImageDictionary.xaml. I have set up some dependency properties in PieceImage
, Color1
and Color2
, that I'd like to bind to the Fill
and Stroke
of the paths in the Canvases. I have a pretty good understanding of custom controls and simple databinding but styles are a little confusing to me.
So basically I have a ResourceDictionary
with Canvases that I am treating as images and I want be able to have multiple PieceImage
Control
instances that can each choose one of these images independently (not a global style) and I want to be able to set the colors using a DependencyProperty
on the PieceImage
Controls.
I have tried wrapping my Canvases in ControlTemplates
but for whatever reason "Canvas
" is not a valid TargetType
.
I got it to apply this template if I set the TargetType
to "Control
" however when I tried adding TemplateBinding
to the Paths
, the Property I wanted to set (Color1)
couldn't be found. I kind of understand this because the Color1
property is in my PieceImage
control, so TemplateBinding
to a Control
or even a Canvas
won't work.
I then tried RelativeBinding
with an AncestorType
of "PieceImage
" but this doesn't work either. No error, just a blank Canvas
. I've tried calling UpdateLayout()
and InvalidateVisual()
on the Canvas
just in case but no change.
I tried using a XamlReader
and Writer to create these canvases instead of using templates, which initially worked when I statically defined the colors but when I tried to add the RelativeBinding
I got a Xaml Parse Error saying it couldnt create the type from the string "local:PieceImage
"
I tried adding a binding in the code but also got a blank Canvas
. I will admit I don't know much about coding bindings, this was my implementation:
var binding = new Binding(Color1Property.Name);
binding.RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent);
BindingOperations.SetBinding(path, Path.FillProperty, binding);
Here is my base code, I've cleaned out all the failed attempts for clarity because it would otherwise be a mess.
PieceImage.cs
public class PieceImage : Control
{
private Canvas _canvas;
static PieceImage()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(PieceImage), new FrameworkPropertyMetadata(typeof(PieceImage)));
}
public static readonly DependencyProperty Color1Property = DependencyProperty.Register(nameof(Color1), typeof(Brush), typeof(PieceImage), new PropertyMetadata(Brushes.BlanchedAlmond));
public static readonly DependencyProperty Color2Property = DependencyProperty.Register(nameof(Color2), typeof(Brush), typeof(PieceImage), new PropertyMetadata(Brushes.DarkGray));
public static readonly DependencyProperty PieceTypeProperty = DependencyProperty.Register(nameof(PieceType), typeof(Enums.PieceType), typeof(PieceImage), new PropertyMetadata(Enums.PieceType.Pawn));
public static readonly DependencyProperty SwapColorsProperty = DependencyProperty.Register(nameof(SwapColors), typeof(bool), typeof(PieceImage), new PropertyMetadata(false));
public Brush Color1
{
get { return (Brush)GetValue(Color1Property); }
set { SetValue(Color1Property, value); }
}
public Brush Color2
{
get { return (Brush)GetValue(Color2Property); }
set { SetValue(Color2Property, value); }
}
public Enums.PieceType PieceType
{
get { return (Enums.PieceType)GetValue(PieceTypeProperty); }
set { SetValue(PieceTypeProperty, value); }
}
public bool SwapColors
{
get { return (bool)GetValue(SwapColorsProperty); }
set { SetValue(SwapColorsProperty, value); }
}
public override void OnApplyTemplate()
{
_canvas = Template.FindName("PART_Canvas", this) as Canvas;
//Here is where most of my logic would go when I was loading xaml or coding bindings
base.OnApplyTemplate();
}
}
Generic.xaml (partial)
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Wagner.Chess.UI.Controls">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="\pieceimagedictionary.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style TargetType="{x:Type local:PieceImage}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:PieceImage">
<Canvas x:Name="PART_Canvas"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
PieceImageDictionary.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Wagner.Chess.UI.Controls">
<Canvas Height="64" Width="64" x:Key="BishopImage">
<Path Fill="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color1}"
Stroke="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color2}"
StrokeThickness="1.5" StrokeMiterLimit="1" StrokeLineJoin="Round">
<Path.Data>
<GeometryGroup>
<PathGeometry Figures="M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38
C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99
6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z"/>
<PathGeometry Figures="M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5
22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z"/>
<PathGeometry Figures="M 25 8 A 2.5 2.5 0 1 1 20,8 A 2.5 2.5 0 1 1 25 8 z"/>
</GeometryGroup>
</Path.Data>
</Path>
<Path Stroke="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color2}"
StrokeThickness="1.5" StrokeMiterLimit="1" StrokeEndLineCap="Round" StrokeStartLineCap="Round" StrokeLineJoin="Miter">
<Path.Data>
<GeometryGroup>
<PathGeometry Figures="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" />
</GeometryGroup>
</Path.Data>
</Path>
<Canvas.RenderTransform>
<ScaleTransform ScaleX="1.42222222222" ScaleY="1.42222222222"/>
</Canvas.RenderTransform>
</Canvas>
<Canvas Height="64" Width="64" x:Key="KnightImage">
<Path Fill="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color1}"
Stroke="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color2}"
StrokeThickness="1.5" StrokeMiterLimit="1" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
<Path.Data>
<GeometryGroup>
<PathGeometry Figures="M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18"/>
<PathGeometry Figures="M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,
30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L
18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10"/>
</GeometryGroup>
</Path.Data>
</Path>
<Path Fill="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color2}"
Stroke="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color2}"
StrokeThickness="0.5" StrokeMiterLimit="1" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
<Path.Data>
<GeometryGroup>
<PathGeometry Figures="M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z"/>
<PathGeometry Figures="M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z">
<PathGeometry.Transform>
<MatrixTransform Matrix="0.866,0.5,-0.5,0.866,9.693,-5.173"/>
</PathGeometry.Transform>
</PathGeometry>
</GeometryGroup>
</Path.Data>
</Path>
<Canvas.RenderTransform>
<ScaleTransform ScaleX="1.42222222222" ScaleY="1.42222222222"/>
</Canvas.RenderTransform>
</Canvas>
</ResourceDictionary>
I was hoping someone could provide some tips or point me in the right direction because MSDocs have guides for StyleSelectors
and DataTemplateSelectors
both of which are more for data item styling rather than setting another variable control using a style.
Thanks,
If I understand your issue correctly, you want to show a piece Canvas
based on the PieceType
in a your PieceImage
control. Then, you do not have to nest a Canvas
inside another Canvas
. You can replace the Canvas
in your PieceImage
template with a ContentPresenter
(PART_ContentPresenter
).
<Style TargetType="{x:Type local:PieceImage}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:PieceImage">
<ContentPresenter x:Name="PART_ContentPresenter"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
In your PieceImage
control, you can then search for the corresponding PieceType
resource (Canvas
) using the FindResource
method and assign it to the ContentPresenter
. Here I assume that we can map the PieceType
using the pattern <Enum Constant Name>Image
. If it is more complex, you could use a dictionary or a custom converter.
public class PieceImage : Control
{
private ContentPresenter _contentPresenter;
// ...your constructor, dependency property definitions, etc..
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_contentPresenter = Template.FindName("PART_ContentPresenter", this) as ContentPresenter;
if (_contentPresenter != null)
_contentPresenter.Content = FindPieceImage();
}
private Canvas FindPieceImage()
{
var pieceTypeName = PieceType.ToString();
var pieceTypeCanvasKey = $"{pieceTypeName}Image";
return FindResource(pieceTypeCanvasKey) as Canvas;
}
}
Since the original Canvas
in the control template is gone, remove all the AncestorLevel=2
attributes in your piece images, otherwise the binding source will not be found. Furthermore, add x:Shared="False"
to your Canvas
es. This is important, because your piece images are now potentially used multiple times (I guess your are building a chess board), but without setting x:Shared
to false
the same instances will be reused.
When set to
false
, modifies WPF resource-retrieval behavior so that requests for the attributed resource create a new instance for each request instead of sharing the same instance for all requests.
This is problematic, because each control can only have a single parent element. This means that when you would assign the same Canvas
instance multiple times, only the last element will get it as child. You can imagine a chess board where only the last assinged pieces are shown and the rest of the board is empty. Here is an excerpt of the modifications to your code.
<Canvas Height="64" Width="64" x:Key="BishopImage" x:Shared="False">
<Path Fill="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,Mode=FindAncestor},Path=Color1}"
Stroke="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,Mode=FindAncestor},Path=Color2}"
StrokeThickness="1.5" StrokeMiterLimit="1" StrokeLineJoin="Round">
<Path.Data>
<GeometryGroup>
<PathGeometry Figures="M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38
C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99
6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z"/>
<PathGeometry Figures="M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5
22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z"/>
<PathGeometry Figures="M 25 8 A 2.5 2.5 0 1 1 20,8 A 2.5 2.5 0 1 1 25 8 z"/>
</GeometryGroup>
</Path.Data>
</Path>
<!-- ...other markup code. -->
The bindings will work now and resolve to PieceImage
without any additional code or markup.