Aplicația web merge bine. E un SaaS construit în Blazor, clienții îl folosesc zilnic din browser și nimeni nu se gândea la desktop. Apoi un client mare trimite o cerere: „Ne trebuie o aplicație instalabilă. Operatorii lucrează cu un cântar conectat pe USB și cu o imprimantă de etichete, iar uneori internetul din depozit cade.”
Echipa se uită la cod. Avem deja zeci de ecrane, validări și componente. Nimeni nu vrea să le rescrie în WPF. Nici nu e nevoie. Unele dintre cele mai folosite aplicații desktop din lume, de la VS Code la Slack, sunt de fapt pagini web împachetate într-o fereastră nativă.
Acesta e al treilea articol din seria despre alegerea tehnologiei de UI. După web și desktop-ul nativ în .NET, acum vorbim despre zona dintre ele: aplicații desktop construite cu tehnologii web. Electron, Tauri, WebView2 și Blazor Hybrid.
Prima întrebare: de ce desktop?
Înainte de framework, merită clarificat ce înseamnă de fapt „ne trebuie desktop”. De obicei, e una dintre aceste nevoi:
- Acces la hardware local — cântare, imprimante de etichete, cititoare de carduri, porturi seriale. Browserul acoperă o parte prin Web Serial sau WebUSB, dar suportul e limitat la browserele Chromium și cere permisiuni explicite.
- Acces la fișiere și sistem — foldere monitorizate, fișiere mari, integrare cu alte aplicații locale.
- Lucru offline real, nu doar o pagină de eroare frumoasă.
- Integrare cu sistemul de operare — iconiță în system tray, pornire automată, notificări native, scurtături globale.
- Distribuție controlată — în multe companii, aplicațiile se instalează centralizat, cu un installer semnat.
Dacă nevoia reală e doar „o iconiță în taskbar și o fereastră fără bara browserului”, o PWA rezolvă problema fără niciun framework nou. Edge și Chrome permit instalarea unei aplicații web pe desktop, cu fereastră proprie, iconiță și chiar funcționare offline prin service workers. Detaliem PWA-urile în articolul despre mobil, dar merită verificată ca primă opțiune și aici.
Modelul comun: shell nativ, WebView și o punte
Toate soluțiile din acest articol au aceeași arhitectură în trei straturi:
- Un shell nativ — procesul care creează fereastra, are acces la sistem și gestionează ciclul de viață al aplicației.
- Un WebView — un motor de browser care afișează interfața, scrisă în HTML, CSS și JavaScript sau Razor.
- O punte (bridge) — canalul prin care interfața cere shell-ului lucruri pe care browserul nu le poate face singur: salvează un fișier, citește cântarul, tipărește o etichetă.
Diferența majoră dintre framework-uri este de unde vine WebView-ul. Unele își aduc propriul browser, cum face Electron cu Chromium. Altele folosesc WebView-ul deja instalat în sistemul de operare: WebView2 pe Windows, WebKit pe macOS și WebKitGTK pe Linux. Prima variantă înseamnă randare identică peste tot, dar aplicații mari. A doua înseamnă aplicații mici, dar cu diferențe subtile de randare între platforme. Aproape toate celelalte compromisuri decurg de aici.
Electron: un browser întreg în fiecare aplicație
Electron împachetează Chromium și Node.js într-o singură aplicație. Interfața rulează în Chromium, iar procesul principal rulează în Node.js, cu acces complet la sistem. Este standardul de facto: VS Code, Slack, Discord și aplicația desktop Figma sunt construite pe Electron.
Avantajele vin din maturitate. Randarea e identică pe Windows, macOS și Linux, pentru că e mereu același Chromium. Uneltele pentru build, semnare digitală și actualizări automate sunt verificate pe milioane de instalări. Iar o echipă care știe JavaScript poate scrie toată aplicația fără să învețe altă limbă.
Prețul e cunoscut și, pentru unele proiecte, prea mare. Un installer Electron are de obicei între 50 și 150 MB, iar o aplicație în repaus consumă de regulă peste 100 MB de RAM. Fiecare aplicație Electron instalată aduce încă un Chromium complet. Iar actualizările de securitate pentru Chromium ajung în aplicația ta doar dacă tu actualizezi Electron. O aplicație rămasă pe o versiune veche rămâne cu vulnerabilitățile acelei versiuni.
Securitatea cere disciplină. Interfața nu trebuie să aibă acces direct la Node.js. Comunicarea se face printr-un script preload, care expune doar funcțiile strict necesare:
// preload.js - singurul loc în care UI-ul atinge sistemul
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('desktop', {
salveazaRaport: (continut) => ipcRenderer.invoke('raport:salveaza', continut)
});
// main.js - procesul principal, cu acces la sistem
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const fs = require('node:fs/promises');
const path = require('node:path');
app.whenReady().then(() => {
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true
}
});
ipcMain.handle('raport:salveaza', async (_event, continut) => {
const { filePath } = await dialog.showSaveDialog(win, { defaultPath: 'raport.csv' });
if (filePath) await fs.writeFile(filePath, continut);
return Boolean(filePath);
});
win.loadFile('index.html');
});
În interfață, apelul e simplu: await window.desktop.salveazaRaport(csv). Interfața nu știe nimic despre sistemul de fișiere, iar procesul principal decide ce e permis.
Tauri: aplicații de câțiva MB, cu backend în Rust
Tauri pornește de la critica principală adusă lui Electron: de ce să livrezi un browser întreg, când sistemul de operare are deja unul? Tauri folosește WebView-ul nativ al fiecărei platforme, iar backend-ul e scris în Rust. Rezultatul: installere de câțiva MB, în loc de sute, și un consum de memorie sensibil mai mic.
Versiunea 2, stabilă din octombrie 2024, a adus două schimbări importante. Prima: suport pentru iOS și Android, deci aceeași aplicație poate ajunge și pe mobil. A doua: un model de securitate bazat pe capabilities. Implicit, interfața nu are acces la nimic din sistem, iar fiecare permisiune trebuie declarată explicit. Ecosistemul de pluginuri a crescut constant și acoperă azi majoritatea nevoilor obișnuite: fișiere, notificări, actualizări automate, dialoguri, shell.
// src-tauri/src/main.rs
#[tauri::command]
fn calculeaza_total(valori: Vec<f64>) -> f64 {
valori.iter().sum()
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![calculeaza_total])
.run(tauri::generate_context!())
.expect("eroare la pornirea aplicației");
}
// în interfață (React, Vue, Svelte sau JavaScript simplu)
import { invoke } from '@tauri-apps/api/core';
const total = await invoke('calculeaza_total', { valori: [120.5, 80, 45.25] });
Limitele sunt reale. Prima e Rust: pentru orice depășește interfața, echipa trebuie să lucreze confortabil într-o limbă cu o curbă de învățare serioasă. A doua e WebView-ul nativ: pe macOS rulezi practic în Safari, iar pe Linux în WebKitGTK, cu propriile diferențe față de Chromium. Ce arată perfect pe Windows trebuie testat separat pe celelalte platforme.
În aceeași familie merită amintit Wails, care face același lucru ca Tauri, dar cu backend în Go. E o alegere bună pentru echipele care lucrează deja în Go.
WebView2: web în aplicația ta .NET existentă
Pentru o echipă .NET, cel mai simplu pas spre „web pe desktop” este deseori controlul WebView2, motorul Chromium din Microsoft Edge, disponibil ca control în WPF, WinForms și WinUI. Runtime-ul e preinstalat pe Windows 11 și se actualizează automat, independent de aplicația ta. Asta rezolvă problema de securitate a lui Electron, deși introduce dependența de o componentă din sistem.
WebView2 e potrivit când vrei să integrezi o aplicație web existentă într-un shell desktop. Aplicația web poate fi încărcată de pe server sau din fișiere locale, iar comunicarea cu codul C# se face prin mesaje. Atenția trebuie să fie la securitate: dacă încarci conținut de pe internet, nu expune acelui conținut obiecte .NET cu acces la sistem. Validează originea fiecărui mesaj primit.
Blazor Hybrid: componente Razor, cu toată puterea .NET
Pentru scenariul din introducere, cu o aplicație Blazor și o cerere de desktop, Blazor Hybrid e de obicei răspunsul cel mai natural. Componentele Razor rulează nativ, în procesul .NET, și sunt randate într-un WebView printr-un canal local. Nu e Blazor WebAssembly și nu e Blazor Server. Nu există WebAssembly și nici conexiune la server. Componenta e cod .NET obișnuit, cu acces complet la fișiere, porturi seriale, imprimante și orice bibliotecă NuGet.
Blazor Hybrid poate fi găzduit în trei locuri: în .NET MAUI, pentru Windows și macOS (și mobil), sau prin controlul BlazorWebView în WPF și WinForms. Varianta din urmă e foarte utilă pentru modernizarea treptată discutată în articolul anterior: poți adăuga ecrane noi, scrise în Blazor, într-o aplicație WPF existentă.
<!-- MainWindow.xaml - proiectul folosește Sdk="Microsoft.NET.Sdk.Razor" -->
<Window x:Class="Depozit.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"
xmlns:ui="clr-namespace:Depozit.Ui;assembly=Depozit.Ui">
<blazor:BlazorWebView HostPage="wwwroot\index.html" Services="{DynamicResource services}">
<blazor:BlazorWebView.RootComponents>
<blazor:RootComponent Selector="#app" ComponentType="{x:Type ui:Receptie}" />
</blazor:BlazorWebView.RootComponents>
</blazor:BlazorWebView>
</Window>
// MainWindow.xaml.cs
public MainWindow()
{
var services = new ServiceCollection();
services.AddWpfBlazorWebView();
services.AddSingleton<ICantarService, CantarSerialService>();
services.AddSingleton<IExportService, ExportDesktopService>();
Resources.Add("services", services.BuildServiceProvider());
InitializeComponent();
}
Aceleași componente pe web și pe desktop
Valoarea reală apare când pui componentele într-un Razor Class Library comun, folosit atât de aplicația web, cât și de cea desktop. Tot ce depinde de platformă trece printr-o interfață, cu câte o implementare pentru fiecare gazdă:
// În biblioteca comună Depozit.Ui
public interface ICantarService
{
bool EsteDisponibil { get; }
Task<decimal> CitesteGreutateaAsync();
}
@* Receptie.razor - aceeași componentă în browser și pe desktop *@
@inject ICantarService Cantar
@if (Cantar.EsteDisponibil)
{
<button @onclick="CitesteAsync">Citește greutatea</button>
}
else
{
<input type="number" @bind="greutate" placeholder="Introdu greutatea manual" />
}
<p>Greutate: @greutate kg</p>
@code {
private decimal greutate;
private async Task CitesteAsync() => greutate = await Cantar.CitesteGreutateaAsync();
}
Pe desktop, CantarSerialService citește direct portul serial cu System.IO.Ports. Pe web, o implementare simplă raportează EsteDisponibil = false, iar operatorul introduce greutatea manual. Componenta e aceeași, iar diferențele țin de platformă, nu de interfață. Este același principiu ca în articolul despre desktop: logica și interfața nu trebuie să știe pe ce platformă rulează.
Limitele Blazor Hybrid țin de gazdă. Prin WPF și WinForms rulezi doar pe Windows. Prin MAUI ajungi pe Windows și macOS, dar nu pe Linux. Pentru Linux există proiecte din comunitate, precum Photino, care oferă un shell ușor pentru Blazor. Iar Electron.NET permite rularea unei aplicații ASP.NET Core în Electron, dacă ai nevoie de randare identică pe toate platformele.
Distribuție și actualizări: partea uitată
O aplicație web se actualizează la fiecare deploy. O aplicație desktop trebuie instalată, semnată și actualizată pe fiecare calculator. Planifică această parte de la început, pentru că o versiune veche rămasă pe 200 de calculatoare e o problemă de suport și de securitate.
- Electron are cele mai mature unelte: electron-builder pentru installere și actualizări automate integrate.
- Tauri oferă un plugin oficial de actualizare și generează installere native pentru fiecare platformă.
- Aplicațiile .NET (WPF, WinForms, Blazor Hybrid) pot folosi MSIX, ClickOnce sau Velopack, o soluție open source modernă pentru installere și actualizări automate.
- Semnarea digitală este obligatorie în practică. Fără ea, Windows SmartScreen și macOS Gatekeeper avertizează sau blochează instalarea, iar utilizatorii își pierd încrederea din primul minut.
Tabelul de decizie
| Situația | Prima alegere | Alternativă serioasă |
|---|---|---|
| Aplicație web care trebuie doar instalabilă | PWA | WebView2 / Tauri |
| Aplicație Blazor existentă + desktop pe Windows | Blazor Hybrid (WPF/WinForms) | Blazor Hybrid în MAUI |
| Aplicație Blazor + Windows și macOS | Blazor Hybrid în MAUI | Electron.NET |
| Ecrane noi într-o aplicație WPF existentă | BlazorWebView în WPF | WebView2 cu aplicație web |
| Echipă JavaScript, randare identică peste tot | Electron | Tauri |
| Aplicație mică, ușoară, cross-platform | Tauri | Wails (pentru echipe Go) |
| Desktop + mobil din aceeași bază web | Tauri 2 | Blazor Hybrid în MAUI |
| Acces intens la hardware, doar Windows | Blazor Hybrid sau WPF nativ | WebView2 |
Greșelile pe care le văd cel mai des
- Desktop fără motiv. Dacă nu ai nevoie de hardware, fișiere, offline sau integrare cu sistemul, o PWA sau aplicația web pur și simplu sunt mai ieftine de întreținut.
- Conținut de pe internet cu acces la sistem. Un WebView care încarcă pagini externe și are o punte deschisă spre sistemul de fișiere e o vulnerabilitate gata făcută.
- Electron rămas în urmă. Fiecare versiune de Electron nelansată în producție înseamnă vulnerabilități Chromium cunoscute, livrate utilizatorilor tăi.
- Testat doar pe Windows. Cu Tauri sau orice WebView nativ, WebKit pe macOS și Linux are alt comportament. Testele pe toate platformele țintă nu sunt opționale.
- Componentele care depind direct de platformă. Un
File.WriteAllBytesîn mijlocul unei componente Razor partajate rupe varianta web. Orice acces la sistem trece printr-o interfață. - Actualizările amânate pentru „mai târziu”. Fără un mecanism de actualizare din prima versiune, vei avea în producție zeci de versiuni diferite.
Recapitulare
- Toate soluțiile au același model: shell nativ, WebView și o punte. Diferența cheie e dacă WebView-ul e livrat cu aplicația sau vine din sistemul de operare.
- Electron oferă randare identică și unelte mature, cu prețul unor aplicații mari și al actualizărilor permanente de securitate.
- Tauri produce aplicații de câțiva MB, cu un model de securitate strict și suport pentru mobil, dar cere Rust și testare pe fiecare WebView.
- WebView2 e cel mai simplu mod de a aduce o aplicație web într-un shell .NET pe Windows.
- Blazor Hybrid rulează componentele Razor nativ, cu acces complet la .NET. Cu un Razor Class Library comun, aceleași componente merg pe web și pe desktop.
- Distribuția și actualizările trebuie planificate din prima zi.
În articolul următor din serie trecem pe ecrane mici: aplicații mobile. Nativ cu Swift și Kotlin, cross-platform cu Flutter, React Native și .NET MAUI, sau PWA. Vorbim și despre lucrurile care lipsesc de obicei din comparații: publicarea în magazine, notificările push și ce faci cu aplicațiile rămase pe Xamarin.