Guide to Blazor JavaScript Interop
Guide to Blazor JavaScript Interop
Blazor is an exciting new framework from Microsoft that allows us to create full featured websites using C# instead of JavaScript. Like many web frameworks it provides for data binding and event handling, which binds DOM elements defined in our HTML to data sources and callback methods in our .Net code. Binding uses a declarative approach as opposed to the programmatical approach typically employed when using JavaScript directly. However, there are cases where binding is not enough and it is necessary to directly invoke a JavaScript function from a .NET method, or conversely, have a .NET method invoked from JavaScript. To make this possible Blazor provides an interoperability mechanism that works for both Blazor Server and Blazor WebAssembly. That is our focus here and we will also look at how to factor these JavaScript interop calls into our code in a clean and encapsulated way.
Code snippets are provided in this document but there is an accompanying project on GitHub: github.com/mveroukis/BlazorHtmlElements. Keep in mind that some of the code snippets differ from what you will find in the GitHub project. This is done intentionally in the interests of keeping the samples simple.
Calling JavaScript
Blazor provides an injectable service that implements the IJSRuntime interface. Blazor implements this interface so that we can invoke any JavaScript function from .NET code. To do this the service provides two main methods: InvokeAsync() and InvokeVoidAsync(). There are a bunch of overloads to these but that is basically it for calling into JavaScript code from .NET code. Let us see it in action:
SimpleInteropCall.razor
@inject IJSRuntime JsRuntime
1
2 <h3>Simple Interop Call</h3>
3
4 <button @onclick="OnButtonClicked">Alert Me</button>
5
6 @code {
7 async Task OnButtonClicked()
8 {
9 await JsRuntime.InvokeVoidAsync("console.log", "C# says hello");
10 await JsRuntime.InvokeVoidAsync("alert", "C# says hello");
11 }
12 }
Here we have a component that presents the user with a single button. When the button is pressed it displays a message in a browser alert and in the developer console. That seems straight forward but let us look a little close at the invoke methods.
We are calling the InvokeVoidAsync() method which is intended for invocations when no results are returned from the JavaScript function. The first parameter is the function identifier, which simply specifies which JavaScript function you intend to call. The function identifier is relative to the global scope, meaning the window object in JavaScript. Functions that reside in child objects to window can be accessed by specifying the object name before the function name, as is shown above when we call console.log() in the code sample above. What happens behind the scenes here is that Blazor calls window.console.log().
After the function identifier, we can add as many parameters as needed to match the expected parameters in the JavaScript function itself. In the case of console.log() and alert() only one parameter is expected, but for JavaScript functions that expect more parameters, we just add them to the parameter list. The one thing to keep in mind is that the parameters must be JSON serializable.
So, what if we need to get a result back from the JavaScript function? In that case we call the generic InvokeAsync() method, which takes the same form of parameters but returns a result. Here is an example:
GettingResults.razor
1 @inject IJSRuntime JsRuntime
2
3 <h3>Getting Results</h3>
4
5 <button @onclick="OnButtonClicked">Prompt Me</button>
6
7 @code {
8 public async Task OnButtonClicked()
9 {
10 var favoriteMovie = await JsRuntime.InvokeAsync<string>("prompt", "What is your favorite movie?");
11
12 await JsRuntime.InvokeVoidAsync("alert", favoriteMovie);
13 }
14 }
This component presents the user with a button, which when clicked, prompts them to enter their favorite movie. Once entered it displays the entered movie name back to them. As you can see, we need to specify the type when calling InvokeAsync() to match the JavaScript function’s result type. This return type must also be JSON-serializable. That is important because certain types are not serializable at all. For example, if the JavaScript function returns a Promise, you are out of luck. However, if you’re looking to make async JavaScript calls from .NET, then you might want to take a look at this: Using C# await against JS Promises in Blazor. Definitely something to check out.
We now know how to call JavaScript functions from .NET code and how to get a result back. What if we wish to do something a little more advanced, like say, call a method on an HTML element object? It turns out that we cannot call an HTML element object’s methods directly, so we need a slight work around. Blazor can inject a reference to an HTML element object into our .NET code, which is great. However, this only solves half the problem as we cannot call its methods directly from .NET. The solution is to create a custom JavaScript function that will accept the HTML element object reference as a parameter, and make the method call as required. Here is an example illustrating how we can set focus on a given HTML element:
GettingFocused.razor
1 @inject IJSRuntime JsRuntime
2
3 <h3>Getting Focused</h3>
4
5 <button @onclick="OnButtonClicked">Focus Me</button>
6
7 <input @ref="_inputRef" />
8
9 @code {
10 private ElementReference _inputRef;
11
12 public async Task OnButtonClicked()
13 {
14 await JsRuntime.InvokeVoidAsync("setFocus", _inputRef);
15 }
16 }
_Host.cshtml
1 <script>
2 window.setFocus = (element) => {
3 element.focus();
4 }
5 </script>
Here we have a component that presents the user with a button and a text input. When the button is pressed, the text input receives focus. As you can see, we have three different forms of code here: HTML, C#, and JavaScript.
Let us start by looking at the C# code first. In the very first line we have a class field declaration of type ElementReference. Blazor will inject a reference to the HTML element object into this field for us, but how does it know which HTML element reference we are expecting? For that we need to look at the HTML code itself, specifically, at the element we want our C# field to reference. The input element has a peculiar looking attribute here by the name of @ref, and its value matches the name of our ElementReference field in .NET: _inputRef. This simple name matching between the @ref value and the class field or property name allows Blazor to inject the reference. And if you are wondering what happens if the @ref value does not match to any field or property, the answer is simple: it fails to compile.
It is not obvious from the code above, but since the @ref attribute is part of the HTML, it will not have the opportunity to set the ElementReference field in the .NET code until the HTML is rendered. This means that the _inputRef field is initially null and will not be set until the OnAfterRender() lifecycle callback is fired. This is a common ‘gotcha’ to be aware of.
With the reference to the HTML element obtained, we can now pass it to our custom JavaScript function. Our JavaScript function setFocus() takes the HTML element object reference as a parameter, and then calls focus() on that object. Of course, this is a regular HTML element reference so we can do whatever we want with it making this approach very flexible. We can even set event listeners and handle them in .NET code, which we will cover in detail later.
Now you may wonder where to store the JavaScript functions. Blazor does not allow us to place these in our component .razor files. For a Blazor WebAssembly app you should add the JavaScript functions to wwwroot/index.html. For a Blazor Server app add them to Pages/_Host.cshtml instead. Furthermore, you may want to consider placing all the JavaScript functions in a separate JavaScript file and just link to it from the wwwroot/index.html or Pages/_Host.cshtml file. If you choose to place all your JavaScript functions in wwwroot/interop.js you would add something like this to the appropriate file:
1 <script src="interop.js"></script>
If your app consists of several projects, you can reference the JavaScript file in a class library project using the following format: _content/{PROJECT_NAME}/{FILEPATH}. For example, if the class library project is called HtmlElements and it’s JavaScript code is located in wwwroot/interop.js, we would add the following script tag in our main app project:
1 <script src="_content/HtmlElements/interop.js"></script>
Calling .NET
Now let us look at how we can make calls into .NET methods from JavaScript code. Blazor allows us to call both static and instance .NET methods. In either case the methods must be publicly accessible and be decorated with the [JSInvokable] attribute.
When calling a static .NET method, we can make use of either the DotNet.invokeMethod() or DotNet.invokeMethodAsync() JavaScript functions. In both cases we pass the assembly name and method name, in that order, followed by any parameters for the target method. Let us look at a quick example:
Javascript
1 window.debug = {
2 log: async (msg) => {
3 await DotNet.invokeMethodAsync("BlazorApp", "DebugLog", msg);
4 }
5 };
C# Blazor Component:
1 @using System.Diagnostics
2
3 @code {
4 [JSInvokable]
5 public static Task DebugLog(string msg)
6 {
7 Debug.WriteLine(msg);
8
9 return Task.CompletedTask;
10 }
11 }
In the JavaScript code block we declare the async function log(). The log() function makes an asynchronous call to the C# async static method, DebugLog() located in the BlazorApp assembly. Note that we do not need to specify the class that the static method resides in, so it is important that your public static methods which are called from JavaScript have a unique name. With all that setup, any call to debug.log() from JavaScript code will cause Visual Studio to write out the passed in message into Visual Studio’s Debug Output window. That is pretty handy, but what if we want to call something that maintains state?
To call a .NET instance method from JavaScript we need a little extra preparation. For JavaScript to invoke a .NET instance method it requires a reference to the .NET object instance. Of course, JavaScript cannot reference .NET object instances directly, so Blazor offers us a helper here; the very aptly named DotNetObjectReference class. It offers us a single static method called Create(). All you need to do is pass a reference to the object instance you wish to reference from JavaScript into DotNetObjectReference.Create(). This will return a generic form of DotNetObjectReference<>, based on the type of the object instance supplied to Create(). Once that is obtained, you pass that to JavaScript by invoking a custom JavaScript function that takes the DotNetObjectReference and stores it for later use.
On the JavaScript side, the passed in DotNetObjectReference exposes a method called invokeMethodAsync(). This is used to invoke methods associated with that particular .NET object instance. Note that this is different from the static DotNet.invokeMethodAsync() as in it does not need the assembly name as the first parameter. Instead, the first parameter it accepts is the name of the method you wish to invoke, followed by any parameters. Let us look at an example where JavaScript code repeatedly calls a .NET method to generate a GUID:
interop.js
1 window.debug = {
2 log: async (msg) => {
3 await DotNet.invokeMethodAsync("BlazorApp", "DebugLog", msg);
4 }
5 };
6
7 window.JsGuidGenerator = {
8 intervalId: null,
9 start: (objRef) => {
10 debug.log("Guid generation started");
11
12 JsGuidGenerator.intervalId = setInterval(async () => {
13 const guid = await objRef.invokeMethodAsync("GenerateGuid");
14
15 debug.log(guid);
16 }, 2000);
17 },
18
19 stop: () => {
20 if (JsGuidGenerator.intervalId) {
21 clearInterval(JsGuidGenerator.intervalId);
22 JsGuidGenerator.intervalId = null;
23
24 debug.log("Guid generation stopped");
25 }
26 }
27 };
JsGuidGenerator.razor
1 @page "/jsguidgenerator"
2
3 @using System.Diagnostics
4
5 @implements IDisposable
6
7 @inject IJSRuntime JsRuntime
8
9 <h3>JavaScript GuidGenerator</h3>
10
11 <p>
12 Look in Visual Studio's <i>Output</i> window to see Debug
13 messages genereted from your front-end code.
14 </p>
15
16
17 @code {
18 private readonly DotNetObjectReference<JsGuidGenerator> _objeRef;
19
20 public JsGuidGenerator()
21 {
22 _objeRef = DotNetObjectReference.Create(this);
23 }
24
25 [JSInvokable]
26 public Task<string> GenerateGuid()
27 {
28 return Task.FromResult(Guid.NewGuid().ToString());
29 }
30
31 [JSInvokable]
32 public static Task DebugLog(string msg)
33 {
34 Debug.WriteLine(msg);
35
36 return Task.CompletedTask;
37 }
38
39 protected override async Task OnInitializedAsync()
40 {
41 await base.OnInitializedAsync();
42 await JsRuntime.InvokeVoidAsync("JsGuidGenerator.start", _objeRef);
43 }
44
45 async void IDisposable.Dispose()
46 {
47 await JsRuntime.InvokeVoidAsync("JsGuidGenerator.stop");
48
49 _objeRef.Dispose();
50 }
51 }
The component itself does not do anything interesting, but when run in debug mode in Visual Studio, GUIDs will appear in the Debug Output window periodically so long as the component is visible. No, not particularly useful but it demonstrates how a JavaScript function can call back into .NET code at any time.
What makes this work is the DotNetObjectReference that is created in .Net and passed into JavaScript code, allowing the callbacks to .NET methods. An interval is set to call back into .NET code periodically to generate a GUID via the component referenced by objRef. The invokeMethodAsync() function returns a promise and since we are in an async lambda, we can await for the result. The resultant GUID is sent back into the .NET realm via a call to the static method discussed earlier; DebugLog().
When the component is initialized it invokes the JavaScript function JsGuidGenerator.start(). The JsGuidGenerator.start() function takes a .NET object reference as its only parameter, and in this case, we are passing in the Blazor component’s instance. Note that it does not need to be the component instance, it could be any object instance, but danger lurks here. Whatever object reference is sent from the .NET code into the JavaScript realm, care must be taken to ensure that the lifespan of that .NET object matches that of the object reference in JavaScript. In other words, we need to clean up the .NET object references in JavaScript (and .NET) as there is no automatic mechanism to do so. In this case we do it when the Blazor component is disposed by calling the JsGuidGenerator.stop() JavaScript function. The JsGuidGenerator.stop() stops the interval from executing, which releases the lambda and with it the reference to objRef.
Back in .NET world, the DotNetObjectReference that we create is something we need to hold onto because it must be disposed of when it is no longer needed. The JSRuntime keeps track of all the DotNetObjectReference instances and if it is not disposed of properly, that object reference will be tracked indefinitely and leak memory. In fact, it could leak the entire component, and if your web app is of the Blazor Server variety, that could cause serious issues in terms of server load. Since Blazor calls a component’s Dispose() method once that component is no longer visible, that is usually a good place to dispose of your object references as well. Interestingly, Blazor components do not implement IDispose so we need to implement it ourselves to get this functionality.
Wrapping HTML elements
Blazor provides two main methods for your .NET code to interact with the HTML elements themselves: data binding and event handlers. And in most cases, this is by far the best way to do it. However, there will be times when a JavaScript function call is required to interact with a particular HTML element. One such example is the <dialog> HTML element.
<dialog> is not yet fully supported by all browsers, but it is fully supported in Chromium browsers, including Edge.
The <dialog> element can be made visible using data binding (it has an open attribute that can be bound to a Boolean property). However, to open it as a modal dialog you need to call the element’s showModal() method (not supported in Firefox as of version 83.0). We have seen above how we can pass a reference of an HTML element into the .NET realm, and how to call an element’s method. The <dialog> element also fires off an event when the dialog is closed, and we have seen how to handle callbacks in .NET code. This all works great but one thing to take into consideration is that the <dialog> element is something likely to be used in a variety of scenarios and from many components. Standardizing how we deal with elements such as this certainly has its merits, and we can do it by wrapping them with .NET code.
We can do this by creating a .NET class for each specific HTML element we wish to wrap. The intent of this class would be to provide a mechanism to interact with the HTML element by making JavaScript calls, and to handle any callbacks. To do this we will need both JavaScript and .NET code, and the two pieces will need to talk to each other. Let us jump straight into an example to see what that might look like:
Interop.js
1 public class HtmlDialog
2 {
3 protected IJSRuntime JsRuntime { get; private set; }
4 protected ElementReference ElementRef { get; private set; }
5 protected DotNetObjectReference<HtmlReferencedElement> ObjectRef { get; }
6
7 private Func<string, Task> _asyncOnCloseAction = null;
8
9 public HtmlDialog(IJSRuntime jsRuntime, ElementReference element, Func<string, Task> action = null)
10 {
11 JsRuntime = jsRuntime;
12 ElementRef = element;
13 _asyncOnCloseAction = action;
14
15 ObjectRef = DotNetObjectReference.Create(this);
16
17 JsRuntime.InvokeVoidAsync("interop.dialog.init", ElementRef, ObjectRef);
18 }
19
20 public async Task ShowModalAsync()
21 {
22 await JsRuntime.InvokeVoidAsync("interop.dialog.showModal", ElementRef);
23 }
24
25 public async Task CloseModalAsync(string returnValue = null)
26 {
27 await JsRuntime.InvokeVoidAsync("interop.dialog.closeModal", ElementRef, returnValue);
28 }
29
30 [JSInvokable]
31 public async Task OnCloseAsync(string returnValue)
32 {
33 if (_asyncOnCloseAction != null)
34 {
35 await _asyncOnCloseAction.Invoke(returnValue);
36 }
37
38 Debug.WriteLine($"Called from JS: {returnValue}");
39 }
40
41 public void Dispose()
42 {
43 ObjectRef.Dispose();
44 }
45 }
HtmlDialog.cs
1 window.interop = {
2 dialog: {
3 init: (dialog, ref) => {
4 dialog.addEventListener("close", async e => {
5 await ref.invokeMethodAsync("OnCloseAsync", dialog.returnValue);
6 });
7 },
8
9 showModal: (dialog) => {
10 if (!dialog.open) {
11 dialog.showModal();
12 }
13 },
14
15 closeModal: (dialog, returnValue) => {
16 if (dialog && dialog.open) {
17 dialog.returnValue = returnValue;
18 dialog.close();
19 }
20 }
21 }
22 }
DialogDemo.razor
1 @page "/dialogdemo"
2
3 @using HtmlElements.Elements
4 @using HtmlElements
5
6 @inject IJSRuntime JsRuntime
7
8 <h3>Dialog Demo</h3>
9
10 <button @onclick="OpenDialog">Open</button>
11
12 <dialog id="my-dialog" @ref="_dialogRef">
13 <div>
14 <p>Hello world!</p>
15 <button @onclick="OnCloseClicked">Close</button>
16 </div>
17 </dialog>
18
19 @code {
20 public ElementReference _dialogRef { get; set; }
21
22 private HtmlDialog Dialog { get; set; } = null;
23
24 protected override async Task OnAfterRenderAsync(bool isFirstRender)
25 {
26 if (isFirstRender)
27 {
28 Dialog = new HtmlDialog(JsRuntime, _dialogRef);
29 }
30 }
31
32 public async Task OpenDialog()
33 {
34 await Dialog.ShowModalAsync();
35 }
36
37 public async void OnCloseClicked()
38 {
39 await Dialog.CloseModalAsync();
40 }
41 }
There’s a fair bit to unpack here, and if you wish to look deeper into the code I suggest cloning the sample project from GitHub if you haven’t already. First, we have the JavaScript code located in interop.js. Here we create a few functions useful for dealing with a <dialog> element. Note how we nest all the related functions under window.interop.dialog. This just makes it easy to manage as different HTML elements would have their own aptly named object under window.interop.
There are only three functions here for <dialog>: init(), showModal() and closeModal(). Perhaps the most interesting one is init() as it requires a reference to the <dialog> element itself, but also to the .NET object that will handle the callbacks. Obviously, we only want to call this once. When the dialog is closed, we want to handle the close event to retrieve any resulting value from it. In the init() function we attach an event listener to the <dialog> element, which when triggered will invoke a predetermined method in the referenced .NET object. It is worth pointing out that we do not maintain a long-lasting reference to the .NET object passed into init() outside of the attached event listener. Once the dialog is closed, the even listener itself will be released and along with it the reference to the .NET object. And since a <dialog> can only be closed once, a .NET method invocation with a stale object reference is impossible.
The showModal() function is straight forward as it just calls the dialog’s showModal() method. The closeModal() function does two things: it sets the result of the dialog in the returnValue property of the <dialog> element itself and then closes the dialog. The dialog’s close() method will then trigger the close event that we attached a listener for in the init() function.
Moving over to the .NET code, we have the C# class HtmlDialog that wraps the <dialog> HTML element. It expects the JavaScript Runtime and a reference to the dialog element in its constructor. The constructor also optionally accepts an action, which is a callback that is fired whenever the dialog is closed. During construction we create a DotNetObjectReference for our new HtmlDialog instance and use it right away in our invocation of the JavaScript function interop.dialog.init(). This will setup the event handler in JavaScript so that it will call into our C# wrapper class whenever the associated <dialog> element fires the close event. Of course, we could skip this step if the caller did not supply a callback action in the constructor, but for the sake of simplicity, I omitted that conditional check.
To handle the <dialog> element’s close event, we added the OnCloseAsync() method and we decorated it with [JSInvokable]. The event listener that is attached to the <dialog> element will invoke this when the dialog is closed. Note that not only is OnCloseAsync() async, but so is the _asyncOnCloseAction Func since it returns a Task. This allows us to await a caller-supplied callback.
The other two noteworthy methods, ShowModalAsync() and CloseModalAsync(), simply invoke corresponding methods in JavaScript. We are not guarding against out of order invocations of these, so it’s possible to call CloseModalAsync() before ShowModalAsync(), for example, and we could choose to fortify the code here but again for simplicity I omitted that from the samples.
The Blazor component brings it all together. Blazor injects the IJSRuntime service and magically sets the _dialogRef ElementReference for us. All we need to do next is create an instance of the HtmlDialog class. Since HtmlDialog’s constructor expects a valid ElementReference we must be careful to instantiate it after our component has been rendered, which is why we create it in OnAfterRenderAsync(). With the HtmlDialog instantiated, it is ready to use as it handles two-way communications between .NET and the JavaScript realm. The opening and closing of the <dialog> element is triggered by user interactions, and those actions are bound from the razor code to C# using Blazor’s event binding. And that is it.
As you can see, the HtmlDialog nicely encapsulates all the unsightly JavaScript interop code out of sight, making the rest of the Blazor component a light read. This can be done with just about any HTML element. You can also use it to interact with JavaScript third-party libraries.
Check out the source
As mentioned above, there is an accompanying project on GitHub that demos everything we talked about here. The demo project goes a little further and if you found this interesting feel free to clone it or fork it and play around with it. The HtmlElements project was created with the intent of containing all the interoperable code in one place. You can take what is there and add it to your existing solution and extend it as needed. Enjoy.
discover more
SQL Saturday Part 2: Learning About Microsoft Fabric
SQL Saturday Part 2: Learning About Microsoft Fabric February 29, 2024 I’ve been digging into Microsoft Fabric recently – well overdue, since it was first released about a year ago.…
My Trip to SQL Saturday Atlanta (BI Edition): Part 1
My Trip to SQL Saturday Atlanta (BI Edition): Part 1 February 23, 2024 Recently, I had the opportunity to attend SQL Saturday Atlanta (BI edition), a free annual event for…
Enabling BitLocker Encryption with Microsoft Intune
Enabling BitLocker Encryption with Microsoft Intune February 15, 2024 In today’s data-driven world, safeguarding sensitive information is paramount, especially with the increase in remote work following the pandemic and the…
Let’s Build Something Amazing Together
From concept to handoff, we’d love to learn more about what you are working on. Send us a message below or call us at 1-800-989-6022.