app-hibrida-wpf-aspnet-react-webview2

How to create a hybrid WPF + ASP.NET Core + React application

  • 5 min

In this tutorial, we will see how to create a hybrid desktop application that combines WPF, Webview2, ASP.NET Core, and React as the frontend.

In the previous article, we explained how to set up the application’s backend. Today we will see how to consume that backend from a React application.

Create the React project

The first step is to create our web application, which will serve as the user interface (UI). In this case, we will use React to build a simple application.

To create the project, open a terminal in the root of the solution and run the following commands:

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

Configure the React application

Now let’s configure the React application to communicate properly with our backend.

To do this, create a .env file in the ClientReact directory and add the following configuration:

VITE_API_URL=http://localhost:5000  

However, when the app starts in desktop mode, the port will vary (it won’t always be 5000). To manage this, we create a .env.production file.

VITE_API_URL=

With this, when we are in production, the calls to the backend will be made with relative paths, so it will work because the web will be served from the backend with the correct port.

Next, modify the vite.config.js file as follows:

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

export default defineConfig({  
  plugins: [react()],  
  server: {  
    proxy: {  
      "/api": "http://localhost:5000", // Redirects requests to the C# API  
    },  
  },  
});  

This configuration redirects API routes to our WPF application with ASP.NET, avoiding CORS issues during development.

Create the application in React

Next, we will create a simple application in React to check that the connection with the ASP.NET backend works correctly.

Create the main component App.jsx

First, create the App.jsx file with the following content:

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

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

export default App;  

Add the router

To handle the routes in our application, install react-router-dom with the following command:

npm install react-router-dom  

Then, create the router.jsx file and add this code:

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;  

Create the views

Now we will create the views for our application in the src/views folder. Our demo will have the following views

The main page:

export default function Home() {  
  return (  
    <div>  
      <h1>Home</h1>  
      <p>This is the home page.</p>  
    </div>  
  );  
}  

A simple page that shows static information about the application (to demonstrate how to create simple pages)

export default function About() {  
  return (  
    <div>  
      <h1>About</h1>  
      <p>This is the information page about the application.</p>  
    </div>  
  );  
}  

A page that shows how to make API calls using fetch. It will connect to our three endpoints (demo, forecast, and 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 || "Loading..."}</h1>
      <ul>
        {contents}
      </ul>
    </>
  )
}

export default App

A page that shows real-time communication using SignalR:

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

function App() {
  const [data, setData] = useState(null)
  const [time, setTime] = useState("") // New state for the time

  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`) // Hub URL
      .withAutomaticReconnect()
      .configureLogging(signalR.LogLevel.Information)
      .build()

    connection.start()
      .then(() => console.log("Connected to SignalR"))
      .catch(err => console.error("Error connecting to SignalR:", err))

    // Listen for the "ReceiveTime" event from the hub
    connection.on("ReceiveTime", (time) => {
      console.log("Received time:", time)
      setTime(time)
    })

    // Cleanup on component unmount
    return () => {
      connection.stop()
    }
  }, [])

  return (
    <>
      <h1>{data?.message || "Loading..."}</h1>
      <h2>Time: {time || "Not connected"}</h2> {/* Display the time in real-time */}
    </>
  )
}

export default App

Style the application

Finally, let’s add some CSS to our project. You can use any library or framework you want, or do it with CSS. Here is a simple example just for the 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;
}

Run the application

Now we can launch our application, either as a web app or as a desktop application, simply by changing the configuration (as we saw in the previous entry).

hybrid-wpf-webview-aspnet-react

Download the code

All the code for this entry is available for download on GitHub.

github-full

In the next tutorial, we will see how to integrate Vue.js into our hybrid application. Don’t miss it! 🚀