从 excel 复制到 blazor 应用程序传播 sheet

Copy From excel spread sheet into blazor app

我有一个托管的 Blazor WebAssembly 应用程序。

我需要有关如何从 excel 电子表格复制值并将其粘贴到应用程序中的策略或示例,最终目标是通过现有 API 将它们添加到我的数据库中.

所以这里的问题是:我应该将值粘贴到哪些组件中,我应该如何处理整个过程:

excel > 剪贴板 > 组件 > 保存在数据库中

实际上比我最初想象的要难。我创建了一个 repo。结果是这样的。

您可以 select Excel 中的任何元素,复制它们,聚焦 Blazor 页面的内容并粘贴。作为一个简单的视图,它显示在table.

让我们来看看解决方案。

Index.razor

@page "/"
<div class="form-group">
    <label for="parser">Parser type</label>
    <select class="form-control" id="parser" @bind="_parserType">
        <option value="text">Text</option>
        <option value="html">HTML</option>
    </select>
</div>

<PasteAwareComponent OnContentPasted="FillTable">
    @if (_excelContent.Any() == false)
    {
        <p>No Content</p>
    }
    else
    {
        <table class="table table-striped">
            @foreach (var row in _excelContent)
            {
                <tr>
                    @foreach (var cell in row)
                    {
                        <td>@cell</td>
                    }
                </tr>
            }
        </table>
    }
</PasteAwareComponent>

<button type="button" class="btn btn-primary" @onclick="@( () => _excelContent = new List<String[]>() )">Clear</button>

@code
{
    private IList<String[]> _excelContent = new List<String[]>();

    ...more content, explained later...
}

如果您将 Excel 中的 selection 复制到剪贴板,复制的不是单个文本,而是相同内容的多个表示形式。在我的实验中,它是三种不同的类型。

我构建了两个不同的解析器:ExcelHtmlContentParserExcelTextContentParser。关于 Excel 中单元格内容的多种不同可能性,我的实现才刚刚完成,应该被视为一种灵感。要查看两个解析器的运行情况,您可以通过更改 select 框中的值在它们之间进行选择。

PasteAwareComponent 处理与 Javascript 的交互。您可以在此组件内放置任何内容。如果此组件(或任何子组件)有焦点,粘贴事件将被正确处理。

<span @ref="_reference">
    @ChildContent
</span>

@code {
    private ElementReference _reference;

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public EventCallback<IEnumerable<IDictionary<String, String>>> OnContentPasted { get; set; }

    [JSInvokable("Pasted")]
    public async void raisePasteEvent(IEnumerable<IDictionary<String, String>> items)
    {
        await OnContentPasted.InvokeAsync(items);
    }

}

该组件处理与 javascript 的互操作。一旦粘贴事件发生,EventCallback<IEnumerable<IDictionary<String, String>>> OnContentPasted 就会被触发。

剪贴板中可能有不止一个元素。因此,我们需要处理一个集合IEnumerable<>。如前图所示,同一个剪贴板项目可以有多个表示。每个表示都有一个 mime 类型,如“text/plain”或“text/html”和值。这由 IDictionary<String, String> 表示,其中键是 mime 类型,值是内容。

在详细介绍 javascript 互操作之前,我们回到 Index 组件。

<PasteAwareComponent OnContentPasted="FillTable">
...
</PasteAwareComponent>

@code {
private async Task FillTable(IEnumerable<IDictionary<String, String>> content)
    {
        if (content == null || content.Count() != 1)
        {
            return;
        }

        var clipboardContent = content.ElementAt(0);
        IExcelContentParser parser = null;
        switch (_parserType)
        {
            case "text":
                parser = new ExcelTextContentParser();
                break;
            case "html":
                parser = new ExcelHtmlContentParser();
                break;
            default:
                break;
        }

        foreach (var item in clipboardContent)
        {
            if (parser.CanParse(item.Key) == false)
            {
                continue;
            }

            _excelContent = await parser.GetRows(item.Value);
        }
    }
}

索引组件在方法 FillTable中使用了这个事件回调。该方法检查剪贴板中是否有一个元素。基于 selection,选择解析器。如果所选解析器可以根据提供的 mime 类型解析每个表示,则在下一步中检查每个表示。如果找到正确的解析器,解析器就会发挥它的魔力,并更新字段 _excelContent 的内容。因为是内部调用EventCallbackStateHasChanged,更新视图

文本解析器 在文本表示中,Excel 使用 \r\n 作为行尾,每个单元格使用 \t,即使是空单元格。解析器逻辑非常简单。

public class ExcelTextContentParser : IExcelContentParser
{
    public String ValidMimeType { get; } = "text/plain";

    public Task<IList<String[]>> GetRows(String input) =>
        Task.FromResult<IList<String[]>>(input.Split("\r\n", StringSplitOptions.RemoveEmptyEntries).Select(x =>
            x.Split("\t").Select(y => y ?? String.Empty).ToArray()
        ).ToList());
}

我还没有测试如果内容更复杂,这种行为会发生怎样的变化。我猜 HTML 表示更 stable。因此,第二个解析器。

HTML 解析器

HTML 表示是 table。使用 <tr><td>。我使用库 AngleSharp 作为 HTML 解析器。

public class ExcelHtmlContentParser : IExcelContentParser
{
    public String ValidMimeType { get; } = "text/html";

    public async Task<IList<String[]>> GetRows(String input)
    {
        var context = BrowsingContext.New(Configuration.Default);
        var document = await context.OpenAsync(reg => reg.Content(input));

        var element = document.QuerySelector<IHtmlTableElement>("table");
        var result = element.Rows.Select(x => x.Cells.Select(y => y.TextContent).ToArray()).ToList();
        return result;
    }
}

我们将剪贴板内容加载为 HTML 文档,获取 table 并遍历所有行,并 select 编辑每一列。

** js 互操作 ***

@inject IJSRuntime runtime
@implements IDisposable

<span @ref="_reference">
    @ChildContent
</span>

@code {

    private ElementReference _reference;

    private DotNetObjectReference<PasteAwareComponent> _objectReference;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        if (firstRender == true)
        {
            _objectReference = DotNetObjectReference.Create(this);
            await runtime.InvokeVoidAsync("BlazorClipboadInterop.ListeningForPasteEvents", new Object[] { _reference, _objectReference });
        }
    }

    public void Dispose()
    {
        GC.SuppressFinalize(this);
        if (_objectReference != null)
        {
            _objectReference.Dispose();
        }
    }
}

PasteAwareComponent 组件覆盖 OnAfterRenderAsync 生命周期,以调用 js 互操作方法。它必须是 OnAfterRenderAsync 因为之前 HTML 引用不存在,我们需要引用来添加粘贴事件侦听器。当粘贴事件发生时,javascript 必须调用这个对象,所以我们需要创建一个 DotNetObjectReference 实例。我们实现了 IDisposable 接口并正确处理引用以防止内存泄漏。

最后一部分是 javascript 部分本身。我创建了一个名为 clipboard-interop.js 的文件并将其放在 wwwroot/js 文件夹中。

var BlazorClipboadInterop = BlazorClipboadInterop || {};

BlazorClipboadInterop.ListeningForPasteEvents = function (element, dotNetObject) {
    element.addEventListener('paste', function (e) { BlazorClipboadInterop.pasteEvent(e, dotNetObject) });
};

我们使用 HTML 引用为 'paste' 事件注册一个事件侦听器。在处理方法中,我们创建传递给 C# 方法的对象。

BlazorClipboadInterop.pasteEvent =
    async function (e, dotNetObject) {

        var data = await navigator.clipboard.read();
        var items = []; //is passed to C#

        for (let i = 0; i < data.length; i++) {
            var item = {};
            items.push(item);
            for (let j = 0; j < data[i].types.length; j++) {

                const type = data[i].types[j];

                const blob = await data[i].getType(type);
                if (blob) {

                    if (type.startsWith("text") == true) {
                        const content = await blob.text();
                        item[type] = content;
                    }
                    else {
                        item[type] = await BlazorClipboadInterop.toBase64(blob);
                    }
                }
            }
        }

        dotNetObject.invokeMethodAsync('Pasted', items);
        e.preventDefault();
    }

当我们在使用js互操作的时候,我们应该使用易于序列化的对象。对于真正的 blob,比如图像,它将是基于 64 位编码的字符串,否则只是内容。

该解决方案使用了 navigator.clipboard 功能。用户需要允许它。因此我们看到了对话框。