In This Chapter
• Reliably determining whether a network connection is available
• Understanding network connection priorities
• Monitoring the network connection state
• Consuming an OData service
• Fetching data when the user scrolls to the end of a ListBox
Network services have the potential to broaden your app’s capabilities by connecting it to a vast collection of online resources. Many services such as Twitter and Facebook offer web APIs that allow your app to interact or even extend the services.
Windows Phone devices have low power mobile processors, which limit their processing power. By leveraging network services, your app can provide capabilities more akin to a desktop application.
This chapter begins by looking at the types of network services available on the phone. It then looks at monitoring network connectivity and at determining the type of network connection that the phone is using—Wi-Fi or cellular—enabling you to tailor the amount of data traffic your app uses accordingly.
Finally, the chapter focuses on the Open Data Protocol (OData) and how to create an app that consumes the eBay OData service. The chapter examines a useful custom component that allows your app to fetch data from a service as soon as the user scrolls to the end of a ListBox
.
Windows Phone provides various ways of consuming network services, including the following:
• SOAP services—SOAP stands for Simple Object Access Protocol. SOAP uses an XML format. In Windows Phone, SOAP services are usually consumed using Windows Communication Foundation (WCF).
• REST services—REST stands for Representational State Transfer. REST relies on HTTP and uses unique URLs to identify resource entities. CRUD (Create Read Update Delete) operations are performed using HTTP GET, POST, PUT, and DELETE. Various open-source projects are available for consuming REST services.
• OData—OData stands for Open Data Protocol. It is a web protocol for querying and updating data. OData is the main topic of this chapter.
• Plain HTTP services—Some services are provided using nonstandardized protocols, where HTTP GET requests are used to consume an API. For example, Google search can be queried using a URL, and the results returned in JSON.
Silverlight for Windows Phone clients can access these services directly, or in the case of WCF or OData, may use a proxy that is generated from the service’s published metadata.
Visual Studio generates proxy classes for SOAP and OData services by selecting the Add Service Reference from the Project menu.
A network service can be hosted on your own server or on the cloud. It may be a service that you have written in-house, or it could also be a third-party service where you can query the service but may not have access to its source code.
While most phones have a data plan from a cellular network provider, a network connection will most likely not be available at all times. In fact, in some areas it can be difficult staying connected to a network. Therefore, it is imperative that your app is engineered to be resilient to network disruptions.
Furthermore, awareness of the type of network connection that the device is using allows you to tailor the volume of data traffic that it uses. When connected to a local area network via Wi-Fi or a USB cable, it may be acceptable for an app to download large data files. Conversely, depleting a user’s cellular broadband connection by transferring a lot of data is probably not acceptable.
The Windows Phone OS automatically chooses a connection type based on the following ordered priorities:
• Network connection to a PC via the Zune software or the Windows Phone Connect Tool.
• Wi-Fi
• Mobile broadband
For more information on the Windows Phone Connect Tool, see Chapter 18, “Extending the Windows Phone Picture Viewer.”
A network connection via a PC is favored above all other connection types. The phone periodically checks whether a higher priority connection is available and switches connections accordingly.
The API for determining the connection status of the device is located in the Microsoft.Phone.Net.NetworkInformation and System.Net.NetworkInformation namespaces. Within this API is an event for notification when a connection change occurs.
The System.Net.NetworkInformation.NetworkInterface
class and the System.Net.NetworkInformation.NetworkChange
class represent the non-phone specific portion of the API, while the Microsoft.Phone.Net.NetworkInformation.NetworkInterface
inherits from System.Net.NetworkInformation.NetworkInterface
and is specific to the phone (see Figure 24.1).
The NetworkInterface.GetIsNetworkAvailable
method allows you to determine whether any network connection is available; however, there is one caveat (see the following note).
GetIsNetworkAvailable
is not always a true indicator of whether a useable network is available. A network connection is considered to be available if any network interface is marked “up” and is not a loopback or tunnel interface. There are cases where the phone may not be connected to a network, yet a network is still considered available, and GetIsNetworkAvailable
returns true.
An alternative and albeit more reliable method for determining network availability is the NetworkInterface.NetworkInterfaceType
property. While the NetworkInterfaceType
enum includes many values, the phone implementation supports only the following five values:
• None—Indicates that no network connection is established.
• MobileBroadbandCdma—Indicates a connection to a CDMA cellular network.
• MobileBroadbandGsm—Indicates a connection to a GSM cellular network.
• Ethernet—Indicates a network connection is established via a PC. Ordinarily, this is done with a USB cable.
• Wireless80211—Indicates a Wi-Fi connection is established to a LAN.
To determine whether the phone is connected to a network, the NetworkInterfaceType
property can be compared to the NetworkInterfaceType.None
enum value, as shown:
bool connected = NetworkInterface.NetworkInterfaceType
!= NetworkInterfaceType.None;
The NetworkInterfaceType.None
value represents a state where the phone does not have Internet access. As Dolhai points out in an article available at http://www.codeproject.com/KB/windows-phone-7/ZuneDetectAndNetworking.aspx, under some circumstances, such as disconnecting the phone from an Ethernet connection, reading the NetworkInterfaceType
property can block the calling thread for many seconds. It is advisable, therefore, to read the property from a background thread. In a moment you see how to do that using a custom class, but first let us briefly look at how to monitor changes to the network connection state.
The NetworkChange.NetworkAddressChanged
event is raised if the IP address of the phone changes and typically occurs when any of the following events occur:
• The device connects to, or disconnects from, a Wi-Fi or mobile network.
• The phone is linked to the PC via the Zune software or the Windows Phone Connect Tool.
• The phone is unlinked from the PC when either the Zune software or the Windows Phone Connect Tool is closed or when the USB cable is disconnected.
When linked to a PC with no Internet connection present (via the Zune software or Windows Phone Connect Tool), the phone device does not automatically switch to an available Wi-Fi connection.
When the phone switches connections, the NetworkAddressChanged
event may be raised several times. This can be troublesome if your event handler performs some expensive task. To remedy this, the custom NetworkConnectionMonitor
class, provided in the downloadable sample code, uses Rx (Reactive Extensions) to sample the event so that, at most, only one NetworkAddressChanged
event is raised each second (see Listing 24.1).
When the event is handled, the NetworkConnectionMonitor.Update
method is called, which sets the network connection type and raises the NetworkConnectionMonitor.NetworkConnectionChanged
event on the UI thread.
For more information on Rx, see Chapter 15, “Geographic Location.”
public class NetworkConnectionMonitor : INetworkConnectionMonitor
{
const int sampleRateMs = 1000;
IDisposable subscription;
public event EventHandler<EventArgs> NetworkConnectionChanged;
public NetworkConnectionType NetworkConnectionType { get; private set; }
public bool Connected
{
get
{
return NetworkConnectionType != NetworkConnectionType.None;
}
}
public NetworkConnectionMonitor()
{
Update();
var observable
= Observable.FromEvent<NetworkAddressChangedEventHandler, EventArgs>(
handler => new NetworkAddressChangedEventHandler(handler),
handler => NetworkChange.NetworkAddressChanged += handler,
handler => NetworkChange.NetworkAddressChanged -= handler);
IObservable<IEvent<EventArgs>> sampler
= observable.Sample(TimeSpan.FromMilliseconds(sampleRateMs));
subscription = sampler.ObserveOn(Scheduler.ThreadPool).Subscribe(
args => Update());
}
void Update()
{
switch (NetworkInterface.NetworkInterfaceType)
{
case NetworkInterfaceType.None:
NetworkConnectionType = NetworkConnectionType.None;
break;
case NetworkInterfaceType.MobileBroadbandCdma:
case NetworkInterfaceType.MobileBroadbandGsm:
NetworkConnectionType = NetworkConnectionType.MobileBroadband;
break;
/* These values do not apply to Windows Phone. */
case NetworkInterfaceType.AsymmetricDsl:
case NetworkInterfaceType.Atm:
/* Content omitted. */
/* Phone values */
case NetworkInterfaceType.Ethernet:
case NetworkInterfaceType.Wireless80211:
default:
NetworkConnectionType = NetworkConnectionType.Lan;
break;
}
Deployment.Current.Dispatcher.BeginInvoke(new Action(
() => NetworkConnectionChanged.Raise(this, EventArgs.Empty)));
}
}
The custom NetworkConnectionMonitor
class does not expose a NetworkInterfaceType
property since only three values apply to the phone. Instead, it uses a custom enum type called NetworkConnectionType
, which provides a simplified view on the type of connection with the following three values:
• None—Indicates that no network connection is established
• Lan—Indicates that a connection to a local area network is established, and that the app can probably be more indulgent with the amount of data it transfers
• MobileBroadband—Indicates that the phone is using a cellular network and that data usage should be used more sparingly, if at all
To use the NetworkConnectionMonitor
, define it as a field in your class and subscribe to the NetworkConnectionChanged
event, as shown:
readonly INetworkConnectionMonitor networkConnectionMonitor;
public YourViewModel()
{
networkConnectionMonitor = new NetworkConnectionMonitor();
networkConnectionMonitor.NetworkConnectionChanged
+= delegate
{
WebServiceAvailable = networkConnectionMonitor.Connected;
CanPerformDownloadLargeFile
= networkConnectionMonitor.NetworkConnectionType
== NetworkConnectionType.Lan;
};
}
Alternatively, an implementation of the INetworkConnectionMonitor
can be passed to the viewmodel, allowing it to be replaced with a mock for unit testing. This is demonstrated later in this chapter.
The example app for this section consumes live eBay data, which is exposed using the Open Data Protocol (OData). OData is a web protocol for querying and updating data. It defines operations on resources using HTTP verbs (PUT, POST, UPDATE, and DELETE), and it identifies those resources using a standard URI syntax. Data is transferred over HTTP using the AtomPub or JSON standards. For AtomPub, the OData protocol defines some conventions on the standard to support the exchange of query and schema information.
For in-depth information on the OData standard, visit http://odata.org.
A key advantage of the OData protocol is its accessibility by a broad range of clients. Client libraries are available for Windows Phone, iPhone, Silverlight 4, PHP, AJAX/Javascript, Ruby, and Java.
An OData service can be implemented on any server that supports HTTP. The .NET implementation is supported through WCF Data Services, which is a .NET Framework component that used to be known as ADO.Net Data Services (codename Astoria). WCF Data Services provides a framework for creating OData web services and includes a set of client libraries (one for the desktop CLR, and one for the Silverlight CLR) for building clients that consume OData feeds.
Services that expose their data using the OData protocol are referred to as OData producers, and clients that consume data exposed using the OData protocol are referred to as consumers. OData producers allow CRUD operations to be performed using query string parameters. The odata.org website provides a list of current producers and consumers. Among the producers, there are applications such as SharePoint 2010, Windows Azure Storage, and IBM WebSphere, as well as several live OData services such as the eBay data service, which is used in the sample app.
The OData tools that accompany the Windows Phone SDK have been much improved since the first release of Windows Phone. In the first release, an external tool had to be downloaded and used to generate the OData service proxies. In addition, the LINQ interpreter on the phone did not support closures on local variables, which meant that using a variable inside a LINQ expression caused a runtime failure. That made LINQ to OData on the phone mostly unusable. This has since been rectified, and service proxies can now be generated using the Add Service Reference dialog.
Before you see how to create an OData consumer using LINQ, you should become familiar with the OData URI syntax.
A URI used by an OData consumer has up to three significant parts, as follows:
• A service root URI
• A resource path
• Query string options
An example of an OData URI is http://ebayodata.cloudapp.net/Items?search=mobile&$top=5&$skip=5.
The components of this URI are then broken down as follows:
• http://ebayodata.cloudapp.net is the service root.
• /Items is the resource path.
• search=phone&$top=3&$skip=5 are the query string options.
If you use IE to navigate to this URI, you should see something like Figure 24.2. The page shows real eBay items for sale. The query retrieves three items—the sixth, seventh, and eighth items from a search result set—that match the phrase phone. This was done using the query string parameters search, $top, and $skip.
IE has a built-in feed reader that is enabled by default. A quick way to view the XML result is to right-click on the page and select View Source. This is a nuisance when developing, however, and you may prefer to disable the feed reader instead.
To disable the feed reader within IE, complete the following steps:
1. Select the Tools menu.
2. Select the Internet Options submenu.
3. Select the Content tab.
4. Click on the Settings button of Feed and Web Slices section to present the Feed Settings dialog.
5. Uncheck the Turn on Feed Reading View option.
6. Click OK, closing all dialog boxes.
Once the feed reader has been switched off, the entire XML query result can be viewed with syntax highlighting, as shown in Figure 24.3.
In this case, the format of the XML is an Atom feed. Some services are capable of supporting other formats, which can be specified using the $format query option.
To generate the proxies for the eBay OData service, right-click on the project node in the Visual Studio Solution Explorer and select Add Service Reference. The Add Service Reference is displayed, allowing you to enter the OData service URL (see Figure 24.4).
Changing the namespace of the generated service proxy and entity classes can be achieved by selecting Show All Files in the Visual Studio Solution Explorer and expanding the Service Reference node. Change the namespace by setting the Custom Tool Namespace property in the properties of the Reference.datasvcmap file using the Visual Studio properties pane.
To retrieve data from an OData service, a URI is constructed that specifies the resources that you are interested in—for example, Products—and you restrict the items returned by the service using various query options.
The following are the three types of OData query string options:
• System query options—System query options are used to control the amount and order of the data that an OData service returns for the resource identified by the URI. The names of all System Query Options are prefixed with a “$” character. Not all system query options need to be supported by a producer. If not supported, the server should return an HTTP status code 405 (Method Not Allowed). The following is a summary list of OData system query options defined by the standard:
• $orderby—This option allows you to order the results of a query by an entity property. For example, to retrieve the list of eBay product categories ordered by Name, the following query could be used: http://ebayodata.cloudapp.net/Categories?$orderby=Name.
• $top—This option allows you to select the first N items from a result set—where N is positive integer specified in the query. For example, the following query selects the first five items from the complete list of eBay items: http://ebayodata.cloudapp.net/Items?$top=5.
• $skip—This option allows you to select items that occur after a particular index in the result set and is useful when performing paging. By default, an OData service usually limits the number of items returned at any one time. In the case of the eBay OData service, this limit is 500 items. The $skip
option, in conjunction with the $top
option, can be used to page through results.
• $filter—This option allows you to restrict the results to those matching the specified criteria. The $filter
option supports various logical, arithmetic, and grouping operators, as well as a set of functions such as substringof
. See the OData website http://www.odata.org/developers/protocols/uri-conventions#QueryStringOptions for more information on the $filter
option.
• $expand—This option allows you to retrieve a graph of entities. For example, rather than retrieving the first three product categories and then the products within those three categories in another query, by using the $expand
option you are able to retrieve them all at once. This is done as shown in the following example: http://services.odata.org/OData/OData.svc/Categories?$top=3&$expand=Products.
The syntax of an $expand
query option is a comma-separated list of navigation properties. Additionally, each navigation property can be followed by a forward slash and another navigation property to enable identifying a multilevel relationship.
• $format—This option allows you to specify the format of the response. Valid $format
values include Atom, XML, and JSON. The $format
value may also be set to a custom value specific to a particular OData service.
• $select—This option allows you to retrieve only the data that you are interested in and can be important for performance when working with OData on the phone. The value of a $select
option is a comma-separated list of selection clauses. Each selection clause may be a property name, navigation property name, or the “*” character. To select only the names of the top five search results for the phrase phone, the following query could be used: http://ebayodata.cloudapp.net/Items?search=phone&$top=5&$select=Title.
• $inlinecount—This option allows you to retrieve a count of the number of items in a result set, along with the entity elements. When combined with a $top
option of 0 (specified by &$top=0
), you can retrieve the number of items in a set without having to go to the trouble of downloading unnecessary bytes.
• Custom query options—Custom query options are specific to the OData service that uses them. They differ in that they are not prefixed by a $ symbol. The following is an example of a custom query option, where xname is the parameter name, and xvalue is its value: http://services.odata.org/OData/OData.svc/Products?xname=xvalue.
• Service operation parameters—A service operation is a function specific to the particular OData service. Service operation parameters work much like custom query options, where key/value pairs are provided to a custom function as arguments. For example, a service operation exists on the OData sample website that retrieves all products with a particular rating. The rating is specified using the rating service operation parameter, as shown: http://services.odata.org/OData/OData.svc/GetProductsByRating?rating=5.
To query an OData producer, such as the eBay OData service, first create an instance of the generated OData model class, using the service root URI, like so:
EBayData ebayData = new EBayData(new Uri("http://ebayodata.cloudapp.net/"));
We then create a DataServiceCollection
, which is used to query the OData service asynchronously and to populate itself with the objects representing items in the response feed. The DataServiceCollection
type inherits from ObservableCollection
, which means it supports INotifyCollectionChanged
out of the box, and it can be used directly within the user interface.
To instantiate a DataServiceCollection
, an OData model instance is passed to its constructor, like so:
searchResult = new DataServiceCollection<Item>(ebayData);
Because the DataServiceCollection
queries the OData producer asynchronously, we subscribe to its LoadCompleted
handler to be notified when the query has completed, as shown in the following example:
searchResult.LoadCompleted
+= (sender, args) =>
{
if (args.Error != null)
{
/* Handle search error. */
}
};
Subscribing to the LoadCompleted
event is, however, optional. Regardless of whether it is handled, if an error does not occur, then the collection is populated automatically once the call completes.
A second URI is used to specify the resources and query options:
var itemsUri = new Uri("/Items?search=phone&$top=5&$select=Title",
UriKind.Relative);
This second URI is then used to fetch the results, using the LoadAsync
method of the DataServiceCollection
:
searchResult.LoadAsync(itemsUri);
Alternatively, a DataServiceQuery
can be created, which eliminates the manual creation of the request URI, as shown:
IQueryable<Item> serviceQuery
= ebayData.Items.AddQueryOption("search", "phone")
.AddQueryOption("$top", 5);
With an IQueryable
<
T
>
the search results can be further restricted using LINQ to OData, as demonstrated in the following example:
serviceQuery = from item in serviceQuery
where item.CurrentPrice > 100.0
select item;
searchResult.LoadAsync(serviceQuery);
When the query completes, the searchResult DataServiceCollection
is automatically populated with strongly typed objects representing the resource; in this case it produces eBay Item
objects.
Although OData is a standard, OData services differ in their coverage of query options; all query options are not provided by all services. Be sure to read any relevant documentation for the OData service that you intend to consume in your app.
The full API documentation for the eBay OData service can be found at http://ebayodata.cloudapp.net/docs.
This section creates an eBay search app that uses the eBay OData service to search for items and to present search results on a page. The section also looks at how to retrieve data on demand when the user scrolls to the bottom of the ListBox
.
The previous section looked at querying an OData service using the OData model class. While accessing the OData service model classes directly from a viewmodel is possible, it can make unit testing difficult because it relies on the actual OData producer.
In the sample for this chapter, an intermediary class called EbayClient
performs all OData queries. EbayClient
implements the custom IEbayClient
interface, shown in the following excerpt:
public interface IEbayClient
{
void SearchItemsAsync(string query,
int beginIndex = 0, int maxItems = 0);
event EventHandler<SearchItemsCompleteEventArgs> SearchItemsComplete;
}
By using the IEbayClient
interface and not a concrete implementation, we are able to substitute the EbayClient
implementation for a mock type to enable testing of the user interface without needing to send data over the wire.
The IEbayClient
interface is designed to be used asynchronously; the SearchItemsAsync
method has a void return type, and an event is used to signal that the call has completed. When the search completes, the SearchItemsComplete
method is raised using a custom extension method (see Listing 24.2).
The static HttpUtility.UrlEncode
method transforms any special characters entered by the user into URL compatible characters. This prevents the user from injecting OData query options or inadvertently causing the request to fail.
public class EbayClient : IEbayClient
{
DataServiceCollection<Item> searchResult;
public void SearchItemsAsync(string query,
int beginIndex = 0, int maxItems = 0)
{
EBayData ebayData
= new EBayData(new Uri("http://ebayodata.cloudapp.net/"));
searchResult = new DataServiceCollection<Item>(ebayData);
searchResult.LoadCompleted
+= (sender, args) =>
{
if (args.Error != null)
{
SearchItemsComplete.Raise(this,
new SearchItemsCompleteEventArgs(
null, query, args.Error));
return;
}
SearchItemsComplete.Raise(this,
new SearchItemsCompleteEventArgs(searchResult, query));
};
string parameter = HttpUtility.UrlEncode(query);
IQueryable<Item> serviceQuery
= ebayData.Items.AddQueryOption("search", parameter)
.AddQueryOption("$skip", beginIndex)
.AddQueryOption("$top", maxItems);
//serviceQuery = from item in serviceQuery
// where item.CurrentPrice > 100.0
// select item;
searchResult.LoadAsync(serviceQuery);
}
public event EventHandler<SearchItemsCompleteEventArgs> SearchItemsComplete;
}
The SearchItemsCompleteEventArgs
class extends the custom ResultEventArgs
, which contains the result produced by the OData proxy, the type of which is defined by a generic parameter. An optional parameter allows it to supply an Exception
instance if the call fails (see Listing 24.3).
public class ResultEventArgs<T> : EventArgs
{
public T Result { get; private set; }
public Exception Error { get; private set; }
public ResultEventArgs(T result, Exception error = null)
{
Result = result;
Error = error;
}
}
The SearchItemsCompleteEventArgs
adds a string Query
property, which identifies the original query (see Listing 24.4).
public class SearchItemsCompleteEventArgs
: ResultEventArgs<ObservableCollection<Item>>
{
public string Query { get; private set; }
public SearchItemsCompleteEventArgs(
ObservableCollection<Item> result, string query, Exception error = null)
: base(result, error)
{
Query = query;
}
}
The sample also comprises a view named EbaySearchView
and its viewmodel: EbaySearchViewModel
.
The viewmodel uses the IEbayClient
instance to query the eBay OData service. The viewmodel exposes two commands:
• SearchCommand—Triggered by entering text into a TextBox
in the view, this command calls the Search
method, which calls the IEbayClient
’s SearchItemsAsync
method.
• FetchMoreDataCommand—This command allows the search results to be paged.
The commands are initialized in the EbaySearchViewModel
constructor, which is shown in the following excerpt:
public EbaySearchViewModel(
Func<IEbayClient> getEbayClientFunc,
INetworkConnectionMonitor networkConnectionMonitor)
{
this.networkConnectionMonitor = ArgumentValidator.AssertNotNull(
networkConnectionMonitor, "networkConnectionMonitor");
this.getEbayClientFunc = ArgumentValidator.AssertNotNull(
getEbayClientFunc, "getEbayClientFunc");
networkConnectionMonitor.NetworkConnectionChanged
+= (sender, args) => UpdateCommands();
SearchText = "star wars action figure";
searchCommand = new DelegateCommand<string>(
query =>
{
if (!string.IsNullOrEmpty(query))
{
Search(query);
}
}, obj => networkConnectionMonitor.Connected);
fetchMoreDataCommand = new DelegateCommand(
obj =>
{
if (!string.IsNullOrEmpty(lastQuery))
{
Search(lastQuery, true);
}
}, obj => networkConnectionMonitor.Connected);
}
The enabled state of each command depends on the Connected
property of the INetworkConnectionMonitor
. When the NetworkConnectionChanged
event is raised, the viewmodel’s UpdateCommands
method is called, which calls the RaiseCanExecuteChanged
method on each command, causing their Enabled
state to be reevaluated:
void UpdateCommands()
{
searchCommand.RaiseCanExecuteChanged();
fetchMoreDataCommand.RaiseCanExecuteChanged();
}
The viewmodel’s Search
method uses a Func
named getEbayClientFunc
to retrieve an IEbayClient
(see Listing 24.5). The Search
method sets the viewmodel’s Busy
property to true, causing a PerformanceProgressBar
in the view to be made visible.
To reduce the amount of data retrieved from the OData service, the number of records retrieved by the IEbayClient
is restricted to 10.
const int chunkSize = 10;
string lastQuery;
bool appendResult;
void Search(string query, bool append = false)
{
if (lastQuery == query && Busy)
{
return;
}
lastQuery = query;
appendResult = append;
IEbayClient ebayClient = getEbayClientFunc();
ebayClient.SearchItemsComplete -= HandleEbayClientSearchItemsComplete;
ebayClient.SearchItemsComplete += HandleEbayClientSearchItemsComplete;
int beginIndex = appendResult ? Items.Count : 0;
try
{
Busy = true;
ebayClient.SearchItemsAsync(query, beginIndex, chunkSize);
}
catch (Exception ex)
{
Busy = false;
Console.WriteLine("Unable to perform search." + ex);
MessageService.ShowError("Unable to perform search.");
}
}
If the call to SearchItemsAsync
fails immediately, then the ViewModelBase
class’s MessageService
is used to present a dialog to the user.
For more information on the MessageService
, see Chapter 2, “Fundamental Concepts in Silverlight Development for Windows Phone.”
When the search completes, the HandleEbayClientSearchItemsComplete
handler is called. Old requests are ignored. The method returns immediately if the search query does not match the last query (see Listing 24.6).
If the search was not a new search but a request to fetch the next page of the current search results, the results are added to the end of the Items
collection.
void HandleEbayClientSearchItemsComplete(
object sender, SearchItemsCompleteEventArgs args)
{
/* Ignore old query results. */
if (args.Query != lastQuery)
{
return;
}
Busy = false;
if (args.Error == null)
{
if (args.Result.Count > 0)
{
if (appendResult)
{
foreach (var item in args.Result)
{
Items.Add(item);
}
}
else
{
Items = new ObservableCollection<Item>(args.Result);
}
}
else
{
MessageService.ShowMessage("No match found.");
}
}
else
{
MessageService.ShowError(
"An error occured while attempting to search.");
}
}
When the viewmodel is created within the view, it receives a Func
that allows it to resolve the EbayClient
(see Listing 24.7).
The view contains a TextBox
to allow the user to enter a search query.
The view subscribes to the KeyUp
event of the TextBox
, which allows the app to detect when the user taps the Enter key on the Software Input Panel (SIP) and to execute the viewmodel
object’s SearchCommand
.
public partial class EbaySearchView : PhoneApplicationPage
{
public EbaySearchView()
{
InitializeComponent();
DataContext = new EbaySearchViewModel(
() => new EbayClient(), new NetworkConnectionMonitor());
}
EbaySearchViewModel ViewModel
{
get
{
return (EbaySearchViewModel)DataContext;
}
}
void TextBox_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
e.Handled = true;
TextBox textBox = (TextBox)sender;
this.Focus();
ViewModel.SearchCommand.Execute(textBox.Text);
}
}
}
The view contains a ListBox
for displaying search results (see Listing 24.8). The ListBox
contains a custom attached property ScrollViewerMonitor.AtEndCommand
, which automatically causes the ListBox
to be populated with more records when the user scrolls to end of the list. The ScrollViewerMonitor
class is discussed in detail in the next section.
<Grid x:Name="ContentPanel" Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox Text="{Binding SearchText}"
KeyUp="TextBox_KeyUp" />
<ListBox Grid.Row="1"
ItemsSource="{Binding Items}"
u:ScrollViewerMonitor.AtEndCommand="{Binding FetchMoreDataCommand}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="14,5,4,10">
<!-- Content omitted. -->
<Image Source="{Binding GalleryUrl}"
MaxWidth="100" MaxHeight="100"
Margin="0, 0, 10, 0"
Grid.RowSpan="4" />
<TextBlock Text="{Binding Title}"
Grid.Column="1" Grid.ColumnSpan="3"
Style="{StaticResource PhoneTextSmallStyle}" />
<TextBlock Text="current price:"
Grid.Row="1" Grid.Column="1"
HorizontalAlignment="Right" />
<TextBlock Text="{Binding CurrentPrice,
Converter={StaticResource StringFormatConverter},
ConverterParameter={0:C}}"
Grid.Row="1" Grid.Column="2"
Margin="10,0,0,0" />
<TextBlock Text="time left:"
Grid.Row="2" Grid.Column="1"
HorizontalAlignment="Right" />
<TextBlock Text="{Binding TimeLeftCustom}"
Grid.Row="2" Grid.Column="2"
Margin="10,0,0,0" />
<HyperlinkButton NavigateUri="{Binding ViewItemUrl}"
Content="view"
TargetName="_blank"
Margin="0, 0, 10, 0"
Grid.Row="2" Grid.Column="3"
HorizontalAlignment="Left" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Grid Grid.Row="2"
Visibility="{Binding Busy,
Converter={StaticResource BooleanToVisibilityConverter}}">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="Loading..."
Style="{StaticResource LoadingStyle}"/>
<PerformanceProgressBar IsIndeterminate="True"
VerticalAlignment="Bottom"
Grid.Row="1" />
</Grid>
</Grid>
In the grid row beneath the ListBox
there is another Grid
containing a TextBlock
and a ProgressBar
. This grid becomes visible when the viewmodel’s Busy
property is true.
When text is entered into the search query TextBox
and the SIP’s Enter button is tapped, the viewmodel’s SearchCommand
is executed (see Figure 24.5).
The eBay logo is displayed using a TextBlock
located in the title panel with a Run
for each letter of the eBay logo, as shown in the following excerpt:
<StackPanel Grid.Row="0" Style="{StaticResource PageTitlePanelStyle}">
<TextBlock Text="Windows Phone 7 Unleashed"
Style="{StaticResource PhoneTextAppTitleStyle}" />
<TextBlock Style="{StaticResource PhoneTextPageTitleStyle}"
FontFamily="{StaticResource LogoFontFamily}">
<Run Foreground="#ff0000">e</Run><Run Foreground="#000099">b</Run>
<Run Foreground="#ffcc00">a</Run><Run Foreground="#99cc00">y</Run>
<Run FontFamily="{StaticResource PhoneFontFamilyNormal}">search</Run>
</TextBlock>
</StackPanel>
For more information on the TextBlock
and Run
classes, see Chapter 6, “Text Elements.”
The next section demonstrates how the viewmodel’s FetchMoreDataCommand
is executed when the user scrolls to the end of the list.
Most phone users are concerned about network usage. Network traffic comes at a premium, and a user’s perception of the quality of your app depends a lot on its responsiveness. When it comes to fetching data from a network service, it should be done in the most efficient way possible. Making the user wait while your app downloads a lot of data is a bad idea. Instead, data should be retrieved in bite-sized chunks.
I have created a ScrollViewerMonitor
class that uses an attached property to monitor a ListBox
and fetch data as the user needs it. You use it by adding an attached property to a control that contains a ScrollViewer
, such as a ListBox
, as shown in the following example:
<ListBox ItemsSource="{Binding Items}"
u:ScrollViewerMonitor.AtEndCommand="{Binding FetchMoreDataCommand}" />
The AtEndCommand
property specifies a command that is executed when the user scrolls to the end of the list.
The ScrollViewerMonitor
works by retrieving the first child ScrollViewer
control from its target (usually a ListBox
). It then listens to its VerticalOffset
property for changes. When a change occurs and the ScrollableHeight
of the scrollViewer
is the same as the VerticalOffset
, the AtEndCommand
is executed (see Listing 24.9).
The VerticalOffset
property is a dependency property, and to monitor it for changes I borrowed some of Pete Blois’s code (http://blois.us/), which allows you to detect changes to any dependency property. This class is called BindingListener
and is located in the downloadable sample code.
public class ScrollViewerMonitor
{
public static DependencyProperty AtEndCommandProperty
= DependencyProperty.RegisterAttached(
"AtEndCommand", typeof(ICommand),
typeof(ScrollViewerMonitor),
new PropertyMetadata(OnAtEndCommandChanged));
public static ICommand GetAtEndCommand(DependencyObject obj)
{
return (ICommand)obj.GetValue(AtEndCommandProperty);
}
public static void SetAtEndCommand(DependencyObject obj, ICommand value)
{
obj.SetValue(AtEndCommandProperty, value);
}
public static void OnAtEndCommandChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FrameworkElement element = (FrameworkElement)d;
if (element != null)
{
element.Loaded -= element_Loaded;
element.Loaded += element_Loaded;
}
}
static void element_Loaded(object sender, RoutedEventArgs e)
{
FrameworkElement element = (FrameworkElement)sender;
element.Loaded -= element_Loaded;
ScrollViewer scrollViewer = FindChildOfType<ScrollViewer>(element);
if (scrollViewer == null)
{
throw new InvalidOperationException("ScrollViewer not found.");
}
var listener = new DependencyPropertyListener();
listener.Changed
+= delegate
{
bool atBottom = scrollViewer.ScrollableHeight > 0
&& scrollViewer.VerticalOffset
>= scrollViewer.ScrollableHeight;
if (atBottom)
{
var atEnd = GetAtEndCommand(element);
if (atEnd != null)
{
atEnd.Execute(null);
}
}
};
Binding binding = new Binding("VerticalOffset") {
Source = scrollViewer };
listener.Attach(scrollViewer, binding);
}
static T FindChildOfType<T>(DependencyObject root) where T : class
{
var queue = new Queue<DependencyObject>();
queue.Enqueue(root);
while (queue.Count > 0)
{
DependencyObject current = queue.Dequeue();
int start = VisualTreeHelper.GetChildrenCount(current) - 1;
for (int i = start; 0 <= i; i--)
{
var child = VisualTreeHelper.GetChild(current, i);
var typedChild = child as T;
if (typedChild != null)
{
return typedChild;
}
queue.Enqueue(child);
}
}
return null;
}
}
The EbaySearchViewModel
contains a FetchMoreDataCommand
. When the user scrolls to the bottom of the list, the command is executed, which then sets a Busy
flag and calls the network service asynchronously.
When adding a service reference to an OData service, the generated entities classes are made partial classes. This provides a useful extensibility point for extending the model and for providing custom logic within entities themselves.
The EbayModel.Item
class, for example, contains a TimeLeft
property. This property is provided as a string. If it was of type TimeSpan
, however, it would be easier to work with.
This section creates a new partial Item
class and, within it, a TimeSpan
property that converts the string value provided by the OData service into a TimeSpan
value.
The following is an example of the format used by the TimeLeft
string:
P0DT0H13M8S
The string contains tokens that indicate value positions for days, hours, minutes, and seconds. Unfortunately, this format is not compatible with the TimeSpan.Parse
method. We, therefore, need to break up the string into its constituents parts. This can be done using a regular expression.
When constructing regular expressions, they can quickly become complex. Having a decent work bench for constructing them can make life easier. I really like the free regular expression tool called Expresso (http://www.ultrapico.com). Expresso allows you to specify sample text to see whether your regular expression produces the appropriate result (see Figure 24.6). It also includes other interfaces that assist in the construction of regular expressions.
The regular expression to deconstruct the TimeLeft
value can be plugged in to a new partial Item
class (see Listing 24.10).
public partial class Item
{
public TimeSpan? TimeLeftCustom
{
get
{
return ConvertToTimeSpan(TimeLeft);
}
}
TimeSpan? ConvertToTimeSpan(string timeLeft)
{
Regex regex = new Regex(
@"P(?<Days>d+)DT(?<Hours>d+)H(?<Minutes>d+)M(?<Seconds>d+)S");
Match match = regex.Match(timeLeft);
if (match.Success)
{
string timeSpanString = string.Format("{0}.{1}:{2}:{3}",
match.Groups["Days"].Value,
match.Groups["Hours"].Value,
match.Groups["Minutes"].Value,
match.Groups["Seconds"].Value);
TimeSpan result;
if (TimeSpan.TryParse(timeSpanString, out result))
{
return result;
}
}
return null;
}
}
The TimeLeftCustom
property is now able to be used in the view, like any of the other Item
class’s properties.
This chapter began by looking at the types of network services available on the phone. It then looked at monitoring network connectivity. You saw how to determine the type of network connection that the phone is using: Wi-Fi or cellular, enabling you to tailor the amount of data traffic accordingly. You saw how GetIsNetworkAvailable
is not a reliable means of determining the network connection state.
Finally, the chapter focused on the OData protocol and how to create an app that consumes an OData service.
3.17.150.119