0

I had a requirement wherein I wanted to have a DatePickerwhich displays month and year only. The selected month and year should be displayed in the DatePicker's TextBox like This.

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

  1. Select a date- The DatePicker shows the date
  2. Again click on the Calender icon on the control(I have changed the style so my icon looks different)
    1. Don't change anything and click outside
    2. The DatePicker dropdown wont close(Need a fix) for this
Apoorv
  • 2,023
  • 1
  • 19
  • 42

1 Answers1

1

Why don't you just define a custom ControlTemplate for the DatePickerTextBox?:

<DatePicker Name="dpActiveFrom">
    <DatePicker.Resources>
        <Style TargetType="{x:Type DatePickerTextBox}">
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate>
                        <TextBox x:Name="PART_TextBox"
                                     Text="{Binding Path=SelectedDate, StringFormat='MMM-yy', 
                                     RelativeSource={RelativeSource AncestorType={x:Type DatePicker}}}" />
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </DatePicker.Resources>
</DatePicker>
mm8
  • 163,881
  • 10
  • 57
  • 88
  • I tried doing this as well. The calender doesn't open at times – Apoorv Sep 06 '19 at 13:28
  • @Apoorv: Did you really copy my sample markup into a new window and test it? It certainly works.The calendar always opens when it's supposed to. – mm8 Sep 06 '19 at 13:42
  • let me try it once again immediately :) – Apoorv Sep 06 '19 at 16:44
  • I dont want the date to be displayed for selected. I want just the "Year" and "Month" fields. Do I need to do something in the calenderOpenedEvent? – Apoorv Sep 06 '19 at 16:54
  • did you get a chance to relook into my question? – Apoorv Sep 09 '19 at 19:32
  • @Apoorv: The selected date should be displayed as for example 'Sep-2019' using my markup. Once again, did you try it? And no, you don't have to something in any event handler. – mm8 Sep 10 '19 at 08:07
  • Yes, I tried it but here is the thing. I want to display year and month only as drop downs in the datePicker control. The code mentioned by you modifies the Template of the DatePicker's textBox. I am looking to only get Year and Month as dropdowns in the DatePicker as well – Apoorv Sep 10 '19 at 08:58
  • "dropdowns"? Do you mean the calendar? – mm8 Sep 10 '19 at 08:58
  • The datepicker Pop up actually. See, once the icon is clicked, it displays a pop up where in we have to select an year, post that a month and then the date. I dont want the date popup. So If i clixk 2019 and then Oct, the pop up should close and shouldnt ask me to select Date – Apoorv Sep 10 '19 at 09:01
  • @Apoorv: You didn't mention this in your original question and it's still unclear if you refer to the built-in Calendar control or something else. – mm8 Sep 10 '19 at 09:04
  • Sir, the first line clearly mentions it that I want a datePicker control and the requirements as well but you just did a -1 :( – Apoorv Sep 10 '19 at 09:05
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/199229/discussion-between-apoorv-and-mm8). – Apoorv Sep 10 '19 at 09:07
  • "*The selected month and year should be displayed in the DatePicker's TextBox like...*". It does using my solution. "*I implemented this solution and it worked fine until I opened the calender and didn't change the already selected date and clicked outside. The Calender Popup is not closing*". This does not happen using my solution. – mm8 Sep 10 '19 at 09:07
  • okay..let me edit and remove the calender part. Would you be able to help ? – Apoorv Sep 10 '19 at 09:08
  • If you remove the calendar part, what else is there to be fixed? – mm8 Sep 10 '19 at 09:09
  • Requirement 1- Displaying only the month and year format - Done, The second requirement is that need to override the default behaviour of the DatePicker where in it allows you to select YEAR, MONTH & DATE. I just want Year and month. In the solution pasted above, there is a bug in which the pop up is not closing if there is a previously selected Month/Year and you dont change anything and click outside. – Apoorv Sep 10 '19 at 09:12
  • @Apoorv: Did you consider using another control that actually supports this? https://stackoverflow.com/questions/1798513/wpf-toolkit-datepicker-month-year-only – mm8 Sep 10 '19 at 09:14
  • The answer with 12 uplikes is what I have implemented. The bug is there in that code itself. Request you to read the comments there as others have also mentiuoned about the bug – Apoorv Sep 10 '19 at 09:17
  • So obviously that solution doesn't work. Why don't you use another control? – mm8 Sep 10 '19 at 09:18
  • working in a restricted environment wherein Nuget installation is also not enabled. Can you by any chance resolve it? – Apoorv Sep 10 '19 at 09:19
  • were you able to look into it ? – Apoorv Sep 27 '19 at 06:56