共用方式為


Managing application data in a Windows Store business app using C#, XAML, and Prism

[This article is for Windows 8.x and Windows Phone 8.x developers writing Windows Runtime apps. If you’re developing for Windows 10, see the latest documentation]

From: Developing a Windows Store business app using C#, XAML, and Prism for the Windows Runtime

Previous page | Next page

Learn how to manage application data including storing data, caching data, authenticating users, and retrieving data from a web service while minimizing the network traffic and battery life of the device the app is running on. The AdventureWorks Shopper reference implementation uses Prism for the Windows Runtime to customize the default Settings pane shown in the Settings charm.

Download

After you download the code, see Getting started using Prism for the Windows Runtime for instructions on how to compile and run the reference implementation, as well as understand the Microsoft Visual Studio solution structure.

You will learn

  • How to store data in the app data stores.
  • How to store passwords in the credential locker.
  • How to use the Settings charm to allow users to change app settings.
  • How to create data transfer objects to transfer data across a network boundary.
  • How to reliably retrieve data from a web service using data transfer objects.
  • How to cache data from a web service on disk.
  • How to perform credentials-based authentication between a Windows Store app and a web service.

Applies to

  • Windows Runtime for Windows 8.1
  • C#
  • Extensible Application Markup Language (XAML)

Making key decisions

Application data is data that the app itself creates and manages. It is specific to the internal functions or configuration of an app, and includes runtime state, user preferences, reference content, and other settings. App data is tied to the existence of the app and is only meaningful to that app. The following list summarizes the decisions to make when managing application data in your app:

  • Where and how should I store application data?
  • What type of data should I store as application data?
  • Do I need to provide a privacy policy for my app, and if so, where should it be displayed to users?
  • How many entries should I include in the Settings charm?
  • What data should be allowed to roam?
  • How should I implement a web service that a Windows Store app will connect to?
  • How should I authenticate users with a web service in a Windows Store app?
  • Should I cache data from the web service locally?

Windows Store apps should use app data stores for settings and files that are specific to each app and user. The system manages the data stores for an app, ensuring that they are kept isolated from other apps and users. In addition, the system preserves the contents of these data stores when the user installs an update to your app and removes the contents of these data stores completely and cleanly when your app is uninstalled.

Application data should not be used to store user data or anything that users might perceive as valuable and irreplaceable. The user's libraries and Microsoft OneDrive should be used to store this sort of information. Application data is ideal for storing app-specific user preferences, settings, reference data, and favorites. For more info see App data.

If your app uses or enables access to any Internet-based services, or collects or transmits any user's personal information, you must maintain a privacy policy. You are responsible for informing users of your privacy policy. The policy must comply with applicable laws and regulations, inform users of the information collected by your app and how that information is used, stored, secured, and disclosed, describe the controls that users have over the use and sharing of their information, and how they may access their information. You must provide access to your privacy policy in the app's settings as displayed in the Settings charm. If you submit your app to the Windows Store you must also provide access to your privacy policy in the Description page of your app on the Windows Store. For more info see App certification requirements for the Windows Store.

The top part of the Settings pane lists entry points for your app settings, with each entry point performing an action such as opening a flyout, or opening an external link. Similar or related options should be grouped together under one entry point in order to avoid adding more than four entry points. For more info see Guidelines for app settings.

Utilizing roaming application data in app is easy and does not require significant code changes. It is best to utilize roaming application data for all size-bound data and settings that are used to preserve a user's settings preferences. For more info see Guidelines for roaming application data.

There are a number of approaches for implementing a web service that a Windows Store app can connect to:

  • Microsoft Azure Mobile Services allow you to add a cloud-based service to your Windows Store app. For more info see Microsoft Azure Mobile Services Dev Center.
  • Windows Communication Foundation (WCF) enables you to develop web services based on SOAP. These services focus on separating the service from the transport protocol. Therefore, you can expose the same service using different endpoints and different protocols such as TCP, User Datagram Protocol (UDP), HTTP, Secure Hypertext Transfer Protocol (HTTPS), and Message Queuing. However, this flexibility comes at the expense of the extensive use of configuration and attributes, and the resulting infrastructure is not always easily testable. In addition, new client proxies need to be generated whenever the input or output model for the service changes.
  • The ASP.NET Web API allows you to develop web services that are exposed directly over HTTP, thus enabling you to fully harness HTTP as an application layer protocol. Web services can then communicate with a broad set of clients whether they are apps, browsers, or back-end services. The ASP.NET Web API is designed to support apps built with REST, but it does not force apps to use a RESTful architecture. Therefore, if the input or output model for the service changes, the client simply has to change the query string that is sent to the web service, or parse the data received from the web service differently.

The primary difference between WCF and the ASP.NET Web API is that while WCF is based on SOAP, the ASP.NET Web API is based on HTTP. HTTP offers the following advantages:

  • It supports verbs that define actions. For example, you query information using GET, and create information using POST.
  • It contains message headers that are meaningful and descriptive. For example, the headers suggest the content type of the message's body.
  • It contains a body that can be used for any type of content, not just XML content as SOAP enforces. The body of HTTP messages can be anything you want including HTML, XML, JavaScript Object Notation (JSON), and binary files.
  • It uses Uniform Resource Identifiers (URIs) to identify both resources and actions.

The decision of whether to use WCF or the ASP.NET Web API in your app can be made by answering the following the following questions:

  • Do you want to create a service that supports special scenarios such as one-way messaging, message queues, and duplex communication? If so you should use WCF.
  • Do you want to a create service that uses fast transport channels when available, such as TCP, named pipes, or UDP? If so you should use WCF.
  • Do you want to create a service that uses fast transport channels when available, but uses HTTP when all other transport channels are unavailable? If so you should use WCF.
  • Do you want to simply serialize objects and deserialize them as the same strongly-typed objects at the other side of the transmission? If so you should use WCF.
  • Do you need to use a protocol other than HTTP? If so you should use WCF.
  • Do you want to create a resource-oriented service that is activated through simple action-oriented verbs such as GET, and that responds by sending content as HTML, XML, a JSON string, or binary data? If so you should use the ASP.NET Web API.
  • Do you have bandwidth constraints? If so you should use the ASP.NET Web API with JSON, as it sends a smaller payload than SOAP.
  • Do you need to support clients that don't have a SOAP stack? If so you should use the ASP.NET Web API.

There are a number of approaches that could be taken to authenticate users of a Windows Store app with a web service. For instance, credentials-based authentication or single sign-on with a Microsoft account could be used. A user can link a local Microsoft Windows account with his or her Microsoft account. Then, when the user signs in to a device using that Microsoft account, any Windows Store app that supports Microsoft account sign-in can automatically detect that the user is already authenticated and the app doesn't require the user to sign in app. The advantage of this approach over credential roaming is that the Microsoft account works for websites and apps, meaning that app developers don't have to create their own authentication system. Alternatively, apps could use the web authentication broker instead. This allows apps to use internet authentication and authorization protocols like Open Identification (OpenID) or Open Authentication (OAuth) to connect to online identity providers. This isolate's the user's credentials from the app, as the broker is the facilitator that communicates with the app. For more info see Managing user info.

Local caching of web service data should be used if you repeatedly access static data or data that rarely changes, or when data access is expensive in terms of creation, access, or transportation. This brings many benefits including improving app performance by storing relevant data as close as possible to the data consumer, and saving network and battery resources.

[Top]

Managing application data in AdventureWorks Shopper

The AdventureWorks Shopper reference implementation uses app data stores to store the user's credentials and cached data from the web service. The user's credentials are roamed. For more info see Storing data in the app data stores and Roaming application data.

AdventureWorks Shopper provides access to its privacy policy in the app's settings as displayed in the Settings charm. The privacy policy is one of several entry points in the Settings charm, and informs users of the personal information that is transmitted, how that information is used, stored, secured, and disclosed. It describes the controls that users have over the use and sharing of their information and how they may access their information. For more info see Local application data.

AdventureWorks Shopper uses the ASP.NET Web API to implement its web service, and performs credentials-based authentication with this web service. This approach creates a web service that can communicate with a broad set of clients including apps, browsers, or back-end services. Product data from the web service is cached locally in the temporary app data store. For more info see Accessing data through a web service and Caching data from a web service.

[Top]

Storing data in the app data stores

When an app is installed, the system gives it its own per-user data stores for application data such as settings and files. The lifetime of application data is tied to the lifetime of the app. If the app is removed, all of the application data will be lost.

There are three data stores for application data:

  • The local data store is used for persistent data that exists only on the device.
  • The roaming data store is used for data that exists on all trusted devices on which the user has installed the app.
  • The temporary data store is used for data that could be removed by the system at any time.

You use the application data API to work with application data with the system being responsible for managing its physical storage.

Settings in the app data store are stored in the registry. When you use the application data API, registry access is transparent. Within its app data store each app has a root container for settings. Your app can add settings and new containers to the root container.

Files in the app data store are stored in the file system. Within its app data store, each app has system-defined root directories—one for local files, one for roaming files, and one for temporary files. Your app can add new files and new directories to the root directory.

App settings and files can be local or roaming. The settings and files that your app adds to the local data store are only present on the local device. The system automatically synchronizes settings and files that your app adds to the roaming data store on all trusted devices on which the user has installed the app.

For more info see Accessing app data with the Windows Runtime.

Local application data

Local application data should be used to store data that needs to be preserved between application sessions, and it is not suitable type or size wise for roaming data. There is no size restriction on local data.

In the AdventureWorks Shopper reference implementation only the SessionStateService class stores data in the local application data store. For more info see Handling suspend, resume, and activation.

For more info see Quickstart: Local application data.

Roaming application data

If you use roaming data in your app, and a user installs your app on multiple devices, Windows keeps the application data in sync. Windows replicates roaming data to the cloud when it is updated and synchronizes the data to the other trusted devices on which the app is installed. This provides a desirable user experience, since the app on different devices is automatically configured according to the user preferences on the first device. Any future changes to the settings and preferences will also transition automatically. Windows can also transition session or state information. This enables users to continue to use an app session that was abandoned on one device when they transfer to a second device.

Roaming data should be used for all size-bound data and settings that are used to preserve a user's settings preferences as well as app session state. Any data that is only meaningful on a specific device, such as the path to a local file, should not be roamed.

Each app has a quota for roaming application data that is defined by the ApplicationData.RoamingStorageQuota property. If your roaming data exceeds the quota it won't roam until its size is less than the quota again. In AdventureWorks Shopper, we wanted to use roaming data to transfer partially completed shopping cart data to other devices when the initial device is abandoned. However, this was not feasible due to the enforced quota. Instead, this functionality is provided by the web service that the AdventureWorks Shopper reference implementation connects to. The data that roams in AdventureWorks Shopper are the user's credentials.

Note  Roaming data for an app is available in the cloud as long as it is accessed by the user from some device within 30 days. If the user does not run an app for longer than 30 days, its roaming data is removed from the cloud. If the user uninstalls an app, its roaming data isn't automatically removed from the cloud. If the user reinstalls the app within 30 days, the roaming data is synchronized from the cloud.

 

Windows roams app data opportunistically and so an instant sync is not guaranteed. For time critical settings a special high priority settings unit is available that provides more frequent updates. It is limited to one specific setting that must be named "HighPriority." It can be a composite setting, but the total size is limited to 8KB. This limit is not enforced and the setting will be treated as a regular setting, meaning that it will be roamed under regular priority, in case the limit is exceeded. However, if you are using a high latency network, roaming could still be significantly delayed.

For more info see Guidelines for roaming application data.

Storing and roaming user credentials

Apps can store the user's password in the credential locker by using the Windows.Security.Credentials namespace. The credential locker provides a common approach for storing and managing passwords in a protected store. However, passwords should only be saved in the credential locker if the user has successfully signed in and opted to save passwords.

Note  The credential locker should only be used for storing passwords and not for other items of data.

 

A credential in the credential locker is associated with a specific app or service. Apps and services do not have access to credentials associated with other apps or services. The credential locker from one trusted device is automatically transferred to any other trusted device for that user. This means that credential roaming is enabled by default for credentials stored in the credential locker on non-domain joined devices. Credentials from local connected accounts on domain-joined computers can roam. However, domain-connected accounts are subject to roaming restrictions if the credentials have only been saved on the domain-joined device.

You can enable credential roaming by connecting your device to the cloud by using your Microsoft account. This allows your credentials to roam to all of your trusted devices whenever you sign in with a Microsoft account.

Note  Data stored in the credential locker will only roam if a user has made a device trusted.

 

The ICredentialStore interface, provided by the Microsoft.Practices.Prism.StoreApps library, defines method signatures for loading and saving credentials. The following code example shows this interface.

Microsoft.Practices.Prism.StoreApps\ICredentialStore.cs

public interface ICredentialStore
{
    void SaveCredentials(string resource, string userName, string password);
    PasswordCredential GetSavedCredentials(string resource);
    void RemoveSavedCredentials(string resource);
}

This interface is implemented by the RoamingCredentialStore class in the AdventureWorks.UILogic project.

The user is invited to enter their credentials on the sign in flyout, which can be invoked from the Settings charm, or on the sign in dialog. When the user selects the Submit button on the SignInFlyOut view, the SignInCommand in the SignInFlyOutViewModel class is executed, which in turns calls the SignInAsync method. This method then calls the SignInUserAsync method on the AccountService instance, which in turn calls the LogOnAsync method on the IdentityServiceProxy instance. The instance of the AccountService class is created by the Unity dependency injection container. Then, provided that the credentials are valid and the user has opted to save the credentials, they are stored in the credential locker by calling the SaveCredentials method in the RoamingCredentialStore instance. The following code example shows how the RoamingCredentialStore class implements the SaveCredentials method to save the credentials in the credential locker.

AdventureWorks.UILogic\Services\RoamingCredentialStore.cs

public void SaveCredentials(string resource, string userName, string password)
{
    var vault = new PasswordVault();

    RemoveAllCredentialsByResource(resource, vault);

    // Add the new credential 
    var passwordCredential = new PasswordCredential(resource, userName, password);
    vault.Add(passwordCredential); 
}

The SaveCredentials method creates a new instance of the PasswordVault class that represents a credential locker of credentials. The old stored credentials for the app are retrieved and removed before the new credentials are added to the credential locker.

For more info see Credential Locker Overview and How to store user credentials.

Temporary application data

Temporary application data should be used for storing temporary information during an application session. The temporary data store works like a cache and its files do not roam. The System Maintenance task can automatically delete data at this location at any time, and the user could also clear files from the temporary data store using Disk Cleanup.

For more info about how AdventureWorks Shopper uses the temporary app data store see Caching data from a web service.

[Top]

Exposing settings through the Settings charm

The Settings charm is a fundamental part of any Windows Store app, and is used to expose app settings. It is invoked by making a horizontal edge gesture, swiping left with a finger or stylus from the right of the screen. This displays the charms and you can then select the Settings charm to display the Settings pane. The Settings pane includes both app and system settings.

The top part of the Settings pane lists entry points for your app settings. Each entry point opens a Settings flyout that displays the settings themselves. Entry points let you create categories of settings, grouping related controls together. Windows provides the Permissions and Rate and review entry points for apps that have been installed through the Windows Store. Side-loaded apps do not have the Rate and review entry point. The following diagram shows the top part of the default Settings pane for AdventureWorks Shopper.

Additional app settings are shown when a user is logged into the app. The bottom part of the Settings pane includes device settings provided by the system, such as volume, brightness, and power.

In order to customize the default Settings pane you can add a SettingsCommand that represents a settings entry. In the AdventureWorks Shopper reference implementation this is performed by the MvvmAppBase class in the Microsoft.Practices.Prism.StoreApps library. The InitializeFrameAsync method in the MvvmAppBase class subscribes to the CommandsRequested event of the SettingsPane class that is raised when the user opens the Settings pane. This is shown in the following code example.

Microsoft.Practices.Prism.StoreApps\MvvmAppBase.cs

SettingsPane.GetForCurrentView().CommandsRequested += OnCommandsRequested;

When the event is raised the OnCommandsRequested event handler in the MvvmAppBase class creates a SettingsCommand collection, as shown in the following code example.

Microsoft.Practices.Prism.StoreApps\MvvmAppBase.cs

private void OnCommandsRequested(SettingsPane sender, SettingsPaneCommandsRequestedEventArgs args)
{
    if (args == null || args.Request == null || args.Request.ApplicationCommands == null)
    {
        return;
    }

    var applicationCommands = args.Request.ApplicationCommands;
    var settingsCommands = GetSettingsCommands();

    foreach (var settingsCommand in settingsCommands)
    {
        applicationCommands.Add(settingsCommand);
    }
}

This method retrieves the SettingsCommand collection and adds each SettingsCommand to the ApplicationCommands. All the SettingsCommands will be shown on the Settings pane before the Permissions entry point.

The SettingsCommands for the app are defined by the GetSettingsCommands override in the App class, as shown in the following code example.

AdventureWorks.Shopper\App.xaml.cs

protected override IList<SettingsCommand> GetSettingsCommands()
{
    var settingsCommands = new List<SettingsCommand>();
    var accountService = _container.Resolve<IAccountService>();
    var resourceLoader = _container.Resolve<IResourceLoader>();
    var eventAggregator = _container.Resolve<IEventAggregator>();

    if (accountService.SignedInUser == null)
    {
        settingsCommands.Add(new SettingsCommand(Guid.NewGuid().ToString(), resourceLoader.GetString("LoginText"), (c) => new SignInFlyout(eventAggregator).Show()));
    }
    else
    {
        settingsCommands.Add(new SettingsCommand(Guid.NewGuid().ToString(), resourceLoader.GetString("LogoutText"), (c) => new SignOutFlyout().Show()));
        settingsCommands.Add(new SettingsCommand(Guid.NewGuid().ToString(), resourceLoader.GetString("AddShippingAddressTitle"), (c) => NavigationService.Navigate("ShippingAddress", null)));
        settingsCommands.Add(new SettingsCommand(Guid.NewGuid().ToString(), resourceLoader.GetString("AddBillingAddressTitle"), (c) => NavigationService.Navigate("BillingAddress", null)));
        settingsCommands.Add(new SettingsCommand(Guid.NewGuid().ToString(), resourceLoader.GetString("AddPaymentMethodTitle"), (c) => NavigationService.Navigate("PaymentMethod", null)));
        settingsCommands.Add(new SettingsCommand(Guid.NewGuid().ToString(), resourceLoader.GetString("ChangeDefaults"), (c) => new ChangeDefaultsFlyout().Show()));
    }
    settingsCommands.Add(new SettingsCommand(Guid.NewGuid().ToString(), resourceLoader.GetString("PrivacyPolicy"), async (c) => await Launcher.LaunchUriAsync(new Uri(resourceLoader.GetString("PrivacyPolicyUrl")))));
    settingsCommands.Add(new SettingsCommand(Guid.NewGuid().ToString(), resourceLoader.GetString("Help"), async (c) => await Launcher.LaunchUriAsync(new Uri(resourceLoader.GetString("HelpUrl")))));

    return settingsCommands;
}

Each SettingsCommand defines an item that is used to populate the Settings pane. In the AdventureWorks Shopper reference implementation they allow one of three possible actions to occur—a flyout to be shown, in-app navigation to take place, or an external hyperlink to be launched.

When a user selects the Login entry point, the SignInFlyout must be displayed. This flyout class derives from the SettingsFlyout class, which provides in-context access to settings that affect the current app. The SettingsFlyout class provides the light dismiss behavior that's seen throughout Windows. Therefore, when the user selects a UI element that is not part of the flyout, the flyout automatically dismisses itself.

For more info see Guidelines for app settings.

Creating data transfer objects

A data transfer object (DTO) is a container for a set of aggregated data that needs to be transferred across a network boundary. DTOs should contain no business logic and limit their behavior to activities such as validation.

Using the Model-View-ViewModel pattern describes the Model-View-ViewModel (MVVM) pattern used in AdventureWorks Shopper. The model elements of the pattern are contained in the AdventureWorks.UILogic and AdventureWorks.WebServices projects, which represent the domain entities used in the app. The following diagram shows the key model classes in the AdventureWorks.UILogic project, and the relationships between them.

The repository and controller classes in the AdventureWorks.WebServices project accept and return the majority of these model objects. Therefore, they are used as DTOs that hold all the data that is passed between the app and the web service. The benefits of using DTOs to pass data to and receive data from a web service are that:

  • By transmitting more data in a single remote call, the app can reduce the number of remote calls. In most scenarios, a remote call carrying a larger amount of data takes virtually the same time as a call that carries only a small amount of data.
  • Passing more data in a single remote call more effectively hides the internals of the web service behind a coarse-grained interface.
  • Defining a DTO can help in the discovery of meaningful business objects. When creating DTOs, you often notice groupings of elements that are presented to a user as a cohesive set of information. Often these groups serve as useful prototypes for objects that describe the business domain that the app deals with.
  • Encapsulating data into a serializable object can improve testability.

For more info about how the model classes are used as DTOs see Consuming data from a web service using DTOs.

[Top]

Accessing data through a web service

Web services extend the World Wide Web infrastructure to provide the means for software to connect to other software apps. Apps access web services via ubiquitous web protocols and data formats such as HTTP, XML, SOAP, with no need to worry about how the web service is implemented.

Connecting to a web service from a Windows Store app introduces a set of development challenges:

  • The app must minimize the use of network bandwidth.
  • The app must minimize its impact on the device's battery life.
  • The web service must offer an appropriate level of security.
  • The web service must be easy to develop against.
  • The web service should potentially support a range of client platforms.

These challenges will be addressed in the following sections.

Note  Windows 8.1 introduces the Windows.Web.Http namespace, which should be used for Windows Store apps that connect to HTTP and REST-based web services.

 

Consuming data

The AdventureWorks Shopper reference implementation stores data in an in-memory database that's accessed through a web service. The app must be able to send data to and receive data from the web service. For example, it must be able to retrieve product data in order to display it to the user, and it must be able to retrieve and send billing data and shopping cart data.

Users may be using AdventureWorks Shopper in a limited bandwidth environment, and so the developers wanted to limit the amount of bandwidth used to transfer data between the app and the web service. In addition to this, the developers wanted to ensure that the data transfer is reliable. Ensuring that data reliably downloads from the web service is important in ensuring a good user experience and hence maximizing the number of potential orders that will be made. Ensuring that shopping cart data reliably uploads to the web service is important in order to maximize actual orders, and their correctness.

The developers also wanted a solution that was simple to implement, and that could be easily customized in the future if, for example, authentication requirements were to change. In addition, the developers wanted a solution that could potentially work with platforms other than Windows.

With these requirements in mind, the AdventureWorks Shopper team had to consider three separate aspects of the solution: how to expose data from the web service, the format of the data that moves between the web service and the app, and how to consume web service data in the app.

Exposing data

The AdventureWorks Shopper reference implementation uses the ASP.NET Web API to implement its web service, and performs credentials-based authentication with this web service. This approach creates a resource-oriented web service that is activated through simple action-oriented verbs such as GET, and that can respond by sending content in a variety of formats such as HTML, XML, a JSON string, or binary data. The web service can communicate with a broad set of clients including apps, browsers, or back-end services. In addition, it offers the advantage that if the input or output model for the service changes in future, the app simply has to change the query string that is sent to the web service, or parse the data received from the web service differently.

Data formats

The AdventureWorks Shopper reference implementation uses the JSON format to transfer order data to the web service, and to cache web service data locally on disk, because it produces a compact payload that reduces bandwidth requirements and is relatively easy to use.

The AdventureWorks developers considered compressing data before transferring it to the web service in order to reduce bandwidth utilization, but decided that the additional CPU and battery usage on devices would outweigh the benefits. You should evaluate this tradeoff between the cost of bandwidth and battery consumption in your app before you decide whether to compress data you need to move over the network.

Note  Additional CPU usage affects both the responsiveness of the device and its battery life.

 

For more info about caching see Caching data from a web service.

Consuming data from a web service using DTOs

Analysis of the data transfer requirements revealed only limited interactions with the web service, so AdventureWorks Shopper implements a set of custom DTO classes to handle the data transfer with the web service. For more info see Creating data transfer objects. In order to further reduce the interaction with the web service, as much data as possible is retrieved in a single call to it. For example, instead of retrieving product categories in one web service call, and then retrieving products for a category in a second web service call, AdventureWorks Shopper retrieves a category and its products in a single web service call.

In the future, AdventureWorks may decide to use the OData protocol in order to use features such as batching and conflict resolution.

Note  AdventureWorks Shopper does not secure the web service with Secure Sockets Layer (SSL), so a malicious client could impersonate the app and send malicious data. In your own app, you should protect any sensitive data that you need to transfer between the app and a web service by using SSL.

 

The following diagram shows the interaction of the classes that implement reading product category data for the hub page in AdventureWorks Shopper.

The ProductCatalogRepository is used to manage the data retrieval process, either from the web service or from a temporary cache stored on disk. The ProductCatalogServiceProxy class is used to retrieve product category data from the web service, with the TemporaryFolderCacheService class being used to retrieve product category data from the temporary cache.

In the OnInitialize method in the App class, the ProductCatalogRepository class is registered as a type mapping against the IProductCatalogRepository type with the Unity dependency injection container. Similarly, the ProductCatalogServiceProxy class is registered as a type mapping against the IProductCatalogService type. Then, when a view model class such as the HubPageViewModel class accepts an IProductCatalogRepository type, the Unity container will resolve the type and return an instance of the ProductCatalogRepository class.

When the HubPage is navigated to, the OnNavigatedTo method in the HubPageViewModel class is called. The following example shows code from the OnNavigatedTo method, which uses the ProductCatalogRepository instance to retrieve category data for display on the HubPage.

AdventureWorks.UILogic\ViewModels\HubPageViewModel.cs

rootCategories = await _productCatalogRepository.GetRootCategoriesAsync(5);

The call to the GetRootCategoriesAsync method specifies the maximum amount of products to be returned for each category. This parameter can be used to optimize the amount of data returned by the web service, by avoiding returning an indeterminate number of products for each category.

The ProductCatalogRepository class, which implements the IProductCatalogRepository interface, uses instances of the ProductCatalogServiceProxy and TemporaryFolderCacheService classes to retrieve data for display on the UI. The following code example shows the GetSubCategoriesAsync method, which is called by the GetRootCategoriesAsync method, to asynchronously retrieve data from either the temporary cache on disk, or from the web service.

AdventureWorks.UILogic\Repositories\ProductCatalogRepository.cs

public async Task<ReadOnlyCollection<Category>> GetSubcategoriesAsync(int parentId, int maxAmountOfProducts)
{
    string cacheFileName = String.Format("Categories-{0}-{1}", parentId, maxAmountOfProducts);

    try
    {
        // Case 1: Retrieve the items from the cache
        return await _cacheService.GetDataAsync<ReadOnlyCollection<Category>>(cacheFileName);
    }
    catch (FileNotFoundException)
    { }

    // Retrieve the items from the service
    var categories = await _productCatalogService.GetCategoriesAsync(parentId, maxAmountOfProducts);

    // Save the items in the cache
    await _cacheService.SaveDataAsync(cacheFileName, categories);

    return categories;
}

The method first calls the GetDataAsync method in the TemporaryFolderCacheService class to check if the requested data exists in the cache, and if it does, whether it has expired or not. Expiration is judged to have occurred if the data is present in the cache, but it is more than 5 minutes old. If the data exists in the cache and hasn't expired it is returned, otherwise a FileNotFoundException is thrown. If the data does not exist in the cache, or if it exists and has expired, a call to the GetCategoriesAsync method in the ProductCatalogServiceProxy class retrieves the data from the web service before it is cached.

The ProductCatalogServiceProxy class, which implements the IProductCatalogService interface, is used to retrieve product data from the web service if the data is not cached, or the cached data has expired. The following code example shows the GetCategoriesAsync method that is invoked by the GetSubCategoriesAsync method in the ProductCatalogRepository class.

AdventureWorks.UILogic\Services\ProductCatalogServiceProxy.cs

public async Task<ReadOnlyCollection<Category>> GetCategoriesAsync(int parentId, int maxAmountOfProducts)
{
    using (var httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(new Uri(string.Format("{0}?parentId={1}&maxAmountOfProducts={2}", _categoriesBaseUrl, parentId, maxAmountOfProducts)));
        response.EnsureSuccessStatusCode();
        var responseContent = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<ReadOnlyCollection<Category>>(responseContent);

        return result;
    }
}

This method asynchronously retrieves the product categories from the web service by using the HttpClient class to send HTTP requests and receive HTTP responses from a URI. The call to HttpClient.GetAsync sends a GET request to the specified URI as an asynchronous operation, and returns a Task of type HttpResponseMessage that represents the asynchronous operation. The returned Task will complete after the content from the response is read. For more info about the HttpClient class see Connecting to an HTTP server using Windows.Web.Http.HttpClient.

When the GetCategoriesAsync method calls HttpClient.GetAsync this calls the GetCategories method in the CategoryController class in the AdventureWorks.WebServices project, which is shown in the following code example.

AdventureWorks.WebServices\Controllers\CategoryController.cs

public IEnumerable<Category> GetCategories(int parentId, int maxAmountOfProducts)
{
    var categories = _categoryRepository.GetAll().Where(c => c.ParentId == parentId);

    var trimmedCategories = categories.Select(NewCategory).ToList();
    FillProducts(trimmedCategories);

    foreach (var trimmedCategory in trimmedCategories)
    {
        var products = trimmedCategory.Products.ToList();
        if (maxAmountOfProducts > 0)
        {
            products = products.Take(maxAmountOfProducts).ToList();
        }
        trimmedCategory.Products = products;
    }

    return trimmedCategories;
}

This method uses an instance of the CategoryRepository class to return a static collection of Category objects that contain the category data returned by the web service.

Caching data from a web service

The AdventureWorks Shopper TemporaryFolderCacheService class is used to cache data from the web service to the temporary app data store used by the app. This helps to reduce communication with the web service, which minimizes the impact on the device's battery life. This service is used by the ProductCatalogRepository class to decide whether to retrieve products from the web service, or from the cache in the temporary app data store.

As previously mentioned, the GetSubCategoriesAsync method in the ProductCatalogRepository class is used to asynchronously retrieve data from the product catalog. When it does this it first attempts to retrieve cached data from the temporary app data store by calling the GetDataAsync method, which is shown in the following code example.

AdventureWorks.UILogic\Services\TemporaryFolderCacheService.cs

public async Task<T> GetDataAsync<T>(string cacheKey)
{
    await CacheKeyPreviousTask(cacheKey);
    var result = GetDataAsyncInternal<T>(cacheKey);
    SetCacheKeyPreviousTask(cacheKey, result);
    return await result;
}

private async Task<T> GetDataAsyncInternal<T>(string cacheKey)
{
    StorageFile file = await _cacheFolder.GetFileAsync(cacheKey);
    if (file == null) throw new FileNotFoundException("File does not exist");
    
    // Check if the file has expired
    var fileBasicProperties = await file.GetBasicPropertiesAsync();
    var expirationDate = fileBasicProperties.DateModified.Add(_expirationPolicy).DateTime;
    bool fileIsValid = DateTime.Now.CompareTo(expirationDate) < 0;
    if (!fileIsValid) throw new FileNotFoundException("Cache entry has expired.");

    string text = await FileIO.ReadTextAsync(file);
    var toReturn = Deserialize<T>(text);

    return toReturn;
}

The CacheKeyPreviousTask method ensures that since only one I/O operation at a time may access a cache key, cache read operations always wait for the prior task of the current cache key to complete before they start. The GetDataAsyncInternal method is called to see if the requested data exists in the cache, and if it does, whether it has expired or not.

The SaveDataAsync method in the TemporaryFolderCacheService class saves data retrieved from the web service to the cache, and is shown in the following code example.

AdventureWorks.UILogic\Services\TemporaryFolderCacheService.cs

public async Task SaveDataAsync<T>(string cacheKey, T content)
{
    await CacheKeyPreviousTask(cacheKey);
    var result = SaveDataAsyncInternal<T>(cacheKey, content);
    SetCacheKeyPreviousTask(cacheKey, result);
    await result;
}

private async Task SaveDataAsyncInternal<T>(string cacheKey, T content)
{
    StorageFile file = await _cacheFolder.CreateFileAsync(cacheKey, CreationCollisionOption.ReplaceExisting);

    var textContent = Serialize<T>(content);
    await FileIO.WriteTextAsync(file, textContent);
}

As with the read operations, since only one I/O operation at a time may access a cache key, cache write operations always wait for the prior task of the current cache key to complete before they start. The SaveDataAsyncInternal method is called to serialize the data from the web service to the cache.

Note  AdventureWorks Shopper does not directly cache images from the web service. Instead, we rely on the Image control’s ability to cache images and display them if the server responds with an image.

 

Authenticating users with a web service

The AdventureWorks Shopper web service needs to know the identity of the user who places an order. The app externalizes as much of the authentication functionality as possible. This provides the flexibility to make changes to the approach used to handle authentication in the future without affecting the app. For example, the approach could be changed to enable users to identify themselves by using a Microsoft account. It's also important to ensure that the mechanism that the app uses to authenticate users is easy to implement on other platforms.

Ideally the web service should use a flexible, standards-based approach to authentication. However, such an approach is beyond the scope of this project. The approach adopted here is that the app requests a password challenge string from the web service that it then hashes using the user's password as the key. This hashed data is then sent to the web service where it's compared against a newly computed hashed version of the password challenge string, using the user's password stored in the web service as the key. Authentication only succeeds if the app and the web service have computed the same hash for the password challenge string. This approach avoids sending the user's password to the web service.

Note  In the future, the app could replace the simple credentials authentication system with a claims-based approach. One option is to use the Simple Web Token and OAuth 2.0 protocol. This approach offers the following benefits:

  • The authentication process is managed externally from the app.
  • The authentication process uses established standards.
  • The app can use a claims-based approach to handle any future authorization requirements.

 

The following illustration shows the interaction of the classes that implement credentials-based authentication in the AdventureWorks Shopper reference implementation.

Credentials-based user authentication is performed by the AccountService and IdentityServiceProxy classes in the app, and by the IdentityController class in the web service. In the OnInitialize method in the App class the AccountService class is registered as a type mapping against the IAccountService type with the Unity dependency injection container. Then, when a view model class such as the SignInFlyoutViewModel class accepts an IAccountService type, the Unity container will resolve the type and return an instance of the AccountService class.

When the user selects the Submit button on the SignInFlyout, the SignInCommand in the SignInFlyOutViewModel class is executed, which in turn calls the SignInAsync method. This method then calls the SignInUserAsync method on the AccountService instance. If the sign in is successful, the SignInFlyOut view is closed. The following code example shows part of the SignInUserAsync method in the AccountService class.

AdventureWorks.UILogic\Services\AccountService.cs

var result = await _identityService.LogOnAsync(userName, password);

The SignInUserAsync method calls the LogOnAsync method in the instance of the IdentityServiceProxy class that's injected into the AccountService constructor from the Unity dependency injection container. The IdentityServiceProxy class, which implements the IIdentityService interface, uses the LogOnAsync method to authenticate user credentials with the web service. The following code example shows this method.

AdventureWorks.UILogic\Services\IdentityServiceProxy.cs

public async Task<LogOnResult> LogOnAsync(string userId, string password)
{
    using (var client = new HttpClient())
    {
        // Ask the server for a password challenge string
        var requestId = CryptographicBuffer.EncodeToHexString(CryptographicBuffer.GenerateRandom(4));
        var challengeResponse = await client.GetAsync(new Uri(_clientBaseUrl + "GetPasswordChallenge?requestId=" + requestId));
        challengeResponse.EnsureSuccessStatusCode();
        var challengeEncoded = await challengeResponse.Content.ReadAsStringAsync();
        challengeEncoded = challengeEncoded.Replace(@"""", string.Empty);
        var challengeBuffer = CryptographicBuffer.DecodeFromHexString(challengeEncoded);

        // Use HMAC_SHA512 hash to encode the challenge string using the password being authenticated as the key.
        var provider = MacAlgorithmProvider.OpenAlgorithm(MacAlgorithmNames.HmacSha512);
        var passwordBuffer = CryptographicBuffer.ConvertStringToBinary(password, BinaryStringEncoding.Utf8);
        var hmacKey = provider.CreateKey(passwordBuffer);
        var buffHmac = CryptographicEngine.Sign(hmacKey, challengeBuffer);
        var hmacString = CryptographicBuffer.EncodeToHexString(buffHmac);

        // Send the encoded challenge to the server for authentication (to avoid sending the password itself)
        var response = await client.GetAsync(new Uri(_clientBaseUrl + userId + "?requestID=" + requestId +"&passwordHash=" + hmacString));

        // Raise exception if sign in failed
        response.EnsureSuccessStatusCode();

        // On success, return sign in results from the server response packet
        var responseContent = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<UserInfo>(responseContent);
        var serverUri = new Uri(Constants.ServerAddress);
        return new LogOnResult { UserInfo = result };
    }
}

This method generates a random request identifier that is encoded as a hex string and sent to the web service. The GetPasswordChallenge method in the IdentityController class in the AdventureWorks.WebServices project receives the request identifier and responds with a hexadecimal encoded password challenge string that the app reads and decodes. The app then hashes the password challenge with the HMACSHA512 hash function, using the user's password as the key. The hashed password challenge is then sent to the web service for authentication by the GetIsValid method in the IdentityController class in the AdventureWorks.WebServices project. If authentication succeeds, a new instance of the LogOnResult class is returned by the method.

The LogOnAsync method communicates with the web service through calls to HttpClient.GetAsync, which sends a GET request to the specified URI as an asynchronous operation, and returns a Task of type HttpResponseMessage that represents the asynchronous operation. The returned Task will complete after the content from the response is read. For more info about the HttpClient class see Connecting to an HTTP server using Windows.Web.Http.HttpClient.

The IdentityController class, in the AdventureWorks.WebServices project, is responsible for sending hexadecimal encoded password challenge strings to the app, and for performing authentication of the hashed password challenges it receives from the app. The class contains a static Dictionary named Identities that contains the valid credentials for the web service. The following code example shows the GetIsValid method in the IdentityController class.

AdventureWorks.WebServices\Controllers\IdentityController.cs

public UserInfo GetIsValid(string id, string requestId, string passwordHash)
{
    byte[] challenge = null;
    if (requestId != null && ChallengeCache.Contains(requestId))
    {
        // Retrieve the saved challenge bytes
        challenge = (byte[])ChallengeCache[requestId];
        // Delete saved challenge (each challenge is used just one time).
        ChallengeCache.Remove(requestId);
    }

    lock (Identities)
    {
        // Check that credentials are valid.
        if (challenge != null && id != null && passwordHash != null && Identities.ContainsKey(id))
        {
            // Compute hash for the previously issued challenge string using the password from the server's credentials store as the key.
            var serverPassword = Encoding.UTF8.GetBytes(Identities[id]);
            using (var provider = new HMACSHA512(serverPassword))
            {
                var serverHashBytes = provider.ComputeHash(challenge);
                // Authentication succeeds only if client and server have computed the same hash for the challenge string.
                var clientHashBytes = DecodeFromHexString(passwordHash);
                if (!serverHashBytes.SequenceEqual(clientHashBytes))
                    throw new HttpResponseException(HttpStatusCode.Unauthorized);
            }

            if (HttpContext.Current != null)
                FormsAuthentication.SetAuthCookie(id, false);
            return new UserInfo { UserName = id };
        }
        else
        {
            throw new HttpResponseException(HttpStatusCode.Unauthorized);
        }
    }
}

This method is called in response to the LogOnAsync method sending a hashed password challenge string to the web service. The method retrieves the previously issued password challenge string that was sent to the app, and then removes it from the cache as each password challenge string is used only once. The retrieved password challenge is then hashed with the HMACSHA512 hash function, using the user's password stored in the web service as the key. The newly computed hashed password challenge string is then compared against the hashed challenge string received from the app. Authentication only succeeds if the app and the web service have computed the same hash for the password challenge string, in which case a new UserInfo instance containing the user name is returned to the LogOnAsync method.

Note  The Windows Runtime includes APIs that provide authentication, authorization and data security. For example, the AdventureWorks Shopper reference implementation uses the MacAlgorithmProvider class to securely authenticate user credentials over an unsecured channel. However, this is only one choice among many. For more info see Introduction to Windows Store app security.

 

[Top]