2012-01-11 13 views
29

tengo una vista XAML con un cuadro de lista:desplazamiento WPF ListBox al SelectedItem establecido en el código en un modelo de vista

<control:ListBoxScroll ItemSource="{Binding Path=FooCollection}" 
         SelectedItem="{Binding SelectedFoo, Mode=TwoWay}" 
         ScrollSelectedItem="{Binding SelectedFoo}"> 
    <!-- data templates, etc. --> 
</control:ListBoxScroll> 

El elemento seleccionado está ligado a una propiedad, en mi opinión. Cuando el usuario selecciona un elemento en el cuadro de lista, mi propiedad SelectedFoo en el modelo de vista se actualiza. Cuando configuro la propiedad SelectedFoo en mi modelo de vista, se selecciona el elemento correcto en el cuadro de lista.

El problema es que si el SelectedFoo que está configurado en el código no está visible en este momento, debo llamar al ScrollIntoView en el cuadro de lista. Como mi ListBox está dentro de una vista y mi lógica está dentro de mi modelo de vista ... No pude encontrar una manera conveniente de hacerlo. Así que me ampliado ListBoxScroll:

class ListBoxScroll : ListBox 
{ 
    public static readonly DependencyProperty ScrollSelectedItemProperty = DependencyProperty.Register(
     "ScrollSelectedItem", 
     typeof(object), 
     typeof(ListBoxScroll), 
     new FrameworkPropertyMetadata(
      null, 
      FrameworkPropertyMetadataOptions.AffectsRender, 
      new PropertyChangedCallback(onScrollSelectedChanged))); 
    public object ScrollSelectedItem 
    { 
     get { return (object)GetValue(ScrollSelectedItemProperty); } 
     set { SetValue(ScrollSelectedItemProperty, value); } 
    } 

    private static void onScrollSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 
    { 
     var listbox = d as ListBoxScroll; 
     listbox.ScrollIntoView(e.NewValue); 
    } 
} 

Básicamente expone una nueva propiedad de dependencia ScrollSelectedItem la que se unen a la propiedad SelectedFoo en mi modelo de vista. Luego engancho en la devolución de llamada cambiada de la propiedad dependiente y despliego el elemento recién seleccionado en la vista.

¿Alguien más sabe de una forma más fácil de llamar a funciones en controles de usuario en una vista XAML respaldada por un modelo de vista? Es un poco de una carrera en torno a:

  1. crear una propiedad dependiente
  2. añadir una devolución de la propiedad cambió de devolución de llamada
  3. función mango invocación dentro de la devolución de llamada estática

Sería bueno para poner la lógica en el método ScrollSelectedItem { set { pero el marco de dependencia parece escabullirse y se las arregla para funcionar sin llamarlo realmente.

+0

Será mucho más fácil establecer 'SelectedIndex'. –

+1

Parece una "preocupación" de Vista en lugar de una ViewModel. Tuve que hacer algo similar, pero dejé el código en la vista. Ver http://matthamilton.net/focus-a-virtualized-listboxitem –

+0

@MattHamilton - este código está técnicamente en la Vista (dentro de un control). ¿Qué código escribirías en una vista (en cualquier lugar) que lograría llamar a ScrollIntoView? Tenga en cuenta que no puedo anular el conjunto en SelectedItem ya que no es virtual. –

Respuesta

30

Después de revisar las respuestas surgió un tema común: clases externas que escuchan el evento SelectionChanged del ListBox. Eso me hizo darse cuenta de que la estrategia de la propiedad dependiente fue exagerado y tan sólo pudiera tener la subclase escuche a sí mismo:

class ListBoxScroll : ListBox 
{ 
    public ListBoxScroll() : base() 
    { 
     SelectionChanged += new SelectionChangedEventHandler(ListBoxScroll_SelectionChanged); 
    } 

    void ListBoxScroll_SelectionChanged(object sender, SelectionChangedEventArgs e) 
    { 
     ScrollIntoView(SelectedItem); 
    } 
} 

Siento que esto es la solución más simple que hace lo que yo quiero.

Mención de honor va a adcool2007 para mostrar los comportamientos. Aquí hay un par de artículos para los interesados:

http://blogs.msdn.com/b/johngossman/archive/2008/05/07/the-attached-behavior-pattern.aspx
http://www.codeproject.com/KB/WPF/AttachedBehaviors.aspx

Creo que para los comportamientos genéricos que se añadirán a varios diferentes controles de usuario (por ejemplo, haga clic en los comportamientos, conductas, comportamientos de arrastre de animación, etc.) entonces los comportamientos adjuntos tienen mucho sentido. La razón por la que no quiero usarlos en este caso particular es que la implementación del comportamiento (llamando al ScrollIntoView) no es una acción genérica que puede ocurrir a cualquier control que no sea un ListBox.

+0

Simple y eficaz, puede obtener menos atención por no ser muy técnico, pero obtuve mi voto :) –

+0

+1 por ser simple y no depende de sdk de mezcla, ¡gracias! – kbo4sho88

+2

No olvide dar de baja del evento: P – adminSoftDK

12

Como esto es estrictamente un problema de Vista, no hay ninguna razón para que no pueda tener un controlador de eventos en el código detrás de su vista para este propósito. Escuche el ListBox.SelectionChanged y úselo para desplazar el elemento recién seleccionado a la vista.

private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e) 
{ 
    ((ListBox)sender).ScrollIntoView(e.AddedItems[0]); 
} 

También no necesitan una deriva ListBox para hacer esto. Simplemente use un control estándar y cuando cambie el valor ListBox.SelectedItem (como se describe en su pregunta original), se ejecutará el controlador anterior y el elemento se desplazará a la vista.

<ListBox 
     ItemsSource="{Binding Path=FooCollection}" 
     SelectedItem="{Binding Path=SelectedFoo}" 
     SelectionChanged="ListBox_SelectionChanged" 
     /> 

Otro enfoque sería escribir una propiedad adjunta que escucha ICollectionView.CurrentChanged y luego invoca ListBox.ScrollIntoView para el nuevo elemento actual. Este es un enfoque más "reutilizable" si necesita esta funcionalidad para varios cuadros de lista. Puede encontrar un buen ejemplo aquí para comenzar: http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/

+0

No me importa escuchar un evento, pero la actividad "desplazarse al elemento seleccionado" realmente parece que es parte del control y no de la vista circundante. Sin embargo, podría escribir el detector de eventos dentro de mi subclase ListBoxScroll, y eso me alejaría de la devolución de propiedades dependiente en un delegado de eventos más limpio. –

+0

Me gusta este enfoque pero es un poco engorroso si tiene un número de ListBoxes, puedo tomar esta funcionalidad y derivar mi propio ListBox. Acepto que es estrictamente un problema de Vista y que el código está bien para estar en la vista ... –

+1

@AdriaanDavel solo enlaza todo el ListBox SelectionChanged al mismo controlador, o tal vez ya lo hagas, y solo estamos hablando de todas las declaraciones de suscripción del controlador de eventos son engorrosas? – Zack

43

¿Ha intentado usar Comportamiento ... Aquí hay una Comportamiento ScrollInViewBehavior. Lo he usado para ListView y DataGrid ..... Creo que debería funcionar para ListBox ......

Hay que añadir una referencia a System.Windows.Interactivity utilizar Behavior<T> class

Comportamiento

public class ScrollIntoViewForListBox : Behavior<ListBox> 
{ 
    /// <summary> 
    /// When Beahvior is attached 
    /// </summary> 
    protected override void OnAttached() 
    { 
     base.OnAttached(); 
     this.AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged; 
    } 

    /// <summary> 
    /// On Selection Changed 
    /// </summary> 
    /// <param name="sender"></param> 
    /// <param name="e"></param> 
    void AssociatedObject_SelectionChanged(object sender, 
              SelectionChangedEventArgs e) 
    { 
     if (sender is ListBox) 
     { 
      ListBox listBox = (sender as ListBox); 
      if (listBox .SelectedItem != null) 
      { 
       listBox.Dispatcher.BeginInvoke(
        (Action) (() => 
            { 
             listBox.UpdateLayout(); 
             if (listBox.SelectedItem != 
              null) 
              listBox.ScrollIntoView(
               listBox.SelectedItem); 
            })); 
      } 
     } 
    } 
    /// <summary> 
    /// When behavior is detached 
    /// </summary> 
    protected override void OnDetaching() 
    { 
     base.OnDetaching(); 
     this.AssociatedObject.SelectionChanged -= 
      AssociatedObject_SelectionChanged; 

    } 
} 

Uso

Agregar alias para XAML como xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

entonces en su Control

 <ListBox ItemsSource="{Binding Path=MyList}" 
        SelectedItem="{Binding Path=MyItem, 
             Mode=TwoWay}" 
        SelectionMode="Single"> 
      <i:Interaction.Behaviors> 
       <Behaviors:ScrollIntoViewForListBox /> 
      </i:Interaction.Behaviors> 
     </ListBox> 

Ahora, cuando la propiedad "MyItem" esté configurada en ViewModel, la lista se desplazará cuando se reflejen los cambios.

+0

Eso es realmente genial. Es similar al enfoque de propiedad adjunto descrito por crazyarabian pero se siente bastante más limpio. Con su solución de propiedad adjunta y su solución de comportamiento tendría que escribir una nueva clase. Simplemente parece tener más sentido extender el control ListBox y hacer el manejo del evento en la subclase. Voy a intentarlo hoy y ver si está tan limpio como tu solución de comportamiento. –

+0

@JamesFassett Behavoir se usa generalmente para darte control de la funcionalidad que actualmente no tiene ... IE .... para evitar extender un control .... pero creo que extenderlo es tan bueno como este ... – Ankesh

+0

Solo usando la versión Express. ¿Esta solución necesita Blend? – paul

10

Sé que esta es una pregunta antigua, pero mi búsqueda reciente del mismo problema me ha llevado a esto. Quería utilizar el enfoque de comportamiento, pero no quería una dependencia en el SDK de mezcla sólo para dar Behavior<T> Así que aquí está mi solución sin ella:

public static class ListBoxBehavior 
{ 
    public static bool GetScrollSelectedIntoView(ListBox listBox) 
    { 
     return (bool)listBox.GetValue(ScrollSelectedIntoViewProperty); 
    } 

    public static void SetScrollSelectedIntoView(ListBox listBox, bool value) 
    { 
     listBox.SetValue(ScrollSelectedIntoViewProperty, value); 
    } 

    public static readonly DependencyProperty ScrollSelectedIntoViewProperty = 
     DependencyProperty.RegisterAttached("ScrollSelectedIntoView", typeof (bool), typeof (ListBoxBehavior), 
              new UIPropertyMetadata(false, OnScrollSelectedIntoViewChanged)); 

    private static void OnScrollSelectedIntoViewChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 
    { 
     var selector = d as Selector; 
     if (selector == null) return; 

     if (e.NewValue is bool == false) 
      return; 

     if ((bool) e.NewValue) 
     { 
      selector.AddHandler(Selector.SelectionChangedEvent, new RoutedEventHandler(ListBoxSelectionChangedHandler)); 
     } 
     else 
     { 
      selector.RemoveHandler(Selector.SelectionChangedEvent, new RoutedEventHandler(ListBoxSelectionChangedHandler)); 
     } 
    } 

    private static void ListBoxSelectionChangedHandler(object sender, RoutedEventArgs e) 
    { 
     if (!(sender is ListBox)) return; 

     var listBox = (sender as ListBox); 
     if (listBox.SelectedItem != null) 
     { 
      listBox.Dispatcher.BeginInvoke(
       (Action)(() => 
        { 
         listBox.UpdateLayout(); 
         if (listBox.SelectedItem !=null) 
          listBox.ScrollIntoView(listBox.SelectedItem); 
        })); 
     } 
    } 
} 

y luego uso es simplemente

<ListBox ItemsSource="{Binding Path=MyList}" 
     SelectedItem="{Binding Path=MyItem, Mode=TwoWay}" 
     SelectionMode="Single" 
     behaviors:ListBoxBehavior.ScrollSelectedIntoView="True"> 
+0

Funciona muy bien, prefiero no arrastrar esa dependencia también. – angularsen

+0

¿No debería eliminar el código 'if (e.NewValue is bool == false)'? Si 'e.NewValue' se convierte en falso, el controlador debe eliminarse no? – Ziriax

+0

@Ziriax esta es una prueba de que el tipo de e.NewValue es booleano, no es falso. – Dutts

7

Prueba esto:

private void lstBox_SelectionChanged(object sender, SelectionChangedEventArgs e) 
{ 
    lstBox.ScrollIntoView(lstBox.SelectedItem); 
} 
+0

Esa es una solución, pero creo que la pregunta quería una solución compatible con el patrón MVVM. – user672951

+2

@ user672951 Esto se ajusta tanto al patrón MVVM como a cualquier otra solución en esta pregunta. – Zack

2

Después de atar varios métodos que encontré lo siguiente para ser la más simple y la mejor

lstbox.Items.MoveCurrentToLast(); 
lstbox.ScrollIntoView(lstbox.Items.CurrentItem); 
0

Respondí la respuesta de Ankesh e hice que no dependiera de la mezcla sdk. La desventaja de mi solución es que se aplicará a todas las listas de tu aplicación. Pero el lado positivo no es una clase personalizada necesaria.

Cuando su aplicación se está iniciando ...

internal static void RegisterFrameworkExtensionEvents() 
    { 
     EventManager.RegisterClassHandler(typeof(ListBox), ListBox.SelectionChangedEvent, new RoutedEventHandler(ScrollToSelectedItem)); 
    } 

    //avoid "async void" unless used in event handlers (or logical equivalent) 
    private static async void ScrollToSelectedItem(object sender, RoutedEventArgs e) 
    { 
     if (sender is ListBox) 
     { 
      var lb = sender as ListBox; 
      if (lb.SelectedItem != null) 
      { 
       await lb.Dispatcher.BeginInvoke((Action)delegate 
       { 
        lb.UpdateLayout(); 
        if (lb.SelectedItem != null) 
         lb.ScrollIntoView(lb.SelectedItem); 
       }); 
      } 
     } 
    } 

Esto hace que todos sus cuadros de lista para desplazarse seleccionado (que me gusta como un comportamiento por defecto).

5

estoy usando esto (en mi opinión) solución clara y fácil

listView.SelectionChanged += (s, e) => 
    listView.ScrollIntoView(listView.SelectedItem); 

donde se listView nombre de ListView de control en XAML, SelectedItem se ve afectado de mi MVVM y el código se inserta en el constructor en archivo xaml.cs

+0

Niza gracias :) señor – Peter

+0

@Peter Su bienvenida. – honzakuzel1989

Cuestiones relacionadas