I had a requirement wherein I wanted to have a DatePicker
which displays month
and year
only. The selected month and year should be displayed in the DatePicker's
TextBox
like .
I implemented this solution and it worked fine until I opened the DatePicker
,a pop up
appeared and I didn't change the already selected month and clicked outside. The DatePicker's Popup is not closing.
The source code has been taken from the above link but pasting the code as well
namespace Sample.Models
{
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using Calendar = System.Windows.Controls.Calendar;
using CalendarMode = System.Windows.Controls.CalendarMode;
using CalendarModeChangedEventArgs = System.Windows.Controls.CalendarModeChangedEventArgs;
using DatePicker = System.Windows.Controls.DatePicker;
public class DatePickerCalendar
{
public static readonly DependencyProperty IsMonthYearProperty =
DependencyProperty.RegisterAttached("IsMonthYear", typeof(bool), typeof(DatePickerCalendar),
new PropertyMetadata(OnIsMonthYearChanged));
public static bool GetIsMonthYear(DependencyObject dobj)
{
return (bool)dobj.GetValue(IsMonthYearProperty);
}
public static void SetIsMonthYear(DependencyObject dobj, bool value)
{
dobj.SetValue(IsMonthYearProperty, value);
}
private static void OnIsMonthYearChanged(DependencyObject dobj, DependencyPropertyChangedEventArgs e)
{
string parameter = string.Empty;
try
{
parameter = "OnIsMonthYearChanged";
var datePicker = (DatePicker)dobj;
Application.Current.Dispatcher
.BeginInvoke(DispatcherPriority.Loaded,
new Action<DatePicker, DependencyPropertyChangedEventArgs>(SetCalendarEventHandlers),
datePicker, e);
}
catch (Exception ex)
{
}
}
private static void SetCalendarEventHandlers(DatePicker datePicker, DependencyPropertyChangedEventArgs e)
{
string parameter = string.Empty;
try
{
parameter = "SetCalendarEventHandlers";
if (e.NewValue == e.OldValue)
return;
if ((bool)e.NewValue)
{
datePicker.CalendarOpened += DatePickerOnCalendarOpened;
datePicker.CalendarClosed += DatePickerOnCalendarClosed;
}
else
{
datePicker.CalendarOpened -= DatePickerOnCalendarOpened;
datePicker.CalendarClosed -= DatePickerOnCalendarClosed;
}
}
catch(Exception ex)
{
}
}
private static void DatePickerOnCalendarOpened(object sender, RoutedEventArgs routedEventArgs)
{
string parameter = string.Empty;
try
{
parameter = "DatePickerOnCalendarOpened";
var calendar = GetDatePickerCalendar(sender);
calendar.DisplayMode = CalendarMode.Year;
calendar.DisplayModeChanged += CalendarOnDisplayModeChanged;
}
catch( Exception ex)
{
}
}
private static void DatePickerOnCalendarClosed(object sender, RoutedEventArgs routedEventArgs)
{
string parameter = string.Empty;
try
{
parameter = "DatePickerOnCalendarClosed";
var datePicker = (DatePicker)sender;
var calendar = GetDatePickerCalendar(sender);
datePicker.SelectedDate = calendar.SelectedDate;
calendar.DisplayModeChanged -= CalendarOnDisplayModeChanged;
}
catch(Exception ex)
{
}
}
private static void CalendarOnDisplayModeChanged(object sender, CalendarModeChangedEventArgs e)
{
string parameter = string.Empty;
try
{
parameter = "CalendarOnDisplayModeChanged";
var calendar = (Calendar)sender;
if (calendar.DisplayMode != CalendarMode.Month)
return;
calendar.SelectedDate = GetSelectedCalendarDate(calendar.DisplayDate);
var datePicker = GetCalendarsDatePicker(calendar);
datePicker.IsDropDownOpen = false;
}
catch(Exception ex)
{
}
}
private static Calendar GetDatePickerCalendar(object sender)
{
string parameter = string.Empty;
var datePicker = (DatePicker)sender;
var popup = (Popup)datePicker.Template.FindName("PART_Popup", datePicker);
try
{
parameter = "GetDatePickerCalendar";
return ((Calendar)popup.Child);
}
catch (Exception ex)
{
return ((Calendar)popup.Child);
}
}
private static DatePicker GetCalendarsDatePicker(FrameworkElement child)
{
var parent = (FrameworkElement)child.Parent;
string parameter = string.Empty;
try
{
if (parent.Name == "PART_Root")
return (DatePicker)parent.TemplatedParent;
return GetCalendarsDatePicker(parent);
}
catch (Exception ex)
{
return GetCalendarsDatePicker(parent);
}
}
private static DateTime? GetSelectedCalendarDate(DateTime? selectedDate)
{
try
{
if (!selectedDate.HasValue)
{
return null;
}
return new DateTime(selectedDate.Value.Year, selectedDate.Value.Month, 1);
}
catch(Exception ex)
{
return new DateTime(selectedDate.Value.Year, selectedDate.Value.Month,1 );
}
}
}
public class DatePickerDateFormat
{
public static readonly DependencyProperty DateFormatProperty =
DependencyProperty.RegisterAttached("DateFormat", typeof(string), typeof(DatePickerDateFormat),
new PropertyMetadata(OnDateFormatChanged));
public static string GetDateFormat(DependencyObject dobj)
{
try
{
return (string)dobj.GetValue(DateFormatProperty);
}
catch(Exception ex)
{
return (string)dobj.GetValue(DateFormatProperty);
}
}
public static void SetDateFormat(DependencyObject dobj, string value)
{
dobj.SetValue(DateFormatProperty, value);
}
private static void OnDateFormatChanged(DependencyObject dobj, DependencyPropertyChangedEventArgs e)
{
var datePicker = (DatePicker)dobj;
try
{
Application.Current.Dispatcher.BeginInvoke(
DispatcherPriority.Loaded, new Action<DatePicker>(ApplyDateFormat), datePicker);
}
catch(Exception ex)
{
}
}
private static void ApplyDateFormat(DatePicker datePicker)
{
try
{
var binding = new Binding("SelectedDate")
{
RelativeSource = new RelativeSource { AncestorType = typeof(DatePicker) },
Converter = new DatePickerDateTimeConverter(),
ConverterParameter = new Tuple<DatePicker, string>(datePicker, GetDateFormat(datePicker)),
StringFormat = GetDateFormat(datePicker) // This is also new but didnt seem to help
};
var textBox = GetTemplateTextBox(datePicker);
textBox.SetBinding(TextBox.TextProperty, binding);
textBox.PreviewKeyDown -= TextBoxOnPreviewKeyDown;
textBox.PreviewKeyDown += TextBoxOnPreviewKeyDown;
var dropDownButton = GetTemplateButton(datePicker);
datePicker.CalendarOpened -= DatePickerOnCalendarOpened;
datePicker.CalendarOpened += DatePickerOnCalendarOpened;
// Handle Dropdownbutton PreviewMouseUp to prevent issue of flickering textboxes
dropDownButton.PreviewMouseUp -= DropDownButtonPreviewMouseUp;
dropDownButton.PreviewMouseUp += DropDownButtonPreviewMouseUp;
}
catch ( Exception ex)
{
}
}
private static ButtonBase GetTemplateButton(DatePicker datePicker)
{
try
{
return (ButtonBase)datePicker.Template.FindName("PART_Button", datePicker);
}
catch(Exception ex)
{
return (ButtonBase)datePicker.Template.FindName("PART_Button", datePicker);
}
}
/// <summary>
/// Prevents a bug in the DatePicker, where clicking the Dropdown open button results in Text being set to default formatting regardless of StringFormat or binding overrides
/// </summary>
private static void DropDownButtonPreviewMouseUp(object sender, MouseButtonEventArgs e)
{
try
{
var fe = sender as FrameworkElement;
if (fe == null) return;
var datePicker = fe.TryFindParent<DatePicker>();
if (datePicker == null || datePicker.SelectedDate == null) return;
var dropDownButton = GetTemplateButton(datePicker);
// Dropdown button was clicked
if (e.OriginalSource == dropDownButton && datePicker.IsDropDownOpen == false)
{
// Open dropdown
datePicker.SetCurrentValue(DatePicker.IsDropDownOpenProperty, true);
// Mimic everything else in the standard DatePicker dropdown opening *except* setting textbox value
datePicker.SetCurrentValue(DatePicker.DisplayDateProperty, datePicker.SelectedDate.Value);
// Important otherwise calendar does not work
dropDownButton.ReleaseMouseCapture();
// Prevent datePicker.cs from handling this event
e.Handled = true;
}
}
catch(Exception ex)
{
}
}
private static TextBox GetTemplateTextBox(Control control)
{
try
{
control.ApplyTemplate();
return (TextBox)control?.Template?.FindName("PART_TextBox", control);
}
catch (Exception ex)
{
return (TextBox)control?.Template?.FindName("PART_TextBox", control);
}
}
private static void TextBoxOnPreviewKeyDown(object sender, KeyEventArgs e)
{
try
{
if (e.Key != Key.Return)
return;
/* DatePicker subscribes to its TextBox's KeyDown event to set its SelectedDate if Key.Return was
* pressed. When this happens its text will be the result of its internal date parsing until it
* loses focus or another date is selected. A workaround is to stop the KeyDown event bubbling up
* and handling setting the DatePicker.SelectedDate. */
e.Handled = true;
var textBox = (TextBox)sender;
var datePicker = (DatePicker)textBox.TemplatedParent;
var dateStr = textBox.Text;
var formatStr = GetDateFormat(datePicker);
datePicker.SelectedDate = DatePickerDateTimeConverter.StringToDateTime(datePicker, formatStr, dateStr);
}
catch(Exception ex)
{
}
}
private static void DatePickerOnCalendarOpened(object sender, RoutedEventArgs e)
{
try
{
/* When DatePicker's TextBox is not focused and its Calendar is opened by clicking its calendar button
* its text will be the result of its internal date parsing until its TextBox is focused and another
* date is selected. A workaround is to set this string when it is opened. */
var datePicker = (DatePicker)sender;
var textBox = GetTemplateTextBox(datePicker);
var formatStr = GetDateFormat(datePicker);
textBox.Text = DatePickerDateTimeConverter.DateTimeToString(formatStr, datePicker.SelectedDate);
}
catch (Exception ex)
{
}
}
private class DatePickerDateTimeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var formatStr = ((Tuple<DatePicker, string>)parameter).Item2;
var selectedDate = (DateTime?)value;
return DateTimeToString(formatStr, selectedDate);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var tupleParam = ((Tuple<DatePicker, string>)parameter);
var dateStr = (string)value;
return StringToDateTime(tupleParam.Item1, tupleParam.Item2, dateStr);
}
public static string DateTimeToString(string formatStr, DateTime? selectedDate)
{
return selectedDate.HasValue ? selectedDate.Value.ToString(formatStr) : null;
}
public static DateTime? StringToDateTime(DatePicker datePicker, string formatStr, string dateStr)
{
DateTime date;
var canParse = DateTime.TryParseExact(dateStr, formatStr, CultureInfo.CurrentCulture,
DateTimeStyles.None, out date);
if (!canParse)
canParse = DateTime.TryParse(dateStr, CultureInfo.CurrentCulture, DateTimeStyles.None, out date);
return canParse ? date : datePicker.SelectedDate;
}
}
}
public static class FEExten
{
/// <summary>
/// Finds a parent of a given item on the visual tree.
/// </summary>
/// <typeparam name="T">The type of the queried item.</typeparam>
/// <param name="child">A direct or indirect child of the
/// queried item.</param>
/// <returns>The first parent item that matches the submitted
/// type parameter. If not matching item can be found, a null
/// reference is being returned.</returns>
public static T TryFindParent<T>(this DependencyObject child)
where T : DependencyObject
{
//get parent item
DependencyObject parentObject = GetParentObject(child);
//we've reached the end of the tree
if (parentObject == null) return null;
//check if the parent matches the type we're looking for
T parent = parentObject as T;
if (parent != null)
{
return parent;
}
else
{
//use recursion to proceed with next level
return TryFindParent<T>(parentObject);
}
}
/// <summary>
/// This method is an alternative to WPF's
/// <see cref="VisualTreeHelper.GetParent"/> method, which also
/// supports content elements. Keep in mind that for content element,
/// this method falls back to the logical tree of the element!
/// </summary>
/// <param name="child">The item to be processed.</param>
/// <returns>The submitted item's parent, if available. Otherwise
/// null.</returns>
public static DependencyObject GetParentObject(this DependencyObject child)
{
if (child == null) return null;
//handle content elements separately
ContentElement contentElement = child as ContentElement;
if (contentElement != null)
{
DependencyObject parent = ContentOperations.GetParent(contentElement);
if (parent != null) return parent;
FrameworkContentElement fce = contentElement as FrameworkContentElement;
return fce != null ? fce.Parent : null;
}
//also try searching for parent in framework elements (such as DockPanel, etc)
FrameworkElement frameworkElement = child as FrameworkElement;
if (frameworkElement != null)
{
DependencyObject parent = frameworkElement.Parent;
if (parent != null) return parent;
}
//if it's not a ContentElement/FrameworkElement, rely on VisualTreeHelper
return VisualTreeHelper.GetParent(child);
}
}
}
The XAML
implementation is
<DatePicker Name="dpActiveFrom" local1:DatePickerCalendar.IsMonthYear="True" Width="160" local1:DatePickerDateFormat.DateFormat="MMM-yyyy" Text="MMM-yyyy" HorizontalAlignment="Left" Margin="33,0,0,0" VerticalAlignment="Center" />
Steps to Reproduce
- Select a date- The DatePicker shows the date
- Again click on the Calender icon on the control(I have changed the style so my icon looks different)
- Don't change anything and click outside
- The DatePicker dropdown wont close(Need a fix) for this