In Modern Web Development in C# we discussed the Asp.NET Core Blazor framework, which allows .NET developers to use C# to build rich web applications, with modes for rendering both on a server and on the client browser. While the promise of such a framework is writing web applications without JavaScript, the reality is that the JavaScript ecosystem is larger, more mature, and has direct access to the HTML DOM. Blazor developers need to know how to tap into this ecosystem, when necessary, to manually update or listen for DOM events, or to add advanced components from a JavaScript library. Using ObjectReferences, we can communicate in two directions between JS and C# objects.
In this post, we are going to explore hooking up a JavaScript-designed rich text editor as a Blazor Component. We will use Quill as our editor. As we proceed, you will learn how to create IJSObjectReference
objects to call into JavaScript functions, and DotNetObjectReference
objects to call back to C# code.
You can begin with a dotnet new blazor wasm
blank template. The full code for this post can be browsed at https://github.com/dymaptic/dy-blazor-object-refs.
First, we will add a few links in our wwwroot/index.html
root web page. In the head
tag, let’s add the following css link.
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
And at the bottom of the body
tag, add the following JavaScript source.
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
Now, create a new JavaScript file in your wwwroot
directory, such as jsInterop.js
. Add the following function, where we introduce our first usage of an ObjectReference.
// export makes this function available by importing the file as a module
export function getQuill(containerId, dotNetRef, options) {
let quill = new Quill(`#${containerId}`, options);
let Delta = Quill.import('delta');
let change = new Delta();
// add an event listener to 'text-change'
quill.on('text-change', (delta) => {
change = change.compose(delta);
// pass the change delta back to .NET
dotNetRef.invokeMethodAsync('JsTextChanged', change);
})
// returns the object to .NET so that you can call functions on that object.
return quill;
}
You can see this basically sets up a new Quill
editor, attaches an event handler, and returns the editor. Note the dotNetRef
object. This is a DotNetObjectReference
from C#, which exposes one function, invokeMethodAsync
, which is used to call back into .NET.
Now, let’s create a Razor Component file called Quill.razor
, where we will create both the .NET ObjectReference, and the JavaScript ObjectReference.
@inject IJSRuntime JsRuntime
<div id="quill-container"></div>
@code
{
[Parameter]
public string? Theme { get; set; }
[Parameter]
public string? Placeholder { get; set; }
// allows calling code to listen for text changed
[Parameter]
public EventCallback<object> TextChanged { get; set; }
// this is the method called in JavaScript by `dotNetRef.invokeMethodAsync`
[JSInvokable]
public async Task JsTextChanged(object delta)
{
await TextChanged.InvokeAsync(delta);
}
// this method works as a "pass-through" to call into a JavaScript function from xona
public async Task<string> GetText()
{
return await _quill!.InvokeAsync<string>("getText");
}
public async Task Enable(bool enabled)
{
await _quill!.InvokeVoidAsync("enable", enabled);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// gets a reference to the file `jsInterop.js` as a JavaScript module
IJSObjectReference module = await JsRuntime.InvokeAsync<IJSObjectReference>("import", "./jsInterop.js");
Dictionary<string, object?> options = new();
if (Theme is not null)
{
options["theme"] = Theme;
}
if (Placeholder is not null)
{
options["placeholder"] = Placeholder;
}
// calls our JavaScript function and gets a reference to the Quill
_quill = await module.InvokeAsync<IJSObjectReference>("getQuill", "quill-container",
DotNetReference, options);
}
}
private IJSObjectReference? _quill;
private DotNetObjectReference<Quill> DotNetReference => DotNetObjectReference.Create(this);
}
Notice that there were two IJSObjectReference
objects created here, one for the module
, which points at our JavaScript file, and then one for the _quill
editor itself. While you can create top-level JavaScript functions, it is normally recommended to keep them all within modules.
The IJSObjectReference
allows you to dynamically call any function on the type that you referenced. According to the Quill API docs, there are a lot of functions we could invoke and expose here. We chose to implement two, getText
, to return the plain text of the editor, and enable
to toggle the editor on/off.
The DotNetObjectReference<Quill>
refers to this
Razor Component. This is passed into our JavaScript function to be used for the text-change
callback.
Here is our Pages/Index.razor
file, with all new code to inject and talk to our Quill
Component.
@page "/"
@using System.Text.Json
<PageTitle>Index</PageTitle>
<h1>Quill Editor</h1>
<button @onclick="ToggleQuill">@(_isEnabled ? "Disable Editor" : "Enable Editor")</button>
@* Here is our Quill component being created in the page *@
<Quill @ref="_quill"
Theme="snow"
Placeholder="Write a blog post..."
TextChanged="OnTextChanged" />
<h2>Delta Content</h2>
<div style="max-height: 400px; overflow-y: scroll">
@((MarkupString)_deltaJson)
</div>
<button @onclick="GetText">Get Text</button>
@if (!string.IsNullOrWhiteSpace(_textContent))
{
<h2>Raw Content</h2>
<div>@_textContent</div>
}
@code
{
private Quill _quill = default!;
// event handler for text changed
private async Task OnTextChanged(object delta)
{
// a little editing to make the json look good in html
_deltaJson = JsonSerializer.Serialize(delta, _jsonOptions)
.Replace(Environment.NewLine, "<br>")
.Replace(" ", "&nbsp;&nbsp;");
}
private async Task GetText()
{
// calls to get the current plain text
_textContent = await _quill.GetText();
}
private async Task ToggleQuill()
{
// toggles the editor on/off
_isEnabled = !_isEnabled;
await _quill.Enable(_isEnabled);
}
private string _deltaJson = string.Empty;
private string _textContent = string.Empty;
private bool _isEnabled = true;
private readonly JsonSerializerOptions _jsonOptions = new()
{
WriteIndented = true
};
}
Go ahead and run your project. You should see something like the screen shot below.
module
by calling JsRuntime.InvokeAsync("import",
"nameOfModuleFile.js")
.module.InvokeAsync("customExportFunction")
in reference to a function you defined that returns the object.ref.InvokeAsync
or ref.InvokeVoidAsync
.InvokeAsync
must be serializable. Note that HTMLElements
will often fail to serialize because of circular references.IJSObjectReference
. If you want to get an object as a data set with properties, you should retrieve it as a C# object
, dynamic
, or matching strongly typed class
, record
, or struct
, instead of as an IJSObjectReference
.DotNetObjectReference.Create(this)
. You could also pass a different .NET object reference in place of this
.DotNetObjectReference
as an argument to a JS function via module.InvokeAsync
or module.InvokeVoidAsync
before calling.[JSInvokable]
attribute.dotNetRef.invokeMethodAsync("methodName",
arguments...)
IJSObjectReference
, there is no way to reference class properties, only methods.JSInvokable
method parameters should be typed to match the incoming JSON with strongly typed objects, generic object
, or dynamic
.I hope this overview of the communication between JavaScript and C# in Blazor was helpful to you. Please share this post and let me know if you have questions via the comment form below, or by contacting tim.purdum@dymaptic.com. If you are looking for a well-wrapped and feature-rich GIS and Mapping Component library, check out GeoBlazor.