WPFでリストの列を動的に変更する

# はじめに

初めまして、現在大学3年生でWindowsのアプリケーション開発を行なっております。

今回は **WPF** と呼ばれるWindowsアプリケーション用に使用するフレームワークで使用するC#の記事になります。
WPFを使用する際には、UIの作成にXAML、クリックイベントやバックエンドにC#と言語を分けて作成します。


**今回は、WPFのDataGridを用いて、リストの列を動的に増やす方法を書いていきたいと思います。**


まずWPFでリストを表示する際には、通常は以下のような流れでそれぞれコーディングをすると思います。

1. C#でListまたはDataTableを作成
2. XAMLで、Header(列名)を設定

これらをそれぞれ行なった上で、**Binding**(バインディング)と呼ばれる連携を行い表示する流れです。

しかし、この方法では、動的に変更することができなかったので、方法を書いていきます。

# 今回やりたいこと
WPFのListViewにおいて、XAML側でHeaderを設定せずに、動的に列を増やしたいと考えています。。

しかし、XAML側で列を動的に増やす方法がなく、C#側でXAMLのListViewの列を増やすメソッドが見つからないという問題が生じてしまいました。

### ちなみになぜこのようなプログラムが必要なのか
条件によって、リストの表示方法を変えたいと言った場合に対応したいと言ったときに、今までの方法では対応できなかったため

例) 
あなたは、会社の人事で、社員の営業状態をチェックしたい。
しかし、部署によってフォーマットが異なり、

```
新規開拓営業部門は、`ID`, `相手の名前`
インサイドセールス部門は、新規開拓営業部門が開拓成功した店舗の情報
`ID`, `相手の名前`,`前回の連絡日付`, `次回の連絡日付`, `担当者`
```

と異なったフォーマットで管理しています。
これらを自社のアプリで表示したい場合、列の名前、列の個数が異なっており、従来のList作成方法では動的に表示することは難しいという設定

また追加機能として、列に担当者の名前全員を追加して、担当しているお店の列には、自分の名前に「１」をつけて表示する仕組みも作りたい

以下のようなイメージ
|ID  |店名    |前回の連絡日付|次回の連絡日付|山田太郎  |高橋花子 |ああ二郎  |
|---|---|---|---|---  |---  |---  |
|1  |俺のリップクリーム    |2022-3-18|2022-3-22|1  |0  |0  |
|2  |俺のスマホ    |2022-3-18|2022-3-22|0|1 |0  |
|3 |俺の天津飯    |2022-3-18|2022-3-23|0 |0 |1  |
|4  |俺のチャオズ  |2022-3-18|2022-3-23|0  |0  |1  |




まず静的にリストを作成して、XAMLのListViewに入れる方法について説明します。


# 動的ではない普通のやり方
先程の流れの1番は一度作成したリストの後、列追加のプログラムを作成すれば済む問題です。
プログラムの例としては以下のようなコードです。

```C#: Example.cs
DataTable datatable = new DataTable();
datatable.Columns.Add("ID", Type.GetType("Int32"));
datatable.Columns.Add("Name", Type.GetType("String"));

datatable.Rows(1,"俺のリップクリーム");
```

次に2番のXAMLでHeaderの作成ですが、以下のようなプログラムを書けば、済みそうな問題だと思います。

```XAML: Example.xaml
<ListView>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}">
            <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}">
        </GridView>
    </ListView.View>
</ListView>
```

# 実際に苦労したこと

以上のようなやり方をするとC#側は、動的に列を増やせそうですが、XAML側は、プログラムを書き換えない限り厳しそうです。

より具体的な話をすると、
C#はif分を書けば状況によって列を変更する
または、TextBoxとButtonを利用して、TextBoxに記載されたテキストをButtonを押すことで、列を追加(Columns.add)をする

と言った方法で、列を追加したり、変更したりすることが可能だが、XAML側は、IF文や動的な操作が使えないので、先程のプログラムのフローでは、表すことができません。

以下は、実際に解決した方法を記載していきます。

# ListViewの列を動的に追加する方法
まず簡単な流れを記載します。
1. 空のデータテーブルを作成
2. **テーブルの変更を通知する(最重要)**
3. 列の追加、
4. 行(データ)の追加
5. 呼び出し、XAMLとの連携

### 0. 今回使うデータ
前提として、先程の部署の例を使用したいと思います。

営業状態の基本的なデータ (店名, ID) は以下な形のディクショナリーに格納されているとします。
```C#: Data.cs
public List<KeyValuePair<string, int>> department_data;
```
また、新規顧客部門では表示せず、インサイドセールス部門で表示するものは辞書型でIDを紐付けされた状態で格納されているとします。（今回は`前回の連絡日付`, `次回の連絡日付`）
```C#: Data.cs
public Dictionary<string, int> pre_list;
public Dictionary<string, int> post_list;
```
そして、担当者の名前を集計して、担当者をリスト化して入れたものを以下の形で格納しているとします。
```C#: Data.cs
public List<string> manager_list = new List<string>();
```
担当者と、そのIDを辞書型に入れて、集計したものを以下の形で格納しているとします。
(ここでは誰がどのIDを担当しているかがわかるものです)
```C#: Data.cs
public Dictionary<string, int> manager_result;
```

## 1. 空のデータテーブルの作成

今回はdataTableを使用します。まず空のDataTableを作成します。

```C#: dataTableView.cs
private readonly DataTable dataTable = new DataTable():
```

## 2. テーブルの変更を通知する(最重要)
そして、今回の鍵となるC#の変更が加わったタイミングで、XAMLに変更を通知するイベントを追加しておきます。

ここのコードで、dataTableに変更が加わったら、呼び出し先の値を変更するプログラムを書いています。

また、DataTableViewという名前のDataViewを作成し、これをXAMLでバインディングをすれば、DataTableViewが変更されたタイミングで、XAMLのViewを書き換えてくれます。


```C#: dataTableView.cs

public DataView DataTableView => new DataView(dataTable);

public event PropertyChangedEventHandler PropertyChanged;

protected virtual OnPropertyChanged(string propertyName = null)
{
   if (PropertyChanged != null)
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}


private void NotifyTableUpdate()
{
   OnPropertyChanged(nameof(DataTableView));
}

```





## 3.列の追加
次に列の追加を行います。

今回、部門同士で共通している、ID、店名は先に入れておきます。
```C#: dataTableView.cs
private void InitializeDataTable()
{
   dataTable.Columns.Clear();
   dataTable.Rows.Clear();
   dataTable.Columns.Add("ID");
   dataTable.Columns.Add("Name");
   NotifyTableUpdate();
}

```

先程のData.csをインスタンス化したものを引数に持つ、メソッドで、行を追加します。
今回は、前回の連絡日時、次回の連絡日時、担当者の名前を追加できるプログラムです
```C#: dataTableView.cs
public void addColumns(Data data)
{
   InitializeDataTable();
   if(data.pre_day != null)
      dataTable.Columns.Add("前回の連絡日時")
   if(data.post_day != null)
      dataTable.Columns.Add("次回の連絡日時")
   foreach(string header in data.manager_list)
   {
      dataTable.Columns.Add(header);
      NotifyTableUpdate();
   }
}
```


## 4.行(データの追加)
次は、行を追加するメソッドを書きます。
先ほどと同様Data.csをインスタンス化したものを引数に持ちます
```C#: dataTableView.cs
public void addRow(Data data)
{
   foreach(KeyValuePair<string, int> shop_data in data.department_data)
   {
      int i = 5
      var row = dataTable.NewRow();
      row[0] = shop_data.Key;
      row[1] = shop_data.Value.ToString();
      if(data.pre_day != null)
         row[3] = data.pre_day[shop_data.Key].ToString();
      if(data.post_day != null)
         row[4] = data.post_dat[shop_data.Key].ToString();
      foreach (string header in data.manager_list)
      {
         if(data.manager_result.ContainsKey(shop_data.Key) && data.manager_result[shop_data.Key].ContainsKey(header))
         {
            row[i] = data.manager_result[shop_data.Key][header].ToString():
         }
         else
         {
            row[i] = 0
         }
         i++;
      }
      dataTable.Rows.Add(row);
      NotifyTableUpdate():           
   }
}
```

## 5.呼び出し、XAMLとの連携
これらのコードを呼び出します。
流れとしては、まずこのクラス(dataTableView.cs)を呼び出された際に、それぞれのコードを呼び出します。
```C#: dataTableView.cs
public dataTableView(Data data)
{
   InitializeDataTable();
   AddCols(data);
   AddRow(data);
}
```

そしてdataTableクラスをインスタンス化すれば、表示されます。
```C# MainWindow.xaml.cs
public void dataTable_Disp(Data data)
{
    DataContext = new dataTableView(data)
}

```
またこの時のXAMLでは、DataTableViewという形で通知がいく用にプログラムを書いているので
(2. テーブルの変更を通知する(最重要))
XAMLは以下のように書いておきます。
```XAML: MainWindow.xaml
<DataGrid ItemSource="{Binding DataTableView}"/>
```



# 参考記事
このようなレベルを自分で考えてプログラムすることが難しかったので、以下の記事を参考させていただきました。
ありがとうございました。


[WPFのDataGridで動的にカラムを追加する](https://qiita.com/ishida722/items/6324b6b85bde5f40ca8b)