4.5. Master-Detail Binding
We've seen binding to a single object. We've seen binding to a
single list of objects. One other very popular thing to do is to bind to more
than one list, especially related lists. For example, if you're showing your
users a list of customers and then, when they select one, you'd like to show
that customer's related orders, you'll want master-detail binding.
Master-detail binding is a form
of filtering, where the selection in the master liste.g., customer 452sets the
filtering parameters for the associated detail datae.g., orders for customer
452.
In our discussion thus far, we don't have customers and orders,
but we do have families and people, which we could further formalize as shown
in Example 4-47.
Example 4-47. Master-detail data for binding
public class Families : ObservableCollection<Family> {}
public class Family {
string familyName;
public string FamilyName {
get { return familyName; }
set { familyName = value; }
}
People members;
public People Members {
get { return members; }
set { members = value; }
}
}
public class People : ObservableCollection<Person> {}
public class Person {
string name;
public string Name {
get { return name; }
set { name = value; }
}
int age;
public int Age {
get { return age; }
set { age = value; }
}
}
In Example 4-47,
we've got our familiar Person class with Name and Age
properties, collected into a familiar People collection. Further, we
have a Family class with a FamilyName property and a Members
property of type People. Finally, we have a Families collection,
which collects Family objects. In other words, families have members,
which consist of people with names and ages.
You could imagine instances of Families, Family,
People, and Person that looked like
Figure 4-19.
In Figure 4-19,
the Families collection forms the master data, holding instances of
the Family class, each of which holds a Members property of
type People, which holds the detail Person data. You could
populate instances of these data structures, as shown in
Example 4-48.
Example 4-48. Declaring sample master-detail data
<!-- Window1.xaml -->
<?Mapping
XmlNamespace="local" ClrNamespace="MasterDetailBinding" ?>
<Window ... xmlns:local="local">
<Window.Resources>
<local:Families x:Key="Families">
<local:Family FamilyName="Stooge">
<local:Family.Members>
<local:People>
<local:Person Name="Larry" Age="21" />
<local:Person Name="Moe" Age="22" />
<local:Person Name="Curly" Age="23" />
</local:People>
</local:Family.Members>
</local:Family>
<local:Family FamilyName="Addams">
<local:Family.Members>
<local:People>
<local:Person Name="Gomez" Age="135" />
<local:Person Name="Morticia" Age="121" />
<local:Person Name="Fester" Age="137" />
</local:People>
</local:Family.Members>
</local:Family>
</local:Families>
</Window.Resources>
...
</Window>
Binding to this data at the top leveli.e., to show the family
namescould look like Example 4-49.
Example 4-49. Binding to master Family data
<!-- Window1.xaml -->
<?Mapping ... ?>
<Window ...>
<Window.Resources>
<local:Families x:Key="Families">...</local:Families>
</Window.Resources>
<Grid DataContext="{StaticResource Families}">
...
<!-- Families Column -->
<TextBlock Grid.Row="0" Grid.Column="0">Families:</TextBlock>
<ListBox Grid.Row="1" Grid.Column="0"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock TextContent="{Binding Path=FamilyName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
In Example 4-49,
we're setting two things in the Families column (column 0). The first
is the header, which is set to the constant string "Families". The
second forms the body, which is a list of Family objects in the Families
collection, showing each family's FamilyName property, as shown in
Figure 4-20.
Figure 4-20 isn't
master-detail, of course, since selecting a master family doesn't show its
associated details. To do that, we need to bind to the next level, as in
Example 4-50.
Example 4-50. Binding to detail Person data
<Grid DataContext="{StaticResource Families}">
...
<!-- Families Column -->
...
<!-- Members Column -->
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal">
<TextBlock TextContent="{Binding Path=FamilyName}" />
<TextBlock TextContent=" Family Members:" />
</StackPanel>
<ListBox Grid.Row="1" Grid.Column="1"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=Members}" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock TextContent="{Binding Path=Name}" />
<TextBlock TextContent=" (age: " />
<TextBlock TextContent="{Binding Path=Age}" />
<TextBlock TextContent=" )" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
In the Members column (column 1), we're also setting a
header and body, but this time the header is bound to the FamilyName of
the currently selected Family object.
Also, recall that in the Families column, our listbox's
items source was bound to the entire collection via a Binding statement
without a Path. In the details case, however, we want to tell the
data-binding engine that we'd like to bind to the Members property of
the currently selected Family object, which is itself a collection of Person
objects. Figure 4-21 shows
master-detail binding in action.
But, wait: there's more! Master-detail binding doesn't stop at
just two levels, oh no. You can go as deep as you like, with each detail
binding becoming the master binding for the next level. To see this in action,
let's add one more level of detail to our data classes, as in
Example 4-51.
Example 4-51. Adding a 3rd level of detail
public class Person {
string name;
public string Name {
get { return name; }
set { name = value; }
}
int age;
public int Age {
get { return age; }
set { age = value; }
}
Traits traits;
public Traits Traits {
get { return traits; }
set { traits = value; }
}
}
public class Traits : ObservableCollection<Trait> {}
public class Trait {
string description;
public string Description {
get { return description; }
set { description = value; }
}
}
Now, not only do families have family names and members which
consist of people with names and ages, but each person also has a set of
traits, each with their own description. Expanding our XAML a bit to include
traits would look like Example 4-52.
Example 4-52. Declaring a third level of detail
<local:Families x:Key="Families">
<local:Family FamilyName="Stooge">
<local:Family.Members>
<local:People>
<local:Person Name="Larry" Age="21">
<local:Person.Traits>
<local:Traits>
<local:Trait Description="In Charge" />
<local:Trait Description="Mean" />
<local:Trait Description="Ugly" />
</local:Traits>
</local:Person.Traits>
</local:Person>
<local:Person Name="Moe" Age="22" >...</local:Person>
...
</local:People>
</local:Famil.Members>
...
</local:Family>
...
</local:Families>
With a third level of detail, we bind as shown in
Example 4-53.
Example 4-53. Binding to a third level of detail data
<Grid DataContext="{StaticResource Families}">
...
<!-- Families Column -->
...
<!-- Members Column -->
...
<!-- Traits Column -->
<StackPanel Grid.Row="0" Grid.Column="2" Orientation="Horizontal">
<TextBlock TextContent="{Binding Path=Members/Name}" />
<TextBlock TextContent=" Traits:" />
</StackPanel>
<ListBox Grid.Row="1" Grid.Column="2"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=Members/Traits}" >
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock TextContent="{Binding Path=Description}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
In the case of the Families column header, recall that
we had no binding at all; the text was hardcoded:
<TextBlock ...>Families:</TextBlock>
In the case of the Members column header, we bound to
the FamilyName of the currently selected Family object like
so:
<TextBlock ... TextContent="{Binding Path=FamilyName}" />
Logically, you could think of this as expanding to the
following:
<TextBlock ... TextContent="{Binding Path=family.FamilyName}" />
where family is the currently selected Family object.
Taking this one level deeper, in the case of the traits column
header, we're binding to the Name property of the currently selected Person
from the Members property of the currently selected Family,
which binds like this:
<TextBlock ...
TextContent="{Binding Path=Members/Name}" />
Again, logically you could think of it expanding like this:
<TextBlock ...
TextContent="{Binding Path=family.Members.person.Name}" />
where family is the currently selected Family object
and person is the currently selected Person object. The / in
the binding statement acts as the separator between objects, with the object at
each level assumed to be "currently selected."
The binding for the listbox's items source works the same way,
except we want the TRaits collection from the currently selected Person,
not the Name. Our tri-level master-detail example looks like
Figure 4-22.
 |