Avalonia: Vinculando comandos a eventos usando propriedades anexadas

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

Tags: Avalonia ReactiveUI propriedades anexadas Comandos Eventos

Publicado em 8-23 16:49