WPFのDatePickerに選択可能な日付を設定する

## 実現したいこと
WPF標準の`DatePicker`はユーザーにカレンダーから日付を選ばせることができるイカしたコントロールですが、そのまま使うと全ての日付が選択できてしまいます。
そこで、`BlackoutDates`プロパティに**選べない日付**を設定することで制御できますが、実際のところ**選べる日付**をBinding等で設定したいときもあり、今回はそれを実現します。

## どうやってやるのか
添付プロパティを使います。以上。
なお私の手元では`MaterialDesignInXAML`を導入しているので、標準のコントロールと見た目が異なりますが、本質的にやっていることは同じです。気になる方は[こちら](http://materialdesigninxaml.net/)から導入してみてください。

## 添付プロパティを作成する
ここではプロジェクトの`Views`フォルダの下に`Behaviors`フォルダを作って、その中に`DatePickerAssist`という名前のクラスを作成します。そこに`DatePicker`にくっつける添付プロパティ`SelectableDates`を定義します。ちなみにいつも全部書くのは大変なので、Visual Studioの`propa`スニペットを使っています。

```c#:DatePickerAssist.cs
namespace DatePickerSample.Views.Behaviors
{
    public class DatePickerAssist
    {
        // Getter
        public static IEnumerable GetSelectableDates(DependencyObject obj)
        {
            return (IEnumerable)obj.GetValue(SelectableDatesProperty);
        }

        // Setter
        public static void SetSelectableDates(DependencyObject obj, IEnumerable value)
        {
            obj.SetValue(SelectableDatesProperty, value);
        }

        // 添付プロパティを登録
        public static readonly DependencyProperty SelectableDatesProperty = DependencyProperty.RegisterAttached(
            "SelectableDates",
            typeof(IEnumerable),
            typeof(DatePickerAssist),
            new PropertyMetadata(null, OnSelectableDatesChanged));

        // コールバック関数の定義
        private static void OnSelectableDatesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is DatePicker datePicker && e.NewValue is IEnumerable<DateTime> dates)
            {
                var minDate = dates.Min();
                var maxDate = dates.Max();

                // 区間の最初と最後を設定
                datePicker.DisplayDateStart = minDate;
                datePicker.DisplayDateEnd = maxDate;

                // 選択できる日付の区間内は、1日ずつ判定して、含まれていなければ BlackoutDates に追加
                for (int i = 1; i < (maxDate - minDate).Days; i++)
                {
                    if (!dates.Contains(minDate.AddDays(i)))
                    {
                        datePicker.BlackoutDates.Add(new CalendarDateRange(minDate.AddDays(i)));
                    }
                }
            }
        }
    }
}
```

## 添付プロパティを設定する
今回はViewModelで`ObservableCollection<DateTime>`型のプロパティとして選択可能な日付を定義して、そいつを↑で作った添付プロパティにBindingしてあげます。

```c#:MainWindowViewModel.cs
namespace DatePickerSample.ViewModels
{
    public class MainWindowViewModel
    {
        public ObservableCollection<DateTime> Dates { get; }

        public MainWindowViewModel()
        {
            Dates = new ObservableCollection<DateTime>()
            {
                new DateTime(2021, 2, 2),
                new DateTime(2021, 2, 3),
                new DateTime(2021, 2, 4),
                new DateTime(2021, 2, 10),
                new DateTime(2021, 2, 16),
            };
        }
    }
}
```

とりあえず5日分だけ定義してあげました。こいつをBindingします。以下ではBehaviors名前空間を`b`で呼び出しています。

```xml:MainWindow.xaml
<Window x:Class="DatePickerSample.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
        xmlns:b="clr-namespace:DatePickerSample.Views.Behaviors"
        xmlns:vm="clr-namespace:DatePickerSample.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow" Height="400" Width="300">
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <DatePicker Width="100" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10,10,0,0"
                    md:CalendarAssist.IsHeaderVisible="False"  b:DatePickerAssist.SelectableDates="{Binding Dates}"/>
    </Grid>
</Window>
```

実行するとこんな感じ。`Dates`プロパティで設定した日付だけがきちんと選べるようになっています。この実装だと2020/2/1以前と2020/2/17以降はそもそも表示されず、期間内で選べない日はグレーアウトになります。
※デフォルトの`DatePicker`コントロールでは、選べない日付には×(バツ)マークが付きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629651/945c9919-ef0d-8a34-9eef-287824bb5b91.png)

## まとめ
- 添付プロパティはめちゃ便利
- 結局 MaterialDesignInXAML がすごく綺麗