Swimburger

Interacting with JavaScript Objects using the new IJSObjectReference in Blazor

Niels Swimberghe

Niels Swimberghe - - .NET

Follow me on Twitter, buy me a coffee

Blazor next to JavaScript logo besides title: Interacting with JavaScript Objects using the new IJSObjectReference in Blazor

New API's to interact with JavaScript from .NET were added with the introduction of Blazor. Using these API's you can invoke JavaScript functions from .NET and .NET methods from JavaScript. This is also referred to as 'JavaScript interoperability'.

The JavaScript interoperability API's provided by Microsoft live under Microsoft.JSInterop namespace. These capabilities were showcased at a previous article: "Communicating between .NET and JavaScript in Blazor with in-browser samples".

Some additional API's have now been introduced with the release of .NET 5. One very important API in particular was introduced: IJSObjectReference.

Calling JavaScript functions before IJSObjectReference #

Using the IJSRuntime interface, you can call JavaScript functions from .NET. Here's an example which calls the prompt and alert JavaScript function on the global window object:

@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}!");
    }
}

This is fairly straightforward, but you can quickly run into the limitations of IJSRuntime. Using IJSRuntime you can only call JavaScript functions that are available on the global window object in the browser. Meanwhile in JavaScript it is very common for one function to return an object holding other functions to invoke.

This is taken to the extreme when JavaScript libraries are designed to be consumed as fluent API's. For example, most of jQuery's functions return a jQuery object which you then call another jQuery function on, which in turn returns a jQuery object, and the cycle continues until broken off. Here's an example of jQuery function chaining:

const currentOrigin = location.origin;
$(`a:not([href^="${currentOrigin}"])`)
  .attr('rel', 'noopener nofollow noreferrer')
  .attr('target', '_blank')
  .addClass('external-link'); 

This script finds all links on the page where the href does not start with the current origin. In other words, any link that points to an external website. Then the rel and target attribute are modified appropriately. Then the ‘external-link' CSS class is added to style the external link differently.

No sane person would actually try to write this code using Blazor JavaScript interop. Instead, you should wrap the code into a global JavaScript function and invoke it when necessary with JS interop like this: await JS.InvokeVoidAsync("configureExternalLinks")

The real challenge starts when you need to invoke JavaScript on state that isn't global or singleton. If you have multiple instances of a Blazor component at the same time, and the component holds some state in JavaScript, your global JavaScript function needs to somehow access the correct component state to perform its functionality. 

In this Bootstrap Carousel example, this is solved by generating a unique id for each component instance. Whenever the Blazor component invokes JavaScript functions, that unique id is passed as a parameter so that the JavaScript can store and fetch the appropriate state:

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];
    }
};

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();
  }
}

Live Sample:

For more details on this component, read the explanation at "Communicating between .NET and JavaScript in Blazor with in-browser samples".

Generating a unique ID, passing it to every single JavaScript function, and then developing your JavaScript to manage the state in a global object is a very cumbersome way to deal with the limitation of IJSRuntime. In .NET Core 3.1, there's no way around doing this, but in .NET 5 you can now use IJSObjectReference.

Calling JavaScript functions with IJSObjectReference #

IJSObjectReference is a new type introduced with .NET 5 which holds a reference to a JavaScript object and has the same methods as IJSRuntime to invoke JavaScript functions.
But whereas IJSRuntime invokes the JavaScript functions available on the window object, IJSObjectReference will invoke the JavaScript functions available on that JavaScript object.

The way you can obtain an instance of IJSObjectReference, is by calling a JavaScript function using IJSRuntime which returns a JavaScript object. In .NET, you have to specify IJSObjectReference as the generic type parameter. Here's an example:

var objectReference = await JS.InvokeAsync<IJSObjectReference>("getJavaScriptObject");

Now that you have the reference to a JavaScript object, you can invoke its functions like this:

await objectReference.InvokeVoidAsync("doSomething");

This means you can now update the Bootstrap Carousel code to be more concise and intuitive.
You'll still need to generate a unique id, but only for the purpose of the Carousel's HTML attributes. Instead of passing that unique id to every JavaScript function, you can add a JavaScript function that creates a JavaScript object holding onto the state and functions.

JavaScript Source:

var createBlazorCarousel = function(){
    return {
        dotNetReference: null,
        $carouselNode: null,
        init: function (dotNetReference, carouselNode) {
            this.dotNetReference = dotNetReference;
            this.$carouselNode = $(carouselNode);
            this.$carouselNode.carousel();
            this.$carouselNode.on('slide.bs.carousel', function (event) {
                dotNetReference.invokeMethod("OnSlide", event.direction, event.from, event.to);
            });
            this.$carouselNode.on('slid.bs.carousel', function (event) {
                dotNetReference.invokeMethod("OnSlid", event.direction, event.from, event.to);
            });
        },
        dispose: function () {
            this.$carouselNode.carousel('dispose');
        }
    }
}

Blazor Source:

@implements IAsyncDisposable
@using Microsoft.Extensions.Logging
@inject IJSRuntime js
@inject ILogger<CarouselV2> 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<CarouselV2> dotNetObjectReference;
  private IJSObjectReference carouselJSObjectReference;

  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    if (firstRender)
    {
      dotNetObjectReference = DotNetObjectReference.Create(this);
      carouselJSObjectReference = await js.InvokeAsync<IJSObjectReference>("createBlazorCarousel");
      await carouselJSObjectReference.InvokeVoidAsync("init", 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 async ValueTask DisposeAsync()
  {
    await carouselJSObjectReference.InvokeVoidAsync("dispose");
    await carouselJSObjectReference.DisposeAsync();
    dotNetObjectReference.Dispose();
  }
}

Live Sample:

Summary #

IJSRuntime allows you to call JavaScript functions from .NET in Blazor, but only functions that are available on the global window object. You cannot call JavaScript functions on objects returned from a function in .NET Core 3.1. You can work around this limitation by storing identifiers on the .NET side and pass them to global JavaScript functions, but this can be very cumbersome. Writing more global JavaScript functions to call the many nested objects and functions is increasingly hard as the complexity of your application increases.

Luckily, a new type is introduced in .NET 5 called IJSObjectReference. This type holds a reference to a JavaScript object and can be used to invoke functions available on that JavaScript object. To obtain an instance of IJSObjectReference, you can call a JavaScript function using the IJSRuntime methods. The JavaScript function has to return a JavaScript object and you have to specify IJSObjectReference as the generic type parameter on IJSRuntime.InvokeAsync.
With IJSObjectReference you can now hold onto JavaScript state more easily and write more intuitive JavaScript interop code.

Related Posts

Related Posts