
Here is a summary of some notes I stepped on when developing MAUI applications.
I have to say that MAUI is quite garbage.
If it weren't for Mono Jinyu, it is estimated that not many people in the community would pay attention to the Wafu MAUI.
Currently,. NET has been upgraded to 7.0, but MAUI is still as extensive as ever. If you have developed MAUI, customized, and customized title bars, you will find how uncomfortable MAUI is.
I don't know what MAUI has to do with UWP, but many things in MAUI feel like continuing the design of UWP, and MAUI is likely to be the next UWP.
If you are developing Windows or Linux desktop, I recommend WPF or Avalonia. MAUI really doesn't work well. You can find many APIs under WPF, but there are none in MAUI...
MAUI Windows is developed based on WINUI, but if you follow WINUI to find information, the things you find cannot be used by MAUI...
Here I recommend tauri, an easy-to-use front-end packaging client tool. Tauri is implemented in rust, and can then be combined with other front-end frameworks to develop applications.
Its development model is much better than MAUI Blazor, and the development experience is also very good. The generated software is surprisingly small. Moreover, the generated app comes with an installation interface, so the generated program is already packaged, saving yourself the need to manually repackage it.
Article introduction: https://www.example.com
Compared with other desktop developers, MAUI is really bloated, and MAUI Blazor is really uncomfortable to develop, including the development experience of Visual Studio itself, the use of the framework itself, and the Blazor UI framework. The experience in three aspects is not good enough.
There are few Blazor frameworks that can be used, and there are even fewer UI frameworks that can be easily extended and transformed. Currently, MASA Blazor has been found to be a better one.
Why do you say this? First of all, during the writing process of Blazor, the editor does not support Razor well. There are often no syntax prompts, the code has errors but the editor does not prompt, the editor prompts errors but the code actually has no errors, etc.
Secondly, about the use of Blazor under MAUI and the selection of Blazor framework. If you use Blazor under MAUI, if you use a third-party UI framework, you will find that it is naturally closed after introduction.
If you use a pure front-end framework for development, you will find that the dependency and reference relationships are very clear, and the compiler will prompt you what packages you need to reference and will prompt you during compilation.
As for the Blazor framework, it is difficult to know which js are used in it. The Blazor dll contains js and other files, which is a kind of closed nature in itself, and the internal situation is even more difficult to understand, making it difficult to debug bugs.
Moreover, the code encapsulated by the Blazor framework is written by C#+ js. Since the C#code cannot be modified after compilation, it is difficult to view the debugging source code when there is a problem with the referenced Blazor library. In addition, from the current Blazor frameworks, I have seen that the code of many frameworks itself is very bloated, and the design and logic inside are not clear. The code in many places limits the extension of components, making it difficult for developers to replace the implementation inside.
Over-design is also a problem, because support for support's sake, for flexibility and flexibility, too much API design and too much parameter presentation, and too much logic encapsulation will actually make these components more difficult to use.
Most Blazor frameworks are maintained by personal repositories. Almost all of the high-quality front-end frameworks on the market are endorsed by large companies, such as ViewJS, Ant Design, etc. The framework itself is maintained by a professional team and the big shots design the framework to jointly maintain a high-quality product.
But the current Blazor, I think, apart from what MASA makes, it is difficult to qualify as a "high-quality".
I also have reasons to praise MASA.
MASA is really devoted to ecology and has attracted many developers and fans to actively participate. Its open source sharing spirit is admirable.
If you have a question about Blazor MAUI development, even if you are not using the MASA framework, you can still go to the MASA public to ask questions. There will be no paid answers to questions, no one will laugh at you for your lack of quality, and no one will laugh at you. I don't understand this. Of course, I am not saying that there is a problem with paid answers for open source projects. I just praise MASA's open source spirit.
Official website: https://www.example.com
I look forward to the MASA team making a masterpiece.
But for now, MAUI + Blazor desktop development has no advantages... It will also bring many problems...
If possible, I don't want to touch MAUI again.
Here are some knowledge points about MAUI.
window
首先,创建项目后, APP.cs 中,有个 Microsoft.Maui.Controls.Window。

MauiProgram.cs 中,有个 Microsoft.UI.Xaml.Window ,然后在 Windows 下 Microsoft.UI.Xaml.Window 是 Microsoft.Maui.MauiWinUIWindow, Microsoft.UI.Xaml.Window 多种平台统一的抽象。

然后 Microsoft.UI.Xaml.Window 可以获取一个 AppWindow。
AppWindow appWindow = nativeWindow.GetAppWindow()!;
The Window class APIs in MAUI are very confusing. Most of them are inherited from UWP. There are many APIs that UWP has, but MAUI does not.
chaos.
如果自己写了一个页面,要弹出这个窗口页面,那么应该使用 Microsoft.Maui.Controls.Window ,但是自己写的页面是 ContentPage,并不是 Window。
Therefore, you cannot use Window directly. Instead, you place ContentPage into Window and generate Window before operating it.
private Microsoft.Maui.Controls.Window BuildUpdateWindow(ContentPage updatePage)
{
Window window = new Window(updatePage);
window.Title = "更新通知";
return window;
}
Then this window pops up.
Application.Current!.OpenWindow(updateWindow!);
如果要异步打开窗口,请使用 Application.Current!.Dispatcher.DispatchAsync。
await Application.Current!.Dispatcher.DispatchAsync(async () =>
{
try
{
Application.Current!.OpenWindow(updateWindow!);
}
catch (Exception ex)
{
Logger.LogError("无法启动更新窗口", ex);
}
});
If you want to close all windows:
await Application.Current!.Dispatcher.DispatchAsync(async () =>
{
var windows = Application.Current!.Windows.ToArray();
foreach (var window in windows)
{
try
{
Application.Current.CloseWindow(window);
}
catch (Exception ex)
{
Debug.Assert(ex != null);
}
}
});
虽然你获得了 Microsoft.Maui.Controls.Window ,但是不能直接管理这个 Window,而是应该通过 Microsoft.UI.Xaml.Window 或 Microsoft.UI.Windowing.AppWindow 管理。
That is, it is written in window life cycle management in dependency injection.
Or unless you can get the AppWindow instance.
遗憾的是,Microsoft.Maui.Controls.Window 转不了 Microsoft.UI.Xaml.Window 或 Microsoft.UI.Windowing.AppWindow。
You should write this:
builder.ConfigureLifecycleEvents(events =>
{
events.AddWindows(wndLifeCycleBuilder =>
{
wndLifeCycleBuilder.OnWindowCreated(window =>
{
var nativeWindow = (window as Microsoft.Maui.MauiWinUIWindow)!;
... ...
})
.OnActivated((window, args) =>
{
})
.OnClosed((window, args) =>
{
});
});
});
private static void MainWindowCreated(MauiWinUIWindow nativeWindow)
{
const int width = 1440;
const int height = 900;
AppWindow appWindow = nativeWindow.GetAppWindow()!;
// 扩展标题栏,要自定义标题栏颜色,必须 true
nativeWindow.ExtendsContentIntoTitleBar = true;
// 这里必须设置为 Overlapped,之后窗口 Presenter 就是 OverlappedPresenter,便于控制
appWindow.SetPresenter(AppWindowPresenterKind.Overlapped);
//if (appWindow.Presenter is OverlappedPresenter p)
//{
// // p.SetBorderAndTitleBar(hasBorder: false, hasTitleBar: true);
//}
// 重新设置默认打开大小
appWindow.MoveAndResize(new RectInt32(1920 / 2 - width / 2, 1080 / 2 - height / 2, width, height));
// 窗口调整的各类事件
appWindow.Changed += (w, e) =>
{
// 位置发生变化
if (e.DidPositionChange)
{
}
if (e.DidPresenterChange) { }
// 大小发生变化
if (e.DidSizeChange) { }
if (e.DidVisibilityChange) { }
if (e.DidZOrderChange) { }
};
appWindow.Closing += async (w, e) =>
{
try
{
Environment.Exit(0);
}
catch (Exception ex)
{
var log = AppHelpers.LoggerFactory.CreateLogger<AppWindow>();
log.LogError(ex, "Can't close WebHost");
ProcessManager.ReleaseLock();
}
finally
{
ProcessManager.ExitProcess(0);
}
};
appWindow.MoveInZOrderAtTop();
}
Secondly, if you want to directly obtain the current window instance, it will be troublesome.
You can use the following code to get all windows open in the current program.
App.Current.Windows
Application.Current.Windows
If you want to obtain a window that is currently in use or activated, I don't know how to obtain it through the API inside... If you use Win32, that's fine.
-
- Question: Is there such an API? **
Current.GetWindos()
In addition, MAUI cannot customize the title bar, even if the king is here. **
If you want to change the background color for the title bar, you will probably be exhausted.
如果要修改窗口标题,只能在窗口创建时修改,也就是 Microsoft.Maui.Controls.Windows,用 Microsoft.UI.Xaml.Window,或 Microsoft.UI.Windowing.AppWindow 都改不了。
Microsoft.Maui.Controls.Window window = base.CreateWindow(activationState);
window.Title = Constants.Name;
If you want to get a native Window handle, you can use:
var nativeWindow = mauiWindow.Handler.PlatformView;
IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(nativeWindow);
window management
前面提到,想管理窗口,API 要用 Microsoft.UI.Xaml.Window,或 Microsoft.UI.Windowing.AppWindow 的。
In some places, you can only use native Window window handles and then use Win32 to operate.
When customizing the window life cycle, be sure to use:
// 这里必须设置为 Overlapped,之后窗口 Presenter 就是 OverlappedPresenter,便于控制
appWindow.SetPresenter(AppWindowPresenterKind.Overlapped);
Then common window methods are:
/*
AppWindow 的 Presenter ,一定是 OverlappedPresenter
*/
public class WindowService : IWindowService
{
private readonly AppWindow _appWindow;
private readonly Window _window;
private WindowService(AppWindow appWindow, Window window)
{
_appWindow = appWindow;
_window = window;
}
// 检查当前窗口是否全屏
public bool FullScreenState
{
get
{
switch (_appWindow.Presenter)
{
case OverlappedPresenter p:return p.State == OverlappedPresenterState.Maximized;
case FullScreenPresenter p:return p.Kind == AppWindowPresenterKind.FullScreen;
case CompactOverlayPresenter p: return p.Kind == AppWindowPresenterKind.FullScreen;
case AppWindowPresenter p: return p.Kind == AppWindowPresenterKind.FullScreen;
default:return false;
}
}
}
// 让窗口全屏
public void FullScreen()
{
switch (_appWindow.Presenter)
{
case OverlappedPresenter overlappedPresenter:
overlappedPresenter.SetBorderAndTitleBar(true, true);
overlappedPresenter.Maximize();
break;
}
// 全屏时去掉任务栏
// _appWindow.SetPresenter(AppWindowPresenterKind.FullScreen);
}
// 退出全屏
public void ExitFullScreen()
{
switch (_appWindow.Presenter)
{
case OverlappedPresenter p: p.Restore();break;
default: _appWindow.SetPresenter(AppWindowPresenterKind.Default); break;
}
}
// 最小化到任务栏
public void Minmize()
{
#if WINDOWS
var mauiWindow = App.Current.Windows.First();
var nativeWindow = mauiWindow.Handler.PlatformView;
IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(nativeWindow);
PInvoke.User32.ShowWindow(windowHandle, PInvoke.User32.WindowShowStyle.SW_MINIMIZE);
#endif
}
/// <summary>
/// 激活当前窗口
/// </summary>
public void Active()
{
_appWindow.Show(true);
}
// 关闭窗口
public void Exit()
{
_window.Close();
}
public void SetSize(int _X, int _Y, int _Width, int _Height)
{
_appWindow.MoveAndResize(new RectInt32(_X, _Y, _Width, _Height));
}
public (int X, int Y) GetPosition()
{
var p = _appWindow.Position;
return (p.X, p.Y);
}
public (int X, int Y) Move(int x, int y)
{
_appWindow.Move(new PointInt32(x, y));
return GetPosition();
}
public (int Width, int Height, int ClientWidth, int ClientHeight) GetSize()
{
var size = _appWindow.Size;
var clientSize = _appWindow.ClientSize;
return (size.Width, size.Height, clientSize.Width, clientSize.Height);
}
public (PointInt32 Position, SizeInt32 Size, SizeInt32 ClientSize) GetAppSize()
{
return (_appWindow.Position, _appWindow.Size, _appWindow.ClientSize);
}
}
There are two ways to make the window full screen. One is to swallow the taskbar when it is full screen, and the window is full screen in the true sense. The other is to keep the taskbar.
// 保留任务栏
switch (_appWindow.Presenter)
{
case OverlappedPresenter overlappedPresenter:
overlappedPresenter.SetBorderAndTitleBar(true, true);
overlappedPresenter.Maximize();
break;
}
// 全屏时去掉任务栏
// _appWindow.SetPresenter(AppWindowPresenterKind.FullScreen);
最小化只能通过 Win32 API 处理,你要先获取 Microsoft.Maui.Controls.Windows,然后转换为 Window 句柄。
var nativeWindow = mauiWindow.Handler.PlatformView;
IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(nativeWindow);
PInvoke.User32.ShowWindow(windowHandle, PInvoke.User32.WindowShowStyle.SW_MINIMIZE);
此时
Microsoft.UI.Xaml.Window,或Microsoft.UI.Windowing.AppWindow就用不上了。
前面提到,要使用 Microsoft.UI.Xaml.Window,或 Microsoft.UI.Windowing.AppWindow ,例如在 MauiProgram.cs 里面记录了窗口的事件,创建窗口时控制大小。
但是,窗口运行中,要设置窗口大小或限制大小,则是要通过 Microsoft.Maui.Controls.Windows。
For example, to control the main window size not to be too small or infinitely reduced, write in APP.cs:
protected override Window CreateWindow(IActivationState? activationState)
{
Window window = base.CreateWindow(activationState);
window.Title ="窗口标题";
var minSize = GetMinSize();
window.MinimumWidth = minSize.MinWidth;
window.MinimumHeight = minSize.MinHeight;
// Give the Window time to resize
window.SizeChanged += (sender, e) =>
{
var minSize = GetMinSize();
window.MinimumWidth = minSize.MinWidth;
window.MinimumHeight = minSize.MinHeight;
};
//window.Created += (s, e) =>
//{
//};
//window.Stopped += (s, e) =>
//{
//};
return window;
(int MinWidth, int MinHeight) GetMinSize()
{
// 获取当前屏幕的长宽,用 X、Y 表示。
var x = PInvoke.User32.GetSystemMetrics(PInvoke.User32.SystemMetric.SM_CXFULLSCREEN);
var y = PInvoke.User32.GetSystemMetrics(PInvoke.User32.SystemMetric.SM_CYFULLSCREEN);
// 设置窗口最小值,可以按照比例计算,也可以直接设置固定大小
return (x / 3 * 2, y / 5 * 4);
}
}
PS, it is impossible to limit the size of events in AppWindow. The main purpose here is observation. It is not possible to limit the size of the window.
// 窗口调整的各类事件
appWindow.Changed += (w, e) =>
{
// 位置发生变化
if (e.DidPositionChange)
{
}
if (e.DidPresenterChange) { }
// 大小发生变化
if (e.DidSizeChange) { }
if (e.DidVisibilityChange) { }
if (e.DidZOrderChange) { }
};
How to limit opening only one program at a time
Scenario: If program D has already been run by process A, then program D is started again to run process B. B will recognize that the same process exists. At this time, B will activate the A window and pop up, and then B will exit.
This not only limits only one process to running, but also makes the user experience better.
Locks can be implemented using Mutex, and everyone can recognize the same lock throughout the operating system.
Then activate another window to use Win32.
// 进程管理器
internal static class ProcessManager
{
private static Mutex ProcessLock;
private static bool HasLock;
/// <summary>
/// 获取进程锁
/// </summary>
public static void GetProcessLock()
{
// 全局锁
ProcessLock = new Mutex(false, "Global\\" + "自定义锁名称", out HasLock);
if (!HasLock)
{
ActiveWindow();
Environment.Exit(0);
}
}
/// <summary>
/// 激活当前进程并将其窗口放到屏幕最前面
/// </summary>
public static void ActiveWindow()
{
string pName = Constants.Name;
Process[] temp = Process.GetProcessesByName(pName);
if (temp.Length > 0)
{
IntPtr handle = temp[0].MainWindowHandle;
SwitchToThisWindow(handle, true);
}
}
/// <summary>
/// 释放当前进程的锁
/// </summary>
/// <remarks>小心使用</remarks>
public static void ReleaseLock()
{
if (ProcessLock != null && HasLock)
{
ProcessLock.Dispose();
HasLock = false;
}
}
// 将另一个窗口激活放到前台。
// Win32 API
[DllImport("user32.dll")]
public static extern void SwitchToThisWindow(IntPtr hWnd, bool fAltTab);
}
Then use it when the program starts.

MAUI program installation mode
If you use native MAUI programs directly, it will be particularly troublesome to install, because this method is the previous UWP.
Therefore, you can use a method that runs directly without installation.
But here we need to understand the differences between the two models.
If you use the native MAUI model, it will be generated into a Windows application market application, which will be very troublesome whether it is released, put on shelves, or installed. But fortunately, you can use many Windows application APIs.
For example, to obtain the application installation directory:
ApplicationData appdata = Windows.Storage.ApplicationData.Current
Get local storage directory and temporary directory:
var localPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path
var tempPath = Windows.Storage.ApplicationData.Current.TemporaryFolder.Path
Catalog General installation location:
"C:\\Users\\{用户名}\\AppData\\Local\\Packages\\{程序GUID}
There are other APIs such as language processing, and it is quite convenient to use the mall application model.
Next, let's talk about the custom packaging mode, which is to directly compile and generate a bunch of files, and then directly start exe to run it without installation. If you want to make an installation package, you can publish it first and then use a packaging tool to package it.
Just add these two sentences to the project document:
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfConatined>true</WindowsAppSDKSelfConatined>
After that, the executable file will be generated directly. There is no need to install the installation package. The generated program can be run by directly clicking. It does not need to be installed to start, and does not require a certificate to run.
Set the language for MAUI Blazor
MAUI Blazor uses WebView 2 on Windows. The running environment of MAUI Blazor has nothing to do with the program. Even if the system is set to Chinese, the assembly is set to Chinese, the local culture is set to Chinese, and CultureInfo is set to Chinese, they are all useless.
You can press F12 after the program starts, then execute JavaScript code to check what language the browser is running in:
navigator.language
'en-US'

Or use the API:
// using Windows.Globalization
var langs = ApplicationLanguages.Languages.ToList<string>();

Pit ①
First, set up Windows. Globalization:
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
Then restart the program and find:

However, the browser language environment remains unchanged:

原因是
Preferences文件需要重新生成才能起效,后面会提到。
Pit ②
程序启动后,会在 {Windows程序数据目录}/{你的程序ID}/LocalState\EBWebView\Default 下面生成一些 WebView2 的文件,其中 Preferences 文件,里面配置了 WebView2 的参数。
Find your own program data directory:
var path = Windows.Storage.ApplicationData.Current.LocalFolder.Path;

Therefore, you can manually modify files to allow WebView 2 to use the Chinese environment.


var langs = ApplicationLanguages.Languages.ToList<string>();
var cultureInfo = CultureInfo.InstalledUICulture;
var index = langs.FindIndex((lang) => cultureInfo.Equals(CultureInfo.CreateSpecificCulture(lang)));
if (index > 0)
{
ApplicationLanguages.PrimaryLanguageOverride = cultureInfo.Name;
// "...this is immediately reflected in the ApplicationLanguages.Languages property."
langs = ApplicationLanguages.Languages.ToList<string>();
}
var selectedLangs = string.Join(",", langs);
// Should check if this is the same as before but...
var preferences = Windows.Storage.ApplicationData.Current.LocalFolder.Path + "\\EBWebView\\Default\\Preferences";
if (File.Exists(preferences))
{
var jsonString = File.ReadAllText(preferences);
var jsonObject = JObject.Parse(jsonString); // using Newtonsoft.JSON
//var intl = jsonObject["intl"];
jsonObject["intl"] = JObject.Parse($@"{{""selected_languages"": ""{selectedLangs}"",""accept_languages"": ""{selectedLangs}""}}");
jsonString = JsonConvert.SerializeObject(jsonObject);
File.WriteAllText(preferences, jsonString);
}
Pit ③
最后我发现, ① 的思路是对的,不起效的原因是 Preferences 文件需要删除等重新创建才行,只要在程序启动时(WebView 尚未启动),设置中文即可。


Check code:
public static class MauiProgram
{
private static void SetWebViewLanguage()
{
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
var basePath = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
var preferencesFile = Path.Combine(basePath, "EBWebView/Default/Preferences"); // Preferences
if (!File.Exists(preferencesFile)) return;
var jsonString = File.ReadAllText(preferencesFile);
var jsonObject = JsonObject.Parse(jsonString).AsObject();
var languages = jsonObject["intl"]["selected_languages"].Deserialize<string>() ?? "";
// "zh-CN,en,en-GB,en-US"
if (!languages.StartsWith("zh-CN"))
{
// File.Delete(preferencesFile);
jsonObject.Remove("intl");
jsonObject.Add("intl", JsonObject.Parse("{\"selected_languages\":\"zh-CN,en,en-GB,en-US\"}"));
jsonString = JsonSerializer.Serialize(jsonObject);
File.WriteAllText(preferencesFile, jsonString);
}
}
// 省略N多代码
}
在 public static MauiApp CreateMauiApp() 中使用:

Configure MAUI projects to start with administrator privileges
problem setting
在 Windows 中,开发的应用可以使用 app.manifest 资产文件配置程序启动时,使用何种角色权限启动。
The effects are as follows:

正常情况下,在 app.manifest 加上以下配置即可:
If this file is not in the project, you can create a new item-manifest file in the project.
<trustInfo xmlns='urn:schemas-microsoft-com:asm.v2'>
<security>
<requestedPrivileges xmlns='urn:schemas-microsoft-com:asm.v3'>
<requestedExecutionLevel level='requireAdministrator' uiAccess='false' />
</requestedPrivileges>
</security>
</trustInfo>

However, in MAUI applications, it cannot be added. If it is added, an error will appear.
Platforms\Windows\app.manifest : manifest authoring error c1010001: Values of attribute "level" not equal in different manifest snippets.
因为 .NET 编译器中,已经默认为程序生成一个 app.manifest 文件,其文件内容中已经包含了 trustInfo 配置。
如果项目中开启了 <WindowsAppSDKSelfConatined>true</WindowsAppSDKSelfConatined>,那么应该查看 Microsoft.WindowsAppSDK.SelfContained.targets 文件:


所以如果要自定义 app.manifest ,要么就是把 Microsoft.WindowsAppSDK.SelfContained.targets 改了,但是这样不太好。
Custom compilation process
如果观察编译过程,会发现 manifest 文件会生成到 obj 目录。

其中 mergeapp.manifest 便是项目中的 app.manifest ,.NET 编译的时候将开发者的文件改名字了。
程序编译时,首先从 Microsoft.WindowsAppSDK.SelfContained.targets 中生成默认的 app.manifest。
接着将开发者项目中的 app.manifest 复制为 mergeapp.manifest 文件,然后将 mergeapp.manifest 合并到 app.manifest。
如果 app.manifest 中已经存在配置,那么 mergeapp.manifest 中重复的记录就会导致编译报错。
Now that we understand the compilation process, we can do things during the compilation process.
我们可以在编译生成 app.manifest 但是还没有编译主程序的时候,将 app.manifest 中的配置替换掉,替换命令是:
powershell -Command ";(gc app.manifest) -replace 'level=''asInvoker''', 'level=''requireAdministrator''' | Out-File -encoding ASCII app.manifest";

The steps used in MSBuild compilation can refer to the official documentation: https://www.example.com view=vs-2022
During the compilation process, there are two important environment variables:
_DeploymentManifestFiles:清单文件所在目录;ApplicationManifest:app.manifest文件路径。
可以在 .csproj 文件中加入以下脚本,这样会在程序编译时,自动修改清单文件。
<Target Name="RequireAdministrator" BeforeTargets="GenerateManifests" Condition="'$(PublishDir)' != ''">
<Exec WorkingDirectory="./" Command="echo $(ApplicationManifest)" />
<Exec WorkingDirectory="./" Command="echo $(_DeploymentManifestFiles)" />
<Exec WorkingDirectory="$(_DeploymentManifestFiles)" Command="dir" />
<Exec WorkingDirectory="$(_DeploymentManifestFiles)" Command="powershell -Command "(gc app.manifest) -replace 'level=''asInvoker''', 'level=''requireAdministrator''' | Out-File -encoding ASCII app.manifest"" />
</Target>
BeforeTargets="GenerateManifests" 表明在 GenerateManifests 之前,执行里面的自定义命令。
Condition="'$(PublishDir)' != ''" 表示触发条件,在 MAUI 中,只有发布的时候才会有这个变量,也可以改成 Condition="'$(Release)' != ''"。
注意,有些情况下 _DeploymentManifestFiles 目录会不存在,因此可以多次测试一下。
Of course, the safest way:
<Target Name="RequireAdministrator" BeforeTargets="GenerateManifests" Condition="'$(PublishDir)' != ''">
<Exec WorkingDirectory="./" Command=" powershell -Command "(gc $(ApplicationManifest)) -replace 'level=''asInvoker''', 'level=''requireAdministrator''' | Out-File -encoding UTF8 $(ApplicationManifest)"" />
</Target>
编译过程主要在以下三步,其中只有 GenerateManifests 能够在 .csproj 中使用。
<Target Name="GenerateManifests"
Condition="'$(GenerateClickOnceManifests)'=='true' or '@(NativeReference)'!='' or '@(ResolvedIsolatedComModules)'!='' or '$(GenerateAppxManifest)' == 'true'"
DependsOnTargets="$(GenerateManifestsDependsOn)"/>
===================================================
GenerateApplicationManifest
Generates a ClickOnce or native application manifest.
An application manifest specifies declarative application identity, dependency and security information.
===================================================
<Target Name="GenerateApplicationManifest"
DependsOnTargets="
_DeploymentComputeNativeManifestInfo;
_DeploymentComputeClickOnceManifestInfo;
ResolveComReferences;
ResolveNativeReferences;
_GenerateResolvedDeploymentManifestEntryPoint"
Inputs="
$(MSBuildAllProjects);
@(AppConfigWithTargetPath);
$(_DeploymentBaseManifest);
@(ResolvedIsolatedComModules);
@(_DeploymentManifestDependencies);
@(_DeploymentResolvedManifestEntryPoint);
@(_DeploymentManifestFiles)"
Outputs="@(ApplicationManifest)">
You can get parameters:
$(_DeploymentBaseManifest);
@(ResolvedIsolatedComModules);
@(_DeploymentManifestDependencies);
@(_DeploymentResolvedManifestEntryPoint);
@(_DeploymentManifestFiles)
@(ApplicationManifest)
MAUI realizes separate development between front and back ends
background
The first one to adopt Maui + Blazor development, using the Blazor UI framework, which is popular in the community.
However, during the development process, there were many holes in Maui and the Blazor UI framework. The pages and styles written using Blazor UI could not pass the eyes of designers and product managers.
In the end, it was decided to use a native front-end combination to generate static content and put it in Maui, and then combine the two to package and publish it.
Start with the front end
For the front-end, it is enough to follow the normal development model and not pollute the front-end code.
You can use VS to create a front-end project and place it in your solution, or you can create a separate directory and place the front-end code in it.

Create the MAUI Blazor project
Create the MAUI Blazor project, and the solution is as follows:

首先将 wwwroot/css/app.css 文件移出来,放到 wwwroot中,然后新建一个 app.js 也是放到 wwwroot 中。
将 app.css 文件中的内容删除与 Blazor 无关的内容,只保留以下内容:
#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;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=)
no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred.";
}
.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);
}
}
接着,将以下代码放到 app.js 中,用于动态导入前端生成的 css 文件。
function InitializeCss(name) {
document.getElementById("app-css").innerHTML =
'<link rel="stylesheet" href="' + name + '">';
}
然后删除 js、css 目录。
The remaining files are as shown in the figure:

然后修改 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>MauiApp3</title>
<base href="/" />
<link href="/app.css" rel="stylesheet" />
</head>
<body>
<div id="app-css"></div>
<div class="status-bar-safe-area"></div>
<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="app.js"></script>
<script src="_framework/blazor.webview.js" autostart="false"></script>
</body>
</html>
增加的 <div id="app-css"></div>,用于动态加载 css 文件。
Other content remains basically unchanged.
打开 MainLayout.razor,这里负责动态加载前端文件,其内容如下:
@using MauiApp3.Data
@inherits LayoutComponentBase
@code {
#region static fields
private static readonly string[] css;
private static readonly string[] js;
#endregion
#region instance fields
[Inject]
private IJSRuntime JS { get; set; }
#endregion
static MainLayout()
{
var path = Windows.ApplicationModel.Package.Current.InstalledLocation.Path;
if (Directory.Exists(Path.Combine(path, "wwwroot", "css")))
{
var cssList = Directory.GetFiles(Path.Combine(path, "wwwroot", "css"))
.Where(x => x.EndsWith(".css"))
.Select(x => Path.GetFileName(x)).ToArray();
css = cssList;
}
else css = Array.Empty<string>();
if (Directory.Exists(Path.Combine(path, "wwwroot", "js")))
{
var jsList = Directory.GetFiles(Path.Combine(path, "wwwroot", "js"))
.Where(x => x.EndsWith(".js"))
.Select(x => Path.GetFileName(x)).ToArray();
js = jsList;
}
else js = Array.Empty<string>();
}
protected override async Task OnInitializedAsync()
{
await Task.CompletedTask;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
foreach (var item in css)
{
await JS.InvokeVoidAsync("InitializeCss", $"css/{item}");
}
foreach (var item in js)
{
await JS.InvokeAsync<IJSObjectReference>("import", $"/js/{item}");
}
}
}
}
然后为了能够将前端生成的内容自动复制到 Maui 中,可以设置脚本,在 Maui 的 .csproj 中,增加以下内容:
<Target Name="ClientBuild" BeforeTargets="BeforeBuild">
<Exec WorkingDirectory="../vueproject1" Command="npm install" />
<Exec WorkingDirectory="../vueproject1" Command="npm run build" />
<Exec WorkingDirectory="../vueproject1" Command="DEL "dist\index.html"" />
<Exec WorkingDirectory="./" Command="RMDIR /s/q "css"" />
<Exec WorkingDirectory="./" Command="RMDIR /s/q "js"" />
<Exec WorkingDirectory="../vueproject1" Command="Xcopy "dist" "../MauiApp3/wwwroot" /E/C/I/Y" />
<Exec WorkingDirectory="./" Command="RMDIR /s/q "$(LayoutDir)""/>
</Target>

This way, when you start the Maui project, the front-end project will be compiled and the generated files (excluding index.html) will be copied to the wwwroot directory.
After startup:

C#automatically generates certificates, installs certificates locally, and resolves trust certificate issues
background
因为开发 Blazor 时 环境是 https://0.0.0.0/ ,也就是 MAUI Blazor 中只能发出 https 请求,既不能发出 http 请求,也不能发出不安全的 https 请求,但是内网的 https 是不安全的 https。
Therefore, you can only implement a proxy service locally and then let the application access through https. In order to make https secure, you need to install automated certificates.

This will cause js to be unable to make requests.
In order to make https secure, the local localhost automatically generates certificates and installs them here.
write code
The certificate generation uses the libraries that come with. NET and does not require the introduction of third-party packages.
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
生成证书的方法参考 https://github.com/dotnetcore/FastGitHub 项目。
The first step is to write a certificate generator, in which the code is copied directly from here: www.example.com
Then, we define the service to manage the generation of certificates. The original author used. NET 7, and the current stable version is. NET 6. Many APIs cannot be used, so they need to be modified. Original address: https://github.com/dotnetcore/FastGitHub/blob/9f9cbce624310c207b01699de2a5818a742e11ca/FastGitHub.HttpServer/Certs/CertService.cs
Define certificate location and name:
private const string CACERT_PATH = "cacert";
/// <summary>
/// 获取证书文件路径
/// </summary>
public string CaCerFilePath { get; } = OperatingSystem.IsLinux() ? $"{CACERT_PATH}/https.crt" : $"{CACERT_PATH}/https.cer";
/// <summary>
/// 获取私钥文件路径
/// </summary>
public string CaKeyFilePath { get; } = $"{CACERT_PATH}/https.key";
Two files are involved here, the client certificate and the private key.
.key是私钥,可以通过私钥来生成服务端证书和客户端证书,因此这里只需要保存.key私钥,不需要导出服务器证书。.csr、.cer是客户端证书,在 Windows 下可以使用 .cer格式。导出客户端证书的原因是要安装证书,而且安装一次即可,不需要动态生成。
证书管理服务的规则是,如果 ssl 目录下没有证书,那么就生成并安装;如果发现文件已经存在,则加载文件到内存,不会重新安装。
The complete code is as follows:
/// <summary>
/// 证书生成服务
/// </summary>
internal class CertService
{
private const string CACERT_PATH = "cacert";
/// <summary>
/// 获取证书文件路径
/// </summary>
public string CaCerFilePath { get; } = OperatingSystem.IsLinux() ? $"{CACERT_PATH}/https.crt" : $"{CACERT_PATH}/https.cer";
/// <summary>
/// 获取私钥文件路径
/// </summary>
public string CaKeyFilePath { get; } = $"{CACERT_PATH}/https.key";
private X509Certificate2? caCert;
/*
本地会生成 cer 和 key 两个文件,cer 文件导入到 Window 证书管理器中。
key 文件用于每次启动时生成 X509 证书,让 Web 服务使用。
*/
/// <summary>
/// 生成 CA 证书
/// </summary>
public bool CreateCaCertIfNotExists()
{
if (!Directory.Exists(CACERT_PATH)) Directory.CreateDirectory(CACERT_PATH);
if (File.Exists(this.CaCerFilePath) && File.Exists(this.CaKeyFilePath))
{
return false;
}
File.Delete(this.CaCerFilePath);
File.Delete(this.CaKeyFilePath);
var notBefore = DateTimeOffset.Now.AddDays(-1);
var notAfter = DateTimeOffset.Now.AddYears(10);
var subjectName = new X500DistinguishedName($"CN=运连网物流管理系统");
this.caCert = CertGenerator.CreateCACertificate(subjectName, notBefore, notAfter);
var privateKeyPem = ExportRSAPrivateKeyPem(this.caCert.GetRSAPrivateKey());
File.WriteAllText(this.CaKeyFilePath, new string(privateKeyPem), Encoding.ASCII);
var certPem = ExportCertificatePem(this.caCert);
File.WriteAllText(this.CaCerFilePath, new string(certPem), Encoding.ASCII);
return true;
}
/// <summary>
/// 获取颁发给指定域名的证书
/// </summary>
/// <param name="domain"></param>
/// <returns></returns>
public X509Certificate2 GetOrCreateServerCert(string? domain)
{
if (this.caCert == null)
{
using var rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText(this.CaKeyFilePath));
this.caCert = new X509Certificate2(this.CaCerFilePath).CopyWithPrivateKey(rsa);
}
var key = $"{nameof(CertService)}:{domain}";
var endCert = GetOrCreateCert();
return endCert!;
// 生成域名的1年证书
X509Certificate2 GetOrCreateCert()
{
var notBefore = DateTimeOffset.Now.AddDays(-1);
var notAfter = DateTimeOffset.Now.AddYears(1);
var extraDomains = GetExtraDomains();
var subjectName = new X500DistinguishedName($"CN={domain}");
var endCert = CertGenerator.CreateEndCertificate(this.caCert, subjectName, extraDomains, notBefore, notAfter);
// 重新初始化证书,以兼容win平台不能使用内存证书
return new X509Certificate2(endCert.Export(X509ContentType.Pfx));
}
}
private static IEnumerable<string> GetExtraDomains()
{
yield return Environment.MachineName;
yield return IPAddress.Loopback.ToString();
yield return IPAddress.IPv6Loopback.ToString();
}
internal const string RasPrivateKey = "RSA PRIVATE KEY";
private static string ExportRSAPrivateKeyPem(RSA rsa)
{
var key = rsa.ExportRSAPrivateKey();
var chars = PemEncoding.Write(RasPrivateKey, key);
return new string(chars);
}
private static string ExportCertificatePem(X509Certificate2 x509)
{
var chars = PemEncoding.Write(PemLabels.X509Certificate, x509.Export(X509ContentType.Cert));
return new string(chars);
}
/// <summary>
/// 安装ca证书
/// </summary>
/// <exception cref="Exception">不能安装证书</exception>
public void Install( )
{
var caCertFilePath = CaCerFilePath;
try
{
using var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
var caCert = new X509Certificate2(caCertFilePath);
var subjectName = caCert.Subject[3..];
foreach (var item in store.Certificates.Find(X509FindType.FindBySubjectName, subjectName, false))
{
if (item.Thumbprint != caCert.Thumbprint)
{
store.Remove(item);
}
}
if (store.Certificates.Find(X509FindType.FindByThumbprint, caCert.Thumbprint, true).Count == 0)
{
store.Add(caCert);
}
store.Close();
}
catch (Exception ex)
{
throw new Exception($"请手动安装CA证书{caCertFilePath}到“将所有的证书都放入下列存储”\\“受信任的根证书颁发机构”" + ex);
}
}
}
}
Using in ASP.NET Core
Load the server certificate in ASP.NET Core (X509 certificate is generated every time it is started).
var sslService = new CertService();
if(sslService.CreateCaCertIfNotExists())
{
try
{
sslService.Install();
}
catch (Exception)
{
}
}
var webhost = WebHost.CreateDefaultBuilder()
.UseStartup<Startup>()
.UseKestrel(serverOptions =>
{
serverOptions.ListenAnyIP(39999,
listenOptions =>
{
var certificate = sslService.GetOrCreateServerCert("localhost");
listenOptions.UseHttps(certificate);
});
})
.Build();
await webhost.RunAsync();