如何在 ItemsSource 集合中的 属性 更改时重绘 ItemsControl ItemsPanel?

How to redraw ItemsControl ItemsPanel when property in the ItemsSource collection is changed?

我制作了一个自定义的 WeightedUniformGrid class,我将其用作 ItemsControl 中的 ItemsPanel。使用 ItemsSource 集合中的对象中的权重 属性 对每个网格元素进行加权。

但是当我在视图模型中更改权重 属性 时,它不会立即显示在视图中。我必须更改 window 的大小,以便使用新值绘制 WeightedUniformGrid。

如何在 ItemsSource 集合中进行 属性 更改以重绘 ItemsControl?我在想也许在 WeightedUniformGrid 中添加一个 DependencyProperty,这将同时影响 AffectsArrange 和 AffectsMeasure。但我不确定我可以将它绑定到什么。

我搜索了好久,有一些类似的问题(比如 一个)。但我无法根据自己的需要调整其中任何一个。

MainWindow.xaml:

<ItemsControl ItemsSource="{Binding Path=ElementGroupCollection}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <local:WeightedUniformGrid Rows="2" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Border BorderBrush="Black" BorderThickness="1"
                    RenderOptions.EdgeMode="Aliased">
                <Viewbox Stretch="Uniform">
                    <TextBlock Text="{Binding Path=Number}" Foreground="Black" FontSize="20" 
                                    HorizontalAlignment="Center" VerticalAlignment="Center"/>
                </Viewbox>
            </Border>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

MainWindowVM.cs:

public class MainWindowVM
{
    public MainWindowVM()
    {
        _elementGroupCollection = new ObservableCollection<ElementGroup>()
        {
            new ElementGroup(1, 60),
            new ElementGroup(2, 150),
            new ElementGroup(3, 90),
            new ElementGroup(4, 80),
            new ElementGroup(5, 60),
            new ElementGroup(6, 160)
        };
    }

    private ObservableCollection<ElementGroup> _elementGroupCollection;
    public ObservableCollection<ElementGroup> ElementGroupCollection
    {
        get { return _elementGroupCollection; }
    }
}

ElementGroup.cs:

public class ElementGroup : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public ElementGroup(int number, double? weight)
    {
        Number = number;
        Weight = (weight >= 0) ? weight : null;
    }

    public int Number { get; }

    private double? _weight;
    public double? Weight
    {
        get { return _weight; }
        set { SetNotify(ref _weight, value); }
    }

    public void SetNotify<T>(ref T storage,
                             T value,
                             [CallerMemberName] string propertyName = null)
    {
        storage = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

WeightedUniformGrid.cs:

public class WeightedUniformGrid : UniformGrid
{

    protected override Size MeasureOverride(Size constraint)
    {
        var size = base.MeasureOverride(constraint);
        double elementsPerRow = Math.Ceiling((double)Children.Count / Rows);
        double elementHeight = size.Height / Rows;
        double unweightedElementWidth = size.Width / elementsPerRow;
        for (int i = 0; i < Children.Count; ++i)
        {
            var child = (FrameworkElement)Children[i];
            var dc = child.DataContext;
            int rowNumber = (int)Math.Floor(i / elementsPerRow);
            double? weight = dc.GetType().GetProperty("Weight")?.GetValue(dc, null) as double?;
            if (weight == null) { weight = 100; }
            double weightedElementWidth = unweightedElementWidth * (double)weight / 100;
            child.Measure(new Size(weightedElementWidth, elementHeight));
        }
        return size;
    }

    protected override Size ArrangeOverride(Size arrangeSize)
    {
        var size = base.ArrangeOverride(arrangeSize);
        int elementsPerRow = (int)Math.Ceiling((double)Children.Count / Rows);
        double elementHeight = size.Height / Rows;
        double unweightedElementWidth = size.Width / elementsPerRow;
        double[] accumulatedWidthPerRow = new double[Rows];
        for (int i = 0; i < Children.Count; ++i)
        {
            var child = (FrameworkElement)Children[i];
            var dc = child.DataContext;
            int rowNumber = i / elementsPerRow;
            double? weight = dc.GetType().GetProperty("Weight")?.GetValue(dc, null) as double?;
            if (weight == null) { weight = 100; }
            double weightedElementWidth = unweightedElementWidth * (double)weight / 100;
            child.Arrange(new Rect(new Point(accumulatedWidthPerRow[rowNumber], rowNumber * elementHeight),
                                    new Point(accumulatedWidthPerRow[rowNumber] + weightedElementWidth, (rowNumber + 1) * elementHeight)));
            accumulatedWidthPerRow[rowNumber] += weightedElementWidth;
        }
        return size;
    }
}

您的 WeightedUniformGrid 不得直接访问其子元素的 DataContext 中的权重 属性。除此之外,这是一种不好的做法,它不会正常工作,因为当视图模型项目的权重发生变化时,没有强制布局传递的机制。

应该有一个绑定到权重的附加 属性。附加的 属性 的 FrameworkPropertyMetadataOptions 将强制布局传递。

public class WeightedUniformGrid : UniformGrid
{
    public static readonly DependencyProperty WeightProperty =
        DependencyProperty.RegisterAttached(
            "Weight", typeof(double), typeof(WeightedUniformGrid),
            new FrameworkPropertyMetadata(double.NaN,
                FrameworkPropertyMetadataOptions.AffectsParentMeasure |
                FrameworkPropertyMetadataOptions.AffectsParentArrange));

    public static double GetWeight(UIElement element)
    {
        return (double)element.GetValue(WeightProperty);
    }

    public static void SetWeight(UIElement element, double value)
    {
        element.SetValue(WeightProperty, value);
    }

    protected override Size MeasureOverride(Size constraint)
    {
        var size = base.MeasureOverride(constraint);
        double elementsPerRow = Math.Ceiling((double)Children.Count / Rows);
        double elementHeight = size.Height / Rows;
        double unweightedElementWidth = size.Width / elementsPerRow;
        for (int i = 0; i < Children.Count; ++i)
        {
            var child = Children[i];
            int rowNumber = (int)Math.Floor(i / elementsPerRow);

            // get attached property value
            double weight = GetWeight(child);
            if (double.IsNaN(weight)) { weight = 100; }

            double weightedElementWidth = unweightedElementWidth * weight / 100;
            child.Measure(new Size(weightedElementWidth, elementHeight));
        }
        return size;
    }

    protected override Size ArrangeOverride(Size arrangeSize)
    {
        var size = base.ArrangeOverride(arrangeSize);
        int elementsPerRow = (int)Math.Ceiling((double)Children.Count / Rows);
        double elementHeight = size.Height / Rows;
        double unweightedElementWidth = size.Width / elementsPerRow;
        double[] accumulatedWidthPerRow = new double[Rows];
        for (int i = 0; i < Children.Count; ++i)
        {
            var child = Children[i];
            int rowNumber = i / elementsPerRow;

            // get attached property value
            double weight = GetWeight(child);
            if (double.IsNaN(weight)) { weight = 100; }

            double weightedElementWidth = unweightedElementWidth * (double)weight / 100;
            child.Arrange(new Rect(new Point(accumulatedWidthPerRow[rowNumber], rowNumber * elementHeight),
                                    new Point(accumulatedWidthPerRow[rowNumber] + weightedElementWidth, (rowNumber + 1) * elementHeight)));
            accumulatedWidthPerRow[rowNumber] += weightedElementWidth;
        }
        return size;
    }
}

在 ItemContainerStyle 中绑定附加的 属性:

<ItemsControl ItemsSource="{Binding Path=ElementGroupCollection}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <local:WeightedUniformGrid Rows="2" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="local:WeightedUniformGrid.Weight" Value="{Binding Weight}"/>
        </Style>
    </ItemsControl.ItemContainerStyle>
    ...
</ItemsControl>