I’m very happy to share with you version 1.0 of ElectronCGI.
If you don’t know what ElectronCGI is, it’s a “two-part” library that allows invoking .NET code from Node.js and Node.js code from .NET. It’s two part because it is comprised of a npm package called electron-cgi and a NuGet package called ElectronCGi.DotNet.
Why is ElectronCGI useful?
Because it allows you to leverage tools like Electron to build a UI for a desktop application and still have all non-UI code written using .NET.
How is ElectronCGI different than other existing alternatives (for example Electron.NET)?
ElectrongCGI does not rely on HTTP for communication. With ElectronCGI a .NET process is launched and maintained through a “connection”.
The connection is established using the processes’ STDIN and STDOUT streams, which are provided by the OS. There’s almost no boilerplate required.
The motivating force for doing this is .NET Core being cross platform but not having a good story regarding GUIs outside of Windows.
This blog post will focus on the changes and new features of version 1.0. If you want a more in-depth introduction to ElectronCGI you can find it in these two blog posts: ElectronCGI – Cross Platform .Net Core GUIs with Electron and ElectronCGI – A solution to cross-platform GUIs for .Net Core.
Also, at the end of this blog post there will be a brief description (more in-depth in a latter blog post) of how ElectronCGI allowed me to port a Windows Store App that was built using MVVM, making it cross platform. All of this with 100% non-UI code reuse.
Version 1.0
The biggest (and breaking) change that version 1.0 introduces is related to the callbacks’ signatures.
In this version callbacks follow Node.js’ convention of having an error object as first argument, and the result as the second argument.
Previously we had this:
connection.send('requestType', args, response => {
//use response
});
But now we have this:
connection.send('requestType', args, (err, response) => {
if (err) {
//handle err
return
}
//use response
});
Alternatively we can now omit the callback and .send
will return a promise so we can write:
try{
const result = await connection.send('requestType', args);
//use result
}catch(err) {
//handler err
}
And this leads us to the second and main improvement in this version of ElectronCGI, which is error propagation.
Error propagation
In previous versions if there was an exception in a handler in .NET, the .NET process would crash and the error would be written to the log file (if logging was enabled).
In this version the exception is serialized and sent to the Node.js process. For example, if this is the handler in .NET:
connection.On("greet", (string name) => {
if (string.IsNullOrEmpty(name)) {
throw new ArgumentException("Name is required", "Name");
}
return "hello " + name;
});
And this request is performed in Node.js:
connection.send('greet', '', (err, res) => {
if (err) {
//err is the .NET exception
console.log(err.Message); // -> Name is required
}
//...
});
or using async await
:
try {
const greeting = await connection.send('greet', '');
}catch(err) {
console.log(err.Message);
}
Now you’ll have a chance to handle the error and, more importantly, the connection will remain open.
Memory Ace
Memory Ace is an application I created for the Window Store around 2014. I had just read Charles Petzold’s Programming Windows 8 Apps with C# and XAML book.
At the time there was excitement about Windows having a store and there was this idea that was being pushed of your apps having an audience of billions of users. There were even developers claiming they were making $30K/month on the windows store.
My guess is that at the time Microsoft was pumping ad money to kick off the store. I missed that wave and never made more than pocket change.
Memory Ace was the first app I created. It was the way I found to make myself familiar with the platform.
Looking back, it would never be even remotely popular. It’s a “game” where you have to memorize a full deck of cards (yes, that’s right, all 52 of them if you want) in the least amount of time. If that sounds like something that is not a lot of fun, I have to agree, but it’s probably the most mindful exercise you can practice. And, although there aren’t any studies I know of that support it, it probably improves your memory.
Anyway, the interesting thing about Memory Ace is that it follows the recommendations in Charles Petzold’s book and uses view models. View models have no dependencies on anything platform specific (i.e. opening files, dialogs, etc).
Because of that I was able to port 100% of it (the view models) to Linux using ElectronCGI (I had to re-create the UI, I used React).
Here’s how it looks (this gif was captured in Ubuntu):
Have a look at Memory Ace in github. The readme file has instructions on how to run it.
This is what gives me confidence to call this version of ElectronCGI 1.0. I was able to comfortably develop this port without any issues and it even was a pleasant development experience.
I won’t go into much detail on the steps (I’m planning to write another blog post with that) but I’ll summarize what I had to do in order to use the view models with ElectronCGI.
In case you are not familiar with the concept of a ViewModel, the idea is that the ViewModel encapsulates the data (Model) displayed on the view and also its behaviors (what should happen when you click a button in the view for example).
The way this is materialized in Windows Store Apps (and WPF) is that ViewModels implement an interface named INotifyPropertyChanged
. The only thing that this interface defines is an event named PropertyChanged
.
The idea is that when a property in the ViewModel changes a PropertyChanged
event is raised with the name of the property that was changed. The view “listens” to this event and this is what allows data binding to happen (if a change is made on a ViewModel’s property the view is notified and can update itself).
For actions performed by the user in the view there’s the ICommand
interface.
A button in a Windows App (Universal Windows Platform or WPF) can be configured to run an instance of an ICommand
stored in the ViewModel. Executing the command updates properties in the ViewModel that in turn leads to the view being updated.
An ICommand
defines two methods CanExecute(object parameter)
that returns a boolean that indicates if the command can be executed or not and Execute(object parameter)
that actually executes the command. It also defines an event CanExecuteChanged
that the view uses to known when to call CanExecute
.
For example in Memory Ace there’s a ViewModel (VM) named MemorizationViewModel
. One property in it is a list of memorized cards named CardsMemorized
. Another property is the CurrentCard
which is the card displayed in the “memorization area”.
An example of a command is TurnCard
which “turns a card” and adds it to CardsMemorized
and also sets CurrentCard
to the card that was just turned:
To use the VM with ElectronCGI you need to send a request from .NET to Node.js when its properties change. This is how that looks like, for CurrentCard
:
memorizationPageViewModel.PropertyChanged += (_, propertyChangedEventArgs) =>
{
switch (propertyChangedEventArgs.PropertyName)
{
case "CurrentCard":
connection.Send("memorization.currentCard", memorizationPageViewModel.CurrentCard);
break;
//...
And for CardsMemorized
which is an ObservableCollection
(which is just a collection that raises events when elements are added/removed from it):
memorizationPageViewModel.CardsSeen.CollectionChanged += (e, args) =>
{
connection.Send("memorization.cardsSeen", memorizationPageViewModel.CardsSeen);
};
In the “other side” of the connection, in this case a React app running inside Electron, you need to handle those events and update the UI. Here is a simplified version of how that looks like using React function components with the useState
hook:
const [currentCard, setCurrentCard] = useState();
const [cardsSeen, setCardsSeen] = useState([]);
connection.on('memorization.currentCard', (err, newCurrentCard) => {
//handle error if it exists
setCurrentCard(newCurrentCard);
});
connection.on('memorization.cardsSeen', (err, cardsSeen) => {
//handle error if it exists
setCardsSeen(cardsSeen);
});
For commands, the action will be triggered from the UI, in this concrete example from a click handler in the React application running in Electron:
async function turnCard() {
try{
await connection.send('memorization.turnCard');
}catch(err) {
//handle error
}
});
And in .NET:
connection.On("memorization.turnCard", () =>
{
if (memorizationPageViewModel.TurnCardCommand.CanExecute(null))
{
memorizationPageViewModel.TurnCardCommand.Execute(null);
}
});
That’s basically all there is to it. There’s other complications of course, namely how to deploy (things gets shuffled around when you use a packager like electron-packager) the app. And also other things related to Electron itself, like for example how to expose the connection to the render process (where the React app is running).
I’m planning to write a blog post detailing all that in the future. In the meanwhile why not follow me on twitter (I’ll tweet when the new post is ready). Also star the project in GitHub, try it out, add issues if you have questions or find something that is broken.