JavaScript Interop in Blazor - Executing JavaScript From C#

One of the main features offered by Blazor is the ability to write C# code that executes in the browser. While this reduces the need for JavaScript in your UI layer, it does not necessarily negate it entirely. There are limitations to the things that WebAssembly modules can do. For example, WebAssembly modules do not have access to the browser's DOM, nor any Web APIs such as local storage, FileReader, Geolocation etc. If you want to make use of these APIs, or indeed if you want to make use of existing JavaScript libraries such as a proven, full featured rich text editor, you need to create a layer between Blazor and JavaScript.

Within Blazor development, the foundation for this layer is known as JavaScript Interop. It is two-way, in that it enables communication from C# code to JavaScript, and back again from JavaScript into C# code.

On this page, you can learn about communicating with JavaScript from C# code. For guidance on communicating with C# from Javascript, see JavaScript Interop in Blazor - Executing C# From JavaScript.

Executing JavaScript From C#

The following example demonstrates the use of the browser's GeoLocation API, which is one of those APIs that WebAssembly does not have access to. First, the contents of a JavaScript file named location.js, placed in a folder named js:

window.getLocation = () => {
    navigator.geolocation.getCurrentPosition((position) => {
        console.log(`Latitude: ${position.coords.latitude}, Longitude: ${position.coords.longitude}`);
    });
}

The getLocation method is declared in global scope, which in the browser is the window object. This is required by Blazor. The function uses the Geolocation API to obtain the current position and then writes the latitude and longitude to the browser console.

In a Blazor application, you reference your JavaScript file in the index.html file that hosts the application:

<body>
    <app>Loading...</app>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
    <script src="js/location.js"></script>
</body>

The following Razor component features a button with a click event handler:

@page "/jsinterop-demo"
@inject IJSRuntime JSRuntime

<h3>JsInterop Example</h3>
<button @onclick=getLocationFromJS>Get Location</button>

@code {
    async Task getLocationFromJS()
    {
        await JSRuntime.InvokeVoidAsync("getLocation");
    }
}

An instance of IJSRuntime is injected into the component. IJSRuntime provides the interop layer between C# and JavaScript and is included as one of the default pre-registered services in a Blazor application. The InvokeVoidAsync method invokes the JavaScript method passed into it asynchronously. The function identifier passed in to the method is relative to the window scope, so the window part is omitted from the identifier.

The getLocationFromJS method passed in to the button's onclick event handler uses IJSRuntime to call the getLocation function which is declared in the location.js file.

Returning Values From JavaScript

The IJSRuntime interface includes an InvokeAsync<TValue> method for managing values returned from JavaScript method calls. The type that you pass to TValue is a C# representation of the data returned by JavaScript. It can be as simple as a bool, string or int, or something more complex. In the next example, the getLocation method is modified to return the string that it logged to the console:

window.getLocation = () => {
    return new Promise((resolve) => {
        navigator.geolocation.getCurrentPosition((position) => {
            resolve(`Latitude: ${position.coords.latitude}, Longitude: ${position.coords.longitude}`);
        });
    });
}

This time the method uses a Promise to return the value because the getCurrentPosition is asynchronous. This is a very basic illustration of the use of Promises. It doesn't include any error handling, for example.

The Razor component is modified too:

@page "/jsinterop-demo"
@inject IJSRuntime JSRuntime

<h3>JsInterop Example</h3>
<button @onclick=getLocationFromJS>Get Location</button>

<p>@location</p>

@code {
    string location { get; set; }
    async Task getLocationFromJS()
    {
        location = await JSRuntime.InvokeAsync<string>("getLocation");
    }
}

This time, the value of the returned string is rendered within the component:

Blazor JS Interop

Returning Complex Values

In the next example, the getLocation method returns an object with two properties, latitude and longitude:

window.getLocation = () => {
    return new Promise((resolve) => {
        navigator.geolocation.getCurrentPosition(
            position => {
                resolve({
                    latitude: position.coords.latitude,
                    longitude: position.coords.longitude
                });
            }
        )
    });
}

The Razor component includes a Geolocation class, having two properties that match on name with the object returned from JavaScript, and being of a suitable datatype:

@page "/jsinterop"
@inject IJSRuntime JSRuntime

<h3>JsInterop Example</h3>
<button @onclick=getLocationFromJS>Get Location</button>
@if (location != null)
{
    <p>
        Latitude: @location.Latitude<br>
        Longitude: @location.Longitude
    </p>
}
@code {
    Geolocation location { get; set; }
    async Task getLocationFromJS()
    {
        location = await JSRuntime.InvokeAsync<Geolocation>("getLocation");
    }

    class Geolocation
    {
        public double Latitude { get; set; }
        public double Longitude { get; set; }
    }
}

The location property is now a Geolocation type instead of a string. A null check is performed in the HTML part of the component and if that passes, the property values are rendered to the browser.

Passing Parameters To JavaScript

Both the InvokeVoidAsync and InvokeAsync<TValue> methods accept parameters to be passed to the JavaScript function that they invoke.

Parameters can be simple types such as a string or int, or complex types so long as they can be serialised to JSON by the default serialiser (System.Text.Json). At the moment, that means that complex types cannot contain circular references.

The following demonstration uses the popular Chartjs library to illustrate how this works. Chartjs is a leading, free, JavaScript charting component that offers an excellent example of the type of library that JavaScript interop within Blazor is designed to support. A CDN-hosted version of the library is referenced in index.html:

<body>
    <app>Loading...</app>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script>
</body>

The following script is a simple example that generates a line chart:

var ctx = document.getElementById('myChart').getContext('2d');
var options = {
    type: 'line',
    data: {
        labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
        datasets: [{
            label: 'Cars sold',
            data: [26, 44, 54, 66, 55, 58],
            backgroundColor: '#57a64a',
            borderColor: '#57a64a',
            fill: false
        }]
    }
};
new Chart(ctx, options);

ChartJS and Blazor JavaScript Interop

This version of the script allows calling code to pass in various options, enabling a certain amount of chart customisation:

window.showChart = (chartType, dataOptions) => {
    var ctx = document.getElementById('myChart').getContext('2d');
    var options = {
        type: chartType,
        data: {
            labels: dataOptions.labels,
            datasets: [{
                label: dataOptions.label,
                data: dataOptions.data,
                backgroundColor: dataOptions.color,
                borderColor:dataOptions.color,
                fill: dataOptions.fill
            }]
        }
    };
    new Chart(ctx, options);
}

It is saved in a separate script file and referenced in index.html:

<script src="_framework/blazor.webassembly.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script>
<script src="js/chart-demo.js"></script>

The next code block features a Razor component that leverages JSRuntime to call the showChart method, passing in parameters for the chart type, and the data options:

@page "/chart"
@inject IJSRuntime JSRuntime
<h3>Chart Demo</h3>

<canvas id="myChart"></canvas>

@code { 

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        var dataOptions = new
        {
            labels = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun" },
            data = new[] { 26, 44, 54, 66, 55, 58 },
            label = "Total Sales",
            color = "#57a64a",
            fill = false
        };
        await JSRuntime.InvokeVoidAsync("showChart", "line", dataOptions);
    }
}

In this example, the InvokeAsync method is called in the OnAfterRenderAsync method instead of a DOM event handler (e.g. the button click event in the first example). OnAfterRender(Async) is the place to call JavaScript methods that you want to take place on page or component load, because at this point, the component has rendered and DOM elements are available.

Passing Element References

In the example above, the chart is restricted to a specific DOM element (myChart) by this line of code:

var ctx = document.getElementById('myChart').getContext('2d');

To make the script more reusable, you can add another parameter to represent the DOM element. You could pass in a string, representing the id attribute value of the element, but Blazor also supports passing a reference to the DOM element itself.

The JavaScript showChart function is amended to accept an HTML element as an argument, and then to call the library's getContext method on the element:

window.showChart = (el, chartType, dataOptions) => {
    var ctx = el.getContext('2d');
    var options = {
        type: chartType,
        data: {
            labels: dataOptions.labels,
            datasets: [{
                label: dataOptions.label,
                data: dataOptions.data,
                backgroundColor: dataOptions.color,
                borderColor:dataOptions.color,
                fill: dataOptions.fill
            }]
        }
    };
    new Chart(ctx, options);
}

The amended Razor component looks like this:

@page "/chart"
@inject IJSRuntime JSRuntime
<h3>Chart Demo</h3>

<canvas @ref=myChart></canvas>

@code { 
    ElementReference myChart { get; set; }
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        var dataOptions = new
        {
            labels = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun" },
            data = new[] { 26, 44, 54, 66, 55, 58 },
            label = "Total Sales",
            color = "#57a64a",
            fill = false
        };
        await JSRuntime.InvokeVoidAsync("showChart", myChart, "line", dataOptions);
    }
}

The canvas element no longer has an id attribute. It has been replaced with a Blazor @ref attribute which has been assigned the value of a new ElementReference property that has been added to the @code block. This is passed in to the InvokeVoidAsync method.

The main benefit of passing references to elements using an ElementReference instead of a string representing the id attribute value is that Blazor will provide a unique attribute to each rendered ElementReference, eliminating any problems arising from the possibility of multiple components containing elements with the same id attribute value.

In this example, the rendered canvas element looks like this:

<canvas _bl_33b01246-a27f-47b8-b92e-72344b2f65db="" width="400" height="200" class="chartjs-render-monitor"></canvas>

Rendering UI From JS Libraries

Whenever you use a JS library to generate UI, such as a map, or chart as in this example, you should always do so in an empty element declared in the Razor component, like the canvas element above. Blazor has no way of knowing that the DOM was altered by JavaScript and the Blazor diffing algorithm will always see the target element as empty and does not try to process the content. Changes made by Blazor to other parts of the DOM can be processed without affecting the content of the empty element.

Last updated: 15/02/2023 09:02:31

Latest Updates

© 2023 - 2024 - Mike Brind.
All rights reserved.
Contact me at Outlook.com