Состояние гонки Silverlight Combobox - PullRequest
6 голосов
/ 27 февраля 2009

В своем стремлении разработать красивое приложение Silverlight, управляемое данными, я, кажется, постоянно сталкиваюсь с какими-то условиями гонки, которые нужно обойти. Последний из них ниже. Любая помощь будет оценена.

У вас есть две таблицы на заднем плане: одна - Компоненты, а другая - Производители. Каждый компонент имеет одного производителя. Совсем не необычные отношения поиска по внешнему ключу.

Я Silverlight, я получаю доступ к данным через службу WCF. Я сделаю вызов Components_Get (id), чтобы получить текущий компонент (для просмотра или редактирования) и вызову Manufacturers_GetAll (), чтобы получить полный список производителей, чтобы заполнить возможные варианты выбора для ComboBox. Затем я привязываю SelectedItem в ComboBox к производителю для текущего компонента и ItemSource на ComboBox к списку возможных производителей. как это:

<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 вызовы асинхронны, и благодаря преимуществу кэширования компонент не возвращается до производителей. Поэтому, когда SelectedItem пытается найти Components.Current.Manufacturer в списке ItemsSource, его там нет, потому что этот список все еще пуст, потому что Manufacturers.All еще не загружен из службы WCF. Опять же, если я отключаю кэширование компонентов, оно снова работает, но кажется НЕПРАВИЛЬНЫМ - как будто мне просто повезло, что время работает. ИМХО правильное исправление для MS - это исправление элемента управления ComboBox / ItemsControl, чтобы понять, что это БУДЕТ, когда асинхронные вызовы являются нормой. Но до тех пор, мне нужен способ исправить это ...

Вот несколько вариантов, о которых я подумал:

  1. Устраните кеширование или включите его через доску, чтобы еще раз замаскировать проблему. Не хорошо ИМХО, потому что это снова не удастся. Не очень-то желаю смести его обратно под ковер.
  2. Создайте промежуточный объект, который будет выполнять синхронизацию для меня (это должно быть сделано в самом ItemsControl). Он будет принимать и Item, и ItemsList, а затем выводить и свойство ItemWithItemsList, когда оба будут получены. Я бы привязал ComboBox к полученному результату, чтобы он никогда не получал один элемент раньше другого. Моя проблема в том, что это похоже на боль, но это гарантирует, что состояние гонки не возникнет снова.

Есть мысли / комментарии?

FWIW: я опубликую свое решение здесь для пользы других.

@ Джо: Большое спасибо за ответ. Я осознаю необходимость обновления пользовательского интерфейса только из потока пользовательского интерфейса. Это мое понимание, и я думаю, что я подтвердил это через отладчик, который в SL2, что код, сгенерированный ссылкой на службу, позаботится об этом за вас. т.е. когда я вызываю Manufacturers_GetAll_Asynch (), я получаю Результат через событие Manufacturers_GetAll_Completed. Если вы загляните внутрь сгенерированного кода Service Reference, он гарантирует, что обработчик события * Completed вызывается из потока пользовательского интерфейса. Моя проблема не в этом, а в том, что я делаю два разных вызова (один для списка производителей и один для компонента, который ссылается на идентификатор производителя), а затем связываю оба этих результата с одним ComboBox. Они оба связываются в потоке пользовательского интерфейса, проблема в том, что если список не попадает туда до выбора, выбор игнорируется.

Также обратите внимание, что это все еще проблема , если вы просто установите ItemSource и SelectedItem в неправильном порядке !!!

Еще одно обновление:В то время как все еще существует состояние расы комбобокса, я обнаружил кое-что еще интересное. Вы должны НИКОГДА генерировать событие PropertyChanged из "getter" для этого свойства. Пример: в моем объекте данных SL типа ManufacturerData у меня есть свойство под названием «Все». В 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 при "попадании в кэш", поскольку он генерирует событие PropertyChanged для "All" из метода All {Get {}}, что обычно вызывает вызов системы привязок Все снова {get {}} ... Я скопировал этот шаблон создания привязываемых объектов данных silverlight из поста в блоге ScottGu, и в целом он мне хорошо послужил, но подобные вещи делают его довольно сложным. К счастью, это просто. Надеюсь, это поможет кому-то еще.

Ответы [ 6 ]

7 голосов
/ 03 марта 2009

Хорошо, я нашел ответ (используя множество Reflector, чтобы выяснить, как работает ComboBox).

Проблема существует, когда ItemSource установлен после SelectedItem. Когда это происходит, 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;
}

Все, что это делает, это (а) сохранить старый SelectedItem и затем (b) проверить, что если после ItemsChanged у нас нет выбора, и старый SelectedItem существует в новом списке ... хорошо ... Выбрал это!

2 голосов
/ 09 июля 2010

Я был возмущен, когда впервые столкнулся с этой проблемой, но я решил, что должен быть способ обойти это. Мои лучшие усилия пока подробно описаны в посте.

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

Кайл

1 голос
/ 19 июня 2009

Я боролся с этой же проблемой, создавая каскадные списки, и наткнулся на сообщение в блоге того, кто нашел простое, но удивительное решение. Вызовите UpdateLayout () после установки .ItemsSource, но до установки SelectedItem. Это должно заставить код блокироваться, пока привязка данных не будет завершена. Я не совсем уверен, почему это исправляет это, но я не испытывал условия гонки снова, так как ...

Источник этой информации: http://compiledexperience.com/Blog/post/Gotcha-when-databinding-a-ComboBox-in-Silverlight.aspx

0 голосов
/ 31 августа 2010

Если вы приехали сюда из-за проблемы с выбором в Combobox, то есть ничего не происходит, когда вы нажимаете на свой элемент в списке. Обратите внимание, что следующие подсказки могут также помочь вам:

1 / убедитесь, что вы не уведомляете что-либо в случае выбора элемента

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

2 / убедитесь, что выбранный вами элемент все еще находится в базовом источнике данных на случай, если вы удалите его случайно

Я сделал обе ошибки;)

0 голосов
/ 18 июня 2009

Вместо того, чтобы связывать ItemSource каждый раз, было бы проще связать его с ObservableCollection <>, а затем вызвать Clear () для него и добавить (...) все элементы. Таким образом, привязка не сбрасывается.

Другой недостаток заключается в том, что выбранный элемент ДОЛЖЕН быть экземпляром объектов в списке. Однажды я допустил ошибку, когда подумал, что запрашиваемый список для элемента по умолчанию исправлен, но восстанавливается при каждом вызове. Таким образом, ток был другим, хотя он имел свойство DisplayPath, которое было таким же, как элемент списка.

Вы все еще можете получить идентификатор текущего элемента (или любой другой, который однозначно определяет его), повторно связать элемент управления, а затем найти в связанном списке элемент с тем же идентификатором и связать этот элемент с текущим.

0 голосов
/ 28 февраля 2009

Из вашего сообщения не ясно, известно ли вам, что вы должны изменить элементы пользовательского интерфейса в потоке пользовательского интерфейса - или у вас будут проблемы. Вот краткий пример, который создает фоновый поток, который изменяет TextBox с текущим временем.

Ключ - MyTextBox.Dispather.BeginInvoke в 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));
            }
        }
    }
}

Итак, если вы хотите получать вещи асинхронно, вам придется использовать Control.Dispatcher.BeginInvoke, чтобы уведомить элемент пользовательского интерфейса о наличии новых данных.

...