Search code examples
wpfdatatemplateattached-properties

Make attached properties work inside DataTemplate


I got an ItemsControl which uses a Canvas as ItemsPanel and its items are rendered to different WPF shapes depending on the bound type, basically like this:

<ItemsControl  ItemsSource="{Binding PreviewShapes}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
    <ItemsControl.Resources>
        <DataTemplate DataType="{x:Type local:UiPreviewLineViewModel}">
            <Line X1="{Binding Start.X}" Y1="{Binding Start.Y}"
                        X2="{Binding End.X}" Y2="{Binding End.Y}" 
                        StrokeThickness="0.75" Stroke="{Binding Brush}" x:Name="Line" ToolTip="{Binding Text}">
            </Line>
        </DataTemplate>
        <DataTemplate DataType="{x:Type local:UiPreviewEllipsisViewModel}">
            <Ellipse Canvas.Left="{Binding UpperLeft.X" Canvas.Top="{Binding UpperLeft.Y}" 
                     Width="{Binding Width}" Height="{Binding Height}" 
                     StrokeThickness="0.75" Stroke="{Binding Brush}" x:Name="Ellipse" ToolTip="{Binding Text}">
            </Ellipse>
        </DataTemplate>
    </ItemsControl.Resources>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas IsItemsHost="True" HorizontalAlignment="Center" VerticalAlignment="Center" x:Name="SketchCanvas" ClipToBounds="False">
            </Canvas>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

So I basically add objects to PreviewShapes of the viewmodel and depending on the type they are rendered to WPF Lines or Ellipses. That basically works but the attached properties Canvas.Left and Canvas.Top are ignored, even when using static values.

Also VS or ReSharper notifies me that the attached property has no effect in the current context.

How can I position the Ellipse on the canvas without using the attached properties? Or what other solution would be appropiate?


Solution

  • Unfortunately nobody felt like posting an answer.

    First, Clemens links are helpful. The items will be inside a ContentPresenter which is the reason why setting Canvas.Left/Top on the Ellipsis does not work.

    Solution 1

    By adding a style to the item container the bindings for the position can be set there:

    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="Canvas.Left" Value="{Binding UpperLeft.X}" />
            <Setter Property="Canvas.Top" Value="{Binding UpperLeft.Y}" />
        </Style>
    </ItemsControl.ItemContainerStyle>
    

    This works but the DataTemplate placing <Line> will produce binding warnings because that view model does not have a property UpperLeft. Nevertheless it works for the ellipsis and the lines are placed by their X1, Y1, X2 and Y2 values.

    Solution 2

    If you would like to use a more fine grained control approach you can set the attached Canvas properties to the ContentPresenter by proxing them with a custom behaviour / attached property. Let's name it CanvasPointProxyBehavior, you could use it for the Ellipse like this:

    <DataTemplate DataType="{x:Type local:UiPreviewEllipsisViewModel}">
        <Ellipse behaviors:CanvasPointProxyBehavior.Point="{Binding UpperLeft}"
                 Width="{Binding Width}" Height="{Binding Height}" 
                 StrokeThickness="0.75" Stroke="{Binding Brush}" x:Name="Ellipse" ToolTip="{Binding Text}">
        </Ellipse>
    </DataTemplate>
    

    The Line will not need it. The code for this attached property might look like this:

    public class CanvasPointProxyBehavior
    {
        public static readonly DependencyProperty PointProperty = DependencyProperty.RegisterAttached("Point", typeof(Point), typeof(CanvasPointProxyBehavior), new UIPropertyMetadata(null, PointChangedCallback));
    
        public static void SetPoint(DependencyObject depObj, Point point)
        {
            depObj.SetValue(PointProperty, point);
        }
    
        public static Point GetPoint(DependencyObject depObj)
        {
            return depObj.GetValue(PointProperty) as Point;
        }
    
        private static void PointChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            UIElement uiElement = (dependencyObject as UIElement);
            if (uiElement == null) return;
            UIElement elementToBePositioned = uiElement;
            var visualParent = VisualTreeHelper.GetParent(uiElement);
            if (visualParent is ContentPresenter)
            {
                elementToBePositioned = visualParent as ContentPresenter;
            }
    
            var point = e.NewValue as Point;
            if (point != null)
            {
                Canvas.SetLeft(elementToBePositioned, point.X);
                Canvas.SetTop(elementToBePositioned, point.Y);
            }
        }
    }
    

    Hoping someone will find one or both solution useful.