从 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 复制到剪贴板,复制的不是单个文本,而是相同内容的多个表示形式。在我的实验中,它是三种不同的类型。
我构建了两个不同的解析器:ExcelHtmlContentParser
和 ExcelTextContentParser
。关于 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
的内容。因为是内部调用EventCallback
StateHasChanged
,更新视图
文本解析器
在文本表示中,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
功能。用户需要允许它。因此我们看到了对话框。
我有一个托管的 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 复制到剪贴板,复制的不是单个文本,而是相同内容的多个表示形式。在我的实验中,它是三种不同的类型。
我构建了两个不同的解析器:ExcelHtmlContentParser
和 ExcelTextContentParser
。关于 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
的内容。因为是内部调用EventCallback
StateHasChanged
,更新视图
文本解析器
在文本表示中,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
功能。用户需要允许它。因此我们看到了对话框。