WP7 Databound Application – Pizza App User Interface Design

Last week, in the introduction to a Windows Phone 7 Databound application, we saw how a databound application works and how to show a collection on the UI.

In this second part, we’ll see how to convert it in to a real world app.

Things we cover

  • Adding new pages
  • Wiring up events
  • Creating a custom panel, the secret behind the toppings selection screen

Before going further, let’s see what we are going to create. Below are the screen-shots of the completed application.

Pizza App Screen-shot
Pizza App Screen-shot
If you observe closely, the toppings selections screen (shown left side in the above image) shows the Pizza topped only with the selected toppings. We’ll cover it later in the tutorial, first let’s start with the ViewModel.

Updating the ViewModel

We create the pizza items with the following structure,

  1. ItemName String The name of a crust item or a topping
  2. ItemImage String Image path of a crust item or a topping
  3. ItemDescription String Description of the item
  4. IsSelected Boolean Indicating if the item is selected

Open the ItemViewModel.cs class file and replace the existing properties with the following properties,

    
private string _ItemName;
        public string ItemName 
        {
            get 
            {
                return _ItemName;
            }
            set 
            {
                _ItemName = value;
                NotifyPropertyChanged("ItemName");
            }
        }

        private string _ItemImage;
        public string ItemImage
        {
            get
            {
                return _ItemImage;
            }
            set
            {
                _ItemImage = value;
                NotifyPropertyChanged("ItemImage");
            }
        }

        private string _ItemDescription;        
        public string ItemDescription
        {
            get
            {
                return _ItemDescription;
            }
            set
            {
                _ItemDescription = value;
                NotifyPropertyChanged("ItemDescription");
            }
        }

        private bool _IsSelected;

        public bool IsSelected
        {
            get { return _IsSelected; }
            set
            {
                _IsSelected = value;
                NotifyPropertyChanged("IsSelected");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String propertyName) 
        {
            if (null != PropertyChanged) 
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

Add sample data

Open MainViewModel.cs.

We will create two collection objects of type ItemViewModel for Crust Items and Toppings,

    
public ObservableCollection CrustItems { get; private set; }     
public ObservableCollection Toppings { get; private set; }     

Now, instantiate the collections in the constructor,

    
this.CrustItems = new ObservableCollection();     
this.Toppings = new ObservableCollection();     

Find the method LoadData and replace the method with the following code,

public void LoadData()
        {
            #region // Sample data; Crust Items
            
            //Sample data: Crusts
            this.CrustItems.Add(new ItemViewModel() { ItemName = "Handmade", ItemImage = "../Images/hand.png",
                ItemDescription = "Delicious and soft handmade crust"});
            this.CrustItems.Add(new ItemViewModel() { ItemName = "Natural", ItemImage = "../Images/natural.png", 
                ItemDescription = "Crust with selected natural dough" });
            this.CrustItems.Add(new ItemViewModel() { ItemName = "Pan Crust", ItemImage = "../Images/pan.png", 
                ItemDescription = "Traditional Italian Crust" });
            this.CrustItems.Add(new ItemViewModel() { ItemName = "Stuffed Crust", ItemImage = "../Images/stuffed.png", 
                ItemDescription = "Seasoned with garlic, stuffed with cheese" });
            this.CrustItems.Add(new ItemViewModel() { ItemName = "Thin N Crispy Crust", ItemImage = "../Images/thincrispy.png", 
                ItemDescription = "Bread dough with cracked crust" });
            
            // Sample data: Toppings
            this.Toppings.Add(new ItemViewModel() { ItemName = "Ham", ItemImage = "../Images/toppings/ham.png" });
            this.Toppings.Add(new ItemViewModel() { ItemName = "Grilled Chicken", ItemImage = "../Images/toppings/grilled_chicken.png" });
            this.Toppings.Add(new ItemViewModel() { ItemName = "Italian Sausage", ItemImage = "../Images/toppings/italian_sausage_crumbled.png" });
            this.Toppings.Add(new ItemViewModel() { ItemName = "Black Olives", ItemImage = "../Images/toppings/olives_black.png" });
            this.Toppings.Add(new ItemViewModel() { ItemName = "Green Olives", ItemImage = "../Images/toppings/olives_green.png" });
            this.Toppings.Add(new ItemViewModel() { ItemName = "Mushrooms", ItemImage = "../Images/toppings/mushrooms.png" });
            this.Toppings.Add(new ItemViewModel() { ItemName = "Pepperoni", ItemImage = "../Images/toppings/pepperoni.png" });
            this.Toppings.Add(new ItemViewModel() { ItemName = "White Onions", ItemImage = "../Images/toppings/onions_white.png" });
            this.Toppings.Add(new ItemViewModel() { ItemName = "Red Onions", ItemImage = "../Images/toppings/onions_red.png" });
            this.Toppings.Add(new ItemViewModel() { ItemName = "Diced Tomatoes", ItemImage = "../Images/toppings/tomatoes_diced.png" });
            this.Toppings.Add(new ItemViewModel() { ItemName = "Roma Tomatoes", ItemImage = "../Images/toppings/tomatoes_roma.png" });
            this.Toppings.Add(new ItemViewModel() { ItemName = "Pineapple", ItemImage = "../Images/toppings/pineapple.png" });
            this.Toppings.Add(new ItemViewModel() { ItemName = "Jalapenos", ItemImage = "../Images/toppings/jalapenos.png" });
             
            #endregion
         
            this.IsDataLoaded = true;            
        }

Now we have our ViewModel ready, let’s create the UI.

Creating the Application Screens

Start Screen (MainPage.xaml)

Open MainPage.xaml.

Remove the ListBox named MainListBox and add a button,

 <Button x:Name="cmdPizzaStart" Content="Make your Pizza" 
                    Click="cmdPizzaStart_Click" Height="100" Width="300"/>

In the event handler, add the below code to navigate to Crust Selection screen,

NavigationService.Navigate(new Uri("/CrustSelection.xaml", UriKind.Relative));

Crust Selection Screen (CrustSelection.xaml)

Right click on the project in ‘Solution Explorer’ and add a new ‘Windows Phone Portrait Page’ to the project. Name it ‘CrustSelection.xaml’.

Go to xaml view and replace the ContentPanel with the following code,

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"
              Background="DarkRed">
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <controls:Panorama ItemsSource="{Binding CrustItems}" x:Name="crustList">
                <controls:Panorama.ItemTemplate>
                    <DataTemplate>
                            <Image Source="{Binding ItemImage}" HorizontalAlignment="Center"/>
                    </DataTemplate>
                </controls:Panorama.ItemTemplate>
                <controls:Panorama.HeaderTemplate>
                    <DataTemplate>
                            <TextBlock Text="{Binding ItemName}" TextWrapping="Wrap" 
                                       HorizontalAlignment="Center"
                                       Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                    </DataTemplate>
                </controls:Panorama.HeaderTemplate>
            </controls:Panorama>
            <Button x:Name="cmdSelectCrust" Content="Select Toppings" Grid.Row="1"
                    HorizontalAlignment="Center" Click="cmdSelectCrust_Click"
                    IsEnabled="{Binding SelectedIndex, ElementName=MainListBox, Converter={StaticResource IndexToEnabledConverter}}"/>
        </Grid>

Let’s see what we have in the above code snippet,

Panorama

It creates a Panorama control bound to the CrustItems collection. The ItemTemplate contains an Image with Source property bound to ItemImage property. The HeaderTemplate contains a TextBlock with the Text property bound to ItemName.

Below is the panorama in action,

CrustSelection_Panorama

Add the below line in the click event handler of the button cmdSelectCrust, to navigate to Toppings Selection screen when clicked.

NavigationService.Navigate(new Uri("/ToppingsSelections.xaml?selectedItem=" + crustList.SelectedIndex, UriKind.Relative));

Toppings Selection Screen (ToppingsSelection.xaml)

Add a new ‘Windows Phone Portrait Page’ to the project and name it ‘ToppingsSelection.xaml’.

Update the ContentPanel Grid as shown,

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" Background="DarkRed">
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
			<Grid.ColumnDefinitions>
				<ColumnDefinition/>
				<ColumnDefinition/>
			</Grid.ColumnDefinitions>	
            <ListBox x:Name="MainListBox" Margin="0,0,-12,0" ItemsSource="{Binding Toppings}" 
                     ScrollViewer.HorizontalScrollBarVisibility="Visible">                
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <CheckBox Content="{Binding ItemName}" IsChecked="{Binding IsSelected, Mode=TwoWay}"
                                  Click="CheckBox_Click"/>
                    </DataTemplate>
                </ListBox.ItemTemplate>                
            </ListBox>
            
            <StackPanel x:Name="SelectedCrust" Grid.Column="1">
                <Grid>
                    <Image Source="{Binding ItemImage}" Width="416" Height="356"/>
                    <ItemsControl x:Name="lstSelectedToppings" BorderThickness="1">
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <local:CustomPanel Background="Transparent"/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <Image Source="{Binding ItemImage}" Width="416" Height="356"/>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                </Grid>
                <StackPanel>
                    <TextBlock Text="Crust Choice:" HorizontalAlignment="Center" VerticalAlignment="Top" FontWeight="Bold"/>
                    <TextBlock Text="{Binding ItemName}" HorizontalAlignment="Center" VerticalAlignment="Top" FontWeight="Bold"/>
                </StackPanel>
            </StackPanel>
            <Button x:Name="cmdReviewOrder" Content="Check out" Grid.Row="1" Grid.ColumnSpan="2"
                    HorizontalAlignment="Right" Click="cmdReviewOrder_Click"
                    IsEnabled="{Binding SelectedIndex, ElementName=MainListBox, Converter={StaticResource IndexToEnabledConverter}}"/>
        </Grid>

The image shown below explains the layout of ToppingsSelection.

toppings_layout Toppings

In code-behind, override the OnNavigatedTo method and update the data context.

OnNavigatedTo is executed when ever user navigates to the page. We will read the selected crust value and selected toppings if any and update the UI.

protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            string selectedIndex = "";
            if (NavigationContext.QueryString.TryGetValue("selectedItem", out selectedIndex))
            {
                index = int.Parse(selectedIndex);
                SelectedCrust.DataContext = App.ViewModel.CrustItems[index];
                UpdateExistingToppings();
            }
        }
private void UpdateExistingToppings()
        {
            lstSelectedToppings.ItemsSource = null;

            var selectedToppings = from p in App.ViewModel.Toppings
                                   where p.IsSelected
                                   select p;

            if (selectedToppings != null && selectedToppings.Count() > 0)
            {
                lstSelectedToppings.ItemsSource = selectedToppings;
            }
        }

When ever user changes the toppings selections, call the method UpdateExistingToppings, which updates the selected topping images on the right side.

Navigate to order review page when clicked on ‘Check Out’.

        private void cmdReviewOrder_Click(object sender, RoutedEventArgs e)
        {
            NavigationService.Navigate(new Uri("/OrderReview.xaml?selectedItem=" + index, UriKind.Relative));
        }

        private void CheckBox_Click(object sender, RoutedEventArgs e)
        {
            UpdateExistingToppings();
        }

Custom Panel

The selected toppings ItemsControl, uses a ‘CustomPanel’ as ItemsPanelTemplate, which shows the toppings layered over each other as we see on a real pizza.

code_custompanel

For detailed information on Custom Panel check Silverlight Custom Panel blog post.

Order Review Page (OrderReview.xaml)

Add another ‘Windows Phone Portrait Page’ and name it ‘OrderReview.xaml’

Update the ContentPanel with the following code, which shows a simple form for user address input and user selections. Again we use the same Custom Panel for showing the toppings.

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"
              Background="DarkRed">
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <StackPanel BindingValidationError="ContentPanel_BindingValidationError">
                <TextBlock Text="Name" Margin="10,0,0,0"/>
                <TextBox x:Name="txtName" InputScope="PersonalFullName" Text="{Binding DeliveryAddress.Name, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"/>
                <TextBlock Text="Address" Margin="10,0,0,0"/>
                <TextBox x:Name="txtAddress1" InputScope="PostalAddress" Text="{Binding DeliveryAddress.AddressLine1, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"/>
                <TextBlock Text="City, State" Margin="10,0,0,0"/>
                <TextBox x:Name="txtAddress2" InputScope="PostalAddress" Text="{Binding DeliveryAddress.AddressLine2, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"/>
            </StackPanel>
            <Grid x:Name="SelectedCrust" Grid.Row="1">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <Image Source="{Binding ItemImage}" Width="216" Height="156" Margin="0, -80, 0, 0"/>
                <ItemsControl x:Name="lstSelectedToppingsImg">
                    <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <local:CustomPanel Background="Transparent"/>
                        </ItemsPanelTemplate>
                    </ItemsControl.ItemsPanel>
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <Image Source="{Binding ItemImage}" Width="226" Height="156"/>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
                                                                       
                <StackPanel  Grid.Column="1" Margin="0,15,0,0">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding ItemName}" HorizontalAlignment="Center" 
                            VerticalAlignment="Top" FontWeight="Bold"/>
                        <TextBlock Text=" Pizza with"></TextBlock>
                    </StackPanel>                    
                    <ListBox x:Name="lstSelectedToppings" Margin="0,5,0,0">
                        <ListBox.ItemTemplate>
                            <DataTemplate>
                                <StackPanel Orientation="Horizontal">
                                    <TextBlock Text="{Binding}"
                                           FontSize="18"/>
                                    <TextBlock Text="." FontSize="18" VerticalAlignment="Center"/>
                                </StackPanel>
                            </DataTemplate>
                        </ListBox.ItemTemplate>
                        <ListBox.ItemsPanel>
                            <ItemsPanelTemplate>                                
                                <toolkit:WrapPanel MaxWidth="220"/>
                            </ItemsPanelTemplate>
                        </ListBox.ItemsPanel>
                    </ListBox>
                </StackPanel>
                <Button Content="Place Order" Grid.Row="1" Grid.ColumnSpan="2" Width="200"
                        Click="Button_Click"/>
            </Grid>
        </Grid>

Similar to what we did in Toppings Selection screen, update the data context in the method OnNavigatedTo and process the order when clicked on ‘Place Order’.

protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            string selectedIndex = "";
            if (NavigationContext.QueryString.TryGetValue("selectedItem", out selectedIndex))
            {
                int index = int.Parse(selectedIndex);
                SelectedCrust.DataContext = App.ViewModel.CrustItems[index];
                lstSelectedToppings.ItemsSource = lstSelectedToppingsImg.ItemsSource = null;
                lstSelectedToppings.ItemsSource = from p in App.ViewModel.Toppings
                                                  where p.IsSelected == true
                                                  select p.ItemName;
                lstSelectedToppingsImg.ItemsSource = from p in App.ViewModel.Toppings
                                                     where p.IsSelected == true
                                                     select p; 
            }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            if (string.IsNullOrEmpty(txtName.Text.Trim()))
                MessageBox.Show("Name cannot be empty");
            else if (string.IsNullOrEmpty(txtAddress1.Text.Trim()))
                MessageBox.Show("Address line 1 cannot be empty");
            else if (string.IsNullOrEmpty(txtAddress2.Text.Trim()))
                MessageBox.Show("Address line 2 cannot be empty");
            else
                ProcessOrder();            
        }

        private void ProcessOrder()
        {
            Random rndConfirm = new Random(1000);
            MessageBox.Show("Thanks for placing the order. Your order confirmation number: " + rndConfirm.Next().ToString());
            NavigationService.Navigate(new Uri("/MainPage.xaml", UriKind.Relative));
        }

Conclusion

In Process Order, we generate a random order number, clear the selections and navigate back to the MainPage. Ideally, we should store the order number and send a e-mail confirmation.

In next post, we will see how to connect this to a OData service instead of hard-coded data.

Click here to download the source code.

Happy Coding!

4 thoughts on WP7 Databound Application – Pizza App User Interface Design

  1. Pingback: Twitted by Chaitanya_VRK

Leave a Reply

Your email address will not be published. Required fields are marked *