[Windows] Fix Image layout inconsistency caused by async decode race in GetDesiredSize - #34699
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34699Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34699" |
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
There was a problem hiding this comment.
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
ImageHandlercached natural image size and use it inGetDesiredSize()forAspectFit. - Populate the cache on
ImageOpened(post-decode) to avoid readingPixelWidth/PixelHeightduring 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. |
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 3 findings
See inline comments for details.
MauiBot
left a comment
There was a problem hiding this comment.
🤖 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.");
});
});
}
MauiBot
left a comment
There was a problem hiding this comment.
🤖 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)
🤖 AI Summary
📊 Review Session —
|
| 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:
- OnImageOpened populates the cache from
GetImageSize()(only when both dimensions > 0), so transitional blankBitmapImageevents do not clobber a valid size. - GetDesiredSize reads the cache instead of
GetImageSize()— eliminating the timing-dependent path. - MapSourceAsync clears the cache and resets
MaxWidth/MaxHeightto ∞ when source changes (so a failed subsequent load can’t cap to the previous image’s dimensions). - UpdatePlatformMaxConstraints uses the live decoded size first, falling back to the cache when 0 — keeping
MaxWidth/MaxHeightcapped during source transitions.
Adds two Windows-only device tests in ImageHandlerTests.Windows.cs:
AspectFit_AfterDecode_PlatformMaxWidthIsConstrainedToNaturalPixelSizeAspectFit_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
UpdatePlatformMaxConstraintswhensz==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:
_cachedImageSizeis now reset to zero on everyMapSourceAsynccall, but the fallback inUpdatePlatformMaxConstraintsis dependent on this — if the new image fails to load entirely,MaxWidth/MaxHeightstay atVirtualView.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:
prandpr-plus-reviewerare 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-reviewerstrictly dominatespr: 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-1andtry-fix-4use theInvalidateMeasure-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] FixImageHandlerVertical&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:
- It is strictly an improvement over the raw
pr(zero behavioral risk added). - It retains the PR's well-scoped, narrowly-targeted fix that maps cleanly to the root cause.
- All four
try-fix-*candidates require rewriting at least one of the PR's regression tests — a coverage downgrade. - Expert reviewer verdict is "approve with minor polish";
pr-plus-revieweris that polish. - 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)
- Consider follow-up work to address the first-decode race (cache empty between
MapSourceAsyncand firstImageOpened). The PR conservatively returns uncapped for one frame in that window — acceptable but improvable. A future change could combine the PR's cache withtry-fix-1'sInvalidateMeasurefor full coverage. This is not required to land [Windows] Fix Image layout inconsistency caused by async decode race in GetDesiredSize #34699.
…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" /> |
…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" /> |
…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
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 theImagecontrol's desired size usingBitmapSource.PixelWidthandBitmapSource.PixelHeightfor correctAspectFitalignment. On Windows, image decoding is asynchronous — pixel dimensions start at0and are populated only after background decoding completes.This creates a race condition: if
GetDesiredSize()runs before decoding finishes,GetImageSize()returns0and 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/Heightfor crop ratio calculations instead of theImagecontrol's own dimensions. Since the page aspect ratio varies with window state but the image is a fixed1:1ratio, the same crop coordinates produce distorted results (e.g., wide or flattened rectangles). Crop calculations must be relative to theImagecontrol's rendered area, not the page.Description of Change
A
_cachedImageSizefield is introduced inImageHandler.Windows.cswith three targeted changes:OnImageOpened — Stores the result of
GetImageSize()into_cachedImageSizeonce decoding completes, but only when the decoded dimensions are greater than zero. This prevents a blank transitionalBitmapImagefrom overwriting a previously valid cached size.GetDesiredSize() — Reads from
_cachedImageSizeinstead of callingGetImageSize()dynamically, eliminating the race condition where layout occurs before the asynchronousBitmapSourcedecode completes.MapSourceAsync — Resets
PlatformView.MaxWidthandPlatformView.MaxHeightto ∞ on source change._cachedImageSizeis intentionally not cleared here — preserving the previous image’s natural size during the transition ensuresGetDesiredSizeremains 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
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
Issue2