You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Avalonia自定义ComboBox:移除硬编码类型依赖的方案咨询

问题描述

使用Avalonia.UI的ComboBox控件时,发现它没有SelectedValuePath和DisplayMemberPath属性,暂时无法自行实现这两个属性的功能。因此基于TemplatedControl实现了自定义控件ChangeTrackingHeaderComboBox,已实现通过DisplayMember显示选项、通过Id(示例中为Project.ManagerId)完成数据加载与保存的功能,但控件后台代码硬编码了LookupItem类型,希望移除该硬编码约束,尝试过x:DataType和StyledProperty但未成功,寻求可行方案。


现有代码

TemplatedControl(ChangeTrackingHeaderComboBox.axaml)

<Styles xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:controls="using:ChangeTrackingHeaderControls.Controls">
    <Design.PreviewWith>
        <controls:ChangeTrackingHeaderComboBox/>
    </Design.PreviewWith>

    <Style Selector="controls|ChangeTrackingHeaderComboBox">
        <!-- Set Defaults -->
        <Setter Property="Template">
            <ControlTemplate>
                <ComboBox Items="{TemplateBinding Items, Mode=OneWay}"
                          SelectedItem="{TemplateBinding SelectedItem, Mode=TwoWay}"
                          SelectedIndex="{TemplateBinding SelectedIndex, Mode=TwoWay}"
                          ItemTemplate="{TemplateBinding ItemTemplate}"
                          HorizontalAlignment="Stretch"/>
            </ControlTemplate>
        </Setter>
    </Style>
</Styles>

控件后台代码(ChangeTrackingHeaderComboBox.cs)

public class ChangeTrackingHeaderComboBox : TemplatedControl
{
    private object? _selectedItem;
    private int? _selectedValue;

    public static readonly DirectProperty<ChangeTrackingHeaderComboBox, object?> SelectedItemProperty =
        AvaloniaProperty.RegisterDirect<ChangeTrackingHeaderComboBox, object?>(nameof(SelectedItem),
            o => o.SelectedItem,
            (o, v) => o.SelectedItem = v,
            defaultBindingMode: BindingMode.TwoWay,
            enableDataValidation: true);

    public static readonly DirectProperty<ChangeTrackingHeaderComboBox, int?> SelectedValueProperty =
        AvaloniaProperty.RegisterDirect<ChangeTrackingHeaderComboBox, int?>(nameof(SelectedValue),
            o => o.SelectedValue,
            (o, v) => o.SelectedValue = v,
            defaultBindingMode: BindingMode.TwoWay,
            enableDataValidation: true);

    public static readonly StyledProperty<IEnumerable> ItemsProperty = AvaloniaProperty
        .Register<ChangeTrackingHeaderComboBox, IEnumerable>(nameof(Items));

    public static readonly StyledProperty<int> SelectedIndexProperty = AvaloniaProperty
        .Register<ChangeTrackingHeaderComboBox, int>(nameof(SelectedIndex));

    public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty = AvaloniaProperty
        .Register<ChangeTrackingHeaderComboBox, IDataTemplate>(nameof(ItemTemplate));

    public object? SelectedItem
    {
        get => _selectedItem;
        set
        {
            SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
            OnSelectedItemChanged();
        }
    }

    public int? SelectedValue
    {
        get => _selectedValue;
        set
        {
            SetAndRaise(SelectedValueProperty, ref _selectedValue, value);
            OnSelectedValueChanged();
        }
    }

    public IEnumerable Items
    {
        get => GetValue(ItemsProperty);
        set => SetValue(ItemsProperty, value);
    }

    public int SelectedIndex
    {
        get => GetValue(SelectedIndexProperty);
        set => SetValue(SelectedIndexProperty, value);
    }

    public IDataTemplate ItemTemplate
    {
        get => GetValue(ItemTemplateProperty);
        set => SetValue(ItemTemplateProperty, value);
    }

    protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
    {
        base.UpdateDataValidation(property, state, error);

        if (property == SelectedItemProperty)
        {
            DataValidationErrors.SetError(this, error);
        }

        if (property == SelectedValueProperty)
        {
            DataValidationErrors.SetError(this, error);
        }
    }

    private void OnSelectedItemChanged()
    {
        // LookupItem should not be hard coded
        if (SelectedItem is LookupItem selectedItem)
        {
            if (SelectedValue == selectedItem.Id) return;

            if (Items is IEnumerable<LookupItem> items)
            {
                var selectedItemDisplayMember = selectedItem.DisplayMember;
                SelectedValue = items.SingleOrDefault(x => x.DisplayMember == selectedItemDisplayMember).Id;
            }
        }
    }

    private void OnSelectedValueChanged()
    {
        // LookupItem should not be hard coded
        if (Items is IEnumerable<LookupItem> items)
        {
            if (SelectedItem == null)
            {
                SelectedItem = items.SingleOrDefault(x => x.Id == SelectedValue);
                return;
            }

            if (SelectedItem is LookupItem selectedItem)
            {
                if (selectedItem.Id == SelectedValue) return;

                SelectedItem = items.SingleOrDefault(x => x.Id == SelectedValue);
            }
        }
    }
}

App.axaml

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:controls="clr-namespace:ChangeTrackingHeaderControls.Controls"
             xmlns:viewModels="clr-namespace:ChangeTrackingHeaderControls.ViewModels"
             x:Class="ChangeTrackingHeaderControls.App">

    <Application.Resources>
        <DataTemplate x:Key="LookupItemItemTemplate" x:DataType="viewModels:LookupItem">
            <ComboBoxItem Content="{CompiledBinding DisplayMember}"
                          HorizontalContentAlignment="Left"
                          VerticalContentAlignment="Center"/>
        </DataTemplate>
    </Application.Resources>

    <Application.Styles>
        <FluentTheme Mode="Light"/>
        <StyleInclude Source="/Controls/ChangeTrackingHeaderComboBox.axaml"/>
    </Application.Styles>
</Application>

MainWindow.axaml

<controls:ChangeTrackingHeaderComboBox Items="{CompiledBinding Managers}"
                                       SelectedValue="{CompiledBinding Project.ManagerId}"
                                       ItemTemplate="{StaticResource LookupItemItemTemplate}"/>

ViewModel(MainViewModel.cs)

public sealed class MainViewModel : ViewModelBase
{
    private Project _project;

    public MainViewModel()
    {
        Managers = new ObservableCollection<LookupItem>();

        // Simulating loading Managers from database
        for (var i = 1; i < 12; i++)
        {
            Managers.Add(new LookupItem
            {
                Id = i,
                DisplayMember = $"Manager {i}"
            });
        }

        // One single Project loaded from database
        Project = new Project
        {
            Id = 1,
            Name = "Project 1",
            ManagerId = Managers.LastOrDefault()?.Id
        };
    }

    public ObservableCollection<LookupItem> Managers { get; }

    public Project Project
    {
        get => _project;
        private set
        {
            if (value == _project) return;
            _project = value;
            OnPropertyChanged();
        }
    }
}

模型类

public sealed class Project
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int? ManagerId { get; set; }
}

public sealed class LookupItem
{
    public int Id { get; init; }
    public string DisplayMember { get; init; }
}

解决方案

要移除LookupItem的硬编码约束,核心是通过添加属性配置值路径与显示路径,并利用反射或表达式树动态获取属性值,摆脱对具体类型的依赖。

步骤1:添加SelectedValuePath和DisplayMemberPath属性

在ChangeTrackingHeaderComboBox中新增两个StyledProperty,用于配置值字段和显示字段的名称:

public static readonly StyledProperty<string> SelectedValuePathProperty = AvaloniaProperty
    .Register<ChangeTrackingHeaderComboBox, string>(nameof(SelectedValuePath));

public static readonly StyledProperty<string> DisplayMemberPathProperty = AvaloniaProperty
    .Register<ChangeTrackingHeaderComboBox, string>(nameof(DisplayMemberPath));

public string SelectedValuePath
{
    get => GetValue(SelectedValuePathProperty);
    set => SetValue(SelectedValuePathProperty, value);
}

public string DisplayMemberPath
{
    get => GetValue(DisplayMemberPathProperty);
    set => SetValue(DisplayMemberPathProperty, value);
}

步骤2:实现动态属性获取工具类

创建一个帮助类,通过属性名称反射获取对象属性值:

public static class PropertyAccessor
{
    public static object? GetPropertyValue(object obj, string propertyName)
    {
        if (obj == null || string.IsNullOrEmpty(propertyName))
            return null;
        
        var property = obj.GetType().GetProperty(propertyName);
        return property?.GetValue(obj);
    }
}

步骤3:修改选择变更逻辑

替换硬编码的LookupItem判断逻辑,改为通过配置的路径动态处理:

private void OnSelectedItemChanged()
{
    if (SelectedItem == null || string.IsNullOrEmpty(SelectedValuePath))
        return;

    var newValue = PropertyAccessor.GetPropertyValue(SelectedItem, SelectedValuePath);
    if (newValue is int intValue && SelectedValue != intValue)
    {
        SelectedValue = intValue;
    }
}

private void OnSelectedValueChanged()
{
    if (SelectedValue == null || Items == null || string.IsNullOrEmpty(SelectedValuePath))
        return;

    foreach (var item in Items)
    {
        var itemValue = PropertyAccessor.GetPropertyValue(item, SelectedValuePath);
        if (itemValue is int itemIntValue && itemIntValue == SelectedValue)
        {
            if (!Equals(SelectedItem, item))
            {
                SelectedItem = item;
            }
            break;
        }
    }
}

步骤4:在XAML中配置属性

使用控件时,直接指定SelectedValuePath和DisplayMemberPath即可,不再依赖LookupItem类型:

<controls:ChangeTrackingHeaderComboBox Items="{CompiledBinding Managers}"
                                       SelectedValue="{CompiledBinding Project.ManagerId}"
                                       SelectedValuePath="Id"
                                       DisplayMemberPath="DisplayMember"/>

如果需要自定义显示模板,依然可以保留ItemTemplate配置,两者互不冲突。

性能优化(可选)

若担心反射性能,可改用表达式树缓存属性访问器,替换上述PropertyAccessor:

public static class PropertyAccessor
{
    private static readonly Dictionary<Type, Dictionary<string, Func<object, object?>>> _accessors = new();

    public static object? GetPropertyValue(object obj, string propertyName)
    {
        if (obj == null || string.IsNullOrEmpty(propertyName))
            return null;
        
        var type = obj.GetType();
        if (!_accessors.TryGetValue(type, out var propertyAccessors))
        {
            propertyAccessors = new Dictionary<string, Func<object, object?>>();
            _accessors[type] = propertyAccessors;
        }

        if (!propertyAccessors.TryGetValue(propertyName, out var accessor))
        {
            var parameter = Expression.Parameter(typeof(object));
            var cast = Expression.Convert(parameter, type);
            var property = Expression.Property(cast, propertyName);
            var convert = Expression.Convert(property, typeof(object));
            accessor = Expression.Lambda<Func<object, object?>>(convert, parameter).Compile();
            propertyAccessors[propertyName] = accessor;
        }

        return accessor(obj);
    }
}

内容的提问来源于stack exchange,提问作者Tobias Johansson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 01:55:17