File: Image\Image.cs
Web Access
Project: src\src\Components\Web\src\Microsoft.AspNetCore.Components.Web.csproj (Microsoft.AspNetCore.Components.Web)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.JSInterop;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.Components.Web.Image;
 
/* This is equivalent to a .razor file containing:
 *
 * <img data-blazor-image
 *      src="@(_currentObjectUrl)"
 *      data-state=@(IsLoading ? "loading" : _hasError ? "error" : null)
 *      @attributes="AdditionalAttributes"
 *      @ref="Element"/>
 *
 */
/// <summary>
/// A component that efficiently renders images from non-HTTP sources like byte arrays.
/// </summary>
public partial class Image : IComponent, IHandleAfterRender, IAsyncDisposable
{
    private RenderHandle _renderHandle;
    private string? _currentObjectUrl;
    private bool _hasError;
    private bool _isDisposed;
    private bool _initialized;
    private bool _hasPendingRender;
    private string? _activeCacheKey;
    private ImageSource? _currentSource;
    private CancellationTokenSource? _loadCts;
    private bool IsLoading => _currentSource != null && string.IsNullOrEmpty(_currentObjectUrl) && !_hasError;
 
    private bool IsInteractive => _renderHandle.IsInitialized &&
                                _renderHandle.RendererInfo.IsInteractive;
 
    private ElementReference? Element { get; set; }
 
    [Inject] private IJSRuntime JSRuntime { get; set; } = default!;
 
    [Inject] private ILogger<Image> Logger { get; set; } = default!;
 
    /// <summary>
    /// Gets or sets the source for the image.
    /// </summary>
    [Parameter, EditorRequired] public required ImageSource Source { get; set; }
 
    /// <summary>
    /// Gets or sets the attributes for the image.
    /// </summary>
    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }
 
    void IComponent.Attach(RenderHandle renderHandle)
    {
        if (_renderHandle.IsInitialized)
        {
            throw new InvalidOperationException("Component is already attached to a render handle.");
        }
        _renderHandle = renderHandle;
    }
 
    Task IComponent.SetParametersAsync(ParameterView parameters)
    {
        var previousSource = Source;
 
        parameters.SetParameterProperties(this);
        if (Source is null)
        {
            throw new InvalidOperationException("Image.Source is required.");
        }
 
        // Initialize on first parameters set
        if (!_initialized)
        {
            Render();
            _initialized = true;
            return Task.CompletedTask;
        }
 
        if (!HasSameKey(previousSource, Source))
        {
            Render();
        }
 
        return Task.CompletedTask;
    }
 
    async Task IHandleAfterRender.OnAfterRenderAsync()
    {
        var source = Source;
        if (!IsInteractive || source is null)
        {
            return;
        }
 
        if (_currentSource != null && HasSameKey(_currentSource, source))
        {
            return;
        }
 
        CancelPreviousLoad();
        var token = ResetCancellationToken();
 
        _currentSource = source;
 
        try
        {
            await LoadImage(source, token);
        }
        catch (OperationCanceledException)
        {
        }
    }
 
    private void Render()
    {
        Debug.Assert(_renderHandle.IsInitialized);
 
        if (!_hasPendingRender)
        {
            _hasPendingRender = true;
            _renderHandle.Render(BuildRenderTree);
            _hasPendingRender = false;
        }
    }
 
    private void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.OpenElement(0, "img");
 
        if (!string.IsNullOrEmpty(_currentObjectUrl))
        {
            builder.AddAttribute(1, "src", _currentObjectUrl);
        }
 
        builder.AddAttribute(2, "data-blazor-image", "");
 
        var showInitialLoad = Source != null && _currentSource == null && string.IsNullOrEmpty(_currentObjectUrl) && !_hasError;
 
        if (IsLoading || showInitialLoad)
        {
            builder.AddAttribute(3, "data-state", "loading");
        }
        else if (_hasError)
        {
            builder.AddAttribute(3, "data-state", "error");
        }
 
        builder.AddMultipleAttributes(4, AdditionalAttributes);
        builder.AddElementReferenceCapture(5, elementReference => Element = elementReference);
 
        builder.CloseElement();
    }
 
    private struct ImageLoadResult
    {
        public bool Success { get; set; }
        public bool FromCache { get; set; }
        public string? ObjectUrl { get; set; }
        public string? Error { get; set; }
    }
 
    private async Task LoadImage(ImageSource source, CancellationToken cancellationToken)
    {
        if (!IsInteractive)
        {
            return;
        }
 
        _activeCacheKey = source.CacheKey;
 
        try
        {
            Log.BeginLoad(Logger, source.CacheKey);
 
            cancellationToken.ThrowIfCancellationRequested();
 
            using var streamRef = new DotNetStreamReference(source.Stream, leaveOpen: true);
 
            var result = await JSRuntime.InvokeAsync<ImageLoadResult>(
                "Blazor._internal.BinaryImageComponent.setImageAsync",
                cancellationToken,
                Element,
                streamRef,
                source.MimeType,
                source.CacheKey,
                source.Length);
 
            if (_activeCacheKey == source.CacheKey && !cancellationToken.IsCancellationRequested)
            {
                if (result.Success)
                {
                    _currentObjectUrl = result.ObjectUrl;
                    _hasError = false;
 
                    if (result.FromCache)
                    {
                        Log.CacheHit(Logger, source.CacheKey);
                    }
                    else
                    {
                        Log.StreamStart(Logger, source.CacheKey);
                    }
 
                    Log.LoadSuccess(Logger, source.CacheKey);
                }
                else
                {
                    _hasError = true;
                    Log.LoadFailed(Logger, source.CacheKey, new InvalidOperationException(result.Error ?? "Image load failed"));
                }
 
                Render();
            }
        }
        catch (OperationCanceledException)
        {
        }
        catch (Exception ex)
        {
            Log.LoadFailed(Logger, source.CacheKey, ex);
            if (_activeCacheKey == source.CacheKey && !cancellationToken.IsCancellationRequested)
            {
                _currentObjectUrl = null;
                _hasError = true;
                Render();
            }
        }
    }
 
    /// <inheritdoc />
    public ValueTask DisposeAsync()
    {
        if (!_isDisposed)
        {
            _isDisposed = true;
 
            // Cancel any pending operations
            CancelPreviousLoad();
        }
 
        return new ValueTask();
    }
 
    private void CancelPreviousLoad()
    {
        try
        {
            _loadCts?.Cancel();
        }
        catch
        {
        }
 
        _loadCts?.Dispose();
        _loadCts = null;
    }
 
    private CancellationToken ResetCancellationToken()
    {
        _loadCts = new CancellationTokenSource();
        return _loadCts.Token;
    }
 
    private static bool HasSameKey(ImageSource? a, ImageSource? b)
    {
        return a is not null && b is not null && string.Equals(a.CacheKey, b.CacheKey, StringComparison.Ordinal);
    }
 
    private static partial class Log
    {
        [LoggerMessage(1, LogLevel.Debug, "Begin load for key '{CacheKey}'", EventName = "BeginLoad")]
        public static partial void BeginLoad(ILogger logger, string cacheKey);
 
        [LoggerMessage(2, LogLevel.Debug, "Loaded image from cache for key '{CacheKey}'", EventName = "CacheHit")]
        public static partial void CacheHit(ILogger logger, string cacheKey);
 
        [LoggerMessage(3, LogLevel.Debug, "Streaming image for key '{CacheKey}'", EventName = "StreamStart")]
        public static partial void StreamStart(ILogger logger, string cacheKey);
 
        [LoggerMessage(4, LogLevel.Debug, "Image load succeeded for key '{CacheKey}'", EventName = "LoadSuccess")]
        public static partial void LoadSuccess(ILogger logger, string cacheKey);
 
        [LoggerMessage(5, LogLevel.Debug, "Image load failed for key '{CacheKey}'", EventName = "LoadFailed")]
        public static partial void LoadFailed(ILogger logger, string cacheKey, Exception exception);
 
        [LoggerMessage(6, LogLevel.Debug, "Revoked image URL on dispose", EventName = "RevokedUrl")]
        public static partial void RevokedUrl(ILogger logger);
    }
}