WPFのGridを動的に作成する

# はじめに
WPFのxamlの基本といえばGridですが、基本的にはデザイン時にrow, columnを設定して使用します。

```C#
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="1"/> // Grid.Row, ColumnでTextBlockの位置を指定して表示させる
    </Grid>
```

ただ、アプリを作っていると、たまにGridのRow, Columnを動的に設定したいというニーズがたまに出てくることがあります。

自分が開発していた時は、ListBoxのItemsPanelTemplateで動的にGridを設定したいということがありました。
カレンダーやスケジューラーのような月によって行数が変わるといった場合に、それをListBoxを使って実現しようと思うと、動的にGridを変更できるといいなということです。
以前 [Drag&Dropの記事](https://qiita.com/mkuwan/items/35d91e7c2f9edfe9884a)で、以下のような画面を紹介しましたが、この日にちごとのコマ割りで使用しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/5da13de6-2b45-4bbb-d13a-95da391bf2c9.png)
こんな感じでListBoxを作っています
見ていただけば、なんとなく構造がわかっていただけるのではと思います。

```C#
    <!--CustomGridを使用して、ListBoxItemを動的配置しています-->
    <ListBox Grid.Row="1"
            Visibility="{Binding CalendarVisitility.Value}"
            ItemsSource="{Binding CaseCalendarDatas}"
            Style="{StaticResource MaterialDesignCardsListBox}"
            ScrollViewer.CanContentScroll="False"
            dd:DragDrop.IsDropTarget="True">
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <customgrid:CustomGrid GridColumnCount="{Binding GridColumnCount.Value}"
                                ColumnDefinitionWidth="0"
                                GridRowCount="{Binding GridRowCount.Value}"
                                RowDefinitionHeight="0"/>
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
        <ListBox.ItemContainerStyle>
            <Style TargetType="ListBoxItem"
                BasedOn="{StaticResource MaterialDesignCardsListBoxItem}">
                <Setter Property="Margin" Value="5"/>
                <Setter Property="Grid.Row" Value="{Binding GridRowIndex}"/>
                <Setter Property="Grid.Column" Value="{Binding GridColumnIndex}"/>
            </Style>
        </ListBox.ItemContainerStyle>
        <ListBox.ItemTemplate>
            <DataTemplate>
                <views:RegisterCaseControl DataContext="{Binding ControlViewModel}"/>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
```

このxaml内でGridを継承したカスタムコントロール(CustomGrid)を使っています。
今回の記事は、このCustomGridのコードをご紹介させていただこうと思います。


# カスタムGrid (AutoGrid)
Gridを継承して、AutoGridというカスタムコントロールを作成します。

```C#
public class AutoGrid : Grid
```

## 添付プロパティ
Columnの「幅」「数」
Rowの「高さ」「数」
　にあたる添付プロパティ(Dependency Property)を作成します。

```C#
        #region ColumnDefinitionWidth
        /// <summary>
        /// Row.Width
        /// 　負数：固定高(50pixel)
        /// 　 0　：可変(1*)
        /// 　正数：指定高
        /// </summary>
        public static readonly DependencyProperty ColumnDefinitionWidthProperty =
                                    DependencyProperty.Register("ColumnDefinitionWidth",
                                        typeof(int),
                                        typeof(AutoGrid),
                                        new PropertyMetadata(50));
        public int ColumnDefinitionWidth
        {
            get { return (int)GetValue(ColumnDefinitionWidthProperty); }
            set { SetValue(ColumnDefinitionWidthProperty, value); }
        }
        #endregion

        #region ColumnCount
        public static readonly DependencyProperty GridColumnCountProperty =
                                    DependencyProperty.RegisterAttached("GridColumnCount",
                                        typeof(int),
                                        typeof(AutoGrid),
                                        new PropertyMetadata(0, new PropertyChangedCallback(OnGridColumnCountChanged)));
        public int GridColumnCount
        {
            get { return (int)GetValue(GridColumnCountProperty); }
            set { SetValue(GridColumnCountProperty, value); }
        }

        #endregion

        #region RowDefinitionHeight
        /// <summary>
        /// Row.Height
        /// 　負数：固定高(14pixel)
        /// 　 0　：可変(1*)
        /// 　正数：指定高
        /// </summary>
        public static readonly DependencyProperty RowDefinitionHeightProperty =
                                    DependencyProperty.Register("RowDefinitionHeight",
                                        typeof(int),
                                        typeof(AutoGrid),
                                        new PropertyMetadata(-1));
        public int RowDefinitionHeight
        {
            get { return (int)GetValue(RowDefinitionHeightProperty); }
            set { SetValue(RowDefinitionHeightProperty, value); }
        }
        #endregion

        #region RowCount 
        public static readonly DependencyProperty GridRowCountProperty =
                                    DependencyProperty.RegisterAttached("GridRowCount",
                                        typeof(int),
                                        typeof(AutoGrid),
                                        new PropertyMetadata(1, new PropertyChangedCallback(OnGridRowCountChanged)));
        public int GridRowCount
        {
            get { return (int)GetValue(GridRowCountProperty); }
            set { SetValue(GridRowCountProperty, value); }
        }

        #endregion
```

## PropertyChangedCallback
Row, Columnの数を設定した際のイベントを作成します。

### OnGridRowCountChanged
Rowを指定された「数」で、指定された「高さ」で作成します

```C#
        /// <summary>
        /// GridRowCountで指定された数のRowDefinitionsを作成
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        private static void OnGridRowCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (!(d is Grid) || (int)e.NewValue < 0)
                return;

            Grid grid = (Grid)d;

            grid.RowDefinitions.Clear();

            int heightProperty = (int)d.GetValue(RowDefinitionHeightProperty);

            for (int i = 0; i < (int)e.NewValue; i++)
            {

                RowDefinition rowDefinition = new RowDefinition();
                if (heightProperty < 0)
                {
                    rowDefinition.Height = new GridLength(14, GridUnitType.Pixel);
                }
                else if (heightProperty > 0)
                {
                    rowDefinition.Height = new GridLength(heightProperty, GridUnitType.Pixel);
                }
                else if (heightProperty == 0)
                {
                    rowDefinition.Height = new GridLength(1, GridUnitType.Star);
                }
                grid.RowDefinitions.Add(rowDefinition);
            }
        }
```

### OnGridColumnCountChanged
Columnを指定された「数」で、指定された「幅」で作成します

```C#
        /// <summary>
        /// GridColumnCountで指定された数のColumnDefinitionsを作成
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        private static void OnGridColumnCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (!(d is Grid) || (int)e.NewValue < 0)
                return;

            Grid grid = (Grid)d;

            grid.ColumnDefinitions.Clear();

            int widthProperty = (int)d.GetValue(ColumnDefinitionWidthProperty);

            for (int i = 0; i < (int)e.NewValue; i++)
            {

                ColumnDefinition columnDefinition = new ColumnDefinition();
                if (widthProperty < 0)
                {
                    columnDefinition.Width = new GridLength(50, GridUnitType.Pixel);
                }
                else if (widthProperty > 0)
                {
                    columnDefinition.Width = new GridLength(widthProperty, GridUnitType.Pixel);
                }
                else if (widthProperty == 0)
                {
                    columnDefinition.Width = new GridLength(1, GridUnitType.Star);
                }
                grid.ColumnDefinitions.Add(columnDefinition);
            }
        }
```

# 実装
あとは、作成したカスタムコントロールを参照指定して、xaml内で以下のような感じで記述すればOKです。
.ValueがついているのはReactivePropertyを使用しているためです。

```C#
xmlns:customgrid="clr-namespace:..(参照先)..."

    <customgrid:CustomGrid GridColumnCount="{Binding GridColumnCount.Value}"
                        ColumnDefinitionWidth="0"
                        GridRowCount="{Binding GridRowCount.Value}"
                        RowDefinitionHeight="0"/>
```