3.5-深入理解XAML:行为Behavior
通过行为Behavior,可以将功能附加到控件上,而不需要在宿主控件上定义,和扩展方法有异曲同功之妙。在MAUI中实现Behavior,有两种方式:①附加行为;②MAUI内置行为。附加行为,通过附加属性方式实现,可以深入理解行为的内在原理;而MAUI的内置行为,封装了实现细节,使用起来非常简洁。
一、附加行为
附加属性可以在非宿主控件类中使用,同时,它也是可绑定属性,可以定义propertyChanged回调函数,在回调函数中可以调用“控件对象、附加属性的新值和旧值”。当控件的附加属性值发生变化时,propertyChanged回调函数执行业务逻辑,从而实现为控件附加功能。这种方式,我们称之为附加行为。
1、定义一个行为类(附加属性方式)(在Behaviors文件夹下)
//在Behaviors文件夹下创建立NumberValidateBehavior行为类
public class NumberValidateBehavior
{
//定义附加属性AttachBehaviorProperty,布尔类型,当值发生改变时propertyChanged,执行OnAttachBehaviorChanged函数
public static readonly BindableProperty AttachBehaviorProperty =
BindableProperty.CreateAttached("AttachBehavior",typeof(bool),typeof(NumberValidateBehavior),false,propertyChanged:OnAttachBehaviorChanged);
public static bool GetAttachBehavior(BindableObject view)
{
return (bool)view.GetValue(AttachBehaviorProperty);
}
public static void SetAttachBehavior(BindableObject view,bool value)
{
view.SetValue(AttachBehaviorProperty, value);
}
//附加属性值发生变化时的回调
//根据附加属性值(true or false),订阅或移除事件TextChanged的处理程序
static void OnAttachBehaviorChanged(BindableObject view,object oldValue,object newValue)
{
//将宿主对象转化为Entry,使用as,要么成功转为Entry,要么返回null。所以,一般行为都只针对特定控件
Entry entry = view as Entry;
if (entry == null)
{
return;
}
//获取附加属性的新值,并根据新值订阅或移除事件处理程序(事件回调)
bool attachBehavior = (bool)newValue;
if (attachBehavior)
{
entry.TextChanged += OnEntryTextChanged;
}
else
{
entry.TextChanged -= OnEntryTextChanged;
}
}
//TextChanged的事件处理程序
//真正给控件附加的功能,如果输入值可以转换为double类型,则字体颜色为黑色,否则为红色
static void OnEntryTextChanged(object sender,TextChangedEventArgs args)
{
double result;
bool isValid = double.TryParse(args.NewTextValue, out result);
((Entry)sender).TextColor = isValid ? Colors.Black : Colors.Red;
}
}
2、使用附加行为
<ContentPage
......
xmlns:behavior="clr-namespace:MauiApp10.Behaviors">
<VerticalStackLayout>
<!--将附加属性NumberValidateBehavior.AttachBehavior赋值为true-->
<Entry Placeholder="请输入数值" behavior:NumberValidateBehavior.AttachBehavior="True"/>
</VerticalStackLayout>
</ContentPage>
3、删除附加行为
<!--将附加属性值更改为false,执行propertyChanged回调-->
<Entry Placeholder="请输入数值" behavior:NumberValidationBehavior.AttachBehavior="false" />
二、内置行为
几乎每个MAUI控件,都有一个Behaviors属性,是派生自Behavior类(或Behavior)的对象集合。Behavior类有两个重写方法OnAttachedTo和OnDetachingFrom,可以将附加到控件上的功能写在这两个方法里。当将Behavior对象添加到集合时,执行OnAttachedTo方法,反之将执行OnDetachingFrom。这种方式,我们称之为内置行为。
1、定义一个行为类(派生自Behavior类方式)
//在Behaviors文件夹中创建SimpleNumberValidateBehavior行为类,派生自Behavior<T>,泛型指定为Entry,行为指定应用于Entry
public class SimpleNumberValidateBehavior: Behavior<Entry>
{
//添加行为时,订阅事件TextChanged的处理程序
protected override void OnAttachedTo(Entry entry)
{
entry.TextChanged += OnEntryTextChanged;
base.OnAttachedTo(entry);
}
//删除行为时,移除事件TextChanged的处理程序
protected override void OnDetachingFrom(Entry entry)
{
entry.TextChanged -= OnEntryTextChanged;
base.OnDetachingFrom(entry);
}
//TextChanged的事件处理程序
//真正给控件附加的功能,如果输入值可以转换为double类型,则字体颜色为黑色,否则为红色
private void OnEntryTextChanged(object sender, TextChangedEventArgs args)
{
double result;
bool isValid = double.TryParse(args.NewTextValue, out result);
((Entry)sender).TextColor = isValid ? Colors.Black : Colors.Red;
}
}
2、使用内置行为
<ContentPage
...
xmlns:behavior="clr-namespace:MauiApp10.Behaviors">
<VerticalStackLayout>
<!--将MAUI内置行为添加到Entry的Behaviors属性集合中-->
<Entry Placeholder="请输入数值">
<Entry.Behaviors>
<behavior:SimpleNumberValidateBehavior/>
</Entry.Behaviors>
</Entry>
</VerticalStackLayout>
</ContentPage>
3、删除内置行为:控件的Behaviors属性是一个集合,所以可以在后台代码中移除或清除添加到集合中的行为对象
三、案例:EventToCommandBehavior
1、MVVM开发模式,我们在ViewModel中定义命令,View的控件绑定命令,如【<Button Text="新增" Command="{Binding AddCommand}"/>
】。但是能够直接触发命令的控件及其事件非常有限,如Button仅限于点击事件可以触发命令,而很多控件无法直接绑定命令的。对于XAML来说,事件是一个非常古老和全面的体系,而Command命令,在UI层面的钩子非常少。解决这个问题的思路,就是利用行为Behavior,当控件的某个事件发生时,触发命令。
2、在CommunityToolkit.MAUI社区工具包有,提供了一个EventToCommandBehavior,它的使用很简单,
如下所示。
<!--第一步:安装NuGet包 CommunityToolkit.Maui-->
<!--第二步:使用事件转命令行为-->
<ContentPage
......
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit">
<Button>
<Button.Behaviors>
<toolkit:EventToCommandBehavior
EventName="Clicked"
Command="{Binding MyCustomCommand}" />
</Button.Behaviors>
</Button>
</ContentPage>
3、解读EventToCommandBehavior源码
public class EventToCommandBehavior : BaseBehavior<VisualElement>
{
//=======================================================================================================================================
//定义MethodInfo,方法信息
readonly MethodInfo eventHandlerMethodInfo = typeof(EventToCommandBehavior).GetTypeInfo()?.GetDeclaredMethod(nameof(OnTriggerHandled)) ?? throw new InvalidOperationException($"Cannot find method {nameof(OnTriggerHandled)}");
//定义EventInfo,事件信息
EventInfo? eventInfo;
//定义事件处理委托
Delegate? eventHandler;
//可绑定属性===============================================================================================================================
//可绑定属性EventName-事件
public static readonly BindableProperty EventNameProperty =
BindableProperty.Create(nameof(EventName), typeof(string), typeof(EventToCommandBehavior), propertyChanged: OnEventNamePropertyChanged);
public string? EventName
{
get => (string?)GetValue(EventNameProperty);
set => SetValue(EventNameProperty, value);
}
//可绑定属性Command-命令
public static readonly BindableProperty CommandProperty =
BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(EventToCommandBehavior));
public ICommand? Command
{
get => (ICommand?)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
//可绑定属性CommandParameter-命令参数
public static readonly BindableProperty CommandParameterProperty = ......
//可绑定属性EventArgs-事件参数
public static readonly BindableProperty EventArgsConverterProperty =......
//内置行为的OnAttachedTo和OnDetachingFrom函数===============================================================================================
//添加行为时,执行RegisterEvent函数,注册事件
protected override void OnAttachedTo(VisualElement bindable)
{
base.OnAttachedTo(bindable);
RegisterEvent();
}
//移除行为时,执行UnregisterEvent函数,注销事件
protected override void OnDetachingFrom(VisualElement bindable)
{
UnregisterEvent();
base.OnDetachingFrom(bindable);
}
//可绑定属性EventName的值更改时,执行RegisterEvent函数,注册事件
static void OnEventNamePropertyChanged(BindableObject bindable, object oldValue, object newValue)
=> ((EventToCommandBehavior)bindable).RegisterEvent();
//RegisterEvent和UnregisterEven注册和注销事件逻辑,eventHandler在事件和命令之间建立委托=========================================================
void RegisterEvent()
{
UnregisterEvent();
var eventName = EventName;
if (View is null || string.IsNullOrWhiteSpace(eventName))
{
return;
}
//获取eventInfo
eventInfo = View.GetType()?.GetRuntimeEvent(eventName) ??
throw new ArgumentException($"{nameof(EventToCommandBehavior)}: Couldn't resolve the event.", nameof(EventName));
ArgumentNullException.ThrowIfNull(eventInfo.EventHandlerType);
ArgumentNullException.ThrowIfNull(eventHandlerMethodInfo);
//******添加一个事件处理方法eventHandler,eventHandlerMethodInfo为触发命令的方法******
eventHandler = eventHandlerMethodInfo.CreateDelegate(eventInfo.EventHandlerType, this) ??
throw new ArgumentException($"{nameof(EventToCommandBehavior)}: Couldn't create event handler.", nameof(EventName));
eventInfo.AddEventHandler(View, eventHandler);
}
void UnregisterEvent()
{
//移处事件处理方法enentHandler,并将eventInfo和eventHandler设置为null
if (eventInfo is not null && eventHandler is not null)
{
eventInfo.RemoveEventHandler(View, eventHandler);
}
eventInfo = null;
eventHandler = null;
}
//定义虚方法OnTriggerHandled,触发命令========================================================================================================
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
protected virtual void OnTriggerHandled(object? sender = null, object? eventArgs = null)
{
var parameter = CommandParameter
?? EventArgsConverter?.Convert(eventArgs, typeof(object), null, null);
var command = Command;
if (command?.CanExecute(parameter) ?? false)
{
command.Execute(parameter);
}
}
}