Visitar URL original
fix(SvgImageSource): Match WinUI on SetSourceAsync failure and Opened by mikernet · Pull Request #25558 · unoplatform/uno · GitHub
Skip to content

fix(SvgImageSource): Match WinUI on SetSourceAsync failure and Opened - #25558

Open
mikernet wants to merge 12 commits into
unoplatform:masterfrom
mikernet:dev/mikernet/svg-stream-source
Open

mikernet wants to merge 12 commits into
unoplatform:masterfrom
mikernet:dev/mikernet/svg-stream-source

Conversation

@mikernet

@mikernet mikernet commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

GitHub Issue: closes #25557 (related: #25554)

PR Type:

🐞 Bugfix

What changed? 🚀

Four differences from WinUI in SvgImageSource (#25557), plus test coverage for stream loading.

1. SetSourceAsync with content that isn't SVG. Uno returned InvalidFormat. WinUI raises OpenFailed with InvalidFormat and completes the operation with E_INVALIDARG (SvgImageSourceSetSourceAsyncOperation::CoreSetError, called from CImageSource::OnImageViewUpdated), so awaiting it throws ArgumentException. The operation now faults with an ArgumentException in that case. OpenFailed is raised as before.

2. Opened on later loads. Uno raised Opened for every successful load of a source. WinUI raises it only for the first: CSvgImageSource::OnDownloadImageAvailableImpl fires it while m_frameIndex is 0, and for an SVG nothing resets that index (only CBitmapImage::PrepareDecode does). WinUI's SvgImageSourceTests creates a new instance when it expects a second Opened. Opened is now raised once per source.

3. ImageOpened on the elements showing the source. CImageSource::FireImageOpened notifies the images and brushes showing the source together with Opened, so Image.ImageOpened and ImageBrush.ImageOpened also fire only for that first load. Image and ImageBrush now each remember the SVG source they raised ImageOpened for, and raise it the first time they show data from a source they haven't raised it for yet. The source only keeps its own once-only Opened. Nothing in an element's decision depends on the source's state while the data is delivered, which can run app code that reloads the source (an ImageOpened or Opened handler). A cancelled load is never published, so it can't use up an element's event. Each load also builds its own data instead of reading the source's current surface. #25124 made Image raise ImageOpened once per SourceVersion; an SVG source keeps the once-per-source rule above, since another stream or UriSource gives it a new version.

4. When SetSourceAsync completes. Each call is one small state machine (SetSourceOperation). It settles exactly once, from its own load rather than from whatever the source publishes, and never runs the awaiting code inside a source reset:

Event while pending Result (as measured on WinUI)
its load fails (not SVG), even if an OpenFailed handler set another stream throws ArgumentException
another SetSourceAsync, e.g. from Opened or from a brush's ImageOpened cancelled (TaskCanceledException)
a new UriSource (or the source is disposed) Success
its load has been published to every element showing the source Success

A brush raises ImageOpened during that publication, so its handler can still cancel the operation. An Image raises it on its next measure, after the operation completed. A load now starts only after it has been stored. Before, a load whose stream reads synchronously ran inside the call that stored it, so a replacement started by its Opened handler was overwritten, and the first load's disposed surface was published again.

These are behavior changes for code that relied on Uno's old behavior. That is why they target master and not a servicing branch.

5. Stream coverage. #25166 fixed SetSourceAsync(stream) on master: it opens the source twice, and the cached load used to be bound to the first open's cancelled token. It only added a test for the URI path, so this adds tests for streams. The 6.8 fix is #25555.

Tests:

  • Given_Image.When_SVGImageSource_SetSourceAsync_File_Stream, with Image.Source set before and after the call: Success, Opened raised, SVG drawn.
  • Given_Image.When_SVGImageSource_SetSourceAsync_Twice: red and then blue on the same source, each color drawn, and Opened raised once.
  • Given_Image.When_SVGImageSource_SetSourceAsync_Not_Svg: a PNG stream throws ArgumentException, and OpenFailed reports InvalidFormat.
  • Given_Image.When_SVGImageSource_Reloaded_Then_ImageOpened_Once: two images show the source while it loads twice, and each raises ImageOpened once.
  • Given_Image.When_SVGImageSource_Reloaded_Before_Measure_Then_ImageOpened_Once: a collapsed image, so not measured, gets a second load before it shows the first, and raises ImageOpened once.
  • Given_ImageBrush.When_SvgImageSource_Reloaded_Then_ImageOpened_Once: the same for an ImageBrush.
  • Given_ImageBrush.When_SvgImageSource_Reloaded_From_ImageOpened_Then_Each_Raises_Once: the first brush's ImageOpened handler reloads the shared source while the first load is still being delivered. A second brush and an Image still raise ImageOpened once, the first operation is cancelled, and the reload succeeds.
  • Given_ImageBrush.When_SvgImageSource_Reloaded_From_Opened_Then_Each_Raises_Once: the source's own Opened handler reloads it before its first load is delivered. The brush and the image still raise ImageOpened once.
  • Given_Image.When_SVGImageSource_SetSourceAsync_From_Opened, with Image.Source set before and after: memory streams, red then blue from the Opened handler. The first operation is cancelled, the second succeeds, and blue is drawn.
  • Given_Image.When_SVGImageSource_SetSourceAsync_From_OpenFailed: a PNG, then a valid SVG from the OpenFailed handler. The first operation throws ArgumentException, the second succeeds, and the SVG is drawn.
  • Given_Image.When_SVGImageSource_SetSourceAsync_From_Image_ImageOpened, with the image visible and collapsed: another stream set from the image's ImageOpened. Both operations succeed and the replacement is drawn.
  • Given_Image.When_SVGImageSource_SetSourceAsync_Replaced_Before_Published: a UriSource queued from Opened replaces the load after it completed and before it was published, and its HTTP load never answers. The operation still completes, with Success.
  • Given_Image.When_SVGImageSource_UriSource_Set_While_SetSourceAsync_Pending: the operation succeeds and the URI's content is drawn.
  • Given_ImageBrush.When_SvgImageSource_Brush_Attached_While_Loading_Reloads_From_ImageOpened: a brush attached after the call starts sets another stream from its ImageOpened. The first operation is cancelled.

Results:

  • WinUI (WinAppSDK): all of these pass. I measured the WinUI behavior first with probes:
    • two stream loads, two UriSource loads, stream then URI, and stream, invalid, stream: each raised Opened once, and the invalid load threw ArgumentException (0x80070057);
    • three loads with an Image attached: one Image.ImageOpened.
    • a stream set from a brush's ImageOpened handler or from the source's Opened handler cancels the first operation; one set from an image's ImageOpened handler doesn't (the image raises it after the operation completed, visible or collapsed); one set from OpenFailed leaves it throwing ArgumentException. Uno now gives the same results with both engines.
  • Mutations (measured before the update to master):
    • With items 1 and 2 reverted, Not_Svg fails (no exception) and Twice fails (Opened raised 2 times), on both engines.
    • With the Image/ImageBrush part of item 3 reverted, the two reload ImageOpened tests fail (2 and 3 events).
    • With Image checking the load at measure time instead of when the data arrives, the before-measure test fails (no ImageOpened).
    • With the source marking the data of later loads (the previous design), the reload-from-Opened test fails (no ImageOpened at all).
    • With the previous completion code, the stalled-replacement test hangs and the brush-attached-while-loading test returns Success.
    • With completion from the shared subscription (the previous design), the two SetSourceAsync_From_* tests and the brush-handler test fail: the operations return the replacement's Success. Without the yield that stores a load before it runs, the From_Opened rows fail as well.
    • With the pre-fix(SvgImageSource): Keep the load alive when an Image subscribes mid-open #25166 per-open token, the source-after stream row fails with Other.
  • After the update to master (fix(imaging): Release a decoded image when nothing displays it #25124 and fix(imaging): Let a long-lived source drop the brushes it painted #25633), on Skia desktop (Win32): Given_Image, Given_ImageSource, Given_ImageBrush, Given_BitmapSource, Given_BitmapImage, Given_WriteableBitmap, Given_Image_Release and Given_SvgImageSource_ManagedEngine 151 passed, 1 failed with the Svg.Skia add-in; Given_Image, Given_ImageSource, Given_ImageBrush and Given_SvgImageSource_ManagedEngine 130/130 with the managed engine (UNO_MANAGED_SVG=1). The failure, When_WriteableBitmap_Assigned_With_Data_Present, fails the same on master (125% scaling). With Image keeping only master's once-per-SourceVersion rule for an SVG source, or ImageBrush raising ImageOpened for every SVG load, the Image and ImageBrush reload tests fail again (2 events). Before the update, on Skia WebAssembly, Given_Image, Given_ImageBrush, Given_BitmapSource and Given_SvgImageSource_ManagedEngine passed 81/81, plus 1 inconclusive (When_Transitive_Asset_With_Link_Loaded).

Not changed: WinUI raises ImageOpened on the elements showing an SVG source when the source opens, so an element given a source that has already opened from a stream gets none. For a UriSource it does get one, because WinUI decodes a URI only once an element shows it. Uno opens URI sources as soon as UriSource is set, so it can't tell those two cases apart. Here, an element given a source that has already opened raises ImageOpened once.

PR Checklist ✅

Behavior changes, each now as on WinUI:

  • An app that checked the returned status for InvalidFormat now gets an ArgumentException.
  • An app that relied on Opened or ImageOpened for later loads of the same SVG source no longer gets them.

SetSourceAsync subscribes to the source, which opens it, then calls
InvalidateSource, which opens it again and cancels the first open's
token. unoplatform#25166 made the SVG load independent of that token, which also
fixed stream loading, but only the URI path has a test.

These load a file stream, whose read is still in flight when the source
is opened again, with Image.Source set before and after the call, and
call SetSourceAsync twice on the same source. Each checks the returned
status and the drawn color; the first also waits for Opened.

The same tests pass on WinUI.
When the content isn't SVG, WinUI raises OpenFailed with InvalidFormat
and completes the SetSourceAsync operation with E_INVALIDARG, so
awaiting it throws ArgumentException (the operation status is Error,
so the await never reads the result). Uno returned InvalidFormat
instead.

The operation now faults with an ArgumentException in that case.
OpenFailed is raised as before.
WinUI raises Opened only for the first successful load of an
SvgImageSource. CSvgImageSource fires it while m_frameIndex is 0, and
only CBitmapImage::PrepareDecode resets that index, so a later
SetSourceAsync or UriSource on the same source doesn't raise it again.
WinUI's own SvgImageSourceTests creates a new instance when it expects
another Opened. Uno raised Opened on every load.

Opened is now raised once per source. OpenFailed is unchanged.
WinUI raises ImageOpened on the images and brushes showing an SVG source
together with the source's Opened (CImageSource::FireImageOpened
notifies their callbacks), so only for its first successful load.
Uno raised it on every load. Measured on WinUI: three loads of one
source raise Image.ImageOpened once, and an ImageBrush behaves the same.

The source now records the load that raised Opened (a generation bumped
in UnloadImageSourceData). Image and ImageBrush raise ImageOpened for an
SVG source only while that load is the current one.

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.

🟡 Changes recommended

A reload before deferred measurement can suppress an attached image’s only ImageOpened notification.

1 open issue
What changed in this PR

Aligns SVG loading failures and opened events with WinUI for #25557, with stream-loading regression coverage related to #25554.

Changes:

  • Throws ArgumentException for invalid SVG streams.
  • Limits source and consumer opened events to the first successful load.
  • Adds stream-loading, rendering, and event-count tests.
File Description
src/​Uno.UI/​UI/​Xaml/​Media/​Imaging/​SvgImageSource.provider.cs Advances the load generation on unload.
src/​Uno.UI/​UI/​Xaml/​Media/​Imaging/​SvgImageSource.cs Updates failure handling and tracks the first opened load.
src/​Uno.UI/​UI/​Xaml/​Media/​ImageBrush.crossruntime.cs Gates SVG brush opened events.
src/​Uno.UI/​UI/​Xaml/​Controls/​Image/​Image.cs Gates SVG image opened events.
src/​Uno.UI.RuntimeTests/​Microsoft/​UI/​Xaml/​Media/​Given_ImageBrush.cs Tests brush rendering and event counts across reloads.
src/​Uno.UI.RuntimeTests/​Microsoft/​UI/​Xaml/​Controls/​Given_Image.cs Tests stream loading, invalid input, rendering, and events.

Review effort: Balanced

Image only processes the data it receives on its next measure, and it
checked the SVG source's opened load at that point. If the source loaded
again before the measure (two SetSourceAsync calls with no layout pass in
between), the first load's data was replaced and the check rejected the
second load, so ImageOpened was never raised.

The decision is now made when the data arrives and stays pending until
the image shows data, or until its Source changes. ImageBrush already
decides when the data arrives.
Copilot AI balanced review requested due to automatic review settings October 17, 2026 03:04

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.

🔵 Needs a closer look

Reloading a shared SVG source during subscriber notification can suppress other consumers’ first opened event.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Preserve published load generation for shared source ImageOpened events

src/​Uno.UI/​UI/​Xaml/​Media/​Imaging/​SvgImageSource.cs:162

[P2] This checks the source's current generation rather than the generation of the data being delivered. ImageSource.OnOpened invokes a snapshot of its subscribers sequentially. If the first brush's ImageOpened handler changes UriSource, UnloadImageSourceData increments _loadGeneration before the remaining images or brushes receive the first load's data. They now see IsOpenedLoad == false and miss their first ImageOpened; later loads cannot raise it either. Capture eligibility with the published load and have both consumers use that immutable value. Add a regression test with a shared source reloaded from the first brush's event handler.

1 resolved since last review

Review effort: Balanced

Delivering a load's data to the subscribers of a source can run app code
(an ImageBrush's ImageOpened handler) that changes the source before the
next subscriber gets the data. Three places read the source's state
during delivery and so saw the newer load:

- Image and ImageBrush asked the source whether the current load raised
  Opened, so the subscribers after such a handler skipped ImageOpened
  for good.
- SetSourceAsync read the last load status, which a SetSourceAsync from
  such a handler had reset, so the first call returned Other.
- The open's continuation took the source's current surface instead of
  the load's own.

The load now builds its data itself and marks it when it isn't the load
that raised Opened (ImageData.SuppressImageOpened); an empty source is
marked too. Image and ImageBrush read that mark, and SetSourceAsync
derives its result from the data (InvalidFormat comes as an
ArgumentException, which it rethrows), so the generation counter and
the stored status go away.
Copilot AI balanced review requested due to automatic review settings October 17, 2026 03:36

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.

🔵 Needs a closer look

Reloading from the source’s first Opened handler can permanently suppress open notifications on attached images and brushes.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Reentrant SVG reload suppresses ImageOpened notification

src/​Uno.UI/​UI/​Xaml/​Media/​Imaging/​SvgImageSource.provider.cs:70

If the first SvgImageSource.Opened handler changes UriSource to another SVG while the initial load is completing asynchronously, attached images and brushes never receive ImageOpened. The reload cancels the current open, so ImageSource.Open() skips publishing its data at the cancellation check. The replacement load then sets SuppressImageOpened = true because _openedRaised is already set. Preserve the once-only consumer notification until it is delivered, without publishing cancelled image data or raising duplicate events. Add a regression test that reloads from the source's Opened handler; the brush-handler test reloads only after publication has begun.

Review effort: Balanced

Deciding an element's ImageOpened from the source's state kept breaking
when the source was reloaded while a load was being delivered: from an
earlier subscriber's handler, before the element was measured, and now
from the source's own Opened handler. That handler's reload cancels the
first load, so only the second one is published, and it carried the
"not the opened load" mark, so no element raised ImageOpened.

The rule now lives in the elements. Image and ImageBrush remember the
SVG source they raised ImageOpened for and raise it the first time they
show data from a source they haven't raised it for yet. The source only
keeps its own once-only Opened. A cancelled load is never published, so
it can't use up an element's event, and the mark on ImageData goes away.
An element also ignores a delivery that was already under way when it
changed source, so that data of the previous source can't use it up
either.
Copilot AI balanced review requested due to automatic review settings October 17, 2026 04:05
mikernet added a commit to Singulink/uno that referenced this pull request Oct 17, 2026
Port of unoplatform#25558 (master 85655f2), from its 6.8 version
(95904afaae).

Three behaviors now match WinUI:
- When the stream isn't SVG, WinUI raises OpenFailed with InvalidFormat
  and completes SetSourceAsync with E_INVALIDARG, so awaiting it throws
  ArgumentException. Uno returned InvalidFormat. The load now gives an
  ArgumentException error, which SetSourceAsync rethrows.
- WinUI raises Opened only for the first successful load of a source.
  CSvgImageSource fires it while m_frameIndex is 0, and nothing resets
  that index for an SVG. Uno raised it on every load.
- ImageOpened on the images and brushes showing the source fires
  together with Opened, so once. ImageBrush remembers, weakly, the SVG
  source it raised it for, and raises it the first time it gets data
  from another one. SetSourceAsync derives its result from the
  delivered data: delivery can run app code that reloads the source, so
  none of these reads the source's state then.
- Tests in Given_Image and Given_ImageBrush.

6.8 differences:
- The 6.8 version is the one ported. 6.8's Image draws an SVG through a
  child canvas, not through a subscription, so it raises ImageOpened
  from the source's Opened event, where master's raises it once per
  element on the surface path.
- On top of the unoplatform#25124 port: ImageBrush's change is in OnSourceOpened
  only, apart from the subscription handling that port added to
  OnSourceChangedPartial, and a brush still raises ImageOpened for each
  bitmap it is given.

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.

🟡 Changes recommended

Reentrant loads can still publish disposed content or complete an operation using another load’s result.

2 open issues
1 previously missed issue

In code that hasn't changed since last review

Medium severity Test does not await or validate the first load result

src/​Uno.UI.RuntimeTests/​Microsoft/​UI/​Xaml/​Media/​Given_ImageBrush.cs:232

[P2] load.IsCompleted also accepts a faulted or cancelled first operation, or one returning Other. Only reload is awaited below, so this test can pass without verifying the original operation's outcome—the behavior addressed by the new data-derived completion logic. Await load and assert Success before leaving this block.

Review effort: Balanced

A load raises Opened and OpenFailed, whose handlers may give the source
another stream. Two things went wrong when they did:

- SetSourceAsync completed from whatever the source published, so when
  an OpenFailed handler set a valid SVG, the failed operation returned
  the replacement's Success instead of throwing.
- A load whose stream reads synchronously (a memory stream) ran inside
  the call that stored it, so the replacement's task was overwritten
  with the first load's, and its disposed surface was published again.

A load now starts after a yield, once EnsureLoad has stored it and its
cancellation, so a replacement can't be overwritten; like on WinUI, its
events no longer fire within the call that set the source. SetSourceAsync
awaits its own load instead of subscribing, and completes after the
open that publishes it. As measured on WinUI: a failed load throws
ArgumentException even if an OpenFailed handler set another stream, and
an operation whose source was replaced before it completed (from Opened
or from an ImageOpened handler) is cancelled.
Copilot AI balanced review requested due to automatic review settings October 17, 2026 04:36

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.

🟡 Changes recommended

Operation completion can precede Image.ImageOpened, preventing the intended cancellation when its handler replaces the stream.

1 open issue
2 resolved since last review

Review effort: Balanced

SetSourceAsync waited one dispatcher turn after its load to let the
brushes showing the source raise ImageOpened first, whose handler may
replace the stream and so cancel the operation, as on WinUI. That
ordering held for brushes, but the layout pass that makes an Image
raise ImageOpened could run in between too.

The operation now completes within the publication of its load, right
after the elements subscribed before it got the data: a brush has
raised ImageOpened by then, and an Image raises it on its next measure.
WinUI orders them the same way: a stream set from a brush's ImageOpened
cancels the operation, one set from an Image's doesn't. A load that
failed or was replaced still completes the operation at once.
Copilot AI balanced review requested due to automatic review settings October 17, 2026 05:02

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.

🟡 Changes recommended

Completion ordering and cancellation gaps can produce premature success or leave an operation pending indefinitely.

2 open issues
1 resolved since last review

Review effort: Balanced

The completion of SetSourceAsync was spread over a load continuation
and a subscriber callback, which left two gaps: a load replaced after
it completed but before it was published left the operation waiting
for the replacement's publication, forever if that stalled; and a
brush attached after the call was notified after the operation had
already committed Success, so its ImageOpened handler couldn't cancel
it as on WinUI.

Each operation is now one SetSourceOperation that settles once, in one
place, without running the awaiting code inline:

- its load failed: Failed (ArgumentException for content that isn't
  SVG, even if an OpenFailed handler set another stream since);
- another SetSourceAsync replaced it: Cancelled;
- a new UriSource or the source's disposal replaced it: Success, as on
  WinUI;
- its load was published to every subscriber (ImageSource.OnPublished,
  after the whole pass): Success.

A replacement settles the operation once the call that made it has
returned, so never from a half-reset source.
Copilot AI balanced review requested due to automatic review settings October 17, 2026 05:43

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.

🔵 Needs a closer look

Reentrant loading, cancellation, and cross-control publication ordering warrant final human review.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

mikernet added a commit to Singulink/uno that referenced this pull request Oct 17, 2026
Port of the follow-up round on unoplatform#25558 (master
36e6462), from its 6.8 commits dbfbc14422, baa0e0bb0e and
732e55a3c8.

SetSourceAsync completed from whatever the source published, so when an
OpenFailed handler set a valid SVG, the failed operation returned the
replacement's Success, and a load replaced after it completed but
before it was published left the operation waiting.
- Each SetSourceAsync is one SetSourceOperation that settles once,
  without running the awaiting code inline: Failed when its load fails
  (ArgumentException, kept even if an OpenFailed handler set another
  stream), Cancelled when another SetSourceAsync replaces it, Success
  when a new UriSource or the source's disposal replaces it (as on
  WinUI), and otherwise Success once its load has been published to
  every subscriber (ImageSource.OnPublished), so a brush's ImageOpened
  handler that sets another stream cancels it.
- The Svg add-in versions its loads and cancels a parse that was
  overtaken, which replaced the newer document and raised Opened for
  stale content, and stops after Opened if a handler replaced the
  source.
- Tests: When_SVGImageSource_SetSourceAsync_From_OpenFailed,
  _From_Opened, _Replaced_Before_Published and
  When_SVGImageSource_UriSource_Set_While_SetSourceAsync_Pending
  (Given_Image), and
  When_SvgImageSource_Brush_Attached_While_Loading_Reloads_From_ImageOpened
  (Given_ImageBrush).

6.8 differences:
- The commits are written against servicing/6.8's ImageSource, whose
  OnOpened was one loop over its listeners. On the unoplatform#25124 port's, git
  put the new OnPublished() call in ReopenForSubscribers, where it
  never ran after an open, so SetSourceAsync never completed (a test
  hung). It is called at the end of OnOpened instead, and only when no
  subscriber changed the source during the delivery, which stops it:
  every subscriber then has the data, and a replacement settles the
  operation through its load's cancellation.
@mikernet

Copy link
Copy Markdown
Contributor Author

Copilot's review is clean now, so here is what changed while working through it.

  • ImageOpened for an SVG source: each Image and ImageBrush raises it once per source, the first time it shows that source's data, as on WinUI. The rule lives in the element, so a source reloaded mid-delivery, before measure, or from an Opened handler no longer loses the event.
  • SetSourceAsync completes from its own load, in one small state machine that follows WinUI: a failed load throws ArgumentException; another SetSourceAsync (from Opened or a brush's ImageOpened) cancels the earlier call; a new UriSource or disposal lets it succeed; otherwise it succeeds once its load is published. An Image raises ImageOpened after the operation completes, so its handler doesn't cancel it.
  • Re-entrant loads: a load starts only after it has been stored, and it builds its own result instead of reading the source's current state. Some of these problems were already on master, for example an Opened handler's new load being overwritten, which published a disposed surface again.

One deliberate difference remains. WinUI decodes a UriSource only once an element shows it, while Uno opens it when it's set. So an element given a source that has already opened still gets one ImageOpened here, where WinUI raises none for stream sources.

Validation: runtime tests on Skia desktop with both the Svg.Skia add-in and the managed engine, on Skia WebAssembly, and on WinUI.

mikernet added a commit to Singulink/uno that referenced this pull request Oct 18, 2026
Port of the Image part of unoplatform#25558 (master 36e6462).

An Image raised ImageOpened for an SVG source from the source's Opened
event, which fires inside the load, before SetSourceAsync completes, so
a stream set from the handler cancelled the operation
(TaskCanceledException). As on WinUI, Opened now fires first, then the
operation completes, then the Image raises ImageOpened the first time
it shows the source's data, and both operations return Success.
- The Image subscribes to the SVG source's data next to its child
  canvas, and raises ImageOpened on the measure after a successful
  delivery: once per source and element, however often the source
  loads.
- Test: When_SVGImageSource_SetSourceAsync_From_Image_ImageOpened, with
  the image visible and collapsed (Given_Image).

6.8 differences:
- 6.8's Image still draws an SVG through its child canvas; the
  subscription only tells it when to raise ImageOpened. Master's Image
  draws the source's surface and raises it from that delivery.
- No stale-delivery check as on master: on the unoplatform#25124 port a source
  delivers only to active subscriptions, and the Image disposes its SVG
  subscription when its source changes.
- Side effects: ImageOpened comes at first show, not at the source's
  Opened, so it waits while the Image is collapsed or off the tree. An
  Image given a source that has already opened raises it once, where
  WinUI raises none for a stream source (as on master). Disposing an
  SVG source that an Image shows now opens it again for the Image, as
  bitmaps and brushes already were since the unoplatform#25124 port.
@mergify

mergify Bot commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue). If it does not match the queue conditions yet, it enters the queue once it does.

  • Queue this pull request

…ream-source

# Conflicts:
#	src/Uno.UI.RuntimeTests/Microsoft/UI/Xaml/Controls/Given_Image.cs
#	src/Uno.UI/UI/Xaml/Controls/Image/Image.cs
#	src/Uno.UI/UI/Xaml/Media/ImageBrush.crossruntime.cs
Since the merge, a reload from the first brush's ImageOpened stops the
delivery of the first load, so the elements after it raise ImageOpened
once, for the reload.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SvgImageSource: SetSourceAsync failure and the Opened/ImageOpened events differ from WinUI

2 participants