This tutorial will demonstrate how to create and bind DataRange objects manually and how to create master-detail relationship between data-ranges representing related tables. The following steps are explained in more details below:
- Prerequisites
- Nesting DataRange objects for master-detail implementation
- Providing details in a master-detail relationship
- Running the application
Prerequisites
This tutorial continues from where Tutorial 1: Getting Started has ended. It is therefore assumed that we have an existing application with a single data-bound report in it and a window with a set up ReportViewer that displays an instance of this report.
1. Nesting DataRange objects for master-detail implementation
We create a new DataRange object within the report's existing XAML declaration in a way similar to the one used in Tutorial 1. The new DataRange object is bound to the Categories data and contains two elements - a Label and a Picture, bound respectively to the CategoryName and Picture fields in the Categories table. The XAML code below illustrates the code of the new DataRange object.
XAML
Copy Code
|
---|
... <r:DataRange Location="0,0" Size="100%,150"> <r:DataRange.ItemTemplate> <DataTemplate> <r:ItemContainer> <r:Label Location="10,10" Size="190,20" Text="[CategoryName]" /> <r:Picture Location="60%,10" Size="38%,135" Image="{Binding Converter={StaticResource imageConverter}, ConverterParameter=Picture}" /> </r:ItemContainer> </DataTemplate> </r:DataRange.ItemTemplate> </r:DataRange> ... |
Notice in the code above that the image uses a converter (defined as static resource) in order to bind to the Picture subelement of the XElement representing the category in the data source. The source code of this converter could look similar to the following:
C#
Copy Code
|
---|
public class ImageConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var xelement = value as XElement; if (xelement != null) { if (parameter is string) { var parameterValue = xelement.Element(parameter.ToString()).Value; byte[] rawData = System.Convert.FromBase64String(parameterValue); MemoryStream stream = new MemoryStream(rawData); BitmapImage image = new BitmapImage(); image.SetSource(stream); return image; } }
return null; }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } |
Visual Basic
Copy Code
|
---|
Public Class ImageConverter Implements IValueConverter
Public Function Convert(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements IValueConverter.Convert
If (TypeOf value Is XElement) Then
Dim xelement = CType(value, XElement) If (xelement IsNot Nothing) Then
If (TypeOf parameter Is String) Then
Dim parameterValue = xelement.Element(parameter.ToString()).Value Dim rawData = System.Convert.FromBase64String(parameterValue) Dim stream As New MemoryStream(rawData) Dim image As New BitmapImage() image.SetSource(stream) Return image
End If
End If
End If
Return Nothing
End Function
Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
Throw New NotImplementedException()
End Function
End Class |
Now we want to embed the old DataRange, showing products, within the newly created DataRange, showing categories and we want to establish a master-detail relationship between the two data ranges so that the result displays products, grouped by category. Here is the new XAML code of the report illustrating the embedded data ranges.
XAML
Copy Code
|
---|
<r:Report x:Key="myReport"> <r:Page> <r:DataRange Location="0,0" Size="100%,150"> <r:DataRange.ItemTemplate> <DataTemplate> <r:ItemContainer> <r:Label Location="10,10" Size="190,20" Text="[CategoryName]" /> <r:Picture Location="60%,10" Size="38%,135" Image="{Binding Converter={StaticResource imageConverter}, ConverterParameter=Picture}" />
<r:DataRange Location="0,75" Size="60%,20" Name="ProductsRange"> <r:DataRange.HeaderTemplate> <DataTemplate> <r:ItemContainer Size="100%,20"> <r:Label Text="ProductID" Location="0%,0" Size="20%,20" /> <r:Label Text="ProductName" Location="20%,0" Size="20%,20" /> <r:Label Text="QuantityPerUnit" Location="40%,0" Size="20%,20" /> <r:Label Text="UnitPrice" Location="60%,0" Size="20%,20" /> <r:Label Text="UnitsInStock" Location="80%,0" Size="20%,20" /> </r:ItemContainer> </DataTemplate> </r:DataRange.HeaderTemplate> <r:DataRange.ItemTemplate> <DataTemplate> <r:ItemContainer> <r:Label Text="[ProductID]" Location="0%,0" Size="20%,20" /> <r:Label Text="[ProductName]" Location="20%,0" Size="20%,20" /> <r:Label Text="[QuantityPerUnit]" Location="40%,0" Size="20%,20" /> <r:Label Text="[UnitPrice]" Location="60%,0" Size="20%,20" /> <r:Label Text="[UnitsInStock]" Location="80%,0" Size="20%,20" /> </r:ItemContainer> </DataTemplate> </r:DataRange.ItemTemplate> </r:DataRange>
</r:ItemContainer> </DataTemplate> </r:DataRange.ItemTemplate> </r:DataRange> </r:Page> </r:Report> |
Note that the old DataRange (the one showing products) has had its data source removed. We will supply the data through the QueryDetails event depending on the category each instance of this DataRange belongs to. Note also that the products range now has a name associated with it. This will help us identify the range within the QueryDetails event handler as illustrated later in this topic. Finally, note that the Location of the embedded DataRange has been modified so that it is positioned correctly in its parent and its size is reduced to 60% so that it doesn't overlap with the image to the right.
Navigate to the page's constructor in the code behind and add the code necessary to populate the categories from the XML data source. Additionally, expose the existing products IEnumerable as a class field because it will be needed in the QueryDetails event handler. The following code (new code highlighted in bold) demonstrates this:
C#
Copy Code
|
---|
document = XDocument.Load("Data/nwind.xml");
var categories = document.Elements("dataroot").Elements("Categories"); products = document.Elements("dataroot").Elements("Products");
myReport.DataContext = categories; ...
private IEnumerable<XElement> products; |
Visual Basic
Copy Code
|
---|
Dim document = XDocument.Load("Data/nwind.xml")
Dim categories = document.Elements("dataroot").Elements("Categories") products = document.Elements("dataroot").Elements("Products")
myReport.DataContext = categories ...
Private products As IEnumerable(Of XElement) |
2. Providing details in a master-detail relationship
To provide a master-detail implementation for the two data ranges, we need to handle the QueryDetails event on the Report class. Keep in mind that the event must be handled before the Run method of the Report is invoked. The following code illustrates how the event handler should look.
C#
Copy Code
|
---|
void myReport_QueryDetails(object sender, QueryDetailsEventArgs e) { DataRange dataRange = sender as DataRange; if (dataRange != null) { if (dataRange.Name == "ProductsRange") { var category = e.MasterRow as XElement;
e.Details = products.Where( element => element.Element("CategoryID").Value == category.Element("CategoryID").Value); } } } |
Visual Basic
Copy Code
|
---|
Sub myReport_QueryDetails(ByVal sender As Object, ByVal e As QueryDetailsEventArgs)
Dim dataRange As DataRange = sender If (Not dataRange Is Nothing) Then
If (dataRange.Name = "ProductsRange") Then
Dim category = CType(e.MasterRow, XElement)
e.Details = _ From product In products _ Where product.Element("CategoryID").Value = category.Element("CategoryID").Value
End If
End If
End Sub |
The code above inspects the report element, which requests details. This is usually a DataRange or a PieChart object. Then, it identifies the object by inspecting its Name. If the object is the one we are expecting, we provide the necessary details by matching the CategoryID of the products in the database against the ID of the category associated with the data range.
Note |
---|
Do not forget to include the System.Linq namespace in order the above code to compile successfully. |
3. Running the application
Running the application will yield a result similar to the one illustrated by the following image: