The sample app for this section is a banking app that allows the user to retrieve a list of bank cards from a WCF service and to add the cards to the phone’s Wallet.
Note
The sample for this chapter requires access to a locally deployed WCF service. Please see Chapter 2, “Fundamental Concepts in Windows Phone Development,” for information on working with locally deployed WCF services.
The app has the following three views:
MainPage.xaml
CardView.xaml
BillPayView.xaml
MainPage.xaml displays the user’s cards and allows the user to tap on a card, which then navigates the app to the CardView.xaml page. The CardView.xaml page displays the card details and allows the user to add the card to, or remove the card from, the Wallet. In addition, the CardView.xaml page allows the user to reduce the card’s available funds by performing a pretend purchase (see Figure 25.2).
The BillPayView.xaml page allows the user to replenish a card’s funds.
The sample app communicates with a WCF Service named BankService, which allows the user to retrieve a list of cards, make a credit card payment, get a list of transactions for a particular card, and to make purchases for credit and debit cards (see Listing 25.1).
The service uses two custom classes that represent a credit card and a debit card. The CreditCard
and DebitCard
classes both derive from the custom Card
class.
The service uses the new awaitable asynchronous features of .NET. Each service method is decorated with an OperationContract
attribute. The AsyncPattern
property indicates that an operation is implemented asynchronously using either a Begin<methodName>
and End<methodName>
method pair in the service contract or, as demonstrated in the sample, the new async keyword combined with a Task
return type.
[ServiceContract]
[ServiceKnownType(typeof(CreditCard))]
[ServiceKnownType(typeof(DebitCard))]
public interface IBankService
{
[OperationContract(AsyncPattern = true)]
Task<IEnumerable<Card>> GetCards();
[OperationContract(AsyncPattern = true)]
Task<PerformPaymentResult> PerformPayment(CreditCard creditCard, decimal amount);
[OperationContract(AsyncPattern = true)]
Task<IEnumerable<AccountTransaction>> GetCardTransactions(Guid cardId);
[OperationContract(AsyncPattern = true)]
Task<MakePurchaseResult> MakeCreditPurchase(CreditCard creditCard, decimal amount);
[OperationContract(AsyncPattern = true)]
Task<MakePurchaseResult> MakeDebitPurchase(DebitCard debitCard, decimal amount);
}
The MainPageViewModel
class retrieves the list of cards from the WCF service (see Listing 25.2).
The service reference code generation of the Windows Phone SDK does not, unfortunately, allow you to generate awaitable methods for asynchronous service methods (see Figure 25.3). Instead, you can use the Task
class’s Factory
object to create an awaitable task, which then allows the method to populate the viewmodel’s Card
property without the use of an event handler.
The Factory class’s FromAsync
method requires access to the BeginGetCards
and EndGetCards
methods of the IBankService
. These interface members are explicitly implemented in the service client. We therefore cast the BankServiceClient
object to the IBankService
interface to access the methods.
public async void Load()
{
var client = new BankServiceClient();
var bankService = (IBankService)client;
try
{
IEnumerable<Card> retrievedCards
= await Task<IEnumerable<Card>>.Factory.FromAsync(
bankService.BeginGetCards, bankService.EndGetCards, null);
Cards = new ObservableCollection<Card>(retrievedCards);
}
catch (Exception ex)
{
Debug.WriteLine("Unable to retrieve cards." + ex);
MessageService.ShowError("Unable to retrieve cards.");
}
}
The viewmodel retrieves the cards from the service and places them in an ObservableCollection
, which is later materialized in the view.
On the server side, the WCF BankService
sends back two dummy cards: a CreditCard
and a DebitCard
(see Listing 25.3). The async keyword means you no longer have to implement a BeginX
and EndX
method for the asynchronous service methods.
public class BankService : IBankService
{
readonly List<Card> cards = new List<Card>
{
new CreditCard
{
Id = Guid.Parse("520E4521-A010-4155-9C61-0B31A0F59C8B"),
AccountNumber = "XXXX-XXXX-XXXX-1234",
CardName = "Unleashed Credit", CustomerName = "Alan Turing",
CreditLimit = 1000, AvailableCredit = 1000,
ExpirationDate = new DateTime(2014, 1, 1),
CultureName = "en-US"
},
new DebitCard
{
Id = Guid.Parse("077C9304-9BD4-4527-8BB3-EED5F0A78284"),
AccountNumber = "XXXX-XXXX-XXXX-5432",
CardName = "Unleashed Debit", CustomerName = "Alan Turing",
Balance = 1000, ExpirationDate = new DateTime(2014, 1, 1),
CultureName = "en-GB"
}
};
public async Task<IEnumerable<Card>> GetCards()
{
return cards;
}
...
}
The custom Card
class has several properties, including an AccountNumber
property, a CardName
property, a CustomerName
property, and an ExpirationDate
property.
MainPage.xaml displays the cards returned from the WCF service using a ListBox
, as shown:
<ListBox ItemsSource="{Binding Cards}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel
v:Commanding.Command="{Binding Content.ViewCardCommand,
Source={StaticResource bridge}}"
v:Commanding.CommandParameter="{Binding}"
Margin="12,10,0,10">
<TextBlock Text="{Binding CardName}" />
<TextBlock Text="{Binding AccountNumber}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The custom commanding infrastructure handles the Tap
event of the StackPanel
, which causes the command’s Execute
method to be called. The DataTemplate
uses the bridge
resource to resolve the viewmodel of the page, which is outside the scope of the DataTemplate
. The bridge resource is a ContentControl
, defined in the page resources as shown:
<phone:PhoneApplicationPage.Resources>
<ContentControl x:Name="bridge"
Content="{Binding Path=DataContext, ElementName=page}" />
</phone:PhoneApplicationPage.Resources>
The MainPageViewModel
class contains a ViewCardCommand
property, so that when the user taps a card, the app navigates to the CardView.xaml page by using the viewmodel’s custom NavigationService
, as shown:
void ViewCard(Card card)
{
Navigate("/CardView/CardView.xaml?CardId=" + card.Id);
}
The CardView
page receives the ID of the card via a querystring parameter, which it then provides to the viewmodel’s Load
method (see Listing 25.4). The viewmodel parses the ID back to a Guid and, for the sake of simplicity, the Card
is retrieved from the entire list of Card
objects retrieved from the server.
The viewmodel’s Title
property is set to the CardName
property of the Card
.
public async void Load(IDictionary<string, string> queryDictionary)
{
string cardIdValue;
if (!queryDictionary.TryGetValue("CardId", out cardIdValue))
{
Debug.WriteLine("Dictionary does not contain CardId key.");
return;
}
Guid cardId;
if (!Guid.TryParse(cardIdValue, out cardId))
{
throw new Exception("CardId cannot be passed.");
}
var bankService = (IBankService)new BankServiceClient();
try
{
IEnumerable<Card> cards = await Task<IEnumerable<Card>>.Factory.FromAsync(
bankService.BeginGetCards, bankService.EndGetCards, null);
Card = cards.SingleOrDefault(c => c.Id == cardId);
if (card != null)
{
Title = card.CardName;
}
}
catch (Exception ex)
{
Debug.WriteLine("Unable to retrieve cards." + ex);
MessageService.ShowError("Unable to retrieve cards.");
}
Refresh();
}
The CardViewModel
class contains a ToggleInWallet DelegateCommand
that adds a PaymentInstrument
representing the Card
to the Wallet. If an item already exists for the card, the command removes the item from the Wallet.
Detecting the presence of a card in the Wallet is done using the Wallet
class’s static FindItem
method, as demonstrated in the following excerpt:
public bool InWallet
{
get
{
return card != null && Wallet.FindItem(card.Id.ToString()) != null;
}
}
The CardViewModel
’s AddToWallet
method constructs a PaymentInstrument
using the card information (see Listing 25.5).
You can add custom properties to Wallet item objects. This allows you to add domain-specific information that is stored alongside a card.
In the sample, the culture name of the card is stored as a custom property, which allows the app to display currency values using the correct currency symbol and format. If the card is a CreditCard
instance, its DisplayCreditLimit
and DisplayAvailableCredit
string properties are set using a CultureInfo
object to produce the appropriate currency format.
The PaymentInstrument
class’s Message
and MessageNavigationUri
allow you to display some text alongside the payment instrument’s logo in the Wallet. When the user taps the message in the Wallet, the Wallet navigates the user to a location in your app.
You must specify the DisplayName
property of the Wallet item, along with logo images for each of its three logo properties. Your app may retrieve the image from the server or, as in the case of the sample, retrieve it locally from a Content resource in the app’s main assembly.
The NavigationUri
property of the payment instrument is used to launch your app when the user taps the Open App link in the instrument’s About page in the Wallet.
void AddToWallet()
{
var instrument = new PaymentInstrument(card.Id.ToString())
{
AccountNumber = card.AccountNumber,
CustomerName = card.CustomerName,
ExpirationDate = card.ExpirationDate,
DisplayName = card.CardName
};
/* The culture name is set as a custom wallet property. */
var cultureCodeProperty = new CustomWalletProperty { Value = card.CultureName };
instrument.CustomProperties.Add("CultureName", cultureCodeProperty);
var cultureInfo = new CultureInfo(card.CultureName);
var creditCard = card as CreditCard;
DebitCard debitCard;
if (creditCard != null)
{
instrument.PaymentInstrumentKinds = PaymentInstrumentKinds.Credit;
instrument.DisplayCreditLimit
= creditCard.CreditLimit.ToString("C", cultureInfo);
instrument.DisplayAvailableCredit
= creditCard.AvailableCredit.ToString("C", cultureInfo);
/* Specify a message and a deep link URL that opens
* the bill pay view from the Wallet. */
instrument.Message = "Pay your credit card bill.";
instrument.MessageNavigationUri = new Uri(
"/BillPayView/BillPayView.xaml?CardId=" + card.Id, UriKind.Relative);
}
else if ((debitCard = card as DebitCard) != null)
{
instrument.PaymentInstrumentKinds = PaymentInstrumentKinds.Debit;
instrument.DisplayBalance = debitCard.Balance.ToString("C", cultureInfo);
}
else
{
throw new Exception("Unknown card type: " + card.GetType());
}
/* Specify a logo that is displayed in the wallet. */
var bitmapImage = new BitmapImage();
var logoUri = new Uri("Assets/UnleashedBankIcon.png", UriKind.Relative);
bitmapImage.SetSource(Application.GetResourceStream(logoUri).Stream);
instrument.Logo99x99 = bitmapImage;
instrument.Logo159x159 = bitmapImage;
instrument.Logo336x336 = bitmapImage;
/* Add a deep link so that when the app is launched from the Wallet item,
* the card view page is shown instead of the main page. */
instrument.NavigationUri = new Uri(
"/CardView/CardView.xaml?CardId=" + card.Id, UriKind.Relative);
addWalletItemTask.Item = instrument;
addWalletItemTask.Show();
}
When the viewmodel calls the AddWalletItemTask
object’s Show
method, the user is presented with a built-in confirmation dialog (see Figure 25.4).
You remove an item from the Wallet using the Wallet
class’s static Remove
method, as shown:
Wallet.Remove(card.Id.ToString());
Within the Wallet Hub, tapping a Wallet item displays the item’s details (see Figure 25.5). If the item’s TransactionHistory IDictionary
has been populated, a pivot item listing all the transactions is included alongside the about pivot item.
The value of the payment instrument’s Message
property is displayed to the right of the logo. Tapping on the message navigates to the URI specified by the instrument’s MessageNavigationUri
property. This capability to link back to your app allows you to notify the user of important information each time the user visits the Wallet Hub. In the next section you look at updating the message using a background agent.
The server-side BankService
includes a MakeCreditPurchase
method that deducts an amount from a specified credit card by reducing its AvailableCredit
property (see Listing 25.6). The service method returns a Success
result if the credit card is located and its available credit reduced.
public async Task<MakePurchaseResult> MakeCreditPurchase(
CreditCard creditCard, decimal amount)
{
CreditCard existingCard = (CreditCard)cards.Single(
card => card.Id == creditCard.Id);
if (existingCard == null)
{
return MakePurchaseResult.NoSuchCard;
}
decimal newAvailableCredit = existingCard.AvailableCredit - amount;
if (newAvailableCredit < 0)
{
return MakePurchaseResult.AmountExceedsAvailableFunds;
}
AccountTransaction transaction = new AccountTransaction(
"Purchase", "+" + amount, DateTime.Now);
List<AccountTransaction> transactionList = GetTransactionList(creditCard.Id);
transactionList.Add(transaction);
existingCard.AvailableCredit = existingCard.AvailableCredit - amount;
return MakePurchaseResult.Success;
}
The BankingService
class also includes a MakeDebitPurchase
method that behaves the same way as the MakeCreditPurchase
method, but instead of reducing the available credit, it reduces the DebitCard
object’s Balance
.
The BankService
class maintains transaction history for each card by storing custom AccountTransaction
objects in a Dictionary
. When a caller requests the list of transactions for a particular card, and it is the first request, the service creates a new List<AccountTransaction>
object as shown:
readonly Dictionary<Guid, List<AccountTransaction>> accountTransactions
= new Dictionary<Guid, List<AccountTransaction>>();
List<AccountTransaction> GetTransactionList(Guid cardId)
{
List<AccountTransaction> result;
if (!accountTransactions.TryGetValue(cardId, out result))
{
result = new List<AccountTransaction>();
accountTransactions.Add(cardId, result);
}
return result;
}
The CardViewModel
class calls either the WCF service’s MakeCreditPurchase
or MakeDebitPurchase
method, depending on the card type, when a MakePurchaseCommand
is executed (see Listing 25.7).
We create a CultureInfo
object using the CultureName
property of the card. A string representing the monetary amount of the purchase is constructed using the CultureInfo
object, which ensures that the correct currency format and symbol are materialized.
async Task MakePurchaseCore()
{
var bankService = (IBankService)new BankServiceClient();
MakePurchaseResult purchaseResult;
CreditCard creditCard = card as CreditCard;
if (creditCard != null)
{
purchaseResult = await Task<MakePurchaseResult>.Factory.FromAsync(
bankService.BeginMakeCreditPurchase,
bankService.EndMakeCreditPurchase,
creditCard, purchaseAmount, null);
}
else
{
purchaseResult = await Task<MakePurchaseResult>.Factory.FromAsync(
bankService.BeginMakeDebitPurchase,
bankService.EndMakeDebitPurchase,
(DebitCard)card, purchaseAmount, null);
}
string amountString = purchaseAmount.ToString(
"C", new CultureInfo(card.CultureName));
if (purchaseResult == MakePurchaseResult.Success)
{
MessageService.ShowMessage("Purchase made for " + amountString);
}
else if (purchaseResult == MakePurchaseResult.AmountExceedsAvailableFunds)
{
MessageService.ShowMessage("Purchase amount exceeds available funds.");
}
else
{
MessageService.ShowError("Unable to perform purchase.");
}
}
Relying on your foreground app to update a payment instrument can lead to out-of-date information in the Wallet. Fortunately, the Windows Phone SDK has tight integration with a specific type of background agent: the WalletAgent
, which allows you to receive notification when the OS senses that a Wallet item may require updating or when a user taps the Refresh application bar menu item in the Wallet.
In the next section, you look at using a custom WalletAgent
implementation to update a Wallet item’s information from a cloud service.
3.145.56.28