將 Windows 8 應用程式移植到 Windows Phone 8

綜述

新的 Windows Phone 8 作業系統提供開發者一些全新以及升級過的功能,包括用原生 C++ 開發遊戲,根據 Windows Runtime (WinRT) 版本作手機客製化,以及與 Windows 8 共用新的核心。

Windows Phone 8 並沒有完整的 Windows Runtime 功能,因為 Windows Runtime 是針對桌上型電腦和平板電腦的。Windows 8 Runtime 有很多不適合 Windows Phone 也與 Windows Phone 無關的功能。此外,Windows Phone 8 提供保留在 Microsoft.Phone 命名空間內特定的功能(從以前的 Windows 手機版本保留下來)。此命名空間和其功能不適合 Windows 8 設備,而只適用於 Windows Phone 8。

這個實驗是指導如何從 Windows Store 的 App 移植到 Windows Phone 8。

當移植一個 Windows Store 的應用程式到手機上,你有可能遭遇一些 Windows 8 API 的編譯錯誤,導致無法在 Windows Phone 版本的 Windows Runtime 執行。

以下問題需要解決:

  • **功能不一致。**在這兩個平台上同時存在的某些功能,但是執行方式不一樣。Windows Phone 應用程式必須使用 Windows Phone 的 API,或修改實作以模仿 Windows 8 的功能,讓他可以在手機上執行。例如,Windows 8 的 Charms API 的設定與設計方式和 Windows Phone Share 的行為不同,你將必須決定是否要將 Windows Phone Share 作為 Windows 8 Charms 的替代品。

  • **遺失的功能。**一些 Windows 8 的功能並不存在於 Windows Phone Runtime。例如,您的 Windows Store 應用程式或許使用了 GridView 控制項,這在 Windows Phone 是不存在的。在這種情況下,你將必須使用不同的內容控制項來呈現您的資料。

  • 顯示方向。 Windows Store 應用程式的顯示為水平(橫向)方向,而 Windows Phone 應用程式通常是直立(縱向)顯示。這會影響應用程式的整體版面、控制項的選擇以及螢幕上的相對位置。


    圖 1
    Windows 8 為橫向顯示,Windows Phone 則為縱向顯示

  • XAML。 Windows Store 的應用程式必須使用與 Windows Phone 8 稍微不同的 XAML 版本。例如,命名空間的定義不同且根元素互不相符。

  • 螢幕尺寸。 Windows Store App 是設計給 10 英寸以上且最低解析度為 1024 × 768 的螢幕使用,而 Windows Phone 的應用程式則是設計給僅 5 英寸大小的螢幕尺寸,兩者需要完全不同的螢幕呈現。此外,相較於 Windows 8,Windows Phone 設備的選擇是比較有限的。

考慮這些應用程式的移植問題後,我們將改寫大部分應用程式中的 XAML 頁面,且只部分沿用現有的 XAML 內容。但幸運的是,我們仍可沿用大部分舊有的商業邏輯和資料範本 (Data Template),這將省去我們大量的工作。

在本次實驗中,我們將使用 Windows 8 Training Kit 中的 Contoso Cookbook 作為 App 樣本。

註 : Windows 8 Training Kit 可從下方連結下載 : https://download.microsoft.com/download/8/A/2/8A200F81-4ACF-4DD5-B7A9-0C060BB58D9E/win8cs.exe (不再可用)

Contoso Cookbook App 的完整原始碼在下方位址,可供您參考

[Lab Installation Folder]/Assets/Contoso CookBook Win8

請花些時間孰悉 Windows Store App。

註 : 實驗目的不在於提供您自動排除移植障礙的方法,而是提供通則及普遍做法以因應可能遭遇的狀況。雖然實驗中已包含了許多普遍的移植問題,但並不保證已將所有可能發生的問題完整包羅。最後,此實驗並非針對遊戲及多媒體 App;而是著重於典型資料呈現的專案。

目標

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

  • 發掘 Windows Store Apps 及 Windows Phone 8 Apps 的不同

  • 解釋將 Windows Store App 移植到 Windows Phone 8 的步驟

  • 解釋 Windows Store App 中可沿用於 Windows Phone 8 的部分

  • 為移植應用程式提供一般性的指導及描述

系統需求

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

  • Microsoft Windows 8

  • Microsoft Visual Studio 2012 for Windows 8

  • Microsoft Visual Studio Express 2012 for Windows Phone 8

練習

本動手做實驗室包含下列練習:

  1. 移植應用程式的 XAML

  2. 移植應用程式的商業邏輯

  3. 處理遺失或者不同的功能

預計完成時間為 : 45 - 60 分鐘

練習 1 : 移植 Contoso Cookbook 的 XAML

在移植前,請先熟悉 Contoso Cookbook App。


圖 2
Windows 8 中 Contoso Cookbook App 的首頁

App 內有三個管理及陳列食譜的頁面,食譜依類型分門別類,首頁中 (如圖 2) 顯示主要的食譜類型以及各類型中部份的食譜內容。


圖 3
食譜類型頁

食譜類別頁簡要介紹各個類型及列出此類型中各項食譜及特定配方。


圖 4
食譜內容頁

食譜內容頁提供詳細的食譜資料,包含材料清單、作法及烹調時間。

移植 Contoso Cookbook 的展示層需要重新改寫大部分的 XAML。以下列出一些例子供參考 :

  • 將最上層元素 從 Windows Store App 中的 LayoutAwarePage 更改為 Windows Phone 8 App 中的 PhoneApplicationPage。
  • 更改應用程式版面以符合手機的直立(縱向)呈現。
  • 尋找可替代的使用者控制項。例如 Windows Store App 中的 GridView 及 FlipView 皆不適用於 Windows Phone。
  • 調整 XAML 的命名空間。例如:Windows Store Apps 中,Grid 控制項位於 Windows.UI.Xaml.Controls 命名空間,而 Windows Phone 8 中,Grid 控制項則在 System.Windows.Controls 命名空間。

我們先從主頁開始進行,再延伸到 App 的其他部分。

任務 1 建立一個新的手機專案,然後匯入 Common 及 Asset 檔案

  1. 執行 Visual Studio Express 2012。

  2. 從檔案目錄中點選 " New " 之後點選 " Project " 。

  3. 在左邊窗格中展開 Templates 選項 -> 展開 Visual C# -> 點選 Windows Phone。

  4. 在右邊窗格中點選 Windows Phone Application, 並將應用程式取名為 ContosoCookbook ,然後按下 OK。

    圖 5
    建立一個新的手機專案

    在版本選擇對話框中,確定選取的版本為”Windows Phone OS 8.0”。 並按下OK。

    圖 6
    版本選擇

  5. 找到 [Lab Install Folder]/Assets/Code/Ex1 檔案夾,複製三個影像檔後貼進建好的 Visual Studio 專案中,我們將使用這些檔案來設計 Windows Phone App。

  6. 找到位於方案總管中的應用程式資訊清單 (WMAppManifest.xml)  並雙擊滑鼠左鍵, 讓 Visual Studio 打開清單設計頁面。

  7. 將 Display Name 修改為 Contoso Cookbook (可參考圖 7)。

  8. 點選 ”... ”按鍵,將 App Icon 指定為 applicationIcon.png,用相同的方式指定 Background.png 給小的和中的動態磚圖示

    圖 7
    應用程式資訊清單修改

  9. 在方案總管建立一個新的專案資料夾,按右鍵選擇 Add-> New Folder,將新資料夾命名為 ”Common ”。

    圖 8
    建立一個新的專案資料夾

  10. 如先前提到的,某些 Windows Store App 的程式碼與 Windows Phone 幾乎可以相容。有個好的轉換例子 (IValueConverter 實作),兩個平台間不同的是命名空間及 IValueConverter.Convert 方法中最後一個參數的類型。(Windows Store App 的命名空間為 Windows.UI.Xaml.Data,Windows Phone App 的命名空間為 System.Windows.Data)

    C# (Windows Store apps)
    public object Convert(object value, Type targetType, object parameter, string language)
    C# (Windows Phone apps)
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)

    註 : 其他 IValueConverter 方法 - ConvertBack, 有同樣的差異

    我們提供了一些 App 的原始碼資源以幫助簡化 App 建立流程,從 /Assets/Code/Ex1/Common 資料夾中增加原始碼檔案:按滑鼠右鍵點選 ”Common ”檔案夾 -> 選擇Add -> 再選擇 Existing Item

    圖 9
    在專案中增加現有的項目

  11. 在文件選擇對話框中,找到 [Lab Installation Folder]/Assets/Code/Ex1/Common 檔案夾,選擇所有檔案並點選 Add 按鍵

    圖 10
    增加已提供的資源

  12. 接下來,我們從原始的 Windows Store App 轉換資源,重複先前的步驟將 [Lab Installation Folder]/Assets/Code/Ex1/Assets 檔案中的資源移到 Windows Phone App 的“Assets”檔案夾中。這麼做的同時,務必要保存資料夾結構(例如 : 建立一個圖示的子資料夾並將對應的資源移入資料夾中。)得到的結構應該與圖 10 類似。

    圖 11
    新增了資源的專案結構

  13. 接著,我們要為 App.xaml 做些修改,宣告那些在先前步驟加入的資源,打開 App.xaml 文件,找到 Application.Resources 的 XML 元素並根據下方的代碼修改它:

    XAML
    <ResourceDictionary>
    <local:LocalizedStrings xmlns:local="clr-namespace:ContosoCookbook"
    x:Key="LocalizedStrings"/>

    <sys:String x:Key="AppName">Contoso Cookbook</sys:String>
    <sys:Double x:Key="LogoImageWidth">150</sys:Double>
    <common:SizeToResolutionConverter x:Key="SizeToResolutionConverter"/>
    <common:StringImageSourceConverter x:Key="ToImageConverter"/>
    <common:ImageSourceToStringConverter x:Key="ToStringConverter"/>
    <common:BooleanToVisibilityConverter x:Key="VisibilityConverter"/>
    <common:BooleanNegationConverter x:Key="BooleanNegation"/>
    <common:UserImagesDisplayConverter x:Key="ImagesDisplayConverter"/>

    <ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="Common/CustomStyles.xaml"/>
    </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>

  14. 接著,替 Application 元素加上兩個 XML 命名空間如下:

    XAML
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    xmlns:common="clr-namespace:ContosoCookbook.Common"

  15. 現在 App 已經可以做 UI 修改了。

任務 2 – 移植主頁

打開 Windows Store 內的 Contoso Cookbook App 主頁 (GroupedItemsPage.xaml 檔案)。在移植時必須注意以下這些介於兩個平台間的差異:

  • 兩者間 XAML 根元素不同,Windows Store App 的根元素為 LayoutAwarePage (源於 Page 類別),而 Windows Phone App 的根元素則為 PhoneApplicationPage

  • Windows Store App 的 XAML 的根元素是包含了有兩個資料列的 Grid 控制項:其一資料列作為標題及後退按鈕,另一資料列作為 PAGE 的內容,下方是 Grid 控制項的第一個資料列:

    XAML (Windows Store app, partial)
    <Grid Style="{StaticResource LayoutRootStyle}">
    <Grid.RowDefinitions>
    <RowDefinition Height="140"/>
    <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <!-- Back button and page title -->
    <Grid>
    <Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto"/>
    <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Button x:Name="backButton" Click="GoBack"
    IsEnabled="{Binding Frame.CanGoBack, ElementName=pageRoot}"
    Style="{StaticResource BackButtonStyle}"/>

    <Image Source="/Images/theme/logo.png" Grid.Column="1"
    HorizontalAlignment="Left" Margin="0,12,0,10"
    Width="280"/ >
    </Grid>
    ...

    當建立一個新的 Windows Phone 專案,Visual Studio 將自動建立 MainPage.xaml 版面範本,如下方所列:

    XAML (Windows Phone app, partial)
    <Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <!--TitlePanel contains the name of the application and page title-->
    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
    <TextBlock Text="MY APPLICATION"
    Style="{StaticResource PhoneTextNormalStyle}" Margin="12,0"/>
    <TextBlock Text="page name" Margin="9,-7,0,0"
    Style="{StaticResource PhoneTextTitle1Style}"/>
    </StackPanel>

    <!--ContentPanel - place additional content here-->
    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">

    </Grid>
    ...

    預設範本包含兩個資料列的 Grid 控制項,第一行資料列為應用程式及頁面的標題。第二行包含另一個頁面內容的 Grid 控制項。

  1. 在對照 Windows Phone 8 及 Windows Store 兩者的 App 首頁後,我們應該為 grid 的標題做兩項更改:

    • 顯示方向:Windows Phone 是直立顯示資料,沒有足夠空間顯示多列資料;需將兩列 Grid 改為 StackPanel。

    • 返回按鍵。手機有硬體上的返回按鍵,代表螢幕上的返回鍵是不必要的。因此,一般來說,我們並不鼓勵 Windows Phone App 內設有螢幕上的返回鍵。
      為了這些兩者間的變化,我們必須修改標題 XAML 如下:

      XAML (Windows Phone)
      <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,15">
      <Image x:Name="imgLogo" Source="Assets/Title.png" Stretch="Uniform"
      HorizontalAlignment="Left" Width="150"/>
      <TextBlock x:Name="PageTitle" Text="Contoso Recipes"
      Foreground="{StaticResource CustomGroupTitleBrush}"
      Style="{StaticResource PhoneTextTitle1Style}" Margin="9,-7,0,0"/>
      </StackPanel>

  2. 完成標題後,我們來看頁面的內容,Windows Store App 使用新的 Semantic Zoom 控制項,這不適用於 Windows Phone。

    註 : Semantic Zoom 是 Windows 8 使用的優化觸控技術,當單一頁面資訊太多的時候,方便使用者顯示及瀏覽頁面中的全部資訊 (例如 : 相本、App 清單及通訊錄) 想要知道更多關於 Sematic Zoom 的資訊,請由以下連結參考 Windows 8 說明文件 : https://msdn.microsoft.com/en-us/library/windows/apps/hh465319.aspx

    Windows Store App 內容部分由 SemanticZoom XAML 標記組成,包含 ”放大 (Zoomed In) ”及 ”縮小 (Zoomed Out) 兩種不同的顯示狀態。Windows Phone 並不支援 Semantic Zoom,這意味著我們必須手動移植 ”放大 (Zoomed In) ”功能到 Windows Phone App 中。以下是 Windows Store App 的 ”放大 (Zoomed In) ”狀態:

    XAML (Windows Store app)
    <GridView x:Name="itemGridView"
    AutomationProperties.AutomationId="ItemGridView"
    AutomationProperties.Name="Grouped Items"
    Margin="0,-3,0,0"
    Padding="116,0,40,46"
    ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"
    ItemTemplate="{StaticResource Standard250x250ItemTemplate}"
    SelectionMode="None"
    IsItemClickEnabled="True"
    ItemClick="ItemView_ItemClick">

    <GridView.ItemsPanel>
    <ItemsPanelTemplate>
    <VirtualizingStackPanel Orientation="Horizontal"/>
    </ItemsPanelTemplate>
    </GridView.ItemsPanel>
    <GridView.GroupStyle>
    <GroupStyle>
    <GroupStyle.HeaderTemplate>
    <DataTemplate>
    <Grid Margin="1,0,0,6">
    <Button
    AutomationProperties.Name="Group Title"
    Content="{Binding Title}"
    Click="Header_Click"
    Style="{StaticResource TextButtonStyle}"/>
    </Grid>
    </DataTemplate>
    </GroupStyle.HeaderTemplate>
    <GroupStyle.Panel>
    <ItemsPanelTemplate>
    <VariableSizedWrapGrid Orientation="Vertical"
    Margin="0,0,80,0"/>
    </ItemsPanelTemplate>
    </GroupStyle.Panel>
    </GroupStyle>
    </GridView.GroupStyle>
    </GridView>

    前面的 XAML 代碼是 Windows Store App 中 GridView 的標準範本,需做如下修改:

    • 以 VirtualizingStackPanel 控制項取代預設的 ItemsPanel

    • 加上 ItemsPanelTemplate 及 HeaderTemplate 的 GroupStyle
      GridView 控制項適用於水平橫向顯示,Windows Phone App 中,我們將以較適合直立顯示的 ListBox 來取代,此外,需將 ListBox’s ItemsPanel 改為 VirtualizingStackPanel,因為進行基礎集合時可包含許多對象。
      首先,在 Windows Phone App 中建立一個新的範本來呈現食譜類型,在 ContentPanel Grid 中加入下面的代碼:

      XAML (Windows Phone)
      <ListBox x:Name="lstGroups" ItemsSource="{Binding}"
      SelectionChanged="lstGroups_SelectionChanged">
      <!--This is the same ItemsPanel Template as in the Win8 app-->
      <ListBox.ItemsPanel>
      <ItemsPanelTemplate>
      <VirtualizingStackPanel Orientation="Vertical"/>
      </ItemsPanelTemplate>
      </ListBox.ItemsPanel>
      <ListBox.ItemTemplate>
      <!--A New DataTemplate suitable for the smaller Phone screen,
      similar to the GroupHeaderTemplate that we had for the GridView
      control in the Win8 app-->
      <DataTemplate>
      <Grid Margin="5" >
      <Grid.ColumnDefinitions>
      <ColumnDefinition Width="150"/>
      <ColumnDefinition Width="*"/>
      <ColumnDefinition Width="20"/>
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
      <RowDefinition/>
      <RowDefinition/>
      </Grid.RowDefinitions>
      <Image Source="{Binding BackgroundImage}" Width="150"
      Stretch="UniformToFill" Grid.RowSpan="2" />
      <TextBlock Text="{Binding Title}" FontSize="48"
      Grid.Column="1" Grid.Row="0"
      Foreground="{StaticResource
      CustomApplicationTextBrush}"/>
      <Image Source="/Assets/lock.png"
      Visibility="{Binding LicensedRequired,
      Converter={StaticResource VisibilityConverter}}"
      Grid.Column="2" Grid.Row="0" HorizontalAlignment="Right" />

      <TextBlock Text="{Binding Description}" FontSize="24"
      Grid.Column="1" Grid.Row="1" Foreground="{StaticResource CustomApplicationTextBrush}"/>
      </Grid>
      </DataTemplate>
      </ListBox.ItemTemplate>
      </ListBox>

  3. 接著,Windows Phone App 頁面中的根元素需做些修改。修改 PhoneApplicationPage 元素中 SystemTray.IsVisible 屬性為 False (預設為 True):

    XAML
    shell:SystemTray.IsVisible="False"

    這設定會隱藏包含時間和電池狀況的系統列,讓 App 在全螢幕的狀態。

  4. 現在,使用 App 中用戶自訂背景筆刷來更改 LayoutRoot Grid 的背景。找到“LayoutRoot” Grid 元素並修改它的背景屬性如下:

    XAML
    Background="{StaticResource CustomApplicationBackgroundBrush}"

  5. 最後,我們必須實現 XAML 代碼中 ListBox 的 SelectionChanged 事件。打開 MainPage.xaml.cs 文件,並在 MainPage 中添加以下代碼:

    C#
    private void lstGroups_SelectionChanged(object sender,
    SelectionChangedEventArgs e)
    {

    }

  6. 這樣就完成了 Windows Phone App 主頁中 XAML 的變更。

    註 : GroupedItemsPage.xaml 文件也包含了 Windows 8 Snapped View 的程式碼。Windows Phone 並不支援 Snapped View,所以我們可以忽略此代碼。

任務 3 – 移植食譜類型頁

GroupDetailPage.xaml 頁面的移植過程將出現移植主頁時我們沒有遭遇過的問題。食譜類型頁呈現各個類型的簡要介紹及類型中的食譜清單。

圖 12
食譜類型頁

移植此頁到 Windows Phone,我們需要一個與手機相容的方式管理兩種資料(第一種是食譜類型的簡介,第二種是食譜內容)。因為 Windows Phone App 是直立顯示,所以我們使用 Pivot 控制項。此控制項提供了一種快速的方式來管理應用程式中的瀏覽,且控制項同時可作為過濾大量資訊及頁面轉換的瀏覽介面。第一個 Pivot 頁面顯示食譜類型的簡介、第二種則顯示各類型中的食譜內容。

  1. 為 App 添加一個新的影像頁面。在 Visual Studio 的方案總管中,按右鍵點擊專案名稱,選擇 Add 並選 New Item:

    圖 13
    為專案增加新的影像頁面

  2. 在打開的對話框中,選擇 Windows Phone Portrait Page,命名為 GroupDetailPage.xaml 並點擊 Add:

    圖 14
    增加新的 Windows Phone 影像頁面

  3. 我們現在修改預設頁面使其相容於兩頁面的 Pivot 控制項,並為 Page 的 XML 元素加入以下命名空間作為準備步驟:

    XAML
    xmlns:controls="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"

  4. 此外,修改 SystemTray.IsVisible 屬性為 False:

    XAML
    shell:SystemTray.IsVisible="False"

  5. 接下來,找到名為 LayoutRoot 的 Grid 並修改其背景屬性如下:

    XAML
    Background="{StaticResource CustomApplicationBackgroundBrush}"

  6. 並將 LayoutRoot 的 Grid 控制項內容改為一個新的 Pivot 控制項如下:

    XAML
    <!--Pivot Control-->
    <controls:Pivot x:Name="pivot">
    <controls:Pivot.Title>
    <Image x:Name="imgLogo" Source="Assets/Title.png" Stretch="Uniform"
    HorizontalAlignment="Left"
    Width="{StaticResource LogoImageWidth}"/>
    </controls:Pivot.Title>

    </controls:Pivot>

    代碼也宣示出應用程式的標誌就是 Pivot 控制項的標題。

  7. 增加第一個 PivotItem 存放食譜類型詳細資料。在 Pivot 新增以下代碼,並放在 Pivot.Title 宣告之後:

    XAML
    <!--Pivot item one-->
    <controls:PivotItem>
    <controls:PivotItem.Header>
    <Grid>
    <TextBlock Text="{Binding Title}"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    </Grid>
    </controls:PivotItem.Header>
    <Grid>
    <Grid.RowDefinitions>
    <RowDefinition Height="250"/>
    <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Image Source="{Binding BackgroundImage}" Stretch="UniformToFill"
    x:Name="img"/>
    <Rectangle Fill="{StaticResource CustomListViewItemOverlayBackgroundBrush}"
    Width="{Binding ElementName=img, Path=ActualWidth}"
    Height="75" VerticalAlignment="Bottom"/>
    <StackPanel Orientation="Horizontal" Margin="5"
    HorizontalAlignment="Right" VerticalAlignment="Bottom" >
    <TextBlock Text="Total Recipes: "
    Foreground="{StaticResource CustomListViewItemOverlayTextBrush}"
    FontSize="48" />
    <TextBlock Text="{Binding RecipesCount}"
    Foreground="{StaticResource CustomListViewItemOverlayTextBrush}"
    FontSize="48" />
    </StackPanel>
    <ScrollViewer Grid.Row="1">
    <TextBlock Text="{Binding Description}" TextWrapping="Wrap"
    Foreground="{StaticResource CustomApplicationTextBrush}"
    FontSize="24"/>
    </ScrollViewer>
    </Grid>
    </controls:PivotItem>

  8. 我們在現存的食譜類型下新增第二個包含 ListBox 及食譜詳細資料的 PivotItem。在第一個 PivotItem 宣告後方增加下面的代碼:

    XAML
    <!--Pivot item two-->
    <controls:PivotItem>
    <controls:PivotItem.Header>
    <StackPanel Orientation="Horizontal">
    <TextBlock Text="Recipes"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    </StackPanel>
    </controls:PivotItem.Header>
    <Grid>
    <ListBox x:Name="lstRecipes" ItemsSource="{Binding Items}"
    SelectionChanged="lstRecipes_SelectionChanged">
    <ListBox.ItemsPanel>
    <ItemsPanelTemplate>
    <VirtualizingStackPanel Orientation="Vertical"/>
    </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemTemplate>
    <DataTemplate>
    <Grid Margin="5">
    <Grid.ColumnDefinitions>
    <ColumnDefinition Width="200"/>
    <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
    <RowDefinition/>
    <RowDefinition/>
    </Grid.RowDefinitions>
    <Image Source="{Binding BackgroundImage}"
    Width="150"
    Stretch="UniformToFill"
    Grid.RowSpan="2" />
    <TextBlock Text="{Binding Title}"
    FontSize="48"
    Grid.Column="1" Grid.Row="0"
    Foreground="{StaticResource CustomApplicationTextBrush}"/>
    <StackPanel Grid.Column="1" Grid.Row="1"
    Orientation="Horizontal">
    <TextBlock Text="Prep time: "
    FontSize="24"
    Foreground="{StaticResource CustomApplicationTextBrush}"/>
    <TextBlock Text="{Binding PrepTime}"
    FontSize="24"
    Foreground="{StaticResource CustomApplicationTextBrush}"/>
    </StackPanel>
    </Grid>
    </DataTemplate>
    </ListBox.ItemTemplate>
    </ListBox>
    </Grid>
    </controls:PivotItem>

  9. 最後,為隱藏代碼 (code-behind) 新增一個事件處理程序。打開 GroupDetailPage.xaml.cs 的文件並加入下方的代碼:

    C#
    private void lstRecipes_SelectionChanged(object sender,
    SelectionChangedEventArgs e)

    註 : 我們會在之後的實驗中實現這個事件處理程序。

  10. 我們完成了前兩頁的移植,現在我們可以前進到最後一頁的移植。

任務 4 – 移植食譜內容頁

最後一個要移植的頁面為顯示食譜內容的頁面 (包含材料清單、作法等) 在 Windows Store App 中,此頁的主要控制項為 FlipView,其內部有個水平滾動的 Grid 控制項使得食譜間轉換的動作可以順利運作。

圖 15
食譜內容頁

Windows Phone 並不支援 FlipView 控件,意味著此功能必須更換,使用 Pivot 控制項來取代 Grid,原來頁面的三個 grid columns 換成三個 PivotItems:食譜內容(包含圖、說明、指示及準備時間),材料及圖片(使用者提供)。Windows Phone App 將也有「應用程式選單」以對應 Windows Store App 的功能。

註 : 將不會移植上方圖片中的定時器功能

  1. 新增一個命名為 RecipeDetailPage 的頁面。按先前任務修改 SystemTray 屬性、LayoutRoot 背景及移除 LayoutRoot 內容。

  2. 先前討論到實作 Pivot 控制項及三個 PivotItems 的 LayoutRoot Grid,我們必須增加以下代碼:

    XAML
    <!--Pivot Control-->
    <controls:Pivot x:Name="pivot">
    <controls:Pivot.Title>
    <Image x:Name="imgLogo" Source="Assets/Title.png" Stretch="Uniform"
    HorizontalAlignment="Left"
    Width="{StaticResource LogoImageWidth}"/>
    </controls:Pivot.Title>

    <!--Pivot item one-->
    <controls:PivotItem>
    <controls:PivotItem.Header>
    <Grid>
    <TextBlock Text="{Binding Title}"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    </Grid>
    </controls:PivotItem.Header>
    <Grid>
    <Grid.RowDefinitions>
    <RowDefinition Height="250"/>
    <RowDefinition Height="*"/>
    <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Image Source="{Binding BackgroundImage}"
    Stretch="UniformToFill"/>
    <ScrollViewer Grid.Row="1">
    <TextBlock Text="{Binding Directions}"
    TextWrapping="Wrap"
    Foreground="{StaticResource CustomApplicationTextBrush}"
    FontSize="24"/>
    </ScrollViewer>
    <StackPanel Grid.Row="2" Orientation="Horizontal" Margin="15">
    <TextBlock Text="Prep time: " FontSize="24"
    Foreground="{StaticResource CustomApplicationTextBrush}"/>
    <TextBlock Text="{Binding PrepTime}" FontSize="24"
    Foreground="{StaticResource CustomApplicationTextBrush}"/>
    </StackPanel>
    </Grid>
    </controls:PivotItem>

    <!--Pivot item two-->
    <controls:PivotItem>
    <controls:PivotItem.Header>
    <Grid>
    <TextBlock Text="Ingredients"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    </Grid>
    </controls:PivotItem.Header>
    <ListBox x:Name="lstIngredieants"
    ItemsSource="{Binding Ingredients}">
    <ListBox.ItemsPanel>
    <ItemsPanelTemplate>
    <VirtualizingStackPanel Orientation="Vertical"/>
    </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemTemplate>
    <DataTemplate>
    <TextBlock Text="{Binding}" FontSize="24"
    Foreground="{StaticResource CustomApplicationTextBrush}"
    Margin="5"/>
    </DataTemplate>
    </ListBox.ItemTemplate>
    </ListBox>
    </controls:PivotItem>

    <!--Pivot item three-->
    <controls:PivotItem x:Name="PicsPivot">
    <controls:PivotItem.Header>
    <Grid>
    <TextBlock Text="My Pictures"
    Foreground="{StaticResource CustomGroupTitleBrush}"/>
    </Grid>
    </controls:PivotItem.Header>
    <Grid>
    <TextBlock Text="No images found."
    Foreground="{StaticResource CustomGroupTitleBrush}"
    FontSize="24" HorizontalAlignment="Center"
    Visibility="{Binding UserImages, Converter={StaticResource ImagesDisplayConverter}}"
    VerticalAlignment="Center" />
    <ListBox x:Name="listUserPictures"
    ItemsSource="{Binding UserImages}">
    <ListBox.ItemsPanel>
    <ItemsPanelTemplate>
    <VirtualizingStackPanel
    Orientation="Vertical"/>
    </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemTemplate>
    <DataTemplate>
    <Grid>
    <Image Source="{Binding Converter={StaticResource ToImageConverter}}"
    Margin="5"/>
    <Rectangle Fill="{StaticResource CustomListViewItemOverlayBackgroundBrush}"
    Height="50"
    VerticalAlignment="Bottom"/>
    <StackPanel
    Orientation="Horizontal"
    Margin="5"
    HorizontalAlignment="Left"
    VerticalAlignment="Bottom">
    <TextBlock Text="{Binding Converter={StaticResource ToStringConverter}}"
    Margin="10,0,0,5"
    Foreground="{StaticResource CustomListViewItemOverlayTextBrush}"
    FontSize="22" />
    </StackPanel>
    </Grid>
    </DataTemplate>
    </ListBox.ItemTemplate>
    </ListBox>
    </Grid>
    </controls:PivotItem>
    </controls:Pivot>

  3. 接著,增加幾個按鈕到 ApplicationBar

    XAML
    <phone:PhoneApplicationPage.ApplicationBar>
    <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True"
    Mode="Default" Opacity="1.0">
    <shell:ApplicationBarIconButton x:Name="btnTakePicture"
    IconUri="/Assets/Icons/camera.png" Click="btnTakePicture_Click"
    Text="Take Picture"/>
    <shell:ApplicationBarIconButton x:Name="btnShareTask"
    IconUri="/Assets/Icons/share.png" Click="btnShareShareTask_Click"
    Text="Share Image"/>
    <shell:ApplicationBarIconButton x:Name="btnStartCooking"
    IconUri="/Assets/Icons/alarm.png" Click="btnStartCooking_Click"
    Text="Start Cooking"/>
    <shell:ApplicationBarIconButton x:Name="btnPinToStart"
    IconUri="/Assets/Icons/like.png" Click="btnPinToStart_Click"
    Text="Pin To Start"/>
    </shell:ApplicationBar>
    </phone:PhoneApplicationPage.ApplicationBar>

  4. 最後,在隱藏代碼 (code-behind) 檔案 (RecipeDetailPage.xaml.cs) 中加入以下事件處理程序:

    C#
    private void btnTakePicture_Click(object sender, EventArgs e)
    {

    }

    private void btnShareShareTask_Click(object sender, EventArgs e)
    {

    }

    private void btnStartCooking_Click(object sender, EventArgs e)
    {

    }

    private void btnPinToStart_Click(object sender, EventArgs e)
    {

    }

    Windows8 和 Windows Phone8 是有著不同 API 界面及設計風格的操作系統,這使得要移植舊有的 XAML 碼到 10 英吋或者更大的螢幕顯示設備幾乎不可能,回顧先前修改的地方,我們不得不重頭改寫 XAML 頁面,不過,仍有某些可以沿用的部分:

    • 樣式 (Style) 將 Windows 8 建立的樣式轉換到 Windows Phone,兩者間有著類似的外觀及感覺。比較原有的 CustomStyles.xaml 檔案及任務 1 中加入的專案,大部分樣式都可在兩個平台正常運作,或許只需要微調及稍加修正即可。

    • 資料範本 (DataTemplate) 所有 Windows Store App 建立的資料範本都可不做任何修改沿用到 Windows Phone 設備,想要大部分的元素看起來相同,只需要做些尺寸及版面調整以符合 Windows Phone 較小的螢幕尺寸。

    註 : 當應用程式的樣式及資料範本使用 Windows Store App 中例如筆刷之類的資源,移植樣式及資料範本到 Windows Phone 8 時就需要做某些修改加以取代。

    當你設計了一個 App 並希望能在 Windows 8 及 Windows Phone 8 同時運作時,本練習中的研究幫助你可預知當移植你的 XAML 會遭遇到的問題。因此,在設計實作 Windows 8 樣式及範本時,請盡量使其容易移植到 Windows Phone 8 系統。

練習 2 :移植 Contoso Cookbook 的商業邏輯

移植展示層到 Windows Phone 8 後,接著要來移植商業邏輯。兩者都使用 C# 語言實行,代表大部分的代碼不需修改。

註 : Windows Store Apps 與 Windows Phone Apps 使用不同的命名空間 ; 大多數情況下,命名空間的名稱不得不改變,即使代碼也是,否則無法完全相容。

任務 1 – 定義可沿用的商業邏輯 c

第一個任務是匯入食譜資料和影像。

  1. 打開 Windows Store App (此實驗中提供的) 及展開 Visual Studio 方案總管中的 Data 資料夾。

    圖 16
    Data 專案資料夾
  2. 打開及找到 Data 檔案中的 Recipes.txt 文件(現在先忽略文件夾中的其他文件)。Recipes.txt 文件中包含了 JSON 格式的 App 資料(食譜及食譜類型).當執行 Windows Phone App 時,我們也要用這些檔案。建立一個新的專案資料夾,命名為 Data,並加入 [Lab Installtion Folder] /Assets/Code/Ex2/Data 資料夾中的 Recipes.txt 檔案。
  3. 接著展開方案總館中的 DataModel 資料夾。

    圖 17
    DataModel 應用程式資料夾
    Windows Store App 的 DataModel 文件夾包含四個文件。但其中有某些與 Windows Store App 的授權因素有關無法用於 Windows Phone App。例如 ProductLicenseDataSource.cs 及 AppLicencseDataSource.cs。
  4. 檢查 RecipeDataSource.cs 和 SampleDataSource.cs 這兩個文件。
    大部分代碼不包含 Windows 8 任何特定功能,代表我們可以直接移植到 Windows Phone App。 但有個例外,RecipeDataSource.cs 檔案使用不能在 Windows Phone 8 中執行的 JsonArray 類別(在 RecipeDataSource.LoadLocalDataAsync 方法中)。在下一個任務裏,我們將看到如何將這段代碼移植到 Windows Phone8。
  5. 最後,複製整個影像檔 (從 [Lab Install Location] /Assets/Code/Ex2/Images) 到專案。最簡單的方法是將 Windows Phone 內的 ContosoCookbook 專案打開並從 Windows Explorer 中拖動整個文件夾到 Visual Studio 方案總管。如以下圖片:

    圖 18
    在方案總管的專案資料夾中加入了影像檔案

任務 2 – 移植 Load Method 到 Windows Phone 8

Windows 8 推出 Windows.Data.Json 命名空間來處理 JSON 格式的數據。但這個命名空間並不存在於 Windows Phone 8 平台。因此,加載 Recipes.txt 文件並以 DataContractJsonSerializer 類別來取代 JSONArray 的用途 (及其他與 JSON 相關的 Windows 8 類別) 我們用下列代碼來加載 Windows Store App 中的食譜文件:

C# (Windows Store App, Partial)
public static async Task LoadLocalDataAsync()
{
// Retrieve recipe data from Recipes.txt
var file = await Package.Current.InstalledLocation.GetFileAsync("Data\\Recipes.txt");
var result = await FileIO.ReadTextAsync(file);

// Parse the JSON recipe data
var recipes = JsonArray.Parse(result);

// Convert the JSON objects into RecipeDataItems and RecipeDataGroups
// The rest of initialization...

移植 Load Method 到 Windows Phone 8,要用 DataContractJsonSerializer 類別取代 JsonArray 的用途。此外,從 App 組件 (package) 檢索 Recipes.txt 文件的代碼必須更改如下:

C# (Window Phone app, partial)
public async void LoadLocalDataAsync()
{
// Retrieve recipe data from Recipes.txt
var sri = App.GetResourceStream(new Uri("Data\\Recipes.txt",
UriKind.Relative));
List<Type> types = new List<Type>();
types.Add(typeof(RecipeDataItem));
types.Add(typeof(RecipeDataGroup));
types.Add(typeof(RecipeDataCommon));
DataContractJsonSerializer deserializer = new
DataContractJsonSerializer(typeof(IEnumerable<RecipeDataItem>), types);

IEnumerable<RecipeDataItem> data =
(IEnumerable<RecipeDataItem>)deserializer.ReadObject(sri.Stream);
// The rest of initialization...

註:代碼不完整;移植 Windows Store 到 Windows Phone。原始碼要再多做些改變。為簡單起見,將省略其他代碼。

我們現在繼續進行的 UI 元件的隱藏代碼 (code behind)。雖然一般的 UI 在這兩個平台上的流程是一樣的,但不同的瀏覽樣式和使用者控制項使得代碼無法完全在兩個平台上通用。

  1. 在 Windows Phone 解決方案裏增加適當的檔案並將 App 連結上資料來源。在 Windows Phone project (方案總管) 中新建 DataModel 專案資料夾,並加入位於 [Lab Installtion Folder] /Assets/Code/Ex2/DataModel 資料夾中的檔案。

  2. 下面幾個步驟中,我們將食譜的數據模型綁定到應用程序中。打開 App.xaml.cs 檔案並在 App 類別前加入下方的 using 陳述式:

    C#
    using ContosoCookbook.Data;

  3. 為 App 類別新增一個新的屬性如下:

    C#
    public static RecipeDataSource Recipes { get; set; }

  4. 最後,初始化屬性,瀏覽 class 的建構函數,並加入下方的代碼在 method 的最後面:

    C#
    Recipes = new RecipeDataSource( );

  5. 打開 MainPage.xaml.cs 檔案並加入下方的 using 陳述式:

    C#
    using ContosoCookbook.Data;

  6. 當 App 在背景下載資料,頁面應該顯示下載進度。Windows Phone App 中使用內建的 ProgressIndicator,顯示在系統列。加入下方內容到 MainPage 類別:

    C#
    private Microsoft.Phone.Shell.ProgressIndicator pi;

  7. 接下來,我們的下載並綁定食譜數據到 UI。每當用戶瀏覽頁面時,撰寫初始化代碼的最好地方是在 OnNavigateTo 方法中,新增以下代碼到 MainPage 類別:

    C#
    protected async override void
    OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
    {
    if (!App.Recipes.IsLoaded)
    {
    pi = new Microsoft.Phone.Shell.ProgressIndicator();
    pi.IsIndeterminate = true;
    pi.Text = "Loading data, please wait...";
    pi.IsVisible = true;

    Microsoft.Phone.Shell.SystemTray.SetIsVisible(this, true);
    Microsoft.Phone.Shell.SystemTray.SetProgressIndicator(this, pi);

    await App.Recipes.LoadLocalDataAsync();

    lstGroups.DataContext = App.Recipes.ItemGroups;

    pi.IsVisible = false;
    Microsoft.Phone.Shell.SystemTray.SetIsVisible(this, false);

    }
    base.OnNavigatedTo(e);
    }

    前面的代碼檢查食譜資料是否已經下載、初始化 ProgressIndicator 並連結到系統列。接下來,該代碼會以非同步的方式下載食譜資料 (使用前面建立的 LoadLocalDataAsync 方法),並綁定下載好的食譜種類到練習 1 中建立的 ListBox 的 DataContext 。
    如預期地,在這個階段執行移植好的 App 有下載食譜資料及顯示主頁資訊的功能:

    圖 19
    從 Windows Store App 移植的主頁

  8. 在主頁的代碼中,最後要改的是實作瀏覽所選擇美食類型的功能。需修改 lstGroups_SelectionChanged 方法為下方代碼:

    C#
    private void lstGroups_SelectionChanged(object sender,
    SelectionChangedEventArgs e)
    {
    if (lstGroups.SelectedIndex > -1)
    {
    NavigationService.Navigate(new Uri("/GroupDetailPage.xaml?ID=" +
    (lstGroups.SelectedItem as RecipeDataGroup).UniqueId,
    UriKind.Relative));
    }
    }

  9. 現在我們進行到 GroupDetailPage 頁面。打開 GroupDetailPage.xaml.cs 並在檔案最上面的 using 區加入下方 using 陳述式。

    C#
    using ContosoCookbook.Data;

  10. 接著,在類別中加入下方類別成員:

    C#
    RecipeDataGroup group;

  11. 用我們在主頁使用的方法,我們現在重寫 OnNavigatedTo 方法以特定 "類型 ID" 篩選食譜資料(來自主頁類似瀏覽參數的資料) 並設定美食類型給 Pivot 控制項的 DataContext 屬性。在 GroupDetailPage  類型中加入以下代碼:

    C#
    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
    string UniqueId = NavigationContext.QueryString["ID"];
    group = App.Recipes.FindGroup(UniqueId);
    pivot.DataContext = group;

    base.OnNavigatedTo(e);
    }

  12. 這頁最後的修改是當用戶點選一個食譜時,支援 RecipeDetailPage 中的瀏覽功能。更改 lstRecipes_SelectionChanged 方式為下方代碼:

    C#
    private void lstRecipes_SelectionChanged(object sender,
    SelectionChangedEventArgs e)
    {
    if (lstRecipes.SelectedItems.Count > 0)
    NavigationService.Navigate(new Uri("/RecipeDetailPage.xaml?ID=" +
    (lstRecipes.SelectedItem as RecipeDataItem).UniqueId,
    UriKind.Relative));
    }

    執行這個階段的 App,首頁仍然有完整的功能示範 (可參考圖 16)。點擊特定的美食類型則可瀏覽類型簡介及類型中相應的食譜資料。

    圖 20
    美食類別的詳細資料

    圖 21
    類別中的食譜清單

  13. 接下來,我們改變食譜頁以顯示食譜資料。如先前作法,打開 RecipeDetailsPage.xaml.cs 檔案,下入下列 using 陳述式到檔案頂端的 using 區域:

    C#
    using ContosoCookbook.Data;

  14. 然後,新增下方類別成員到 RecipeDetailsPage 類別中:

    C#
    private RecipeDataItem item;

  15. 最後,覆蓋 OnNavigatedTo 方法以下載及綁定食譜到 UI 中。新增以下的 code block 到類型:

    C#
    protected async override void OnNavigatedTo(NavigationEventArgs e)
    {
    string UniqueId = "";

    UniqueId = NavigationContext.QueryString["ID"];

    if (!App.Recipes.IsLoaded)
    await App.Recipes.LoadLocalDataAsync();

    NavigateToRecipe(UniqueId);

    base.OnNavigatedTo(e);
    }

    private void NavigateToRecipe(string UniqueId)
    {
    item = App.Recipes.FindRecipe(UniqueId);
    pivot.DataContext = item;
    }

  16. 這個步驟,App 是有完整功能的,你可以隨意執行瀏覽三個頁面。

    圖 22
    食譜頁

    圖 23
    材料清單

  17. 雖然 App 擁有基本完整的功能。但缺少 Windows Store App 的附加功能,包括分享、提醒及釘選特定的食譜到主頁上,因此我們要在下階段練習中增加這些功能。

練習 3 :手機定製功能

這部分的練習重點在於了解 Windows 8 的特定功能如何對應到 Windows Phone 8 系統中。 許多 Windows 8 的特定功能,當移植到 Windows Phone 8 時存在著稍微不同的格式。想要成功移植 App 到 Windows Phone 8,你必須了解有那些特定功能存在、需要做哪些修改以及 Windows Phone 8 不支援那些功能

這一個練習裡探討的功能如下:

  • Charms. Windows 8 中引入一項新功能稱為 Charms。利用Share Charm 來分享 App 資料;App 僅需要申明那些可以被分享的資料型態為何,任何其他可以處裡同種資料型態的App都可以分享這些資料。


    圖 24
    Windows 8 Charms

    Charms 無法用在 Windows Phone 系統,但可以透過其他方式讓某些功能在 Windows Phone 系統運作,可以從 Microsoft.Phone.Tasks 命名空間中挑選工作來提供部分 Charm 的功能。

    下面是某些可以在 Windows Phone 中運作的例子:

    • CameraCaptureTask. 這個功能啟動相機 App (Camera App)。讓使用者可以藉由 App 拍照。

    • SearchTask. 這個功能啟動網路搜尋 App (Web Search App)。讓使用者可以藉由 App 搜尋網路。

    • ShareStatusTask. 這個功能啟動對話讓使用者可以在任何支援的社群網路分享狀態訊息。

  • 釘選 兩種平台上,使用者都可以釘選項目到首頁。但確切的 API 細節略有不同。主要區別在建立及使用哪個物件釘選動態磚到首頁。

  • 通知 兩種平台上,App 可以利用 toast notifications 展示訊息給使用者,但每個平台使用不一樣的方式及獨立的 API,Windows 8 App 使用 ToastNotificationManager 類別 (位在 Windows.UI.Notifications 命名空間);Windows Phone App 則使用 ShellToast 類別 (位在 Microsoft.Phone.Shell 命名空間)。雖然 Windows Phone App 可以重新建立 toast 提醒功能,但我們會使用更合適的方法 - Reminder 類別 (位於 Microsoft.Phone.Scheduler 命名空間),Reminder 類別會透過 ScheduledActionService 類別 (位於相同的命名空間) 定時發送通知,定時器用來提醒使用者烹調時間及程序。

任務 1 – 在 Windows Phone 8 App 實作照片分享

Windows Store App 使用 Share Charm 來分享使用者抓取的影像, 並利用內建的圖像捕捉 API 來擷取。Windows Phone 也有這兩個功能但 API 不同。

我們從圖像擷取功能開始。擷取圖像我們要使用標準 CameraCaptureTask 類別。接著,用 ShareMediaTask 取代 Windows 8 Share Charm,新的 Windows Phone 8 可以允許應用程式在社群網路透過系統提供的標準對話框分享媒體檔案。

  1. 打開 RecipeDetailPage.xaml.cs 檔案並加入下方 using 陳述式到檔案頂端的 using 區:

    C#
    using Microsoft.Phone.Tasks;
    using System.IO.IsolatedStorage;
    using System.Windows.Media.Imaging;

  2. 接著,在 RecipeDetailPage 類別建立一個新的類別成員如下:

    C#
    CameraCaptureTask camera;

  3. 更改類別的建構式來初始化“camera”變數,在建構式呼叫 InitializeComponent 後增加下方代碼。

    C#
    camera = new CameraCaptureTask();
    camera.Completed += camera_Completed;

  4. 接著,增加下方代碼到類別中 (這個代碼處理 CameraCaptureTask 物件的 Completed 事件):

    C#
    void camera_Completed(object sender, PhotoResult e)
    {
    if (e.TaskResult == TaskResult.OK)
    {
    System.Windows.Media.Imaging.BitmapImage bmp = new
    System.Windows.Media.Imaging.BitmapImage();
    bmp.SetSource(e.ChosenPhoto);

    using (IsolatedStorageFile isoStore =
    IsolatedStorageFile.GetUserStoreForApplication())
    {
    if (!isoStore.DirectoryExists(item.Group.Title))
    isoStore.CreateDirectory(item.Group.Title);

    string fileName = string.Format("{0}/{1}(zh-tw,MSDN.10).jpg",
    item.Group.Title,
    DateTime.Now.ToString("dd-MM-yyyy HH-mm-ss"));

    using (IsolatedStorageFileStream targetStream =
    isoStore.CreateFile(fileName))
    {
    WriteableBitmap wb = new WriteableBitmap(bmp);
    wb.SaveJpeg(targetStream, wb.PixelWidth,
    wb.PixelHeight, 0, 100);
    targetStream.Close();
    }

    if (null == item.UserImages)
    item.UserImages = new
    System.Collections.ObjectModel.ObservableCollection<string>();

    item.UserImages.Add(fileName);
    }
    }
    }

    上方代碼會將抓取到的影像儲存到 App 獨立儲存資料夾的使用者影像集合中。

  5. 最後,我們增加實際的代碼來抓取影像。回憶練習 2 中,我們為 ApplicationBar 工作圖片建立一個空的事件處理器按鈕。找到 btnTakePicture_Click 方法並添加以下代碼:

    C#
    try
    {
    camera.Show();
    }
    catch (System.InvalidOperationException ex)
    {
    MessageBox.Show("An error occurred.");
    }

  6. 現在我們來處理共享功能。找到 btnShareShareTask_Click 方式並添加以下代碼:

    C#
    ShareMediaTask shareMediaTask = new ShareMediaTask( );
    if (null != item.UserImages && item.UserImages.Count > 0)
    shareMediaTask.FilePath = string.Format("{0}", item.UserImages[0]);
    else
    shareMediaTask.FilePath = string.Format("{0}", item.GetImageUri( ));
    shareMediaTask.Show( );

    這允許用戶在社群網絡上共享圖像,並替換原來的 Windows Store App 使用的 Share Charm。

    註: 你不能在 Windows Phone 模擬器進行測試分享資料給社群網絡。要測試這個功能,必須部署應用程式到實際的設備。

任務 2 – 在 Windows Phone 8 App 中實作次要磚釘選

兩種平台都支援釘選次要動態磚到首頁,動態磚提供使用者關於 App 的附加資訊,也可以直接連結到那些物件上。雖然兩種平台都支援此功能,但 API 不同:在 Windows 8 釘選動態磚,我們要先建一個 SecondaryTile 類別的實體 (位於 Windows.UI.StartScreen 命名空間),以資料初始化並展示到動態磚,然後呼叫 RequestCreateAsync 方法如下:

C# (Windows Store App)
private async void OnPinRecipeButtonClicked(object sender, RoutedEventArgs e)
{
var item = (RecipeDataItem)this.flipView.SelectedItem;
var uri = new Uri(item.TileImagePath.AbsoluteUri);

var tile = new SecondaryTile(
item.UniqueId, // Tile ID
item.ShortTitle, // Tile short name
item.Title, // Tile display name
item.UniqueId, // Activation argument
TileOptions.ShowNameOnLogo, // Tile options
uri // Tile logo URI
);

await tile.RequestCreateAsync( );
}

在 Windows Phone 系統裡,建立動態磚的過程與 Windows 8 相似,但並不是建立一個動態磚物件,而是建立一個 StandardTileData 類別的實體 (位於 Windows.Phone.Shell 命名空間),然後呼叫 ShellTile.Create 方法 (位於 Microsoft.Phone.Shell 命名空間) 來建立動態磚,如下:

C#
public static void SetTile(RecipeDataItem item, string NavSource)
{
StandardTileData tileData = new StandardTileData
{
Title = "Contoso Cookbook",
BackTitle = item.Group.Title,
BackContent = item.Title,
BackBackgroundImage = new Uri(item.Group.GetImageUri(), UriKind.Relative),
BackgroundImage = new Uri(item.GetImageUri(), UriKind.Relative)
};
ShellTile.Create(new Uri(NavSource, UriKind.Relative), tileData);
}

我們將從 Windows Store App 移植次要動態磚程式碼到 Windows Phone。

  1. 從 [Lab Installation Folder]/Assets/Code/Ex3/Common 增加現有的 Features.cs 檔案到 Visual Studio 內的 Common 專案資料夾。

  2. 打開 Features.cs 檔案並孰悉動態磚類型,在上面所描述過的 SetTile 類型外,動態磚類型提供了兩個額外的方式:

    • TileExists – 次要動態磚存在時返回 true,否則返回 false;

    • DeleteTile – 從首頁移除先前釘選的動態磚

    註:Features.cs 文件還包括了下一段任務中要討論的“通知”類別。

  3. 接著,我們更改食譜內容頁(RecipeDetailPage.xaml.cs 文件),使用 Tile 類別。先增加以下 using 陳述式到檔案頂端的 using 區:

    C#
    using ContosoCookbook.Common;

  4. 然後,增加兩個類別成員如下:

    C#
    private const string removeFavUri = "/Assets/Icons/unlike.png";
    private const string FavUri = "/Assets/Icons/like.png";

    每當使用者釘選或者移除釘選項目時,這個代碼定義 ApplicationBar 按鈕 (練習一加入) 的圖示

  5. 找到 NavigateToRecipe 方式並增加以下呼叫到方法尾端:

    C#
    SetPinBar( );

  6. 接著,我們增加 SetPinBar 功能,增加下列的代碼到類別中:

    C#
    void SetPinBar()
    {
    var uri = NavigationService.Source.ToString();
    if (Features.Tile.TileExists(uri))
    {
    pinBtn.IconUri = new Uri(removeFavUri, UriKind.Relative);
    pinBtn.Text = "Unpin";
    }
    else
    {
    pinBtn.IconUri = new Uri(FavUri, UriKind.Relative);
    pinBtn.Text = "Pin";
    }
    }

    此方法使用 Tile 類別驗證和改變次要動態磚,並改變 ApplicationBar 按鈕上的文字和圖示。

  7. 然後, 增加 pinBtn 屬性到類別中:

    C#
    public ApplicationBarIconButton pinBtn
    {
    get
    {
    var appBar = (ApplicationBar)ApplicationBar;
    var count = appBar.Buttons.Count;
    for (var i = 0; i < count; i++)
    {
    ApplicationBarIconButton btn = appBar.Buttons[i]
    as ApplicationBarIconButton;
    if (btn.IconUri.OriginalString.Contains("like"))
    return btn;
    }
    return null;
    }
    }

    用屬性中的 get 存取子巡迴 applicationBar 的所有按鈕,並尋找我們想變更的特定按鈕。

  8. 最後,瀏覽到 btnPinToStart_Click 方式 (練習一加入的) 並新增下列代碼。代碼使用 Features.cs 檔案中的 Tile 類別釘選或移除釘選次要動態磚:

    C#
    var uri = NavigationService.Source.ToString( );
    if (Features.Tile.TileExists(uri))
    Features.Tile.DeleteTile(uri);
    else
    Features.Tile.SetTile(item, uri);

    SetPinBar( );

  9. 執行 App 瀏覽食譜頁。點擊釘選按鈕並回到首頁檢查次要動態磚:

    圖 25
    釘選次要動態磚

    圖 26
    首頁上釘選完成的食譜動態磚

  10. 瀏覽回 App 查看將已釘選的 App 變更成未釘選的按鈕

    圖 27
    將已釘選的 App 改設定為移除釘選的按鈕

  11. 點擊移除釘選按鈕移除已釘選的動態磚:

    圖 28
    刪除次要動態磚

  12. 下一階段,我們將為 App 新增最後一個遺失的功能 - 烹調時間提示

任務 3 – 在 Windows Phone 8 App 中實行提醒功能

我們將在 Windows Phone App 中實行特定烹調時間後彈跳出提醒訊息的功能。將使用先前任務已增加的 Features.cs 檔案裡的 Notifications 類別。Notifications 類別提供兩種功能:

  • SetReminder – 設定或移除提醒;

  • IsScheduled – 當安排了提醒功能則返回 "true",否則為 "false"。

  1. 從 RecipeDetailPage 類別修改起。新增下列類別成員到 RecipeDetailPage 類別中

    C#
    private const string removeAlarmUri = "/Assets/Icons/alarmRemove.png";
    private const string AlarmUri = "/Assets/Icons/alarm.png";

  2. 找到 NavigateToRecipe 方法並在方法尾端增加以下呼叫:

    C#
    SetScheduleBar(item.UniqueId);

  3. 增加 SetScheduleBar 方法到類別,如下:

    C#
    void SetScheduleBar(string name)
    {
    var isScheduled = Features.Notifications.IsScheduled(name);
    if (isScheduled)
    {
    alarmBtn.IconUri = new Uri(removeAlarmUri, UriKind.Relative);
    alarmBtn.Text = "Remove Alarm";
    }
    else
    {
    alarmBtn.IconUri = new Uri(AlarmUri, UriKind.Relative);
    alarmBtn.Text = "Set Alarm";
    }
    }

    這個方法與先前新增 SetPinBar 方法非常雷同:依據 IsScheduled 回傳的內容來改變 ApplicationBar 的圖示及文字敘述。

  4. 接下來,利用下方代碼增加一個新的屬性到類別中:

    C#
    public ApplicationBarIconButton alarmBtn
    {
    get
    {
    var appBar = (ApplicationBar)ApplicationBar;
    var count = appBar.Buttons.Count;
    for (var i = 0; i < count; i++)
    {
    ApplicationBarIconButton btn = appBar.Buttons[i]
    as ApplicationBarIconButton;
    if (btn.IconUri.OriginalString.Contains("alarm"))
    return btn;
    }
    return null;
    }
    }

    用屬性的中 get 存取子巡迴 applicationBar 並搜尋需要的特定按鈕

  5. 最後,更改 btnStartCooking_Click 方法,如同練習一加入的,在方法中增加以下代碼:

    C#
    Features.Notifications.SetReminder(item);
    SetScheduleBar(item.UniqueId);

  6. 執行 App 並瀏覽特定的食譜。點擊提醒按鈕:

    圖 29
    設定提醒功能

    然後應用程式的按鍵更改如下:

    圖 30
    移除提醒功能按鍵

    再次點擊按鍵將移除已設定的提醒功能,不要點擊它的話,過些時間,提醒訊息會跳出:

    圖 31
    跳出烹調時間提示訊息

  7. 現在已完成了將 Contoso Cookbook 從 Windows Store 移植到 Windows Phone 8 的步驟。

總結

在這個實驗中,我們探討從 Windows 8 移植一個 App 到 Windows Phone 8 的過程會遇到下列的四個問題:

  • XAML - Windows Store App 的 XAML 沒法立即被移植到 Windows Phone,必須使用替代控制項如 GridView 及 FlipView。

  • 顯示方向 - Windows 8 的顯示方向通常是橫向的,但 Windows Phone 8 則通常是直立顯示。我們必須更改 App 的顯示以適應手機預設的顯示方向。

  • 商業邏輯 - 移植時我們幾乎可以沿用大部分舊有的商業邏輯代碼,除了命名空間的變化和 JSON 的反序列化。

  • 平台的特定功能 - 某些 Windows 8 的功能無法用在 Windows Phone 8 或需以不同的方式實現:

    • Charm 是無法使用在 Windows Phone 上。我們使用最接近的替代品 ShareMediaTask 來取代 Share Charm。

    • Windows Phone 支援 Toast notifications 及次要動態磚釘選功能,但我們還是必須用 Windows Phone API 重新改寫代碼。

繼續下一個實驗