This is a library more than a framework that allows you to create native C# WPF - Winforms applications that host a web-app with interoperability using IPC between C# and JS.
Having one more tool in your pockets is always better than having one. The aim of this library is to allow you to embed any web-app of your choice whether that's native JS, React, Vue, Angular etc.
- Why not?
- WebView2 ! Low RAM consumption compared to a whole chromium instance.
- You get to write your native code in C# instead of JS :)
- You have full control over your WPF / WinForms project.
Yes and no. Technically yes it does, however whilst the C# -> JS communication is simple, the JS -> C# communication not so much. How WebView2 achieves JS -> C# communication is with two ways:
- One way - Using the
WebMessageReceived
event - Two ways - Using Host Objects
Addressing "2"
first, adding Host Objects
have limitations around passing parameters. Even having a
public Task<CustomClass> Calculate(AnotherClass A, List<AnotherClass> B)
will not work easily. I'm not even sure if it's somehow possible. The "1"st point is interesting, however it's a global channel. On the javascript side you just add an event listener for "message" and everything comes there! Also there's no way to easily "return" a value, since it's one-way...
The goal is to fix "2" so that it provides better DX for your project. Ideally you should just be able to register objects from the C# side and call them from JS. How does the Phosphorus.NET API look ? Simple!
- Create the wrapper classes that you wanna "expose" to IPC. This can be anything, REST Calls, Database Calls, reading
from the FS, displaying notifications etc...
Use the
IpcExpose
attribute on the class to mark all public methods to be exposed, or you can also use it on individual methods.
[IpcExpose]
public class Calculator
{
public int Multiply(int a, int b)
{
return a * b;
}
public async Task<int> MultiplyAsync(int a, int b)
{
await Task.Delay(2000);
return a * b;
}
}
- Create a class that extends
IpcContainer
and callRegister(string, object)
to attach your C# instances. The first parameter is important it's the "instance name", you will need to remember it to access the object from JS-land.
class Container : IpcContainer
{
private Calculator _calculator = new();
public Container()
{
Register("calculator", _calculator);
}
}
- After initializing CoreWebView2, attach your Container using the extension method
RegisterIpcHandler(IpcContainer)
await webView.EnsureCoreWebView2Async();
var container = new Container();
webView.RegisterIpcHandler(container);
- Install the
@ultrawelfare/phosphorus.net-ipc
npm package on your webapp and in the initial entry-point of your app (main.jsx for React, for others it depends on your framework) import it
import "@ultrawelfare/phosphorus.net-ipc";
That's it! You've attached your container to the webview, which in turn attaches the objects to JS-land and you
installed the npm package to allow you to communicate!
The script attaches a global ipc
object to window which you can call like this:
// General syntax:
// ipc.{instanceName}.{methodName}({args});
// or for a better example using the calculator:
const result = await ipc.calculator.Multiply(2, 5);
console.log(result); // 10
Note: you will always need to await
, even if the C# method is not a Task.
Minimum .NET 6.0 version is required.
Only windows supported at the time, although I'd like to explore the possibility to make MAUI work with this.
- NOT PRODUCTION READY - There's no guarantee about the stability of the library (although it works on my machine ¯\(ツ)/¯).
- Uses reflection, so it's not the fastest thing in the world.
- Due to how the IPC works (using JSON) you're not "permitted" to pass everything as arguments or return everything. Unfortunately it will have to stay within the bounds of the JSON Serialization rules, so it is not allowed to pass functions (Func, Action) with the IPC.
So far what is allowed is:
- Double, Float, Int (as number)
- String
- Boolean
- Null
- Classes
- Lists
- Objects
- There's currently no support to invoke Generic methods.
You can either take a look at the manual-installation.md or use the Wpf template to quickly get started:
Use the dotnet CLI to create a new project using the template:
dotnet new install PhosphorusNET.Templates
(if you're running .NET6 you'll need to run this instead: "dotnet new --install PhosphorusNET.Templates")
dotnet new phosphorus.net-wpf -n YourAppName -o YourAppName
The template comes without a web-app pre-installed. Feel free to copy your existing web-app or create a new one for example using vite:
mkdir wwwroot
cd wwwroot
npm create vite@latest .
Then follow the prompts.
After your web-app is created and you've already done an npm install
to install all the packages, you have to install the PhosphorusNET IPC javascript library:
npm install @ultrawelfare/phosphorus.net-ipc
Next you'll have to import the package in your web-app entry-point. Depending on which framework you use it will be different, for example for React projects it's either the main.jsx or main.tsx file:
import '@ultrawelfare/phosphorus.net-ipc';
That's it!
The Wpf template assumes that when your configuration is set to "Debug" you will have the vite server running at localhost:5173
(using npm run dev
).
You can change this behaviour in MainWindow.xaml.cs
inside the CoreWebView2InitializationCompleted
event.
Likewise for either making a Release
build or publishing a Release
build, it assumes that you've built your vite project (using npm run build
)
and the output files are at wwwroot/dist
which will be copied to wwwroot/
on the published/built folder.
To change this, you can edit the C# .csproj
file and change the <WwwrootFiles Include="wwwroot\dist\**\*.*" />
to wherever your built web-app will be.
Refer to the manual-installation.md for a step-by-step guide on how to install the library to your project. Note that this is for more advanced users.
As with all things in life, be careful with what you expose to the web. The library does not provide any security mechanisms, so it's up to you to ensure that you're not exposing anything sensitive.
At any moment, your web application can be injected with a malicious script (especially when the application is communicating with the Web) that can call your exposed methods and potentially damage your application or the user's computer.
You take full responsibility for the security of your application.
A small synopsis:
- The library uses a custom
IpcExpose
attribute to mark classes that you want to expose to the IPC. - The
IpcContainer
class is used to register the instances of the classes that you want to expose. - The
IpcContainer
class is then attached to theCoreWebView2
instance using theRegisterIpcHandler
extension method. - The
@ultrawelfare/phosphorus.net-ipc
npm package is used to communicate with the C# instances from the web app.
The whole idea is passing JSON back'n'forth.
At the root of ipc
in JS-Land, it's just a proxy which gets the instance name, method name and the arguments.
Using window.chrome.webview.postMessage
it sends the data to the C# side. This message is a JSON object which looks
like:
{
"uuid": "3a97d12a-6b76-4ce5-8e3e-37cfe9629a8e",
"instance": "calculator",
"method": "Multiply",
"args": [
2,
5
]
}
The uuid
is used to track the response while going between C# and JS. The C# side will respond with the same uuid
so
that the JS-land can match the response.
Once the C# side receives a message from JS (that's what RegisterIpcHandler
does), it will look inside the user's
Container for the instance name and try to deserialize the arguments according to the method (using Reflection)
The response is then sent back to JS-land using the same uuid
so that the JS-land can match the response.
At the JS-land once the user tries to "invoke" an ipc method and the message is sent it returns a Promise that will
resolve once the response is received (hence why you need to await
even sync functions).