Open
Description
Is there an existing issue for this?
- I have searched the existing issues
Is your feature request related to a problem? Please describe the problem.
It would be helpful to have an easy way to import JS modules - for any given fooModule.js
to have an adapter FooModule.cs
.
I wrote some code for that, and then I heard @SteveSandersonMS say he'd like something like that in v8, and he also published his code. Mine is very similar, below.
I'm opening this tracking issue so the idea doesn't get lost. It would be a nice addition.
Describe the solution you'd like
I've linked Steve's code above. Mine's below.
JsModule.cs
:
public abstract class JsModule : IAsyncDisposable
{
private bool _isDisposed = false;
// From `dotnet new razorclasslib` template. It's nice because it
// avoids the loading hit when the user does not create a need for
// the JS module.
private readonly Lazy<Task<IJSObjectReference>> _jsModuleProvider;
protected JsModule(IJSRuntime jsRuntime, string? modulePath = null)
{
if (jsRuntime == null) throw new ArgumentNullException(nameof(jsRuntime), "Argument was null.");
if (modulePath != null && string.IsNullOrWhiteSpace(modulePath)) throw new ArgumentException("Argument was empty or whitespace.", nameof(modulePath));
var path = modulePath ?? GetPathOfJsModule();
_jsModuleProvider = new(
() => jsRuntime.InvokeAsync<IJSObjectReference>("import", path)
.AsTask()
);
}
private string GetPathOfJsModule()
{
var nameAssembly = this.GetType().Assembly.GetName().Name; // e.g. "MyCompany.MyProject"
var nameType = this.GetType().Name; // e.g. "MyJsModule"
var nameScript = Camelise(nameType) + ".js"; // => "myJsModule.js"
var path = $"./_content/{nameAssembly}/{nameScript}"; // => "./_content/MyCompany.MyProject/myJsModule.js"
Debug.Assert(nameAssembly != null);
return path;
}
private string Camelise(string input) => input[0].ToString().ToLowerInvariant() + input[1..];
protected async ValueTask InvokeVoidAsync(string jsFunctionName, params object[]? args)
{
if (string.IsNullOrWhiteSpace(jsFunctionName)) throw new ArgumentNullException(nameof(jsFunctionName));
var module = await _jsModuleProvider.Value;
await module.InvokeVoidAsync(Camelise(jsFunctionName), args);
}
protected async ValueTask<T> InvokeAsync<T>(string jsFunctionName, params object[]? args)
{
if (string.IsNullOrWhiteSpace(jsFunctionName)) throw new ArgumentNullException(nameof(jsFunctionName));
var module = await _jsModuleProvider.Value;
return await module.InvokeAsync<T>(Camelise(jsFunctionName), args);
}
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
GC.SuppressFinalize(this);
}
protected virtual async ValueTask DisposeAsyncCore() {
if (_isDisposed) return;
if (_jsModuleProvider.IsValueCreated)
{
var module = await _jsModuleProvider.Value;
await module.DisposeAsync().ConfigureAwait(false);
}
_isDisposed = true;
}
}
- I added the async dispose pattern
- To make it easy for users, I inferred the module file's path, to match that of the c# class; many people do it that way, e.g.
MyProject/Foo/Bar/SomeModule.cs
and its correspondingMyProject/wwwroot/Foo/Bar/someModule.js
- But user can supply a custom path (into ctor) to override that assumption
- The js module is assumed to be camel cased, as is the convention for JS
- The JS object reference is lazy, as is done in the
dotnet new razorclasslib
template; this avoids an unnecessary load when the user doesn't use that module - I wanted to use
[CallerMemberName] string jsFunctionName = ""
to further simplify callsites, but couldn't find a way to do that because of theparams
array
MyJsModule.cs
:
public sealed class MyJsModule : JsModule
{
public MyJsModule(IJSRuntime jsRuntime) : base(jsRuntime /*, custom js file path here if needed*/) { }
public async ValueTask Foo(string arg)
{
if (string.IsNullOrWhiteSpace(arg)) throw new ArgumentNullException(nameof(arg));
await InvokeVoidAsync(nameof(Foo), arg);
}
public async ValueTask<int> Bar(string arg1, string arg2) {
if (string.IsNullOrWhiteSpace(arg1)) throw new ArgumentNullException(nameof(arg1));
if (string.IsNullOrWhiteSpace(arg2)) throw new ArgumentNullException(nameof(arg2));
await InvokeVoidAsync<int>(nameof(Bar), arg1, arg2);
}
}
- I use
nameof(MethodName)
to ease refactoring/renaming
This works nicely, and greatly simplifies subclasses.
Additional context
No.