MAUI :通过自定义附加属性实现事件与命令的绑定

一、需求背景与实现思路

重写旧项目过程中,发现 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 附加属性关键差异

虽然两者核心逻辑(静态属性 + 事件关联 + 数据绑定)一致,但在基类、注册方法等细节上存在差异,跨框架迁移时需重点适配:

对比维度MAUIAvalonia
核心基类基于 BindableObject(所有可绑定对象的基类)基于 AvaloniaObject(Avalonia 可绑定对象基类)
附加属性注册方法BindableProperty.CreateAttachedAvaloniaProperty.RegisterAttached
控件加载事件VisualElement.Loaded(作用于可视化控件)Control.LoadedEvent(作用于 Control 子类)

适配建议

  1. MAUI 中,自定义附加属性可作用于 VisualElement 及其子类(如 PageButtonCollectionView);
  2. Avalonia 中,需将事件源改为 Control 及其子类,避免因基类不匹配导致事件绑定失败。

五、跨平台复用优势

自定义的附加属性基于 MAUI 原生 API 开发,无需额外修改即可在 Android、iOS、Windows、macOS 等平台生效。例如本文中的 LoadedCommand,在不同平台的页面加载时,均能稳定触发 ViewModel 中的 InitCommand,实现 “一次开发,多端复用”。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值