乘风破浪,.Net Core遇见MAUI(.NET Multi-platform App UI),进击现代化跨设备应用框架
阅读原文时间:2022年05月16日阅读:1

https://github.com/dotnet/maui

.NET Multi-platform App UI (MAUI) 的前身是Xamarin.Forms(适用于Android,iOS和UWP的跨平台移动优先框架),.NET MAUI是Xamarin.Forms的演进。我们拥有7年的为客户提供技术支持的经验,服务对象从独立开发人员到一些全球性的大公司,我们正在改善产品的核心功能,加快UI渲染,投资研发一致的系统设计模式,并从移动端扩展到桌面端。

.NET 6 deep dive; what's new and what's coming

MAUI是一个跨iOS、Android、Windows、MacOs多设备多生态的应用框架,而WinUI仅限于Windows。

.NET MAUI is for developers who want to:

  • Write cross-platform apps in XAML and C#, from a single shared code-base in Visual Studio.
  • Share UI layout and design across platforms.
  • Share code, test, and business logic across platforms.

.NET MAUI将Android、iOS、macOS和WindowsAPI统一为单个API,允许在任何地方开发人员处进行笔写体验,同时提供对每个本地平台各个方面的深度访问。

.NET 6为创建应用提供了一系列特定于平台的框架:.Android的.NET、iOS的.NET、macOS的.NET和WindowsUI(WinUI)库。这些框架都可以访问相同的.NET 6基础类库(BCL)。此库将基础平台的详细信息从您的代码中摘要出来。BCL取决于.NET运行时间为您的代码提供执行环境。对于安卓、iOS和macOS,环境由单声道实施,这是.NET运行时间的实现。在Windows上,WinRT执行相同的角色,但窗口平台已优化除外。

虽然BCL使在不同平台上运行的应用能够共享共同的商业逻辑,但不同的平台具有定义应用用户界面的不同方式,它们为指定用户界面元素的沟通和互操作方式提供了不同的模型。您可以使用适当的平台特定框架(.Android的NET、iOS的.NET、macOS的.NET或WinUI)分别为每个平台制作UI,但此方法需要您为每个设备系列维护代码基础。

.NET MAUI为为移动和桌面应用构建UI提供了单一框架。下图显示了.NETMAUI应用程序的架构的高层视图:

在.NET MAUI应用中,您编写的代码主要与NET MAUI API交互。.NET MAUI然后直接消耗原生平台API。此外,如果需要,应用代码可以直接执行平台API。

.NET MAUI应用可以写在PC或Mac上,并编译为原生应用包:

  • 使用.NET MAUI构建的Android应用从C#编译为中间语言(IL),然后在应用启动时及时(JIT)编译到本地装配中。
  • 使用.NET MAUI构建的iOS应用完全提前从C#编译为原生ARM装配代码。
  • 使用.NET MAUI构建的macOS应用使用MacCatalyst,这是苹果的解决方案,将使用UIKit构建的iOS应用引入桌面,并根据需要通过额外的AppKit和平台API来增强它。
  • 使用.NET MAUI构建的Windows应用使用WindowsUI库(WinUI)3创建可以针对Windows桌面和通用视窗平台(UWP)的原生应用。

.NET MAUI应用通常由一个可以针对安卓、iOS、macOS和Windows的项目组成。这提供了以下好处:

  • 一个针对多个平台和设备的项目。
  • 一个位置来管理资源,如字体和图像。
  • 多目标组织平台特定代码。

.NET多平台应用UI(MAUI)应用可用于以下平台编写:

  • Android 5.0 (API 21) or higher.
  • iOS 10 or higher.
  • macOS 11 (Big Sur) or higher.
  • Windows desktop and the Universal Windows Platform (UWP), using Windows UI Library (WinUI) 3.

适用于安卓、iOS和视窗的NET MAUI应用程序可在视觉工作室内置。但是,使用最新版本的Xcode和Apple所需的macOS的最小版本进行iOS开发需要联网Mac。

适用于安卓、iOS和macOS的NET茂宜岛应用程序可在Mac视觉工作室内置。

创建MAUI的前置条件是.Net的版本必须大于等于.NET 6 Preview 5

而且Visual Studio版本必须大于等于Visual Studio 16.11 Preview 2

如果要创建MAUI For Windows App,还需要额外安装两个扩展:

我们可以使用官方提供的一个工具来检查我们需要运行MAUI的环境是否齐全,还是很贴心哈。

https://github.com/redth/dotnet-maui-check

安装MAUI环境检查工具(Maui.Check)

dotnet tool install -g Redth.Net.Maui.Check

运行MAUI环境检查工具(Maui.Check)

maui-check

然后会弹出这个检查工具的命令行界面,根据提示,一步步把缺失的进行安装和补充即可。

如果发现打开maui-check工具后,立即闪退,据说改用管理员权限重新打开终端再执行能解决这个问题。

如果接下来,你继续遇到了raw.githubusercontent.com:443的报错,要么全局代理,要么手动下载并且指定文件maui.manifest.json了。

maui-check -m maui.manifest.json

要命的是,如果你还是继续遇到raw.githubusercontent.com:443的报错,估计是在maui.manifest.json有个文件你过不去,最好也是手动下载并且写死本地路径最好。文件是:Versions.props

恭喜,这次算是过去了。

这里默认推荐你安装OpenJDK 11了,而且还是给你下载由微软构建的Microsoft OpenJDK 11

发现少了安卓SDK,那就Fix吧!

发现少了.net 6全新的工作负载.NET SDK - Workloads,那就Fix吧!

发现少了MAUI SDK全家桶,各个平台的都要,那就Fix吧!

终于,可以愉快的结束了,谢谢你哦!

再执行一次,看看全貌!确认下胜利的果实。

https://github.com/TaylorShi/HelloMaui

创建第一个MAUI项目“HelloMaui”

创建MAUI项目的方式有两种,一种是基于Visual Studio 2019创建,一种是基于DotNet Core CLI创建。

1. 基于Visual Studio 2019创建。

2. 基于DotNet Core CLI创建。

dotnet new maui -n HelloMaui

基于DotNet-Cli构建工具new关键词新建maui项目模板的项目,项目名称为HelloMaui

然后切换到项目目录

cd HelloMaui

用Visual Studio Code打开它。

code .

还原初创建项目

dotnet restore

构建初创项目

dotnet build -t:Run -f net6.0-android
dotnet build -t:Run -f net6.0-ios
dotnet build -t:Run -f net6.0-maccatalyst

如果是IOS项目,还可以选择模拟器:

dotnet build -t:Run -f net6.0-ios -p:_DeviceName=:v2:udid=<UDID>

运行第一个MAUI项目“HelloMaui”

折腾半天,发现用Visual Studio Code还是有点玩不装MAUI,还是转站Visual Studio 2019 Preview吧!

先用Visual Studio 2019 Preview打开Hello MaUI项目。

从结构上看,貌似是拆分成了WINUI项目和非WINUI项目,非WINUI项目主要是放Android、Linux、IOS平台的。

切换到Hello MAUI这个默认的选项后,运行,上来就是熟悉的,让你创建安卓模拟器配置了。

根据提示,乖乖就范吧,不然怕出什么幺蛾子。

等它下载完毕后,我们启动它,一般调试安卓都是要先开一个模拟器或者真机,这个流程我们还是知道的。

我的硬件还算争气,没出幺蛾子,直接就运行成功模拟器了。

接下来,根据实操经验,建议重启一次Visual Studio,这样它比较更好的识别我们的模拟器。

切换到Android Emulator模式,跑起来吧。

哈哈,不错,算是开始顺利,毕竟是成功部署进去了。

打量第一个MAUI项目“HelloMaui”

我们先顺着官方指引,看看MAUI对包的依赖。

<ItemGroup>
    <PackageReference Include="Microsoft.Maui" Version="6.0.100-preview.5.794" />
</ItemGroup>

<ItemGroup>
    <PackageReference Include="Microsoft.ProjectReunion" Version="0.8.0-preview" />
    <PackageReference Include="Microsoft.ProjectReunion.Foundation" Version="0.8.0-preview" />
    <PackageReference Include="Microsoft.ProjectReunion.WinUI" Version="0.8.0-preview" />
    <FrameworkReference Update="Microsoft.Windows.SDK.NET.Ref" RuntimeFrameworkVersion="10.0.19041.16" />
    <FrameworkReference Update="Microsoft.Windows.SDK.NET.Ref" TargetingPackVersion="10.0.19041.16" />
</ItemGroup>

安卓模式下,能看到你的模拟器实例,到时候调试也是可以选择的。

关于应用的资源设置,我们也可以找到线索,可以看到貌似为了保持多个平台一致性,官方的案例里面就直接用SVG这种矢量格式了,其实这还是蛮值得推荐的,就是设计师要注意输出这种格式给开发了。

<ItemGroup>
    <!-- App Icon -->
    <MauiImage
        Include="Resources\appicon.svg"
        ForegroundFile="Resources\appiconfg.svg"
        IsAppIcon="true"
        Color="#512BD4" />

    <!-- Splash Screen -->
    <MauiSplashScreen Include="Resources\appiconfg.svg" Color="#512BD4" />

    <!-- Images -->
    <MauiImage Include="Resources\Images\*" />

    <!-- Custom Fonts -->
    <MauiFont Include="Resources\Fonts\*" />
</ItemGroup>

针对不同平台的代码是分了文件夹放置的,这点和React Native还是挺类似的。

启动程序代码还是比较按最新的.Net Core的规范来,这里有个UseMauiApp,应该是指定应用的根视图的,加载字体的话,可以用ConfigureFonts来定制。

public class Startup : IStartup
{
    public void Configure(IAppHostBuilder appBuilder)
    {
        appBuilder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });
    }
}

既然启动的时候,主动找了App,那么我们就看下App内部做了什么,这里设置了下资源文件目录,然后启动了一个Microsoft.Maui.Controls.Window类型的窗体。

public partial class App : Application
{
    public App()
    {
        InitializeComponent();
    }

    protected override IWindow CreateWindow(IActivationState activationState)
    {
        this.On<Microsoft.Maui.Controls.PlatformConfiguration.Windows>()
            .SetImageDirectory("Assets");

        return new Microsoft.Maui.Controls.Window(new MainPage());
    }
}

前往MainPage,我们一看究竟,哇,这不就是对Windows开发童鞋最熟悉的Xaml,是不是很爽!这里只是控件全部基于Microsoft.Maui.Controls重新来了一套嘛,其实很熟悉了。

using System;
using Microsoft.Maui.Controls;

namespace HelloMaui
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
        }

        int count = 0;
        private void OnCounterClicked(object sender, EventArgs e)
        {
            count++;
            CounterLabel.Text = $"Current count: {count}";
        }
    }
}

我们再看看WINUI项目的内容,发现其实是个空壳子,所有的实现都不在这里,这里就是原来UWP那套应用配置项而已。

1. 学习MAUI的启动入口规范。

首先.NET Multi-platform App UI (MAUI)它是遵循了.NET 通用主机(.NET Generic Host)的启动规范的,这个启动入口的设计,就在主项目的Startup.cs文件中。

在这个Startup.cs中必须存在一个对IStartup接口方法的实现,并且IAppHostBuilder必须至少添加一个应用的实现才可以跑起来,这里举例就是appBuilder.UseMauiApp<App>()这个。

using Microsoft.Maui;
using Microsoft.Maui.Hosting;

public class Startup : IStartup
{
    public void Configure(IAppHostBuilder appBuilder)
    {
        appBuilder.UseMauiApp<App>();
    }
}

然后在App中,又必须存在一个对Application方法的继承实现,并且重写CreateWindow方法,而且至少实现一个Window的创建返回。

using Microsoft.Maui;
using Microsoft.Maui.Controls;

public partial class App : Application
{
    protected override IWindow CreateWindow(IActivationState activationState)
    {
        return new Window(new MainPage());
    }
}

这里举例的MainPage窗体,是一个继承自ContentPage的实现者。

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }
}

2. 在MAUI中玩转自定义字体

如果需要注册新的字体加到应用中,可以在Startup.csConfigure方法中,通过ConfigureFonts来添加。

using Microsoft.Maui;
using Microsoft.Maui.Hosting;

public class Startup : IStartup
{
    public void Configure(IAppHostBuilder appBuilder)
    {
        appBuilder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("Lobster-Regular.ttf", "Lobster");
            });
    }
}

IFontCollection对象的AddFont方法中,第一个参数是字体的全称路径,第二个参数是这个字体的别名,并且所有定制字体都必须描述进项目的描述文件(.csproj)中,这里可以直接用目录 + *来包括整个目录的定制字体。

<ItemGroup>
   <MauiFont Include="Resources\Fonts\*" />
</ItemGroup>

添加的字体,可以直接在Xaml里面,通过指定FontFamily来使用,可以使用无格式后缀的文件名全称,也可以用字体的别名。

<!-- Use font name -->
<Label Text="Hello .NET MAUI"
       FontFamily="Lobster-Regular" />


<!-- Use font alias -->
<Label Text="Hello .NET MAUI"
       FontFamily="Lobster" />

说了这么多,还是找个自定义字体试试,这里想到了自定义图标字体,从https://icofont.com/icons 找了一组品牌的图标字体,里面有微软的图标,下载后把ttf文件重命名下为Brand-IconFont.ttf,然后拖到项目的\Resources\Fonts文件夹下即可。

Startup.cs中,添加对这个字体的注册,别名取为BrandIcoFont吧。

public void Configure(IAppHostBuilder appBuilder)
{
    appBuilder
        .UseMauiApp<App>()
        .ConfigureFonts(fonts =>
        {
            fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            fonts.AddFont("Brand-IconFont.ttf", "BrandIcoFont");
        });
}

切换到构建界面的MainPage.xaml,找个位置把使用字体的文本控件插进去。

<Label
    Grid.Row="2"
    FontFamily="BrandIcoFont"
    FontSize="32"
    HorizontalOptions="CenterAndExpand"
    SemanticProperties.HeadingLevel="Level1"
    Text="" />

这里我们用别名的方式指定FontFamily为BrandIcoFont,至于Text的值,我们需要从字体网站获取下,一般复制HTML Entity的值就好了。

好了,跑起来,看看效果吧,还行,基本达到预期,看到微软Logo了。

3. 在MAUI中玩转自定义事件。

如果需要将控件的自定义事件加到应用中,可以在Startup.csConfigure方法中,通过ConfigureMauiHandlers来添加。

using Microsoft.Maui;
using Microsoft.Maui.Hosting;

public class Startup : IStartup
{
    public void Configure(IAppHostBuilder appBuilder)
    {
        appBuilder
            .UseMauiApp<App>()
            .ConfigureMauiHandlers(handlers =>
            {
                handlers.AddHandler(typeof(MyEntry), typeof(MyEntryHandler));
            });
    }
}

IMauiHandlersCollection对象的AddHandler方法中,第一个参数是控件的实体名称,第二个参数是需要绑定的自定义事件。

这样注册之后,所有MyEntry控件就都会绑定MyEntryHandler这个事件了。

4. 在MAUI中玩转自定义渲染器。

如果需要将控件的自定义渲染器加到应用中,可以在Startup.csConfigure方法中,通过ConfigureMauiHandlers来添加。

using Microsoft.Maui;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Controls.Compatibility;

public class Startup : IStartup
{
    public void Configure(IAppHostBuilder appBuilder)
    {
        appBuilder
            .UseMauiApp<App>()
            #if __ANDROID__
            .ConfigureMauiHandlers(handlers =>
            {
                handlers.AddCompatibilityRenderer(typeof(Microsoft.Maui.Controls.BoxView),
                    typeof(Microsoft.Maui.Controls.Compatibility.Platform.Android.BoxRenderer));
                handlers.AddCompatibilityRenderer(typeof(Microsoft.Maui.Controls.Frame),
                    typeof(Microsoft.Maui.Controls.Compatibility.Platform.Android.FastRenderers.FrameRenderer));
            });
            #elif __IOS__
            .ConfigureMauiHandlers(handlers =>
            {
                handlers.AddCompatibilityRenderer(typeof(Microsoft.Maui.Controls.BoxView),
                    typeof(Microsoft.Maui.Controls.Compatibility.Platform.iOS.BoxRenderer));
                handlers.AddCompatibilityRenderer(typeof(Microsoft.Maui.Controls.Frame),
                    typeof(Microsoft.Maui.Controls.Compatibility.Platform.iOS.FrameRenderer));
            });
            #endif
    }
}

这里可以通过跨平台设备的if条件,来编写,通过IMauiHandlersCollection对象的AddCompatibilityRenderer方法来添加指定的控件走什么渲染器。

注意要添加using Microsoft.Maui.Controls.Compatibility;

using Microsoft.Maui;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Controls.Hosting;
using Microsoft.Maui.Controls.Compatibility;

namespace HelloMaui
{
    public class Startup : IStartup
    {
        public void Configure(IAppHostBuilder appBuilder)
        {
            appBuilder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("Brand-IconFont.ttf", "BrandIcoFont");
                })
                #if __ANDROID__
                .ConfigureMauiHandlers(handlers =>
                {
                    handlers.AddCompatibilityRenderer(typeof(Microsoft.Maui.Controls.BoxView),
                        typeof(Microsoft.Maui.Controls.Compatibility.Platform.Android.BoxRenderer));
                    handlers.AddCompatibilityRenderer(typeof(Microsoft.Maui.Controls.Frame),
                        typeof(Microsoft.Maui.Controls.Compatibility.Platform.Android.FastRenderers.FrameRenderer));
                });
                #elif __IOS__
                .ConfigureMauiHandlers(handlers =>
                {
                    handlers.AddCompatibilityRenderer(typeof(Microsoft.Maui.Controls.BoxView),
                        typeof(Microsoft.Maui.Controls.Compatibility.Platform.iOS.BoxRenderer));
                    handlers.AddCompatibilityRenderer(typeof(Microsoft.Maui.Controls.Frame),
                        typeof(Microsoft.Maui.Controls.Compatibility.Platform.iOS.FrameRenderer));
                });
                #endif
        }
    }
}

5. 在MAUI中玩转自定义控件。

.NET多平台应用UI(MAUI)提供可用于显示数据、启动操作、指示活动、显示集合、拾取数据等的控件集合。默认情况下,处理程序将这些跨平台控件映射到每个平台上的原生控件。例如,在iOS上,Button控件将会映射成UIButton。在安卓系统上,Button控件将会映射成AppCompatButton

比如我们要自定义安卓平台,所有的界面背景颜色。

using Microsoft.Maui;
using Microsoft.Maui.Controls;

public partial class App : Application
{
    public App()
    {
        InitializeComponent();

#if __ANDROID__
        Microsoft.Maui.Handlers.ViewHandler.ViewMapper[nameof(IView.BackgroundColor)] = (h, v) =>
        {
            (h.NativeView as Android.Views.View).SetBackgroundColor(Microsoft.Maui.Graphics.Colors.Cyan.ToNative());
        };
#endif
    }
}

如果要移除安卓组件的下划线

using Microsoft.Maui;
using Microsoft.Maui.Controls;

public partial class MainPage : ContentPage, IPage
{
    public MainPage()
    {
        InitializeComponent();
#if __ANDROID__
        Handlers.EntryHandler.EntryMapper[nameof(IEntry.BackgroundColor)] = (h, v) =>
        {
            (h.NativeView as global::Android.Views.Entry).UnderlineVisible = false;
        };
#endif
    }
}

如果想自定义控件,可以继承自Entry来做。

using Microsoft.Maui.Controls;

namespace HelloMaui
{
    public class MyEntry : Entry
    {

    }
}

还可以自定义一个处理给到自定义控件。

using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;

namespace MauiApp1
{
      public partial class App : Application
      {
          public App()
          {
          InitializeComponent();

          Microsoft.Maui.Handlers.EntryHandler.EntryMapper[nameof(IView.BackgroundColor)] = (handler, view) =>
          {
              if (view is MyEntry)
              {
#if __ANDROID__
                  handler.NativeView.SetBackgroundColor(Colors.Red.ToNative());
#elif __IOS__
                  handler.NativeView.BackgroundColor = Colors.Red.ToNative();
                  handler.NativeView.BorderStyle = UIKit.UITextBorderStyle.Line;
#elif WINDOWS
                  handler.NativeView.Background = Colors.Red.ToNative();
#endif
              }
          };
          }
      }
}