Skip to content

[Android] Fix DatePicker MinimumDate/MaximumDate not updating dynamically#33687

Open
HarishwaranVijayakumar wants to merge 8 commits intodotnet:mainfrom
HarishwaranVijayakumar:fix-19256
Open

[Android] Fix DatePicker MinimumDate/MaximumDate not updating dynamically#33687
HarishwaranVijayakumar wants to merge 8 commits intodotnet:mainfrom
HarishwaranVijayakumar:fix-19256

Conversation

@HarishwaranVijayakumar
Copy link
Contributor

@HarishwaranVijayakumar HarishwaranVijayakumar commented Jan 23, 2026

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

  • Android DatePicker minimum/maximum date constraints don't update properly when changed dynamically. In a dependent DatePicker scenario (LEFT picker's date sets RIGHT picker's minimum), the RIGHT picker fails to respect the updated minimum date constraint.

Behavior:

  • First time setting MinimumDate/MaximumDate → Works correctly
  • Second time (or subsequent times) setting MinimumDate/MaximumDate → Fails to update

Root Cause of the issue

  • Android's DatePicker internally caches MinDate/MaxDate values and ignores subsequent setMinDate()/setMaxDate() calls unless the values are first reset.

Reference: StackOverflow

Description of Change

Android DatePicker Minimum/Maximum Date Update Fix:

  • Framework Fixes:

    • In DatePickerHandler.Android.cs, a handler is added for the dialog's ShowEvent to always re-apply the minimum and maximum dates after the dialog is initialized, ensuring changes are respected even when the dialog is reused.
    • In DatePickerExtensions.cs, both UpdateMinimumDate and UpdateMaximumDate methods now reset the underlying Android values (MinDate to 0, MaxDate to long.MaxValue) before setting the new constraint, forcing Android to invalidate its cached values and accept updates. [1] [2]
  • Testing Improvements:

    • Added a new UI test page (Issue19256.cs) to the host app that sets up two dependent DatePicker controls and buttons to simulate the bug scenario and verify the fix.
    • Added a corresponding NUnit UI test that automates the scenario and uses screenshots to verify that the minimum date updates dynamically as expected.

These changes collectively ensure that the Android DatePicker now correctly updates its constraints in response to changes, matching expected behavior.

Issues Fixed

Fixes #19256
Fixes #33583

Output

Before After
Before_fix.mov
After_fix.mov

@dotnet-policy-service dotnet-policy-service bot added the community ✨ Community Contribution label Jan 23, 2026
@dotnet-policy-service
Copy link
Contributor

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.

@dotnet-policy-service dotnet-policy-service bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Jan 23, 2026
@sheiksyedm sheiksyedm marked this pull request as ready for review January 27, 2026 11:23
Copilot AI review requested due to automatic review settings January 27, 2026 11:23
@sheiksyedm
Copy link
Contributor

/azp run maui-pr-uitests 

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@PureWeen
Copy link
Member

/rebase

@PureWeen
Copy link
Member

/azp run maui-pr-uitests, maui-pr-devicetests

@azure-pipelines
Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@PureWeen
Copy link
Member

PureWeen commented Feb 2, 2026

/rebase

Copy link
Member

@PureWeen PureWeen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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:

  1. UpdateMinimumDate(2023) → sets MinDate = 2023
  2. 💥 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.

@HarishwaranVijayakumar
Copy link
Contributor Author

🤖 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:

  1. UpdateMinimumDate(2023) → sets MinDate = 2023
  2. 💥 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.

@kubaflo
Copy link
Contributor

kubaflo commented Feb 5, 2026

🤖 AI Summary

📊 Expand Full Review
🔍 Pre-Flight — Context & Validation
📝 Review SessionModified the fix which resolve the issue 33583 · 43fc76c

Issue: #19256 - [Android] DatePicker control minimum date issue
Also Fixes: #33583 - DatePicker does not update MinimumDate / MaximumDate in the Popup after first opening
Platforms Affected: Android only
Labels: platform/android, area-controls-datetimepicker, , partner/syncfusion community

Issue Summary

Android's DatePicker internally caches MinDate/MaxDate values and ignores subsequent setMinDate()/setMaxDate() calls unless the values are first reset. This manifests in dependent DatePicker scenarios (e.g., LEFT picker sets RIGHT picker's minimum), where the RIGHT picker fails to update its constraints after the first set.

Root Cause: Known Android platform limitation (10+ years per StackOverflow). The dialog caches min/max values and won't accept the same range-direction values without a reset.

Fix Approach (Final State)

The author went through several iterations following PureWeen's review:

  • Original approach: Reset MinDate=0 / MaxDate=long.MaxValue in extensions + ShowEvent handler to re-apply constraints
  • After PureWeen CHANGES_REQUESTED: Switched to ResetDialog() approach - dialog is recreated when MinimumDate/MaximumDate changes

Current approach in DatePickerHandler.Android.cs:
ResetDialog(), else call UpdateMinimumDate(null)
ResetDialog(), else call UpdateMaximumDate(null)

  • ResetDialog() dismisses and nulls _dialog, so next ShowPickerDialog call creates a fresh one with correct constraints

Files Changed

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###

  1. .github/agent-pr-session/pr-19256.md - Agent session file committed to repo (should not be included)
  2. Test Issue19256.cs uses #if ANDROID preprocessor (wraps entire class) - acceptable given Android-only issue
  3. Test Issue33583.cs is #if !IOS - tests Android, Windows, MacCatalyst, but snapshot only for Android

🚦 Gate — Test Verification
📝 Review SessionModified 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 SessionModified 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 SessionModified 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: Call ResetDialog() when a non-null constraint is set, nulling _dialog
  • ResetDialog(): 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, both UpdateMinimumDate and UpdateMaximumDate methods now reset the underlying Android values (MinDate to 0, MaxDate to long.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 _dialog in 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:

  1. When MapMinimumDate or MapMaximumDate is called (triggered by property changes), the existing _dialog is dismissed and set to null
  2. The next time ShowPickerDialog is called, it detects _dialog is null and creates a fresh DatePickerDialog instance
  3. Immediately after creating the new dialog, we apply the current min/max constraints via UpdateMinimumDate and UpdateMaximumDate
  4. 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:

  1. Add a _dialogNeedsRefresh bool field to handler
  2. In MapMinimumDate/MapMaximumDate, set _dialogNeedsRefresh = true (don't touch dialog)
  3. In ShowPickerDialog, if _dialogNeedsRefresh is true, null out dialog and create fresh
  4. 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:

  1. 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
  2. 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
  3. 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:

  1. Use Android.OS.Handler with main looper to post constraint updates
  2. Constraints are applied asynchronously in the next message loop iteration
  3. 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:

  1. 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.

  2. Reset+Apply synchronously works partially: Setting MinDate = long.MinValue followed by MinDate = newValue does 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.

  3. 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:

  • MapMinimumDate and MapMaximumDate now call ResetDialog() 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.
  • ShowPickerDialog applies min/max constraints immediately after creating a new dialog.
  • Introduced a ResetDialog() helper method (also used by DisconnectHandler) to centralize dialog teardown logic.

DatePickerExtensions.cs is not modified.

Testing:

  • Issue19256.cs (HostApp + NUnit): Two-DatePicker dependent scenario — LEFT picker sets RIGHT picker's MinimumDate. 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, change MaximumDate via button, re-open picker. Verifies updated constraint appears in the dialog (Android, Windows, MacCatalyst).

Issues Fixed

Fixes #19256
Fixes #33583

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 (like Issue19256.cs does with #if ANDROID)

4. Indentation Inconsistency in Issue33583 Files

Files:

  • src/Controls/tests/TestCases.HostApp/Issues/Issue33583.cs
  • src/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 CA1822

5. 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 DatePickerDialog caching behavior. Compared to the "reset-before-set" approach (which manipulates internal state), recreation is more reliable.
  • ResetDialog() refactor: Extracting the dialog teardown into ResetDialog() is a good cleanup that reduces duplication between DisconnectHandler and MapMinimumDate/MapMaximumDate.
  • ShowPickerDialog applying constraints on creation: Applying min/max immediately after dialog creation in ShowPickerDialog ensures 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.
  • VerifyScreenshotOrSetException pattern in Issue19256: Correctly ensures the dialog is dismissed via App.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.


@HarishwaranVijayakumar
Copy link
Contributor Author

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.

@dotnet dotnet deleted a comment from kubaflo Feb 18, 2026
@rmarinho rmarinho added s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-lose AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Feb 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-controls-datetimepicker DatePicker, TimePicker community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/android s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-lose AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

6 participants

Comments