
以下は、MAUIアプリの開発中に私が踏んだ穴と、いくつかのメモの概要です。
MAUIはゴミだと言われている。
Mono Jinyuがいなければ、コミュニティはMaUIの失敗に注目する人はあまりいないでしょう。
現在、. NETは7.0にアップグレードされていますが、MAUIは相変わらずプルされています。MAUIを開発したり、カスタマイズしたり、タイトルバーをカスタマイズしたりすると、MAUIがどれほど難しいかがわかります。
MAUIはUWPと何の関係があるのか分からないが、MAUIの多くはUWPのデザインを継承しているように感じており、MAUIは次のUWPになるかもしれない。
WindowsやLinuxのデスクトップ開発であれば、WPFやAvaloniaをお勧めします。MAUIはあまりありません。WPFの下には多くのAPIがありますが、MAUIはありません。
MAUI WindowsはWINUIに基づいて開発されましたが、WINUIに従って情報を探すとMAUIが使えません。
ここでは、Rustで実装され、他のフロントエンドフレームワークと組み合わせてアプリケーションを開発できる、便利なフロントエンドパッケージクライアントツールtauriをお勧めします。
その開発モデルはMAUI Blazorよりもはるかに優れており、開発経験は非常に良く、生成されたソフトウェアのサイズは驚くほど小さく、生成されたアプリにはインストールインターフェイスが付属しており、生成されたプログラムは手動で再パッケージする代わりにパッケージ化されています。
文章介绍:https://www.whuanle.cn/archives/21062
他のデスクトップと比較して、MAUIは本当に肥大化しており、MAUI BlazorはVisual Studio自体の開発エクスペリエンス、フレームワーク自体の使用、Blazor UIフレームワークの3つの側面のいずれも十分ではありません。
プレイできるBlazorフレームワークは少なく、簡単に拡張できるUIフレームワークはさらに少なく、現在使用できるBlazorフレームワークはMASA Blazorが良いです。
なぜそう言うのか、まず第一に、Blazorを書くとき、エディタはRazorをサポートしていない、構文プロンプトがない、コードにエラーがあるがエディタにはエラーがない、エディタにはエラーがある、などがあります。
次に、MAUIでのBlazorの使用とBlazorフレームワークの選択について説明します。MAUIの下でBlazorを使用すると、サードパーティ製のUIフレームワークを使用すると、導入後に自然に閉じられた性質があります。
純粋なフロントエンドフレームワークで開発すると、依存関係が明確になり、どのパッケージを参照する必要があるか、コンパイラがプロンプトを表示し、コンパイル時にプロンプトを表示します。
Blazorフレームワークでは、どのjsが使用されているかを知ることは困難ですが、Blazor dllはjsやその他のファイルをネストしており、それ自体が閉鎖的なものであり、内部の状況を理解することはより困難であり、バグデバッグが困難です。
また、Blazorフレームワークでカプセル化されたコードはC#+ jsで書かれており、C#コードはコンパイル後に変更できないため、参照したBlazorライブラリに問題がある場合にデバッグソースコードを表示することは困難です。また、現在のBlazorフレームワークから、多くのフレームワーク自体のコードが非常に肥大化しており、内部の設計とロジックが明確ではなく、コードの多くの場所がコンポーネントの拡張を制限しており、開発者は内部の実装を置き換えることが困難であることがわかります。
過剰な設計も問題です。なぜなら、サポートのためのサポート、柔軟性と柔軟性のためのAPI設計、パラメータのレンダリング、ロジックのカプセル化が多すぎると、これらのコンポーネントが実際に使いにくくなるからです。
ほとんどのBlazorフレームワークは個人リポジトリでメンテナンスされます。市場に出回っているブティックフロントエンドフレームワークは、ほとんどの場合、大企業、ViewJS、Ant Designなどの支持を持っており、そのフレームワーク自体には専門チームがメンテナンスし、フレームワークを設計し、共通のブティックを維持しています。
しかし、現在のBlazorは、MASAがやっていること以外に、“ブティック”とは言えないと思います。
マーサにも理由があります。
MASAは本当にエコロジーに取り組んでおり、多くの開発者やファンが積極的に参加しており、オープンソースの共有精神は賞賛に値する。
Blazorに問題がある場合、MAUI開発に問題がある場合は、MASAフレームワークを使用していなくても、MASA群衆に質問することができます。有料の回答はありません。誰もあなたの料理を笑わないし、誰もあなたが理解していないことを笑うことはありません。もちろん、オープンソースプロジェクトが問題に答えるために支払うという意味ではなく、MASAのオープンソース精神を賞賛しているだけです。
公式サイト:https//www.masastack.com/blazor
MASAチームが素晴らしい作品を作ることを期待してください。
しかし、今のところ、MAUI + Blazorデスクトップ開発には利点がありません。多くの問題を引き起こすでしょう。
できれば、もうMAUIに触れたくない。
MAUIの知識をいくつか紹介します。
窓から
首先,创建项目后, 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()!;
MAUIのWindowクラスAPIは混乱しており、ほとんどはUWPから継承されており、UWPには多くのAPIがありますが、MAUIにはありません。
混沌とした
如果自己写了一个页面,要弹出这个窗口页面,那么应该使用 Microsoft.Maui.Controls.Window ,但是自己写的页面是 ContentPage,并不是 Window。
したがってWindowをそのまま使用するのではなく,ContentPageをWindowに入れてWindowを生成してから操作する.
private Microsoft.Maui.Controls.Window BuildUpdateWindow(ContentPage updatePage)
{
Window window = new Window(updatePage);
window.Title = "更新通知";
return window;
}
このウィンドウをポップアップします。
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);
}
});
すべてのウィンドウを閉じる:
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 管理。
これは依存性注入のウィンドウライフサイクル管理で記述されています。
または、AppWindowインスタンスを取得できない限り。
遗憾的是,Microsoft.Maui.Controls.Window 转不了 Microsoft.UI.Xaml.Window 或 Microsoft.UI.Windowing.AppWindow。
こう書くべきです。
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();
}
次に、現在のウィンドウインスタンスを直接取得したい場合も面倒です。
現在のプログラムで開いているすべてのウィンドウは、次のコードで取得できます。
App.Current.Windows
Application.Current.Windows
現在使用中またはアクティブなウィンドウを取得したい場合は、内部のAPIから取得する方法がわかりません。Win 32では可能です。
Current.GetWindos()
また、MAUIはタイトル欄をカスタマイズすることができない、天王老子が来てもダメ。
タイトルバーの背景色を変更しようとすると、疲れてしまいます。
如果要修改窗口标题,只能在窗口创建时修改,也就是 Microsoft.Maui.Controls.Windows,用 Microsoft.UI.Xaml.Window,或 Microsoft.UI.Windowing.AppWindow 都改不了。
Microsoft.Maui.Controls.Window window = base.CreateWindow(activationState);
window.Title = Constants.Name;
ネイティブのウィンドウハンドルを取得したい場合は、以下を使用できます。
var nativeWindow = mauiWindow.Handler.PlatformView;
IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(nativeWindow);
ウィンドウの管理
前面提到,想管理窗口,API 要用 Microsoft.UI.Xaml.Window,或 Microsoft.UI.Windowing.AppWindow 的。
ネイティブのウィンドウハンドルを使用し、Win 32で操作する場所もあります。
ウィンドウのライフサイクルをカスタマイズする場合は、必ず次の項目を使用します。
// 这里必须设置为 Overlapped,之后窗口 Presenter 就是 OverlappedPresenter,便于控制
appWindow.SetPresenter(AppWindowPresenterKind.Overlapped);
一般的なウィンドウメソッドは以下の通り。
/*
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);
}
}
ウィンドウをフルスクリーンにする2つの方法があります。1つはフルスクリーンで、ウィンドウはタスクバーを飲み込み、真の意味でのフルスクリーンであり、もう1つはタスクバーを保持することです。
// 保留任务栏
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。
例えば、メインウィンドウのサイズを無限に縮小するには小さすぎてはならない制御は、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,AppWindow内のイベントのサイズ制限はできないが,ここでは主に観察するため,ウィンドウサイズなどの制限をしたいのではない.
// 窗口调整的各类事件
appWindow.Changed += (w, e) =>
{
// 位置发生变化
if (e.DidPositionChange)
{
}
if (e.DidPresenterChange) { }
// 大小发生变化
if (e.DidSizeChange) { }
if (e.DidVisibilityChange) { }
if (e.DidZOrderChange) { }
};
一度に1つのプログラムに制限する方法
シナリオでは、プログラムDがプロセスAによって実行されている場合、プログラムDを再起動してプロセスBを実行すると、Bは同じプロセスが存在することを認識し、BはAウィンドウを起動してポップアップし、Bは終了する。
これにより、実行できるプロセスを1つに制限するだけでなく、ユーザーエクスペリエンスも向上します。
ロックはMutexを使用して実装でき、オペレーティングシステム全体で同じロックを認識できます。
次に、Win 32を使用できる別のウィンドウをアクティブにします。
// 进程管理器
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);
}
プログラムの起動時に使用します。

MAUIプログラムのインストールモード
ネイティブMAUIプログラムを直接使用すると、この方法は以前のUWPであるため、インストールが特に面倒になります。
したがって、インストールを必要とせずに直接実行する方式を使用できます。
しかし、ここでは2つのモデルの違いを理解する必要があります。
ネイティブMAUIモードを使用している場合、リリース、棚、インストールにかかわらず、Windowsアプリケーションマーケットプレイスアプリケーションが生成されます。多くのWindowsアプリケーションAPIを使用できます。
たとえば、アプリケーションのインストール·ディレクトリを取得するには、次の手順に従います。
ApplicationData appdata = Windows.Storage.ApplicationData.Current
ローカル·ストレージ·ディレクトリと一時ディレクトリを取得するには、次の手順に従います。
var localPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path
var tempPath = Windows.Storage.ApplicationData.Current.TemporaryFolder.Path
ディレクトリ一般的なインストール場所:
"C:\\Users\\{用户名}\\AppData\\Local\\Packages\\{程序GUID}
他にも言語処理などのAPIがあり、ショッピングモールアプリケーションモードを使用するのは非常に便利です。
次に、カスタムパッケージングモードについて話します。直接コンパイルしてファイルの束を生成し、インストールなしで実行できるexeを直接起動します。インストールパッケージを作成したい場合は、最初にリリースしてからパッケージングツールを使用してパッケージします。
プロジェクト文書に以下の2つの文を追加します。
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfConatined>true</WindowsAppSDKSelfConatined>
その後、実行可能ファイルが直接生成され、インストールパッケージをインストールする必要はなく、生成されたプログラムは、クリックして直接実行でき、インストール後に起動する必要はなく、実行する証明書も必要ありません。
MAUI Blazor用の言語設定
MAUI BlazorはWindows 上でWebView2を使用しており、MAUI Blazor実行環境はプログラムとは関係ありません。たとえシステムが中国語を設定し、アセンブリが中国語を設定し、ローカル文化が中国語を設定し、CultureInfoが中国語を設定しても、すべて使用されません。
プログラムが起動したら、F 12を押してJava Scriptコードを実行して、ブラウザが実行されている言語を確認できます。
navigator.language
'en-US'

APIの使用:
// using Windows.Globalization
var langs = ApplicationLanguages.Languages.ToList<string>();

ピット①
まず、Windows.Globalizationを設定します。
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
プログラムを再起動すると:

ブラウザの言語は変わりません。

原因是
Preferences文件需要重新生成才能起效,后面会提到。
ピットⅡ
程序启动后,会在 {Windows程序数据目录}/{你的程序ID}/LocalState\EBWebView\Default 下面生成一些 WebView2 的文件,其中 Preferences 文件,里面配置了 WebView2 的参数。
自分のプログラムデータディレクトリを見つける:
var path = Windows.Storage.ApplicationData.Current.LocalFolder.Path;

したがって、WebView2が中国語環境を使用できるようにファイルを手動で変更することができます。


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);
}
ピット③
最后我发现, ① 的思路是对的,不起效的原因是 Preferences 文件需要删除等重新创建才行,只要在程序启动时(WebView 尚未启动),设置中文即可。


コードをチェック:
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() 中使用:

MAUIプロジェクトの設定管理者権限での起動
質問の背景
在 Windows 中,开发的应用可以使用 app.manifest 资产文件配置程序启动时,使用何种角色权限启动。
効果は以下のとおり。

正常情况下,在 app.manifest 加上以下配置即可:
プロジェクトにこのファイルがない場合は、プロジェクト内に新しいアイテム-マニフェストファイルを作成できます。
<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>

しかし、MAUIアプリでは、追加することはできず、追加するとエラーが発生します。
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 改了,但是这样不太好。
コンパイルプロセスのカスタマイズ
如果观察编译过程,会发现 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 中重复的记录就会导致编译报错。
コンパイルプロセスを理解したので、コンパイルプロセスで何かをすることができます。
我们可以在编译生成 app.manifest 但是还没有编译主程序的时候,将 app.manifest 中的配置替换掉,替换命令是:
powershell -Command ";(gc app.manifest) -replace 'level=''asInvoker''', 'level=''requireAdministrator''' | Out-File -encoding ASCII app.manifest";

MSBuildのコンパイル手順については、公式ドキュメントを参照してください。https//learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-targets?ビュー = VS-2022
コンパイル時には、2つの重要な環境変数があります。
_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 目录会不存在,因此可以多次测试一下。
もちろん、最も安全な方法:
<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)">
パラメータの取得:
$(_DeploymentBaseManifest);
@(ResolvedIsolatedComModules);
@(_DeploymentManifestDependencies);
@(_DeploymentResolvedManifestEntryPoint);
@(_DeploymentManifestFiles)
@(ApplicationManifest)
MAUIは前後端分離開発を実現
背景は
最初に使用されたのはMaui + Blazor開発で、コミュニティの人気の高いBlazor UIフレームワークを使用しています。
しかし、開発の過程で、Mauiは巨大なマルチピット、Blazor UIフレームワークも巨大なマルチピットであり、Blazor UIによって書かれたページとスタイルは、デザイナーやプロダクトマネージャーを通過することはできません。
最終的には、ネイティブフロントエンドの組み合わせを使用し、Mauiに静的コンテンツを生成し、2つを組み合わせてパッケージ化することにしました
最初にフロントエンドを
フロントエンドについては、通常の開発モードに従い、フロントエンドコードを汚染しません。
VSを使用してフロントエンドプロジェクトを作成し、ソリューションに配置するか、フロントエンドコードを配置するために別のディレクトリを作成することができます。

MAUI Blazorプロジェクトを作成する
MAUI Blazorプロジェクトを作成すると、解決策は次のようになります。

首先将 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 目录。
残りの文書は以下のとおりです。

然后修改 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 文件。
その他の内容は基本的に変わらない。
打开 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>

Mauiプロジェクトを起動すると、フロントエンドプロジェクトがコンパイルされ、結果のファイルindex.htmlを除くがwwwrootディレクトリにコピーされます。
起動後:

C#証明書の自動生成、証明書のローカルインストール、信頼できる証明書の問題解決
背景は
因为开发 Blazor 时 环境是 https://0.0.0.0/ ,也就是 MAUI Blazor 中只能发出 https 请求,既不能发出 http 请求,也不能发出不安全的 https 请求,但是内网的 https 是不安全的 https。
したがって、ローカルにプロキシサービスを実装することができ、HTTPS経由でアプリケーションにアクセスさせ、HTTPSを安全にするためには自動証明書がインストールされます。

JSはリクエストを送信しません。
httpsを安全にするために、ローカルlocal hostが証明書を自動的に生成し、インストールするプロセスを実装します。
コードを書く。
証明書の生成は. NETライブラリを使用し、サードパーティ製パッケージを導入する必要はありません。
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
生成证书的方法参考 https://github.com/dotnetcore/FastGitHub 项目。
最初のステップは、コードをここから直接コピーする証明書ジェネレータを書くことです:https://github.com/dotnetcore/FastGitHub/blob/9f9cbce624310c207b01699de2a5818a742e11ca/FastGitHub.HttpServer/Certs/CertGenerator.cs
次に、証明書の生成を管理するサービスを定義します。オリジナルの作成者は. NET 7を使用していますが、現在の安定バージョンは. NET 6であり、多くのAPIが利用できないため、変更が必要です。オリジナルのアドレス: https://github.com/dotnetcore/FastGitHub/blob/9f9cbce624310c207b01699de2a5818a742e11ca/FastGitHub.HttpServer/Certs/CertService.cs
証明書の場所と名前を定義します。
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";
ここには、クライアント証明書と秘密鍵の2つのファイルがあります。
.key是私钥,可以通过私钥来生成服务端证书和客户端证书,因此这里只需要保存.key私钥,不需要导出服务器证书。.csr、.cer是客户端证书,在 Windows 下可以使用 .cer格式。导出客户端证书的原因是要安装证书,而且安装一次即可,不需要动态生成。
证书管理服务的规则是,如果 ssl 目录下没有证书,那么就生成并安装;如果发现文件已经存在,则加载文件到内存,不会重新安装。
完全なコードは以下の通り。
/// <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);
}
}
}
}
ASP.NET Coreでの使用
ASP.NET Coreでサーバー証明書をロードします(起動するたびにX 509証明書を生成します)。
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();