Browse Source

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
Tom Edwards 2 months ago
parent
commit
995f8eda5d

+ 195 - 162
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 
     /// <see cref="DatePicker"/>
     /// </summary>
-    [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
     {
         /// <summary>
@@ -102,24 +101,61 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<bool> YearVisibleProperty =
             DatePicker.YearVisibleProperty.AddOwner<DatePickerPresenter>();
 
-        // 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<Grid>("PART_PickerContainer");
-            _monthHost = e.NameScope.Get<Panel>("PART_MonthHost");
-            _dayHost = e.NameScope.Get<Panel>("PART_DayHost");
-            _yearHost = e.NameScope.Get<Panel>("PART_YearHost");
 
-            _monthSelector = e.NameScope.Get<DateTimePickerPanel>("PART_MonthSelector");
-            _monthSelector.SelectionChanged += OnMonthChanged;
-
-            _daySelector = e.NameScope.Get<DateTimePickerPanel>("PART_DaySelector");
-            _daySelector.SelectionChanged += OnDayChanged;
-
-            _yearSelector = e.NameScope.Get<DateTimePickerPanel>("PART_YearSelector");
-            _yearSelector.SelectionChanged += OnYearChanged;
-
-            _acceptButton = e.NameScope.Get<Button>("PART_AcceptButton");
-
-            _monthUpButton = e.NameScope.Find<RepeatButton>("PART_MonthUpButton");
-            if (_monthUpButton != null)
-            {
-                _monthUpButton.Click += OnSelectorButtonClick;
-            }
-            _monthDownButton = e.NameScope.Find<RepeatButton>("PART_MonthDownButton");
-            if (_monthDownButton != null)
+            _templateItems = new()
             {
-                _monthDownButton.Click += OnSelectorButtonClick;
-            }
+                // These are requirements, so throw if not found
+                _pickerContainer = e.NameScope.Get<Grid>(TemplateItems.PickerContainerName),
+                _monthHost = e.NameScope.Get<Panel>(TemplateItems.MonthHostName),
+                _dayHost = e.NameScope.Get<Panel>(TemplateItems.DayHostName),
+                _yearHost = e.NameScope.Get<Panel>(TemplateItems.YearHostName),
+
+                _monthSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.MonthSelectorName),
+                _daySelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.DaySelectorName),
+                _yearSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.YearSelectorName),
+
+                _acceptButton = e.NameScope.Get<Button>(TemplateItems.AcceptButtonName),
+
+                _monthUpButton = SelectorButton(TemplateItems.MonthUpButtonName, DateTimePickerPanelType.Month, SpinDirection.Decrease),
+                _monthDownButton = SelectorButton(TemplateItems.MonthDownButtonName, DateTimePickerPanelType.Month, SpinDirection.Increase),
+                _dayUpButton = SelectorButton(TemplateItems.DayUpButtonName, DateTimePickerPanelType.Day, SpinDirection.Decrease),
+                _dayDownButton = SelectorButton(TemplateItems.DayDownButtonName, DateTimePickerPanelType.Day, SpinDirection.Increase),
+                _yearUpButton = SelectorButton(TemplateItems.YearUpButtonName, DateTimePickerPanelType.Year, SpinDirection.Decrease),
+                _yearDownButton = SelectorButton(TemplateItems.YearDownButtonName, DateTimePickerPanelType.Year, SpinDirection.Increase),
+
+                _dismissButton = e.NameScope.Find<Button>(TemplateItems.DismissButtonName),
+                _firstSpacer = e.NameScope.Find<Control>(TemplateItems.FirstSpacerName),
+                _secondSpacer = e.NameScope.Find<Control>(TemplateItems.SecondSpacerName),
+            };
 
-            _dayUpButton = e.NameScope.Find<RepeatButton>("PART_DayUpButton");
-            if (_dayUpButton != null)
-            {
-                _dayUpButton.Click += OnSelectorButtonClick;
-            }
-            _dayDownButton = e.NameScope.Find<RepeatButton>("PART_DayDownButton");
-            if (_dayDownButton != null)
-            {
-                _dayDownButton.Click += OnSelectorButtonClick;
-            }
+            _templateItems.Value._acceptButton.Click += OnAcceptButtonClicked;
+            _templateItems.Value._monthSelector.SelectionChanged += OnMonthChanged;
+            _templateItems.Value._daySelector.SelectionChanged += OnDayChanged;
+            _templateItems.Value._yearSelector.SelectionChanged += OnYearChanged;
 
-            _yearUpButton = e.NameScope.Find<RepeatButton>("PART_YearUpButton");
-            if (_yearUpButton != null)
+            if (_templateItems.Value._dismissButton is { } dismissButton)
             {
-                _yearUpButton.Click += OnSelectorButtonClick;
-            }
-            _yearDownButton = e.NameScope.Find<RepeatButton>("PART_YearDownButton");
-            if (_yearDownButton != null)
-            {
-                _yearDownButton.Click += OnSelectorButtonClick;
+                dismissButton.Click += OnDismissButtonClicked;
             }
 
-            _dismissButton = e.NameScope.Find<Button>("PART_DismissButton");
-            _spacer1 = e.NameScope.Find<Rectangle>("PART_FirstSpacer");
-            _spacer2 = e.NameScope.Find<Rectangle>("PART_SecondSpacer");
+            InitPicker();
 
-            if (_acceptButton != null)
-            {
-                _acceptButton.Click += OnAcceptButtonClicked;
-            }
-            if (_dismissButton != null)
+            Button? SelectorButton(string name, DateTimePickerPanelType type, SpinDirection direction)
             {
-                _dismissButton.Click += OnDismissButtonClicked;
+                if (e.NameScope.Find<Button>(name) is { } button)
+                {
+                    button.Click += (s, e) => OnSelectorButtonClick(type, direction);
+                    return button;
+                }
+                return null;
             }
-            InitPicker();
         }
 
         protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@@ -350,63 +371,63 @@ namespace Avalonia.Controls
         private void InitPicker()
         {
             // OnApplyTemplate must've been called before we can init here...
-            if (_pickerContainer == null)
+            if (_templateItems is not { } items)
                 return;
 
             _suppressUpdateSelection = true;
 
-            _monthSelector!.MaximumValue = 12;
-            _monthSelector.MinimumValue = 1;
-            _monthSelector.ItemFormat = MonthFormat;
+            items._monthSelector.MaximumValue = 12;
+            items._monthSelector.MinimumValue = 1;
+            items._monthSelector.ItemFormat = MonthFormat;
 
-            _daySelector!.ItemFormat = DayFormat;
+            items._daySelector.ItemFormat = DayFormat;
 
-            _yearSelector!.MaximumValue = MaxYear.Year;
-            _yearSelector.MinimumValue = MinYear.Year;
-            _yearSelector.ItemFormat = YearFormat;
+            items._yearSelector.MaximumValue = MaxYear.Year;
+            items._yearSelector.MinimumValue = MinYear.Year;
+            items._yearSelector.ItemFormat = YearFormat;
 
-            SetGrid();
+            SetGrid(items);
 
             // Date should've been set when we reach this point
             var dt = Date;
             if (DayVisible)
             {
-                _daySelector.FormatDate = dt.Date;
+                items._daySelector.FormatDate = dt.Date;
                 var maxDays = _calendar.GetDaysInMonth(dt.Year, dt.Month);
-                _daySelector.MaximumValue = maxDays;
-                _daySelector.MinimumValue = 1;
-                _daySelector.SelectedValue = dt.Day;
+                items._daySelector.MaximumValue = maxDays;
+                items._daySelector.MinimumValue = 1;
+                items._daySelector.SelectedValue = dt.Day;
             }
 
             if (MonthVisible)
             {
-                _monthSelector.SelectedValue = dt.Month;
-                _monthSelector.FormatDate = dt.Date;
+                items._monthSelector.SelectedValue = dt.Month;
+                items._monthSelector.FormatDate = dt.Date;
             }
 
             if (YearVisible)
             {
-                _yearSelector.SelectedValue = dt.Year;
-                _yearSelector.FormatDate = dt.Date;
+                items._yearSelector.SelectedValue = dt.Year;
+                items._yearSelector.FormatDate = dt.Date;
             }
 
             _suppressUpdateSelection = false;
 
-            SetInitialFocus();
+            SetInitialFocus(items);
         }
 
-        private void SetGrid()
+        private void SetGrid(TemplateItems items)
         {
             var fmt = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
             var columns = new List<(Panel?, int)>
             {
-                (_monthHost, MonthVisible ? fmt.IndexOf("m", StringComparison.OrdinalIgnoreCase) : -1),
-                (_yearHost, YearVisible ? fmt.IndexOf("y", StringComparison.OrdinalIgnoreCase) : -1),
-                (_dayHost, DayVisible ? fmt.IndexOf("d", StringComparison.OrdinalIgnoreCase) : -1),
+                (items._monthHost, MonthVisible ? fmt.IndexOf("m", StringComparison.OrdinalIgnoreCase) : -1),
+                (items._yearHost, YearVisible ? fmt.IndexOf("y", StringComparison.OrdinalIgnoreCase) : -1),
+                (items._dayHost, DayVisible ? fmt.IndexOf("d", StringComparison.OrdinalIgnoreCase) : -1),
             };
 
             columns.Sort((x, y) => x.Item2 - y.Item2);
-            _pickerContainer!.ColumnDefinitions.Clear();
+            items._pickerContainer.ColumnDefinitions.Clear();
 
             var columnIndex = 0;
 
@@ -421,48 +442,53 @@ namespace Avalonia.Controls
                 {
                     if (columnIndex > 0)
                     {
-                        _pickerContainer.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
+                        items._pickerContainer.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
                     }
 
-                    _pickerContainer.ColumnDefinitions.Add(
-                        new ColumnDefinition(column.Item1 == _monthHost ? 138 : 78, GridUnitType.Star));
+                    items._pickerContainer.ColumnDefinitions.Add(
+                        new ColumnDefinition(column.Item1 == items._monthHost ? 138 : 78, GridUnitType.Star));
 
                     if (column.Item1.Parent is null)
                     {
-                        _pickerContainer.Children.Add(column.Item1);
+                        items._pickerContainer.Children.Add(column.Item1);
                     }
 
                     Grid.SetColumn(column.Item1, (columnIndex++ * 2));
                 }
             }
 
-            var isSpacer1Visible = columnIndex > 1;
-            var isSpacer2Visible = columnIndex > 2;
-            // ternary conditional operator is used to make sure grid cells will be validated
-            Grid.SetColumn(_spacer1!, isSpacer1Visible ? 1 : 0);
-            Grid.SetColumn(_spacer2!, isSpacer2Visible ? 3 : 0);
+            ConfigureSpacer(items._firstSpacer, columnIndex > 1);
+            ConfigureSpacer(items._secondSpacer, columnIndex > 2);
+
+            static void ConfigureSpacer(Control? spacer, bool visible)
+            {
+                if (spacer == null)
+                    return;
+
+                // ternary conditional operator is used to make sure grid cells will be validated
+                Grid.SetColumn(spacer, visible ? 1 : 0);
+                spacer.IsVisible = visible;
 
-            _spacer1!.IsVisible = isSpacer1Visible;
-            _spacer2!.IsVisible = isSpacer2Visible;
+            }
         }
 
-        private void SetInitialFocus()
+        private void SetInitialFocus(TemplateItems items)
         {
-            int monthCol = MonthVisible ? Grid.GetColumn(_monthHost!) : int.MaxValue;
-            int dayCol = DayVisible ? Grid.GetColumn(_dayHost!) : int.MaxValue;
-            int yearCol = YearVisible ? Grid.GetColumn(_yearHost!) : int.MaxValue;
+            int monthCol = MonthVisible ? Grid.GetColumn(items._monthHost) : int.MaxValue;
+            int dayCol = DayVisible ? Grid.GetColumn(items._dayHost) : int.MaxValue;
+            int yearCol = YearVisible ? Grid.GetColumn(items._yearHost) : int.MaxValue;
 
             if (monthCol < dayCol && monthCol < yearCol)
             {
-                _monthSelector?.Focus(NavigationMethod.Pointer);
+                items._monthSelector.Focus(NavigationMethod.Pointer);
             }
             else if (dayCol < monthCol && dayCol < yearCol)
             {
-                _monthSelector?.Focus(NavigationMethod.Pointer);
+                items._monthSelector.Focus(NavigationMethod.Pointer);
             }
             else if (yearCol < monthCol && yearCol < dayCol)
             {
-                _yearSelector?.Focus(NavigationMethod.Pointer);
+                items._yearSelector.Focus(NavigationMethod.Pointer);
             }
         }
 
@@ -477,29 +503,36 @@ namespace Avalonia.Controls
             OnConfirmed();
         }
 
-        private void OnSelectorButtonClick(object? sender, RoutedEventArgs e)
+        private void OnSelectorButtonClick(DateTimePickerPanelType type, SpinDirection direction)
         {
-            if (sender == _monthUpButton)
-                _monthSelector!.ScrollUp();
-            else if (sender == _monthDownButton)
-                _monthSelector!.ScrollDown();
-            else if (sender == _yearUpButton)
-                _yearSelector!.ScrollUp();
-            else if (sender == _yearDownButton)
-                _yearSelector!.ScrollDown();
-            else if (sender == _dayUpButton)
-                _daySelector!.ScrollUp();
-            else if (sender == _dayDownButton)
-                _daySelector!.ScrollDown();
+            var target = type switch
+            {
+                DateTimePickerPanelType.Month => _templateItems?._monthSelector,
+                DateTimePickerPanelType.Day => _templateItems?._daySelector,
+                DateTimePickerPanelType.Year=> _templateItems?._yearSelector,
+                _ => throw new NotImplementedException(),
+            };
+
+            switch (direction)
+            {
+                case SpinDirection.Increase:
+                    target?.ScrollDown();
+                    break;
+                case SpinDirection.Decrease:
+                    target?.ScrollUp();
+                    break;
+                default:
+                    throw new NotImplementedException();
+            }
         }
 
         private void OnYearChanged(object? sender, EventArgs e)
         {
-            if (_suppressUpdateSelection)
+            if (_suppressUpdateSelection || _templateItems is not { } items)
                 return;
 
-            int maxDays = _calendar.GetDaysInMonth(_yearSelector!.SelectedValue, _syncDate.Month);
-            var newDate = new DateTimeOffset(_yearSelector.SelectedValue, _syncDate.Month,
+            int maxDays = _calendar.GetDaysInMonth(items._yearSelector.SelectedValue, _syncDate.Month);
+            var newDate = new DateTimeOffset(items._yearSelector.SelectedValue, _syncDate.Month,
                 _syncDate.Day > maxDays ? maxDays : _syncDate.Day, 0, 0, 0, _syncDate.Offset);
 
             _syncDate = newDate;
@@ -510,30 +543,30 @@ namespace Avalonia.Controls
 
             _suppressUpdateSelection = true;
 
-            _daySelector!.FormatDate = newDate.Date;
+            items._daySelector.FormatDate = newDate.Date;
 
-            if (_daySelector.MaximumValue != maxDays)
-                _daySelector.MaximumValue = maxDays;
+            if (items._daySelector.MaximumValue != maxDays)
+                items._daySelector.MaximumValue = maxDays;
             else
-                _daySelector.RefreshItems();
+                items._daySelector.RefreshItems();
 
             _suppressUpdateSelection = false;
         }
 
         private void OnDayChanged(object? sender, EventArgs e)
         {
-            if (_suppressUpdateSelection)
+            if (_suppressUpdateSelection || _templateItems is not { } items)
                 return;
-            _syncDate = new DateTimeOffset(_syncDate.Year, _syncDate.Month, _daySelector!.SelectedValue, 0, 0, 0, _syncDate.Offset);
+            _syncDate = new DateTimeOffset(_syncDate.Year, _syncDate.Month, items._daySelector.SelectedValue, 0, 0, 0, _syncDate.Offset);
         }
 
         private void OnMonthChanged(object? sender, EventArgs e)
         {
-            if (_suppressUpdateSelection)
+            if (_suppressUpdateSelection || _templateItems is not { } items)
                 return;
 
-            int maxDays = _calendar.GetDaysInMonth(_syncDate.Year, _monthSelector!.SelectedValue);
-            var newDate = new DateTimeOffset(_syncDate.Year, _monthSelector.SelectedValue,
+            int maxDays = _calendar.GetDaysInMonth(_syncDate.Year, items._monthSelector.SelectedValue);
+            var newDate = new DateTimeOffset(_syncDate.Year, items._monthSelector.SelectedValue,
                 _syncDate.Day > maxDays ? maxDays : _syncDate.Day, 0, 0, 0, _syncDate.Offset);
 
             if (!DayVisible)
@@ -544,24 +577,24 @@ namespace Avalonia.Controls
 
             _suppressUpdateSelection = true;
 
-            _daySelector!.FormatDate = newDate.Date;
+            items._daySelector.FormatDate = newDate.Date;
             _syncDate = newDate;
 
-            if (_daySelector.MaximumValue != maxDays)
-                _daySelector.MaximumValue = maxDays;
+            if (items._daySelector.MaximumValue != maxDays)
+                items._daySelector.MaximumValue = maxDays;
             else
-                _daySelector.RefreshItems();
+                items._daySelector.RefreshItems();
 
             _suppressUpdateSelection = false;
         }
 
         internal double GetOffsetForPopup()
         {
-            if (_monthSelector is null)
+            if (_templateItems is not { } items)
                 return 0;
 
-            var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41;
-            return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_monthSelector.ItemHeight / 2);
+            var acceptDismissButtonHeight = items._acceptButton.Bounds.Height;
+            return -(MaxHeight - acceptDismissButtonHeight) / 2 - (items._monthSelector.ItemHeight / 2);
         }
     }
 }

+ 209 - 173
src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs

@@ -1,9 +1,8 @@
-using Avalonia.Controls.Metadata;
+using System;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
-using Avalonia.Controls.Shapes;
 using Avalonia.Input;
 using Avalonia.Interactivity;
-using System;
 
 namespace Avalonia.Controls
 {
@@ -11,25 +10,25 @@ namespace Avalonia.Controls
     /// Defines the presenter used for selecting a time. Intended for use with
     /// <see cref="TimePicker"/> but can be used independently
     /// </summary>
-    [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
     {
         /// <summary>
@@ -37,7 +36,7 @@ namespace Avalonia.Controls
         /// </summary>
         public static readonly StyledProperty<int> MinuteIncrementProperty =
             TimePicker.MinuteIncrementProperty.AddOwner<TimePickerPresenter>();
-        
+
         /// <summary>
         /// Defines the <see cref="SecondIncrement"/> property
         /// </summary>
@@ -49,7 +48,7 @@ namespace Avalonia.Controls
         /// </summary>
         public static readonly StyledProperty<string> ClockIdentifierProperty =
             TimePicker.ClockIdentifierProperty.AddOwner<TimePickerPresenter>();
-        
+
         /// <summary>
         /// Defines the <see cref="UseSeconds"/> property
         /// </summary>
@@ -72,26 +71,54 @@ namespace Avalonia.Controls
             KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue<TimePickerPresenter>(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;
 
         /// <summary>
         /// Gets or sets the minute increment in the selector
@@ -101,7 +128,7 @@ namespace Avalonia.Controls
             get => GetValue(MinuteIncrementProperty);
             set => SetValue(MinuteIncrementProperty, value);
         }
-        
+
         /// <summary>
         /// Gets or sets the second increment in the selector
         /// </summary>
@@ -119,7 +146,7 @@ namespace Avalonia.Controls
             get => GetValue(ClockIdentifierProperty);
             set => SetValue(ClockIdentifierProperty, value);
         }
-        
+
         /// <summary>
         /// Gets or sets the current clock identifier, either 12HourClock or 24HourClock
         /// </summary>
@@ -142,54 +169,54 @@ namespace Avalonia.Controls
         {
             base.OnApplyTemplate(e);
 
-            _pickerContainer = e.NameScope.Get<Grid>("PART_PickerContainer");
-            _periodHost = e.NameScope.Get<Panel>("PART_PeriodHost");
-            _secondHost = e.NameScope.Find<Panel>("PART_SecondHost");
-
-            _hourSelector = e.NameScope.Get<DateTimePickerPanel>("PART_HourSelector");
-            _minuteSelector = e.NameScope.Get<DateTimePickerPanel>("PART_MinuteSelector");
-            _secondSelector = e.NameScope.Find<DateTimePickerPanel>("PART_SecondSelector");
-            _periodSelector = e.NameScope.Get<DateTimePickerPanel>("PART_PeriodSelector");
-            
-            _spacer2 = e.NameScope.Get<Rectangle>("PART_SecondSpacer");
-            _spacer3 = e.NameScope.Find<Rectangle>("PART_ThirdSpacer");
-
-            _acceptButton = e.NameScope.Get<Button>("PART_AcceptButton");
-            _acceptButton.Click += OnAcceptButtonClicked;
-
-            _hourUpButton = e.NameScope.Find<RepeatButton>("PART_HourUpButton");
-            if (_hourUpButton != null)
-                _hourUpButton.Click += OnSelectorButtonClick;
-            _hourDownButton = e.NameScope.Find<RepeatButton>("PART_HourDownButton");
-            if (_hourDownButton != null)
-                _hourDownButton.Click += OnSelectorButtonClick;
-
-            _minuteUpButton = e.NameScope.Find<RepeatButton>("PART_MinuteUpButton");
-            if (_minuteUpButton != null)
-                _minuteUpButton.Click += OnSelectorButtonClick;
-            _minuteDownButton = e.NameScope.Find<RepeatButton>("PART_MinuteDownButton");
-            if (_minuteDownButton != null)
-                _minuteDownButton.Click += OnSelectorButtonClick;
-            
-            _secondUpButton = e.NameScope.Find<RepeatButton>("PART_SecondUpButton");
-            if (_secondUpButton != null)
-                _secondUpButton.Click += OnSelectorButtonClick;
-            _secondDownButton = e.NameScope.Find<RepeatButton>("PART_SecondDownButton");
-            if (_secondDownButton != null)
-                _secondDownButton.Click += OnSelectorButtonClick;
-
-            _periodUpButton = e.NameScope.Find<RepeatButton>("PART_PeriodUpButton");
-            if (_periodUpButton != null)
-                _periodUpButton.Click += OnSelectorButtonClick;
-            _periodDownButton = e.NameScope.Find<RepeatButton>("PART_PeriodDownButton");
-            if (_periodDownButton != null)
-                _periodDownButton.Click += OnSelectorButtonClick;
-
-            _dismissButton = e.NameScope.Find<Button>("PART_DismissButton");
-            if (_dismissButton != null)
-                _dismissButton.Click += OnDismissButtonClicked;
+            _templateItems = new()
+            {
+                _pickerContainer = e.NameScope.Get<Grid>(TemplateItems.PickerContainerName),
+                _periodHost = e.NameScope.Get<Panel>(TemplateItems.PeriodHostName),
+                _secondHost = e.NameScope.Find<Panel>(TemplateItems.SecondHostName),
+
+                _hourSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.HourSelectorName),
+                _minuteSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.MinuteSelectorName),
+                _secondSelector = e.NameScope.Find<DateTimePickerPanel>(TemplateItems.SecondSelectorName),
+                _periodSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.PeriodSelectorName),
+
+                _secondSpacer = e.NameScope.Get<Control>(TemplateItems.SecondSpacerName),
+                _thirdSpacer = e.NameScope.Find<Control>(TemplateItems.ThirdSpacerName),
+
+                _acceptButton = e.NameScope.Get<Button>(TemplateItems.AcceptButtonName),
+
+                _hourUpButton = SelectorButton(TemplateItems.HourUpButtonName, DateTimePickerPanelType.Hour, SpinDirection.Decrease),
+                _hourDownButton = SelectorButton(TemplateItems.HourDownButtonName, DateTimePickerPanelType.Hour, SpinDirection.Increase),
+
+                _minuteUpButton = SelectorButton(TemplateItems.MinuteUpButtonName, DateTimePickerPanelType.Minute, SpinDirection.Decrease),
+                _minuteDownButton = SelectorButton(TemplateItems.MinuteDownButtonName, DateTimePickerPanelType.Minute, SpinDirection.Increase),
+
+                _secondUpButton = SelectorButton(TemplateItems.SecondUpButtonName, DateTimePickerPanelType.Second, SpinDirection.Decrease),
+                _secondDownButton = SelectorButton(TemplateItems.SecondDownButtonName, DateTimePickerPanelType.Second, SpinDirection.Increase),
+
+                _periodUpButton = SelectorButton(TemplateItems.PeriodUpButtonName, DateTimePickerPanelType.TimePeriod, SpinDirection.Decrease),
+                _periodDownButton = SelectorButton(TemplateItems.PeriodDownButtonName, DateTimePickerPanelType.TimePeriod, SpinDirection.Increase),
+
+                _dismissButton = e.NameScope.Find<Button>(TemplateItems.DismissButtonName),
+            };
+
+            _templateItems.Value._acceptButton.Click += OnAcceptButtonClicked;
+            if (_templateItems.Value._dismissButton is { } dismissButton)
+            {
+                dismissButton.Click += OnDismissButtonClicked;
+            }
 
             InitPicker();
+
+            Button? SelectorButton(string name, DateTimePickerPanelType type, SpinDirection direction)
+            {
+                if (e.NameScope.Find<Button>(name) is { } button)
+                {
+                    button.Click += (s, e) => OnSelectorButtonClick(type, direction);
+                    return button;
+                }
+                return null;
+            }
         }
 
         protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@@ -232,100 +259,105 @@ namespace Avalonia.Controls
 
         protected override void OnConfirmed()
         {
-            var hr = _hourSelector!.SelectedValue;
-            var min = _minuteSelector!.SelectedValue;
-            var sec = _secondSelector?.SelectedValue ?? 0;
-            var per = _periodSelector!.SelectedValue;
-
-            if (ClockIdentifier == "12HourClock")
+            if (_templateItems is { } items)
             {
-                hr = per == 1 ? (hr == 12) ? 12 : hr + 12 : per == 0 && hr == 12 ? 0 : hr;
-            }
+                var hr = items._hourSelector.SelectedValue;
+                var min = items._minuteSelector.SelectedValue;
+                var sec = items._secondSelector?.SelectedValue ?? 0;
+                var per = items._periodSelector.SelectedValue;
 
-            SetCurrentValue(TimeProperty, new TimeSpan(hr, min, UseSeconds ? sec : 0));
+                if (ClockIdentifier == "12HourClock")
+                {
+                    hr = per == 1 ? (hr == 12) ? 12 : hr + 12 : per == 0 && hr == 12 ? 0 : hr;
+                }
 
+                SetCurrentValue(TimeProperty, new TimeSpan(hr, min, UseSeconds ? sec : 0));
+            }
             base.OnConfirmed();
         }
 
         private void InitPicker()
         {
-            if (_pickerContainer == null)
+            if (_templateItems is not { } items)
                 return;
 
             bool clock12 = ClockIdentifier == "12HourClock";
-            _hourSelector!.MaximumValue = clock12 ? 12 : 23;
-            _hourSelector.MinimumValue = clock12 ? 1 : 0;
-            _hourSelector.ItemFormat = "%h";
+            items._hourSelector.MaximumValue = clock12 ? 12 : 23;
+            items._hourSelector.MinimumValue = clock12 ? 1 : 0;
+            items._hourSelector.ItemFormat = "%h";
             var hr = Time.Hours;
-            _hourSelector.SelectedValue = !clock12 ? hr :
+            items._hourSelector.SelectedValue = !clock12 ? hr :
                 hr > 12 ? hr - 12 : hr == 0 ? 12 : hr;
 
-            _minuteSelector!.MaximumValue = 59;
-            _minuteSelector.MinimumValue = 0;
-            _minuteSelector.Increment = MinuteIncrement;
-            _minuteSelector.ItemFormat = "mm";
-            _minuteSelector.SelectedValue = Time.Minutes;
+            items._minuteSelector.MaximumValue = 59;
+            items._minuteSelector.MinimumValue = 0;
+            items._minuteSelector.Increment = MinuteIncrement;
+            items._minuteSelector.ItemFormat = "mm";
+            items._minuteSelector.SelectedValue = Time.Minutes;
 
-            if (_secondSelector is not null)
+            if (items._secondSelector is { } secondSelector)
             {
-                _secondSelector.MaximumValue = 59;
-                _secondSelector.MinimumValue = 0;
-                _secondSelector.Increment = SecondIncrement;
-                _secondSelector.ItemFormat = "ss";
-                _secondSelector.SelectedValue = Time.Seconds;
+                secondSelector.MaximumValue = 59;
+                secondSelector.MinimumValue = 0;
+                secondSelector.Increment = SecondIncrement;
+                secondSelector.ItemFormat = "ss";
+                secondSelector.SelectedValue = Time.Seconds;
             }
 
-            _periodSelector!.MaximumValue = 1;
-            _periodSelector.MinimumValue = 0;
-            _periodSelector.SelectedValue = hr >= 12 ? 1 : 0;
+            items._periodSelector.MaximumValue = 1;
+            items._periodSelector.MinimumValue = 0;
+            items._periodSelector.SelectedValue = hr >= 12 ? 1 : 0;
 
-            SetGrid();
-            _hourSelector?.Focus(NavigationMethod.Pointer);
+            SetGrid(items);
+            items._hourSelector.Focus(NavigationMethod.Pointer);
         }
 
-        private void SetGrid()
+        private void SetGrid(TemplateItems items)
         {
             var use24HourClock = ClockIdentifier == "24HourClock";
-            var canUseSeconds = _secondHost is not null && _spacer3 is not null;
-            
-            var columnsD = new ColumnDefinitions();
-            columnsD.Add(new ColumnDefinition(GridLength.Star));
-            columnsD.Add(new ColumnDefinition(GridLength.Auto));
-            columnsD.Add(new ColumnDefinition(GridLength.Star));
-            if (canUseSeconds && UseSeconds)
-            {
-                columnsD.Add(new ColumnDefinition(GridLength.Auto));
-                columnsD.Add(new ColumnDefinition(GridLength.Star));
-            }
-            if (!use24HourClock)
-            {
-                columnsD.Add(new ColumnDefinition(GridLength.Auto));
-                columnsD.Add(new ColumnDefinition(GridLength.Star));
-            }
 
-            _pickerContainer!.ColumnDefinitions = columnsD;
+            var columnsD = new ColumnDefinitions
+            {
+                new(GridLength.Star),
+                new(GridLength.Auto),
+                new(GridLength.Star)
+            };
 
-            if (canUseSeconds)
+            if (items._secondHost is not null && items._thirdSpacer is not null)
             {
-                _spacer2!.IsVisible = UseSeconds;
-                _secondHost!.IsVisible = UseSeconds;
-                _spacer3!.IsVisible = !use24HourClock;
-                _periodHost!.IsVisible = !use24HourClock;
+                if (UseSeconds)
+                {
+                    columnsD.Add(new ColumnDefinition(GridLength.Auto));
+                    columnsD.Add(new ColumnDefinition(GridLength.Star));
+                }
+
+                items._secondSpacer.IsVisible = UseSeconds;
+                items._secondHost.IsVisible = UseSeconds;
+                items._thirdSpacer.IsVisible = !use24HourClock;
+                items._periodHost.IsVisible = !use24HourClock;
 
                 var amPmColumn = (UseSeconds) ? 6 : 4;
 
-                Grid.SetColumn(_spacer2, UseSeconds ? 3 : 0);
-                Grid.SetColumn(_secondHost, UseSeconds ? 4 : 0);
-                Grid.SetColumn(_spacer3, use24HourClock ? 0 : amPmColumn-1);
-                Grid.SetColumn(_periodHost, use24HourClock ? 0 : amPmColumn);
+                Grid.SetColumn(items._secondSpacer, UseSeconds ? 3 : 0);
+                Grid.SetColumn(items._secondHost, UseSeconds ? 4 : 0);
+                Grid.SetColumn(items._thirdSpacer, use24HourClock ? 0 : amPmColumn - 1);
+                Grid.SetColumn(items._periodHost, use24HourClock ? 0 : amPmColumn);
             }
             else
             {
-                _spacer2!.IsVisible = !use24HourClock;
-                _periodHost!.IsVisible = !use24HourClock;
-                Grid.SetColumn(_spacer2, use24HourClock ? 0 : 3);
-                Grid.SetColumn(_periodHost, use24HourClock ? 0 : 4);
+                items._secondSpacer.IsVisible = !use24HourClock;
+                items._periodHost.IsVisible = !use24HourClock;
+                Grid.SetColumn(items._secondSpacer, use24HourClock ? 0 : 3);
+                Grid.SetColumn(items._periodHost, use24HourClock ? 0 : 4);
             }
+
+            if (!use24HourClock)
+            {
+                columnsD.Add(new ColumnDefinition(GridLength.Auto));
+                columnsD.Add(new ColumnDefinition(GridLength.Star));
+            }
+
+            items._pickerContainer.ColumnDefinitions = columnsD;
         }
 
         private void OnDismissButtonClicked(object? sender, RoutedEventArgs e)
@@ -338,33 +370,37 @@ namespace Avalonia.Controls
             OnConfirmed();
         }
 
-        private void OnSelectorButtonClick(object? sender, RoutedEventArgs e)
+        private void OnSelectorButtonClick(DateTimePickerPanelType type, SpinDirection direction)
         {
-            if (sender == _hourUpButton)
-                _hourSelector!.ScrollUp();
-            else if (sender == _hourDownButton)
-                _hourSelector!.ScrollDown();
-            else if (sender == _minuteUpButton)
-                _minuteSelector!.ScrollUp();
-            else if (sender == _minuteDownButton)
-                _minuteSelector!.ScrollDown();
-            else if (sender == _secondUpButton)
-                _secondSelector!.ScrollUp();
-            else if (sender == _secondDownButton)
-                _secondSelector!.ScrollDown();
-            else if (sender == _periodUpButton)
-                _periodSelector!.ScrollUp();
-            else if (sender == _periodDownButton)
-                _periodSelector!.ScrollDown();
+            var target = type switch
+            {
+                DateTimePickerPanelType.Hour => _templateItems?._hourSelector,
+                DateTimePickerPanelType.Minute => _templateItems?._minuteSelector,
+                DateTimePickerPanelType.Second => _templateItems?._secondSelector,
+                DateTimePickerPanelType.TimePeriod => _templateItems?._periodSelector,
+                _ => throw new NotImplementedException(),
+            };
+
+            switch (direction)
+            {
+                case SpinDirection.Increase:
+                    target?.ScrollDown();
+                    break;
+                case SpinDirection.Decrease:
+                    target?.ScrollUp();
+                    break;
+                default:
+                    throw new NotImplementedException();
+            }
         }
 
         internal double GetOffsetForPopup()
         {
-            if (_hourSelector is null)
+            if (_templateItems is not { } items)
                 return 0;
 
-            var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41;
-            return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_hourSelector.ItemHeight / 2);
+            var acceptDismissButtonHeight = items._acceptButton.Bounds.Height;
+            return -(MaxHeight - acceptDismissButtonHeight) / 2 - (items._hourSelector.ItemHeight / 2);
         }
     }
 }