如何在启用ChangeTrackEnabled时避免ListView.SelectedItem被设为null
首先得搞清楚问题根源:当你给ReactiveList<Attachment>开启ChangeTrackEnabled后,ReactiveUI会为集合里的每个Attachment对象创建ChangeTrackingItem包装器。这会导致ListView在判断选中项是否存在于集合中时,因为引用或相等性判断的变化,找不到原来的选中项,最终把SelectedItem设为null。
下面是几个针对性的解决方案,按推荐程度排序:
1. 为Attachment类实现稳定的Equals和GetHashCode
这是最根本的解决办法。ChangeTrackEnabled会监控对象的属性变化,如果你的Equals方法依赖可变属性,当属性变更时,对象的相等性会改变,ListView就会认为原来的选中项不在集合里了。
你应该基于对象的唯一标识(比如Id字段)来实现相等判断,示例代码如下:
public class Attachment : IEquatable<Attachment> { public Guid Id { get; set; } // 假设这是对象的唯一标识 // 其他属性... public bool Equals(Attachment other) { if (other is null) return false; return Id.Equals(other.Id); } public override bool Equals(object obj) => Equals(obj as Attachment); public override int GetHashCode() => Id.GetHashCode(); }
这样即使对象的其他属性变化,只要Id不变,ListView就能正确识别选中项。
2. 在ViewModel中手动维护选中项的引用
如果没法修改Attachment类的相等性实现,你可以在AttachmentsViewModel里监听集合的变更事件,手动保持选中项的引用有效。
假设你有一个SelectedAttachmentViewModel属性,代码示例:
public class AttachmentsViewModel : ReactiveObject, ISupportsActivation { private ReactiveList<Attachment> _attachments; public ReactiveList<Attachment> Attachments { get => _attachments; set => this.RaiseAndSetIfChanged(ref _attachments, value); } private IReactiveDerivedList<AttachmentViewModel> _attachmentViewModels; public IReactiveDerivedList<AttachmentViewModel> AttachmentViewModels { get => _attachmentViewModels; set => this.RaiseAndSetIfChanged(ref _attachmentViewModels, value); } private AttachmentViewModel _selectedAttachmentViewModel; public AttachmentViewModel SelectedAttachmentViewModel { get => _selectedAttachmentViewModel; set => this.RaiseAndSetIfChanged(ref _selectedAttachmentViewModel, value); } public ViewModelActivator Activator { get; } = new ViewModelActivator(); public AttachmentsViewModel() { this.WhenActivated(disposables => { // 初始化集合并开启ChangeTrackEnabled Attachments = new ReactiveList<Attachment> { ChangeTrackingEnabled = true }; // 监听集合项的属性变更,维护选中项引用 Attachments.ItemChanged.Subscribe(args => { if (SelectedAttachmentViewModel?.Attachment == args.Sender) { var matchingVm = AttachmentViewModels.FirstOrDefault(vm => vm.Attachment.Id == args.Sender.Id); if (matchingVm != null) { SelectedAttachmentViewModel = matchingVm; } } }).DisposeWith(disposables); // 监听集合变更,确保选中项存在于派生列表中 Attachments.CollectionChanged.Subscribe(args => { if (SelectedAttachmentViewModel != null && !AttachmentViewModels.Contains(SelectedAttachmentViewModel)) { var matchingVm = AttachmentViewModels.FirstOrDefault(vm => vm.Attachment.Id == SelectedAttachmentViewModel.Attachment.Id); SelectedAttachmentViewModel = matchingVm ?? null; } }).DisposeWith(disposables); }); } }
3. 绑定SelectedItem时使用标识匹配
在View层绑定SelectedItem的时候,不要直接绑定引用,而是通过唯一标识来匹配对应的ViewModel。比如在WPF中,可以用SelectedValuePath和SelectedValue:
<ListView ItemsSource="{Binding AttachmentViewModels}" SelectedValuePath="Attachment.Id" SelectedValue="{Binding SelectedAttachmentId}"/>
然后在ViewModel中维护SelectedAttachmentId属性,并通过它来获取对应的SelectedAttachmentViewModel:
private Guid? _selectedAttachmentId; public Guid? SelectedAttachmentId { get => _selectedAttachmentId; set => this.RaiseAndSetIfChanged(ref _selectedAttachmentId, value); } // 在构造函数中绑定关联关系 this.WhenAnyValue(x => x.SelectedAttachmentId) .Select(id => id.HasValue ? AttachmentViewModels.FirstOrDefault(vm => vm.Attachment.Id == id.Value) : null) .BindTo(this, x => x.SelectedAttachmentViewModel) .DisposeWith(disposables);
这种方式完全避开了引用相等性的问题,非常可靠。
内容的提问来源于stack exchange,提问作者Mitkins

