TimePicker with UseSeconds=true drops the seconds component of SelectedTime on confirm
Describe the bug
TimePicker with UseSeconds="True" drops the seconds component of SelectedTime when the user confirms the flyout (the ✓ button, or Enter) — even if nothing was changed. Since SelectedTimeProperty is registered with defaultBindingMode: BindingMode.TwoWay, this silently overwrites the bound source property with a truncated value.
I ran into this in production, not in a synthetic test. The app is ClassIsland (a school timetable display, Windows x64, Avalonia 11.3.17). A timetable had a lesson ending at 11:29:30, with the following break starting at the exact same instant. After the time point was opened in the profile editor and the time picker was confirmed, the lesson's end time was silently rewritten to 11:29:00 in the saved profile file, leaving a 30-second hole before the break. Only the field whose picker was opened was affected; the adjacent break's start time kept its :29:30.
This is not a one-off: comparing two automatic backups from the same machine a week apart shows the same 30-second truncation applied to two separate time points (morning and afternoon), in exactly the same way.
To Reproduce
Sandbox XAML:
<StackPanel>
<TimePicker UseSeconds="True" x:Name="Picker"/>
<TextBlock Text="{Binding #Picker.SelectedTime}"/>
</StackPanel>- Set
Picker.SelectedTime = new TimeSpan(11, 29, 30). - Click the picker to open the flyout presenter.
- Click the accept (✓) button without changing anything (or press Enter).
SelectedTimeis now11:29:00— theTextBlockshows a seconds component of00.
Caveat: I could not run this repro myself — the machine I have access to has no .NET SDK installed, and the production instance is a deployed app I can't attach a debugger to. The steps above are derived from the code path in "Additional context" plus the production data. If it does not reproduce, please say so and I'll help dig further rather than leave a bad report open.
Note that closing the flyout without confirming (clicking away / Esc) does not write back — TimePicker.OnDismissPicker only closes the popup. You must press ✓ or Enter. That's why the symptom appears intermittently to end users.
Expected behavior
Confirming the picker without modifying anything should be a no-op, and UseSeconds="True" should mean the seconds component is preserved.
Avalonia version
11.3.17
I also checked that src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs is byte-identical on main today, so this is not already fixed. TimePicker.cs on main differs only in unrelated changes (VerticalContentAlignment, GetLayoutManager(), data-validation plumbing).
OS
Windows
Additional context
The code path:
TimePicker.OnConfirmed(TimePicker.cs:397) →SetCurrentValue(SelectedTimeProperty, _presenter!.Time);— TwoWay, so this writes back to the bound source.TimePickerPresenter.OnConfirmed(TimePickerPresenter.cs:264) rebuilds the value from the three panels:So seconds are only lost ifvar 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));secreads as0(or_secondSelectorisnull).- The only place that ever assigns the seconds selector is
TimePickerPresenter.InitPicker()(TimePickerPresenter.cs:298-305):if (items._secondSelector is { } secondSelector) { secondSelector.MaximumValue = 59; secondSelector.MinimumValue = 0; secondSelector.Increment = SecondIncrement; secondSelector.ItemFormat = "ss"; secondSelector.SelectedValue = Time.Seconds; }
What I ruled out by reading the source:
DateTimePickerPanel._selectedValuehas exactly one writer in the whole file — theSelectedValuesetter (DateTimePickerPanel.cs:173) — plus its field initializer. The getter is a plain field read; there is no scroll-offset-based recomputation anywhere. So a0cannot be a "misread" of a correctly-set selector.DateTimePickerPanel.PanelTypehas no property-changed handler that resets min/max, andUpdateHelperInfo()only recomputes_range/_totalItems/_extent/_offset.- The Fluent theme does provide
PART_SecondSelector/PART_SecondHost(TimePicker.xaml), so_secondSelectoris notnull. - The theme doesn't set min/max on the selectors (the field defaults are
_minimumValue = 1; _maximumValue = 2; _selectedValue = 1;), butInitPicker()setsMaximumValue→MinimumValue→Increment→SelectedValuein that order, and withIncrement == 1theCoerceSelected()path returns the value unchanged, so the range checks should pass.
Given all of that, a 0 can only mean InitPicker()'s secondSelector.SelectedValue = Time.Seconds; did not take effect before OnConfirmed() ran. I could not determine why from static reading, and I have no way to run it.
One suspicion worth checking first: in TimePicker.OnFlyoutButtonClicked, the presenter's Time is assigned before the popup is opened, i.e. while TimePickerPresenter's template is not yet applied:
_presenter.Time = SelectedTime ?? DateTime.Now.TimeOfDay;
...
_popup.IsOpen = true;
// Overlay popup hosts won't get measured until the next layout pass, but we need the
// template to be applied to `_presenter` now. Detect this case and force a layout pass.
if (!_presenter.IsMeasureValid)
this.GetLayoutManager()?.ExecuteInitialLayoutPass();InitPicker() starts with if (_templateItems is not { } items) return;, so that first invocation is a no-op. The forced layout pass right after should apply the template and call InitPicker() again with the correct Time — but something in that ordering may not hold in practice (e.g. if TimeProperty does not raise a change notification when the bound value round-trips to the same TimeSpan, or if the second selector is populated before its host becomes visible).
Related but opposite: #17024 and #17251 deal with seconds being retained when UseSeconds is false. This is the mirror image — seconds being dropped when UseSeconds is true — and touches the same OnConfirmed/InitPicker region.
Source: AvaloniaUI/Avalonia