Using a ListBox to display Color objects or FontFamily objects is fine for some special applications, but what are you going to put in your items control?
In general, you’ll be filling an ItemsControl or ListBox with those vague but ubiquitous entities known as business objects.
For example, if you’re creating an application that lets a user pick a hotel, it’s likely you’ll have a class named Hotel, and objects of Hotel will go into your ListBox. As a business object, Hotel is not going to derive from FrameworkElement. But it is very likely that Hotel will implement INotifyPropertyChanged so it can dynamically indicate a room rate that’s just been reduced. Another business object will maintain a collection of Hotel objects, probably using ObservableCollection and implementing INotifyCollectionChanged to dynamically indicate changes when a new hotel opens its doors.
The http://www.charlespetzold.com/Students
directory of my Web site contains a file named students.xml that
contains data on 69 students. The directory also contains lovely
black-and-white photographs of all these students. These photographs are
from high school yearbooks from El Paso, Texas for the years 1912
through 1914. The yearbooks are in the public domain and were graciously
digitized by the El Paso Public Library and available on their Web site at http://www.elpasotexas.gov/library/ourlibraries/main_library/yearbooks/yearbooks.asp.
Here’s the Student class. It implements INotifyPropertyChanged
and has several properties pertaining to the student, including name,
sex, a filename referencing the photograph, and a grade point average:
Example 1. Silverlight Project: ElPasoHighSchool File: Student.cs
using System; using System.ComponentModel; namespace ElPasoHighSchool { public class Student : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged;
string fullName; string firstName; string middleName; string lastName; string sex; string photoFilename; decimal gradePointAverage;
public string FullName { set { if (fullName != value) { fullName = value; OnPropertyChanged("FullName"); } } get { return fullName; } }
public string FirstName { set { if (firstName != value) { firstName = value; OnPropertyChanged("FirstName"); } } get { return firstName; } }
public string MiddleName { set { if (middleName != value) { middleName = value; OnPropertyChanged("MiddleName"); } } get { return middleName; } }
public string LastName { set { if (lastName != value) { lastName = value; OnPropertyChanged("LastName"); } } get { return lastName; } }
public string Sex { set { if (sex != value) { sex = value; OnPropertyChanged("Sex"); } } get { return sex; } }
public string PhotoFilename { set { if (photoFilename != value) { photoFilename = value; OnPropertyChanged("PhotoFilename"); } } get { return photoFilename; } }
public decimal GradePointAverage { set { if (gradePointAverage != value) { gradePointAverage = value; OnPropertyChanged("GradePointAverage"); } } get { return gradePointAverage; } }
protected virtual void OnPropertyChanged(string propChanged) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propChanged)); } } }
|
There will be one instance of the Student class for each student. Changes to any of these properties cause a PropertyChanged event to fire. Thus, this class is suitable as a source for data bindings.
The StudentBody class also implements INotifyPropertyChanged:
Example 2. Silverlight Project: ElPasoHighSchool File: StudentBody.cs
using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Xml.Serialization;
namespace ElPasoHighSchool { public class StudentBody : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; string school; ObservableCollection<Student> students = new ObservableCollection<Student>();
public string School { set { if (school != value) { school = value; OnPropertyChanged("School"); } } get { return school; } }
public ObservableCollection<Student> Students { set { if (students != value) { students = value; OnPropertyChanged("Students"); } } get { return students; } }
protected virtual void OnPropertyChanged(string propChanged) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propChanged)); } } }
|
This class contains a property indicating the name of the school and an ObservableCollection of type Student to store all the Student objects. ObservableCollection is a very popular collection class in Silverlight because it implements the INotifyCollectionChanged interface, which means that it fires a CollectionChanged event whenever an item is added to or removed from the collection.
Before continuing, let’s take a look at an excerpt of the student.xml file, which resides on my Web site:
<?xml version="1.0" encoding="utf-8"?> <StudentBody xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <School>El Paso High School</School> <Students> <Student> <FullName>Adkins Bowden</FullName> <FirstName>Adkins</FirstName> <MiddleName /> <LastName>Bowden</LastName> <Sex>Male</Sex> <PhotoFilename> http://www.charlespetzold.com/Students/AdkinsBowden.png </PhotoFilename> <GradePointAverage>2.71</GradePointAverage> </Student> <Student> <FullName>Alfred Black</FullName> <FirstName>Alfred</FirstName> <MiddleName /> <LastName>Black</LastName> <Sex>Male</Sex> <PhotoFilename> http://www.charlespetzold.com/Students/AlfredBlack.png </PhotoFilename> <GradePointAverage>2.87</GradePointAverage> </Student> . . .
<Student> <FullName>William Sheley Warnock</FullName> <FirstName>William</FirstName> <MiddleName>Sheley</MiddleName> <LastName>Warnock</LastName> <Sex>Male</Sex> <PhotoFilename> http://www.charlespetzold.com/Students/WilliamSheleyWarnock.png </PhotoFilename> <GradePointAverage>1.82</GradePointAverage> </Student> </Students> </StudentBody>
|
As you can see, the element tags correspond to properties in the Student and StudentBody classes. I created this file using XML serialization with the XmlSerializer class, and XML deserialization can convert it back into Student and StudentBody objects. That is the function of the StudentBodyPresenter class, which again implements INotifyPropertyChanged:
Example 4. Silverlight Project: ElPasoHighSchool File: StudentBodyPresenter.cs
using System; using System.ComponentModel; using System.IO; using System.Net; using System.Windows.Threading; using System.Xml.Serialization;
namespace ElPasoHighSchool { public class StudentBodyPresenter : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged;
StudentBody studentBody; Random rand = new Random();
public StudentBodyPresenter() { Uri uri = new Uri("http://www.charlespetzold.com/Students/students.xml");
WebClient webClient = new WebClient(); webClient.DownloadStringCompleted += OnDownloadStringCompleted; webClient.DownloadStringAsync(uri); }
void OnDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs args) { StringReader reader = new StringReader(args.Result); XmlSerializer xml = new XmlSerializer(typeof(StudentBody)); StudentBody = xml.Deserialize(reader) as StudentBody;
DispatcherTimer tmr = new DispatcherTimer(); tmr.Tick += TimerOnTick; tmr.Interval = TimeSpan.FromMilliseconds(100); tmr.Start(); }
public StudentBody StudentBody { protected set { if (studentBody != value) { studentBody = value; OnPropertyChanged("StudentBody"); } } get { return studentBody; } }
protected virtual void OnPropertyChanged(string propChanged) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propChanged)); }
void TimerOnTick(object sender, EventArgs args) { int index = rand.Next(studentBody.Students.Count); Student student = studentBody.Students[index];
double factor = 1 + (rand.NextDouble() - 0.5) / 5;
student.GradePointAverage = Math.Max(0, Math.Min(5, Decimal.Round((decimal)factor * student.GradePointAverage, 2))); } } }
|
The constructor of the StudentBodyPresenter class uses WebClient to access the students.xml file. As you’ll recall, WebClient performs asynchronous web accesses, so it needs a callback to signal the program when it’s completed. The Deserialize method of the XmlSerializer class then converts the XML text file into an actual StudentBody object, which is available as a public property to this class. When the OnDownloadStringCompleted callback sets that StudentBody property, the class fires its first and only PropertyChanged event.
The OnDownloadStringCompleted callback also starts up a DispatcherTimer that simulates changes to the data. Ten times a second, the GradePointAverage property of one of the students changes, causing the particular Student class to fire a PropertyChanged event. We hope very much to witness those dynamic changes on screen.
You can begin experimenting with this database by opening up a new Silverlight project, making a reference to the ElPasoHighSchool.dll library, and putting an XML namespace declaration in the MainPage.xaml file:
xmlns:elpaso="clr-namespace:ElPasoHighSchool;assembly=ElPasoHighSchool"
You then instantiate this the StudentBodyPresenter class in the Resources collection:
<phone:PhoneApplicationPage.Resources>
<elpaso:StudentBodyPresenter x:Key="studentBodyPresenter" />
</phone:PhoneApplicationPage.Resources>
You can then put a TextBlock in the content area with a binding to that resource:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Source={StaticResource studentBodyPresenter},
Path=StudentBody.School}" />
</Grid>
The screen indicates that the program is successfully downloading and deserializing the students.xml file:
Changing the binding path from StudentBody.School to StudentBody.Students indicates an ObservableCollection:
It’s possible to access the Count property of the ObservableCollection:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Source={StaticResource studentBodyPresenter},
Path=StudentBody.Students.Count}" />
</Grid>
And the Students collection can be indexed:
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Source={StaticResource studentBodyPresenter},
Path=StudentBody.Students[23]}" />
This shows us that the Students collection contains objects of type Student:
To avoid making this binding even longer, let’s split it up by setting a DataContext on the content Grid. The DataContext is inherited through the visual tree and simplifies the binding on the TextBlock:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"
DataContext="{Binding Source={StaticResource studentBodyPresenter},
Path=StudentBody}">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Path=Students[23].FullName}" />
</Grid>
That binding references a particular student’s name:
The binding can be simplified even more by eliminating the “Path=” part:
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Students[23].FullName}" />
Now let’s replace the TextBlock with an Image element referencing the PhotoFilename property of the Student class:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"
DataContext="{Binding Source={StaticResource studentBodyPresenter},
Path=StudentBody}">
<Image HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="None"
Source="{Binding Students[23].PhotoFilename}" />
</Grid>
And we get the photo successfully downloaded and displayed:
Now it’s time to stop fooling around and put an actual ListBox in there:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"
DataContext="{Binding Source={StaticResource studentBodyPresenter},
Path=StudentBody}">
<ListBox ItemsSource="{Binding Students}" />
</Grid>
The Students property is of type ObservableCollection, which of course implements IEnumerable, which is all that ListBox really requires for its ItemsSource. But ListBox also determines if the object bound to ItemsSource can do a little more, for example, if it implements INotifyCollectionChanged, which ObservableCollection does. So if somehow a new Student were added to the collection, or other students were removed from the collection as they graduated, the ListBox would know about that and change the items it was displaying.
At the moment, the ListBox doesn’t seem to be overjoyed with this data:
Whenever you see a ListBox or ItemsControl with a bunch of identical class names listed, don’t despair. You should instead rejoice! Such a display shows that the ListBox has been successfully filled with items of the same type, and all it needs to display something meaningful is a DataTemplate or (if we’re lazy) a DisplayMemberPath setting;
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"
DataContext="{Binding Source={StaticResource studentBodyPresenter},
Path=StudentBody}">
<ListBox ItemsSource="{Binding Students}"
DisplayMemberPath="FullName" />
</Grid>
Here it is:
Let’s leave the ListBox like that for now, and instead focus on displaying the selected item from the ListBox.
By adding another row to the Grid, we can put a TextBlock down at the bottom of the display:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"
DataContext="{Binding Source={StaticResource studentBodyPresenter},
Path=StudentBody}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
Name="listBox"
ItemsSource="{Binding Students}"
DisplayMemberPath="FullName" />
<TextBlock Grid.Row="1"
FontSize="{StaticResource PhoneFontSizeLarge}"
HorizontalAlignment="Center"
Text="{Binding ElementName=listBox,
Path=SelectedItem.FullName}" />
</Grid>
Notice the binding on the TextBlock. The SelectedItem property of the ListBox is of type Student, so the binding path can reference a property of Student, such as FullName. Now when an item is selected from the ListBox, the TextBlock displays the item’s FullName property:
Or, replace the TextBlock with an Image element:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"
DataContext="{Binding Source={StaticResource studentBodyPresenter},
Path=StudentBody}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
Name="listBox"
ItemsSource="{Binding Students}"
DisplayMemberPath="FullName" />
<Image Grid.Row="1"
HorizontalAlignment="Center"
Stretch="None"
Source="{Binding ElementName=listBox,
Path=SelectedItem.PhotoFilename}" />
</Grid>
You can now go through the ListBox and select an item to view that student’s picture:
To view multiple properties of the selected item, you might want to put another DataContext definition on a Border:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"
DataContext="{Binding Source={StaticResource studentBodyPresenter},
Path=StudentBody}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
Name="listBox"
ItemsSource="{Binding Students}"
DisplayMemberPath="FullName" />
<Border Grid.Row="1"
BorderBrush="{StaticResource PhoneForegroundBrush}"
BorderThickness="{StaticResource PhoneBorderThickness}"
HorizontalAlignment="Center"
DataContext="{Binding ElementName=listBox,
Path=SelectedItem}">
</Border>
</Grid>
Within this Border can go a panel and elements with bindings that reference properties of the Student class. This is what I’ve done in the StudentBodyListBox program. The XAML file contains an XML namespace declaration for the ElPasoHighSchool library:
xmlns:elpaso="clr-namespace:ElPasoHighSchool;assembly=ElPasoHighSchool"
The Resources collection instantiates the StudentBodyPresenter class:
Example 5. Silverlight Project: StudentBodyListBox File: MainPage.xaml (excerpt)
<phone:PhoneApplicationPage.Resources> <elpaso:StudentBodyPresenter x:Key="studentBodyPresenter" /> </phone:PhoneApplicationPage.Resources>
|
Here’s the content area:
Example 6. Silverlight Project: StudentBodyListBox File: MainPage.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" DataContext="{Binding Source={StaticResource studentBodyPresenter}, Path=StudentBody}"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding School}" FontSize="{StaticResource PhoneFontSizeLarge}" HorizontalAlignment="Center" TextDecorations="Underline" />
<ListBox Grid.Row="1" Name="listBox" ItemsSource="{Binding Students}" DisplayMemberPath="FullName" />
<Border Grid.Row="2" BorderBrush="{StaticResource PhoneForegroundBrush}" BorderThickness="{StaticResource PhoneBorderThickness}" HorizontalAlignment="Center" DataContext="{Binding ElementName=listBox, Path=SelectedItem}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding FullName}" TextAlignment="Center" />
<Image Grid.Row="1" Width="225" Height="300" Margin="24 6" Source="{Binding PhotoFilename}" />
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center"> <TextBlock Text="GPA=" /> <TextBlock Text="{Binding GradePointAverage}" /> </StackPanel> </Grid>
</Border> </Grid>
|
Within the Border is a Grid with three rows, containing a TextBlock with a binding to the FullName property, an Image element, and a StackPanel to display the grade point average. Notice I’ve given the Image element a specific size based on my knowledge of the size of the images. This avoids a change in size of the Image element after it’s able to download the photo.
You can now scroll through the ListBox and look at each student in detail
Wait a little while and you should be able to see a change in the grade point average. That’s the beauty of INotifyPropertyChanged and dependency properties at work.