2011-03-09 20 views
20

Ok, esto me ha estado molestando por un tiempo. Y me pregunto cómo otros manejan el siguiente caso:ComboBox ItemsSource changed => SelectedItem está en ruinas

<ComboBox ItemsSource="{Binding MyItems}" SelectedItem="{Binding SelectedItem}"/> 

El código del objeto DataContext:

public ObservableCollection<MyItem> MyItems { get; set; } 
public MyItem SelectedItem { get; set; } 

public void RefreshMyItems() 
{ 
    MyItems.Clear(); 
    foreach(var myItem in LoadItems()) MyItems.Add(myItem); 
} 

public class MyItem 
{ 
    public int Id { get; set; } 
    public override bool Equals(object obj) 
    { 
     return this.Id == ((MyItem)obj).Id; 
    } 
} 

Obviamente, cuando el método RefreshMyItems() se llama el cuadro combinado recibe los acontecimientos de colecciones cambiado, actualiza sus artículos y lo hace no encontrar el elemento seleccionado en la colección actualizada => establece el elemento seleccionado en null. Pero necesitaría el cuadro combinado para usar el método Equals para seleccionar el elemento correcto en la nueva colección.

En otras palabras, la colección ItemsSource todavía contiene el MyItem correcto, pero es un objeto new. Y quiero que el cuadro combinado use algo como Equals para seleccionarlo automáticamente (esto se hace aún más difícil porque primero la colección fuente llama a Clear() que restablece la colección y ya en ese punto el elemento seleccionado se establece en null).

ACTUALIZACIÓN 2 ¡Antes de copiar y pegar el siguiente código, tenga en cuenta que está lejos de la perfección! Y tenga en cuenta que no se une de dos maneras por defecto.

ACTUALIZACIÓN Por si alguien tiene el mismo problema (una propiedad que se adjunta como propuesto por Pavlo Glazkov en su respuesta):

public static class CBSelectedItem 
{ 
    public static object GetSelectedItem(DependencyObject obj) 
    { 
     return (object)obj.GetValue(SelectedItemProperty); 
    } 

    public static void SetSelectedItem(DependencyObject obj, object value) 
    { 
     obj.SetValue(SelectedItemProperty, value); 
    } 

    // Using a DependencyProperty as the backing store for SelectedIte. This enables animation, styling, binding, etc... 
    public static readonly DependencyProperty SelectedItemProperty = 
     DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(CBSelectedItem), new UIPropertyMetadata(null, SelectedItemChanged)); 


    private static List<WeakReference> ComboBoxes = new List<WeakReference>(); 
    private static void SelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 
    { 
     ComboBox cb = (ComboBox) d; 

     // Set the selected item of the ComboBox since the value changed 
     if (cb.SelectedItem != e.NewValue) cb.SelectedItem = e.NewValue; 

     // If we already handled this ComboBox - return 
     if(ComboBoxes.SingleOrDefault(o => o.Target == cb) != null) return; 

     // Check if the ItemsSource supports notifications 
     if(cb.ItemsSource is INotifyCollectionChanged) 
     { 
      // Add ComboBox to the list of handled combo boxes so we do not handle it again in the future 
      ComboBoxes.Add(new WeakReference(cb)); 

      // When the ItemsSource collection changes we set the SelectedItem to correct value (using Equals) 
      ((INotifyCollectionChanged) cb.ItemsSource).CollectionChanged += 
       delegate(object sender, NotifyCollectionChangedEventArgs e2) 
        { 
         var collection = (IEnumerable<object>) sender; 
         cb.SelectedItem = collection.SingleOrDefault(o => o.Equals(GetSelectedItem(cb))); 
        }; 

      // If the user has selected some new value in the combo box - update the attached property too 
      cb.SelectionChanged += delegate(object sender, SelectionChangedEventArgs e3) 
             { 
              // We only want to handle cases that actually change the selection 
              if(e3.AddedItems.Count == 1) 
              { 
               SetSelectedItem((DependencyObject)sender, e3.AddedItems[0]); 
              } 
             }; 
     } 

    } 
} 
+1

Iv'e se encontró con este problema y lo resolvió de la siguiente manera http://stackoverflow.com/questions/12337442/proper-use-of-propertychangedtrigger-and-changepropertyaction/12341649#12341649 –

Respuesta

7

La norma ComboBox no tiene esa lógica. Y como mencionó SelectedItem se convierte en null después de llamar al Clear, por lo que el ComboBox no tiene idea sobre su intención de agregar el mismo elemento más adelante y, por lo tanto, no hace nada para seleccionarlo. Una vez dicho esto, tendrá que memorizar el elemento previamente seleccionado manualmente y después de que haya actualizado su colección, restaure la selección también manualmente. Por lo general, se hace algo como esto:

public void RefreshMyItems() 
{ 
    var previouslySelectedItem = SelectedItem; 

    MyItems.Clear(); 
    foreach(var myItem in LoadItems()) MyItems.Add(myItem); 

    SelectedItem = MyItems.SingleOrDefault(i => i.Id == previouslySelectedItem.Id); 

} 

Si desea aplicar el mismo comportamiento a todos los ComboBoxes (o tal vez todos los controles Selector), se puede considerar la creación de un Behavior (un attached property o blend behavior). Este comportamiento se suscribirá a los eventos SelectionChanged y CollectionChanged y guardará/restaurará el elemento seleccionado cuando corresponda.

+0

Sí, exactamente mis pensamientos :) Pensé en escribir una propiedad adjunta para este caso. ¡Gracias por la pista! (Actualicé la publicación con el código de propiedad adjunto) – Jefim

+0

Este es un problema común con ComboBoxes y otros controles Selector en WPF, Silverlight y UWP. Aquí hay una solución propuesta para solucionar el problema en todos los controles y plataformas Selector sin tener que escribir códigos detrás del código cada vez. http://stackoverflow.com/questions/36003805/uwp-silverlight-combobox-selector-itemssource-selecteditem-race-condition-solu –

+0

@MelbourneDeveloper el enlace está desactivado - devuelve la página no encontrada. – tomosius

0

Usted puede considerar el uso de un valueconverter para seleccionar el SlectedItem correcta de su colección

+0

Es una solución, por supuesto. Aunque luego viene una pregunta: ¿hay alguna manera de aplicar esto a un conjunto completo de cuadros combinados? Tampoco creo que haya una buena forma de acceder al objetivo vinculante desde un convertidor de valor (y necesitaré acceder a ItemsSource para seleccionar el correcto). – Jefim

+0

Puede usar un convertidor multivalor y pasarlo en su colección como vinculante – biju

+0

El convertidor de valor no lo ayudará porque después de llamar a 'Borrar' no hay nada que seleccionar. Necesitarás almacenar el elemento previamente seleccionado en algún lugar de todos modos. –

13

Este es el resultado de Google superior "WPF ItemsSource es igual a" en este momento, por lo que cualquiera que trate el mismo enfoque que en la pregunta, que hace trabajo, siempre y cuando totalmente implementar funciones de igualdad. Aquí es una implementación completa myItem:

public class MyItem : IEquatable<MyItem> 
{ 
    public int Id { get; set; } 

    public bool Equals(MyItem other) 
    { 
     if (Object.ReferenceEquals(other, null)) return false; 
     if (Object.ReferenceEquals(other, this)) return true; 
     return this.Id == other.Id; 
    } 

    public sealed override bool Equals(object obj) 
    { 
     var otherMyItem = obj as MyItem; 
     if (Object.ReferenceEquals(otherMyItem, null)) return false; 
     return otherMyItem.Equals(this); 
    } 

    public override int GetHashCode() 
    { 
     return this.Id.GetHashCode(); 
    } 

    public static bool operator ==(MyItem myItem1, MyItem myItem2) 
    { 
     return Object.Equals(myItem1, myItem2); 
    } 

    public static bool operator !=(MyItem myItem1, MyItem myItem2) 
    { 
     return !(myItem1 == myItem2); 
    } 
} 

He probado con éxito este con un cuadro de lista de selección múltiple, donde listbox.SelectedItems.Add(item) estaba fallando para seleccionar el elemento coincidente, pero trabajé después he implementado lo anterior en item.

+0

Gracias por su respuesta! ¡Había olvidado implementar operator == y operator! = ... ¡y esto me estaba causando dolores de cabeza! – cplotts

-3
public MyItem SelectedItem { get; set; } 
    private MyItem selectedItem ; 
    // <summary> 
    /////// 
    // </summary> 
    public MyItem SelectedItem 
    { 
     get { return selectedItem ; } 
     set 
     { 
      if (value != null && selectedItem != value) 
      { 
       selectedItem = value; 
       if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem ")); } 
      } 
     } 
    } 
+0

Es extremadamente difícil ver cómo esta es una respuesta a la pregunta planteada. Sin alguna explicación aquí, parece que acabas de publicar algún código aleatorio. –

6

Por desgracia la hora de establecer ItemsSource en un objeto de selección se establece de inmediato SelectedValue o SelectedItem a nulo, incluso si el artículo correspondiente está en el nuevo ItemsSource.

No importa si implementa las funciones Iguales .. o si utiliza un tipo implícitamente comparable para su SelectedValue.

Bueno, puede guardar SelectedItem/Value antes de establecer ItemsSource y restaurar. Pero, ¿qué ocurre si hay un enlace en SelectedItem/Value que se llamará dos veces: establecido en nulo restaurar original.

Eso es una carga adicional e incluso puede causar un comportamiento no deseado.

Aquí hay una solución que hice. Funcionará para cualquier objeto Selector. Simplemente borre el enlace SelectedValue antes de establecer ItemsSource.

UPD: se ha agregado try/finally para proteger de excepciones en los controladores, también se ha agregado la verificación nula para el enlace.

public static class ComboBoxItemsSourceDecorator 
{ 
    public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.RegisterAttached(
     "ItemsSource", typeof(IEnumerable), typeof(ComboBoxItemsSourceDecorator), new PropertyMetadata(null, ItemsSourcePropertyChanged) 
    ); 

    public static void SetItemsSource(UIElement element, IEnumerable value) 
    { 
     element.SetValue(ItemsSourceProperty, value); 
    } 

    public static IEnumerable GetItemsSource(UIElement element) 
    { 
     return (IEnumerable)element.GetValue(ItemsSourceProperty); 
    } 

    static void ItemsSourcePropertyChanged(DependencyObject element, 
        DependencyPropertyChangedEventArgs e) 
    { 
     var target = element as Selector; 
     if (element == null) 
      return; 

     // Save original binding 
     var originalBinding = BindingOperations.GetBinding(target, Selector.SelectedValueProperty); 

     BindingOperations.ClearBinding(target, Selector.SelectedValueProperty); 
     try 
     { 
      target.ItemsSource = e.NewValue as IEnumerable; 
     } 
     finally 
     { 
      if (originalBinding != null) 
       BindingOperations.SetBinding(target, Selector.SelectedValueProperty, originalBinding); 
     } 
    } 
} 

Aquí está un ejemplo de XAML:

   <telerik:RadComboBox Grid.Column="1" x:Name="cmbDevCamera" DataContext="{Binding Settings}" SelectedValue="{Binding SelectedCaptureDevice}" 
            SelectedValuePath="guid" e:ComboBoxItemsSourceDecorator.ItemsSource="{Binding CaptureDeviceList}" > 
       </telerik:RadComboBox> 

Test Unit

Este es un caso de prueba de unidad que demuestra que funciona. Simplemente comente el #define USE_DECORATOR para ver que la prueba falla al usar los enlaces estándar.

#define USE_DECORATOR 

using System.Collections; 
using System.Collections.Concurrent; 
using System.Collections.Generic; 
using System.Security.Permissions; 
using System.Threading.Tasks; 
using System.Windows; 
using System.Windows.Controls; 
using System.Windows.Controls.Primitives; 
using System.Windows.Data; 
using System.Windows.Threading; 
using FluentAssertions; 
using ReactiveUI; 
using ReactiveUI.Ext; 
using ReactiveUI.Fody.Helpers; 
using Xunit; 

namespace Weingartner.Controls.Spec 
{ 
    public class ComboxBoxItemsSourceDecoratorSpec 
    { 
     [WpfFact] 
     public async Task ControlSpec() 
     { 
      var comboBox = new ComboBox(); 
      try 
      { 

       var numbers1 = new[] {new {Number = 10, i = 0}, new {Number = 20, i = 1}, new {Number = 30, i = 2}}; 
       var numbers2 = new[] {new {Number = 11, i = 3}, new {Number = 20, i = 4}, new {Number = 31, i = 5}}; 
       var numbers3 = new[] {new {Number = 12, i = 6}, new {Number = 20, i = 7}, new {Number = 32, i = 8}}; 

       comboBox.SelectedValuePath = "Number"; 
       comboBox.DisplayMemberPath = "Number"; 


       var binding = new Binding("Numbers"); 
       binding.Mode = BindingMode.OneWay; 
       binding.UpdateSourceTrigger=UpdateSourceTrigger.PropertyChanged; 
       binding.ValidatesOnDataErrors = true; 

#if USE_DECORATOR 
       BindingOperations.SetBinding(comboBox, ComboBoxItemsSourceDecorator.ItemsSourceProperty, binding); 
#else 
       BindingOperations.SetBinding(comboBox, ItemsControl.ItemsSourceProperty, binding); 
#endif 

       DoEvents(); 

       var selectedValueBinding = new Binding("SelectedValue"); 
       BindingOperations.SetBinding(comboBox, Selector.SelectedValueProperty, selectedValueBinding); 

       var viewModel = ViewModel.Create(numbers1, 20); 
       comboBox.DataContext = viewModel; 

       // Check the values after the data context is initially set 
       comboBox.SelectedIndex.Should().Be(1); 
       comboBox.SelectedItem.Should().BeSameAs(numbers1[1]); 
       viewModel.SelectedValue.Should().Be(20); 

       // Change the list of of numbers and check the values 
       viewModel.Numbers = numbers2; 
       DoEvents(); 

       comboBox.SelectedIndex.Should().Be(1); 
       comboBox.SelectedItem.Should().BeSameAs(numbers2[1]); 
       viewModel.SelectedValue.Should().Be(20); 

       // Set the list of numbers to null and verify that SelectedValue is preserved 
       viewModel.Numbers = null; 
       DoEvents(); 

       comboBox.SelectedIndex.Should().Be(-1); 
       comboBox.SelectedValue.Should().Be(20); // Notice that we have preserved the SelectedValue 
       viewModel.SelectedValue.Should().Be(20); 


       // Set the list of numbers again after being set to null and see that 
       // SelectedItem is now correctly mapped to what SelectedValue was. 
       viewModel.Numbers = numbers3; 
       DoEvents(); 

       comboBox.SelectedIndex.Should().Be(1); 
       comboBox.SelectedItem.Should().BeSameAs(numbers3[1]); 
       viewModel.SelectedValue.Should().Be(20); 


      } 
      finally 
      { 
       Dispatcher.CurrentDispatcher.InvokeShutdown(); 
      } 
     } 

     public class ViewModel<T> : ReactiveObject 
     { 
      [Reactive] public int SelectedValue { get; set;} 
      [Reactive] public IList<T> Numbers { get; set; } 

      public ViewModel(IList<T> numbers, int selectedValue) 
      { 
       Numbers = numbers; 
       SelectedValue = selectedValue; 
      } 
     } 

     public static class ViewModel 
     { 
      public static ViewModel<T> Create<T>(IList<T> numbers, int selectedValue)=>new ViewModel<T>(numbers, selectedValue); 
     } 

     /// <summary> 
     /// From http://stackoverflow.com/a/23823256/158285 
     /// </summary> 
     public static class ComboBoxItemsSourceDecorator 
     { 
      private static ConcurrentDictionary<DependencyObject, Binding> _Cache = new ConcurrentDictionary<DependencyObject, Binding>(); 

      public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.RegisterAttached(
       "ItemsSource", typeof(IEnumerable), typeof(ComboBoxItemsSourceDecorator), new PropertyMetadata(null, ItemsSourcePropertyChanged) 
      ); 

      public static void SetItemsSource(UIElement element, IEnumerable value) 
      { 
       element.SetValue(ItemsSourceProperty, value); 
      } 

      public static IEnumerable GetItemsSource(UIElement element) 
      { 
       return (IEnumerable)element.GetValue(ItemsSourceProperty); 
      } 

      static void ItemsSourcePropertyChanged(DependencyObject element, 
          DependencyPropertyChangedEventArgs e) 
      { 
       var target = element as Selector; 
       if (target == null) 
        return; 

       // Save original binding 
       var originalBinding = BindingOperations.GetBinding(target, Selector.SelectedValueProperty); 
       BindingOperations.ClearBinding(target, Selector.SelectedValueProperty); 
       try 
       { 
        target.ItemsSource = e.NewValue as IEnumerable; 
       } 
       finally 
       { 
        if (originalBinding != null) 
         BindingOperations.SetBinding(target, Selector.SelectedValueProperty, originalBinding); 
       } 
      } 
     } 

     [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)] 
     public static void DoEvents() 
     { 
      DispatcherFrame frame = new DispatcherFrame(); 
      Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame); 
      Dispatcher.PushFrame(frame); 
     } 

     private static object ExitFrame(object frame) 
     { 
      ((DispatcherFrame)frame).Continue = false; 
      return null; 
     } 


    } 
} 
+2

Esta solución también funciona para el cuadro combinado predeterminado. Sin embargo, debe verificar si originalBinding no es nulo antes de ejecutar la última línea de código, porque en la primera carga de la fuente del elemento, el enlace original será nulo. – alexandrudicu

+0

Esto fue muy útil para un problema similar repentino que encontramos desde que se ejecutaba en el tiempo de ejecución 4.5.1. ¡Gracias! –

+0

Esto se debe subir más, ya que es la única respuesta válida válida, ya que elimina el problema por completo y funciona en aplicaciones de múltiples hilos. – bokibeg

-1

Después de perder la mitad de mis pelos de la cabeza y rompiendo mi teclado varias veces, pienso que para el control de cuadro combinado, es preferible no escribir la selectedItem, SelectedIndex y ItemsSource expresión de enlace en el XAML, ya que no puede compruebe si ItemsSource ha cambiado, al usar la propiedad ItemsSource, por supuesto.

En la ventana o el constructor de control de usuario configuro la propiedad ItemsSource del Combobox luego en el controlador de eventos cargado de la ventana o control de usuario, configuro la expresión de enlace y funciona perfectamente. Si estableciera la expresión de enlace de ItemsSource en el XAML sin el elemento "selectedItem", no encontraría ningún controlador de eventos para establecer la expresión de enlace SelectedItem al tiempo que evita que combobox actualice la fuente con una referencia nula (selectedIndex = -1).

0

La verdadera solución a este problema es no eliminar los elementos que están en la nueva lista. ES DECIR. No borre toda la lista, simplemente elimine los que no están en la lista nueva y luego agregue los que tiene esa nueva lista que no estaban en la lista anterior.

Ejemplo.

Artículos

actual cuadro combinado manzana, naranja, plátano

nuevos Combo elementos de cuadro manzana, naranja, pera

para poblar los nuevos artículos Retire Plátano y Añadir pera

Ahora el combo bow sigue siendo válido para los elementos que podría haber seleccionado y los elementos ahora se borran si fueron seleccionados.

0

acabo de implementar una anulación muy simple y parece estar funcionando visualmente, sin embargo, esto corta montón de lógica interna, así que no estoy seguro de que es una solución segura:

public class MyComboBox : ComboBox 
{ 
    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e) 
    { 
     return; 
    } 
} 

lo tanto, si se utiliza este control y luego cambiar Items/ItemsSource no afectará SelectedValue y Text; permanecerán intactos.

Háganme saber si encuentra problemas que cause.

Cuestiones relacionadas