2009-02-27 11 views
6

Nella mia ricerca per sviluppare una graziosa app Silverlight basata sui dati, mi sembra di imbattersi continuamente in una sorta di condizione di gara che deve essere risolta. L'ultimo è qui sotto. Qualsiasi aiuto sarebbe apprezzato.Silverlight Combobox Condizioni di gara per la legatura dei dati

Hai due tabelle sul back-end: una è Componenti e una è Produttori. Ogni componente ha UN produttore. Niente affatto una strana relazione di ricerca di chiavi estranee.

I Silverlight, accesso ai dati tramite il servizio WCF. Effettuerò una chiamata a Components_Get (id) per ottenere il componente Corrente (da visualizzare o modificare) e una chiamata a Manufacturers_GetAll() per ottenere l'elenco completo dei produttori per popolare le possibili selezioni per un ComboBox. Quindi lego il SelectedItem sul ComboBox al produttore per il componente corrente e il ItemSource sul ComboBox all'elenco dei possibili produttori. in questo modo:

<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> 

Questo ha funzionato grande per il tempo più lungo, fino a quando ho avuto intelligente e fatto un po 'di caching lato client del componente (che ho programmato accendere per i produttori pure). Quando ho attivato la memorizzazione nella cache per il Component e ho ottenuto un hit della cache, tutti i dati sarebbero presenti negli oggetti correttamente, ma il SelectedItem non sarebbe riuscito a eseguire il binding. La ragione di ciò è che le chiamate sono asincrone in Silverlight e con il vantaggio della memorizzazione nella cache, il componente non viene restituito prima dei produttori. Quindi, quando l'oggetto SelectedItem tenta di trovare Components.Current.Manufacturer nell'elenco ItemsSource, non è lì, perché questo elenco è ancora vuoto perché Manufacturers.All non è ancora stato caricato dal servizio WCF. Di nuovo, se spengo la cache del componente, funziona di nuovo, ma mi sembra sbagliato - come se fossi solo fortunato che il tempismo stia funzionando. La correzione corretta IMHO è per MS per risolvere il controllo ComboBox/ItemsControl per capire che questo accadrà con le chiamate Asynch che sono la norma. Ma fino ad allora, ho bisogno di un bisogno di un modo yo fissarlo ...

Qui ci sono alcune opzioni che ho pensato:

  1. eliminare il caching o accenderlo su tutta la linea per mascherare ancora una volta il problema. Non buono IMHO, perché questo fallirà di nuovo. Non proprio disposto a spazzarlo di nuovo sotto il tappeto.
  2. Creare un oggetto intermedio che esegua la sincronizzazione per me (che dovrebbe essere eseguito in ItemsControl stesso). Accetterebbe e Item e un ItemsList e quindi l'output e la proprietà ItemWithItemsList quando entrambi sono arrivati. Vorrei legare il ComboBox all'output risultante in modo che non potesse mai ottenere un oggetto prima dell'altro. Il mio problema è che questo sembra un dolore, ma farà in modo che la condizione di gara non si ripresenti.

Qualche idea/commenti?

FWIW: posterò la mia soluzione qui a beneficio degli altri.

@Joe: Grazie mille per la risposta. Sono consapevole della necessità di aggiornare l'interfaccia utente solo dal thread dell'interfaccia utente. È una mia comprensione e penso di aver confermato questo tramite il debugger che in SL2, il codice generato dalla Service Reference si prende cura di questo per voi. Quando chiamo Manufacturers_GetAll_Asynch(), ottengo il risultato attraverso l'evento Manufacturer_GetAll_Completed. Se si guarda all'interno del codice di riferimento del servizio che viene generato, si assicura che il * gestore di eventi completato sia chiamato dal thread dell'interfaccia utente. Il mio problema non è questo, è che faccio due chiamate diverse (una per l'elenco produttori e una per il componente che fa riferimento a un id di un produttore) e quindi associare entrambi questi risultati a un singolo ComboBox. Entrambi si collegano al thread dell'interfaccia utente, il problema è che se la lista non arriva prima della selezione, la selezione viene ignorata.

Si noti che questo è ancora un problema if you just set the ItemSource and the SelectedItem in the wrong order !!!

Un altro aggiornamento: Mentre c'è ancora la condizione della competizione combo, ho scoperto qualcos'altro di interessante. È necessario MAI generare un evento PropertyChanged dall'interno del "getter" per quella proprietà. Esempio: nel mio oggetto dati SL di tipo ManufacturerData, ho una proprietà chiamata "All". Nella Get {} controlla per vedere se è stato caricato, se non lo carica in questo modo:

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; 
    } 

} 

Si noti che questo codice NON su una "cache hit", perché genererà un evento PropertyChanged per "Tutti" dal metodo All {Get {}} che normalmente causerebbe al Sistema Binding di chiamare All {get {}} di nuovo ... Ho copiato questo modello di creazione di oggetti dati Silverlight associabili da un blog ScottGu postando e mi è servito molto bene, ma roba del genere lo rende piuttosto complicato. Fortunatamente la soluzione è semplice. Spero che questo aiuti qualcun altro.

risposta

7

Ok ho trovato la risposta (utilizzando un sacco di Reflector per capire come funziona il ComboBox).

Il problema si verifica quando ItemSource viene impostato dopo l'impostazione di SelectedItem. Quando ciò accade, Combobx lo vede come un Reset completo della selezione e cancella SelectedItem/SelectedIndex. Potete vedere questo qui in System.Windows.Controls.Primitives.Selector (la classe base per la ComboBox):

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 l'ultimo caso - il reset ... Quando si carica un nuovo ItemSource si finisce quassù e qualsiasi SelectedItem/SelectedIndex viene spazzato via?!?!

Bene, la soluzione è stata piuttosto semplice alla fine. Ho appena sottoclassato il ComboBox errante e fornito e sovrascrivo questo metodo come segue. Anche se ho dovuto aggiungere 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; 
} 

tutto ciò che questo fa è (a) salvare il vecchio SelectedItem e poi (b) verificare che, se dopo l'ItemsChanged, se non abbiamo selezione effettuata e il vecchio SelectedItem esiste nella nuova lista ... beh ... Selected It!

+0

Questa soluzione è comune. Per molto tempo, ho cercato una soluzione più generica che copra tutti i controlli Selector; non solo ComboBoxes, e lo fa senza ereditare da alcun controllo. C'è un modo per farlo con i comportamenti. Questa soluzione proposta funziona anche in UWP e probabilmente in WPF: http://stackoverflow.com/questions/36003805/uwp-silverlight-combobox-selector-itemssource-selecteditem-race-condition-solu –

0

Non è chiaro dal tuo post se sei consapevole del fatto che è necessario modificare gli elementi dell'interfaccia utente nel thread dell'interfaccia utente, altrimenti si avranno problemi. Ecco un breve esempio che crea un thread in background che modifica un TextBox con l'ora corrente.

La chiave è MyTextBox.Dispather.BeginInvoke in 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)); 
      } 
     } 
    } 
} 

Quindi, se si vuole ottenere le cose in modo asincrono, si dovranno utilizzare per notificare Control.Dispatcher.BeginInvoke l'elemento dell'interfaccia utente che hai alcuni nuovi dati.

+0

sono consapevole della necessità di aggiornare l'interfaccia utente dal thread dell'interfaccia utente. Guardami modificare nella domanda originale su questo (spazio limitato qui e sembrava giustificarlo inserendolo lì). – caryden

0

Piuttosto che ricontrollare ItemsSource ogni volta sarebbe stato più semplice collegarlo a ObservableCollection <> e quindi chiamare Cancella() su di esso e Aggiungi (...) a tutti gli elementi. In questo modo il binding non viene ripristinato.

Un altro trucco è che l'elemento selezionato DEVE essere un'istanza degli oggetti nell'elenco. Ho fatto un errore una volta quando pensavo che l'elenco interrogato per l'elemento predefinito fosse corretto, ma che fosse rigenerato per ogni chiamata. Quindi la corrente era diversa sebbene avesse una proprietà DisplayPath uguale a un elemento della lista.

È ancora possibile ottenere l'ID dell'articolo corrente (o qualsiasi cosa lo definisca in modo univoco), ricollegare il controllo e quindi trovare nell'elenco associato l'elemento con lo stesso ID e vincolare tale elemento come corrente.

1

Ho affrontato lo stesso problema durante la creazione di combobox a cascata e sono incappato in un post di un post di qualcuno che ha trovato una soluzione semplice ma sorprendente. Chiama UpdateLayout() dopo aver impostato .ItemsSource ma prima di impostare l'oggetto SelectedItem. Questo deve forzare il codice a bloccare fino a quando il database non è completo. Io non sono esattamente sicuro perché si risolve, ma io non ho sperimentato ancora una volta la condizione di competizione in quanto ...

Fonte di queste informazioni: http://compiledexperience.com/Blog/post/Gotcha-when-databinding-a-ComboBox-in-Silverlight.aspx

2

ero irritato quando ho incontrato questo problema, ma Ho pensato che doveva esserci un modo per aggirarlo. Il mio miglior sforzo finora è dettagliato nel post.

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

ero abbastanza felice come si restringeva la sintassi a qualcosa di simile alla seguente.

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

Kyle

+0

Grazie Kyle. Tutto ciò che ho provato fino ad ora è ex: ComboBox.Mode = "AsyncEager" ma ha rimosso il vincolo che l'oggetto SelectedItem deve essere impostato prima di ItemsSource, che sembra essere il nucleo di molti dei problemi descritti qui. Sai se ci sarà una soluzione nativa in arrivo su Silverlight 5? –

+0

Non so cosa sarà in SL5, ma non ho sentito nulla in questo senso. –

0

Nel caso in cui si arriva qui perché avete un problema di selezione Combobox, significato, non accade nulla quando si fa clic sul vostro articolo nella lista. Si noti che i seguenti suggerimenti possono anche aiutare a:

1/assicurarsi se non notificate qualcosa nel caso in cui si seleziona un elemento

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

2/assicurarsi che la voce che si seleziona è ancora nell'origine dati sottostante nel caso in cui si elimina per caso

ho fatto entrambi gli errori;)

Problemi correlati