Uncategorized

WPF : DatePicker With Holiday Blackouts And ToolTips

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 theuser 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:

 

image

 

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 prerformance. There must be an aweful 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>();

            // IMPORTANT NOTE
            // Due to the way the DatePicker Calendar BlackoutDates collection works
            // It is dog slow to clear the BlackoutDates collection and add items in one by one
            // so we have to perform some voodoo, where we remove the Blackout dates that are not 
            // in the new NonWorking value, and then add in ONLY those that are missing from the new
            // NonWorking days value into the BlackoutDates collection. It sucks but it makes a BIG difference
            // Using Clear() and Add in for loop its like 1500ms (Per DatePicker), and using this method its down to
            // something like 35ms (Per DatePicker).......So please do not change this logic
            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

    /// <summary>
    /// Gets a tool tip for a date passed in. Could also return null
    /// </summary>
    /// <remarks>
    /// The 'values' array parameter has the following elements:
    /// 
    /// values[0] = Binding #1: The date to be looked up. This should be set up as a pathless binding; 
    /// the Calendar control will provide the date.
    /// 
    /// values[1] = Binding #2: A binding reference to the DatePicker control that is invoking this converter.
    ///
    /// values[2] = Binding #3: Attached property CalendarBehavior.BlackOutDatesTextLookup for DatePicker
    /// </remarks>
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        // Exit if values not set
        if (values[0] == null || values[1] == null || values[2] == null)
            return null;

        // Get values passed in
        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;
    }

    /// <summary>
    /// Not used.
    /// </summary>
    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 : https://dl.dropboxusercontent.com/u/2600965/Blogposts/2013/DatepickerWithWorkingDayTooltips.zip

Advertisements
Uncategorized

XAML : Binding To Nullable Enums With Friendly Names

I was at work the other day and I had a situation where I needed to use a nullable enum. I wanted this to be available within a ComboBox but It was not compulsary for the users to select something. I also wanted to allow the ComboBox to print a friendly name of the Enum value.

The friendly name thing is something I have covered before in a previous article Binding-and-Using-Friendly-Enums-in-WPF but I have never really come across a time where I needed to allow a ComboBox with null selection support for the user.

I did not want to polute my enum values with a special “None” value, as that is really what the nullable aspect of it was doing. So I started to explore things a bit, and this is what I ended up with

My ViewModel and Enum

public enum Animals
{
    [EnumMember(Value = "Its a nice kitty")]
    Cat=1,

    [EnumMember(Value = "Its a bow wow")]
    Dog = 2
}

public class MainWindowViewModel : INPCBase
{
    private Animals? selectedAnimal;

    public MainWindowViewModel()
    {
        SelectedAnimal = null;
    }

    public Animals? SelectedAnimal
    {
        get { return this.selectedAnimal; }
        set
        {
            RaiseAndSetIfChanged(ref this.selectedAnimal, value, () => this.SelectedAnimal);
            MessageBox.Show(
                "SelectedAnimal: " + (!selectedAnimal.HasValue ?
                    NullHelper.NullComboStringValue : selectedAnimal.ToString()));
        }
    }

}

Next up was my View. The interesting points here are

  1. We use a ObjectDataProvider to supply the enum values
  2. We use a CompositeCollection (which doesn’t work with Binding ie ItemsSource being bound) where we get the Enum values and also a special “Null” value from a helper class
  3. We use a special NullableEnumToFriendlyNameConverter value converter to supply the friendly name lookup value for the enum (providing its not the special “Null” value
  4. We use a special NullableEnumConverter to convetr back to either null or a picked enum value for the ViewModel property setter
<Window x:Class="NullEnumCombo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:nullEnumCombo="clr-namespace:NullEnumCombo"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>

        <ObjectDataProvider x:Key="animalTypeFromEnum"
                            MethodName="GetValues"
                            ObjectType="{x:Type sys:Enum}">
            <ObjectDataProvider.MethodParameters>
                <x:Type TypeName="nullEnumCombo:Animals" />
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
    </Window.Resources>

    <Grid>
        <ComboBox Width="150" Height="20" 
            HorizontalAlignment="Center" 
            VerticalAlignment="Center"
            SelectedItem="{Binding SelectedAnimal, 
                Converter={x:Static nullEnumCombo:NullableEnumConverter.Instance},
                    ConverterParameter={x:Static nullEnumCombo:Animals.Cat}}">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock  Text="{Binding   Path=., Mode=OneWay,
                        Converter={x:Static 
                           nullEnumCombo:NullableEnumToFriendlyNameConverter.Instance}}"
                        Height="Auto"
                        Margin="0"
                        VerticalAlignment="Center"/>
                </DataTemplate>
            </ComboBox.ItemTemplate>

            <ComboBox.ItemsSource>
                <CompositeCollection>
                    <x:Static Member="nullEnumCombo:NullHelper.NullComboStringValue"/>
                    <CollectionContainer
                        Collection="{Binding Source={StaticResource animalTypeFromEnum}}" />
                </CompositeCollection>
            </ComboBox.ItemsSource>

        </ComboBox>
    </Grid>
</Window>

Here are the 2 value converters and the small helper class

public class NullableEnumConverter : IValueConverter
{
    private NullableEnumConverter()
    {

    }

    static NullableEnumConverter()
    {
        Instance = new NullableEnumConverter();
    }

    public static NullableEnumConverter Instance { get; private set; }

    public object Convert(object value, Type targetType, 
           object parameter, CultureInfo culture)
    {
        if (value == null)
        {
            return NullHelper.NullComboStringValue;
        }
        return value;
    }

    public object ConvertBack(object value, Type targetType, 
           object parameter, CultureInfo culture)
    {
        Type enumType = parameter.GetType();
        if (value.ToString().Equals(NullHelper.NullComboStringValue))
        {
            return null;
        }
        object rawEnum = Enum.Parse(enumType, value.ToString());
        return System.Convert.ChangeType(rawEnum, enumType);
    }
}

/// <summary>
/// This class simply takes an enum and uses some reflection to obtain
/// the friendly name for the enum. Where the friendlier name is
/// obtained using the EnumMemberAttribute, which hold the localized
/// value read from the resource file for the enum
/// </summary>
[ValueConversion(typeof(object), typeof(String))]
public class NullableEnumToFriendlyNameConverter : IValueConverter
{

    private NullableEnumToFriendlyNameConverter()
    {

    }

    static NullableEnumToFriendlyNameConverter()
    {
        Instance = new NullableEnumToFriendlyNameConverter();
    }

    public static NullableEnumToFriendlyNameConverter Instance { get; private set; }

    #region IValueConverter implementation

    /// <summary>
    /// Convert value for binding from source object
    /// </summary>
    public object Convert(object value, Type targetType, 
           object parameter, CultureInfo culture)
    {
        // To get around the stupid wpf designer bug
        if (value != null && !string.IsNullOrEmpty(value.ToString()) && 
             !value.ToString().Equals(NullHelper.NullComboStringValue))
        {
            FieldInfo fi = value.GetType().GetField(value.ToString());

            // To get around the stupid wpf designer bug
            if (fi != null)
            {
                var attributes =
                    (EnumMemberAttribute[])fi.GetCustomAttributes(typeof(EnumMemberAttribute), false);

                return ((attributes.Length > 0) &&
                        (!String.IsNullOrEmpty(attributes[0].Value)))
                            ?
                                attributes[0].Value
                            : value.ToString();
            }
        }

        return NullHelper.NullComboStringValue;
    }

    /// <summary>
    /// ConvertBack value from binding back to source object
    /// </summary>
    public object ConvertBack(object value, Type targetType, 
           object parameter, CultureInfo culture)
    {
        throw new Exception("Cant convert back");
    }
    #endregion
}

public class NullHelper
{
    public static string NullComboStringValue
    {
        get
        {
            return "(None)";
        }
    }
}
WinRt

WinRT : StyleMVVM Demo App 2 of 2

So i have managed to get myself out of bed (yes feeling quite sick today), just long enough to push out something I finished writing on Friday. Which means I don’t have to take my laptop with me tomorrow now. Less weight, cool.

 

Anyway its part 2 of my StyleMVVM series. This time we look at a full demo app I have written in Style MVVM. I think this app is worth a look:

 

http://www.codeproject.com/Articles/678087/WinRT-StyleMVVM-Demo-2-of-2

 

Enjoy