How to defeat a bug with Grid.Row / Column in ItemsPanelTemplate?

一曲冷凌霜 提交于 2021-02-11 15:47:50

问题


Created a simple Attached property to simplify bindings from an element template. Instead of this:

<ItemsControl ItemsSource="{Binding Mode=OneWay, Source={StaticResource Points.Grid}}" 
              ItemsPanel="{StaticResource Grid.Panel}">
    <ItemsControl.ItemTemplate>
        <DataTemplate DataType="Point">
            <Ellipse Fill="Coral"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.ItemContainerStyle>
        <Style>
            <Setter Property="Grid.Row" Value="{Binding Y}"/>
            <Setter Property="Grid.Column" Value="{Binding X}"/>
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>

You can go this way:

<ItemsControl Grid.Row="1"
        ItemsSource="{Binding Mode=OneWay, Source={StaticResource Points.Grid}}"
        ItemsPanel="{StaticResource Grid.Panel}">
    <ItemsControl.ItemTemplate>
        <DataTemplate DataType="Point">
            <Ellipse Fill="LightBlue"
                             pa:Grid.Row="{Binding Y}"
                             pa:Grid.Column="{Binding X}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl> 

Here is the complete code the attached property:

public static partial class Grid
{
    public static int GetRow(FrameworkElement element)
    {
        return (int)element.GetValue(RowProperty);
    }

    public static void SetRow(FrameworkElement element, int value)
    {
        element.SetValue(RowProperty, value);
    }

    // Using a DependencyProperty as the backing store for Row.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty RowProperty =
        DependencyProperty.RegisterAttached("Row", typeof(int), typeof(Grid),
        new FrameworkPropertyMetadata
        (
            0,
            FrameworkPropertyMetadataOptions.AffectsParentArrange | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
            RowChanged
        ));

    private static void RowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is FrameworkElement element))
            throw new ArgumentException("Must be FrameworkElement", nameof(d));

        FrameworkElement parent;

        while ((parent = VisualTreeHelper.GetParent(element) as FrameworkElement) != null && !(parent is System.Windows.Controls.Grid))
            element = parent;

        if (parent is System.Windows.Controls.Grid grid)
            element.SetValue(System.Windows.Controls.Grid.RowProperty, (int)e.NewValue);
    }

    private static void GridLoaded(object sender, RoutedEventArgs e)
        => ((System.Windows.Controls.Grid)sender).InvalidateMeasure();

    public static int GetColumn(FrameworkElement element)
    {
        return (int)element.GetValue(ColumnProperty);
    }

    public static void SetColumn(FrameworkElement element, int value)
    {
        element.SetValue(ColumnProperty, value);
    }

    // Using a DependencyProperty as the backing store for Column.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ColumnProperty =
        DependencyProperty.RegisterAttached("Column", typeof(int), typeof(Grid),
            new FrameworkPropertyMetadata
            (
                0,
                FrameworkPropertyMetadataOptions.AffectsParentArrange | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                ColumnChanged
            ));

    private static void ColumnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is FrameworkElement element))
            throw new ArgumentException("Must be FrameworkElement", nameof(d));

        FrameworkElement parent;

        while ((parent = VisualTreeHelper.GetParent(element) as FrameworkElement) != null && !(parent is System.Windows.Controls.Grid))
            element = parent;

        if (parent is System.Windows.Controls.Grid grid)
            element.SetValue(System.Windows.Controls.Grid.ColumnProperty, (int)e.NewValue);
    }
}

The property is nothing complicated. With Canvas, the same works fine. And with the Grid, problems arise when attaching a collection with elements or when adding the first element to the collection - elements are displayed in the Grid without regard to their position. Although when viewing in the visual tree and in the properties browser, the attached properties of Grid.Row / Column are set correctly. And at the slightest change in the window, the elements fall into place.

In my opinion, a frank bug. But how to deal with it?

Full XAML Demo Code:

<Window x:Class="AttachedPropertiesWPF.BindParentWind"
        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:AttachedPropertiesWPF"
        mc:Ignorable="d"
        Title="BindParentWind" Height="450" Width="800"
        xmlns:pa="clr-namespace:AttachedProperties;assembly=AttachedProperties">
    <Window.Resources>
        <x:Array x:Key="Points.Grid" Type="Point">
            <Point X="1" Y="0"/>
            <Point X="0" Y="2"/>
            <Point X="2" Y="1"/>
        </x:Array>
        <ItemsPanelTemplate x:Key="Grid.Panel">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
            </Grid>
        </ItemsPanelTemplate>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <ItemsControl ItemsSource="{Binding Mode=OneWay, Source={StaticResource Points.Grid}}" 
                      ItemsPanel="{StaticResource Grid.Panel}">
            <ItemsControl.ItemTemplate>
                <DataTemplate DataType="Point">
                    <Ellipse Fill="Coral"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemContainerStyle>
                <Style>
                    <Setter Property="Grid.Row" Value="{Binding Y}"/>
                    <Setter Property="Grid.Column" Value="{Binding X}"/>
                </Style>
            </ItemsControl.ItemContainerStyle>
        </ItemsControl>
        <ItemsControl Grid.Row="1"
                      ItemsSource="{Binding Mode=OneWay, Source={StaticResource Points.Grid}}"
                      ItemsPanel="{StaticResource Grid.Panel}">
            <ItemsControl.ItemTemplate>
                <DataTemplate DataType="Point">
                    <Ellipse Fill="LightBlue"
                             pa:Grid.Row="{Binding Y}"
                             pa:Grid.Column="{Binding X}"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

Outputs like this:

And it should be like this:


回答1:


It seems to be kind of a timing issue that initially setting Grid.Row and Grid.Column on the ContentPresenter won't result in another layout cycle.

While it would obviously be a good idea to drop the whole helper class and set the Grid properties directly in an ItemContainerStyle, an ugly workaround would be to asynchronously set the Grid properties, like shown here:

public class ContentPresenterHelper
{
    public static readonly DependencyProperty ColumnProperty =
        DependencyProperty.RegisterAttached(
            "Column", typeof(int), typeof(ContentPresenterHelper),
            new PropertyMetadata(0, ColumnPropertyChanged));

    public static readonly DependencyProperty RowProperty =
        DependencyProperty.RegisterAttached(
            "Row", typeof(int), typeof(ContentPresenterHelper),
            new PropertyMetadata(0, RowPropertyChanged));

    public static int GetRow(DependencyObject o)
    {
        return (int)o.GetValue(RowProperty);
    }

    public static void SetColumn(DependencyObject o, int value)
    {
        o.SetValue(ColumnProperty, value);
    }

    public static int GetColumn(DependencyObject o)
    {
        return (int)o.GetValue(ColumnProperty);
    }

    public static void SetRow(DependencyObject o, int value)
    {
        o.SetValue(RowProperty, value);
    }

    private static void ColumnPropertyChanged(
        DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        o.Dispatcher.InvokeAsync(() =>
            FindContentPresenterParent(o)?.SetValue(
                Grid.ColumnProperty, (int)e.NewValue));
    }

    private static void RowPropertyChanged(
        DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        o.Dispatcher.InvokeAsync(() =>
            FindContentPresenterParent(o)?.SetValue(
                Grid.RowProperty, (int)e.NewValue));
    }

    private static ContentPresenter FindContentPresenterParent(DependencyObject element)
    {
        if (element == null)
        {
            return null;
        }

        var parent = VisualTreeHelper.GetParent(element);

        return (parent as ContentPresenter) ?? FindContentPresenterParent(parent);
    }
}



回答2:


Welcome to SO!

I'll be honest, there are many, many problems with this code, but I'll stick to what you've posted....

Clemens is correct, it looks like you're a bit confused as to how you should be positioning your elements on the grid. In your first ItemsControl you're doing it via the ItemContainerStyle, in the second one you're applying it directly to the Ellipse (albeit, confusingly, using your custom Grid helper DPs). What you do to the first control won't affect the second one, so of course the layout behavior you see will be different between them as well.

Item templates such as your ellipse don't get added to the parent panel container directly, they get encapsulated in a ContentPresenter. So your first control is doing it properly. Set the ItemContainerStyle in your second ItemsControl as well, remove the ap:Grid.Row and ap:Grid.Column setters from the Ellipse tag in your second control, and get rid of that Grid helper class altogether, you don't need it.




回答3:


Working implementation of Attached Properties:

public static partial class Grid
{
    public static int GetRow(FrameworkElement element)
    {
        return (int)element.GetValue(RowProperty);
    }

    public static void SetRow(FrameworkElement element, int value)
    {
        element.SetValue(RowProperty, value);
    }

    // Using a DependencyProperty as the backing store for Row.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty RowProperty =
        DependencyProperty.RegisterAttached("Row", typeof(int), typeof(Grid),
            new FrameworkPropertyMetadata
            (
                0,
                FrameworkPropertyMetadataOptions.AffectsParentArrange | FrameworkPropertyMetadataOptions.AffectsParentMeasure
                | FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure
                | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                RowChanged
            ));

    private static void RowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is FrameworkElement element))
            throw new ArgumentException("Must be FrameworkElement", nameof(d));

        FrameworkElement parent;

        while ((parent = VisualTreeHelper.GetParent(element) as FrameworkElement) != null && !(parent is System.Windows.Controls.Grid))
            element = parent;

        if (parent is System.Windows.Controls.Grid grid)
            element.Dispatcher.BeginInvoke((Action<FrameworkElement, DependencyProperty, object>)SetValueAsync, element, System.Windows.Controls.Grid.RowProperty, (int)e.NewValue);
    }

    private static void SetValueAsync(FrameworkElement element, DependencyProperty property, object value)
        => element.SetValue(property, value);

    public static int GetColumn(FrameworkElement element)
    {
        return (int)element.GetValue(ColumnProperty);
    }

    public static void SetColumn(FrameworkElement element, int value)
    {
        element.SetValue(ColumnProperty, value);
    }

    // Using a DependencyProperty as the backing store for Column.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ColumnProperty =
        DependencyProperty.RegisterAttached("Column", typeof(int), typeof(Grid),
            new FrameworkPropertyMetadata
            (
                0,
                FrameworkPropertyMetadataOptions.AffectsParentArrange | FrameworkPropertyMetadataOptions.AffectsParentMeasure
                | FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure
                | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                ColumnChanged
            ));

    private static void ColumnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is FrameworkElement element))
            throw new ArgumentException("Must be FrameworkElement", nameof(d));

        FrameworkElement parent;

        while ((parent = VisualTreeHelper.GetParent(element) as FrameworkElement) != null && !(parent is System.Windows.Controls.Grid))
            element = parent;

        if (parent is System.Windows.Controls.Grid grid)
            element.Dispatcher.BeginInvoke((Action<FrameworkElement, DependencyProperty, object>)SetValueAsync, element, System.Windows.Controls.Grid.ColumnProperty, (int)e.NewValue);
    }


}

I have not figured out the reasons for the incomprehensible behavior of the basу properties Grid.Row/Column. Later I will watch their source code. If I understand the reason, I'll post it here.



来源:https://stackoverflow.com/questions/62202419/how-to-defeat-a-bug-with-grid-row-column-in-itemspaneltemplate

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!