Search code examples
c#wpfitemscontrol

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


I've made a custom WeightedUniformGrid class that I'm using as ItemsPanel in an ItemsControl. Each grid element is weighted using a Weight property in the objects in the ItemsSource collection.

enter image description here

But when I change the Weight property in the view model, it isn't immediately shown in the View. I have to change the size of the window for the WeightedUniformGrid to be drawn with the new values.

How can I get property changes in the ItemsSource collection to cause the ItemsControl to be redrawn? I'm thinking perhaps adding a DependencyProperty in the WeightedUniformGrid, that will both AffectsArrange and AffectsMeasure. But I'm unsure what I could bind it to.

I've searched this for a good while, and there are some questions that are similar (like this one). But I haven't been able to adapt any of them to my need.

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

Solution

  • Your WeightedUniformGrid must not directly access the Weight property in the DataContext of its child elements. Besides that it is bad practice it will not work correctly, as there is no mechanism that forces a layout pass when a view model item's Weight changes.

    There should instead be an attached property that is bound to Weight. The attached property's FrameworkPropertyMetadataOptions will force a layout pass.

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

    Bind the attached property in an 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>