- Exploring the Execution Model
- Running Under the Lock Screen
- Page Navigation
- Walking Through the Bookshop Sample Application
- Summary
Walking Through the Bookshop Sample Application
This chapter’s sample app provides the beginnings of a simple data driven e-commerce app that demonstrates the use of navigation, transient and persistent state, image caching, and WCF services. It allows the user to select from a list of books, retrieved from a WCF service, and to view each item’s details on a separate details page.
The ProductsViewModel class retrieves a list of Product objects from a WCF service. Each product has various properties such as a description, price, and an image URI.
The ProductsViewModel saves and restores its own transient state consisting of the list of products it retrieves from the WCF service (see Listing 3.4).
The code for this section resides in the Navigation directory of the WindowsPhone7Unleashed.Examples project in the downloadable sample code.
The viewmodel’s constructor determines whether transient state exists for itself. If so, it restores the list of Products or else it requests the list of products from the WCF using the BookshopServiceClient. The call occurs asynchronously, and the products list is populated once the call completes.
The ViewModelBase class subclasses the NotifyPropertyChangeBase class, which implements INotifyPropertyChanged. The source for NotifyPropertyChangeBase is located in the downloadable sample code, and was discussed in Chapter 2, “Fundamental Concepts in Silverlight Development for Windows Phone.”
Listing 3.4. ProductsViewModel Class (excerpt)
public class
ProductsViewModel
:ViewModelBase
{readonly
IDictionary<
string,
object>
transientStateDictionary;const string
transientStateKey ="ProductsViewModel_Products"
;public
ProductsViewModel(IDictionary
<string,
object
> transientStateDictionary) {this
.transientStateDictionary =ArgumentValidator
.AssertNotNull( transientStateDictionary,"transientStateDictionary"
); LoadTransientState();if
(products !=null
) {return
; }BookshopServiceClient
client =new
BookshopServiceClient
(); client.GetProductsCompleted += (sender, args) => {if
(args.Error !=null
) { MessageService.ShowError("Unable to retrieve products."
); return; } Products = args.Result; Loaded =true;
}; client.GetProductsAsync(); }ObservableCollection
<Product
> products;public
ObservableCollection
<Product
> Products {get
{return
products; }private set
{ Assign(() => Products,ref
products,value);
} }bool
loaded;public bool
Loaded {get
{return
loaded; }private set
{ Assign(() => Loaded,ref
loaded,value
); } }public void
SaveTransientState() { transientStateDictionary[transientStateKey] = products; }public void
LoadTransientState() {object
transientState;if
(transientStateDictionary.TryGetValue( transientStateKey,out
transientState)) { products = transientStateas
ObservableCollection
<Product
>;if
(products !=null
) { Loaded =true
; } } } }
Within the OnNavigatingTo method of the ProductsView page, a ProductsViewModel is instantiated and assigned to the page’s DataContext. The ProductsViewModel is passed the transient state dictionary for the page (seeListing 3.5).
The OnNavigatingTo and OnNavigatedFrom methods are used to inform the viewmodel when to save its state.
Listing 3.5. ProductsView Class
public partial class
ProductsView :
PhoneApplicationPage
{public
ProductsView() { InitializeComponent(); }ProductsViewModel
ViewModel {get
{return
(ProductsViewModel
)DataContext; } }bool
loaded;protected override void
OnNavigatedTo(NavigationEventArgs
e) {Debug
.WriteLine("ProductsView OnNavigatedTo"
);base
.OnNavigatedTo(e);if
(!loaded) { DataContext =new
ProductsViewModel(State);
loaded =true;
} ViewModel.LoadTransientState(); }protected override void OnNavigatedFrom(
NavigationEventArgs
e) {base.OnNavigatedFrom
(e);Debug.WriteLine(
"ProductsView OnNavigatedFrom"
); ViewModel.SaveTransientState(); } }
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 3.11 shows the ProductsView displaying a list of books.
Figure 3.11. 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 3.12).
Figure 3.12. 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.6).
Listing 3.6. 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 3.7). 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 3.7. 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 = transientStateas
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 3.8).
Listing 3.8. 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.
Design-Time Data
It can be difficult and time consuming constructing a page or control without knowing how the content will appear at runtime. The dimensions of images can disturb the layout, as can the length of text and text wrapping settings. The d:DataContext markup extension, which exists in the http://schemas.microsoft.com/expression/blend/2008 namespace, allows you to simulate the runtime DataContext of a control with a design-time object (see Figure 3.13).
Figure 3.13. The d:DataContext markup extension provides for design-time sample data.
Here a design-time instance of the ProductDetailsViewModel class presents some sample data to improve the design-time experience of the developer or designer.
The content StackPanel includes a d:DataContext attribute, which causes a ProductDetailsViewModel instance to be loaded from a sample data file, as shown in the following excerpt:
<
StackPanel
Grid.Row
="1"
Style
="{
StaticResource
PageContentPanelStyle
}"
d
:
DataContext
="{
d
:
DesignData
Source
=ProductDetailsViewSampleData.xaml}">
...
</
StackPanel
>
You can see that the d:DesignData markup extension has its Source property set to the location of a sample data file, ProductDetailsViewSampleData.xaml. The sample data file defines the property values of the viewmodel (see Listing 3.9). The design-time environment of Visual Studio or Expression Blend instantiates the sample viewmodel at design-time.
Listing 3.9. ProductDetailsViewSampleData.xaml
<
local:ProductDetailsViewModel
xmlns
:
local
="clr-namespace:DanielVaughan.WindowsPhone7Unleashed
.Examples.Navigation"
xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns
:
BookshopServiceReference
="clr-namespace:DanielVaughan
.WindowsPhone7Unleashed.Examples.BookshopServiceReference">
<
local
:
ProductDetailsViewModel.Product
>
<
BookshopServiceReference
:
Product
Id
="1"
Title
="Windows Phone 7 Unleashed"
Author
="Daniel Vaughan"
Description
="The complete guide to programming..."
Price
="31.49"
Isbn10
="0672333481"
Isbn13
="978-0672333484"
SmallImageUri
="/DanielVaughan.WindowsPhone7Unleashed
.Examples.Silverlight;component/Navigation/Images/Product01Small.jpg"
LargeImageUri
="/DanielVaughan.WindowsPhone7Unleashed
.Examples.Silverlight;component/Navigation/Images/Product01Large.jpg"
ExternalUrl
="
http://www.amazon.com/Windows-Phone-Unleashed-Daniel-Vaughan/dp/0672333481/"
/>
</
local
:
ProductDetailsViewModel.Product
>
</
local:ProductDetailsViewModel
>
Notice the relative component URIs of the images. The design-time environment will fail to resolve the image location unless relative component URIs are used and the Build Action of the image is set to Resource.
Image Caching
While the viewmodel saves the result of the WCF service call, which allows the app to restore its state after being tombstoned, downloaded images are not saved in the state dictionary, but rather, the app relies on some custom image caching.
A custom IValueConverter, called ImageCacheConverter, is used to download the image from the specified image URI, as shown in the following excerpt:
<
Image
Source
="{
Binding
Product.
LargeImageUri,
Converter
={
StaticResource
ImageCacheConverter
}}" />
By using the ImageCacheConverter, images can be downloaded once and stored in isolated storage for an arbitrary period. Once that period has elapsed, the image will be downloaded again. This allows the application to work offline (see Listing 3.10).
Listing 3.10. ImageCacheConverter Class
public class
ImageCacheConverter
:IValueConverter
{public object
Convert(object
value,Type
targetType,object
parameter,CultureInfo
culture) {if
(EnvironmentValues
.DesignTime) {return
value; }string
url = valueas string
;if
(url !=null
) {try
{return
ImageCache
.GetImage(new
BitmapImage
(new
Uri
(url))); }catch
(IsolatedStorageException
e) {Console
.WriteLine(e);return
value; } }BitmapImage
bitmapImage = valueas
BitmapImage;
if
(bitmapImage !=null
) {return
ImageCache
.GetImage(bitmapImage); }return
value; }public object
ConvertBack(object
value,Type
targetType,object
parameter,CultureInfo
culture) {throw new
NotImplementedException
(); } }
The ImageCacheConverter can be used in conjunction with a URL or a BitMapImage. In the sample code, it is used with a URL, supplied by the product’s SmallImageUri and LargeImageUri properties. The ImageCache class maintains a dictionary of URI keyed cached images. It stores the dictionary in isolated storage, and when an image is requested, it attempts to locate it in the dictionary. If found it checks to ensure that the image has not expired, and then returns the image.
Many thanks to Peter Nowak (http://winphonedev.de/) for his image cache code, which I have adapted, with his permission, for use in the downloadable sample code.
The ImageCache class, in the downloadable sample code, maintains a list of ImageCacheItem objects, which represent cached images. The ImageCache.GetImage method is used to retrieve an image from the cache. If the image is not located in the cache, it is scheduled to be downloaded by the static ImageDownloader class.
The ImageDownloader coordinates an asynchronous download of the image file. It uses an HttpWebRequest to retrieve the image from a remote server, and then stores the downloaded file in isolated storage. Once downloaded, the Source property of the original image is assigned, which means that, if it is present in the UI, the image will appear (see the ImageDownloader class, located in the Data/ImageCache directory of the WindowsPhone7Unleashed project, in the downloadable sample code, for details).
Overview of the Sample Bookshop WCF Service
The Bookshop demo application includes a server-side component, which is used by both the ProductsViewModel and ProductDetailsViewModel classes, providing the application with a set of products to display. The server-side component is fairly arbitrary and is presented here merely for the sake of completeness.
The WCF service is called BookshopService and resides in the WindowsPhone7Unleashed.Web project of the downloadable sample code (see Listing 3.11).
Listing 3.11. BookshopService Class
[AspNetCompatibilityRequirements
( RequirementsMode =AspNetCompatibilityRequirementsMode
.Allowed)]public class
BookshopService
:IBookshopService
{public
IEnumerable<
Product
> GetProducts() {return
ProductManager
.Products; }public
Product
GetProductById(int
productId) {return
ProductManager
.GetProductById(productId); } }
The BookshopService exposes static methods of the ProductManager class, shown in Listing 3.12. The ProductManager class creates an XDocument instance, using an XML file, to populate a list of Products.
Listing 3.12. ProductManager Class
public static class
ProductManager
{static readonly
List
<Product> products =
new
List
<Product
>();public static
IEnumerable
<Product
> Products {get
{return
products; } }static
ProductManager() {string
path =HttpContext
.Current.Server.MapPath("~/Services/Bookshop/Products.xml"
);XDocument
document =XDocument
.Load(path);foreach
(XElement element
in
document.Element("Products"
).Elements("Product"
)) {var
product =new
Product
(element); product.SmallImageUri =ServerUtility
.ResolveServerUrl(product.SmallImageUri); product.LargeImageUri =ServerUtility
.ResolveServerUrl(product.LargeImageUri); products.Add(product); } }public static
Product
GetProductById(int id)
{if
(id < 0 || id > products.Count) {throw new
ArgumentOutOfRangeException(
"id"
); }return
products[id - 1]; } }
The Product class contains the properties that are used to display each book’s details, such as Title and Author. The Product class also knows how to populate itself from an XElement. The explicit casting operators of the XElement class make it easy to extract the values to the Product properties, as can be seen in the following excerpt from the Product class:
public
Product(XElement
element) {if
(element ==null
) {throw new
ArgumentNullException
("element"
); } Id = (int
)element.Element("Id"
); Title = (string
)element.Element("Title"
); Author = (string
)element.Element("Author"
); Description = (string
)element.Element("Description"
); SmallImageUri = (string
)element.Element("SmallImageUri"
); LargeImageUri = (string
)element.Element("LargeImageUri"
); Price = (double
)element.Element("Price"
); Isbn10 = (string
)element.Element("ISBN-10"
); Isbn13 = (string
)element.Element("ISBN-13"
); ExternalUrl = (string
)element.Element("ExternalUrl"
); }