Skip to content

Conversation

@rolandVi
Copy link
Owner

@rolandVi rolandVi commented Jul 30, 2025

Initial Image Component for Blazor

Summary

This PR introduces an Image component that renders images from non-HTTP sources (bytes or streams) with streaming JS interop and optional browser-side caching.

Features

  • Simple source model
    • ImageSource from byte[] or Stream with MimeType and CacheKey properties
    • Single-use design: one ImageSource instance corresponds to exactly one image load
    • Properties: MimeType, CacheKey, Length.
  • Automatic caching
    • Caching backed by the browser Cache Storage API, keyed by CacheKey.
  • Stream-based transfer
    • Uses DotNetStreamReference for efficient streaming from .NET to JS
    • Supports progress tracking via CSS custom property --blazor-image-progress
    • Robust cancellation/race handling: only the latest request for an element is finalized
  • Minimal markup and CSS-driven UX
    • Renders a single with class blazor-image.
    • data-state="loading" or "error" enables styling via CSS.
    • Container can read a CSS custom property --blazor-image-progress for simple progress UI.
  • Lifecycle and memory safety
    • Blob URL creation/revocation handled in JS.

API surface

  • Parameters
    • Source: ImageSource
    • AdditionalAttributes: forwarded to the underlying (e.g., alt, class, style)
  • Render output
    • <img class="blazor-image ..." data-state="loading|error" ... />

Implementation

  • Component and model
    • src/Components/Web/src/Image/Image.cs
    • src/Components/Web/src/Image/ImageSource.cs
  • JavaScript interop
    • src/Components/Web.JS/src/Rendering/BinaryImageComponent.ts
  • Tests
    • Unit: src/Components/Web/test/Image/ImageTest.cs
    • E2E test component: src/Components/test/testassets/BasicTestApp/ImageTest/ImageTestComponent.razor
    • E2E: src/Components/test/E2ETest/Tests/ImageTest.cs

Performance considerations

  • Stream-based transfer avoids large memory allocations
  • Blob URLs are revoked when no longer needed.
  • Cache-first load avoids redundant streaming for repeated images
@using Microsoft.AspNetCore.Components.Web.Image

@code {
    private ImageSource? _photo;

    protected override async Task OnInitializedAsync()
    {
        var bytes = await PhotoService.GetBytesAsync(id: 42);
        _photo = new ImageSource(bytes, "image/jpeg", cacheKey: "photo-42");
    }
}

<Image Source="@_photo"
       alt="Product photo"
       style="max-width: 100%; height: auto;" />

Related - dotnet#25274

@rolandVi rolandVi self-assigned this Jul 30, 2025
@rolandVi
Copy link
Owner Author

updated

Comment on lines 303 to 307
try {
await reader.cancel();
} catch {
// ignore
}

Choose a reason for hiding this comment

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

I don't think this is correct?

This is calling cancel once you've effectively read the entire stream. I think what we would want is to use an AbortController (the JS equivalent of CancellationTokenSource) to check if the source for our image changed and we need to abort reading more data, aren't we?

Copy link
Owner Author

Choose a reason for hiding this comment

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

You are right, that was incorrect.
I've implemented AbortController tracking:

  • Each streamAndCreateUrl creates and tracks an AbortController per element
  • When a new cache key comes in, we abort the previous controller, the signal is passed to iterateStream which cancels the reader on abort

Comment on lines 187 to 196
const chunks: Uint8Array[] = [];
let bytesRead = 0;

for await (const chunk of this.iterateStream(displayStream)) {
if (this.activeCacheKey.get(imgElement) !== cacheKey) {
return null;
}

chunks.push(chunk);
bytesRead += chunk.byteLength;

Choose a reason for hiding this comment

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

Somewhere around in this method, we need to track the cache keys that we are currently loading and trigger an abort to stop reading chunks from the stream.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Instead, I trigger an abort from setImageAsync when a stream with new cache key comes in

@rolandVi rolandVi merged commit d2c308f into roland/image-component Aug 21, 2025
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.

3 participants