0

I have changed my view from simple Canvas to ItemsControl that uses Canvas, because I want to bind Canvas children to my ViewModel.

It was like this:

<Canvas x:Name="worksheetCanvas">   
    <local:BlockControl DataContext="{Binding x}"/>                         
    <local:BlockControl DataContext="{Binding y}"/>                         
    <local:BlockControl DataContext="{Binding z}"/>                         
</Canvas>

I "moved" step forward to MVVM and now I have this:

<ItemsControl x:Name="itemsControl" ItemsSource="{Binding Blocks}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas x:Name="worksheetCanvas">   
                <!-- Here I have some attached properties defined -->
            </Canvas>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="Canvas.Top" Value="{Binding BlockTop}"/>
            <Setter Property="Canvas.Left" Value="{Binding BlockLeft}"/>
        </Style>
    </ItemsControl.ItemContainerStyle>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <local:BlockControl/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

I have to access Canvas from code behind (I don't want pure MVVM, there will be some code behind). I have set x:Name property for Canvas inside ItemsPanelTemplate, but it doesn't work:

Error CS0103 The name 'worksheetCanvas' does not exist in the current context

I guess this is because Canvas is created after compilation and cannot be accessed like this.

What is the best (efficient) way to get my Canvas reference in this scenario?

Kamil
  • 13,363
  • 24
  • 88
  • 183
  • This answer by Fredrik Hedblad should work https://stackoverflow.com/a/4744947/3137337 You need to keep in mind that your Canvas won't be captured before Window.Loaded event. – emoacht Aug 24 '22 at 02:31

3 Answers3

1

You could create a derived ItemsControl (as a WPF custom control) with a Canvas as items host and a property that makes the Canvas accessible.

public class CanvasItemsControl : ItemsControl
{
    static CanvasItemsControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(CanvasItemsControl),
            new FrameworkPropertyMetadata(typeof(CanvasItemsControl)));
    }

    public Canvas Canvas { get; private set; }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        Canvas = Template.FindName("Canvas", this) as Canvas;
    }
}

Accessing the Canvas like this works with a default Style in Themes/Generic.xaml as shown below. It does not set the ItemsPanel property, but instead directly puts the hosting Canvas into the ControlTemplate of the ItemsControl.

<Style TargetType="{x:Type local:CanvasItemsControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ItemsControl">
                <Canvas x:Name="Canvas" IsItemsHost="True"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Your XAML would then look like this:

<local:CanvasItemsControl x:Name="itemsControl" ItemsSource="{Binding Blocks}">
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="Canvas.Top" Value="{Binding BlockTop}"/>
            <Setter Property="Canvas.Left" Value="{Binding BlockLeft}"/>
        </Style>
    </ItemsControl.ItemContainerStyle>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <local:BlockControl/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</local:CanvasItemsControl>

As soon as the Template has been applied, you are able to access the Canvas property, e.g. in a Loaded event handler of the Window:

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    itemsControl.Canvas.Background = Brushes.AliceBlue;
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • Thanks for good solution with clear explanation. One more question - when exactly Template is applied? Does it happen when Window (or UserControl) is being loaded, not during Window/UserControl initialization? – Kamil Aug 24 '22 at 19:03
  • Afaik, it happens before the Loaded event occurs, but after Initialized. – Clemens Aug 24 '22 at 19:05
1

You could use the VisualTreeHelper class to find the Canvas in the visual tree once the ItemsControl has been loaded, e.g.:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Loaded += OnLoaded;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        Canvas worksheetCanvas = FindVisualChild<Canvas>(itemsControl);
        //...
    }

    private static childItem FindVisualChild<childItem>(DependencyObject obj)
        where childItem : DependencyObject
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(obj, i);
            if (child != null && child is childItem)
            {
                return (childItem)child;
            }
            else
            {
                childItem childOfChild = FindVisualChild<childItem>(child);
                if (childOfChild != null)
                    return childOfChild;
            }
        }
        return null;
    }
}
mm8
  • 163,881
  • 10
  • 57
  • 88
  • I did this for a moment, but I feel that "digging" thru VisualTree too often eats too much resources. So I stored canvas refecence in some property and when I need it very often - I just read it. – Kamil Aug 24 '22 at 19:06
-1

You can create UserControl wrapper. And then access to canvas by Content property

<ItemsPanelTemplate>
    <local:MyCanvasWrapper>
        <Canvas x:Name="worksheetCanvas">   
            <!-- Here I have some attached properties defined -->
        </Canvas>
    </local:MyCanvasWrapper>
</ItemsPanelTemplate>

Code behind

public partial class MyCanvasWrapper : UserControl // Or ContentControl
{
    public MyCanvasWrapper()
    {
        InitializeComponent();
        Loaded += (s, e) => {
            var canvas = Content as Canvas;
        }
    }
}