【问题标题】:How to defeat a bug with Grid.Row / Column in ItemsPanelTemplate?如何解决 ItemsPanelTemplate 中 Grid.Row / Column 的错误?
【发布时间】:2020-06-04 19:11:52
【问题描述】:

创建了一个简单的附加属性来简化元素模板的绑定。 而不是这个:

<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> 

这是附加属性的完整代码:

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);
    }
}

该属性并不复杂。 使用 Canvas,同样可以正常工作。 而对于 Grid,在使用元素附加集合或将第一个元素添加到集合时会出现问题 - 元素显示在 Grid 中而不考虑它们的位置。 尽管在可视化树和属性浏览器中查看时,Grid.Row / Column 的附加属性设置正确。 并且在窗口发生最轻微的变化时,元素就会到位。

在我看来,一个坦率的错误。 但是怎么处理呢?

完整的 XAML 演示代码:

<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>

这样的输出:

它应该是这样的:

【问题讨论】:

  • 这与简化完全相反。放下它,认真地继续使用 ItemContainerStyle 中的绑定。您的帮助程序类被命名为 Grid,这非常令人困惑。
  • 除此之外,这不是错误。你的假设是错误的。设置 AffectsParentArrange 无效,因为 Grid 不是 Ellipse 的直接父级(在其中设置附加属性)。中间有一个 ItemsPresenter。
  • "Grid 不是 Ellipse 的直接父级 ..." - 我知道。注意方法 Row/ColumnChanged。设置值发生在其中。我的属性只是访问基本 Grod.Row/Column 的一种便捷方式。它们附加到直接在面板(网格)中的元素(ContentPresenter)。在动态可视化树中,所有这些都是可见的,基本属性具有我所期望的含义。但外观与属性值不匹配。
  • 总的来说,我同意@Clemens 和 Mark Feldman 的观点,即 Grid 助手和 AP 方法在这里并没有真正的好处,但是为了考虑用 AP 解决这个问题的想法: - 在代码中,我们可以看到在更改处理程序中,在 ContentPresenter 上设置了 Grid.Row/Column,就像在第一个版本中所做的那样(没有 AP)。 (虽然没有明确说明,因此很难在代码中发现这种行为) - 我在本地尝试过,如果我在绑定中将 IsAsync 设置为 true,它对我来说可以正常工作。显然这不是正确的解决方案,但可以指示真正的潜在问题是什么。

标签: wpf grid row attached-properties itemspaneltemplate


【解决方案1】:

最初在 ContentPresenter 上设置 Grid.RowGrid.Column 似乎是一种时间问题,不会导致另一个布局周期。

虽然删除整个辅助类并直接在 ItemContainerStyle 中设置 Grid 属性显然是个好主意,但一个丑陋的解决方法是异步设置 Grid 属性,如下所示:

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】:

    欢迎来到 SO!

    说实话,这段代码有很多很多问题,但我会坚持你发布的内容......

    Clemens 是正确的,看起来您对如何在网格上定位元素有点困惑。在您的第一个 ItemsControl 中,您通过 ItemContainerStyle 执行此操作,在第二个中,您将其直接应用于 Ellipse(尽管使用您的自定义 Grid helper DPs 会令人困惑)。您对第一个控件所做的操作不会影响第二个控件,因此您看到的布局行为当然也会在它们之间有所不同。

    诸如椭圆之类的项目模板不会直接添加到父面板容器中,它们会封装在 ContentPresenter 中。所以你的第一个控制是正确的。同样在第二个 ItemsControl 中设置 ItemContainerStyle,从第二个控件的 Ellipse 标记中删除 ap:Grid.Row 和 ap:Grid.Column 设置器,然后完全摆脱那个 Grid 帮助器类,你不需要它。

    【讨论】:

    【解决方案3】:

    附加属性的工作实现:

    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);
        }
    
    
    }
    

    我还没有弄清楚基本属性 Grid.Row/Column 的不可理解行为的原因。 稍后我会看他们的源代码。 如果我明白原因,我会在这里发布。

    【讨论】:

      猜你喜欢
      • 2019-06-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-10-06
      • 2020-11-01
      • 2016-12-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多