Swimburger

Communicating between .NET and JavaScript in Blazor with in-browser samples

Niels Swimberghe

Niels Swimberghe - - .NET

Follow me on Twitter, buy me a coffee

Blazor logo next to JavaScript logo with title: Communicating between .NET & JavaScript in Blazor with in-browser samples

The success of Blazor relies heavily upon how well it can integrate with the existing rich JavaScript ecosystem. The way Blazor allows you to integrate, is by enabling you to call JavaScript functions from Blazor and .NET functions from JavaScript. This is also referred to as 'JavaScript interoperability'.
The JavaScript interoperability API's provided by Microsoft live under Microsoft.JSInterop namespace.

Call JavaScript from Blazor #

To invoke JavaScript functions from Blazor, you need an instance of the IJSRuntime interface.
You can obtain an instance using ASP.NET's built-in dependency injection. If you want to have the IJSRuntime injected into a '.razor' component, use the @inject directive at the top of your file like this:

@inject IJSRuntime js

You can now use the js variable throughout the component.

Alternatively, you can use constructor injection like this:

public class Demo
{
    private readonly IJSRuntime js;

    public Demo(IJSRuntime js)
    {
        this.js = js;
    }
}

Or you can mark properties using the Inject attribute:

public class Demo
{
    [Inject]
    public IJSRuntime JS { get; set; }
}

Once you have an instance of IJSRuntime, you can call JavaScript functions using its methods:

InvokeAsync
This method will invoke the specified JavaScript function. It requires 1 parameter; A string to specify which JavaScript function to run. After the first parameter, you can pass as many extra parameters as you need to. Those subsequent parameters will be passed along to the JavaScript function. 
You also must specify the return type you are expecting to receive from the JavaScript function using the generic type parameter. Blazor will try to convert the JavaScript return value to the requested .NET type.
InvokeVoidAsync
As the name gives away, this method functions like InvokeAsync, but it doesn't return the JavaScript return value.

Note: Even though the JavaScript return value isn't returned to you, it is still converted to a .NET type and then discarded. This behavior will change in future versions so that the JavaScript return value isn't unnecessarily converted.

Here's a basic example of how to use the methods:

@inject IJSRuntime JS
@code {
    protected override async Task OnInitializedAsync()
    {
        string name = await JS.InvokeAsync<string>("prompt", "What is your name?");
        await JS.InvokeVoidAsync("alert", $"Hello {name}!");
    }
}

The sample above will call the window.prompt function which shows a dialog with an input box. Once you fill out your name and click "OK", the text entered into the prompt is returned to .NET. 
The entered text is then passed along to the window.alert function which show a simple dialog without returning any values. 

Being able to call JavaScript function from .NET is essential, but this implementation has caveats:

  • You can only call functions that are available in the global scope. With other words, only functions defined on the window object or an objected nested within the window object.
  • It's common for a function to return an object which you use to call a subsequent function, but in .NET 3.1 Blazor, that is not easily done.
    .NET 5 will introduce IJSObjectReference which refers to an object in JavaScript. Once you have a IJSObjectReference instance, you can call the same InvokeAsync method on it.
  • Since you can only call functions, you can also not use the new operator to create instances of classes/prototypes. As a workaround, you can create a dedicated JavaScript function to create those objects using the new operator.
  • You may want to pass complete JavaScript statements such as "document.getElementById('nameInput').value", but that is not possible and will cause errors. If Blazor were to support this, it would be exposed to JavaScript injection attacks. If you really do want this type of behavior, you could use the eval function in JavaScript like this sample, though it is very risky and not recommended.

For more details on .NET to JavaScript interop, check out Microsoft's "Call JavaScript functions from .NET methods in ASP.NET Core Blazor".

.NET to JavaScript Samples #

Call JavaScript functions with simple parameters from .NET #

The InvokeVoidAsync on IJSRuntime allows you to call JavaScript functions without expecting any return value.
These functions have to be available on the global window variable.
You can use Blazor's dependency injection using the @inject to get an instance of IJSRuntime.
The first parameter of InvokeVoidAsync is the JavaScript function you want to invoke. You can pass as many subsequent parameters and they will be passed to the JavaScript function.
ElementReference's passed to InvokeVoidAsync will automatically be converted to the HTMLElement.

Live Sample:

Blazor Source:

@implements IDisposable
@inject IJSRuntime js

<button type="button" class="btn btn-primary" onclick="jsToDotNetSamples.printPersonFromDotNet()">Print person from .NET class instance</button>
<br><br>
<code id="personCodeBlock"></code>

@code{
  private JsToDotNetBridge jsToDotNetBridge = new JsToDotNetBridge();
  private DotNetObjectReference<JsToDotNetBridge> jsToDotNetBridgeReference;

  protected override Task OnInitializedAsync()
  {
    jsToDotNetBridgeReference = DotNetObjectReference.Create(jsToDotNetBridge);
    js.InvokeVoidAsync("jsToDotNetSamples.setDotNetReference", jsToDotNetBridgeReference);
    return base.OnInitializedAsync();
  }

  public void Dispose()
  {
    jsToDotNetBridgeReference?.Dispose();
  }

  // class embedded for demo purposes, move this class out of your razor component
  public class JsToDotNetBridge
  {
    [JSInvokable]
    public object GetPerson()
    {
      var obj = new
      {
        FistName = "Jon",
        LastName = "Doe",
        Age = 27,
        BirthDate = DateTime.Now.AddYears(-27), 
        LikesDotNet = true
      };
      return obj;
    }
  }
}

JavaScript Source:

window.dotNetToJsSamples = {
    setText: function(node, text)
    {
        node.textContent = text;
    }
};

Call JavaScript functions with a .NET object as parameter from .NET #

In addition to simple .NET types, you can also pass .NET objects when invoking JavaScript functions using InvokeVoidAsync.

Live Sample:

Blazor Source:

@inject IJSRuntime js
<button type="button" class="btn btn-primary" @onclick="PrintDotNetObject">Pass .NET object to JavaScript</button>
<br><br>
<code @ref="codeBlock"></code>
@code{
  private ElementReference codeBlock;

  private async Task PrintDotNetObject()
  {
    var obj = new {
      FistName = "Jon",
      LastName = "Doe",
      Age = 27,
      BirthDate = DateTime.Now.AddYears(-27), 
      LikesDotNet = true,
    };
    await js.InvokeVoidAsync("dotNetToJsSamples.printDotNetObject", codeBlock, obj);
  }
}

JavaScript Source:

window.dotNetToJsSamples = {
    printDotNetObject: function(node, objectFromDotNet)
    {
        node.textContent = JSON.stringify(objectFromDotNet);
    }
};

Call 'eval' JavaScript functions from .NET #

eval evaluates the string parameter as JavaScript and then executes the JavaScript. This is very powerful, but also very dangerous! If possible, avoid eval at all cost. Using eval could expose your applications to script injection. Read more about eval at MDN's documenation, including why you should not use it.

Since eval is a function available on the window object, you can also call it from .NET. Use at your own risk!

Live Sample:

Blazor Source:

@inject IJSRuntime js

<button type="button" class="btn btn-primary" @onclick="ShowEvalWarningAsync">Run JavaScript from .NET using EVAL</button>
<br><br>
<div class="eval-warning alert alert-danger font-weight-bold" role="alert">[WARNING MESSAGE GOES HERE]</div>

@code{
  private async Task ShowEvalWarningAsync()
  {
    var warning = "Using <code>eval</code> is a huge security risk." +
      "<a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#Never_use_eval!\">" +
    " Learn more about the risks of using <code>eval</code> at MDN." +
    "</a>";
    // add null; add the end to prevent .NET from trying to serialize return value to object
    var scriptToEval = string.Format("$('.eval-warning').html('{0}');null;", warning);
    await js.InvokeVoidAsync("eval", scriptToEval);
  }
}

Call JavaScript functions with return value from .NET #

In addition to the method InvokeVoidAsync on IJSRuntime, there's another method InvokeAsync.
InvokeAsync let's you invoke a JavaScript function and receive its return value in .NET.
Example: InvokeAsync<string>("computeStringInJavaScript").
These JavaScript functions can also return objects which will be converted into .NET objects.

Live Sample:

Blazor Source:

@inject IJSRuntime js

<textarea class="form-control" placeholder="Enter message" @ref="textAreaReference"></textarea>
<br>
<button type="button" class="btn btn-primary" @onclick="PrintMessageAsync">
  Get return value from JavaScript function invoked from .NET
</button>
<br>
<br>
<div class="alert alert-primary" role="alert">
  Message: @message
</div>


@code{
  private ElementReference textAreaReference;
  private string message;
  private async Task PrintMessageAsync()
  {
    message = await js.InvokeAsync<string>("dotNetToJsSamples.getValue", textAreaReference);
  }
}

JavaScript Source:

window.dotNetToJsSamples = {
    getValue: function(node)
    {
        return node.value;
    }
};

Call JavaScript functions returning a Promise from .NET #

In addition to returning simple values and objects from JavaScript functions, these functions can also return a Promise.
Promise in JavaScript is the equivalent of a Task in .NET, though they function slightly different.
When calling a JavaScript function from .NET which returns a promise, you can simply await the Task returned by InvokeAsync.
So from the perspective of your .NET code, there's no difference between a function returning something immediately or using a Promise.

Live Sample:

Blazor Source:

@using System.Text.Json
@inject IJSRuntime js

<button type="button" class="btn btn-primary" @onclick="GetWeatherDataUsingJavaScript">
  Invoke JavaScript function with Promise return value from .NET
</button>
<br><br>
<code>
  @serializedObject
</code>

@code{
  private string serializedObject;
  private async Task GetWeatherDataUsingJavaScript()
  {
    var weatherDataObject = await js.InvokeAsync<Object>("dotNetToJsSamples.getWeatherData");
    serializedObject = JsonSerializer.Serialize(weatherDataObject);
  }
}

JavaScript Source:

window.dotNetToJsSamples = {
    getWeatherData: function(){
        return fetch('/sample-data/weather.json')
            .then(response => response.json());
    }
};

Call .NET methods from JavaScript #

You can also call .NET methods from JavaScript in Blazor. You can call both static .NET methods and instance .NET methods.

Before you can call .NET methods from JavaScript, you need to mark the methods with the JSInvokable attribute.

[JSInvokable]
public static void ConsoleWriteLine(string message)
{   
    Console.WriteLine(message);
}  

[JSInvokable("otherConsoleWriteLine")]
public static void NamedConsoleWriteLine(string message)
{
    Console.WriteLine(message);
}

[JSInvokable]
public void InstanceConsoleWriteLine(string message)
{
    Console.WriteLine(message);
}

You can pass in a string to the attribute to explicitly configure the identifier of the .NET method. If you don't explicitly configure the identifier, you can simply use the name of the method.

Warning: Within a single assembly, you cannot have multiple static methods marked with the JSInvokable attribute that have the same identifier (implicitly by method name or explicitly by passing the identifier parameter).
If you do have colliding identifiers for static methods, errors will be thrown when attempting to invoke those methods from JavaScript.

Call static .NET methods from JavaScript #

Blazor's JavaScript adds a new object named DotNet as a global variable which has two functions to invoke static .NET methods:

invokeMethod
This method invokes the specified static .NET method and returns the .NET value converted as a JavaScript value. The first parameter is the name of the assembly where the static .NET method resides. The second parameter is the identifier/method name of the static .NET method. Any subsequent parameters will be converted to .NET values and passed to the .NET method.
invokeMethodAsync
This method invokes static .NET methods and returns a JavaScript Promise. When the Promise is fulfilled, the Promise result will be the return value from the .NET method.
The first parameter is the name of the assembly where the static .NET method resides. The second parameter is the identifier/method name of the static .NET method. Any subsequent parameters will be converted to .NET values and passed to the .NET method.

Here's a basic example of how to use the methods:

DotNet.invokeMethod('BlazorJavaScriptInterop', 'ConsoleWriteLine', 'Hello from JavaScript')
DotNet.invokeMethod('BlazorJavaScriptInterop', 'otherConsoleWriteLine', 'Hello from JavaScript 2')

The sample above will call the static ConsoleWriteLine and NamedConsoleWriteLine .NET method shown prior. The result will be the following console output:

Hello from JavaScript
Hello from JavaScript 2

Call .NET instance methods from JavaScript #

You can also call .NET instance methods, but you need a reference to the object before you can invoke its functions. You can create a DotNetObjectReference instance by calling DotNetObjectReference.Create. You need to pass in the object you want a reference to as a parameter. When a DotNetObjectReference is returned to JavaScript from a .NET method or passed to a JavaScript function, it is converted to a JavaScript object which you can use to call the methods of the .NET object.

The JavaScript object converted from a DotNetObjectReference instance has the same two functions as `DotNet` to call .NET methods:

invokeMethod
This method behaves the same as DotNet.invokeMethod, but instead of calling a static .NET method, it will invoke the .NET method of the referenced .NET object.
invokeMethodAsync
This method behaves the same as DotNet.invokeMethodAsync, but instead of calling a static .NET method, it will invoke the .NET method of the referenced .NET object.

For more details on JavaScript to .NET interop, check out Microsoft's "Call .NET methods from JavaScript functions in ASP.NET Core Blazor".

JavaScript to .NET Samples #

Call static .NET method from JavaScript #

Blazor will add a global variable DotNet to the JavaScript window object.
Using DotNet.invokeMethod and DotNet.invokeMethodAsync you can invoke public static .NET methods that have been marked with the JSInvokable attribute.
The JSInvokable attribute accepts an argument to specify which name you can use to invoke the method from JavaScript.
To see the result of this sample, open the browser DevTools and observe the console output.

Live Sample:

To see the result of this sample, open the browser DevTools and observe the console output.

Blazor Source:

<button type="button" class="btn btn-primary" onclick="DotNet.invokeMethod('BlazorJavaScriptInterop', 'ConsoleWriteLine', 'Hello from JavaScript')">
  Call static .NET ConsoleWriteLine function from JavaScript
</button>
<br>
<br>
<button type="button" class="btn btn-primary" onclick="DotNet.invokeMethod('BlazorJavaScriptInterop', 'otherConsoleWriteLine', 'Hello from JavaScript 2')">
  Call static .NET NamedConsoleWriteLine function  named 'otherConsoleWriteLine' from JavaScript
</button>

@code{
  [JSInvokable]
  public static void ConsoleWriteLine(string message)
  {
    Console.WriteLine(message);
  }

  [JSInvokable("otherConsoleWriteLine")]
  public static void NamedConsoleWriteLine(string message)
  {
    Console.WriteLine(message);
  }
}

Call .NET class instance methods from JavaScript #

The sample below does the following:

  1. Create an instance of JsToDotNetBridge. This class has a method GetPerson marked with a JSInvokable attribute. The method returns an anonymous object representing a person.
  2. Create a DotNetObjectReference for the JsToDotNetBridge instance using DotNetObjectReference.Create(jsToDotNetBridge);.
  3. Invoke the JavaScript function jsToDotNetSamples.setDotNetReference which accepts the object reference and stores it.
  4. When the button below is clicked, jsToDotNetSamples.printPersonFromDotNet will be called which invokes the JsToDotNetBridge.GetPerson instance method.
  5. The method returns an object which is converted into JSON to print below the button.

Note: You have to make sure to dispose of DotNetObjectReference. In the sample, the Blazor component implements the IDisposable interface and disposes of the object reference in the Dispose method.

Live Sample:

Blazor Source:

@implements IDisposable
@inject IJSRuntime js

<button type="button" class="btn btn-primary" onclick="jsToDotNetSamples.printPersonFromDotNet()">Print person from .NET class instance</button>
<br><br>
<code id="personCodeBlock"></code>

@code{
  private JsToDotNetBridge jsToDotNetBridge = new JsToDotNetBridge();
  private DotNetObjectReference<JsToDotNetBridge> jsToDotNetBridgeReference;

  protected override Task OnInitializedAsync()
  {
    jsToDotNetBridgeReference = DotNetObjectReference.Create(jsToDotNetBridge);
    js.InvokeVoidAsync("jsToDotNetSamples.setDotNetReference", jsToDotNetBridgeReference);
    return base.OnInitializedAsync();
  }

  public void Dispose()
  {
    jsToDotNetBridgeReference?.Dispose();
  }

  // class embedded for demo purposes, move this class out of your razor component
  public class JsToDotNetBridge
  {
    [JSInvokable]
    public object GetPerson()
    {
      var obj = new
      {
        FistName = "Jon",
        LastName = "Doe",
        Age = 27,
        BirthDate = DateTime.Now.AddYears(-27), 
        LikesDotNet = true
      };
      return obj;
    }
  }
}

JavaScript Source:

window.jsToDotNetSamples = {
    dotNetReference: null,
    setDotNetReference: function(dotNetReference)
    {
        this.dotNetReference = dotNetReference;
    },
    printPersonFromDotNet: function(){
        var person = this.dotNetReference.invokeMethod("GetPerson");
        document.getElementById('personCodeBlock').textContent = JSON.stringify(person);
    }
};

Call .NET Blazor component instance methods from JavaScript #

Live Sample:

The previous sample created a dedicated class with instance methods to be called from JavaScript.
You can simplify this by creating a DotNetObjectReference of the Blazor component class.
Simply define instance methods on your Blazor component and mark them with the JSInvokable attribute.
Instead of creating a new object and passing it to DotNetObjectReference.Create, simply pass this to create a reference to the Blazor component instance.
After creating the reference, pass it as a parameter when invoking JavaScript and have the JavaScript function store the object reference.

Blazor Source:

@implements IDisposable
@inject IJSRuntime js

<button type="button" class="btn btn-primary" onclick="jsToDotNetSamples.printPersonFromDotNet()">Print person from .NET Blazor component instance method</button>
<br><br>
<code id="personCodeBlock"></code>

@code{
  private DotNetObjectReference<JsToDotNetSample3> dotNetObjectReference;

  protected override Task OnInitializedAsync()
  {
    dotNetObjectReference = DotNetObjectReference.Create(this);
    js.InvokeVoidAsync("jsToDotNetSamples.setDotNetReference", dotNetObjectReference);
    return base.OnInitializedAsync();
  }

  [JSInvokable]
  public object GetPerson()
  {
    var obj = new
    {
      FistName = "Jon",
      LastName = "Doe",
      Age = 27,
      BirthDate = DateTime.Now.AddYears(-27), 
      LikesDotNet = true
    };
    return obj;
  }

  public void Dispose()
  {
    dotNetObjectReference.Dispose();
  }
}

JavaScript Source:

window.jsToDotNetSamples = {
    dotNetReference: null,
    setDotNetReference: function(dotNetReference)
    {
        this.dotNetReference = dotNetReference;
    },
    printPersonFromDotNet: function(){
        var person = this.dotNetReference.invokeMethod("GetPerson");
        document.getElementById('personCodeBlock').textContent = JSON.stringify(person);
    }
};

Call Async Task .NET methods from JavaScript #

Live Sample:

In addition to calling .NET methods with simple return values and objects, you can also call .NET methods which return a Task using invokeMethodAsync.
When invoking a .NET method returning a Task, the return value in JavaScript will be a Promise.

The sample below does the following:

  1. DotNetObjectReference referring to the Blazor component instance is created and passed to JavaScript.
  2. JavaScript receives the object reference and stores it for future use.
  3. When the button below is clicked, JavaScript will call the GetWeatherAsync instance method on the object reference.
  4. GetWeatherAsync will use the .NET HttpClient to make an HTTP request to sample-data/weather.json.
  5. The JSON result is deserialized to a .NET object which is then returned.
  6. In JavaScript, a Promise was returned by invokeMethodAsync.
  7. When the .NET function is done requesting the weather data, the object is passed to then callback.
  8. Inside of the callback, the object is deserialized and printed below the button.

Blazor Source:

@implements IDisposable
@inject IJSRuntime js
@inject NavigationManager navigationManager

<button type="button" class="btn btn-primary" onclick="jsToDotNetSamples.getWeatherDataFromDotNet()">Get WeatherData from .NET async task method</button>
<br><br>
<code id="weatherCodeBlock"></code>

@code{
  private static HttpClient httpClient = new HttpClient();
  private DotNetObjectReference<JsToDotNetSample4> dotNetObjectReference;

  protected override Task OnInitializedAsync()
  {
    dotNetObjectReference = DotNetObjectReference.Create(this);
    js.InvokeVoidAsync("jsToDotNetSamples.setDotNetReference", dotNetObjectReference);
    return base.OnInitializedAsync();
  }

  [JSInvokable]
  public async Task<object> GetWeatherAsync()
  {
      var rootUrl = navigationManager.BaseUri;
      return await httpClient.GetFromJsonAsync<object>($"{rootUrl}/sample-data/weather.json");
  }

  public void Dispose()
  {
    dotNetObjectReference?.Dispose();
  }
}

JavaScript Source:

window.jsToDotNetSamples = {
    dotNetReference: null,
    setDotNetReference: function(dotNetReference)
    {
        this.dotNetReference = dotNetReference;
    },
    getWeatherDataFromDotNet: function(node){
        this.dotNetReference.invokeMethodAsync('GetWeatherAsync')
            .then(weatherData => {
                document.getElementById('weatherCodeBlock').textContent = JSON.stringify(weatherData);
            });
    }
};

Support multiple Blazor component instances with JavaScript state #

For some Blazor components, you can be certain that there will only ever be one instance of that component at a single time. In that case, you can simply store the JavaScript state in a single object and not worry about any collision. But for many Blazor components, the component needs to support multiple instances at the same time and their state in JavaScript should not overwrite each other.
To learn how to support multiple instances of components, this sample will use Bootstrap's Carousel component which requires some initial setup to reference some CSS and JavaScript files.

The main trick is to store each component's JavaScript state in an object with a unique component ID as the key, and the component's state as the value.
Subsequently, for any JavaScript function you want to call, you have to pass this unique component ID so that the JavaScript function can retrieve the correct state and execute its functionality.

The sample below illustrates how you can have multiple Carousel components on a single page, each with their own state in JavaScript.
Here's how it works:

  • First, you need to create a safe unique ID for the component instace; You can do this using this line: $"id_{Guid.NewGuid().ToString().Replace("-", "")}";
  • Create a JavaScript function that you can call from Blazor to initialize your component and its JavaScript state. blazorCarousel.init takes in the component ID, a DotNetObjectReference, and an ElementReference.
    • The component ID will be used as the key to store state on the blazorCarousel.instances object.
    • The DotNetObjectReference will be used to call back into the .NET Blazor component when events occur.
    • The ElementReference will be used to call Bootstrap's Carousel function.
    The blazorCarousel.init does the following:
    • Initialize the Bootstrap carousel by calling the jQuery Bootstrap carousel() function on the jQuery wrapped carouselNode.
    • Hook up a JavaScript callback function to the Carousel 'slide' event. The callback calls into the OnSlide .NET method passing in the event data.
    • Hook up a JavaScript callback function to the Carousel 'slid' event. The callback calls into the OnSlid .NET method passing in the event data.
    • Store state for the Carousel component on the blazorCarousel.instances object, using the component ID as key and the state as value.
      The state is holding on to the DotNetReference and jQuery wrapped carouselNode.
  • In the OnAfterRenderAsync, specifically on firstRender, the JavaScript blazorCarousel.init function is called.
  • When the Carousel Blazor component is removed from the application, the Dispose method will be called. Inside of Dispose, the JavaScript blazorCarousel.dispose function is called doing the following:
    • The jQuery Bootstrap dispose is called like this .carousel('dispose'). This is important to not cause memory leaks.
    • The instance state is removed from the blazorCarousel.instances object using the delete keyword.

    Note: IAsyncDisposable would be a better interface to implement since invoking the JavaScript will result in a Task. Unfortunately, IAsyncDisposable is not supported in .NET 3.1 Blazor, but it will be in future versions.

Live Sample:

Blazor Source:

@implements IDisposable
@using Microsoft.Extensions.Logging
@inject IJSRuntime js
@inject ILogger<Carousel> logger
<div class="carousel slide" data-ride="carousel" id="@carouselId" @ref="carouselReference">
  <div class="carousel-inner">
    @for (int i = 1; i <= 10; i++)
    {
      <div class="carousel-item @(i == 1 ? "active": "")">
        <svg class="bd-placeholder-img bd-placeholder-img-lg d-block w-100" width="800" height="400">
          <!-- SVG image content goes here -->
        </svg>
      </div>
    }
  </div>
  <a class="carousel-control-prev" data-target="#@carouselId" role="button" data-slide="prev">
    <span class="carousel-control-prev-icon" aria-hidden="true"></span>
    <span class="sr-only">Previous</span>
  </a>
  <a class="carousel-control-next" data-target="#@carouselId" role="button" data-slide="next">
    <span class="carousel-control-next-icon" aria-hidden="true"></span>
    <span class="sr-only">Next</span>
  </a>
</div>

@code{
  // prefix id with 'id_' to make sure id's don't start with a number or other invalid character
  // add Guid to ensure unique-ness but remove '-' as they are invalid characters for JS property names
  private string carouselId = $"id_{Guid.NewGuid().ToString().Replace("-", "")}";
  private ElementReference carouselReference;
  private DotNetObjectReference<Carousel> dotNetObjectReference;

  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    if (firstRender)
    {
      dotNetObjectReference = DotNetObjectReference.Create(this);
      await js.InvokeVoidAsync("blazorCarousel.init", carouselId, dotNetObjectReference, carouselReference);
    }
  }

  [JSInvokable]
  public void OnSlide(string direction, int from, int to)
  {
    logger.LogInformation($"Carousel is sliding from {from} to {to}.");
  }

  [JSInvokable]
  public void OnSlid(string direction, int from, int to)
  {
    logger.LogInformation($"Carousel slid from {from} to {to}.");
  }

  public void Dispose()
  {
    // IAsyncDisposable not supported yet in .NET Core 3.1
    Task.Run(() => js.InvokeVoidAsync("blazorCarousel.dispose", carouselId));
    dotNetObjectReference.Dispose();
  }
}

JavaScript Source:

var blazorCarousel = {
    instances: {},
    init: function (carouselId, dotNetReference, carouselNode) {
        var carousel = {
            dotNetReference: dotNetReference,
            $carouselNode: $(carouselNode)
        };
        carousel.$carouselNode.carousel();
        carousel.$carouselNode.on('slide.bs.carousel', function (event) {
            dotNetReference.invokeMethod("OnSlide", event.direction, event.from, event.to);
        });
        carousel.$carouselNode.on('slid.bs.carousel', function (event) {
            dotNetReference.invokeMethod("OnSlid", event.direction, event.from, event.to);
        });
        this.instances[carouselId] = carousel;
    },
    dispose: function (carouselId) {
        this.instances[carouselId].$carouselNode.carousel('dispose');
        delete this.instances[carouselId];
    }
};

Summary #

With Blazor, .NET Core introduced new API's to support JavaScript interop. Using IJSRuntime you can call JavaScript functions from .NET. With the global JavaScript variable DotNet, you can invoke static .NET methods marked with the JSInvokable attribute. By passing a DotNetObjectReference to JavaScript, JavaScript can call the .NET methods on the referenced .NET object.

The interop capabilities have some caveats:

  • You can only invoke global JavaScript functions and you cannot chain subsequent function calls in .NET Core 3.1. .NET 5 introduces the concept of IJSObjectReference which resolves this limitation.
  • You cannot create objects from classes/prototypes using the new keyword. Instead, you must create a dedicated global function to create those objects for you.
  • You can have a reference to a .NET object in JavaScript using DotNetObjectReference, but not a reference to JavaScript object in .NET. .NET 5 introduces the concept of IJSObjectReference which resolves this limitation.

If you need to support multiple instances of a component at the same time, you have to make sure the JavaScript state doesn't collide with each other. Develop your JavaScript so that the state of each instance is stored separately. You can generate an ID for each component and then store the ID as the key in a JavaScript object and store the state as the value.

Related Posts

Related Posts