From 7ec750c5dd86d597814eeb75f1e5ee82171946f5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 3 Jul 2025 14:02:15 +0000
Subject: [PATCH 1/5] Initial plan
From 758dfd359984a0735bc928c080c03af1eceeebd5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 3 Jul 2025 14:21:09 +0000
Subject: [PATCH 2/5] Implement culture change detection for DatePicker across
all platforms
Co-authored-by: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com>
---
.../TestCases.HostApp/Issues/Issue9.xaml | 52 +++++++++
.../TestCases.HostApp/Issues/Issue9.xaml.cs | 101 ++++++++++++++++++
.../Tests/Issues/Issue9.cs | 63 +++++++++++
src/Core/src/CultureTracker.cs | 88 +++++++++++++++
.../DatePicker/DatePickerHandler.Android.cs | 15 +++
.../DatePicker/DatePickerHandler.Windows.cs | 15 +++
.../DatePicker/DatePickerHandler.iOS.cs | 16 +++
.../Platform/Android/DatePickerExtensions.cs | 3 +
.../Platform/Windows/DatePickerExtensions.cs | 3 +
.../src/Platform/iOS/DatePickerExtensions.cs | 3 +
.../UnitTests/Views/DatePickerCultureTests.cs | 72 +++++++++++++
11 files changed, 431 insertions(+)
create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue9.xaml
create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue9.xaml.cs
create mode 100644 src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue9.cs
create mode 100644 src/Core/src/CultureTracker.cs
create mode 100644 src/Core/tests/UnitTests/Views/DatePickerCultureTests.cs
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue9.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue9.xaml
new file mode 100644
index 000000000000..ec303381b1c3
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue9.xaml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue9.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue9.xaml.cs
new file mode 100644
index 000000000000..599a5465cddd
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue9.xaml.cs
@@ -0,0 +1,101 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using Microsoft.Maui.Controls;
+
+namespace Maui.Controls.Sample.Issues;
+
+[Issue(IssueTracker.Github, 9, "DatePicker Does Not Update Its Format When the Culture Is Changed at Runtime",
+ PlatformAffected.All)]
+public partial class Issue9 : ContentPage, INotifyPropertyChanged
+{
+ private string _currentCulture = string.Empty;
+ private DateTime _testDate = DateTime.Today;
+ private string _testResults = "No tests run yet.";
+
+ public Issue9()
+ {
+ InitializeComponent();
+ BindingContext = this;
+
+ CurrentCulture = CultureInfo.CurrentCulture.DisplayName;
+ ChangeCultureCommand = new Command(async (culture) => await ChangeCulture(culture));
+ }
+
+ public string CurrentCulture
+ {
+ get => _currentCulture;
+ set
+ {
+ _currentCulture = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public DateTime TestDate
+ {
+ get => _testDate;
+ set
+ {
+ _testDate = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public string TestResults
+ {
+ get => _testResults;
+ set
+ {
+ _testResults = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public ICommand ChangeCultureCommand { get; }
+
+ private async Task ChangeCulture(string cultureName)
+ {
+ try
+ {
+ var previousFormat = GetCurrentDateFormat();
+
+ // Change the culture
+ var culture = new CultureInfo(cultureName);
+ CultureInfo.CurrentCulture = culture;
+ CultureInfo.CurrentUICulture = culture;
+
+ CurrentCulture = culture.DisplayName;
+
+ // Wait a moment for the culture change to propagate
+ await Task.Delay(100);
+
+ var newFormat = GetCurrentDateFormat();
+
+ TestResults = $"Culture changed to {cultureName}\n" +
+ $"Previous format: {previousFormat}\n" +
+ $"New format: {newFormat}\n" +
+ $"Date example: {TestDate.ToString("d")}\n" +
+ $"Test completed at: {DateTime.Now:HH:mm:ss}";
+ }
+ catch (Exception ex)
+ {
+ TestResults = $"Error changing culture: {ex.Message}";
+ }
+ }
+
+ private string GetCurrentDateFormat()
+ {
+ return TestDate.ToString("d", CultureInfo.CurrentCulture);
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue9.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue9.cs
new file mode 100644
index 000000000000..43b7190e1ba4
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue9.cs
@@ -0,0 +1,63 @@
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+
+public class Issue9 : _IssuesUITest
+{
+ public Issue9(TestDevice testDevice) : base(testDevice)
+ {
+ }
+
+ public override string Issue => "DatePicker Does Not Update Its Format When the Culture Is Changed at Runtime";
+
+ [Test]
+ [Category(UITestCategories.DatePicker)]
+ public void DatePickerFormatUpdatesWhenCultureChanges()
+ {
+ App.WaitForElement("TestDatePicker");
+
+ // Get the initial date format
+ var initialDateFormat = GetDatePickerText();
+
+ // Change culture to German
+ App.Tap("German (Germany)");
+ App.WaitForElement("TestDatePicker");
+
+ // Wait for culture change to propagate
+ App.WaitForNoElement("No tests run yet.", timeout: TimeSpan.FromSeconds(5));
+
+ // Get the new date format after culture change
+ var germanDateFormat = GetDatePickerText();
+
+ // The formats should be different
+ Assert.That(germanDateFormat, Is.Not.EqualTo(initialDateFormat),
+ "DatePicker format should change when culture changes");
+
+ // Change culture to French
+ App.Tap("French (France)");
+ App.WaitForElement("TestDatePicker");
+
+ // Wait for culture change to propagate
+ System.Threading.Thread.Sleep(500);
+
+ // Get the French date format
+ var frenchDateFormat = GetDatePickerText();
+
+ // The French format should be different from German
+ Assert.That(frenchDateFormat, Is.Not.EqualTo(germanDateFormat),
+ "DatePicker format should change when culture changes to French");
+
+ // Verify we can still interact with the DatePicker
+ App.Tap("TestDatePicker");
+ // On some platforms, this opens a date picker dialog
+ // The test passes if no exception is thrown
+ }
+
+ private string GetDatePickerText()
+ {
+ var datePicker = App.FindElement("TestDatePicker");
+ return datePicker.GetText();
+ }
+}
\ No newline at end of file
diff --git a/src/Core/src/CultureTracker.cs b/src/Core/src/CultureTracker.cs
new file mode 100644
index 000000000000..1b977c56f8c9
--- /dev/null
+++ b/src/Core/src/CultureTracker.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Concurrent;
+using System.Globalization;
+
+namespace Microsoft.Maui
+{
+ ///
+ /// Provides culture change detection functionality for MAUI controls.
+ ///
+ internal static class CultureTracker
+ {
+ static CultureInfo? s_currentCulture;
+ static readonly ConcurrentDictionary s_subscribers = new();
+
+ ///
+ /// Checks if the culture has changed since the last call and notifies subscribers if it has.
+ ///
+ public static void CheckForCultureChanges()
+ {
+ var currentCulture = CultureInfo.CurrentCulture;
+
+ if (s_currentCulture == null || !s_currentCulture.Equals(currentCulture))
+ {
+ s_currentCulture = currentCulture;
+ NotifyCultureChanged();
+ }
+ }
+
+ ///
+ /// Subscribes an object to culture change notifications.
+ ///
+ /// The object to subscribe
+ /// The action to invoke when culture changes
+ public static void Subscribe(object subscriber, Action action)
+ {
+ var weakRef = new WeakReference(subscriber);
+ s_subscribers.TryAdd(weakRef, action);
+ }
+
+ ///
+ /// Unsubscribes an object from culture change notifications.
+ ///
+ /// The object to unsubscribe
+ public static void Unsubscribe(object subscriber)
+ {
+ // Find and remove the weak reference
+ foreach (var kvp in s_subscribers)
+ {
+ if (kvp.Key.IsAlive && ReferenceEquals(kvp.Key.Target, subscriber))
+ {
+ s_subscribers.TryRemove(kvp.Key, out _);
+ break;
+ }
+ }
+ }
+
+ static void NotifyCultureChanged()
+ {
+ // Clean up dead references and notify live ones
+ var deadRefs = new System.Collections.Generic.List();
+
+ foreach (var kvp in s_subscribers)
+ {
+ if (!kvp.Key.IsAlive)
+ {
+ deadRefs.Add(kvp.Key);
+ }
+ else
+ {
+ try
+ {
+ kvp.Value?.Invoke();
+ }
+ catch
+ {
+ // Ignore exceptions from subscribers to prevent one bad subscriber from affecting others
+ }
+ }
+ }
+
+ // Clean up dead references
+ foreach (var deadRef in deadRefs)
+ {
+ s_subscribers.TryRemove(deadRef, out _);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs b/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs
index e277fd74c626..4a4cbf4dd16d 100644
--- a/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs
+++ b/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs
@@ -33,6 +33,9 @@ protected override void ConnectHandler(MauiDatePicker platformView)
platformView.ViewAttachedToWindow += OnViewAttachedToWindow;
platformView.ViewDetachedFromWindow += OnViewDetachedFromWindow;
+ // Subscribe to culture changes
+ CultureTracker.Subscribe(this, OnCultureChanged);
+
if (platformView.IsAttachedToWindow)
OnViewAttachedToWindow();
}
@@ -61,6 +64,9 @@ protected override void DisconnectHandler(MauiDatePicker platformView)
platformView.ViewDetachedFromWindow -= OnViewDetachedFromWindow;
OnViewDetachedFromWindow();
+ // Unsubscribe from culture changes
+ CultureTracker.Unsubscribe(this);
+
base.DisconnectHandler(platformView);
}
@@ -164,5 +170,14 @@ void OnMainDisplayInfoChanged(object? sender, DisplayInfoChangedEventArgs e)
ShowPickerDialog(currentDialog.DatePicker.Year, currentDialog.DatePicker.Month, currentDialog.DatePicker.DayOfMonth);
}
}
+
+ void OnCultureChanged()
+ {
+ // Refresh the date format when culture changes
+ if (PlatformView != null && VirtualView != null)
+ {
+ PlatformView.UpdateDate(VirtualView);
+ }
+ }
}
}
diff --git a/src/Core/src/Handlers/DatePicker/DatePickerHandler.Windows.cs b/src/Core/src/Handlers/DatePicker/DatePickerHandler.Windows.cs
index 807af51488de..b146f59f44d9 100644
--- a/src/Core/src/Handlers/DatePicker/DatePickerHandler.Windows.cs
+++ b/src/Core/src/Handlers/DatePicker/DatePickerHandler.Windows.cs
@@ -11,11 +11,17 @@ public partial class DatePickerHandler : ViewHandler _handler;
diff --git a/src/Core/src/Platform/Android/DatePickerExtensions.cs b/src/Core/src/Platform/Android/DatePickerExtensions.cs
index c2319f51cf82..3eb0fc29f1f2 100644
--- a/src/Core/src/Platform/Android/DatePickerExtensions.cs
+++ b/src/Core/src/Platform/Android/DatePickerExtensions.cs
@@ -55,6 +55,9 @@ public static void UpdateMaximumDate(this MauiDatePicker platformDatePicker, IDa
internal static void SetText(this MauiDatePicker platformDatePicker, IDatePicker datePicker)
{
+ // Check for culture changes before updating
+ CultureTracker.CheckForCultureChanges();
+
platformDatePicker.Text = datePicker.Date.ToString(datePicker.Format);
}
}
diff --git a/src/Core/src/Platform/Windows/DatePickerExtensions.cs b/src/Core/src/Platform/Windows/DatePickerExtensions.cs
index 70a06cf55e15..3ceeba2335d7 100644
--- a/src/Core/src/Platform/Windows/DatePickerExtensions.cs
+++ b/src/Core/src/Platform/Windows/DatePickerExtensions.cs
@@ -9,6 +9,9 @@ public static class DatePickerExtensions
{
public static void UpdateDate(this CalendarDatePicker platformDatePicker, IDatePicker datePicker)
{
+ // Check for culture changes before updating
+ CultureTracker.CheckForCultureChanges();
+
var date = datePicker.Date;
platformDatePicker.UpdateDate(date);
diff --git a/src/Core/src/Platform/iOS/DatePickerExtensions.cs b/src/Core/src/Platform/iOS/DatePickerExtensions.cs
index 871a749e9bb4..4bf02ce0e2ff 100644
--- a/src/Core/src/Platform/iOS/DatePickerExtensions.cs
+++ b/src/Core/src/Platform/iOS/DatePickerExtensions.cs
@@ -56,6 +56,9 @@ public static void UpdateDate(this UIDatePicker picker, IDatePicker datePicker)
public static void UpdateDate(this MauiDatePicker platformDatePicker, IDatePicker datePicker, UIDatePicker? picker)
{
+ // Check for culture changes before updating
+ CultureTracker.CheckForCultureChanges();
+
if (picker != null && picker.Date.ToDateTime().Date != datePicker.Date.Date)
picker.SetDate(datePicker.Date.ToNSDate(), false);
diff --git a/src/Core/tests/UnitTests/Views/DatePickerCultureTests.cs b/src/Core/tests/UnitTests/Views/DatePickerCultureTests.cs
new file mode 100644
index 000000000000..091770ddbdba
--- /dev/null
+++ b/src/Core/tests/UnitTests/Views/DatePickerCultureTests.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Globalization;
+using Microsoft.Maui.Controls;
+using Xunit;
+
+namespace Microsoft.Maui.UnitTests.Views
+{
+ [Category(TestCategory.Core, TestCategory.View)]
+ public class DatePickerCultureTests
+ {
+ [Fact]
+ public void DatePickerCultureTrackerInitializes()
+ {
+ // Test that the culture tracker can be used
+ CultureTracker.CheckForCultureChanges();
+
+ // This should not throw
+ Assert.True(true);
+ }
+
+ [Fact]
+ public void DatePickerCultureTrackerSubscription()
+ {
+ var called = false;
+ var subscriber = new object();
+
+ CultureTracker.Subscribe(subscriber, () => called = true);
+
+ // Change culture to trigger notification
+ var originalCulture = CultureInfo.CurrentCulture;
+ try
+ {
+ CultureInfo.CurrentCulture = new CultureInfo("de-DE");
+ CultureTracker.CheckForCultureChanges();
+
+ Assert.True(called);
+ }
+ finally
+ {
+ CultureInfo.CurrentCulture = originalCulture;
+ CultureTracker.Unsubscribe(subscriber);
+ }
+ }
+
+ [Theory]
+ [InlineData("en-US", "d")]
+ [InlineData("de-DE", "d")]
+ [InlineData("fr-FR", "d")]
+ public void DatePickerFormatsWithDifferentCultures(string cultureName, string format)
+ {
+ var originalCulture = CultureInfo.CurrentCulture;
+ try
+ {
+ CultureInfo.CurrentCulture = new CultureInfo(cultureName);
+
+ var datePicker = new DatePicker
+ {
+ Date = new DateTime(2023, 12, 25),
+ Format = format
+ };
+
+ Assert.NotNull(datePicker);
+ Assert.Equal(format, datePicker.Format);
+ Assert.Equal(new DateTime(2023, 12, 25), datePicker.Date);
+ }
+ finally
+ {
+ CultureInfo.CurrentCulture = originalCulture;
+ }
+ }
+ }
+}
\ No newline at end of file
From acc137a4cb21188cdfcc6e4261ce66168d5b28df Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 4 Jul 2025 04:25:48 +0000
Subject: [PATCH 3/5] Fix culture change detection to work automatically
without manual triggers
Co-authored-by: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com>
---
src/Core/src/CultureTracker.cs | 41 +++++++++++++++++++
.../Platform/Android/DatePickerExtensions.cs | 3 --
.../Platform/Windows/DatePickerExtensions.cs | 3 --
.../src/Platform/iOS/DatePickerExtensions.cs | 3 --
.../UnitTests/Views/DatePickerCultureTests.cs | 36 +++++++++++++++-
5 files changed, 76 insertions(+), 10 deletions(-)
diff --git a/src/Core/src/CultureTracker.cs b/src/Core/src/CultureTracker.cs
index 1b977c56f8c9..06c9eb37e2c2 100644
--- a/src/Core/src/CultureTracker.cs
+++ b/src/Core/src/CultureTracker.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Globalization;
+using System.Threading;
namespace Microsoft.Maui
{
@@ -11,6 +12,8 @@ internal static class CultureTracker
{
static CultureInfo? s_currentCulture;
static readonly ConcurrentDictionary s_subscribers = new();
+ static Timer? s_cultureCheckTimer;
+ static readonly object s_lockObject = new();
///
/// Checks if the culture has changed since the last call and notifies subscribers if it has.
@@ -35,6 +38,19 @@ public static void Subscribe(object subscriber, Action action)
{
var weakRef = new WeakReference(subscriber);
s_subscribers.TryAdd(weakRef, action);
+
+ // Start monitoring when first subscriber is added
+ lock (s_lockObject)
+ {
+ if (s_cultureCheckTimer == null && s_subscribers.Count > 0)
+ {
+ // Initialize current culture if not set
+ s_currentCulture = CultureInfo.CurrentCulture;
+
+ // Check for culture changes every 100ms when there are subscribers
+ s_cultureCheckTimer = new Timer(OnTimerTick, null, TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(100));
+ }
+ }
}
///
@@ -52,6 +68,21 @@ public static void Unsubscribe(object subscriber)
break;
}
}
+
+ // Stop monitoring when no subscribers remain
+ lock (s_lockObject)
+ {
+ if (s_subscribers.Count == 0 && s_cultureCheckTimer != null)
+ {
+ s_cultureCheckTimer.Dispose();
+ s_cultureCheckTimer = null;
+ }
+ }
+ }
+
+ static void OnTimerTick(object? state)
+ {
+ CheckForCultureChanges();
}
static void NotifyCultureChanged()
@@ -83,6 +114,16 @@ static void NotifyCultureChanged()
{
s_subscribers.TryRemove(deadRef, out _);
}
+
+ // Stop monitoring if all references are dead
+ lock (s_lockObject)
+ {
+ if (s_subscribers.Count == 0 && s_cultureCheckTimer != null)
+ {
+ s_cultureCheckTimer.Dispose();
+ s_cultureCheckTimer = null;
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Core/src/Platform/Android/DatePickerExtensions.cs b/src/Core/src/Platform/Android/DatePickerExtensions.cs
index 3eb0fc29f1f2..c2319f51cf82 100644
--- a/src/Core/src/Platform/Android/DatePickerExtensions.cs
+++ b/src/Core/src/Platform/Android/DatePickerExtensions.cs
@@ -55,9 +55,6 @@ public static void UpdateMaximumDate(this MauiDatePicker platformDatePicker, IDa
internal static void SetText(this MauiDatePicker platformDatePicker, IDatePicker datePicker)
{
- // Check for culture changes before updating
- CultureTracker.CheckForCultureChanges();
-
platformDatePicker.Text = datePicker.Date.ToString(datePicker.Format);
}
}
diff --git a/src/Core/src/Platform/Windows/DatePickerExtensions.cs b/src/Core/src/Platform/Windows/DatePickerExtensions.cs
index 3ceeba2335d7..70a06cf55e15 100644
--- a/src/Core/src/Platform/Windows/DatePickerExtensions.cs
+++ b/src/Core/src/Platform/Windows/DatePickerExtensions.cs
@@ -9,9 +9,6 @@ public static class DatePickerExtensions
{
public static void UpdateDate(this CalendarDatePicker platformDatePicker, IDatePicker datePicker)
{
- // Check for culture changes before updating
- CultureTracker.CheckForCultureChanges();
-
var date = datePicker.Date;
platformDatePicker.UpdateDate(date);
diff --git a/src/Core/src/Platform/iOS/DatePickerExtensions.cs b/src/Core/src/Platform/iOS/DatePickerExtensions.cs
index 4bf02ce0e2ff..871a749e9bb4 100644
--- a/src/Core/src/Platform/iOS/DatePickerExtensions.cs
+++ b/src/Core/src/Platform/iOS/DatePickerExtensions.cs
@@ -56,9 +56,6 @@ public static void UpdateDate(this UIDatePicker picker, IDatePicker datePicker)
public static void UpdateDate(this MauiDatePicker platformDatePicker, IDatePicker datePicker, UIDatePicker? picker)
{
- // Check for culture changes before updating
- CultureTracker.CheckForCultureChanges();
-
if (picker != null && picker.Date.ToDateTime().Date != datePicker.Date.Date)
picker.SetDate(datePicker.Date.ToNSDate(), false);
diff --git a/src/Core/tests/UnitTests/Views/DatePickerCultureTests.cs b/src/Core/tests/UnitTests/Views/DatePickerCultureTests.cs
index 091770ddbdba..fd8700b3b542 100644
--- a/src/Core/tests/UnitTests/Views/DatePickerCultureTests.cs
+++ b/src/Core/tests/UnitTests/Views/DatePickerCultureTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
+using System.Threading;
using Microsoft.Maui.Controls;
using Xunit;
@@ -31,7 +32,9 @@ public void DatePickerCultureTrackerSubscription()
try
{
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
- CultureTracker.CheckForCultureChanges();
+
+ // Wait a bit for the timer to detect the change (max 200ms)
+ Thread.Sleep(250);
Assert.True(called);
}
@@ -42,6 +45,37 @@ public void DatePickerCultureTrackerSubscription()
}
}
+ [Fact]
+ public void DatePickerCultureTrackerAutoDetection()
+ {
+ var callCount = 0;
+ var subscriber = new object();
+
+ CultureTracker.Subscribe(subscriber, () => callCount++);
+
+ var originalCulture = CultureInfo.CurrentCulture;
+ try
+ {
+ // Change culture multiple times
+ CultureInfo.CurrentCulture = new CultureInfo("de-DE");
+ Thread.Sleep(150); // Allow detection
+
+ CultureInfo.CurrentCulture = new CultureInfo("fr-FR");
+ Thread.Sleep(150); // Allow detection
+
+ CultureInfo.CurrentCulture = new CultureInfo("ja-JP");
+ Thread.Sleep(150); // Allow detection
+
+ // Should have detected at least 3 culture changes
+ Assert.True(callCount >= 3, $"Expected at least 3 culture changes, but got {callCount}");
+ }
+ finally
+ {
+ CultureInfo.CurrentCulture = originalCulture;
+ CultureTracker.Unsubscribe(subscriber);
+ }
+ }
+
[Theory]
[InlineData("en-US", "d")]
[InlineData("de-DE", "d")]
From 0433bc3ebef9508bfc72f64261053d9baa07c134 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 6 Jul 2025 15:33:45 +0000
Subject: [PATCH 4/5] Fix culture monitoring race conditions and thread safety
issues
Co-authored-by: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com>
---
src/Core/src/CultureTracker.cs | 47 +++++++++++++++++++---------------
1 file changed, 26 insertions(+), 21 deletions(-)
diff --git a/src/Core/src/CultureTracker.cs b/src/Core/src/CultureTracker.cs
index 06c9eb37e2c2..32a5bcfadc16 100644
--- a/src/Core/src/CultureTracker.cs
+++ b/src/Core/src/CultureTracker.cs
@@ -10,21 +10,22 @@ namespace Microsoft.Maui
///
internal static class CultureTracker
{
- static CultureInfo? s_currentCulture;
+ static string? s_currentCultureName;
static readonly ConcurrentDictionary s_subscribers = new();
static Timer? s_cultureCheckTimer;
static readonly object s_lockObject = new();
+ static int s_subscriberCount;
///
/// Checks if the culture has changed since the last call and notifies subscribers if it has.
///
public static void CheckForCultureChanges()
{
- var currentCulture = CultureInfo.CurrentCulture;
+ var currentCultureName = CultureInfo.CurrentCulture.Name;
- if (s_currentCulture == null || !s_currentCulture.Equals(currentCulture))
+ if (s_currentCultureName == null || !s_currentCultureName.Equals(currentCultureName, StringComparison.Ordinal))
{
- s_currentCulture = currentCulture;
+ s_currentCultureName = currentCultureName;
NotifyCultureChanged();
}
}
@@ -36,16 +37,17 @@ public static void CheckForCultureChanges()
/// The action to invoke when culture changes
public static void Subscribe(object subscriber, Action action)
{
- var weakRef = new WeakReference(subscriber);
- s_subscribers.TryAdd(weakRef, action);
-
- // Start monitoring when first subscriber is added
lock (s_lockObject)
{
- if (s_cultureCheckTimer == null && s_subscribers.Count > 0)
+ var weakRef = new WeakReference(subscriber);
+ s_subscribers.TryAdd(weakRef, action);
+ s_subscriberCount = s_subscribers.Count;
+
+ // Start monitoring when first subscriber is added
+ if (s_cultureCheckTimer == null && s_subscriberCount > 0)
{
// Initialize current culture if not set
- s_currentCulture = CultureInfo.CurrentCulture;
+ s_currentCultureName = CultureInfo.CurrentCulture.Name;
// Check for culture changes every 100ms when there are subscribers
s_cultureCheckTimer = new Timer(OnTimerTick, null, TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(100));
@@ -59,20 +61,22 @@ public static void Subscribe(object subscriber, Action action)
/// The object to unsubscribe
public static void Unsubscribe(object subscriber)
{
- // Find and remove the weak reference
- foreach (var kvp in s_subscribers)
+ lock (s_lockObject)
{
- if (kvp.Key.IsAlive && ReferenceEquals(kvp.Key.Target, subscriber))
+ // Find and remove the weak reference
+ foreach (var kvp in s_subscribers)
{
- s_subscribers.TryRemove(kvp.Key, out _);
- break;
+ if (kvp.Key.IsAlive && ReferenceEquals(kvp.Key.Target, subscriber))
+ {
+ s_subscribers.TryRemove(kvp.Key, out _);
+ break;
+ }
}
- }
- // Stop monitoring when no subscribers remain
- lock (s_lockObject)
- {
- if (s_subscribers.Count == 0 && s_cultureCheckTimer != null)
+ s_subscriberCount = s_subscribers.Count;
+
+ // Stop monitoring when no subscribers remain
+ if (s_subscriberCount == 0 && s_cultureCheckTimer != null)
{
s_cultureCheckTimer.Dispose();
s_cultureCheckTimer = null;
@@ -118,7 +122,8 @@ static void NotifyCultureChanged()
// Stop monitoring if all references are dead
lock (s_lockObject)
{
- if (s_subscribers.Count == 0 && s_cultureCheckTimer != null)
+ s_subscriberCount = s_subscribers.Count;
+ if (s_subscriberCount == 0 && s_cultureCheckTimer != null)
{
s_cultureCheckTimer.Dispose();
s_cultureCheckTimer = null;
From c5a6d203793a7ab9f4b08287ab37555a2b9e108f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 6 Jul 2025 15:59:50 +0000
Subject: [PATCH 5/5] Fix culture change detection by removing timer-based
approach and providing manual notification
Co-authored-by: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com>
---
src/Core/src/CultureTracker.cs | 92 +++++--------------
.../net-android/PublicAPI.Unshipped.txt | 4 +
.../net-tizen/PublicAPI.Unshipped.txt | 4 +
.../src/PublicAPI/net/PublicAPI.Unshipped.txt | 4 +
.../netstandard/PublicAPI.Unshipped.txt | 4 +
.../netstandard2.0/PublicAPI.Unshipped.txt | 4 +
6 files changed, 41 insertions(+), 71 deletions(-)
diff --git a/src/Core/src/CultureTracker.cs b/src/Core/src/CultureTracker.cs
index 32a5bcfadc16..e5ada5bc8ff6 100644
--- a/src/Core/src/CultureTracker.cs
+++ b/src/Core/src/CultureTracker.cs
@@ -1,34 +1,15 @@
using System;
using System.Collections.Concurrent;
using System.Globalization;
-using System.Threading;
namespace Microsoft.Maui
{
///
- /// Provides culture change detection functionality for MAUI controls.
+ /// Provides culture change notification functionality for MAUI controls.
///
- internal static class CultureTracker
+ public static class CultureTracker
{
- static string? s_currentCultureName;
static readonly ConcurrentDictionary s_subscribers = new();
- static Timer? s_cultureCheckTimer;
- static readonly object s_lockObject = new();
- static int s_subscriberCount;
-
- ///
- /// Checks if the culture has changed since the last call and notifies subscribers if it has.
- ///
- public static void CheckForCultureChanges()
- {
- var currentCultureName = CultureInfo.CurrentCulture.Name;
-
- if (s_currentCultureName == null || !s_currentCultureName.Equals(currentCultureName, StringComparison.Ordinal))
- {
- s_currentCultureName = currentCultureName;
- NotifyCultureChanged();
- }
- }
///
/// Subscribes an object to culture change notifications.
@@ -37,22 +18,8 @@ public static void CheckForCultureChanges()
/// The action to invoke when culture changes
public static void Subscribe(object subscriber, Action action)
{
- lock (s_lockObject)
- {
- var weakRef = new WeakReference(subscriber);
- s_subscribers.TryAdd(weakRef, action);
- s_subscriberCount = s_subscribers.Count;
-
- // Start monitoring when first subscriber is added
- if (s_cultureCheckTimer == null && s_subscriberCount > 0)
- {
- // Initialize current culture if not set
- s_currentCultureName = CultureInfo.CurrentCulture.Name;
-
- // Check for culture changes every 100ms when there are subscribers
- s_cultureCheckTimer = new Timer(OnTimerTick, null, TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(100));
- }
- }
+ var weakRef = new WeakReference(subscriber);
+ s_subscribers.TryAdd(weakRef, action);
}
///
@@ -61,35 +28,29 @@ public static void Subscribe(object subscriber, Action action)
/// The object to unsubscribe
public static void Unsubscribe(object subscriber)
{
- lock (s_lockObject)
+ // Find and remove the weak reference
+ foreach (var kvp in s_subscribers)
{
- // Find and remove the weak reference
- foreach (var kvp in s_subscribers)
+ if (kvp.Key.IsAlive && ReferenceEquals(kvp.Key.Target, subscriber))
{
- if (kvp.Key.IsAlive && ReferenceEquals(kvp.Key.Target, subscriber))
- {
- s_subscribers.TryRemove(kvp.Key, out _);
- break;
- }
- }
-
- s_subscriberCount = s_subscribers.Count;
-
- // Stop monitoring when no subscribers remain
- if (s_subscriberCount == 0 && s_cultureCheckTimer != null)
- {
- s_cultureCheckTimer.Dispose();
- s_cultureCheckTimer = null;
+ s_subscribers.TryRemove(kvp.Key, out _);
+ break;
}
}
}
- static void OnTimerTick(object? state)
- {
- CheckForCultureChanges();
- }
-
- static void NotifyCultureChanged()
+ ///
+ /// Notifies all subscribers that the culture has changed.
+ /// Call this method from your application when you change the culture at runtime.
+ ///
+ ///
+ ///
+ /// // In your application code when changing culture:
+ /// CultureInfo.CurrentCulture = new CultureInfo("de-DE");
+ /// CultureTracker.NotifyCultureChanged();
+ ///
+ ///
+ public static void NotifyCultureChanged()
{
// Clean up dead references and notify live ones
var deadRefs = new System.Collections.Generic.List();
@@ -118,17 +79,6 @@ static void NotifyCultureChanged()
{
s_subscribers.TryRemove(deadRef, out _);
}
-
- // Stop monitoring if all references are dead
- lock (s_lockObject)
- {
- s_subscriberCount = s_subscribers.Count;
- if (s_subscriberCount == 0 && s_cultureCheckTimer != null)
- {
- s_cultureCheckTimer.Dispose();
- s_cultureCheckTimer = null;
- }
- }
}
}
}
\ No newline at end of file
diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
index e70e6e1d5cb6..00bd76d13e25 100644
--- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
@@ -1,4 +1,5 @@
#nullable enable
+Microsoft.Maui.CultureTracker
*REMOVED*Microsoft.Maui.Platform.MauiPicker.ShowPopupOnFocus.get -> bool
*REMOVED*Microsoft.Maui.Platform.MauiPicker.ShowPopupOnFocus.set -> void
*REMOVED*override Microsoft.Maui.Platform.MauiPicker.OnFocusChanged(bool gainFocus, Android.Views.FocusSearchDirection direction, Android.Graphics.Rect? previouslyFocusedRect) -> void
@@ -68,6 +69,9 @@ override Microsoft.Maui.SwipeViewSwipeEnded.ToString() -> string!
override Microsoft.Maui.SwipeViewSwipeStarted.Equals(object? obj) -> bool
override Microsoft.Maui.SwipeViewSwipeStarted.GetHashCode() -> int
override Microsoft.Maui.SwipeViewSwipeStarted.ToString() -> string!
+static Microsoft.Maui.CultureTracker.NotifyCultureChanged() -> void
+static Microsoft.Maui.CultureTracker.Subscribe(object! subscriber, System.Action! action) -> void
+static Microsoft.Maui.CultureTracker.Unsubscribe(object! subscriber) -> void
static Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.operator !=(Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate? left, Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate? right) -> bool
static Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.operator ==(Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate? left, Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate? right) -> bool
static Microsoft.Maui.Handlers.LayoutHandlerUpdate.operator !=(Microsoft.Maui.Handlers.LayoutHandlerUpdate? left, Microsoft.Maui.Handlers.LayoutHandlerUpdate? right) -> bool
diff --git a/src/Core/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
index 67eb5636a1b3..2fecf2c6e61f 100644
--- a/src/Core/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
@@ -1,4 +1,8 @@
#nullable enable
+Microsoft.Maui.CultureTracker
+static Microsoft.Maui.CultureTracker.NotifyCultureChanged() -> void
+static Microsoft.Maui.CultureTracker.Subscribe(object! subscriber, System.Action! action) -> void
+static Microsoft.Maui.CultureTracker.Unsubscribe(object! subscriber) -> void
virtual Microsoft.Maui.Animations.Lerp.LerpDelegate.Invoke(object! start, object! end, double progress) -> object!
Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.ContextFlyoutItemHandlerUpdate(Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate! original) -> void
Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.Deconstruct(out int Index, out Microsoft.Maui.IMenuElement! MenuElement) -> void
diff --git a/src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt
index 8f90f96f0e58..8c6728ce300b 100644
--- a/src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt
@@ -1,4 +1,8 @@
#nullable enable
+Microsoft.Maui.CultureTracker
+static Microsoft.Maui.CultureTracker.NotifyCultureChanged() -> void
+static Microsoft.Maui.CultureTracker.Subscribe(object! subscriber, System.Action! action) -> void
+static Microsoft.Maui.CultureTracker.Unsubscribe(object! subscriber) -> void
virtual Microsoft.Maui.Animations.Lerp.LerpDelegate.Invoke(object! start, object! end, double progress) -> object!
Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.ContextFlyoutItemHandlerUpdate(Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate! original) -> void
Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.Deconstruct(out int Index, out Microsoft.Maui.IMenuElement! MenuElement) -> void
diff --git a/src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt
index 4d1c8bf7c8ab..3092c4b14341 100644
--- a/src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt
@@ -1,4 +1,5 @@
#nullable enable
+Microsoft.Maui.CultureTracker
Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.ContextFlyoutItemHandlerUpdate(Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate! original) -> void
Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.Deconstruct(out int Index, out Microsoft.Maui.IMenuElement! MenuElement) -> void
Microsoft.Maui.Handlers.LayoutHandlerUpdate.Deconstruct(out int Index, out Microsoft.Maui.IView! View) -> void
@@ -59,6 +60,9 @@ override Microsoft.Maui.SwipeViewSwipeEnded.ToString() -> string!
override Microsoft.Maui.SwipeViewSwipeStarted.Equals(object? obj) -> bool
override Microsoft.Maui.SwipeViewSwipeStarted.GetHashCode() -> int
override Microsoft.Maui.SwipeViewSwipeStarted.ToString() -> string!
+static Microsoft.Maui.CultureTracker.NotifyCultureChanged() -> void
+static Microsoft.Maui.CultureTracker.Subscribe(object! subscriber, System.Action! action) -> void
+static Microsoft.Maui.CultureTracker.Unsubscribe(object! subscriber) -> void
static Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.operator !=(Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate? left, Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate? right) -> bool
static Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.operator ==(Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate? left, Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate? right) -> bool
static Microsoft.Maui.Handlers.LayoutHandlerUpdate.operator !=(Microsoft.Maui.Handlers.LayoutHandlerUpdate? left, Microsoft.Maui.Handlers.LayoutHandlerUpdate? right) -> bool
diff --git a/src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
index 4d1c8bf7c8ab..3092c4b14341 100644
--- a/src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
@@ -1,4 +1,5 @@
#nullable enable
+Microsoft.Maui.CultureTracker
Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.ContextFlyoutItemHandlerUpdate(Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate! original) -> void
Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.Deconstruct(out int Index, out Microsoft.Maui.IMenuElement! MenuElement) -> void
Microsoft.Maui.Handlers.LayoutHandlerUpdate.Deconstruct(out int Index, out Microsoft.Maui.IView! View) -> void
@@ -59,6 +60,9 @@ override Microsoft.Maui.SwipeViewSwipeEnded.ToString() -> string!
override Microsoft.Maui.SwipeViewSwipeStarted.Equals(object? obj) -> bool
override Microsoft.Maui.SwipeViewSwipeStarted.GetHashCode() -> int
override Microsoft.Maui.SwipeViewSwipeStarted.ToString() -> string!
+static Microsoft.Maui.CultureTracker.NotifyCultureChanged() -> void
+static Microsoft.Maui.CultureTracker.Subscribe(object! subscriber, System.Action! action) -> void
+static Microsoft.Maui.CultureTracker.Unsubscribe(object! subscriber) -> void
static Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.operator !=(Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate? left, Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate? right) -> bool
static Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.operator ==(Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate? left, Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate? right) -> bool
static Microsoft.Maui.Handlers.LayoutHandlerUpdate.operator !=(Microsoft.Maui.Handlers.LayoutHandlerUpdate? left, Microsoft.Maui.Handlers.LayoutHandlerUpdate? right) -> bool