In my quest to develop a pretty data-driven silverlight app, I seem to continually come up against some sort of race condition that needs to be worked around. The latest one is below. Any help would be appreciated.
You have two tables on the back end: one is Components and one is Manufacturers. Every Component has ONE Manufacturer. Not at all an unusual, foreign key lookup-relationship.
I Silverlight, I access data via WCF service. I will make a call to Components_Get(id) to get the Current component (to view or edit) and a call to Manufacturers_GetAll() to get the complete list of manufacturers to populate the possible selections for a ComboBox. I then Bind the SelectedItem on the ComboBox to the Manufacturer for the Current Component and the ItemSource on the ComboBox to the list of possible Manufacturers. like this:
<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>
This worked great for the longest time, until I got clever and did a little client side caching of the Component (which I planned turn on for the Manufacturers as well). When I turned on caching for the Component and I got a cache hit, all of the data would be there in the objects correctly, but the SelectedItem would fail to Bind. The reason for this, is that the calls are Asynchronous in Silverlight and with the benefit of the caching, the Component is not being returned prior to the Manufacturers. So when the SelectedItem tries to find the Components.Current.Manufacturer in the ItemsSource list, it is not there, because this list is still empty because Manufacturers.All has not loaded from the WCF service yet. Again, if I turn off the Component caching, it works again, but it feels WRONG - like I am just getting lucky that the timing is working out. The correct fix IMHO is for MS to fix the ComboBox/ ItemsControl control to understand that this WILL happen with Asynch calls being the norm. But until then, I need a need a way yo fix it...
Here are some options that I have thought of:
- Eliminate the caching or turn it on across the board to once again mask the problem. Not Good IMHO, because this will fail again. Not really willing to sweep it back under the rug.
- Create an intermediary object that would do the synchronization for me (that should be done in the ItemsControl itself). It would accept and Item and an ItemsList and then output and ItemWithItemsList property when both have a arrived. I would Bind the ComboBox to the resulting output so that it would never get one item before the other. My problem is that this seems like a pain but it will make certain that the race condition does not re-occur.
Any thougnts/Comments?
FWIW: I will post my solution here for the benefit of others.
@Joe: Thanks so much for the response. I am aware of the need to update the UI only from the UI thread. It is my understanding and I think I have confirmed this through the debugger that in SL2, that the code generated by the the Service Reference takes care of this for you. i.e. when I call Manufacturers_GetAll_Asynch(), I get the Result through the Manufacturers_GetAll_Completed event. If you look inside the Service Reference code that is generated, it ensures that the *Completed event handler is called from the UI thread. My problem is not this, it is that I make two different calls (one for the manufacturers list and one for the component that references an id of a manufacturer) and then Bind both of these results to a single ComboBox. They both Bind on the UI thread, the problem is that if the list does not get there before the selection, the selection is ignored.
Also note that this is still a problem if you just set the ItemSource and the SelectedItem in the wrong order!!!
Another Update: While there is still the combobox race condition, I discovered something else interesting. You should NEVER genrate a PropertyChanged event from within the "getter" for that property. Example: in my SL data object of type ManufacturerData, I have a property called "All". In the Get{} it checks to see if it has been loaded, if not it loads it like this:
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;
}
}
Note that this code FAILS on a "cache hit" because it will generate an PropertyChanged event for "All" from within the All { Get {}} method which would normally cause the Binding System to call All {get{}} again...I copied this pattern of creating bindable silverlight data objects from a ScottGu blog posting way back and it has served me well overall, but stuff like this makes it pretty tricky. Luckily the fix is simple. Hope this helps someone else.