Skip to content

Commit 6490390

Browse files
Vignesh-SF3580StephaneDelcroix
authored andcommitted
moved changes from PR-21989
1 parent 0081d68 commit 6490390

File tree

7 files changed

+460
-2
lines changed

7 files changed

+460
-2
lines changed

src/Controls/src/Core/BindableProperty.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ public sealed class BindableProperty
4545
{ typeof(Uri), new UriTypeConverter() },
4646
{ typeof(Easing), new Maui.Converters.EasingTypeConverter() },
4747
{ typeof(Maui.Graphics.Color), new ColorTypeConverter() },
48-
{ typeof(ImageSource), new ImageSourceConverter() }
48+
{ typeof(ImageSource), new ImageSourceConverter() },
49+
#if NET6_0_OR_GREATER
50+
{ typeof(DateTime), new DateTimeTypeConverter() },
51+
{ typeof(TimeSpan), new TimeSpanTypeConverter() }
52+
#endif
4953
};
5054

5155
internal static readonly Dictionary<Type, IValueConverter> KnownIValueConverters = new Dictionary<Type, IValueConverter>
@@ -269,7 +273,10 @@ internal bool TryConvert(ref object value)
269273
value = Convert.ChangeType(value, returnType);
270274
return true;
271275
}
272-
if (KnownTypeConverters.TryGetValue(returnType, out TypeConverter typeConverterTo) && typeConverterTo.CanConvertFrom(valueType))
276+
277+
Type targetType = Nullable.GetUnderlyingType(returnType) ?? returnType;
278+
279+
if (KnownTypeConverters.TryGetValue(targetType, out TypeConverter typeConverterTo) && typeConverterTo.CanConvertFrom(valueType))
273280
{
274281
value = typeConverterTo.ConvertFromInvariantString(value.ToString());
275282
return true;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#if NET6_0_OR_GREATER
2+
using System;
3+
using System.ComponentModel;
4+
using System.Globalization;
5+
using Microsoft.Maui.Controls.Xaml;
6+
7+
namespace Microsoft.Maui.Controls;
8+
9+
internal class DateTimeTypeConverter : TypeConverter
10+
{
11+
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type? sourceType)
12+
=> sourceType == typeof(DateOnly);
13+
14+
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
15+
=> destinationType == typeof(DateTime);
16+
17+
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
18+
{
19+
if (value is DateTime dateTime)
20+
{
21+
return dateTime;
22+
}
23+
if (value is DateOnly dateOnly)
24+
{
25+
return dateOnly.ToDateTime(TimeOnly.MinValue);
26+
}
27+
if (value is string stringValue)
28+
{
29+
if (DateOnly.TryParse(stringValue, culture, DateTimeStyles.None, out DateOnly dateTimeOnly))
30+
{
31+
return dateTimeOnly.ToDateTime(TimeOnly.MinValue);
32+
}
33+
else if (DateTime.TryParse(stringValue, culture, DateTimeStyles.None, out dateTime))
34+
{
35+
return dateTime;
36+
}
37+
}
38+
39+
throw new NotImplementedException($"Cannot convert \"{value}\" into {typeof(DateTime)}");
40+
}
41+
42+
public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
43+
{
44+
if (value is DateOnly dateOnly)
45+
{
46+
return ConvertToDestinationType(dateOnly, destinationType, culture);
47+
}
48+
else if (value is DateTime dateTime)
49+
{
50+
return ConvertToDestinationType(dateTime, destinationType, culture);
51+
}
52+
else if (value is string stringValue)
53+
{
54+
if (DateTime.TryParse(stringValue, culture, DateTimeStyles.None, out dateTime))
55+
{
56+
return ConvertToDestinationType(dateTime, destinationType, culture);
57+
}
58+
else if (DateOnly.TryParse(stringValue, culture, out dateOnly))
59+
{
60+
return ConvertToDestinationType(dateOnly, destinationType, culture);
61+
}
62+
}
63+
64+
throw new NotImplementedException($"Cannot convert \"{value}\" into {destinationType}");
65+
}
66+
67+
static object ConvertToDestinationType(DateOnly dateOnly, Type? destinationType, CultureInfo? culture)
68+
{
69+
if (destinationType == typeof(string))
70+
{
71+
return dateOnly.ToString(culture);
72+
}
73+
else if (destinationType == typeof(DateTime))
74+
{
75+
return dateOnly.ToDateTime(TimeOnly.MinValue);
76+
}
77+
else if (destinationType == typeof(DateOnly))
78+
{
79+
return dateOnly;
80+
}
81+
82+
throw new NotImplementedException($"Cannot convert \"{dateOnly}\" into {destinationType}");
83+
}
84+
85+
static object ConvertToDestinationType(DateTime dateTime, Type? destinationType, CultureInfo? culture)
86+
{
87+
if (destinationType == typeof(string))
88+
{
89+
return dateTime.ToString(culture);
90+
}
91+
else if (destinationType == typeof(DateTime))
92+
{
93+
return dateTime;
94+
}
95+
else if (destinationType == typeof(DateOnly))
96+
{
97+
return DateOnly.FromDateTime(dateTime);
98+
}
99+
100+
throw new NotImplementedException($"Cannot convert \"{dateTime}\" into {destinationType}");
101+
}
102+
}
103+
104+
#endif
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#if NET6_0_OR_GREATER
2+
using System;
3+
using System.ComponentModel;
4+
using System.Globalization;
5+
using Microsoft.Maui.Controls.Xaml;
6+
7+
namespace Microsoft.Maui.Controls;
8+
9+
internal class TimeSpanTypeConverter : TypeConverter
10+
{
11+
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type? sourceType)
12+
=> sourceType == typeof(TimeOnly);
13+
14+
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
15+
=> destinationType == typeof(TimeSpan);
16+
17+
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
18+
{
19+
if (value is TimeSpan timeSpan)
20+
{
21+
return timeSpan;
22+
}
23+
else if (value is TimeOnly timeOnly)
24+
{
25+
return timeOnly.ToTimeSpan();
26+
}
27+
else if (value is string stringValue)
28+
{
29+
if (TimeOnly.TryParse(stringValue, culture, out var timeOnlyResult))
30+
{
31+
return timeOnlyResult.ToTimeSpan();
32+
}
33+
else if (TimeSpan.TryParse(stringValue, culture, out var timeSpanResult))
34+
{
35+
return timeSpanResult;
36+
}
37+
}
38+
throw new NotImplementedException($"Cannot convert \"{value}\" into {typeof(TimeSpan)}");
39+
}
40+
41+
public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
42+
{
43+
if (value is TimeSpan timeSpan)
44+
{
45+
return ConvertToDestinationType(timeSpan, destinationType, culture);
46+
}
47+
else if (value is TimeOnly timeOnly)
48+
{
49+
return ConvertToDestinationType(timeOnly, destinationType, culture);
50+
}
51+
else if (value is string stringValue)
52+
{
53+
if (TimeOnly.TryParse(stringValue, culture, out timeOnly))
54+
{
55+
return ConvertToDestinationType(timeOnly, destinationType, culture);
56+
}
57+
else if (TimeSpan.TryParse(stringValue, culture, out var timeSpan1))
58+
{
59+
return ConvertToDestinationType(timeSpan1, destinationType, culture);
60+
}
61+
}
62+
63+
throw new NotImplementedException($"Cannot convert \"{value}\" into {destinationType}");
64+
}
65+
66+
static object ConvertToDestinationType(TimeOnly timeOnly, Type? destinationType, CultureInfo? culture)
67+
{
68+
if (destinationType == typeof(string))
69+
{
70+
return timeOnly.ToString(culture);
71+
}
72+
else if (destinationType == typeof(TimeSpan))
73+
{
74+
return timeOnly.ToTimeSpan();
75+
}
76+
else if (destinationType == typeof(TimeOnly))
77+
{
78+
return timeOnly;
79+
}
80+
81+
throw new NotImplementedException($"Cannot convert \"{timeOnly}\" into {destinationType}");
82+
}
83+
84+
static object ConvertToDestinationType(TimeSpan timeSpan, Type? destinationType, CultureInfo? culture)
85+
{
86+
if (destinationType == typeof(string))
87+
{
88+
return timeSpan.ToString();
89+
}
90+
else if (destinationType == typeof(TimeSpan))
91+
{
92+
return timeSpan;
93+
}
94+
else if (destinationType == typeof(TimeOnly))
95+
{
96+
return TimeOnly.FromTimeSpan(timeSpan);
97+
}
98+
99+
throw new NotImplementedException($"Cannot convert \"{timeSpan}\" into {destinationType}");
100+
}
101+
}
102+
#endif
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#if NET6_0_OR_GREATER
2+
3+
namespace Microsoft.Maui.Controls.Core.UnitTests;
4+
5+
using System;
6+
using System.ComponentModel;
7+
using System.Globalization;
8+
using System.Threading.Tasks;
9+
using Xunit;
10+
11+
public class DateOnlyTypeConverterTests : BaseTestFixture
12+
{
13+
[Fact]
14+
public void DateOnlyToDateTimeConversion()
15+
{
16+
var converter = new DateTimeTypeConverter();
17+
18+
var dateOnlyValue = new DateOnly(2025, 2, 21);
19+
20+
var actualDateTime = converter.ConvertFrom(null, CultureInfo.InvariantCulture, dateOnlyValue);
21+
var expectedDateTime = new DateTime(2025, 2, 21);
22+
23+
Assert.Equal(expectedDateTime, actualDateTime);
24+
}
25+
26+
[Fact]
27+
public void DateTimeToDateOnlyConversion()
28+
{
29+
var converter = new DateTimeTypeConverter();
30+
31+
var dateTimeValue = new DateTime(2025, 2, 21);
32+
33+
var actualDateOnly = converter.ConvertTo(null, CultureInfo.InvariantCulture, dateTimeValue, typeof(DateOnly));
34+
var expectedDateOnly = new DateOnly(2025, 2, 21);
35+
36+
Assert.Equal(expectedDateOnly, actualDateOnly);
37+
}
38+
39+
[Fact]
40+
public void DateOnlyToDatePickerBinding()
41+
{
42+
var datePicker = new DatePicker();
43+
var source = new Issue20438DatePickerViewModel
44+
{
45+
SelectedDate = new DateOnly(2025, 3, 15)
46+
};
47+
datePicker.BindingContext = source;
48+
datePicker.SetBinding(DatePicker.DateProperty, "SelectedDate");
49+
var expectedDateTime = new DateTime(2025, 3, 15);
50+
Assert.Equal(expectedDateTime, datePicker.Date);
51+
}
52+
53+
[Fact]
54+
public void DateOnlyToNonNullableBinding()
55+
{
56+
var dateProperty = BindableProperty.Create("Date", typeof(DateTime), typeof(DatePicker), null, BindingMode.TwoWay);
57+
var source = new Issue20438DatePickerViewModel
58+
{
59+
SelectedDate = new DateOnly(2025, 3, 15)
60+
};
61+
var bo = new MockBindable { BindingContext = source };
62+
63+
bo.SetBinding(dateProperty, "SelectedDate");
64+
var expectedDateTime = new DateTime(2025, 3, 15);
65+
Assert.Equal(expectedDateTime, bo.GetValue(dateProperty));
66+
}
67+
68+
public class Issue20438DatePickerViewModel
69+
{
70+
public DateOnly SelectedDate { get; set; }
71+
}
72+
}
73+
#endif
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#if NET6_0_OR_GREATER
2+
3+
namespace Microsoft.Maui.Controls.Core.UnitTests;
4+
5+
using System;
6+
using System.Globalization;
7+
using System.Threading.Tasks;
8+
using Xunit;
9+
10+
public class TimeOnlyTypeConverterTests : BaseTestFixture
11+
{
12+
[Fact]
13+
public void TimeOnlyToTimeSpanConversion()
14+
{
15+
var converter = new TimeSpanTypeConverter();
16+
17+
var timeOnlyValue = new TimeOnly(8, 30, 0);
18+
19+
var actualTimeSpan = converter.ConvertFrom(null, CultureInfo.InvariantCulture, timeOnlyValue);
20+
var expectedTimeSpan = new TimeSpan(8, 30, 0);
21+
22+
Assert.Equal(expectedTimeSpan, actualTimeSpan);
23+
}
24+
25+
[Fact]
26+
public void TimeSpanToTimeOnlyConversion()
27+
{
28+
var converter = new TimeSpanTypeConverter();
29+
30+
var timeSpanValue = new TimeSpan(8, 30, 0);
31+
32+
var actualTimeOnly = converter.ConvertTo(null, CultureInfo.InvariantCulture, timeSpanValue, typeof(TimeOnly));
33+
var expectedTimeOnly = new TimeOnly(8, 30, 0);
34+
35+
Assert.Equal(expectedTimeOnly, actualTimeOnly);
36+
}
37+
38+
[Fact]
39+
public void TimeOnlyToTimePickerBinding()
40+
{
41+
var timePicker = new TimePicker();
42+
var source = new Issue20438TimePickerViewModel
43+
{
44+
SelectedTime = new TimeOnly(14, 30, 0)
45+
};
46+
timePicker.BindingContext = source;
47+
timePicker.SetBinding(TimePicker.TimeProperty, "SelectedTime");
48+
var expectedTimeSpan = new TimeSpan(14, 30, 0);
49+
Assert.Equal(expectedTimeSpan, timePicker.Time);
50+
}
51+
52+
[Fact]
53+
public void TimeOnlyToNonNullableBinding()
54+
{
55+
var timeProperty = BindableProperty.Create("Time", typeof(TimeSpan), typeof(TimePicker), null, BindingMode.TwoWay);
56+
var source = new Issue20438TimePickerViewModel
57+
{
58+
SelectedTime = new TimeOnly(14, 30, 0)
59+
};
60+
var bo = new MockBindable { BindingContext = source };
61+
62+
bo.SetBinding(timeProperty, "SelectedTime");
63+
var expectedTimeSpan = new TimeSpan(14, 30, 0);
64+
Assert.Equal(expectedTimeSpan, bo.GetValue(timeProperty));
65+
}
66+
67+
public class Issue20438TimePickerViewModel
68+
{
69+
public TimeOnly SelectedTime { get; set; }
70+
}
71+
}
72+
#endif

0 commit comments

Comments
 (0)