実現したいこと
WPF標準のDatePicker
はユーザーにカレンダーから日付を選ばせることができるイカしたコントロールですが、そのまま使うと全ての日付が選択できてしまいます。
そこで、BlackoutDates
プロパティに選べない日付を設定することで制御できますが、実際のところ選べる日付をBinding等で設定したいときもあり、今回はそれを実現します。
どうやってやるのか
添付プロパティを使います。以上。
なお私の手元ではMaterialDesignInXAML
を導入しているので、標準のコントロールと見た目が異なりますが、本質的にやっていることは同じです。気になる方はこちらから導入してみてください。
添付プロパティを作成する
ここではプロジェクトのViews
フォルダの下にBehaviors
フォルダを作って、その中にDatePickerAssist
という名前のクラスを作成します。そこにDatePicker
にくっつける添付プロパティSelectableDates
を定義します。ちなみにいつも全部書くのは大変なので、Visual Studioのpropa
スニペットを使っています。
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してあげます。
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
で呼び出しています。
<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
コントロールでは、選べない日付には×(バツ)マークが付きます。
まとめ
- 添付プロパティはめちゃ便利
- 結局 MaterialDesignInXAML がすごく綺麗