How to preserve selection in an MVVM-bound WPF ListView?

Herman

Well-known member
Joined
Oct 18, 2011
Messages
883
Location
Montreal, QC, CA
Programming Experience
10+
I have a stupid problem, which seems it should have a solution implemented in the control itself, but I cannot figure out how to do it.

I have the following ListView bound to the window's view model:

        <ListView x:Name="lvItems" Grid.Row="0" Margin="0" FontSize="10" FontWeight="SemiBold"
                  SelectionMode="Single" ItemsSource="{Binding CurrentSnapshot}">
            
            <ListView.Resources>
                <ContextMenu x:Key="cmItemsContextMenu">
                    <MenuItem x:Name="miManageAlerts" Header="Manage Alerts..." />
                </ContextMenu>
            </ListView.Resources>
            
            <ListView.ItemContainerStyle>
                <Style TargetType="{x:Type ListViewItem}">
                    <Setter Property="ContextMenu" Value="{StaticResource cmItemsContextMenu}" />
                </Style>
            </ListView.ItemContainerStyle>
                
            <ListView.View>
                <GridView>
                    <GridViewColumn Width="Auto" DisplayMemberBinding="{Binding MachineName}" Header="Machine Name  " />
                    <GridViewColumn Width="Auto" DisplayMemberBinding="{Binding CpuUsagePercent}" Header="CPU Usage (%)  " />
                    <GridViewColumn Width="Auto" DisplayMemberBinding="{Binding DiskUsagePercent}" Header="HDD Usage (%)  " />
                    <GridViewColumn Width="Auto" DisplayMemberBinding="{Binding MemoryAvailableMB}" Header="Available Memory (MB)  " />
                    <GridViewColumn Width="Auto" DisplayMemberBinding="{Binding NetUsageInKBytesSec}" Header="Network In (KB/s)  " />
                    <GridViewColumn Width="Auto" DisplayMemberBinding="{Binding NetUsageOutKBytesSec}" Header="Network Out (KB/s)  " />
                    <GridViewColumn Width="Auto" DisplayMemberBinding="{Binding TerminalServicesSessions}" Header="# of Sessions  " />
                </GridView>
            </ListView.View>
        </ListView>



Everything is bound nicely and everything works up to this point. However the view model has a timer running that periodically refreshes the CurrentSnapshot object. When it does, because the ViewModel implements INotifyPropertyChanged, the ItemsSource for the listview is refreshed, the new items are bound, and the existing selection is lost. Without MVVM I would likely have something like this:

VB.NET:
    Private Sub lvItems_SelectionChanged(sender As Object, e As SelectionChangedEventArgs)
        _selectedSample = lvItems.SelectedItem

        If _selectedSample IsNot Nothing Then
            _lastSelectedMachine = _selectedSample.MachineName
        Else
            _lastSelectedMachine = ""
        End If
    End Sub

And then I would RemoveHandler and AddHandler before and after assigning to the ItemsSource. I cannot see how I can do this while respecting the MVVM pattern however. I have seen some examples online, that suggest using an attached behavior and whatnot. Isn't there a simple and easy way to handle this without having to go dig through dependency properties?


Thank you.
 
So I finally figured it out, and while it's simple and works well, it's rather poorly documented. Here is what I did:

 <ListView Grid.Row="0" Margin="0" FontSize="10" FontWeight="SemiBold" ItemsSource="{Binding CurrentSnapshotView}" 
           SelectionMode="Single" IsSynchronizedWithCurrentItem="True" >


In the view model:

VB.NET:
    Private _currentSnapshot As New ObservableCollection(Of KeyedPerformanceSample)
    Public Property CurrentSnapshot As ObservableCollection(Of KeyedPerformanceSample)
        Get
            Return _currentSnapshot
        End Get
        Set(value As ObservableCollection(Of KeyedPerformanceSample))
            _currentSnapshot = value

            RemoveHandler CurrentSnapshotView.CurrentChanged, AddressOf CurrentSnapshotView_CurrentChanged
            _currentSnapshotView = CollectionViewSource.GetDefaultView(_currentSnapshot)
            AddHandler CurrentSnapshotView.CurrentChanged, AddressOf CurrentSnapshotView_CurrentChanged

            NotifyPropertyChanged("CurrentSnapshot")
            NotifyPropertyChanged("CurrentSnapshotView")
        End Set
    End Property

    Private _currentSnapshotView As ICollectionView = CollectionViewSource.GetDefaultView(CurrentSnapshot)
    Public ReadOnly Property CurrentSnapshotView As ICollectionView
        Get
            Return _currentSnapshotView
        End Get
    End Property

    Public Sub CurrentSnapshotView_CurrentChanged(sender As Object, e As EventArgs)
        SelectedSample = CurrentSnapshotView.CurrentItem
    End Sub

    Private Sub ViewModel_PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Handles Me.PropertyChanged
        If e.PropertyName = "CurrentSnapshot" Then
            If SelectedSample IsNot Nothing Then
                Dim newSelection = CurrentSnapshot.Where(Function(x) x.Key = SelectedSample.Key).FirstOrDefault
                If newSelection IsNot Nothing Then CurrentSnapshotView.MoveCurrentTo(newSelection)
            Else
                CurrentSnapshotView.MoveCurrentTo(Nothing)
            End If
        End If
    End Sub

So this way everything is in the ViewModel and nothing bleeds out!

Hopefully this will help someone else.
 
Back
Top