Click here to Skip to main content
15,922,145 members
Articles / Web Development / HTML5

Photino: Open Source for Building Cross-Platform Desktop Apps via .NET Core

Rate me:
Please Sign up or sign in to vote.
5.00/5 (14 votes)
30 May 2022CPOL12 min read 21.7K   257   23   14
Fully Open Source Library for building Cross-Platform Desktop apps on .NET Core
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.


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).


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).


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 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.

Image 1

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:


Just change the 5 to a 6 and then you'll be able to build.


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:

Image 2

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:

  1. create a new directory named FirstOne (-o is output) underneath my photino directory
  2. Create a new .NET Core project (including .csproj file) & all the rest of the basic app files.
  3. 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.

Image 3

Click the [Close] button so you can see the main interface.

Image 4

Click the [Call .NET] button and you'll see the following:

Image 5

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.

Image 6

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:


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:

Image 7

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.

Image 8

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:

  1. Add a button to fire the functionality -- of course, this button will be created in the index.html
  2. 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.
  3. Send a message back to the User Interface (index.html)
  4. 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

Image 9

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:

  1. Command message
  2. 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 = {}; // create basic object
    message.command = "getUserProfile";
    message.parameters = "";
    let sMessage = JSON.stringify(message);

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 and you'll see the received message popup in the app.

Image 10

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;

                    // The message argument is coming in from sendMessage.
                    // "window.external.sendMessage(message: string)"
                    string response = $"Received message: \"{message}\"";

                    // Send a message back the to JavaScript event handler.
                    // "window.external.receiveMessage(callback: Function)"

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);

                        case "getUserProfile":{
                            window.SendWebMessage($"I got : {wm.Command}");
                        default :{
                            // The message argument is coming in from sendMessage.
                            // "window.external.sendMessage(message: string)"
                            string response = $"Received message: \"{wm.Parameters}\"";

                            // Send a message back the to JavaScript event handler.
                            // "window.external.receiveMessage(callback: Function)"

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.

Image 11

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:

  1. Add an onload function to the HTML which will run an initialization & set up the Message Receiver
  2. 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}`;

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);

     case "getUserProfile":{
     wm.Parameters = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
      default :{
        // The message argument is coming in from sendMessage.
        // "window.external.sendMessage(message: string)"
        wm.Parameters = $"Received message: \"{wm.Parameters}\"";

        // Send a message back the to JavaScript event handler.
        // "window.external.receiveMessage(callback: Function)"

Here's a snapshot after I click the new button.

Image 12

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.


  • 26th May, 2022: First publication


This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Written By
Software Developer (Senior) RADDev Publishing
United States United States
"Everything should be made as simple as possible, but not simpler."

Comments and Discussions

Questionwindow.external deprecated? Pin
Member 1370217816-Feb-23 22:20
Member 1370217816-Feb-23 22:20 
AnswerRe: window.external deprecated? Pin
raddevus17-Feb-23 2:18
mvaraddevus17-Feb-23 2:18 
AnswerRe: window.external deprecated? Pin
raddevus17-Feb-23 2:43
mvaraddevus17-Feb-23 2:43 
AnswerRe: window.external deprecated? Pin
raddevus17-Feb-23 3:35
mvaraddevus17-Feb-23 3:35 
GeneralRe: window.external deprecated? Pin
Member 1370217817-Feb-23 23:02
Member 1370217817-Feb-23 23:02 
GeneralMy vote of 5 Pin
Igor Ladnik7-Jun-22 1:38
professionalIgor Ladnik7-Jun-22 1:38 
GeneralRe: My vote of 5 Pin
raddevus7-Jun-22 4:14
mvaraddevus7-Jun-22 4:14 
GeneralMy vote of 5 Pin
luca_covolo31-May-22 1:58
luca_covolo31-May-22 1:58 
GeneralRe: My vote of 5 Pin
raddevus31-May-22 2:36
mvaraddevus31-May-22 2:36 
QuestionLooks interesting Pin
MarcinSzn30-May-22 23:58
MarcinSzn30-May-22 23:58 
Suggestionwindow.external May Clashes With User Definition Pin
Hanz Haxor28-May-22 11:27
Hanz Haxor28-May-22 11:27 
GeneralRe: window.external May Clashes With User Definition Pin
raddevus29-May-22 6:00
mvaraddevus29-May-22 6:00 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA27-May-22 1:42
professionalȘtefan-Mihai MOGA27-May-22 1:42 
GeneralRe: My vote of 5 Pin
raddevus27-May-22 2:11
mvaraddevus27-May-22 2:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.