app-hibrida-wpf-aspnet-react-webview2

Cómo crear una aplicación híbrida WPF + ASP.NET Core + React

  • 11 min

En este tutorial, vamos a ver cómo crear una aplicación híbrida desktop que combine WPF, Webview2, ASP.NET y React (¡ahí es nada!).

Hace muchos años que oímos que el futuro del desarrollo es Web, y el Desktop está condenado a desaparecer. En muchos sentidos, es así, no voy a ser yo el que lo niegue.

Pero, aunque muchas veces se ignora, aún siguen haciendo falta aplicaciones de escritorio. Porque sí, aunque ignoremos ese elefante de en medio de la habitación, aún no todo puede ser web.

Mientras esperamos que el mundo Desktop termine de colapsar, o de reinventarse, han surgido un buen puñado de tecnologías para cubrir el hueco. Electron, Tauri, Fluter, Blazor hybrid, Reactive Native, … todo el mundo tiene su solución (otra cosa es que alguna funcione realmente bien o tenga futuro).

Por si no eran suficientes opciones, hoy vamos a ver una de estas soluciones para generar aplicaciones Desktop usando tecnologías “modernas”. En concreto crearemos una aplicación híbrida que combina lo mejor de tres mundos:

  • .NET WPF o MAUI para la interfaz de escritorio. El ejemplo voy a hacerlo con WPF (Windows). Pero sería básicamente igual en MAUI (Android, Mac).
  • Webview2 para renderización del UI (baso en Chromium)
  • ASP.NET Core como backend (con opción embebido local o stand-alone)
  • React para el UI. Pero cualquier framework o tecnología Web os va a funcionar igualmente.

¿Por qué elegir esta combinación frente a otras tantas? Pues principalmente porque usa tecnologías nativas de .NET (WPF/MAUI, Asp.NET) junto a Webview2 como control nativo.

Es decir, tecnologías probadas, mantenidas, bien conocidas, y con un alto rendimiento. Sin dependencias, sin librerías. Y la experiencia de desarrollo es muy cómoda.

Vamos, que tan mal stack tecnológico no será, ya que es muy similar a la que está usando el propio Microsoft en sus aplicaciones.

Y como no hay un tutorial en el que te expliquen como montar este tinglado, pues vamos a verlo paso a paso 👇

Requisitos Previos

Antes de comenzar, supongo que tienes lo necesario.

  1. Visual Studio 2022 (o superior) con las cargas de trabajo de .NET y desarrollo de escritorio.
  2. Node.js y npm para manejar las dependencias de React.
  3. WebView2 Runtime instalado en tu sistema.
  4. Conocimientos básicos de C#, WPF, ASP.NET, y React.

Si te falta alguno de ellos, date un paseo por la web, que tenemos todos explicados 😉.

Estructura del proyecto

Nuestra aplicación híbrida tendrá la siguiente estructura:

  • AppWPF: Un proyecto WPF que integrará la aplicación web usando WebView2.
  • ServerAspnet: Un proyecto ASP.NET Core que actuará como servidor API embabido.
  • ClientReact: Una aplicación React que se ejecutará en el navegador o dentro de WebView2.

Y está una estructura de los ficheros, para que no nos perdamos.

HybridApp/

├── AppWPF/                     # Proyecto WPF (Frontend Desktop)
   ├── MainWindow.xaml          # Interfaz de usuario de WPF
   └── MainWindow.xaml.cs       # Lógica de la ventana principal

├── ServerAspnet/                # Proyecto ASP.NET Core (Backend)
   ├── Controllers/             # Controladores de ASP.NET Core
   ├── wwwroot/                 # Archivos estáticos (React build)
   ├── appsettings.json         # Configuración de la aplicación
   └── ServerAspnet.csproj      # Archivo de proyecto de ASP.NET Core

└── ClientReact/                 # Proyecto React (Frontend Web)
    ├── public/                  # Archivos estáticos (HTML, imágenes, etc.)
    ├── src/                     # Código fuente de React
   ├── App.js               # Componente principal de React
   └── index.js             # Punto de entrada de React    
    └── package.json             # Dependencias de Node.js

Solo he puesto los ficheros principales, para entender las relaciones. Haber, habrá muchos más ficheros, lógicamente

Crear proyecto en ASP.NET

Vamos a empezar creando el proyecto de ASP.NET, que es la parte más fácil. Para ello, creamos una carpeta para nuestro proyecto, por ejemplo, MiAppHibrida

Dentro de esta carpeta ejecutamos,

dotnet new webapi -n ServerAspnet

Cuando haya creado el proyecto, abrimos el fichero Program.cs y lo cambiamos por este código de ejemplo que os paso.

using Microsoft.AspNetCore.Mvc.ApplicationParts;

namespace ServerAspnet
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            builder.Services.AddCors(options =>
            {
                options.AddPolicy(
                    "AllowAll",
                    policy =>
                    {
                        policy
#if DEBUG
						   // Resolver del CORS sólo en Debug
                        .WithOrigins("http://localhost:5173")
#endif
                        .AllowAnyMethod().AllowAnyHeader();
                    }
                );
            });

// Esto hace falta para que descubra los controladores al invocarla desde WPF
builder.Services.AddControllers().PartManager.ApplicationParts.Add(new AssemblyPart(typeof(Program).Assembly));
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            var app = builder.Build();

            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }


            app.UseCors("AllowAll");
            app.UseHttpsRedirection();
            app.MapControllers(); // configurar Controllers

			 app.UseDefaultFiles(); // Sirve index.html por defecto
            app.UseStaticFiles();  // Sirve archivos de wwwroot
            app.MapFallbackToFile("index.html"); // Para rutas de React

			 // Demo de endpoint "sin controller"
            app.MapGet("/api/demo", () => new { message = "¡Funciona! 🎉" });

		     // Este parámetro lo pasaremos al ejecutarlo desde WPF
		     // porque en ese caso tendrá que ser RunAsync, en lugar de Run
            if(args.Any(x=> x=="run_async")) app.RunAsync();
            else app.Run();

        }
    }
}

Básicamente lo que hemos hecho es una API de ASP.NET, que ejecutaremos de forma embebida en nuestra App de WPF, o bien de forma independiente.

Por supuesto podéis ajustar vuestra configuración, esto es un ejemplo para ver cómo ejecutarla en modo híbrido.

Crear proyecto WPF

Ahora toca crear el esqueleto de nuestra aplicación de escritorio, creando la aplicación de WPF. Para ello abrimos una terminar y ejecutamos

dotnet new wpf -n HybridAppWPF
cd HybridAppWPF

A continuación, tenemos que instalar los paquetes y dependencias necesarios. Para ello hacemos lo siguiente,

dotnet add package Microsoft.AspNetCore.App
dotnet add package Microsoft.Web.WebView2

Si lo hacéis así os funcionará, pero os dará un warning NETSDK1080 de que

PackageReference a Microsoft.AspNetCore.App no es necesario cuando el destino es .NET Core 3.0 o una versión posterior. Si se usa Microsoft.NET.Sdk.Web, se hará referencia automáticamente al marco compartido. De lo contrario, PackageReference debe reemplazarse por frameworkReference.

AspNETCore.App no le gusta que lo añadamos así. Esto es debido a que es un Paquete pensado para usar en proyectos de Microsoft.NET.Sdk.Web y nosotros estamos usando un proyecto de WPF, y por lo tanto es Microsoft.NET.Sdk

Podemos arreglarlo mejor si lo hacemos así en el fichero del proyecto

Añadir el Webview2

Para que nuestra aplicación de WPF muestre nuestra App web en React (o en cualquier otra cosa que queramos hacerla), debemos modificar los siguientes ficheros

<Window x:Class="HybridAppWPF.MainWindow"
	   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	   xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
	   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
	   xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
	   mc:Ignorable="d"
	   Title="Hybrid App" Height="600" Width="800">
   <Grid>
	   <wv2:WebView2 Name="webView" />
   </Grid>
</Window>
using System.Windows;

namespace HybridAppWPF;

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    private readonly WebServer _server = new WebServer();

    public MainWindow()
    {
        InitializeComponent();
        Loaded += MainWindow_Loaded;
        Closed += (sender, e) => _server.Stop();
    }

    private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        _server.Start();
        await webView.EnsureCoreWebView2Async();

#if DEBUG
        webView.CoreWebView2.Navigate("http://localhost:5173"); // React dev server
#else
        webView.CoreWebView2.Navigate("http://localhost:5000");
#endif
    }
}

Es decir,

  • Hemos añadido el control Webview2 en el XAML
  • Hemos configurado la lógica para que, en modo DEBUG, muestre la página de React (que lanzaremos en paralelo con NPM para desarrollo.
  • En modo RELEASE alojaremos y serviremos la web final desde el WPF.

Embeber el proyecto ASP.NET

Ahora, para crear el servidor de AP.NET Core dentro de WPF, en primer lugar añadimos como dependencia el proyecto que hemos creado anteriormente.

aspnet-webview2-wpf-hibrid

Por otro lado, vamos a añadir un fichero llamado WebServer.cs que actúe de intermediario entre WPF y ASP.NET.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System.Windows;

namespace HybridAppWPF
{
	public class WebServer
    {
        private WebApplication? _app;

        public void Start()
        {
            try
            {
                ServerAspnet.Program.Main(new string[] { "run_async" });
            }
            catch (Exception ex)
            {
                MessageBox.Show($"Error en el servidor: {ex.Message}");
            }
        }
        
        public void Stop() => _app?.StopAsync();
    }
}

Lo que estamos haciendo es crear un adaptador en el proyecto de WPF, que lance y apague (por si queréis) el proyecto de ASP.NET. Al lanzarlo, este quedará “embebido” dentro del WPF.

Nuevamente, podéis añadir más lógica aquí. Este es el ejemplo mínimo para que os funcione

Crear proyecto React

Vamos viendo la luz al final del tunel. Ahora toca crear nuestra aplicación Web, que usaremos como UI. En nuestro caso, una app sencilla con React.

Para crear nuestro proyecto, abrimos otra terminal en la raiz de la solución, y hacemos lo siguiente,

npm create vite@latest ClientReact -- --template react
cd ClientReact
npm install

Ahora, vamos a configurar la App en React para que funcione con nuestro Backend. Para ello, primero creamos un archivos llamado .env en ClientReact con:

VITE_API_URL=http://localhost:5000

Además, modificamos el fichero vite.config.js así.

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
    plugins: [react()],
    server: {
        proxy: {
            "/api": "http://localhost:5000", // Redirige las peticiones a la API de C#
        },
    },
});

Básicamente hemos configurado las rutas para que funcionen con nuestra App WPF y su ASP.NET embebido, y no nos de problemas de CORS durante el desarrollo.

Crear nuestra App de React

Ahora vamos a crear una App sencilla en React, para comprobar que se comunica correctamente con el backend de ASP.NET embebido.

import { useEffect, useState } from 'react'

function App() {
 const [data, setData] = useState(null)

 useEffect(() => {
   fetch(`${import.meta.env.VITE_API_URL}/api/demo`)
	 .then(res => res.json())
	 .then(data => setData(data))
 }, [])

 return <h1>{data?.message || "Cargando..."}</h1>
}

export default App

Simplemente hacemos una llamada al endpoint /api/demo para comprobar que todo funciona correctamente.

Ejecutar la aplicación en desarrollo

Cuando estemos desarrollando con nuestra aplicación, simplemente tenemos que ejecutar la app de WPF en modo Debug.

Veremos un error diciendo que no hay web react-webview-aspnet-core-hibrid-wpf

Ahora, por otro lado, nos vamos a otro terminal, abrimos el proyecto de React, y ejecutamos

cd ClientReact
npm run dev

Pulsamos F5 en nuestra App de WPF y ¡tadá!… Vemos el mensaje “¡Funciona! 🎉” proveniente del servidor ASP.NET

hibrid-react-webview-aspnet-core-wpf

No es una gran App, pero sí que lo es

Ahora podemos desarrollar nuestra App de ASP.NET y de React como haríamos con cualquier App “normal”, con Hot Reload, depuración, consola, devtools, y toa la pesca.

Si queremos podemos mejorar el proces creando un Script sencillo en PowerShell en la raiz de la solución (fuera de ambos proyectos), por ejemplo llamado dev.ps1

# Iniciar servidor ASP.NET Core
Start-Process dotnet -ArgumentList "run --configuration Debug --project AppWPF" -WindowStyle Hidden

# Iniciar Vite
npm --prefix ClientReact run dev

Así únicamente tenemos que hacer

.dev

Para iniciar todo en modo desarrollo de forma simultánea.

Lanzar como una web

¿Te va gustando? Pues hay más. La ventaja de tener nuestro proyecto ASP.NET separado es que podemos lanzar fácilmente todo como una web.

Simplemente cambiamos la configuración del proyecto WPF al proyecto ASP.NET, y arrancará todo como una web “normal”.

hibrid-webview2-aspnet-react-2

Hasta podemos usar Swagger durante el desarrollo.

hibrid-webview2-aspnet-react-swagger

Incluso, llegado el caso, podemos hacer un doble build, por un lado de nuestra app desktop, y por otro lado de nuestra app web en un contenedor Docker (como haríamos con cualquier proyecto ASP.NET, porque… bueno, porque es lo que es 😉).

Configuración de Build automatizada

Finalmente, como hacemos un deploy de esto, para que nuestra App de WPF hibrida sea autocontenida. Para ello, en el fichero de proyecto de WPF AppWPF.csproj vamos a añadir lo siguiente

 <Target Name="BuildReactApp" BeforeTargets="BeforeBuild" Condition="'$(Configuration)' == 'Release'">
    <Message Importance="high" Text="Construyendo React App..." />
    <Exec Command="npm install" WorkingDirectory="../ClientReact" />
    <Exec Command="npm run build" WorkingDirectory="../ClientReact" />
    <ItemGroup>
      <ReactBuildFiles Include="../ClientReact/dist/**" />
    </ItemGroup>
    <Copy SourceFiles="@(ReactBuildFiles)" 
          DestinationFiles="@(ReactBuildFiles->'wwwroot\%(RecursiveDir)%(Filename)%(Extension)')" />

    <ItemGroup>
     <Content Include="wwwroot\**">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
   </ItemGroup>
  </Target>

Con esto

  • Hemos hecho que al hacer un build en Realease, se lance el npm run build de la app de React.
  • Los ficheros de la app ya “buildeada” se copiarán a wwwroot de nuestra app.

Ahora, simplemente hacemos

dotnet build
dotnet run

O si la prefieres selfcontained

dotnet publish -c Release -o publish -r win-x64 --self-contained true

E igualmente podemos hacer un un PowerShell en la raiz de la solución, que haga las acciones por nosotros

# Publicar
dotnet publish HybridAppWPF -c Release -o HybridAppWPF/publish

Así que publicar es tan sencillo como hacer

./publish.ps

Bonus

Bonus pack final, requete final. También podemos hacer una comunicación bidireccional entre la app Web embebida en el Webview2, y la app Host en WPF / MAUI. Para ello haríamos

// WPF a JavaScript
webView.CoreWebView2.PostWebMessageAsString("Hola desde WPF");

// JavaScript a WPF
webView.CoreWebView2.WebMessageReceived += (s, e) => 
    MessageBox.Show(e.WebMessageAsJson);

Pero lo veremos en otro tutorial, que este ya ha quedado larguísimo 😅.

Descarga el código

Todo el código de esta entrada está disponible para su descarga en Github. El código que he subido es un pelín (tranquilos, un pelin pequeño) más complicado, porque he añadido un controller para que lo tengáis de ejemplo.

hibrid-webview2-aspnet-react

Por lo demás, es lo que hemos visto aquí. Os dejo el enlace.

github-full