大致瞭解了Blazor和MAUI之後,嘗試建立一個.NET MAUI Blazor應用。
需要注意的是: 雖然都叫MAUI,但.NET MAUI
與.NET MAUI Blazor
並不相同,MAUI還是以xaml
為主,而MAUI Blazor則是以razor
為主。
這個系列還是以MAUI Blazor
為主,要建立一個MAUI Blazor
應用,需要安裝Visual Studio 2022 17.3 或更高版本
,並在安裝程式上,勾選.NET Multi-platform App UI 開發!最好是升級到最新的.NET 7。
開啟Visual Studio 2022,選擇建立新專案
在搜尋方塊輸入MAUI,選擇.NET MAUI Blazor應用
,點下一步
!
給專案起一個好聽的名字,選擇專案存在的位置,點下一步
!
選擇目標框架,這裡選擇的是.NET 7
,點選建立
。
等待建立專案及其依賴項還原。完成後的目錄結構如下:
.NET MAUI Blazor 執行在WebView2
上,WebView2
是微軟推出的新一代用於桌面端混合開發的解決方案。它可以讓本地應用程式(WinForm、WPF、WinUI、Win32)、移動應用程式(MAUI)輕鬆嵌入Web技術。WebView2 控制元件使用 Microsoft Edge 作為呈現引擎在使用者端應用程式及App中顯示 Web 內容。使用 WebView2 可以將 Web 程式碼嵌入到使用者端應用程式及App中的不同部分,或在單個 WebView 範例中構建所有本機應用程式。
可以這麼看MAUI Blazor, .NET MAUI 包含 BlazorWebView 控制元件,該控制元件執行將 Razor 元件呈現到嵌入式 Web View 中。 通過結合使用 .NET MAUI 和 Blazor,可以跨移動裝置、桌面裝置和 Web 重複使用一組 Web UI 元件。
說人話就是,它就是一個Hybrid App(混合應用) !
在windows上偵錯 MAUI Blazor應用,需要Windows 10 1809及更高版本上,並開啟開發者模式。
windows 11上,位於設定
->隱私和安全性
->開發者選項
->開發人員模式
點選Windows Machine
,執行程式!
如無意外,執行成功!
這時,MAUI Blazor使用的是bootstrap樣式以及open-iconic圖示。
在wwwroot/index.html
中也可以看到
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
現在已經有個很多基於Blazor的元件庫,所以暫時把預設的bootstrap替換成第三方元件庫,這裡使用的是AntDesignBlazor
。
PM> NuGet\Install-Package AntDesign.ProLayout -Version 0.13.1
在MauiProgram.cs
注入AntDesign
服務與設定基本設定,完整的MauiProgram.cs
程式碼
using Microsoft.Extensions.Logging;
using MauiBlazorApp.Data;
namespace MauiBlazorApp;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
builder.Services.AddMauiBlazorWebView();
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
builder.Logging.AddDebug();
#endif
builder.Services.AddSingleton<WeatherForecastService>();
//注入AntDesign
builder.Services.AddAntDesign();
//基本設定
builder.Services.Configure<ProSettings>(settings =>
{
settings.NavTheme = "light";
settings.Layout = "side";
settings.ContentWidth = "Fluid";
settings.FixedHeader = false;
settings.FixSiderbar = true;
settings.Title = "DotNet寶藏庫";
settings.PrimaryColor = "daybreak";
settings.ColorWeak = false;
settings.SplitMenus= false;
settings.HeaderRender= true;
settings.FooterRender= false;
settings.MenuRender= true;
settings.MenuHeaderRender= true;
settings.HeaderHeight = 48;
});
return builder.Build();
}
}
設定項都寫上了。引數含義從表達的意思就能看出來,不做註釋了!
開啟wwwroot/index.html
。由於我們使用的是AntDesign
,所以需要改造下index.html
,修改後內容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>DotNet寶藏庫</title>
<base href="/" />
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<link rel="stylesheet" href="_content/AntDesign.ProLayout/css/ant-design-pro-layout-blazor.css" />
</head>
<body>
<div class="status-bar-safe-area"></div>
<div id="app">
<style>
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}
#app {
background-repeat: no-repeat;
background-size: 100% auto;
}
.page-loading-warp {
padding: 98px;
display: flex;
justify-content: center;
align-items: center;
}
.ant-spin {
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5;
list-style: none;
-webkit-font-feature-settings: 'tnum';
font-feature-settings: 'tnum';
position: absolute;
display: none;
color: #1890ff;
text-align: center;
vertical-align: middle;
opacity: 0;
-webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86), -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
}
.ant-spin-spinning {
position: static;
display: inline-block;
opacity: 1;
}
.ant-spin-dot {
position: relative;
display: inline-block;
font-size: 20px;
width: 20px;
height: 20px;
}
.ant-spin-dot-item {
position: absolute;
display: block;
width: 9px;
height: 9px;
background-color: #1890ff;
border-radius: 100%;
-webkit-transform: scale(0.75);
-ms-transform: scale(0.75);
transform: scale(0.75);
-webkit-transform-origin: 50% 50%;
-ms-transform-origin: 50% 50%;
transform-origin: 50% 50%;
opacity: 0.3;
-webkit-animation: antSpinMove 1s infinite linear alternate;
animation: antSpinMove 1s infinite linear alternate;
}
.ant-spin-dot-item:nth-child(1) {
top: 0;
left: 0;
}
.ant-spin-dot-item:nth-child(2) {
top: 0;
right: 0;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.ant-spin-dot-item:nth-child(3) {
right: 0;
bottom: 0;
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.ant-spin-dot-item:nth-child(4) {
bottom: 0;
left: 0;
-webkit-animation-delay: 1.2s;
animation-delay: 1.2s;
}
.ant-spin-dot-spin {
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
-webkit-animation: antRotate 1.2s infinite linear;
animation: antRotate 1.2s infinite linear;
}
.ant-spin-lg .ant-spin-dot {
font-size: 32px;
width: 32px;
height: 32px;
}
.ant-spin-lg .ant-spin-dot i {
width: 14px;
height: 14px;
}
.status-bar-safe-area {
display: none;
}
@supports (-webkit-touch-callout: none) {
.status-bar-safe-area {
display: flex;
position: sticky;
top: 0;
height: env(safe-area-inset-top);
background-color: #f7f7f7;
width: 100%;
z-index: 1;
}
.flex-column, .navbar-brand {
padding-left: env(safe-area-inset-left);
}
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.ant-spin-blur {
background: #fff;
opacity: 0.5;
}
}
@-webkit-keyframes antSpinMove {
to {
opacity: 1;
}
}
@keyframes antSpinMove {
to {
opacity: 1;
}
}
@-webkit-keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
</style>
<div style="
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
min-height: 420px;
height: 100%;
">
<div class="page-loading-warp">
<div class="ant-spin ant-spin-lg ant-spin-spinning">
<span class="ant-spin-dot ant-spin-dot-spin">
<i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i>
</span>
</div>
</div>
<div style="display: flex; justify-content: center; align-items: center;">
<div class="loading-progress-text"></div>
</div>
</div>
</div>
<script src="_framework/blazor.webview.js" autostart="false"></script>
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>
</body>
</html>
在_Imports.razor
新增AntDesign
名稱空間:
@using System.Net.Http
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using MauiBlazorApp
@using MauiBlazorApp.Shared
//引入AntDesign
@using AntDesign
在Main.razor
中加入<AntContainer />
<Router AppAssembly="@typeof(Main).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
<!--設定容器-->
<AntContainer />
MainLayout.razor
。MainLayout.razor
中預設佈局刪除AntDesign.ProLayout
AntDesign.ProLayout
images
,把提前準備好的logo放進去完整程式碼如下:
@using AntDesign.ProLayout
@inherits LayoutComponentBase
<AntDesign.ProLayout.BasicLayout
Logo="@("images/logo.png")"
MenuData="MenuData">
<ChildContent>
@Body
</ChildContent>
<FooterRender>
<FooterView Copyright="MauiBlazorApp" Links="Links"></FooterView>
</FooterRender>
</AntDesign.ProLayout.BasicLayout>
<SettingDrawer />
@code
{
private readonly MenuDataItem[] MenuData =
{
new MenuDataItem
{
Path = "/",
Name = "Home",
Key = "Home",
Icon = "home"
},
new MenuDataItem
{
Path = "/Counter",
Name = "Counter",
Key = "Counter",
Icon = "plus"
},
new MenuDataItem
{
Path = "/FetchData",
Name = "FetchData",
Key = "FetchData",
Icon = "cloud"
}
};
private readonly LinkItem[] Links =
{
new LinkItem
{
Key = "DotNet寶藏庫",
Title = "基於Ant Design Blazor",
Href = "https://antblazor.com",
BlankTarget = true
}
};
}
這時可以把專案中無用的內容刪除掉了,如Shared/NavMenu.razor
、wwwroot/css
檔案。
由於刪除掉了css
資料夾,頁面元素肯定沒有樣式了。那麼就簡單的改造下預設的幾個頁面!
開啟Pages/Index.razor
,將演示元件SurveyPrompt
刪掉。順便把Shared/SurveyPrompt.razor
也刪除掉。將<h1>Hello, world!</h1>
替換為Ant Design
元件。
@page "/"
<Title Level="1">Hello,DotNet寶藏庫</Title>
<br />
<Text Type="success">歡迎關注我的公眾號!</Text>
開啟 Pages/Counter.razor
,將程式碼改為如下:
@page "/counter"
<Title Level="2">HCounter</Title>
<Divider />
<p role="status">Current count: @currentCount</p>
<Button @onclick="IncrementCount" Type="primary">AntDesign 按鈕</Button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
開啟Pages/FetchData.razor
,將資料表格替換為Ant Design
,刪除頁面所有程式碼,替換為Ant Design的範例!
@page "/fetchdata"
@using System.ComponentModel
@using AntDesign.TableModels
@using System.Text.Json
@using MauiBlazorApp.Data
@inject WeatherForecastService ForecastService
<Table @ref="table"
TItem="WeatherForecast"
DataSource="@forecasts"
Total="_total"
@bind-PageIndex="_pageIndex"
@bind-PageSize="_pageSize"
@bind-SelectedRows="selectedRows"
OnChange="OnChange">
<Selection Key="@(context.Id.ToString())" />
<PropertyColumn Property="c=>c.Id" Sortable />
<PropertyColumn Property="c=>c.Date" Format="yyyy-MM-dd" Sortable />
<PropertyColumn Property="c=>c.TemperatureC" Sortable />
<PropertyColumn Title="Temp. (F)" Property="c=>c.TemperatureF" />
<PropertyColumn Title="Hot" Property="c=>c.Hot">
<Switch @bind-Value="@context.Hot"></Switch>
</PropertyColumn>
<PropertyColumn Property="c=>c.Summary" Sortable />
<ActionColumn>
<Space>
<SpaceItem><Button Danger OnClick="()=>Delete(context.Id)">Delete</Button></SpaceItem>
</Space>
</ActionColumn>
</Table>
<br />
<p>PageIndex: @_pageIndex | PageSize: @_pageSize | Total: @_total</p>
<br />
<h5>selections:</h5>
@if (selectedRows != null && selectedRows.Any())
{
<Button Danger Size="small" OnClick="@(e => { selectedRows = null; })">Clear</Button>
@foreach (var selected in selectedRows)
{
<Tag @key="selected.Id" Closable OnClose="e=>RemoveSelection(selected.Id)">@selected.Id - @selected.Summary</Tag>
}
}
<Button Type="@ButtonType.Primary" OnClick="()=> { _pageIndex--; }">Previous page</Button>
<Button Type="@ButtonType.Primary" OnClick="()=> { _pageIndex++; }">Next Page</Button>
@code {
private WeatherForecast[] forecasts;
IEnumerable<WeatherForecast> selectedRows;
ITable table;
int _pageIndex = 1;
int _pageSize = 10;
int _total = 0;
protected override async Task OnInitializedAsync()
{
forecasts = await GetForecastAsync(1, 50);
_total = 50;
}
public class WeatherForecast
{
public int Id { get; set; }
[DisplayName("Date")]
public DateTime? Date { get; set; }
[DisplayName("Temp. (C)")]
public int TemperatureC { get; set; }
[DisplayName("Summary")]
public string Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public bool Hot { get; set; }
}
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public void OnChange(QueryModel<WeatherForecast> queryModel)
{
Console.WriteLine(JsonSerializer.Serialize(queryModel));
}
public Task<WeatherForecast[]> GetForecastAsync(int pageIndex, int pageSize)
{
var rng = new Random();
return Task.FromResult(Enumerable.Range((pageIndex - 1) * pageSize + 1, pageSize).Select(index =>
{
var temperatureC = rng.Next(-20, 55);
return new WeatherForecast
{
Id = index,
Date = DateTime.Now.AddDays(index),
TemperatureC = temperatureC,
Summary = Summaries[rng.Next(Summaries.Length)],
Hot = temperatureC > 30,
};
}).ToArray());
}
public void RemoveSelection(int id)
{
var selected = selectedRows.Where(x => x.Id != id);
selectedRows = selected;
}
private void Delete(int id)
{
forecasts = forecasts.Where(x => x.Id != id).ToArray();
_total = forecasts.Length;
}
}
暫無,下次再會