2009-02-27 9 views
6

Bei meiner Suche nach einer schönen datengesteuerten Silverlight-App stehe ich immer wieder vor einer Art von Race-Condition, die es zu bewältigen gilt. Die neueste ist unten. Jede Hilfe wäre willkommen.Silverlight Combobox Databinding Race Zustand

Sie haben zwei Tabellen auf der Rückseite: eins ist Komponenten und eins ist Hersteller. Jede Komponente hat einen Hersteller. Überhaupt keine ungewöhnliche Fremdschlüssel-Lookup-Beziehung.

I Silverlight, ich greife auf Daten über den WCF-Dienst zu. Ich werde Components_Get (ID) aufrufen, um die Current-Komponente (zum Anzeigen oder Bearbeiten) und einen Aufruf von Manufacters_GetAll() aufzurufen, um die vollständige Liste der Hersteller zu erhalten, die die möglichen Auswahlen für eine ComboBox auffüllen. Dann binde ich das SelectedItem auf der ComboBox an den Hersteller für die aktuelle Komponente und die ItemSource auf der ComboBox an die Liste der möglichen Hersteller. dies wie:

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

Das funktionierte gut für die längste Zeit, bis ich einen kleinen Client-seitiges Caching der Komponente klug und tat bekam (die ich auch für die Hersteller einschalten geplant). Wenn ich das Caching für die Komponente aktivierte und einen Cache-Treffer erhielt, waren alle Daten in den Objekten korrekt vorhanden, aber SelectedItem konnte nicht binden. Der Grund dafür ist, dass die Aufrufe in Silverlight asynchron sind und mit dem Vorteil der Zwischenspeicherung die Komponente nicht vor den Herstellern zurückgegeben wird. Wenn das SelectedItem also versucht, den Components.Current.Manufacturer in der ItemsSource-Liste zu finden, ist es nicht vorhanden, da diese Liste noch leer ist, da Manufacters.All noch nicht aus dem WCF-Dienst geladen wurde. Wenn ich das Komponenten-Caching wieder ausschalte, funktioniert es wieder, aber es fühlt sich FALSCH an - als ob ich gerade Glück hätte, dass das Timing funktioniert. Die richtige Korrektur IMHO ist für MS, um das ComboBox/ItemsControl-Steuerelement zu beheben, um zu verstehen, dass dies geschehen wird, da Asynch-Aufrufe die Norm sind. Aber bis dahin muss ich ein Bedürfnis einen Weg, yo es zu beheben ...

Hier einige Optionen, die ich gedacht habe von:

  1. das Caching beseitigen oder schalten Sie ihn auf der ganzen Linie auf einmal wieder zu maskieren das Problem. Nicht gut, IMHO, denn das wird wieder scheitern. Nicht wirklich bereit, es unter den Teppich kehren.
  2. Erstellen Sie ein Zwischenobjekt, das die Synchronisierung für mich tun würde (das sollte in ItemsControl selbst durchgeführt werden). Es würde und Item und eine ItemsList und dann Ausgabe und ItemWithItemsList-Eigenschaft akzeptieren, wenn beide ein angekommen sind. Ich würde die ComboBox an die resultierende Ausgabe binden, so dass es nie ein Element vor dem anderen bekommen würde. Mein Problem ist, dass dies wie ein Schmerz aussieht, aber es wird sicherstellen, dass die Race-Bedingung nicht wieder auftritt.

Irgendwelche Gedanken/Kommentare?

FWIW: Ich werde hier meine Lösung zum Wohle anderer posten.

@Joe: Vielen Dank für die Antwort. Ich bin mir bewusst, dass die Benutzeroberfläche nur über den UInthread aktualisiert werden muss. Es ist mein Verständnis und ich glaube, ich habe dies durch den Debugger bestätigt, dass in SL2 der von der Service-Referenz generierte Code das für Sie erledigt. Wenn ich Manufactrers_GetAll_Asynch() aufrufen, erhalte ich das Ergebnis über das Manufacturers_GetAll_Completed-Ereignis. Wenn Sie in den generierten Service-Referenzcode schauen, wird sichergestellt, dass der Ereignishandler * Completed vom UI-Thread aufgerufen wird. Mein Problem ist nicht das, es ist, dass ich zwei verschiedene Aufrufe (eine für die Herstellerliste und eine für die Komponente, die auf eine ID eines Herstellers verweist) mache und dann beide Ergebnisse an eine einzelne ComboBox binden. Sie binden beide an den UI-Thread. Das Problem besteht darin, dass die Auswahl ignoriert wird, wenn die Liste vor der Auswahl nicht angezeigt wird.

Beachten Sie auch, dass diese if you just set the ItemSource and the SelectedItem in the wrong order immer noch ein Problem ist !!!

Ein weiteres Update: Zwar gibt es noch die Combobox Race-Bedingung ist, entdeckte ich etwas anderes interessant. Sie sollten nie ein PropertyChanged-Ereignis aus dem "Getter" für diese Eigenschaft generieren. Beispiel: In meinem SL-Datenobjekt vom Typ ManufacturerData habe ich eine Eigenschaft namens "All". Im Get {} überprüft er, ob es geladen ist, wenn es nicht es so lädt:

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

} 

Beachten Sie, dass dieser Code FAILS auf einem „Cache-Treffer“, weil es ein Property Ereignis erzeugen für "All" aus der All {Get {}} - Methode heraus, die normalerweise dazu führen würde, dass das Binding-System alle {get {}} erneut aufruft ... Ich habe dieses Muster der Erstellung von bindefähigen Silverlight-Datenobjekten aus einem Blog von ScottGu kopiert und es hat mir insgesamt gut gedient, aber solche Sachen machen es ziemlich schwierig. Zum Glück ist die Lösung einfach. Hoffe das hilft jemand anderem.

Antwort

7

Ok ich die Antwort gefunden haben (viel Reflektor, um herauszufinden, wie die ComboBox funktioniert).

Das Problem liegt vor, wenn die ItemSource gesetzt wird, nachdem die SelectedItem festgelegt ist. Wenn dies der Fall ist, sieht der Combobx dies als vollständiges Zurücksetzen der Auswahl und löscht das SelectedItem/SelectedIndex. Sie können dies hier in der System.Windows.Controls.Primitives.Selector (die Basisklasse für die ComboBox) sehen:

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

Hinweis der letzte Fall - der Reset ... Wenn Sie ein neues ItemSource laden Sie am Ende hier oben und jedes SelectedItem/SelectedIndex wird weggeblasen?!?!

Nun, die Lösung war am Ende ziemlich einfach. Ich habe gerade die fehlerhafte ComboBox unterklassifiziert und wie folgt für diese Methode bereitgestellt und überschrieben. Obwohl ich muss nicht hinzugefügt a:

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

Alles, was dies tut, ist (a) die alte SelectedItem speichern aus und dann (b) überprüfen, dass, wenn nach dem Itemschanged, wenn wir haben keine Auswahl getroffen und die alte SelectedItem existiert in der neuen Liste ... naja ... Selected It!

+0

Diese Lösung ist eine gemeinsame Lösung. Lange habe ich nach einer allgemeineren Lösung gesucht, die alle Selector-Steuerelemente abdeckt; nicht nur ComboBoxen, und dies ohne jegliche Kontrolle. Es gibt eine Möglichkeit, dies mit Verhaltensweisen zu tun. Diese vorgeschlagene Lösung funktioniert auch in UWP und wahrscheinlich WPF: http://stackoverflow.com/questions/36003805/uwp-silverlight-combobox-selector-itemssource-selecteditem-race-condition-solu –

0

Es ist aus Ihrem Post nicht klar, ob Sie wissen, dass Sie UI-Elemente auf dem UI-Thread ändern müssen - oder Sie werden Probleme haben. Hier ist ein kurzes Beispiel, das einen Hintergrund-Thread erstellt, der eine TextBox mit der aktuellen Zeit ändert.

Der Schlüssel ist 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)); 
      } 
     } 
    } 
} 

Also, wenn Sie wollen asynchron Dinge zu bekommen, müssen Sie Control.Dispatcher.BeginInvoke verwenden benachrichtigen das UI-Element, dass Sie einige neue Daten haben.

+0

ich bin von der Notwendigkeit bewusst, die Benutzeroberfläche von dem UI-Thread zu aktualisieren. Sehen Sie mich, editieren Sie in der ursprünglichen Frage auf diesem (Raum begrenzt hier und es schien zu rechtfertigen, es dort zu setzen). – caryden

0

Anstatt rebinding die Itemssource jedes Mal, wäre es einfacher gewesen, damit Sie es an eine ObservableCollection < binden> und dann Löschen() aufrufen, auf ihm und hinzufügen (...) alle Elemente. Auf diese Weise wird die Bindung nicht zurückgesetzt.

Ein weiterer Fehler ist, dass das ausgewählte Element eine Instanz der Objekte in der Liste sein muss. Ich habe einmal einen Fehler gemacht, als ich dachte, dass die abgefragte Liste für das Standard-Item repariert wurde, aber bei jedem Aufruf neu generiert wurde. Daher war der Strom anders, obwohl er eine DisplayPath-Eigenschaft hatte, die mit einem Element der Liste identisch war.

Sie könnten immer noch die ID des aktuellen Elements (oder was auch immer es eindeutig definiert), das Steuerelement erneut binden und dann in der gebundenen Liste das Element mit der gleichen ID finden und dieses Element als das aktuelle binden.

1

Ich kämpfte durch dasselbe Problem beim Erstellen von Cascading Comboboxen und stolperte über einen Blogbeitrag von jemandem, der eine einfache, aber überraschende Lösung fand. Rufen Sie UpdateLayout() auf, nachdem Sie die .ItemsSource festgelegt haben, aber bevor Sie SelectedItem festlegen. Dies muss den Code zwingen, zu blockieren, bis die Datenbindung abgeschlossen ist. Ich bin mir nicht ganz sicher, warum es behebt es, aber ich habe nicht das Rennen Zustand wieder erlebt seit ...

Quelle dieser Informationen: http://compiledexperience.com/Blog/post/Gotcha-when-databinding-a-ComboBox-in-Silverlight.aspx

2

Ich war erbost, als ich zum ersten Mal in dieses Problem lief, aber Ich dachte mir, es müsste einen Weg geben. Mein bisher bester Versuch ist in der Post ausführlich beschrieben.

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

Ich war ziemlich glücklich, wie es um die Syntax etwas wie die folgenden verengt.

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

Kyle

+0

Danke Kyle. Alles, was ich bisher versucht habe, ist ex: ComboBox.Mode = "AsyncEager", aber es hat die Einschränkung entfernt, dass das SelectedItem vor der ItemsSource gesetzt werden muss, was der Kern vieler der hier beschriebenen Probleme zu sein scheint. Wissen Sie, ob es in Silverlight 5 eine native Lösung geben wird? –

+0

Ich weiß nicht, was in SL5 sein wird, aber ich habe nichts in dieser Richtung gehört. –

0

Bei gelangen Sie hier, weil Sie eine Combobox Auswahlproblem, Bedeutung haben, passiert nichts, wenn Sie auf Ihre Artikel klicken Sie in der Liste. Beachten Sie, dass die folgenden Hinweise auch dir helfen könnten:

1/stellen Sie sicher, nicht benachrichtigen, falls etwas Sie ein Element auswählen

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

2/sicherstellen, dass das Element, das Sie ist wählen immer noch in der zugrunde liegenden Datenquelle für den Fall, löschen Sie es durch Zufall

ich beiden Fehler gemacht;)

Verwandte Themen