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.

csharp
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:

csharp
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:

html
<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:

html
<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:

html
@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
@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):

javascript
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 8 reference applications install the following parts together on one page:

Part of the packageRequirementHow it is connected
Viewer resources, mount, and objViewerRequiredCore document renderer
Viewer toolbarRequired in the reference compositionHost markup; buttons call the same objViewer
Search ribbonOptional, licensed moduledoconutSearchBar(...).attach(objViewer)
Annotation ribbonOptional, licensed moduledoconutAnnotationBar(...).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:

html
<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:

  1. Emit Viewer, Search, and Annotation resources together.
  2. Render the Viewer toolbar, Ribbon mounts, and Viewer mount together.
  3. Initialize docViewer first.
  4. Create each licensed Ribbon and attach it to that same objViewer.
  5. Open the document and retain its token for 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.

html
<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.

csharp
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):

javascript
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:

text
AddDoconut + middleware
    -> render CSS/scripts and mount div
    -> initialize docViewer
    -> OpenDocumentAsync
    -> return opaque token
    -> objViewer.View(token)
    -> page/search/annotation requests
    -> Close / CloseDocument

Treat 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?