The future you've always dreamed of is finally here: Build your desktop app once, run it anywhere, build your first .NET Core Cross-Platform app using Photino (Open Source Library). The target app will run the same on Linux, Mac, Windows with no changes.
Introduction
This article will guide you through building your first Photino Desktop app (built on top of .NET Core) which will run on all of the Big 3 Platforms (Linux, Mac, Windows).
Why?
The future you've always dreamed of is finally here: Build Your Desktop App Once, Run It Anywhere.
Yes, this future does come with HTML5 (HTML, JavaScript, CSS) but it is fine, my seasoned Desktop Developer Friend. It's fine because now you have the power of the .NET Core Framework behind you.
Build your User Interface one time (using HTML5, JavaScript & CSS) while leveraging all the power of .NET Core to get to the Desktop API functionality (read/write files, Cryptographic APIs, everything that is exposed via .NET Core).
Background
Why Was I Interested In Cross-Platform Apps
I've written a Password Generator (windows store link[^] FOSS (Fully Open Source Software) so you can get the source at my Github link[^].
If you're going to write a Password Generator that people are going to use, it is going to have to run on every known platform so no matter where a user needs her password, it will be available.
The original version is written using ElectronJS (Chrome engine) and runs on all the major platforms also. Now that Photino has arrived, I am going to convert the app to .NET Core and it has an easy path to do so.
Official Photino Project Docs
By the way, Photino is backed by the good people at CODE Magazine and you can see all the documentation at tryphotino.io. Also, as I said, it's all Open Source & you can get all the code at github.
Here's a quick, example of a FileViewer
I'm working on. Remember, the UI is built on HTML5, JavaScript & CSS, but it is able to call local "desktop" APIs via .NET Core -- Directory.GetFiles()
, etc.
But, to see what Photino can do for you, let's write our first program using the library.
Getting Started
Update Note : .NET 5.x versus 6.x
After cloning the code to a fresh system that didn't have .NET Core installed that when I installed .NET Core 6.x the project wouldn't build.
.NET Core 6.x is the new standard so it would be a pain to have to also install an old version.
Instead of doing that, you can simply update the HelloPhotino.NET.csproj*
file to reference .net6.0.
*This name is the default project name that the Photino template gives your project. I should've renamed it. 😖
Just open the .csproj
file in your editor and change the following line:
<TargetFramework>net5.0</TargetFramework>
Just change the 5 to a 6 and then you'll be able to build.
<TargetFramework>net6.0</TargetFramework>
What You Will Need
- .NET Core 5.0 or 6.0 SDK installed & ready to go: Go here to get it from Microsoft.
- Photino Project Templates - makes creating project very simple from command line
- Code Editor: I use Visual Studio Code in this article
I'll start off assuming that you do have .NET Core 5 or 6 installed.
You can determine which version you have with the following command:
$ dotnet --version
Install Photino Project Templates
Open up a command line prompt and run the following:
$ dotnet new -i TryPhotino.VSCode.Project.Templates
That will simply add a list of project templates which will be available to the dotnet new
command
You can run the following command to see the list of all Project Templates (you will see the new ones included in the list):
$ dotnet new -l // that's a lowercase L for list
You'll see a list of all project templates which looks something like the following:
Create Our First Project
Now that we have the Photino project templates installed, we can go to development directory (I name mine dev/dotnet/photino/ to contain all my photino
projects) and then issue the following command.
~/dev/dotnet/photino $ dotnet new photinoapp -o FirstOne
Running that command will:
- create a new directory named FirstOne (
-o
is output) underneath my photino directory - Create a new .NET Core project (including .csproj file) & all the rest of the basic app files.
- Create a wwwroot -- special folder used by Photino to store your User Interface files (HTML, JavaScript, CSS)
Run the Basic App
Once you create the boilerplate project, you can run it immediately.
Just move into the new directory and run:
$ dotnet run // compiles & runs the app
The app will start up & a popup dialog will appear in the middle of it to demonstrate that you can do things via JavaScript.
Click the [Close] button so you can see the main interface.
Click the [Call .NET] button and you'll see the following:
Not Too Amazing...Yet
Nothing too amazing so far. Let's take a look at the files and code which are included in the project so we can get an idea of what is really happening. After that, we'll make a "Desktop API" call via C# which would never work in a Web App, to prove that this application really is quite amazing.
Program.cs: Where Everything Starts
Here's one big snapshot of the project from within Visual Studio Code which shows a lot of detail.
Good Old Main Entry Point
On the top right side inside the Program.cs file, you can see that we have our normal Main()
method that we've come to know and love.
Here Comes the Magic: How It Works
This is an actual C# .NET Program. The magic is in the fact that it auto-loads the WebView2 (Microsoft docs) as the main Form
interface and then loads your target HTML inside that WebView2
control.
If we scrolled a bit further down in the code, you would see that the last call that the Main()
method makes is the following Photino
library call:
.Load("wwwroot/index.html");
Of course, as you can see over on the left, that index.html file is located in the wwwroot folder.
The index.html file looks like the following:
It's all just simple HTML but that file makes up the entire User Interface for this app. That's pretty amazing.
Now You Can Dream
That means you can now take any HTML5 (web-based) app and wrap it inside of Photino and turn it into a desktop app which will run on any Mac, Linux or Windows machine natively.
Extreme Example
As an experiment, I created a template Photino project, took my web-based C'YaPass app (Password Generator), dropped in the HTML (index.html), JavaScript and CSS files and ran the Photino app and got the following with no code changes.
That app uses HTML5 Canvas
, localStorage
and various other HTML technology but runs perfectly on any desktop.
But Why?
That app also generates SHA-256 hash codes (for use as passwords) via a JavaScript function. Now, with Photino, I can remove the JavaScript and use the .NET Core Cryptopgraphy libraries to make everything a bit cleaner. I can do that because I can make calls to the desktop APIs via C# within the Photino
framework.
Let's see how we can make a simple call to a .NET API.
Make a Call to Desktop API via C#
To prove this out, we really do need to make a call to the Desktop
API via C#.
What We Need To Do
To do this work, we will:
- Add a button to fire the functionality -- of course, this button will be created in the index.html
- When the button is clicked, we need to Send A Message to the Photino window (C# side) which will request the associated desktop API be called.
- Send a message back to the User Interface (index.html)
- Display the result of our call in the User Interface (index.html)
Get Source Code
I'll add the completed code at the top of this article so you can try it out easily.
FYI - Removed Code From Template
The code that does that auto-popup is annoying so I removed it.
Step 1: Add A Button
To keep this simple, I am going to add a new button right under the existing one (from the project template):
<button id="callApiButton" onclick="callApi()">Call API</button>
FYI - Yes, I know that many people don't like having the event-handler (onclick
) right on the HTML element, but this is simplified for our example.
After adding it, you can run and see the button exists, but does nothing.
If you're following along to run the app, just go to your project command line and type:
$ dotnet run
Now, let's go make the button do something.
Step 2: Send a Message to the C# Side
I'm going to add a new JavaScript file (api.js) and include it at the top of the index.html file. The api.js file will include the code to handle the callApi()
function.
I'm copying the boilerplate code out of the index.html which is used to send a message to the app when the first button is clicked:
window.external.sendMessage('Hi .NET! 🤖');
That is JavaScript code which is used to interact the Photino library which handles the message sending.
Alter the Message
The message the template project sends is very naive because it is just a string
. In reality, we'll probably want / need to send some kind of structure which contains:
- Command message
- One or more parameters which will be used by the target function on the C# side.
JavaScript Object & JSON
I'm going to create a JavaScript object, then use JSON.stringify
(create perfect JSON) to send the string
across to the C# side which will then deserialize it and get the command out.
Here's the entire code list of api.js:
function callApi(){
let message = {};
message.command = "getUserProfile";
message.parameters = "";
let sMessage = JSON.stringify(message);
console.log(sMessage);
window.external.sendMessage(sMessage);
}
In this case, I'm not using any other parameters but I'm passing them in anyways.
Also, I didn't have to create a separate sMessage
variable but I'm doing that so you can take a look at the actual string
(JSON) that we are passing across.
Now Our Button Will Do Something
If you're following along, don't forget to add the reference to our new api.js at the top of index.html.
After you've got it all set up, run the app ($ dotnet run
) and click the new button.
You will see some logging in your console window (from Photino.net) and you'll see the received message popup in the app.
Act on Received Message
This isn't complete yet though, because we want it to capture the message.Command
and act accordingly (call a specific desktop API).
Parse JSON into Object
To do that work, we need to change the Program.cs to parse out the JSON we sent into an appropriate object. We need to do that work on the C# side of things.
First, Let's Create A Simple DTO (Data Transfer Object)
I've added a new folder named Model (for Domain Model objects) and I've created the new DTO class file named WindowMessage.cs. (You'll see this all in the final code attached to this article.)
Here's the simple code that will now make it extremely easy to use the C# JSON serializer/deserializer in our code.
using System;
class WindowMessage{
public WindowMessage(String command, String parameters)
{
this.Command = command;
this.Parameters = parameters;
this.AllParameters = parameters.Split(',',StringSplitOptions.RemoveEmptyEntries);
}
public String Command{get;set;}
public String[] AllParameters{get;set;}
public String Parameters{get;set;}
}
The incoming parameters will be a comma-delimited string
and then the class will automatically split on it and create an array of String
that are the parameters we may want to use.
Let's go use this code now.
In Program.cs, the main Message Handler (from project template) is a simplified method which looks like the following:
.RegisterWebMessageReceivedHandler((object sender, string message) => {
var window = (PhotinoWindow)sender;
string response = $"Received message: \"{message}\"";
window.SendWebMessage(response);
})
You can see that the incoming message is just a string
.
Of course, in our new code, we are guaranteeing that we send a WindowMessage
object (via JSON).
Because C# makes JSON deserialization so easy, we can add the following code to deserialize into our DTO (WindowMessage
) and handle the Command
value.
I added using
statements at the top of Program.cs:
using System.Text.Json;
using System.Text.Json.Serialization;
Now I can add the following code at the top of the .RegisterWebMessageReceivedHandler()
function call:
WindowMessage wm = JsonSerializer.Deserialize<WindowMessage>(message);
This will parse the incoming message String
into our target DTO.
Switch on WindowMessage.Command
Now, our code in the .RegisterWebMessageRecievedHandler()
looks like:
.RegisterWebMessageReceivedHandler((object sender, string message) => {
var window = (PhotinoWindow)sender;
WindowMessage wm = JsonSerializer.Deserialize<WindowMessage>(message);
switch(wm.Command){
case "getUserProfile":{
window.SendWebMessage($"I got : {wm.Command}");
break;
}
default :{
string response = $"Received message: \"{wm.Parameters}\"";
window.SendWebMessage(response);
break;
}
}
})
We simply deserialize the JSON into our DTO and then switch on the wm.Command
value.
NOTE: I made a change to the original Button
JavaScript so it'll pass a valid WindowMessage
object too, but you can take a look at that code on your own.
Here's what a run looks like when you click the new button.
We can successfully run various C# code now, dependent upon what our Command
in our WindowMessage
is. Seasoned Devs: Isn't it interesting how this all harkens back to the original Windows Message loop (of Windows API programming) and handling messages?
Wrap It Up: Get User Profile Via Environment
Well, this was supposed to be a fast introduction to Photino, so let's add a call to a .NET API and call it a day.
However, wrap this up properly, we also need to show you how to use the value that is returned back to the User Interface side (HTML).
Register Message Receiver on User Interface Side (HTML)
To get the value back, we need to register a Message Receiver on the User Interface side when the app loads.
We'll do two things:
- Add an
onload
function to the HTML which will run an initialization & set up the Message Receiver - Add the
initApi()
method to the api.js.
Here's the code (in api.js)which will be initialized when the app starts (on HTML load).
function initApi(){
window.external.receiveMessage(response => {
response = JSON.parse(response);
switch (response.Command){
case "getUserProfile":{
alert(`user home is: ${response.Parameters}`);
document.querySelector("#output").innerHTML = `${response.Parameters}`;
break;
}
default:{
alert(response.Parameters);
break;
}
}
});
}
This code will get a response (sent from the C# side) after the Desktop
API is called. It will contain the value of the User's Home directory (retrieved via C# with Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
).
Once this code (JavaScript) receives the value, it will display it using an alert()
and write it into the main HTML using document.querySelector("#output").innerHTML
.
Here's the final C# code.
.RegisterWebMessageReceivedHandler((object sender, string message) => {
var window = (PhotinoWindow)sender;
WindowMessage wm = JsonSerializer.Deserialize<WindowMessage>(message);
switch(wm.Command){
case "getUserProfile":{
wm.Parameters = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
window.SendWebMessage(JsonSerializer.Serialize(wm));
break;
}
default :{
wm.Parameters = $"Received message: \"{wm.Parameters}\"";
window.SendWebMessage(JsonSerializer.Serialize(wm));
break;
}
}
})
Here's a snapshot after I click the new button.
Now, you go and try it and make some of your own apps.
Remember: Build & Deploy To Any OS
Remember, you can now take this code & build it and deploy it to any OS and it will run properly. Amazing!
What Did You Think
Is this the new way to build desktop apps? I think it is a pretty cool way to build a User Interface that will run on any platform. I think it's amazing and I will continue to pursue further development.
History
- 26th May, 2022: First publication