Combine Blazor and WinForms to Build a Document Scanning Desktop App

Since .NET 6 Preview 3, it is possible to host Blazor web applications in a WinForms/WPF app using the BlazorWebView control.

Behind BlazorWebView is WebView2, which uses Microsoft Edge as the rendering engine to display the web content in native apps. Developers can now adopt a hybrid app approach, utilizing both native and web technologies.

In the previous article, we’ve talked about Blazor Server and Blazor WebAssembly and how to use them to create a document scanning app with Dynamic Web TWAIN (DWT). In this article, we are going to demonstrate how to build a WinForms document scanning desktop app with Blazor.

Getting Started With Dynamic Web TWAIN

Building a WinForms Document Scanning App with Blazor

Let’s do this in steps.

New Project

Open Visual Studio, create a new C# WinForms project:

new project

Choose .NET 6:

.net 6

Add Blazor Functionality

  1. Update the SDK used in the app’s project file to Microsoft.NET.Sdk.Razor.

    - <Project Sdk="Microsoft.NET.Sdk">
    + <Project Sdk="Microsoft.NET.Sdk.Razor">
    
  2. Add a package reference to Microsoft.AspNetCore.Components.WebView.WindowsForms.

  3. Create a new wwwroot folder with an index.html file and an app.css file.

    HTML:

     <!DOCTYPE html>
     <html>
    
     <head>
         <meta charset="utf-8" />
         <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
         <title>Blazor app</title>
         <base href="/" />
         <link href="{PROJECT NAME}.styles.css" rel="stylesheet" /> <!-- remember to change {PROJECT NAME} -->
         <link href="app.css" rel="stylesheet" />
     </head>
    
     <body>
         <div id="app"></div>
    
         <div id="blazor-error-ui">
             An unhandled error has occurred.
             <a href="" class="reload">Reload</a>
             <a class="dismiss">🗙</a>
         </div>
    
         <script src="_framework/blazor.webview.js"></script>
     </body>
     </html>
    

    CSS:

     html, body {
         font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
     }
    
     .valid.modified:not([type=checkbox]) {
         outline: 1px solid #26b050;
     }
    
     .invalid {
         outline: 1px solid red;
     }
    
     .validation-message {
         color: red;
     }
    
     #blazor-error-ui {
         background: lightyellow;
         bottom: 0;
         box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
         display: none;
         left: 0;
         padding: 0.6rem 1.25rem 0.7rem 1.25rem;
         position: fixed;
         width: 100%;
         z-index: 1000;
     }
    
     #blazor-error-ui .dismiss {
         cursor: pointer;
         position: absolute;
         right: 0.75rem;
         top: 0.5rem;
     }
    
  4. Add a root Blazor component, Counter.razor, to the project:

     @using Microsoft.AspNetCore.Components.Web
    
     <h1>Counter</h1>
    
     <p>The current count is: @currentCount</p>
     <button @onclick="IncrementCount">Count</button>
    
     @code {
         int currentCount = 0;
    
         void IncrementCount()
         {
             currentCount++;
         }
     }
    
  5. In Form1, create a BlazorWebView control named blazor using Toolbox.

    toolbox

  6. Setup the BlazorWebView.

     public Form1()
     {
         InitializeComponent();
         var serviceCollection = new ServiceCollection();
         serviceCollection.AddBlazorWebView();
         blazor.Services = serviceCollection.BuildServiceProvider();
         blazor.HostPage = "wwwroot/index.html";
         blazor.RootComponents.Add<Counter>("#app");
     }
    

    Now, we can run the app to have a test.

    counter

Set up Dynamic Web TWAIN

Let’s continue to add document scanning ability to the app. First, we need to set up Dynamic Web TWAIN in the project.

  1. Download Dynamic Web TWAIN: Download Page.

  2. Put the Resources folder in wwwroot. You can find the folder in the place like C:\Program Files (x86)\Dynamsoft\Dynamic Web TWAIN SDK 17.2.5.

  3. Open Resources\dynamsoft.webtwain.config.js. We can fill in our license key to use the SDK (apply for a license). Meanwhile, disable AutoLoad because we will load it manually.

     Dynamsoft.DWT.AutoLoad = false;
     Dynamsoft.DWT.ProductKey = '<your license key>';
    
  4. Open wwwroot/index.html, add the following:

     <script src="Resources/dynamsoft.webtwain.initiate.js"></script>
     <script src="Resources/dynamsoft.webtwain.config.js"></script>
    

To check whether the SDK is successfully added, we can do the following to have a test.

  1. Add the following to body of wwwroot/index.html:

     <script>
         window.getVersion = () => {
             return Dynamsoft.DWT.JSVersion;
         };
     </script>
    
  2. Modify Counter.razor to check the DWT’s version. IJSRuntime is used for JavaScript interop:

     @using Microsoft.AspNetCore.Components.Web
     @using Microsoft.JSInterop
     @inject IJSRuntime JS
    
     @if (version != null)
     {
       <p>The current Dynamic Web TWAIN version is: @version</p>    
     }
    
     <button @onclick="CheckDWTVersion">Check version</button>
    
     @code {
         string version;
    
         async void CheckDWTVersion()
         {
             version = await JS.InvokeAsync<string>("getVersion");
             StateHasChanged();
         }
     }
    

version

Load DWT and Bind the Viewer

Next, we need to create a DWT object and add a viewer which can be used to view scanned documents.

  1. Create a new DWT.js file and include it in the index.html. We will write our JavaScript in it.

     <script src="DWT.js"></script>
    
  2. Add a div for viewer with its ID as dwtcontrolContainer in the razor file:

     <div id="dwtcontrolContainer"></div>
    
  3. Add the following to the DWT.js file:

     var DWObject = null;
    
     function CreateDWT() {
         return new Promise(function (resolve, reject) {
             var success = function (obj) {
                 DWObject = obj;
                 DWObject.Viewer.bind(document.getElementById('dwtcontrolContainer'));
                 DWObject.Viewer.width = 270;
                 DWObject.Viewer.height = 320;
                 DWObject.SetViewMode(2, 2);
                 DWObject.Viewer.show();
                 resolve(true);
             };
    
             var error = function (err) {
                 resolve(false);
             };
    
             Dynamsoft.DWT.CreateDWTObjectEx({
                 WebTwainId: 'dwtcontrol'
             },
                 success,
                 error
             );
         })
     }
    
  4. In the razor file, call the above JavaScript to create the DWT object and bind the viewer when it is initialized.

     protected override async void OnInitialized()
     {
         await CreateDWT();
     }
    
     private async Task CreateDWT()
     {
         await JS.InvokeVoidAsync("CreateDWT");
     }
    

Run the app and we can see an empty viewer.

viewer

Add a Scan Button

Next, we are going to scan documents.

  1. Add a Scan Button in the razor file:

     <button @onclick="Scan">Scan</button>
     @code {
         private async Task Scan()
         {
             await JS.InvokeVoidAsync("Scan");
         }
     }
    
  2. Create a Scan function in DWT.js:

     function Scan() {
         if (DWObject) {
             DWObject.SelectSource(function () {
                 DWObject.OpenSource();
                 DWObject.AcquireImage();
             },
                 function () {
                     console.log("SelectSource failed!");
                 }
             );
         }
     }
    

Now, we can scan documents in a Blazor app.

viewer with documents

Add Scan Options

In the above step, when users click the scan button, they have to select which scanner to use and select scanning options like resolution and color in a dialog.

We can pass those options manually.

Define a ScanOptions Class

Create a ScanOptions.cs file:

class ScanOptions
{
    public int SelectedIndex { get; set; }
    public int PixelType { get; set; }
    public int Resolution { get; set; }
}

Scan with Specified Options

Modify the Scan function in DWT.js to use the options:

function Scan(options) {
    return new Promise(function (resolve, reject) {
        if (DWObject) {

            DWObject.SelectSourceByIndex(options.selectedIndex);
            DWObject.CloseSource();
            DWObject.OpenSource();
            DWObject.IfShowUI = false;
            DWObject.PixelType = options.pixelType;
            DWObject.Resolution = options.resolution;
            
            var OnAcquireImageSuccess = function () {
                resolve(true);
            }
            var OnAcquireImageError = function () {
                resolve(false);
            }
            DWObject.AcquireImage(OnAcquireImageSuccess, OnAcquireImageError);
        } else {
            reject(false);
        }
    });
}

In the razor file, use the ScanOptions:

ScanOptions options = new ScanOptions();
options.PixelType = 0; //Black and White
options.Resolution = 300;
options.SelectedIndex = 0;
await JS.InvokeVoidAsync("Scan",options);

Add Options Controls

We can make those options configurable on the page. The EditForm component is used here.

@using Microsoft.AspNetCore.Components.Forms

<EditForm Model="scanOptions">
    Select Source:
    <InputSelect @bind-Value=scanOptions.SelectedIndex>
    @if (@scanners.Count>0)
    {
        for (int i = 0; i < scanners.Count; i++)
        {
            <option value="@i">@scanners[i]</option>
        }
    }
    </InputSelect>
    <br/>
    Resolution:
    <InputNumber style="width:50px;" @bind-Value=scanOptions.Resolution></InputNumber>
    <br/>
    Pixel Type:
    <InputRadioGroup @bind-Value=@scanOptions.PixelType>
        @foreach (DictionaryEntry option in pixelTypeOptions)
        {
            <InputRadio Value="option.Value" /> @option.Key
        }
    </InputRadioGroup>
</EditForm>

The form is bounded with the scanOptions variable.

@using System.Collections;
@using System.Collections.Specialized;
private List<string> scanners = new List<string>();
ListDictionary pixelTypeOptions = new ListDictionary();

private ScanOptions scanOptions = new ScanOptions() {
    PixelType = 0,
    SelectedIndex = 0,
    Resolution = 300,
};
protected override async void OnInitialized()
{
    pixelTypeOptions.Add( "B&W", 0 );
    pixelTypeOptions.Add( "Gray", 1 );
    pixelTypeOptions.Add( "Color", 2 );
    await CreateDWT();
}

The scanners list is loaded upon initialization.

In DWT.js:

function GetScannersList() {
    var scanners = [];
    if (DWObject) {
        var count = DWObject.SourceCount;
        for (var i = 0; i < count; i++) {
            scanners.push(DWObject.GetSourceNameItems(i));
        }
    }
    return scanners;
}

In the razor file:

protected override async void OnInitialized()
{
    await CreateDWT();
    scanners.AddRange(await JS.InvokeAsync<IEnumerable<string>>("GetScannersList"));
    StateHasChanged();
}

viewer with options

Add a Save Button

After documents are scanned, we can then save them as a PDF file. Use the following code to achieve such a function.

In DWT.js:

function Save() {
    return new Promise(function (resolve, reject) {
        DWObject.IfShowFileDialog = true;
        // The path is selected in the dialog, therefore we only need the file name
        DWObject.SaveAllAsPDF("Sample.pdf",
            function () {
                console.log('Successful!');
                resolve(true);
            },
            function (errCode, errString) {
                resolve(false);
            }
        );
    });
}

In the razor file:

<button @onclick="Save">Save</button>
private async Task Save()
{
    Boolean success = await JS.InvokeAsync<Boolean>("Save");
    if (success)
    {
        MessageBox.Show("Saved.");
    }
    else
    {
        MessageBox.Show("Failed.");
    }
}

A WinForms MessageBox will be used to notify users whether the documents are saved successfully. In the Blazor app, we can use all the WinForms APIs and .NET libraries, which is convenient.

messagebox

Source Code

https://github.com/xulihang/BlazorWinForms-Document-Scanner