Building Blazor Desktop Apps with Electron.NET
Tags: webdev, devops, dotnet, tutorial
Published: 2023-11-08 21:11:54

---
canonical_url: https://developer.mescius.com/blogs/building-blazor-desktop-apps-with-electron-dot-net
description: This article illustrates how you can use Electron.NET to create various Blazor desktop applications.
---
We wrote about using the open-source tool [Electron.NET](https://github.com/ElectronNET/Electron.NET) to implement cross-platform desktop applications for Windows, macOS, and Linux platforms in a previous [blog post](https://developer.mescius.com/blogs/building-cross-platform-desktop-apps-with-electron-dot-net). By providing a wrapper around a standard Electron application with an embedded ASP.NET Core website, Electron.NET allows C# developers to target multiple platforms without coding in JavaScript.

This blog post illustrates how you can apply the same techniques to create Blazor desktop applications. Topics covered include:

*   Modifying the default Blazor Server application to use Electron.NET
*   Using Visual Studio Code to debug Blazor pages
*   Implementing native UI elements such as message boxes
*   Adding third-party Blazor controls
*   Building deployment media for other platforms

### Background

[Electron](http://electronjs.org/) is a framework that supports the development of desktop applications using web technologies such as the Chromium rendering engine and the Node.js runtime. Supported operating systems include Windows, macOS, and Linux. It leverages familiar standards such as HTML, CSS, and JavaScript.

Electron.NET allows .NET developers to invoke native Electron APIs using C#. It consists of two components:

*   A NuGet package that adds Electron APIs to an ASP.NET Core or Blazor project.
*   A .NET Core command-line extension that builds and launches applications for Windows, macOS, and Linux platforms.

Electron.NET requires the prior installation of the following software:

*   [Node.js](https://nodejs.org/)
*   [.NET 5.0](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)

Follow the steps below and get started by building the canonical Blazor application from the command line.

1.  [Create a Blazor Server Application](#Create-App)
2.  [Electronize It](#Electronize)
3.  [Install the Command Line Tool](#Install-Command)
4.  [Run the Electronized Application](#Run-App)
5.  [Debug Blazor Code](#Debug)
6.  [Customize the Sample Data Page](#Customize)
7.  [Add the Detail View](#Add-Detail)
8.  [Add Third-Party Controls](#Add-Third)
9.  [Build for Other Platforms](#Build)

### <a id="Create-App"></a>Create a Blazor Server Application

For this exercise, we're using Visual Studio Code running on a Mac. First, open a terminal window and run the following commands to create a new project called _BlazorApp_.

```
    mkdir BlazorApp  
    cd BlazorApp  
    dotnet new blazorserver  
    code .  
```

When prompted by Visual Studio Code, say _Yes_ to load the required assets for the project. Press F5 to build and run the application, then open a browser on localhost:5001 to view the default Blazor page. Close the page, return to VS Code, and stop debugging.

### <a id="Electronize"></a>Electronize It!

Let's turn our boilerplate Blazor project into a desktop Electron application. This involves adding a NuGet package to the project file, inserting some initialization code, and installing a command-line tool to perform builds. First, open the file _BlazorApp.csproj_ and insert a package reference for the Electron.NET API–hosted on nuget.org:

```
    <ItemGroup>  
      <PackageReference Include="ElectronNET.API" Version="11.5.1" />  
    </ItemGroup>  
```

Save the file, then restore packages when prompted to do so by VS Code. This restore gives you immediate access to Intellisense for subsequent modifications to the code.

Next, edit _Program.cs_ and insert a using statement for the newly added package:

```
    using ElectronNET.API; 
```

Locate the static method **CreateHostBuilder** and insert the following two lines before the call to **UseStartup**:

```
    webBuilder.UseElectron(args);  
    webBuilder.UseEnvironment("Development");  
```

The first line is necessary. The second is convenient during development, as it allows detailed error messages to be displayed.

Edit _Startup.cs_ and insert the following using statement:

```
    using ElectronNET.API;  
```

Locate the **Configure** method and add the following lines to the end of its body:

```
    if (HybridSupport.IsElectronActive)  
    {  
        CreateWindow();  
    } 
```

Finally, add the following method to the **Startup** class to create the main Electron window:

```
    private async void CreateWindow()  
    {  
        var window = await Electron.WindowManager.CreateWindowAsync();  
        window.OnClosed += () => {  
            Electron.App.Quit();  
        };  
    }  
```

Since our application consists of a single window, we handle the **OnClosed** event to terminate the application if the user closes the window (instead of choosing Quit or Exit from the main menu).

### <a id="Install-Command"></a>Install the Command Line Tool

In addition to the runtime package that you previously referenced in the project file, Electron.NET provides a command line tool to perform build and deployment tasks. In VS Code, create a new terminal window and type:

```
    dotnet tool install ElectronNET.CLI -g  
```

This one-time step will install a .NET Core global tool that implements a command named _electronize_. To see a list of tools/commands installed on your system, type the following:

```
    dotnet tool list -g
```

### <a id="Run-App"></a>Run the Electronized Application

After installing the command-line tool, type these lines in the VS Code terminal window:

```
    electronize init  
    electronize start 
```

The first line is a one-time step that creates a manifest file named _electron.manifest.json_ and adds it to your project. Use the second line to launch the Electron application (don't use F5, as this will only open the Blazor application in the browser). Note that the content now appears in an application window, not a browser.

![Blazor application](https://global-cdn.grapecity.com/blogs/componentone/20210325-building-blazor-desktop-apps-with-electron-dot-net/blazor-app-1.jpg)

Note the default Electron application menu. On a Mac, this menu is not part of the window itself, but anchored to the top of the screen.

![Electron dot NET application](https://global-cdn.grapecity.com/blogs/componentone/20210325-building-blazor-desktop-apps-with-electron-dot-net/electron-2.jpg)

### <a id="Debug"></a>Debug Blazor Code

Since we launched our application with an external command instead of F5, we need to attach a debugger to the running Blazor process. With the application window open, go to VS Code, open _Pages/Counter.razor_, and set a breakpoint on line 14\. Click Run on the activity bar, select _.NET Core Attach_ from the dropdown control, then click the adjacent icon to reveal a list of processes.

Start typing the application's name (BlazorApp) into the list and select the one remaining item. (If by some chance there are multiple processes still displayed, pick the one with the most significant value of _electronWebPort_.)

![Attaching to a-process for debugging in VS Code](https://global-cdn.grapecity.com/blogs/componentone/20210325-building-blazor-desktop-apps-with-electron-dot-net/dot-net-core-attach-3.png)

In the application window, visit the Counter page and click the button to trigger the breakpoint. Continue execution, close the application window, and note that the debugger is automatically disconnected.

### <a id="Customize"></a>Customize the Sample Data Page

To illustrate the cross-platform capabilities of Electron.NET, let's replace the default weather data page with a list of active system processes. Later on, we'll build a Linux version and observe the differences on that platform.

First, create a new file in the _Data_ folder named _ProcessService.cs_ and enter the following code:

```
    using System;  
    using System.Linq;  
    using System.Threading.Tasks;  
    using System.Diagnostics;

    namespace BlazorApp.Data  
    {  
        public class ProcessService  
        {  
            public Task<Process[]> GetProcessesAsync()  
            {  
                return Task.FromResult(Process.GetProcesses().Where(p => !String.IsNullOrEmpty(p.ProcessName)).ToArray());  
            }  
        }  
    }  
```

Next, register this service by opening _Startup.cs_ and adding the following line to the end of the **ConfigureServices** method:

```
    services.AddSingleton<ProcessService>();  
```

Create a new file in the _Pages_ folder named _Processes.razor_ and paste the following code:

```
    @page "/processes"

    @using BlazorApp.Data  
    @using System.Diagnostics  
    @inject ProcessService ProcessService

    <h1>Processes</h1>

    <p>This component displays a list of system processes.</p>

    @if (processes == null)  
    {  
        <p><em>Loading...</em></p>  
    }  
    else  
    {  
        <table class="table">  
            <thead>  
                <tr>  
                    <th>Id</th>  
                    <th>Process Name</th>  
                    <th>Physical Memory</th>  
                </tr>  
            </thead>  
            <tbody>  
                @foreach (var process in processes)  
                {  
                    <tr>  
                        <td>@process.Id</td>  
                        <td>@FormatName(process)</td>  
                        <td>@process.WorkingSet64</td>  
                    </tr>  
                }  
            </tbody>  
        </table>  
    }

    @code {  
        private System.Diagnostics.Process[] processes;

        protected override async Task OnInitializedAsync()  
        {  
            processes = await ProcessService.GetProcessesAsync();  
        }

        private string FormatName(System.Diagnostics.Process process)  
        {  
            string name;  
            const int maxLength = 30;  
            try  
            {  
                name = process.MainModule.ModuleName;  
            }  
            catch  
            {  
                name = process.ProcessName;  
            }  
            if (name.Length > maxLength)  
            {  
                name = name.Substring(0, maxLength) + "...";  
            }  
            return name;  
        }  
    } 
```

This will display a table of named processes with columns for the id number, process name, and the amount of physical memory allocated for the process. Note the use of the **FormatName** function. On some platforms, the process may be truncated so the module name is favored, with the process name serving as a fallback value. In either case, the result string is truncated to 30 characters.

Lastly, edit the file _Shared/NavMenu.razor_ and replace the final list element with the following:

```
    <li class="nav-item px-3">  
        <NavLink class="nav-link" href="processes">  
            <span class="oi oi-cog" aria-hidden="true"></span> Processes  
        </NavLink>  
    </li> 
```

Electron.NET supports a watch mode where it will monitor your changes and automatically rebuild and relaunch your application. To invoke the watch mode, run the following command:

```
    electronize start /watch  
```

Now, save all of your changes to the project. After the application restarts, click the Processes link on the left, and you will see a display similar to the following:

![Blazor application running processes](https://global-cdn.grapecity.com/blogs/componentone/20210325-building-blazor-desktop-apps-with-electron-dot-net/processes-4.jpg)

### <a id="Add-Detail"></a>Add the Detail View

In a typical CRUD application, items in a list contain a link to a detail page where users can view the item in greater detail or modify it if appropriate. Let's create a simple view for an individual process. First, add a new file to the _Pages_ folder named _Process.razor_ and insert the following markup:

```
    @page "/process/{Id:int}" 

    @using BlazorApp.Data  
    @using ElectronNET.API  
    @using ElectronNET.API.Entities  
    @inject ProcessService ProcessService  
    @inject NavigationManager UriHelper

    <h1>Process view</h1>

    <p>This component displays details for a single system process.</p>

    @if (process == null)  
    {  
        <p><em>Loading...</em></p>  
    }  
    else  
    {  
        <div>  
            <dl class="row">  
                @foreach (var property in @PropertyList.Select(name => typeof(System.Diagnostics.Process).GetProperty(name)))  
                {  
                <dt class="col-sm-4">  
                    @property.Name  
                </dt>  
                <dd class="col-sm-8">  
                    @property.GetValue(process)  
                </dd>  
                }  
            </dl>  
        </div>  
        <div>  
            <hr />  
            <button class="btn btn-danger" @onclick="@(() => Delete())">Kill Process</button>  
            <a class="btn btn-light" href="processes">Back to List</a>  
        </div>  
    }

    @code {  
        private System.Diagnostics.Process process;  
        private static readonly string[] PropertyList = new[]  
        {  
            "Id", "ProcessName", "PriorityClass", "WorkingSet64"  
        };

        [Parameter]  
        public int Id { get; set; }

        protected override void OnParametersSet()  
        {  
            process = System.Diagnostics.Process.GetProcessById(Id);  
        }

        private async Task Delete()  
        {  
            await Task.Run(() =>  
            {  
                process.Kill();  
                UriHelper.NavigateTo("processes");  
            });  
        }  
    } 
```

The first line specifies the routing for the detail page with an integer parameter that denotes the process id number. The string array **PropertyList** defines the list of **Process** object properties displayed in the detail view. Rather than hard code these strings in the page markup, we use reflection to derive property names and values at run time.

To link the detail view to individual items on the Processes page, edit _Pages/Processes.razor_ and replace the expression:

```
    @process.Id 
```

with this anchor tag:

```
    <a href="process/@process.Id">@process.Id</a> 
```

Run the application. Note that the _Id_ column now contains hyperlinks that navigate to a page similar to this one:

![Blazor application single process](https://global-cdn.grapecity.com/blogs/componentone/20210325-building-blazor-desktop-apps-with-electron-dot-net/process-view-5.jpg)

You may have noticed that the **onclick** handler for the Kill Process button unconditionally calls the Kill method without giving the user a chance to think it over and cancel the operation. Let's rewrite the **Delete** method to use the **ShowMessageBoxAsync** API of Electron.NET to display a platform-specific confirmation dialog box to the user:

```
    private async Task Delete()  
    {  
        const string msg = "Are you sure you want to kill this process?";  
        MessageBoxOptions options = new MessageBoxOptions(msg);  
        options.Type = MessageBoxType.question;  
        options.Buttons = new string[] {"No", "Yes"};  
        options.DefaultId = 1;  
        options.CancelId = 0;  
        MessageBoxResult result = await Electron.Dialog.ShowMessageBoxAsync(options);

        if (result.Response == 1)  
        {  
            await Task.Run(() =>  
            {  
                process.Kill();  
                UriHelper.NavigateTo("processes");  
            });  
        }  
    }
```

This way, if the user cancels, the detail page remains current. Otherwise, the application redirects to the Processes page after killing the process.

![Electron dot NET application](https://global-cdn.grapecity.com/blogs/componentone/20210325-building-blazor-desktop-apps-with-electron-dot-net/killing-the-process-6.jpg)

### <a id="Add-Third"></a>Add Third-Party Controls

As with any Blazor project, you can add third-party controls to an Electron.NET application. Let's replace the Counter page with a ComponentOne calendar control. First, add the following package reference to the .csproj file:

```
    <PackageReference Include="C1.Blazor.Calendar" Version="6.0.20232.620*" />
```

Edit _Pages/_Host.cshtml_ and insert the following stylesheet references before the closing tag:

```
    <link rel="stylesheet" href="/_content/C1.Blazor.Core/styles.css" />  
    <link rel="stylesheet" href="/_content/C1.Blazor.Calendar/styles.css" />  
```

Also, in the same file, insert the following script references before the closing tag:

```
    <script src="/_content/C1.Blazor.Core/scripts.js"></script>  
    <script src="/_content/C1.Blazor.Calendar/scripts.js"></script>
```

Create a new file in the _Pages_ folder named _Calendar.razor_ and paste the following code:

```
    @page "/calendar"

    @using C1.Blazor.Calendar

    <h1>Calendar</h1>

    <C1Calendar></C1Calendar>
```

Lastly, edit the file _Shared/NavMenu.razor_ and replace the second list element with the following:

```
    <li class="nav-item px-3">  
        <NavLink class="nav-link" href="calendar">  
            <span class="oi oi-calendar" aria-hidden="true"></span> Calendar  
        </NavLink>  
    </li>
```

Save all of your changes. Note that the build process will create a 30-day trial license for ComponentOne Studio Enterprise. Click the Calendar link on the left, and you should see a display similar to the following:

![Blazor calendar control](https://global-cdn.grapecity.com/blogs/componentone/20210325-building-blazor-desktop-apps-with-electron-dot-net/calendar-7.jpg)

To access more Blazor Calendar and other UI control samples, download the Blazor Edition.

### <a id="Build"></a>Build for Other Platforms

To build installation media for other platforms, run the following command in a terminal window:

```
    electronize build /target xxx /PublishReadyToRun false 
```

Where _xxx_ is one of win, linux, or osx. Output goes to the _bin/Desktop_ folder, for example:

*   BlazorApp Setup 1.0.0.exe (windows)
*   BlazorApp-1.0.0.AppImage (linux)
*   BlazorApp-1.0.0.dmg (osx)

Note that the Windows executable is a setup program, not the application itself. Users can only build OSX targets on a Mac, but Windows/Linux targets can be built on any platform. To change the version number, copyright notice, and other attributes, edit _electron.manifest.json_ before building the installation media.

Here's what the application looks like running on Linux:

![Blazor application Linux](https://global-cdn.grapecity.com/blogs/componentone/20210325-building-blazor-desktop-apps-with-electron-dot-net/processes-8.jpg)

### Conclusion and Sample Code

Electron.NET is an open-source tool that adds value to Blazor by providing C# developers with a vehicle for delivering cross-platform desktop applications for Windows, Linux, and macOS. It is also compatible with third-party components such as the Blazor controls in ComponentOne Studio Enterprise.

The source code for the completed project described in this article is available on GitHub at [https://github.com/jjuback/BlazorApp](https://github.com/jjuback/BlazorApp).

* * *