[Please note that comments and discussion for published tutorials are at http://i2w.CodePlex.com ]
In the previous tutorial we looked at binding data to fields on a Windows Phone 7 page. In this tutorial we take the next step and create a list on a first page and then display the details on a second page. This turns out to be shockingly easy to do, but along the way we’ll uncover some other useful controls and a few subtle features.
To get started open Visual Studio 2010 and click on New Project. In the New Project dialog, click on the Silverlight for Windows Phone template and then on Windows Phone List Application. I called my example i2WPCustomerList
Before you do anything else, run the application. You’ll find that a list appears. You can move the list up and down with the mouse (try “flicking” up and down as well!). When you click on an entry you are taken to a details page for that entry.
Before we modify this application to use our customers, let’s take a look at how it is accomplishing all of this.
Dissecting The List Application
Stop debugging (don’t shut the emulator) and open MainPage.xaml. Let’s dissect this line by line, area by area.
The PhoneApplicationPage Element
At the top, within the opening PhoneApplicationPage element, are a number of name space declarations
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
The effect of each of these is to name an alias to a namespace. That is, for example, the second line creates the alias “x” for the namespace found in http://schemas.microsoft.com/winfx/2006/xaml. Having declared this, you can then use that alias to find the element Name defined in that name space by writing
x:Name
which tells the compiler where Name is defined.
Immediately after the name space declarations, the design time DataContext is defined, pointing to the SampleData folder in which resides the file MainViewModelSampleData.xaml. You can see that file in the Solution Explorer window under the folder Sample Data, and you can see it on your disk as well by navigating to the directory (folder) that holds the new application, then to the Sample Data directory, in which is that very file.
The DataContext assignment is followed by three resource assignments. We’ll take a long look at Static Resources in an upcoming tutorial, but the upshot of these three lines is to define the FontFamily, FontSize and Foreground color by using three predefined values. (This makes it very easy for your application to look and feel like a Windows Phone 7 application.)
The next statement tells the application which orientations (vertical, horizontal or both) you intend to support in this application.
The final two lines of this opening element set the width and height of the design surface and indicate that this application will be visible in the System Tray.
The Outer and Inner Grids
Within the PhoneApplicationPage is a grid, LayoutRoot which has two rows. The first row holds a StackPanel with the ApplicationTitle and ListTitle TextBlocks.
The second row of the grid holds an innerGrid named ContentPanel, which in turn holds a ListBox.
The ListBox
The Listbox has two interesting features. First, its ItemSource is bound to a property named Items. The ItemsSource is an ObservableCollection that is used to populate the ListBox. An ObservableCollection is a list that implements INotifyCollectionChanged, and so, as items are added or removed or modified, the UI is notified through that interface.
<ListBox x:Name="MainListBox" ItemsSource="{Binding Items}" SelectionChanged="MainListBox_SelectionChanged">
At the very end of the ListBox element is the assignment of a method name to the SelectionChanged event. There are two ways to hook up events for elements defined in the Xaml. One is to define the event handler in the Xaml itself (as shown here) and the other is to define the event handler in the code-behind file (as shown previously).
The ItemTemplate
The second interesting aspect of the ListBox is that how it displays its data is defined in the ListBox.ItemTemplate, which in turn holds a DataTemplate, which, in its turn, holds the elements that will be repeated for each item in the list,
<ListBox.ItemTemplate> <DataTemplate> </DataTemplate> </ListBox.ItemTemplate>
In this case the DataTemplate holds a StackPanel whose orientation is Horizontal. The first item in the StackPanel is an image, the second item in the StackPanel is an inner StackPanel, which in turn holds two TextBlocks. Since the orientation for the inner StackPanel is not specified, the default (vertical) is used. You can see the effect in the running application (the image is the arrow head in a circle), where this specification repeats for each item (I’ve added a line between the first two items).
When The Program Starts
Out of the box, when the program starts up, the code in App.xaml.cs will run. We won’t delve deep into this file for now, but the net effect is that the code in the constructor of MainView.xaml.cs will be invoked, which will initialize the visible components and when the page is displayed the method OnNavigatedTo will be invoked,
protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); if (DataContext == null) DataContext = App.ViewModel; }
Note that the DataContext is set to the ViewModel static value in the App class, which itself is initialized to MainView. This delays the creation of the view until it is needed. The MainView itself is composed of the databound list we looked at above, and so the sample data is displayed.
Handling the SelectionChangedEvent
When the user clicks on an element in the list, the SelectionChanged Event is fired, and the SelectionChangedEvent handler method in MainPage.xaml.cs is called,
private void MainListBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (MainListBox.SelectedIndex == -1) return; NavigationService.Navigate(new Uri("/DetailsPage.xaml?selectedItem=" + MainListBox.SelectedIndex, UriKind.Relative)); MainListBox.SelectedIndex = -1; }
The first line checks for an invalid index, and the third line resets the index (using –1 and not 0 as 0 is the offset to the first item in the list). The middle line is where the action is; here we call the NavigationService, which is the approach of choice for navigating from one page to another. The static Navigate method takes a URI which we create on the fly based on the user’s choice.
Control of the application then switches to the DetailsPage and the constructor and OnNavigatedTo methods are invoked there,
public DetailsPage() { InitializeComponent(); } / When page is navigated to, set data context to selected item in list protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); string selectedIndex; if (NavigationContext.QueryString.TryGetValue( "selectedItem", out selectedIndex)) { int index = int.Parse(selectedIndex); DataContext = App.ViewModel.Items[index]; } }
Taking this apart, the Navigation system provides a NavigationContext, which provides a query string. We call TryGetValue, passing which value we want (selectedItem) and a string for the value. We then turn that string into an integer, and ask the ViewModel Items property for the object at that index, and assign that as the DataContext.
Modifying the Application For Customers
With the out of the box list application reasonably understood, you can see that adding our Customer data will not be terribly difficult. Start by adding a folder named Model and into that folder place the customer.cs file from the previous tutorial (reproduced here in full for your convenience),
public class CustomerCollection { private readonly List<Customer> _customers = new List<Customer>(); public List<Customer> Customers { get { return _customers; } } public CustomerCollection() { GenerateCustomers(500); } public void GenerateCustomers(int howMany) { var firsts = new List<String> { "Abe", "Alice", "Barry", "Basha", "Charlie", "Colette", "David", "Davida", "Edgar", "Elizabeth", "Frank", "Fran", "George", "Gary", "Harry", "Isaac", "Jesse", "Jessica", "Kevin", "Katrina", "Larry", "Linda", "Mark", "Melinda", "Nick", "Nancy", "Oscar", "Ophilia", "Peter", "Patricia", "Quince", "Quintina", "Robert", "Roberta", "Shy", "Sarah", "Tom", "Teri", "Uberto", "Uma", "Victor", "Victoria", "Walter", "Wendy", "Xerxes", "Xena", "Yaakov", "Yakira", "Zach", "Zahara" }; var lasts = new List<String> { "Anderson", "Baker", "Connors", "Davidson", "Edgecumbe", "Franklin", "Gregory", "Hines", "Isaacson", "Johnson", "Kennedy", "Liberty", "Mann", "Nickerson", "O'Dwyer", "Patterson", "Quimby", "Richardson", "Stevenson", "Tino", "Usher", "Van Dam", "Walker", "Xenason", "Yager", "Zachery" }; var streets = new List<String> { "Ash", "Beech", "Cedar", "Dogwood", "Elm", "Filbert", "Ginkgo", "Hawtorn", "Ironwood", "Juniper", "Katsura", "Lilac", "Magnolia", "Nectarine", "Oak", "Palm", "Quince", "Redwood", "Sassafrass", "Tupelo", "Vibrunum", "Walnut", "Yellowwood", "Zelkova" }; var cities = new List<String> { "Acton", "Boston", "Canton", "Dell", "Everstone", "Flintwood", "Gary", "Houston", "Imperial", "Jackson", "Kalamazoo", "Levinworth", "Macon", "New York", "Oak Hill", "Paducah", "Quinzy", "Rochester", "South Falls", "Terra Haute", "Union", "Victoria", "Waipio", "Xenia", "York", "Zanesville" }; var isp = new List<String> { "ATT", "Verizon", "Hotmail", "Gmail", "Sprintnet", "Yahoo" }; var states = new List<String> { "AL", "AK", "AS", "AR", "CA", "CO", "CT", "DE", "FL", "GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", "NM", "NY", "NC", "ND", "OH", "OK", "PA", "RI", "SC", "SD", "TN", "TX", "UT", "VA", "WA", "WI", "WY" }; var random = new Random(); for (var i = 0; i < howMany; i++) { var first = firsts[random.Next(0, firsts.Count)]; var last = lasts[random.Next(0, lasts.Count)]; var streetNumberInt = random.Next(101, 999); var streetNumber = streetNumberInt.ToString(); var zipCode = random.Next(10000, 99999).ToString(); var homePhone = random.Next(200, 999) + "-555-" + random.Next(2000, 9099); var workPhone = random.Next(200, 999) + "-555-" + random.Next(2000, 9099); var fax = random.Next(200, 999) + "-555-" + random.Next(2000, 9099); var homeEmail = first + "@" + isp[random.Next(0, isp.Count)] + ".com"; var workEmail = last + "@" + isp[random.Next(0, isp.Count)] + ".com"; var isMale = (random.Next(1,3)) % 2 == 0 ? true : false; var creditRating = (short)random.Next(100, 800); var firstContact = new DateTime(2010, 1, 1); var lastContact = DateTime.Now; _customers.Add( new Customer( first, last, streetNumber + " " + streets[ random.Next( 0, streets.Count - 1) ] + " Street", cities[ random.Next(0, cities.Count - 1) ], states[ random.Next(0, states.Count - 1) ], zipCode, workPhone, homePhone, fax, homeEmail, workEmail, isMale, "No notes at this time", creditRating, firstContact, lastContact)); } } } public class Customer { public string First { get; set; } public string Last { get; set; } public string Address { get; set; } public string City { get; set; } public string State { get; set; } public string Zip { get; set; } public string HomePhone { get; set; } public string WorkPhone { get; set; } public string Fax { get; set; } public string WorkEmail { get; set; } public string HomeEmail { get; set; } public bool IsMale { get; set; } public string Notes { get; set; } public int CreditRating { get; set; } public DateTime FirstContact { get; set; } public DateTime LastContact { get; set; } public Customer() { } public Customer( string first, string last, string address, string city, string state, string zip, string homePhone, string workPhone, string fax, string workEmail, string homeEmail, bool isMale, string notes, Int16 creditRating, DateTime firstContact, DateTime lastContact) { First = first; Last = last; Address = address; City = city; State = state; Zip = zip; HomePhone = homePhone; WorkPhone = workPhone; Fax = fax; HomeEmail = homeEmail; WorkEmail = workEmail; IsMale = isMale; Notes = notes; CreditRating = creditRating; FirstContact = firstContact; LastContact = lastContact; } }
Setting the Bindings
With the customer class in place, we’re in a position to decide which fields to bind to in the list. The first line will display the full name, and so will need a pair of TextBlocks in a StackPanel and careful handling of the margins to manage the space between them,
<ListBox x:Name="MainListBox" ItemsSource="{Binding Items}" SelectionChanged="MainListBox_SelectionChanged"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel x:Name="DataTemplateStackPanel" Orientation="Horizontal"> <Image x:Name="ItemImage" Source="/i2wCustomerList;component/Images/ArrowImg.png" Height="43" Width="43" VerticalAlignment="Top" Margin="10,0,20,0" /> <StackPanel> <StackPanel Orientation="Horizontal" Margin="-2,-13,0,0"> <TextBlock x:Name="First" Text="{Binding First}" Style="{StaticResource PhoneTextExtraLargeStyle}" /> <TextBlock x:Name="Last" Text="{Binding Last}" Margin="2,0,0,0" Style="{StaticResource PhoneTextExtraLargeStyle}" /> </StackPanel> <TextBlock x:Name="City" Text="{Binding City}" Margin="0,-6,0,3" Style="{StaticResource PhoneTextSubtleStyle}" /> </StackPanel> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
The missing piece is to set the DataContext which we do in MainViewModel.cs. We can simplify that file tremendously,
using System; using System.ComponentModel; using System.Collections.ObjectModel; namespace WindowsPhoneListApplication1.ViewModels { public class MainViewModel : INotifyPropertyChanged { public MainViewModel() { var custColl = new Model.CustomerCollection(); Items = custColl.Customers; } public ObservableCollection<Model.Customer> Items { get; private set; } public event PropertyChangedEventHandler PropertyChanged; private void NotifyPropertyChanged(String propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (null != handler) { handler(this, new PropertyChangedEventArgs(propertyName)); } } } }
Notice the automatic property Items. Automatic properties must have both a getter and a setter. In this case, since we want to use an automatic property, but we want Items to be read only, we make the setter private.
For this to compile, you’ll need to comment out, or remove, the files under the folder SampleData (or the entire folder!)
Set the TitlePanel TextBlock values in MainPage.xaml,
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="24,24,0,12"> <TextBlock x:Name="ApplicationTitle" Text="List and Details" Style="{StaticResource PhoneTextNormalStyle}"/> <TextBlock x:Name="ListTitle" Text="Customers" Margin="-3,-8,0,0" Style="{StaticResource PhoneTextTitle1Style}"/> </StackPanel>
and run the application.
Notice that the fields from the Customer class are appropriately displayed, and that you can flick the list up and down as you could with the sample data. This time, however, the data is drawn from the observable collection of Customers, which we could, of course, have obtained from a Web Service, a Database or any other data source.
When the user clicks on a customer, we want to show the detail. At this point that is as simple as setting the right bindings in Details.xaml.
Notice, first, that on this page you bind the ListTitle to the customer name. This take a small adjustment in the title block,
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="24,24,0,12"> <TextBlock x:Name="PageTitle" Text="List and Details" Style="{StaticResource PhoneTextNormalStyle}"/> <StackPanel Orientation="Horizontal" Margin="-2,-13,0,0"> <TextBlock x:Name="First" Text="{Binding First}" Style="{StaticResource PhoneTextExtraLargeStyle}" /> <TextBlock x:Name="Last" Text="{Binding Last}" Margin="2,0,0,0" Style="{StaticResource PhoneTextExtraLargeStyle}" /> </StackPanel> </StackPanel>
The details can be as much of the customer record as you choose to display. We have a precedent for what to display in a previous tutorial, so let’s use that. Here is the complete ContentPanel,
<Grid x:Name="ContentPanel" Grid.Row="1"> <Grid x:Name="ContentGrid" Grid.Row="1"> <Grid.RowDefinitions> <RowDefinition Height="1*" /> <RowDefinition Height="1*" /> <RowDefinition Height="1*" /> <RowDefinition Height="1*" /> <RowDefinition Height="1*" /> <RowDefinition Height="1*" /> <RowDefinition Height="1*" /> <RowDefinition Height="1*" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="1*" /> <ColumnDefinition Width="2*" /> </Grid.ColumnDefinitions> <TextBlock Grid.ColumnSpan="1" Grid.RowSpan="1" Height="30" HorizontalAlignment="Right" Margin="5" Name="NamePrompt" Text="Full Name" VerticalAlignment="Center" /> <TextBlock Height="30" HorizontalAlignment="Right" Margin="5" Name="AddressPrompt" Text="Street Address" VerticalAlignment="Center" Grid.Row="1" /> <TextBlock Height="30" HorizontalAlignment="Right" Margin="5" Name="CityStateZipPrompt" Text="City, State, Zip" VerticalAlignment="Center" Grid.Row="2" /> <TextBlock Height="30" HorizontalAlignment="Right" Margin="5" Name="PhonePrompt" Text="Phone" VerticalAlignment="Center" FontWeight="Bold" Grid.Row="3" /> <TextBlock FontWeight="Bold" Height="30" HorizontalAlignment="Right" Name="FaxPrompt" Text="Fax" VerticalAlignment="Center" Margin="5" Grid.Row="4" /> <TextBlock FontWeight="Bold" Height="30" HorizontalAlignment="Right" Name="EmailPrompt" Text="Email" VerticalAlignment="Center" Margin="5" Grid.Row="5" /> <TextBox Grid.Column="1" Height="70" HorizontalAlignment="Left" Margin="5,0,0,5" Name="FullName" VerticalAlignment="Bottom" Width="303" Text="{Binding Name}" /> <TextBox Height="70" HorizontalAlignment="Left" Margin="5,0,0,5" Name="Address" VerticalAlignment="Bottom" Width="303" Grid.Column="1" Grid.Row="1" Text="{Binding Address}" /> <StackPanel Grid.Column="1" Grid.Row="2" Grid.RowSpan="1" HorizontalAlignment="Stretch" Name="cityStateZipStack" VerticalAlignment="Stretch" Orientation="Horizontal"> <TextBox Height="70" Name="City" Width="150" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="5,0,0,5" Text="{Binding City}" /> <TextBox Height="70" Name="State" Width="74" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="0,0,0,5" Text="{Binding State}" /> <TextBox Height="70" Name="Zip" Width="93" Margin="0,0,0,5" Text="{Binding Zip}" /> </StackPanel> <TextBox Height="70" HorizontalAlignment="Left" Margin="5,0,0,5" Name="Phone" VerticalAlignment="Bottom" Width="303" Grid.Column="1" Grid.Row="3" Text="{Binding WorkPhone}" /> <TextBox Height="70" HorizontalAlignment="Left" Margin="5,0,0,5" Name="Fax" VerticalAlignment="Bottom" Width="303" Grid.Column="1" Grid.Row="4" Text="{Binding Fax}" /> <TextBox Height="70" HorizontalAlignment="Left" Margin="5,0,0,5" Name="Email" VerticalAlignment="Bottom" Width="303" Grid.Column="1" Grid.Row="5" Text="{Binding WorkEmail}" /> </Grid> </Grid>
What Have You Learned Dorothy?
…if I ever go looking for my heart’s desire again, I won’t look any further than my own backyard; because if it isn’t there, I never really lost it to begin with
By using the Windows Phone List Application, we can significantly cut down on the work while increasing the visual compatibility of List/Detail projects.
Now, that said, if your heart’s desire is in your own backyard then you haven’t lost it to begin with either, no?
MindMap
Here is a mind-map of some of the concpets covered in this tutorial (click inside to move around in the map or click here for the full map
—-
Post Script: True story, a number of years ago I received an email with this quote: “I am sir your translator for book to Korean. I have question, what means sir, “I’ve a feeling we’re not in Kansas anymore.”
Previous Tutorial | Next Tutorial
I’d suggest adding to the the “The ItemTemplate” section, just to point out to (completely new SL devs) that you can really template the entire row however you like. On the iPhone you are, in practice, limited to the ContentView and a set of built in constraints:
http://www.shrinkrays.net/Upload/UITableViewControllerstyles.png
http://www.shrinkrays.net/Upload/uitablestyle-views.png
@Michael L Perry
Yes, thanks, you are absolutely correct
ObservableCollection implements INotifyCollectionChanged, not INotifyPropertyChanged.