Chapter 26. Data Entry, Data Views


For generations of programmers, one of the most common jobs has been the creation of programs that facilitate the entry, editing, and viewing of data. For such a program, generally you create a data-input form that has controls corresponding to the fields of the records in a database.

Let's assume you want to maintain a database of famous people (music composers, perhaps). For each person, you want to store the first name, middle name, last name, birth date, and date of death, which could be null if the person is still living. A good first step is to define a class containing public properties for all the items you want to maintain. As usual, these public properties provide a public interface to private fields.

It is very advantageous that such a class implement the INotifyPropertyChanged interface. Strictly speaking, the INotifyPropertyChanged interface requires only that the class have an event named PropertyChanged defined in accordance with the PropertyChangedEventHandler delegate. But such an event is worthless unless the properties of the class fire the event whenever the values of the properties change.

Here is such a class.

Person.cs

[View full width]

//--------------------------------------- // Person.cs (c) 2006 by Charles Petzold //--------------------------------------- using System; using System.ComponentModel; using System.Xml.Serialization; namespace Petzold.SingleRecordDataEntry { public class Person: INotifyPropertyChanged { // PropertyChanged event definition. public event PropertyChangedEventHandler PropertyChanged; // Private fields. string strFirstName = "<first name>"; string strMiddleName = "" ; string strLastName = "<last name>"; DateTime? dtBirthDate = new DateTime(1800, 1, 1); DateTime? dtDeathDate = new DateTime(1900, 12, 31); // Public properties. public string FirstName { set { strFirstName = value; OnPropertyChanged("FirstName"); } get { return strFirstName; } } public string MiddleName { set { strMiddleName = value; OnPropertyChanged("MiddleName"); } get { return strMiddleName; } } public string LastName { set { strLastName = value; OnPropertyChanged("LastName"); } get { return strLastName; } } [XmlElement(DataType="date")] public DateTime? BirthDate { set { dtBirthDate = value; OnPropertyChanged("BirthDate"); } get { return dtBirthDate; } } [XmlElement(DataType = "date")] public DateTime? DeathDate { set { dtDeathDate = value; OnPropertyChanged("DeathDate"); } get { return dtDeathDate; } } // Fire the PropertyChanged event. protected virtual void OnPropertyChanged (string strPropertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs (strPropertyName)); } } }



Implementing the INotifyPropertyChanged interface lets the properties of this class participate as sources in data bindings. They cannot be targets because they are not backed by dependency properties, but usually it's enough that they can be sources.

The OnPropertyChanged method is not required, but it provides a convenient place to fire the PropertyChanged event, and classes that implement the INotifyPropertyChanged interface usually include such a method. Some classes define the OnPropertyChanged method to have an argument of type PropertyChangedEventArgs, while other classes define it as I've done, which involves somewhat less code overall.

The next step is a data-input form. You could use a Window or a Page for this job, but the most flexible solution is a Panel of some sort. The following PersonPanel class uses a Grid to display three TextBox controls for the first, middle, and last names, and two DatePicker controls (from the CreateDatePicker project in the previous chapter) for the birth date and death date. Label controls identify each of the data-input controls.

PersonPanel.xaml

[View full width]

<!-- ============================================== PersonPanel.xaml (c) 2006 by Charles Petzold ============================================= = --> <Grid xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" xmlns:dp="clr-namespace:Petzold .CreateDatePicker" xmlns:src="/books/4/266/1/html/2/clr-namespace:Petzold .SingleRecordDataEntry" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.PersonPanel" > <Grid.Resources> <Style TargetType="{x:Type Label}"> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="Margin" Value="12" /> </Style> <Style TargetType="{x:Type TextBox}"> <Setter Property="Margin" Value="12" /> </Style> <Style TargetType="{x:Type dp:DatePicker}"> <Setter Property="Margin" Value="12" /> </Style> </Grid.Resources> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Label Grid.Row="0" Grid.Column="0" Content="_First Name: " /> <TextBox Grid.Row="0" Grid.Column="1" Margin="12" Text="{Binding Path=FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <Label Grid.Row="1" Grid.Column="0" Content="_Middle Name: " /> <TextBox Grid.Row="1" Grid.Column="1" Margin="12" Text="{Binding Path=MiddleName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <Label Grid.Row="2" Grid.Column="0" Content="_Last Name: " /> <TextBox Grid.Row="2" Grid.Column="1" Margin="12" Text="{Binding Path=LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <Label Grid.Row="3" Grid.Column="0" Content="_Birth Date: " /> <dp:DatePicker Grid.Row="3" Grid.Column="1" Margin="12" HorizontalAlignment="Center" Date="{Binding Path=BirthDate, Mode=TwoWay}" /> <Label Grid.Row="4" Grid.Column="0" Content="_Death Date: " /> <dp:DatePicker Grid.Row="4" Grid.Column="1" Margin="12" HorizontalAlignment="Center" Date="{Binding Path=DeathDate, Mode=TwoWay}" /> </Grid>



Notice that the three TextBox controls contain bindings to their Text properties, and the two DatePicker controls contain bindings to their Date properties. Each of these bindings has a Path property that indicates a particular property of the Person class, and each of the bindings has an explicit TwoWay mode setting so that any change to the control will be reflected in the source data. In addition, the TextBox bindings have the UpdateSourceTrigger property set to PropertyChanged so that the source data is updated whenever the Text property changes and not (as is the default behavior) when the TextBox loses input focus.

These bindings are incomplete because they are missing a data source. Generally this source is given as the Source or ElementName property of the binding, but another way to provide a binding source is through the DataContext property defined by FrameworkElement.

The characteristic of DataContext that makes it so valuable is that it is inherited through the visual tree. The implications are simple and profound: If you set the DataContext property of a PersonPanel to an object of type Person, the TextBox and DatePicker controls will display the properties of that Person object, and any user input to those controls will be reflected in the Person object.

This is how one binding between an object and a panel's DataContext property becomes multiple bindings between properties of that object and controls on the panel.

The PersonPanel.cs code-behind file is trivial.

PersonPanel.cs

//-------------------------------------------- // PersonPanel.cs (c) 2006 by Charles Petzold //-------------------------------------------- namespace Petzold.SingleRecordDataEntry {     public partial class PersonPanel     {         public PersonPanel()         {             InitializeComponent();         }     } } 



So that you can better grasp the mechanics of the binding between the Person object and the controls on the PersonPanel, the first project in this chapter loads and saves files containing only a single Person object. That's why the project is named SingleRecordDataEntry. Any database created from this program has only one record of type Person.

The next part of the SingleRecordDataEntry project is a Window containing the PersonPanel input panel. I've also included a menu with New, Open, and Save items.

SingleRecordDataEntry.xaml

[View full width]

<!-- === ===================================================== SingleRecordDataEntry.xaml (c) 2006 by Charles Petzold ============================================= =========== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:pnl="clr-namespace:Petzold .SingleRecordDataEntry" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.SingleRecordDataEntry" Title="Single-Record Data Entry" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize"> <DockPanel Name="dock"> <Menu DockPanel.Dock="Top"> <MenuItem Header="_File"> <MenuItem Header="_New" Command="New" /> <MenuItem Header="_Open..." Command="Open" /> <MenuItem Header="_Save..." Command="Save" /> </MenuItem> </Menu> <!-- PersonPanel for entering information. --> <pnl:PersonPanel x:Name="pnlPerson" /> </DockPanel> <Window.CommandBindings> <CommandBinding Command="New" Executed="NewOnExecuted" /> <CommandBinding Command="Open" Executed="OpenOnExecuted" /> <CommandBinding Command="Save" Executed="SaveOnExecuted" /> </Window.CommandBindings> </Window>



As you'll note, the three items on the File menu are associated with Executed handlers through command bindings. The PersonPanel object that occupies the interior of the DockPanel is given a name of pnlPerson.

The three Executed event handlers are located in the code-behind file that completes the SingleRecordDataEntry project.

SingleRecordDataEntry.cs

[View full width]

//--------- --------------------------------------------- // SingleRecordDataEntry.cs (c) 2006 by Charles Petzold //------------------------------------------------ ------ using Microsoft.Win32; using System; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Xml.Serialization; namespace Petzold.SingleRecordDataEntry { public partial class SingleRecordDataEntry : Window { const string strFilter = "Person XML files (*.PersonXml)|" + "*.PersonXml|All files (*.*)|*.*"; XmlSerializer xml = new XmlSerializer (typeof(Person)); [STAThread] public static void Main() { Application app = new Application(); app.Run(new SingleRecordDataEntry()); } public SingleRecordDataEntry() { InitializeComponent(); // Simulate File New command. ApplicationCommands.New.Execute(null, this); // Set focus to first TextBox in panel. pnlPerson.Children[1].Focus(); } // Event handlers for menu items. void NewOnExecuted(object sender, ExecutedRoutedEventArgs args) { pnlPerson.DataContext = new Person(); } void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args) { OpenFileDialog dlg = new OpenFileDialog(); dlg.Filter = strFilter; Person pers; if ((bool)dlg.ShowDialog(this)) { try { StreamReader reader = new StreamReader(dlg.FileName); pers = (Person) xml .Deserialize(reader); reader.Close(); } catch (Exception exc) { MessageBox.Show("Could not load file: " + exc.Message, Title, MessageBoxButton.OK, MessageBoxImage.Exclamation); return; } pnlPerson.DataContext = pers; } } void SaveOnExecuted(object sender, ExecutedRoutedEventArgs args) { SaveFileDialog dlg = new SaveFileDialog(); dlg.Filter = strFilter; if ((bool)dlg.ShowDialog(this)) { try { StreamWriter writer = new StreamWriter(dlg.FileName); xml.Serialize(writer, pnlPerson.DataContext); writer.Close(); } catch (Exception exc) { MessageBox.Show("Could not save file: " + exc.Message, Title, MessageBoxButton.OK, MessageBoxImage.Exclamation); return; } } } } }



The New command (which is also simulated in the constructor of the class) creates a new object of type Person and sets it to the DataContext property of the PersonPanel. When you start up the program, you'll see the default values defined in Person displayed in PersonPanel.

The Open and Save commands display OpenFileDialog and SaveFileDialog windows, respectively. These dialogs share the strFilter string defined at the top of the class and, by default, access files with the file name extension PersonXml. The OpenOnExecuted and SaveOnExecuted methods also make use of the same XmlSerializer object defined at the top of the class to store Person objects in XML format. The SaveOnExecuted method calls Serialize to convert the Person object currently stored as the DataContext property of the PersonPanel to an XML file. The OpenOnExecuted method loads a file saved earlier, calls Deserialize to convert it to a Person object, and then sets it to the DataContext property of the PersonPanel.

You can save multiple files with the SingleRecordDataEntry program, but each file contains only one Person object. A typical file looks like this:

<?xml version="1.0" encoding="utf-8"?> <Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xmlns:xsd="http://www.w3.org/2001/XMLSchema">   <FirstName>Johannes</FirstName>   <MiddleName />   <LastName>Brahms</LastName>   <BirthDate>1833-05-07</BirthDate>   <DeathDate>1897-04-03</DeathDate> </Person> 


The next obvious job requires maintaining a collection of Person objects. The .NET Framework contains several classes that store collections of objects. I tend to use the generic List class when I want something simple. But when working with databases, it's often advantageous to use the generic ObservableCollection class defined in the System.Collections.ObjectModel namespace. You can define a collection of Person objects like this:

ObservableCollection<Person> people = new ObservableCollection<Person>(); 


Or you can derive a class (named People, perhaps) from the ObservableCollection class:

public class People : ObservableCollection<Person> { } 


And then you can create an object of type People:

People people = new People(); 


One advantage of the latter approach is that you can put some methods in the People class that save People objects to files and later load them. (This is the approach I took in the upcoming MultiRecordDataEntry project.)

Because ObservableCollection is a generic class, you can add new objects of type Person to the collection without casting:

people.Add(new Person()); 


You can also index the collection and the result is an object of type Person:

Person person = people[5]; 


Beyond those two conveniences found in any generic collection class, ObservableCollection also defines two important events: PropertyChanged and CollectionChanged.

The PropertyChanged event is fired whenever a property of one of the members of the collection changes. Of course, this only works if the ObservableCollection object is based on a class that implements the INotifyPropertyChanged event and which fires its own PropertyChanged event when one of the properties changes. (Fortunately, the Person class qualifies.) Obviously, ObservableCollection attaches an event handler to the PropertyChanged event of each member of its collection and then fires its own PropertyChanged event in response.

The CollectionChanged event is fired whenever the collection as a whole changes, that is, whenever an item is added to the collection, removed from the collection, replaced, or moved within the collection.

The MultiRecordDataEntry project includes links to the files Person.cs, PersonPanel.xaml, and PersonPanel.cs from the SingleRecordDataEntry project, as well as the DatePicker files from the CreateDatePicker project in Chapter 25. The People.cs file is the first of three new files for this project. The file defines a People class that derives from ObservableCollection to maintain a collection of Person objects.

People.cs

[View full width]

//--------------------------------------- // People.cs (c) 2006 by Charles Petzold //--------------------------------------- using Microsoft.Win32; using Petzold.SingleRecordDataEntry; using System; using System.IO; using System.Collections.ObjectModel; using System.Windows; using System.Xml.Serialization; namespace Petzold.MultiRecordDataEntry { public class People : ObservableCollection<Person> { const string strFilter = "People XML files (*.PeopleXml)|" + "*.PeopleXml|All files (*.*)|*.*"; public static People Load(Window win) { OpenFileDialog dlg = new OpenFileDialog(); dlg.Filter = strFilter; People people = null; if ((bool)dlg.ShowDialog(win)) { try { StreamReader reader = new StreamReader(dlg.FileName); XmlSerializer xml = new XmlSerializer(typeof(People)); people = (People)xml .Deserialize(reader); reader.Close(); } catch (Exception exc) { MessageBox.Show("Could not load file: " + exc.Message, win.Title, MessageBoxButton.OK, MessageBoxImage.Exclamation); people = null; } } return people; } public bool Save(Window win) { SaveFileDialog dlg = new SaveFileDialog(); dlg.Filter = strFilter; if ((bool)dlg.ShowDialog(win)) { try { StreamWriter writer = new StreamWriter(dlg.FileName); XmlSerializer xml = new XmlSerializer(GetType()); xml.Serialize(writer, this); writer.Close(); } catch (Exception exc) { MessageBox.Show("Could not save file: " + exc.Message, win.Title, MessageBoxButton.OK, MessageBoxImage.Exclamation); return false; } } return true; } } }



I've also used this class for two methods named Load and Save that display the OpenFileDialog and SaveFileDialog windows and then use the XmlSerializer to load and save objects of type People. Notice that Load is a static method that returns an object of type People. Both Load and Save have arguments that you set to the parent Window object. The methods use this object to pass to the ShowDialog method and as a title in the two MessageBox.Show calls in case a problem is encountered with the file input or output. (Although putting the XmlSerializer code in the People class makes sense, normally I wouldn't also put the dialog boxes in there. There might be occasions when you'll want to load or save files without displaying dialog boxes. I've put the dialog boxes in the People class for convenience in simplifying the other programs in this chapter.)

The MultiRecordDataEntry program uses PersonPanel to display the information for one person, and yet it deals with a file containing multiple people. To accomplish this feat, the program must have some provision for navigating through the database. One approach is to keep track of a "current" record, and to have buttons for navigating to the next record and to the previous record, and to go to the first record and to the last record.

In the general case, you don't want to just view or edit the existing records of the file. You want to add new records and possibly delete records. In addition to the navigation buttons, you'll probably want to have buttons for Add New and Delete.

The following file, MultiRecordDataEntry.xaml, defines the layout of a window that includes a menu with the familiar New, Open, and Save commands. The remainder of the window has a PersonPanel and the six buttons I've described.

MultiRecordDataEntry.xaml

[View full width]

<!-- === ==================================================== MultiRecordDataEntry.xaml (c) 2006 by Charles Petzold ============================================= ========== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:pnl="clr-namespace:Petzold .SingleRecordDataEntry" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.MultiRecordDataEntry" Title="Multi-Record Data Entry" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize"> <DockPanel Name="dock"> <Menu DockPanel.Dock="Top"> <MenuItem Header="_File"> <MenuItem Header="_New" Command="New" /> <MenuItem Header="_Open..." Command="Open" /> <MenuItem Header="_Save..." Command="Save" /> </MenuItem> </Menu> <StackPanel> <!-- PersonPanel to enter data. --> <pnl:PersonPanel x:Name="pnlPerson" /> <!-- Buttons for navigation, adding, deleting. --> <UniformGrid Columns="6" HorizontalAlignment="Center"> <UniformGrid.Resources> <Style TargetType="{x:Type Button}"> <Setter Property="Margin" Value="6" /> </Style> </UniformGrid.Resources> <Button Name="btnFirst" Content="First" Click="FirstOnClick" /> <Button Name="btnPrev" Content="Previous" Click="PrevOnClick" /> <Button Name="btnNext" Content="Next" Click="NextOnClick" /> <Button Name="btnLast" Content="Last" Click="LastOnClick" /> <Button Name="btnAdd" Content="Add New" Click="AddOnClick" /> <Button Name="btnDel" Content="Delete" Click="DelOnClick" /> </UniformGrid> </StackPanel> </DockPanel> <Window.CommandBindings> <CommandBinding Command="New" Executed="NewOnExecuted" /> <CommandBinding Command="Open" Executed="OpenOnExecuted" /> <CommandBinding Command="Save" Executed="SaveOnExecuted" /> </Window.CommandBindings> </Window>



The following MultiRecordDataEntry.cs file completes the definition of the program's Window object with event handlers for the three menu items and the six buttons. This class maintains two fields: an object of type People, which is the file currently loaded, and an integer named index. This index variable is the index of the current record displayed in the PersonPanel. This value indexes the People object and is altered by the event handlers for the First, Previous, Next, and Last buttons.

With the Load and Save methods already defined in the People class, the three event handlers for the menu items are fairly simple. Both the New and Open items cause the creation of a new People object and a call to the InitializeNewPeopleObject method. This method sets the index variable to 0. If the People object has no records (which will be the case for the New command), this method creates a Person record and adds it to the collection. In either case, the DataContext of the PersonPanel is set to the first Person object in the People collection. A call to EnableAndDisableButtons enables the Previous button if the record being displayed is not the first record and enables the Next button if the record displayed is not the last record.

MultiRecordDataEntry.cs

[View full width]

//--------- -------------------------------------------- // MultiRecordDataEntry.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using Petzold.SingleRecordDataEntry; using System; using System.Collections.Specialized; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.MultiRecordDataEntry { public partial class MultiRecordDataEntry { People people; int index; [STAThread] public static void Main() { Application app = new Application(); app.Run(new MultiRecordDataEntry()); } public MultiRecordDataEntry() { InitializeComponent(); // Simulate File New command. ApplicationCommands.New.Execute(null, this); // Set focus to first TextBox in panel. pnlPerson.Children[1].Focus(); } // Event handlers for menu items. void NewOnExecuted(object sender, ExecutedRoutedEventArgs args) { people = new People(); InitializeNewPeopleObject(); } void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args) { people = People.Load(this); InitializeNewPeopleObject(); } void SaveOnExecuted(object sender, ExecutedRoutedEventArgs args) { people.Save(this); } void InitializeNewPeopleObject() { index = 0; if (people.Count == 0) people.Insert(0, new Person()); pnlPerson.DataContext = people[0]; EnableAndDisableButtons(); } // Event handlers for buttons. void FirstOnClick(object sender, RoutedEventArgs args) { pnlPerson.DataContext = people[index = 0]; EnableAndDisableButtons(); } void PrevOnClick(object sender, RoutedEventArgs args) { pnlPerson.DataContext = people[index -= 1]; EnableAndDisableButtons(); } void NextOnClick(object sender, RoutedEventArgs args) { pnlPerson.DataContext = people[index += 1]; EnableAndDisableButtons(); } void LastOnClick(object sender, RoutedEventArgs args) { pnlPerson.DataContext = people[index = people.Count - 1]; EnableAndDisableButtons(); } void AddOnClick(object sender, RoutedEventArgs args) { people.Insert(index = people.Count, new Person()); pnlPerson.DataContext = people[index]; EnableAndDisableButtons(); } void DelOnClick(object sender, RoutedEventArgs args) { people.RemoveAt(index); if (people.Count == 0) people.Insert(0, new Person()); if (index > people.Count - 1) index--; pnlPerson.DataContext = people[index]; EnableAndDisableButtons(); } void EnableAndDisableButtons() { btnPrev.IsEnabled = index != 0; btnNext.IsEnabled = index < people .Count - 1; pnlPerson.Children[1].Focus(); } } }



The event handlers for the six buttons are fairly straightforward. For the four navigation buttons, the event handler simply calculates a new value of index, uses that to index the people collection, and sets the DataContext property of the PersonPanel to that indexed Person object. The Add New button handler creates a new Person object and inserts it into the collection. The Delete button deletes the current record. If that results in an empty collection, a new Person object is added.

The MultiRecordDataEntry program saves and loads files that look like this:

<?xml version="1.0" encoding="utf-8"?> <ArrayOfPerson xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"                xmlns:xsd="http://www.w3.org/2001/XMLSchema">   <Person>     <FirstName>Franz</FirstName>     <MiddleName />     <LastName>Schubert</LastName>     <BirthDate>1797-01-07</BirthDate>     <DeathDate>1828-11-19</DeathDate>   </Person>   <Person>     <FirstName>Johannes</FirstName>     <MiddleName />     <LastName>Brahms</LastName>     <BirthDate>1833-05-07</BirthDate>     <DeathDate>1897-04-03</DeathDate>   </Person>   <Person>     <FirstName>John</FirstName>     <MiddleName />     <LastName>Adams</LastName>     <BirthDate>1947-02-15</BirthDate>     <DeathDate xsi:nil="true" />   </Person> </ArrayOfPerson> 


Notice that the last Person element in this sample file is a living composer, so the DeathDate is null. This is indicated in XML using the nil attribute, as documented in the XML Schema specifications available at http://www.w3.org/XML/Schema.

The logic connected with using the index variable to indicate the current record is referred to as a currency manager. The System.Windows.Data namespace contains a class that itself implements a currency manager, and some other important features as well. This class is CollectionView, and much of the remainder of this chapter will focus on CollectionView and its important derivative, ListCollectionView.

As the names suggest, a CollectionView or ListCollectionView object is actually a view of a collection, which might be only part of the total collection or might be the total collection sorted or arranged in different ways. Changing the view does not change the underlying collection.

You can create a CollectionView object based on a collection of some sort using the CollectionView constructor:

CollectionView collview = new CollectionView(coll); 


The coll argument must implement the IEnumerable interface. The ListCollectionView class has a similar constructor:

ListCollectionView lstcollview = new ListCollectionView(coll); 


However, the coll argument to the ListCollectionView constructor must implement the IList interface, which encompasses IEnumerable and ICollection (which defines a Count property), and also defines an indexer and Add and Remove methods. The static CollectionViewSource.GetDefaultView method returns a CollectionView or a ListCollectionView, depending on the interfaces implemented by its argument. (CollectionViewSource is also handy for defining a CollectionView in XAML.) A CollectionView object cannot sort or group its collection; a ListCollectionView object can.

If you want to base a CollectionView object on an ObservableCollection, you can explicitly create a ListCollectionView because ObservableCollection implements the IList interface.

The currency manager in CollectionView is exposed through several properties and methods. The Count, CurrentItem, and CurrentPosition properties are all read-only. You change the current position using the methods MoveCurrentTo, MoveCurrentToPosition, MoveCurrentToFirst, MoveCurrentToLast, MoveCurrentToPrevious, and MoveCurrentToNext. The class will allow you to move to a position one less than the index of the first item, and one greater than the index of the last item, and the IsCurrentBeforeFirst and IsCurrentAfterLast properties will tell you if that's the case. CollectionView also defines events named CollectionChanged, PropertyChanged, CurrentChanging, and CurrentChanged.

In conjunction with using a CollectionView class, let's also reconsider the navigation interface. The .NET Framework 2.0 included a new Windows Forms control named BindingNavigator that displayed several graphical buttons to navigate through a database, and to add and delete records. I have attempted to duplicate the appearance of the Windows Forms BindingNavigator control in the following NavigationBar control.

NavigationBar.xaml

[View full width]

<!-- ================================================ NavigationBar.xaml (c) 2006 by Charles Petzold ============================================= === --> <ToolBar xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x:> <Button Click="FirstOnClick" ToolTip="Move first"> <Image Source="DataContainer_MoveFirstHS. png" Stretch="None" /> </Button> <Button Name="btnPrev" Click="PreviousOnClick" ToolTip="Move previous"> <Image Source="DataContainer_MovePreviousHS.png" Stretch="None" /> </Button> <Separator /> <TextBox Name="txtboxCurrent" Width="48" ToolTip="Current position" GotKeyboardFocus="TextBoxOnGotFocus" LostKeyboardFocus="TextBoxOnLostFocus" KeyDown="TextBoxOnKeyDown" /> <TextBlock Text="of " VerticalAlignment="Center" /> <TextBlock Name="txtblkTotal" Text="0" VerticalAlignment="Center" ToolTip="Total number of items"/> <Separator /> <Button Name="btnNext" Click="NextOnClick" ToolTip="Move next"> <Image Source="DataContainer_MoveNextHS .png" Stretch="None" /> </Button> <Button Click="LastOnClick" ToolTip="Move last"> <Image Source="DataContainer_MoveLastHS .png" Stretch="None" /> </Button> <Separator /> <Button Click="AddOnClick" ToolTip="Add new"> <Image Source="DataContainer_NewRecordHS. png" Stretch="None" /> </Button> <Button Name="btnDel" Click="DeleteOnClick" ToolTip="Delete"> <Image Source="DeleteHS.png" Stretch="None" /> </Button> </ToolBar>



The PNG files were obtained from the image collection that comes with Visual Studio.

The code-behind file for the NavigationBar class defines two properties. The first property is named Collection of type IList. (It's anticipated that an ObservableCollection will be set to this property.) The set accessor uses the static CollectionViewSource.GetDefaultView method to create a CollectionView object, which will actually be a ListCollectionView object. The set accessor then sets event handlers to keep the navigation bar controls updated.

The second property is named ItemType, and it's the type of the item stored in the collection. The NavigationBar requires this type to implement the Add New button. Otherwise, most of the logic is similar to the navigation buttons shown earlier. In addition, the NavigationBar allows the user to type in a record number, and the three TextBox event handlers at the bottom of the file keep that number within proper bounds.

NavigationBar.cs

[View full width]

//---------------------------------------------- // NavigationBar.cs (c) 2006 by Charles Petzold //---------------------------------------------- using System; using System.Collections; // for IList. using System.Collections.Specialized; // for NotifyCollectionChangedEventArgs. using System.ComponentModel; // for ICollectionView. using System.Reflection; // for ConstructorInfo. using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; namespace Petzold.DataEntry { public partial class NavigationBar : ToolBar { IList coll; ICollectionView collview; Type typeItem; // Public constructor. public NavigationBar() { InitializeComponent(); } // Public properties. public IList Collection { set { coll = value; // Create CollectionView and set event handlers. collview = CollectionViewSource .GetDefaultView(coll); collview.CurrentChanged += CollectionViewOnCurrentChanged; collview.CollectionChanged += CollectionViewOnCollectionChanged; // Call an event handler to initialize TextBox and Buttons. CollectionViewOnCurrentChanged (null, null); // Initialize TextBlock. txtblkTotal.Text = coll.Count .ToString(); } get { return coll; } } // This is the type of the item in the collection. // It's used for the Add command. public Type ItemType { set { typeItem = value; } get { return typeItem; } } // Event handlers for CollectionView. void CollectionViewOnCollectionChanged (object sender, NotifyCollectionChangedEventArgs args) { txtblkTotal.Text = coll.Count.ToString(); } void CollectionViewOnCurrentChanged(object sender, EventArgs args) { txtboxCurrent.Text = (1 + collview .CurrentPosition).ToString(); btnPrev.IsEnabled = collview .CurrentPosition > 0; btnNext.IsEnabled = collview .CurrentPosition < coll.Count - 1; btnDel.IsEnabled = coll.Count > 1; } // Event handlers for buttons. void FirstOnClick(object sender, RoutedEventArgs args) { collview.MoveCurrentToFirst(); } void PreviousOnClick(object sender, RoutedEventArgs args) { collview.MoveCurrentToPrevious(); } void NextOnClick(object sender, RoutedEventArgs args) { collview.MoveCurrentToNext(); } void LastOnClick(object sender, RoutedEventArgs args) { collview.MoveCurrentToLast(); } void AddOnClick(object sender, RoutedEventArgs args) { ConstructorInfo info = typeItem.GetConstructor(System .Type.EmptyTypes); coll.Add(info.Invoke(null)); collview.MoveCurrentToLast(); } void DeleteOnClick(object sender, RoutedEventArgs args) { coll.RemoveAt(collview.CurrentPosition); } // Event handlers for txtboxCurrent TextBox. string strOriginal; void TextBoxOnGotFocus(object sender, KeyboardFocusChangedEventArgs args) { strOriginal = txtboxCurrent.Text; } void TextBoxOnLostFocus(object sender, KeyboardFocusChangedEventArgs args) { int current; if (Int32.TryParse(txtboxCurrent.Text, out current)) if (current > 0 && current <= coll .Count) collview.MoveCurrentToPosition (current - 1); else txtboxCurrent.Text = strOriginal; } void TextBoxOnKeyDown(object sender, KeyEventArgs args) { if (args.Key == Key.Escape) { txtboxCurrent.Text = strOriginal; args.Handled = true; } else if (args.Key == Key.Enter) { args.Handled = true; } else return; MoveFocus(new TraversalRequest (FocusNavigationDirection.Right)); } } }



The two files that comprise the NavigationBar class and the required image files are part of a library project named Petzold.DataEntry that creates a dynamic-link library named Petzold.DataEntry.dll. That project and a project named DataEntryWithNavigation are the two projects in the DataEntryWithNavigation solution.

The DataEntryWithNavigation project has links to the Person.cs file, the People.cs file, the two files that make up the PersonPanel class, and the two files that make up the DatePicker class. The project also contains a XAML file and a C# file for the program's window.

The XAML file defines a Window with a DockPanel. The familiar menu is docked at the top, a NavigationBar is docked at the bottom, and a PersonPanel fills the interior.

DataEntryWithNavigation.xaml

[View full width]

<!-- === ======================================================= DataEntryWithNavigation.xaml (c) 2006 by Charles Petzold ============================================= ============= --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:pnl="clr-namespace:Petzold .SingleRecordDataEntry" xmlns:nav="clr-namespace:Petzold .DataEntry;assembly=Petzold.DataEntry" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.DataEntryWithNavigation" Title="Data Entry with Navigation" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize"> <DockPanel Name="dock"> <Menu DockPanel.Dock="Top"> <MenuItem Header="_File"> <MenuItem Header="_New" Command="New" /> <MenuItem Header="_Open..." Command="Open" /> <MenuItem Header="_Save..." Command="Save" /> </MenuItem> </Menu> <!-- NavigationBar for navigating. --> <nav:NavigationBar Name="navbar" DockPanel .Dock="Bottom" /> <!-- PersonPanel for displaying and entering data. --> <pnl:PersonPanel x:Name="pnlPerson" /> </DockPanel> <Window.CommandBindings> <CommandBinding Command="New" Executed="NewOnExecuted" /> <CommandBinding Command="Open" Executed="OpenOnExecuted" /> <CommandBinding Command="Save" Executed="SaveOnExecuted" /> </Window.CommandBindings> </Window>



The code-behind file becomes fairly trivial. Like the original SingleRecordDataEntry project, it need only implement event handlers for the three items on its menu. For both New and Open, the event handler calls the InitializeNewPeopleObject method to link the People object with the PersonPanel and the NavigationBar.

DataEntryWithNavigation.cs

[View full width]

//--------- ----------------------------------------------- // DataEntryWithNavigation.cs (c) 2006 by Charles Petzold //------------------------------------------------ -------- using Petzold.DataEntry; using Petzold.MultiRecordDataEntry; using Petzold.SingleRecordDataEntry; using System; using System.Collections.Specialized; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.DataEntryWithNavigation { public partial class DataEntryWithNavigation { People people; [STAThread] public static void Main() { Application app = new Application(); app.Run(new DataEntryWithNavigation()); } public DataEntryWithNavigation() { InitializeComponent(); // Simulate File New command. ApplicationCommands.New.Execute(null, this); // Set focus to first TextBox in panel. pnlPerson.Children[1].Focus(); } // Event handlers for menu items. void NewOnExecuted(object sender, ExecutedRoutedEventArgs args) { people = new People(); people.Add(new Person()); InitializeNewPeopleObject(); } void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args) { people = People.Load(this); InitializeNewPeopleObject(); } void SaveOnExecuted(object sender, ExecutedRoutedEventArgs args) { people.Save(this); } void InitializeNewPeopleObject() { navbar.Collection = people; navbar.ItemType = typeof(Person); pnlPerson.DataContext = people; } } }



The InitializeNewPeopleObject method first assigns the Collection property of the NavigationBar to the newly created People object. This Collection property in NavigationBar creates a new object of type ListCollectionView and attaches event handlers to maintain the navigation bar. The second statement of InitializeNewPeopleObject assigns the ItemType property of NavigationBar so that NavigationBar can create new objects and add them to the collection when the user clicks the Add New button.

The last statement of InitializeNewPeopleObject assigns the DataContext of PersonPanel to the People object. This may seem strange. In previous programs, the DataContext property of PersonPanel has been assigned to a Person object so that the panel can display all the properties defined by the Person class. Assigning DataContext to a People object doesn't seem quite right, but it works. The People collection has been made the basis of a ListCollectionView, which is maintaining a current record, and that current record is what the PersonPanel actually displays.

Although the NavigationBar might be a good solution for some data-entry programs, it isn't quite adequate when you need a good view of the overall array of data in the database. You might wish you had a ListBox available to scroll through the database and select items to view or edit.

This approach is actually much easier than it sounds, and is demonstrated in the next project, which is called DataEntryWithListBox. The project requires the files contributing to the Person, People, PersonPanel, and DatePicker classes. The window is defined in the following XAML file and contains the familiar menu, a ListBox, and a PersonPanel.

DataEntryWithListBox.xaml

[View full width]

<!-- === ==================================================== DataEntryWithListBox.xaml (c) 2006 by Charles Petzold ============================================= ========== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:pnl="clr-namespace:Petzold .SingleRecordDataEntry" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.DataEntryWithListBox" Title="Data Entry With ListBox" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize"> <DockPanel Name="dock"> <Menu DockPanel.Dock="Top"> <MenuItem Header="_File"> <MenuItem Header="_New" Command="New" /> <MenuItem Header="_Open..." Command="Open" /> <MenuItem Header="_Save..." Command="Save" /> </MenuItem> </Menu> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <!-- ListBox to display and select items. --> <ListBox Name="lstbox" Grid.Column="0" Width="300" Height="300" Margin="24"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Path=FirstName}" /> <TextBlock Text=" " /> <TextBlock Text="{Binding Path=MiddleName}" /> <TextBlock Text=" " Name="txtblkSpace"/> <TextBlock Text="{Binding Path=LastName}" /> <TextBlock Text=" (" /> <TextBlock Text="{Binding Path=BirthDate.Year}" /> <TextBlock Text="-" /> <TextBlock Text="{Binding Path=DeathDate.Year}" Name="txtblkDeath" /> <TextBlock Text=")" /> </StackPanel> <DataTemplate.Triggers> <DataTrigger Binding="{Binding Path=DeathDate}" Value="{x :Null}"> <Setter TargetName="txtblkDeath" Property="Text" Value="present" /> </DataTrigger> <DataTrigger Binding="{Binding Path=MiddleName}" Value=""> <Setter TargetName="txtblkSpace" Property="Text" Value="" /> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <!-- PersonPanel to enter and edit item. --> <pnl:PersonPanel x:Name="pnlPerson" Grid.Column="1" /> <!-- Buttons to add and delete items. --> <StackPanel Orientation="Horizontal" Grid.Row="1" Grid.Column="1"> <Button Margin="12" Click="AddOnClick"> Add new item </Button> <Button Margin="12" Click="DeleteOnClick"> Delete item </Button> </StackPanel> </Grid> </DockPanel> <Window.CommandBindings> <CommandBinding Command="New" Executed="NewOnExecuted" /> <CommandBinding Command="Open" Executed="OpenOnExecuted" /> <CommandBinding Command="Save" Executed="SaveOnExecuted" /> </Window.CommandBindings> </Window>



The ListBox contains a definition of its ItemTemplate property under the assumption that the ListBox will be displaying items of type Person. A StackPanel with a horizontal orientation combines ten TextBlock elements that result in the display of items that look like this:

Franz Schubert (17971828)

The StackPanel is followed by a DataTemplate.Triggers section with two DataTrigger elements to fix two problems. First, if the DeathDate property is null, instead of displaying the Year property of the DeathDate I want to display the word "present" so that the entry looks like this:

John Adams (1947present)

Without that DataTrigger, nothing would be displayed after the dash, which is also an acceptable way of displaying the information. The second DataTrigger fixes a problem that results if the composer doesn't have a middle name. Normally one space separates the first name from the middle name and another space separates the middle name from the last name. If there's no middle name, two blanks separate the first name from the last name. The second DataTrigger eliminates one of those blanks.

The window layout concludes with a PersonPanel and two buttons labeled "Add new item" and "Delete item." The navigation buttons aren't required because the ListBox allows scrolling through the items and selecting an item for viewing or editing.

Here's the code-behind file.

DataEntryWithListBox.cs

[View full width]

//--------- -------------------------------------------- // DataEntryWithListBox.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using Petzold.MultiRecordDataEntry; using Petzold.SingleRecordDataEntry; using System; using System.Collections.Specialized; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; namespace Petzold.DataEntryWithListBox { public partial class DataEntryWithListBox { ListCollectionView collview; People people; [STAThread] public static void Main() { Application app = new Application(); app.Run(new DataEntryWithListBox()); } public DataEntryWithListBox() { InitializeComponent(); // Simulate File New command. ApplicationCommands.New.Execute(null, this); // Set focus to first TextBox in panel. pnlPerson.Children[1].Focus(); } void NewOnExecuted(object sender, ExecutedRoutedEventArgs args) { people = new People(); people.Add(new Person()); InitializeNewPeopleObject(); } void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args) { people = People.Load(this); if (people != null) InitializeNewPeopleObject(); } void SaveOnExecuted(object sender, ExecutedRoutedEventArgs args) { people.Save(this); } void InitializeNewPeopleObject() { // Create a ListCollectionView object based on the People object. collview = new ListCollectionView(people); // Define a property to sort the view. collview.SortDescriptions.Add( new SortDescription("LastName", ListSortDirection.Ascending)); // Link the ListBox and PersonPanel through the ListCollectionView. lstbox.ItemsSource = collview; pnlPerson.DataContext = collview; // Set the selected index of the ListBox. if (lstbox.Items.Count > 0) lstbox.SelectedIndex = 0; } void AddOnClick(object sender, RoutedEventArgs args) { Person person = new Person(); people.Add(person); lstbox.SelectedItem = person; pnlPerson.Children[1].Focus(); collview.Refresh(); } void DeleteOnClick(object sender, RoutedEventArgs args) { if (lstbox.SelectedItem != null) { people.Remove(lstbox.SelectedItem as Person); if (lstbox.Items.Count > 0) lstbox.SelectedIndex = 0; else AddOnClick(sender, args); } } } }



Both the New and Open commands on the File menu result in a call to InitializeNewPeopleObject. This method creates a ListCollectionView based on the People object.

As I emphasized earlier, a CollectionView is a view of a collection. The CollectionView defines a way of arranging the items of a collection for viewing, but this view doesn't change anything in the underlying collection. The CollectionView supports three general ways in which the view can be altered:

  • Sorting

  • Filtering

  • Grouping

You can sort the items based on one or more properties. Filtering is the restriction of the view to only those items that meet certain criteria. With grouping you arrange the items in groups, again based on certain criteria.

CollectionView supports three properties named CanSort, CanFilter, and CanGroup. The CanSort and CanGroup properties are false for a CollectionView object but true for a ListCollectionView.

To sort a ListCollectionView object, you create objects of type SortDescription and add those objects to the SortDescriptions property. Here's how the DataEntryWithListBox program sorts the ListCollectionView by the LastName property:

collview.SortDescriptions.Add(                 new SortDescription("LastName", ListSortDirection.Ascending)); 


The SortDescriptions collection can have multiple SortDescription objects. The second SortDescription is used if two properties encountered by the first SortDescription are equal. The SortDescriptions property defined by CollectionView is of type SortDescriptionCollection, and it has methods named SetItem, InsertItem, RemoveItem, and ClearItems for manipulating the collection.

After defining a SortDescription, the InitializeNewPeopleObject method sets the ItemsSource property of the ListBox to this ListCollectionView:

lstbox.ItemsSource = collview; 


The ListBox then displays all the items in the collection sorted by last name. That much should be obvious. But you also get a bonus. You are essentially linking the ListBox with the CollectionView so that the CurrentItem property of the CollectionView is set from the SelectedItem property of the ListBox. The ListBox becomes a means of navigating through the collection. This means that you set the DataContext property of the PersonPanel to this same CollectionView:

pnlPerson.DataContext = collview; 


And now the PersonPanel displays the item selected in the ListBox, and you can use the PersonPanel to edit the item. The PropertyChanged events defined by Person, ObservableCollection, and CollectionView help the ListView update itself with every change.

The remainder of the program shouldn't be difficult to understand. The event handler for the Add button adds a new Person object to the People class. That new Person object automatically shows up in the ListBox at the bottom of the list. The event handler continues by selecting that new object in the ListBox, which causes the properties of the item to be displayed in the PersonPanel.

As you enter information in PersonPaneland particularly as you enter the person's last namethe item will not automatically get a new position in the ListBox as a result of the sort description. To re-sort the contents of the CollectionView based on new or altered items, a program must call Refresh. The particular place where you call Refresh will depend on the architecture of your program. Some data-entry programs allow a new item to be filled in and the Add button then adds that completed item to the collection. If you have such an Add button, you can add the new item to the collection and call Refresh at that time.

In the DataEntryWithListBox program, there is no button to signal that an item has been completed. Instead, the program calls Refresh at the end of the AddOnClick method to re-sort the collection with any previously added items. This also causes the newly created Person item to be moved to the top of the list because the default string of "<last name>" precedes any alphabetic characters.

Of course, you can add an interface to the program to sort by different fields. This is one way of facilitating searches for particular items. Another approach is called filtering. You restrict the CollectionView to only those items that satisfy particular criteria.

To filter a CollectionView, you must have a method defined in accordance with the Predicate delegate:

bool MyFilter(object obj) {     ... } 


The argument to the method is an object in the collection. The method returns true if the item is to be included in the view, and false if not. You set the Filter property of the CollectionView to this method:

collview.Filter = MyFilter; 


If you look at the definition of the Predicate delegate, you'll discover that it's actually a generic delegate defined like this:

public delegate bool Predicate<T> (T obj) 


This fact might lead you to believe that you could define the filter method with an argument of any type. However, the definition of the Filter property in CollectionView calls for a method of type Predicate<Object>, so the filter method must have an argument of type object.

The CollectionViewWithFilter program has links to the Person.cs and People.cs files and defines a window with XAML and C# files. The window includes three radio buttons that let you select whether you want to view living composers, dead composers, or all composers. Here's the XAML layout of the radio buttons and ListBox.

CollectionViewWithFilter.xaml

[View full width]

<!-- === =========== ============================================= CollectionViewWithFilter.xaml (c) 2006 by Charles Petzold ============================================= ============== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.CollectionViewWithFilter" Title="CollectionView with Filter" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize"> <DockPanel Name="dock"> <Menu DockPanel.Dock="Top"> <MenuItem Header="_File"> <MenuItem Header="_Open..." Command="Open" /> </MenuItem> </Menu> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <!-- GroupBox with RadioButtons for filtering. --> <GroupBox Grid.Row="0" Margin="24" HorizontalAlignment="Center" Header="Criteria"> <StackPanel> <RadioButton Name="radioLiving" Content="Living" Checked="RadioOnChecked" Margin="6" /> <RadioButton Name="radioDead" Content="Dead" Checked="RadioOnChecked" Margin="6" /> <RadioButton Name="radioAll" Content="All" Checked="RadioOnChecked" Margin="6" /> </StackPanel> </GroupBox> <!-- ListBox to display items. --> <ListBox Name="lstbox" Grid.Row="1" HorizontalAlignment="Center" Width="300" Height="300" Margin="24"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Path=FirstName}" /> <TextBlock Text=" " /> <TextBlock Text="{Binding Path=MiddleName}" /> <TextBlock Text=" " Name="txtblkSpace"/> <TextBlock Text="{Binding Path=LastName}" /> <TextBlock Text=" (" /> <TextBlock Text="{Binding Path=BirthDate.Year}" /> <TextBlock Text="-" /> <TextBlock Text="{Binding Path=DeathDate.Year}" Name="txtblkDeath" /> <TextBlock Text=")" /> </StackPanel> <DataTemplate.Triggers> <DataTrigger Binding="{Binding Path=DeathDate}" Value="{x :Null}"> <Setter TargetName="txtblkDeath" Property="Text" Value="present" /> </DataTrigger> <DataTrigger Binding="{Binding Path=MiddleName}" Value=""> <Setter TargetName="txtblkSpace" Property="Text" Value="" /> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </DockPanel> <Window.CommandBindings> <CommandBinding Command="Open" Executed="OpenOnExecuted" /> </Window.CommandBindings> </Window>



To keep this program relatively simple and focused, I didn't include a PersonPanel or buttons to add or delete items from the collection. The program simply lets you view an existing file based on the filter settings. You shouldn't assume from this program that once filtering enters the picture, you've lost the ability to enter new data. That's not the case.

The Chapter 26 directory of the companion content for this book includes a file named Composers.PeopleXml with which you can experiment in this program and the next two programs of this chapter.

The three RadioButton controls share the same event handler, which is located in the code-behind file. Based on the Name property of the three buttons, the handler sets the Filter property of the CollectionView to the PersonIsLiving method, the PersonIsDead method, or to null, which allows the display of all items.

CollectionViewWithFilter.cs

[View full width]

//--------- ------------------------------------------------ // CollectionViewWithFilter.cs (c) 2006 by Charles Petzold //------------------------------------------------ --------- using Petzold.SingleRecordDataEntry; using Petzold.MultiRecordDataEntry; using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; namespace Petzold.CollectionViewWithFilter { public partial class CollectionViewWithFilter : Window { ListCollectionView collview; [STAThread] public static void Main() { Application app = new Application(); app.Run(new CollectionViewWithFilter()); } public CollectionViewWithFilter() { InitializeComponent(); } void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args) { People people = People.Load(this); if (people != null) { collview = new ListCollectionView (people); collview.SortDescriptions.Add( new SortDescription("LastName", ListSortDirection.Ascending)); lstbox.ItemsSource = collview; if (lstbox.Items.Count > 0) lstbox.SelectedIndex = 0; radioAll.IsChecked = true; } } // Event handlers for RadioButtons. void RadioOnChecked(object sender, RoutedEventArgs args) { if (collview == null) return; RadioButton radio = args.Source as RadioButton; switch (radio.Name) { case "radioLiving": collview.Filter = PersonIsLiving; break; case "radioDead": collview.Filter = PersonIsDead; break; case "radioAll": collview.Filter = null; break; } } bool PersonIsLiving(object obj) { return (obj as Person).DeathDate == null; } bool PersonIsDead(object obj) { return (obj as Person).DeathDate != null; } } }



At any time, there can be only one method assigned to the Filter property of the CollectionView. If you want to filter based on multiple criteria, the best approach is probably not to have a whole bunch of different filter methods, but to have just one filter method that you assign just once to the Filter property. This single method examines the object against all the criteria and returns a verdict. Whenever there's a change to the criteria, the program can call Refresh on the CollectionView to force all the items to pass through the filter method once again.

One common use of filtering is a TextBox that lets the user type a letter or multiple letters. The ListBox then displays all the items in which the last name begins with those letters. This feature is easier to implement than it first seems. Here's a XAML file that resembles CollectionViewWithFilter.xaml except it has a Label and TextBox instead of a GroupBox and radio buttons. The TextBox has a Name property of txtboxFilter and a TextChanged event handler.

FilterWithText.xaml

[View full width]

<!-- ================================================= FilterWithText.xaml (c) 2006 by Charles Petzold ============================================= ==== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.FilterWithText" Title="Filter with Text" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize"> <DockPanel Name="dock"> <Menu DockPanel.Dock="Top"> <MenuItem Header="_File"> <MenuItem Header="_Open..." Command="Open" /> </MenuItem> </Menu> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <!-- TextBox with Label. --> <StackPanel Grid.Row="0" Margin="24" Orientation="Horizontal" HorizontalAlignment="Center"> <Label Content="Search: " /> <TextBox Name="txtboxFilter" MinWidth="1in" TextChanged="TextBoxOnTextChanged" /> </StackPanel> <!-- ListBox for displaying items. --> <ListBox Name="lstbox" Grid.Row="1" HorizontalAlignment="Center" Width="300" Height="300" Margin="24"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Path=FirstName}" /> <TextBlock Text=" " /> <TextBlock Text="{Binding Path=MiddleName}" /> <TextBlock Text=" " Name="txtblkSpace"/> <TextBlock Text="{Binding Path=LastName}" /> <TextBlock Text=" (" /> <TextBlock Text="{Binding Path=BirthDate.Year}" /> <TextBlock Text="-" /> <TextBlock Text="{Binding Path=DeathDate.Year}" Name="txtblkDeath" /> <TextBlock Text=")" /> </StackPanel> <DataTemplate.Triggers> <DataTrigger Binding="{Binding Path=DeathDate}" Value="{x :Null}"> <Setter TargetName="txtblkDeath" Property="Text" Value="present" /> </DataTrigger> <DataTrigger Binding="{Binding Path=MiddleName}" Value=""> <Setter TargetName="txtblkSpace" Property="Text" Value="" /> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </DockPanel> <Window.CommandBindings> <CommandBinding Command="Open" Executed="OpenOnExecuted" /> </Window.CommandBindings> </Window>



The code-behind file for the FilterWithText class prepares the window for a new CollectionView object by setting the Text property in the TextBox to an empty string, and assigning the LastNameFilter method to the Filter property:

txtboxFilter.Text = ""; collview.Filter = LastNameFilter; 


The LastNameFilter method and the TextChanged event handler are both located toward the bottom of the code-behind file.

FilterWithText.cs

[View full width]

//----------------------------------------------- // FilterWithText.cs (c) 2006 by Charles Petzold //----------------------------------------------- using Petzold.SingleRecordDataEntry; using Petzold.MultiRecordDataEntry; using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; namespace Petzold.FilterWithText { public partial class FilterWithText : Window { ListCollectionView collview; [STAThread] public static void Main() { Application app = new Application(); app.Run(new FilterWithText()); } public FilterWithText() { InitializeComponent(); } void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args) { People people = People.Load(this); if (people != null) { collview = new ListCollectionView (people); collview.SortDescriptions.Add( new SortDescription("BirthDate", ListSortDirection.Ascending)); txtboxFilter.Text = ""; collview.Filter = LastNameFilter; lstbox.ItemsSource = collview; if (lstbox.Items.Count > 0) lstbox.SelectedIndex = 0; } } bool LastNameFilter(object obj) { return (obj as Person).LastName .StartsWith(txtboxFilter.Text, StringComparison.CurrentCultureIgnoreCase); } void TextBoxOnTextChanged(object sender, TextChangedEventArgs args) { if (collview == null) return; collview.Refresh(); } } }



The LastNameFilter method simply returns true if the LastName property of the Person object starts with the text in the TextBox, with case ignored. Any change in the TextBox causes a call to the Refresh method to re-evaluate all the items in the collection.

Besides sorting and filtering, a CollectionView can also group the items. The final program in this chapter will demonstrate this technique.

To group items, you supply a method that assigns a group name (actually an object) to each item in the collection. Within the ListBox, the items are grouped by this name, but you can also supply custom headers and panels to differentiate the groups.

CollectionView has a property named GroupDescriptions that is itself an ObservableCollection, and more specifically, of type ObservableCollection<GroupDescription>. GroupDescription is an abstract class with one derivative named PropertyGroupDescription, which you can use if you want to group items based on a property of the item. If this is not the case, you must define your own class that derives from GroupDescription. You are required to override the method named GroupNameFromItem.

I decided I wanted the composers in the sample file to be grouped by the musical period they are associated with. Generally and approximately, these periods are Baroque (1600-1750), Classical (1750-1820), Romantic (1820-1900), and Twentieth Century (after 1900), but I added other periods I called Pre-Baroque and Post-War. Because my file doesn't store information about when the composers were active, I based my criteria on their birth years.

Here's a class named PeriodGroupDescription that derives from GroupDescription and overrides the GroupNameFromItem method to return a text string for each item.

PeriodGroupDescription.cs

[View full width]

//--------- ---------------------------------------------- // PeriodGroupDescription.cs (c) 2006 by Charles Petzold //------------------------------------------------ ------- using Petzold.SingleRecordDataEntry; using System; using System.ComponentModel; using System.Globalization; namespace Petzold.ListBoxWithGroups { public class PeriodGroupDescription : GroupDescription { public override object GroupNameFromItem (object item, int level, CultureInfo culture) { Person person = item as Person; if (person.BirthDate == null) return "Unknown"; int year = ((DateTime)person .BirthDate).Year; if (year < 1575) return "Pre-Baroque"; if (year < 1725) return "Baroque"; if (year < 1795) return "Classical"; if (year < 1870) return "Romantic"; if (year < 1910) return "20th Century"; return "Post-War"; } } }



Let me show you the C# part of the window class first.

ListBoxWithGroups.cs

[View full width]

//-------------------------------------------------- // ListBoxWithGroups.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using Petzold.MultiRecordDataEntry; using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ListBoxWithGroups { public partial class ListBoxWithGroups : Window { ListCollectionView collview; [STAThread] public static void Main() { Application app = new Application(); app.Run(new ListBoxWithGroups()); } public ListBoxWithGroups() { InitializeComponent(); } void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args) { People people = People.Load(this); if (people != null) { collview = new ListCollectionView (people); collview.SortDescriptions.Add( new SortDescription("BirthDate", ListSortDirection.Ascending)); // Add PeriodGroupsDescription to GroupsDescriptions collection. collview.GroupDescriptions.Add(new PeriodGroupDescription()); lstbox.ItemsSource = collview; if (lstbox.Items.Count > 0) lstbox.SelectedIndex = 0; } } } }



The OpenOnExecuted handler loads an existing People file, as usual, and creates a ListCollectionView object from it. The view is sorted by birth date, and a new object of type PeriodGroupDescription is added to the GroupDescriptions collections to group the items by the criteria in that class.

The XAML file for the program's window has the same ListBox you're accustomed to seeing, but also includes a property element for ListBox.GroupStyle.

ListBoxWithGroups.xaml

[View full width]

<!-- === ================================================= ListBoxWithGroups.xaml (c) 2006 by Charles Petzold ============================================= ======= --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.ListBoxWithGroups" Title="ListBox with Groups" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize"> <DockPanel Name="dock"> <Menu DockPanel.Dock="Top"> <MenuItem Header="_File"> <MenuItem Header="_Open..." Command="Open" /> </MenuItem> </Menu> <!-- ListBox to display items. --> <ListBox Name="lstbox" Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" Width="300" Height="300" Margin="24"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Path=FirstName}" /> <TextBlock Text=" " /> <TextBlock Text="{Binding Path=MiddleName}" /> <TextBlock Text=" " Name="txtblkSpace"/> <TextBlock Text="{Binding Path=LastName}" /> <TextBlock Text=" (" /> <TextBlock Text="{Binding Path=BirthDate.Year}" /> <TextBlock Text="-" /> <TextBlock Text="{Binding Path=DeathDate.Year}" Name="txtblkDeath" /> <TextBlock Text=")" /> </StackPanel> <DataTemplate.Triggers> <DataTrigger Binding="{Binding Path=DeathDate.Year}" Value="{x:Null}"> <Setter TargetName="txtblkDeath" Property="Text" Value="present" /> </DataTrigger> <DataTrigger Binding="{Binding Path=MiddleName}" Value=""> <Setter TargetName="txtblkSpace" Property="Text" Value="" /> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> </ListBox.ItemTemplate> <!-- GroupStyle defines header for each group. --> <ListBox.GroupStyle> <GroupStyle> <GroupStyle.HeaderTemplate> <DataTemplate> <TextBlock Text="{Binding Path=Name}" Foreground="White" Background="DarkGray" FontWeight="Bold" FontSize="12pt" Margin="6" /> </DataTemplate> </GroupStyle.HeaderTemplate> </GroupStyle> </ListBox.GroupStyle> </ListBox> </DockPanel> <Window.CommandBindings> <CommandBinding Command="Open" Executed="OpenOnExecuted" /> </Window.CommandBindings> </Window>



The GroupStyle property of ListBox is a collection of GroupStyle objects. If you have more than one GroupStyle object in the collection, you must also set the GroupStyleSelector property of ListBox to a method defined in accordance with the GroupStyleSelector delegate. This method gets a group name as an argument and returns a GroupStyle for that name.

If you have only one GroupStyle, it applies to all the groups displayed by the ListBox. Each group can have its own Style (which you set with the ContainerStyle property), its own type of panel for displaying the items in the group (which you set with the Panel property), and its own HeaderTemplate. This latter property is the only one I took advantage of. I used it to define a TextBlock that displays white text on a dark gray background. The Text itself is a binding to a property named Name, which is actually the group name. Thus, each group is preceded by a non-selectable header that identifies the period with which those composers are associated.

The grouping feature implemented by ItemsControl is very powerful and adds a whole other dimension to the data displayed in a ListBox. Not only can each item be displayed in a unique manner, but any consolidation of items can also have a unique identity within the control. It's one of those features that may not have an immediate application in your work. But on that day when a problem comes up that calls for ListBox groupings, you'll suddenly grow quite fond of this spectacular implementation.




Applications = Code + Markup. A Guide to the Microsoft Windows Presentation Foundation
Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation (Pro - Developer)
ISBN: 0735619573
EAN: 2147483647
Year: 2006
Pages: 72

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net