[WPF] DataGridの列幅を保存・リセットするコントロール

#はじめに
WPF標準の DataGrid は、マウスで列幅や順番の変更ができます。
この変更値を、保存したりリセットする、ユーザ コントロールを作成しました。

![GridColumnSetting.gif](https://qiita-image-store.s3.amazonaws.com/0/177379/74d8c9c8-2c9d-4808-00c9-924c0b0695e6.gif)

#環境
* .NET Framework 4.6.1

※デザインテーマは、[Material Design In XAML Toolkit](https://github.com/ButchersBoy/MaterialDesignInXamlToolkit)を使っています。
（[導入例](http://qiita.com/kirishu/items/2f226d7f7c69c644f787)）

#準備
今回はテストなので、設定の保存には簡便な Visual Studio の ユーザ設定を使います。
プロジェクトの「設定」に、保存するキー名をstringで登録しておきます。

![image](https://qiita-image-store.s3.amazonaws.com/0/177379/01087644-7a11-5bb0-6b23-1d7b8e90fb50.png)

"dataGrid1Columns"という名前を使います。

> 実際の現場では、このVSのユーザ設定を使ったことは無いです。
> 設定ファイルの保存先が、`C:\Users\(UserName)\AppData\Local\(CompanyName)\(appdomainname)_(eid)_(hash)\(version)\user.config` などと凶悪なうえ、任意の場所にするには難しいので。

#コントロールのコード

Material Design Toolkit の PopupBox を作り、子要素に

* 現在の列状態を保存
* 列の設定をリセット

というメニューを作ります。
それぞれのClickイベントと、コントロールのLoadedイベントを定義します。

![image](https://qiita-image-store.s3.amazonaws.com/0/177379/48fb9cd4-0346-6ff0-8883-8f8e25559376.png)

```xml:GridColumnSetting.xaml
<UserControl x:Class="WpfApplication1.Controls.GridColumnSetting"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:materialDesign="clr-namespace:MaterialDesignThemes.Wpf;assembly=MaterialDesignThemes.Wpf"
             mc:Ignorable="d" 
             d:DesignHeight="30" d:DesignWidth="30"
             Loaded="GridColumnSetting_Loaded">
    <UserControl.Resources>
        <materialDesign:PackIcon x:Shared="False" x:Key="GridSettingPopupBoxIcon" Kind="TableLarge" Width="24" Height="24"
                                 Foreground="{DynamicResource AccentColorBrush}"/>
    </UserControl.Resources>

    <materialDesign:PopupBox ToggleContent="{StaticResource GridSettingPopupBoxIcon}"
                             ToolTip="グリッド列幅設定"
                             ToolTipService.VerticalOffset="-70"
                             VerticalAlignment="Bottom"
                             PlacementMode="BottomAndAlignRightEdges"
                             StaysOpen="False">
        <StackPanel>
            <Button Click="ButtonSave_Click">
                <StackPanel Orientation="Horizontal">
                    <materialDesign:PackIcon Kind="ContentSave" Width="24" Height="24"/>
                    <TextBlock Text="現在の列状態を保存"
                               Margin="8 0 0 0"/>
                </StackPanel>
            </Button>
            <Separator/>
            <Button Click="ButtonReset_Click">
                <StackPanel Orientation="Horizontal">
                    <materialDesign:PackIcon Kind="UndoVariant" Width="24" Height="24"/>
                    <TextBlock Text="列の設定をリセット"
                               Margin="8 0 0 0"/>
                </StackPanel>
            </Button>
        </StackPanel>
    </materialDesign:PopupBox>

</UserControl>
```

partial コードに、依存プロパティとイベントの内容を記述します。

```csharp:GridColumnSetting.xaml.cs
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;

namespace WpfApplication1.Controls
{
    /// <summary>
    /// DataGrid 列設定ボタンコントロール
    /// </summary>
    public partial class GridColumnSetting : UserControl
    {
        #region Dependency Properties

        /// <summary>依存プロパティ定義 - 対象のDataGrid</summary>
        /// <summary></summary>
        public static readonly DependencyProperty TargetGridProperty =
            DependencyProperty.Register("TargetGrid",
                typeof(DataGrid),
                typeof(GridColumnSetting),
                new FrameworkPropertyMetadata(null));

        /// <summary>対象のDataGrid</summary>
        public DataGrid TargetGrid
        {
            get { return (DataGrid)GetValue(TargetGridProperty); }
            set { SetValue(TargetGridProperty, value); }
        }

        /// <summary>依存プロパティ定義 - 設定ファイルのキー</summary>
        public static readonly DependencyProperty SettingsKeyProperty =
            DependencyProperty.Register("SettingsKey",
                typeof(string),
                typeof(GridColumnSetting),
                new FrameworkPropertyMetadata(null));

        /// <summary>設定ファイルのキー</summary>
        public string SettingsKey
        {
            get { return (string)GetValue(SettingsKeyProperty); }
            set { SetValue(SettingsKeyProperty, value); }
        }
        #endregion

        // 元（デザイン状態）の列設定
        private List<DataGridColumnInfo> _defaultSettings;

        /// <summary>
        /// constractor
        /// </summary>
        public GridColumnSetting()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Loaded event
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void GridColumnSetting_Loaded(object sender, RoutedEventArgs e)
        {
            // デフォルトの並び順・幅を確保する
            _defaultSettings = this.TargetGrid.Columns
                                    .Select((x, index) => new DataGridColumnInfo()
                                    {
                                        ColumnIndex = index,
                                        DisplayIndex = x.DisplayIndex,
                                        Width = x.Width.Value,
                                    })
                                    .ToList();

            // ユーザ設定から保存値を取得し、DataGridに反映する
            var saveValue = Properties.Settings.Default[SettingsKey].ToString();
            if (!string.IsNullOrEmpty(saveValue))
            {
                var settings = xmlDeserialize<List<DataGridColumnInfo>>(saveValue);
                setColumns(settings);
            }
        }

        /// <summary>
        /// 保存ボタン押下
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ButtonSave_Click(object sender, RoutedEventArgs e)
        {
            // 列情報コレクションに現在のDisplayIndexとWidthを確保
            var newSettings = this.TargetGrid.Columns
                                    .Select((x, index) => new DataGridColumnInfo()
                                    {
                                        ColumnIndex = index,
                                        DisplayIndex = x.DisplayIndex,
                                        Width = x.Width.Value,
                                    })
                                    .ToList();

            // ユーザ設定へ保存
            Properties.Settings.Default[SettingsKey] = xmlSerialize<List<DataGridColumnInfo>>(newSettings);
            Properties.Settings.Default.Save();
        }

        /// <summary>
        /// リセットボタン押下
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ButtonReset_Click(object sender, RoutedEventArgs e)
        {
            // 最初にとっておいた_defaultSettingsで設定する
            setColumns(_defaultSettings);

            // ユーザ設定へはnullを保存する
            Properties.Settings.Default[SettingsKey] = null;
            Properties.Settings.Default.Save();
        }

        /// <summary>
        /// DataGrid列設定
        /// </summary>
        /// <param name="settings"></param>
        private void setColumns(List<DataGridColumnInfo> settings)
        {
            if (settings != null && settings.Count > 0)
            {
                for (var i = 0; i < this.TargetGrid.Columns.Count; i++)
                {
                    var setting = settings[i];
                    this.TargetGrid.Columns[i].DisplayIndex = setting.DisplayIndex;
                    this.TargetGrid.Columns[i].Width = new DataGridLength(setting.Width);
                }
            }
        }

        #region XAMLシリアライズ（テスト用）
        /// <summary>
        /// XMLシリアライズ
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj"></param>
        /// <returns></returns>
        private string xmlSerialize<T>(object obj)
        {
            var sb = new StringBuilder();
            using (var w = new System.IO.StringWriter(sb))
            {
                var serializer = new System.Xml.Serialization.XmlSerializer(typeof(T));
                serializer.Serialize(w, obj);
            }
            return sb.ToString();
        }

        /// <summary>
        /// XMLデシリアライズ
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="xml"></param>
        /// <returns></returns>
        private T xmlDeserialize<T>(string xml)
        {
            T result;
            using (var tr = new System.IO.StringReader(xml))
            {
                var serializer = new System.Xml.Serialization.XmlSerializer(typeof(T));
                result = (T)serializer.Deserialize(tr);
            }
            return result;
        }
        #endregion

        /// <summary>
        /// 列情報クラス
        /// </summary>
        public class DataGridColumnInfo
        {
            /// <summary>列index</summary>
            public int ColumnIndex { get; set; }
            /// <summary>表示index</summary>
            public int DisplayIndex { get; set; }
            /// <summary>横幅</summary>
            public double Width { get; set; }
        }
    }
}
```

#コードの説明
###依存プロパティ
操作対象となる DataGrid と、設定保存のキー名を受け取るためのプロパティ（DependencyProperty）を定義しています。

###Loadedイベント
XAMLに定義されている列順と列幅を取得し、メンバ変数に確保します。
この値が、デフォルトに戻すときの値になります。

ユーザ設定から、保存されている設定値を読み取ります。
設定値は、DataGridColumnInfoクラスのコレクション（`List<DataGridColumnInfo>`）をXMLシリアライズした文字列になっています。
こいつをオブジェクトに戻した後、DataGrid の列プロパティを変更していきます。

###保存ボタン押下イベント
表示中の DataGrid の列情報を `List<DataGridColumnInfo>` に格納し、XMLシリアライズした値を、ユーザ設定に保存します。

###リセットボタン押下イベント
Loadedイベントで確保したデフォルト値で DataGrid の列を更新します。
ユーザ設定には、nullを保存します。

#使い方

```xml:MainWindow.xaml
<!--
コントロールへの名前空間を定義しておきます
xmlns:controls="clr-namespace:WpfApplication1.Controls"
-->

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <StackPanel Grid.Row="0" Orientation="Horizontal">
            <TextBlock Text="TextBox"/>
            <TextBox MinWidth="120"/>
            <TextBlock Text="ComboBox"/>
            <ComboBox MinWidth="120"/>
            <Button Content="Button" Width="100"/>
        </StackPanel>

        <!-- 列設定コントロール -->
        <Grid Grid.Row="1" HorizontalAlignment="Right" Margin="0 0 10 0">
            <controls:GridColumnSetting TargetGrid="{Binding ElementName=dataGrid1}"
                                        SettingsKey="dataGrid1Columns"/>
        </Grid>

        <!-- 設定対象の DataGrid -->
        <DataGrid x:Name="dataGrid1"
                  Grid.Row="2"
                  Margin="10">
            <DataGrid.Columns>
                <DataGridTextColumn Header="氏名" Binding="{Binding Name}"
                                    Width="100"/>
                <DataGridTextColumn Header="ふりがな" Binding="{Binding Ruby}"
                                    Width="100"/>
                <DataGridTextColumn Header="性別" Binding="{Binding Gender}"
                                    Width="40"/>
                <DataGridTextColumn Header="誕生日" Binding="{Binding Birthday, StringFormat={}{0:yyyy/MM/dd}}"
                                    Width="80"/>
                <DataGridTextColumn Header="カレーの食べ方" Binding="{Binding Curry}"
                                    Width="200"/>
            </DataGrid.Columns>
        </DataGrid>
        
    </Grid>
```

作成したコントロール「GridColumnSetting」には、TargetGrid（対象DataGrid）と、SettingsKey（設定キー名）のプロパティを指定します。

これだけで、DataGrid の列設定の保存・リセットが動くようになります。

#まとめとか
この例では、列幅と順序だけの操作に留まっていますが、列の非表示や、固定列の設定なども、簡単に行えると思います。
（そのような設定の場合、コントロールのUIを作るのが大変だけど）