Where I work, we deal with FX (Foreign Exchange), and as such we have to deal with a lot of different holidays both for all the countries of the world and the 2 currencies normally involved in a FX deal. We would also typically like to show this to the user by way of a blacked out Date
within the standard WPF DatePicker
. Luckily, the standard WPF DatePicker
does support the idea of a BlackoutDate
collection which makes the WPF DatePicker
Calendar look like this:
All good so far, but our users would like to know WHY they can’t do a trade on this date. Some sort of ToolTip would be nice. This blog will show you how to achieve that.
So how can we do that exactly?
Step 1
We need some sort of NonWorking
day item that we could use to bind against. Here is one I think fits the bill.
[DataContract]
[DebuggerDisplay("{Date.ToShortDateString()}({Description})")]
public class NonWorkingDayDto : IEquatable<NonWorkingDayDto>
{
public NonWorkingDayDto(DateTime date, string description)
{
Date = date;
Description = description;
}
[DataMember]
public DateTime Date { get; set; }
[DataMember]
public string Description { get; set; }
public bool Equals(NonWorkingDayDto other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return this.Date.Equals(other.Date);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj.GetType() != this.GetType())
{
return false;
}
return Equals((NonWorkingDayDto)obj);
}
public override int GetHashCode()
{
return this.Date.GetHashCode();
}
public static bool operator ==(NonWorkingDayDto left, NonWorkingDayDto right)
{
return Equals(left, right);
}
public static bool operator !=(NonWorkingDayDto left, NonWorkingDayDto right)
{
return !Equals(left, right);
}
}
public class NonWorkingDayComparer : IEqualityComparer<NonWorkingDayDto>
{
public bool Equals(NonWorkingDayDto x, NonWorkingDayDto y)
{
return x.Date == y.Date;
}
public int GetHashCode(NonWorkingDayDto obj)
{
return obj.Date.GetHashCode();
}
}
Step 2
We need to create a few attached properties for working with the Calendar
. You will notice that one is going to be some sort of text lookup. We will be using that for the tooltips later. It is an Attached
property that we can hook into. The 2nd one allows us to bind a number of NonWorkingDayDto
objects, which will then create the DatePicker
/Calendar
BlackoutDate
collection. This collection should be treated with a lot of care, as if you attempt to Clear()
the collection and then add the new items in again you will see very bad performance. There must be an awful lot of binding stuff going on based on that, it also seems to be a factor if you have opened several Calendar
months from the UI, the DatePicker
seems to cache these previously shown Calendar
instances internally. I found the best way was to do some set type intersections and just remove the ones I didn’t want anymore, and add the ones I wanted that were not present right now.
public static class CalendarBehavior
{
#region BlackOutDatesTextLookup
public static readonly DependencyProperty BlackOutDatesTextLookupProperty =
DependencyProperty.RegisterAttached("BlackOutDatesTextLookup",
typeof(Dictionary<DateTime, string>), typeof(CalendarBehavior),
new FrameworkPropertyMetadata(new Dictionary<DateTime, string>()));
public static Dictionary<DateTime,
string> GetBlackOutDatesTextLookup(DependencyObject d)
{
return (Dictionary<DateTime, string>)d.GetValue(BlackOutDatesTextLookupProperty);
}
public static void SetBlackOutDatesTextLookup
(DependencyObject d, Dictionary<DateTime, string> value)
{
d.SetValue(BlackOutDatesTextLookupProperty, value);
}
#endregion
#region NonWorkingDays
public static readonly DependencyProperty NonWorkingDaysProperty =
DependencyProperty.RegisterAttached("NonWorkingDays",
typeof(IEnumerable<NonWorkingDayDto>), typeof(CalendarBehavior),
new FrameworkPropertyMetadata(null, OnNonWorkingDaysChanged));
public static IEnumerable<NonWorkingDayDto> GetNonWorkingDays(DependencyObject d)
{
return (IEnumerable<NonWorkingDayDto>)d.GetValue(NonWorkingDaysProperty);
}
public static void SetNonWorkingDays(DependencyObject d, IEnumerable<NonWorkingDayDto> value)
{
d.SetValue(NonWorkingDaysProperty, value);
}
private static void OnNonWorkingDaysChanged
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DatePicker datePicker = d as DatePicker;
if (e.NewValue != null && datePicker != null)
{
IEnumerable<NonWorkingDayDto>
localNonWorkingDays = (IEnumerable<NonWorkingDayDto>)e.NewValue;
Dictionary<DateTime,
string> blackoutDatesTextLookup = new Dictionary<DateTime, string>();
var toRemove = datePicker.BlackoutDates.Select
(x => x.Start).Except(localNonWorkingDays.Select(y => y.Date)).ToList();
foreach (DateTime dateTime in toRemove)
{
datePicker.BlackoutDates.Remove
(datePicker.BlackoutDates.Single(x => x.Start == dateTime));
}
foreach (NonWorkingDayDto nonWorkingDay in localNonWorkingDays)
{
if (!datePicker.BlackoutDates.Contains(nonWorkingDay.Date))
{
CalendarDateRange range = new CalendarDateRange(nonWorkingDay.Date);
datePicker.BlackoutDates.Add(range);
}
blackoutDatesTextLookup[nonWorkingDay.Date] = nonWorkingDay.Description;
}
datePicker.SetValue(BlackOutDatesTextLookupProperty, blackoutDatesTextLookup);
}
}
#endregion
}
Step 3
We also need a simple value converter which we can use to obtain the ToolTip. This simply looks up the text from the attached properties we declared above.
public class CalendarToolTipConverter : IMultiValueConverter
{
private CalendarToolTipConverter()
{
}
static CalendarToolTipConverter()
{
Instance = new CalendarToolTipConverter();
}
public static CalendarToolTipConverter Instance { get; private set; }
#region IMultiValueConverter Members
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
if (values[0] == null || values[1] == null || values[2] == null)
return null;
DateTime targetDate = (DateTime)values[0];
DatePicker dp = (DatePicker)values[1];
Dictionary<DateTime,
string> blackOutDatesTextLookup = (Dictionary<DateTime, string>)values[2];
string tooltip = null;
blackOutDatesTextLookup.TryGetValue(targetDate, out tooltip);
return tooltip;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, System.Globalization.CultureInfo culture)
{
return new object[0];
}
#endregion
}
Step 4
We would need to create a collection of NonWorking
days and add them to a DatePicker
. This would normally be a ViewModel
bindings, but for brevity I have just done this in code as follows:
List<NonWorkingDayDto> nonWorkingDayDtos = new List<NonWorkingDayDto>();
nonWorkingDayDtos.Add(new NonWorkingDayDto(DateTime.Now.AddDays(1).Date, "Today +1 is no good"));
nonWorkingDayDtos.Add(new NonWorkingDayDto(DateTime.Now.AddDays(2).Date, "Today +2 is no good"));
dp.SetValue(CalendarBehavior.NonWorkingDaysProperty, nonWorkingDayDtos);
Step 5
Next, we need a custom CalendarStyle
for the DatePicker
.
<Grid x:Name="LayoutRoot">
<DatePicker x:Name="dp" HorizontalAlignment="Left"
Margin="192,168,0,0" VerticalAlignment="Top"
CalendarStyle="{StaticResource NonWorkingDayCalendarStyle}"/>
</Grid>
Step 6
The last piece to the puzzle is how to apply the ToolTip
to the Calendar
, which is done below. Have a look at the Calendar
which uses a special “CalendarDayButtonStyle
” which is the specific Style
that deals with showing the ToolTip
.
<Style x:Key="NonWorkingDayTooltipCalendarDayButtonStyle"
TargetType="{x:Type CalendarDayButton}">
<Setter Property="MinWidth" Value="5"/>
<Setter Property="MinHeight" Value="5"/>
<Setter Property="FontSize" Value="10"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CalendarDayButton}">
<Grid x:Name="ToolTipTargetBorder">
.......
.......
.......
.......
<Rectangle x:Name="TodayBackground"
Fill="#FFAAAAAA"
Opacity="0"
RadiusY="1"
RadiusX="1"/>
<Rectangle x:Name="SelectedBackground"
Fill="#FFBADDE9"
Opacity="0"
RadiusY="1"
RadiusX="1"/>
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"/>
<Rectangle x:Name="HighlightBackground"
Fill="#FFBADDE9"
Opacity="0"
RadiusY="1"
RadiusX="1"/>
<ContentPresenter x:Name="NormalText"
TextElement.Foreground="Black"
HorizontalAlignment="
{TemplateBinding HorizontalContentAlignment}"
Margin="5,1,5,1"
VerticalAlignment="
{TemplateBinding VerticalContentAlignment}"/>
<Path x:Name="Blackout"
Data="M8.1772461,11.029181 L10.433105,11.029181 L11.700684,12.801641 .....z"
Fill="Black"
HorizontalAlignment="Stretch"
Margin="3"
Opacity="0"
RenderTransformOrigin="0.5,0.5"
Stretch="Fill"
VerticalAlignment="Stretch"/>
<Rectangle x:Name="DayButtonFocusVisual"
IsHitTestVisible="false"
RadiusY="1"
RadiusX="1"
Stroke="Black"
Visibility="Collapsed"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsBlackedOut" Value="True">
<Setter TargetName="ToolTipTargetBorder" Property="ToolTip">
<Setter.Value>
<MultiBinding Converter="
{x:Static local:CalendarToolTipConverter.Instance}">
<MultiBinding.Bindings>
<Binding />
<Binding RelativeSource="{RelativeSource FindAncestor,
AncestorType={x:Type DatePicker}}" />
<Binding RelativeSource="{RelativeSource FindAncestor,
AncestorType={x:Type DatePicker}}"
Path="(local:CalendarBehavior.BlackOutDatesTextLookup)" />
</MultiBinding.Bindings>
</MultiBinding>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="NonWorkingDayCalendarStyle" TargetType="{x:Type Calendar}">
<Setter Property="CalendarDayButtonStyle"
Value="{StaticResource NonWorkingDayTooltipCalendarDayButtonStyle}"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Background" Value="WhiteSmoke"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
As always, here is a small demo project: