从Xamarin.Forms到.NET MAUI的迁移过程中,不少团队把注意力放在UI控件替换上,却忽略了启动入口、依赖注入和平台文件结构的变化,导致项目虽然能编译通过,真机运行却频繁崩溃。实际上,MAUI吸收了Xamarin.Forms的页面模型,但底层用更细粒度的Handler替代了传统的渲染器体系,同时统一了跨平台资源管理和生命周期事件。本文从实际迁移项目出发,梳理自动升级工具能完成的部分、必须手工调整的代码以及常见的兼容性陷阱,帮助团队在升级前建立完整的改造预期。

项目结构与启动入口的差异
Xamarin.Forms传统解决方案通常包含一个共享代码库和多个平台头项目,例如Android平台的MainActivity中调用Xamarin.Forms.Forms.Init,iOS平台在AppDelegate中调用LoadApplication。迁移到.NET MAUI后,这些平台头项目被统一为单项目结构,平台相关代码按目录组织,例如Platforms\Android、Platforms\iOS和Platforms\Windows。这种变化不只是目录调整,还意味着启动流程完全重写,原来的平台初始化代码需要合并到统一的入口文件。
MAUI的启动入口从各平台的AppDelegate或MainActivity转移到了公共项目中的MauiProgram.cs。这个文件包含一个CreateMauiApp方法,负责创建MauiApp实例、注册字体、配置依赖注入以及映射自定义控件。下面是一个典型的MAUI启动入口代码:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
return builder.Build();
}
}
升级助手会自动生成单项目结构,并尝试将Forms.Init、字体注册等代码迁移到MauiProgram中。但如果你的项目之前有自定义初始化逻辑、第三方SDK启动调用或平台特定的权限配置,这些不会自动转换。例如Android平台上的OnCreate中可能有二维码扫描SDK的初始化,迁移后需要手动放到ConfigureLifecycleEvents委托里,否则运行时才会发现SDK没有就绪。iOS的FinishedLaunching中的推送通知注册也同样需要改写。所以迁移前的第一步不是直接编译,而是先梳理所有平台启动代码,逐一确认它们在MAUI单项目中的新位置。
命名空间与Xamarin.Essentials API映射
从Xamarin.Forms到MAUI,最直观的变化是命名空间。原来的using Xamarin.Forms;要替换为using Microsoft.Maui.Controls;,using Xamarin.Essentials;则被拆分到多个以Microsoft.Maui开头的命名空间中。很多页面类型如ContentPage、Shell、Grid的类名保持不变,但所属命名空间变了,这导致编译时会先出现大量找不到类型的错误。使用全局替换工具可以快速解决一部分问题,不过不能无脑全部替换,因为部分类型在MAUI中已经移除或改名。
// Xamarin.Forms 旧命名空间 using Xamarin.Forms; using Xamarin.Forms.Platform.Android; using Xamarin.Essentials; // .NET MAUI 新命名空间 using Microsoft.Maui; using Microsoft.Maui.Controls; using Microsoft.Maui.Handlers; using Microsoft.Maui.Devices;
Xamarin.Essentials提供的跨平台设备能力在MAUI中仍然可用,但变成了Microsoft.Maui.Devices、Microsoft.Maui.Media、Microsoft.Maui.Storage等多个子命名空间。例如获取网络状态,旧的Connectivity.NetworkAccess写法需要改成Microsoft.Maui.Networking.Connectivity.Current.NetworkAccess。这种调用方式从静态类变成了带Current属性的单例对象,语义上更清晰,但也意味着迁移时不能只改using,还要调整调用代码。建议先把所有Xamarin.Essentials的API列出清单,再逐个对照官方映射表修改,避免遗漏导致真机运行时抛出NullReferenceException。
另一个容易忽略的是设备信息API。在Xamarin.Forms中Device.RuntimePlatform经常被用来判断当前平台,而MAUI中推荐使用DeviceInfo.Platform和DeviceInfo.Idiom。虽然DeviceInfo.Platform返回的是字符串枚举值,但写法更明确。迁移过程中如果遇到条件编译符号,比如原来在共享库里用#if __ANDROID__,MAUI单项目会改用#if ANDROID。这些符号变化直接写在项目文件里,升级助手会处理一部分,但自定义的编译条件需要人工检查。
自定义渲染器迁移到Handler
Xamarin.Forms的自定义控件通常分两层实现:共享库中继承标准控件,平台项目中继承对应的Renderer并重写OnElementChanged方法。这种方式虽然灵活,但每个渲染器都会创建额外的包装视图,性能开销较大。MAUI彻底抛弃了Renderer体系,改用Handler直接操作平台原生视图。Handler的职责更单一:创建原生视图、连接虚拟控件与原生控件、断开连接并释放资源。迁移自定义渲染器时,需要把原来的Renderer类改写为Handler类。
// Xamarin.Forms 自定义渲染器(Android)
public class BorderlessEntryRenderer : EntryRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (Control != null)
{
Control.SetBackgroundColor(Android.Graphics.Color.Transparent);
Control.SetPadding(0, 0, 0, 0);
}
}
}
// .NET MAUI 对应 Handler
public class BorderlessEntryHandler : EntryHandler
{
protected override void ConnectHandler(Android.Widget.EditText platformView)
{
base.ConnectHandler(platformView);
platformView.SetBackgroundColor(Android.Graphics.Color.Transparent);
platformView.SetPadding(0, 0, 0, 0);
}
}
上面代码里,旧的OnElementChanged在元素变化时被调用,而新的ConnectHandler在平台视图创建后调用,参数直接给出原生控件,不再需要从Element或Control属性中查找。这种改变让代码更直观,也避免了渲染器中常见的时序问题。如果你之前在渲染器中重写了Dispose来释放资源,MAUI中应该把清理逻辑放到DisconnectHandler方法里,因为该方法会在控件从视觉树移除时触发。
Handler注册方式也发生了变化。Xamarin.Forms使用ExportRenderer特性标注在渲染器类上,由框架自动发现;MAUI则需要在MauiProgram中显式注册映射关系。这种显式注册的好处是减少反射扫描,提高启动速度,但迁移时如果忘记注册,自定义控件会退化为默认控件,不会报错,排查起来比较隐蔽。下面是在MAUI中注册Handler的代码:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<BorderlessEntry, BorderlessEntryHandler>();
});
return builder.Build();
}
}
如果自定义控件较多,可以按模块拆分注册方法,避免MauiProgram变得臃肿。建议每个自定义控件库提供一个扩展方法,例如AddCustomControls,在CreateMauiApp中集中调用。这样既保持了启动入口的简洁,也方便其他项目复用同一套Handler迁移成果。
迁移后常见问题与检查清单
完成代码迁移并通过编译只是第一步,真机运行阶段还会暴露不少问题。最常见的是启动时崩溃,原因通常是资源字典或样式文件里仍然引用旧的命名空间,例如xmlns声明没有从Xamarin.Forms改为http://schemas.microsoft.com/dotnet/2021/maui。这类错误在编译阶段不会提示,只有运行时解析XAML资源时才会发生。遇到启动崩溃时,优先检查App.xaml和所有合并资源字典中的命名空间声明是否正确。
另一个高频问题是依赖注入重复注册。Xamarin.Forms项目多数使用Prism或自行管理服务容器,迁移到MAUI后框架内置了IServiceCollection,如果同时保留原来的容器初始化,可能造成服务被覆盖或内存泄漏。建议统一使用MAUI的builder.Services.AddSingleton和AddTransient管理依赖,删除旧容器代码。平台特定服务可以用#if ANDROID等条件编译分别注册,这样不会在单项目结构中产生多余的平台分支。
迁移过程中还有几个必须检查的清单项:
- 确认Android的
AndroidManifest.xml已移动到Platforms\Android目录,并且权限声明没有丢失。 - iOS的
Info.plist是否包含原有权限描述,例如相机、定位、推送通知的使用说明。 - 检查所有自定义控件的Handler是否注册,尤其是那些只在特定页面出现的控件。
- 验证Xamarin.Essentials的每个API是否都映射到正确的MAUI命名空间,避免静态调用在运行时返回默认值。
- 测试深链、通知、后台任务等平台服务,因为它们不依赖UI,最容易在迁移后遗漏。
最后建议按照“先自动升级、再解决编译错误、然后跑模拟器验证页面、最后真机测试平台能力”的顺序推进。每次只改一个模块,提交一次可运行版本,这样即使遇到难以定位的崩溃,也能快速缩小范围。MAUI的调试输出中会带有Microsoft.Maui前缀,真机连接后可以通过控制台过滤关键日志,帮助定位XAML解析、Handler映射或平台权限等问题。