Estou estudando Avalonia e experimentando o incrível programação reativa com ReactiveUI. Durante essa experiência, surgiu uma necessidade de vincular comandos a eventos de controles. Normalmente, instala-se os pacotes: install-package xaml.behaviors ou install-package xaml.behaviors.interactions, ambos atendem à necessidade.
<Interaction.Behaviors>
<EventTriggerBehavior EventName="Loaded">
<InvokeCommandAction Command="{Binding InitCommand}"/>
</EventTrigger.Behavior>
</Interaction.Behaviors>
Se perguntar ao Deepseek, Douba, Yuanbao, eles usam pacotes obsoletos. A forma correta é como eu estou usando agora. Também podemos usar propriedades anexadas para alcançar esse objetivo. Crie uma pasta chamada Behaviors no projeto e adicione um arquivo chamado LoadedBehavior.cs, que deve herdar de AvaloniaObject. O código é o seguinte.
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Interactivity;
using System.Windows.Input;
namespace AttachedPropertyDemo.Behaviors
{
public class LoadedBehavior : AvaloniaObject
{
static LoadedBehavior()
{
ExecuteCommandOnLoadProperty.Changed.AddClassHandler<Interactive>(OnExecuteCommandOnLoadChanged);
}
public static readonly AttachedProperty<ICommand> ExecuteCommandOnLoadProperty =
AvaloniaProperty.RegisterAttached<LoadedBehavior, Interactive, ICommand>("ExecuteCommandOnLoad", default, false, BindingMode.OneTime);
public static ICommand? GetExecuteCommandOnLoad(AvaloniaObject element) => element.GetValue(ExecuteCommandOnLoadProperty);
public static void SetExecuteCommandOnLoad(AvaloniaObject element, ICommand value)
{
element.SetValue(ExecuteCommandOnLoadProperty!, value);
}
private static void OnExecuteCommandOnLoadChanged(Interactive element, AvaloniaPropertyChangedEventArgs e)
{
if (e.NewValue is ICommand command)
{
element.AddHandler(Control.LoadedEvent, Handler);
}
else
{
element.RemoveHandler(Control.LoadedEvent, Handler);
}
}
private static void Handler(object? sender, RoutedEventArgs e)
{
if (sender is Interactive element)
{
ICommand command = element.GetValue(ExecuteCommandOnLoadProperty);
if (command?.CanExecute(null) == true)
{
command.Execute(null);
}
}
}
}
}
Criar propriedade anexada dessa maneira é a forma correta, igual ao site oficial. Deepseek, Douba e Yuanbao têm problemas. No meu ViewModel, refleti cores do espaço Avalonia.Media para manter o código mais limpo, é necessário instalar o pacote ReactiveUi.SourceGenerators, que gera código. O código é o seguinte:
using Avalonia.Media;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using System.Collections.ObjectModel;
using System.Linq;
namespace AttachedPropertyDemo.ViewModels
{
public partial class ColorsViewModel : ViewModelBase
{
[Reactive]
private string? _colorName;
[Reactive]
private Color? _color;
public ObservableCollection<ColorsViewModel> Colors { get; } = [];
public ColorsViewModel()
{
}
[ReactiveCommand]
private void Init()
{
var properties = typeof(Colors).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)
.Where(p => p.PropertyType == typeof(Color));
foreach( var property in properties)
{
if(property.GetValue(null) is Color color)
{
Colors.Add(new ColorsViewModel
{
Color = color,
ColorName = property.Name
});
}
}
}
}
}
Ao usar o VS2022 com Avalonia, é importante garantir que os namespaces estejam corretamente referenciados.
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:AttachedPropertyDemo.ViewModels"
xmlns:b="using:AttachedPropertyDemo.Behaviors"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="AttachedPropertyDemo.Views.ColorsView"
x:DataType="vm:ColorsViewModel">
<Grid RowDefinitions="Auto,*" b:LoadedBehavior.ExecuteCommandOnLoad="{Binding InitCommand}">
<TextBlock Text="{Binding Colors.Count,StringFormat='Avalonia.Media Colors:{0}'}"/>
<ScrollViewer Grid.Row="1">
<ItemsControl ItemsSource="{Binding Colors}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Spacing="10" Margin="5">
<Rectangle Width="600" Height="30">
<Rectangle.Fill>
<SolidColorBrush Color="{Binding Color}"/>
</Rectangle.Fill>
</Rectangle>
<TextBlock Text="{Binding ColorName}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>
O padrão gerado é assim: x:Class="AttachedPropertyDemo.ColorsView", devemos alterá-lo manualmente para x:Class="AttachedPropertyDemo.Views.ColorsView", para que o ViewLocator.cs funcione normalmente. Código do MainWindowViewModel
using ReactiveUI.SourceGenerators;
namespace AttachedPropertyDemo.ViewModels
{
public partial class MainWindowViewModel : ViewModelBase
{
[Reactive]
private ViewModelBase? _currentPage;
public MainWindowViewModel()
{
CurrentPage = new ColorsViewModel();
}
}
}
Arquivo MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:AttachedPropertyDemo.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="1024" d:DesignHeight="560"
Width="1024" Height="560"
x:Class="AttachedPropertyDemo.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/avalonia-logo.ico"
Title="AttachedPropertyDemo">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainWindowViewModel/>
</Design.DataContext>
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" Height="100" Background="{DynamicResource PrimaryGradient}">
<TextBlock Text="Executando comandos por propriedades anexadas" Classes="head"/>
</Border>
<TransitioningContentControl Grid.Row="1" Content="{Binding CurrentPage}"/>
</Grid>
</Window>
Defina estilos na App.
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="AttachedPropertyDemo.App"
xmlns:local="using:AttachedPropertyDemo"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Resources>
<SolidColorBrush x:Key="PrimaryBackground">#14172D</SolidColorBrush>
<SolidColorBrush x:Key="PrimaryForeground">#cfcfcf</SolidColorBrush>
<LinearGradientBrush x:Key="PrimaryGradient" StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Offset="0" Color="#111214"/>
<GradientStop Offset="1" Color="#151E3E"/>
</LinearGradientBrush>
</Application.Resources>
<Application.Styles>
<FluentTheme/>
<Style Selector="Grid">
<Setter Property="Background" Value="{DynamicResource PrimaryBackground}"/>
</Style>
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource PrimaryForeground}"/>
</Style>
<Style Selector="TextBlock.head">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="30"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style Selector=":is(TextBlock)">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="5"/>
</Style>
</Application.Styles>
</Application>
Imagem de execução