Question

I've been asked to sort of create a hack around the already existing DateTimePicker control. Normally, the date/time picker has that wonderful image of a calendar, then the textbox next to it for showing an actual date. The user can click on the image and have the popup calendar presented to them, and upon selection, the date is refreshed into the textbox area.

The problem I have is the other designers don't like the calendar graphic and want just a plain textbox control, but if the user double-clicks, to open a popup calendar, get the date and refresh it. I'm very close on this thanks to other help as found out here on S/O.

So, to describe my control template, and keeping as plain (non custom control unless I need to per suggestions). The control template is based on a textbox control. We have a border with the PART_ContentHost for the textbox, and then a popup of a standard calendar control.

For the control template triggers, I have one linked to the ScrollViewer (textbox entry area) to it's MouseDoubleClick event. If triggered, sets the IsOpen of the popup to true and exposes the calendar. This works fine.

Now, to finish it. If the user selects a date from the calendar, the next trigger closes the popup (IsOpen set to false). This too works.

My problem. I also want upon selection to take the date selected and get its ToString() date representation put into the ScrollViewer.Content (x:Name="PART_ContentHost).

<ControlTemplate TargetType="{x:Type TextBox}" x:Key="CTTextBox" >
   <StackPanel>
      <Border x:Name="targetBorder" 
         BorderBrush="{TemplateBinding BorderBrush}"
         SnapsToDevicePixels="true">

         <ScrollViewer x:Name="PART_ContentHost"
            Background="{TemplateBinding Background}"
            BorderBrush="{TemplateBinding BorderBrush}"
            Foreground="{TemplateBinding Foreground}" />
      </Border>
      <Popup PlacementTarget="{Binding ElementName=PART_ContentHost}" x:Name="PopCal">
         <Calendar x:Name="ActualCalendar"/>
      </Popup>
   </StackPanel>

   <ControlTemplate.Triggers>
      <EventTrigger RoutedEvent="ScrollViewer.MouseDoubleClick" SourceName="PART_ContentHost">
         <BeginStoryboard>
            <Storyboard>
               <BooleanAnimationUsingKeyFrames 
                  Storyboard.TargetName="PopCal" 
                  Storyboard.TargetProperty="(Popup.IsOpen)">
                  <DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="True"/>
               </BooleanAnimationUsingKeyFrames>
            </Storyboard>
         </BeginStoryboard>
      </EventTrigger>

      <EventTrigger RoutedEvent="Calendar.SelectedDatesChanged" SourceName="ActualCalendar">
         <BeginStoryboard>
            <Storyboard>
               <BooleanAnimationUsingKeyFrames 
                  Storyboard.TargetName="PopCal" 
                  Storyboard.TargetProperty="(Popup.IsOpen)">
                  <DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="False"/>
               </BooleanAnimationUsingKeyFrames>
            </Storyboard>


            WHAT WOULD I PUT HERE to have the selected date of the popup calendar
            inserted into the content of the PART_ContentHost...
            <Storyboard>
               <BooleanAnimationUsingKeyFrames 
                  Storyboard.TargetName="PART_ContentHost" 
                  Storyboard.TargetProperty="(Content)">
                  <DiscreteBooleanKeyFrame KeyTime="00:00:00" Value=" ????? "/>
               </BooleanAnimationUsingKeyFrames>
            </Storyboard>

         </BeginStoryboard>
      </EventTrigger>
   </ControlTemplate.Triggers>
</ControlTemplate>

<Style TargetType="{x:Type TextBox}" x:Key="STextBox" >
   <!--<Setter Property="OverridesDefaultStyle" Value="True"/>-->
   <Setter Property="FontFamily" Value="Arial" />
   <Setter Property="FontSize" Value="12" />
   <Setter Property="Height" Value="20" />
   <Setter Property="Width" Value="100" />
   <Setter Property="VerticalContentAlignment" Value="Bottom" />
   <Setter Property="HorizontalContentAlignment" Value="Left" />
   <Setter Property="HorizontalAlignment" Value="Left" />
   <!-- Padding is Left, Top, Right, Bottom -->
   <Setter Property="Padding" Value="2,0,0,2" />
   <Setter Property="Margin" Value="0,0,0,0" />

   <Setter Property="Visibility" Value="Visible" />
   <Setter Property="IsEnabled" Value="True" />

   <Setter Property="CharacterCasing" Value="Upper" />
   <Setter Property="BorderThickness" Value="1" />

   <Setter Property="BorderBrush" Value="Black" />
   <Setter Property="Background" Value="White" />
   <Setter Property="Foreground" Value="Black" />

   <Setter Property="Template" Value="{StaticResource CTTextBox}" />
</Style>
Was it helpful?

Solution

I am sure that the problem can be solved in several ways, but in these situations I usually do attached behaviour. But while situation is not quite normal, because there is realized a template for the control. In any case, I think attached behaviour would suit for this case, in your place I would be so done.

Attached behaviour is very powerful and convenient solution that fully satisfies the MVVM pattern, which can also be used in the Blend (with a pre-defined interface). Attached behaviour - is an attached property, which has an event handler to change this property and all the logic is implemented in this handler.

Before starting to realize the behaviour I would consider some changes that I made to your template.

I'm a little not understand why you are using as PART_ContentHost ScrollViewer control, perhaps there will be several dates and will need to display them with scrolling. In WPF, there are two controls that are needed to display the contents:

  1. ContentPresenter
  2. ContentControl

that is their main goal. The first lightest is usually always used in the templates, but it does not support the events, that we need to coordinate the work, so I chose ContentControl. On the little things added binding properties for the template, set for Popup:

AllowsTransparency="True"
VerticalOffset="4"
HorizontalOffset="-5" 

For better visualization. Now move on to the example of behavior.

XAML

<Window.Resources>
    <ControlTemplate x:Key="CTTextBox" TargetType="{x:Type TextBox}">
        <StackPanel AttachedBehaviors:SelectDateBehavior.IsEnabled="True"> <!-- Here is determined behaviour -->
            <Border x:Name="targetBorder" 
                    Width="{TemplateBinding Width}"
                    Height="{TemplateBinding Height}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}"
                    Background="{TemplateBinding Background}"
                    TextBlock.Foreground="{TemplateBinding Foreground}"
                    SnapsToDevicePixels="True">

                <ContentControl x:Name="ContentHost"
                                Content="{TemplateBinding Text}"
                                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
                                Margin="4,0,0,0" />
            </Border>

            <Popup x:Name="PopCal" 
                   AllowsTransparency="True"
                   VerticalOffset="4"
                   HorizontalOffset="-5"
                   PlacementTarget="{Binding ElementName=ContentHost}">

                <Calendar x:Name="ActualCalendar" />
            </Popup>
        </StackPanel>
    </ControlTemplate>

    <Style TargetType="{x:Type TextBox}" x:Key="STextBox">
        <Setter Property="FontFamily" Value="Arial" />
        <Setter Property="FontSize" Value="12" />
        <Setter Property="Height" Value="25" />
        <Setter Property="Width" Value="100" />
        <Setter Property="VerticalContentAlignment" Value="Center" />
        <Setter Property="HorizontalContentAlignment" Value="Left" />
        <Setter Property="HorizontalAlignment" Value="Center" />       
        <Setter Property="CharacterCasing" Value="Upper" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="BorderBrush" Value="Gray" />
        <Setter Property="Background" Value="AliceBlue" />
        <Setter Property="Foreground" Value="Black" />
        <Setter Property="Template" Value="{StaticResource CTTextBox}" />
    </Style>
</Window.Resources>

<Grid>
    <TextBox Style="{StaticResource STextBox}"
             Text="Select date" />
</Grid>

Atatched Behavior

public class SelectDateBehavior
{
    #region IsEnabled Dependency Property

    public static readonly DependencyProperty IsEnabledProperty;

    public static void SetIsEnabled(DependencyObject DepObject, bool value)
    {
        DepObject.SetValue(IsEnabledProperty, value);
    }

    public static bool GetIsEnabled(DependencyObject DepObject)
    {
        return (bool)DepObject.GetValue(IsEnabledProperty);
    }

    static SelectDateBehavior()
    {
        IsEnabledProperty = DependencyProperty.RegisterAttached("IsEnabled",
                                                            typeof(bool),
                                                            typeof(SelectDateBehavior),
                                                            new UIPropertyMetadata(false, IsEnabledChanged));
    }

    #endregion

    #region IsEnabledChanged Handler

    private static void IsEnabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) 
    {
        Panel panel = sender as Panel;

        if (panel != null)
        {
            if (e.NewValue is bool && ((bool)e.NewValue) == true)
            {
                panel.Loaded += new RoutedEventHandler(panelLoaded);
            }
            else
            {
                panel.Loaded -= new RoutedEventHandler(panelLoaded);
            }
        }
    }

    #endregion

    #region Panel Loaded Handler

    private static void panelLoaded(object sender, RoutedEventArgs e) 
    {
        Panel panel = sender as Panel;
        Border border = panel.FindName("targetBorder") as Border;
        ContentControl contentHost = border.FindName("ContentHost") as ContentControl;
        Popup popup = panel.FindName("PopCal") as Popup;

        if (popup != null)
        {
            Calendar calendar = popup.FindName("ActualCalendar") as Calendar;                
            calendar.SelectedDatesChanged += new EventHandler<SelectionChangedEventArgs>(calendarSelectedDatesChanged);
        }

        if (contentHost != null)
        {
            contentHost.MouseDoubleClick += new MouseButtonEventHandler(contentHostMouseDoubleClick);
        }          
    }

    #endregion

    #region ContentHost MouseDoubleClick Handler

    private static void contentHostMouseDoubleClick(object sender, MouseButtonEventArgs e) 
    {
        ContentControl contentHost = sender as ContentControl;
        Border border = contentHost.Parent as Border;
        Panel panel = border.Parent as Panel;
        Popup popup = panel.FindName("PopCal") as Popup;

        if (popup != null) 
        {
            popup.IsOpen = true;
        }
    }

    #endregion

    #region Calendar SelectedDatesChanged Handler

    private static void calendarSelectedDatesChanged(object sender, SelectionChangedEventArgs e) 
    {
        Calendar calendar = sender as Calendar;
        Popup popup = calendar.Parent as Popup;
        Panel panel = popup.Parent as Panel;
        Border border = panel.FindName("targetBorder") as Border;
        ContentControl contentHost = border.FindName("ContentHost") as ContentControl;

        if (popup != null) 
        {
            contentHost.Content = calendar.SelectedDate;
            popup.IsOpen = false;
        }
    }

    #endregion
}

Output

enter image description here

Setting the current date is carried here:

private static void calendarSelectedDatesChanged(object sender, SelectionChangedEventArgs e) 
{
    // Skipped a few lines of code
    if (popup != null) 
    {
        contentHost.Content = calendar.SelectedDate;
        popup.IsOpen = false;
    }
}

Some notes

Let me draw attention to some features. First, we had to get rid of EvenTrigger Storyboard, because in WPF animation at the highest priority setting values​​, this means that if once we set the value IsOpen in animation, access from other sources (code, etc.) is not possible. So I had all the triggers / events leave on the side behavior.

Second, the solution is strongly tied to the structure of the template and control. This means that if you have to change the structure of the template, you have to change and behavior (probably not much).

This example is available here.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top