I ran into a baffling data binding issue when using a DataForm containing a ComboBox recently which caused me to spin my wheels for way too long. Luckily, Keith Jones was nice enough to set me straight and give me a solution.
I have a DataForm with a ComboBox in its EditTemplate. This is bound to a DataGrid's SelectedItem in a standard Master/Detail, and it’s all using .NET RIA Services.
I was seeing some kind of weird delay in the binding of the Grid's SelectedItem to the ComboBox. If you clicked on a row, the combobox was not bound properly. But if you clicked on a few rows (3 rows was the magic number), then the ComboBox got bound properly from that point on. It seemed like maybe the ComboBox items were taking forever to load, but in looking at an HTTP trace, there was no data being requested across the wire.
It turns out this was a timing issue with the data binding. The SelectedItem was being evaluated properly, but at the time the ItemsSource of the ComboBox was null. You see, I was using the DataForm’s ContentLoaded event to fill the ComboBox with items, something like this:
<ComboBox x:Name="cboCategory"
SelectedItem="{Binding Brewers, Mode=TwoWay}"
DisplayMemberPath="BreweryName" />
void dataForm1_ContentLoaded(object sender, DataFormContentLoadEventArgs e)
{
ComboBox cboBrewery = dataForm1.FindNameInContent("cboBrewery") as ComboBox;
cboBrewery.ItemsSource = _beerContext.Brewers;
}
…and while this fills the ComboBox just fine, it does it a bit too late --- the SelectedItem of the ComboBox has already been evaluated. Makes sense, but why would three clicks on the DataGrid set the binding straight? As Keith pointed out to me, the DataForm uses a caching mechanism that starts with 2 empty slots. After the first click, the first cache slot was filled. After the second click, the second cache slot was filled. Then on the third click, the first cache slot is reused – and since it had data by the third click, the data was shown.
The Fix: ComboBox in DataForm
There are probably a dozen ways to do this, but here is what I ended up using. You can also use this method for other list bound controls inside a DataForm such as a ListBox. First, add an instance of your Domain Context inside your page resources. Then have your ComboBox bind its ItemsSource to that context. This is shown highlighted below.
<navigation:Page
x:Class="TestBeer1.Home"
<!-- Note I removed some stuff here for brevity -->
xmlns:testBeer1="clr-namespace:TestBeer1.Web"
NavigationCacheMode="Enabled"
Style="{StaticResource PageStyle}">
<navigation:Page.Resources>
<testBeer1:BeerDomainContext x:Key="beerDomainContext" />
<DataTemplate x:Key="DataTemplate1">
<StackPanel>
<TextBox Text="{Binding BeerName, Mode=TwoWay, UpdateSourceTrigger=Explicit}" />
<ComboBox x:Name="cboBrewery"
ItemsSource="{Binding Brewers, Source={StaticResource beerDomainContext}}"
SelectedItem="{Binding Brewers, Mode=TwoWay}"
DisplayMemberPath="BreweryName"
/>
</StackPanel>
</DataTemplate>
</navigation:Page.Resources>
<Grid x:Name="LayoutRoot" >
<ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}" >
<StackPanel x:Name="ContentStackPanel" Style="{StaticResource ContentStackPanelStyle}">
<data:DataGrid x:Name="grdBeers" Height="200" />
<StackPanel Orientation="Horizontal" Margin="20,20,20,20">
<dataControls:DataForm x:Name="dataForm1"
CommandButtonsVisibility="Commit,Cancel" AutoEdit="True"
IsReadOnly="False" Width="300" AutoCommit="True"
EditEnded="DataForm_EditEnded"
EditTemplate="{StaticResource DataTemplate1}" >
<d:DataContext>
<testBeer1:Beers></testBeer1:Beers>
</d:DataContext>
</dataControls:DataForm>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</navigation:Page>
… And then it is a matter of getting a reference to the domain context and loading up the data. We can do this in the Loaded event for a page or the OnNavigatedTo event for a View in a Navigation Application…
public partial class Home : Page
{
BeerDomainContext _beerContext;
public Home()
{
InitializeComponent();
}
// Executes when the user navigates to this page.
protected override void OnNavigatedTo(NavigationEventArgs e)
{
_beerContext = this.Resources["beerDomainContext"] as BeerDomainContext;
_beerContext.Load(_beerContext.GetBeersQuery(), LoadBeersComplete, null);
_beerContext.Load(_beerContext.GetBrewersQuery(), LoadBrewersComplete, null);
grdBeers.ItemsSource = _beerContext.Beers;
}
void LoadBeersComplete(LoadOperation<Beers> op)
{
if (op.HasError)
throw op.Error;
}
void LoadBrewersComplete(LoadOperation<Brewers> op)
{
if (op.HasError)
throw op.Error;
}
private void DataForm_EditEnded(object sender, System.Windows.Controls.DataFormEditEndedEventArgs e)
{
dataForm1.CommitEdit();
if (e.EditAction == DataFormEditAction.Commit)
_beerContext.SubmitChanges(SubmitChangesComplete, null);
}
void SubmitChangesComplete(SubmitOperation op)
{
if (op.HasError)
throw op.Error;
}
}