Tuesday, January 21, 2014

Binding to a List - don't do it

In an earlier blog I described how to bind a datagrid to a Public Property List defined in the code behind. It turns out that if you deploy that solution it does not work. There's no error, the grid's ItemsSource has the correct number of rows, but no data shows in the DataGrid.

If you replace the List with an ObservableCollection (you will need to Import System.Collection.ObjectModel), the datagrid gets populated.

This must be because the ObservableCollection implements IPropertyChanged, an interface that allows bound controls to be made aware of changes to the collection. List does not implement this interface.

Why is the datagrid populated correctly in Visual Studio?

The List is being populated by a WCF call. I think in VS the call is quick and the collection is populated prior to the datagrid being bound to it. When I run the application on another computer via ClickOnce the WCF call takes longer so the List is not populated yet when the datagrid is bound.

Because IPropertyChanged is not implemented, subsequent changes to the List are not propagated to the datagrid.

Sunday, January 19, 2014

Binding validation rules and styles in code behind

Normally I prefer to do things in XAML because it's a little easier and there's less chance of a support programmer having to figure out how I did something. I have a requirement to make a textbox required. When a required textbox is empty I want both the textbox and its associated label to be highlighted.

This is fairly readily handled in XAML with a validation rule and a converter. We put a validation rule on the textbox and bind the color of the label to the text of the textbox using a converter. The XAML looks like this.


<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication13"
    Title="MainWindow" Height="350" Width="525"
    DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <local:RequiredValidator x:Key="RequiredValidator"/>
        <local:RequiredColorConverter x:Key="RequiredColorConverter"/>
    </Window.Resources>
    <StackPanel Orientation="Horizontal" Height="30">
        <Label Name="DescriptionLabel" Content="Description:">
            <Label.Style>
                <Style TargetType="{x:Type Label}">
                    <Setter Property="Foreground" Value="{Binding ElementName=DescriptionTextBox, Path=(Validation.HasError), Converter={StaticResource RequiredColorConverter}}"/>
                </Style>
            </Label.Style>
        </Label>
        <TextBox Name="DescriptionTextBox" Width="200">
            <TextBox.Text>
                <Binding Path="Description" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
                    <Binding.ValidationRules>
                        <StaticResource ResourceKey="RequiredValidator"/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
    </StackPanel>
</Window>


--------------------------------------------------------

Class MainWindow 

    Public Property Description As String
    Public Sub New()
        InitializeComponent()
    End Sub
End Class

As you can see will still need to define our validation rule and converter, both of which are fairly trivial classes.


Public Class RequiredValidator
    Inherits ValidationRule

    Public Overloads Overrides Function Validate(value As Object, cultureInfo As Globalization.CultureInfo) As ValidationResult

        If String.IsNullOrWhiteSpace(value) Then
            Return New ValidationResult(False, "Error")
        Else
            Return New ValidationResult(True, Nothing)
        End If
    End Function
End Class

Public Class RequiredColorConverter
    Implements IValueConverter

    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.Convert
        If CBool(value) Then
            Return New SolidColorBrush(System.Windows.Media.Colors.Red)
        Else
            Return New SolidColorBrush(System.Windows.Media.Colors.Black)
        End If
    End Function

    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.ConvertBack
        Throw New NotImplementedException("RequiredColorConverter")
    End Function
End Class

As you can see when you run the project, both the textbox and the label turn red when the textbox is empty. As a side note, I tried to bind the label to the (Validation.Errors) collection but when I did that Visual Studio kept crashing (bug?) I know you can use this in a trigger without a converter so I think for some reason you can't pass the Validation.Errors to a converter.

Well this all works fine but it's very repetitive if you have several mandatory fields in an application. Even if I created a Style I would need two styles, one for the label and one for the textbox. So I wondered how difficult it would be to do this in code instead. The idea would be to write a utility method that took a textbox and an optional label and add the validator and style in code.

    MakeTextboxRequired(EditDescriptionTextBox, EditDescriptionLabel)

It turns out that creating validation rules and styles in code is quite difficult and verbose.

Change the XAML to...
        <Label Name="DescriptionLabel" Content="Description:"></Label>
        <TextBox Name="DescriptionTextBox" Width="200" Text="{Binding Description}"></TextBox>


This is what the code looks like...


    Public Sub MakeTextboxRequired(oTextBox As TextBox, oLabel As Label)
        ' Attach a mandatory validation rule to the control
        ' Attach a binding to the color of the label

        Dim sElementName As String = oTextBox.Name
        Dim ValidationBinding As New Binding
        Dim RequiredValidator As ValidationRule = Me.FindResource("RequiredValidator")

        Try
            ValidationBinding.Path = New PropertyPath("Description")
            ValidationBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
            ValidationBinding.ValidatesOnDataErrors = True
            ValidationBinding.NotifyOnValidationError = True
            ValidationBinding.ValidationRules.Add(RequiredValidator)
            oTextBox.SetBinding(TextBox.TextProperty, ValidationBinding)

            If oLabel IsNot Nothing Then
                Dim Style As New Style(GetType(Label))
                Dim ColorConverter As IValueConverter = Me.FindResource("RequiredColorConverter")
                Dim ColorBinding As New Binding
                ColorBinding.ElementName = sElementName
                ColorBinding.Path = New PropertyPath("(Validation.HasError)")
                ColorBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
                ColorBinding.ValidatesOnDataErrors = True
                ColorBinding.NotifyOnValidationError = True
                ColorBinding.Converter = ColorConverter
                Style.Setters.Add(New Setter(Label.ForegroundProperty, ColorBinding))
                oLabel.Style = Style
            End If
        Catch ex As Exception
            Throw New Exception("MakeTextboxRequired: " & ex.Message)
        End Try
    End Sub

Now we can get rid of the styles in XAML and let the code do all the hard work.

I'm not saying this is a better approach, but it was an interesting exercise.

Thursday, January 16, 2014

The difference between DataGridTextColumn style and ElementStyle

WPF Version 4.0

Sometimes I can be a bit retarded. I have some XAML that looks like this

<DataGrid Name="EditItemDetailsDataGrid" ItemsSource="{Binding}" AutoGenerateColumns="false">
   <DataGrid.Columns>
       <DataGridTextColumn Header="#" Binding="{Binding Path=Sequence}" IsReadOnly="true" Foreground="Red"/>
   </DataGrid.Columns>
<DataGrid/>

Simple stuff right? Now I have a column in the backing store for this grid that is called "HasErrors". When HasErrors is one I want the Sequence column to have a red foreground, otherwise a black foreground. My first attempt was to use an existing converter called HasErrorsToBrush like this...

<DataGrid Name="EditItemDetailsDataGrid" ItemsSource="{Binding}" AutoGenerateColumns="false">
   <DataGrid.Columns>
       <DataGridTextColumn Header="#" Binding="{Binding Path=Sequence}" IsReadOnly="true" Foreground="{Binding Path=HasErrors, Converter={StaticResource HasErrorsToBrush}}"/>
   </DataGrid.Columns>
<DataGrid/>

But this doesn't work. I put a breakpoint in the converter but it's being used by other grids on the page so I couldn't tell whether this column was calling it or not. Here's a nifty tip...

Converters have a converter parameter. I modified the binding to add a parameter like this.


    Foreground="{Binding Path=HasErrors, Converter={StaticResource HasErrorsToBrush}, ConverterParamter=SEQ}"

Now when I put a break point in the converter I can see that it's never getting called with a parameter of SEQ. What the hell?

Then I realized I was binding the Foreground for the DataGridCell, not for the TextBlock element in the cell. To do that I needed to work a little harder.

<DataGrid Name="EditItemDetailsDataGrid" ItemsSource="{Binding}" AutoGenerateColumns="false">
   <DataGrid.Columns>
       <DataGridTextColumn Header="#" Binding="{Binding Path=Sequence}" IsReadOnly="true"/>
           <DataGridTextColumn.ElementStyle>
               <Style TargetType="TextBlock">
                   <Setter Property=Foreground" Value="{Binding Path=HasErrors, Converter={StaticResource HasErrorsToBrush}}"/>
               </Style>
           </DataGridTextColumn.ElementStyle>
       </DataGridTextColumn>
   </DataGrid.Columns>
<DataGrid/>

Simple stuff, to be sure, but if you're not paying attention you can waste a lot of time on simple stuff.

Wednesday, January 15, 2014

TextInput event on DataGrid

WPF Version 4.0

This had me scratching my head for quite a while. Eventually it was Microsoft's documentation that revealed the way (could be a first!).

The problem is that the TextInput event bubbles, which means the TextBox got a crack at it before the DataGrid. The TextBox marked the event as handled, so it was not getting raised for the DataGrid. However, I vaguely remembered that there is a way to attach an event handler so that it gets raised EVEN FOR HANDLED EVENTS. I found some examples on Google but they are all for C#. The syntax for doing this in VB is as follows...

myDataGrid.AddHandler(TextBox.TextInputEvent, DirectCast(AddressOf myTextInputHandler, RoutedEventHandler), True)

For C#
myDataGrid.AddHandler(TextBox.TextInputEvent, new RoutedEventHandler(myTextInputHandler), true);

This is a very powerful technique because it can also attach handlers for events the target does not directly support. For example, you could attack a click event to a Label which would cause the Label to call your event handler when anything inside that label raised a click event. You can do this even though the Label control does not support the click event. In this case the following would not work.

AddHandler MyLabel.Click, Addressof MyLabelClickHandler

Friday, January 10, 2014

UpdateSourceTrigger on DataGrid

WPF Version 4.0 This had me stumped for a while. I have a data grid with editable fields. Three of the fields have a relationship - Quantity * Unit Price = Extended Price. Both the Quantity and Unit Price fields are editable, but the Extended Price field is read-only. When the user changes either the Quantity or the Unit Price on a row, the Extended Price is updated. Now this could be done with a multi-converter or it could be done in code behind. I'll be looking at the second approach. Here's snippet of XAML and an event handler. So here's some XAML

<DataGrid Name="DataGrid" Binding={Binding} CurrentCellChanged="DataGrid_CurrentCellChanged">
    <DataGrid.Columns>
        <DataGridTextColumn x:Name="Quantity" Header="Qty"/>
        <DataGridTextColumn x:Name="UnitPrice" Header="Unit Price"/>
        <DataGridTextColumn x:Name="ExtPrice" Header="Ext. Price" IsReadOnly="true"/>
    </Columns>
</DataGrid>

Private Sub DataGrid_CurrentCellChanged(Sender as Object, e as EventArgs)
    try
        Dim Q as double = DataGrid.SelectedItem("Quantity")
        Dim U as double = DataGrid.SelectedItem("UnitPrice")
        DataGrid.SelectedItem("ExtPrice") = Q * U
    catch
    end try
End Sub

When you enter a value in Quantity and tab out the CurrentCellChanged event handler is raised but when you peek inside SelectedItem you see that Quantity is empty. You can see it on the screen, but it isn't in the selected item yet. If you look around you can find UpdateSourceTrigger will allow you to control when a control updates it's backing source, but for a DataGrid the entire row is considered one control, so the backing source isn't updated until the entire row loses focus.

This isn't going to work for us because we want to update Extended Price while we're still on the row. You could use the VisualTreeManager to scrape the value out of the control without looking at the backing store but that's slow and brittle. The answer is to simply commit the datagrid in code by putting a line like this at the beginning of the event handler.


    DataGrid.CommitEdit()

This causes pending edits to be updated to the backing store. It's the same thing you do when you define UpdateSourceTrigger="Explicit". Of course, if we could set UpdateSourceTrigger="CellLostFocus" on a datagrid we could avoid this, but we can't.

Wednesday, January 1, 2014

Grouping and Filtering DataGrids

WPF Version 4.0

DataGrids (and other item collection controls) have considerable support for Grouping, Filtering, and Sorting, and Navigating. The default Sort behavior is exactly what my users want (hallelujah) so I don't need to do anything special for that. I don't have any requirements for programmatic Navigation at this time so that just leaves Grouping and Filtering.

Let's create a DataGrid and populate it with Products from the AdventureWorks2012 sample database. We will add checkboxes to allow us to optionally group by ProductLine and optionally filter out all products that are not Black.

Here is the XAML and the code-behind. I add/remove the group and filter in code but it can be done in XAML with the judicious use of converters. Code behind is easier.

<Window x:Class="GroupingAndFiltering.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Grouping and Filtering" Height="350" Width="1000">
    <Grid Name="LayoutGrid">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="0" Orientation="Horizontal">
            <TextBlock Text="Group By Product Line?" Margin="0,0,10,0"/>
            <CheckBox Name="GroupCheckBox" Checked="GroupCheckBox_Checked" Unchecked="GroupCheckBox_Unchecked"/>
            <TextBlock Text="Show Black Products Only?" Margin="10,0,10,0"/>
            <CheckBox Name="FilterCheckBox" Checked="FilterCheckBox_Checked" Unchecked="FilterCheckBox_Unchecked"/>
        </StackPanel>
        <DataGrid Name="dg" Grid.Row="1" ItemsSource="{Binding}" AutoGenerateColumns="False" IsReadOnly="true">
            <DataGrid.GroupStyle>
                <GroupStyle>
                    <GroupStyle.HeaderTemplate>
                        <DataTemplate>
                            <Border BorderBrush="Black" BorderThickness="1" CornerRadius="5" Padding="5,5,5,5">
                                <StackPanel Orientation="Horizontal">
                                <TextBlock Text="Product Line Group '" FontWeight="Bold"/>
                                <TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>
                                <TextBlock Text="', Number of Products = " FontWeight="Bold"/>
                                    <TextBlock Text="{Binding Path=ItemCount}" FontWeight="Bold"/>
                                </StackPanel>
                            </Border>
                        </DataTemplate>
                    </GroupStyle.HeaderTemplate>
                </GroupStyle>
            </DataGrid.GroupStyle>
            <DataGrid.Columns>
                <DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
                <DataGridTextColumn Header="Product Number" Binding="{Binding ProductNumber}"/>
                <DataGridTextColumn Header="Product Line" Binding="{Binding ProductLine}"/>
                <DataGridTextColumn Header="Color" Binding="{Binding Color}"/>
                <DataGridTextColumn Header="Class" Binding="{Binding Class}"/>
                <DataGridTextColumn Header="Style" Binding="{Binding Style}"/>
                <DataGridTextColumn Header="Size" Binding="{Binding Size}"/>
                <DataGridTextColumn Header="Size Unit" Binding="{Binding SizeUnitMeasureCode}"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>


using System;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Data;
using System.Data.SqlClient;
using System.ComponentModel;

namespace GroupingAndFiltering
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            string sConn = "Server=(local); Database=AdventureWorks2012;Trusted_Connection=true";
            SqlConnection conn = new SqlConnection(sConn);
            string sSQL = "SELECT * FROM [Production].[Product] ORDER BY ProductLine, Name";
            SqlCommand comm;
            SqlDataReader DR;
            DataTable DT = new DataTable();
            DataTable UoM = new DataTable();

            conn.Open();
            comm = new SqlCommand(sSQL, conn);
            DR = comm.ExecuteReader();
            DT.Load(DR);
            LayoutGrid.DataContext = DT;
            DR.Close();
            conn.Close();
        }

        private void GroupCheckBox_Checked(object sender, RoutedEventArgs e)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(LayoutGrid.DataContext);
            view.GroupDescriptions.Clear();
            view.GroupDescriptions.Add(new PropertyGroupDescription("ProductLine"));
        }

        private void GroupCheckBox_Unchecked(object sender, RoutedEventArgs e)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(LayoutGrid.DataContext);
            view.GroupDescriptions.Clear();
        }

        private void FilterCheckBox_Checked(object sender, RoutedEventArgs e)
        {
            ((DataTable)LayoutGrid.DataContext).DefaultView.RowFilter = "Color='Black'";
        }

        private void FilterCheckBox_Unchecked(object sender, RoutedEventArgs e)
        {
            ((DataTable)LayoutGrid.DataContext).DefaultView.RowFilter = "";
        }
    }
}

Try Sorting and Filtering with no Groups. Now turn Grouping on and try Sorting and Filtering again. See how slow Grouping makes everything? I think it's because you can't virtualize a control that has grouping. This example only has about 500 records - not a lot. In my opinion Grouping makes everything too slow. I am not going to offer it to my users. If you don't have many records there's no point in grouping, but if you have a lot of records it's too slow.

On the other hand, Filtering is very fast (when Grouping is off) and I can see a use for it. For example, I have some screens that have a document type combobox and a location combobox. The list of valid locations depends on the selected documents type. This requirement will be easy to satisfy with a filter on the location dataview.

A word about the filter. You'll notice I implement the group by adding a PropertyGroupDescription object to the View's GroupDescriptions collection. The View also has a FilterDescriptions collection but a closer look at the View shows that the CanFilter property is false. Trying to add a filter to the FilterDescriptions throws an exception. This approach only works if the view's class implements ICollectionView, which DataView does not.

When the View has a DataTable as it's backing store you have to implement filters through the DataView's RowFilter property which is a simple SQL clause such as "Color='Black'". It's actually easier.

Master/Detail Binding

WPF version 4.0

A popular screen design involves a read-only list (possible a grid or listbox) in one part of the window and a collection of discreet fields in the other part. As you scroll through the list, the currently selected item is displayed in the discreet fields where they can be modified. At some point, either when a field is modified, a different item is selected, or the user clicks a [Save] button, all the changes are posted back to the underlying database.

I recently came across the binding syntax that makes this very easy. It looks like this...

    <TextBox Text="{Binding Path=/Name, Mode=TwoWay}"/>

This forward slash syntax causes the Name property of the currently selected item in the textbox's ItemsSource to be displayed as the text in the textbox. If the user modifies the text, the currently selected item gets updated too.

When a grid or listbox has a DataView as its ItemsSource the IsSynchronizedWithCurrentItem property is false by default. This means when the user selects a different item in the control, the current item in the DataView is not changed which means the discreet controls are not updated. You have to set this property to true to keep the master control synchronized with the detail controls.

Here's a chunk of XAML with its code-behind that shows how to do all this. It uses the AdventureWorks2012 sample database on the local machine. All the code-behind does is populate and assign the datatables required for the example.


<Window x:Class="ParentChild.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Parent/Child" Height="350" Width="1000">
    <Window.Resources>
        <Style TargetType="{x:Type TextBlock}" x:Key="Prompt">
            <Setter Property="HorizontalAlignment" Value="Right"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="Margin" Value="0,2,10,2"/>
        </Style>
        <Style TargetType="{x:Type TextBox}" x:Key="Value">
            <Setter Property="BorderBrush" Value="Gray"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="Background" Value="LightYellow"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="Margin" Value="0,2,0,2"/>
        </Style>
        <Style TargetType="{x:Type CheckBox}" x:Key="cbValue">
            <Setter Property="HorizontalAlignment" Value="Left"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
        </Style>
        <Style TargetType="{x:Type ComboBox}" x:Key="ddValue">
            <Setter Property="HorizontalAlignment" Value="Left"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="MinWidth" Value="100"/>
        </Style>
    </Window.Resources>
    <Grid Name="LayoutGrid">
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <DataGrid Name="dg" Grid.Row="0" ItemsSource="{Binding}" AutoGenerateColumns="False" IsReadOnly="true" IsSynchronizedWithCurrentItem="true">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
                <DataGridTextColumn Header="Product Number" Binding="{Binding ProductNumber}"/>
                <DataGridTextColumn Header="Product Line" Binding="{Binding ProductLine}"/>
                <DataGridTextColumn Header="Color" Binding="{Binding Color}"/>
                <DataGridTextColumn Header="Class" Binding="{Binding Class}"/>
                <DataGridTextColumn Header="Style" Binding="{Binding Style}"/>
                <DataGridTextColumn Header="Size" Binding="{Binding Size}"/>
                <DataGridTextColumn Header="Size Unit" Binding="{Binding SizeUnitMeasureCode}"/>
            </DataGrid.Columns>
        </DataGrid>
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition></ColumnDefinition>
                <ColumnDefinition></ColumnDefinition>
                <ColumnDefinition></ColumnDefinition>
                <ColumnDefinition></ColumnDefinition>
                <ColumnDefinition></ColumnDefinition>
                <ColumnDefinition></ColumnDefinition>
                <ColumnDefinition></ColumnDefinition>
                <ColumnDefinition></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"></RowDefinition>
                <RowDefinition Height="auto"></RowDefinition>
                <RowDefinition Height="auto"></RowDefinition>
                <RowDefinition Height="auto"></RowDefinition>
            </Grid.RowDefinitions>
            <TextBlock Grid.Row="0" Grid.Column="0" Text="Name:" Style="{StaticResource Prompt}"/>
            <TextBox Name="NameTextBox" Grid.Row="0" Grid.Column="1" Style="{StaticResource Value}" Text="{Binding Path=/Name, Mode=TwoWay}"/>
            <TextBlock Grid.Row="0" Grid.Column="2" Text="Product Number:" Style="{StaticResource Prompt}"/>
            <TextBox Name="ProductNumberTextBox" Grid.Row="0" Grid.Column="3" Style="{StaticResource Value}" Text="{Binding Path=/ProductNumber, Mode=TwoWay}"/>
            <TextBlock Grid.Row="0" Grid.Column="4" Text="Product Line?" Style="{StaticResource Prompt}"/>
            <TextBox Name="ProductLine" Grid.Row="0" Grid.Column="5" Style="{StaticResource Value}" Text="{Binding Path=/ProductLine, Mode=TwoWay}"/>
            <TextBlock Grid.Row="0" Grid.Column="6" Text="Finished?" Style="{StaticResource Prompt}"/>
            <CheckBox Name="FinishedCheckBox" Grid.Row="0" Grid.Column="7" Style="{StaticResource cbValue}" IsChecked="{Binding Path=/FinishedGoodsFlag, Mode=TwoWay}"/>
            
            <TextBlock Grid.Row="1" Grid.Column="0" Text="Color:" Style="{StaticResource Prompt}"/>
            <TextBox Name="ColorTextBox" Grid.Row="1" Grid.Column="1" Style="{StaticResource Value}" Text="{Binding Path=/Color, Mode=TwoWay}"/>
            <TextBlock Grid.Row="1" Grid.Column="2" Text="Safety Stock:" Style="{StaticResource Prompt}"/>
            <TextBox Name="SafetyTextBox" Grid.Row="1" Grid.Column="3" Style="{StaticResource Value}" Text="{Binding Path=/SafetyStockLevel, StringFormat=#, Mode=TwoWay}"/>
            <TextBlock Grid.Row="1" Grid.Column="4" Text="Reorder Point:" Style="{StaticResource Prompt}"/>
            <TextBox Name="ReorderTextBox" Grid.Row="1" Grid.Column="5" Style="{StaticResource Value}" Text="{Binding Path=/ReorderPoint, StringFormat=#, Mode=TwoWay}"/>
            <TextBlock Grid.Row="1" Grid.Column="6" Text="Standard Cost:" Style="{StaticResource Prompt}"/>
            <TextBox Name="CostTextBox" Grid.Row="1" Grid.Column="7" Style="{StaticResource Value}" Text="{Binding Path=/StandardCost, StringFormat=C, Mode=TwoWay}"/>

            <TextBlock Grid.Row="2" Grid.Column="0" Text="List Price:" Style="{StaticResource Prompt}"/>
            <TextBox Name="ListPriceTextBox" Grid.Row="2" Grid.Column="1" Style="{StaticResource Value}" Text="{Binding Path=/ListPrice, StringFormat=C, Mode=TwoWay}"/>
            <TextBlock Grid.Row="2" Grid.Column="2" Text="Size:" Style="{StaticResource Prompt}"/>
            <TextBox Name="SizeTextBox" Grid.Row="2" Grid.Column="3" Style="{StaticResource Value}" Text="{Binding Path=/Size, Mode=TwoWay}"/>
            <TextBlock Grid.Row="2" Grid.Column="4" Text="Size Unit:" Style="{StaticResource Prompt}"/>
            <ComboBox Name="SizeUnitCombo" Grid.Row="2" Grid.Column="5" Style="{StaticResource ddValue}" SelectedValue="{Binding Path=/SizeUnitMeasureCode}" DisplayMemberPath="Name" SelectedValuePath="UnitMeasureCode"/>
            <TextBlock Grid.Row="2" Grid.Column="6" Text="Days To Make:" Style="{StaticResource Prompt}"/>
            <TextBox Name="DaysToMakeTextBox" Grid.Row="2" Grid.Column="7" Style="{StaticResource Value}" Text="{Binding Path=/DaysToManufacture, StringFormat=#, Mode=TwoWay}"/>
        </Grid>
    </Grid>
</Window>



using System;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Data;
using System.Data.SqlClient;
using System.ComponentModel;

namespace ParentChild
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            string sConn = "Server=(local); Database=AdventureWorks2012;Trusted_Connection=true";
            SqlConnection conn = new SqlConnection(sConn);
            string sSQL = "SELECT * FROM [Production].[Product] ORDER BY ProductLine, Name";
            SqlCommand comm;
            SqlDataReader DR;
            DataTable DT = new DataTable();
            DataTable UoM = new DataTable();

            conn.Open();
            comm = new SqlCommand(sSQL, conn);
            DR = comm.ExecuteReader();
            DT.Load(DR);
            LayoutGrid.DataContext = DT;
            DR.Close();

            sSQL = "SELECT * FROM [Production].[UnitMeasure] ORDER BY Name";
            comm = new SqlCommand(sSQL, conn);
            DR = comm.ExecuteReader();
            UoM.Load(DR);
            SizeUnitCombo.ItemsSource = UoM.DefaultView;
            conn.Close();
        }
    }
}

Notice how we set the DataContext of the parent LayoutGrid control so that the DataGrid and all the editable controls use it. This is the simplest way to get all the ItemsSources synchronized.

As you select different products in the DataGrid, the discreet controls get updated. Try changing a color and tabbing off the color TextBox. The DataGrid is instantly updated because the underlying DataView (and therefore the underlying DataTable) got updated by the TwoWay binding. If you called the datatable's Update method the changes would be saved to the database.