Windows Phone Silverlight 到 UWP 案例研究:Bookstore2
本案例研究以 Bookstore1 中提供的資訊為基礎,從 Windows Phone Silverlight 應用程式開始,其會在 LongListSelector 中顯示分組資料。 在檢視模型中,作者類別的每個執行個體代表該作者撰寫的書籍群組,在 LongListSelector 中,我們可以查看依照作者分組的書籍列表,也可以縮小以查看作者的捷徑清單。 跳躍清單的瀏覽方式比捲動書籍清單更快。 我們會逐步解說將應用程式移植到 Windows 10 通用 Windows 平台 (UWP) 應用程式的步驟。
注意 在 Visual Studio 中開啟 Bookstore2Universal_10 時,如果看到訊息「Visual Studio 需要更新」,請按照在 TargetPlatformVersion 中設定目標平台版本的步驟操作。
下載
下載 Bookstore2WPSL8 Windows Phone Silverlight 應用程式。
下載 Bookstore2Universal_10 Windows 10 應用程式。
Windows Phone Silverlight 應用程式
下圖顯示了 Bookstore2WPSL8 (我們要移植的應用程式) 大致的樣子。 它是垂直捲動的書籍 LongListSelector (依作者分組)。 您可以縮小至捷徑清單,然後從那裡瀏覽回任意群組。 該應用程式分為兩個主要部分:一個是提供分組資料來源的檢視模型,一個是繫結到該檢視模型的使用者介面。 如我們所見,這兩個部分都可以輕鬆地從 Windows Phone Silverlight 技術移植到通用 Windows 平台 (UWP)。
移植到 Windows 10 專案
您可以快速地在 Visual Studio 中建立一個新專案、將檔案從 Bookstore2WPSL8 複製到專案,並將複製的檔案加入新專案中。 首先建立一個新的空白應用程式 (Windows 通用) 專案。 將其命名為 Bookstore2Universal_10。 以下是要從 Bookstore2WPSL8 複製到 Bookstore2Universal_10 的檔案。
- 複製包含書籍封面影像 PNG 檔案的資料夾 (該資料夾為 \Assets\CoverImages)。 複製資料夾後,在方案總管中,請確認顯示所有檔案已開啟。 在您複製的資料夾上按右鍵,然後按一下加入至專案。 該命令就是所謂的在專案中「加入」檔案或資料夾。 每次複製檔案或資料夾時,請在方案總管中按一下重新整理,然後將該檔案或資料夾加入專案中。 對於您要在目的地中取代的檔案,無需執行此操作。
- 複製包含檢視模型來源檔案的資料夾 (該資料夾為 \ViewModel)。
- 複製 MainPage.xaml,然後取代目的地中的檔案。
我們可以將 Visual Studio 產生的 App.xaml 和 App.xaml.cs 保留在 Windows 10 專案中。
編輯剛剛複製的原始程式碼和標記檔案,將對 Bookstore2WPSL8 命名空間的所有參考變更為 Bookstore2Universal_10。 若想快速執行此動作,可使用取代檔案功能。 在檢視模型原始檔中的命令式程式碼中,需要進行以下移植變更。
- 將
System.ComponentModel.DesignerProperties
變更為DesignMode
,然後對其使用 Resolve 命令。 刪除IsInDesignTool
屬性,然後使用 IntelliSense 新增正確的屬性名稱:DesignModeEnabled
。 - 對
ImageSource
使用 Resolve 命令。 - 對
BitmapImage
使用 Resolve 命令。 - 刪除
using System.Windows.Media;
和using System.Windows.Media.Imaging;
。 - 將 Bookstore2Universal_10.BookstoreViewModel.AppName 屬性傳回的值從「BOOKSTORE1WPSL8」變更為「BOOKSTORE1UNIVERSAL」。
- 就像我們對 Bookstore1 所做的那樣,更新 BookSku.CoverImage 屬性的實作 (請參閱將影像繫結到檢視模型)。
在 MainPage.xaml 中,需要進行以下初始移植變更。
- 將
phone:PhoneApplicationPage
變更為Page
(包括屬性元素語法中出現的情況)。 - 刪除
phone
和shell
命名空間前置詞宣告。 - 將剩餘命名空間前置詞宣告中的「clr-namespace」變更為「using」。
- 刪除
SupportedOrientations="Portrait"
和Orientation="Portrait"
,然後在新專案的應用程式套件資訊清單中設定縱向。 - 刪除
shell:SystemTray.IsVisible="True"
。 - 捷徑清單項目轉換器的類型 (其以資源的形式存在於標記中) 已移至 Windows.UI.Xaml.Controls.Primitives 命名空間。 新增命名空間前置詞宣告 Windows_UI_Xaml_Controls_Primitives,並將其對應到 Windows.UI.Xaml.Controls.Primitives。 在捷徑清單項目轉換器資源上,將前置詞從
phone:
變更為Windows_UI_Xaml_Controls_Primitives:
。 - 就像我們對 Bookstore1 所做的那樣,將所有對
PhoneTextExtraLargeStyle
TextBlock 樣式的參考取代為對SubtitleTextBlockStyle
的參考,將PhoneTextSubtleStyle
取代為SubtitleTextBlockStyle
,將PhoneTextNormalStyle
取代為CaptionTextBlockStyle
,並將PhoneTextTitle1Style
取代為HeaderTextBlockStyle
。 BookTemplate
中有一個例外。 第二個 TextBlock 的樣式應參考CaptionTextBlockStyle
。- 從
AuthorGroupHeaderTemplate
內的 TextBlock 中移除 FontFamily 屬性,然後將框線的背景設為參考SystemControlBackgroundAccentBrush
而不是PhoneAccentBrush
。 - 由於變更與檢視像素相關,請檢查標記,並將所有固定大小維度 (邊界、寬度、高度等) 乘以 0.8。
取代 LongListSelector
用 SemanticZoom 控制項取代 LongListSelector 需要幾個步驟,我們這就開始吧。 LongListSelector 直接繫結到分組資料來源,但 SemanticZoom 包含 ListView 或 GridView 控制項,它們透過 CollectionViewSource 配接器間接繫結到資料。 CollectionViewSource 需要以資源的形式存在於標記中,因此我們先將其新增至 <Page.Resources>
內 MainPage.xaml 中的標記。
<CollectionViewSource
x:Name="AuthorHasACollectionOfBookSku"
Source="{Binding Authors}"
IsSourceGrouped="true"/>
請注意,LongListSelector.ItemsSource 上的繫結將變為 CollectionViewSource.Source 的值,並且 LongListSelector.IsGroupingEnabled 將變為 CollectionViewSource.IsSourceGrouped。 CollectionViewSource 有一個名稱 (注意:這不是索引鍵,您可能已經猜到了),因此我們可以繫結至該名稱。
接下來,用此標記取代 phone:LongListSelector
,這將提供我們要使用的初步 SemanticZoom。
<SemanticZoom>
<SemanticZoom.ZoomedInView>
<ListView
ItemsSource="{Binding Source={StaticResource AuthorHasACollectionOfBookSku}}"
ItemTemplate="{StaticResource BookTemplate}">
<ListView.GroupStyle>
<GroupStyle
HeaderTemplate="{StaticResource AuthorGroupHeaderTemplate}"
HidesIfEmpty="True"/>
</ListView.GroupStyle>
</ListView>
</SemanticZoom.ZoomedInView>
<SemanticZoom.ZoomedOutView>
<ListView
ItemsSource="{Binding CollectionGroups, Source={StaticResource AuthorHasACollectionOfBookSku}}"
ItemTemplate="{StaticResource ZoomedOutAuthorTemplate}"/>
</SemanticZoom.ZoomedOutView>
</SemanticZoom>
簡單列表和捷徑清單模式的 LongListSelector 概念分別在放大和縮小檢視的 SemanticZoom 概念中得到了答案。 放大檢視是一個屬性,您可以將該屬性設定為 ListView 的執行個體。 在本例中,縮小檢視也設定為 ListView,並且兩個 ListView 控制項都會繫結到 CollectionViewSource。 放大檢視使用與 LongListSelector 的簡單列表相同的項目範本、群組標題範本和 HideEmptyGroups 設定 (現在名為 HidesIfEmpty)。 縮小檢視所使用的項目範本與 LongListSelector 捷徑清單樣式 (AuthorNameJumpListStyle
) 中的範本十分相似。 另請注意,縮小檢視會繫結到名為 CollectionGroups 的 CollectionViewSource 的特殊屬性,該屬性是包含群組而非項目的集合。
我們不再需要 AuthorNameJumpListStyle
,至少不需要全部。 我們只需要縮小檢視中群組 (此應用程式中的作者) 的資料範本。 因此,我們刪除 AuthorNameJumpListStyle
樣式,並用此資料範本取代它。
<DataTemplate x:Key="ZoomedOutAuthorTemplate">
<Border Margin="9.6,0.8" Background="{Binding Converter={StaticResource JumpListItemBackgroundConverter}}">
<TextBlock Margin="9.6,0,9.6,4.8" Text="{Binding Group.Name}" Style="{StaticResource SubtitleTextBlockStyle}"
Foreground="{Binding Converter={StaticResource JumpListItemForegroundConverter}}" VerticalAlignment="Bottom"/>
</Border>
</DataTemplate>
請注意,由於此資料範本的資料內容是一個群組而不是一個項目,因此我們會繫結到一個名為 Group 的特殊屬性。
現在,您可以建置並執行應用程式了。 以下該應用程式在行動仿真器上的樣子。
檢視模型以及放大和縮小檢視可以正確地協同運作,問題是我們需要執行更多的樣式和範本工作。 例如,尚未使用正確的樣式和筆刷,因此文字在群組標題上不可見,您可以按一下以縮小。如果您在桌面裝置上執行該應用程式,那麼您會發現第二個問題,也就是該應用程式尚未調整其使用者介面,以在較大裝置 (其視窗可能比行動裝置螢幕大) 上提供最佳體驗和使用空間。 因此,在接下來的幾節 (初始樣式和範本、調適型 UI 和最終樣式) 中,我們將解決這些問題。
初始樣式和範本
若要妥善地間隔群組標題,請編輯 AuthorGroupHeaderTemplate
,並在框線上設定 "0,0,0,9.6"
的邊界。
若要妥善間隔書籍項目,請編輯 BookTemplate
,並將兩個 TextBlock 上的邊界設為 "9.6,0"
。
若要更好地佈局應用程式名稱和頁面標題,請在 TitlePanel
中,透過將值設為 "7.2,0,0,0"
來刪除第二個 TextBlock 的上邊界。 在 TitlePanel
上,將邊界設為 0
(或任何您認為合適的值)
將 LayoutRoot
的背景變更為 "{ThemeResource ApplicationPageBackgroundThemeBrush}"
。
調適型 UI
因為我們一開始用的是手機應用程式,所以移植的應用程式 UI 佈局在此階段當然也只適用於小型裝置和窄型視窗。 但我們也希望當應用程式在寬視窗中執行階段,UI 佈局能自我調整並更妥善地使用空間 (這僅在大螢幕裝置上執行),並且只在應用程式視窗較窄時使用我們目前擁有的 UI (這通常發生在小型裝置上,但在大型裝置上也可能發生)。
我們可以使用調適型「視覺狀態管理器」功能來實現這一點。 我們將設定視覺元素的屬性,以在預設情況下,使用我們現在範本將 UI 佈置在窄狀態下。 然後,我們將偵測應用程式的視窗寬度是否大於或等於特定大小 (以有效像素為測量單位),然後我們將根據偵測結果變更視覺元素的屬性,以便取得更大、更寬的版面配置。 我們將這些屬性變更置於視覺狀態中,並將使用調適型觸發程序來連續監控並確定是否套用該視覺狀態,具體取決於視窗的有效像素寬度。 在本例中,我們會對視窗寬度上觸發,但也可以對視窗高度觸發。
此使用案例適用最小視窗寬度 548 epx,因為這是我們想要顯示寬版面配置的最小裝置大小。 手機通常小於 548 epx,因此在這樣的小型裝置上,我們會保留預設的窄型版面配置。 在電腦上,視窗將以預設寬度啟動,其寬度足以觸發切換到寬型狀態,這將顯示 250x250 大小的項目。 在這裡,您可以把視窗拖曳成窄型狀態,以顯示至少兩欄 250x250 的項目。 任何比該寬度更窄的值都將導致觸發程序停用,移除寬視覺狀態,並且預設的窄型版面配置將會生效。
在處理調適型視覺狀態管理器部分之前,我們需要先設計寬型狀態,這代表要在標記上新增一些新的視覺元素和範本。 以下步驟說明如何執行此操作。 透過視覺元素和範本的命名慣例,我們將在用於寬型狀態的任何元素或範本的名稱中包含「wide」一詞。 如果元素或範本不包含「wide」一詞,則可以假設其為窄型狀態,這是預設狀態,其屬性值設定為頁面中可視元素的本機值。 只有寬型狀態的屬性值是透過標記中的實際視覺狀態所設定。
- 在標記中建立 SemanticZoom 控制項的副本,並在副本上設定
x:Name="narrowSeZo"
。 在原始版本上,設定x:Name="wideSeZo"
和Visibility="Collapsed"
,讓預設不顯示寬版。 - 在
wideSeZo
中,將放大檢視和縮小檢視中的 ListView 變更為 GridView。 - 製作這三個資源
AuthorGroupHeaderTemplate
、ZoomedOutAuthorTemplate
和BookTemplate
的副本,並將單字Wide
附加到副本的索引鍵。 此外,更新wideSeZo
,使其參考這些新資源的索引鍵。 - 將
AuthorGroupHeaderTemplateWide
的內容取代為<TextBlock Style="{StaticResource SubheaderTextBlockStyle}" Text="{Binding Name}"/>
。 - 將
ZoomedOutAuthorTemplateWide
的內容取代為:
<Grid HorizontalAlignment="Left" Width="250" Height="250" >
<Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}"/>
<StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
<TextBlock Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}"
Style="{StaticResource SubtitleTextBlockStyle}"
Height="80" Margin="15,0" Text="{Binding Group.Name}"/>
</StackPanel>
</Grid>
- 將
BookTemplateWide
的內容取代為:
<Grid HorizontalAlignment="Left" Width="250" Height="250">
<Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}"/>
<Image Source="{Binding CoverImage}" Stretch="UniformToFill"/>
<StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}"
Foreground="{StaticResource ListViewItemOverlaySecondaryForegroundThemeBrush}"
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis"
Margin="12,0,24,0" Text="{Binding Title}"/>
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{Binding Author.Name}"
Foreground="{StaticResource ListViewItemOverlaySecondaryForegroundThemeBrush}" TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis" Margin="12,0,12,12"/>
</StackPanel>
</Grid>
- 對於寬型狀態,放大檢視中的群組將需要周圍有更多的垂直留白空間。 建立並參考項目面板範本將提供我們想要的結果。 標記的外觀如下。
<ItemsPanelTemplate x:Key="ZoomedInItemsPanelTemplate">
<ItemsWrapGrid Orientation="Horizontal" GroupPadding="0,0,0,20"/>
</ItemsPanelTemplate>
...
<SemanticZoom x:Name="wideSeZo" ... >
<SemanticZoom.ZoomedInView>
<GridView
...
ItemsPanel="{StaticResource ZoomedInItemsPanelTemplate}">
...
- 最後,加入適當的視覺狀態管理器標記作為
LayoutRoot
的第一個子標記。
<Grid x:Name="LayoutRoot" ... >
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="WideState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="548"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="wideSeZo.Visibility" Value="Visible"/>
<Setter Target="narrowSeZo.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
...
最終樣式
剩下的就是一些最終樣式調整。
- 在
AuthorGroupHeaderTemplate
中,在 TextBlock 上設定Foreground="White"
,以便其以正確的外觀在行動裝置系列上執行。 - 將
FontWeight="SemiBold"
新增至AuthorGroupHeaderTemplate
和ZoomedOutAuthorTemplate
中的 TextBlock。 - 在
narrowSeZo
中,縮小檢視中的群組標題和作者是靠左對齊而不是延展,讓我們來處理這個問題。 我們將為放大檢視建立 HeaderContainerStyle,並將 HorizontalContentAlignment 設為Stretch
。 我們要為包含相同設定器的縮小檢視建立 ItemContainerStyle。 其看起來像這樣。
<Style x:Key="AuthorGroupHeaderContainerStyle" TargetType="ListViewHeaderItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
<Style x:Key="ZoomedOutAuthorItemContainerStyle" TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
...
<SemanticZoom x:Name="narrowSeZo" ... >
<SemanticZoom.ZoomedInView>
<ListView
...
<ListView.GroupStyle>
<GroupStyle
...
HeaderContainerStyle="{StaticResource AuthorGroupHeaderContainerStyle}"
...
<SemanticZoom.ZoomedOutView>
<ListView
...
ItemContainerStyle="{StaticResource ZoomedOutAuthorItemContainerStyle}"
...
最後一連串的樣式設定會讓應用程式看起來像這樣。
在桌面裝置上執行的移植 Windows 10 應用程式,放大檢視,兩種大小的視窗
在桌面裝置上執行的移植 Windows 10 應用程式、縮小檢視、兩種大小的視窗
在行動裝置上執行的移植後 Windows 10 應用程式,放大檢視
在行動裝置上執行的移植後 Windows 10 應用程式,縮小檢視
使檢視模型更加靈活
本節包含一個功能範例,透過將應用程式移至使用 UWP 來向我們開放這些功能。 以下將說明您可以遵循的選擇性步驟,以使您的檢視模型在透過 CollectionViewSource 存取時更加靈活。 我們從 Windows Phone Silverlight 應用程式 Bookstore2WPSL8 移植的檢視模型 (來源檔案位於 ViewModel\BookstoreViewModel.cs 中) 包含一個名為 Author 的類別,該類別衍生自 List<T>,其中 T 是 BookSku。 這代表 Author 類別是 BookSku 的群組。
當我們將 CollectionViewSource.Source 繫結到 Authors 時,唯一要傳達的是 Author 中的每個 Author 都是一組東西。 我們讓 CollectionViewSource 來確定 Author 在本例中是一組 BookSku。 這是可行的,但並不靈活。 如果我們希望 Author 既是一組 BookSku 又是一組作者居住過的地址呢? Author 不能同時屬於這兩個群組。 但是,Author 可以擁有任意數量的群組。 所以解決方案就會是:使用 has-a-group 模式來取代我們目前使用的 is-a-group 模式,或兩者皆使用。 方法如下:
- 變更 Author,使其不再衍生自 List<T>。
- 將此欄位新增至
- 將此屬性新增至
- 當然,我們可以重複上述兩個步驟,根據需要向 Author 新增任意數量的群組。
- 將 AddBookSku 方法的實作變更為
this.BookSkus.Add(bookSku);
。 - 現在 Author 至少有一個群組,我們需要與 CollectionViewSource 進行通訊,了解它應該使用其中的哪一個群組。 若要這樣做,請將此屬性新增至 CollectionViewSource:
ItemsPath="BookSkus"
這些變更使該應用程式的功能保持不變,但現在您知道如何根據需要擴充 Author 和 CollectionViewSource 了。 讓我們對 Author 進行最後一個改動,如果我們在不指定 CollectionViewSource.ItemsPath 的情況下使用它,就會使用我們選擇的預設群組:
public class Author : IEnumerable<BookSku>
{
...
public IEnumerator<BookSku> GetEnumerator()
{
return this.BookSkus.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.BookSkus.GetEnumerator();
}
}
現在,如果我們想的話,可以選擇移除 ItemsPath="BookSkus"
,而應用程式仍會以相同的方式運作。
結論
此案例研究的使用者介面比前一個案例更複雜。 我們發現 Windows Phone Silverlight LongListSelector 的所有功能和概念 (以及更多) 都可以 SemanticZoom、ListView、GridView 和 CollectionViewSource 的形式供 UWP 應用程式使用。 我們示範了如何在 UWP 應用程式中重複使用,或複製和編輯命令式程式碼和標記,以量身打造功能、UI 和互動,使其適用於從最窄到最寬的 Windows 裝置外形規格。