app-hibrida-wpf-aspnet-react-webview2

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

  • 5 min

En este tutorial vamos a ver cómo crear una aplicación híbrida de escritorio que combina WPF, Webview2, ASP.NET Core y React como frontend.

En el artículo anterior, explicamos cómo montar el backend de la aplicación. Hoy veremos cómo consumir ese backend desde una aplicación en React.

Crear el proyecto de React

El primer paso es crear nuestra aplicación web, que servirá como la interfaz de usuario (UI). En este caso, usaremos React para construir una aplicación sencilla.

Para crear el proyecto, abre una terminal en la raíz de la solución y ejecuta los siguientes comandos:

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

Configurar la aplicación React

Ahora vamos a configurar la aplicación React para que se comunique correctamente con nuestro backend.

Para ello, crea un archivo .env en el directorio ClientReact y añade la siguiente configuración:

VITE_API_URL=http://localhost:5000

Sin embargo, cuando la app arranque en modo desktop, el puerto va a variar (no siempre va a ser el 5000). Para gestionar esto, creamos un fichero .env.production.

VITE_API_URL=

Con esto, cuando estemos en producción las llamadas al backend se harán con rutas relativas, por lo que funcionará porque la web será servida desde el backend con el puerto correcto.

A continuación, modifica el archivo vite.config.js de la siguiente manera:

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#
    },
  },
});

Esta configuración redirige las rutas de la API hacia nuestra aplicación WPF con ASP.NET, evitando problemas de CORS durante el desarrollo.

Crear la aplicación en React

A continuación, vamos a crear una aplicación sencilla en React para comprobar que la conexión con el backend de ASP.NET funciona correctamente. Primero, crea el archivo App.jsx con el siguiente contenido:

import { Link } from 'react-router-dom';

function App() {
  return (
    <div>
      <nav>
        <Link to="/">Inicio</Link>
        <Link to="/about">Acerca de</Link>
        <Link to="/fetch">Fetch</Link>
        <Link to="/signalr">SignalR</Link>
      </nav>
    </div>
  );
}

export default App;

Añadir el router

Para manejar las rutas en nuestra aplicación, instala react-router-dom con el siguiente comando:

npm install react-router-dom

Luego, crea el archivo router.jsx y añade este código:

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './views/Home';
import About from './views/About';
import Fetch from './views/Fetch';
import SignalR from './views/SignalR';
import App from './App';

function AppRouter() {
  return (
    <Router>
      <App />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/fetch" element={<Fetch />} />
        <Route path="/signalr" element={<SignalR />} />
      </Routes>
    </Router>
  );
}

export default AppRouter;

Crear las vistas

Ahora crearemos las vistas de nuestra aplicación en la carpeta src/views. Nuestra Demo va a tener las siguientes vistas

La página principal:

export default function Home() {
  return (
    <div>
      <h1>Inicio</h1>
      <p>Esta es la página de inicio.</p>
    </div>
  );
}

Una página sencilla que muestra información estática sobre la aplicación (para demostrar cómo crear páginas sencillas)

export default function About() {
  return (
    <div>
      <h1>Acerca de</h1>
      <p>Esta es la página de información sobre la aplicación.</p>
    </div>
  );
}

Una página que muestra cómo realizar llamadas a la API mediante fetch. Se conectará a nuestros tres endpoints (demo, forecast y files):

import { useEffect, useState } from 'react'

function App() {
  const [data, setData] = useState(null)
  const [forecasts, setForecasts] = useState()
  const [files, setFiles] = useState()


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

  useEffect(() => {
    populateWeatherData()
    getFiles()
  }, [])

  async function populateWeatherData() {
    const response = await fetch(`${import.meta.env.VITE_API_URL}/weatherforecast`)
    if (response.ok) {
      const data = await response.json()
      setForecasts(data)
    }
  }

  async function getFiles() {
    const response = await fetch(`${import.meta.env.VITE_API_URL}/files`)
    if (response.ok) {
      const data = await response.json()
      setFiles(data)
    }
  }

  const contents = forecasts === undefined || files === undefined
  ? <p><em>Loading... Please refresh once the ASP.NET backend has started. See <a href="https://aka.ms/jspsintegrationreact">https://aka.ms/jspsintegrationreact</a> for more details.</em></p>
  : <>
    <ul>
      {files.map(file => <li key={file}>{file}</li>)}
    </ul>
  </>

  return (
    <>
      <h1>{data?.message || "Cargando..."}</h1>
      <ul>
        {contents}
      </ul>
    </>
  )
}

export default App

Una página que muestra la comunicación en tiempo real mediante SignalR:

import { useEffect, useState } from 'react'
import * as signalR from '@microsoft/signalr'

function App() {
  const [data, setData] = useState(null)
  const [time, setTime] = useState("") // Nuevo estado para la hora

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

  useEffect(() => {
    const connection = new signalR.HubConnectionBuilder()
      .withUrl(`${import.meta.env.VITE_API_URL}/clockHub`) // Ruta del Hub
      .withAutomaticReconnect()
      .configureLogging(signalR.LogLevel.Information)
      .build()

    connection.start()
      .then(() => console.log("Conectado a SignalR"))
      .catch(err => console.error("Error al conectar a SignalR:", err))

    // Escuchar el evento "ReceiveTime" del hub
    connection.on("ReceiveTime", (time) => {
      console.log("Hora recibida:", time)
      setTime(time)
    })

    // Limpieza al desmontar el componente
    return () => {
      connection.stop()
    }
  }, [])

  return (
    <>
      <h1>{data?.message || "Cargando..."}</h1>
      <h2>Hora: {time || "Sin conexión"}</h2> {/* Muestra la hora en tiempo real */}
    </>
  )
}

export default App

Estilizar la aplicación

Finalmente, añadimos un poco de CSS a nuestro proyecto. Podéis usar cualquier librería o framework que queráis, o hacerlo con CSS. Aquí tenéis un ejemplo sencillo solo para la demo.

body {
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f4f4f9;
  color: #333;
}

nav {
  background-color: #1f1d21;
  padding: 1rem;
  display: flex;
  gap: 1rem;
}

nav a {
  color: white;
  text-decoration: none;
  font-weight: bold;
}

nav a:hover {
  text-decoration: underline;
}

.container {
  padding: 2rem;
}

h1 {
  color: #1f1d21;
}

Ejecutar la aplicación

Ahora podemos lanzar nuestra aplicación, bien sea como web, o como aplicación de Desktop, simplemente cambiando la configuración (como vimos en la entrada anterior).

hybrid-wpf-webview-aspnet-react

Descarga el código

Todo el código de esta entrada está disponible para su descarga en Github.

github-full

En el próximo tutorial, veremos cómo integrar Vue.js en nuestra aplicación híbrida. ¡No te lo pierdas! 🚀