Modifying an ItemsPanel's Grid RowDefinitionCo

2019-03-06 09:49发布

问题:

This is a followup for ItemsControl has no children during MainWindow's constructor

Based on the answer to SO question "WPF: arranging collection items in a grid", I have the following:

 <ItemsControl Name="itemsControl1" ItemsSource="{Binding MyItems}"> 
    <ItemsControl.ItemsPanel> 
        <ItemsPanelTemplate> 
            <Grid Name="theGrid" ShowGridLines="True" /> 
        </ItemsPanelTemplate> 
    </ItemsControl.ItemsPanel> 
    <ItemsControl.ItemContainerStyle> 
        <Style TargetType="{x:Type FrameworkElement}"> 
            <Setter Property="Grid.Row" Value="{Binding RowIndex}" /> 
            <Setter Property="Grid.Column" Value="{Binding ColumnIndex}" /> 
        </Style> 
    </ItemsControl.ItemContainerStyle> 
</ItemsControl> 

Now, I want to set the number of rows and columns of theGrid in the code behind: theGrid.RowDefinitions.Clear(); theGrid.ColumnDefinitions.Clear();

        for (uint i = 0; i < theNumberOfRows; i++) 
            theGrid.RowDefinitions.Add(new RowDefinition()); 

        for (uint i = 0; i < theNumberOfCols; i++) 
            theGrid.ColumnDefinitions.Add(new ColumnDefinition()); 

As per MattHamilton's answer there, the gird is available once itemsControl1. ItemContainerGenerator.StatusChanged fires with status of GeneratorStatus.ContainersGenerated.

However, trying to modify the grid from the event handler raises an "Cannot modify 'RowDefinitionCollection' in read-only state" exception.

So, how can I set theGrid's row and column collections before the window is shown to the user?

edit: I'm modifying the Grid's properties from itemsControl1.ItemContainerGenerator.StatusChanged event handler:

        if (itemsControl1.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
            return;

        itemsControl1.ItemContainerGenerator.StatusChanged -= ItemContainerGeneratorStatusChanged;

        SetGridRowsAndColumns(InitialNumberOfRows, InitialMaxNumberOfCols);

Notice that SetGridRowsAndColumns(numberOfRows, numberOfCols) does work later, in a response to a button click.

回答1:

I would use attached behavior as opposed to low level customisation for ItemsControl.

If all you need is a matrix control - you might consider using a row Grid instead of ItemsControl (that's what we've ended up with). ItemsControl is unlimitedly powerful creaturem but some time it can be a challange to squeeze a small but useful extra future through its sound design.

The changes you'll have to make following this approach are: 1. Use Dimension and bind it to to a Size you want it to be. 2. Create a custom ItemTemplate and add GridEx.Position to its root visual bound to the relvant Point property.

On those two, just give us a shout and I'll update my answer with more details.

here's the class:

public class GridEx
    {
        public static DependencyProperty DimensionProperty =
            DependencyProperty.Register("Dimension",
            typeof(Size),
            typeof(Grid),
            new PropertyMetadata((o, e) => 
            {
                GridEx.OnDimensionChanged((Grid)o, (Size)e.NewValue);
            }));

        public static DependencyProperty PositionProperty =
            DependencyProperty.Register("Position",
            typeof(Point),
            typeof(UIElement),
            new PropertyMetadata((o, e) =>
            {
                GridEx.OnPostionChanged((UIElement)o, (Point)e.NewValue);
            }));

        private static void OnDimensionChanged(Grid grid, Size resolution)
        {
            grid.RowDefinitions.Clear();
            grid.ColumnDefinitions.Clear();

            for (int i = 0; i < resolution.Width; i++)
            {
                grid.ColumnDefinitions.Add(new ColumnDefinition());
            }

            for (int i = 0; i < resolution.Height; i++)
            {
                grid.RowDefinitions.Add(new RowDefinition());
            }
        }

        private static void OnPostionChanged(UIElement item, Point position)
        {
            Grid.SetColumn(item, Convert.ToInt32((position.X)));
            Grid.SetRow(item, Convert.ToInt32(position.Y));
        }

        public static void SetDimension(Grid grid, Size dimension)
        {
            grid.SetValue(GridEx.DimensionProperty, dimension);
        }

        public static Size GetDimension(Grid grid)
        {
            return (Size)grid.GetValue(GridEx.DimensionProperty);
        }

        public static void SetPosition(UIElement item, Point position)
        {
            item.SetValue(GridEx.PositionProperty, position);
        }

        public static Point GetPosition(Grid grid)
        {
            return (Point)grid.GetValue(GridEx.PositionProperty);
        }
    }

And here's how we use it:

<Window x:Class="GridDefs.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:GridDefs"
        Title="MainWindow" Height="350" Width="525">
    <Grid local:GridEx.Dimension="3,3">
        <Button local:GridEx.Position="0,0">A</Button>
        <Button local:GridEx.Position="1,1">A</Button>
        <Button local:GridEx.Position="2,2">A</Button>
    </Grid>
</Window>


回答2:

Here's how you can create a matrix without using ItemsControl, note, that you pertain the main thing - the ability to specify templates for the items.

Code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace GridDefs
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            //
            this.DataContext = this; // alterantively use RelativeSource
        }

        public IEnumerable<Item> MyItems
        {
            get
            {
                List<Item> items = new List<Item>(3);

                items.Add(Item.Create("A1", new Point(0, 0)));
                items.Add(Item.Create("B2", new Point(1, 1)));
                items.Add(Item.Create("C3", new Point(2, 2)));

                return items;
            }
        }
    }

    public interface IMatrixItem
    {
        Point Position { get; }
    }

    // Model, note - it has to implement INotifyPropertyChanged if
    // you want to propagate its changes up to the UI
    public class Item: IMatrixItem
    {
        public static Item Create(string text,
            Point position)
        {
            Item item = new Item();

            item.Text = text;
            item.Position = position;

            return item;
        }

        public string Text
        {
            get;
            private set;
        }

        public Point Position
        {
            get;
            private set;
        }
    }

    public class GridEx
    {
        public static DependencyProperty DimensionProperty =
            DependencyProperty.RegisterAttached("Dimension",
            typeof(Size),
            typeof(GridEx),
            new PropertyMetadata(new Size(0, 0),
                (o, e) =>
                {
                    GridEx.OnDimensionChanged((Grid)o, (Size)e.NewValue);
                }));

        public static DependencyProperty PositionProperty =
            DependencyProperty.RegisterAttached("Position",
            typeof(Point),
            typeof(GridEx),
            new FrameworkPropertyMetadata(new Point(-1, -1),
                (o, e) =>
                {
                    GridEx.OnPostionChanged((UIElement)o, (Point)e.NewValue);
                }));

        public static DependencyProperty ItemStyleProperty =
           DependencyProperty.RegisterAttached("ItemStyle",
           typeof(Style),
           typeof(GridEx));

        public static DependencyProperty ItemsProperty =
            DependencyProperty.RegisterAttached("Items",
            typeof(IEnumerable<IMatrixItem>),
            typeof(GridEx),
            new PropertyMetadata((o, e) =>
            {
                GridEx.OnItemsChanged((Grid)o, (IEnumerable<IMatrixItem>)e.NewValue);
            }));

        #region "Dimension"

        private static void OnDimensionChanged(Grid grid, Size resolution)
        {
            grid.RowDefinitions.Clear();
            grid.ColumnDefinitions.Clear();

            for (int i = 0; i < resolution.Width; i++)
            {
                grid.ColumnDefinitions.Add(new ColumnDefinition());
            }

            for (int i = 0; i < resolution.Height; i++)
            {
                grid.RowDefinitions.Add(new RowDefinition());
            }
        }

        public static void SetDimension(Grid grid, Size dimension)
        {
            grid.SetValue(GridEx.DimensionProperty, dimension);
        }

        public static Size GetDimension(Grid grid)
        {
            return (Size)grid.GetValue(GridEx.DimensionProperty);
        }

        #endregion

        #region "Position"


        private static void OnPostionChanged(UIElement item, Point position)
        {
            item.SetValue(Grid.ColumnProperty, Convert.ToInt32(position.X));
            item.SetValue(Grid.RowProperty, Convert.ToInt32(position.Y));
        }

        private static T GetParentOfType<T>(DependencyObject current)
          where T : DependencyObject
        {
            for (DependencyObject parent = VisualTreeHelper.GetParent(current);
                parent != null;
                parent = VisualTreeHelper.GetParent(parent))
            {
                T result = parent as T;

                if (result != null)
                    return result;
            }

            return null;
        }

        public static void SetPosition(UIElement item, Point position)
        {
            item.SetValue(GridEx.PositionProperty, position);
        }

        public static Point GetPosition(UIElement grid)
        {
            return (Point)grid.GetValue(GridEx.PositionProperty);
        }

        #endregion

        #region "ItemStyle"

        public static void SetItemStyle(Grid item, Style style)
        {
            item.SetValue(GridEx.ItemStyleProperty, style);
        }

        public static Style GetItemStyle(Grid grid)
        {
            return (Style)grid.GetValue(GridEx.ItemStyleProperty);
        }

        #endregion

        #region "Items"

        private static void OnItemsChanged(Grid grid, IEnumerable<IMatrixItem> items)
        {
            grid.Children.Clear();

            // template
            Style style = GetItemStyle(grid);

            foreach (IMatrixItem item in items)
            {
                Control itemControl = new Control();

                grid.Children.Add(itemControl);

                itemControl.Style = style;
                itemControl.DataContext = item;

            }
        }

        public static void SetItems(Grid grid, IEnumerable<IMatrixItem> items)
        {
            grid.SetValue(GridEx.ItemsProperty, items);
        }

        public static IEnumerable<IMatrixItem> GetItems(Grid grid)
        {
            return (IEnumerable<IMatrixItem>)grid.GetValue(GridEx.ItemsProperty);
        }

        #endregion
    }
}

Markup:

<Window x:Class="GridDefs.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:GridDefs"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <Style TargetType="Control" x:Key="t">
            <Setter Property="local:GridEx.Position" Value="{Binding Position}"></Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Button Content="{Binding Text}" />
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

    </Window.Resources>

    <Grid local:GridEx.Dimension="3,3" 
          local:GridEx.ItemStyle="{StaticResource t}"
          local:GridEx.Items="{Binding MyItems}">
    </Grid>
</Window>