#22238·Avalonia

TimePicker with UseSeconds=true drops the seconds component of SelectedTime on confirm

Author: HongtaoWang75Created Sep 15, 2026Updated Sep 15, 2026

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:

xml
<StackPanel>
  <TimePicker UseSeconds="True" x:Name="Picker"/>
  <TextBlock Text="{Binding #Picker.SelectedTime}"/>
</StackPanel>
  1. Set Picker.SelectedTime = new TimeSpan(11, 29, 30).
  2. Click the picker to open the flyout presenter.
  3. Click the accept (✓) button without changing anything (or press Enter).
  4. SelectedTime is now 11:29:00 — the TextBlock shows a seconds component of 00.

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:
    csharp
    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));
    So seconds are only lost if sec reads as 0 (or _secondSelector is null).
  • The only place that ever assigns the seconds selector is TimePickerPresenter.InitPicker() (TimePickerPresenter.cs:298-305):
    csharp
    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._selectedValue has exactly one writer in the whole file — the SelectedValue setter (DateTimePickerPanel.cs:173) — plus its field initializer. The getter is a plain field read; there is no scroll-offset-based recomputation anywhere. So a 0 cannot be a "misread" of a correctly-set selector.
  • DateTimePickerPanel.PanelType has no property-changed handler that resets min/max, and UpdateHelperInfo() only recomputes _range/_totalItems/_extent/_offset.
  • The Fluent theme does provide PART_SecondSelector / PART_SecondHost (TimePicker.xaml), so _secondSelector is not null.
  • The theme doesn't set min/max on the selectors (the field defaults are _minimumValue = 1; _maximumValue = 2; _selectedValue = 1;), but InitPicker() sets MaximumValueMinimumValueIncrementSelectedValue in that order, and with Increment == 1 the CoerceSelected() 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:

csharp
_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.