Skip to content

Commit 5f2e050

Browse files
Code Cleanup, fixed bug in File creation, and extended README with samples of use.
1 parent 625734f commit 5f2e050

15 files changed

Lines changed: 329 additions & 49 deletions

File tree

README.md

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,136 @@ A Blazor wrapper for the browser [File API](https://www.w3.org/TR/FileAPI/)
1010

1111
The API provides a standard for representing file objects in the browser and ways to select them and access their data. One of the most used interfaces that is at the core of this API is the [Blob](https://www.w3.org/TR/FileAPI/#dfn-Blob) interface. This project implements a wrapper around the API for Blazor so that we can easily and safely interact with files in the browser.
1212

13-
**The wrapper is still being developed so the API Coverage is very limited currently.**
14-
1513
# Demo
1614
The sample project can be demoed at https://kristofferstrube.github.io/Blazor.FileAPI/
1715

1816
On each page you can find the corresponding code for the example in the top right corner.
1917

20-
On the *API Coverage Status* page you can get an overview over what parts of the API we support currently.
18+
On the *API Coverage Status* page you can get an overview over what parts of the API we support currently.
19+
20+
# Getting Started
21+
## Prerequisites
22+
You need to install .NET 6.0 or newer to use the library.
23+
24+
[Download .NET 6](https://dotnet.microsoft.com/download/dotnet/6.0)
25+
26+
## Installation
27+
You can install the package via NuGet with the Package Manager in your IDE or alternatively using the command line:
28+
```bash
29+
dotnet add package KristofferStrube.Blazor.FileAPI
30+
```
31+
32+
# Usage
33+
The package can be used in Blazor WebAssembly and Blazor Server projects.
34+
## Import
35+
You also need to reference the package in order to use it in your pages. This can be done in `_Import.razor` by adding the following.
36+
```razor
37+
@using KristofferStrube.Blazor.FileAPI
38+
```
39+
40+
## Creating wrapper instances
41+
Most of this library is wrapper classes which can be instantiated from your code using the static `Create` and `CreateAsync` methods on the wrapper classes.
42+
An example could be to create an instance of a `Blob` that contains the text `"Hello World!"` and gets its `Size` and `Type`, read it as a `ReadableStream`, read as text directly, and slice it into a new `Blob` like this.
43+
```csharp
44+
Blob blob = await Blob.CreateAsync(
45+
JSRuntime,
46+
blobParts: new BlobPart[] {
47+
new("Hello "),
48+
new(new byte[] { 0X57, 0X6f, 0X72, 0X6c, 0X64, 0X21 })
49+
},
50+
options: new() { Type = "text/plain" }
51+
);
52+
ulong size = await blob.GetSizeAsync(); // 12
53+
string type = await blob.GetTypeAsync(); // "text/plain"
54+
ReadableStream stream = await blob.StreamAsync();
55+
string text = await blob.TextAsync(); // "Hello World!"
56+
Blob worldBlob = await blob.SliceAsync(6, 11); // Blob containing "World"
57+
```
58+
All creator methods take an `IJSRuntime` instance as the first parameter. The above sample will work in both Blazor Server and Blazor WebAssembly. If we only want to work with Blazor WebAssembly we can use the `InProcess` variant of the wrapper class. This is equivalent to the relationship between `IJSRuntime` and `IJSInProcessRuntime`. We can recreate the above sample using the `BlobInProcess` which will simplify some of the methods we can call on the `Blob` and how we access attributes.
59+
```csharp
60+
BlobInProcess blob = await BlobInProcess.CreateAsync(
61+
JSRuntime,
62+
blobParts: new BlobPart[] {
63+
new("Hello "),
64+
new(new byte[] { 0X57, 0X6f, 0X72, 0X6c, 0X64, 0X21 })
65+
},
66+
options: new() { Type = "text/plain" }
67+
);
68+
ulong size = blob.Size; // 12
69+
string type = blob.Type; // "text/plain"
70+
ReadableStreamInProcess stream = await blob.StreamAsync();
71+
string text = await blob.TextAsync(); // "Hello World!"
72+
BlobInProcess worldBlob = blob.Slice(6, 11); // BlobInProcess containing "World"
73+
```
74+
Some of the methods wrap a `Promise` so even in the `InProcess` variant we need to await it like we see for `TextAsync` above.
75+
76+
If you have an `IJSObjectReference` or an `IJSInProcessObjectReference` for a type equivalent to one of the classes wrapped in this package then you can construct a wrapper for it using another set of overloads of the static `Create` and `CreateAsync` methods on the appropriate class. In the below example we create wrapper instances from existing JS references to a `File` object.
77+
```csharp
78+
// Blazor Server compatible.
79+
IJSObjectReference jSFile; // JS Reference from other package or your own JSInterop.
80+
File file = File.Create(JSRuntime, jSFile)
81+
82+
// InProcess only supported in Blazor WebAssembly.
83+
IJSInProcessObjectReference jSFileInProcess; // JS Reference from other package or your own JSInterop.
84+
FileInProcess fileInProcess = await File.CreateAsync(JSRuntime, jSFileInProcess)
85+
```
86+
87+
## Add to service collection
88+
We have a single service in this package that wraps the `URL` interface. An easy way to make the service available in all your pages is by registering it in the `IServiceCollection` so that it can be dependency injected in the pages that need it. This is done in `Program.cs` by adding the following before you build the host and run it.
89+
```csharp
90+
var builder = WebAssemblyHostBuilder.CreateDefault(args);
91+
builder.RootComponents.Add<App>("#app");
92+
builder.RootComponents.Add<HeadOutlet>("head::after");
93+
94+
// Other services are added.
95+
96+
builder.Services.AddURLService();
97+
98+
await builder.Build().RunAsync();
99+
```
100+
## Inject in page
101+
Then the service can be injected in a page and be used to create Blob URLs and revoke them like so:
102+
```razor
103+
@implements IAsyncDisposable
104+
@inject IURLService URL;
105+
106+
<img src="@blobURL" alt="Some blob as image" />
107+
108+
@code {
109+
private string blobURL = "";
110+
111+
protected override async Task OnInitializedAsync()
112+
{
113+
Blob blob; // We have some blob from somewhere.
114+
115+
blobURL = await URL.CreateObjectURLAsync(blob);
116+
}
117+
118+
public async ValueTask DisposeAsync()
119+
{
120+
await URL.RevokeObjectURLAsync(blobURL);
121+
}
122+
}
123+
```
124+
You can likewise add the `InProcess` variant of the service (`IURLServiceInProcess`) using the `AddURLServiceInProcess` extension method which is only supported in Blazor WebAssembly projects.
125+
126+
# Issues
127+
Feel free to open issues on the repository if you find any errors with the package or have wishes for features.
128+
129+
# Related repositories
130+
This project uses this package to return a rich `ReadableStream` from the `StreamAsync` method on a `Blob`.
131+
- https://github.com/KristofferStrube/Blazor.Streams
132+
133+
This project is going to be used in this package to return a rich `File` object when getting the `File` from a `FileSystemFileHandle` and when writing a `Blob` to a `FileSystemWritableFileSystem`.
134+
- https://github.com/KristofferStrube/Blazor.FileSystemAccess
135+
136+
This project uses a combination of the two styles present in the two above packages which both eventually will go more towards the style present in this project.
137+
138+
# Related articles
139+
This repository was build with inspiration and help from the following series of articles:
140+
141+
- [Wrapping JavaScript libraries in Blazor WebAssembly/WASM](https://blog.elmah.io/wrapping-javascript-libraries-in-blazor-webassembly-wasm/)
142+
- [Call anonymous C# functions from JS in Blazor WASM](https://blog.elmah.io/call-anonymous-c-functions-from-js-in-blazor-wasm/)
143+
- [Using JS Object References in Blazor WASM to wrap JS libraries](https://blog.elmah.io/using-js-object-references-in-blazor-wasm-to-wrap-js-libraries/)
144+
- [Blazor WASM 404 error and fix for GitHub Pages](https://blog.elmah.io/blazor-wasm-404-error-and-fix-for-github-pages/)
145+
- [How to fix Blazor WASM base path problems](https://blog.elmah.io/how-to-fix-blazor-wasm-base-path-problems/)

samples/KristofferStrube.Blazor.FileAPI.WasmExample/Pages/Index.razor

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
@page "/"
2-
@implements IAsyncDisposable
2+
@implements IDisposable
33

44
@inject IJSRuntime JSRuntime
55
@inject HttpClient HttpClient
6-
@inject URLService URL
6+
@inject IURLServiceInProcess URL
77

88
<PageTitle>FileAPI - Index</PageTitle>
99

@@ -35,11 +35,11 @@ content type: @file?.Type
3535
fileName: imageName,
3636
options: new() { Type = "image/png", LastModified = DateTime.Now }
3737
);
38-
blobURL = await URL.CreateObjectURLAsync(file);
38+
blobURL = URL.CreateObjectURL(file);
3939
}
4040

41-
public async ValueTask DisposeAsync()
41+
public void Dispose()
4242
{
43-
await URL.RevokeObjectURLAsync(blobURL);
43+
URL.RevokeObjectURL(blobURL);
4444
}
4545
}

samples/KristofferStrube.Blazor.FileAPI.WasmExample/Program.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
using KristofferStrube.Blazor.FileAPI.WasmExample;
33
using Microsoft.AspNetCore.Components.Web;
44
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
5-
using Microsoft.JSInterop;
65

76
var builder = WebAssemblyHostBuilder.CreateDefault(args);
87
builder.RootComponents.Add<App>("#app");
98
builder.RootComponents.Add<HeadOutlet>("head::after");
109

1110
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
12-
builder.Services.AddScoped<URLService>();
11+
builder.Services.AddURLServiceInProcess();
1312

1413
await builder.Build().RunAsync();

src/KristofferStrube.Blazor.FileAPI/Blob.InProcess.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.JSInterop;
1+
using KristofferStrube.Blazor.Streams;
2+
using Microsoft.JSInterop;
23

34
namespace KristofferStrube.Blazor.FileAPI;
45

@@ -35,8 +36,8 @@ public static async Task<BlobInProcess> CreateAsync(IJSRuntime jSRuntime, IJSInP
3536
object?[]? jsBlobParts = blobParts?.Select<BlobPart, object?>(blobPart => blobPart.type switch
3637
{
3738
BlobPartType.BufferSource => blobPart.byteArrayPart,
38-
BlobPartType.Blob => blobPart.stringPart,
39-
_ => blobPart.blobPart?.JSReference
39+
BlobPartType.Blob => blobPart.blobPart?.JSReference,
40+
_ => blobPart.stringPart
4041
})
4142
.ToArray();
4243
IJSInProcessObjectReference jSInstance = await inProcesshelper.InvokeAsync<IJSInProcessObjectReference>("constructBlob", jsBlobParts, options);
@@ -55,6 +56,16 @@ internal BlobInProcess(IJSRuntime jSRuntime, IJSInProcessObjectReference inProce
5556
JSReference = jSReference;
5657
}
5758

59+
/// <summary>
60+
/// Creates a new <see cref="ReadableStreamInProcess"/> from the <see cref="Blob"/>.
61+
/// </summary>
62+
/// <returns>A new wrapper for a <see cref="ReadableStreamInProcess"/></returns>
63+
public new async Task<ReadableStreamInProcess> StreamAsync()
64+
{
65+
IJSInProcessObjectReference jSInstance = JSReference.Invoke<IJSInProcessObjectReference>("stream");
66+
return await ReadableStreamInProcess.CreateAsync(jSRuntime, jSInstance);
67+
}
68+
5869
/// <summary>
5970
/// The size of this blob.
6071
/// </summary>
@@ -73,12 +84,12 @@ internal BlobInProcess(IJSRuntime jSRuntime, IJSInProcessObjectReference inProce
7384
/// <param name="start">The start index of the range. If <see langword="null"/> or negative then <c>0</c> is assumed.</param>
7485
/// <param name="end">The start index of the range. If <see langword="null"/> or larger than the size of the original <see cref="Blob"/> then the size of the original <see cref="Blob"/> is assumed.</param>
7586
/// <param name="contentType">An optional MIME type of the new <see cref="Blob"/>. If <see langword="null"/> then the MIME type of the original <see cref="Blob"/> is used.</param>
76-
/// <returns>A new <see cref="Blob"/>.</returns>
77-
public Blob Slice(long? start = null, long? end = null, string? contentType = null)
87+
/// <returns>A new <see cref="BlobInProcess"/>.</returns>
88+
public BlobInProcess Slice(long? start = null, long? end = null, string? contentType = null)
7889
{
7990
start ??= 0;
8091
end ??= (long)Size;
81-
IJSObjectReference jSInstance = JSReference.Invoke<IJSObjectReference>("slice", start, end, contentType);
82-
return new Blob(jSRuntime, jSInstance);
92+
IJSInProcessObjectReference jSInstance = JSReference.Invoke<IJSInProcessObjectReference>("slice", start, end, contentType);
93+
return new BlobInProcess(jSRuntime, inProcessHelper, jSInstance);
8394
}
8495
}

src/KristofferStrube.Blazor.FileAPI/Blob.cs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ public static async Task<Blob> CreateAsync(IJSRuntime jSRuntime, IList<BlobPart>
3232
object?[]? jsBlobParts = blobParts?.Select<BlobPart, object?>(blobPart => blobPart.type switch
3333
{
3434
BlobPartType.BufferSource => blobPart.byteArrayPart,
35-
BlobPartType.Blob => blobPart.stringPart,
36-
_ => blobPart.blobPart?.JSReference
35+
BlobPartType.Blob => blobPart.blobPart?.JSReference,
36+
_ => blobPart.stringPart
3737
})
3838
.ToArray();
3939
IJSObjectReference jSInstance = await helper.InvokeAsync<IJSObjectReference>("constructBlob", jsBlobParts, options);
@@ -92,16 +92,6 @@ public async Task<ReadableStream> StreamAsync()
9292
return ReadableStream.Create(jSRuntime, jSInstance);
9393
}
9494

95-
/// <summary>
96-
/// Creates a new <see cref="ReadableStreamInProcess"/> from the <see cref="Blob"/>.
97-
/// </summary>
98-
/// <returns>A new wrapper for a <see cref="ReadableStreamInProcess"/> which can access members and call non-promise methods synchronously.</returns>
99-
public async Task<ReadableStreamInProcess> StreamInProcessAsync()
100-
{
101-
IJSInProcessObjectReference jSInstance = await JSReference.InvokeAsync<IJSInProcessObjectReference>("stream");
102-
return await ReadableStreamInProcess.CreateAsync(jSRuntime, jSInstance);
103-
}
104-
10595
/// <summary>
10696
/// The content of the blob as a string.
10797
/// </summary>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.JSInterop;
3+
4+
namespace KristofferStrube.Blazor.FileAPI;
5+
6+
public static class IServiceCollectionExtensions
7+
{
8+
public static IServiceCollection AddURLService(this IServiceCollection serviceCollection)
9+
{
10+
return serviceCollection.AddScoped<IURLService, URLService>();
11+
}
12+
13+
public static IServiceCollection AddURLServiceInProcess(this IServiceCollection serviceCollection)
14+
{
15+
return serviceCollection.AddScoped<IURLServiceInProcess>(sp => new URLServiceInProcess((IJSInProcessRuntime)sp.GetRequiredService<IJSRuntime>()));
16+
}
17+
}

src/KristofferStrube.Blazor.FileAPI/File.InProcess.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.JSInterop;
1+
using KristofferStrube.Blazor.Streams;
2+
using Microsoft.JSInterop;
23

34
namespace KristofferStrube.Blazor.FileAPI;
45

@@ -56,6 +57,16 @@ internal FileInProcess(IJSRuntime jSRuntime, IJSInProcessObjectReference inProce
5657
JSReference = jSReference;
5758
}
5859

60+
/// <summary>
61+
/// Creates a new <see cref="ReadableStreamInProcess"/> from the <see cref="Blob"/>.
62+
/// </summary>
63+
/// <returns>A new wrapper for a <see cref="ReadableStreamInProcess"/></returns>
64+
public new async Task<ReadableStreamInProcess> StreamAsync()
65+
{
66+
IJSInProcessObjectReference jSInstance = JSReference.Invoke<IJSInProcessObjectReference>("stream");
67+
return await ReadableStreamInProcess.CreateAsync(jSRuntime, jSInstance);
68+
}
69+
5970
/// <summary>
6071
/// The size of this blob.
6172
/// </summary>
@@ -68,6 +79,21 @@ internal FileInProcess(IJSRuntime jSRuntime, IJSInProcessObjectReference inProce
6879
/// <returns>The MIME type of this blob.</returns>
6980
public string Type => inProcessHelper.Invoke<string>("getAttribute", JSReference, "type");
7081

82+
/// <summary>
83+
/// Gets some range of the content of a <see cref="Blob"/> as a new <see cref="Blob"/>.
84+
/// </summary>
85+
/// <param name="start">The start index of the range. If <see langword="null"/> or negative then <c>0</c> is assumed.</param>
86+
/// <param name="end">The start index of the range. If <see langword="null"/> or larger than the size of the original <see cref="Blob"/> then the size of the original <see cref="Blob"/> is assumed.</param>
87+
/// <param name="contentType">An optional MIME type of the new <see cref="Blob"/>. If <see langword="null"/> then the MIME type of the original <see cref="Blob"/> is used.</param>
88+
/// <returns>A new <see cref="BlobInProcess"/>.</returns>
89+
public BlobInProcess Slice(long? start = null, long? end = null, string? contentType = null)
90+
{
91+
start ??= 0;
92+
end ??= (long)Size;
93+
IJSInProcessObjectReference jSInstance = JSReference.Invoke<IJSInProcessObjectReference>("slice", start, end, contentType);
94+
return new BlobInProcess(jSRuntime, inProcessHelper, jSInstance);
95+
}
96+
7197
/// <summary>
7298
/// The name of the file including file extension.
7399
/// </summary>

0 commit comments

Comments
 (0)