From 862aee2b02291cdeeebb24947b199771064d7997 Mon Sep 17 00:00:00 2001 From: Tom Edwards <109803929+TomEdwardsEnscape@users.noreply.github.com> Date: Fri, 8 Aug 2025 10:52:18 +0200 Subject: [PATCH] Removed all nullability overrides from TimePickerPresenter and DatePickerPresenter (#19241) Fixed obscure cases where NullReferenceException could be thrown if a template hasn't been applied yet, or where it provides only some optional items Replaced repeated string literals with shared const values Relaxed template part requirements: RepeatButton to Button, Rectancle to Control --- .../DateTimePickers/DatePickerPresenter.cs | 357 ++++++++-------- .../DateTimePickers/TimePickerPresenter.cs | 382 ++++++++++-------- 2 files changed, 404 insertions(+), 335 deletions(-) diff --git a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs index b745ed7779..bfe928309f 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs @@ -1,6 +1,5 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.Interactivity; using System; @@ -13,23 +12,23 @@ namespace Avalonia.Controls /// Defines the presenter used for selecting a date for a /// /// - [TemplatePart("PART_AcceptButton", typeof(Button), IsRequired = true)] - [TemplatePart("PART_DayDownButton", typeof(RepeatButton))] - [TemplatePart("PART_DayHost", typeof(Panel), IsRequired = true)] - [TemplatePart("PART_DaySelector", typeof(DateTimePickerPanel), IsRequired = true)] - [TemplatePart("PART_DayUpButton", typeof(RepeatButton))] - [TemplatePart("PART_DismissButton", typeof(Button))] - [TemplatePart("PART_FirstSpacer", typeof(Rectangle))] - [TemplatePart("PART_MonthDownButton", typeof(RepeatButton))] - [TemplatePart("PART_MonthHost", typeof(Panel), IsRequired = true)] - [TemplatePart("PART_MonthSelector", typeof(DateTimePickerPanel), IsRequired = true)] - [TemplatePart("PART_MonthUpButton", typeof(RepeatButton))] - [TemplatePart("PART_PickerContainer", typeof(Grid), IsRequired = true)] - [TemplatePart("PART_SecondSpacer", typeof(Rectangle))] - [TemplatePart("PART_YearDownButton", typeof(RepeatButton))] - [TemplatePart("PART_YearHost", typeof(Panel), IsRequired = true)] - [TemplatePart("PART_YearSelector", typeof(DateTimePickerPanel), IsRequired = true)] - [TemplatePart("PART_YearUpButton", typeof(RepeatButton))] + [TemplatePart(TemplateItems.AcceptButtonName, typeof(Button), IsRequired = true)] + [TemplatePart(TemplateItems.DayDownButtonName, typeof(Button))] + [TemplatePart(TemplateItems.DayHostName, typeof(Panel), IsRequired = true)] + [TemplatePart(TemplateItems.DaySelectorName, typeof(DateTimePickerPanel), IsRequired = true)] + [TemplatePart(TemplateItems.DayUpButtonName, typeof(Button))] + [TemplatePart(TemplateItems.DismissButtonName, typeof(Button))] + [TemplatePart(TemplateItems.FirstSpacerName, typeof(Control))] + [TemplatePart(TemplateItems.MonthDownButtonName, typeof(Button))] + [TemplatePart(TemplateItems.MonthHostName, typeof(Panel), IsRequired = true)] + [TemplatePart(TemplateItems.MonthSelectorName, typeof(DateTimePickerPanel), IsRequired = true)] + [TemplatePart(TemplateItems.MonthUpButtonName, typeof(Button))] + [TemplatePart(TemplateItems.PickerContainerName, typeof(Grid), IsRequired = true)] + [TemplatePart(TemplateItems.SecondSpacerName, typeof(Control))] + [TemplatePart(TemplateItems.YearDownButtonName, typeof(Button))] + [TemplatePart(TemplateItems.YearHostName, typeof(Panel), IsRequired = true)] + [TemplatePart(TemplateItems.YearSelectorName, typeof(DateTimePickerPanel), IsRequired = true)] + [TemplatePart(TemplateItems.YearUpButtonName, typeof(Button))] public class DatePickerPresenter : PickerPresenterBase { /// @@ -102,24 +101,61 @@ namespace Avalonia.Controls public static readonly StyledProperty YearVisibleProperty = DatePicker.YearVisibleProperty.AddOwner(); - // Template Items - private Grid? _pickerContainer; - private Button? _acceptButton; - private Button? _dismissButton; - private Rectangle? _spacer1; - private Rectangle? _spacer2; - private Panel? _monthHost; - private Panel? _yearHost; - private Panel? _dayHost; - private DateTimePickerPanel? _monthSelector; - private DateTimePickerPanel? _yearSelector; - private DateTimePickerPanel? _daySelector; - private Button? _monthUpButton; - private Button? _dayUpButton; - private Button? _yearUpButton; - private Button? _monthDownButton; - private Button? _dayDownButton; - private Button? _yearDownButton; + private struct TemplateItems + { + public Grid _pickerContainer; + public const string PickerContainerName = "PART_PickerContainer"; + + public Button _acceptButton; + public const string AcceptButtonName = "PART_AcceptButton"; + + public Button? _dismissButton; + public const string DismissButtonName = "PART_DismissButton"; + + public Control? _firstSpacer; + public const string FirstSpacerName = "PART_FirstSpacer"; + + public Control? _secondSpacer; + public const string SecondSpacerName = "PART_SecondSpacer"; + + public Panel _monthHost; + public const string MonthHostName = "PART_MonthHost"; + + public Panel _yearHost; + public const string YearHostName = "PART_YearHost"; + + public Panel _dayHost; + public const string DayHostName = "PART_DayHost"; + + public DateTimePickerPanel _monthSelector; + public const string MonthSelectorName = "PART_MonthSelector"; + + public DateTimePickerPanel _yearSelector; + public const string YearSelectorName = "PART_YearSelector"; + + public DateTimePickerPanel _daySelector; + public const string DaySelectorName = "PART_DaySelector"; + + public Button? _monthUpButton; + public const string MonthUpButtonName = "PART_MonthUpButton"; + + public Button? _dayUpButton; + public const string DayUpButtonName = "PART_DayUpButton"; + + public Button? _yearUpButton; + public const string YearUpButtonName = "PART_YearUpButton"; + + public Button? _monthDownButton; + public const string MonthDownButtonName = "PART_MonthDownButton"; + + public Button? _dayDownButton; + public const string DayDownButtonName = "PART_DayDownButton"; + + public Button? _yearDownButton; + public const string YearDownButtonName = "PART_YearDownButton"; + } + + private TemplateItems? _templateItems; private DateTimeOffset _syncDate; @@ -235,69 +271,54 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - // These are requirements, so throw if not found - _pickerContainer = e.NameScope.Get("PART_PickerContainer"); - _monthHost = e.NameScope.Get("PART_MonthHost"); - _dayHost = e.NameScope.Get("PART_DayHost"); - _yearHost = e.NameScope.Get("PART_YearHost"); - _monthSelector = e.NameScope.Get("PART_MonthSelector"); - _monthSelector.SelectionChanged += OnMonthChanged; - - _daySelector = e.NameScope.Get("PART_DaySelector"); - _daySelector.SelectionChanged += OnDayChanged; - - _yearSelector = e.NameScope.Get("PART_YearSelector"); - _yearSelector.SelectionChanged += OnYearChanged; - - _acceptButton = e.NameScope.Get - [TemplatePart("PART_AcceptButton", typeof(Button), IsRequired = true)] - [TemplatePart("PART_DismissButton", typeof(Button))] - [TemplatePart("PART_HourDownButton", typeof(RepeatButton))] - [TemplatePart("PART_HourSelector", typeof(DateTimePickerPanel), IsRequired = true)] - [TemplatePart("PART_HourUpButton", typeof(RepeatButton))] - [TemplatePart("PART_MinuteDownButton", typeof(RepeatButton))] - [TemplatePart("PART_MinuteSelector", typeof(DateTimePickerPanel), IsRequired = true)] - [TemplatePart("PART_MinuteUpButton", typeof(RepeatButton))] - [TemplatePart("PART_SecondDownButton", typeof(RepeatButton))] - [TemplatePart("PART_SecondHost", typeof(Panel), IsRequired = true)] - [TemplatePart("PART_SecondSelector", typeof(DateTimePickerPanel), IsRequired = true)] - [TemplatePart("PART_SecondUpButton", typeof(RepeatButton))] - [TemplatePart("PART_PeriodDownButton", typeof(RepeatButton))] - [TemplatePart("PART_PeriodHost", typeof(Panel), IsRequired = true)] - [TemplatePart("PART_PeriodSelector", typeof(DateTimePickerPanel), IsRequired = true)] - [TemplatePart("PART_PeriodUpButton", typeof(RepeatButton))] - [TemplatePart("PART_PickerContainer", typeof(Grid), IsRequired = true)] - [TemplatePart("PART_SecondSpacer", typeof(Rectangle), IsRequired = true)] - [TemplatePart("PART_ThirdSpacer", typeof(Rectangle), IsRequired = true)] + [TemplatePart(TemplateItems.AcceptButtonName, typeof(Button), IsRequired = true)] + [TemplatePart(TemplateItems.DismissButtonName, typeof(Button))] + [TemplatePart(TemplateItems.HourDownButtonName, typeof(Button))] + [TemplatePart(TemplateItems.HourSelectorName, typeof(DateTimePickerPanel), IsRequired = true)] + [TemplatePart(TemplateItems.HourUpButtonName, typeof(Button))] + [TemplatePart(TemplateItems.MinuteDownButtonName, typeof(Button))] + [TemplatePart(TemplateItems.MinuteSelectorName, typeof(DateTimePickerPanel), IsRequired = true)] + [TemplatePart(TemplateItems.MinuteUpButtonName, typeof(Button))] + [TemplatePart(TemplateItems.SecondDownButtonName, typeof(Button))] + [TemplatePart(TemplateItems.SecondHostName, typeof(Panel))] + [TemplatePart(TemplateItems.SecondSelectorName, typeof(DateTimePickerPanel))] + [TemplatePart(TemplateItems.SecondUpButtonName, typeof(Button))] + [TemplatePart(TemplateItems.PeriodDownButtonName, typeof(Button))] + [TemplatePart(TemplateItems.PeriodHostName, typeof(Panel), IsRequired = true)] + [TemplatePart(TemplateItems.PeriodSelectorName, typeof(DateTimePickerPanel), IsRequired = true)] + [TemplatePart(TemplateItems.PeriodUpButtonName, typeof(Button))] + [TemplatePart(TemplateItems.PickerContainerName, typeof(Grid), IsRequired = true)] + [TemplatePart(TemplateItems.SecondSpacerName, typeof(Control), IsRequired = true)] + [TemplatePart(TemplateItems.ThirdSpacerName, typeof(Control))] public class TimePickerPresenter : PickerPresenterBase { /// @@ -37,7 +36,7 @@ namespace Avalonia.Controls /// public static readonly StyledProperty MinuteIncrementProperty = TimePicker.MinuteIncrementProperty.AddOwner(); - + /// /// Defines the property /// @@ -49,7 +48,7 @@ namespace Avalonia.Controls /// public static readonly StyledProperty ClockIdentifierProperty = TimePicker.ClockIdentifierProperty.AddOwner(); - + /// /// Defines the property /// @@ -72,26 +71,54 @@ namespace Avalonia.Controls KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); } - // TemplateItems - private Grid? _pickerContainer; - private Button? _acceptButton; - private Button? _dismissButton; - private Rectangle? _spacer2; - private Rectangle? _spacer3; - private Panel? _secondHost; - private Panel? _periodHost; - private DateTimePickerPanel? _hourSelector; - private DateTimePickerPanel? _minuteSelector; - private DateTimePickerPanel? _secondSelector; - private DateTimePickerPanel? _periodSelector; - private Button? _hourUpButton; - private Button? _minuteUpButton; - private Button? _secondUpButton; - private Button? _periodUpButton; - private Button? _hourDownButton; - private Button? _minuteDownButton; - private Button? _secondDownButton; - private Button? _periodDownButton; + private struct TemplateItems + { + public Grid _pickerContainer; + public const string PickerContainerName = "PART_PickerContainer"; + + public Button _acceptButton; + public const string AcceptButtonName = "PART_AcceptButton"; + public Button? _dismissButton; + public const string DismissButtonName = "PART_DismissButton"; + + public Control _secondSpacer; // the 2nd spacer, not seconds of time + public const string SecondSpacerName = "PART_SecondSpacer"; + public Control? _thirdSpacer; + public const string ThirdSpacerName = "PART_ThirdSpacer"; + + public Panel? _secondHost; + public const string SecondHostName = "PART_SecondHost"; + public Panel _periodHost; + public const string PeriodHostName = "PART_PeriodHost"; + + public DateTimePickerPanel _hourSelector; + public const string HourSelectorName = "PART_HourSelector"; + public DateTimePickerPanel _minuteSelector; + public const string MinuteSelectorName = "PART_MinuteSelector"; + public DateTimePickerPanel? _secondSelector; + public const string SecondSelectorName = "PART_SecondSelector"; + public DateTimePickerPanel _periodSelector; + public const string PeriodSelectorName = "PART_PeriodSelector"; + + public Button? _hourUpButton; + public const string HourUpButtonName = "PART_HourUpButton"; + public Button? _minuteUpButton; + public const string MinuteUpButtonName = "PART_MinuteUpButton"; + public Button? _secondUpButton; + public const string SecondUpButtonName = "PART_SecondUpButton"; + public Button? _periodUpButton; + public const string PeriodUpButtonName = "PART_PeriodUpButton"; + public Button? _hourDownButton; + public const string HourDownButtonName = "PART_HourDownButton"; + public Button? _minuteDownButton; + public const string MinuteDownButtonName = "PART_MinuteDownButton"; + public Button? _secondDownButton; + public const string SecondDownButtonName = "PART_SecondDownButton"; + public Button? _periodDownButton; + public const string PeriodDownButtonName = "PART_PeriodDownButton"; + } + + private TemplateItems? _templateItems; /// /// Gets or sets the minute increment in the selector @@ -101,7 +128,7 @@ namespace Avalonia.Controls get => GetValue(MinuteIncrementProperty); set => SetValue(MinuteIncrementProperty, value); } - + /// /// Gets or sets the second increment in the selector /// @@ -119,7 +146,7 @@ namespace Avalonia.Controls get => GetValue(ClockIdentifierProperty); set => SetValue(ClockIdentifierProperty, value); } - + /// /// Gets or sets the current clock identifier, either 12HourClock or 24HourClock /// @@ -142,54 +169,54 @@ namespace Avalonia.Controls { base.OnApplyTemplate(e); - _pickerContainer = e.NameScope.Get("PART_PickerContainer"); - _periodHost = e.NameScope.Get("PART_PeriodHost"); - _secondHost = e.NameScope.Find("PART_SecondHost"); - - _hourSelector = e.NameScope.Get("PART_HourSelector"); - _minuteSelector = e.NameScope.Get("PART_MinuteSelector"); - _secondSelector = e.NameScope.Find("PART_SecondSelector"); - _periodSelector = e.NameScope.Get("PART_PeriodSelector"); - - _spacer2 = e.NameScope.Get("PART_SecondSpacer"); - _spacer3 = e.NameScope.Find("PART_ThirdSpacer"); - - _acceptButton = e.NameScope.Get