Combining Viewbox, Canvas, ItemsPanelTemplate and ItemsPresenter in a user control inheriting from ItemsPanel

866 views Asked by At

I want to extend ItemsPanel so that I can display a "layered" visual structure where I have a "frame" with a known size and a lot of overlays, similar to what a cartographic or illustration application would be.

The problem I am having is to find out how to combine things so that everything works as expected. What I have done so far:

  • Created a control which inherits from ItemsControl;
  • Inside the control, put a Viewbox containing an ItemsPresenter
  • In the control's Resources, created a Style targeting its own type, setting the ItemsPanel to an ItemsTemplate consisting of a Canvas.

So I would expect that, under Live Tree Inspection, I should see, in a nested structure:

  • LayerContainer (the class name of my control)
    • ViewBox
      • ItemsPresenter
      • Canvas
        • Item1
        • Item2

Instead, what I see is this:

  • LayerContainer
    • Border
      • ItemsPresenter
      • Canvas
        • Viewbox
        • Item1
        • Item2

So the problem is that the ViewBox is contained inside Canvas, alongside the rendered items.

My question then would be: how do I structure my LayerContainer control in a way that the nesting order is ItemsPresenter->Viewbox->Canvas->Items?

Here is my control (the name is not actually LayerContainer)

<ItemsControl x:Class="Miotec.PressureMapping.UserControls.BaroLayerContainer"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
            xmlns:local="clr-namespace:Miotec.PressureMapping.UserControls"
            mc:Ignorable="d" 
            d:DesignHeight="450" d:DesignWidth="800">

    <ItemsControl.Resources>
        <Style TargetType="local:BaroLayerContainer">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <Canvas Width="{Binding Parametros.Colunas}"
                                Height="{Binding Parametros.Linhas}"
                                IsItemsHost="True"/>
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ItemsControl.Resources>

    <Viewbox Stretch="Uniform" x:Name="container">
        <ItemsPresenter
            Width="{Binding ActualWidth, ElementName=container}"
            Height="{Binding ActualHeight, ElementName=container}"/>
    </Viewbox>
</ItemsControl>
2

There are 2 answers

1
Mark Feldman On

If you want a working example of how to do this then check out my Perfy editor on GitHub, the relevant part is in the MainWindow.xaml file. It also shows how to implementing zoom and scrolling (if you want to support both then you don't actually need a ViewBox, just a ScrollViewer parent and a LayoutTransform on the ItemsControl).

To answer your question, each item gets wrapped in a ContentPresenter, the trick is to set your Canvas position on this parent item instead. ItemsControl exposes the ItemContainerStyle which allows you to do just that:

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

In particular, note that you do not need to explicitly declare an ItemsPresenter yourself, that's done for you by virtue of the fact that you're already using an ItemsControl to begin with. Just set your ItemsPanel to Canvas, set the style of the ContentPresenter via ItemContainerStyle and then use DataTemplates and/or triggers to specify the look of the collection items themselves.

0
heltonbiker On

After some thinking and googling, I realized that I could "override" the Template property of my control, with a ControlTemplate where I would put the Viewbox as root and, inside it, the ItemsPresenter. Then upon rendering, each item would be placed in the ItemsPanelTemplate containing the Canvas, assigned to the ItemsPanel property.

So below is the final working form. The solution principle was to set also the Template property (for the ViewBox) besides the ItemsPanel property (for the Canvas).

<ItemsControl x:Class="Miotec.PressureMapping.UserControls.BaroLayerContainer"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
            xmlns:local="clr-namespace:Miotec.PressureMapping.UserControls"
            mc:Ignorable="d" 
            d:DesignHeight="450" d:DesignWidth="800">

    <ItemsControl.Template>
        <ControlTemplate TargetType="ItemsControl">
            <Grid x:Name="container">
                <Viewbox Stretch="Uniform" 
                        Width="{Binding ActualWidth, ElementName=container}"
                        Height="{Binding ActualHeight, ElementName=container}">
                    <ItemsPresenter/>
                </Viewbox>
            </Grid>
        </ControlTemplate>
    </ItemsControl.Template>

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas Width="{Binding Parametros.Colunas}"
                    Height="{Binding Parametros.Linhas}"
                    IsItemsHost="True"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

</ItemsControl>