[Android] Fix DatePicker MinimumDate/MaximumDate not updating dynamically#33687
[Android] Fix DatePicker MinimumDate/MaximumDate not updating dynamically#33687HarishwaranVijayakumar wants to merge 8 commits intodotnet:mainfrom
Conversation
|
Hey there @@HarishwaranVijayakumar! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
This PR fixes a long-standing Android platform bug where DatePicker minimum and maximum date constraints fail to update dynamically after the initial setting. The issue manifests in dependent DatePicker scenarios where changing one picker's date should update another picker's minimum/maximum constraint.
Changes:
- Implements the established Android workaround by resetting min/max values before setting new constraints
- Adds ShowEvent handler to ensure constraints are re-applied after Android's internal dialog initialization
- Includes comprehensive UI tests with screenshot verification to validate the fix
Reviewed changes
Copilot reviewed 5 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/Core/src/Platform/Android/DatePickerExtensions.cs |
Added reset logic (MinDate=0, MaxDate=long.MaxValue) before setting new constraint values to force Android to invalidate cached values |
src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs |
Added ShowEvent handler to re-apply min/max dates after dialog initialization, working around Android's dialog reuse caching issue |
src/Controls/tests/TestCases.HostApp/Issues/Issue19256.cs |
Created UI test page with two dependent DatePickers and buttons to reproduce the issue scenario |
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19256.cs |
Created NUnit test with two-screenshot verification approach to validate dynamic MinimumDate updates |
src/Controls/tests/TestCases.Android.Tests/snapshots/android/DatePickerMinimumDateShouldUpdateDynamically_*.png |
Reference screenshots for automated visual verification (2 files) |
.github/agent-pr-session/pr-19256.md |
Agent session documentation tracking the fix development process |
|
/rebase |
172932c to
b4a8906
Compare
|
/azp run maui-pr-uitests, maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
|
/rebase |
PureWeen
left a comment
There was a problem hiding this comment.
🤖 AI Code Review - Recommendations
Thanks for this fix! The approach correctly addresses the known Android DatePicker caching bug. After reviewing the implementation, I have two recommendations:
🔴 Critical: Event Handler May Leak
The current code only unsubscribes inside the if block. If VirtualView or _dialog becomes null between subscription and event firing, the handler stays subscribed.
Current code:
updateMinMaxDates = (sender, e) =>
{
if (VirtualView is not null && _dialog is not null)
{
PlatformView?.UpdateMinimumDate(VirtualView, _dialog);
PlatformView?.UpdateMaximumDate(VirtualView, _dialog);
_dialog.ShowEvent -= updateMinMaxDates; // ❌ Only unsubscribes on success
}
};Recommended fix:
updateMinMaxDates = (sender, e) =>
{
var dialog = _dialog; // Capture locally to avoid race
try
{
if (VirtualView is not null && dialog is not null)
{
PlatformView?.UpdateMinimumDate(VirtualView, dialog);
PlatformView?.UpdateMaximumDate(VirtualView, dialog);
}
}
finally
{
if (dialog is not null)
dialog.ShowEvent -= updateMinMaxDates; // ✅ Always unsubscribe
}
};🟡 Potential Crash with Disjoint Date Ranges
When updating from range [2020-2021] to [2023-2024], the sequential update can crash:
UpdateMinimumDate(2023)→ setsMinDate = 2023- 💥
IllegalArgumentException:MinDate (2023) > MaxDate (2021)
Recommended fix - reset both extremes in the handler BEFORE calling individual updates:
if (VirtualView is not null && dialog is not null)
{
// Widen constraints first to prevent IllegalArgumentException
// when transitioning between disjoint date ranges
dialog.DatePicker.MinDate = 0;
dialog.DatePicker.MaxDate = long.MaxValue;
PlatformView?.UpdateMinimumDate(VirtualView, dialog);
PlatformView?.UpdateMaximumDate(VirtualView, dialog);
}The rest of the implementation looks good - the StackOverflow reference is helpful, and the approach is sound. These changes would make it more robust for edge cases.
@PureWeen, As suggested, I committed the code changes. |
🤖 AI Summary📊 Expand Full Review🔍 Pre-Flight — Context & Validation📝 Review Session — Modified the fix which resolve the issue 33583 ·
|
| File | Type | Changes |
|---|---|---|
src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs |
Fix | +44/-8 |
src/Controls/tests/TestCases.HostApp/Issues/Issue19256.cs |
Test UI | +79 |
src/Controls/tests/TestCases.HostApp/Issues/Issue33583.cs |
Test UI | +39 |
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19256.cs |
Test NUnit | +53 |
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33583.cs |
Test NUnit | +50 |
src/Controls/tests/TestCases.Android.Tests/snapshots/android/*.png |
Snapshots | 3 files |
.github/agent-pr-session/pr-19256.md |
Agent | +274 |
Prior Review Discussion
| Reviewer | Concern | Status |
|---|---|---|
| Copilot | StackOverflow URL missing from inline Not addressed in final code | comments |
| PureWeen | Event handler may leak (unsubscription in finally block Addressed by switching to ResetDialog() | ) |
| PureWeen | Disjoint date ranges could crash (IllegalArgumentException Addressed by switching to ResetDialog() | ) |
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #33687 | ResetDialog() on min/max change; recreate fresh dialog on PENDING (Gate) | DatePickerHandler.Android.cs (+44/-8) |
Final after addressing review comments | show |
Issues to Flag###
.github/agent-pr-session/pr-19256.md- Agent session file committed to repo (should not be included)- Test
Issue19256.csuses#if ANDROIDpreprocessor (wraps entire class) - acceptable given Android-only issue - Test
Issue33583.csis#if !IOS- tests Android, Windows, MacCatalyst, but snapshot only for Android
🚦 Gate — Test Verification
📝 Review Session — Modified the fix which resolve the issue 33583 · 43fc76c
Result PASSED:
Platform: android
Mode: Full Verification (RequireFullVerification: true)
Test Filter: Issue19256
- Tests FAIL without fix
- Tests PASS with fix
Summary: Gate verification confirms the fix in DatePickerHandler.Android.cs correctly resolves issue #19256. The test DatePickerMinimumDateShouldUpdateDynamically properly catches the bug (fails without fix) and validates the fix (passes with fix).
🔧 Fix — Analysis & Comparison
📝 Review Session — Modified the fix which resolve the issue 33583 · 43fc76c
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | claude-sonnet-4.5 | Track _minDateChanged/_maxDateChanged flags + re-apply constraints + Init() refresh before FAIL | DatePickerHandler.Android.cs |
Android's SimpleMonthView caches don't re-query constraints via Init() | Show |
| 2 | claude-opus-4.6 | Reset MinDate=0/MaxDate=long.MaxValue in extensions + re-apply before Show + FAIL | DatePickerExtensions.cs, DatePickerHandler.Android.cs |
ViewPager month view pages are cached at a level Init() can't refresh | Init() |
| 3 | gpt-5.2 | ViewPager adapter reset via reflection (setAdapter(null)+setAdapter( FAIL | DatePickerHandler.Android.cs |
Cache persists deeper than the adapter layer | existing)) |
| 4 | gpt-5.2-codex | Always recreate dialog on every ShowPickerDialog call PASS | DatePickerHandler.Android.cs |
Works but wasteful - recreates even when min/max hasn't changed | |
| 5 | gemini-3-pro-preview | Clear min/max + re-apply + DatePicker.Init() re-init with dialog as FAIL | DatePickerHandler.Android.cs |
Same deeper caching blocks visual update | listener |
| PR | PR #33687 | ResetDialog() when min/max changes (lazy recreation on next Show PASS (Gate) | DatePickerHandler.Android.cs |
Optimal: only recreates when constraints actually change | ) |
Cross-Pollination (Round 2):
| Model | Response |
|---|---|
| claude-sonnet-4.5 | NEW IDEA (removeAllViews + fresh adapter; mTempCalendar reflection) - blocked by rate limit |
| claude-opus-4.6 | NO NEW IDEAS - confirmed PR's lazy ResetDialog() is optimal |
| gpt-5.2 | NEW IDEA (swap internal DatePicker view) - similar to Attempt 3, likely same failure |
| gpt-5.2-codex | NEW IDEA (replace internal DatePicker view) - same as gpt-5.2 idea |
| gemini-3-pro-preview | NEW IDEA (clamp date before min/max) - blocked by rate limit |
Rate limit note: Cross-pollination attempts 6+ were blocked by token rate limits. Given that 4/5 non-recreation approaches failed, and Attempt 4 confirmed that recreation works (PR's approach is a superior lazy version of Attempt 4), the evidence strongly supports the PR's fix as optimal.
Exhausted: Yes (partial - rate limit prevented 2 additional cross-pollination tests)
Selected Fix: PR's fix - ResetDialog() lazy recreation is superior to Attempt 4 (always-recreate) because it only incurs dialog recreation cost when min/max actually changes, and is already validated by Gate.
SimpleMonthView pages). No public or reflection-based API can reliably flush this cache in-place. Dialog recreation is the only reliable fix.
📋 Report — Final Recommendation
📝 Review Session — Modified the fix which resolve the issue 33583 · 43fc76c
Final Recommendation: APPROVE (with minor notes)
Summary
PR #33687 fixes a long-standing Android platform limitation where DatePickerDialog internally caches MinDate/MaxDate values and ignores subsequent updates after the dialog is first shown. The fix uses lazy dialog recreation (ResetDialog()) when MinimumDate or MaximumDate changes, ensuring a fresh dialog with correct constraints is created on the next Show().
SimpleMonthView) cannot be reliably refreshed in-place.
Root Cause
Android's DatePickerDialog widget caches MinDate/MaxDate values internally. When setMinDate() or setMaxDate() is called on an existing dialog instance, the cached constraint values in internal SimpleMonthView pages (rendered via ViewPager) are NOT refreshed. The visual state (enabled/disabled dates) remains stale. This is a known Android platform limitation dating back 10+ years (SO #19616575).
Fix Quality
The final approach is correct and well-implemented:
In DatePickerHandler.Android.cs:
MapMinimumDate/MapMaximumDate: CallResetDialog()when a non-null constraint is set, nulling_dialogResetDialog(): Safely dismisses and nulls the dialog (extracted as reusable helper)ShowPickerDialog: When_dialog is null, creates fresh dialog and immediately applies min/max constraints
Why this is optimal (confirmed by try-fix):
- Attempt 4 (always-recreate on every Show) also passed tests but is creates a new dialog even when constraints haven't changedwasteful
- PR's lazy approach only recreates when constraints actually lower overheadchange
- All 5 in-place update approaches failed: Init() refresh, reset-before-set, ViewPager adapter reset, Init() re-init
Code Review
** Correct behavior for constraint clearing:**
When is set to null (constraint removed), calls with potentially null this does nothing immediately. On next , a fresh dialog is created and is called with null MinimumDate, correctly setting MinDate to DateTime.MinValue. dialog
** ResetDialog() is safe:**
Properly unsubscribes before dismissing to avoid handler loops.
** Tests are well-structured:**
- Issue19256: Tests dynamic MinimumDate update scenario (two dependent pickers)
- Issue33583: Tests MaximumDate update after first open (single picker)
- Android-only scope is appropriate; snapshots provided
Minor Issues to Address###
1. Agent Session File Should Not Be Committed
.github/agent-pr-session/pr-19256.md (+274 lines) is a development artifact from the AI agent workflow. This should NOT be committed to the repository. It should be removed from the PR or added to .gitignore.
2. PR Description Describes Old Approach
The PR description says:
"In
DatePickerExtensions.cs, bothUpdateMinimumDateandUpdateMaximumDatemethods now reset the underlying Android values (MinDateto 0,MaxDatetolong.MaxValue) before setting the new constraint..."
This describes the original approach that was replaced after PureWeen's review. DatePickerExtensions.cs has no changes in this PR. The description should be updated to accurately describe the ResetDialog() approach.
3. Missing StackOverflow URL in Inline Comment (Copilot suggestion, not addressed)
The comments say "Android's DatePickerDialog caches min/max dates internally, making it unreliable to update them after the dialog is created." but don't include the StackOverflow reference URL. Minor but helpful for future maintainers.
PR Title Assessment
Current: [Android] Fix DatePicker MinimumDate/MaximumDate not updating dynamically
Verdict Accurate and no change needed.descriptive :
What NOT to Do (for future agents)
Don't try to update min/max in-place on existing Android's SimpleMonthView page cache cannot be flushed via Init(), setAdapter(null), or reflectionDatePickerDialog -
Don't use reset-before-set (MinDate= the StackOverflow workaround doesn't reliably work on modern Android due to deeper ViewPager caching0) -
Don't recreate on every works but wasteful; lazy recreation on constraint change (PR's approach) is betterShow -
Platforms Tested
- Android (Gate verified + 5 try-fix attempts)
- iOS (not bug is Android-specific)affected
- Windows (not affected)
- MacCatalyst (not affected)
🔧 Try-Fix Analysis: ✅ 2 passed, ❌ 1 failed
✅ Fix 1
Approach: Recreate Dialog on Constraint Changes
Force recreation of DatePickerDialog when MinimumDate or MaximumDate changes by nulling out the _dialog field in the MapMinimumDate and MapMaximumDate handlers. This ensures a fresh dialog instance is created on next show, completely bypassing Android's internal caching.
Different from existing fix:
- PR's fix: Keeps same dialog instance, uses ShowEvent + reset workaround to force updates
- This fix: Destroys and recreates dialog when constraints change, avoiding the cache entirely
Implementation:
- Dispose and null
_dialogin MapMinimumDate/MapMaximumDate - Next ShowPickerDialog call will create fresh dialog with new constraints
- Apply min/max constraints immediately after creating the dialog in ShowPickerDialog
diff --git a/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs b/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs
index 731e511b3f..f00e83e7ef 100644
--- a/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs
+++ b/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs
@@ -106,13 +106,31 @@ namespace Microsoft.Maui.Handlers
public static partial void MapMinimumDate(IDatePickerHandler handler, IDatePicker datePicker)
{
if (handler is DatePickerHandler platformHandler)
- handler.PlatformView?.UpdateMinimumDate(datePicker, platformHandler._dialog);
+ {
+ // Force dialog recreation to avoid Android's min/max caching bug
+ // See: https://stackoverflow.com/questions/19616575/setmindate-for-datepicker-doesnt-work-when-invoked-a-second-time
+ if (platformHandler._dialog is not null)
+ {
+ platformHandler._dialog.DismissEvent -= platformHandler.OnDialogDismiss;
+ platformHandler._dialog.Dismiss();
+ platformHandler._dialog = null;
+ }
+ }
}
public static partial void MapMaximumDate(IDatePickerHandler handler, IDatePicker datePicker)
{
if (handler is DatePickerHandler platformHandler)
- handler.PlatformView?.UpdateMaximumDate(datePicker, platformHandler._dialog);
+ {
+ // Force dialog recreation to avoid Android's min/max caching bug
+ // See: https://stackoverflow.com/questions/19616575/setmindate-for-datepicker-doesnt-work-when-invoked-a-second-time
+ if (platformHandler._dialog is not null)
+ {
+ platformHandler._dialog.DismissEvent -= platformHandler.OnDialogDismiss;
+ platformHandler._dialog.Dismiss();
+ platformHandler._dialog = null;
+ }
+ }
}
public static partial void MapCharacterSpacing(IDatePickerHandler handler, IDatePicker datePicker)
@@ -170,6 +188,13 @@ namespace Microsoft.Maui.Handlers
if (_dialog is null)
{
_dialog = CreateDatePickerDialog(year, month, day);
+
+ // Apply min/max constraints to newly created dialog
+ if (VirtualView is not null)
+ {
+ PlatformView?.UpdateMinimumDate(VirtualView, _dialog);
+ PlatformView?.UpdateMaximumDate(VirtualView, _dialog);
+ }
}
else
{
Analysis
Result: Pass ✅
What happened:
The test executed successfully and all assertions passed. The DatePicker MinimumDate/MaximumDate constraints now update dynamically when changed.
Why it worked:
This fix works by completely bypassing Android's internal caching bug. Instead of trying to force Android to accept new min/max values on the same DatePickerDialog instance, we simply destroy and recreate the dialog whenever the constraints change.
Approach details:
- When
MapMinimumDateorMapMaximumDateis called (triggered by property changes), the existing_dialogis dismissed and set to null - The next time
ShowPickerDialogis called, it detects_dialog is nulland creates a fresh DatePickerDialog instance - Immediately after creating the new dialog, we apply the current min/max constraints via
UpdateMinimumDateandUpdateMaximumDate - The fresh dialog has no cached state, so it accepts the constraints correctly
Advantages over PR's approach:
- Simpler logic - no event handler subscription/unsubscription needed
- No try/finally blocks for memory leak prevention
- No need to reset min/max to extreme values before setting new values
- Cleaner separation of concerns - constraint changes trigger dialog recreation, dialog creation applies constraints
Potential concerns:
- Dialog recreation might have a slight performance cost compared to reusing the same instance
- Any custom dialog state (beyond date/min/max) would be lost, but this doesn't apply to MAUI's current implementation
- The dismissed dialog might briefly flash if it was showing, though MapMinimumDate/MapMaximumDate are unlikely to be called while dialog is open
Insights for future attempts:
- Sometimes the simplest solution is to avoid the problematic code path entirely rather than working around it
- Android's DatePickerDialog caching is at the instance level, so instance recreation is a valid workaround
- The MAUI architecture already supports lazy dialog creation in
ShowPickerDialog, making this approach natural
✅ Fix 2
Approach: Mark Dialog Stale + Lazy Recreation
Instead of immediately recreating the dialog (attempt #1) or updating constraints on existing dialog (PR), mark the dialog as "stale" when constraints change and lazily recreate it only when shown.
Theory: The Android caching bug means we can't reliably update constraints on an existing dialog. But we can mark it as needing recreation and defer the actual work until the user taps to show the picker.
Implementation:
- Add a
_dialogNeedsRefreshbool field to handler - In MapMinimumDate/MapMaximumDate, set
_dialogNeedsRefresh = true(don't touch dialog) - In ShowPickerDialog, if
_dialogNeedsRefreshis true, null out dialog and create fresh - Apply fresh constraints to the new dialog
Different from existing fixes:
- PR's approach: Immediately updates dialog with reset-then-set pattern + ShowEvent
- Prior attempt [Draft] Readme WIP #1: Immediately destroys and recreates dialog on constraint change
- This approach: Lazy invalidation - just marks stale, recreates only when actually shown
diff --git a/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs b/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs
index 731e511b3f..f2a48814c4 100644
--- a/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs
+++ b/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs
@@ -9,6 +9,7 @@ namespace Microsoft.Maui.Handlers
public partial class DatePickerHandler : ViewHandler<IDatePicker, MauiDatePicker>
{
DatePickerDialog? _dialog;
+ bool _dialogConstraintsStale;
internal DatePickerDialog? DatePickerDialog => _dialog;
@@ -106,13 +107,21 @@ namespace Microsoft.Maui.Handlers
public static partial void MapMinimumDate(IDatePickerHandler handler, IDatePicker datePicker)
{
if (handler is DatePickerHandler platformHandler)
- handler.PlatformView?.UpdateMinimumDate(datePicker, platformHandler._dialog);
+ {
+ // Mark dialog as stale so it will be recreated on next show
+ // This avoids Android's min/max caching bug
+ platformHandler._dialogConstraintsStale = true;
+ }
}
public static partial void MapMaximumDate(IDatePickerHandler handler, IDatePicker datePicker)
{
if (handler is DatePickerHandler platformHandler)
- handler.PlatformView?.UpdateMaximumDate(datePicker, platformHandler._dialog);
+ {
+ // Mark dialog as stale so it will be recreated on next show
+ // This avoids Android's min/max caching bug
+ platformHandler._dialogConstraintsStale = true;
+ }
}
public static partial void MapCharacterSpacing(IDatePickerHandler handler, IDatePicker datePicker)
@@ -167,9 +176,25 @@ namespace Microsoft.Maui.Handlers
var month = (date?.Month ?? DateTime.Today.Month) - 1;
var day = date?.Day ?? DateTime.Today.Day;
+ // If constraints changed since dialog was created, dispose it so we create fresh
+ if (_dialogConstraintsStale && _dialog is not null)
+ {
+ _dialog.DismissEvent -= OnDialogDismiss;
+ _dialog.Dismiss();
+ _dialog = null;
+ _dialogConstraintsStale = false;
+ }
+
if (_dialog is null)
{
_dialog = CreateDatePickerDialog(year, month, day);
+
+ // Apply constraints to freshly created dialog
+ if (VirtualView is not null)
+ {
+ PlatformView?.UpdateMinimumDate(VirtualView, _dialog);
+ PlatformView?.UpdateMaximumDate(VirtualView, _dialog);
+ }
}
else
{
Analysis
Result: Pass
What happened: The test DatePickerMinimumDateShouldUpdateDynamically passed successfully. Both screenshots (FutureDate and EarlierDate) matched their baselines.
Why it worked: The "lazy stale invalidation" approach successfully solves the Android DatePicker caching bug by:
- Avoiding the bug entirely: Instead of trying to update constraints on an existing dialog (which Android ignores due to caching), we simply mark the dialog as "stale" when constraints change
- Deferred recreation: When the user taps to show the picker, we check the stale flag. If stale, we dispose the old dialog and create a fresh one with correct constraints
- Fresh constraints applied: The new dialog has no cached min/max values, so it accepts the correct constraints immediately
Key differences from other approaches:
- Unlike attempt [Draft] Readme WIP #1 (immediate recreation): We don't recreate until actually needed, avoiding unnecessary work if constraints change multiple times before showing
- Unlike PR's reset pattern: We don't try to fight Android's caching by resetting values - we sidestep it entirely with a new dialog
Insights: This approach is cleaner because it:
- Requires no changes to DatePickerExtensions.cs (only handler changes)
- Uses a simple boolean flag for lazy invalidation
- Is similar to attempt [Draft] Readme WIP #1 in effect (both create fresh dialogs) but different in timing (lazy vs immediate)
❌ Fix 3
Approach: Deferred Constraint Application via Handler.post()
Post the MinDate/MaxDate updates to the next message loop iteration using Android.OS.Handler.Post(), allowing Android's internal DatePicker state to settle before applying new constraints.
Rationale: Android DatePicker caches min/max date constraints internally. By deferring the update to the next message loop iteration, we give Android's internal state machine time to complete any ongoing operations before applying new constraints.
Different from existing fixes:
- PR fix: Resets min/max before setting + uses ShowEvent handler
- Attempt [Draft] Readme WIP #1: Recreates dialog on constraint changes
- Attempt Update README.md #2: Marks dialog stale, recreates on Show
- Attempt Third #3 (this): Uses Handler.Post() to defer constraint application to next iteration - no dialog recreation, no ShowEvent hooks
Implementation:
- Use
Android.OS.Handlerwith main looper to post constraint updates - Constraints are applied asynchronously in the next message loop iteration
- This gives Android's widget internal state time to settle
diff --git a/src/Core/src/Platform/Android/DatePickerExtensions.cs b/src/Core/src/Platform/Android/DatePickerExtensions.cs
index 478167df75..8d1628f50e 100644
--- a/src/Core/src/Platform/Android/DatePickerExtensions.cs
+++ b/src/Core/src/Platform/Android/DatePickerExtensions.cs
@@ -1,11 +1,16 @@
using System;
using Android.App;
using Android.Content.Res;
+using Android.OS;
namespace Microsoft.Maui.Platform
{
public static class DatePickerExtensions
{
+ // Handler for posting constraint updates to the next message loop iteration.
+ // This allows Android's internal DatePicker state to settle before applying new constraints.
+ // Reference: https://stackoverflow.com/questions/19616575/setmindate-for-datepicker-doesnt-work-when-invoked-a-second-time
+ static readonly Handler _mainHandler = new Handler(Looper.MainLooper!);
public static void UpdateFormat(this MauiDatePicker platformDatePicker, IDatePicker datePicker)
{
platformDatePicker.SetText(datePicker);
@@ -38,16 +43,46 @@ namespace Microsoft.Maui.Platform
{
if (datePickerDialog is not null)
{
+ var nativeDatePicker = datePickerDialog.DatePicker;
+
+ long minDate;
if (datePicker.MinimumDate is null)
{
- datePickerDialog.DatePicker.MinDate = (long)DateTime.MinValue.ToUniversalTime()
+ minDate = (long)DateTime.MinValue.ToUniversalTime()
.Subtract(DateTime.MinValue.AddYears(1969)).TotalMilliseconds;
-
- return;
+ }
+ else
+ {
+ minDate = (long)datePicker.MinimumDate.Value
+ .ToUniversalTime().Subtract(DateTime.MinValue.AddYears(1969)).TotalMilliseconds;
}
- datePickerDialog.DatePicker.MinDate = (long)datePicker.MinimumDate.Value
- .ToUniversalTime().Subtract(DateTime.MinValue.AddYears(1969)).TotalMilliseconds;
+ // Two-phase update pattern with forced UI refresh:
+ // 1. Reset to extreme to clear cached state
+ // 2. Apply the actual value
+ // 3. Force a UI update via Handler.Post() to ensure Android's internal state syncs
+ // Reference: https://stackoverflow.com/questions/19616575/setmindate-for-datepicker-doesnt-work-when-invoked-a-second-time
+
+ // Reset to extreme first to clear cached state
+ nativeDatePicker.MinDate = long.MinValue;
+
+ // Ensure MaxDate > MinDate to avoid conflicts
+ if (nativeDatePicker.MaxDate <= minDate)
+ {
+ nativeDatePicker.MaxDate = minDate + (long)TimeSpan.FromDays(1).TotalMilliseconds;
+ }
+
+ // Apply the actual constraint synchronously
+ nativeDatePicker.MinDate = minDate;
+
+ // Force a deferred UI invalidation to ensure Android's internal state syncs
+ _mainHandler.Post(() =>
+ {
+ if (nativeDatePicker is null || nativeDatePicker.Handle == IntPtr.Zero)
+ return;
+
+ nativeDatePicker.Invalidate();
+ });
}
}
@@ -60,16 +95,46 @@ namespace Microsoft.Maui.Platform
{
if (datePickerDialog is not null)
{
+ var nativeDatePicker = datePickerDialog.DatePicker;
+
+ long maxDate;
if (datePicker.MaximumDate is null)
{
- datePickerDialog.DatePicker.MaxDate = (long)DateTime.MaxValue.ToUniversalTime()
+ maxDate = (long)DateTime.MaxValue.ToUniversalTime()
.Subtract(DateTime.MinValue.AddYears(1969)).TotalMilliseconds;
-
- return;
+ }
+ else
+ {
+ maxDate = (long)datePicker.MaximumDate.Value
+ .ToUniversalTime().Subtract(DateTime.MinValue.AddYears(1969)).TotalMilliseconds;
}
- datePickerDialog.DatePicker.MaxDate = (long)datePicker.MaximumDate.Value
- .ToUniversalTime().Subtract(DateTime.MinValue.AddYears(1969)).TotalMilliseconds;
+ // Two-phase update pattern with forced UI refresh:
+ // 1. Reset to extreme to clear cached state
+ // 2. Apply the actual value
+ // 3. Force a UI update via Handler.Post() to ensure Android's internal state syncs
+ // Reference: https://stackoverflow.com/questions/19616575/setmindate-for-datepicker-doesnt-work-when-invoked-a-second-time
+
+ // Reset to extreme first to clear cached state
+ nativeDatePicker.MaxDate = long.MaxValue;
+
+ // Ensure MinDate < MaxDate to avoid conflicts
+ if (nativeDatePicker.MinDate >= maxDate)
+ {
+ nativeDatePicker.MinDate = maxDate - (long)TimeSpan.FromDays(1).TotalMilliseconds;
+ }
+
+ // Apply the actual constraint synchronously
+ nativeDatePicker.MaxDate = maxDate;
+
+ // Force a deferred UI invalidation to ensure Android's internal state syncs
+ _mainHandler.Post(() =>
+ {
+ if (nativeDatePicker is null || nativeDatePicker.Handle == IntPtr.Zero)
+ return;
+
+ nativeDatePicker.Invalidate();
+ });
}
}
Analysis
Result: Fail
What happened:
Tests compiled and ran but failed with visual snapshot mismatch (0.75% difference). The test DatePickerMinimumDateShouldUpdateDynamically detected that the DatePicker's minimum date constraints were not being correctly applied when the minimum date was changed from a future date (June 25) to an earlier date (June 20).
Why it failed:
The Handler.post() deferred constraint approach does not solve the Android DatePicker caching bug because:
-
Timing mismatch: The deferred Invalidate() call happens AFTER the dialog has already rendered with the wrong constraints. Android's DatePicker reads and caches MinDate/MaxDate when the dialog opens, not on subsequent invalidation.
-
Reset+Apply synchronously works partially: Setting
MinDate = long.MinValuefollowed byMinDate = newValuedoes work in some cases, but Android's internal CalendarView (used by DatePicker in spinner mode on some APIs) may cache bounds earlier in the layout pass. -
Invalidate() is insufficient: Calling
Invalidate()on the DatePicker only triggers a redraw of the current state - it doesn't force the widget to re-read its MinDate/MaxDate properties. The internal mMinDate/mMaxDate fields remain cached.
Root cause insight:
Android's DatePicker widget has internal fields (mMinDate, mMaxDate) that are read once when constraints are set, and the UI (particularly the CalendarView and spinner adapters) cache these values. Simply resetting and re-setting the property values doesn't reliably force all internal components to re-read the new constraints.
The working approaches (dialog recreation, ShowEvent hook, or reflection reset) all ensure that the constraints are correctly applied BEFORE the dialog renders - not after.
Insights for future attempts:
- Handler.post() deferral is the wrong direction - constraints must be applied BEFORE dialog shows
- The issue is in Android's internal caching, not in when we apply constraints on our side
- Approaches that work involve either: (1) recreating the dialog, or (2) resetting constraints just before show, or (3) using reflection to bypass the cache
📋 Expand PR Finalization Review
Title: ✅ Good
Current: [Android] Fix DatePicker MinimumDate/MaximumDate not updating dynamically
Description: ⚠️ Needs Update
Description needs updates. See details below.
✨ Suggested PR Description
[!NOTE]
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Root Cause
Android's DatePickerDialog internally caches MinDate/MaxDate values. Once set, subsequent setMinDate()/setMaxDate() calls are ignored unless the dialog is re-created. This is a known Android platform limitation documented on StackOverflow.
Description of Change
src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs:
MapMinimumDateandMapMaximumDatenow callResetDialog()when a non-null min/max value is provided, forcing the dialog to be re-created on next open. This ensures the new constraints are always respected by a fresh dialog.ShowPickerDialogapplies min/max constraints immediately after creating a new dialog.- Introduced a
ResetDialog()helper method (also used byDisconnectHandler) to centralize dialog teardown logic.
DatePickerExtensions.cs is not modified.
Testing:
Issue19256.cs(HostApp + NUnit): Two-DatePicker dependent scenario — LEFT picker sets RIGHT picker'sMinimumDate. Verifies that the right picker's minimum updates correctly on second and subsequent changes (Android only).Issue33583.cs(HostApp + NUnit): Single-picker scenario — open picker, dismiss, changeMaximumDatevia button, re-open picker. Verifies updated constraint appears in the dialog (Android, Windows, MacCatalyst).
Issues Fixed
Platforms Tested
- Android
- iOS (not affected — iOS DatePicker uses a wheel picker, not a calendar dialog)
- Windows
- MacCatalyst
Code Review: ⚠️ Issues Found
Code Review: PR #33687
🔴 Critical Issues
1. Internal Agent Session File Committed to Repo
File: .github/agent-pr-session/pr-19256.md
Problem: This is an internal agent work file containing detailed reasoning steps, fix iterations, and workaround analysis. It should not be committed to the public repository. It exposes implementation history that is not appropriate as permanent source content, and this directory (.github/agent-pr-session/) is not part of the standard MAUI repo structure.
Recommendation: Remove this file from the PR (delete from the branch and add .github/agent-pr-session/ to .gitignore if needed).
🟡 Suggestions
2. Issue33583 HostApp: PlatformAffected.Android Does Not Match Test Coverage
File: src/Controls/tests/TestCases.HostApp/Issues/Issue33583.cs
Problem: The [Issue] attribute declares PlatformAffected.Android, but the corresponding UI test (Issue33583.cs) runs on Android, Windows, and MacCatalyst (excluded only on iOS via #if !IOS). The HostApp attribute should reflect all affected platforms.
// Current
[Issue(IssueTracker.Github, 33583, "...", PlatformAffected.Android)]
// Recommended
[Issue(IssueTracker.Github, 33583, "...", PlatformAffected.All)]
// or more precisely:
[Issue(IssueTracker.Github, 33583, "...", PlatformAffected.Android | PlatformAffected.Windows | PlatformAffected.macOS)]3. Missing Snapshots for Windows and MacCatalyst (Issue33583 Test)
File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33583.cs
Problem: The test VerifyMaximumDateInRuntime uses VerifyScreenshot() and runs on Android, Windows, and MacCatalyst. Only the Android snapshot (TestCases.Android.Tests/snapshots/android/VerifyMaximumDateInRuntime.png) is included in this PR. The first CI run on Windows and MacCatalyst will create baseline snapshots, but this means those platforms will show as "new baseline" runs without a proper pre-verified baseline.
Recommendation: Either:
- Add Windows and MacCatalyst snapshots to
TestCases.Windows.Tests/snapshots/windows/and the MacCatalyst equivalent, OR - Restrict the
VerifyScreenshot()call to Android only (likeIssue19256.csdoes with#if ANDROID)
4. Indentation Inconsistency in Issue33583 Files
Files:
src/Controls/tests/TestCases.HostApp/Issues/Issue33583.cssrc/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33583.cs
Problem: Both files use 4-space indentation while the MAUI codebase and the companion Issue19256.cs files use tabs. This is inconsistent with codebase conventions.
Recommendation: Run dotnet format on these files to align with the tab-based indentation used throughout the repository:
dotnet format Microsoft.Maui.sln --no-restore --exclude Templates/src --exclude-diagnostics CA18225. MapMinimumDate/MapMaximumDate: Dialog Dismissed Without User Interaction
File: src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs
Problem: If the user has the DatePicker dialog open and the app programmatically updates MinimumDate or MaximumDate, ResetDialog() will dismiss the open dialog without user action. This could be a UX regression in dynamic binding scenarios.
public static partial void MapMinimumDate(IDatePickerHandler handler, IDatePicker datePicker)
{
if (handler is DatePickerHandler platformHandler)
{
if (datePicker.MinimumDate is not null)
{
platformHandler.ResetDialog(); // ← Dialog closed mid-interaction if open
}
// ...
}
}Recommendation: Evaluate whether the dialog should be dismissed when open, or whether the min/max update should be deferred until the dialog is next opened. At minimum, this behavior should be documented in a comment.
✅ Looks Good
- Fix approach is sound: Forcing dialog re-creation is a clean workaround for Android's
DatePickerDialogcaching behavior. Compared to the "reset-before-set" approach (which manipulates internal state), recreation is more reliable. ResetDialog()refactor: Extracting the dialog teardown intoResetDialog()is a good cleanup that reduces duplication betweenDisconnectHandlerandMapMinimumDate/MapMaximumDate.ShowPickerDialogapplying constraints on creation: Applying min/max immediately after dialog creation inShowPickerDialogensures the constraints are always applied on a fresh dialog.- Issue19256 test: Well-structured two-screenshot verification correctly captures the before/after state of the min date constraint update.
VerifyScreenshotOrSetExceptionpattern in Issue19256: Correctly ensures the dialog is dismissed viaApp.Tap("Cancel")before propagating a test failure.- NOTE block present in PR description.
Pre-existing Issue (Not Introduced by This PR)
File: src/Core/src/Platform/Android/DatePickerExtensions.cs (line ~58)
public static void UpdateMaximumDate(this MauiDatePicker platformDatePicker, IDatePicker datePicker)
{
platformDatePicker.UpdateMinimumDate(datePicker, null); // ← Bug: calls UpdateMinimumDate, not UpdateMaximumDate
}The single-parameter UpdateMaximumDate overload calls UpdateMinimumDate instead of itself. This is a pre-existing copy-paste bug. Since this PR's fix bypasses these no-dialog overloads entirely (by recreating the dialog), it doesn't affect correctness here. Should be fixed in a separate PR.
|
Since this fix does not resolve the related issue #33583, the fix has been modified to address both issues. The solution forces the recreation of the DatePickerDialog whenever MinimumDate or MaximumDate changes by nulling out the _dialog field in the MapMinimumDate and MapMaximumDate handlers. This ensures that a fresh dialog instance is created the next time it is shown, completely bypassing Android’s internal caching behavior. |
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Issue Details
Behavior:
MinimumDate/MaximumDate→ Works correctlyMinimumDate/MaximumDate→ Fails to updateRoot Cause of the issue
Reference: StackOverflow
Description of Change
Android DatePicker Minimum/Maximum Date Update Fix:
Framework Fixes:
DatePickerHandler.Android.cs, a handler is added for the dialog'sShowEventto always re-apply the minimum and maximum dates after the dialog is initialized, ensuring changes are respected even when the dialog is reused.DatePickerExtensions.cs, bothUpdateMinimumDateandUpdateMaximumDatemethods now reset the underlying Android values (MinDateto 0,MaxDatetolong.MaxValue) before setting the new constraint, forcing Android to invalidate its cached values and accept updates. [1] [2]Testing Improvements:
Issue19256.cs) to the host app that sets up two dependentDatePickercontrols and buttons to simulate the bug scenario and verify the fix.These changes collectively ensure that the Android
DatePickernow correctly updates its constraints in response to changes, matching expected behavior.Issues Fixed
Fixes #19256
Fixes #33583
Output
Before_fix.mov
After_fix.mov