2009-02-27 146 views
6

在我寻求开发漂亮的数据驱动Silverlight应用程序时,我似乎不断遇到需要解决的某种竞争条件。最新的一个在下面。任何帮助,将不胜感激。Silverlight Combobox数据绑定竞赛条件

您在后端有两个表格:一个是组件,一个是制造商。每个组件都有一个制造商。根本不是一个不寻常的外键查找关系。

我Silverlight,我通过WCF服务访问数据。我将调用Components_Get(id)以获取Current组件(查看或编辑)以及致电Manufacturers_GetAll()以获取制造商的完整列表,以填充ComboBox的可能选择。然后,我将ComboBox上的SelectedItem绑定到当前组件的制造商和ComboBox上的ItemSource到可能制造商的列表中。像这样:

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

这时间最长的伟大的工作,直到我聪明,做组件的一个小客户端缓存(这是我的计划打开了制造商以及)。当我打开组件缓存并获得缓存命中时,所有数据都将正确存在于对象中,但SelectedItem将无法绑定。原因是Silverlight中的调用是异步的,并且缓存的好处是,Component不会在制造商之前返回。因此,当SelectedItem试图在ItemsSource列表中查找Components.Current.Manufacturer时,它不在那里,因为这个列表仍然是空的,因为Manufacturers.All尚未从WCF服务加载。同样,如果我关闭组件缓存,它会再次运行,但它感觉错误 - 就像我很幸运,时间正在运行。正确的修正恕我直言是为MS修复ComboBox/ItemsControl控件,以了解这将发生与异步调用是常态。但在那之前,我需要需要一种方式哟修复它...

这里是我想到的一些选项:

  1. 消除缓存或开启全线再次掩盖问题。不好恕我直言,因为这会再次失败。不是真的愿意把它打回地毯之下。
  2. 创建一个中介对象,为我做同步(这应该在ItemsControl本身完成)。它会接受Item和ItemList,然后在两者都到达时输出和ItemWithItemsList属性。我会将ComboBox绑定到结果输出,以便它永远不会获得一个项目在另一个之前。我的问题是,这似乎是一个痛苦,但它会确保竞争条件不会再发生。

任何thougnts /评论?

FWIW:我会在这里发布我的解决方案,为他人的利益。

@Joe:非常感谢回复。我知道只需从UI线程更新UI。这是我的理解,我想我已经通过调试器证实了这一点,即在SL2中,服务参考生成的代码为您处理此问题。即当我打电话Manufacturers_GetAll_Asynch()时,我通过Manufacturers_GetAll_Completed事件得到结果。如果您查看生成的服务引用代码,它可以确保从UI线程调用* Completed事件处理程序。我的问题不是这样,而是我做了两个不同的调用(一个用于制造商列表,另一个用于引用制造商ID的组件),然后将这两个结果绑定到一个ComboBox。它们都绑定在UI线程上,问题是如果列表在选择之前没有到达那里,那么选择将被忽略。

还要注意,这仍然是一个问题if you just set the ItemSource and the SelectedItem in the wrong order !!!

另一个更新: 虽然仍然存在组合框竞争条件,但我发现了其他有趣的东西。你应该永远不要从该属性的“getter”中生成一个PropertyChanged事件。例如:在我的类型为ManufacturerData的SL数据对象中,我有一个名为“All”的属性。在获取{}它检查,看它是否已被加载,如果没有加载它是这样的:

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

} 

注意,此代码失败一个“缓存命中”,因为它会产生一个PropertyChanged事件对于所有{Get {}}方法中的“All”,通常会导致绑定系统再次调用All {get {}} ...我复制了这种从ScottGu博客发布方式创建可绑定silverlight数据对象的模式它总体上让我满意,但像这样的东西使它非常棘手。幸运的是修复很简单。希望这可以帮助别人。

回答

7

好的我找到了答案(使用大量的Reflector来弄清楚ComboBox是如何工作的)。

在SelectedItem设置后设置ItemSource时存在问题。发生这种情况时,Combobx将其视为选择的完全重置并清除SelectedItem/SelectedIndex。您可以在这里System.Windows.Controls.Primitives.Selector看到这个(组合框基类):

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

注意最后一种情况 - 复位......当你加载一个新的ItemSource你结束在这里,任何SelectedItem/SelectedIndex都会被吹走?!?!

那么最终解决方案非常简单。我只是将错误的组合框分类并提供并覆盖此方法,如下所示。虽然我也有添加:

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

所有这确实是(一)保存关闭旧的SelectedItem,然后(b)检查,如果ItemsChanged后,没有做出选择和旧的SelectedItem如果我们存在于新列表中......以及...选定它!

+0

该解决方案是一个常见的解决方案。很长一段时间,我一直在寻找覆盖所有选择器控件的更通用的解决方案;不只是ComboBoxes,而且不会从任何控制继承。有一种方法可以用行为来做到这一点。此提出的解决方案也适用于UWP,大概WPF:http://stackoverflow.com/questions/36003805/uwp-silverlight-combobox-selector-itemssource-selecteditem-race-condition-solu – 2016-03-15 22:35:20

0

从您的帖子中不清楚您是否知道您必须修改UI线程上的UI元素 - 否则您将遇到问题。下面是一个简单的例子,它创建一个后台线程,用当前时间修改一个TextBox。

关键是Page.xaml.cs.中的MyTextBox.Dispather.BeginInvoke。

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

所以,如果你想要得到的东西异步,你将不得不使用Control.Dispatcher.BeginInvoke通知您有一些新数据的UI元素。

+0

我知道需要从UI线程更新UI。看到我在原来的问题编辑(这里的空间有限,它似乎有必要把它放在那里)。 – caryden 2009-03-02 13:11:59

0

而不是重新绑定ItemsSource每次它将更容易绑定到一个ObservableCollection <>然后调用它的Clear()和Add(...)所有元素。这样绑定不会被重置。

另一个问题是选择的项目必须是列表中对象的一个​​实例。当我认为默认项目的查询列表已修复但每次调用都重新生成时,我犯了一次错误。因此,尽管它具有与列表项目相同的DisplayPath属性,但当前不同。

你仍然可以得到当前项目的ID(或任何唯一地定义它),重新绑定控件,然后在绑定列表中找到具有相同ID的项目,并结合该项目作为当前。

1

我在构建级联组合框时遇到了同样的问题,并偶然发现了一个发现了简单但令人惊讶的修复方法的人的博客文章。在设置.ItemsSource之后但在设置SelectedItem之前调用UpdateLayout()。这必须强制代码阻塞,直到数据绑定完成。我不知道是什么原因它修复它,但我不会因为再次经历了比赛状态......这些信息的

来源:http://compiledexperience.com/Blog/post/Gotcha-when-databinding-a-ComboBox-in-Silverlight.aspx

2

我被激怒了,当我第一次遇到了这个问题,但我觉得必须有解决办法。我迄今为止的最大努力在帖子中详述。

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

我很高兴,因为它缩小了语法类似于下面的东西。

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

凯尔

+0

感谢凯尔。所有这一切我迄今试图为前:ComboBox.Mode =“AsyncEager”,但它并删除的的SelectedItem必须以这似乎是一个很大的这里所描述的问题的核心的ItemsSource之前设定的约束。你知道它是否会成为Silverlight 5中的本地解决方案吗? – 2011-07-13 00:00:32

0

如果你来到这里,因为你有一个组合框的选择问题,这意味着,当你点击列表中的您的项目没有任何反应。请注意下面的提示也可以帮助你:

1 /请确保你没有的情况下,通知的东西你选择一个项目

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

2 /请确保您选择的项目仍处于底层数据源如果你意外删除它

我犯了这两个错误;)