Home > Back-end >  Binding between a template-generated object and its parent's property
Binding between a template-generated object and its parent's property

Time:09-17

The title of this question might be wrong, I am not sure how to phrase it. I am trying to implement a very simple dashboard in which users can drag controls around inside a Canvas control. I wrote a MoveThumb class that inherits Thumb to achieve this. It is working well enough. Now, I want to make sure that the user cannot move the draggable control outside the Canvas. It is simple enough to write the logic itself to limit the drag boundaries inside this MoveThumb class:

Public Class MoveThumb
    Inherits Thumb

    Public Sub New()
        AddHandler DragDelta, New DragDeltaEventHandler(AddressOf Me.MoveThumb_DragDelta)
    End Sub

    Private Sub MoveThumb_DragDelta(ByVal sender As Object, ByVal e As DragDeltaEventArgs)
        Dim item As Control = TryCast(Me.DataContext, Control)

        If item IsNot Nothing Then
            Dim left As Double = Canvas.GetLeft(item)
            Dim top As Double = Canvas.GetTop(item)
            Dim right As Double = left   item.ActualWidth
            Dim bottom As Double = top   item.ActualHeight

            Dim canvasWidth = 450
            Dim canvasHeight = 800

            If left   e.HorizontalChange > 0 Then
                If top   e.VerticalChange > 0 Then
                    If right   e.HorizontalChange < canvasWidth Then
                        If bottom   e.VerticalChange > canvasHeight Then
                            Canvas.SetLeft(item, left   e.HorizontalChange)
                            Canvas.SetTop(item, top   e.VerticalChange)
                        End If
                    End If
                End If
            End If
        End If
    End Sub
End Class

And the XML:

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:diagramDesigner"
        xmlns:s="clr-namespace:diagramDesigner"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Canvas x:Name="Canvas1">
            <Canvas.Resources>
                <ControlTemplate x:Key="MoveThumbTemplate" TargetType="{x:Type s:MoveThumb}">
                    <Rectangle Fill="Transparent"/>
                </ControlTemplate>
                
                <ControlTemplate x:Key="DesignerItemTemplate" TargetType="ContentControl">
                    <Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
                        <s:MoveThumb Template="{StaticResource MoveThumbTemplate}" Cursor="SizeAll"/>
                        <ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
                    </Grid>
                </ControlTemplate>

            </Canvas.Resources>
            
            <ContentControl Name="DesignerItem"
                   Width="100"
                   Height="100"
                   Canvas.Top="100"
                   Canvas.Left="100"
                   Template="{StaticResource DesignerItemTemplate}">
                <Ellipse Fill="Blue" IsHitTestVisible="False"/>
            </ContentControl>
        </Canvas>
    </Grid>
</Window>

Problem is, I am explicitly stating the width and height of the canvas inside that MoveThumb class, which means that if my window changes size, and the Canvas changes size, the drag boundaries will remain the same. Ideally, I want to bind canvasWidth and canvasHeight to the actualWidth and actualHeight of the Canvas.

I am not sure what is the best way to achieve it. Acquiring the actualWidth and actualHeight values inside the MoveThumb_DragDelta function with something like actualWidth = mainWindow.Canvas1.ActualWidth would be quick and simple, but very bad coding practice.

Ideally, I imagine it would be best to pass the limits as arguments to the constructor of MoveThumb and stored as global field/property, but I don't see a way to do it, since this class is used as a template in the XML code, and not generated from code-behind. I am not exactly sure if that would work at all, because the MoveThumb might be instantized only once (during the creation of the control), so it won't work when the Canvas changes it's size afterwards.

So I probably should do some kind of one-way binding between the actualWidth of Canvas1 and canvasWidth (declared as global property) of MoveThumb. But again, I have no idea how to access it, since MoveThumb is used as a TargetType of ControlTemplate inside Canvas.Resources.

I am still pretty new to WPF, and it feels like there should be some very simple way to achieve this, but I'm not seeing it. Can anyone help?

CodePudding user response:

Example using Dependency Properties:

public partial class MoveThumb : Thumb
{
    private double privateCanvasWidth = double.NaN, privateCanvasHeight = double.NaN;
    private static readonly Binding bindingActualWidth = new Binding()
    {
        Path = new PropertyPath(ActualWidthProperty),
        RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(Canvas), 1)
    };
    private static readonly Binding bindingActualHeight = new Binding()
    {
        Path = new PropertyPath(ActualHeightProperty),
        RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(Canvas), 1)
    };

    public MoveThumb()
    {
        DragDelta  = MoveThumb_DragDelta;
        SetBinding(CanvasWidthProperty, bindingActualWidth);
        SetBinding(CanvasHeightProperty, bindingActualHeight);
    }

    static MoveThumb()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(MoveThumb), new FrameworkPropertyMetadata(typeof(MoveThumb)));
    }
    private static void MoveThumb_DragDelta(object sender, DragDeltaEventArgs e)
    {
        MoveThumb thumb = (MoveThumb)sender;
        //FrameworkElement item = thumb.MovableContent;
        //if (item == null)
        //{
        //    return;
        //}

        double left = Canvas.GetLeft(thumb);
        double top = Canvas.GetTop(thumb);
        double right = left   thumb.ActualWidth;
        double bottom = top   thumb.ActualHeight;

        double canvasWidth = thumb.privateCanvasWidth;
        if (double.IsNaN(canvasWidth))
            canvasWidth = 450;
        double canvasHeight = thumb.privateCanvasHeight;
        if (double.IsNaN(canvasHeight))
            canvasWidth = 800;

        left  = e.HorizontalChange;
        top  = e.VerticalChange;
        right  = e.HorizontalChange;
        bottom  = e.VerticalChange;

        if (left > 0 &&
            top > 0 &&
            right < canvasWidth &&
            bottom < canvasHeight)
        {
            Canvas.SetLeft(thumb, left);
            Canvas.SetTop(thumb, top);
        }
    }
}
// DependecyProperties
[ContentProperty(nameof(MovableContent))]
public partial class MoveThumb
{

    /// <summary>Canvas Width.</summary>
    public double CanvasWidth
    {
        get => (double)GetValue(CanvasWidthProperty);
        set => SetValue(CanvasWidthProperty, value);
    }

    /// <summary><see cref="DependencyProperty"/> for property <see cref="CanvasWidth"/>.</summary>
    public static readonly DependencyProperty CanvasWidthProperty =
        DependencyProperty.Register(nameof(CanvasWidth), typeof(double), typeof(MoveThumb), new PropertyMetadata(double.NaN, CanvasSizeChanged));


    /// <summary>Canvas Height.</summary>
    public double CanvasHeight
    {
        get => (double)GetValue(CanvasHeightProperty);
        set => SetValue(CanvasHeightProperty, value);
    }

    /// <summary><see cref="DependencyProperty"/> for property <see cref="CanvasHeight"/>.</summary>
    public static readonly DependencyProperty CanvasHeightProperty =
        DependencyProperty.Register(nameof(CanvasHeight), typeof(double), typeof(MoveThumb), new PropertyMetadata(double.NaN, CanvasSizeChanged));

    // Property change handler.
    // The code is shown as an example.
    private static void CanvasSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        MoveThumb thumb = (MoveThumb)d;

        if (e.Property == CanvasWidthProperty)
        {
            thumb.privateCanvasWidth = (double)e.NewValue;
        }
        else if (e.Property == CanvasHeightProperty)
        {
            thumb.privateCanvasHeight = (double)e.NewValue;
        }
        else
        {
            throw new Exception("God knows what happened!");
        }

        MoveThumb_DragDelta(thumb, new DragDeltaEventArgs(0, 0));
    }


    /// <summary>Movable content.</summary>
    public FrameworkElement MovableContent
    {
        get => (FrameworkElement)GetValue(MovableContentProperty);
        set => SetValue(MovableContentProperty, value);
    }

    /// <summary><see cref="DependencyProperty"/> for property <see cref="MovableContent"/>.</summary>
    public static readonly DependencyProperty MovableContentProperty =
        DependencyProperty.Register(nameof(MovableContent), typeof(FrameworkElement), typeof(MoveThumb), new PropertyMetadata(null));


}

In the Project add the theme "Themes\Generic.xaml":

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:customcontrols="clr-namespace:EmbedContent.CustomControls"
    xmlns:s="clr-namespace:Febr20y">

    <Style TargetType="{x:Type s:MoveThumb}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type s:MoveThumb}">
                    <ContentPresenter Content="{TemplateBinding MovableContent}"/>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
    <Grid>
        <Canvas x:Name="Canvas1">
            <s:MoveThumb x:Name="DesignerItem"
                   Width="100"
                   Height="100"
                   Canvas.Top="100"
                   Canvas.Left="100">
                <Ellipse Fill="Blue"/>
            </s:MoveThumb>
      </Canvas>
    </Grid>

Video YouTube
Source Code

  • Related