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
相关产品推荐
相关产品推荐

