В своем стремлении разработать красивое приложение 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, чтобы понять, что это БУДЕТ, когда асинхронные вызовы являются нормой. Но до тех пор, мне нужен способ исправить это ...
Вот несколько вариантов, о которых я подумал:
- Устраните кеширование или включите его через доску, чтобы еще раз замаскировать проблему. Не хорошо ИМХО, потому что это снова не удастся. Не очень-то желаю смести его обратно под ковер.
- Создайте промежуточный объект, который будет выполнять синхронизацию для меня (это должно быть сделано в самом 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, и в целом он мне хорошо послужил, но подобные вещи делают его довольно сложным. К счастью, это просто. Надеюсь, это поможет кому-то еще.