一、需求背景与实现思路
重写旧项目过程中,发现 MAUI 社区常用的 EventToCommand 工具突然失效。考虑到排查问题耗时且暂不想重新安装依赖包,决定参考 Avalonia 的设计思路,自定义附加属性来实现 “控件事件绑定到 ViewModel 命令” 的核心需求 —— 无需依赖第三方库,仅通过原生 API 完成功能开发,同时保证跨平台兼容性。
二、自定义附加属性核心代码
以下代码针对 VisualElement 的 Loaded 事件设计,可直接复用,关键步骤已添加注释说明逻辑:
using System.Windows.Input;
using Microsoft.Maui.Controls; // MAUI 控件核心命名空间
namespace FileNexus.Behaviours
{
/// <summary>
/// 为 VisualElement 提供 Loaded 事件与命令的绑定能力
/// </summary>
public static class LoadedBehaviors // 修正原文拼写:Behavious → Behaviors
{
/// <summary>
/// 注册附加属性:LoadedCommand(用于绑定 ViewModel 中的命令)
/// </summary>
public static readonly BindableProperty LoadedCommandProperty =
BindableProperty.CreateAttached(
propertyName: "LoadedCommand", // 附加属性名称
returnType: typeof(ICommand), // 属性类型(ICommand 符合 MVVM 命令规范)
declaringType: typeof(LoadedBehaviors), // 所属类
defaultValue: null, // 默认值
defaultBindingMode: BindingMode.OneWay, // 绑定模式(单向:View 接收 ViewModel 命令)
propertyChanged: OnLoadedCommandChanged // 属性值变化时的回调方法
);
/// <summary>
/// 属性值变化回调:绑定/解除绑定 Loaded 事件
/// </summary>
private static void OnLoadedCommandChanged(BindableObject bindable, object oldValue, object newValue)
{
// 确保当前对象是 VisualElement(MAUI 中可视化控件的基类,如 Page、Button 等)
if (bindable is not VisualElement element) return;
// newValue 不为空:绑定事件;为空:解除旧事件绑定(避免内存泄漏)
if (newValue is ICommand command)
{
element.Loaded += Handler;
}
else
{
element.Loaded -= Handler;
}
}
/// <summary>
/// 附加属性的 Get 方法(遵循 MAUI 附加属性命名规范)
/// </summary>
public static ICommand GetLoadedCommand(BindableObject bindable)
{
return (ICommand)bindable.GetValue(LoadedCommandProperty);
}
/// <summary>
/// 附加属性的 Set 方法(供 XAML 或代码中设置命令)
/// </summary>
public static void SetLoadedCommand(BindableObject bindable, ICommand command)
{
bindable.SetValue(LoadedCommandProperty, command);
}
/// <summary>
/// Loaded 事件处理器:执行绑定的命令
/// </summary>
private static void Handler(object? sender, EventArgs e)
{
if (sender is not VisualElement visualElement) return;
// 获取控件上绑定的命令
var command = (ICommand)visualElement.GetValue(LoadedCommandProperty);
// 检查命令是否可执行,避免空引用或非法调用
if (command.CanExecute(null))
{
command.Execute(null); // 此处可根据需求传递参数(如 e 或 sender)
}
}
}
}
三、XAML 中使用附加属性
在页面 XAML 中引入自定义行为命名空间,将 LoadedCommand 绑定到 ViewModel 的 InitCommand(页面加载时自动执行初始化逻辑),完整代码如下:
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:b="clr-namespace:FileNexus.Behaviours" <!-- 引入自定义行为命名空间 -->
<!-- 绑定 Loaded 事件命令:页面加载时执行 InitCommand -->
b:LoadedBehaviors.LoadedCommand="{Binding InitCommand}"
x:Class="FileNexus.Views.FileCollectionPage"
Title="文件列表"
BackgroundColor="Teal"
NavigatedTo="ContentPage_NavigatedTo">
<!-- 工具栏:绑定 ViewModel 中的操作命令 -->
<ContentPage.ToolbarItems>
<ToolbarItem Text="删除" Command="{Binding RemoveItemCommand}"/>
<ToolbarItem Text="发文" Command="{Binding SendTextCommand}"/>
<ToolbarItem Text="回退" Command="{Binding GoBackCommand}"/>
</ContentPage.ToolbarItems>
<!-- 主布局:按功能分区(加载状态、路径信息、文件列表) -->
<Grid RowDefinitions="Auto,Auto,Auto,*" Padding="10">
<!-- 上传状态指示器:绑定 IsUploading(布尔值)控制显示/隐藏 -->
<ActivityIndicator
IsRunning="{Binding IsUploading}"
IsVisible="{Binding IsUploading}"
Color="Orange"
Grid.Row="0"/>
<!-- 当前路径显示:通过 StringFormat 格式化文本 -->
<Label
Text="{Binding CurrentDirectory, StringFormat='当前路径:{0}'}"
TextColor="White"
FontSize="16"
FontAttributes="Bold"
Grid.Row="1"/>
<!-- 文件数量显示:绑定 WebFiles 集合的 Count 属性 -->
<Label
Text="{Binding WebFiles.Count, StringFormat='项目数:{0}'}"
TextColor="White"
FontSize="16"
Grid.Row="2"/>
<!-- 滑动操作视图:为文件列表添加左滑上传/下载功能 -->
<SwipeView Grid.Row="3">
<SwipeView.LeftItems>
<SwipeItem Text="上传" BackgroundColor="Orange" Command="{Binding UploadCommand}"/>
<SwipeItem Text="下载" BackgroundColor="Chocolate" Command="{Binding DownloadCommand}"/>
</SwipeView.LeftItems>
<!-- 文件列表:绑定 WebFiles 集合,选中项触发命令 -->
<CollectionView
ItemsSource="{Binding WebFiles}"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectedItemCommand}"
<!-- 传递选中项作为命令参数 -->
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}"
x:Name="collectionView">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="5"/>
</CollectionView.ItemsLayout>
<!-- 文件列表项模板:区分文件/文件夹(图标+时间格式不同) -->
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="Auto,*" RowDefinitions="*,*" RowSpacing="2" ColumnSpacing="10">
<!-- 图标:文件用 documents.png,文件夹用 folder.png(通过 DataTrigger 切换) -->
<Image
Source="documents.png"
Aspect="AspectFill"
Grid.Row="0" Grid.Column="0"
WidthRequest="48" HeightRequest="48"
Grid.RowSpan="2">
<Image.Triggers>
<DataTrigger
TargetType="Image"
Binding="{Binding IsDirectory}"
Value="True">
<Setter Property="Source" Value="folder.png"/>
</DataTrigger>
</Image.Triggers>
</Image>
<!-- 文件名:加粗显示 -->
<Label
Text="{Binding FileName}"
TextColor="White"
Grid.Column="1" Grid.Row="0"
FontSize="16"
FontAttributes="Bold"/>
<!-- 时间显示:文件显示修改时间,文件夹显示创建时间 -->
<Label
Text="{Binding LastModifiedTime, StringFormat='修改时间:{0:yyyy-MM-dd HH:mm:ss}'}"
Grid.Column="1" Grid.Row="1"
TextColor="White">
<Label.Triggers>
<DataTrigger
TargetType="Label"
Binding="{Binding IsDirectory}"
Value="True">
<Setter Property="Text" Value="{Binding CreationTime, StringFormat='创建时间:{0:yyyy-MM-dd HH:mm:ss}'}"/>
</DataTrigger>
</Label.Triggers>
</Label>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</SwipeView>
</Grid>
</ContentPage>
四、MAUI 与 Avalonia 附加属性关键差异
虽然两者核心逻辑(静态属性 + 事件关联 + 数据绑定)一致,但在基类、注册方法等细节上存在差异,跨框架迁移时需重点适配:
| 对比维度 | MAUI | Avalonia |
|---|---|---|
| 核心基类 | 基于 BindableObject(所有可绑定对象的基类) | 基于 AvaloniaObject(Avalonia 可绑定对象基类) |
| 附加属性注册方法 | BindableProperty.CreateAttached | AvaloniaProperty.RegisterAttached |
| 控件加载事件 | VisualElement.Loaded(作用于可视化控件) | Control.LoadedEvent(作用于 Control 子类) |
适配建议:
- MAUI 中,自定义附加属性可作用于
VisualElement及其子类(如Page、Button、CollectionView); - Avalonia 中,需将事件源改为
Control及其子类,避免因基类不匹配导致事件绑定失败。
五、跨平台复用优势
自定义的附加属性基于 MAUI 原生 API 开发,无需额外修改即可在 Android、iOS、Windows、macOS 等平台生效。例如本文中的 LoadedCommand,在不同平台的页面加载时,均能稳定触发 ViewModel 中的 InitCommand,实现 “一次开发,多端复用”。
99

被折叠的 条评论
为什么被折叠?



