Create a desktop app with Electron, React and C#

An Electron is a framework to create native desktop applications for Windows, MacOS, and Linux. And what is the wow part in it, you can use vanilla javascript or any other javascript framework for building UI.

First years in my software craftsmanship started with Delphi 7. It was amazing, it was time when the internet was semi-empty. It was hard to find examples, ask help, basically, you were on your own. Ohh good old times, I wouldn’t change that experience, but I wouldn’t want to do that again.

Time goes on, everything evolves, new technologies come in play. Time to time, I stop and look at what has been changed. And that is so cool, always there is something new to look into. To change my own biases, yes painful, but the world isn’t stuck in time warp.

Ladies and gentlemen I bring to you my experience with Electron.

What is an Electron

An Electron is a framework to create native desktop applications for Windows, MacOS, and Linux. And what is the wow part in it, you can use vanilla javascript or any other javascript framework for building UI.

This is amazing, you can be a web app developer and by reusing the same skillset you can build a desktop app.

If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.### The project

Let us create an empty npm project

npm init --yes

Add Electron stuff and start command

npm i -D electron

Add start script inside package.json

"start":"electron ."
{
  "name": "electron-demo",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "Kristaps Vītoliņš",
  "license": "MIT",
  "devDependencies": {
    "electron": "^4.0.6"
  }
}

If we execute the npm start, we should get a popup error from Electron. That is ok, this only means that Electron is alive and we don’t know how to boot it.

The main and renderer process

Before we dive into coding, it is important to understand the basics of Electrons architecture.

If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.> If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.> If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.
My oversimplified explanation would be. There is a main process which creates a window and that window is a Chromium. Where Chromium is a process by itself aka renderer.

Typescript

Starting from the 1 June of the year 2017, Electron supports Typescript. Good, let’s use it.

npm i -D typescript
npm i -D tslint
npm i -D prettier

Add tslint.jsonin the project root

{
  "extends": "tslint:recommended",
  "rules": {
    "max-line-length": {
      "options": [
        120
      ]
    },
    "new-parens": true,
    "no-arg": true,
    "no-bitwise": true,
    "no-conditional-assignment": true,
    "no-consecutive-blank-lines": false
  },
  "jsRules": {
    "max-line-length": {
      "options": [
        120
      ]
    }
  }
}

Add tsconfig.json in the root of the project

{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "es5"
  },
  "exclude": [
    "node_modules"
  ],
  "compileOnSave": false,
  "buildOnSave": false
}

For typescript compilation, tsc could be used, but as the end game is to use React and manipulate with templates. Webpack is the way to go this time.

Web pack

Setup for the web pack

npm i -D webpack webpack-cli
npm i -D html-webpack-plugin
npm i -D @babel/cli @babel/core @babel/preset-env babel-loader @babel/plugin-proposal-class-properties @babel/plugin-transform-arrow-functions
npm i -D @babel/preset-typescript



Add .babel.rc in the root of the project

{
  "presets": [
    "@babel/env",
    "@babel/preset-typescript"
  ],
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-transform-arrow-functions"
  ]
}

So far so good, now let us create a main process of Electron and say hello to Mom!

Create a folder src

Add main.ts file in src folder

const url = require("url");
const path = require("path");

import { app, BrowserWindow } from "electron";

let window: BrowserWindow | null;

const createWindow = () => {
  window = new BrowserWindow({ width: 800, height: 600 });

  window.loadURL(
    url.format({
      pathname: path.join(__dirname, "index.html"),
      protocol: "file:",
      slashes: true
    })
  );

  window.on("closed", () => {
    window = null;
  });
};

app.on("ready", createWindow);

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", () => {
  if (window === null) {
    createWindow();
  }
});

line 8 creating a window and loading index.html into window aka Chromium.

Add index.html to the src folder

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Hi mom</title>
  </head>
  <body>
    <h1>Hi mom!</h1>
  </body>
</html>

Add webpack.config.js in the root. Instruction for the web pack how to handle Electrons main process build. Important target:"electron-main"

const path = require("path");
const HtmlWebPackPlugin = require("html-webpack-plugin");

const htmlPlugin = new HtmlWebPackPlugin({
  template: "./src/index.html",
  filename: "./index.html",
  inject: false
});

const config = {
  target: "electron-main",
  devtool: "source-map",
  entry: "./src/main.ts",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist")
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      }
    ]
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js"]
  },
  node: {
    __dirname: false,
    __filename: false
  },
  plugins: [htmlPlugin]
};

module.exports = (env, argv) => {
  return config;
};

Adjust a bit package.json

{
  "name": "electron-demo",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "build": "webpack --mode development",
    "start": "electron ./dist/main.js"
  },
  "keywords": [],
  "author": "Kristaps Vītoliņš",
  "license": "MIT",
  "devDependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.3.4",
    "@babel/plugin-proposal-class-properties": "^7.3.4",
    "@babel/plugin-transform-arrow-functions": "^7.2.0",
    "@babel/preset-env": "^7.3.4",
    "@babel/preset-typescript": "^7.3.3",
    "babel-loader": "^8.0.5",
    "electron": "^4.0.6",
    "html-webpack-plugin": "^3.2.0",
    "prettier": "^1.16.4",
    "tslint": "^5.13.1",
    "typescript": "^3.3.3333",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.2.3"
  }
}

Execute commands

npm run build
npm run start

And here it is. A fully functional desktop application. Which is saying hi to mom.

And this is actually the place where your front end developer skills kick in. As it is a Chromium you can use any kind of frontend technology, React, Vue, Angular, plain javascript.

The React

React will live in Electrons renderer process for that reason we will have to create a separate web pack build configuration. And teach babel to use react loader

npm i -D @babel/preset-react
npm i -S react react-dom
npm i -D @types/react @types/react-dom

Adjust.babelrc

{
  "presets": [
    "@babel/env",
    "@babel/preset-typescript",
    "@babel/preset-react"
  ],
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-transform-arrow-functions"
  ]
}

Adjust webpack.config.js by removing the responsibility of index.html template generation. It is the responsibility of the renderer build process.

const path = require("path");

const config = {
  target: "electron-main",
  devtool: "source-map",
  entry: "./src/main.ts",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist")
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      }
    ]
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js"]
  },
  node: {
    __dirname: false,
    __filename: false
  }
};

module.exports = (env, argv) => {
  return config;
};

Add new web pack config file webpack.react.config.js . This configuration is responsible for compiling react stuff and making sure that compiled result is injected insideindex.html

const path = require("path");
const HtmlWebPackPlugin = require("html-webpack-plugin");

const htmlPlugin = new HtmlWebPackPlugin({
  template: "./src/index.html",
  filename: "./index.html"
});

const config = {
  target: "electron-renderer",
  devtool: "source-map",
  entry: "./src/app/renderer.tsx",
  output: {
    filename: "renderer.js",
    path: path.resolve(__dirname, "dist")
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      }
    ]
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js"]
  },
  plugins: [htmlPlugin]
};

module.exports = (env, argv) => {
  return config;
};

Adjust index.html so that it contains a container where React can place its component.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Hi mom</title>
  </head>
  <body>
    <div id="renderer"></div>
  </body>
</html>

Create a folder app inside src and create renderer.tsx

import * as ReactDOM from 'react-dom';
import * as React from 'react';
import {Dashboard} from "./components/Dashboard";

ReactDOM.render(<Dashboard />, document.getElementById('renderer'));

Now let’s say Hello mom again, only now we will use a React to do so.

Create a folder components inside app and create Dashboard.tsx

import * as React from 'react';

export const Dashboard = () => {
    return <div>Hello Mom!</div>;
};

Adjust the package.json and add a new command so we can compile the renderer.

{
  "name": "electron-demo",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "build:react": "webpack --mode development --config webpack.react.config.js",
    "build": "webpack --mode development",
    "start": "electron ./dist/main.js"
  },
  "keywords": [],
  "author": "Kristaps Vītoliņš",
  "license": "MIT",
  "devDependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.3.4",
    "@babel/plugin-proposal-class-properties": "^7.3.4",
    "@babel/plugin-transform-arrow-functions": "^7.2.0",
    "@babel/preset-env": "^7.3.4",
    "@babel/preset-react": "^7.0.0",
    "@babel/preset-typescript": "^7.3.3",
    "@types/react": "^16.8.6",
    "@types/react-dom": "^16.8.2",
    "babel-loader": "^8.0.5",
    "electron": "^4.0.6",
    "html-webpack-plugin": "^3.2.0",
    "prettier": "^1.16.4",
    "tslint": "^5.13.1",
    "typescript": "^3.3.3333",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.2.3"
  },
  "dependencies": {
    "react": "^16.8.3",
    "react-dom": "^16.8.3"
  }
}
npm run build:react
npm run build
npm run start

Now, this is something I don’t see every day. A native desktop app running React inside of it. Well maybe I do, I just don’t know it as Electron is a popular framework and is used all over the place. For example, Visual Studio code, try to guess what is powering it ;).

The C# what?

A blog post by Rui Figueiredo ignited my interest in Electron.

Electron using C#. That is an interesting synergy going on here. And as it is C# core, it is cross-platform as well.

Cross-platform desktop app powered by Electron using React for UI and extended functionality by C# goodness. And now not only you can use your frontend skills, but backend as well.

Install new package for npm project

npm i -D electron-cgi

Adjust main.ts for a test run, we will send Mom to C# console app and it will return Hello Mom back. And we will console log that.

const { ConnectionBuilder } = require("electron-cgi");

...

let connection = new ConnectionBuilder()
  .connectTo("dotnet", "run", "--project", "./core/Core")
  .build();

connection.onDisconnect = () => {
  console.log("lost");
};

connection.send("greeting", "Mom", (response: any) => {
  console.log(response);
  connection.close();
});

Create a simple dotnet C# core console app. Add ElectronCgi.DotNet nuget.

using ElectronCgi.DotNet;

namespace Core
{
    class Program
    {
        static void Main(string[] args)
        {
            var connection = new ConnectionBuilder()
                .WithLogging()
                .Build();
            
            connection.On<string, string>("greeting", name => "Hello " + name);
            
            connection.Listen();    
        }
    }
}
npm run build
npm run start

Amazing isn’t it? Rui did go an extra mile and added this to C# as well

connection.OnAsync();

Now we are talking serious stuff. Imagen what possibilities this opens? Async communication to a database, to Rest API, to Queues, all nice packages for clouds, Amazon, Azure etc. All the good stuff from C# at your fingertips.

And it is not limited only to sending strings, it can be strongly typed object in C#.

Here is how

If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.> If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.> If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.> If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.
Heavy use of stdout. Which isn’t necessarily something bad, for example, php.exe and the frameworks which surround it ;).

I generally like the approach, clean, smart and innovating. I do endorse read a full blog post of Rui.

Extra mile with React and C#

Let’s make this even interesting, send the message to React from C#.

For that make changes in renderer by changing the Dashboard functional component to a component with the state.

import * as React from "react";
import { ipcRenderer } from "electron";

interface IState {
  message: string;
}

export class Dashboard extends React.Component<{}, IState> {
  public state: IState = {
    message: ""
  };

  public componentDidMount(): void {
    ipcRenderer.on("greeting", this.onMessage);
  }

  public componentWillUnmount(): void {
    ipcRenderer.removeAllListeners("greeting");
  }

  public render(): React.ReactNode {
    return <div>{this.state.message}</div>;
  }

  private onMessage = (event: any, message: string) => {
    this.setState({ message: message });
  };
}

What is going there? We subscribe to the channel greeging and upon receiving a message from the main process we put the message into the state. And from that point React takes ower, notices the state changes and renders the message.

Changes in the main process by sending received message from C# to React

const url = require("url");
const path = require("path");
const { ConnectionBuilder } = require("electron-cgi");

import { app, BrowserWindow } from "electron";

...

connection.send("greeting", "Mom from C#", (response: any) => {
  window.webContents.send("greeting", response);
  connection.close();
});

Create a legit EXE file

Add package

npm i -D electron-packager

Adjust the package.json file by adding package-win command and point main to dist folder file main.js.

Tutorial for all platform build commands here.

{
  "main": "dist/main.js",
  "scripts": {
    "package-win": "electron-packager . electron-demo --overwrite --asar=true --platform=win32 --arch=ia32 --icon=assets/icons/win/icon.ico --prune=true --out=release-builds --version-string.CompanyName=CE --version-string.FileDescription=CE --version-string.ProductName=\"Electron-demo\""
  }
}
npm run package-win

Copy C# folder “core” into “release-builds\electron-demo-win32-ia32” and run the electron-demo.exe

The end

This journey of mine from Delphi 7 to React, Electron, C# is awesome. As I said before, nothing is stuck in time, every day new technologies pop up which makes us better and breaks our personal biases.

To stay open minded is our biggest challenge.

Sourcecode of demo in GitHub

#reactjs #c-sharp #web-development

Create a desktop app with Electron, React and C#
125.55 GEEK