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.

Call anonymous C# functions from JS in Blazor WASM

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#.

The popper view

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 DescriptionAttributes. 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.

Updated placement

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.

Aligned right

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.

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website