Localizing a Silverlight DatePicker control, a study in pain thresholds

For shame Microsoft. There is currently no way to localize a DatePicker. Instead of letting us simply specify a format string or any of the other beautiful conventions we are forced to basically re-template the entire control.

In our application, at my day job, we support many different languages/cultures. This was becoming a mess with the various date formats so we decreed that all dates would be in the ISO standard format (yyyy-MM-dd HH:mm:ss). Styling everything in the app was a 10 minute job, until I got to the DatePickers that we use for our search fields. I couldn't find a nice FormatString to set in XAML. I couldn't find a way to set anything else in the code behind either. I changed how we display the selected date by reformatting the text string that the Text property is bound to. That was fixed, but now I was left with a watermark stating when the actual format was yyyy-mm-dd I resorted to Google.

I found a lot of rants and only a couple possible solutions.

http://blogs.msdn.com/b/silverlight_sdk/archive/2010/05/06/changing-the-watermark-text-in-a-datepicker-control.aspx

This blog shows a way to change the watermark, but the solution quickly fails after selecting a date and then deleting it. It will revert back to the original watermark. Unacceptable.

One of the last comments says that this code will fix it:
[sourcecode language="csharp"] public class CustomDatePicker : DatePicker
{ public CustomDatePicker() { SelectedDateChanged += DateChanged; Unloaded += CustomDatePickerUnloaded; }

public override void OnApplyTemplate() { base.OnApplyTemplate(); HandleWatermark(); }

private void DateChanged(object sender, SelectionChangedEventArgs e) { HandleWatermark(); }

private void CustomDatePickerUnloaded(object sender, RoutedEventArgs e) { SelectedDateChanged -= DateChanged; }

public void HandleWatermark() { var box = GetTemplateChild("TextBox") as DatePickerTextBox;

   if (box == null) return;

   if (SelectedDateFormat == DatePickerFormat.Short)
       box.Watermark = "mm/dd/yyyy";

} } [/sourcecode]

This code is closer, but there is still a bug. From the time that you delete the date until the time that you unfocus the DatePicker the original watermark is displayed. So close, but so far.

I eventually found this solution, which seems like entirely too much work for such a simple problem. You basically have to style the DatePicker and the DatePickerTextBox that it contains.

[sourcecode language="xml"] <Style x:Key="IsoDatePickerTextBoxStyle" TargetType="SystemWindowsControlsPrimitives:DatePickerTextBox"> <Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="HorizontalContentAlignment" Value="Left"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="SystemWindowsControlsPrimitives:DatePickerTextBox"> <Grid x:Name="Root"> <Grid.Resources> <SolidColorBrush x:Key="WatermarkBrush" Color="#FFAAAAAA"/> </Grid.Resources> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="CommonStates"> <VisualStateGroup.Transitions> <VisualTransition GeneratedDuration="0"/> <VisualTransition GeneratedDuration="0:0:0.1" To="MouseOver"/> </VisualStateGroup.Transitions> <VisualState x:Name="Normal"/> <VisualState x:Name="MouseOver"> <Storyboard> <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="ContentElement"> <SplineColorKeyFrame KeyTime="0" Value="#FF99C1E2"/> </ColorAnimationUsingKeyFrames> <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="ContentElement2"> <SplineColorKeyFrame KeyTime="0" Value="#FF99C1E2"/> </ColorAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> <VisualStateGroup x:Name="WatermarkStates"> <VisualStateGroup.Transitions> <VisualTransition GeneratedDuration="0"/> </VisualStateGroup.Transitions> <VisualState x:Name="Unwatermarked"/> <VisualState x:Name="Watermarked"> <Storyboard> <DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ContentElement"/> <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Watermark"/> </Storyboard> </VisualState> </VisualStateGroup> <VisualStateGroup x:Name="FocusStates"> <VisualStateGroup.Transitions> <VisualTransition GeneratedDuration="0"/> </VisualStateGroup.Transitions> <VisualState x:Name="Unfocused"/> <VisualState x:Name="Focused"> <Storyboard> <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="FocusVisual"/> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Border x:Name="Border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="1" Opacity="1"> <Grid x:Name="WatermarkContent" Background="{TemplateBinding Background}"> <Border x:Name="ContentElement" BorderBrush="#FFFFFFFF" BorderThickness="1" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}"/> <Border x:Name="ContentElement2" BorderBrush="#FFFFFFFF" BorderThickness="1"> <ContentControl x:Name="Watermark" Background="{TemplateBinding Background}" Content="{Binding ApplicationStrings.WatermarkDateFormat, Source={StaticResource ResourceWrapper}}" Foreground="{StaticResource WatermarkBrush}" FontSize="{TemplateBinding FontSize}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="False" IsTabStop="False" Opacity="0" Padding="2" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> </Border> <Border x:Name="FocusVisual" BorderBrush="#FF6DBDD1" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="1" IsHitTestVisible="False" Opacity="0"/> </Grid> </Border> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style x:Key="IsoDatePickerStyle" TargetType="controls:DatePicker"> <Setter Property="IsTabStop" Value="False"/> <Setter Property="Background" Value="#FFFFFFFF"/> <Setter Property="Padding" Value="2"/> <Setter Property="SelectionBackground" Value="#FF444444"/> <Setter Property="BorderBrush"> <Setter.Value> <LinearGradientBrush EndPoint=".5,0" StartPoint=".5,1"> <GradientStop Color="#FF617584" Offset="0"/> <GradientStop Color="#FF718597" Offset="0.375"/> <GradientStop Color="#FF8399A9" Offset="0.375"/> <GradientStop Color="#FFA3AEB9" Offset="1"/> </LinearGradientBrush> </Setter.Value> </Setter> <Setter Property="BorderThickness" Value="1"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="controls:DatePicker"> <Grid x:Name="Root"> <Grid.Resources> <SolidColorBrush x:Key="DisabledBrush" Color="#8CFFFFFF"/> <ControlTemplate x:Key="DropDownButtonTemplate" TargetType="Button"> <Grid FlowDirection="LeftToRight"> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="CommonStates"> <VisualStateGroup.Transitions> <VisualTransition GeneratedDuration="0"/> <VisualTransition GeneratedDuration="0:0:0.1" To="MouseOver"/> <VisualTransition GeneratedDuration="0:0:0.1" To="Pressed"/> </VisualStateGroup.Transitions> <VisualState x:Name="Normal"/> <VisualState x:Name="MouseOver"> <Storyboard> <ColorAnimation Duration="0" To="#FF448DCA" Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" Storyboard.TargetName="Background"/> <ColorAnimationUsingKeyFrames BeginTime="0" Duration="00:00:00.001" Storyboard.TargetProperty="(Border.Background).(GradientBrush.GradientStops)[3].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"> <SplineColorKeyFrame KeyTime="0" Value="#7FFFFFFF"/> </ColorAnimationUsingKeyFrames> <ColorAnimationUsingKeyFrames BeginTime="0" Duration="00:00:00.001" Storyboard.TargetProperty="(Border.Background).(GradientBrush.GradientStops)[2].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"> <SplineColorKeyFrame KeyTime="0" Value="#CCFFFFFF"/> </ColorAnimationUsingKeyFrames> <ColorAnimationUsingKeyFrames BeginTime="0" Duration="00:00:00.001" Storyboard.TargetProperty="(Border.Background).(GradientBrush.GradientStops)[1].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"> <SplineColorKeyFrame KeyTime="0" Value="#F2FFFFFF"/> </ColorAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="Pressed"> <Storyboard> <ColorAnimationUsingKeyFrames BeginTime="0" Duration="00:00:00.001" Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" Storyboard.TargetName="Background"> <SplineColorKeyFrame KeyTime="0" Value="#FF448DCA"/> </ColorAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames BeginTime="0" Duration="00:00:00.001" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="Highlight"> <SplineDoubleKeyFrame KeyTime="0" Value="1"/> </DoubleAnimationUsingKeyFrames> <ColorAnimationUsingKeyFrames BeginTime="0" Duration="00:00:00.001" Storyboard.TargetProperty="(Border.Background).(GradientBrush.GradientStops)[1].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"> <SplineColorKeyFrame KeyTime="0" Value="#EAFFFFFF"/> </ColorAnimationUsingKeyFrames> <ColorAnimationUsingKeyFrames BeginTime="0" Duration="00:00:00.001" Storyboard.TargetProperty="(Border.Background).(GradientBrush.GradientStops)[2].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"> <SplineColorKeyFrame KeyTime="0" Value="#C6FFFFFF"/> </ColorAnimationUsingKeyFrames> <ColorAnimationUsingKeyFrames BeginTime="0" Duration="00:00:00.001" Storyboard.TargetProperty="(Border.Background).(GradientBrush.GradientStops)[3].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"> <SplineColorKeyFrame KeyTime="0" Value="#6BFFFFFF"/> </ColorAnimationUsingKeyFrames> <ColorAnimationUsingKeyFrames BeginTime="0" Duration="00:00:00.001" Storyboard.TargetProperty="(Border.Background).(GradientBrush.GradientStops)[0].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"> <SplineColorKeyFrame KeyTime="0" Value="#F4FFFFFF"/> </ColorAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="Disabled"> <Storyboard> <DoubleAnimationUsingKeyFrames BeginTime="0" Duration="00:00:00.001" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="DisabledVisual"> <SplineDoubleKeyFrame KeyTime="0" Value="1"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Grid Background="#11FFFFFF" HorizontalAlignment="Center" Height="18" Margin="0" VerticalAlignment="Center" Width="19"> <Grid.ColumnDefinitions> <ColumnDefinition Width="20"/> <ColumnDefinition Width="20"/> <ColumnDefinition Width="20"/> <ColumnDefinition Width="20"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="23"/> <RowDefinition Height="19"/> <RowDefinition Height="19"/> <RowDefinition Height="19"/> </Grid.RowDefinitions> <Border x:Name="Highlight" BorderBrush="#FF6DBDD1" BorderThickness="1" Grid.ColumnSpan="4" CornerRadius="0,0,1,1" Margin="-1" Opacity="0" Grid.Row="0" Grid.RowSpan="4"/> <Border x:Name="Background" BorderBrush="#FFFFFFFF" BorderThickness="1" Background="#FF1F3B53" Grid.ColumnSpan="4" CornerRadius=".5" Margin="0,-1,0,0" Opacity="1" Grid.Row="1" Grid.RowSpan="3"/> <Border x:Name="BackgroundGradient" BorderBrush="#BF000000" BorderThickness="1" Grid.ColumnSpan="4" CornerRadius=".5" Margin="0,-1,0,0" Opacity="1" Grid.Row="1" Grid.RowSpan="3"> <Border.Background> <LinearGradientBrush EndPoint=".7,1" StartPoint=".7,0"> <GradientStop Color="#FFFFFFFF" Offset="0"/> <GradientStop Color="#F9FFFFFF" Offset="0.375"/> <GradientStop Color="#E5FFFFFF" Offset="0.625"/> <GradientStop Color="#C6FFFFFF" Offset="1"/> </LinearGradientBrush> </Border.Background> </Border> <Rectangle Grid.ColumnSpan="4" Grid.RowSpan="1" StrokeThickness="1"> <Rectangle.Fill> <LinearGradientBrush EndPoint="0.3,-1.1" StartPoint="0.46,1.6"> <GradientStop Color="#FF4084BD"/> <GradientStop Color="#FFAFCFEA" Offset="1"/> </LinearGradientBrush> </Rectangle.Fill> <Rectangle.Stroke> <LinearGradientBrush EndPoint="0.48,-1" StartPoint="0.48,1.25"> <GradientStop Color="#FF494949"/> <GradientStop Color="#FF9F9F9F" Offset="1"/> </LinearGradientBrush> </Rectangle.Stroke> </Rectangle> <Path Grid.ColumnSpan="4" Grid.Column="0" Data="M11.426758,8.4305077 L11.749023,8.4305077 L11.749023,16.331387 L10.674805,16.331387 L10.674805,10.299648 L9.0742188,11.298672 L9.0742188,10.294277 C9.4788408,10.090176 9.9094238,9.8090878 10.365967,9.4510155 C10.82251,9.0929432 11.176106,8.7527733 11.426758,8.4305077 z M14.65086,8.4305077 L18.566387,8.4305077 L18.566387,9.3435936 L15.671368,9.3435936 L15.671368,11.255703 C15.936341,11.058764 16.27293,10.960293 16.681133,10.960293 C17.411602,10.960293 17.969301,11.178717 18.354229,11.615566 C18.739157,12.052416 18.931622,12.673672 18.931622,13.479336 C18.931622,15.452317 18.052553,16.438808 16.294415,16.438808 C15.560365,16.438808 14.951641,16.234707 14.468243,15.826504 L14.881817,14.929531 C15.368796,15.326992 15.837872,15.525723 16.289043,15.525723 C17.298809,15.525723 17.803692,14.895514 17.803692,13.635098 C17.803692,12.460618 17.305971,11.873379 16.310528,11.873379 C15.83071,11.873379 15.399232,12.079271 15.016094,12.491055 L14.65086,12.238613 z" Fill="#FF2F2F2F" HorizontalAlignment="Center" Margin="4,3,4,3" Grid.Row="1" Grid.RowSpan="3" RenderTransformOrigin="0.5,0.5" Stretch="Fill" VerticalAlignment="Center"/> <Ellipse Grid.ColumnSpan="4" Fill="#FFFFFFFF" HorizontalAlignment="Center" Height="3" StrokeThickness="0" VerticalAlignment="Center" Width="3"/> <Border x:Name="DisabledVisual" BorderBrush="#B2FFFFFF" BorderThickness="1" Grid.ColumnSpan="4" CornerRadius="0,0,.5,.5" Opacity="0" Grid.Row="0" Grid.RowSpan="4"/> </Grid> </Grid> </ControlTemplate> </Grid.Resources> <Grid.ColumnDefinitions> <ColumnDefinition Width=""/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="Normal"/> <VisualState x:Name="Disabled"> <Storyboard> <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="DisabledVisual"/> </Storyboard> </VisualState> </VisualStateGroup> <VisualStateGroup x:Name="ValidationStates"> <VisualState x:Name="Valid"/> <VisualState x:Name="InvalidUnfocused"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationErrorElement"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="InvalidFocused"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationErrorElement"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="IsOpen" Storyboard.TargetName="validationTooltip"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <System:Boolean>True</System:Boolean> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <System_Windows_Controls_Primitives:DatePickerTextBox Background="White" x:Name="TextBox" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Grid.Column="0" Padding="{TemplateBinding Padding}" SelectionBackground="{TemplateBinding SelectionBackground}" Style="{StaticResource IsoDatePickerTextBoxStyle}"/> <Border x:Name="ValidationErrorElement" BorderBrush="#FFDB000C" BorderThickness="1" Grid.Column="0" CornerRadius="1" Visibility="Collapsed"> <ToolTipService.ToolTip> <ToolTip x:Name="validationTooltip" DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}" Placement="Right" PlacementTarget="{Binding ElementName=TextBox}" Template="{StaticResource CommonValidationToolTipTemplate}"> <ToolTip.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="IsHitTestVisible" Storyboard.TargetName="validationTooltip"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <System:Boolean>true</System:Boolean> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </ToolTip.Triggers> </ToolTip> </ToolTipService.ToolTip> <Grid Background="Transparent" HorizontalAlignment="Right" Height="12" Margin="1,-4,-4,0" VerticalAlignment="Top" Width="12"> <Path Data="M 1,0 L6,0 A 2,2 90 0 1 8,2 L8,7 z" Fill="#FFDC000C" Margin="1,3,0,0"/> <Path Data="M 0,0 L2,0 L 8,6 L8,8" Fill="#FFFFFF" Margin="1,3,0,0"/> </Grid> </Border> <Button x:Name="Button" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Grid.Column="1" Foreground="{TemplateBinding Foreground}" Margin="2,0,2,0" Template="{StaticResource DropDownButtonTemplate}" Width="20"/> <Grid x:Name="DisabledVisual" Grid.ColumnSpan="2" IsHitTestVisible="False" Opacity="0"> <Grid.ColumnDefinitions> <ColumnDefinition Width=""/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Rectangle Fill="#8CFFFFFF" RadiusY="1" RadiusX="1"/> <Rectangle Grid.Column="1" Fill="#8CFFFFFF" Height="18" Margin="2,0,2,0" RadiusY="1" RadiusX="1" Width="19"/> </Grid> <Popup x:Name="Popup"/> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> [/sourcecode]

You then reference this style in your DatePicker control as follows:

[sourcecode language="xml"] <controls:DatePicker Style="{StaticResource IsoDatePickerStyle}" /> [/sourcecode]

And there you go, a several hundred line solution to a one line problem. For shame Microsoft.