家好,我是沙漠盡頭的狼。
.NET是免費(fèi),跨平臺,開源,用于構(gòu)建所有應(yīng)用的開發(fā)人員平臺。
本文演示如何在WPF中使用Blazor開發(fā)漂亮的UI,為客戶端開發(fā)注入新活力。
注 要使WPF支持Blazor,.NET版本必須是 6.0 或更高版本,本文所有示例使用的.NET 7.0,版本要求見鏈接,截圖看如下文字:
本文從創(chuàng)建WPF Hello World開發(fā):
使用WPF模板創(chuàng)建一個默認(rèn)程序,取名【W(wǎng)PFBlazorChat】,項(xiàng)目組織結(jié)構(gòu)如下:
運(yùn)行項(xiàng)目,一個空白窗口:
接著往下看,我們添加Blazor支持,本小節(jié)代碼在這WPF默認(rèn)程序源碼。
依然使用上面的工程,添加Blazor支持,此部分參考微軟文檔生成 Windows Presentation Foundation (WPF) Blazor 應(yīng)用,本小節(jié)快速略過。
雙擊工程文件WPFBlazorChat.csproj,修改處如下:
_Imports.razor文件類似一個Global using文件,專門給Razor組件使用,放置一些用的比較多的全局的命名空間,精簡代碼。
內(nèi)容如下,引入了一個命名空間Microsoft.AspNetCore.Components.Web,這是Razor常用命名空間,包含用于向 Blazor 框架提供有關(guān)瀏覽器事件的信息的類型。:
@using Microsoft.AspNetCore.Components.Web
和Vue、React一樣,需要一個html文件承載Razor組件,頁面內(nèi)容類似:
<!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>
頁面的基本樣式,通用的樣式可放在這個文件:
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;}
加一個Razor的經(jīng)典組件Counter.razor,Blazor的Hello World程序就有這么一個組件,文件路徑:/RazorViews/Counter.razor,之所以放RazorViews目錄,是為了和WPF常用的Views目錄區(qū)分,該組件內(nèi)容如下:
<h1>Counter</h1> <p>好開心,你點(diǎn)我了,現(xiàn)在是:<span style="color: red;">@currentCount</span></p> <button class="btn btn-primary" @onclick="IncrementCount">快快點(diǎn)我</button> @code { private int currentCount=0; private void IncrementCount() { currentCount++; }}
一個按鈕【快快點(diǎn)我】,點(diǎn)擊@onclick="IncrementCount"使變量currentCount自增,同時(shí)頁面顯示此變量值,相信你能看懂。
這是兩者產(chǎn)生關(guān)系的關(guān)鍵一步,打開窗體MainWindow.xaml,修改如下:
如上代碼,要點(diǎn)如下:
打開MainWindow.xaml.cs,修改如下:
在WPF里可以使用Prism等框架提供的Unity、DryIoc等Ioc容器實(shí)現(xiàn)視圖與服務(wù)的注入;Razor組件這里,默認(rèn)使用ASP.NET Core的IServiceCollection容器;如果WPF窗體與Razor組件需要共享數(shù)據(jù),可以通過后面要說的Messager發(fā)送消息,也可以通過Ioc容器注入的方式實(shí)現(xiàn),比如從WPF窗體中注入的數(shù)據(jù)(通過MainWindow構(gòu)造函數(shù)注入),通過IServiceCollection容器再注入Razor組件使用,這里后面也有提到。
上面步驟做完后,運(yùn)行程序:
OK,WPF與Blazor集成成功,打完收工?
等等,還沒完呢,本小節(jié)源碼在這WPF中添加Blazor,接著往下看。
看上圖,窗體邊框是WPF默認(rèn)的樣式,有時(shí)會感覺比較丑,或者不丑,設(shè)計(jì)師有其他的窗體風(fēng)格設(shè)計(jì),往往我們要自定義窗體,本節(jié)分享部分WPF與Blazor的自定義窗體實(shí)現(xiàn),更多定制化功能可能需要您自行研究。
一般實(shí)現(xiàn)是設(shè)置窗體的三個屬性WindowStyle="None" AllowsTransparency="True" Background="Transparent",即可隱藏默認(rèn)窗體的邊框,然后在內(nèi)容區(qū)自己畫標(biāo)題欄、最小化、最大化、關(guān)閉按鈕、客戶區(qū)等。
MainWindow.xaml:隱藏WPF默認(rèn)窗體邊框
<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默認(rèn)窗體的邊框,運(yùn)行程序如下:
看上圖,點(diǎn)擊窗體中的按鈕(其實(shí)是Razor組件的按鈕),但未執(zhí)行按鈕點(diǎn)擊事件,且窗體消失了,這是怎么回事?您可以嘗試研究下為什么,我沒有研究個所以然來,暫時(shí)加個背景處理BlazorWebView穿透的問題。
簡單的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="這里是窗體標(biāo)題欄,左側(cè)可放Logo、標(biāo)題,右側(cè)放窗體操作按鈕:最小化、最大化、關(guān)閉等" /> <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>
我們給整個窗體客戶端區(qū)域加了一個背景Border(您可以去掉Border背景色,點(diǎn)擊界面按鈕試試),然后又套了一個Grid,用于放置自定義的標(biāo)題欄(標(biāo)題和窗體控制按鈕)和BlazorWebView(用于渲染Razor組件的瀏覽器組件),下面是窗體控制按鈕的響應(yīng)事件:
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; }}
代碼簡單,處理了窗體最小化、窗體最大化(還原)、關(guān)閉、標(biāo)題欄雙擊窗體最大化(還原),上面的實(shí)現(xiàn)不是一個完美的自定義窗體實(shí)現(xiàn),至少有這兩個問題:
在后面的3.4小節(jié),站長使用一個第三庫實(shí)現(xiàn)了窗體圓角問題,更多比較好的WPF自定義窗體實(shí)現(xiàn)可看這篇文章:WPF三種自定義窗體的實(shí)現(xiàn),本小節(jié)中示例源碼在這WPF自定義窗體。
異形窗體的需求,使用WPF實(shí)現(xiàn)是比較方便的,本來打算寫寫的,感覺偏離主題太遠(yuǎn)了,給篇文章自行看看吧:WPF異形窗體演示,文中異形窗體效果如下:
下面介紹將窗體的標(biāo)題欄也放Razor組件中實(shí)現(xiàn)的方式。
上面使用了WPF制作自定義窗體,有沒有這種需求,把菜單放置到標(biāo)題欄?這個簡單,WPF能很好實(shí)現(xiàn)。
如果放Tab類控件呢?Tab Header是在標(biāo)題欄顯示,TabItem是在客戶端區(qū)域,Tab Header與TabItem風(fēng)格統(tǒng)一,在一套代碼里面實(shí)現(xiàn)和維護(hù)也方便,那么在WPF+Blazor混合開發(fā)的情況怎么實(shí)現(xiàn)呢?相信通過本節(jié)Razor組件實(shí)現(xiàn)標(biāo)題欄的介紹,你能做出來。
MainWindow.xaml恢復(fù)代碼,只設(shè)置隱藏WPF默認(rèn)窗體邊框,并給BlazorWebView套一層背景:
后面的代碼有參考 BlazorDesktopWPF-CustomTitleBar 開源項(xiàng)目實(shí)現(xiàn)。
我們把標(biāo)題欄做到Counter.razor組件,即標(biāo)題欄、客戶區(qū)放一個組件里,當(dāng)然你也可以分離,這里我們方便演示:
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"> 測試窗體標(biāo)題 </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>好開心,你點(diǎn)我了,現(xiàn)在是:<span style="color: red;">@currentCount</span></p> <button class="btn btn-primary" @onclick="IncrementCount">快快點(diǎn)我</button> @code { private int currentCount=0; protected override void OnInitialized() { WindowService.Init(); base.OnInitialized(); } private void IncrementCount() { currentCount++; }}
下面給出代碼簡單說明:
運(yùn)行效果如下:
實(shí)現(xiàn)這個效果,還有一些代碼:
窗體拖動
首先添加Nuget包Simplify.Windows.Forms,用于獲取鼠標(biāo)光標(biāo)的位置:
<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); }}
上面的代碼用于窗體的最小化、最大化(還原)、關(guān)閉等實(shí)現(xiàn),需要在Razor組件里正確的調(diào)用這些方法:
上面效果的樣式文件修改如下,wwwroot\css\app.css:
/*BlazorDesktopWPF-CustomTitleBar - ? Copyright 2021 - Jam-Es.comLicensed 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;}
上面的一些代碼即實(shí)現(xiàn)了由Razor組件實(shí)現(xiàn)窗體的標(biāo)題顯示、窗體的最小化、最大化(還原)、關(guān)閉、移動等操作,然而還是會有3.1結(jié)尾出現(xiàn)的問題,即窗體圓角和窗體最大化鋪滿操作系統(tǒng)桌面任務(wù)欄的問題,下面一小節(jié)我們嘗試解決他。
小節(jié)總結(jié):通過上面的代碼,如果放Tab控件鋪滿整個窗體,是不是有思路了?
本小節(jié)源碼在這Razor組件實(shí)現(xiàn)窗體標(biāo)題欄功能
其實(shí)上面的代碼可以當(dāng)做學(xué)習(xí),即使有不小瑕疵(哈哈),本小節(jié)我們還是使用第三包解決窗體圓角和最大化問題。
首先添加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>
就上面三處修改,我們運(yùn)行看看:
是不是和3.3效果一樣?其實(shí)仔細(xì)看,窗體下面的圓角也有了:
最終還是WPF解決了所有問題...
具體怎么實(shí)現(xiàn)的窗體最大化未占操作系統(tǒng)的任務(wù)欄,以及窗體圓角問題的解決(竟然能讓BlazorWebView部分透明了)可以查看該組件相關(guān)代碼,本文不過多深究。
另外,WPF熟手可能比較清楚,前面的代碼還不能正常的拖動改變窗體大小(不知道你發(fā)現(xiàn)沒,我當(dāng)你沒發(fā)現(xiàn)。),使用該庫后也解決了:
本小節(jié)源碼在這解決圓角和最大化問題,下面開始本文的下半部分了,好累,終于到這了。
工欲善其事,必先利其器!
鑒于大部分同學(xué)前端基礎(chǔ)可能不是太好,即使使用Blazor可以少用或者不用JavaScript,但有那么一款漂亮、便捷的Blazor組件庫,這不是如虎添翼嗎?本文使用Masa Blazor做示例展示,如今Blazor組件庫眾多,選擇自己喜歡的、順手的就成:
站長前些日子介紹過MAUI使用Masa blazor組件庫一文,本小節(jié)思路也是類似,且看我表演。
打開Masa Blazor文檔站點(diǎn):https://blazor.masastack.com/getting-started/installation,一起來往WPF中引入這款Blazor組件庫吧。
打開工程文件WPFBlazorChat.csproj直接復(fù)制下面的包版本,或通過NuGet包管理器搜索Masa.Blazor安裝:
<PackageReference Include="Masa.Blazor" Version="0.6.0" />
打開wwwroot\index.html,在<head></head>節(jié)點(diǎn)添加如下資源:
<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>
打開_Imports.razor文件,修改如下:
@using Microsoft.AspNetCore.Components.Web@using Masa.Blazor@using BlazorComponent
打開MainWindow.xaml.cs,添加一行代碼 serviceCollection.AddMasaBlazor();
上面4步的準(zhǔn)備工作做好后,我們簡單來使用下Masa.Blazor組件。
打開Tab組件鏈接:https://blazor.masastack.com/components/tabs,嘗試這個Demo:
Demo的代碼我?guī)缀醪蛔兊囊耄蜷_RazorViews\Counter.razor文件,保留3.4節(jié)的標(biāo)題欄,替換了客戶區(qū)域內(nèi)容,代碼如下:
@using WPFBlazorChat.Services <MApp> <!--上一小節(jié)的標(biāo)題欄開始--> <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"> 測試窗體標(biāo)題 </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> <!--上一小節(jié)的標(biāo)題欄結(jié)束--> <!--新增的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案例代碼結(jié)束--></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(); }}
運(yùn)行效果如下:
是不是有那味兒了?再嘗試把Tab移到標(biāo)題欄,前面有提過的效果:
上面的效果,代碼修改如下,刪除了原標(biāo)題欄代碼,將窗體操作按鈕放到了MToolbar里面,并使用MToolbar添加了雙擊事件、鼠標(biāo)按下、釋放事件實(shí)現(xiàn)窗體拖動:
<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案例代碼結(jié)束--></MApp>
窗體操作按鈕的背景色也做部分修改:
其實(shí)上面的窗體效果還是有點(diǎn)瑕疵,注意到窗體右側(cè)的豎直滾動條了嗎?在沒引入Masa.Blazor之前,右側(cè)正常顯示,引入后多了一個豎直滾動條:
這個想去掉也簡單,在wwwroot\css\app.css追加樣式(當(dāng)時(shí)也是折騰了好一會兒,最后在Masa.Blazor群里群友給出了解決方案,十分感謝):
問題解決css代碼:
::-webkit-scrollbar { width: 0px;}
因?yàn)?/span>Razor組件是在BlazorWebView里渲染的,即BlazorWebView就是個小型的瀏覽器呀,上面的樣式即把瀏覽器的滾動條寬度設(shè)置為0,它不就沒有了嗎?現(xiàn)在效果如下,是不是舒服了?
添加Masa.Blazor就介紹到這里,本小節(jié)示例代碼在這里WPF中使用Masa.Blazor,下面講解WPF與Blazor混合開發(fā)后多窗體消息通知問題。
一般C/S窗體之間通信使用委托、事件,而在WPF開發(fā)中,可以使用一些框架提供的抽象事件訂閱\發(fā)布組件,比如Prism的事件聚集器IEventAggregator,或MvvmLight的Messager。在B/S開發(fā)中,進(jìn)程內(nèi)事件通知可能就使用MediatR組件居多了,不論是在C/S還是B/S開發(fā),這些組件在一定程度上,各大程序模板可以通用的,更不用說分布式的消息隊(duì)列RabbitMQ 和 Kafka是萬能的進(jìn)程間通信標(biāo)準(zhǔn)選擇了。
上面是一些套話,站長根據(jù)Prism的事件聚集器和MvvmLight的Messager源碼閱讀,簡單封裝了一個Messager,可以適用于一般的業(yè)務(wù)需求。
本來不想貼代碼直接給源碼鏈接的,想想代碼也不多,直接上吧。
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
消息接口,只定義了三個接口:
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
消息的管理,消息中轉(zhuǎn)等實(shí)現(xiàn):
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); } }}
有興趣的看上面的代碼,封裝代碼上面簡單全部給上,后面的消息通知都是基于上面的三個類實(shí)現(xiàn)的,比較核心。
第 5 節(jié)涉及到多窗體及多Razor組件了,需要創(chuàng)建一些目錄存放這些文件,方便分類管理。
先看本示例效果,再給出相關(guān)代碼說明:
圖中有三個操作:
三個消息類定義如下:
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傳遞了一個業(yè)務(wù)Number屬性,另兩個消息只是起到通知作用(所以沒有額外屬性定義),實(shí)際開發(fā)時(shí)可能需要傳遞業(yè)務(wù)數(shù)據(jù)。
即上面的第一個操作:點(diǎn)擊主窗體A的【+】按鈕,發(fā)送了OpenSecondViewMessage消息,打開子窗體B。
在RazorViews\MainView.razor中執(zhí)行按鈕點(diǎn)擊,發(fā)送打開子窗體消息:
...<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() { // 訂閱打開子窗口消息,在主窗口點(diǎn)擊【+】按鈕 Messenger.Default.Subscribe<OpenSecondViewMessage>(this, msg=> { var chatWin=new SecondWindowView(); chatWin.Show(); }, ThreadOption.UiThread); }}
實(shí)際開發(fā)可能情況更復(fù)雜,發(fā)送的消息OpenSecondViewMessage里帶WPF窗體路由(定義的一套路徑規(guī)則尋找窗體或ViewModel),訂閱的地方也可能不在主程序,在子模塊的Module類里。
即第二個操作:打開子窗體B后,再點(diǎn)擊主窗體A的【桃心】按鈕,發(fā)送了SendRandomDataMessage消息,子窗體B的第二個TabItem Header顯示了消息傳來的數(shù)字。
...<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));}...}
@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() { // 訂閱業(yè)務(wù)消息,在主窗口點(diǎn)擊桃心按鈕時(shí)觸發(fā) Messenger.Default.Subscribe<SendRandomDataMessage>(this, msg=> { this.InvokeAsync(()=> { this.tagCount=msg.Number; }); this.StateHasChanged(); }, ThreadOption.UiThread); } void ReponseMessage() { // 通知主窗體,我已經(jīng)收到消息,請不要再發(fā) Messenger.Default.Publish(this, new ReceivedResponseMessage(this)); }}
注意看,上面收到消息時(shí)有兩個方法要簡單說一下,看OnInitialized()里的代碼:
上面的代碼把子窗體消息回應(yīng)也貼上了,即點(diǎn)擊安卓圖標(biāo)按鈕時(shí)發(fā)送了ReceivedResponseMessage消息,在主窗體RazorViews\MainView.razor里也訂閱了這個消息,和上面的代碼類似:
... <!--確認(rèn)對話框開始--> <PConfirm Visible="_showComfirmDialog" Title="子窗體來回應(yīng)了" Type="AlertTypes.Warning" OnCancel="()=> _showComfirmDialog=false" OnOk="()=> _showComfirmDialog=false"> 說你別沒事一直發(fā),它們煩! </PConfirm> <!--確認(rèn)對話框結(jié)束--></MApp> @code{... // 是否顯示確認(rèn)對話框 bool _showComfirmDialog; protected override void OnInitialized() { WindowService.Init(); // 訂閱子窗體響應(yīng)的消息,它已經(jīng)收到消息了,我可以休息下再發(fā) Messenger.Default.Subscribe<ReceivedResponseMessage>(this, msg=> { this.InvokeAsync(()=> { _showComfirmDialog=true; }); this.StateHasChanged(); }, ThreadOption.UiThread); base.OnInitialized(); }...}
在OnInitialized()方法里訂閱消息ReceivedResponseMessage,收到后將變量_showComfirmDialog置為true,即上面對話框的屬性Visible綁定的值,同理需要在InvokeAsync()中處理數(shù)據(jù)接收,也需要調(diào)用StateHasChanged通知UI數(shù)據(jù)變化。
上面說了部分代碼,可能講的不太清楚,可以看本節(jié)示例源碼:多窗體消息通知。
本來想寫完整Demo說明的,發(fā)現(xiàn)上面把基本要點(diǎn)都拉了一遍,再粘貼一些重復(fù)代碼有點(diǎn)沒完沒了了,有興趣的拉源碼WPF與Blazor混合開發(fā)Demo查看、運(yùn)行,下面是項(xiàng)目代碼結(jié)構(gòu):
下面是最后的示例效果圖,前面部分文章已經(jīng)發(fā)過,再發(fā)一次,哈哈:
用戶列表窗口
打開子窗口
聊天窗口
演示發(fā)送消息
上一篇文章鏈接:快速創(chuàng)建軟件安裝包-ClickOnce,本文示例Click Once安裝頁面:https://dotnet9.com/WPFBlazorChat
WPF雖然相較Winform做出比較好看的UI相對容易一些,但比起Blazor,或者直接說html開發(fā)界面,還是差了一點(diǎn)點(diǎn),更何況html的資源更多一點(diǎn),嘗試一下為何不可?
最低支持Windows 7 SP1吧,有群友已經(jīng)嘗試在Windows 7正常運(yùn)行成功,這是本文示例Click Once安裝頁面:https://dotnet9.com/WPFBlazorChat
Blazor混合開發(fā)的話,除了WPF,還有MAUI(跨平臺框架,支持平臺包括Windows\Mac\Linux\Android\iOS等)、Winform(同WPF,只能在Windows平臺運(yùn)行)等,建議閱讀微軟文檔繼續(xù)學(xué)習(xí),本文只是個引子:
文中各小節(jié)代碼、最后的示例代碼都給出了相應(yīng)鏈接,您可返回查看。
本號已有原創(chuàng)文章320+篇,以軟件工程為綱,DevOps為基,洞察研發(fā)效能全貌,涵蓋從需求管理、應(yīng)用/游戲開發(fā)、軟件測試、發(fā)布部署到運(yùn)營監(jiān)控的完整流程。無論您是項(xiàng)目經(jīng)理、產(chǎn)品經(jīng)理、開發(fā)人員、測試人員,還是運(yùn)維人員,在這里您都可以有所收獲,同時(shí)深入理解其他角色的工作內(nèi)容,共同助力DevOps的成功落地。歡迎關(guān)注,有任何問題可發(fā)送私信~
在.NET的世界里,dotnet
命令是一個極其強(qiáng)大的工具,它不僅僅用于構(gòu)建、運(yùn)行和發(fā)布應(yīng)用程序,還提供了廣泛的內(nèi)置功能,幫助開發(fā)者高效地完成各種任務(wù)。然而,你可能未曾意識到,通過 dotnet tool
命令,dotnet
還相當(dāng)于一個.NET命令行工具的倉庫和包管理器,為開發(fā)者提供了強(qiáng)大的擴(kuò)展能力。這一點(diǎn),在其他編程語言如Java中,是完全沒有類似功能的。
讓我們先來看看 dotnet tool
的基本使用。通過 dotnet tool
,你可以輕松地安裝、升級、卸載各種.NET命令行工具。這些工具可能是微軟官方提供的,也可能是由社區(qū)成員開發(fā)的,它們都有助于提高開發(fā)效率、實(shí)現(xiàn)自動化工作流、簡化部署等。
安裝一個 dotnet
命令行工具非常簡單。你可以使用以下命令來安裝一個名為 dotnetsay
的示例工具:
dotnet tool install -g dotnetsay
這里的 -g
參數(shù)表示全局安裝,意味著這個工具將在系統(tǒng)的任何地方都可使用。如果你只想在當(dāng)前項(xiàng)目中安裝這個工具,可以省略 -g
參數(shù)。
安裝完成后,你就可以在命令行中直接使用這個工具了。可以通過以下方式調(diào)用它:
dotnetsay
要升級一個已安裝的 dotnet
命令行工具,只需運(yùn)行:
dotnet tool update -g dotnetsay
同樣,卸載一個工具也很直接:
dotnet tool uninstall -g dotnetsay
dotnet tool
命令的靈活性和強(qiáng)大之處,在于它為開發(fā)者和用戶提供了一個統(tǒng)一的入口,用于管理和使用各種命令行工具。這種管理方式使得.NET開發(fā)者能夠非常方便地集成和使用各種工具,從而提升工作效率和產(chǎn)品質(zhì)量。
現(xiàn)在,讓我們來看看一些 dotnet tool
可安裝的常用和強(qiáng)大的命令行工具。
docfx:
- 描述:docfx 是一個由微軟開發(fā)的文檔生成工具,用于構(gòu)建 API 文檔、技術(shù)文章等。
- 用途:為開發(fā)者提供了一種簡單的方法來從源代碼中提取注釋,并根據(jù)這些注釋生成結(jié)構(gòu)化的文檔網(wǎng)站。
- 安裝命令:
dotnet tool install -g docfx
2. csys:
- 描述:csys 是一個.NET Core命令行工具,用于系統(tǒng)監(jiān)視和診斷。它提供了如進(jìn)程查看、網(wǎng)絡(luò)監(jiān)視、系統(tǒng)資源使用統(tǒng)計(jì)等功能。
- 用途:幫助開發(fā)者或系統(tǒng)管理員了解當(dāng)前系統(tǒng)的狀態(tài),識別潛在的性能瓶頸或問題。
- 安裝命令:
dotnet tool install -g csys
3. BBDown(嗶哩嗶哩視頻下載):
- 描述:BBDown 是一個用于下載嗶哩嗶哩(B站)視頻和音頻的命令行工具。它支持多種格式的下載,包括視頻、音頻和彈幕。
- 用途:對于希望從嗶哩嗶哩保存內(nèi)容的用戶來說,BBDown 是一個便捷的工具。
- 安裝命令:
dotnet tool install -g BBDown
以上只是 dotnet tool
可安裝工具中的一小部分,實(shí)際上還有更多的工具和庫可供使用。這些工具的存在,使得最終用戶可以根據(jù)自己的需求和喜好,靈活地選擇和使用各種工具。
隨著.NET Core的不斷發(fā)展,.NET的跨平臺能力和生態(tài)系統(tǒng)也在逐漸強(qiáng)大。dotnet tool
作為其中的一部分,通過提供統(tǒng)一的命令行工具管理和使用機(jī)制,使得.NET開發(fā)者能夠更加方便地集成和使用各種工具和庫,進(jìn)一步促進(jìn)了.NET生態(tài)的發(fā)展。同時(shí)它也是面向最終用戶的,能夠用來安裝、使用和管理命令行工具和軟件。