Skip to content

Commit 1be153f

Browse files
committed
fix: a date that is not set drew as 1/1/1926
Reported against a GeoPackage with an empty DATE column, which is what a freshly created file looks like. Bound through a classic {Binding}, a null reaching CalendarDatePicker.Date arrives as default(DateTimeOffset) - the first of January in year one - and the picker clamps that to the earliest date it will show, a hundred years ago. The row was right throughout and nothing was written to the model; the value was lost between the row and the control. So the three date editors stop binding their pickers. One control now pushes the values in and pulls them out, where "no date" can stay no date and the picker shows its placeholder. The same guard the other editors already had covers DateValue and TimeValue too: a picker being realized pushes its own empty state, and a plain DateTime has nowhere to put it. Making that control taught me a rule worth having in writing. The editor templates live in a dictionary merged into Application.Resources; the control styles live in the control dictionary, which is not. A {ThemeResource} across that line resolves to nothing and takes the process down with an unhandled XAML exception - it cost a crash with no message beyond a fault in Microsoft.UI.Xaml.dll. The mode is a plain enum on the control instead, so nothing has to look anything up.
1 parent 4d08d73 commit 1be153f

8 files changed

Lines changed: 342 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ API can still change. Builds before the tag are published as `0.1.0-dev.N`.
8585
switching objects cleared the value on the one being left behind. A null now reaches the model
8686
only when nothing is a choice the list actually offered, and the control is told to read again so
8787
it does not sit there showing an empty state.
88+
- A date that was not set drew as a real one. Bound through a classic `{Binding}`, a null reaching
89+
`CalendarDatePicker.Date` arrives as `default(DateTimeOffset)` and is clamped to the earliest date
90+
the picker shows — a hundred years ago — so an empty column read as `1/1/1926`. The three date
91+
editors are now one control that pushes and pulls the values itself, and an empty date shows the
92+
placeholder.
8893
- A `bool` holding true was drawn as an indeterminate check box. Same cause seen from the other
8994
side: the box pushed null on creation, the write was refused because a `bool` cannot be cleared,
9095
and nothing then told the box to go and read the real value. The same guard now covers a number

samples/PropertyGridGallery/SampleModel.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class SampleModel : ObservableObject
2626
private DateTime moment = new(2026, 8, 13, 9, 30, 0, DateTimeKind.Local);
2727
private DateOnly day = new(2026, 3, 1);
2828
private TimeOnly clock = new(9, 30);
29+
private DateTime? missing;
2930
private TimeSpan every = TimeSpan.FromMinutes(90);
3031
private Windows.UI.Color fill = Microsoft.UI.Colors.CornflowerBlue;
3132
private Uri? address = new("https://example.com/parcels.gpkg");
@@ -174,6 +175,15 @@ public TimeOnly Clock
174175

175176
[Category("Standard items")]
176177
[PropertyOrder(14)]
178+
[Description("A nullable date that is empty, which is what a column nobody has filled in looks like.")]
179+
public DateTime? Missing
180+
{
181+
get => missing;
182+
set => SetProperty(ref missing, value);
183+
}
184+
185+
[Category("Standard items")]
186+
[PropertyOrder(15)]
177187
[Description("A duration, in a text box: a clock cannot express more than a day or less than zero.")]
178188
public TimeSpan Every
179189
{

src/Digi21.WinUI.PropertyGrid/Model/PropertyGridPropertyRow.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,16 @@ public DateTimeOffset? DateValue
200200
DateOnly date => new DateTimeOffset(date.ToDateTime(TimeOnly.MinValue, DateTimeKind.Local)),
201201
_ => null,
202202
};
203-
set => TryWrite(value is null ? null : FromDate(value.Value));
203+
set
204+
{
205+
if (value is null && !AcceptsNull)
206+
{
207+
RejectEditorDefault();
208+
return;
209+
}
210+
211+
TryWrite(value is null ? null : FromDate(value.Value));
212+
}
204213
}
205214

206215
/// <summary>Gets or sets the time of day part of the value, for the clock editors.</summary>
@@ -214,7 +223,16 @@ public TimeSpan? TimeValue
214223
DateTime moment => moment.TimeOfDay,
215224
_ => null,
216225
};
217-
set => TryWrite(value is null ? null : FromTime(value.Value));
226+
set
227+
{
228+
if (value is null && !AcceptsNull)
229+
{
230+
RejectEditorDefault();
231+
return;
232+
}
233+
234+
TryWrite(value is null ? null : FromTime(value.Value));
235+
}
218236
}
219237

220238
/// <summary>Gets the members to choose from when the property is an enumeration, or an empty list.</summary>
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
using Microsoft.UI.Xaml;
2+
using Microsoft.UI.Xaml.Controls;
3+
4+
namespace Digi21.WinUI.PropertyGrid.Primitives;
5+
6+
/// <summary>Which halves of a moment a <see cref="PropertyGridDateEditor"/> lets the user set.</summary>
7+
public enum PropertyGridDateEditorMode
8+
{
9+
/// <summary>A calendar and a clock.</summary>
10+
DateAndTime,
11+
12+
/// <summary>A calendar only.</summary>
13+
Date,
14+
15+
/// <summary>A clock only.</summary>
16+
Time,
17+
}
18+
19+
/// <summary>A calendar, a clock, or both, for the date and time properties.</summary>
20+
/// <remarks>
21+
/// <para>
22+
/// A control rather than a plain template because of what a binding does to nothing. Bound with a
23+
/// classic <c>{Binding}</c>, a null reaching <c>CalendarDatePicker.Date</c> arrives as
24+
/// <c>default(DateTimeOffset)</c> — the first of January in year one — which the picker then clamps
25+
/// to the earliest date it will show, a hundred years ago. An empty date came out as
26+
/// <c>1/1/1926</c>, which is a date somebody could believe.
27+
/// </para>
28+
/// <para>
29+
/// So nothing is bound: the values are pushed in and pulled out here, where "no date" can stay no
30+
/// date and the picker shows its placeholder instead.
31+
/// </para>
32+
/// </remarks>
33+
public partial class PropertyGridDateEditor : Control
34+
{
35+
/// <summary>Identifies the <see cref="Row"/> dependency property.</summary>
36+
public static readonly DependencyProperty RowProperty = DependencyProperty.Register(
37+
nameof(Row),
38+
typeof(PropertyGridPropertyRow),
39+
typeof(PropertyGridDateEditor),
40+
new PropertyMetadata(null, (d, _) => ((PropertyGridDateEditor)d).OnRowChanged()));
41+
42+
/// <summary>Identifies the <see cref="Mode"/> dependency property.</summary>
43+
public static readonly DependencyProperty ModeProperty = DependencyProperty.Register(
44+
nameof(Mode),
45+
typeof(PropertyGridDateEditorMode),
46+
typeof(PropertyGridDateEditor),
47+
new PropertyMetadata(PropertyGridDateEditorMode.DateAndTime, (d, _) => ((PropertyGridDateEditor)d).Show()));
48+
49+
private CalendarDatePicker? calendar;
50+
private TimePicker? clock;
51+
private PropertyGridPropertyRow? subscribed;
52+
private bool showing;
53+
54+
/// <summary>Initializes a new instance of the <see cref="PropertyGridDateEditor"/> class.</summary>
55+
public PropertyGridDateEditor()
56+
{
57+
DefaultStyleKey = typeof(PropertyGridDateEditor);
58+
DefaultStyleResourceUri = new Uri("ms-appx:///Digi21.WinUI.PropertyGrid/Themes/Generic.xaml");
59+
PropertyGridThemeResources.Ensure();
60+
61+
DataContextChanged += (_, arguments) =>
62+
{
63+
if (arguments.NewValue is PropertyGridPropertyRow row)
64+
{
65+
Row = row;
66+
}
67+
};
68+
}
69+
70+
/// <summary>Gets or sets the property being edited.</summary>
71+
public PropertyGridPropertyRow? Row
72+
{
73+
get => (PropertyGridPropertyRow?)GetValue(RowProperty);
74+
set => SetValue(RowProperty, value);
75+
}
76+
77+
/// <summary>Gets or sets which halves of a moment the user can set.</summary>
78+
/// <remarks>
79+
/// One template holds both pickers and the mode hides whichever is not wanted, rather than there
80+
/// being three styles to choose between. The editor templates live in a dictionary merged into
81+
/// the application's resources and the styles would live in the control dictionary, which is not
82+
/// — a <c>{ThemeResource}</c> across that line resolves to nothing and takes the process with it.
83+
/// </remarks>
84+
public PropertyGridDateEditorMode Mode
85+
{
86+
get => (PropertyGridDateEditorMode)GetValue(ModeProperty);
87+
set => SetValue(ModeProperty, value);
88+
}
89+
90+
/// <inheritdoc />
91+
protected override void OnApplyTemplate()
92+
{
93+
base.OnApplyTemplate();
94+
95+
Detach();
96+
97+
// Whichever parts the template has: one editor serves the calendar, the clock and both.
98+
calendar = GetTemplateChild("PART_Calendar") as CalendarDatePicker;
99+
clock = GetTemplateChild("PART_Clock") as TimePicker;
100+
101+
if (calendar is not null)
102+
{
103+
calendar.DateChanged += OnCalendarChanged;
104+
}
105+
106+
if (clock is not null)
107+
{
108+
clock.SelectedTimeChanged += OnClockChanged;
109+
}
110+
111+
Show();
112+
}
113+
114+
private void OnRowChanged()
115+
{
116+
if (subscribed is not null)
117+
{
118+
subscribed.PropertyChanged -= OnRowPropertyChanged;
119+
}
120+
121+
subscribed = Row;
122+
123+
if (subscribed is not null)
124+
{
125+
subscribed.PropertyChanged += OnRowPropertyChanged;
126+
}
127+
128+
Show();
129+
}
130+
131+
private void OnRowPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
132+
{
133+
if (e.PropertyName is nameof(PropertyGridPropertyRow.DateValue)
134+
or nameof(PropertyGridPropertyRow.TimeValue)
135+
or nameof(PropertyGridPropertyRow.Value))
136+
{
137+
Show();
138+
}
139+
}
140+
141+
// Row to controls. The flag is up throughout, because setting either picker raises the same
142+
// event the user does and the answer would go straight back to the row.
143+
private void Show()
144+
{
145+
if (showing)
146+
{
147+
return;
148+
}
149+
150+
showing = true;
151+
try
152+
{
153+
if (calendar is not null)
154+
{
155+
calendar.Visibility = Mode == PropertyGridDateEditorMode.Time ? Visibility.Collapsed : Visibility.Visible;
156+
calendar.Date = Row?.DateValue;
157+
calendar.IsEnabled = Row?.IsEditable ?? false;
158+
}
159+
160+
if (clock is not null)
161+
{
162+
clock.Visibility = Mode == PropertyGridDateEditorMode.Date ? Visibility.Collapsed : Visibility.Visible;
163+
clock.SelectedTime = Row?.TimeValue;
164+
clock.IsEnabled = Row?.IsEditable ?? false;
165+
}
166+
}
167+
finally
168+
{
169+
showing = false;
170+
}
171+
}
172+
173+
private void OnCalendarChanged(CalendarDatePicker sender, CalendarDatePickerDateChangedEventArgs arguments)
174+
{
175+
if (!showing && Row is { } row)
176+
{
177+
row.DateValue = arguments.NewDate;
178+
}
179+
}
180+
181+
private void OnClockChanged(object? sender, TimePickerSelectedValueChangedEventArgs arguments)
182+
{
183+
if (!showing && Row is { } row)
184+
{
185+
row.TimeValue = arguments.NewTime;
186+
}
187+
}
188+
189+
private void Detach()
190+
{
191+
if (calendar is not null)
192+
{
193+
calendar.DateChanged -= OnCalendarChanged;
194+
}
195+
196+
if (clock is not null)
197+
{
198+
clock.SelectedTimeChanged -= OnClockChanged;
199+
}
200+
}
201+
}

src/Digi21.WinUI.PropertyGrid/Themes/Generic.xaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,32 @@
452452
</Setter>
453453
</Style>
454454

455+
<Style BasedOn="{StaticResource DefaultPropertyGridDateEditorStyle}" TargetType="primitives:PropertyGridDateEditor" />
456+
457+
<Style x:Key="DefaultPropertyGridDateEditorStyle" TargetType="primitives:PropertyGridDateEditor">
458+
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
459+
<Setter Property="IsTabStop" Value="False" />
460+
<Setter Property="Template">
461+
<Setter.Value>
462+
<ControlTemplate TargetType="primitives:PropertyGridDateEditor">
463+
<StackPanel Orientation="Horizontal" Spacing="4">
464+
<CalendarDatePicker
465+
x:Name="PART_Calendar"
466+
MinHeight="0"
467+
VerticalAlignment="Center"
468+
BorderThickness="0"
469+
PlaceholderText="{ThemeResource PropertyGridSelectDatePlaceholderText}" />
470+
<TimePicker
471+
x:Name="PART_Clock"
472+
MinHeight="0"
473+
VerticalAlignment="Center"
474+
BorderThickness="0" />
475+
</StackPanel>
476+
</ControlTemplate>
477+
</Setter.Value>
478+
</Setter>
479+
</Style>
480+
455481
<Style x:Key="DefaultPropertyGridPathEditorStyle" TargetType="primitives:PropertyGridPathEditor">
456482
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
457483
<Setter Property="IsTabStop" Value="False" />

src/Digi21.WinUI.PropertyGrid/Themes/PropertyGridEditors.xaml

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -164,41 +164,21 @@
164164
SelectedItem="{Binding SelectedStandardValue, Mode=TwoWay}" />
165165
</DataTemplate>
166166

167+
<!--
168+
None of the three binds its picker. A null reaching CalendarDatePicker.Date through a classic
169+
binding arrives as default(DateTimeOffset) and is clamped to a hundred years ago, so an empty
170+
date drew as a real one. PropertyGridDateEditor pushes and pulls the values itself.
171+
-->
167172
<DataTemplate x:Key="PropertyGridDateTimeEditorTemplate">
168-
<StackPanel Orientation="Horizontal" Spacing="4">
169-
<CalendarDatePicker
170-
MinHeight="0"
171-
VerticalAlignment="Center"
172-
BorderThickness="0"
173-
Date="{Binding DateValue, Mode=TwoWay}"
174-
IsEnabled="{Binding IsEditable}" />
175-
<TimePicker
176-
MinHeight="0"
177-
VerticalAlignment="Center"
178-
BorderThickness="0"
179-
IsEnabled="{Binding IsEditable}"
180-
SelectedTime="{Binding TimeValue, Mode=TwoWay}" />
181-
</StackPanel>
173+
<primitives:PropertyGridDateEditor Mode="DateAndTime" />
182174
</DataTemplate>
183175

184176
<DataTemplate x:Key="PropertyGridDateEditorTemplate">
185-
<CalendarDatePicker
186-
MinHeight="0"
187-
HorizontalAlignment="Stretch"
188-
VerticalAlignment="Center"
189-
BorderThickness="0"
190-
Date="{Binding DateValue, Mode=TwoWay}"
191-
IsEnabled="{Binding IsEditable}" />
177+
<primitives:PropertyGridDateEditor Mode="Date" />
192178
</DataTemplate>
193179

194180
<DataTemplate x:Key="PropertyGridTimeEditorTemplate">
195-
<TimePicker
196-
MinHeight="0"
197-
HorizontalAlignment="Stretch"
198-
VerticalAlignment="Center"
199-
BorderThickness="0"
200-
IsEnabled="{Binding IsEditable}"
201-
SelectedTime="{Binding TimeValue, Mode=TwoWay}" />
181+
<primitives:PropertyGridDateEditor Mode="Time" />
202182
</DataTemplate>
203183

204184
<!--

src/Digi21.WinUI.PropertyGrid/Themes/PropertyGridResources.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
validation message has no control to ask.
136136
-->
137137
<x:String x:Key="PropertyGridSearchPlaceholderText">Search properties</x:String>
138+
<x:String x:Key="PropertyGridSelectDatePlaceholderText">Pick a date</x:String>
138139
<x:String x:Key="PropertyGridBrowseToolTipText">Browse…</x:String>
139140
<x:String x:Key="PropertyGridEditToolTipText">Edit…</x:String>
140141
<x:String x:Key="PropertyGridOkButtonText">OK</x:String>

0 commit comments

Comments
 (0)