1. Problem
You need to display additional detail information about a bound row in a DataGrid on demand so that the details portion is displayed in place within the DataGrid.
2. Solution
Use the RowDetailsTemplate property of the DataGrid to associate a data template that can be used to display additional data on demand.
3. How It Works
The DataGrid.RowDetailsTemplate property accepts a data template
that can be used to display additional data in place, associated with a
bound row. This feature comes handy in many scenarios—to provide
master-detail data where multiple detail records need to be displayed
for a top-level row or where additional information, not otherwise bound
to top-level columns, needs to be displayed adjacent to a row.
The DataGrid.RowDetailsVisibilityMode property controls the visibility of the row details information at DataGrid scope. That is, setting it to Visible keeps it always visible for every bound row, whereas setting it to VisibleWhenSelected
makes the details portion of a row visible when the row is selected and
collapsed back when selection moves off to another row. To control row
details' visibility in code, set this property to Collapsed, which hides row details for every row, and instead use the DataGridRow.DetailsVisibility property on the individual row.
The DataGrid also exposes two useful events: LoadingRowDetails and UnloadingRowDetails. LoadingRowDetails is raised when the DataGrid initially applies the RowDetailsTemplate
to the row. This is especially useful if you want to load the data for
the row details in a delayed fashion—placing the code to load the data
in the handler for LoadingRowDetails ensures that the data is
only loaded when the user first expands the row details and is never
executed again, unless the row details are explicitly unloaded. UnloadingRowDetails is raised when the rows are unloaded, such as when a method like DataGrid.ClearRows() is invoked.
The DataGrid also raises another event called RowDetailsVisibilityChanged every time a row detail is either made visible or is collapsed.
NOTE
The DataGrid control is found in the System.Windows.Controls.Data assembly in the System.Windows.Controls namespace.
1. The Code
For this code sample, you bind a DataGrid to product data sourced from the AdventureWorks
WCF service. Each row, in addition to the bound columns, also displays
some row details data, including an image of the product, inventory
information, a product description, and another DataGrid displaying the cost history records of the product demonstrating a master-detail arrangement. Figure 5-19 shows the DataGrid with the row details of a row expanded.
Listing 1 shows the XAML for the page.
Listing 1. XAML for the page hosting the DataGrid with row details
<UserControl x:Class="Recipe5_5.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data=
"clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
Width="900" Height="600" >
<UserControl.Resources>
<DataTemplate x:Key="dtProductRowDetails">
<Grid Height="350" Width="646">
<Grid.RowDefinitions>
<RowDefinition Height="0.127*"/>
<RowDefinition Height="0.391*"/>
<RowDefinition Height="0.482*"/>
</Grid.RowDefinitions>
<Grid.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF7D7A7A"/>
<GradientStop Color="#FFFFFFFF" Offset="1"/>
</LinearGradientBrush>
</Grid.Background>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.245*"/>
<ColumnDefinition Width="0.755*"/>
</Grid.ColumnDefinitions>
<Border HorizontalAlignment="Stretch" Margin="5,5,5,5"
VerticalAlignment="Stretch" Grid.RowSpan="2"
BorderThickness="4,4,4,4">
<Border.BorderBrush>
<LinearGradientBrush
EndPoint="1.02499997615814,0.448000013828278"
StartPoint="−0.0130000002682209,0.448000013828278">
<GradientStop Color="#FF000000"/>
<GradientStop Color="#FF6C6C6C" Offset="1"/>
</LinearGradientBrush>
</Border.BorderBrush>
<Image MinHeight="50" MinWidth="50"
Source="{Binding ProductPhoto.LargePhotoPNG}"
Stretch="Fill"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"/>
</Border>
<Grid HorizontalAlignment="Stretch" Margin="8,8,8,0"
VerticalAlignment="Stretch"
Grid.Column="1" Grid.RowSpan="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.25*"/>
<ColumnDefinition Width="0.3*"/>
<ColumnDefinition Width="0.05*"/>
<ColumnDefinition Width="0.4*"/>
</Grid.ColumnDefinitions>
<StackPanel HorizontalAlignment="Stretch" Grid.Column="0"
Orientation="Horizontal" Margin="1,0,1,0">
<Ellipse Height="15" Width="15"
Fill="{Binding InventoryLevelBrush}" Margin="0,0,2,0" />
<TextBlock Text="{Binding InventoryLevelMessage}" FontSize="12"
FontWeight="Bold"
VerticalAlignment="Center" Margin="2,0,0,0"/>
</StackPanel>
<TextBlock HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Grid.ColumnSpan="1"
Text="{Binding ProductCategory.Name}"
TextAlignment="Right" TextWrapping="Wrap"
Grid.Column="1" FontSize="13"/>
<TextBlock HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Grid.Column="2" Text="/"
TextWrapping="Wrap" TextAlignment="Center"
FontSize="13" />
<TextBlock HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Grid.Column="3" Grid.ColumnSpan="1"
Text="{Binding ProductSubCategory.Name}"
TextWrapping="Wrap" TextAlignment="Left"
FontSize="13"/>
</Grid>
<StackPanel Orientation="Vertical"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="8,8,8,8"
Grid.ColumnSpan="2"
Grid.Row="2" Grid.RowSpan="1" >
<TextBlock Height="Auto" Width="Auto"
FontSize="12" FontWeight="Bold"
Text="Cost History" Margin="0,0,0,10"/>
<data:DataGrid AutoGenerateColumns="False"
ItemsSource="{Binding ProductCostHistories}">
<data:DataGrid.Columns>
<data:DataGridTextColumn Binding="{Binding StartDate}"
Header="Start"/>
<data:DataGridTextColumn Binding="{Binding EndDate}"
Header="End"/>
<data:DataGridTextColumn
Binding="{Binding StandardCost}"
Header="Cost"/>
</data:DataGrid.Columns>
</data:DataGrid>
</StackPanel>
<Border HorizontalAlignment="Stretch"
Margin="8,8,8,8"
VerticalAlignment="Stretch"
Grid.Column="1"
Grid.Row="1"
Grid.RowSpan="1"
BorderBrush="#FF000000"
BorderThickness="1,1,1,1">
<TextBox Height="Auto" Width="Auto"
FontSize="12"
FontWeight="Bold"
Text="{Binding ProductDescription.Description,Mode=TwoWay}"
TextWrapping="Wrap"/>
</Border>
</Grid>
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<data:DataGrid x:Name="dgProducts" AutoGenerateColumns="False"
RowDetailsTemplate="{StaticResource dtProductRowDetails}"
RowDetailsVisibilityMode="Collapsed">
<data:DataGrid.Columns>
<data:DataGridTextColumn
Binding="{Binding ProductID}" Header="ID" />
<data:DataGridTextColumn
Binding="{Binding Name}" Header="Name" />
<data:DataGridTextColumn
Binding="{Binding ProductNumber}" Header="Number"/>
<data:DataGridTextColumn
Binding="{Binding ListPrice}" Header="List Price"/>
<data:DataGridTextColumn
Binding="{Binding Style}" Header="Style"/>
<data:DataGridTextColumn
Binding="{Binding Color}" Header="Color"/>
<data:DataGridTemplateColumn>
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate x:Key="dtShowDetailTemplate">
<Button Content="..." x:Name="ShowDetails"
Click="ShowDetails_Click" />
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
</data:DataGridTemplateColumn>
</data:DataGrid.Columns>
</data:DataGrid>
</Grid>
</UserControl>
|
The data template used for the RowDetailsTemplate is named dtProductRowDetails and contains fields bound to several properties in the Product data class plus some nested classes. It displays an image of the product along with category and inventory information.
To use the data template in the DataGrid named dgProducts, set the DataGrid.RowDetailsVisibilityMode to Collapsed
so that all rows have their detail information hidden to start with. To
allow users to display row details on demand, an extra column of type DataGridTemplateColumn is added to the DataGrid with a specific CellTemplate containing a Button. (You will learn more about column templates in the next recipe.) The CellTemplate causes a Button to be displayed in the last column of each bound row, and you use the Button's click handler ShowDetails_Click() to allow the user to toggle the Visibility of the row detail information, as shown the codebehind for the page in Listing 2.
Listing 2. Codebehind for the MainPage hosting the DataGrid
using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Recipe5_5.AdvWorks;
namespace Recipe5_5
{
public partial class MainPage : UserControl
{
AdvWorksDataServiceClient client =
new AdvWorksDataServiceClient();
public MainPage()
{
InitializeComponent();
//async completion callbacks for the web service calls to get data
client.GetPhotosCompleted +=
new EventHandler<GetPhotosCompletedEventArgs>(
delegate(object s1, GetPhotosCompletedEventArgs e1)
{
(e1.UserState as Product).ProductPhoto = e1.Result;
});
client.GetInventoryCompleted +=
new EventHandler<GetInventoryCompletedEventArgs>(
delegate(object s2, GetInventoryCompletedEventArgs e2)
{
(e2.UserState as Product).ProductInventories = e2.Result;
(e2.UserState as Product).InventoryLevelBrush = null;
(e2.UserState as Product).InventoryLevelMessage = null;
});
client.GetCategoryCompleted +=
new EventHandler<GetCategoryCompletedEventArgs>(
delegate(object s3, GetCategoryCompletedEventArgs e3)
{
(e3.UserState as Product).ProductCategory = e3.Result;
});
client.GetSubcategoryCompleted +=
new EventHandler<GetSubcategoryCompletedEventArgs>(
delegate(object s4, GetSubcategoryCompletedEventArgs e4)
{
(e4.UserState as Product).ProductSubCategory = e4.Result;
});
client.GetDescriptionCompleted +=
new EventHandler<GetDescriptionCompletedEventArgs>(
delegate(object s5, GetDescriptionCompletedEventArgs e5)
{
(e5.UserState as Product).ProductDescription = e5.Result;
});
client.GetProductCostHistoryCompleted +=
new EventHandler<GetProductCostHistoryCompletedEventArgs>(
delegate(object s6, GetProductCostHistoryCompletedEventArgs e6)
{
(e6.UserState as Product).ProductCostHistories = e6.Result;
});
//LoadingRowDetails handler - here we make the calls to load
//row details data on demand
dgProducts.LoadingRowDetails +=
new EventHandler<DataGridRowDetailsEventArgs>(
delegate(object sender, DataGridRowDetailsEventArgs e)
{
Product prod = e.Row.DataContext as Product;
if (prod.ProductInventories == null)
client.GetInventoryAsync(prod, prod);
if (prod.ProductCategory == null && prod.ProductSubcategoryID != null)
client.GetCategoryAsync(prod, prod);
if (prod.ProductSubCategory == null &&
prod.ProductSubcategoryID != null)
client.GetSubcategoryAsync(prod, prod);
if (prod.ProductDescription == null)
client.GetDescriptionAsync(prod, prod);
if (prod.ProductPhoto == null)
client.GetPhotosAsync(prod, prod);
if (prod.ProductCostHistories == null)
client.GetProductCostHistoryAsync(prod, prod);
});
GetData();
}
private void GetData()
{
//get the top level product data
client.GetProductsCompleted +=
new EventHandler<GetProductsCompletedEventArgs>(
delegate(object sender, GetProductsCompletedEventArgs e)
{
dgProducts.ItemsSource = e.Result;
});
client.GetProductsAsync();
}
private void ShowDetails_Click(object sender, RoutedEventArgs e)
{
DataGridRow row = DataGridRow.GetRowContainingElement(sender as Button);
row.DetailsVisibility =
(row.DetailsVisibility == Visibility.Collapsed ?
Visibility.Visible : Visibility.Collapsed);
}
}
}
namespace Recipe5_5.AdvWorks
{
public partial class ProductPhoto
{
private BitmapImage _LargePhotoPNG;
public BitmapImage LargePhotoPNG
{
get
{
BitmapImage bim = new BitmapImage();
MemoryStream ms = new MemoryStream(this.LargePhoto.Bytes);
bim.SetSource(ms);
ms.Close();
return bim;
}
set
{
RaisePropertyChanged("LargePhotoPNG");
}
}
}
public partial class Product
{
private SolidColorBrush _InventoryLevelBrush;
public SolidColorBrush InventoryLevelBrush
{
get
{
return (this.ProductInventories == null
|| this.ProductInventories.Count == 0) ?
new SolidColorBrush(Colors.Gray) :
(this.ProductInventories[0].Quantity > this.SafetyStockLevel ?
new SolidColorBrush(Colors.Green) :
(this.ProductInventories[0].Quantity > this.ReorderPoint ?
new SolidColorBrush(Colors.Yellow) :
new SolidColorBrush(Colors.Red)));
}
set
{
//no actual value set here - just property change raised
RaisePropertyChanged("InventoryLevelBrush");
}
}
private string _InventoryLevelMessage;
public string InventoryLevelMessage
{
get
{
return (this.ProductInventories == null
|| this.ProductInventories.Count == 0) ?
"Stock Level Unknown" :
(this.ProductInventories[0].Quantity > this.SafetyStockLevel ?
"In Stock" :
(this.ProductInventories[0].Quantity > this.ReorderPoint ?
"Low Stock" : "Reorder Now"));
}
set
{
//no actual value set here - just property change raised
RaisePropertyChanged("InventoryLevelMessage");
}
}
private ProductSubcategory _productSubCategory;
public ProductSubcategory ProductSubCategory
{
get { return _productSubCategory; }
set
{
_productSubCategory = value;
RaisePropertyChanged("ProductSubCategory");
}
}
private ProductCategory _productCategory;
public ProductCategory ProductCategory
{
get { return _productCategory; }
set { _productCategory = value; RaisePropertyChanged("ProductCategory"); }
}
private ProductDescription _productDescription;
public ProductDescription ProductDescription
{
get { return _productDescription; }
set
{
_productDescription = value;
RaisePropertyChanged("ProductDescription");
}
}
private ProductReview _productReview;
public ProductReview ProductReview
{
get { return _productReview; }
set { _productReview = value; RaisePropertyChanged("ProductReview"); }
}
private ProductPhoto _productPhoto;
public ProductPhoto ProductPhoto
{
get { return _productPhoto; }
set { _productPhoto = value; RaisePropertyChanged("ProductPhoto"); }
}
}
}
|
Once again, the data is acquired by calling the AdventureWorks WCF service. The GetData() method loads the initial product data into the rows to which the DataGrid is bound. You set the DataGrid's ItemsSource in the completion handler for the GetProductsAsync() web service call. When the user toggles the visibility of a row's details for the first time, the LoadingRowDetails event is raised, and the row detail data is fetched from the web service in that handler, defined using an anonymous delegate.
The row detail data, once fetched, is bound to various parts of the UI by setting appropriate properties in the already bound Product instance, which, in turn, uses property change notification to update the UI. Just as in the previous recipes, you extend the Product partial class, as generated by the WCF service proxy, to include the additional property definitions.