学无先后达者为师!
不忘初心,砥砺前行。

Avalonia 使用 Tag + Style 选择器实现状态驱动 UI

在开发 Avalonia 应用时,我们经常需要根据数据的不同状态展示不同的 UI。比如:

  • 任务的状态(等待、进行中、完成、失败)
  • 用户的权限级别(普通用户、VIP、管理员)
  • 消息的类型(信息、警告、错误)

传统的做法可能是使用多个 IsVisible 绑定,或者编写复杂的 IDataTemplate 实现。今天我要介绍一种更优雅的方案:枚举 + Tag 绑定 + Style 选择器

这种模式的核心优势:

  • 纯 XAML 实现,无需额外 C# 代码
  • 性能优秀,只渲染当前状态的 UI
  • 易于维护,新增状态只需添加一个 Style
  • 类型安全,使用枚举避免魔法字符串

核心原理

这个模式基于 Avalonia 的三个特性:

  1. Tag 属性:每个控件都有一个 Tag 属性,可以存储任意对象
  2. 属性选择器:Style 可以通过 [Property=Value] 语法匹配特定属性值
  3. 动态模板切换:通过 Style Setter 动态改变 ContentTemplate

工作流程:

枚举状态 → 绑定到 Tag → Style 选择器匹配 → 应用对应的 DataTemplate

完整示例:任务状态管理器

让我们构建一个完整的示例,展示如何使用这个模式。

1. 定义状态枚举

namespace AvaloniaStateDemo.Models
{
    public enum TaskStatus
    {
        Waiting,      // 等待中
        Uploading,    // 上传中
        Processing,   // 处理中
        Success,      // 成功
        Failed        // 失败
    }
}

2. 创建 ViewModel

TaskViewModel

using CommunityToolkit.Mvvm.ComponentModel;
using System;

namespace AvaloniaStateDemo.ViewModels
{
    public partial class TaskViewModel : ObservableObject
    {
        [ObservableProperty]
        private string _name = string.Empty;

        [ObservableProperty]
        private TaskStatus _status;

        [ObservableProperty]
        private int _progress;

        [ObservableProperty]
        private string _errorMessage = string.Empty;

        public TaskViewModel(string name, TaskStatus status)
        {
            Name = name;
            Status = status;
        }

        // 模拟状态变化
        public void SimulateProgress()
        {
            Status = TaskStatus.Uploading;
            Progress = 0;

            // 实际项目中这里会是真实的异步操作
            var timer = new System. Timers.Timer(100);
            timer.Elapsed += (s, e) =>
            {
                Progress += 5;
                if (Progress >= 50 && Status == TaskStatus.Uploading)
                {
                    Status = TaskStatus.Processing;
                }
                if (Progress >= 100)
                {
                    timer.Stop();
                    // 随机成功或失败
                    if (Random.Shared.Next(2) == 0)
                    {
                        Status = TaskStatus.Success;
                    }
                    else
                    {
                        Status = TaskStatus.Failed;
                        ErrorMessage = "网络连接超时";
                    }
                }
            };
            timer. Start();
        }
    }
}

MainViewModel

using CommunityToolkit.Mvvm.ComponentModel;
using System. Collections.ObjectModel;

namespace AvaloniaStateDemo. ViewModels
{
    public partial class MainViewModel : ObservableObject
    {
        public ObservableCollection<TaskViewModel> Tasks { get; } = new();

        public MainViewModel()
        {
            // 添加示例任务
            Tasks.Add(new TaskViewModel("文档上传任务", TaskStatus.Waiting));
            Tasks.Add(new TaskViewModel("图片处理任务", TaskStatus. Uploading) { Progress = 35 });
            Tasks.Add(new TaskViewModel("视频转码任务", TaskStatus.Processing) { Progress = 78 });
            Tasks.Add(new TaskViewModel("数据备份任务", TaskStatus.Success));
            Tasks.Add(new TaskViewModel("文件同步任务", TaskStatus.Failed) 
            { 
                ErrorMessage = "目标服务器无响应" 
            });
        }

        public void AddNewTask()
        {
            var task = new TaskViewModel($"任务 #{Tasks.Count + 1}", TaskStatus.Waiting);
            Tasks.Add(task);
            task.SimulateProgress();
        }
    }
}

3. 创建状态驱动的 UI(核心部分)

MainWindow.axaml


<Window xmlns="https://github.com/avaloniaui"
        xmlns: x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:AvaloniaStateDemo.ViewModels"
        xmlns:models="using:AvaloniaStateDemo.Models"
        x:Class="AvaloniaStateDemo.Views.MainWindow"
        x:DataType="vm:MainViewModel"
        Width="700" Height="500"
        Title="Avalonia 状态驱动 UI 示例">

    <Design.DataContext>
        <vm:MainViewModel />
    </Design.DataContext>

    <Window.Styles>
        <!-- 基础样式 -->
        <Style Selector="ContentControl.taskStatus">
            <Setter Property="HorizontalAlignment" Value="Stretch" />
            <Setter Property="Padding" Value="12,8" />
        </Style>

        <!-- 等待状态 -->
        <Style Selector="ContentControl.taskStatus[Tag=Waiting]">
            <Setter Property="ContentTemplate">
                <DataTemplate x:DataType="vm:TaskViewModel">
                    <Border Background="#FFF3E0" 
                            CornerRadius="4"
                            Padding="12,8">
                        <StackPanel Orientation="Horizontal" Spacing="8">
                            <PathIcon Data="M12,1A11,11 0 0,0 1,12A11,11 0 0,0 12,23A11,11 0 0,0 23,12A11,11 0 0,0 12,1M12,3A9,9 0 0,1 21,12A9,9 0 0,1 12,21A9,9 0 0,1 3,12A9,9 0 0,1 12,3M12.5,8H11V14L15.75,16.85L16.5,15.62L12.5,13.25V8Z"
                                      Width="20" Height="20"
                                      Foreground="#F57C00" />
                            <TextBlock Text="{Binding Name}" 
                                       VerticalAlignment="Center"
                                       FontWeight="Medium" />
                            <TextBlock Text="等待中..." 
                                       Foreground="#F57C00"
                                       VerticalAlignment="Center" />
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </Setter>
        </Style>

        <!-- 上传中状态 -->
        <Style Selector="ContentControl.taskStatus[Tag=Uploading]">
            <Setter Property="ContentTemplate">
                <DataTemplate x: DataType="vm:TaskViewModel">
                    <Border Background="#E3F2FD" 
                            CornerRadius="4"
                            Padding="12,8">
                        <StackPanel Spacing="8">
                            <StackPanel Orientation="Horizontal" Spacing="8">
                                <PathIcon Data="M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z"
                                          Width="20" Height="20"
                                          Foreground="#1976D2" />
                                <TextBlock Text="{Binding Name}" 
                                           VerticalAlignment="Center"
                                           FontWeight="Medium" />
                                <TextBlock Text="{Binding Progress, StringFormat='上传中 {0}%'}" 
                                           Foreground="#1976D2"
                                           VerticalAlignment="Center" />
                            </StackPanel>
                            <ProgressBar Value="{Binding Progress}" 
                                         Maximum="100"
                                         Height="6"
                                         Foreground="#1976D2" />
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </Setter>
        </Style>

        <!-- 处理中状态 -->
        <Style Selector="ContentControl. taskStatus[Tag=Processing]">
            <Setter Property="ContentTemplate">
                <DataTemplate x:DataType="vm:TaskViewModel">
                    <Border Background="#F3E5F5" 
                            CornerRadius="4"
                            Padding="12,8">
                        <StackPanel Spacing="8">
                            <StackPanel Orientation="Horizontal" Spacing="8">
                                <PathIcon Data="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z"
                                          Width="20" Height="20"
                                          Foreground="#7B1FA2">
                                    <PathIcon.RenderTransform>
                                        <RotateTransform />
                                    </PathIcon. RenderTransform>
                                    <PathIcon. Styles>
                                        <Style Selector="PathIcon">
                                            <Style.Animations>
                                                <Animation Duration="0:0:1" IterationCount="INFINITE">
                                                    <KeyFrame Cue="0%">
                                                        <Setter Property="(PathIcon.RenderTransform).(RotateTransform.Angle)" Value="0" />
                                                    </KeyFrame>
                                                    <KeyFrame Cue="100%">
                                                        <Setter Property="(PathIcon.RenderTransform).(RotateTransform.Angle)" Value="360" />
                                                    </KeyFrame>
                                                </Animation>
                                            </Style. Animations>
                                        </Style>
                                    </PathIcon.Styles>
                                </PathIcon>
                                <TextBlock Text="{Binding Name}" 
                                           VerticalAlignment="Center"
                                           FontWeight="Medium" />
                                <TextBlock Text="处理中..." 
                                           Foreground="#7B1FA2"
                                           VerticalAlignment="Center" />
                            </StackPanel>
                            <ProgressBar IsIndeterminate="True" 
                                         Height="6"
                                         Foreground="#7B1FA2" />
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </Setter>
        </Style>

        <!-- 成功状态 -->
        <Style Selector="ContentControl.taskStatus[Tag=Success]">
            <Setter Property="ContentTemplate">
                <DataTemplate x:DataType="vm:TaskViewModel">
                    <Border Background="#E8F5E9" 
                            CornerRadius="4"
                            Padding="12,8">
                        <StackPanel Orientation="Horizontal" Spacing="8">
                            <PathIcon Data="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M11,16. 5L6.5,12L7.91,10.59L11,13.67L16.59,8.09L18,9.5L11,16.5Z"
                                      Width="20" Height="20"
                                      Foreground="#388E3C" />
                            <TextBlock Text="{Binding Name}" 
                                       VerticalAlignment="Center"
                                       FontWeight="Medium" />
                            <TextBlock Text="✓ 完成" 
                                       Foreground="#388E3C"
                                       FontWeight="SemiBold"
                                       VerticalAlignment="Center" />
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </Setter>
        </Style>

        <!-- 失败状态 -->
        <Style Selector="ContentControl.taskStatus[Tag=Failed]">
            <Setter Property="ContentTemplate">
                <DataTemplate x: DataType="vm:TaskViewModel">
                    <Border Background="#FFEBEE" 
                            CornerRadius="4"
                            Padding="12,8">
                        <StackPanel Spacing="6">
                            <StackPanel Orientation="Horizontal" Spacing="8">
                                <PathIcon Data="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M12.5,7H11V13H12.5V7M11,15V16.5H12.5V15H11Z"
                                          Width="20" Height="20"
                                          Foreground="#D32F2F" />
                                <TextBlock Text="{Binding Name}" 
                                           VerticalAlignment="Center"
                                           FontWeight="Medium" />
                                <TextBlock Text="✗ 失败" 
                                           Foreground="#D32F2F"
                                           FontWeight="SemiBold"
                                           VerticalAlignment="Center" />
                            </StackPanel>
                            <TextBlock Text="{Binding ErrorMessage}" 
                                       Foreground="#C62828"
                                       FontSize="12"
                                       Margin="28,0,0,0" />
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </Setter>
        </Style>
    </Window.Styles>

    <DockPanel Margin="20">
        <!-- 顶部标题栏 -->
        <Border DockPanel.Dock="Top" 
                Background="#6200EA"
                CornerRadius="8"
                Padding="20"
                Margin="0,0,0,20">
            <StackPanel Spacing="8">
                <TextBlock Text="任务管理器" 
                           FontSize="24"
                           FontWeight="Bold"
                           Foreground="White" />
                <TextBlock Text="演示:Tag + Style 选择器实现状态驱动 UI" 
                           FontSize="14"
                           Foreground="#E1BEE7" />
            </StackPanel>
        </Border>

        <!-- 操作按钮 -->
        <Button DockPanel.Dock="Bottom"
                Content="➕ 添加新任务"
                HorizontalAlignment="Center"
                Padding="16,8"
                Margin="0,20,0,0"
                Command="{Binding AddNewTask}" />

        <!-- 任务列表 -->
        <ScrollViewer>
            <ItemsControl ItemsSource="{Binding Tasks}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel Spacing="12" />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>

                <ItemsControl.ItemTemplate>
                    <DataTemplate x:DataType="vm:TaskViewModel">
                        <!-- 🔑 核心:ContentControl + Tag 绑定 + Class -->
                        <ContentControl Classes="taskStatus"
                                        Tag="{Binding Status}"
                                        Content="{Binding}" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>
    </DockPanel>
</Window>

运行效果

运行这个示例,你会看到:

  • 🟠 等待中:橙色背景,时钟图标
  • 🔵 上传中:蓝色背景,上传图标 + 进度条
  • 🟣 处理中:紫色背景,旋转的加载图标 + 不确定进度条
  • 🟢 成功:绿色背景,对勾图标
  • 🔴 失败:红色背景,错误图标 + 错误信息

点击”添加新任务”按钮,会创建新任务并自动模拟状态变化过程。

深入理解:工作原理

1. Tag 属性的作用

<ContentControl Tag="{Binding Status}" />

Tag 属性充当”状态标识符”,存储当前的枚举值。

2. Style 选择器语法

<Style Selector="ContentControl.taskStatus[Tag=Waiting]">

这个选择器的含义:

  • ContentControl:匹配 ContentControl 类型
  • .taskStatus:必须有 taskStatus 这个 CSS 类
  • [Tag=Waiting]:Tag 属性值必须等于 Waiting 枚举值

3. 优先级和匹配规则

Avalonia 会自动比较 Tag 的值和枚举成员:

  • ✅ 使用 == 运算符比较
  • ✅ 支持任何实现了 Equals 的类型
  • ✅ 枚举会自动转换为其名称进行匹配

与其他方案的对比

方案代码量性能可维护性类型安全
Tag + Style⭐⭐⭐⭐⭐ 少⭐⭐⭐⭐⭐ 优秀⭐⭐⭐⭐⭐ 极佳⭐⭐⭐⭐⭐ 强
IDataTemplate⭐⭐⭐ 中⭐⭐⭐⭐⭐ 优秀⭐⭐⭐ 一般⭐⭐⭐⭐⭐ 强
IsVisible 绑定⭐⭐ 多⭐⭐ 差⭐⭐ 差⭐⭐⭐⭐ 较强
ValueConverter⭐⭐⭐⭐ 较少⭐⭐⭐⭐ 良好⭐⭐⭐ 一般⭐⭐⭐ 一般

实际应用场景

这个模式非常适合:

  1. 任务/作业状态展示(本文示例)
  2. 用户角色权限 UI
   public enum UserRole { Guest, Member, VIP, Admin }
  1. 订单状态跟踪
   public enum OrderStatus { Pending, Paid, Shipped, Delivered, Cancelled }
  1. 文件上传状态
   public enum UploadState { Idle, Uploading, Uploaded, Failed }
  1. 网络连接状态
   public enum ConnectionState { Disconnected, Connecting, Connected, Error }

总结

Tag + Style 选择器模式是 Avalonia 中实现状态驱动 UI 的最佳实践之一。它结合了:

  • 🎯 简洁性:纯 XAML,无需额外 C# 代码
  • 高性能:只渲染当前状态的 UI 元素
  • 🔧 易维护:新增状态只需添加一个 Style 块
  • 🛡️ 类型安全:使用枚举避免魔法字符串
  • 🎨 灵活性:每个状态可以有完全不同的 UI 结构

希望这个模式能帮助你写出更优雅、更易维护的 Avalonia 应用!

赞(0) 打赏
未经允许不得转载:码农很忙 » Avalonia 使用 Tag + Style 选择器实现状态驱动 UI

评论 抢沙发

给作者买杯咖啡

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫

登录

找回密码

注册