1675 字
8 分钟
.NET MAUI 正式版初体验

.NET Multi-platform App UI (MAUI) 现已加入 Visual Studio 2022 17.3,成为工作负载(Workload)之一,遥想上次 VS 增加新 workload 还是在上一次。VS 的默认项目模板支持 Android、iOS、MacCatalyst、Tizen 和 Windows。Linux仅有社区支持。

项目结构#

默认项目的目录结构是这样的:

.
└── MauiApp1
   ├── App.xaml
   ├── App.xaml.cs
   ├── AppShell.xaml
   ├── AppShell.xaml.cs
   ├── MainPage.xaml
   ├── MainPage.xaml.cs
   ├── MauiApp1.csproj
   ├── MauiProgram.cs
   ├── Platforms
   │   ├── Android
   │   ├── MacCatalyst
   │   ├── Tizen
   │   ├── Windows
   │   └── iOS
   ├── Properties
   │   └── launchSettings.json
   └── Resources

结构说明#

MauiProgram.cs 中定义了一个静态类和一个名为CreateMauiApp的静态方法,方法内部使用 Generic Hosting 模式创建了一个 MauiApp 并配置依赖注入,就像 ASP.NET Core 一样。

编译时,对应平台会使用Platforms文件夹中的配置,这些文件夹中的内容是特定于平台的。

例如 Windows 平台是一个 WinUI3 项目的结构,它的 App 类继承自 MauiWinUIApplication, 而后者继承自Microsoft.UI.Xaml.Application,也就是 windows app sdk 提供的 WinUI3 程序的基类。这个App类便会调用MauiProgram.CreateMauiApp

Android 平台则是 一个MainApplication类,它继承自MauiApplication,而后者继承自Android.App.Application,这是 Android 应用程序的基类。

这使得可以修改应用程序对于特定平台的配置, 例如可以分别配置 Android 平台的 AndroidMainifest 和 WinUI 的 app.mainifest与以及MSIX打包的 Package.appxmanifest。

MAUI 使用平台原生UI框架#

xaml文件最终会被编译为各个平台的原生UI。这里我就在默认实例上加几个控件。 MAUI WinUI 和 Android 控件对比

可以看到,尽管控件样式基本一致,但都渲染为平台自己的UI框架对应的控件。比如 Switch 渲染为了 WinUI 的 ToggleSwitch,它会在旁边显示 OnOff 的文字;而 Android 的 Switch 控件则不会,而且还在左侧空出了空间。

更明显的,可以看看两个平台的日期选择控件: MAUI WinUI 和 Android 日期选择控件对比

这就像是同样的 html input 标签,不同的浏览器会使用不同默认样式。同样的 xaml 会被编译为平台本机的 UI。

就像用css来统一html样式,指定 Style 可以是 MAUI 在各个平台上获得几乎一样的表现,同时使用平台原生的UI交互。

开发#

特定于平台的代码#

尽管 MAUI 在于统一各个平台,但是仍然可以指定不同平台的不同实现。文档的 Platform Integration一节介绍了 MAUI 库提供的不同平台 API 的统一抽象,但是对于这些 API 没有覆盖到的地方,仍然要自行编写针对不同平台的实现。

这是一个常见的场景,例如要获取当前应用是否处于流量计费的网络,目前 MAUI 平台库没有现成的封装,要自己写的话,很显然 Windows 上的实现和 Android/iOS 上是不同的(而我甚至不知道 Mac 有没有这个功能)。

实例:统一接口和平台特定的实现#

配置项目多目标#

借由依赖注入模式,可以让一个接口在不同平台对应不同的实现。 首先你参考官方文档配置多目标。这里就直接用最后一项方案,结合基于目录和基于文件名的多目标。在项目配置里加上这个:

<ItemGroup Condition="$(TargetFramework.StartsWith('net6.0-android')) != true">
<Compile Remove="**\**\*.Android.cs" />
<None Include="**\**\*.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
<Compile Remove="**\Android\**\*.cs" />
<None Include="**\Android\**\*.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith('net6.0-ios')) != true AND $(TargetFramework.StartsWith('net6.0-maccatalyst')) != true">
<Compile Remove="**\**\*.iOS.cs" />
<None Include="**\**\*.iOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
<Compile Remove="**\iOS\**\*.cs" />
<None Include="**\iOS\**\*.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true ">
<Compile Remove="**\*.Windows.cs" />
<None Include="**\*.Windows.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
<Compile Remove="**\Windows\**\*.cs" />
<None Include="**\Windows\**\*.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

效果是:例如,当编译 Android 平台时,所有 *.Windows.cs*.iOS.cs 文件,或是任何 WindowsiOS 文件夹下的文件,都会被忽略并跳过编译,Roslyn也不会分析对应的代码,从而保证 IntelliSense 的正确性。

定义接口#

创建一个INetStatusService.cs文件,内容如下:

namespace MauiApp1
{
public interface INetStatusService
{
public bool IsMeteredConnection();
}
}

如果你在这个共享文件里直接尝试访问平台命名空间,IntelliSense会给出这样的提示: IntelliSense提示:平台可用性 所以,必须分别编写三个平台的代码,实现这个接口。

分平台实现接口#

Windows:

using Windows.Networking.Connectivity;
namespace MauiApp1
{
public class NetStatusService : INetStatusService
{
public bool IsMeteredConnection()
{
var profile = NetworkInformation.GetInternetConnectionProfile();
var cost = profile.GetConnectionCost();
return cost.NetworkCostType switch
{
NetworkCostType.Unrestricted => false,
NetworkCostType.Unknown => false,
_ => true
};
}
}
}

另外,你还会注意到这里的switch表达式模板匹配语法,这也是 MAUI 的好处之一,它使用最新的 .NET SDK,支持最新的 C# 语法。

Android:

using Android.Content;
using Android.Net;
namespace MauiApp1
{
public class NetStatusService : INetStatusService
{
public bool IsMeteredConnection()
{
var manager = (ConnectivityManager)Android.App.Application.Context.GetSystemService(Context.ConnectivityService);
return manager.IsActiveNetworkMetered;
}
}
}

iOS (我不会~~(因为没Mac)~~,那就 throw not impl吧)

namespace MauiApp1
{
public class NetStatusService : INetStatusService
{
public bool IsMeteredConnection()
{
throw new NotImplementedException();
}
}
}
依赖注入#

由于 Maui App 使用 Generic Host 和 依赖注入模式。所以可以轻松地为这个接口配置多实现。

MauiProgram.cs 加一句

builder.Services.AddTransient<INetStatusService, NetStatusService>();

虽然构造函数注入很好用,但是对于这个简单的示例,或是一些特殊情况,还是要拿到 IServiceProvider 的。由于各个平台的App基类不一样,所以给 App.xaml.cs 加一个属性方便使用:

namespace MauiApp1;
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
public static IServiceProvider Services =>
#if ANDROID
MauiApplication.Current.Services;
#elif WINDOWS10_0_17763_0_OR_GREATER
MauiWinUIApplication.Current.Services;
#elif MACCATALYST || IOS
MauiUIApplicationDelegate.Current.Services;
#else
null;
#endif
}

这样就可以随时通过App.Services来得到 DI 容器了。

MainPage.xaml.cs 设置一个属性用于绑定,并在构造函数里设置默认的BindingContext,实际项目应该用ViewModel实现MVVM的,不过这个示例就从简了。

public MainPage()
{
InitializeComponent();
this.BindingContext = this;
}
private readonly INetStatusService _netStatus = App.Services.GetRequiredService<INetStatusService>();
public bool IsMeteredConnection => _netStatus.IsMeteredConnection();

然后我们在示例页面加个控件测试一下:

<StackLayout Orientation="Horizontal">
<CheckBox
IsChecked="{x:Binding IsMeteredConnection}"
IsEnabled="False" />
<Label Text="使用按流量计费的网络" />
</StackLayout>

在 Windows 上的效果:

Windows示例

在 Android 上的效果: Android示例

其它#

MAUI 还有强大的热重载,包括UI和后台代码的即时加载,而且在所有平台都可用。

MAUI 短期内不会添加官方的Linux支持,这是因为Linux桌面环境碎片严重。

比起早先的 Xamarin Forms, MAUI 生成的 Android 应用体积小了不少。另外对于 Windows 平台的 WinUI3,可以像其它 WinUI3 应用一样配置为不使用MSIX打包发布,这样最终发布的应用是一个可以直接执行的 .exe 文件,就像传统win32应用程序一样。

参考资料#

.NET Multi-platform App UI documentation .NET MAUI Roadmap Introducing .NET MAUI - One Codebase, Many Platforms - .NET Blog

.NET MAUI 正式版初体验
https://blog.a33.su/posts/maui-taste/
作者
artiga033
发布于
2022-08-13
许可协议
CC BY-NC-SA 4.0