Call anonymous C# functions from JS in Blazor WASM
In this article, we will look at how we can call C# functions from a JS library in Blazor WASM. This can be useful to integrate with existing JS libraries that react to different events. This article is a continuation of a project made in our other article Wrapping JavaScript libraries in Blazor WebAssembly. We will recap what we did in this project first. But if you wish to follow the code examples yourself then it's recommended to read that article first.
Recap
In the previous article, we made a wrapper for the JS library Popper, which we could parse a typed parameter that fitted the signature for the constructor for the library. This included specifying JSON names for the different fields when serializing and creating a custom serializer that can serialize enums using Description attributes. This left us with the following view which is the result of us constructing the popper from C#.
Extending the Options class.
The signature for the options class is specified as follows.
type Options = {|
placement: Placement, // default "bottom"
modifiers: Array<$Shape<Modifier<any>>>, // default []
strategy: PositioningStrategy, // default "absolute"
onFirstUpdate?: ($Shape<State>) => void, // default undefined
|};
We have already added the Placement
enum to our model, but we would like to also add the onFirstUpdate
field.
public class Options
{
[JsonConverter(typeof(EnumDescriptionConverter<Placement>))]
[JsonPropertyName("placement")]
public Placement Placement { get; set; }
[JsonIgnore]
public Action<State> OnFirstUpdate { get; set; }
[JSInvokable("CallOnFirstUpdate")]
public void CallOnFirstUpdate(State state) => OnFirstUpdate?.Invoke(state);
}
public class State
{
[JsonConverter(typeof(EnumDescriptionConverter<Placement>))]
[JsonPropertyName("placement")]
public Placement Placement { get; set; }
}
The C# type for a method that doesn't return anything is an Action. We have specified that this Action
takes one argument, a State
object, as specified in the JS type. The State
object includes a lot of fields that could be useful like references to the objects used, the sizes and locations of the objects referenced, and our original options argument. But for now, we will just focus on the Placement
of the popper. We have specified that the OnFirstUpdate
should not be serialized by adding the [JsonIgnore] attribute. We have also added a method called CallOnFirstUpdate
to the Options
class which takes a State
and then uses that to invoke the action that is bound in the OnFirstUpdate
field. This field has the attribute JSInvokable
which specifies that this method can be called from JavaScript given an object reference to an instance of the Options
class. So we need to make this reference and parse it to our constructor.
public class Popper : IDisposable
{
private readonly IJSRuntime jSRuntime;
private DotNetObjectReference<Options> objRef;
public Popper(IJSRuntime jSRuntime)
{
this.jSRuntime = jSRuntime;
}
public async Task CreatePopperAsync(ElementReference reference, ElementReference popper, Options options = null)
{
objRef = DotNetObjectReference.Create(options);
await jSRuntime.InvokeVoidAsync("PopperWrapper.createPopper", reference, popper, options, objRef);
}
public void Dispose()
{
objRef?.Dispose();
}
}
We have added a DotNetObjectReference
to our Popper
class. This object reference is set just before we call the createPopper
method and it is parsed as an extra argument to this method. The DotNetObjectReference
is disposable and we want to be sure that we dispose of this field when we are done with it to avoid memory leaks. For this reason, we make the Popper
class disposable as well by implementing the IDisposable
interface and then disposing the object reference in the Dispose
method. The Popper object will automatically be disposed of once it is no longer used since we dependency inject it with AddTransient
.
Extending the JS wrapper.
We now need to extend the JavaScript wrapper for the Popper constructor that we made in a JavaScript file called PopperWrapper.js
.
window.PopperWrapper = {
createPopper: function (reference, popper, options, objRef) {
options.onFirstUpdate = (state) => {
const stateCopy = {
placement: state.placement
}
objRef.invokeMethodAsync('CallOnFirstUpdate', stateCopy)
};
if (options != null) {
Popper.createPopper(reference, popper, options);
} else {
Popper.createPopper(reference, popper);
}
}
}
We extend the createPopper
function so that it also takes the objRef
. Then before we pass the options
argument we set the onFirstUpdate
to a new function. The function takes a state and then creates a copy of this state so that we only have the field we need and use the objRef
to invoke the CallOnFirstUpdate
C# method. We strip the state for unused fields both to make the object lighter and because the state contains structural references that loop, which can not be serialized and deserialized in C# again.
Deserializing the Placement enum
In our previous article we only made a JsonConverter
for writing an enum to JSON, but not to deserialize an enum from JSON again. We need this now because the state contains the placement.
class EnumDescriptionConverter<T> : JsonConverter<T> where T : IComparable, IFormattable, IConvertible
{
// Previous implementation for the Write method is omitted.
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string jsonValue = reader.GetString();
foreach (var fi in typeToConvert.GetFields())
{
DescriptionAttribute description = (DescriptionAttribute)fi.GetCustomAttribute(typeof(DescriptionAttribute), false);
if (description != null)
{
if (description.Description == jsonValue)
{
return (T)fi.GetValue(null);
}
}
}
throw new JsonException($"string {jsonValue} was not found as a description in the enum {typeToConvert}");
}
}
The Read method is called when System.Text.Json serializes from JSON to C# objects. We wish to return an enum from this method and find out which enum it is by using the reader that is supplied when the method is called. We first read the field as a string which would be e.g. "Bottom"
. Then we go through all the attributes of the enum that we wish to convert to and try to cast the attributes to DescriptionAttribute
s. Then we check if the attribute was actually a DescriptionAttribute
by making a null check. If it was then we check if it is equal to our string version of the enum that we had read. In the end, the associated field is cast to our enum type and returned. If no field was found with a matching description then an exception is thrown.
Parsing a function to the constructor.
Now we just need to test this by adding a function as an argument in our test.
@page "/"
@inject Popper Popper
<span id="reference" @ref=reference style="background-color:blue;">Reference</span>
<span id="popper" @ref=popper style="background-color:red;">@popperString</span>
@code {
protected string popperString;
protected ElementReference reference;
protected ElementReference popper;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var options = new Options()
{
Placement = Placement.Left,
OnFirstUpdate = (state) =>
{
popperString = state.Placement.ToString();
StateHasChanged();
}
};
await Popper.CreatePopperAsync(reference, popper, options);
}
}
}
We have added a string called popperString
to our existing example. We use this string in our popper to show where the popper aligned to. This can be useful because Popper doesn't necessarily align with what was requested if there wasn't space for the popper in the requested place. Another useful scenario could have been if we parsed the location of the popper if we wanted to place some animation or canvas drawing manually. Then, we add an anonymous function to the OnFirstUpdate
field. We make this function update the popperString
to the placement from the state. When there is space we will see that the popper says Left
.
But if there is not any place to the left, e.g. if the website is viewed on a mobile, it will align to the right instead as it can be seen here both visually and with the text.
Conclusion
We have now added functionality that enables an existing JS library to call C# code when reacting to different events. This wrapper is very versatile since any C# function can be bound to the OnFirstUpdate
field that we specified, which opens up for the possibility that this code can be used in multiple places with ease. There are still more functionalities that could be added to this wrapper to make it even more useful, such as references to the JavaScript objects. A nice way to reference JavaScript objects has been added in .NET 5 and we will maybe touch more on this in a future article. If you have any questions for the code in this article or if you have any feedback, then please reach out.