WPF Toolkit DatePicker Month/Year Only

前端 未结 7 474
暗喜
暗喜 2020-12-02 23:45

I\'m using the Toolkit\'s Datepicker as above but I\'d like to restrict it to month and year selections only, as in this situation the users don\'t know or care about the ex

相关标签:
7条回答
  • 2020-12-03 00:08

    This is a combination of Simon's and Dr. ABT's answers, including all extra required code and with the reference conflicts between Windows and Microsoft controls fixed.

    It's pretty ridiculous that this requires a class of 345 lines to achieve, but it should work if you include DatePickerCalendar.cs (with your namespace), build your project, and use the following XAML.

    <DatePicker local:DatePickerCalendar.IsMonthYear="True" 
                local:DatePickerDateFormat.DateFormat="MMM-yyyy"
                Text="MMM-yyyy"></DatePicker>
    

    DatePickerCalendar.cs

    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;
    
    namespace <YourProject>
    {
        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)
            {
                var datePicker = (DatePicker)dobj;
    
                Application.Current.Dispatcher
                    .BeginInvoke(DispatcherPriority.Loaded,
                                 new Action<DatePicker, DependencyPropertyChangedEventArgs>(SetCalendarEventHandlers),
                                 datePicker, e);
            }
    
            private static void SetCalendarEventHandlers(DatePicker datePicker, DependencyPropertyChangedEventArgs e)
            {
                if (e.NewValue == e.OldValue)
                    return;
    
                if ((bool)e.NewValue)
                {
                    datePicker.CalendarOpened += DatePickerOnCalendarOpened;
                    datePicker.CalendarClosed += DatePickerOnCalendarClosed;
                }
                else
                {
                    datePicker.CalendarOpened -= DatePickerOnCalendarOpened;
                    datePicker.CalendarClosed -= DatePickerOnCalendarClosed;
                }
            }
    
            private static void DatePickerOnCalendarOpened(object sender, RoutedEventArgs routedEventArgs)
            {
                var calendar = GetDatePickerCalendar(sender);
                calendar.DisplayMode = CalendarMode.Year;
    
                calendar.DisplayModeChanged += CalendarOnDisplayModeChanged;
            }
    
            private static void DatePickerOnCalendarClosed(object sender, RoutedEventArgs routedEventArgs)
            {
                var datePicker = (DatePicker)sender;
                var calendar = GetDatePickerCalendar(sender);
                datePicker.SelectedDate = calendar.SelectedDate;
    
                calendar.DisplayModeChanged -= CalendarOnDisplayModeChanged;
            }
    
            private static void CalendarOnDisplayModeChanged(object sender, CalendarModeChangedEventArgs e)
            {
                var calendar = (Calendar)sender;
                if (calendar.DisplayMode != CalendarMode.Month)
                    return;
    
                calendar.SelectedDate = GetSelectedCalendarDate(calendar.DisplayDate);
    
                var datePicker = GetCalendarsDatePicker(calendar);
                datePicker.IsDropDownOpen = false;
            }
    
            private static Calendar GetDatePickerCalendar(object sender)
            {
                var datePicker = (DatePicker)sender;
                var popup = (Popup)datePicker.Template.FindName("PART_Popup", datePicker);
                return ((Calendar)popup.Child);
            }
    
            private static DatePicker GetCalendarsDatePicker(FrameworkElement child)
            {
                var parent = (FrameworkElement)child.Parent;
                if (parent.Name == "PART_Root")
                    return (DatePicker)parent.TemplatedParent;
                return GetCalendarsDatePicker(parent);
            }
    
            private static DateTime? GetSelectedCalendarDate(DateTime? selectedDate)
            {
                if (!selectedDate.HasValue)
                    return null;
                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)
            {
                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;
    
                Application.Current.Dispatcher.BeginInvoke(
                    DispatcherPriority.Loaded, new Action<DatePicker>(ApplyDateFormat), datePicker);
            }
            private static void ApplyDateFormat(DatePicker datePicker)
            {
                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;
            }
    
            private static ButtonBase GetTemplateButton(DatePicker datePicker)
            {
                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)
            {
                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;
                }
            }
    
    
    
            private static TextBox GetTemplateTextBox(Control control)
            {
                control.ApplyTemplate();
                return (TextBox)control?.Template?.FindName("PART_TextBox", control);
            }
    
            private static void TextBoxOnPreviewKeyDown(object sender, KeyEventArgs e)
            {
                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);
            }
    
            private static void DatePickerOnCalendarOpened(object sender, RoutedEventArgs e)
            {
                /* 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);
            }
    
            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);
            }
        }
    }
    

    Screenshot

    0 讨论(0)
  • 2020-12-03 00:11

    I want to provide an alternative solution that doesn't require a lot of code. You can bind to a DatePickers's CalendarOpened event. In that event you can set the display mode of the Calendar to Decade and add an event handler for the DisplayModeChanged event on the Calendar. Then in the DisplayModeChanged event handler you can close the calendar if the mode is month, and also set the SelectedDate to the current DisplayDate. This has worked well for my purposes

    XAML

    <DatePicker
    Name="YearPicker"
    Grid.Column="1"
    SelectedDate="{Binding SelectedDate, Mode=TwoWay}"
    CalendarOpened="DatePicker_Opened">
    <DatePicker.Resources>
        <Style TargetType="DatePickerTextBox">
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate>
                        <TextBox x:Name="PART_TextBox"
                                Text="{Binding Path=SelectedDate, StringFormat = {}{0:MM-yyyy}, 
                                RelativeSource={RelativeSource AncestorType={x:Type DatePicker}}}" />
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </DatePicker.Resources>
    

    Then for the code behind

    private void DatePicker_Opened(object sender, RoutedEventArgs e)
    {
        DatePicker datepicker = (DatePicker)sender;
        Popup popup = (Popup)datepicker.Template.FindName("PART_Popup", datepicker);
        Calendar cal = (Calendar)popup.Child;
        cal.DisplayModeChanged += Calender_DisplayModeChanged;
        cal.DisplayMode = CalendarMode.Decade;
    }
    
    private void Calender_DisplayModeChanged(object sender, CalendarModeChangedEventArgs e)
    {
        Calendar calendar = (Calendar)sender;
        if (calendar.DisplayMode == CalendarMode.Month)
        {
            calendar.SelectedDate = calendar.DisplayDate;
            YearPicker.IsDropDownOpen = false;
        }
    }
    

    I hope this helps!

    0 讨论(0)
  • 2020-12-03 00:15

    To add to Simon's brilliant answer, I've made some changes to DatePickerDateFormat.cs to prevent a bug where the Textbox momentarily flickers between dd/MM/yyyy (original string format) and MM/yyyy (overridden string format).

    This occurs due to the DatePicker setting the PART_Textbox text internally immediately before the dropdown is opened. It sets it to the string formatted text and there is ntohing you can do about it - it will override your binding.

    To prevent this behaviour, I added this code to the DatePickerDateFormat class from above.

    1. in ApplyDateFormat, get the dropdown button and handle PreviewMouseUp

      private static void ApplyDateFormat(DatePicker datePicker)
      {
          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;
      }
      
      private static ButtonBase GetTemplateButton(DatePicker datePicker)
      {
          return (ButtonBase)datePicker.Template.FindName("PART_Button", datePicker);
      }
      

    When PreviewMouseUp fires, if there is a Selected date, override the On Dropdown Shown behaviour (we mimic everything that the Datepicker does, but we don't set the PART_TextBox Text)

        /// <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)
        {
            var fe = sender as FrameworkElement;
            if (fe == null) return;
    
            var datePicker = fe.TryFindAncestorOrSelf<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;                
            }
        }
    

    where the extension method TryFindAncestorOrSelf walks up the visual tree to find an object of type T. You can find an implementation of this here: http://www.hardcodet.net/2008/02/find-wpf-parent

    Hope this helps someone!

    0 讨论(0)
  • 2020-12-03 00:16

    Thanks to @Fernando García for the basis of this.

    I have written DateFormat and IsMonthYear attached properties for DatePicker to enable Month/Year selection.

    The IsMonthYear attached property restricts DatePicker's Calendar.DisplayMode to Year and Decade to prevent selection from CalendarMode.Month. It also sets the day portion of DatePicker.SelectedDate to 1.

    The DateFormat attached property is not limited to this scenario, it can also be used separate from IsMonthYear to provide a format string for DatePicker's display and input.

    Example usage of the attached properties:

    <DatePicker my:DatePickerCalendar.IsMonthYear="True"
                my:DatePickerDateFormat.DateFormat="MM/yyyy"/>
    

    The IsMonthYear attached property is:

    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)
        {
            var datePicker = (DatePicker) dobj;
    
            Application.Current.Dispatcher
                .BeginInvoke(DispatcherPriority.Loaded,
                             new Action<DatePicker, DependencyPropertyChangedEventArgs>(SetCalendarEventHandlers),
                             datePicker, e);
        }
    
        private static void SetCalendarEventHandlers(DatePicker datePicker, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue == e.OldValue)
                return;
    
            if ((bool)e.NewValue)
            {
                datePicker.CalendarOpened += DatePickerOnCalendarOpened;
                datePicker.CalendarClosed += DatePickerOnCalendarClosed;
            }
            else
            {
                datePicker.CalendarOpened -= DatePickerOnCalendarOpened;
                datePicker.CalendarClosed -= DatePickerOnCalendarClosed;
            }
        }
    
        private static void DatePickerOnCalendarOpened(object sender, RoutedEventArgs routedEventArgs)
        {
            var calendar = GetDatePickerCalendar(sender);
            calendar.DisplayMode = CalendarMode.Year;
    
            calendar.DisplayModeChanged += CalendarOnDisplayModeChanged;
        }
    
        private static void DatePickerOnCalendarClosed(object sender, RoutedEventArgs routedEventArgs)
        {
            var datePicker = (DatePicker) sender;
            var calendar = GetDatePickerCalendar(sender);
            datePicker.SelectedDate = calendar.SelectedDate;
    
            calendar.DisplayModeChanged -= CalendarOnDisplayModeChanged;
        }
    
        private static void CalendarOnDisplayModeChanged(object sender, CalendarModeChangedEventArgs e)
        {
            var calendar = (Calendar) sender;
            if (calendar.DisplayMode != CalendarMode.Month)
                return;
    
            calendar.SelectedDate = GetSelectedCalendarDate(calendar.DisplayDate);
    
            var datePicker = GetCalendarsDatePicker(calendar);
            datePicker.IsDropDownOpen = false;
        }
    
        private static Calendar GetDatePickerCalendar(object sender)
        {
            var datePicker = (DatePicker) sender;
            var popup = (Popup) datePicker.Template.FindName("PART_Popup", datePicker);
            return ((Calendar) popup.Child);
        }
    
        private static DatePicker GetCalendarsDatePicker(FrameworkElement child)
        {
            var parent = (FrameworkElement) child.Parent;
            if (parent.Name == "PART_Root")
                return (DatePicker) parent.TemplatedParent;
            return GetCalendarsDatePicker(parent);
        }
    
        private static DateTime? GetSelectedCalendarDate(DateTime? selectedDate)
        {
            if (!selectedDate.HasValue)
                return null;
            return new DateTime(selectedDate.Value.Year, selectedDate.Value.Month, 1);
        }
    }
    

    And the DateFormat attached property is:

    public class DatePickerDateFormat
    {
        public static readonly DependencyProperty DateFormatProperty =
            DependencyProperty.RegisterAttached("DateFormat", typeof (string), typeof (DatePickerDateFormat),
                                                new PropertyMetadata(OnDateFormatChanged));
    
        public static string GetDateFormat(DependencyObject dobj)
        {
            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;
    
            Application.Current.Dispatcher.BeginInvoke(
                DispatcherPriority.Loaded, new Action<DatePicker>(ApplyDateFormat), datePicker);
        }
    
        private static void ApplyDateFormat(DatePicker datePicker)
        {
            var binding = new Binding("SelectedDate")
                {
                    RelativeSource = new RelativeSource {AncestorType = typeof (DatePicker)},
                    Converter = new DatePickerDateTimeConverter(),
                    ConverterParameter = new Tuple<DatePicker, string>(datePicker, GetDateFormat(datePicker))
                };
            var textBox = GetTemplateTextBox(datePicker);
            textBox.SetBinding(TextBox.TextProperty, binding);
    
            textBox.PreviewKeyDown -= TextBoxOnPreviewKeyDown;
            textBox.PreviewKeyDown += TextBoxOnPreviewKeyDown;
    
            datePicker.CalendarOpened -= DatePickerOnCalendarOpened;
            datePicker.CalendarOpened += DatePickerOnCalendarOpened;
        }
    
        private static TextBox GetTemplateTextBox(Control control)
        {
            control.ApplyTemplate();
            return (TextBox) control.Template.FindName("PART_TextBox", control);
        }
    
        private static void TextBoxOnPreviewKeyDown(object sender, KeyEventArgs e)
        {
            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);
        }
    
        private static void DatePickerOnCalendarOpened(object sender, RoutedEventArgs e)
        {
            /* 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);
        }
    
        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;
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-03 00:16

    Recently I had a specific requirement for calendar with the option to choose only month and year. So i have a previous post about custom format for the wpf datepicker, but in this case we need to do a little tricky answer. First you need to create a specific control like this:

    public class DatePickerCo : DatePicker
    

    Once you have already do this. Exists another post when someone else, I don't rememberer the url for that post, but anyway, the think is you need to overwrite the OnCalendarOpened method just like this:

     
     protected override void OnCalendarOpened(RoutedEventArgs e)
            {
                var popup = this.Template.FindName(
                    "PART_Popup", this) as Popup;
                if (popup != null && popup.Child is System.Windows.Controls.Calendar)
                {
                    ((System.Windows.Controls.Calendar)popup.Child).DisplayMode = CalendarMode.Year;
                }
    
                ((System.Windows.Controls.Calendar)popup.Child).DisplayModeChanged += new EventHandler(DatePickerCo_DisplayModeChanged);
            }
    

    Notice that we add a last line with the handler for the event DisplayModeChanged. This is really important for the next steps. Ok so the next step is define the DisplayModeChanged

    
     private void DatePickerCo_DisplayModeChanged(object sender, CalendarModeChangedEventArgs e)
            {
                var popup = this.Template.FindName(
                    "PART_Popup", this) as Popup;
                if (popup != null && popup.Child is System.Windows.Controls.Calendar)
                {
                    var _calendar = popup.Child as System.Windows.Controls.Calendar;
                    if (_calendar.DisplayMode == CalendarMode.Month)
                    {
                        _calendar.DisplayMode = CalendarMode.Year;
    
                        if (IsDropDownOpen)
                        {
                            this.SelectedDate = GetSelectedMonth(_calendar.DisplayDate);
                            this.IsDropDownOpen = false;
                            ((System.Windows.Controls.Calendar)popup.Child).DisplayModeChanged -= new EventHandler(DatePickerCo_DisplayModeChanged);
                        }
                    }
    
                }
            }
    
     private DateTime? GetSelectedMonth(DateTime? selectedDate)
            {
                if (selectedDate == null)
                {
                    selectedDate = DateTime.Now;
                }
    
                int monthDifferenceStart = DateTimeHelper.CompareYearMonth(selectedDate.Value, DisplayDateRangeStart);
                int monthDifferenceEnd = DateTimeHelper.CompareYearMonth(selectedDate.Value, DisplayDateRangeEnd);
    
                if (monthDifferenceStart >= 0 && monthDifferenceEnd  0, "monthDifferenceEnd should be greater than 0!");
                        _selectedMonth = DateTimeHelper.DiscardDayTime(DisplayDateRangeEnd);
                    }
                }
    
                return _selectedMonth;
            }
    

    Here there's a couple things, first you need to pick a month, so thats the reason for create the function GetSelectedMonth. The second thing is that method use a class of the WPFToolkit called DateTimeHelper.cs. Ok Until now we just created the functionality for pick up the month. But once you have click on the month calendar, we proceed to show the selected date in month/year format. For accomplish that we just need to create a specific style to our custom control like this:

    In my first answer How to change format (e.g. dd/MMM/yyyy) of DateTimePicker in WPF application

    I explained all about with the custom format of the date picker. I hope this help you. please any comment or suggestion, you just let me know, regards!!.

    0 讨论(0)
  • 2020-12-03 00:25

    i translated Simon's answer into VB, i post here the results (please give credits to him )

    Imports System.Windows.Threading
    Imports System.Windows.Controls.Primitives
    Imports System.Windows.Controls
    
    ''' <summary>     Allows the Date Picker Calendar to select month.
    '''                      Use with the attached property IsMonthYear Property.
    ''' </summary>
    ''' <remarks> Source : https://stackoverflow.com/questions/1798513/wpf-toolkit-datepicker-month-year-only 
    '''           Author : </remarks>
    Public Class DatePickerCalendar
    
    Public Shared IsMonthYearProperty As DependencyProperty = DependencyProperty.RegisterAttached("IsMonthYear", GetType(System.Boolean), GetType(DatePickerCalendar), New PropertyMetadata(AddressOf OnIsMonthYearChanged))
    
    Public Shared Function GetIsMonthYear(ByVal dobj As DependencyObject) As Boolean
        Return CType(dobj.GetValue(IsMonthYearProperty), Boolean)
    End Function
    
    Public Shared Sub SetIsMonthYear(ByVal dobj As DependencyObject, ByVal value As Boolean)
        dobj.SetValue(IsMonthYearProperty, value)
    End Sub
    
    Private Shared Sub OnIsMonthYearChanged(ByVal dobj As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
        Dim MyDatePicker = CType(dobj, DatePicker)
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, _
                        New Action(Of DatePicker, DependencyPropertyChangedEventArgs)(AddressOf SetCalendarEventHandlers), MyDatePicker, e)
    End Sub
    
    Private Shared Sub SetCalendarEventHandlers(ByVal datePicker As DatePicker, ByVal e As DependencyPropertyChangedEventArgs)
        If (e.NewValue = e.OldValue) Then
            Return
        End If
        If CType(e.NewValue, Boolean) Then
            AddHandler datePicker.CalendarOpened, AddressOf DatePickerOnCalendarOpened
            AddHandler datePicker.CalendarClosed, AddressOf DatePickerOnCalendarClosed
        Else
            RemoveHandler datePicker.CalendarOpened, AddressOf DatePickerOnCalendarOpened
            RemoveHandler datePicker.CalendarClosed, AddressOf DatePickerOnCalendarClosed
        End If
    End Sub
    
    Private Shared Sub DatePickerOnCalendarOpened(ByVal sender As Object, ByVal routedEventArgs As RoutedEventArgs)
        Dim MyCalendar = GetDatePickerCalendar(sender)
        MyCalendar.DisplayMode = CalendarMode.Year
        AddHandler MyCalendar.DisplayModeChanged, AddressOf CalendarOnDisplayModeChanged
    End Sub
    
    Private Shared Sub DatePickerOnCalendarClosed(ByVal sender As Object, ByVal routedEventArgs As RoutedEventArgs)
        Dim MyDatePicker = CType(sender, DatePicker)
        Dim MyCalendar = GetDatePickerCalendar(sender)
        MyDatePicker.SelectedDate = MyCalendar.SelectedDate
        RemoveHandler MyCalendar.DisplayModeChanged, AddressOf CalendarOnDisplayModeChanged
    End Sub
    
    Private Shared Sub CalendarOnDisplayModeChanged(ByVal sender As Object, ByVal e As CalendarModeChangedEventArgs)
        Dim MyCalendar = CType(sender, Calendar)
        If (MyCalendar.DisplayMode <> CalendarMode.Month) Then
            Return
        End If
        MyCalendar.SelectedDate = GetSelectedCalendarDate(MyCalendar.DisplayDate)
        Dim MyDatePicker = GetCalendarsDatePicker(MyCalendar)
        MyDatePicker.IsDropDownOpen = False
    End Sub
    
    Private Shared Function GetDatePickerCalendar(ByVal sender As Object) As Calendar
        Dim MyDatePicker = CType(sender, DatePicker)
        Dim MyPopup = CType(MyDatePicker.Template.FindName("PART_Popup", MyDatePicker), Popup)
        Return CType(MyPopup.Child, Calendar)
    End Function
    
    Private Shared Function GetCalendarsDatePicker(ByVal child As FrameworkElement) As DatePicker
        Dim MyParent = CType(child.Parent, FrameworkElement)
        If (MyParent.Name = "PART_Root") Then
            Return CType(MyParent.TemplatedParent, DatePicker)
        End If
        Return GetCalendarsDatePicker(MyParent)
    End Function
    
    Private Shared Function GetSelectedCalendarDate(ByVal selectedDate As DateTime?) As DateTime?
        If Not selectedDate.HasValue Then
            Return Nothing
        End If
        Return New DateTime(selectedDate.Value.Year, selectedDate.Value.Month, 1)
    End Function
    End Class
    

    But for the format class, i couldn't get it to work.
    I used and slighlty modified the (simpler) answer from petrycol for the month / year display. Source :
    Changing the string format of the WPF DatePicker

        <DatePicker   SelectedDate="{Binding FromDate}"  
                      l:DatePickerCalendar.IsMonthYear="True"
                      x:Name="MonthCalendar" HorizontalAlignment="Center"                   
                     >
            <DatePicker.Resources>
                <!--Source : https://stackoverflow.com/questions/3819832/changing-the-string-format-of-the-wpf-datepicker
                    Author : petrycol -->
                <Style TargetType="{x:Type DatePickerTextBox}">
                    <Setter Property="Control.Template">
                        <Setter.Value>
                            <ControlTemplate>
                                <TextBox Width="60"    TextAlignment="Center" x:Name="PART_TextBox"
                                         Text="{Binding Path=SelectedDate, StringFormat='MM yy', 
                                         RelativeSource={RelativeSource AncestorType={x:Type DatePicker}},FallbackValue='-- --'}" />
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </DatePicker.Resources>
            <!--CalendarOpened="DatePicker_CalendarOpened"-->
        </DatePicker>
    
    0 讨论(0)
提交回复
热议问题