Wpf - Sincronizar dos ListView

Descripción general:

Quiero sincronizar el contenido de dos listView, de forma que se muestra la información complementaria en cada uno de ellos.

Wpf - Sincronizar dos ListView

Situación de ejemplo.

Tengo dos listView en uno se muestran (por ejemplo) nombres, el de la izquierda y sus DNI en el de la derecha.

Primero: quiero sincronizar las barras de scroll

Quiero que la información que muestren los dos controles este sincronizada, que si desplazo en vertical uno de ellos, (sea pulsando en la barra de scroll o con la rueda del ratón o con pulsaciones de teclas) el otro también se desplace y muestre la información complementaria que corresponde a los datos del primer control

Segundo: quiero sincronizar la selección de los elementos de los controles

Quiero que cuando se seleccione un elemento del listView de la izquierda (por ejemplo un nombre), se seleccione también su DNI correspondiente que esta en el ListView de la derecha.


Sincronizar las barras de scroll

Para hacer este trabajo necesitamos detectar que se hace scroll ( desplazamiento ) en los controles a sincronizar . y el problema esta en que los controles normales (TextBox, ListBox, RichTextBox, ListView, etc.) no disponen de un evento que se dispare al hacer Scroll en el contenido del mismo.

La solución es envolverlo, (ponerlo dentro de ), un control ScrollViewer , control que si dispone del evento ScrollChanged, evento que podemos interceptar y usarlo para sincronizar los contenidos

Pantalla con los resultados
[Imagen 01] Imagen antes de la sincronización vertical
Pantalla con los resultados
[Imagen 02] Imagen después de la sincronización vertical
La solución
  • En el código XAML rodearemos nuestro ListView con un control ScrollViewer
  • En el código del control escribiremos un escuchado del evento (uno por cada control a sincronizar) que será el que realmente sincroniza los contenidos

Código Xaml


            <GroupBox Grid.Column = "0" Grid.Row = "0"  Header = ""
                          HorizontalAlignment = "Stretch" VerticalAlignment = "Stretch">
                <ScrollViewer x:Name = "ScrollViewerListaOriginal"
                              HorizontalScrollBarVisibility = "Auto" VerticalScrollBarVisibility = "Visible">
                      <ListView x:Name = "ListViewListaFicherosOriginales"
                              ItemsSource = "{DynamicResource ColeccionObservableRenameColeccion}"
                              HorizontalAlignment = "Stretch" VerticalAlignment = "Stretch" >
                        <ListView.View>
                            <GridView>
                                <GridViewColumn
                                           Header = "Nombre original"
                                          DisplayMemberBinding = "{Binding FicheroOriginalViejoNF.Name}" />
                            </GridView>
                        </ListView.View>
                    </ListView>
                </ScrollViewer>
            </GroupBox>

A modo de observaciones de este código
  • El control GroupBox enmarca todo el conjunto como decoración
  • El control ScrollViewer envuelve al control ListView
  • El control ListView carga la información que muestra de un objeto colección observable, que carga dinámicamente
  • La información se muestra en un control GridView y se el elemento que se muestra se carga con un enlace de datos
  • Lo importante de este código es que veas que hay un control ScrollViewer que rodea a un control ListView

Código Visual Basic


#Region "Scroll sincronizado de los listBox"
        '-----------------------------
        ' Apunte táctico
        ' Controles participantes que quiero sincronizar
        ' Son dos ScrollViewer que rodena cada uno de ellos a un ListView.
        ' Observación: No es relevante el tipo de información,
        '              no importa ni como se llama el ListView ni su contenido)
        ' [ScrollViewerListaOriginal]   Envuelve a un control listView
        ' [ScrollViewerListaModificada] Envuelve a un control listView
        '-----------------------------
        '-----------------------------
        ' variable que se usa para evitar que los eventos se metan en un bucle sin fin
        Private SemaforoScrollChanged As Boolean
        '-----------------------------
        ' Escuchador del primer control
        '-----------------------------
        ' Observa que:
        ' * Escucho al ScrollViewer ListaOriginal,
        '       y lo sincronizo con el ScrollViewer ListaModificada
        ' * Los ListView contenidos dentro del ScrollViewer no aparecen.
        '   Ni se usan (en este escuchador), ni importa el valor que tengan
        ' * Solo se ejecuta si no hay ningún evento de sincronización ejecutándose
        '   (Usa el valor de la variable SemaforoScrollChanged)
        '-----------------------------
        Private Sub ScrollViewerListaOriginal_ScrollChanged(
            sender As Object,
            e As ScrollChangedEventArgs) Handles ScrollViewerListaOriginal.ScrollChanged
        If SemaforoScrollChanged Then Return
        '  (pseudo) bloquear el acceso a otros eventos de sincronización
        SemaforoScrollChanged = True
        '---------------------
        Dim sv As System.Windows.Controls.ScrollViewer = TryCast(sender, ScrollViewer)
        If sv Is ScrollViewerListaOriginal Then
        ' El scroll vertical
        ' [ScrollToVerticalOffset] se encarga de la sincronización
        '   de los controles contenidos ListView
        ' Observa que los ListView no aparecen por ningún sitio en este ejemplo
            ScrollViewerListaModificada.ScrollToVerticalOffset(e.VerticalOffset)
        ' El scroll horizontal
            ScrollViewerListaModificada.ScrollToHorizontalOffset(e.HorizontalOffset)
        End If
        '---------------------
        ' liberar el (pseudo) bloqueo
        SemaforoScrollChanged = False
        ' marcar que el evento se ha ejecutado totalmente
        e.Handled = True
        End Sub

        '-----------------------------
        ' Escuchador del segundo control
        '-----------------------------
        ' Observa que:
        ' * Escucho al ScrollViewer ListaOriginal, y
        '   lo sincronizo con el ScrollViewer ListaModificada
        ' * Los ListView contenidos dentro del ScrollViewer no aparecen.
        '   Ni se usan (en este escuchador), ni importa el valor que tengan
        ' * Solo se ejecuta si no hay ningún evento de sincronización ejecutándose
        '   (Usa el valor de la variable SemaforoScrollChanged)
        '-----------------------------
        Private Sub ScrollViewerListaModificada_ScrollChanged(
            sender As Object,
            e As ScrollChangedEventArgs) Handles ScrollViewerListaModificada.ScrollChanged
        If SemaforoScrollChanged Then Return
        SemaforoScrollChanged = True
        Dim sv As System.Windows.Controls.ScrollViewer = TryCast(sender, ScrollViewer)
        If sv Is ScrollViewerListaModificada Then
            ScrollViewerListaOriginal.ScrollToVerticalOffset(e.VerticalOffset)
            ScrollViewerListaOriginal.ScrollToHorizontalOffset(e.HorizontalOffset)
        End If
        SemaforoScrollChanged = False
        ' marcar que el evento se ha ejecutado totalmente
        e.Handled = True
        End Sub
#End Region

Un ultima observación

Se pueden sincronizar de la misma manera cualquier control, por ejemplo Un listBox, un comboBox, cualquiera, solo hay que adaptar este código que te he mostrado

Bibliografía

La idea de este código esta ¿copiada? (mejor decir adaptada) , de las siguientes páginas del Blog [elguillemola]. En ellas se muestra como sincronizar el contenido de dos TextBox multilínea (por si te interesa)

Sincronizar la selección de los elementos de los controles

Quiero que si selecciona un nombre del listView de la izquierda (por ejemplo un nombre), se seleccione también su DNI correspondiente que esta en el ListView de la derecha.

Pantalla con los resultados
[Imagen 03] Imagen antes de la selección de un elemento
Pantalla con los resultados
[Imagen 04] Imagen despues de la selección de un elemento

        '-----------------------------
        ' Apunte táctico
        ' Controles participantes que quiero sincronizar
        ' Son dos ListView que lo que hago es igualar el valor del evento [SelectedItem]
        ' El evento SelectionChanged, cambia al seleccionar un elemento del ListView
        '
        ' [ListViewListaFicherosOriginales]           contiene uno de los ListView
        ' [ListViewListaFicherosConNombreConvertido]  contiene uno de los ListView
        '-----------------------------
        '-----------------------------
        ' variable que se usa para evitar que los eventos se metan en un bucle sin fin
        ' La he copiado aquí para facilitar la lectura del código
        ' pero ya esta definida anteriormente
        Private SemaforoScrollChanged As Boolean


        Private Sub ListViewListaFicherosOriginales_SelectionChanged(
            sender As Object,
            e As SelectionChangedEventArgs) _
        Handles ListViewListaFicherosOriginales.SelectionChanged
            ' si algún evento [SelectionChanged] se esta ejecutando salgo de aquí sin hacer nada
            If SemaforoScrollChanged Then Return
            '  (pseudo) bloquear el acceso a otros eventos de sincronización
            SemaforoScrollChanged = True
            ' Contendrá el objeto ListView que dispara el evento
            Dim LV As ListView
            ' Contendrá el objeto seleccionado que contiene el List ListView
            Dim RI As RenameItem
            ' obtenemos el ListView que dispara el evento SelectionChanged
            ' en este caso es el [ListViewListaFicherosOriginales]
            LV = CType(sender, ListView)
            ' obtenemos el objeto seleccionado
            RI = CType(LV.SelectedItem, RenameItem)
            ' lo paso al otro control  ListView
            ListViewListaFicherosConNombreConvertido.SelectedItem = LV.SelectedItem
            '---------------------
            ' liberar el (pseudo) bloqueo
            SemaforoScrollChanged = False
            ' marcar que el evento se ha ejecutado totalmente
            e.Handled = True
        End Sub


        Private Sub ListViewListaFicherosConNombreConvertido_SelectionChanged(
            sender As Object,
            e As SelectionChangedEventArgs) _
        Handles ListViewListaFicherosConNombreConvertido.SelectionChanged
            If SemaforoScrollChanged Then Return
            SemaforoScrollChanged = True
            Dim LV As ListView
            Dim RI As RenameItem
            LV = CType(sender, ListView)
            RI = CType(LV.SelectedItem, RenameItem)
            ListViewListaFicherosOriginales.SelectedItem = LV.SelectedItem
            SemaforoScrollChanged = False
            e.Handled = True
        End Sub

Un observación

Los dos procesos mostrados en esta pagina son independientes pero complementarios, una cosa es el desplazamiento sincronizado, y otra diferente es la selección en ambos controles.

De hecho, si usas solamente el código que selecciona en ambos controles, el proceso funciona, pero al no estar sincronizadas las vistas, no se desplaza el control y no se muestra el elemento seleccionado en el otro control.

Dicho de otra forma, o usas solo la sincronización de desplazamientos, o usas los dos a la vez, peor no uses (de momento) el segundo código porque no acaba de estar pulido