C#与MAUI
.NET MAUI跨平台开发
概述
.NET MAUI(Multi-platform App UI)是微软推出的跨平台应用开发框架。它允许你用一套 C# 和 XAML 代码,同时构建 Android、iOS、macOS 和 Windows 应用程序。MAUI 是 Xamarin.Forms 的演进版本,采用了单一项目结构、源生成器优化和统一的资源管理系统,大幅简化了跨平台开发流程。
为什么需要 MAUI?如果你希望用 C# 同时覆盖移动端和桌面端,不想为每个平台分别学习 Swift、Kotlin 或原生 Windows 开发技术,MAUI 提供了一条统一的路径。它将平台差异封装在底层,让你专注于业务逻辑和界面设计。
基础概念
单一项目模型:与 Xamarin.Forms 每个平台一个项目不同,MAUI 使用单一项目,所有平台共享同一份代码和资源。平台特定的代码通过条件编译或部分类来处理。
Handler 架构:MAUI 用 Handler 替代了 Xamarin.Forms 的 Renderer 架构。Handler 更轻量,只映射你需要修改的属性,而不是整个控件。这意味着更好的性能和更简单的自定义。
XAML:XAML 是一种声明式标记语言,用于定义界面布局。每个页面是一个 XAML 文件,配合一个 C# 代码隐藏文件处理逻辑。
数据绑定:MAUI 支持强大的数据绑定机制,可以将界面元素与数据源关联起来,当数据变化时界面自动更新。
快速上手
首先确保安装了 .NET 8.0 或更高版本的 SDK,然后安装 MAUI 工作负载:
# 安装 MAUI 工作负载
dotnet workload install maui
# 创建新的 MAUI 项目
dotnet new maui -n MyFirstApp
# 进入项目目录
cd MyFirstApp
# 在 Windows 上运行
dotnet run -f net8.0-windows
# 在 Android 上运行(需要连接设备或模拟器)
dotnet run -f net8.0-android
创建项目后,你会看到以下项目结构:
MyFirstApp/
├── App.xaml # 应用入口
├── App.xaml.cs # 应用逻辑
├── MainPage.xaml # 主页面布局
├── MainPage.xaml.cs # 主页面逻辑
├── Platforms/ # 平台特定代码
│ ├── Android/
│ ├── iOS/
│ ├── MacCatalyst/
│ └── Windows/
└── Resources/ # 共享资源
├── Fonts/
├── Images/
└── AppIcon/
最简单的页面示例如下:
<!-- MainPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyFirstApp.MainPage">
<VerticalStackLayout Spacing="20" Padding="30">
<Label Text="Hello MAUI!"
FontSize="32"
HorizontalOptions="Center" />
<Button Text="点击我"
Clicked="OnButtonClicked"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ContentPage>
// MainPage.xaml.cs
namespace MyFirstApp;
public partial class MainPage : ContentPage
{
private int _clickCount = 0;
public MainPage()
{
InitializeComponent();
}
// 按钮点击事件处理
private void OnButtonClicked(object sender, EventArgs e)
{
_clickCount++;
DisplayAlert("提示", $"你点击了 {_clickCount} 次", "确定");
}
}
详细用法
布局系统
MAUI 提供了多种布局容器来组织界面元素:
<!-- 垂直堆叠布局 -->
<VerticalStackLayout Spacing="10" Padding="20">
<Label Text="第一行" />
<Label Text="第二行" />
</VerticalStackLayout>
<!-- 水平堆叠布局 -->
<HorizontalStackLayout Spacing="10">
<Label Text="左侧" />
<Label Text="右侧" />
</HorizontalStackLayout>
<!-- 网格布局 -->
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,*">
<Label Text="左上" Grid.Row="0" Grid.Column="0" />
<Label Text="右上" Grid.Row="0" Grid.Column="1" />
<Label Text="左下" Grid.Row="1" Grid.Column="0" />
<Label Text="右下" Grid.Row="1" Grid.Column="1" />
</Grid>
<!-- 绝对布局 -->
<AbsoluteLayout>
<BoxView Color="Red"
AbsoluteLayout.LayoutBounds="0,0,100,100" />
<BoxView Color="Blue"
AbsoluteLayout.LayoutBounds="50,50,100,100" />
</AbsoluteLayout>
导航
MAUI 支持基于页面的导航,最常用的是 Shell 导航:
// App.xaml.cs 中注册路由
public partial class App : Application
{
public App()
{
InitializeComponent();
// 使用 Shell 作为主页面
MainPage = new AppShell();
}
}
<!-- AppShell.xaml -->
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyFirstApp"
x:Class="MyFirstApp.AppShell">
<!-- 底部标签页 -->
<TabBar>
<ShellContent Title="首页"
Icon="home.png"
ContentTemplate="{DataTemplate local:MainPage}" />
<ShellContent Title="设置"
Icon="settings.png"
ContentTemplate="{DataTemplate local:SettingsPage}" />
</TabBar>
</Shell>
// 代码中进行页面导航
await Shell.Current.GoToAsync("//details?id=42");
// 目标页面接收参数
[QueryProperty(nameof(ItemId), "id")]
public partial class DetailPage : ContentPage
{
private string _itemId;
public string ItemId
{
get => _itemId;
set { _itemId = value; LoadItem(value); }
}
private void LoadItem(string id)
{
// 根据 id 加载数据
}
}
数据绑定与 MVVM
MVVM 模式是 MAUI 推荐的架构模式,它将界面、逻辑和数据分离:
// ViewModel 基类,实现属性变更通知
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
// 通用属性设置方法,自动触发通知
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string name = "")
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(name);
return true;
}
}
// 具体 ViewModel
public class MainViewModel : ViewModelBase
{
private string _greeting = "你好,MAUI";
public string Greeting
{
get => _greeting;
set => SetProperty(ref _greeting, value);
}
// 命令绑定
public ICommand UpdateCommand { get; }
public MainViewModel()
{
UpdateCommand = new Command(UpdateGreeting);
}
private void UpdateGreeting()
{
Greeting = "更新后的问候";
}
}
<!-- 在 XAML 中绑定 ViewModel -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MyFirstApp.ViewModels"
x:Class="MyFirstApp.MainPage"
x:DataType="vm:MainViewModel">
<VerticalStackLayout Padding="30">
<!-- 绑定属性 -->
<Label Text="{Binding Greeting}"
FontSize="24" />
<!-- 绑定命令 -->
<Button Text="更新"
Command="{Binding UpdateCommand}" />
</VerticalStackLayout>
</ContentPage>
// 在代码隐藏中设置绑定上下文
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = new MainViewModel();
}
}
平台特定代码
当需要处理平台差异时,可以使用条件编译或部分类:
// 在 Platforms/Android/PlatformClass.android.cs 中
#if ANDROID
public static string GetPlatformName() => "Android";
#endif
// 在 Platforms/iOS/PlatformClass.ios.cs 中
#if IOS
public static string GetPlatformName() => "iOS";
#endif
// 也可以通过部分类实现
// Shared/PlatformService.cs
public partial class PlatformService
{
// 共享代码
public string GetInfo() => $"运行在 {GetPlatformName()}";
}
// 部分方法由各平台实现
public partial class PlatformService
{
partial string GetPlatformName();
}
自定义 Handler
当内置控件不能满足需求时,可以通过 Handler 自定义原生控件的行为:
// 定义自定义控件
public class CustomEntry : Entry
{
// 自定义属性
public static readonly BindableProperty BorderColorProperty =
BindableProperty.Create(nameof(BorderColor), typeof(Color), typeof(CustomEntry), Colors.Gray);
public Color BorderColor
{
get => (Color)GetValue(BorderColorProperty);
set => SetValue(BorderColorProperty, value);
}
}
// 定义 Handler
public class CustomEntryHandler : EntryHandler
{
public CustomEntryHandler() : base(Mapper)
{
}
// 属性映射:当 BorderColor 变化时更新原生控件
public static IPropertyMapper<CustomEntry, CustomEntryHandler> Mapper =
new PropertyMapper<CustomEntry, CustomEntryHandler>(EntryHandler.Mapper)
{
[nameof(CustomEntry.BorderColor)] = MapBorderColor
};
private static void MapBorderColor(CustomEntryHandler handler, CustomEntry entry)
{
#if ANDROID
// Android 平台设置边框颜色
handler.PlatformView.BackgroundTintList =
Android.Content.Res.ColorStateList.ValueOf(entry.BorderColor.ToPlatform());
#elif IOS
// iOS 平台设置边框颜色
handler.PlatformView.Layer.BorderColor = entry.BorderColor.ToPlatform().CGColor;
handler.PlatformView.Layer.BorderWidth = 1;
#endif
}
}
常见场景
列表页面
展示数据列表是最常见的场景之一:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyFirstApp.ListPage">
<CollectionView ItemsSource="{Binding Items}"
SelectionMode="Single"
SelectionChangedCommand="{Binding ItemSelectedCommand}"
SelectedItem="{Binding SelectedItem}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame Margin="10" Padding="15">
<HorizontalStackLayout Spacing="15">
<Image Source="{Binding ImageUrl}"
WidthRequest="60" HeightRequest="60" />
<VerticalStackLayout>
<Label Text="{Binding Title}" FontSize="18" />
<Label Text="{Binding Subtitle}" FontSize="14" />
</VerticalStackLayout>
</HorizontalStackLayout>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
偏好设置存储
使用 Preferences 存储简单的键值对数据:
// 保存设置
Preferences.Set("username", "张三");
Preferences.Set("dark_mode", true);
Preferences.Set("font_size", 14.0);
// 读取设置,可指定默认值
string username = Preferences.Get("username", "默认用户");
bool darkMode = Preferences.Get("dark_mode", false);
double fontSize = Preferences.Get("font_size", 12.0);
// 检查键是否存在
bool hasKey = Preferences.ContainsKey("username");
// 删除某个键
Preferences.Remove("username");
// 清除所有设置
Preferences.Clear();
注意事项
首次运行较慢:MAUI 项目首次编译和部署可能需要较长时间,尤其是 Android 平台。这是正常的,后续运行会快很多。
热重载限制:XAML 热重载对布局修改支持较好,但 C# 代码修改通常需要重新编译。建议先设计好界面再编写逻辑。
平台差异:虽然 MAUI 尽量统一了各平台的 API,但某些功能在不同平台上的行为可能不同。例如状态栏颜色、导航栏样式等需要分别处理。
性能优化:避免在循环中创建大量对象,使用 CollectionView 代替 ListView 处理大数据列表,合理使用虚拟化。
Android SDK 版本:确保安装了正确版本的 Android SDK。MAUI 通常要求较新的 API Level,具体版本随 .NET 版本更新。
进阶用法
使用 CommunityToolkit.Mvvm
社区工具包提供了更简洁的 MVVM 写法,减少样板代码:
// 使用源生成器自动生成绑定代码
public partial class MainViewModel : ObservableObject
{
// 自动生成属性变更通知代码
[ObservableProperty]
private string _greeting = "你好";
// 自动生成命令代码
[RelayCommand]
private void UpdateGreeting()
{
Greeting = "更新后的问候";
}
// 带条件的命令
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save() { /* 保存逻辑 */ }
private bool CanSave() => !string.IsNullOrEmpty(Greeting);
}
集成 EF Core 数据库
在 MAUI 应用中使用 SQLite 数据库:
// 定义数据上下文
public class AppDbContext : DbContext
{
public DbSet<TodoItem> TodoItems => Set<TodoItem>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// 使用应用数据目录存放数据库文件
var dbPath = Path.Combine(
FileSystem.AppDataDirectory, "app.db");
options.UseSqlite($"Data Source={dbPath}");
}
}
public class TodoItem
{
public int Id { get; set; }
public string Title { get; set; } = "";
public bool IsDone { get; set; }
}
多窗口支持
在桌面平台上支持多窗口:
// 打开新窗口
var window = new Window(new DetailPage());
Application.Current.OpenWindow(window);
// 关闭当前窗口
Application.Current.CloseWindow(Window);