Prevent ListView.SelectedItem from being set to null when ChangeTrackEnabled is set

Given the following view model:

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 _selected;
     public AttachmentViewModel Selected
     {
         get => _selected;
         set => this.RaiseAndSetIfChanged( ref _selected, value );
     }
}

Attachments and AttachmentViewModels are set using the following code:

var items = DataManager.GetAttachmentsList();
Attachments = new ReactiveList<Attachment>( items ) { ChangeTrackingEnabled = true };
AttachmentViewModels = Attachments.CreateDerivedCollection( x => new AttachmentViewModel(x) );

AttachmentViewModels is bound to a ListView:

this.OneWayBind( ViewModel, vm => vm.AttachmentViewModels, v => v.List.ItemsSource );
this.Bind( ViewModel, vm => vm.Selected, v => v.List.SelectedItem );

However, if I update one of the Attachments via an AttachmentViewModel, then because Attachments.ChangeTrackingEnabled is set to true, the AttachmentViewModels.CollectionChanged is fired. For some reason, this in turn sets List.SelectedItem to null.

Is there any way to avoid this behaviour? Another SO post implies that this may be because I haven't implemented the appropriate equality operators in AttachmentViewModel. I tried following this advice, but it didn't seem to help.

Also, I had to set ListView.IsSynchronizedWithCurrentItem to true - otherwise the ListView will not maintain the original selection. However, this doesn't prevent SelectedItem from being set to null, it just means that it is set twice. Once to null, then another time back to the original selection. This would be okay, except it results in visual glitches in the UI while SelectedItem changes.

1 answer

  • answered 2018-04-17 05:20 Mitkins

    When the property of an item in a ReactiveList has changed and ChangeTrackingEnabled is true - the CollectionChanged event is fired. For an associated IReactiveDerivedList, this means that the entire collection is re-created. Since a new AttachmentViewModel is generated (for the same Attachment), SelectedItem no longer points to an item in the list. It's default behaviour is to become null.

    By implementing a cache, the same AttachmentViewModel can be re-instated - in spite of the collection change event.

    Based on this ReactiveUI issue, here's what I implemented:

    public class AttachmentsViewModel : ReactiveObject
    {
        private readonly Dictionary<Attachment, AttachmentViewModel> _cache = new Dictionary<Attachment, AttachmentViewModel>();
    
        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 _selected;
        public AttachmentViewModel Selected
        {
            get => _selected;
            set => this.RaiseAndSetIfChanged( ref _selected, value );
        }
    
        public void LoadAttachments()
        {
            _cache.Clear();
    
            var items = DataManager.GetAttachmentsList();
            Attachments = new ReactiveList<Attachment>( items ) { ChangeTrackingEnabled = true };
            AttachmentViewModels = Attachments.CreateDerivedCollection( x => {
                AttachmentViewModel viewModel;
    
                if ( _cache.ContainsKey(x) ) {
                    viewModel = _cache[x];
                } else {
                    viewModel = new AttachmentViewModel( x );
    
                    _cache.Add( x, viewModel );
                }
    
                return viewModel;
            });
        }
    }
    

    Note that the Attachment object has its own equality operators (based on an ID property). Users can revert their changes, so I had to add the _cache.Clear() to compensate.