Wednesday, January 1, 2014

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.

No comments:

Post a Comment