12 Oct 2021

Use Viewer from Blazor

Blazor is a framework for building interactive client-side web UI with .NET

It has two hosting models: Blazor Server and Blazor WebAssembly

If you are using Blazor WebAssembly then all your code resides on the client side. If you wanted to do a 2-legged authentication from it in order to access the files in your buckets then the code would need to have your Client Secret as well, which would expose it to anyone.

Client Secret exposed

If you use Blazor WebAssembly you should have the authentication related code on the server side, so that you can hide your credentials. Once you publish your Blazor WebAssembly using `dotnet publish` the generated files could be served by any HTTP server: ASP.NET, nodejs, etc - for testing I was simply using the Visual Studio Code extension called Live Server.

If using Blazor Server than you can keep all your credentials safe on the server.

To test the Viewer with both Blazor hosting models I just created a sample project of each type:
`dotnet new blazorwasm -o forge-viewer-blazor-wasm` and `dotnet new blazorserver -o forge-viewer-blazor-server`

In case of both projects I added ForgeViewer.js under wwwroot/js

ForgeViewer.js

var viewer;

async function launchViewer(urn, accessToken) {
  var options = {
    env: "AutodeskProduction",
    getAccessToken: (callback) => callback(accessToken, 3600),
  };

  Autodesk.Viewing.Initializer(options, () => {
    var documentId = "urn:" + urn;
    Autodesk.Viewing.Document.load(
      documentId,
      onDocumentLoadSuccess,
      onDocumentLoadFailure
    );
  });
}

function onDocumentLoadSuccess(doc) {
  var viewables = doc.getRoot().getDefaultGeometry();
  viewer = new Autodesk.Viewing.GuiViewer3D(
    document.getElementById("forgeViewer"),
    { extensions: ["Autodesk.DocumentBrowser"] }
  );
  viewer.start();
  viewer.loadDocumentNode(doc, viewables).then((i) => {
    // documented loaded, any action?
  });
}

function onDocumentLoadFailure(viewerErrorCode, viewerErrorMsg) {
  console.error(
    "onDocumentLoadFailure() - errorCode:" +
      viewerErrorCode +
      "\n- errorMessage:" +
      viewerErrorMsg
  );
}

Inside NavMenu.razor I added this after the last </li> tag:

<li class="nav-item px-3">
  <NavLink class="nav-link" href="viewer">
    <span class="oi oi-image" aria-hidden="true"></span> Viewer
  </NavLink>
</li>

Added a Viewer.razor file in the Pages folder. The content was different for the two hosting models. In the case of Blazor Server I was doing the authentication call on the server side and made it available for the page through a service called ForgeTokenService:

Viewer.razor (in Pages folder)

@page "/viewer"

@using forge_viewer_blazor_server.Data
@inject ForgeTokenService ForgeTokenService
@inject IJSRuntime JS

<h1>Viewer</h1>

<p>This component demonstrates using the Forge Viewer in a Blazor Server project.</p>

@if (token == null)
{
  <p><em>Loading...</em></p>
}
else
{
  <div id="forgeViewer" style="width: 800px; height: 400px; position: relative"></div>
}

@functions {
  public string token;

  protected override async Task OnInitializedAsync()
  {
    if (token == null) {
      token = await ForgeTokenService.GetForgeTokenAsync();
      Console.WriteLine(token);
      ValueTask t = JS.InvokeVoidAsync(
        "launchViewer", 
        "dXJu..", // file urn
        token
      );
    } 
  }
}

ForgeTokenService.cs (in Data folder)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace forge_viewer_blazor_server.Data
{
  public class ForgeTokenService
  {
    public async Task<string> GetForgeTokenAsync()
    {
      var dict = new Dictionary<string, string>();
      dict.Add("client_id", "<client id>");
      dict.Add("client_secret", "<client secret>");
      dict.Add("grant_type", "client_credentials");
      dict.Add("scope", "viewables:read");
      var response =
        await new HttpClient()
          .PostAsync("https://developer.api.autodesk.com/authentication/v1/authenticate",
          new FormUrlEncodedContent(dict));
      var token = await response.Content.ReadAsAsync<TokenResponse>();

      return token.access_token;
    }
  }

  public class TokenResponse
  {
    public string access_token;
  }
}

I also had to add this line to Startup.cs inside the ConfigureServices() function to make the service available:

services.AddSingleton<ForgeTokenService>();

In the case of Blazor WebAssembly, the Viewer.razor page included all these things:

@page "/viewer"
@using System.Collections.Generic
@inject HttpClient Http
@inject IJSRuntime JSRuntime

<h1>Viewer</h1>

<p>This component demonstrates using the Forge Viewer in a Blazor WebAssembly project.</p>

@if (token == null)
{
  <p><em>Loading...</em></p>
}
else
{
  <div id="forgeViewer" style="width: 800px; height: 400px; position: relative"></div>
}

@code {
  private TokenResponse token;

  protected override async Task OnInitializedAsync()
  {
    var dict = new Dictionary<string, string>();
    dict.Add("client_id", "<client id>");
    dict.Add("client_secret", "<client secret>");
    dict.Add("grant_type", "client_credentials");
    dict.Add("scope", "viewables:read");
    // Should be done on the server!!!
    var response = await Http.PostAsync(
      "https://developer.api.autodesk.com/authentication/v1/authenticate", 
      new FormUrlEncodedContent(dict)
    );
    
    token = await response.Content.ReadAsAsync<TokenResponse>();
    await JSRuntime.InvokeVoidAsync(
      "launchViewer", 
      "dXJu...", // file urn
      token.access_token
    );
  }

  public class TokenResponse {
    public string access_token; 
  }
}

In order to trigger our JavaScript code, we had to use JSRuntime.InvokeVoidAsync - see JavaScript Interop

Then had to add references to the Viewer css and js files plus ForgeViewer.js. In the case of Blazor Server this had to go in the <head> section of _Host.cshtml (Pages folder), in case of Blazor WebAssembly the same references had to be added to the <head> section of index.html (wwwroot folder)  

<link rel="stylesheet" href="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/style.min.css" type="text/css">
<script src="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.min.js"></script>
<script src="js/ForgeViewer.js"></script>

In order to use response.Content.ReadAsAsync<TokenResponse>(); in the code, I also had to add Microsoft.AspNet.WebApi.Client to both projects using

dotnet add package Microsoft.AspNet.WebApi.Client

In order to test the projects you can just use:

dotnet run

Source code: https://github.com/adamenagy/forge-viewer-blazor 

Related Article