在 WPF 中整合 ASP.NET Core 和 WebView2 用於整合 SPA 應用

Aoba_xu發表於2024-05-11

背景

我們有些工具在 Web 版中已經有了很好的實踐,而在 WPF 中重新開發也是一種費時費力的操作,那麼直接整合則是最省事省力的方法了。

思路解釋

  1. 為什麼要使用 WPF?莫問為什麼,老 C# 開發的堅持,另外因為 Windows 上已經裝了 Webview2/edge 整體打包比 electron 小很多,release 後的體積主要是 ASP.NET Core 的檔案。
  2. 為什麼要使用 ASP.NET Core 進行代理呢?很簡單,因為很多操作要求使用 HTTP Context,在類似 file:/// 的連結下是不能使用的,如果做成聯網的有些資源進行跨域請求也是不能的。舉個很簡單的例子,vite 打包後的 SPA 如果直接點開那麼裡面打包的 ES Module 的檔案全部不允許請求。
  3. 那你這個專案不聯網能用嗎?看你的需求了,不聯網當然能用,這裡整合的 SPA 不一定全部都得是完整的 SPA,整套整合如果客戶在有網的環境下可以直接引用網頁的 URL 就好了。比如我們要用 monaco-editor 或者其他的文字編輯器又或者是 3D 編輯器,在 C# 上找不到或不好找到類似的庫,那麼整合 npm 上現成的庫就是最佳選擇。

修改專案檔案

我們首先修改專案檔案,讓 WPF 專案可以包含 ASP.NET Core 的庫,以及引用 WebView2 控制元件。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
	<OutputType>WinExe</OutputType>
	<TargetFramework>net8.0-windows</TargetFramework>
	<Nullable>enable</Nullable>
	<ImplicitUsings>enable</ImplicitUsings>
	<UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <!-- 這裡插入 WebView2 的包,用於顯示網頁 -->
	<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2478.35" />
    <!-- 這裡插入 ASP.NET Core 的框架引用,用於代理資原始檔 -->
	<FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

  <ItemGroup>
    <!-- 這裡模仿 ASP.NET Core,將 SPA 資原始檔存於 wwwroot 資料夾下 -->
	<None Update="wwwroot\**">
	  <CopyToOutputDirectory>Always</CopyToOutputDirectory>
	</None>
  </ItemGroup>

</Project>

修改 App.xamlApp.xaml.cs 以使用 ASP.NET Core 的 WebApplication.CreateBuilder()

這裡為了全域性使用依賴注入,我們將 WebApplication.CreateBuilder() 放在 App.xaml.cs 中全域性使用。為了使用依賴注入應註釋掉預設啟動視窗,並接管 Startup 事件。

<Application x:Class="WpfAircraftViewer.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfAircraftViewer"
             Startup="ApplicationStartup">
    <!-- 這裡將 StartupUri 屬性刪除,然後註冊 Startup 事件 -->
    <Application.Resources>
         
    </Application.Resources>
</Application>

然後透過修改 Startup 事件的程式碼來實現相應的載入動作。

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

namespace WpfAircraftViewer
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application, IAsyncDisposable
    {
        public WebApplication? WebApplication { get; private set; }

        public async ValueTask DisposeAsync()
        {
            if (WebApplication is not null)
            {
                await WebApplication.DisposeAsync();
            }
            GC.SuppressFinalize(this);
        }
        
        private async void ApplicationStartup(object sender, StartupEventArgs e)
        {
            // 這裡是建立 ASP.NET 版通用主機的程式碼
            var builder = WebApplication.CreateBuilder(Environment.GetCommandLineArgs());
            // 註冊主視窗和其他服務
            builder.Services.AddSingleton<MainWindow>();
            builder.Services.AddSingleton(this);
            var app = builder.Build();
            // 這裡是檔案型別對映,如果你的靜態檔案在瀏覽器中載入報 404,那麼需要在這裡註冊,這裡我載入一個 3D 場景檔案的型別
            var contentTypeProvider = new FileExtensionContentTypeProvider();
            contentTypeProvider.Mappings[".glb"] = "model/gltf-binary";
            app.UseStaticFiles(new StaticFileOptions
            {
                ContentTypeProvider = contentTypeProvider,
            });
            // 你如果使用了 Vue Router 或者其他前端路由了,需要在這裡新增這句話讓路由返回前端,而不是 ASP.NET Core 處理
            app.MapFallbackToFile("/index.html");
            WebApplication = app;
            // 處理退出事件,退出 App 時關閉 ASP.NET Core
            Exit += async (s, e) => await WebApplication.StopAsync();
            // 顯示主視窗
            MainWindow = app.Services.GetRequiredService<MainWindow>();
            MainWindow.Show();
            await app.RunAsync().ConfigureAwait(false);
        }
    }
}

此時,我們已經可以正常開啟一個預設介面的 MainWindow 了。

使用 WebView2 控制元件

這時我們就可以先將 SPA 檔案從 npm 專案的 dist 複製到 wwwroot 了,在編輯 MainWindow 加入 WebView2 控制元件後就可以檢視了。

<Window x:Class="WpfAircraftViewer.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:local="clr-namespace:WpfAircraftViewer"
        xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
        mc:Ignorable="d" MinHeight="450" MinWidth="800" SnapsToDevicePixels="True">
        <!-- 在上面加入 xmlns:wv2 屬性用於引用 WebView2 控制元件 -->
    <Grid>
        <!-- 這裡插入 WebView2 控制元件,我們預設可以讓 Source 是 http://localhost:5000,這是 ASP.NET Core 的預設監聽地址 -->
        <wv2:WebView2 Name="webView"
                  Source="{Binding SourceUrl, FallbackValue='http://localhost:5000'}" AllowDrop="True" SnapsToDevicePixels="True"/>
    </Grid>
</Window>

我們可以繼續編輯視窗的資訊,讓他可以關聯 ASP.NET Core 的監聽地址。

using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using System.Windows;

namespace WpfAircraftViewer
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public string SourceUrl { get; set; }
        public MainWindow(IServer server)
        {
            InitializeComponent();
            // 這裡透過注入的 IServer 物件來獲取監聽的 Url
            var addresses = server.Features.Get<IServerAddressesFeature>()?.Addresses;
            SourceUrl = addresses is not null ? (addresses.FirstOrDefault() ?? "http://localhost:5000") : "http://localhost:5000";
            // 無 VM,用自身當 VM
            DataContext = this;
        }
    }
}

這時我們就可以看到視窗開啟了我們的 SPA 頁面了。

示例程式碼

  • SofiaXu/WpfWebView2SpaLoader

相關文章