Avalonia:用 ReactiveUI 的方法绑定数据、事件和命令

Avalonia集成了ReactiveUI,使用它的方法绑定数据、事件和命令很特色,据说可以预防内存泄露的风险。
还是在基础导航的基础上,体验一下,先建ColorsViewModel。

using Avalonia.Data.Converters;
using Avalonia.Media;
using ReactiveUI.SourceGenerators;
using System;
using System.Collections.ObjectModel;
using System.Reflection;

namespace ReactiveUIDemo.ViewModels
{
    public partial class ColorsViewModel : ViewModelBase
    {
        [Reactive]
        private string? _colorName;
        [Reactive]
        private Color? _color;

        public readonly ObservableCollection<ColorsViewModel> Colors = [];
        public ColorsViewModel()
        {

        }

        [ReactiveCommand]
        private void Init()
        {
            var properties = typeof(Colors).GetProperties(BindingFlags.Static | BindingFlags.Public);

            foreach(var property in properties)
            {
                if(property.GetValue(null) is Color color)
                {
                    Colors.Add(new ColorsViewModel
                    {
                        ColorName = property.Name,
                        Color = color
                    });
                }
            }
        }

        public static FuncValueConverter<Color, string> ToHex { get; } = new FuncValueConverter<Color, string>(color =>
        $"#{color.R:X2}{color.G:X2}{color.B:X2}");

        public static FuncValueConverter<Color, string> ToCMYK { get; } = new FuncValueConverter<Color, string>(color =>
        {
            double r = color.R / 255.0;
            double g = color.G / 255.0;
            double b = color.B / 255.0;

            double k = 1 - Math.Max(Math.Max(r, g), b);

            double c = k < 1 ? (1 - r - k) / (1 - k) : 0;
            double m = k < 1 ? (1 - g - k) / (1 - k) : 0;
            double y = k < 1 ? (1 - b - k) / (1 - k) : 0;

            return $"C = {Math.Round(c * 100, 1)}% M = {Math.Round(m * 100, 1)}% Y = {Math.Round(y * 100, 1)}% K = {Math.Round(k * 100, 1)}%";
        });

    }
}

再建ColorsView自定义控件。

<rxui:ReactiveUserControl xmlns="https://github.com/avaloniaui"
			 xmlns:rxui ="http://reactiveui.net"
			 x:TypeArguments ="vm:ColorsViewModel"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
			 xmlns:vm="using:ReactiveUIDemo.ViewModels"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"						  
             x:Class="ReactiveUIDemo.Views.ColorsView">
  <Grid RowDefinitions="Auto,*" x:Name="GridRoot">
	  <TextBlock x:Name="ColorsCountTextBlock" Grid.Row="0" FontSize="16"/>
	  <ScrollViewer Grid.Row="1">
		  <ItemsControl x:Name="ColorsItemsControl">
			  <ItemsControl.ItemTemplate>
				  <DataTemplate DataType="vm:ColorsViewModel">
					  <StackPanel Orientation="Horizontal" Spacing="5">
						  <Rectangle Width="300" Height="30">
							  <Rectangle.Fill>
								  <SolidColorBrush Color="{Binding Color}"/>
							  </Rectangle.Fill>
						  </Rectangle>
						  <TextBlock Text="{Binding ColorName}" Width="120"/>
						  <TextBlock Text="{Binding Color,Converter={x:Static vm:ColorsViewModel.ToHex}}" Width="80"/>
						  <TextBlock Text="{Binding Color,Converter={x:Static vm:ColorsViewModel.ToCMYK}}"/>
					  </StackPanel>
				  </DataTemplate>
			  </ItemsControl.ItemTemplate>
		  </ItemsControl>
	  </ScrollViewer>
  </Grid>
</rxui:ReactiveUserControl>

注意要先引用xmlns:rxui ="http://reactiveui.net",然后引用ReactiveUI的控件,rxui:ReactiveUserControl,类型参数必须加上 x:TypeArguments ="vm:ColorsViewModel"。
在ColorsView.axaml.cs文件中绑定数据和事件,很有特色。

using Avalonia.ReactiveUI;
using ReactiveUI;
using ReactiveUIDemo.ViewModels;
using System.Reactive.Disposables;

namespace ReactiveUIDemo.Views;

public partial class ColorsView : ReactiveUserControl<ColorsViewModel>
{    
    public ColorsView()
    {    
        InitializeComponent();
        this.WhenActivated(disposables =>
        {
            this.OneWayBind(ViewModel, vm => vm.Colors.Count, v => v.ColorsCountTextBlock.Text, value => $"Avalonia.Media Colors {value}")
            .DisposeWith(disposables);

            this.BindCommand(ViewModel, vm => vm.InitCommand, v => v.GridRoot, nameof(GridRoot.Loaded))
            .DisposeWith(disposables);

            this.OneWayBind(ViewModel, vm => vm.Colors, v => v.ColorsItemsControl.ItemsSource)
            .DisposeWith(disposables);          
            
        });
    }
}

我们发现绑定命令到事件非常容易,不像Xaml平台,需要引用包或者定义附加属性。this.BindCommand(ViewModel, vm => vm.InitCommand, v => v.GridRoot, nameof(GridRoot.Loaded))这样的写法是为了有智能提示,也可以直接写事件名,this.BindCommand(ViewModel, vm => vm.InitCommand, v => v.GridRoot, “Loaded”)也是可以的。
再建AboutViewModel。

using ReactiveUI.SourceGenerators;
using System.Reflection;

namespace ReactiveUIDemo.ViewModels
{
    public partial class AboutViewModel : ViewModelBase
    {
        [Reactive]
        private string? _appName;
        [Reactive]
        private string? _version;

        public string Message => "这是采用 Avalonia 框架的应用程序,集成 ReactiveUI,使用 ReactiveUI 方法绑定数据、事件和命令。";

        public AboutViewModel()
        {
            this.AppName = Assembly.GetExecutingAssembly().GetName().Name;

            this.Version = Assembly.GetExecutingAssembly()?.GetName()?.Version?.ToString();
        }
    }
}

在Views文件夹下新建AboutView.axaml。

<rxui:ReactiveUserControl xmlns="https://github.com/avaloniaui"
						  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"						   
						  xmlns:rxui="http://reactiveui.net"
						  x:TypeArguments ="vm:AboutViewModel"
						  xmlns:vm="using:ReactiveUIDemo.ViewModels"
						  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
						  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
						  mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
						  x:Class="ReactiveUIDemo.Views.AboutView">
	<StackPanel>
		<StackPanel Orientation="Horizontal">
			<TextBlock x:Name="AppNameTextBlock" FontSize="20" FontWeight="Bold"/>
			<TextBlock x:Name="VersionTextBlock" FontSize="18" FontStyle="Italic"/>
		</StackPanel>
		<TextBlock x:Name="MessageTextBlock" FontSize="16"/>
	</StackPanel>	
</rxui:ReactiveUserControl>

AboutView.axaml.cs代码后台。

using Avalonia.ReactiveUI;
using ReactiveUI;
using ReactiveUIDemo.ViewModels;
using System.Reactive.Disposables;

namespace ReactiveUIDemo.Views;

public partial class AboutView : ReactiveUserControl<AboutViewModel>
{
    public AboutView()
    {
        InitializeComponent();

        this.WhenActivated(disposables =>
        {
            this.WhenAnyValue(x => x.ViewModel!.AppName)
            .BindTo(this, v => v.AppNameTextBlock.Text)
            .DisposeWith(disposables);

            this.OneWayBind(ViewModel, vm => vm.Version, v => v.VersionTextBlock.Text)
            .DisposeWith(disposables);

            this.OneWayBind(ViewModel, vm => vm.Message, v => v.MessageTextBlock.Text)
            .DisposeWith(disposables);
        });
    }
}

在MainWindowViewModel中添加逻辑代码。

using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Splat;
using System.Reactive.Linq;

namespace ReactiveUIDemo.ViewModels
{
    public partial class MainWindowViewModel : ViewModelBase
    {
        [Reactive]
        private ViewModelBase? _currentPage;

        public MainWindowViewModel()
        {
            CurrentPage = Locator.Current.GetService<ColorsViewModel>();

            _isColor = this.WhenAnyValue(x => x.CurrentPage)
                .Select(page => page?.GetType() == typeof(ColorsViewModel))
                .ToProperty(this, x=>x.IsColorPage);

            _isAbout = this.WhenAnyValue(x => x.CurrentPage)
                .Select(page => page?.GetType() == typeof(AboutViewModel))
                .ToProperty(this, x => x.IsAboutPage);
        }

        [ReactiveCommand]
        private void GotoColors()
        {
            if(CurrentPage is not ColorsViewModel)
            {
                CurrentPage = Locator.Current.GetService<ColorsViewModel>();
            }
        }

        [ReactiveCommand]
        private void GotoAbout()
        {
            if(CurrentPage is not AboutViewModel)
            {
                CurrentPage = Locator.Current.GetService<AboutViewModel>();
            }
        }

        private readonly ObservableAsPropertyHelper<bool> _isColor;
        public bool IsColorPage => _isColor.Value;

        private readonly ObservableAsPropertyHelper<bool> _isAbout;
        public bool IsAboutPage => _isAbout.Value;
    }
}

在MainWindow中设计布局。

<rxui:ReactiveWindow xmlns="https://github.com/avaloniaui"
					 x:TypeArguments="vm:MainWindowViewModel"
					 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
					 xmlns:vm="using:ReactiveUIDemo.ViewModels"
					 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
					 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
					 xmlns:rxui="http://reactiveui.net"
					 mc:Ignorable="d" d:DesignWidth="1084" d:DesignHeight="560"
					 Width="1084" Height="560"
					 x:Class="ReactiveUIDemo.Views.MainWindow"
					 x:DataType="vm:MainWindowViewModel"
					 Icon="/Assets/avalonia-logo.ico"
					 Title="ReactiveUIDemo">	

    <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 ColumnDefinitions="Auto,*">
		<Border Grid.Column="0" x:Name="Menu">
			<StackPanel x:Name="StackPanelMenu">
				<TextBlock Text="基  础  导  航" x:Name="Caption"/>
				<Button x:Name="ColorsButton">
					<StackPanel Orientation="Horizontal" Spacing="10">
						<Image Source="/Assets/Images/color.png" Width="32" Height="32"/>
						<TextBlock Text="色  彩"/>
					</StackPanel>
				</Button>
				<Button x:Name="AboutButton">
					<StackPanel Orientation="Horizontal" Spacing="10">
						<Image Source="/Assets/Images/about.png" Width="32" Height="32"/>
						<TextBlock Text="关  于"/>
					</StackPanel>
				</Button>
			</StackPanel>
		</Border>
		<Border Grid.Column="1" x:Name="Client">
			<TransitioningContentControl x:Name="TransitioningContent"/>
		</Border>
	</Grid>

</rxui:ReactiveWindow>

MainWindow.axaml.cs后台代码。

using Avalonia.ReactiveUI;
using ReactiveUI;
using ReactiveUIDemo.ViewModels;
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;

namespace ReactiveUIDemo.Views
{
    public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
    {        
        public MainWindow()
        {
            InitializeComponent();
            this.WhenActivated(disposables => 
            {
                this.OneWayBind(ViewModel, vm => vm.CurrentPage, v => v.TransitioningContent.Content)
                .DisposeWith(disposables);

                this.BindCommand(ViewModel, vm => vm.GotoColorsCommand, v => v.ColorsButton)
                .DisposeWith(disposables);

                this.BindCommand(ViewModel, vm => vm.GotoAboutCommand, v => v.AboutButton)
                .DisposeWith(disposables);

                this.WhenAnyValue(x => x.ViewModel!.IsColorPage)
                .Subscribe(active =>
                {
                    var classes = this.ColorsButton.Classes;
                    if (active)
                    {

                        if (!classes.Contains("active"))
                        {
                            classes.Add("active");
                        }
                    }
                    else
                    {
                        classes.Remove("active");
                    }
                }).DisposeWith(disposables);

                this.WhenAnyValue(x => x.ViewModel!.IsAboutPage)
                .Subscribe(active =>
                {
                    var classes = this.AboutButton.Classes;
                    if (active)
                    {

                        if (!classes.Contains("active"))
                        {
                            classes.Add("active");
                        }
                    }
                    else
                    {
                        classes.Remove("active");
                    }
                }).DisposeWith(disposables);

            });

        }
    }
}

在App.axaml中设计样式

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="ReactiveUIDemo.App"
             xmlns:local="using:ReactiveUIDemo"
             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="PrimaryGradientBackground" StartPoint="0%,0%" EndPoint="100%,0%">
			<GradientStop Offset="0" Color="#111214"/>
			<GradientStop Offset="100" Color="#151E3E"/>
		</LinearGradientBrush>
		<SolidColorBrush x:Key="PrimaryHoverForeground">White</SolidColorBrush>
		<SolidColorBrush x:Key="PrimaryHoverBackground">#334455</SolidColorBrush>
		<SolidColorBrush x:Key="PrimaryActiveBackground">#115599</SolidColorBrush>
	</Application.Resources>
  
    <Application.Styles>
        <FluentTheme />
		<!--设计样式-->
		<Style Selector="Border#Menu">
			<Setter Property="Background" Value="{DynamicResource PrimaryGradientBackground}"/>
			<Setter Property="Padding" Value="10"/>
		</Style>
		<Style Selector="Border#Client">
			<Setter Property="Background" Value="{DynamicResource PrimaryBackground}"/>	
			<Setter Property="Padding" Value="10"/>
		</Style>
		<Style Selector="TextBlock">
			<Setter Property="Foreground" Value="{DynamicResource PrimaryForeground}"/>	
			<Setter Property="Margin" Value="5"/>
			<Setter Property="VerticalAlignment" Value="Center"/> 
		</Style>		
		<Style Selector="TextBlock#Caption">
			<Setter Property="FontSize" Value="28"/>						
			<Setter Property="HorizontalAlignment" Value="Center"/>
		</Style>
		<Style Selector="Button">
			<Setter Property="HorizontalAlignment" Value="Center"/>
			<Setter Property="Margin" Value="5"/>
			<Setter Property="Width" Value="150"/>
			<Setter Property="HorizontalContentAlignment" Value="Center"/>
		</Style>
		<Style Selector="Button /template/ ContentPresenter">
			<Setter Property="Foreground" Value="{DynamicResource PrimaryForeground}"/>
			<Setter Property="FontSize" Value="20"/>			
			<Setter Property="Padding" Value="5"/>
			<Setter Property="Transitions">
				<Transitions>
					<BrushTransition Property="Background" Duration="0:0:0.1"/>
				</Transitions>
			</Setter>
		</Style>
		<Style Selector="Button.active">
			<Setter Property="Background" Value="{DynamicResource PrimaryActiveBackground}"/>
		</Style>
		<Style Selector="Button:pointerover /template/ ContentPresenter">
			<Setter Property="Background" Value="{DynamicResource PrimaryHoverBackground}"/>
			<Setter Property="Foreground" Value="{DynamicResource PrimaryHoverForeground}"/>			
		</Style>
		<Style Selector="StackPanel#StackPanelMenu">
			<Setter Property="Width" Value="200"/>
		</Style>
		<Style Selector="StackPanel > Rectangle">
			<Setter Property="Margin" Value="5"/>
		</Style>		
    </Application.Styles>
</Application>

在App.axaml.cs中使用Splat(斯普拉特)容器。

using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using ReactiveUIDemo.ViewModels;
using ReactiveUIDemo.Views;
using Splat;

namespace ReactiveUIDemo
{
    public partial class App : Application
    {
        public override void Initialize()
        {
            AvaloniaXamlLoader.Load(this);                     
        }

        public override void OnFrameworkInitializationCompleted()
        {
            RegisterViewModel();

            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
            {
                desktop.MainWindow = new MainWindow
                {
                    DataContext = new MainWindowViewModel(),
                };          
            }

            base.OnFrameworkInitializationCompleted(); 
        }

        private void RegisterViewModel()
        {
            Locator.CurrentMutable.Register(() => new ColorsViewModel(), typeof(ColorsViewModel));
            Locator.CurrentMutable.Register(() => new AboutViewModel(), typeof(AboutViewModel));
        }

    }
}

运行效果。

屏幕截图 2025-09-16 234740

屏幕截图 2025-09-16 234759

posted @ 2025-09-16 23:50  孤独的小苗  阅读(236)  评论(1)    收藏  举报