Quick Start
Render your first document in minutes
This walkthrough takes an ASP.NET Core app from an empty Program.cs to a document rendered in the browser: server registration, the complete Viewer package (Viewer toolbar, Viewer mount, and optional Search/Annotation ribbons), asset references, client initialization, document opening, and execution.
Server setup
AddDoconut() registers the services; UseDoconutResources() and UseDoconut() wire the middleware. The resources call must come first. The session calls are required too — Doconut's default document security validates every page request against ASP.NET session state. Already registered Doconut during Installation? Skip ahead to the next section.
builder.Services.AddDoconut(options =>
{
options.LicensePath = "doconut.lic";
});
builder.Services.AddSession(); // Doconut document security rides on ASP.NET session state
app.UseSession(); // call UseSession() before UseDoconut()
app.UseDoconutResources(); // must be registered before UseDoconut()
app.UseDoconut();For a production-style path layout, map the document middleware to an explicit branch and keep the four path settings aligned:
builder.Services.AddDoconut(options =>
{
options.LicensePath = Path.Combine(AppContext.BaseDirectory, "Doconut.lic");
options.MiddlewarePath = "/doconut";
options.ResourcesPath = "/doconut-res";
options.UnsafeMode = false;
});
builder.Services.AddSession();
app.UseRouting();
app.UseSession();
app.UseDoconutResources();
app.Map("/doconut", branch => branch.UseDoconut());MiddlewarePath is a coordination value; it does not map an ASP.NET Core branch by itself. In this example the host maps /doconut, so the client must use BasePath: '/doconut'. ResourcesPath serves the embedded bundle at /doconut-res, and the widget's image resource path is therefore ResPath: '/doconut-res/images'.
Add the viewer to a page
The Viewer is the required core of the page. Its rendering surface uses two nested divs:
<div id="divDocViewer">
<div id="div_ctlDoc"></div>
</div>Treat the toolbar, module mounts, and Viewer surface as one page composition. Search and
Annotation inject their embedded ribbons into optional mounts, but those modules are never
standalone: they always attach to the Viewer on the same page. Use the same order as
Doconut.TestApp and Doconut.TestApp.Distributed:
<nav id="toolbar" aria-label="Document viewer controls">
<!-- Viewer navigation, zoom, Search, and Annotation buttons -->
</nav>
<div id="searchBarMount"></div>
<div id="annBarMount"></div>
<div id="divDocViewer">
<div id="div_ctlDoc"></div>
</div>Reference the viewer assets
In a Razor view, the injected Viewer service emits the viewer's <link> and <script> tags in dependency order — the widget is a jQuery plugin, so jQuery must be loaded before the viewer scripts:
@inject Doconut.Viewer Viewer
@Html.Raw(Viewer.ReferenceCss(new CssConfig
{
IncludeBootstrapCss = true,
IncludeViewerCss = true
}))
@Html.Raw(Viewer.ReferenceScripts(new ScriptConfig
{
IncludeJQuery = true,
IncludeBootstrap = true,
IncludeViewerScripts = true
}))For the complete Viewer package, request the Viewer and module resources together:
@Html.Raw(Viewer.ReferenceCss(new CssConfig
{
IncludeBootstrapCss = true,
IncludeViewerCss = true,
IncludeSearchCss = true,
IncludeAnnotationCss = true
}))
@Html.Raw(Viewer.ReferenceScripts(new ScriptConfig
{
IncludeJQuery = true,
IncludeBootstrap = true,
IncludeViewerScripts = true,
IncludeSearchScripts = true,
IncludeSearchBar = true,
IncludeAnnotationScripts = true,
IncludeAnnotationBar = true
}))IncludeViewerCss and IncludeViewerScripts are the mandatory core flags. Never publish a
Search or Annotation Ribbon example without them, the Viewer mount, and a docViewer
instance. ReferenceCss and ReferenceScripts omit an optional module's resources when
the current license does not grant that capability; the core Viewer still starts.
Initialise the viewer
The client-side widget is a jQuery plugin. This is a minimal set of real init options (not pseudocode):
let searchBar = null;
let annBar = null;
const objViewer = $('#div_ctlDoc').docViewer({
showThumbs: true,
autoLoad: false,
pageZoom: 100,
FitType: 'width',
BasePath: '/doconut',
ResPath: '/doconut-res/images',
onViewerReady: function () {
// pages are visible; safe to hide a loading spinner here
},
// Forward annotation lifecycle events to the embedded ribbon when it is present.
onAnnLoaded: () => annBar?.handleAnnLoaded(),
onAnnSaved: () => annBar?.handleAnnSaved(),
onAnnSaveError: () => annBar?.handleAnnSaveError(),
onAnnClosed: () => annBar?.handleAnnClosed(),
onError: function (message) {
console.error('Doconut viewer error:', message);
}
});The option casing is genuinely mixed — showThumbs, autoLoad, and pageZoom are camelCase, but FitType, BasePath, and ResPath are PascalCase. There is no consistent rule; get the casing wrong and the option is silently ignored (the widget falls back to its default instead of throwing).
Assemble the complete Viewer package
Both .NET 6 reference applications install the following parts together on one page:
| Part of the package | Requirement | How it is connected |
|---|---|---|
Viewer resources, mount, and objViewer | Required | Core document renderer |
| Viewer toolbar | Required in the reference composition | Host markup; buttons call the same objViewer |
| Search ribbon | Optional, licensed module | doconutSearchBar(...).attach(objViewer) |
| Annotation ribbon | Optional, licensed module | doconutAnnotationBar(...).attach(objViewer) |
Although the main Viewer toolbar is host markup, it is installed alongside the Viewer and must never be documented as an isolated control. This keeps its layout, labels, icons, and authorization rules under your application's control while every button drives the same Viewer instance:
<nav id="toolbar" aria-label="Document viewer controls">
<button type="button" onclick="objViewer.GotoPage(1)">First</button>
<button type="button" onclick="objViewer.Next(false)">Previous</button>
<button type="button" onclick="objViewer.Next(true)">Next</button>
<button type="button" onclick="objViewer.GotoPage(objViewer.TotalPages())">Last</button>
<button type="button" onclick="objViewer.Zoom(false)">Zoom out</button>
<button type="button" onclick="objViewer.Zoom(true)">Zoom in</button>
<button type="button" onclick="objViewer.FitType('width')">Fit width</button>
<button type="button" onclick="objViewer.FitType('height')">Fit height</button>
<button type="button" id="openSearch">Search</button>
<button type="button" id="openAnnotations">Annotations</button>
</nav>The full reference toolbar also copies wwwroot/js/viewerToolbar.js into the host
application for rotation, thumbnail, print, fullscreen, layout, and button-state helpers.
Load that host file after Viewer.ReferenceScripts(...). Keep the helper and its
<nav id="toolbar"> markup together when copying the full demo implementation.
Keep the package initialization order used by both reference applications:
- Emit CSS for the Viewer and licensed modules.
- Render the Viewer toolbar, Search/Annotation mounts, and Viewer mount together.
- Emit scripts for the Viewer and licensed modules.
- Load the host application's
viewerToolbar.js. - Initialize
docViewerand keep the resultingobjViewer. - Initialize each licensed Search or Annotation Ribbon.
- Call
attach(objViewer)on every Ribbon. - Open the document and retain its token for Viewer and module requests.
Doconut.TestApp.Distributed keeps this exact UI composition and the same Viewer-toolbar
helper. Its additional access request value and asynchronous-render retry settings belong
to the distributed transport; they do not change how the Viewer, toolbar, or Ribbons are
assembled.
The server-side guards are important: when an optional capability is unavailable, its script is not emitted, so its jQuery plug-in function does not exist.
<script>
let currentToken = '';
const refitViewer = () =>
requestAnimationFrame(() => objViewer.Refit());
@if (Viewer.IsSearchEnabled)
{
<text>
searchBar = $('#searchBarMount').doconutSearchBar({
docId: 'ctlDoc',
getRequestParams: () => ({ token: currentToken }),
onLayout: refitViewer
});
searchBar.attach(objViewer);
</text>
}
@if (Viewer.IsAnnotationEnabled)
{
<text>
annBar = $('#annBarMount').doconutAnnotationBar({
docId: 'ctlDoc',
getRequestParams: () => ({ token: currentToken }),
onLayout: refitViewer
});
annBar.attach(objViewer);
</text>
}
document.getElementById('openSearch').addEventListener('click', () => {
if (!searchBar) return;
searchBar.isOpen() ? searchBar.close() : searchBar.open();
});
document.getElementById('openAnnotations').addEventListener('click', () => {
if (!annBar) return;
annBar.isOpen() ? annBar.close() : annBar.open();
});
</script>Both embedded components generate their own Ribbon DOM. Search contains Find, Options,
and Results groups. Annotation contains its authoring tools, style controls, save actions,
and optional export/image actions. The bars expose open(), close(), reset(), and
isOpen(); always call attach(objViewer) once after creating them.
The example above omits optional host callbacks and Annotation export/image endpoints to keep startup minimal. See Search and Annotations for the complete feature-specific setup, or Custom Themes to style or replace the host-owned Viewer toolbar.
Open a document
The server side is one endpoint: the injected Viewer service opens the document and returns a session token.
app.MapPost("/api/open", async (Viewer viewer) =>
{
// The token is opaque — hand it to the widget, never log or persist it.
string token = await viewer.OpenDocumentAsync("wwwroot/files/Sample.pdf");
return Results.Ok(new { token });
});The client fetches that token and hands it to the widget with objViewer.View(token):
fetch('/api/open', { method: 'POST' })
.then(resp => resp.json())
.then(data => {
currentToken = data.token;
objViewer.View(currentToken);
});Close the document
Call objViewer.Close() when the user leaves the viewer or opens a replacement document. On server-driven workflows, viewer.CloseDocument(token) immediately removes the cached session, disposes the rendering engine, deletes its security marker, and revokes the token. Sliding expiration eventually performs the same cleanup, but explicit close is recommended for large documents.
The completed request flow is:
AddDoconut + middleware
-> render CSS/scripts and mount div
-> initialize docViewer
-> OpenDocumentAsync
-> return opaque token
-> objViewer.View(token)
-> page/search/annotation requests
-> Close / CloseDocumentTreat the token like a bearer credential: never log it, never persist it, hand it only to the widget. It identifies a live document session on the server and stops working when that session expires — re-open the document to get a fresh one.
Run it
Put a PDF at wwwroot/files/Sample.pdf, run dotnet run, and open the page that hosts the widget. The first page renders in the viewer, with a thumbnail panel on the left. If it doesn't, see Troubleshooting.
What you get without a license
A missing license does not throw. The viewer renders normally, but every page carries an evaluation watermark. See License Setup for how Doconut finds a license and what changes once it does.
Was this page helpful?