電子錢包

綜述

想像一下:你正走在大街上,當你看到一個咖啡廳,想起錢包裡有一張咖啡的優惠券。你走進店裡點上一杯咖啡,並用錢包裡的咖啡優惠券付費。當然,你也可以使用任何其他已經預先儲值的預付卡或紅利卡付費。

現在,將你的手機試想為一個錢包。透過手機瀏覽安裝的 App,當你看到想要的東西,不論是永久或消費性的產品,你可以直接使用信用卡購買產品,也可以用手機預付功能購買產品,手機現在就像一個安全且可自由使用的錢包,可以預先儲值資金供需要時隨時使用。

在這個實驗中,我們將修改 Contoso Cookbook App 使其可以使用 Windows Phone 手機電子錢包功能。只購買烹調時間的優惠券,用戶還沒法擁有烹飪時間。要獲得烹飪時間,使用者必須成為 ”Contoso 烹飪俱樂部 ”的會員。加入會員後,可以在電子錢包內選擇並加入優惠券,並用它們來兌換烹調時間。下一步,我們將實作電子錢包的後台管理程式,負責更新電子錢包內的優惠券。當收到新的訊息時負責週期性的更新優惠券資訊。

註 : 本實驗不包括 Windows Phone Store 功能,它將只討論 Windows Phone 8 App 中開發人員能夠使用的 API 和購買選項。更多相關於 Windows Phone Store 的資訊,請參閱 MSDN 說明文件。

目標

此實驗將引導我們達到下列目標:

  • 支援電子錢包功能

  • 使用 Microsoft.Phone.Wallet.Deal 來支援 App 優惠券

  • 模擬會員註冊程式並在電子錢包內增加優惠券

系統需求

您必須符合以下系統需求:

  • Microsoft Visual Studio 2012 or Microsoft Visual Studio 2012 Express for Windows Phone 8

  • Windows phone 開發經驗

實驗架構

包含兩部分的練習及以下任務:

  1. 更改 App 資料模組

  2. 實作優惠券管理頁面

  3. 實作俱樂部會員頁面

  4. 納入電子錢包後台管理程式

  5. 測試

預計完成時間

需要至少 45 分鐘

練習 1

在本練習中,我們將修改 Contoso Cookbook App 以支援電子錢包功能,並允許使用者加入 Contoso 的烹飪俱樂部和接收優惠券。之後,使用者不需使用信用卡(App 內部購買機制),這些優惠券可用於購買烹飪時間。

任務 1 – 更改應用程式清單

要使用手機電子錢包的 API (Wallet APIs),Contoso Cookbook App 必須符合 :

  • 聲明支援電子錢包的能力

  • 建立一個 Microsoft.Phone.Wallet.Deal 類別的實體來代表電子錢包的交易,如單一的優惠券。

  • 建立及檢索一個 Microsoft.Phone.Wallet.WalletTransactionItem 類別的實體來代表手機中一個交易項目 (或 "transaction store",例如一個內有優惠券的俱樂部會員卡)

  • 建立一個 Microsoft.Phone.Wallet.WalletTransaction 類別的實體來代表一個包含 WalletTransactionItem 實體的實際交易,如購買更多的優惠券或兌換優惠券。

在這個任務中,我們將為應用程式清單增加電子錢包功能。

  1. 打開 Visual Studio 2012

  2. 找到 Sources\EX1\Begin 資料夾

  3. 打開 ContosoBookbook.sln solution

  4. 找到 WMAppManifest.xml 檔案並雙擊打開它

  5. 瀏覽到 "Capabilities" 選項 (左邊第二個選項),並在  "ID_CAP_WALLET" 選項打勾如下圖 :


    圖 1
    功能

    註 : 如果你打開應用程序清單中的 XML 編輯器(而不是上面所描述的 UI),找到 Capabilities 段落並增加以下內容: <Capability Name="ID_CAP_WALLET"/>

任務 2 – 在 App 資料模型新增優惠券訊息

Microsoft.Phone.Wallet.Deal 類別代表一個單獨的協議(優惠券)。因為這個類別沒有屬性來表示是否存在錢包中,我們用自己的 MembersCoupon 類別包裹它,並儲存該交易是否已存在電子錢包的訊息。

  1. 按右鍵點擊 DataModel 資料夾並選擇 Add > New Item 並新增一個叫做 MembersCoupon 的新類別

  2. 在類別中新增一個宣告來實作 INotifyPropertyChanged 介面

  3. 在檔案開端新增下列 "using" 陳述式

    C#
    using Microsoft.Phone.Wallet;

  4. 以下列內容取代 MembersCoupon 階層的內容 :

    C#
    class MembersCoupon : INotifyPropertyChanged
    {
    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(string property)
    {
    if (null != PropertyChanged)
    {
    PropertyChanged(this, new PropertyChangedEventArgs(property));
    }
    }

    public string CouponID { get; set; }

    private Deal coupon;
    public Deal Coupon
    {
    get { return coupon; }
    set
    {
    coupon = value;
    NotifyPropertyChanged("Coupon");
    }
    }

    bool isInWallet;
    public bool IsInWallet
    {
    get { return isInWallet; }
    set
    {
    isInWallet = value;
    NotifyPropertyChanged("IsInWallet");
    }
    }
    }

    上面的代碼實行了一個類別代表一個優惠券。它包含唯一的優惠券 ID、目前的優惠券狀態(是否在電子錢包中)及電子錢包中代表實際交易的交易對象(來自 Microsoft.Phone.Wallet 命名空間)。

任務 3 – 新增一個優惠券管理頁面

為了讓使用者能夠管理電子錢包內的優惠券,需要一個管理頁面。它會顯示未使用的優惠券列表,並讓使用者可以兌換他們。該頁面將只提供給已註冊且加入 Contoso 烹飪俱樂部的使用者。如果使用者還沒有加入俱樂部,頁面將改顯示一個連結到俱樂部會員加入表格的按鈕,這個按鈕稍後我們再增加。

  1. 新增一個叫做 CouponsPage 的新直立頁面 (Portrait page)

  2. 找到的主要 phone.PhoneApplicationPage 元素,並設定 shell:SystemTray.IsVisible 的屬性為 False(當打開頁面後,這將移除螢幕頂端的系統圖示列)。

  3. 找到 .LayoutRoot Grid 元素並以下列代碼更換內容 :

    XAML
    <Grid x:Name="LayoutRoot"
    Background="{StaticResource CustomApplicationBackgroundBrush}">
    <Grid.Resources>
    <local:InWalletToStringConverter
    xmlns:local="clr-namespace:ContosoCookbook.Common"
    x:Key="InWalletToStringConverter"/>
    </Grid.Resources>
    <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
    <Image x:Name="imgLogo" Source="Assets/Title.png" Stretch="Uniform"
    HorizontalAlignment="Left"
    Width="{StaticResource LogoImageWidth}"/>
    <TextBlock x:Name="PageTitle" Text="Coupons" Margin="9,-7,0,0"
    Style="{StaticResource PhoneTextTitle1Style}"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    </StackPanel>

    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <StackPanel Orientation="Vertical" VerticalAlignment="Center"
    HorizontalAlignment="Center" x:Name="stkMembershipOnly" >
    <TextBlock Text="To view coupons please become join the Contoso Cooking Club."
    TextWrapping="Wrap" HorizontalAlignment="Center"
    VerticalAlignment="Center"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    <Button Content="Join the club" Click="btnBuyMembership_Click"
    Margin="0,30" x:Name="btnBuyMembership"
    Foreground="{StaticResource CustomGroupTitleBrush}"
    BorderBrush="{StaticResource CustomGroupTitleBrush}"/>
    </StackPanel>
    <TextBlock Text="Coupons for you" x:Name="txtDeals"
    VerticalAlignment="Top" Margin="10"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    <ListBox x:Name="lstCoupons" ItemsSource="{Binding}"
    Margin="10,50,10,10"
    SelectionChanged="lstCoupons_SelectionChanged">
    <ListBox.ItemTemplate>
    <DataTemplate>
    <Grid>
    <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
    <ColumnDefinition Width="3*"/>
    <ColumnDefinition Width="2*"/>
    </Grid.ColumnDefinitions>
    <TextBlock Text="{Binding Coupon.DisplayName}"
    Style="{StaticResource PhoneTextTitle2Style}"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    <TextBlock Text="{Binding Coupon.Description}"
    Grid.Row="1" TextWrapping="Wrap"
    Style="{StaticResource PhoneTextSmallStyle}"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    <TextBlock Text="{Binding Coupon.ExpirationDate, StringFormat=Expiration Date: \{0:d\}}"
    Style="{StaticResource PhoneTextSmallStyle}"
    FontWeight="Bold"
    Foreground="{StaticResource CustomGroupTitleBrush}"
    Grid.Row="2"
    HorizontalAlignment="Center"
    VerticalAlignment="Stretch"/>
    <TextBlock Text="{Binding IsInWallet, Converter={StaticResource InWalletToStringConverter}}"
    Grid.Row="2" Grid.Column="1"
    TextWrapping="Wrap"
    Style="{StaticResource PhoneTextSmallStyle}"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    </Grid>
    </DataTemplate>
    </ListBox.ItemTemplate>
    </ListBox>
    </Grid>
    </Grid>

    上面的代碼定義了一個顯示電子錢包內優惠券的 ListBox 頁面,以及一個導向註冊頁面的按鈕。如果使用者已經是俱樂部的成員,那麼按鈕則會被隱藏。

  4. 注意在 Grid.Resources 元素內部的 InWalletToStringConverter 宣告,會在本頁面隨後使用到。該轉換器轉換布林變數 IsInWallet 為一個文字。增加一個新的 InWalletToStringConverter 類別到 Common 文件夾。

  5. 確定新的類別實作了 IValueConverter 界面並且是公開的。記住在檔案頂端加入 using System.Windows.Data。

  6. 用以下代碼取代類別的內容

    C#
    public object Convert(object value, Type targetType, object parameter,
    System.Globalization.CultureInfo culture)
    {
    bool val = bool.Parse(value.ToString());
    string retVal = "Not in wallet";

    if (val)
    retVal = "In wallet";

    return retVal;
    }

    public object ConvertBack(object value, Type targetType, object parameter,
    System.Globalization.CultureInfo culture)
    {
    throw new NotImplementedException();
    }

    根據上面的代碼轉換一個布林變數為文字 "In wallet" 或 "Not in wallet"。

  7. 接下來,我們轉到隱藏代碼文件。打開 CouponsPage.xaml.cs 檔案。

  8. 在檔案頂端增加下面的命名空間聲明。

    C#
    using Microsoft.Phone.Wallet;
    using System.Threading.Tasks;
    using ContosoCookbook.DataModel;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;

  9. 增加下列類別會員 (它將包含可使用的優惠券清單)

    C#
    List<MembersCoupon> coupons = new List<MembersCoupon>();

  10. 重寫 OnNavigatedTo 方法。這個方法將檢索優惠券列表並顯示或隱藏 "Join the club!" 按鈕:

    C#
    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
    WalletTransactionItem membershipCard = Wallet.FindItem("membershipCard")
    as WalletTransactionItem;

    if (null != membershipCard)
    {
    //Show coupons list
    txtDeals.Visibility = Visibility.Visible;
    lstCoupons.Visibility = Visibility.Visible;
    stkMembershipOnly.Visibility = Visibility.Collapsed;

    //Init coupons
    LoadCoupons();
    }
    else
    {
    //Hide coupons list
    txtDeals.Visibility = Visibility.Collapsed;
    lstCoupons.Visibility = Visibility.Collapsed;
    stkMembershipOnly.Visibility = Visibility.Visible;
    }
    }

    上面的方法嘗試檢索 WalletTransactionItem 實體,代表電子錢包中的俱樂部會員卡。如果檢索到,優惠券列表就被載入且顯示(使用我們下一步要加入的 LoadCoupons 方法),如果沒有檢索到,清單將會隱藏並且顯示 "Join the club!" 按鈕。

  11. 代碼在上一步驟中呼叫了用來載入優惠券到電子錢包裡的 LoadCoupons 方法,增加此方法:

    C#
    private void LoadCoupons()
    {
    AddFakeCoupons();

    foreach (var coupon in coupons)
    {
    var walletDeal = Wallet.FindItem(coupon.CouponID);

    if (null != walletDeal)
    coupon.IsInWallet = true;
    }

    lstCoupons.DataContext = coupons;
    }

    前面的方法呼叫了 AddFakeCoupons 方法(我們將在下一步新增的方法),這個方法建立了一套假優惠券並列舉出來。代碼根據優惠券是否已存在電子錢包內來設定 IsInWallet 屬性。最後一個步驟將 ListBox 綁定到 coupons 清單來顯示。

  12. Contoso Cookbook App 不產生實體優惠券,以 LoadCoupons 方法呼叫 AddFakeCoupons 方法來增加優惠券到電子錢包中。

    註 : 作為商品販售的 App,伺服器可能會負責管理使用者的優惠券,並應 App 要求反饋可用的優惠券資訊給特定的使用者 (客戶)。之後的實驗中會講到,當我們增加代碼來兌換優惠券,同樣會忽略任何來自伺服器端標記已使用的優惠券的注意事項。

    增加下列方法 :

    C#
    private void AddFakeCoupons()
    {
    Random rnd = new Random();

    for (int i = 5; i < 50; i += 5)
    {
    Deal theDeal = new Deal("Deals." + i);
    theDeal.DisplayName = i + " minutes free";
    theDeal.Description = "Get " + i + " minutes of cooking time free";
    theDeal.ExpirationDate = DateTime.Now.AddDays(rnd.Next(1, 14));
    theDeal.Code = "DEAL_" + i;
    theDeal.IssuerName = "Contoso Cookbooks";
    theDeal.MerchantName = "Contoso Cookbooks";
    theDeal.IssuerWebsite = new Uri("https://www.contoso.com");
    theDeal.StartDate = DateTime.Now;

    CustomWalletProperty couponValueProperty =
    new CustomWalletProperty(i.ToString());
    theDeal.CustomProperties.Add("RedemptionValue",
    couponValueProperty);

    BitmapImage bmp = new BitmapImage();
    Uri barcodeUri = new Uri("Assets/Barcode.png", UriKind.Relative);
    bmp.SetSource(Application.GetResourceStream(barcodeUri).Stream);
    theDeal.BarcodeImage = bmp;

    bmp = new BitmapImage();
    Uri logoUri = new Uri("Assets/SmallLogo.png", UriKind.Relative);
    bmp.SetSource(Application.GetResourceStream(logoUri).Stream);
    theDeal.Logo99x99 = bmp;
    theDeal.Logo159x159 = bmp;
    theDeal.Logo336x336 = bmp;

    MembersCoupon coupon = new MembersCoupon();
    coupon.CouponID = "Deals." + i;
    coupon.Coupon = theDeal;
    coupon.IsInWallet = false;
    coupons.Add(coupon);
    }
    }

    前面的代碼建立 Deal 對象。Deal 物件宣告了顯示的名稱、描述、到期日、App 特定的代碼、發行人及開發商名稱、開始日期。我們還可以為 App 特定的資訊增加自定義屬性。在這裡的例子中,贖回的價值被存儲為 Deal 物件的自定義屬性,等同於目前優惠券提供的分鐘數。

  13. 現在,我們有優惠券被載入到清單中,使用者可選擇其中一個兌換。新增 SelectionChanged 事件處理器如下:

    C#
    private async void lstCoupons_SelectionChanged(object sender,
    SelectionChangedEventArgs e)
    {
    if (lstCoupons.SelectedIndex > -1)
    {
    var selectedCoupon = lstCoupons.SelectedItem as MembersCoupon;

    // Check if the selected coupon is already in the wallet
    if (selectedCoupon.IsInWallet)
    {
    // Show the coupon details
    NavigationService.Navigate(new Uri("/CouponView.xaml?ID=" +
    selectedCoupon.CouponID, UriKind.Relative));
    }
    else
    {
    // Offer the user to purchase the coupon
    int couponPrice =
    int.Parse(selectedCoupon.Coupon.CustomProperties["RedemptionValue"].Value);
    if (MessageBox.Show("This coupon is not in you wallet. Do you want to purchase it for "
    + couponPrice
    + " membership credits and save to your wallet before proceeding?",
    "Save Coupon", MessageBoxButton.OKCancel)
    == MessageBoxResult.OK)
    {
    WalletTransactionItem membershipCard =
    Wallet.FindItem("membershipCard")
    as WalletTransactionItem;
    // Extract the current balance from the WalletTransactionItem
    int currentBalance =
    int.Parse(membershipCard.DisplayBalance);

    if (currentBalance >= couponPrice)
    {
    //Add purchase transaction on membership card
    WalletTransaction transaction =
    new WalletTransaction();
    transaction.Description =
    "Coupon purchase";
    transaction.DisplayAmount =
    (-couponPrice).ToString("C");
    transaction.TransactionDate =
    DateTime.Now;
    membershipCard.TransactionHistory
    .Add("CouponPurchase_"
    + transaction.TransactionDate, transaction);

    //Update card balance
    membershipCard.DisplayBalance =
    (currentBalance -
    couponPrice).ToString();
    membershipCard
    .DisplayAvailableBalance =
    membershipCard
    .DisplayBalance;
    await membershipCard.SaveAsync();

    await selectedCoupon.Coupon
    .SaveAsync();
    NavigationService.Navigate(new
    Uri("/CouponView.xaml?ID=" +
    selectedCoupon.CouponID,
    UriKind.Relative));
    }
    else
    MessageBox.Show("Insufficient funds of membership credits. The coupon cannot be purchased.\nPurchase more membership credits and try again.");
    }
    else
    MessageBox.Show("Cannot show unsaved coupon details");
    }
    }

    lstCoupons.SelectedIndex = -1;
    }

    前面的代碼檢查選擇的優惠券是否已經在電子錢包內。如果在的話,代碼將導向將在之後增加的對應頁面 CouponView.xaml,以顯示優惠券訊息。

    如果電子錢包內沒有優惠券,系統會向使用者建議購買和保存新的優惠券。經使用者核准後,該方法會尋找代表俱樂部會員卡的 WalletTransactionItem。當使用者從 MembershipPage.xaml 頁面 (稍後要新增的) 加入 Contoso 烹飪俱樂部時,商品 (優惠券) 就被建立。

    如果使用者有足夠的資金來購買優惠券,一個新的購買交易將透過建立一個新的 WalletTransaction 對象被加入會員卡,並且被加入到會員卡的 TransactionHistory 屬性。之後,該代碼將更新卡上的餘額並將資料儲存到電子錢包,接著瀏覽到 CouponView.xaml 頁面並顯示選擇的優惠券的詳細訊息。

  14. 最後,我們透過瀏覽 MembershipPage.xaml 處理 "Join the club!" 按鈕的 click 事件,MembershipPage.xaml 將在之後討論如何加入:

    C#
    private void btnBuyMembership_Click(object sender, RoutedEventArgs e)
    {
    NavigationService.Navigate(new Uri("/MembershipPage.xaml",
    UriKind.Relative));
    }

任務 4 –支援俱樂部會員功能

當使用者決定加入 Contoso 的烹飪俱樂部以獲得優惠券,會被轉到一個要求填寫詳細資料的頁面。App 之後會建立一個對應的 WalletTransactionItem 來代表電子錢包中使用者的會員卡。

註 : 作為商品販售的 App,伺服器將負責檢索使用者擁有的所有會員卡,或負責建立新的會員卡並將其保存到電子錢包中。

  1. 新增一個名為 MembershipPage 新的直立頁面 (Portrait page)

  2. 找到主要的 phone.PhoneApplicationPage 元素並將 shell:SystemTray.IsVisible" 屬性設定為 False

  3. 找到 LayoutRootGrid 元素並用以下代碼取代

    XAML
    <Grid x:Name="LayoutRoot"
    Background="{StaticResource CustomApplicationBackgroundBrush}">
    <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
    <Image x:Name="imgLogo" Source="Assets/Title.png" Stretch="Uniform"
    HorizontalAlignment="Left"
    Width="{StaticResource LogoImageWidth}"/>
    <TextBlock x:Name="PageTitle" Text="new member" Margin="9,-7,0,0"
    Style="{StaticResource PhoneTextTitle1Style}"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    </StackPanel>

    <StackPanel x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <StackPanel.Resources>
    <Style TargetType="TextBlock">
    <Setter Property="Foreground"
    Value="{StaticResource CustomGroupTitleBrush}" />
    <Setter Property="FontSize"
    Value="{StaticResource PhoneFontSizeSmall}" />
    <Setter Property="Margin" Value="20,0,0,0"/>
    </Style>
    <Style TargetType="TextBox">
    <Setter Property="Width" Value="460"/>
    </Style>
    </StackPanel.Resources>

    <TextBlock x:Name="FirstNameLabel" Text="First Name"/>
    <TextBox x:Name="FirstNameInput"/>
    <TextBlock x:Name="LastNameLabel" Text="Last Name"/>
    <TextBox x:Name="LastNameInput"/>
    <TextBlock x:Name="PhoneNumberLabel" Text="Phone Number"/>
    <TextBox x:Name="PhoneNumberInput" InputScope="TelephoneNumber"/>
    <TextBlock x:Name="EmailLabel" Text="Email"/>
    <TextBox x:Name="EmailInput" InputScope="EmailUserName"/>
    <Button x:Name="btnSignUp" Click="btnSignUp_Click" Content="Join"
    Foreground="{StaticResource CustomGroupTitleBrush}"
    BorderBrush="{StaticResource CustomGroupTitleBrush}"/>
    </StackPanel>
    </Grid>

    上面的代碼定義了一個包含使用者的姓名、電話號碼及電子郵件表格的頁面,作為加入 Contoso 烹飪俱樂部的資料。

  4. 接下來,我們轉到隱藏代碼。打開 MembershipPage.xaml.cs 檔案。

  5. 在檔案頂端新增以下命名空間的聲明。

    C#
    using Microsoft.Phone.Wallet;
    using System.Windows.Media.Imaging;
    using System.Reflection;
    using Microsoft.Phone.Tasks;

  6. 新增下列類別成員 :

    C#
    AddWalletItemTask addWalletItemTask = new AddWalletItemTask( );

    當執行繁重的後端工作時,為了使我們的 App UI 回應正常,我們將使用 Windows Phone tasks。AddWalletItemTask 在後台運作,並當完成時引發 Completed 事件。結果是,UI 整個過程都維持回應。

  7. 在結構函數的尾端增加下列事件的註冊

    C#
    addWalletItemTask.Completed += addWalletItemTask_Completed;

  8. 接著,當電子錢包登入結束後,要增加事件處理器刪除紀錄的功能:

    C#
    void addWalletItemTask_Completed(object sender, AddWalletItemResult e)
    {
    if (e.TaskResult == Microsoft.Phone.Tasks.TaskResult.OK)
    {
    MessageBox.Show(e.Item.DisplayName + " was added to your wallet!");

    NavigationService.GoBack();
    }
    else if (e.TaskResult == Microsoft.Phone.Tasks.TaskResult.Cancel)
    {
    MessageBox.Show("Save operation was cancelled");
    }
    else if (e.TaskResult == Microsoft.Phone.Tasks.TaskResult.None)
    {
    MessageBox.Show("None");
    }
    }

    前面的事件處理器提醒使用者電子錢包交易的結果:成功或失敗。

  9. 當使用者點選 "Join" 按鈕,上面方法中看到的 AddWalletItemTask 會被觸發。現在我們要增加 "Join" 按鈕的事件處理器:

    C#
    private void btnSignUp_Click(object sender, RoutedEventArgs e)
    {
    try
    {
    WalletTransactionItem membershipItem;
    membershipItem = new WalletTransactionItem("membershipCard");
    membershipItem.IssuerName = "Contoso Cookbooks";
    membershipItem.DisplayName = "Contoso Cooking Club Membership";
    membershipItem.IssuerPhone.Business = "+1 (425) 555 1234";
    membershipItem.CustomerName = FirstNameInput.Text + " " +
    LastNameInput.Text;
    membershipItem.AccountNumber = Guid.NewGuid().ToString();
    membershipItem.BillingPhone = PhoneNumberInput.Text;
    membershipItem.IssuerWebsite = new Uri("https://www.contoso.com");
    membershipItem.CustomProperties.Add("email", new
    CustomWalletProperty(EmailInput.Text));
    membershipItem.DisplayAvailableBalance = "100";
    membershipItem.DisplayBalance = "100";

    BitmapImage bmp = new BitmapImage();
    Uri logoUri = new Uri("Assets/SmallLogo.png", UriKind.Relative);
    bmp.SetSource(Application.GetResourceStream(logoUri).Stream);

    membershipItem.Logo99x99 = bmp;
    membershipItem.Logo159x159 = bmp;
    membershipItem.Logo336x336 = bmp;
    addWalletItemTask.Item = membershipItem;
    addWalletItemTask.Show();
    }
    catch (Exception ex)
    {
    MessageBox.Show("The following error occurred when saving your membership to the wallet: " + ex.Message);
    }
    }

    前面的代碼建立一個 WalletTransactionItem 實例,名為 membershipCard,這是用在 LoadCoupons 方法。會員卡是 "Contoso Cookbooks" 發行者建立的,包含了客戶資料、初始餘額和使用者定義圖像。然後,我們為新建的 WalletTransactionItem 設定 AddWalletItemTask 的 Item 屬性,並呼叫 Show 方法在電子錢包 UI 中來完成交易。

任務 5 –顯示優惠券

在以前的任務中我們建立了 CouponsPage.xaml 的頁面,負責將畫面導向一個顯示優惠券詳細訊息的頁面。此頁面允許使用者決定優惠券的使用目的、狀態和價值,以及從電子錢包使用或刪除優惠券的功能。

  1. 新增一個叫做 CouponView 的新直立頁面 (Portrait page)

  2. 找到主要的 phone.PhoneApplicationPage 元素並將 shell:SystemTray.IsVisible 設定為 False

  3. 找到 LayoutRootGrid 元素並以下方代碼替換 :

    XAML
    <Grid x:Name="LayoutRoot"
    Background="{StaticResource CustomApplicationBackgroundBrush}">
    <Grid.Resources>
    <local:IsUsedToVisibilityConverter
    xmlns:local="clr-namespace:ContosoCookbook.Common"
    x:Key="IsUsedToVisibilityConverter"/>
    </Grid.Resources>

    <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
    <Image x:Name="imgLogo" Source="Assets/Title.png" Stretch="Uniform"
    HorizontalAlignment="Left"
    Width="{StaticResource LogoImageWidth}"/>
    <TextBlock x:Name="PageTitle" Text="Coupon" Margin="9,-7,0,0"
    Style="{StaticResource PhoneTextTitle1Style}"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    </StackPanel>

    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <Grid.RowDefinitions>
    <!-- Coupon Title -->
    <RowDefinition Height="Auto"/>
    <!-- Description -->
    <RowDefinition Height="Auto"/>
    <!-- Expiration -->
    <RowDefinition Height="Auto"/>
    <!-- Padding -->
    <RowDefinition Height="30"/>
    <!-- Barcode -->
    <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    <TextBlock Grid.Row="0" x:Name="DealTitle"
    Text="{Binding DisplayName}"
    Style="{StaticResource PhoneTextTitle1Style}"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    <TextBlock Grid.Row="1" x:Name="DealDescription"
    Text="{Binding Description}"
    Style="{StaticResource PhoneTextNormalStyle}"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    <TextBlock Grid.Row="2" x:Name="DealExpiration" Text=""
    Foreground="{StaticResource PhoneAccentBrush}"
    Margin="12,0,0,0"/>
    <Image Grid.Row="4" x:Name="DealBarcode"
    Source="{Binding BarcodeImage}"
    Visibility="{Binding IsUsed, Converter={StaticResource IsUsedToVisibilityConverter}}"
    Stretch="None" VerticalAlignment="Center"
    HorizontalAlignment="Center" />
    </Grid>
    </Grid>

    上面的代碼定義了一個有許多文字區塊的頁面,頁面顯示優惠券的詳細訊息:包含標題、描述、到期日及條碼。在 Contoso Cookbook App,只顯示一個假的條碼圖,而不是產生一個真正的優惠券條碼。

  4. IsUsedToVisibilityConverter 類別透過繫結將布林變數 IsUsed 的屬性轉為 Visibility。添加一個新的 IsUsedToVisibilityConverter 類別到 Common 資料夾。

  5. 確保新的類別實作 IvalueConverter 介面並且是公共的。

  6. 在檔案頂端新增下列命名空間聲明 :

    C#
    using System.Windows;
    using System.Windows.Data;

  7. 用類別的代碼用下面的內容替換:

    C#
    public object Convert(object value, Type targetType, object parameter,
    System.Globalization.CultureInfo culture)
    {
    bool val = bool.Parse(value.ToString());
    return val ? Visibility.Visible : Visibility.Collapsed;
    }

    public object ConvertBack(object value, Type targetType, object parameter,
    System.Globalization.CultureInfo culture)
    {
    throw new NotImplementedException();
    }

    前面的代碼將所提供的布林變數轉換為 Visibility.Visible 或 Visibility.Collapsed。結果是,尚未使用的優惠券的條碼圖才能被看見。

  8. 接下來,我們轉到隱藏代碼。打開 CouponView.xaml.cs 檔案。

  9. 在檔案頂端增加下列命名空間聲明 :

    C#
    using Microsoft.Phone.Wallet;

  10. 增加下列類別會員 :

    C#
    Deal currentCoupon;

  11. 覆蓋 OnNavigatedTo 方法 :

    C#
    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
    string passedID = "";
    if (NavigationContext.QueryString.TryGetValue("ID", out passedID))
    {
    // Set the current deal.
    currentCoupon = Wallet.FindItem(passedID) as Deal;

    // Set the data context to current deal.
    DataContext = currentCoupon;

    // Set the expiration text
    DealExpiration.Text = "Expires " +
    currentCoupon.ExpirationDate.Value.ToShortDateString();

    BuildApplicationBar();
    }
    }

    上述方法從詢問串提取優惠券 ID,從電子錢包載入優惠券訊息,並根據訊息設定文字區塊。然後呼叫 BuildApplicationBar 方法 (將在下階段添加)。

  12. 增加下列方法 :

    C#
    private void BuildApplicationBar()
    {
    // Set the page's ApplicationBar to a new instance of ApplicationBar.
    ApplicationBar = new ApplicationBar();

    if (!currentCoupon.IsUsed)
    {
    ApplicationBarIconButton appBarButton =
    new ApplicationBarIconButton(new Uri("/Assets/UseCoupon.png",
    UriKind.Relative));
    appBarButton.Text = "Use";
    appBarButton.Click += appBarButton_Click;
    ApplicationBar.Buttons.Add(appBarButton);
    }
    else
    {
    ApplicationBarIconButton appBarButton =
    new ApplicationBarIconButton(new Uri("/Assets/RemoveCoupon.png",
    UriKind.Relative));
    appBarButton.Text = "Remove";
    appBarButton.Click += appBarButtonRemoveUsed_Click;
    ApplicationBar.Buttons.Add(appBarButton);
    }
    }

    上面的方法根據目前的優惠券狀態:使用或不使用,在應用程式列增加 "Use" 或 "Remove" 按鈕。

  13. 接著為 "Remove" 按鈕增加事件處理器:

    C#
    private void appBarButtonRemoveUsed_Click(object sender, EventArgs e)
    {
    if (null != currentCoupon)
    {
    if (MessageBox.Show("Remove used coupon?", "My Coupons",
    MessageBoxButton.OKCancel) == MessageBoxResult.OK)
    {
    Wallet.Remove(currentCoupon.Id);
    NavigationService.GoBack();
    }
    }
    }

  14. 之後,為 "Use" 按鈕新增事件處理器:

    C#
    private async void appBarButton_Click(object sender, EventArgs e)
    {
    if (null != currentCoupon)
    {
    if (MessageBox.Show("Use the coupon?", "My Coupons",
    MessageBoxButton.OKCancel) == MessageBoxResult.OK)
    {
    //Redeem coupon value
    int value =
    int.Parse(currentCoupon.CustomProperties["RedemptionValue"].Value);
    App.RemainingCookingTime += value;

    currentCoupon.IsUsed = true;
    await currentCoupon.SaveAsync();

    MessageBox.Show("Coupon used. Feel free to remove it from your wallet");
    NavigationService.GoBack();
    }
    }
    }

    前面的事件處理器在需要的情況下修改烹飪時間,或從電子錢包裡刪除優惠券。

  15. 新加入的的方法使用 Barcode.png,UseCoupon.png,和 RemoveCoupon.png 圖像。現在,我們將這些資料新增。找到主要項目下的 Assets 資料夾。

  16. 按右鍵點擊 project 並選擇 Add > Existing item

  17. 從 Assets 資料夾新增下列影像檔案 :

    • Barcode.png

    • UseCoupon.png

    • RemoveCoupon.png

任務 6 –支援主頁優惠券

大多數的業務邏輯和界面現在已經實作完成,但使用者仍無法訪問優惠券清單。這個過程將從主頁上的“Coupons”App 選項啟動。

  1. 找到 MainPage.xaml 頁面

    找到主要的 phone:PhoneApplicationPage.ApplicationBar 元素並設定 IsMenuEnabled 屬性為 True, 使選項呈現。

  2. 在 shell:ApplicationBar 元素內部新增下列代碼 :

    XAML
    <shell:ApplicationBar.MenuItems>
    <shell:ApplicationBarMenuItem x:Name="mnuCoupons" Text="Coupons"
    Click="mnuCoupons_Click"/>
    </shell:ApplicationBar.MenuItems>

    上面的代碼將在選單上增加一個新的 "Coupons" 按鈕。

  3. 接著,轉到隱藏代碼。打開 MainPage.xaml.cs 檔案。

  4. 新增下列事件處理器 :

    C#
    private void mnuCoupons_Click(object sender, EventArgs e)
    {
    NavigationService.Navigate(new Uri("/CouponsPage.xaml", UriKind.Relative));
    }

    點擊按鈕將顯示這個練習開始時加入的 CouponsPage.xaml 頁面,觸發了整個程序。

任務 7 –增加電子錢包後台代理程式

販售作為商品的 App,會員卡訊息與負責管理使用者和他們的優惠券的服務器應該是同步的。要做到這一點,我們實作了一個後台管理程式,當電子錢包刷新資料時,程式就會運作。後台管理程式的類別是從 WalletAgent 類別衍生並在同一個解決方案中實作一個預定的代理項目。

在 Contoso Cookbook App,我們不會真正的與雲端服務同步。相反的,我們將模擬定期性在電子錢包增加新的交易。

  1. 建立一個新的 "Windows Phone Scheduled Task Agent" 項目在 ContosoCookbook.sln 解決方案。將它命名為 ContosoCookbookWalletAgent。選擇“Windows Phone OS 8.0”為目標的 OS 版本。

  2. 在新建的項目打開 ScheduledAgent.cs 檔案。

    在該檔案的頂端增加下面的“using”的聲明:

    C#
    using Microsoft.Phone.Wallet;

  3. 更改類別宣告繼承 WalletAgent :

    C#
    public class ScheduledAgent : WalletAgent

  4. 移除屬於之前基礎類別的 OnInvoke 方法,並增加以下 OnRefreshData 覆蓋方法,當管理程式被呼叫刷新電子錢包訊息時執行:

    C#
    protected override async void OnRefreshData(RefreshDataEventArgs args)
    {
    foreach (var item in args.Items)
    {
    WalletTransactionItem membershipCard = item
    as WalletTransactionItem;
    if (null != membershipCard) //membership card
    {
    if (int.Parse(membershipCard.DisplayBalance) < 5)
    {
    //Simulate purchasing more credits
    WalletTransaction transaction =
    new WalletTransaction();
    transaction.Description = "Credits purchase";
    transaction.DisplayAmount = 100.ToString("C");
    transaction.TransactionDate = DateTime.Now;

    membershipCard.Message = "Membership credits balance filled";
    membershipCard.MessageNavigationUri =
    new Uri("/MainPage.xaml", UriKind.Relative);
    membershipCard.DisplayBalance = "100";
    membershipCard.DisplayAvailableBalance =
    membershipCard.DisplayBalance;
    membershipCard.TransactionHistory.Add(
    "CreditsPurchase_" +
    transaction.TransactionDate, transaction);
    }
    else
    {
    membershipCard.Message = "You might be interested in special deals for you. Click here to check it out!";
    membershipCard.MessageNavigationUri =
    new Uri("/CouponsPage.xaml", UriKind.Relative);

    //Simulate updating balance as result of usage
    membershipCard.DisplayBalance =
    (int.Parse(membershipCard.DisplayBalance) -
    5).ToString();
    membershipCard.DisplayAvailableBalance =
    membershipCard.DisplayBalance;
    }

    await membershipCard.SaveAsync();
    }

    Deal coupon = item as Deal; //coupon
    if (null != coupon)
    {
    if (coupon.ExpirationDate.Value.Date <= DateTime.Now.Date)
    {
    //Remove coupon
    Wallet.Remove(coupon.Id);
    }
    else
    {
    coupon.Message = "You might be interested in special deals for you. Click here to check it out!";
    coupon.MessageNavigationUri =
    new Uri("/CouponsPage.xaml",
    UriKind.Relative);
    await coupon.SaveAsync();
    }
    }
    }

    NotifyComplete();
    }

    操作系統呼叫上面的方法來刷新電子錢包的數據。模擬檢索會員卡資料並且為目前錢包內的優惠券儲值,如果使用者從其他位置購買,這種方法也可以模擬從伺服器獲取新的交易報價,以備使用者後續可以購買。

任務 8 – 測試

App 現在已經完成了。請執行 App 來測試看看。

  1. 建置、發布並執行應用程式 (F5)


    圖 2
    主頁

  2. 點選解鎖的食譜類別之一並滑動畫面找到 Recipes 選單項。


    圖 3
    食譜頁

  3. 選擇至少需 40 分鐘準備時間的食譜,然後點擊螢幕底部的選單按鈕。


    圖 4
    需要長時間烹飪的食譜

  4. 點擊 "set alarm" 按鈕,可以看到你沒有足夠的烹調時間。


    圖 5
    烹飪時間商品頁面

  5. 你不希望購買更多的時間,而是想使用優惠券。返回到 App 主頁面,點擊在螢幕下方的選單按鈕。


    圖 6
    主頁上的 App 功能選單列

  6. 點選 "coupons" 項目


    圖 7
    優惠券頁面

  7. 因為你還沒有加入俱樂部(會員卡不在你的電子錢包中),點擊 "Sign-in for membership" 按鈕。


    圖 8
    會員表格

  8. 填妥會員表格並按下 "Join" 按鈕


    圖 9
    將 Contoso Cooking Club 加入到電子錢包中

  9. 點擊 "save" 以完成交易並在電子錢包中保存您的新會員卡。


    圖 10
    電子錢包儲存確認畫面

  10. 點選 "ok" 來核准並回到優惠券頁面


    圖 11
    優惠券列表頁

  11. 滾動選單並點擊 40 分鐘的優惠券。點選 ”Save ”按鈕將優惠券保存到錢包中


    圖 12
    儲存優惠券

  12. 在優惠券列表頁點選 ”Use ”按鈕

    滾動選單並點擊 40 分鐘的優惠券。點選 ”Save ”按鈕將優惠券保存到錢包中


    圖 13
    優惠券頁面

  13. 點選底部的 "ok" 按鈕


    圖 14
    兌換優惠券

  14. 點選 "ok" 按鈕關閉確認訊息

  15. 回到你之前有興趣的食譜,並點擊 "set alarm" 選項。這將設定計時器開始計時。


    圖 15
    已經設定定時器的食譜頁

  16. 你使用優惠券而不是在 App 內購買定時器時間來設定定時器

  17. 接下來,我們檢查錢包。而後回到首頁並向左滾動以顯示 App。向下滾動找到“電子錢包”App 並點擊它。


    圖 16
    電子錢包 App

  18. 檢查目前得餘額.點選 "Contoso Cookbook" 電子錢包項目


    圖 17
    電子錢包項目

  19. 可以隨意瀏覽其他錢包內的項目

  20. 練習在此結束

總結

本實驗,從 App 代碼中更改與新增啟用了支援電子錢包的功能。電子錢包機制使開發人員能夠進行特定 App 的儲值及管理使用者的優惠券資料,並讓使用者能在需要時使用優惠券。

繼續下一個實驗