背景 我們有些工具在 Web 版中已經有了很好的實踐,而在 WPF 中重新開發也是一種費時費力的操作,那麼直接集成則是最省事省力的方法了。 思路解釋 為什麼要使用 WPF?莫問為什麼,老 C# 開發的堅持,另外因為 Windows 上已經裝了 Webview2/edge 整體打包比 electron ...
背景
我們有些工具在 Web 版中已經有了很好的實踐,而在 WPF 中重新開發也是一種費時費力的操作,那麼直接集成則是最省事省力的方法了。
思路解釋
- 為什麼要使用 WPF?莫問為什麼,老 C# 開發的堅持,另外因為 Windows 上已經裝了 Webview2/edge 整體打包比 electron 小很多,release 後的體積主要是 ASP.NET Core 的文件。
- 為什麼要使用 ASP.NET Core 進行代理呢?很簡單,因為很多操作要求使用 HTTP Context,在類似
file:///
的鏈接下是不能使用的,如果做成聯網的有些資源進行跨域請求也是不能的。舉個很簡單的例子,vite 打包後的 SPA 如果直接點開那麼裡面打包的 ES Module 的文件全部不允許請求。 - 那你這個項目不聯網能用嗎?看你的需求了,不聯網當然能用,這裡集成的 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.xaml
和 App.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 頁面了。