-
Notifications
You must be signed in to change notification settings - Fork 1
architecture
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Avalonia │ │ Blazor Server │ │ Photino │
│ (Desktop │◄──►│ (Web UI & │◄──►│ (WebView │
│ Framework) │ │ Components) │ │ Hosting) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
▲ ▲ ▲
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Native OS │ │ MudBlazor │ │ File System │
│ Integration │ │ (Material UI) │ │ Access │
└─────────────────┘ └──────────────────┘ └─────────────────┘
In .NET 10, blazor.web.js ships in the Microsoft.AspNetCore.App.Internal.Assets NuGet package. Its MSBuild targets register it as a static web asset — but only for OutputType=Exe projects using Microsoft.NET.Sdk.Web. Desktop apps use OutputType=WinExe, so the framework skips them entirely regardless of SDK choice.
CheapAvaloniaBlazor supplies the file itself, in three layers. Each one covers a case the layer above it cannot reach, and the app only needs one of them to succeed.
1. Build and publish (PackageReference consumers). Build/CheapAvaloniaBlazor.targets adds the script as Content targeting wwwroot/_framework/blazor.web.js, sourced from the restored Microsoft.AspNetCore.App.Internal.Assets package. Because it is Content and not a copy into the output folder, it survives dotnet publish and travels to machines with no SDK and no package cache. This is the normal path.
2. NuGet cache extraction (developer machines, ProjectReference). BlazorFrameworkExtractor runs at startup, picks the cached version matching the running runtime, and copies it into wwwroot/_framework/. MSBuild props and targets from a package are not imported through a ProjectReference, so the samples in this repo rely on this layer.
3. Embedded copy (everything else). A copy of the script is baked into CheapAvaloniaBlazor.dll at build time. When layers 1 and 2 both come up empty the extractor writes it out to wwwroot/_framework/, and if the install directory is read-only a middleware serves it straight from the assembly at /_framework/blazor.web.js. The DLL always ships with the app, so this layer cannot be missing.
UseStaticFiles() serves whatever landed on disk; the embedded middleware sits directly behind it and only answers when nothing else did.
The embedded copy is frozen at the version CheapAvaloniaBlazor was packed against, so on layer 3 an app running a newer 10.0.x runtime gets a slightly older bootstrapper. That is fine: blazor.web.js negotiates over a protocol that is stable across a major version. Layers 1 and 2 always match the app's own package graph, and layer 3 only runs when neither could supply anything at all.
Before 3.4.1 only layer 2 existed. A published app on a clean machine has no NuGet cache, so the script 404'd and the window rendered blank white with no error — the app was running and serving its HTML the whole time.
| Component | Technology | Version |
|---|---|---|
| UI Layer | Blazor Server + MudBlazor | 9.5.0 |
| Desktop Framework | Avalonia | 12.0.4 |
| WebView Host | Photino.NET | 4.0.16 |
| Backend | ASP.NET Core | .NET 10.0 |
| Interop | Custom desktop services | Built-in |
| Feature | Windows | Linux | macOS |
|---|---|---|---|
| Blazor UI / MudBlazor | Tested | Untested | Untested |
| File Dialogs | Tested | Untested | Untested |
| Window Management | Tested | Untested | Untested |
| Clipboard | Tested | Untested | Untested |
| Desktop Toasts | Tested | Untested | Untested |
| System Tray | Tested | Varies by DE | Untested |
| Minimize to Tray (hide window) | Tested | Fallback to minimize | Fallback to minimize |
| System Notifications (JS) | Tested | Untested | Untested |
| Settings Persistence | Tested | Untested | Untested |
| App Lifecycle Events | Tested | Untested | Untested |
| Theme Detection | Tested | Untested | Untested |
| Global Hotkeys | Tested | Tested (D-Bus/X11) | Not supported |
| Native Menu Bar | Tested | Not supported | Not supported |
| Multi-Window / Child Windows | Tested | Untested | Untested |
| Modal Dialogs (parent disable) | Tested | Not supported | Not supported |
Minimize to Tray uses Windows
user32.dllP/Invoke to fully hide the window. On Linux/macOS, the window falls back to a regular minimize (taskbar icon stays visible). System Tray behavior on Linux depends on the desktop environment's support for Avalonia'sTrayIconAPI.
| Service | Lifetime | Purpose |
|---|---|---|
IDesktopInteropService |
Scoped | File dialogs, window control, clipboard, system paths |
INotificationService |
Singleton | Desktop toasts + OS system notifications |
ISystemTrayService |
Singleton | Tray icon, context menu, minimize/restore to tray |
ISettingsService |
Singleton | JSON settings persistence (key-value + typed sections) |
IAppLifecycleService |
Singleton | Window lifecycle events (minimize, maximize, focus, close) |
IThemeService |
Singleton | OS dark/light mode detection and runtime change tracking |
IHotkeyService |
Singleton | System-wide global hotkeys (Windows + Linux, IsSupported for detection) |
IMenuBarService |
Singleton | Native Win32 menu bar (Windows only, IsSupported for detection) |
IWindowService |
Singleton | Child windows, modal dialogs, inter-window messaging |
IDiagnosticLoggerFactory |
Singleton | Conditional diagnostic logging |
PhotinoMessageHandler |
Singleton | JavaScript ↔ C# bridge communication |
MyDesktopApp/
├── Program.cs # Application entry point
├── Components/
│ ├── App.razor # HTML document root (loads scripts, CSS, renders Routes)
│ ├── Routes.razor # Blazor router configuration
│ └── MainLayout.razor # Main application layout
├── Pages/
│ ├── Index.razor # Home page
│ └── Files.razor # File management page
├── wwwroot/ # Static web assets
│ └── css/
└── Services/ # Your business logic
└── IMyService.cs