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.

The host application must target a runtime that supports netstandard2.1 — .NET Core 3.0 or later. The reference applications shipped with this release target net8.0.

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.Viewer.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();

The reference application keeps the historic DocImage.axd request shape and mounts the document middleware on a matching branch. This is the arrangement to copy when you are carrying an existing .NET Standard application forward, because the client keeps calling the same URL it always did:

csharp
builder.Services.AddDoconut(options =>
{
    options.UnsafeMode      = false;   // keep Doconut's session check enabled
    options.ShowDoconutInfo = false;
});
builder.Services.AddSession(options =>
{
    options.IdleTimeout        = TimeSpan.FromSeconds(3600);
    options.Cookie.HttpOnly    = true;
    options.Cookie.IsEssential = true;
});

// Required for multi-byte encodings in some document formats.
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

app.UseStaticFiles();
app.UseRouting();
app.UseSession();
app.UseDoconutResources();   // BEFORE the document branch
app.MapWhen(
    context => context.Request.Path.Value?.EndsWith("DocImage.axd", StringComparison.OrdinalIgnoreCase) == true,
    branch => branch.UseDoconut());

UseDoconut() takes no options in this release — everything moved to AddDoconut(). MiddlewarePath is a coordination value; it does not map an ASP.NET Core branch by itself. 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.

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.

These calls replace hand-copied script files. If the application still loads its own docViewer.js, documentLinks.js, or docViewer.UI.js, remove those tags once the page works from the emitted resources — see the migration guide.

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:   '/',
    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);
    }
});

Two details decide whether this page works at all:

  • Callbacks are options. onViewerReady, onViewerBusy, onCopy, and onAutoLoadStatus are passed in the options object. The previous viewer derived global function names from the container id and called them; this one does not. A handler declared only as a global function never runs, and nothing is reported.
  • ResPath points at the resources prefix, not the application root. With the default ResourcesPath, that value is doconut-res/images. If you changed ResourcesPath, use your own prefix instead.

Both of these fail silently — see Troubleshooting and the migration guide.

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

Keep the package initialization order used by both reference applications:

  1. Emit CSS for the Viewer and licensed modules.
  2. Render the Viewer toolbar, Search/Annotation mounts, and Viewer mount together.
  3. Emit scripts for the Viewer and licensed modules.
  4. Load the host application's own toolbar helper script.
  5. Initialize docViewer and keep the resulting objViewer.
  6. Initialize each licensed Search or Annotation Ribbon.
  7. Call attach(objViewer) on every Ribbon.
  8. Open the document and retain its token for Viewer and module requests.

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. Opening is asynchronous in this release.

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);
    });

There is nothing to warm up before the first open. If you are carrying an application forward, delete any InitCache() call and the flag guarding it — the method no longer exists.

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.

หน้านี้เป็นประโยชน์หรือไม่?