Silverlight Combobox数据绑定竞争条件

时间:2009-02-27 18:41:22

标签: wcf silverlight data-binding silverlight-2.0

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

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

我是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时,它不存在,因为此列表仍然为空,因为尚未从WCF服务加载Manufacturers.All。同样,如果我关闭组件缓存,它会再次运行,但感觉不对 - 就像我很幸运时机正在运行。正确的修复恕我直言,MS用于修复ComboBox / ItemsControl控件,以了解这将发生Asynch调用是常态。但在那之前,我需要一种方法来解决它......

以下是我想到的一些选项:

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

    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”的属性。在Get {}中,它会检查它是否已被加载,如果没有,它会加载它:

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

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

6 个答案:

答案 0 :(得分:7)

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

在设置SelectedItem之后设置ItemSource时存在问题。当发生这种情况时,Combobx将其视为选择的完全重置并清除SelectedItem / SelectedIndex。你可以在System.Windows.Controls.Primitives.Selector(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();
}

注意最后一种情况 - 重置...当你加载一个新的ItemSource时,你最终会在这里被任何SelectedItem / SelectedIndex吹走?!?!

最终解决方案非常简单。我只是将错误的ComboBox子类化,并为此方法提供和覆盖如下。虽然我确实需要添加:

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

所有这一切都是(a)保存旧的SelectedItem然后(b)检查是否在ItemsChanged之后,如果我们没有做出选择并且旧的SelectedItem存在于新列表中......那么......选择它!

答案 1 :(得分: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" />

凯尔

答案 2 :(得分:1)

我在构建级联组合框时遇到了同样的问题,偶然发现了一篇博客文章,其中发现了一个简单但令人惊讶的解决方案。在设置.ItemsSource之后但在设置SelectedItem之前调用UpdateLayout()。这必须强制代码阻塞,直到数据绑定完成。我不确定为什么要解决这个问题但是我没有经历过竞争条件,因为......

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

答案 3 :(得分: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元素您有一些新数据。

答案 4 :(得分:0)

不是每次都重新绑定ItemsSource,而是将它绑定到ObservableCollection&lt;&gt;然后在其上调用Clear()并添加(...)所有元素。这样就不会重置绑定。

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

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

答案 5 :(得分:0)

如果你到达这里是因为你有一个Combobox选择问题,这意味着当你点击列表中的项目时没有任何反应。请注意,以下提示也可能对您有所帮助:

1 /确保在选择项目

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

2 /确保您选择的项目仍在基础数据源中,以防您意外删除它

我犯了两个错误;)