iPhone to Windows Phone 7 Tutorial #4: Lists and Details

i2WLogo2-Tutorials

[Please note that comments and discussion for published tutorials are at http://i2w.CodePlex.com ]

iStock_WhiteBoardSmallPartialIn 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>

DataTemplate 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 CustomersList 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?

144px-Judy_Garland_in_The_Wizard_of_Oz_trailer_2 …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 TutorialNext Tutorial

Share

About Jesse Liberty

Jesse Liberty is a Master Consultant for Falafel Software, and has three decades of experience writing and delivering software projects. He is the author of 2 dozen books and multiple Pluralsight courses, and has been a Technical Evangelist for Telerik and for Microsoft, a Distinguished Software Engineer for AT&T, a VP for Information Services for Citibank and a Software Architect for PBS.
This entry was posted in iPhoneToWP7, WindowsPhone and tagged , , , , . Bookmark the permalink.

5 Responses to iPhone to Windows Phone 7 Tutorial #4: Lists and Details

  1. Pingback: iPhone Developer? How to move to Windows Phone 7! | I Love Windows Phone!

  2. Pingback: DotNetShoutout

  3. Chris says:

    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

  4. @Michael L Perry
    Yes, thanks, you are absolutely correct

  5. ObservableCollection implements INotifyCollectionChanged, not INotifyPropertyChanged.

Comments are closed.