Displaying the Product List
The list of products exposed by the ProductsViewModel.Products
property is displayed using a ListBox
control in the ProductsView page. The ListBox
’s ItemTemplate
has various controls that are used to display the details of each Product
, as shown in the following excerpt:
<StackPanel Grid.Row="1" Margin="10"
Visibility="{Binding Loaded,
Converter={StaticResource BooleanToVisibilityConverter},
ConverterParameter=Visible}">
<ScrollViewer>
<ListBox ItemsSource="{Binding Products}" Height="610">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding SmallImageUri}"
MaxWidth="150" MaxHeight="150"
Margin="0,0,10,10" />
<StackPanel Margin="5">
<TextBlock Text="{Binding Title}"
TextWrapping="Wrap" />
<TextBlock Text="{Binding Price,
StringFormat=\{0:C\}}" />
<HyperlinkButton
NavigateUri="{Binding Id,
StringFormat=/ProductDetails/\{0\}}"
Content="View Details"
HorizontalAlignment="Left" Margin="0,10,0,0" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
</StackPanel>
An Image
control displays a thumbnail of the product, using the SmallImageUri
property of the Product
.
A string format is used to convert the Price
property, which is a double value, to a currency formatted string using the format {0:C}
. Similarly, a link is provided for the product details page, using the format /ProductDetails/{0}
, and the value of the Product
’s Id
is substituted for the {0}
placeholder. The UriMapping
for this product details URI causes the application to reroute to the
full URI of the ProductDetailsView.xaml page and includes the productId
query string parameter.
Figure 1 shows the ProductsView
displaying a list of books.
FIGURE 1 Products View.
When the user presses the HyperlinkButton
,
he is directed to the ProductDetailsView.xaml page. This page displays
the various properties of the product and includes a link for an
external website, where the user can find more information about the
product (see Figure 2).
FIGURE 2 View a product’s details.
When navigating to the ProductDetailsView
the page attempts to retrieve the productId query string parameter from the NavigationContext
(see Listing 3).
LISTING 3. ProductDetailsView
Class (excerpt)
public partial class ProductDetailsView : PhoneApplicationPage
{
public ProductDetailsView()
{
InitializeComponent();
DataContext = new ProductDetailsViewModel(
PhoneApplicationService.Current.State); }
ProductDetailsViewModel ViewModel
{
get
{
return (ProductDetailsViewModel)DataContext;
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{ base.OnNavigatedTo(e);
string productIdString = NavigationContext.QueryString["productId"];
int productId = int.Parse(productIdString);
ViewModel.LoadProduct(productId);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{ ViewModel.SaveTransientState();
base.OnNavigatedFrom(e);
}
}
The view then passes the parameter along to the ProductDetailsViewModel
class, which handles the loading of the specified product (see Listing 4). The LoadProduct
method first tests for the existence of the product in transient state.
If not present, it retrieves the product using the service client.
LISTING 4. ProductDetailsViewModel
Class (excerpt)
public class ProductDetailsViewModel : ViewModelBase
{
const string transientStateKey = "ProductDetailsViewModel_Product";
readonly IDictionary<string, object> transientStateDictionary;
public ProductDetailsViewModel(
IDictionary<string, object> transientStateDictionary)
{
this.transientStateDictionary = ArgumentValidator.AssertNotNull(
transientStateDictionary, "transientStateDictionary");
}
public void LoadProduct(int productId)
{
object transientState;
if (PhoneApplicationService.Current.State.TryGetValue(
transientStateKey, out transientState))
{
product = transientState as Product;
if (product != null && product.Id == productId)
{
return;
}
}
BookshopServiceClient client = new BookshopServiceClient();
client.GetProductByIdCompleted += (sender, args) =>
{
if (args.Error != null)
{
throw args.Error;
}
Product = args.Result;
};
client.GetProductByIdAsync(productId);
}
Product product;
public Product Product
{
get
{
return product;
}
/* Setter is not private to enable sample data.
* See ProductDetailsViewSampleData.xaml */
internal set
{
product = value;
OnPropertyChanged("Product");
}
}
public void SaveTransientState()
{ transientStateDictionary[transientStateKey] = product;
}
}
When navigating away from the page, the viewmodel’s SaveTransientState
method is called, which places the product in the state dictionary.
The ProductDetailsView.xaml page presents the product details via the viewmodel’s Product
property (see Listing 5).
LISTING 5. ProductDetailsView.xaml (excerpt)
<StackPanel Grid.Row="1"
Style="{StaticResource PageContentPanelStyle}"
d:DataContext="{d:DesignData Source=ProductDetailsViewSampleData.xaml}">
<TextBlock Text="{Binding Product.Title}" TextWrapping="Wrap"
Style="{StaticResource PhoneTextTitle2Style}"/>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Product.LargeImageUri,
Converter={StaticResource ImageCacheConverter}}"
MaxWidth="250" MaxHeight="250" Margin="10,10,0,10" />
<StackPanel>
<TextBlock Text="{Binding Product.Author}" TextWrapping="Wrap"
Style="{StaticResource PhoneTextTitle3Style}"/>
<TextBlock Text="{Binding Product.Price, StringFormat=\{0:C\}}"
Style="{StaticResource PhoneTextTitle3Style}"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="ISBN"
Style="{StaticResource PhoneTextTitle3Style}" />
<TextBlock Text="{Binding Product.Isbn13}"
TextWrapping="Wrap"
Style="{StaticResource PhoneTextNormalStyle}" />
</StackPanel>
<HyperlinkButton
NavigateUri="{Binding Product.ExternalUrl,
StringFormat=/WebBrowser/\{0\}}"
Content="External Page"
Margin="0,10,0,0" HorizontalAlignment="Left" />
</StackPanel>
</StackPanel>
<TextBlock Text="{Binding Product.Description}"
Margin="10,20,0,10" TextWrapping="Wrap" />
</StackPanel>
The StackPanel
includes a d:DataContext
attribute that defines a design-time data context object, discussed in the next section.