BlazorOnDesktop-870x220

We explore how to make Blazor web apps run on Desktop.

Blazor is one of the most exciting technologies for web developers on the .NET stack and allows for building client/server-side web apps entirely in C#. Blazor isn't just for web apps though and has clear implications for desktop/mobile.

Turns out, there are multiple ways of building modern desktop apps with Blazor. The techniques are nothing now—what's called for is a shell which hosts a desktop browser sandbox, that runs regular Blazor apps, just as if running on the web. The key to successful hybrid desktop apps, however, is in the implementation and managing resource bloat. And as one can see with the first .NET 6 Preview, Blazor on desktop has been a reality and the story only gets better. The lure is simple—use Razor syntax and the familiar Blazor component model towards building native desktop apps.

Let's take a closer look at desktop apps powered by Blazor—we'll explore two popular ways for some clarity and see the desktop shells in action.

The Electron Way

The desire to see web apps running on desktop has been long running. And given how exciting Blazor has been for .NET web developers, there has been a lot of zeal to make Blazor apps power desktop solutions. Enter the most ubiquitous solution for such needs—Electron. ElectronJS is an open source project to build cross-platform apps with web technologies and can target any desktop—Windows, Mac OS and Linux. Many of the most heavily-used desktop apps are essentially web apps wrapped inside the Electron shell, like Visual Studio Code, Microsoft Teams, Slack and Figma. Electron has been around for a while and gains credibility from a strong developer community/ecosystem.

Can Blazor web apps be wrapped inside the Electron shell to transform them into desktop apps? You bet.

First up, we start a Blazor server-side web project running on .NET Core 3.1. Next, we add the ElectronNET.API NuGet package that will enable the Blazor app to be bootstrapped within Electron.

BlazorInElectronProjectDeps

In our Program.cs, we enable the use of Electron with the function UseElectron() within the CreateHostBuilder() method.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ElectronNET.API;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace BlazorInShell
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseElectron(args);
                    webBuilder.UseStartup<Startup>();
                });
    }
}

Next up, we head over to the Startup.cs file and arrange for the Blazor app to be bootstrapped within the Electron shell. Notice how we're setting up a desktop window with dimensions and title, and firing it up.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using BlazorInShell.Data;
using ElectronNET.API;
using ElectronNET.API.Entities;

namespace BlazorInShell
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor();
            services.AddSingleton<WeatherForecastService>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapBlazorHub();
                endpoints.MapFallbackToPage("/_Host");
            });

            if (HybridSupport.IsElectronActive)
            {
                ElectronBootstrap();
            }

        }

        public async void ElectronBootstrap()
        {
            var browserWindow = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions
            {
                Width = 1920,
                Height = 1080,
                Show = false
            });

            await browserWindow.WebContents.Session.ClearCacheAsync();

            browserWindow.OnReadyToShow += () => browserWindow.Show();
            browserWindow.SetTitle("Blazor in Shell");
        }
    }
}

To bootstrap our Blazor app within Electron, we need to install the Electron.NET CLI tool. This is done from Terminal/Command Prompt of course.

dotnet tool install ElectronNET.CLI

While we can install the Electron.NET CLI globally, one needs to jump inside the Blazor project and initialize the Electron app—this drops a electron.manifest.json file within the project.

dotnet run electronize init     

The last step should create a few configurations; but if not, the launchSettings.json file can be manually updated.

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:37307",
      "sslPort": 44311
    }
  },
  "profiles": {
    "Electron.NET App": {
      "commandName": "Executable",
      "executablePath": "electronize",
      "commandLineArgs": "start",
      "workingDirectory": "."
    },

    "IIS Express":
    {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },

    "Electron":
    {
      "commandName": "Executable",
      "executablePath": "dotnet",
      "commandLineArgs": "electronize start",
      "workingDirectory": "$(ProjectDir)"
    },
    "BlazorInShell":
    {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
    }
}

We're now ready to run our Blazor app within the Electron shell. Time for the three little words, from within the project directory:

dotnet electronize start

If everything is set up right, the CLI command will show the Blazor app running on specified Localhost port—and a few things more.

ElectronCLI

Voila. Electron launches the desktop window with given dimensions and hosts the Blazor app within it. Just magical.

BlazorInElectron

Yes, you get a fully functional Blazor app—just hosted as a desktop application, running web code. And thanks to Electron, this would work on Windows, Mac OS and Linux. Hallelujah!

BlazorWeatherInElectron

A Few Considerations

Electron is pretty awesome. However, there is a small hesitation. And this stems from today's consciousness that a majority of people run a modern evergreen browser on their computers. One of Electron's key selling points may now be sowing the seeds of doubt in the minds of developers.

At the core of Electron's benefit as a shell to host web apps is the idea of a stable and predictable environment. To that end, every Electron app comes bundled with two things that provide dependability:

  1. Electron provides its own copy of Chromium engine for rendering consistency.
  2. Electron includes Node.JS runtime, which provides stability to the browser sandbox and offers OS integrations.

Let's keep in mind though that Electron is battle-tested and provides developers extensive APIs for the native system integration. Some of Electron's key benefits include direct access to file-system, running processes outside sandbox, kiosk mode, screen recording, system-tray support for minimized apps and much more. If Electron is serving your desktop needs today, there is no reason to look elsewhere.

However, what if today we assumed a stable computing environment and the presence of a modern browser? Could a shell like Electron do away with bundling Chromium and Node.JS? This is not an easy question to answer, since we are essentially trading stability for a smaller footprint. Perhaps your desktop app does not need deep integrations with the OS.

Steve Sanderson from the Blazor team wrote a detailed post with comparisons of app size and memory impact when Chromium and Node.JS are not bundled. It does beg the question as to whether lighter web shells could do the job of hosting modern Blazor apps with a level of consistency. Time will tell, but thankfully there are some alternatives to play around with.

The WebView Way

How do Blazor web developers reach mobile or desktop land? An elegant experimental solution is Blazor Mobile Bindings, enabling developers to build cross-platform native/hybrid apps with Razor syntax and the Blazor component model. Slated to be eventually a part of proposed .NET MAUI evolution with LTS .NET 6, Blazor Mobile Bindings sit on top of Xamarin.Forms technology stack and can easily reach desktop—Windows through WPF renderers and MacOS through Mac renderers for Xamarin.Forms.

Having a way to reach desktop while writing Blazor code, Blazor Mobile Bindings opens up hosting hybrid views—essentially a web shell inside the bootstrapped app that can run a web code. Sound familiar? This time though, we'll use the new BlazorWebView—a lightweight alternative that does not include Chromium or Node.JS.

First up is getting the CLI template and firing up a new hybrid 'Hello World' Blazor Mobile Bindings project.

dotnet new blazorhybrid -o BlazorHybridHW

This scaffolds a solution with several platform-specific projects—a .NET Standard library and projects for iOS, Android, Windows and MacOS. Here is a trimmed down version highlighting the MacOS project:

BlazorHybridProject

The shared project dependencies show reliance on Xamarin and Mobile Blazor Bindings, not much else.

BlazorHybridDeps

Essentially, this project is a combination of native and web UI lighting solutions for various mobile/desktop platforms. In the root folder of the shared project, App.cs provides the main UI entry point and things start up just as they do for every Xamarin.Forms app through Mobile Blazor Bindings. CounterState provides a component/service that would be used to keep track of click counters in both native/hybrid code.

using System;

namespace BlazorHybridHW
{
    internal class CounterState
    {
        public int CurrentCount { get; private set; }

        public void IncrementCount()
        {
            CurrentCount++;
            StateChanged?.Invoke();
        }

        public event Action StateChanged;
    }
}

Inside the shared project is a folder named WebUI—this is meant to house the Blazor app, with all the code and components as expected from a Blazor web project. There is also a wwwroot folder serving up static content, as one does for a regular Blazor web app.

BlazorHybridWebProject

Now we get to best part—Main.razor. This serves as the main UI rendered through Blazor, but is a combination of native and hybrid UI.

@inject CounterState CounterState

<ContentView>
    <StackLayout>

        <StackLayout Margin="new Thickness(20)" Orientation="StackOrientation.Horizontal">
            <Label Text="@($"Hello, World! {CounterState.CurrentCount}")" FontSize="40" HorizontalOptions="LayoutOptions.StartAndExpand" />
            <Button Text="Increment" OnClick="@CounterState.IncrementCount" VerticalOptions="LayoutOptions.Center" Padding="10" />
        </StackLayout>

        <BlazorWebView VerticalOptions="LayoutOptions.FillAndExpand">
            <BlazorHybridHW.WebUI.App />
        </BlazorWebView>

    </StackLayout>
</ContentView>

The code above almost reads like XAML, but with a few twists. The container is native through Xamarin.Forms, as is the first StackLayout containing the label and the button. But then comes BlazorWebView—the modern webview meant to host a full Blazor app or any other web app inside. And where do we get the Blazor app to host? The WebUI folder of course. You can see how nifty it is to have a full Blazor app within the project and weave in native UI, along with hosting hybrid web UI through the BlazorWebView.

The last piece of the puzzle is in the desktop specific projects for Windows or MacOS, using the corresponding renderers. For MacOS, this is no different from how the Xamarin.Forms renderers for MacOS bootstrap a desktop app—define an NSWindow with appropriate settings, initialize Xamarin.Forms and allow Forms to paint the UI inside the window.

using AppKit;
using Foundation;

namespace BlazorHybridHW.macOS
{
    [Register("AppDelegate")]
    public class AppDelegate : Xamarin.Forms.Platform.MacOS.FormsApplicationDelegate
    {
        public AppDelegate()
        {
            var style = NSWindowStyle.Closable | NSWindowStyle.Resizable | NSWindowStyle.Titled;
            var rect = new CoreGraphics.CGRect(200, 1000, 1024, 768);
            MainWindow = new NSWindow(rect, style, NSBackingStore.Buffered, false)
            {
                Title = "My Application",
                TitleVisibility = NSWindowTitleVisibility.Visible,
            };
        }

        public override NSWindow MainWindow { get; }

        public override void DidFinishLaunching(NSNotification notification)
        {
            // Menu options to make it easy to press cmd+q to quit the app
            NSApplication.SharedApplication.MainMenu = MakeMainMenu();

            Xamarin.Forms.Forms.Init();
            LoadApplication(new App());
            base.DidFinishLaunching(notification);
        }

        public override void WillTerminate(NSNotification notification)
        {
            // Insert code here to tear down your application
        }

        private NSMenu MakeMainMenu()
        {
            // top bar app menu
            var menubar = new NSMenu();
            var appMenuItem = new NSMenuItem();
            menubar.AddItem(appMenuItem);

            var appMenu = new NSMenu();
            appMenuItem.Submenu = appMenu;

            // add separator
            var separator = NSMenuItem.SeparatorItem;
            appMenu.AddItem(separator);

            // add quit menu item
            var quitTitle = string.Format("Quit {0}", "BlazorHybridHW.macOS");
            var quitMenuItem = new NSMenuItem(quitTitle, "q", delegate
            {
                NSApplication.SharedApplication.Terminate(menubar);
            });
            appMenu.AddItem(quitMenuItem);

            return menubar;
        }
    }
}

That's it. Time to set the desktop Windows/MacOS projects as startup and fire things up. Out comes a shiny desktop app showing the mix of native and hybrid UI. Hallelujah!

BlazorHybrid

Notice how the Counter component is shared between native Xamarin.Forms UI and the corresponding Blazor component—they remain in sync through both interfaces.

BlazorHybridCounter

So, you have a functional desktop app within a web shell that is hosting a full Blazor web app. Technology is beautiful.

Conclusion

Blazor is exciting and enables .NET web developers to build modern web apps with C#. Blazor invites developers into a rich ecosystem seeing lots of innovation and availability of polished UI component frameworks like Telerik UI for Blazor. However, Blazor does not need to be for web apps only and can play easily in the desktop space.

Blazor apps can be effortlessly wrapped inside Electron to make compelling and consistent desktop solutions. However, modern webviews provide a lightweight alternative to Electron, thus minimizing the footprint of Blazor apps running on desktop. Blazor Mobile Bindings provides an experimental way to write native/hybrid cross-platform apps using Razor syntax and the Blazor component model. The new BlazorWebView component allows for easy hosting of any web content, in particular, full Blazor apps as intended for the web. Modern technology stacks allow developers to mix and match coding paradigms and cross application platform barriers for added portability.

Upwards and onwards to what the future holds.


SamBasu
About the Author

Sam Basu

Sam Basu is a technologist, author, speaker, Microsoft MVP, gadget-lover and Progress Developer Advocate for Telerik products. With a long developer background, he now spends much of his time advocating modern web/mobile/cloud development platforms on Microsoft/Telerik technology stacks. His spare times call for travel, fast cars, cricket and culinary adventures with the family. You can find him on the internet.

Related Posts

Comments

Comments are disabled in preview mode.