Search code examples
c#wpfxamldata-bindingwpf-controls

Applying Styles With Binding


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,


Solution

  • 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 Canvases. 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.