Skip to content

Commit 758dfd3

Browse files
CopilotSuthiYuvaraj
andcommitted
Implement culture change detection for DatePicker across all platforms
Co-authored-by: SuthiYuvaraj <[email protected]>
1 parent 7ec750c commit 758dfd3

File tree

11 files changed

+431
-0
lines changed

11 files changed

+431
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
x:Class="Maui.Controls.Sample.Issues.Issue9"
5+
Title="DatePicker Culture Change Test">
6+
<ScrollView>
7+
<StackLayout Padding="20">
8+
<Label Text="DatePicker Culture Change Test" FontSize="18" FontAttributes="Bold" Margin="0,0,0,20"/>
9+
10+
<Label Text="Instructions:" FontAttributes="Bold"/>
11+
<Label Text="1. Note the initial date format below"/>
12+
<Label Text="2. Change the culture using the buttons"/>
13+
<Label Text="3. Observe that the DatePicker format updates immediately"/>
14+
<Label Text="4. Try picking a new date to verify it works properly"/>
15+
16+
<StackLayout Orientation="Horizontal" Margin="0,20">
17+
<Label Text="Current Culture:" VerticalOptions="Center"/>
18+
<Label x:Name="CurrentCultureLabel" Text="{Binding CurrentCulture}" VerticalOptions="Center" FontAttributes="Bold"/>
19+
</StackLayout>
20+
21+
<DatePicker x:Name="TestDatePicker"
22+
Date="{Binding TestDate}"
23+
Format="d"
24+
Margin="0,10"/>
25+
26+
<Label Text="Culture Selection:" FontAttributes="Bold" Margin="0,20,0,10"/>
27+
28+
<Button Text="English (US)"
29+
Command="{Binding ChangeCultureCommand}"
30+
CommandParameter="en-US"
31+
Margin="0,5"/>
32+
33+
<Button Text="German (Germany)"
34+
Command="{Binding ChangeCultureCommand}"
35+
CommandParameter="de-DE"
36+
Margin="0,5"/>
37+
38+
<Button Text="French (France)"
39+
Command="{Binding ChangeCultureCommand}"
40+
CommandParameter="fr-FR"
41+
Margin="0,5"/>
42+
43+
<Button Text="Japanese (Japan)"
44+
Command="{Binding ChangeCultureCommand}"
45+
CommandParameter="ja-JP"
46+
Margin="0,5"/>
47+
48+
<Label Text="Test Results:" FontAttributes="Bold" Margin="0,20,0,10"/>
49+
<Label x:Name="TestResultsLabel" Text="{Binding TestResults}" BackgroundColor="LightGray" Padding="10"/>
50+
</StackLayout>
51+
</ScrollView>
52+
</ContentPage>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.Globalization;
4+
using System.Runtime.CompilerServices;
5+
using System.Threading.Tasks;
6+
using System.Windows.Input;
7+
using Microsoft.Maui.Controls;
8+
9+
namespace Maui.Controls.Sample.Issues;
10+
11+
[Issue(IssueTracker.Github, 9, "DatePicker Does Not Update Its Format When the Culture Is Changed at Runtime",
12+
PlatformAffected.All)]
13+
public partial class Issue9 : ContentPage, INotifyPropertyChanged
14+
{
15+
private string _currentCulture = string.Empty;
16+
private DateTime _testDate = DateTime.Today;
17+
private string _testResults = "No tests run yet.";
18+
19+
public Issue9()
20+
{
21+
InitializeComponent();
22+
BindingContext = this;
23+
24+
CurrentCulture = CultureInfo.CurrentCulture.DisplayName;
25+
ChangeCultureCommand = new Command<string>(async (culture) => await ChangeCulture(culture));
26+
}
27+
28+
public string CurrentCulture
29+
{
30+
get => _currentCulture;
31+
set
32+
{
33+
_currentCulture = value;
34+
OnPropertyChanged();
35+
}
36+
}
37+
38+
public DateTime TestDate
39+
{
40+
get => _testDate;
41+
set
42+
{
43+
_testDate = value;
44+
OnPropertyChanged();
45+
}
46+
}
47+
48+
public string TestResults
49+
{
50+
get => _testResults;
51+
set
52+
{
53+
_testResults = value;
54+
OnPropertyChanged();
55+
}
56+
}
57+
58+
public ICommand ChangeCultureCommand { get; }
59+
60+
private async Task ChangeCulture(string cultureName)
61+
{
62+
try
63+
{
64+
var previousFormat = GetCurrentDateFormat();
65+
66+
// Change the culture
67+
var culture = new CultureInfo(cultureName);
68+
CultureInfo.CurrentCulture = culture;
69+
CultureInfo.CurrentUICulture = culture;
70+
71+
CurrentCulture = culture.DisplayName;
72+
73+
// Wait a moment for the culture change to propagate
74+
await Task.Delay(100);
75+
76+
var newFormat = GetCurrentDateFormat();
77+
78+
TestResults = $"Culture changed to {cultureName}\n" +
79+
$"Previous format: {previousFormat}\n" +
80+
$"New format: {newFormat}\n" +
81+
$"Date example: {TestDate.ToString("d")}\n" +
82+
$"Test completed at: {DateTime.Now:HH:mm:ss}";
83+
}
84+
catch (Exception ex)
85+
{
86+
TestResults = $"Error changing culture: {ex.Message}";
87+
}
88+
}
89+
90+
private string GetCurrentDateFormat()
91+
{
92+
return TestDate.ToString("d", CultureInfo.CurrentCulture);
93+
}
94+
95+
public event PropertyChangedEventHandler? PropertyChanged;
96+
97+
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
98+
{
99+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
100+
}
101+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using NUnit.Framework;
2+
using UITest.Appium;
3+
using UITest.Core;
4+
5+
namespace Microsoft.Maui.TestCases.Tests.Issues;
6+
7+
public class Issue9 : _IssuesUITest
8+
{
9+
public Issue9(TestDevice testDevice) : base(testDevice)
10+
{
11+
}
12+
13+
public override string Issue => "DatePicker Does Not Update Its Format When the Culture Is Changed at Runtime";
14+
15+
[Test]
16+
[Category(UITestCategories.DatePicker)]
17+
public void DatePickerFormatUpdatesWhenCultureChanges()
18+
{
19+
App.WaitForElement("TestDatePicker");
20+
21+
// Get the initial date format
22+
var initialDateFormat = GetDatePickerText();
23+
24+
// Change culture to German
25+
App.Tap("German (Germany)");
26+
App.WaitForElement("TestDatePicker");
27+
28+
// Wait for culture change to propagate
29+
App.WaitForNoElement("No tests run yet.", timeout: TimeSpan.FromSeconds(5));
30+
31+
// Get the new date format after culture change
32+
var germanDateFormat = GetDatePickerText();
33+
34+
// The formats should be different
35+
Assert.That(germanDateFormat, Is.Not.EqualTo(initialDateFormat),
36+
"DatePicker format should change when culture changes");
37+
38+
// Change culture to French
39+
App.Tap("French (France)");
40+
App.WaitForElement("TestDatePicker");
41+
42+
// Wait for culture change to propagate
43+
System.Threading.Thread.Sleep(500);
44+
45+
// Get the French date format
46+
var frenchDateFormat = GetDatePickerText();
47+
48+
// The French format should be different from German
49+
Assert.That(frenchDateFormat, Is.Not.EqualTo(germanDateFormat),
50+
"DatePicker format should change when culture changes to French");
51+
52+
// Verify we can still interact with the DatePicker
53+
App.Tap("TestDatePicker");
54+
// On some platforms, this opens a date picker dialog
55+
// The test passes if no exception is thrown
56+
}
57+
58+
private string GetDatePickerText()
59+
{
60+
var datePicker = App.FindElement("TestDatePicker");
61+
return datePicker.GetText();
62+
}
63+
}

src/Core/src/CultureTracker.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Globalization;
4+
5+
namespace Microsoft.Maui
6+
{
7+
/// <summary>
8+
/// Provides culture change detection functionality for MAUI controls.
9+
/// </summary>
10+
internal static class CultureTracker
11+
{
12+
static CultureInfo? s_currentCulture;
13+
static readonly ConcurrentDictionary<WeakReference, Action> s_subscribers = new();
14+
15+
/// <summary>
16+
/// Checks if the culture has changed since the last call and notifies subscribers if it has.
17+
/// </summary>
18+
public static void CheckForCultureChanges()
19+
{
20+
var currentCulture = CultureInfo.CurrentCulture;
21+
22+
if (s_currentCulture == null || !s_currentCulture.Equals(currentCulture))
23+
{
24+
s_currentCulture = currentCulture;
25+
NotifyCultureChanged();
26+
}
27+
}
28+
29+
/// <summary>
30+
/// Subscribes an object to culture change notifications.
31+
/// </summary>
32+
/// <param name="subscriber">The object to subscribe</param>
33+
/// <param name="action">The action to invoke when culture changes</param>
34+
public static void Subscribe(object subscriber, Action action)
35+
{
36+
var weakRef = new WeakReference(subscriber);
37+
s_subscribers.TryAdd(weakRef, action);
38+
}
39+
40+
/// <summary>
41+
/// Unsubscribes an object from culture change notifications.
42+
/// </summary>
43+
/// <param name="subscriber">The object to unsubscribe</param>
44+
public static void Unsubscribe(object subscriber)
45+
{
46+
// Find and remove the weak reference
47+
foreach (var kvp in s_subscribers)
48+
{
49+
if (kvp.Key.IsAlive && ReferenceEquals(kvp.Key.Target, subscriber))
50+
{
51+
s_subscribers.TryRemove(kvp.Key, out _);
52+
break;
53+
}
54+
}
55+
}
56+
57+
static void NotifyCultureChanged()
58+
{
59+
// Clean up dead references and notify live ones
60+
var deadRefs = new System.Collections.Generic.List<WeakReference>();
61+
62+
foreach (var kvp in s_subscribers)
63+
{
64+
if (!kvp.Key.IsAlive)
65+
{
66+
deadRefs.Add(kvp.Key);
67+
}
68+
else
69+
{
70+
try
71+
{
72+
kvp.Value?.Invoke();
73+
}
74+
catch
75+
{
76+
// Ignore exceptions from subscribers to prevent one bad subscriber from affecting others
77+
}
78+
}
79+
}
80+
81+
// Clean up dead references
82+
foreach (var deadRef in deadRefs)
83+
{
84+
s_subscribers.TryRemove(deadRef, out _);
85+
}
86+
}
87+
}
88+
}

src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ protected override void ConnectHandler(MauiDatePicker platformView)
3333
platformView.ViewAttachedToWindow += OnViewAttachedToWindow;
3434
platformView.ViewDetachedFromWindow += OnViewDetachedFromWindow;
3535

36+
// Subscribe to culture changes
37+
CultureTracker.Subscribe(this, OnCultureChanged);
38+
3639
if (platformView.IsAttachedToWindow)
3740
OnViewAttachedToWindow();
3841
}
@@ -61,6 +64,9 @@ protected override void DisconnectHandler(MauiDatePicker platformView)
6164
platformView.ViewDetachedFromWindow -= OnViewDetachedFromWindow;
6265
OnViewDetachedFromWindow();
6366

67+
// Unsubscribe from culture changes
68+
CultureTracker.Unsubscribe(this);
69+
6470
base.DisconnectHandler(platformView);
6571
}
6672

@@ -164,5 +170,14 @@ void OnMainDisplayInfoChanged(object? sender, DisplayInfoChangedEventArgs e)
164170
ShowPickerDialog(currentDialog.DatePicker.Year, currentDialog.DatePicker.Month, currentDialog.DatePicker.DayOfMonth);
165171
}
166172
}
173+
174+
void OnCultureChanged()
175+
{
176+
// Refresh the date format when culture changes
177+
if (PlatformView != null && VirtualView != null)
178+
{
179+
PlatformView.UpdateDate(VirtualView);
180+
}
181+
}
167182
}
168183
}

src/Core/src/Handlers/DatePicker/DatePickerHandler.Windows.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, CalendarDatePi
1111
protected override void ConnectHandler(CalendarDatePicker platformView)
1212
{
1313
platformView.DateChanged += DateChanged;
14+
15+
// Subscribe to culture changes
16+
CultureTracker.Subscribe(this, OnCultureChanged);
1417
}
1518

1619
protected override void DisconnectHandler(CalendarDatePicker platformView)
1720
{
1821
platformView.DateChanged -= DateChanged;
22+
23+
// Unsubscribe from culture changes
24+
CultureTracker.Unsubscribe(this);
1925
}
2026

2127
public static partial void MapFormat(IDatePickerHandler handler, IDatePicker datePicker)
@@ -85,5 +91,14 @@ public static partial void MapBackground(IDatePickerHandler handler, IDatePicker
8591
{
8692
handler.PlatformView?.UpdateBackground(datePicker);
8793
}
94+
95+
void OnCultureChanged()
96+
{
97+
// Refresh the date format when culture changes
98+
if (PlatformView != null && VirtualView != null)
99+
{
100+
PlatformView.UpdateDate(VirtualView);
101+
}
102+
}
88103
}
89104
}

0 commit comments

Comments
 (0)