Skip to content

[Windows] Fix Image layout inconsistency caused by async decode race in GetDesiredSize - #34699

Merged
kubaflo merged 7 commits into
dotnet:inflight/currentfrom
praveenkumarkarunanithi:fix-32393
May 12, 2026
Merged

[Windows] Fix Image layout inconsistency caused by async decode race in GetDesiredSize#34699
kubaflo merged 7 commits into
dotnet:inflight/currentfrom
praveenkumarkarunanithi:fix-32393

Conversation

@praveenkumarkarunanithi

@praveenkumarkarunanithi praveenkumarkarunanithi commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

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

PR #30936 introduced logic in GetDesiredSize() to cap the Image control's desired size using BitmapSource.PixelWidth and BitmapSource.PixelHeight for correct AspectFit alignment. On Windows, image decoding is asynchronous — pixel dimensions start at 0 and are populated only after background decoding completes.

This creates a race condition: if GetDesiredSize() runs before decoding finishes, GetImageSize() returns 0 and size capping is skipped; if it runs after, it reads actual dimensions (e.g., 512×512) and applies capping. Window state affects this timing — maximized windows trigger layout after decode, while minimized/restored windows trigger it before — resulting in inconsistent, timing-dependent layout behavior.

Sample-Side Issue

The reproduction sample amplifies the issue by using ContentPage.Width/Height for crop ratio calculations instead of the Image control's own dimensions. Since the page aspect ratio varies with window state but the image is a fixed 1:1 ratio, the same crop coordinates produce distorted results (e.g., wide or flattened rectangles). Crop calculations must be relative to the Image control's rendered area, not the page.

Description of Change

A _cachedImageSize field is introduced in ImageHandler.Windows.cs with three targeted changes:

  • OnImageOpened — Stores the result of GetImageSize() into _cachedImageSize once decoding completes, but only when the decoded dimensions are greater than zero. This prevents a blank transitional BitmapImage from overwriting a previously valid cached size.

  • GetDesiredSize() — Reads from _cachedImageSize instead of calling GetImageSize() dynamically, eliminating the race condition where layout occurs before the asynchronous BitmapSource decode completes.

  • MapSourceAsync — Resets PlatformView.MaxWidth and PlatformView.MaxHeight to ∞ on source change. _cachedImageSize is intentionally not cleared here — preserving the previous image’s natural size during the transition ensures GetDesiredSize remains correctly constrained while the new image is still decoding.

The cache is populated once per image load (post-decode) and intentionally retained across source transitions, ensuring consistent size calculations regardless of window state or layout timing.

Issues Fixed

Fixes #32393

Tested the behaviour in the following platforms

  • Android
  • Windows
  • iOS
  • Mac

Note: This issue is Windows-specific (BitmapSource async decode is a WinUI behavior) and the fix is scoped to ImageHandler.Windows.cs only — no other platforms are affected.

Screenshots

Issue1

Before Fix After Fix
withoutfix withfix

Issue2

Before Fix After Fix
withoutfix withfix
@github-actions

github-actions Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34699

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34699"
@dotnet-policy-service dotnet-policy-service Bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Mar 27, 2026
@karthikraja-arumugam karthikraja-arumugam added the community ✨ Community Contribution label Mar 27, 2026
@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Mar 28, 2026
@praveenkumarkarunanithi praveenkumarkarunanithi changed the title [WIP] [Windows] Fix Image layout inconsistency caused by async decode race in GetDesiredSize Apr 2, 2026
@sheiksyedm
sheiksyedm marked this pull request as ready for review April 6, 2026 06:43
Copilot AI review requested due to automatic review settings April 6, 2026 06:43
@sheiksyedm

Copy link
Copy Markdown
Contributor

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

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a Windows-specific Image layout inconsistency caused by WinUI’s async image decode leaving BitmapSource.PixelWidth/PixelHeight at 0 during early layout passes, which made GetDesiredSize() timing-dependent.

Changes:

  • Add a Windows ImageHandler cached natural image size and use it in GetDesiredSize() for AspectFit.
  • Populate the cache on ImageOpened (post-decode) to avoid reading PixelWidth/PixelHeight during the race window.
  • Add Windows device tests covering post-decode constraints and the “source transition” race window.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/Core/src/Handlers/Image/ImageHandler.Windows.cs Introduces _cachedImageSize and uses it during GetDesiredSize() to remove timing dependence on WinUI decode completion.
src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.Windows.cs Adds regression tests validating max-constraint behavior after decode and desired-size behavior during a simulated source transition.
Comment thread src/Core/src/Handlers/Image/ImageHandler.Windows.cs
Comment thread src/Core/src/Handlers/Image/ImageHandler.Windows.cs
@MauiBot MauiBot added s/agent-review-incomplete and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues labels Apr 10, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 11, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 30, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Expert Review — 3 findings

See inline comments for details.

@dotnet dotnet deleted a comment from MauiBot May 1, 2026
@dotnet dotnet deleted a comment from MauiBot May 1, 2026
MauiBot
MauiBot previously requested changes May 1, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against #2 automatically generated candidates and selected try-fix-2 as the strongest fix.

Why: Try-Fix Candidate 2 (claude-sonnet-4.6) is the only empirically verified passing candidate (1977/2088 tests, 0 failures). It eliminates the _cachedImageSize field entirely in favor of calling VirtualView.InvalidateMeasure() from OnImageOpened after decode, fixing the unconditional UpdatePlatformMaxConstraints warning and rewriting the second test to use the real MapSourceAsync production path — directly addressing all three issues identified by code review.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (`try-fix-2`)
diff --git a/src/Core/src/Handlers/Image/ImageHandler.Windows.cs b/src/Core/src/Handlers/Image/ImageHandler.Windows.cs
index 3654c88dc9..4cf2d277ce 100644
--- a/src/Core/src/Handlers/Image/ImageHandler.Windows.cs
+++ b/src/Core/src/Handlers/Image/ImageHandler.Windows.cs
@@ -222,8 +222,20 @@ namespace Microsoft.Maui.Handlers
 			if (this.IsConnected())
 			{
 				UpdateValue(nameof(IImage.IsAnimationPlaying));
-				// Apply platform constraints when the decoded size is available
-				UpdatePlatformMaxConstraints();
+
+				// Only update constraints and trigger re-layout when the decode produced
+				// positive dimensions. A blank BitmapImage fires ImageOpened with PixelWidth=0;
+				// calling UpdatePlatformMaxConstraints unconditionally in that case would reset
+				// MaxWidth to infinity and undo any previously applied cap.
+				var sz = GetImageSize();
+				if (sz.Width > 0 && sz.Height > 0)
+				{
+					UpdatePlatformMaxConstraints();
+					// Request a new layout pass so GetDesiredSize uses the now-available
+					// pixel dimensions. Without this, the first (pre-decode) layout pass
+					// may have measured at zero-cap and the view would not self-correct.
+					VirtualView.InvalidateMeasure();
+				}
 			}
 		}
 
diff --git a/src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.Windows.cs b/src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.Windows.cs
index 748409a120..cf0d0ca204 100644
--- a/src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.Windows.cs
+++ b/src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.Windows.cs
@@ -67,11 +67,11 @@ namespace Microsoft.Maui.DeviceTests
 		}
 
 		// https://github.com/dotnet/maui/issues/32393
-		// GetDesiredSize must use _cachedImageSize (set in OnImageOpened) during source transitions.
-		// The cache persists while a new source is decoding so layout stays capped to the previous
-		// natural size instead of expanding to fill the container (PixelWidth=0 while pending).
-		// The race window is frozen by replacing platformView.Source with a blank BitmapImage
-		// (PixelWidth=0 always) and setting platformView.Width so base.GetDesiredSize returns large.
+		// After a source change, MapSourceAsync resets MaxWidth/MaxHeight to infinity.
+		// Once decode completes, OnImageOpened must re-apply MaxWidth/MaxHeight caps
+		// and trigger InvalidateMeasure so the next layout pass uses the live pixel size.
+		// This test verifies that the full source-reload cycle correctly restores constraints
+		// and that GetDesiredSize returns the capped natural size after re-decode.
 		[Fact]
 		public async Task AspectFit_GetDesiredSize_DuringSourceTransition_UsesCachedNaturalSize()
 		{
@@ -90,7 +90,7 @@ namespace Microsoft.Maui.DeviceTests
 
 				await AttachAndRun(platformView, async () =>
 				{
-					// wait for initial decode so _cachedImageSize is populated
+					// wait for initial decode so OnImageOpened has fired
 					await image.WaitUntilDecoded();
 
 					var bitmapSource = platformView.Source as BitmapSource;
@@ -99,21 +99,26 @@ namespace Microsoft.Maui.DeviceTests
 					int naturalPixelHeight = bitmapSource.PixelHeight;
 					Assert.True(naturalPixelWidth > 0);
 
-					// simulate the race window: MaxWidth reset, source replaced with a
-					// blank BitmapImage (PixelWidth=0) before decode completes
-					const double largeConstraint = 2000d;
+					// Simulate source transition: MapSourceAsync clears MaxWidth/MaxHeight before reload
 					platformView.MaxWidth = double.PositiveInfinity;
 					platformView.MaxHeight = double.PositiveInfinity;
-					platformView.Width = largeConstraint;
-					platformView.Source = new BitmapImage();
 
-					Assert.Equal(0, ((BitmapImage)platformView.Source).PixelWidth);
+					// Reload the same source via MapSourceAsync (mirrors real usage).
+					// After the new decode, OnImageOpened must re-apply MaxWidth cap
+					// and call VirtualView.InvalidateMeasure().
+					await ImageHandler.MapSourceAsync(handler, image);
+					await image.WaitUntilDecoded();
+
+					// MaxWidth must be recapped to naturalPixelWidth by OnImageOpened
+					Assert.True(platformView.MaxWidth <= naturalPixelWidth + 0.5,
+						$"MaxWidth ({platformView.MaxWidth:F0}) should be recapped to PixelWidth ({naturalPixelWidth}) after source reload (issue #32393).");
 
-					var desiredSize = handler.GetDesiredSize(largeConstraint, largeConstraint);
+					// GetDesiredSize must return capped natural size via live pixel dimensions
+					var desiredSize = handler.GetDesiredSize(2000d, 2000d);
 					Assert.True(desiredSize.Width <= naturalPixelWidth + 0.5,
-						$"GetDesiredSize width={desiredSize.Width:F0} exceeds naturalPixelWidth={naturalPixelWidth} during source transition (issue #32393).");
+						$"GetDesiredSize width={desiredSize.Width:F0} exceeds naturalPixelWidth={naturalPixelWidth} after source reload (issue #32393).");
 					Assert.True(desiredSize.Height <= naturalPixelHeight + 0.5,
-						$"GetDesiredSize height={desiredSize.Height:F0} exceeds naturalPixelHeight={naturalPixelHeight} during source transition.");
+						$"GetDesiredSize height={desiredSize.Height:F0} exceeds naturalPixelHeight={naturalPixelHeight} after source reload.");
 				});
 			});
 		}
@dotnet dotnet deleted a comment from MauiBot May 3, 2026
@kubaflo
kubaflo dismissed MauiBot’s stale review May 3, 2026 02:14

Resetting for re-review

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against #1 automatically generated candidates and selected try-fix-1 as the strongest fix.

Why: Try-fix-1 (claude-opus-4.6) passes all 2088 device tests with a live-first + cache-fallback approach in GetDesiredSize that is strictly safer than the PR's cache-only approach. The PR description also contradicts the implementation (claims cache is NOT cleared in MapSourceAsync when it is), which is a documentation blocker. Try-fix-1 fixes both the live-first gap and lifecycle cleanup in DisconnectHandler.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (`try-fix-1`)
diff --git a/src/Core/src/Handlers/Image/ImageHandler.Windows.cs b/src/Core/src/Handlers/Image/ImageHandler.Windows.cs
index 3654c88dc9..dcabe40f30 100644
--- a/src/Core/src/Handlers/Image/ImageHandler.Windows.cs
+++ b/src/Core/src/Handlers/Image/ImageHandler.Windows.cs
@@ -10,6 +10,8 @@ namespace Microsoft.Maui.Handlers
 {
 	public partial class ImageHandler : ViewHandler<IImage, WImage>
 	{
+		private Graphics.Size _cachedImageSize;
+
 		/// <inheritdoc/>
 		protected override WImage CreatePlatformView() => new WImage();
 
@@ -26,6 +28,7 @@ namespace Microsoft.Maui.Handlers
 		{
 			platformView.ImageOpened -= OnImageOpened;
 			platformView.Loaded -= OnImageLoaded;
+			_cachedImageSize = default;
 
 			base.DisconnectHandler(platformView);
 			SourceLoader.Reset();
@@ -69,7 +72,12 @@ namespace Microsoft.Maui.Handlers
 			// unconstrained here and rely on layout constraints.
 			if (VirtualView.Aspect == Aspect.AspectFit)
 			{
+				// Live-first: use BitmapSource dimensions when available, fall back to
+				// cached size during source transitions where PixelWidth is still zero.
 				var imageSize = GetImageSize();
+				if (imageSize.Width <= 0 || imageSize.Height <= 0)
+					imageSize = _cachedImageSize;
+
 				double w = possibleSize.Width;
 				double h = possibleSize.Height;
 
@@ -205,11 +213,17 @@ namespace Microsoft.Maui.Handlers
 		/// <param name="image">The associated <see cref="Image"/> instance.</param>
 		public static Task MapSourceAsync(IImageHandler handler, IImage image)
 		{
-			// Reset platform caps so we don't keep stale values between sources
-			if (handler is ImageHandler ih && ih.PlatformView is not null)
+			// Reset the size cache so we don't keep stale values between sources.
+			// Clearing the cache here ensures a failed subsequent load (where OnImageOpened never fires)
+			// does not cap GetDesiredSize to the previous image's dimensions.
+			if (handler is ImageHandler ih)
 			{
-				ih.PlatformView.MaxWidth = double.PositiveInfinity;
-				ih.PlatformView.MaxHeight = double.PositiveInfinity;
+				ih._cachedImageSize = Graphics.Size.Zero;
+				if (ih.PlatformView is not null)
+				{
+					ih.PlatformView.MaxWidth = double.PositiveInfinity;
+					ih.PlatformView.MaxHeight = double.PositiveInfinity;
+				}
 			}
 
 			return handler.SourceLoader.UpdateImageSourceAsync();
@@ -221,8 +235,17 @@ namespace Microsoft.Maui.Handlers
 			// handler hasn't been disconnected
 			if (this.IsConnected())
 			{
+				// Cache decoded dimensions when positive; a blank BitmapImage
+				// (e.g. during source transitions) fires ImageOpened with PixelWidth=0,
+				// so we preserve the last-known-good size.
+				var sz = GetImageSize();
+				bool hasValidDimensions = sz.Width > 0 && sz.Height > 0;
+				if (hasValidDimensions)
+					_cachedImageSize = sz;
+
 				UpdateValue(nameof(IImage.IsAnimationPlaying));
-				// Apply platform constraints when the decoded size is available
+				// Let UpdatePlatformMaxConstraints decide via its own cache fallback
+				// whether constraints need updating.
 				UpdatePlatformMaxConstraints();
 			}
 		}
@@ -238,7 +261,12 @@ namespace Microsoft.Maui.Handlers
 
 			if (VirtualView.Aspect == Aspect.AspectFit)
 			{
+				// Use live decoded size when available; fall back to cache during source
+				// transitions so MaxWidth/MaxHeight are not reset to ∞ while a new image
+				// is still decoding (blank BitmapImage reports PixelWidth=0).
 				var sz = GetImageSize();
+				if (sz.Width <= 0 || sz.Height <= 0)
+					sz = _cachedImageSize;
 
 				// Width: cap to intrinsic only if horizontal alignment isn't Fill
 				if (VirtualView.HorizontalLayoutAlignment != Primitives.LayoutAlignment.Fill && sz.Width > 0)
@dotnet dotnet deleted a comment from MauiBot May 11, 2026
@MauiBot

MauiBot commented May 11, 2026

Copy link
Copy Markdown
Collaborator

🤖 AI Summary

👋 @praveenkumarkarunanithi — new AI review results are available. Please review the latest session below.

📊 Review Session0769eb7 · Update ImageHandler.Windows.cs · 2026-05-12 09:37 UTC
🚦 Gate — Test Before & After Fix

Gate Result: ❌ FAILED

Platform: ANDROID · Base: main · Merge base: f8cb875e

🩺 Test does not reproduce the bug — ran the same in both states (PASS without fix, PASS with fix). The repro test is not exercising the issue. Strengthen the test before reviewing the fix.

Test Without Fix (expect FAIL) With Fix (expect PASS)
📱 ImageHandlerTests (AspectFit_AfterDecode_PlatformMaxWidthIsConstrainedToNaturalPixelSize, AspectFit_GetDesiredSize_DuringSourceTransition_UsesCachedNaturalSize) Category=Image ❌ PASS — 497s ✅ PASS — 354s
🔴 Without fix — 📱 ImageHandlerTests (AspectFit_AfterDecode_PlatformMaxWidthIsConstrainedToNaturalPixelSize, AspectFit_GetDesiredSize_DuringSourceTransition_UsesCachedNaturalSize): PASS ❌ · 497s

(truncated to last 15,000 chars)

cluded test (filtered by Trait; 'Category':'RadioButton'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'WindowOverlay'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ActivityIndicator'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Border'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Button'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'CheckBox'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ContentView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Element'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'DatePicker'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Dispatcher'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Editor'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Entry'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'FlowDirection'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'FlyoutView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Fonts'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Graphics'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'GraphicsView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ImageButton'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ImageSource'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'IndicatorView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Label'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Layout'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Memory'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'NavigationPage'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Page'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Picker'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ProgressBar'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'RefreshView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ScrollView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.530  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'SearchBar'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.531  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ShapeView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.531  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Slider'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.531  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Stepper'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.531  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'SwipeView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.531  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Switch'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.531  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Formatting'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.531  7342  7372 I DOTNET  : [FILTER] Excluded test (filtered by Trait; 'Category':'TimePicker'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.531  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'View'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.531  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'WebView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.531  7342  7372 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Window'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:19:17.585  7342  7372 I DOTNET  : [Test environment: 64-bit .NET .NET 10.0 [collection-per-class, non-parallel]]
      05-11 14:19:17.585  7342  7372 I DOTNET  : [Test framework: xUnit.net 2.9.0.0]
      05-11 14:19:17.620  7342  7372 I DOTNET  : Test collection for Microsoft.Maui.DeviceTests.ImageHandlerTests
      05-11 14:19:17.746  7342  7372 I DOTNET  : 	[PASS] Native View Bounds are not empty
      05-11 14:19:17.764  7342  7372 I DOTNET  : 	[PASS] Native View Bounds are not empty
      05-11 14:19:18.284  7342  7508 I DOTNET  : 	[PASS] UpdatingSourceToNullClearsImage
      05-11 14:19:18.322  7342  7508 I DOTNET  : 	[PASS] ScaleX Initialize Correctly
      05-11 14:19:18.325  7342  7508 I DOTNET  : 	[PASS] ScaleX Initialize Correctly
      05-11 14:19:18.337  7342  7508 I DOTNET  : 	[PASS] Clip Initializes ContainerView Correctly
      05-11 14:19:18.341  7342  7508 I DOTNET  : 	[PASS] TranslationY Initialize Correctly
      05-11 14:19:18.348  7342  7508 I DOTNET  : 	[PASS] TranslationY Initialize Correctly
      05-11 14:19:19.500  7342  7585 I DOTNET  : 	[PASS] LoadDrawableAsyncReturnsWithSameImageAndDoesNotHang
      05-11 14:19:19.504  7342  7585 I DOTNET  : 	[PASS] DisconnectHandlerDoesntCrash
      05-11 14:19:19.965  7342  7612 I DOTNET  : 	[PASS] InterruptingLoadCancelsAndStartsOverWithChecks
      05-11 14:19:19.968  7342  7612 I DOTNET  : 	[PASS] MinimumWidthInitializes
      05-11 14:19:19.968  7342  7612 I DOTNET  : 	[PASS] MinimumWidthInitializes
      05-11 14:19:19.994  7342  7612 I DOTNET  : 	[PASS] Visibility is set correctly
      05-11 14:19:19.996  7342  7612 I DOTNET  : 	[PASS] Visibility is set correctly
      05-11 14:19:20.020  7342  7612 I DOTNET  : 	[PASS] View Renders To Image
      05-11 14:19:20.060  7342  7612 I DOTNET  : 	[PASS] View Renders To Image
      05-11 14:19:20.061  7342  7612 I DOTNET  : 	[PASS] HandlersHaveAllExpectedContructors
      05-11 14:19:20.064  7342  7612 I DOTNET  : 	[PASS] NeedsContainerWhenInputTransparent
      05-11 14:19:20.439  7342  7629 I DOTNET  : 	[PASS] ImageLoadSequenceIsCorrectWithChecks
      05-11 14:19:20.477  7342  7629 I DOTNET  : 	[PASS] PlatformView Transforms are not empty
      05-11 14:19:20.479  7342  7629 I DOTNET  : 	[PASS] PlatformView Transforms are not empty
      05-11 14:19:20.589  7342  7629 I DOTNET  : 	[PASS] ContainerView Remains If Shadow Mapper Runs Again
      05-11 14:19:20.592  7342  7629 I DOTNET  : 	[PASS] Native View Bounding Box is not empty
      05-11 14:19:20.593  7342  7629 I DOTNET  : 	[PASS] Native View Bounding Box is not empty
      05-11 14:19:20.595  7342  7629 I DOTNET  : 	[PASS] ScaleY Initialize Correctly
      05-11 14:19:20.596  7342  7629 I DOTNET  : 	[PASS] ScaleY Initialize Correctly
      05-11 14:19:20.904  7342  7641 I DOTNET  : 	[PASS] UpdatingSourceWorks
      05-11 14:19:20.954  7342  7646 I DOTNET  : 	[PASS] Semantic Heading is set correctly
      05-11 14:19:20.955  7342  7646 I DOTNET  : 	[IGNORED] AnimatedSourceInitializesCorrectly
      05-11 14:19:23.325  7342  7750 I DOTNET  : 	[PASS] UpdatingSourceUpdatesImageCorrectly
      05-11 14:19:25.678  7342  7778 I DOTNET  : 	[PASS] UpdatingSourceUpdatesImageCorrectly
      05-11 14:19:28.003  7342  7787 I DOTNET  : 	[PASS] UpdatingSourceUpdatesImageCorrectly
      05-11 14:19:28.229  7342  7793 I DOTNET  : 	[PASS] ImageLoadSequenceIsCorrect
      05-11 14:19:28.241  7342  7798 I DOTNET  : 	[PASS] Setting Semantic Description makes element accessible
      05-11 14:19:28.249  7342  7798 I DOTNET  : 	[PASS] Opacity is set correctly
      05-11 14:19:28.252  7342  7798 I DOTNET  : 	[PASS] Opacity is set correctly
      05-11 14:19:28.253  7342  7798 I DOTNET  : 	[IGNORED] SourceInitializesCorrectly
      05-11 14:19:28.256  7342  7798 I DOTNET  : 	[PASS] Rotation Initialize Correctly
      05-11 14:19:28.259  7342  7798 I DOTNET  : 	[PASS] Rotation Initialize Correctly
      05-11 14:19:28.482  7342  7804 I DOTNET  : 	[PASS] InterruptingLoadCancelsAndStartsOver
      05-11 14:19:28.634  7342  7809 I DOTNET  : 	[PASS] InitializingSourceOnlyUpdatesImageOnce
      05-11 14:19:28.813  7342  7814 I DOTNET  : 	[PASS] ContainerView Adds And Removes
      05-11 14:19:28.877  7342  7819 I DOTNET  : 	[PASS] Automation Id is set correctly
      05-11 14:19:28.923  7342  7819 I DOTNET  : 	[PASS] FlowDirection is set correctly
      05-11 14:19:28.926  7342  7819 I DOTNET  : 	[PASS] FlowDirection is set correctly
      05-11 14:19:29.050  7342  7824 I DOTNET  : 	[PASS] LoadDrawableAsyncWithFileLoadsFileInsteadOfResource
      05-11 14:19:29.197  7342  7830 I DOTNET  : 	[PASS] LoadDrawableAsyncWithFileLoadsFileInsteadOfResource
      05-11 14:19:29.331  7342  7835 I DOTNET  : 	[PASS] LoadDrawableAsyncWithFileLoadsFileInsteadOfResource
      05-11 14:19:29.355  7342  7840 I DOTNET  : 	[PASS] TranslationX Initialize Correctly
      05-11 14:19:29.357  7342  7840 I DOTNET  : 	[PASS] TranslationX Initialize Correctly
      05-11 14:19:29.530  7342  7845 I DOTNET  : 	[PASS] InitializingNullSourceOnlyUpdatesNull
      05-11 14:19:29.645  7342  7850 I DOTNET  : 	[PASS] InitializingNullSourceOnlyUpdatesNull
      05-11 14:19:29.762  7342  7855 I DOTNET  : 	[PASS] InitializingNullSourceOnlyUpdatesNull
      05-11 14:19:29.996  7342  7860 I DOTNET  : 	[PASS] UpdatingSourceOnlyUpdatesImageOnce
      05-11 14:19:29.998  7342  7860 I DOTNET  : 	[PASS] Null Semantics Doesnt throw exception
      05-11 14:19:29.999  7342  7860 I DOTNET  : 	[IGNORED] Semantic Description is set correctly
      05-11 14:19:30.367  7342  7865 I DOTNET  : 	[PASS] UpdatingSourceToNonexistentSourceClearsImage
      05-11 14:19:30.370  7342  7865 I DOTNET  : 	[PASS] MinimumHeightInitializes
      05-11 14:19:30.371  7342  7865 I DOTNET  : 	[PASS] MinimumHeightInitializes
      05-11 14:19:30.374  7342  7865 I DOTNET  : 	[PASS] RotationY Initialize Correctly
      05-11 14:19:30.397  7342  7865 I DOTNET  : 	[PASS] RotationY Initialize Correctly
      05-11 14:19:30.402  7342  7865 I DOTNET  : 	[PASS] RotationX Initialize Correctly
      05-11 14:19:30.408  7342  7865 I DOTNET  : 	[PASS] RotationX Initialize Correctly
      05-11 14:19:30.408  7342  7865 I DOTNET  : 	[IGNORED] InvalidSourceFailsToLoad
      05-11 14:19:30.410  7342  7865 I DOTNET  : 	[PASS] Setting Semantic Hint makes element accessible
      05-11 14:19:30.420  7342  7865 I DOTNET  : 	[PASS] AspectInitializesCorrectly
      05-11 14:19:30.422  7342  7865 I DOTNET  : 	[PASS] AspectInitializesCorrectly
      05-11 14:19:30.423  7342  7865 I DOTNET  : 	[IGNORED] Semantic Hint is set correctly
      05-11 14:19:30.426  7342  7865 I DOTNET  : Microsoft.Maui.DeviceTests.ImageHandlerTests 12.4284511 ms
      05-11 14:19:30.453  7342  7372 I DOTNET  : Xml file was written to the provided writer.
      05-11 14:19:30.453  7342  7372 I DOTNET  : Tests run: 1323 Passed: 85 Inconclusive: 0 Failed: 0 Ignored: 1233
�[40m�[32minfo�[39m�[22m�[49m: <<XHARNESS_RESULT_START>>
      {
        "version": 1,
        "machineName": "runnervmfz4j5",
        "exitCode": 0,
        "exitCodeName": "SUCCESS",
        "platform": "android",
        "instrumentationExitCode": 0,
        "device": "emulator-5554",
        "deviceOsVersion": "API 30",
        "architecture": "x86_64",
        "files": [
          {
            "name": "testResults.xml",
            "type": "test-results"
          },
          {
            "name": "adb-logcat-com.microsoft.maui.core.devicetests-default.log",
            "type": "logcat"
          }
        ]
      }
      <<XHARNESS_RESULT_END>>
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.core.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26230.4/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.core.devicetests'
�[40m�[32minfo�[39m�[22m�[49m: Successfully uninstalled com.microsoft.maui.core.devicetests
XHarness exit code: 0
  Tests completed successfully

🟢 With fix — 📱 ImageHandlerTests (AspectFit_AfterDecode_PlatformMaxWidthIsConstrainedToNaturalPixelSize, AspectFit_GetDesiredSize_DuringSourceTransition_UsesCachedNaturalSize): PASS ✅ · 354s

(truncated to last 15,000 chars)

ight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'WindowOverlay'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ActivityIndicator'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Border'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Button'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'CheckBox'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ContentView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Element'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'DatePicker'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Dispatcher'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Editor'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Entry'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'FlowDirection'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'FlyoutView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Fonts'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Graphics'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'GraphicsView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ImageButton'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ImageSource'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'IndicatorView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Label'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Layout'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Memory'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'NavigationPage'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Page'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Picker'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ProgressBar'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'RefreshView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ScrollView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'SearchBar'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'ShapeView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Slider'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Stepper'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'SwipeView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Switch'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Formatting'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Excluded test (filtered by Trait; 'Category':'TimePicker'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'View'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'WebView'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.248  9819  9852 I DOTNET  : [FILTER] Included test (filtered by Trait; 'Category':'Window'): [TimePicker] Font Family and Weight Initializes Correctly
      05-11 14:24:57.274  9819  9852 I DOTNET  : [Test environment: 64-bit .NET .NET 10.0 [collection-per-class, non-parallel]]
      05-11 14:24:57.274  9819  9852 I DOTNET  : [Test framework: xUnit.net 2.9.0.0]
      05-11 14:24:57.293  9819  9852 I DOTNET  : Test collection for Microsoft.Maui.DeviceTests.ImageHandlerTests
      05-11 14:24:57.408  9819  9871 I DOTNET  : 	[PASS] Native View Bounds are not empty
      05-11 14:24:57.418  9819  9841 I DOTNET  : 	[PASS] Native View Bounds are not empty
      05-11 14:24:57.419  9819  9841 I DOTNET  : 	[PASS] Native View Bounds are not empty
      05-11 14:24:57.924  9819  9878 I DOTNET  : 	[PASS] UpdatingSourceToNullClearsImage
      05-11 14:24:57.942  9819  9878 I DOTNET  : 	[PASS] ScaleX Initialize Correctly
      05-11 14:24:57.945  9819  9878 I DOTNET  : 	[PASS] ScaleX Initialize Correctly
      05-11 14:24:57.952  9819  9878 I DOTNET  : 	[PASS] Clip Initializes ContainerView Correctly
      05-11 14:24:57.956  9819  9878 I DOTNET  : 	[PASS] TranslationY Initialize Correctly
      05-11 14:24:57.957  9819  9878 I DOTNET  : 	[PASS] TranslationY Initialize Correctly
      05-11 14:24:58.371  9819  9887 I DOTNET  : 	[PASS] LoadDrawableAsyncReturnsWithSameImageAndDoesNotHang
      05-11 14:24:58.416  9819  9892 I DOTNET  : 	[PASS] DisconnectHandlerDoesntCrash
      05-11 14:24:58.710  9819  9898 I DOTNET  : 	[PASS] InterruptingLoadCancelsAndStartsOverWithChecks
      05-11 14:24:58.713  9819  9898 I DOTNET  : 	[PASS] MinimumWidthInitializes
      05-11 14:24:58.714  9819  9898 I DOTNET  : 	[PASS] MinimumWidthInitializes
      05-11 14:24:58.722  9819  9898 I DOTNET  : 	[PASS] Visibility is set correctly
      05-11 14:24:58.722  9819  9898 I DOTNET  : 	[PASS] Visibility is set correctly
      05-11 14:24:58.736  9819  9898 I DOTNET  : 	[PASS] View Renders To Image
      05-11 14:24:58.743  9819  9898 I DOTNET  : 	[PASS] View Renders To Image
      05-11 14:24:58.745  9819  9898 I DOTNET  : 	[PASS] HandlersHaveAllExpectedContructors
      05-11 14:24:58.747  9819  9898 I DOTNET  : 	[PASS] NeedsContainerWhenInputTransparent
      05-11 14:24:58.968  9819  9904 I DOTNET  : 	[PASS] ImageLoadSequenceIsCorrectWithChecks
      05-11 14:24:58.989  9819  9904 I DOTNET  : 	[PASS] PlatformView Transforms are not empty
      05-11 14:24:58.991  9819  9904 I DOTNET  : 	[PASS] PlatformView Transforms are not empty
      05-11 14:24:59.000  9819  9904 I DOTNET  : 	[PASS] ContainerView Remains If Shadow Mapper Runs Again
      05-11 14:24:59.004  9819  9904 I DOTNET  : 	[PASS] Native View Bounding Box is not empty
      05-11 14:24:59.005  9819  9904 I DOTNET  : 	[PASS] Native View Bounding Box is not empty
      05-11 14:24:59.008  9819  9904 I DOTNET  : 	[PASS] ScaleY Initialize Correctly
      05-11 14:24:59.009  9819  9904 I DOTNET  : 	[PASS] ScaleY Initialize Correctly
      05-11 14:24:59.291  9819  9909 I DOTNET  : 	[PASS] UpdatingSourceWorks
      05-11 14:24:59.306  9819  9909 I DOTNET  : 	[PASS] Semantic Heading is set correctly
      05-11 14:24:59.307  9819  9909 I DOTNET  : 	[IGNORED] AnimatedSourceInitializesCorrectly
      05-11 14:25:01.697  9819  9917 I DOTNET  : 	[PASS] UpdatingSourceUpdatesImageCorrectly
      05-11 14:25:04.024  9819  9927 I DOTNET  : 	[PASS] UpdatingSourceUpdatesImageCorrectly
      05-11 14:25:06.344  9819  9932 I DOTNET  : 	[PASS] UpdatingSourceUpdatesImageCorrectly
      05-11 14:25:06.568  9819  9938 I DOTNET  : 	[PASS] ImageLoadSequenceIsCorrect
      05-11 14:25:06.572  9819  9938 I DOTNET  : 	[PASS] Setting Semantic Description makes element accessible
      05-11 14:25:06.590  9819  9938 I DOTNET  : 	[PASS] Opacity is set correctly
      05-11 14:25:06.595  9819  9938 I DOTNET  : 	[PASS] Opacity is set correctly
      05-11 14:25:06.595  9819  9938 I DOTNET  : 	[IGNORED] SourceInitializesCorrectly
      05-11 14:25:06.601  9819  9938 I DOTNET  : 	[PASS] Rotation Initialize Correctly
      05-11 14:25:06.604  9819  9938 I DOTNET  : 	[PASS] Rotation Initialize Correctly
      05-11 14:25:06.816  9819  9944 I DOTNET  : 	[PASS] InterruptingLoadCancelsAndStartsOver
      05-11 14:25:06.989  9819  9949 I DOTNET  : 	[PASS] InitializingSourceOnlyUpdatesImageOnce
      05-11 14:25:07.188  9819  9954 I DOTNET  : 	[PASS] ContainerView Adds And Removes
      05-11 14:25:07.254  9819  9954 I DOTNET  : 	[PASS] Automation Id is set correctly
      05-11 14:25:07.314  9819  9954 I DOTNET  : 	[PASS] FlowDirection is set correctly
      05-11 14:25:07.315  9819  9954 I DOTNET  : 	[PASS] FlowDirection is set correctly
      05-11 14:25:07.452  9819  9959 I DOTNET  : 	[PASS] LoadDrawableAsyncWithFileLoadsFileInsteadOfResource
      05-11 14:25:07.616  9819  9964 I DOTNET  : 	[PASS] LoadDrawableAsyncWithFileLoadsFileInsteadOfResource
      05-11 14:25:07.770  9819  9969 I DOTNET  : 	[PASS] LoadDrawableAsyncWithFileLoadsFileInsteadOfResource
      05-11 14:25:07.801  9819  9974 I DOTNET  : 	[PASS] TranslationX Initialize Correctly
      05-11 14:25:07.802  9819  9974 I DOTNET  : 	[PASS] TranslationX Initialize Correctly
      05-11 14:25:07.915  9819  9979 I DOTNET  : 	[PASS] InitializingNullSourceOnlyUpdatesNull
      05-11 14:25:08.034  9819  9984 I DOTNET  : 	[PASS] InitializingNullSourceOnlyUpdatesNull
      05-11 14:25:08.148  9819  9989 I DOTNET  : 	[PASS] InitializingNullSourceOnlyUpdatesNull
      05-11 14:25:08.384  9819  9994 I DOTNET  : 	[PASS] UpdatingSourceOnlyUpdatesImageOnce
      05-11 14:25:08.386  9819  9994 I DOTNET  : 	[PASS] Null Semantics Doesnt throw exception
      05-11 14:25:08.386  9819  9994 I DOTNET  : 	[IGNORED] Semantic Description is set correctly
      05-11 14:25:08.759  9819  9999 I DOTNET  : 	[PASS] UpdatingSourceToNonexistentSourceClearsImage
      05-11 14:25:08.764  9819  9999 I DOTNET  : 	[PASS] MinimumHeightInitializes
      05-11 14:25:08.770  9819  9999 I DOTNET  : 	[PASS] MinimumHeightInitializes
      05-11 14:25:08.772  9819  9999 I DOTNET  : 	[PASS] RotationY Initialize Correctly
      05-11 14:25:08.775  9819  9999 I DOTNET  : 	[PASS] RotationY Initialize Correctly
      05-11 14:25:08.778  9819  9999 I DOTNET  : 	[PASS] RotationX Initialize Correctly
      05-11 14:25:08.781  9819  9999 I DOTNET  : 	[PASS] RotationX Initialize Correctly
      05-11 14:25:08.782  9819  9999 I DOTNET  : 	[IGNORED] InvalidSourceFailsToLoad
      05-11 14:25:08.783  9819  9999 I DOTNET  : 	[PASS] Setting Semantic Hint makes element accessible
      05-11 14:25:08.794  9819  9999 I DOTNET  : 	[PASS] AspectInitializesCorrectly
      05-11 14:25:08.796  9819  9999 I DOTNET  : 	[PASS] AspectInitializesCorrectly
      05-11 14:25:08.796  9819  9999 I DOTNET  : 	[IGNORED] Semantic Hint is set correctly
      05-11 14:25:08.801  9819  9999 I DOTNET  : Microsoft.Maui.DeviceTests.ImageHandlerTests 11.2525367 ms
      05-11 14:25:08.833  9819  9852 I DOTNET  : Xml file was written to the provided writer.
      05-11 14:25:08.833  9819  9852 I DOTNET  : Tests run: 1323 Passed: 85 Inconclusive: 0 Failed: 0 Ignored: 1233
�[40m�[32minfo�[39m�[22m�[49m: <<XHARNESS_RESULT_START>>
      {
        "version": 1,
        "machineName": "runnervmfz4j5",
        "exitCode": 0,
        "exitCodeName": "SUCCESS",
        "platform": "android",
        "instrumentationExitCode": 0,
        "device": "emulator-5554",
        "deviceOsVersion": "API 30",
        "architecture": "x86_64",
        "files": [
          {
            "name": "testResults.xml",
            "type": "test-results"
          },
          {
            "name": "adb-logcat-com.microsoft.maui.core.devicetests-default.log",
            "type": "logcat"
          }
        ]
      }
      <<XHARNESS_RESULT_END>>
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.core.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26230.4/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.core.devicetests'
�[40m�[32minfo�[39m�[22m�[49m: Successfully uninstalled com.microsoft.maui.core.devicetests
XHarness exit code: 0
  Tests completed successfully

⚠️ Failure Details

  • ImageHandlerTests (AspectFit_AfterDecode_PlatformMaxWidthIsConstrainedToNaturalPixelSize, AspectFit_GetDesiredSize_DuringSourceTransition_UsesCachedNaturalSize) PASSED without fix (should fail) — tests don't catch the bug
📁 Fix files reverted (1 files)
  • src/Core/src/Handlers/Image/ImageHandler.Windows.cs

🧪 UI Tests — ViewBaseTests

Detected UI test categories: ViewBaseTests

OK Deep UI tests - 118 passed, 0 failed

Category Tests
controls-ViewBaseTests 118/119 pass

🔍 Pre-Flight — Context & Validation

Pre-Flight — PR #34699

Title: [Windows] Fix Image layout inconsistency caused by async decode race in GetDesiredSize
Issue: #32393 — Image cropping produces inconsistent results when window is minimized or resized (Windows)
Author: @praveenkumarkarunanithi · Partner: Syncfusion · Milestone: .NET 10 SR6
Labels: platform/windows, area-controls-image, partner/syncfusion, s/agent-reviewed, s/agent-changes-requested, s/agent-fix-win
Base: main 720a9d4a · Head: 0769eb74 · 7 commits, +117/-4, 2 files

Bug

ImageHandler.Windows.GetDesiredSize (introduced in #30936) caps the AspectFit desired size by BitmapSource.PixelWidth/PixelHeight. On WinUI, BitmapSource is decoded asynchronously and the pixel dimensions are 0 until decode completes. Whether GetDesiredSize runs before or after decode depends on window state (minimized/maximized), producing visually inconsistent layout/crop results across runs — a regression from .NET 10 RC1 → RC2.

Fix

Adds a _cachedImageSize field in ImageHandler.Windows.cs:

  1. OnImageOpened populates the cache from GetImageSize() (only when both dimensions > 0), so transitional blank BitmapImage events do not clobber a valid size.
  2. GetDesiredSize reads the cache instead of GetImageSize() — eliminating the timing-dependent path.
  3. MapSourceAsync clears the cache and resets MaxWidth/MaxHeight to ∞ when source changes (so a failed subsequent load can’t cap to the previous image’s dimensions).
  4. UpdatePlatformMaxConstraints uses the live decoded size first, falling back to the cache when 0 — keeping MaxWidth/MaxHeight capped during source transitions.

Adds two Windows-only device tests in ImageHandlerTests.Windows.cs:

  • AspectFit_AfterDecode_PlatformMaxWidthIsConstrainedToNaturalPixelSize
  • AspectFit_GetDesiredSize_DuringSourceTransition_UsesCachedNaturalSize

Files changed

  • src/Core/src/Handlers/Image/ImageHandler.Windows.cs (+25/-2)
  • src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.Windows.cs (+92/-2)

Prior review feedback (already addressed in current diff)

  • copilot-pull-request-reviewer requested clearing the cache on source change → addressed in MapSourceAsync.
  • Same reviewer requested fallback in UpdatePlatformMaxConstraints when sz==0 → addressed.

Gate result

Gate FAILED on Android, but this PR is Windows-only — the device test class is partial-Windows and lives in Handlers/Image/ImageHandlerTests.Windows.cs, so it never compiles for Android. The gate result is non-signal for this PR; verification requires a Windows runner.

Risk / Scope

  • Pure Windows handler change. No public API. No XAML / shared layout code touched.
  • Behavioral change: the cache is intentionally kept across source transitions in UpdatePlatformMaxConstraints (fallback path) so MaxWidth/MaxHeight stay capped while a new image is decoding; this can briefly cap to the previous image's natural size before the new one decodes — acceptable per PR description.
  • One subtle concern: _cachedImageSize is now reset to zero on every MapSourceAsync call, but the fallback in UpdatePlatformMaxConstraints is dependent on this — if the new image fails to load entirely, MaxWidth/MaxHeight stay at VirtualView.Maximum* (effectively ∞), which is the pre-PR behavior. OK.

Verifiability in this environment

Linux host — Windows handler tests cannot be executed here. Review is code-only.


🔧 Fix — Analysis & Comparison

Try-Fix Aggregate Narrative — PR #34699

Four independent candidate fixes were generated against PR #34699 (issue #32393). Each was grounded in a different maui-expert-reviewer dimension and produced as a sandbox-only diff. Windows TFM build was not available on the review host (Linux), so none of the candidates were validated by execution — all evaluation is code-only.

try-fix-1 — Handler-lifecycle (InvalidateMeasure from OnImageOpened)

Idea: drop _cachedImageSize entirely. After WinUI decode completes, OnImageOpened calls (VirtualView as IView)?.InvalidateMeasure(), forcing the layout engine to re-measure with BitmapSource.PixelWidth/Height now populated. A tiny _lastInvalidatedSize guard de-duplicates redundant invalidates from animated/multi-frame sources.
Differs from PR: no managed mirror of pixel size; one extra measure pass per decode.
Risk: an additional layout cycle per image load; first measure may briefly report uncapped size before the second pass fixes it (one-frame flash possible).

try-fix-2 — Async/threading (await decode inside MapSourceAsync)

Idea: after SourceLoader.UpdateImageSourceAsync() returns, if PixelWidth==0, await a TaskCompletionSource<bool> driven by BitmapImage.ImageOpened/ImageFailed (post-subscribe re-check, RunContinuationsAsynchronously), then UpdatePlatformMaxConstraints + InvalidateMeasure. No cache field; GetDesiredSize reads live.
Differs from PR: pushes the wait into source-loading instead of guarding measure-time reads.
Risk: increased latency of MapSourceAsync completion (now blocks on decode); behavioral change observable to consumers that await MapSourceAsync; source-swap during decode requires careful awaiter ownership (addressed in candidate). Larger diff (≈19 KB), wider regression surface.

try-fix-3 — Performance / measure (trust WinUI's native AspectFit)

Idea: remove the AspectFit cap from GetDesiredSize entirely. WinUI Image.Stretch=Uniform already performs AspectFit and base.GetDesiredSize returns the platform Image's measured desired size honoring MaxWidth/MaxHeight (which UpdatePlatformMaxConstraints already sets correctly post-decode). The managed cap added in #30936 is redundant given the WinUI MeasureOverride semantics.
Differs from PR: deletes the GetDesiredSize AspectFit branch outright.
Risk: highest. PR test AspectFit_GetDesiredSize_DuringSourceTransition_UsesCachedNaturalSize would fail (no cache, no managed cap). #30936's alignment scenario could regress if MaxWidth/MaxHeight arrive late. Aggressive refactor; not minimum-surface.

try-fix-4 — Regression patterns (smallest surgical fix: revert + InvalidateMeasure)

Idea: revert ImageHandler.Windows.cs to origin/main (PR #30936 state) and add exactly one line — VirtualView?.InvalidateMeasure() — at the end of OnImageOpened. Net: 1 functional line, 7 comment lines.
Differs from PR: no new field, no behavioral change beyond completing the state-machine edge ("decode complete → re-measure") that #30936 forgot.
Risk: same as try-fix-1 (extra measure pass) but with much smaller code footprint. The PR's AspectFit_GetDesiredSize_DuringSourceTransition_UsesCachedNaturalSize test would FAIL because it deliberately replaces Source with a blank BitmapImage and asserts the cache fallback; the test would need to be rewritten (or removed) and replaced with one that asserts InvalidateMeasure is invoked after decode.

Common limitations

  • Windows TFM cannot be built on the Linux review host (NETSDK1100). No candidate has empirical test coverage from this run.
  • The PR's own device tests would need to be reviewed/adapted for try-fix-1, -2, -3, -4 because they assert against the cache-fallback semantics that those candidates remove.

📋 Report — Final Recommendation

Comparative Report — PR #34699 (issue #32393)

Bug: On Windows, ImageHandler.GetDesiredSize caps AspectFit desired size by BitmapSource.PixelWidth/PixelHeight. WinUI decodes images asynchronously, so those values are 0 until decode completes. Whether decode races layout depends on window state (minimized/maximized) — producing inconsistent crops/layout. The capping was introduced by PR #30936; the regression was first observed in 10.0.0-rc.2.

Review host limitation: Linux, no Windows TFM (NETSDK1100). No candidate could be executed. Comparison is code-only. The provided gate run was on Android, which does not compile this Windows-only handler — the failed gate is non-signal.

Candidates

# Candidate Approach LOC vs main New state Existing PR tests still pass? Risk
1 pr Cache _cachedImageSize in OnImageOpened; GetDesiredSize reads cache; UpdatePlatformMaxConstraints reads live with cache fallback; MapSourceAsync resets cache + caps. +25/-2 1 field Low — small surface, scoped to handler, deterministic post-first-decode. Subtle window between source-swap and first decode where GetDesiredSize returns uncapped (cache=0).
2 pr-plus-reviewer PR + 3 mechanical hardening fixes: (a) reset _cachedImageSize in DisconnectHandler for connect/disconnect symmetry; (b) de-dup GetImageSize() calls in OnImageOpened/UpdatePlatformMaxConstraints via overload; (c) strengthen test to assert desiredSize.Width < largeConstraint. +30/-3 1 field, 1 overload ✅ + stricter Lowest — strictly an improvement over pr. No behavioral change for the user-visible scenario.
3 try-fix-1 Drop cache; OnImageOpened calls InvalidateMeasure() on VirtualView; small _lastInvalidatedSize guard to de-dup animated-image events. +30/-25 1 field (de-dup guard) ❌ — AspectFit_GetDesiredSize_DuringSourceTransition_UsesCachedNaturalSize asserts cache fallback and would fail Medium — adds extra measure pass per decode; one-frame uncapped flash possible. Mirrors patterns in GraphicsViewHandler.Windows / TimePickerHandler.Windows.
4 try-fix-2 Drop cache; MapSourceAsync awaits BitmapImage.ImageOpened/ImageFailed via TCS before completing. GetDesiredSize reads live. +120/-15 TCS-driven awaiter ❌ (cache test would need rewrite) Medium-high — largest diff (~19 KB). Behavioral change to MapSourceAsync completion timing; deadlock risk if any caller blocks UI thread (none currently). Heavier rework of source-loading path.
5 try-fix-3 Remove the AspectFit cap from GetDesiredSize entirely. Trust WinUI Image.MeasureOverride + MaxWidth/MaxHeight (which UpdatePlatformMaxConstraints already sets). -25 (net) none ❌ — both new PR tests fail Highest — re-opens the #30936 alignment scenario for the brief window before UpdatePlatformMaxConstraints runs. Aggressive refactor of measure semantics.
6 try-fix-4 Revert handler to origin/main (#30936 state) and add exactly one line: VirtualView?.InvalidateMeasure() at the end of OnImageOpened. +1 functional / +7 comment none ❌ — cache-fallback test would need rewrite Low-medium — tiniest possible footprint; same one-frame-flash risk as try-fix-1.

Comparative analysis

Gate signal: All candidates have the same gate-test situation — none could be Windows-validated in this run. Per the rule "candidates that failed regression tests must rank lower than those that passed," all candidates are equal on the gate axis (no candidate has a passing Windows regression test in this environment). The Android failure is non-applicable evidence.

Behavioral comparison:

  • pr and pr-plus-reviewer are the only candidates whose existing test suite continues to assert the documented behavior. try-fix-{1,2,3,4} all change the semantics that the PR's tests assert, so adopting any of them requires rewriting/removing tests — a regression-coverage downgrade.
  • pr-plus-reviewer strictly dominates pr: same fix, plus three mechanical improvements (Disconnect symmetry, single WinRT property read, stricter test sanity assertion) that fix real (if minor) issues without changing semantics.
  • try-fix-1 and try-fix-4 use the InvalidateMeasure-after-decode pattern. It is architecturally clean and mirrors other handlers, but introduces an extra layout pass per image load and a one-frame window where AspectFit isn't capped. The PR's approach trades that for managed state.
  • try-fix-2's "await decode in MapSourceAsync" is the most invasive and produces the largest diff. It increases source-load latency to include decode time — a perceptible UX change for large images, with no clear win.
  • try-fix-3's "remove the cap" gambit deletes the [Windows] Fix ImageHandler Vertical&Horizontal Options with AspectFit #30936 fix; appealing for simplicity but reopens a separate bug the PR's author was not asked to solve.

Inline-finding feedback already applied: the PR already addresses earlier reviewer feedback (clear cache on source change; UpdatePlatformMaxConstraints cache fallback). pr-plus-reviewer applies the next round of polish.

Winner: pr-plus-reviewer

Why:

  1. It is strictly an improvement over the raw pr (zero behavioral risk added).
  2. It retains the PR's well-scoped, narrowly-targeted fix that maps cleanly to the root cause.
  3. All four try-fix-* candidates require rewriting at least one of the PR's regression tests — a coverage downgrade.
  4. Expert reviewer verdict is "approve with minor polish"; pr-plus-reviewer is that polish.
  5. The candidate diff is mechanical (one field reset, one method overload to de-dup a WinRT read, one extra test assertion) — verifiable without running anything.

Critical merge prerequisite (not graded as candidate selection): the Windows device-test leg of CI must be green before merge. The Android gate is non-signal. Reviewers should verify both new device tests run on the Windows queue and pass.

Open follow-ups (not gating merge)


@MauiBot MauiBot added s/agent-review-incomplete and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues labels May 11, 2026
@kubaflo
kubaflo changed the base branch from main to inflight/current May 12, 2026 10:06
@kubaflo
kubaflo merged commit 02566ae into dotnet:inflight/current May 12, 2026
130 of 164 checks passed
@github-actions github-actions Bot added this to the .NET 10 SR7 milestone May 12, 2026
@kubaflo kubaflo added s/agent-gate-failed AI could not verify tests catch the bug s/agent-approved AI agent recommends approval - PR fix is correct and optimal and removed s/agent-review-incomplete labels May 20, 2026
PureWeen pushed a commit that referenced this pull request Jun 2, 2026
…in GetDesiredSize (#34699)

<!-- Please let the below note in for people that find this PR -->
   > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
  Thank you!
 
### Root Cause

PR #30936 introduced logic in `GetDesiredSize()` to cap the `Image`
control's desired size using `BitmapSource.PixelWidth` and
`BitmapSource.PixelHeight` for correct `AspectFit` alignment. On
Windows, image decoding is asynchronous — pixel dimensions start at `0`
and are populated only after background decoding completes.

This creates a race condition: if `GetDesiredSize()` runs before
decoding finishes, `GetImageSize()` returns `0` and size capping is
skipped; if it runs after, it reads actual dimensions (e.g., `512×512`)
and applies capping. Window state affects this timing — maximized
windows trigger layout after decode, while minimized/restored windows
trigger it before — resulting in inconsistent, timing-dependent layout
behavior.

**Sample-Side Issue**

The reproduction sample amplifies the issue by using
`ContentPage.Width`/`Height` for crop ratio calculations instead of the
`Image` control's own dimensions. Since the page aspect ratio varies
with window state but the image is a fixed `1:1` ratio, the same crop
coordinates produce distorted results (e.g., wide or flattened
rectangles). Crop calculations must be relative to the `Image` control's
rendered area, not the page.

### Description of Change

A `_cachedImageSize` field is introduced in `ImageHandler.Windows.cs`
with three targeted changes:

* **OnImageOpened** — Stores the result of `GetImageSize()` into
`_cachedImageSize` once decoding completes, but only when the decoded
dimensions are greater than zero. This prevents a blank transitional
`BitmapImage` from overwriting a previously valid cached size.

* **GetDesiredSize()** — Reads from `_cachedImageSize` instead of
calling `GetImageSize()` dynamically, eliminating the race condition
where layout occurs before the asynchronous `BitmapSource` decode
completes.

* **MapSourceAsync** — Resets `PlatformView.MaxWidth` and
`PlatformView.MaxHeight` to ∞ on source change. `_cachedImageSize` is
intentionally not cleared here — preserving the previous image’s natural
size during the transition ensures `GetDesiredSize` remains correctly
constrained while the new image is still decoding.

The cache is populated once per image load (post-decode) and
intentionally retained across source transitions, ensuring consistent
size calculations regardless of window state or layout timing.


### Issues Fixed
Fixes #32393 
 
Tested the behaviour in the following platforms
- [ ] Android
- [x] Windows
- [ ] iOS
- [ ] Mac

**Note:** This issue is Windows-specific (BitmapSource async decode is a
WinUI behavior) and the fix is scoped to ImageHandler.Windows.cs only —
no other platforms are affected.

### Screenshots
**Issue1**
| Before Fix | After Fix |
|------------|-----------|
| <img width="350" alt="withoutfix"
src="https://github.com/user-attachments/assets/592bb99e-aa1c-436d-a46d-096ecf7fe432"
/> | <img width="350" alt="withfix"
src="https://github.com/user-attachments/assets/c5cd8a47-7ce1-452d-a82d-a67125e5fb6e"
/> |

**Issue2**
| Before Fix | After Fix |
|------------|-----------|
| <img width="350" alt="withoutfix"
src="https://github.com/user-attachments/assets/c86b609c-4ec3-4b90-bd03-82fa39a6b275"
/> | <img width="350" alt="withfix"
src="https://github.com/user-attachments/assets/59a2b1f4-5df2-4ef0-95b5-399d4b9e6b2e"
/> |
PureWeen pushed a commit that referenced this pull request Jun 11, 2026
…in GetDesiredSize (#34699)

<!-- Please let the below note in for people that find this PR -->
   > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
  Thank you!
 
### Root Cause

PR #30936 introduced logic in `GetDesiredSize()` to cap the `Image`
control's desired size using `BitmapSource.PixelWidth` and
`BitmapSource.PixelHeight` for correct `AspectFit` alignment. On
Windows, image decoding is asynchronous — pixel dimensions start at `0`
and are populated only after background decoding completes.

This creates a race condition: if `GetDesiredSize()` runs before
decoding finishes, `GetImageSize()` returns `0` and size capping is
skipped; if it runs after, it reads actual dimensions (e.g., `512×512`)
and applies capping. Window state affects this timing — maximized
windows trigger layout after decode, while minimized/restored windows
trigger it before — resulting in inconsistent, timing-dependent layout
behavior.

**Sample-Side Issue**

The reproduction sample amplifies the issue by using
`ContentPage.Width`/`Height` for crop ratio calculations instead of the
`Image` control's own dimensions. Since the page aspect ratio varies
with window state but the image is a fixed `1:1` ratio, the same crop
coordinates produce distorted results (e.g., wide or flattened
rectangles). Crop calculations must be relative to the `Image` control's
rendered area, not the page.

### Description of Change

A `_cachedImageSize` field is introduced in `ImageHandler.Windows.cs`
with three targeted changes:

* **OnImageOpened** — Stores the result of `GetImageSize()` into
`_cachedImageSize` once decoding completes, but only when the decoded
dimensions are greater than zero. This prevents a blank transitional
`BitmapImage` from overwriting a previously valid cached size.

* **GetDesiredSize()** — Reads from `_cachedImageSize` instead of
calling `GetImageSize()` dynamically, eliminating the race condition
where layout occurs before the asynchronous `BitmapSource` decode
completes.

* **MapSourceAsync** — Resets `PlatformView.MaxWidth` and
`PlatformView.MaxHeight` to ∞ on source change. `_cachedImageSize` is
intentionally not cleared here — preserving the previous image’s natural
size during the transition ensures `GetDesiredSize` remains correctly
constrained while the new image is still decoding.

The cache is populated once per image load (post-decode) and
intentionally retained across source transitions, ensuring consistent
size calculations regardless of window state or layout timing.


### Issues Fixed
Fixes #32393 
 
Tested the behaviour in the following platforms
- [ ] Android
- [x] Windows
- [ ] iOS
- [ ] Mac

**Note:** This issue is Windows-specific (BitmapSource async decode is a
WinUI behavior) and the fix is scoped to ImageHandler.Windows.cs only —
no other platforms are affected.

### Screenshots
**Issue1**
| Before Fix | After Fix |
|------------|-----------|
| <img width="350" alt="withoutfix"
src="https://github.com/user-attachments/assets/592bb99e-aa1c-436d-a46d-096ecf7fe432"
/> | <img width="350" alt="withfix"
src="https://github.com/user-attachments/assets/c5cd8a47-7ce1-452d-a82d-a67125e5fb6e"
/> |

**Issue2**
| Before Fix | After Fix |
|------------|-----------|
| <img width="350" alt="withoutfix"
src="https://github.com/user-attachments/assets/c86b609c-4ec3-4b90-bd03-82fa39a6b275"
/> | <img width="350" alt="withfix"
src="https://github.com/user-attachments/assets/59a2b1f4-5df2-4ef0-95b5-399d4b9e6b2e"
/> |
kubaflo pushed a commit that referenced this pull request Jun 15, 2026
…test failures on candidate branch (#35923)

<!-- Please let the below note in for people that find this PR -->
   > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
  Thank you!

## Root Cause

PR #34699 introduced `_cachedImageSize` to eliminate timing-dependent
behavior in `GetDesiredSize()`. As part of that change,
`MapSourceAsync()` resets the cache to `Zero` whenever a new image
source is assigned.

However, `GetDesiredSize()` was updated to rely exclusively on
`_cachedImageSize` and no longer considered the live decoded size
available on `BitmapSource`. This created a timing gap where WinUI had
already completed image decoding and populated
`BitmapSource.PixelWidth`/`PixelHeight`, but the asynchronous
`ImageOpened` event had not yet fired to update the cache.

If a layout pass occurred during this window, `GetDesiredSize()`
observed a zero-sized cache, skipped the natural-size constraint, and
allowed the image to expand to the available container size instead of
respecting its decoded pixel dimensions.

## Description of Change

Two minimal updates were made in `ImageHandler.Windows.cs`:

`GetDesiredSize()
`
* Reads the live decoded size from `BitmapSource` first.
* Falls back to `_cachedImageSize` only when the live size is not yet
available.
* Aligns with the existing live-first, cache-fallback pattern already
used in `UpdatePlatformMaxConstraints()`.

`GetImageSize()
`
* Added a null-conditional guard for `PlatformView` to prevent a
potential null-reference exception when `GetDesiredSize()` is called
after handler disconnection.

## Regression Introduced By
PR #34699 
 
### Issues Fixed
Fixes the following candidate branch regressions:
In `Issue30403SmallImages.cs` file:
`ConstrainedContainers_ShouldHandleSmallImages` 
`SmallImageEndAlignment_ShouldRespectAlignment`
`SmallImageInLargeContainer_ShouldNotExceedIntrinsicSize`
`SmallImageStartAlignment_ShouldRespectAlignment`
`SizeComparison_ShouldShowDifferentSizes`
`TinyImage_ShouldRemainTiny` 
 
Tested the behaviour in the following platforms
- [ ] Android
- [x] Windows
- [ ] iOS
- [ ] Mac
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 20, 2026
@github-actions github-actions Bot modified the milestones: .NET 10 SR7, .NET 10 SR9 Jul 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-controls-image Image control community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/windows s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-win AI found a better alternative fix than the PR s/agent-gate-failed AI could not verify tests catch the bug s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

7 participants