After a general understanding of Blazor and MAUI, try to create a .NET MAUI Blazor application.
It should be noted that although they are both called MAUI, they are .NET MAUI
not .NET MAUI Blazor
the same as .xaml
razor
This series is still MAUI Blazor
based on the idea that to create an MAUI Blazor
application, it needs to be installed Visual Studio 2022 17.3 或更高版本
, and on the installer, check .NET Multi-platform App UI development! It would be best to upgrade to the latest .NET 7.
Table of contents
Create a .NET MAUI Blazor application
Open Visual Studio 2022, select创建新项目
Enter MAUI in the search box, select .NET MAUI Blazor应用
, click 下一步
!
Give the project a nice name, select the location where the project exists, click 下一步
!
Select the target framework, which is selected here .NET 7
, click 创建
.
Wait for the project to be created and its dependencies restored. The completed directory structure is as follows:
.NET MAUI Blazor requires attention
.NET MAUI Blazor runs on WebView2
, WebView2
and is a new generation solution for desktop hybrid development launched by Microsoft. It allows native applications (WinForm, WPF, WinUI, Win32), mobile applications (MAUI) to easily embed web technologies. The WebView2 control uses Microsoft Edge as the rendering engine to display web content in client applications and apps. Use WebView2 to embed web code into different parts of client applications and Apps, or build all native applications in a single WebView instance.
Looking at MAUI Blazor this way, .NET MAUI includes the BlazorWebView control, which enables rendering of Razor components into embedded Web Views. By using .NET MAUI with Blazor, you can reuse a set of web UI components across mobile, desktop, and the web.
In other words, it is a Hybrid App (mixed application)!
Debugging .NET MAUI Blazor
To debug MAUI Blazor applications on Windows, Windows 10 1809 and later versions are required, and developer mode must be turned on.
On windows 11, located at 设置
-> 隐私和安全性
-> 开发者选项
-> 开发人员模式
Click Windows Machine
to run the program!
If there is no accident, the operation is successful!
At this time, MAUI Blazor uses the bootstrap style and open-iconic icon. can also be seen
inwwwroot/index.html
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
There are already many Blazor-based component libraries, so temporarily replace the default bootstrap with a third-party component library, which is used here AntDesignBlazor
.
Use AntDesignBlazor component library
Installation dependencies:
PM> NuGet\Install-Package AntDesign.ProLayout -Version 0.13.1
Inject AntDesign
Inject MauiProgram.cs
the AntDesign
service and set the basic configuration, the complete MauiProgram.cs
code
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();
}
}
The configuration items are all written. The meaning of the parameters can be seen from the meaning of the expression, no comments!
import style
open wwwroot/index.html
. Since we are using AntDesign
, we need to modify it index.html
. The modified content is as follows:
<!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>
join namespace
After _Imports.razor
adding AntDesign
the namespace:
@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
set container
Main.razor
join in<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 />
Modify MainLayout
- revise
MainLayout.razor
. - Delete
MainLayout.razor
the default layout in - introduce
AntDesign.ProLayout
- Set the layout to
AntDesign.ProLayout
- Construction menu, links in footer, copyright
- Create a new folder under the wwwroot directory
images
and put the prepared logo in it
The complete code is as follows:
@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
}
};
}
At this time, useless content in the project can be deleted, such Shared/NavMenu.razor
as wwwroot/css
. Since the folder
was deleted , the page elements must have no styles. css
Then simply transform the default pages!
Transform the default page
index.razor
Open Pages/Index.razor
and delete the demo component SurveyPrompt
. Shared/SurveyPrompt.razor
Also delete it by the way . Replace <h1>Hello, world!</h1>
with Ant Design
component.
@page "/"
<Title Level="1">Hello,DotNet宝藏库</Title>
<br />
<Text Type="success">欢迎关注我的公众号!</Text>
Counter.razor
Open it Pages/Counter.razor
and change the code to the following:
@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++;
}
}
FetchData.razor
Open Pages/FetchData.razor
, replace the data table with Ant Design
, delete all the code on the page, and replace it with the example of 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;
}
}
running result:
Summarize
No, see you next time
Click on the official account card below to follow me! Learn together and progress together!