Building Blazor Desktop Apps with Electron.NET
Quick Start Guide | |
---|---|
What You Will Need |
Visual Studio 2022 |
Controls Referenced | |
Tutorial Concept | This tutorial shows how to build an ASP.NET Blazor web app for the desktop. |
We wrote about using the open-source tool Electron.NET to implement cross-platform desktop applications for Windows, macOS, and Linux platforms in a previous blog post. 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 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:
Follow the steps below and get started by building the canonical Blazor application from the command line.
- Create a .NET 8 Blazor Application
- Electronize It
- Install the Command Line Tool
- Run the Electronized Application
- Debug Blazor Code
- Customize the Sample Data Page
- Add the Detail View
- Add Third-Party Controls
- Build for Other Platforms
Ready to Try It Out? Download ComponentOne Today!
Create .NET 8 Blazor Application
For this tutorial, we will use the new Blazor Web App Project Template in Visual Studio 2022. This creates a single Blazor app that supports both server-side rendering and client interactivity. It was introduced with .NET 8.
Select Server interactive render mode. This will give you just a Blazor Server module.
You can keep the other default options when creating the project.
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, add a package reference for the Electron.NET API–hosted on NuGet.org:
Next, edit Program.cs and insert a using statement for the newly added package:
using ElectronNET.API;
Locate the static method CreateBuilder and insert the following two lines below it:
builder.WebHost.UseElectron(args);
builder.WebHost.UseEnvironment("Development");
The first line is necessary. The second is convenient during development, as it allows detailed error messages to be displayed.
Finally, add the following code after the app runs or starts to create the main Electron window:
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).
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
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.
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.
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.)
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.
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:
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:
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.
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="8.0.20241.*" />
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:
To access more Blazor Calendar and other UI control samples, download the Blazor Edition.
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:
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 here.
Ready to Try It Out? Download ComponentOne Today!