Blazor is a web framework provided by Microsoft that allows developers to build interactive applications using C# and .NET. In this tutorial, I’ll cover the basics of setting up a minimal 3D model viewer using HOOPS Communicator.
Before beginning, it is recommended that you follow Micrsoft’s Blazor Intro tutorial here.
Also, this tutorial assumes you’ve already downloaded the HOOPS Communicator package (you can start a free evaluation here.)
-
Launch Visual Studio 2022 → Create a New Project → search “blazor” and select “Blazor Web App”
-
Name your project, select the location, and click “Next”.
-
For “Additional information”, confirm the default settings and click “Create”:
-
Click the “Start Debugging” button (green arrow) to start the app and confirm it runs as expected (see the Microsoft tutorial mentioned above if not):
First, we’ll add a simple 3D model viewer to the homepage.
- In the Solution Explorer, navigate to the Pages directory, right click “Pages” and click “Add”, then “Razor Component”:
Name this Viewer.razor
and click “Add”.
- At the bottom of
Home.razor
, add<Viewer />
so this component will appear on the home page:
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<Viewer />
-
Add the assets required for model viewing - the model and the HOOPS Communicator JavaScript module. (For simplicity, we will be using the monolith version of the web viewer.) In your project’s “wwwroot” directory, add:
a)
microengine.scs
, found inHOOPS_Communicator\quick_start\converted_models\standard\scs_models
.b)
hoops-web-viewer-monolith.mjs
, found inHOOPS_Communicator\web_viewer
. -
In
Viewer.razor
, add the container for the web viewer:
<div id="viewer" style="position: absolute; width: 800px; height: 500px; margin: 0 auto; border: 2px solid black">
</div>
- Set up the component for JavaScript interop and interactivity by adding the following at the top of
Viewer.razor
:
@rendermode InteractiveServer
@inject IJSRuntime JS
@implements IAsyncDisposable
-
Right click the Pages directory in the Solution Explorer, select “Add Item”, and create
Viewer.razor.js
. -
In this new file, add the following Communicator code:
import * as Communicator from "../../hoops-web-viewer-monolith.mjs"
let hwv;
export function startViewer() {
hwv = new Communicator.WebViewer({
containerId: "viewer",
endpointUri: "./microengine.scs"
});
hwv.start();
}
- Now, to call this code, go back to
Viewer.razor
and add the following in the@code {}
section:
@code {
private IJSObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Load Communicator:
module = await JS.InvokeAsync<IJSObjectReference>("import", "./Components/Pages/Viewer.razor.js");
await module.InvokeVoidAsync("startViewer");
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
}
- Finally, to enable serving of SCS files and prevent caching of the app as changes are made during development, open your project’s
Program.cs
, locateapp.UseStaticFiles
, and replace it with the following:
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".scs"] = "application/octet-stream";
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = provider,
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Append(
"Clear-Site-Data", "\"*\""); // Prevent caching in development
}
});
- Click the “Start Debugging” button to see the basic model viewer:
Let’s add a bit more interactivity to the app.
First, we’ll add a drop down menu to change the draw mode of the model.
After that, we’ll use a callback to update the UI to display the name and ID of the node that’s selected.
- Add the following function to
Viewer.razor.js
:
export function setDrawMode(value) {
let drawMode;
switch (value) {
case "WireframeOnShaded":
drawMode = Communicator.DrawMode.WireframeOnShaded;
break;
case "Shaded":
drawMode = Communicator.DrawMode.Shaded;
break;
case "Wireframe":
drawMode = Communicator.DrawMode.Wireframe;
break;
case "HiddenLine":
drawMode = Communicator.DrawMode.HiddenLine;
break;
case "XRay":
drawMode = Communicator.DrawMode.XRay;
break;
case "Toon":
drawMode = Communicator.DrawMode.Toon;
break;
case "Gooch":
drawMode = Communicator.DrawMode.Gooch;
break;
default:
console.log("Invalid draw mode selected.");
}
hwv.view.setDrawMode(drawMode);
}
- In
Viewer.razor
, add a<select>
element:
<div id="viewerControls" style="display: block; margin: 20px 0px;">
<label for="drawModeSelection">Draw Mode:</label>
<select id="drawModeSelection" @onchange="UpdateDrawMode">
<option value="WireframeOnShaded" selected>WireframeOnShaded</option>
<option value="Shaded">Shaded</option>
<option value="Wireframe">Wireframe</option>
<option value="HiddenLine">HiddenLine</option>
<option value="XRay">XRay</option>
<option value="Toon">Toon</option>
<option value="Gooch">Gooch</option>
</select>
</div>
- In the
@code {}
section, defineUpdateDrawMode
:
private async void UpdateDrawMode(ChangeEventArgs args)
{
if (module is not null)
{
await module.InvokeVoidAsync("setDrawMode", args.Value);
}
}
- Now, to display the selected node info when the model is clicked, first set up the selectionArray callback in
Viewer.razor.js
:
hwv.setCallbacks({
selectionArray: (selectionEvents) => {
if (selectionEvents.length === 0) {
return;
};
const lastSelection = selectionEvents[0];
const nodeId = lastSelection.getSelection().getNodeId();
const nodeName = hwv.model.getNodeName(nodeId);
}
});
- In
Viewer.razor
, create an output area to display the selected node name and ID below the viewerControls<div>
and the viewer<div>
:
<div id="nodeInfo">
<p>Selected node: <span id="selectedNode">@selectedNode</span></p>
</div>
- In the
@code {}
section, add aDotNetObjectReference
to pass the .NET instance to JS and add the variableselectedNode
:
private DotNetObjectReference<Viewer>? dotNetHelper;
private string selectedNode = "Select a node";
- Set up the object reference and update the
startViewer
call:
dotNetHelper = DotNetObjectReference.Create(this);
await module.InvokeVoidAsync("startViewer", dotNetHelper);
- Provide a function to update the state of the selected node using
[JSInvokable]
:
[JSInvokable]
public void updateSelectedNode(string nodeInfo)
{
selectedNode = nodeInfo;
StateHasChanged();
}
- In
Viewer.razor.js
, update the startViewer parameter:
export function startViewer(dotNetHelper) { ...
- Update the selectionArray callback to call the .NET function:
selectionArray: (selectionEvents) => {
if (selectionEvents.length === 0) {
dotNetHelper.invokeMethodAsync("updateSelectedNode", "Select a node");
return;
};
const lastSelection = selectionEvents[0];
const nodeId = lastSelection.getSelection().getNodeId();
const nodeName = hwv.model.getNodeName(nodeId);
dotNetHelper.invokeMethodAsync("updateSelectedNode", `${nodeName} (Node ID: ${nodeId})`);
}
- Finally, let’s update
Home.razor
- delete the “Hello World” and “Welcome to your new app” lines and replace them with something more appropriate like:
<h1>Simple CAD Model Viewer</h1>
Click “Start Debugging” to run your app!
Below is the full code:
Home.razor
@page "/"
<PageTitle>Home</PageTitle>
<h1>Simple CAD Model Viewer</h1>
<Viewer />
Viewer.razor
@rendermode InteractiveServer
@inject IJSRuntime JS
@implements IAsyncDisposable
<div id="viewerControls" style="display: block; margin: 20px 0px;">
<label for="drawModeSelection">Draw Mode:</label>
<select id="drawModeSelection" @onchange="UpdateDrawMode">
<option value="WireframeOnShaded" selected>WireframeOnShaded</option>
<option value="Shaded">Shaded</option>
<option value="Wireframe">Wireframe</option>
<option value="HiddenLine">HiddenLine</option>
<option value="XRay">XRay</option>
<option value="Toon">Toon</option>
<option value="Gooch">Gooch</option>
</select>
</div>
<div id="nodeInfo">
<p>Selected node: <span id="selectedNode">@selectedNode</span></p>
</div>
<div id="viewer" style="position: absolute; width: 800px; height: 500px; margin: 0 auto; border: 2px solid black">
</div>
@code {
private IJSObjectReference? module;
private DotNetObjectReference<Viewer>? dotNetHelper;
private string selectedNode = "Select a node";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Load Communicator:
module = await JS.InvokeAsync<IJSObjectReference>("import", "./Components/Pages/Viewer.razor.js");
// Set up the reference:
dotNetHelper = DotNetObjectReference.Create(this);
await module.InvokeVoidAsync("startViewer", dotNetHelper);
}
}
private async void UpdateDrawMode(ChangeEventArgs args)
{
if (module is not null)
{
await module.InvokeVoidAsync("setDrawMode", args.Value);
}
}
[JSInvokable]
public void updateSelectedNode(string nodeInfo)
{
selectedNode = nodeInfo;
StateHasChanged();
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
}
Viewer.razor.js
import * as Communicator from "../../hoops-web-viewer-monolith.mjs"
let hwv;
export function startViewer(dotNetHelper) {
hwv = new Communicator.WebViewer({
containerId: "viewer",
endpointUri: "./microengine.scs"
});
hwv.setCallbacks({
selectionArray: (selectionEvents) => {
if (selectionEvents.length === 0) {
dotNetHelper.invokeMethodAsync("updateSelectedNode", "Select a node");
return;
};
const lastSelection = selectionEvents[0];
const nodeId = lastSelection.getSelection().getNodeId();
const nodeName = hwv.model.getNodeName(nodeId);
dotNetHelper.invokeMethodAsync("updateSelectedNode", `${nodeName} (Node ID: ${nodeId})`);
}
});
hwv.start();
}
export function setDrawMode(value) {
let drawMode;
switch (value) {
case "WireframeOnShaded":
drawMode = Communicator.DrawMode.WireframeOnShaded;
break;
case "Shaded":
drawMode = Communicator.DrawMode.Shaded;
break;
case "Wireframe":
drawMode = Communicator.DrawMode.Wireframe;
break;
case "HiddenLine":
drawMode = Communicator.DrawMode.HiddenLine;
break;
case "XRay":
drawMode = Communicator.DrawMode.XRay;
break;
case "Toon":
drawMode = Communicator.DrawMode.Toon;
break;
case "Gooch":
drawMode = Communicator.DrawMode.Gooch;
break;
default:
console.log("Invalid draw mode selected.");
}
hwv.view.setDrawMode(drawMode);
}
Program.cs
using BasicApp.Components;
using Microsoft.AspNetCore.StaticFiles;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".scs"] = "application/octet-stream";
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = provider,
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Append(
"Clear-Site-Data", "\"*\""); // Prevent caching
}
});
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
Project structure