2009-02-27 7 views
6

En mi afán por desarrollar una bonita aplicación Silverlight basada en datos, parece que continuamente me encuentro con algún tipo de condición de carrera que necesita ser solucionada. El último está abajo. Cualquier ayuda sería apreciada.Silverlight Combobox Databinding race condition

Tiene dos tablas en la parte posterior: una es Componentes y una son Fabricantes. Cada componente tiene UN fabricante. En absoluto, una relación de búsqueda de claves extranjeras inusual.

I Silverlight, acceso a los datos a través del servicio WCF. Haré una llamada a Components_Get (id) para obtener el componente actual (para ver o editar) y una llamada a Manufacturers_GetAll() para obtener la lista completa de fabricantes para completar las posibles selecciones para un ComboBox. A continuación, enlace el elemento seleccionado en el ComboBox con el fabricante del componente actual y el ItemSource del ComboBox con la lista de posibles fabricantes. de esta manera:

<UserControl.Resources> 
    <data:WebServiceDataManager x:Key="WebService" /> 
</UserControl.Resources> 
<Grid DataContext={Binding Components.Current, mode=OneWay, Source={StaticResource WebService}}> 
    <ComboBox Grid.Row="2" Grid.Column="2" Style="{StaticResource ComboBoxStyle}" Margin="3" 
       ItemsSource="{Binding Manufacturers.All, Mode=OneWay, Source={StaticResource WebService}}" 
       SelectedItem="{Binding Manufacturer, Mode=TwoWay}" > 
     <ComboBox.ItemTemplate> 
      <DataTemplate> 
       <Grid> 
        <TextBlock Text="{Binding Name}" Style="{StaticResource DefaultTextStyle}"/> 
       </Grid> 
      </DataTemplate> 
     </ComboBox.ItemTemplate> 
    </ComboBox> 
</Grid> 

Esto funcionó muy bien para el tiempo más largo, hasta que llegué inteligente y hice un poco de almacenamiento en caché del lado del cliente del Componente (que había planeado encender para los fabricantes también). Cuando encendí el caché para el Componente y recibí un golpe de caché, todos los datos estarían allí en los objetos correctamente, pero SelectedItem no se uniría. La razón de esto es que las llamadas son asíncronas en Silverlight y, con la ventaja del almacenamiento en caché, el componente no se devuelve antes de los fabricantes. Por lo tanto, cuando SelectedItem intenta encontrar los Components.Current.Manufacturer en la lista ItemsSource, no está allí, porque esta lista todavía está vacía porque Manufacturers.All aún no se ha cargado desde el servicio WCF. Nuevamente, si apago el caché de Componente, funciona nuevamente, pero se siente EQUIVOCADO, como que estoy teniendo suerte de que el tiempo esté funcionando. La solución correcta en mi humilde opinión es que MS arregle el control ComboBox/ItemsControl para comprender que esto sucederá con las llamadas Asynch siendo la norma. Pero hasta entonces, necesito una necesidad de una manera yo lo fijan ...

Estas son algunas de las opciones que he pensado:

  1. eliminar la caché o convertirlo en todos los ámbitos para enmascarar una vez más el problema. No es bueno en mi humilde opinión, porque esto fracasará nuevamente. No estoy realmente dispuesto a barrerlo bajo la alfombra.
  2. Crear un objeto intermediario que haría la sincronización para mí (esto debería hacerse en ItemsControl). Aceptaría una propiedad Item y una lista de Items y luego una salida y ItemWithItemsList cuando ambos hayan llegado. Enlazaría el ComboBox al resultado resultante para que nunca llegue un elemento antes que el otro. Mi problema es que esto parece un dolor, pero asegurará que la condición de carrera no vuelva a ocurrir.

Any thougnts/Comments?

FWIW: Voy a publicar mi solución aquí para el beneficio de los demás.

@Joe: Muchísimas gracias por la respuesta. Soy consciente de la necesidad de actualizar la interfaz de usuario solo desde el hilo de la interfaz de usuario. Tengo entendido y creo que he confirmado esto a través del depurador que en SL2, que el código generado por la referencia de servicio se ocupa de esto por usted. es decir, cuando llamo a Manufacturers_GetAll_Asynch(), obtengo el resultado a través del evento Manufacturers_GetAll_Completed. Si mira dentro del código de referencia de servicio que se genera, se asegura de que el controlador de evento * Completado se invoque desde el hilo de la interfaz de usuario. Mi problema no es esto, es que realizo dos llamadas diferentes (una para la lista de fabricantes y otra para el componente que hace referencia a una identificación de un fabricante) y luego unir ambos resultados a un solo ComboBox. Ambos se unen en el hilo de la interfaz de usuario, el problema es que si la lista no llega antes de la selección, la selección se ignora.

También tenga en cuenta que esto sigue siendo un problema if you just set the ItemSource and the SelectedItem in the wrong order !!!

Otra actualización: Si bien aún existe la condición de carrera de la combobox, descubrí algo más interesante. Debe NUNCA generar un evento PropertyChanged dentro del "captador" para esa propiedad. Ejemplo: en mi objeto de datos SL de tipo ManufacturerData, tengo una propiedad llamada "Todos". En el Get {} comprueba si se ha cargado, si no se carga de esta manera:

public class ManufacturersData : DataServiceAccessbase 
{ 
    public ObservableCollection<Web.Manufacturer> All 
    { 
     get 
     { 
      if (!AllLoaded) 
       LoadAllManufacturersAsync(); 
      return mAll; 
     } 
     private set 
     { 
      mAll = value; 
      OnPropertyChanged("All"); 
     } 
    } 

    private void LoadAllManufacturersAsync() 
    { 
     if (!mCurrentlyLoadingAll) 
     { 
      mCurrentlyLoadingAll = true; 

      // check to see if this component is loaded in local Isolated Storage, if not get it from the webservice 
      ObservableCollection<Web.Manufacturer> all = IsoStorageManager.GetDataTransferObjectFromCache<ObservableCollection<Web.Manufacturer>>(mAllManufacturersIsoStoreFilename); 
      if (null != all) 
      { 
       UpdateAll(all); 
       mCurrentlyLoadingAll = false; 
      } 
      else 
      { 
       Web.SystemBuilderClient sbc = GetSystemBuilderClient(); 
       sbc.Manufacturers_GetAllCompleted += new EventHandler<hookitupright.com.silverlight.data.Web.Manufacturers_GetAllCompletedEventArgs>(sbc_Manufacturers_GetAllCompleted); 
       sbc.Manufacturers_GetAllAsync(); ; 
      } 
     } 
    } 
    private void UpdateAll(ObservableCollection<Web.Manufacturer> all) 
    { 
     All = all; 
     AllLoaded = true; 
    } 
    private void sbc_Manufacturers_GetAllCompleted(object sender, hookitupright.com.silverlight.data.Web.Manufacturers_GetAllCompletedEventArgs e) 
    { 
     if (e.Error == null) 
     { 
      UpdateAll(e.Result.Records); 
      IsoStorageManager.CacheDataTransferObject<ObservableCollection<Web.Manufacturer>>(e.Result.Records, mAllManufacturersIsoStoreFilename); 
     } 
     else 
      OnWebServiceError(e.Error); 
     mCurrentlyLoadingAll = false; 
    } 

} 

Tenga en cuenta que este código FALLA en un "acierto de caché", ya que generará un evento PropertyChanged para "Todos" desde el método Todo {Obtener {}} que normalmente haría que el Sistema de enlace llame a Todos {get {}} otra vez ... Copié este patrón de creación de objetos de datos silverlight vinculables desde un blog ScottGuard y me ha servido bien en general, pero cosas como esta lo hacen bastante complicado. Por suerte, la solución es simple. Espero que esto ayude a alguien más.

Respuesta

7

Ok He encontrado la respuesta (utilizando un gran cantidad de Reflector para descubrir cómo funciona el ComboBox).

El problema existe cuando ItemSource se establece después de que se haya establecido SelectedItem. Cuando esto sucede, el Combobx lo ve como un Restablecimiento completo de la selección y borra el SelectedItem/SelectedIndex. Esto se puede ver aquí en el System.Windows.Controls.Primitives.Selector (la clase base para el cuadro combinado):

protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e) 
{ 
    base.OnItemsChanged(e); 
    int selectedIndex = this.SelectedIndex; 
    bool flag = this.IsInit && this._initializingData.IsIndexSet; 
    switch (e.Action) 
    { 
     case NotifyCollectionChangedAction.Add: 
      if (!this.AddedWithSelectionSet(e.NewStartingIndex, e.NewStartingIndex + e.NewItems.Count)) 
      { 
       if ((e.NewStartingIndex <= selectedIndex) && !flag) 
       { 
        this._processingSelectionPropertyChange = true; 
        this.SelectedIndex += e.NewItems.Count; 
        this._processingSelectionPropertyChange = false; 
       } 
       if (e.NewStartingIndex > this._focusedIndex) 
       { 
        return; 
       } 
       this.SetFocusedItem(this._focusedIndex + e.NewItems.Count, false); 
      } 
      return; 

     case NotifyCollectionChangedAction.Remove: 
      if (((e.OldStartingIndex > selectedIndex) || (selectedIndex >= (e.OldStartingIndex + e.OldItems.Count))) && (e.OldStartingIndex < selectedIndex)) 
      { 
       this._processingSelectionPropertyChange = true; 
       this.SelectedIndex -= e.OldItems.Count; 
       this._processingSelectionPropertyChange = false; 
      } 
      if ((e.OldStartingIndex <= this._focusedIndex) && (this._focusedIndex < (e.OldStartingIndex + e.OldItems.Count))) 
      { 
       this.SetFocusedItem(-1, false); 
       return; 
      } 
      if (e.OldStartingIndex < selectedIndex) 
      { 
       this.SetFocusedItem(this._focusedIndex - e.OldItems.Count, false); 
      } 
      return; 

     case NotifyCollectionChangedAction.Replace: 
      if (!this.AddedWithSelectionSet(e.NewStartingIndex, e.NewStartingIndex + e.NewItems.Count)) 
      { 
       if ((e.OldStartingIndex <= selectedIndex) && (selectedIndex < (e.OldStartingIndex + e.OldItems.Count))) 
       { 
        this.SelectedIndex = -1; 
       } 
       if ((e.OldStartingIndex > this._focusedIndex) || (this._focusedIndex >= (e.OldStartingIndex + e.OldItems.Count))) 
       { 
        return; 
       } 
       this.SetFocusedItem(-1, false); 
      } 
      return; 

     case NotifyCollectionChangedAction.Reset: 
      if (!this.AddedWithSelectionSet(0, base.Items.Count) && !flag) 
      { 
       this.SelectedIndex = -1; 
       this.SetFocusedItem(-1, false); 
      } 
      return; 
    } 
    throw new InvalidOperationException(); 
} 

Nota el último caso - el restablecimiento ... Cuando se carga un nuevo ItemSource se termina aquí y cualquier SelectedItem/SelectedIndex se queda sin aliento?!?!

Bueno, la solución fue bastante simple al final. Acabo de subclasificar el ComboBox errante y proporcioné y anulé este método de la siguiente manera. A pesar de que tenía que añadir un:

public class FixedComboBox : ComboBox 
{ 
    public FixedComboBox() 
     : base() 
    { 
     // This is here to sync the dep properties (OnSelectedItemChanged is private is the base class - thanks M$) 
     base.SelectionChanged += (s, e) => { FixedSelectedItem = SelectedItem; }; 
    } 

    // need to add a safe dependency property here to bind to - this will store off the "requested selectedItem" 
    // this whole this is a kludgy wrapper because the OnSelectedItemChanged is private in the base class 
    public readonly static DependencyProperty FixedSelectedItemProperty = DependencyProperty.Register("FixedSelectedItem", typeof(object), typeof(FixedComboBox), new PropertyMetadata(null, new PropertyChangedCallback(FixedSelectedItemPropertyChanged))); 
    private static void FixedSelectedItemPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) 
    { 
     FixedComboBox fcb = obj as FixedComboBox; 
     fcb.mLastSelection = e.NewValue; 
     fcb.SelectedItem = e.NewValue; 
    } 
    public object FixedSelectedItem 
    { 
     get { return GetValue(FixedSelectedItemProperty); } 
     set { SetValue(FixedSelectedItemProperty, value);} 
    } 
    protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e) 
    { 
     base.OnItemsChanged(e); 
     if (-1 == SelectedIndex) 
     { 
      // if after the base class is called, there is no selection, try 
      if (null != mLastSelection && Items.Contains(mLastSelection)) 
       SelectedItem = mLastSelection; 
     } 
    } 

    protected object mLastSelection = null; 
} 

Todo lo que esto hace es (a) guardar el viejo SelectedItem y después (b) comprobar que si después de la ItemsChanged, si no tenemos ninguna selección realizada y el viejo SelectedItem existe en la nueva lista ... bueno ... ¡Seleccionado!

+0

Esta solución es común. Durante mucho tiempo, he estado buscando una solución más genérica que cubra todos los controles Selector; no solo ComboBoxes, y lo hace sin heredar de ningún control. Hay una manera de hacer esto con comportamientos. Esta solución propuesta también funciona en UWP, y probablemente WPF: http://stackoverflow.com/questions/36003805/uwp-silverlight-combobox-selector-itemssource-selecteditem-race-condition-solu –

0

No está claro en su publicación si es consciente de que debe modificar los elementos de la IU en el hilo de UI, o tendrá problemas. Aquí hay un breve ejemplo que crea un hilo de fondo que modifica un TextBox con la hora actual.

La clave es MyTextBox.Dispather.BeginInvoke en Page.xaml.cs.

Page.xaml:

<UserControl x:Class="App.Page" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300" 
      Loaded="UserControl_Loaded"> 
    <Grid x:Name="LayoutRoot"> 
     <TextBox FontSize="36" Text="Just getting started." x:Name="MyTextBox"> 
     </TextBox> 
    </Grid> 
</UserControl> 

Page.xaml.cs:

using System; 
using System.Windows; 
using System.Windows.Controls; 

namespace App 
{ 
    public partial class Page : UserControl 
    { 
     public Page() 
     { 
      InitializeComponent(); 
     } 

     private void UserControl_Loaded(object sender, RoutedEventArgs e) 
     { 
      // Create our own thread because it runs forever. 
      new System.Threading.Thread(new System.Threading.ThreadStart(RunForever)).Start(); 
     } 

     void RunForever() 
     { 
      System.Random rand = new Random(); 
      while (true) 
      { 
       // We want to get the text on the background thread. The idea 
       // is to do as much work as possible on the background thread 
       // so that we do as little work as possible on the UI thread. 
       // Obviously this matters more for accessing a web service or 
       // database or doing complex computations - we do this to make 
       // the point. 
       var now = System.DateTime.Now; 
       string text = string.Format("{0}.{1}.{2}.{3}", now.Hour, now.Minute, now.Second, now.Millisecond); 

       // We must dispatch this work to the UI thread. If we try to 
       // set MyTextBox.Text from this background thread, an exception 
       // will be thrown. 
       MyTextBox.Dispatcher.BeginInvoke(delegate() 
       { 
        // This code is executed asynchronously on the 
        // Silverlight UI Thread. 
        MyTextBox.Text = text; 
       }); 
       // 
       // This code is running on the background thread. If we executed this 
       // code on the UI thread, the UI would be unresponsive. 
       // 
       // Sleep between 0 and 2500 millisends. 
       System.Threading.Thread.Sleep(rand.Next(2500)); 
      } 
     } 
    } 
} 

lo tanto, si usted quiere conseguir las cosas de forma asíncrona, que tendrán que utilizar para notificar Control.Dispatcher.BeginInvoke el elemento UI que tiene algunos datos nuevos.

+0

soy consciente de la necesidad de actualizar la interfaz de usuario de la interfaz de usuario de rosca. Verme editar en la pregunta original sobre esto (espacio limitado aquí y parecía justificar ponerlo allí). – caryden

0

En lugar de volver a enlazar el ItemsSource cada vez hubiera sido más fácil para usted vincularlo a un ObservableCollection <> y luego llamar a Clear() en él y Agregar (...) todos los elementos. De esta forma, el enlace no se restablece.

Otro problema es que el elemento seleccionado DEBE ser una instancia de los objetos en la lista. Cometí un error una vez cuando pensé que la lista consultada para el artículo predeterminado se había solucionado, pero se regeneró en cada llamada. Por lo tanto, la corriente era diferente, aunque tenía una propiedad DisplayPath que era la misma que un elemento de la lista.

Aún puede obtener el ID del elemento actual (o lo que sea que lo defina de manera única), vuelva a enlazar el control y luego encuentre en la lista encuadernada el artículo con el mismo ID y vincule ese elemento como el actual.

1

Luché con este mismo problema al crear cuadros combinados en cascada, y tropecé con una publicación de blog de alguien que encontró una solución fácil pero sorprendente. Llamar a UpdateLayout() después de establecer .ItemsSource pero antes de establecer SelectedItem. Esto debe obligar al código a bloquearse hasta que se complete el enlace de datos. No estoy exactamente seguro de por qué lo fija pero no he experimentado la condición de carrera de nuevo desde ...

Fuente de esta información: http://compiledexperience.com/Blog/post/Gotcha-when-databinding-a-ComboBox-in-Silverlight.aspx

2

estaba indignado cuando me encontré con este problema, pero Pensé que tenía que haber una forma de evitarlo. Mi mejor esfuerzo hasta ahora se detalla en la publicación.

http://blogs.msdn.com/b/kylemc/archive/2010/06/18/combobox-sample-for-ria-services.aspx

Yo era muy feliz, ya que reducía la sintaxis a algo como lo siguiente.

<ComboBox Name="AComboBox" 
     ItemsSource="{Binding Data, ElementName=ASource}" 
     SelectedItem="{Binding A, Mode=TwoWay}" 
     ex:ComboBox.Mode="Async" /> 

Kyle

+0

Gracias Kyle. Todo lo que he intentado hasta ahora es, por ejemplo, ComboBox.Mode = "AsyncEager", pero eliminó la restricción de que SelectedItem debe establecerse antes de ItemsSource, que parece ser el núcleo de muchos de los problemas descritos aquí. ¿Sabes si habrá una solución nativa en Silverlight 5? –

+0

No sé lo que será en SL5, pero no he escuchado nada en este sentido. –

0

En caso de llegar aquí porque usted tiene un problema de selección de cuadro combinado, es decir, no pasa nada cuando se hace clic en el elemento de la lista. Tenga en cuenta que los siguientes consejos también te pueden ayudar:

1/Asegúrese de que usted no notifica algo en caso de que seleccione un elemento

public string SelectedItem 
     { 
      get 
      { 
       return this.selectedItem; 
      } 
      set 
      { 
       if (this.selectedItem != value) 
       { 
        this.selectedItem = value; 
        //this.OnPropertyChanged("SelectedItem"); 
       } 
      } 
     } 

2/asegúrese de que el elemento que seleccione todavía está en el origen de datos subyacente en caso de que la elimine por accidente

hice dos errores;)