File: Media\FileDownload.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 Microsoft.AspNetCore.Components.Rendering;
using Microsoft.JSInterop;
 
namespace Microsoft.AspNetCore.Components.Web.Media;
 
/* This is equivalent to a .razor file containing:
 *
 * <a data-blazor-file-download
 *    href="javascript:void(0)"
 *    data-state=@(IsLoading ? "loading" : _hasError ? "error" : null)
 *    @attributes="AdditionalAttributes"
 *    @ref="Element"
 *    @onclick="OnClickAsync">@(Text ?? "Download")</a>
 *
 */
/// <summary>
/// A component that provides an anchor element to download the provided media source.
/// </summary>
public sealed class FileDownload : MediaComponentBase
{
    /// <summary>
    /// File name to suggest to the browser for the download. Must be provided.
    /// </summary>
    [Parameter, EditorRequired] public string FileName { get; set; } = default!;
 
    /// <summary>
    /// Provides custom link text. Defaults to "Download".
    /// </summary>
    [Parameter] public string? Text { get; set; }
 
    internal override string TargetAttributeName => string.Empty; // Not used – object URL not tracked for downloads.
 
    /// <inheritdoc />
    internal override bool ShouldAutoLoad => false;
 
    /// <summary>
    /// Allows customizing the rendering of the file download component.
    /// </summary>
    [Parameter] public RenderFragment<FileDownloadContext>? ChildContent { get; set; }
 
    private protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        if (ChildContent is not null)
        {
            var context = new FileDownloadContext
            {
                IsLoading = IsLoading,
                HasError = _hasError,
                FileName = FileName,
            };
            context.Initialize(r => Element = r, EventCallback.Factory.Create(this, OnClickAsync));
            builder.AddContent(0, ChildContent, context);
            return;
        }
 
        // Default rendering
        builder.OpenElement(0, "a");
 
        builder.AddAttribute(1, "data-blazor-file-download", "");
 
        if (IsLoading)
        {
            builder.AddAttribute(2, "data-state", "loading");
        }
        else if (_hasError)
        {
            builder.AddAttribute(2, "data-state", "error");
        }
 
        builder.AddAttribute(3, "href", "javascript:void(0)");
        builder.AddAttribute(4, "onclick", EventCallback.Factory.Create(this, OnClickAsync));
 
        IEnumerable<KeyValuePair<string, object>>? attributesToRender = AdditionalAttributes;
        if (AdditionalAttributes is not null && AdditionalAttributes.ContainsKey("href"))
        {
            var copy = new Dictionary<string, object>(AdditionalAttributes.Count);
            foreach (var kvp in AdditionalAttributes)
            {
                if (kvp.Key == "href")
                {
                    continue;
                }
                copy.Add(kvp.Key, kvp.Value!);
            }
            attributesToRender = copy;
        }
        builder.AddMultipleAttributes(6, attributesToRender);
 
        builder.AddElementReferenceCapture(7, elementReference => Element = elementReference);
 
        builder.AddContent(8, Text ?? "Download");
 
        builder.CloseElement();
    }
 
    private async Task OnClickAsync()
    {
        if (Source is null || !IsInteractive || string.IsNullOrWhiteSpace(FileName))
        {
            return;
        }
 
        CancelPreviousLoad();
        var token = ResetCancellationToken();
        _hasError = false;
        _currentSource = Source;
        Render();
 
        var source = Source;
 
        using var streamRef = new DotNetStreamReference(source.Stream, leaveOpen: true);
 
        try
        {
            var result = await JSRuntime.InvokeAsync<bool>(
                "Blazor._internal.BinaryMedia.downloadAsync",
                token,
                Element,
                streamRef,
                source.MimeType,
                source.Length,
                FileName);
 
            if (!token.IsCancellationRequested)
            {
                _currentSource = null;
                if (!result)
                {
                    _hasError = true;
                }
                Render();
            }
        }
        catch (OperationCanceledException)
        {
            _currentSource = null;
            Render();
        }
        catch
        {
            _currentSource = null;
            _hasError = true;
            Render();
        }
    }
}
 
/// <summary>
/// Extended media context for the FileDownload component providing click invocation and filename.
/// </summary>
public sealed class FileDownloadContext : MediaContext
{
    /// <summary>
    /// Gets the file name suggested to the browser when initiating the download.
    /// </summary>
    public string FileName { get; internal set; } = string.Empty;
    private EventCallback _onClick;
    internal void Initialize(Action<ElementReference> capture, EventCallback onClick)
    {
        base.Initialize(capture);
        _onClick = onClick;
    }
    /// <summary>
    /// Initiates the download by invoking the underlying click handler of the parent.
    /// </summary>
    public Task InvokeAsync() => _onClick.InvokeAsync();
}