Chapter 24. Network Services


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.

Network Service Technologies

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.

Monitoring Network Connectivity

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.

Connection Priorities

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).

Image

Figure 24.1. Network connection related classes

The NetworkInterface.GetIsNetworkAvailable method allows you to determine whether any network connection is available; however, there is one caveat (see the following note).


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.

Monitoring Connection Events

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.


Note

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.”

Listing 24.1. NetworkConnectionMonitor Class


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.

Introduction to OData

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.

Consuming OData

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.

OData URI Structure

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.

Image

Figure 24.2. By default, IE presents the query result using its built-in feed reader.

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.

Image

Figure 24.3. Disabling the built-in feed reader allows you to view the XML.

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.

Generating an OData Proxy

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).

Image

Figure 24.4. Add Service Reference dialog is used to generate the OData service proxies.


Tip

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.


OData Query Options

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.

Using an OData Proxy

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.

Building an eBay OData Consumer Application

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.

Creating an OData Wrapper

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.

Listing 24.2. EbayClient Class


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).

Listing 24.3. ResultEventArgs Class


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).

Listing 24.4. SearchItemsCompleteEventArgs Class


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.

EbaySearchViewModel Class

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.

Listing 24.5. EbaySearchViewModel.Search Method and Related Members


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.

Listing 24.6. EbaySearchViewModel.HandleEbayClientSearchItemsComplete Method


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.");
    }
}


EbaySearchView Page

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.

Listing 24.7. EbaySearchView Class


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.

Listing 24.8. EbaySearchView.xaml (excerpt)


<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).

Image

Figure 24.5. The eBay search page

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.

Fetching Data When the User Scrolls to the End of a 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.

Listing 24.9. ScrollViewerMonitor Class


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.

Extending OData Entity Classes

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.

Image

Figure 24.6. Expresso, regular expression editor

The regular expression to deconstruct the TimeLeft value can be plugged in to a new partial Item class (see Listing 24.10).

Listing 24.10. Custom Item Class


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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.150.119