【絢麗】從0開始做一個WPF+Blazor對話小程式

【絢麗】從0開始做一個WPF+Blazor對話小程式

從一個WPF Hello World程式開始,逐漸引入Blazor,做個免費能看的對話小程式玩玩。

最後更新 2022/11/7 下午11:11
沙漠尽头的狼
預計閱讀 36 分鐘
分類
WPF Blazor
標籤
.NET C# Blazor WPF

大家好,我是沙漠盡頭的狼。

.NET 是免費、跨平台、開源,用於建置所有應用程式的開發人員平台。

本文演示如何在 WPF 中使用 Blazor 開發漂亮的 UI,為客戶端開發注入新活力。

要使 WPF 支援 Blazor,.NET 版本必須是 6.0 或更高版本,本文所有範例使用的 .NET 7.0,版本要求見連結,截圖看如下文字:

.NET版本要求

1. WPF 預設程式

本文從建立 WPF Hello World 開發:

使用 WPF 範本建立一個預設程式,取名【WPFBlazorChat】,專案組織結構如下:

空白WPF專案

執行專案,一個空白視窗:

WPF專案空白視窗

接著往下看,我們新增 Blazor 支援,本小節程式碼在這WPF預設程式原始碼

2. 新增 Blazor 支援

依然使用上面的工程,新增 Blazor 支援,此部分參考微軟文件建立 Windows Presentation Foundation (WPF) Blazor 應用程式,本小節快速略過。

2.1 編輯專案檔案

雙擊專案檔案 WPFBlazorChat.csproj,修改處如下:

專案檔案修改對比

  1. 在專案檔案的頂部,將 SDK 更改為 Microsoft.NET.Sdk.Razor
  2. 新增節點 <RootNameSpace>WPFBlazorChat</RootNameSpace>,將專案命名空間 WPFBlazorChat 設定為應用程式的根命名空間。
  3. 新增 NuGet 套件 Microsoft.AspNetCore.Components.WebView.Wpf,版本看你選擇的 .NET 版本而定。

2.2 新增 _Imports.razor 檔案

_Imports.razor 檔案類似一個 Global using 檔案,專門給 Razor 元件使用,放置一些用得比較多的全域命名空間,精簡程式碼。

內容如下,引入了一個命名空間 Microsoft.AspNetCore.Components.Web,這是 Razor 常用命名空間,包含用於向 Blazor 框架提供有關瀏覽器事件的資訊的類型。

@using Microsoft.AspNetCore.Components.Web

2.3 新增 wwwroot\index.html 檔案

VueReact 一樣,需要一個 html 檔案承載 Razor 元件,頁面內容類似:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WPFBlazorChat</title>
    <base href="/" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="WpfBlazor.styles.css" rel="stylesheet" />
</head>

<body>
<div id="app">Loading...</div>

<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webview.js"></script>
</body>

</html>
  1. app.css 檔案在下面給出定義。
  2. <div id="app">Loading...</div>,這裡是承載 Razor 元件的地方,後面所有載入的 Razor 元件都是在這裡渲染出來的。
  3. 其他暫時不管。

2.4 新增 wwwroot\css\app.css 檔案

頁面的基本樣式,通用的樣式可放在這個檔案:

html, body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1:focus {
    outline: none;
}

a, .btn-link {
    color: #0071c1;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid red;
}

.validation-message {
    color: red;
}

#blazor-error-ui {
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

2.5 新增一個 Razor 元件

加一個 Razor 的經典元件 Counter.razorBlazorHello World 程式就有這麼一個元件,檔案路徑:/RazorViews/Counter.razor,之所以放 RazorViews 目錄,是為了和 WPF 常用的 Views 目錄區分,該元件內容如下:

<h1>Counter</h1>

<p>好開心,你點我了,現在是:<span style="color: red;">@currentCount</span></p>

<button class="btn btn-primary" @onclick="IncrementCount">快點我</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

一個按鈕【快點我】,點擊 @onclick="IncrementCount" 使變數 currentCount 自增,同時頁面顯示此變數值,相信你能看懂。

2.6 Blazor 與 WPF 表單關聯

這是兩者產生關係的關鍵一步,打開表單 MainWindow.xaml,修改如下:

表單Xaml修改

如上程式碼,要點如下:

  1. 添加上面引入的 NuGet 套件 Microsoft.AspNetCore.Components.WebView.Wpf 的命名空間,命名為 blazor,主要是要使用 BlazorWebView 元件;
  2. BlazorWebView 元件屬性 HostPage 指定承載的 html 檔案,Services 指定 razor 元件的 Ioc 容器,看下面 MainWindow() 裡標紅的程式碼;
  3. RootComponentSelector="#app" 屬性指示 Razor 元件渲染的位置,看 index.html 中 id 為 app 的 html 元素,ComponentType 指示需要在 #app 中渲染的 Razor 元件類型。

打開 MainWindow.xaml.cs,修改如下:

注入Ioc容器

在 WPF 裡可以使用 Prism 等框架提供的 UnityDryIocIoc 容器實現檢視與服務的注入;Razor 元件這裡,預設使用 ASP.NET CoreIServiceCollection 容器;如果 WPF 表單與 Razor 元件需要共用資料,可以透過後面要說的 Messager 發送訊息,也可以透過 Ioc 容器注入的方式實現,比如從 WPF 表單中注入的資料(透過 MainWindow 建構函式注入),透過 IServiceCollection 容器再注入 Razor 元件使用,這裡後面也有提到。

WPF與Razor元件之間透過Ioc資料傳輸

上面步驟做完後,執行程式:

WPF整合Blazor的預設程式

OK,WPFBlazor 整合成功,打完收工?

等等,還沒完呢,本小節原始碼在這WPF中新增Blazor,接著往下看。

3. 自訂表單

WPF預設表單

看上圖,表單邊框是 WPF 預設的樣式,有時會感覺比較醜,或者不醜,設計師有其他的表單風格設計,往往我們要自訂表單,本節分享部分 WPF 與 Blazor 的自訂表單實現,更多客製化功能可能需要您自行研究。

3.1 WPF 自訂表單

一般實現是設定表單的三個屬性 WindowStyle="None" AllowsTransparency="True" Background="Transparent",即可隱藏預設表單的邊框,然後在內容區自己畫標題列、最小化、最大化、關閉按鈕、客戶區等。

MainWindow.xaml:隱藏WPF預設表單邊框

<Window
    x:Class="WPFBlazorChat.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:razorViews="clr-namespace:WPFBlazorChat.RazorViews"
    Title="MainWindow"
    Width="800"
    Height="450"
    AllowsTransparency="True"
    Background="Transparent"
    WindowStyle="None"
    mc:Ignorable="d">
    <Grid>
        <blazor:BlazorWebView HostPage="wwwroot\index.html" Services="{DynamicResource services}">
            <blazor:BlazorWebView.RootComponents>
                <blazor:RootComponent ComponentType="{x:Type razorViews:Counter}" Selector="#app" />
            </blazor:BlazorWebView.RootComponents>
    </Grid>
</Window>

上面的程式碼只是隱藏了 WPF 預設表單的邊框,執行程式如下:

隱藏WPF預設表單邊框

看上圖,點擊表單中的按鈕(其實是 Razor 元件的按鈕),但未執行按鈕點擊事件,且表單消失了,這是怎麼回事?您可以嘗試研究下為什麼,我沒有研究個所以然來,暫時加個背景處理 BlazorWebView 穿透的問題。

簡單的WPF自訂表單樣式

我們加上自訂表單的基本樣式看看:

帶基本樣式的WPF自訂表單

MainWindow.xaml 程式碼如下:

<Window
    x:Class="WPFBlazorChat.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:razorViews="clr-namespace:WPFBlazorChat.RazorViews"
    Title="MainWindow"
    Width="800"
    Height="450"
    AllowsTransparency="True" Background="Transparent" WindowStyle="None"
    mc:Ignorable="d">
    <Window.Resources>
        <Style TargetType="{x:Type Button}">
            <Setter Property="Width" Value="35" />
            <Setter Property="Height" Value="25" />
            <Setter Property="Margin" Value="2" />
            <Setter Property="Background" Value="Transparent" />
            <Setter Property="BorderThickness" Value="0" />
            <Setter Property="Foreground" Value="White" />
        </Style>
    </Window.Resources>
    <Border Background="#7160E8" CornerRadius="5">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="35" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Border
                Background="#7160E8" CornerRadius="5 5 0 0" MouseLeftButtonDown="MoveWindow_MouseLeftButtonDown">
                <Grid>
                    <TextBlock
                        Margin="10,10,5,5"
                        Foreground="White"
                        Text="這裡是表單標題列,左側可放Logo、標題,右側放表單操作按鈕:最小化、最大化、關閉等" />
                    <StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
                        <Button Click="MinimizeWindow_Click" Content="―" />
                        <Button Click="MaximizeWindow_Click" Content="口" />
                        <Button Click="CloseWindow_Click" Content="X" />
                    </StackPanel>
                </Grid>
            </Border>
            <blazor:BlazorWebView Grid.Row="1" HostPage="wwwroot\index.html" Services="{DynamicResource services}">
                <blazor:BlazorWebView.RootComponents>
                    <blazor:RootComponent ComponentType="{x:Type razorViews:Counter}" Selector="#app" />
                </blazor:BlazorWebView.RootComponents>
            </blazor:BlazorWebView>
        </Grid>
    </Border>
</Window>

我們給整個表單客戶端區域加了一個背景 Border(您可以去掉 Border 背景色,點擊介面按鈕試試),然後又套了一個 Grid,用於放置自訂的標題列(標題和表單控制按鈕)和 BlazorWebView(用於渲染 Razor 元件的瀏覽器元件),下面是表單控制按鈕的響應事件:

using Microsoft.Extensions.DependencyInjection;
using System.Windows;

namespace WPFBlazorChat;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddWpfBlazorWebView();
        Resources.Add("services", serviceCollection.BuildServiceProvider());
    }

    private void MoveWindow_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        if (e.ClickCount == 1)
        {
            this.DragMove();
        }
        else
        {
            MaximizeWindow_Click(null, null);
        }
    }

    private void CloseWindow_Click(object sender, RoutedEventArgs e)
    {
        this.Close();
    }

    private void MinimizeWindow_Click(object sender, RoutedEventArgs e)
    {
        this.WindowState = WindowState.Minimized;
    }

    private void MaximizeWindow_Click(object sender, RoutedEventArgs e)
    {
        this.WindowState = this.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
    }
}

程式碼簡單,處理了表單最小化、表單最大化(還原)、關閉、標題列雙擊表單最大化(還原),上面的實現不是一個完美的自訂表單實現,至少有這兩個問題:

  • 當您嘗試最大化後,表單鋪滿了整個作業系統桌面(連工作列區域也佔用了);
  • 表單工作列兩個圓角未生效(紅色矩形框選的部分),即表單下面的兩個圓角,站長未找到讓 BlazorWebView 出現圓角的屬性或其他方法;標題列區域(綠色矩形框選的部分)是 WPF 控制項,所以圓角顯示正常。

表單圓角

在後面的 3.4 小節,站長使用一個第三庫實現了表單圓角問題,更多比較好的 WPF 自訂表單實現可看這篇文章:WPF三種自訂視窗的實現,本小節中範例原始碼在這WPF自訂表單

3.2 WPF 異形表單

異形表單的需求,使用 WPF 實現是比較方便的,本來打算寫寫的,感覺偏離主題太遠了,給篇文章自行看看吧:WPF異形視窗演示,文中異形表單效果如下:

WPF異形表單

下面介紹將表單的標題列也放 Razor 元件中實現的方式。

3.3 Blazor 實現自訂表單效果

上面使用了 WPF 製作自訂表單,有沒有這種需求,把選單放置到標題列?這個簡單,WPF 能很好實現。

如果放 Tab 類控制項呢?Tab Header 是在標題列顯示,TabItem 是在客戶端區域,Tab HeaderTabItem 風格統一,在一套程式碼裡面實現和維護也方便,那麼在 WPF+Blazor 混合開發的情況怎麼實現呢?相信透過本節 Razor 元件實現標題列的介紹,你能做出來。

MainWindow.xaml 恢復程式碼,只設定隱藏 WPF 預設表單邊框,並給 BlazorWebView 套一層背景:

WPF透明表單

後面的程式碼有參考 BlazorDesktopWPF-CustomTitleBar 開源專案實現。

我們把標題列做到 Counter.razor 元件,即標題列、客戶區放一個元件裡,當然你也可以分離,這裡我們方便演示:

Counter.razor

@using WPFBlazorChat.Services

<div class="titlebar" @ondblclick="WindowService.Maximize" @onmouseup="WindowService.StopMove" @onmousedown="WindowService.StartMove">
    <button class="titlebar-btn" onclick="alert('js alert: navigation pressed');">
        <img src="svg/navigation.svg" />
    </button>
    <div class="window-title">
        測試表單標題
    </div>
    <div style="flex-grow:1"></div>
    <button class="titlebar-btn" onclick="alert('js alert: settings pressed');">
        <img src="svg/settings.svg" />
    </button>
    <button class="titlebar-btn" @onclick="WindowService.Minimize">
        <img src="svg/minimize.svg" />
    </button>
    <button class="titlebar-btn" @onclick="WindowService.Maximize">
        @if (WindowService.IsMaximized())
        {
            <img src="svg/restore.svg" />
        }
        else
        {
            <img src="svg/maximize.svg" />
        }
    </button>
    <button class="titlebar-cbtn" @onclick="()=>WindowService.Close(false)">
        <img src="svg/dismiss.svg" />
    </button>
</div>

<p>好開心,你點我了,現在是:<span style="color: red;">@currentCount</span></p>

<button class="btn btn-primary" @onclick="IncrementCount">快點我</button>

@code {
    private int currentCount = 0;

    protected override void OnInitialized()
    {

        WindowService.Init();
        base.OnInitialized();
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}

下面給出程式碼簡單說明:

  1. 第一個 div 充做表單的標題列區域,註冊了雙擊事件呼叫表單最大化(還原)方法、滑鼠按下與釋放呼叫表單的移動開始與結束方法;
  2. 在第一個 div 裡,其中有 3 個按鈕,即表單的控制按鈕,呼叫表單最小化、最大化(還原)、關閉方法呼叫;
  3. 另有兩個按鈕,演示單擊呼叫 JavaScriptalert 方法彈出訊息。

WPF透明表單

執行效果如下:

WPF透明表單

實現這個效果,還有一些程式碼:

  1. 上面的程式碼呼叫了一些方法實現表單操作最小化、關閉等,程式碼如下;
  2. 因為是 Razor 元件,即 html 實現的介面,介面的 html 元素也定義了一些 css 樣式,程式碼也一併給出。
  3. 標題列的按鈕使用了一些 svg 圖片,在倉庫裡,可自行獲取。

表單拖動

首先新增 NuGet 套件 Simplify.Windows.Forms,用於獲取滑鼠游標的位置:

<PackageReference Include="Simplify.Windows.Forms" Version="1.1.2" />

新增表單幫助類:Services\WindowService.cs

using System;
using System.Linq;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Threading;
using Application = System.Windows.Application;

namespace WPFBlazorChat.Services;

public class WindowService
{
    private static bool _isMoving;
    private static double _startMouseX;
    private static double _startMouseY;
    private static double _startWindLeft;
    private static double _startWindTop;

    public static void Init()
    {
        DispatcherTimer dispatcherTimer = new();
        dispatcherTimer.Tick += UpdateWindowPos;
        dispatcherTimer.Interval = TimeSpan.FromMilliseconds(17);
        dispatcherTimer.Start();
    }

    public static void StartMove()
    {
        _isMoving = true;
        _startMouseX = GetX();
        _startMouseY = GetY();
        var window = GetActiveWindow();
        if (window == null)
        {
            return;
        }

        _startWindLeft = window.Left;
        _startWindTop = window.Top;
    }

    public static void StopMove()
    {
        _isMoving = false;
    }

    public static void Minimize()
    {
        var window = GetActiveWindow();
        if (window != null)
        {
            window.WindowState = WindowState.Minimized;
        }
    }

    public static void Maximize()
    {
        var window = GetActiveWindow();
        if (window != null)
        {
            window.WindowState =
                window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
        }
    }


    public static bool IsMaximized()
    {
        var window = GetActiveWindow();
        if (window != null)
        {
            return window.WindowState == WindowState.Maximized;
        }

        return false;
    }

    public static void Close(bool allWindow = false)
    {
        if (allWindow)
        {
            Application.Current?.Shutdown();
            return;
        }

        var window = GetActiveWindow();
        if (window != null)
        {
            window.Close();
        }
    }


    private static void UpdateWindowPos(object? sender, EventArgs e)
    {
        if (!_isMoving)
        {
            return;
        }

        double moveX = GetX() - _startMouseX;
        double moveY = GetY() - _startMouseY;
        Window? window = GetActiveWindow();
        if (window == null)
        {
            return;
        }

        window.Left = _startWindLeft + moveX;
        window.Top = _startWindTop + moveY;
    }

    private static int GetX()
    {
        return Control.MousePosition.X;
    }

    private static int GetY()
    {
        return Control.MousePosition.Y;
    }

    private static Window? GetActiveWindow()
    {
        return Application.Current.Windows.Cast<Window>().FirstOrDefault(currentWindow => currentWindow.IsActive);
    }
}

上面的程式碼用於表單的最小化、最大化(還原)、關閉等實現,需要在 Razor 元件裡正確的呼叫這些方法:

  1. Counter.razor 元件的 OnInitialized 初始化生命週期方法裡呼叫 WindowService.Init();,如上程式碼,這個方法開啟定時器,定時呼叫 UpdateWindowPos 方法檢查滑鼠是否按下,如果按下,檢查間隔內表單的位置變化範圍,然後修改表單位置,從而實現表單位置移動(移動表單無法使用 WPF 的 DragMove 方法,您可以嘗試使用看看它報什麼錯),移動表單有更好的方法歡迎留言。

  2. Razor 元件裡表單控制按鈕的使用看上面的程式碼不難理解,不過多解釋。

上面效果的樣式檔案修改如下,wwwroot\css\app.css

/*
BlazorDesktopWPF-CustomTitleBar - © Copyright 2021 - Jam-Es.com
Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
*/

html, body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
    padding: 0;
    margin: 0;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid red;
}

.validation-message {
    color: red;
}

#blazor-error-ui {
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

.page-container {
    display: flex;
    flex-direction: column;
    height: 100vh;
}

.content-container {
    padding: 0px 20px 20px 20px;
    flex-grow: 1;
    overflow-y: scroll;
}

.titlebar {
    width: 100%;
    height: 32px;
    min-height: 32px;
    background-color: #7160E8;
    display: flex;
    flex-direction: row;
}

.titlebar-btn, .titlebar-cbtn {
    width: 46px;
    background-color: #7160E8;
    color: white;
    border: none;
    border-radius: 0;
}

.titlebar-btn:hover {
    background-color: #5A5A5A;
}

.titlebar-btn:focus, .titlebar-cbtn:focus {
    outline: 0;
}

.titlebar-cbtn:hover {
    background-color: #E81123;
}

.window-title {
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin-left: 5px;
    color: white;
}

上面的一些程式碼即實現了由 Razor 元件實現表單的標題顯示、表單的最小化、最大化(還原)、關閉、移動等操作,然而還是會有 3.1 結尾出現的問題,即表單圓角和表單最大化鋪滿作業系統桌面工作列的問題,下面一小節我們嘗試解決他。

小節總結:透過上面的程式碼,如果放 Tab 控制項鋪滿整個表單,是不是有思路了?

本小節原始碼在這Razor元件實現表單標題列功能

3.4 Blazor 與 WPF 比較完美的實現效果

其實上面的程式碼可以當作學習,即使有不小瑕疵(哈哈),本小節我們還是使用第三套件解決表單圓角和最大化問題。

首先新增 NuGet 套件 ModernWpfUI,該 WPF 控制項庫本站介紹連結開源WPF控制項庫:ModernWpf

<PackageReference Include="ModernWpfUI" Version="0.9.7-preview.2" />

然後打開 App.xaml,引用上面開源 WPF 控制項的樣式:

<Application x:Class="WPFBlazorChat.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:ui="http://schemas.modernwpf.com/2019"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ui:ThemeResources />
                <ui:XamlControlsResources />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

最後打開 MainWindow.xaml,修改如下(主要是引入的幾個屬性 ui:xxxxx):

<Window x:Class="WPFBlazorChat.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:ui="http://schemas.modernwpf.com/2019"
        xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"
        xmlns:razorViews="clr-namespace:WPFBlazorChat.RazorViews"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        ui:TitleBar.ExtendViewIntoTitleBar="True"
        ui:TitleBar.IsBackButtonVisible="False"
        ui:TitleBar.Style="{DynamicResource AppTitleBarStyle}"
        ui:WindowHelper.UseModernWindowStyle="True">
    <Border Background="#7160E8" CornerRadius="5">
        <blazor:BlazorWebView HostPage="wwwroot\index.html" Services="{DynamicResource services}">
            <blazor:BlazorWebView.RootComponents>
                <blazor:RootComponent Selector="#app" ComponentType="{x:Type razorViews:Counter}" />
            </blazor:BlazorWebView.RootComponents>
        </blazor:BlazorWebView>
    </Border>
</Window>

就上面三處修改,我們執行看看:

WPF與Blazor自訂表單比較完美的解決

是不是和 3.3 效果一樣?其實仔細看,表單下面的圓角也有了:

表單圓角

最終還是 WPF 解決了所有問題...

我笑了

具體怎麼實現的表單最大化未佔操作系統的工作列,以及表單圓角問題的解決(竟然能讓 BlazorWebView 部分透明了)可以查看該元件相關程式碼,本文不過多深究。

另外,WPF 熟手可能比較清楚,前面的程式碼還不能正常的拖動改變表單大小(不知道你有沒有發現,我當你沒發現。),使用該庫後也解決了:

表單手動改變大小

本小節原始碼在這解決圓角和最大化問題,下面開始本文的下半部分了,好累,終於到這了。

我累了

4. 新增第三方 Blazor 元件

工欲善其事,必先利其器!

鑑於大部分同學前端基礎可能不是太好,即使使用 Blazor 可以少用或者不用 JavaScript,但有那麼一款漂亮、便捷的 Blazor 元件庫,這不是如虎添翼嗎?本文使用 Masa Blazor 做範例展示,如今 Blazor 元件庫眾多,選擇自己喜歡的、順手的就成:

Masa Blazor

站長前些日子介紹過 MAUI 使用 Masa blazor 元件庫一文,本小節思路也是類似,且看我表演。

看我表演

打開 Masa Blazor 文件站點:https://blazor.masastack.com/getting-started/installation,一起來往 WPF 中引入這款 Blazor 元件庫吧。

4.1 引入 Masa.Blazor 套件

打開專案檔案 WPFBlazorChat.csproj 直接複製下面的套件版本,或透過 NuGet 套件管理員搜尋 Masa.Blazor 安裝:

<PackageReference Include="Masa.Blazor" Version="0.6.0" />

4.2 新增 Masa.Blazor 帶來的資源

打開 wwwroot\index.html,在 <head></head> 節點新增如下資源:

<link href="_content/Masa.Blazor/css/masa-blazor.min.css" rel="stylesheet" />

<link href="https://cdn.masastack.com/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.masastack.com/npm/materialicons/materialicons.css" rel="stylesheet">
<link href="https://cdn.masastack.com/npm/fontawesome/v5.0.13/css/all.css" rel="stylesheet">

<script src="_content/BlazorComponent/js/blazor-component.js"></script>

完整程式碼如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WPFBlazorChat</title>
    <base href="/" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="WpfBlazor.styles.css" rel="stylesheet" />

    <link href="_content/Masa.Blazor/css/masa-blazor.min.css" rel="stylesheet" />

    <link href="https://cdn.masastack.com/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
    <link href="https://cdn.masastack.com/npm/materialicons/materialicons.css" rel="stylesheet">
    <link href="https://cdn.masastack.com/npm/fontawesome/v5.0.13/css/all.css" rel="stylesheet">

    <script src="_content/BlazorComponent/js/blazor-component.js"></script>
</head>

<body>
<div id="app">Loading...</div>

<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webview.js"></script>
</body>

</html>

4.3 引入 Masa.Blazor 命名空間

打開 _Imports.razor 檔案,修改如下:

@using Microsoft.AspNetCore.Components.Web
@using Masa.Blazor
@using BlazorComponent

4.4 Razor 元件新增 Masa.Blazor

打開 MainWindow.xaml.cs,新增一行程式碼 serviceCollection.AddMasaBlazor();

Ioc中新增Masa Blazor

4.5 嘗試 Masa.Blazor 案例

上面4步的準備工作做好後,我們簡單來使用下 Masa.Blazor 元件。

打開 Tab 元件連結:https://blazor.masastack.com/components/tabs,嘗試這個 Demo:

Masa Blazor的Tab元件案例

Demo 的程式碼我幾乎不變的引入,打開 RazorViews\Counter.razor 檔案,保留 3.4 節的標題列,替換了客戶區域內容,程式碼如下:

@using WPFBlazorChat.Services

<MApp>
    <!--上一小節的標題列開始-->
    <div class="titlebar" @ondblclick="WindowService.Maximize" @onmouseup="WindowService.StopMove" @onmousedown="WindowService.StartMove">
        <button class="titlebar-btn" onclick="alert('js alert: navigation pressed');">
            <img src="svg/navigation.svg"/>
        </button>
        <div class="window-title">
            測試表單標題
        </div>
        <div style="flex-grow: 1"></div>
        <button class="titlebar-btn" onclick="alert('js alert: settings pressed');">
            <img src="svg/settings.svg"/>
        </button>
        <button class="titlebar-btn" @onclick="WindowService.Minimize">
            <img src="svg/minimize.svg"/>
        </button>
        <button class="titlebar-btn" @onclick="WindowService.Maximize">
            @if (WindowService.IsMaximized())
            {
                <img src="svg/restore.svg"/>
            }
            else
            {
                <img src="svg/maximize.svg"/>
            }
        </button>
        <button class="titlebar-cbtn" @onclick="() => WindowService.Close(false)">
            <img src="svg/dismiss.svg"/>
        </button>
    </div>
    <!--上一小節的標題列結束-->
    
    <!--新增的Masa.Blazor Tab案例程式碼開始-->
    <MCard>
        <MToolbar Color="cyan" Dark Flat>
            <ChildContent>
                <MAppBarNavIcon></MAppBarNavIcon>

                <MToolbarTitle>Your Dashboard</MToolbarTitle>

                <MSpacer></MSpacer>

                <MButton Icon>
                    <MIcon>mdi-magnify</MIcon>
                </MButton>

                <MButton Icon>
                    <MIcon>mdi-dots-vertical</MIcon>
                </MButton>
            </ChildContent>

            <ExtensionContent>
                <MTabs @bind-Value="tab"
                       AlignWithTitle
                       SliderColor="yellow">
                    @foreach (var item in items)
                    {
                        <MTab Value="item">
                            @item
                        </MTab>
                    }
                </MTabs>
            </ExtensionContent>
        </MToolbar>

        <MTabsItems @bind-Value="tab">
            @foreach (var item in items)
            {
                <MTabItem Value="item">
                    <MCard Flat>
                        <MCardText>@text</MCardText>
                    </MCard>
                </MTabItem>
            }
        </MTabsItems>
    </MCard>
    <!--新增的Masa.Blazor Tab案例程式碼結束-->
</MApp>

@code {

    #region Masa.Blazor Tab案例C#程式碼
    StringNumber tab;

    List<string> items = new()
    {
        "web", "shopping", "videos", "images", "news",
    };

    string text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.";

    #endregion
    
    
    protected override void OnInitialized()
    {
        WindowService.Init();
        base.OnInitialized();
    }
}

執行效果如下:

Masa Blazor的Tab元件案例整合

是不是有那味兒了?再嘗試把 Tab 移到標題列,前面有提過的效果:

Tab放標題列

上面的效果,程式碼修改如下,刪除了原標題列程式碼,將表單操作按鈕放到了 MToolbar 裡面,並使用 MToolbar 新增了雙擊事件、滑鼠按下、釋放事件實現表單拖動:

<MApp>

    <!--新增的Masa.Blazor Tab案例程式碼開始-->
    <MCard>
        <MToolbar Color="cyan" Dark Flat @ondblclick="WindowService.Maximize" @onmouseup="WindowService.StopMove" @onmousedown="WindowService.StartMove">
            <MTabs @bind-Value="tab"
                   AlignWithTitle
                   SliderColor="yellow">
                @foreach (var item in items)
                {
                    <MTab Value="item">
                        @item
                    </MTab>
                }
            </MTabs>
            
            <div style="flex-grow: 1"></div>
            <button class="titlebar-btn" onclick="alert('js alert: settings pressed');">
                <img src="svg/settings.svg"/>
            </button>
            <button class="titlebar-btn" @onclick="WindowService.Minimize">
                <img src="svg/minimize.svg"/>
            </button>
            <button class="titlebar-btn" @onclick="WindowService.Maximize">
                @if (WindowService.IsMaximized())
                {
                    <img src="svg/restore.svg"/>
                }
                else
                {
                    <img src="svg/maximize.svg"/>
                }
            </button>
            <button class="titlebar-cbtn" @onclick="() => WindowService.Close(false)">
                <img src="svg/dismiss.svg"/>
            </button>
        </MToolbar>

        <MTabsItems @bind-Value="tab">
            @foreach (var item in items)
            {
                <MTabItem Value="item">
                    <MCard Flat>
                        <MCardText>@text</MCardText>
                    </MCard>
                </MTabItem>
            }
        </MTabsItems>
    </MCard>
    <!--新增的Masa.Blazor Tab案例程式碼結束-->
</MApp>

表單操作按鈕的背景色也做部分修改:

樣式部分修改

其實上面的表單效果還是有點瑕疵,注意到表單右側的豎直捲動條了嗎?在沒引入 Masa.Blazor 之前,右側正常顯示,引入後多了一個豎直捲動條:

引入Masa.Blazor後多了豎直捲動條

這個想去掉也簡單,在 wwwroot\css\app.css 追加樣式(當時也是折騰了好一會兒,最後在 Masa.Blazor 群裡群友給出了解決方案,十分感謝):

問題解決過程

問題解決 css 程式碼:

::-webkit-scrollbar {
    width: 0px;
}

因為 Razor 元件是在 BlazorWebView 裡渲染的,即 BlazorWebView 就是個小型的瀏覽器呀,上面的樣式即把瀏覽器的捲動條寬度設定為 0,它不就沒有了嗎?現在效果如下,是不是舒服了?

根據後介面

新增 Masa.Blazor 就介紹到這裡,本小節範例程式碼在這裡WPF中使用Masa.Blazor,下面講解 WPF 與 Blazor 混合開發後多表單訊息通知問題。

5. 多表單訊息通知

一般 C/S 表單之間通訊使用委託、事件,而在 WPF 開發中,可以使用一些框架提供的抽象事件 訂閱\發佈 元件,比如 Prism 的事件聚集器 IEventAggregator,或 MvvmLightMessager。在 B/S 開發中,行程內事件通知可能就使用 MediatR 元件居多了,不論是在 C/S 還是 B/S 開發,這些元件在一定程度上,各大程式範本可以通用的,更不用說分散式的訊息佇列 RabbitMQKafka 是萬能的行程間通訊標準選擇了。

上面是一些套話,站長根據 Prism 的事件聚集器和 MvvmLight 的 Messager 原始碼閱讀,簡單封裝了一個 Messager,可以適用於一般的業務需求。

5.1 Messager 封裝

本來不想貼程式碼直接給原始碼連結的,想想程式碼也不多,直接上吧。

Message

訊息抽象類別,用於定義訊息類型,具體的訊息需要繼承該類別,比如後面的打開子表單訊息 OpenSecondViewMessage

using System;

namespace WPFBlazorChat.Messages;

public abstract class Message
{
    protected Message(object sender)
    {
        this.Sender = sender ?? throw new ArgumentNullException(nameof(sender));
    }

    public object Sender { get; }
}

IMessenger

訊息介面,只定義了三個介面:

  1. Subscribe:訊息訂閱
  2. Unsubscribe:取消訊息訂閱
  3. Publish:訊息發送
using System;

namespace WPFBlazorChat.Messages;

public interface IMessenger
{
    void Subscribe<TMessage>(object recipient, Action<TMessage> action,
        ThreadOption threadOption = ThreadOption.PublisherThread) where TMessage : Message;

    void Unsubscribe<TMessage>(object recipient, Action<TMessage>? action = null) where TMessage : Message;

    void Publish<TMessage>(object sender, TMessage message) where TMessage : Message;
}

public enum ThreadOption
{
    PublisherThread,
    BackgroundThread,
    UiThread
}

Messenger

訊息的管理,訊息中轉等實現:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace WPFBlazorChat.Messages;

public class Messenger : IMessenger
{
    public static readonly Messenger Default = new Messenger();
    private readonly object registerLock = new object();

    private Dictionary<Type, List<WeakActionAndToken>>? recipientsOfSubclassesAction;

    public void Subscribe<TMessage>(object recipient, Action<TMessage> action, ThreadOption threadOption)
        where TMessage : Message
    {
        lock (this.registerLock)
        {
            var messageType = typeof(TMessage);

            this.recipientsOfSubclassesAction ??= new Dictionary<Type, List<WeakActionAndToken>>();

            List<WeakActionAndToken> list;

            if (!this.recipientsOfSubclassesAction.ContainsKey(messageType))
            {
                list = new List<WeakActionAndToken>();
                this.recipientsOfSubclassesAction.Add(messageType, list);
            }
            else
            {
                list = this.recipientsOfSubclassesAction[messageType];
            }

            var item = new WeakActionAndToken
            { Recipient = recipient, ThreadOption = threadOption, Action = action };

            list.Add(item);
        }
    }

    public void Unsubscribe<TMessage>(object? recipient, Action<TMessage>? action) where TMessage : Message
    {
        var messageType = typeof(TMessage);

        if (recipient == null || this.recipientsOfSubclassesAction == null ||
            this.recipientsOfSubclassesAction.Count == 0 || !this.recipientsOfSubclassesAction.ContainsKey(messageType))
        {
            return;
        }

        var lstActions = this.recipientsOfSubclassesAction[messageType];
        for (var i = lstActions.Count - 1; i >= 0; i--)
        {
            var item = lstActions[i];
            var pastAction = item.Action;

            if (pastAction != null
                && recipient == pastAction.Target
                && (action == null || action.Method.Name == pastAction.Method.Name))
            {
                lstActions.Remove(item);
            }
        }
    }

    public void Publish<TMessage>(object sender, TMessage message) where TMessage : Message
    {
        var messageType = typeof(TMessage);

        if (this.recipientsOfSubclassesAction != null)
        {
            var listClone = this.recipientsOfSubclassesAction.Keys.Take(this.recipientsOfSubclassesAction.Count)
                .ToList();

            foreach (var type in listClone)
            {
                List<WeakActionAndToken>? list = null;

                if (messageType == type || messageType.IsSubclassOf(type) || type.IsAssignableFrom(messageType))
                {
                    list = this.recipientsOfSubclassesAction[type]
                        .Take(this.recipientsOfSubclassesAction[type].Count)
                        .ToList();
                }

                if (list is { Count: > 0 })
                {
                    this.SendToList(message, list);
                }
            }
        }
    }

    private void SendToList<TMessage>(TMessage message, IEnumerable<WeakActionAndToken> weakActionsAndTokens)
        where TMessage : Message
    {
        var list = weakActionsAndTokens.ToList();
        var listClone = list.Take(list.Count()).ToList();

        foreach (var item in listClone)
        {
            if (item.Action is { Target: { } })
            {
                switch (item.ThreadOption)
                {
                    case ThreadOption.BackgroundThread:
                        Task.Run(() => { item.ExecuteWithObject(message); });
                        break;
                    case ThreadOption.UiThread:
                        SynchronizationContext.Current!.Post(_ => { item.ExecuteWithObject(message); }, null);
                        break;
                    default:
                        item.ExecuteWithObject(message);
                        break;
                }
            }
        }
    }
}

public class WeakActionAndToken
{
    public object? Recipient { get; set; }

    public ThreadOption ThreadOption { get; set; }

    public Delegate? Action { get; set; }

    public string? Tag { get; set; }

    public void ExecuteWithObject<TMessage>(TMessage message) where TMessage : Message
    {
        if (this.Action is Action<TMessage> factAction)
        {
            factAction.Invoke(message);
        }
    }
}

有興趣的看上面的程式碼,封裝程式碼上面簡單全部給上,後面的訊息通知都是基於上面的三個類別實現的,比較核心。

5.2 程式碼整理

第 5 節涉及到多表單及多 Razor 元件了,需要建立一些目錄存放這些檔案,方便分類管理。

整理後程式碼

  1. A:放 Message,即一些訊息通知類;

  2. B:放 Razor 元件,如果需要與 Maui\Blazor Server(Wasm) 等共用 Razor 元件,可以建立 Razor 類別庫 儲存;

  3. C:放通用服務,這裡只放了一個表單管理靜態類,實際情況可以放 Redis 服務、RabbitMQ 訊息服務等;

  4. D:放 WPF 檢視,本範例 WPF 表單只是一個殼,承載 BlazorWebView 使用;

5.3 範例及程式碼說明

先看本範例效果,再給出相關程式碼說明:

訊息通知範例

圖中有三個操作:

  1. 點擊主表單A的 【+】 按鈕,發送了 OpenSecondViewMessage 訊息,打開子表單 B;
  2. 打開子表單 B 後,再點擊主表單 A 的 【桃心】 按鈕,發送了 SendRandomDataMessage 訊息,子表單 B 的第二個 TabItem Header 顯示了訊息傳來的數字;
  3. 點擊子表單 B 的 【安卓】 圖示按鈕,給主表單 A 響應了訊息 ReceivedResponseMessage,主表單收到後彈出一個對話框。

三個訊息類別定義如下:

public class OpenSecondViewMessage : Message
{
    public OpenSecondViewMessage(object sender) : base(sender)
    {
    }
}

public class SendRandomDataMessage : Message
{
    public SendRandomDataMessage(object sender, int number) : base(sender)
    {
        Number = number;
    }

    public int Number { get; set; }
}

public class ReceivedResponseMessage : Message
{
    public ReceivedResponseMessage(object sender) : base(sender)
    {
    }
}

除了 SendRandomDataMessage 傳遞了一個業務 Number 屬性,另兩個訊息只是起到通知作用(所以沒有額外屬性定義),實際開發時可能需要傳遞業務資料。

5.3.1 打開多表單

即上面的第一個操作:點擊主表單 A 的 【+】 按鈕,發送了 OpenSecondViewMessage 訊息,打開子表單 B。

RazorViews\MainView.razor 中執行按鈕點擊,發送打開子表單訊息:

...
<MCol>
    <MButton class="mx-2" Fab Dark Color="indigo" OnClick="OpenNewSecondView">
        <MIcon>mdi-plus</MIcon>
    </MButton>
</MCol>
...

@code{
...
void OpenNewSecondView()
{
	Messenger.Default.Publish(this, new OpenSecondViewMessage(this));
}
...
}

App.xaml.cs 裡訂閱打開子表單訊息:

public partial class App : Application
{
    public App()
    {
        // 訂閱打開子視窗訊息,在主視窗點擊【+】按鈕
        Messenger.Default.Subscribe<OpenSecondViewMessage>(this, msg =>
        {
            var chatWin = new SecondWindowView();
            chatWin.Show();
        }, ThreadOption.UiThread);
    }
}

實際開發可能情況更複雜,發送的訊息 OpenSecondViewMessage 裡帶 WPF 表單路由(定義的一套路徑規則尋找表單或 ViewModel),訂閱的地方也可能不在主程式,在子模組的 Module 類別裡。

5.3.2 發送業務資料

即第二個操作:打開子表單 B 後,再點擊主表單 A 的 【桃心】 按鈕,發送了 SendRandomDataMessage 訊息,子表單 B 的第二個 TabItem Header 顯示了訊息傳來的數字。

  1. RazorViews\MainView.razor 中執行按鈕點擊,發送業務訊息(就當前時間的 Millisecond):
...
<MCol>
    <MButton class="mx-2" Fab Small Dark Color="pink" OnClick="SendNumber">
        <MIcon>mdi-heart</MIcon>
    </MButton>
</MCol>
...

@code{
...
void SendNumber()
{
	Messenger.Default.Publish(this, new SendRandomDataMessage(this, DateTime.Now.Millisecond));
}
...
}
  1. RazorViews\SecondView.razorOnInitialized() 方法裡訂閱業務訊息通知:
@using WPFBlazorChat.Messages
<MApp>
    <MToolbar>
        <MTabs BackgroundColor="primary" Grow Dark>
            <MTab>
                <MBadge Color="pink" Dot>
                    Item One
                </MBadge>
            </MTab>
            <MTab>
                <MBadge Color="green" Content="tagCount">
                    Item Two
                </MBadge>
            </MTab>
            <MTab>
                <MBadge Color="deep-purple accent-4" Icon="mi-masa">
                    Item Three
                </MBadge>
            </MTab>
        </MTabs>
    </MToolbar>
    
    <MRow>
        <MButton class="mx-2" Fab Dark Large Color="purple" OnClick="ReponseMessage">
            <MIcon>
                mdi-android
            </MIcon>
        </MButton>
    </MRow>
</MApp>

@code
{
    private int tagCount = 6;

    protected override void OnInitialized()
    {
    	// 訂閱業務訊息,在主視窗點擊桃心按鈕時觸發
        Messenger.Default.Subscribe<SendRandomDataMessage>(this, msg =>
        {
            this.InvokeAsync(() => { this.tagCount = msg.Number; });
            this.StateHasChanged();
        }, ThreadOption.UiThread);
    }

    void ReponseMessage()
    {
        // 通知主視窗,我已經收到訊息,請不要再發
        Messenger.Default.Publish(this, new ReceivedResponseMessage(this));
    }
}

注意看,上面收到訊息時有兩個方法要簡單說一下,看 OnInitialized() 裡的程式碼:

  • InvokeAsync:將 Number 賦值給變數 tagCount 的程式碼是在 InvokeAsync 方法裡執行的,這個和 WPF 裡的 Dispatcher.Invoke 是一個意思,相當於接收資料是在子執行緒,而賦值這個操作會即時的繫結到 <MBadge Color="green" Content="tagCount"> 上,就需要 UI 執行緒同步。
  • StateHasChanged:相當於 WPF MVVM 裡的 PropertyChanged 事件通知,通知 UI 這裡有值變化了,請你重新整理一下,我要看看最新值。

上面的程式碼把子表單訊息回應也貼上了,即點擊安卓圖示按鈕時發送了 ReceivedResponseMessage 訊息,在主表單 RazorViews\MainView.razor 裡也訂閱了這個訊息,和上面的程式碼類似:

...
    <!--確認對話框開始-->
    <PConfirm Visible="_showComfirmDialog"
              Title="子表單來回應了"
              Type="AlertTypes.Warning"
              OnCancel="() => _showComfirmDialog = false"
              OnOk="() => _showComfirmDialog = false">
        說你別沒事一直發,它們煩!
    </PConfirm>
    <!--確認對話框結束-->
</MApp>

@code{
...
	// 是否顯示確認對話框
    bool _showComfirmDialog;
	protected override void OnInitialized()
    {
        WindowService.Init();

    // 訂閱子表單回應的訊息,它已經收到訊息了,我可以休息下再發
        Messenger.Default.Subscribe<ReceivedResponseMessage>(this, msg =>
        {
            this.InvokeAsync(() => { _showComfirmDialog = true; });
            this.StateHasChanged();
        }, ThreadOption.UiThread);
        base.OnInitialized();
    }
...
}

OnInitialized() 方法裡訂閱訊息 ReceivedResponseMessage,收到後將變數 _showComfirmDialog 置為 true,即上面對話框的屬性 Visible 繫結的值,同理需要在 InvokeAsync() 中處理資料接收,也需要呼叫 StateHasChanged 通知 UI 資料變化。

上面說了部分程式碼,可能講得不太清楚,可以看本節範例原始碼:多表單訊息通知

6. 本文範例

本來想寫完整 Demo 說明的,發現上面把基本要點都拉了一遍,再貼一些重複程式碼有點沒完沒了了,有興趣的拉原始碼 WPF與Blazor混合開發Demo 查看、執行,下面是專案程式碼結構:

Demo程式碼結構

下面是最後的範例效果圖,前面部分文章已經發過,再發一次,哈哈:

使用者列表視窗

使用者列表

開啟子視窗

開啟視窗

聊天視窗

聊天視窗

示範發送訊息

7. Click Once 發佈嘗試

上一篇文章連結:快速建立軟體安裝包-ClickOnce,本文範例 Click Once 安裝頁面:https://dotnet9.com/WPFBlazorChat

8. Q&A

8.1 為啥要在 WPF 裡使用 Blazor?吃飽了撐的?

WPF 雖然相較 Winform 做出比較好看的 UI 相對容易一些,但比起 Blazor,或者直接說 html 開發介面,還是差了一點點,更何況 html 的資源更多一點,嘗試一下為何不可?

8.2 WPF + Blazor 支援哪些作業系統

最低支援 Windows 7 SP1 吧,有群友已經嘗試在 Windows 7 正常執行成功,這是本文範例 Click Once 安裝頁面:https://dotnet9.com/WPFBlazorChat

8.3 Blazor 混合開發還支援哪些既有框架?

Blazor 混合開發 的話,除了 WPF,還有 MAUI(跨平台框架,支援平台包括 Windows\Mac\Linux\Android\iOS 等)、Winform(同 WPF,只能在 Windows 平台 執行)等,建議閱讀 微軟文件 繼續學習,本文只是個引子:

微軟文件學習Blazor

8.4 Blazor 元件庫除了 Masa.Blazor 還有哪些?

8.5 本文範例程式碼?

文中各小節程式碼、最後的範例程式碼都給出了相應連結,您可返回查看。

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2025/1/26

WPF 藉助自訂 XML 檔案實現國際化

本文詳細介紹了在WPF程式中使用自訂XML檔案實現國際化的方法,包括安裝必備NuGet套件、動態獲取語言清單、動態切換語言、在程式碼和XAML介面中使用翻譯字串等內容,同時提供了原始碼連結,幫助開發者輕鬆實現WPF應用程式的國際化。

繼續閱讀