WPF: Enable Specific Dates Only In a DatePicker
The DatePicker control in WPF has a BlackoutDates property that you can use to set a collection of dates that should not be selectable in the calendar. It's convenient to use together with the DisplayDateStart and DisplayDateEnd properties if you intend to disable only a few dates in a relatively small range, e.g.:
<DatePicker DisplayDateStart="6/1/2019"
DisplayDateEnd="6/30/2019">
<DatePicker.BlackoutDates>
<CalendarDateRange Start="6/1/2019" End="6/2/2019"/>
<CalendarDateRange Start="6/8/2019" End="6/9/2019"/>
<CalendarDateRange Start="6/15/2019" End="6/16/2019"/>
<CalendarDateRange Start="6/22/2019" End="6/23/2019"/>
<CalendarDateRange Start="6/29/2019" End="6/30/2019"/>
</DatePicker.BlackoutDates>
</DatePicker>
If you, on the other hand, want to enable only a small set of dates in a fairly large date range, there is, unfortunately, no built-in "EnabledDate" property or similar.
You can solve this by creating a value converter and a CalendarStyle with a data trigger that binds to the DataContext of each of the CalendarDayButton elements in Calendar.
Below is an example of a class that implements the System.Windows.Data.IValueConverter interface. It expects to be applied to a data binding that resolves to a DateTime object and returns a bool to indicate whether this particular date should be enabled.
public class DateTimeToBoolConverter : IValueConverter
{
private static readonly HashSet<DateTime> s_datesToBeEnabled = new HashSet<DateTime>
{
new DateTime(2019, 6, 3),
new DateTime(2019, 6, 10),
new DateTime(2019, 6, 17)
};
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
s_datesToBeEnabled.Contains((DateTime)value);
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
Here is how to use it in a CalendarStyle that sets the IsHitTestVisible property to false and decrease the opacity for all dates that are not present in the HashSet<DateTime> in the converter:
<DatePicker DisplayDateStart="6/1/2019"
DisplayDateEnd="6/30/2019">
<DatePicker.CalendarStyle>
<Style TargetType="Calendar">
<Setter Property="CalendarDayButtonStyle">
<Setter.Value>
<Style TargetType="CalendarDayButton">
<Style.Resources>
<local:DateTimeToBoolConverter x:Key="DateTimeToBoolConverter" />
</Style.Resources>
<Setter Property="IsHitTestVisible" Value="False" />
<Setter Property="Opacity" Value="0.5" />
<Style.Triggers>
<DataTrigger Binding="{Binding Converter={StaticResource DateTimeToBoolConverter}}" Value="True">
<Setter Property="IsHitTestVisible" Value="True" />
<Setter Property="Opacity" Value="1" />
</DataTrigger>
</Style.Triggers>
</Style>
</Setter.Value>
</Setter>
</Style>
</DatePicker.CalendarStyle>
</DatePicker>
If the dates to be enabled are defined in a source property of a view model that you want to bind to, instead of hard-coding the dates in the converter class, you could instead implement the System.Windows.Data.IMultiValueConverter interface and bind to two properties - the same DateTime as before and the view model property with the dates:
<DatePicker DisplayDateStart="6/1/2019"
DisplayDateEnd="6/30/2019">
<DatePicker.CalendarStyle>
<Style TargetType="Calendar">
<Setter Property="CalendarDayButtonStyle">
<Setter.Value>
<Style TargetType="CalendarDayButton">
<Style.Resources>
<local:DateTimesToBoolConverter x:Key="DateTimesToBoolConverter" />
</Style.Resources>
<Setter Property="IsHitTestVisible" Value="False" />
<Setter Property="Opacity" Value="0.5" />
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource DateTimesToBoolConverter}">
<Binding Path="DataContext.DatesToBeEnabled" RelativeSource="{RelativeSource AncestorType=DatePicker}" />
<Binding Path="." />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="IsHitTestVisible" Value="True" />
<Setter Property="Opacity" Value="1" />
</DataTrigger>
</Style.Triggers>
</Style>
</Setter.Value>
</Setter>
</Style>
</DatePicker.CalendarStyle>
</DatePicker>
The view model in this example is rather simplistic and contains only a property that exposes the selectable dates:
public class ViewModel
{
public IEnumerable<DateTime> DatesToBeEnabled { get; } = new HashSet<DateTime>
{
new DateTime(2019, 6, 3),
new DateTime(2019, 6, 10),
new DateTime(2019, 6, 17)
};
}
Don't forget to set the DataContext of the view to an instance of it. You may, for example, do this in the XAML markup:
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
The multi-value converter simply checks whether the first property (the DatesToBeEnabled collection in the view model) contains the value of the second property (the DateTime in the calendar):
public class DateTimesToBoolConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) =>
(values == null || values.Length < 2) ? true :
(values[0] as IEnumerable<DateTime>)?.Contains((DateTime)values[1]) ?? true;
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
The visual result and behavior should be the same for both approaches. Of course, you may also apply this solution to dates that should be disabled instead of enabled. It's simply a matter of inverting the values of the setters in the CalendarDayButtonStyle.