.NET Magazine國際中文電子雜誌
作 者:羅慧真
審 稿:張智凱
文章編號:N140214501
出刊日期:2014/2/12
開發工具: Visual Studio 2012
版本:Windows Run-Time 8.0
版面的設計對於Windows 8 Style UI是很重要的!
在開始之前,讓我們先來看看幾個Windows Store App~
參考幾個現成的Apps
Windows 8 Style UI的特色之一,除了沒有視窗外框之外,進入應用程式直接就看到內容,不需從功能鍵進入。這是Skype,你可以看到最後通話的訊息,常用聯絡人及聯絡人等資訊,畫面的上方看下來是應用程式的標題名稱、下一格則是副標名稱(功能或是資料群組名稱)。標題名稱的左邊邊界留有寬約120px,高約140px,這個位置預留給回上一頁的按鈕。同時為了視覺的平衡,左邊界也是內容的起點。

圖1 - Skype
這是SkyDrive,在內容的部分SkyDrive是網路硬碟,資料夾名稱就是它的內容,點按進去會看到檔名。在版面的部份似乎也有著類似的風格,大標題名稱,雖說它沒有副標,不過似乎可以看出它也預留了副標的顯示位置。

圖2 - SkyDrive
這是Contoso Cookbook,Microsoft 的Hands-on labs for Windows 8[1],也是一份食譜應用程式,它的內容便是看起來香噴噴的成品照,看到這裡,有沒有發現Windows 8 Style應用程式非常直覺,第一眼看到的就是應用程式的內容,內容項目即是磚、磚即是Button,按下任何內容項目便可導入該項目的頁面。

圖3 - Contoso Cookbook
頁面的導覽
Contoso Cookbook屬於階層式系統,首頁是以國別為群組顯示所有的食譜項目。

圖4-Contoso Cookbook內容的群組與項目
點按食譜照片時進入第二個頁面,呈現的是該份食譜的使用材料及作法(就是項目的詳細資訊),可以按左上的「前頁」按鈕回到第一頁。如果按下群組名稱,例如French便會看到法式餐點的詳細資訊及所屬的食譜項目清單。

圖5-Contoso Cookbook的頁面導覽
這個範例是使用Visual Studio 2012的範本 -- 「格線應用程式」所建立而成。首頁就是GroupedItemsPage.xaml、項目內容就是ItemDetalPage.xaml,群組細節就是GroupDetailPage.xaml。

圖6 - ContosoCookbook的專案檔
圖6是Contoso Cookbook應用程式的專案,除了上述的三個XAML檔之外,還有一個App.xaml,它的內容是應用程式的資源檔內容:
<Application
x:Class="ContosoCookbook.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ContosoCookbook"
xmlns:localData="using:ContosoCookbook.Data">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Common/StandardStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<x:String x:Key="AppName">Contoso Cookbook</x:String>
</ResourceDictionary>
</Application.Resources>
</Application>
Visual Studio 2012的Windows市集應用程式範本都會自動加入StandardStyle.xaml檔到Common目錄,這個檔案包含一般應用程式所需的文字樣式、AppBar按鈕的樣式、以及顯示內容項目的DataTemplate。
App類別同時也繼承Winodws.UI.Xaml.Application,主控整個應用程式的生命週期及Windows 8搜尋、分享…等特性的事件處理。其中OnLaunched是應用程式啟動的事件程序,我們會在這裡利用Frame的Navigate方法指定要瀏覽的頁面以及要帶入的資料,這個範例的起始頁面是GroupedItemsPage。

圖7 - App.xaml.cs
資料的關聯性
DataModel資料夾的RecipeDataSource.cs包含這個應用程式資料的結構及從資料來源取得資料的程式邏輯,它有四個類別分別是RecipeDataCommon、RecipeDataItem、RecipeDataGroup、RecipeDataSource。

圖8 - RecipeDataSource.cs
RecipeDataGroup就是Chinese、French...等群組資料,它包含一個Items屬性就是該群組的詳細項目。RecipeDataItem就是個別的食譜內容,包含所屬群組、照片、材料與作法。RecipeDataCommon是RecipeDataGroup及RecipeDataItem的基本類別。RecipeDataSource則是負責從資料來源取得資料並且讓資料被具體化成物件(RecipeDataGroup、RecipeDataItem)。
資料載入與資料繫結
當應用程式啟動之後便會觸發OnLaunched事件,這個事件會瀏覽到GroupedItemsPage.xaml並執行LoadState方法,在這個方法中呼叫RecipeDataSource的GetGroups方法並取得前面帶進來的參數navigationParameter(圖8中的程式碼帶進來的是”AllGroup”)以便從資料來源取得資料,取得資料之後將取得的結果放入DefaultViewModel集合並指定Key為”Group”。
圖9的上面是GroupedItemsPage.xaml.cs的程式碼,下面是GroupedItemPage.xaml的內容,XAML支援Data Binding的功能,這裡將Page的DataContent與程式碼中的DefaultViewModel集合屬性進行資料繫結。
XAML使用CollectionViewSource來處理Collection資料繫結的群組/排序…等檢視功能,因此會先將集合的內容先與CollectionViewSource進行資料繫結,圖9下方的XAML便是與DefaultViewMode[“Groups”]進行資料繫結的動作。

圖8 - GroupedItemsPage.xaml
自己設計一個App
前面簡單介紹了Windows Store App的樣式及設計範本,現在你可以自己想一個App開始動手設計囉!
這一年裡除了研究Windows 8平版app(Windows Store app)的技術之外,閒暇的時間就是與鄰居一起玩烘焙,才發現原來烘焙是一件有趣的事,不過也與程式開發一樣有許多細節要注意,買了很多烘焙的書來研究,書上描述得很清楚,但是如果完全照著操作卻不一定百分百的成功,可能隨著天氣、溫、溼度、烤箱、操作手法力道…等變因都會影響成品的口感及品質。
有時我們也會隨著季節改變而選用當令食材,例如:火龍果吐司,這時麵團水分的比例就成了一項考驗,所以我需要一個行動App可以幫我記錄烘焙的過程及結果。
以下是我的烘焙記事簿的設計手稿:

圖 9 - 我的烘焙記事簿
App第一個出現的畫面會是食譜的照片並按照分類排列,當我點按某張照片時會開啟第二個畫面,顯示那份食譜的照片、說明、材料及作法。在第二個畫面有下方的AppBar,可以使用那些按鈕新增、修改、刪除食譜的內容。在點按第一個畫面的分類名稱,就會進入第三個畫面,第三個畫面是一個分割頁,左邊的清單會呈現該分類的所有食譜,使用者可以從清單選擇某一個食譜項目,右邊則是呈現被選取的食譜詳細資訊。
Windows Store App支援以HTTP協定存取的技術,所以舉凡WCF Service、WCF Data Service、傳統的HTTP GET、POST …等方式都可以連接,並且支援SOAP、XML、JSON…及一般的純文字格式,所以也可以存取Facebook、Google Map…等各大網站提供的Web API。
目前這個App只是雛形,我打算使用WCF Data Service做為資料來源,如果這個App有正式上架WCF Data Service及資料庫可以放到雲端。
下圖是EDM模型,我將食譜分類為蛋糕、麵包、餅乾…這些資訊放在BakingType資料表,食譜的內容則放在BakingNote的資料表。

圖 10 - EDM模型
以下是我建構這個App的主要步驟:
- 設計資料庫結構
- 建立WCF Data Service專案
- 建立Local資料庫(範例程式使用Local資料庫才方便分享給讀者)
- 資料庫的EDM模型
- WCF Data Service
- 建立Windows Store App專案
- 加入服務參考
- 撰寫DataSource,存取WCF Data Service
- 設計頁面、樣式、範本、資料繫結
那麼就動手吧!
建立WCF Data Service專案
首先開啟Visual Studio 2012,然後新增專案,選擇Visual C# à Web àASP.NET空白Web應用程式,指定名稱為BakingNoteService。
建立Local資料庫
專案建立完成之後,找到App_Data資料夾,在此加入一個SQL Server資料庫,取名為BakingNote。

圖11 - 加入SQL Server資料庫
從方案總管雙擊剛才建立的BakingNote,它會被開啟在「伺服器總管」,在「伺服器總管」找到「資料表」項目按滑鼠右鍵選取「加入新資料表」

圖12 - 加入新的資料表到BakingNote資料庫
BakingType資料表有兩個欄位分別是TypeID、TypeName,TypeID為主索引鍵並且不允許Null。完成請按左上的「更新」按鈕。

圖13 - BakingType資料表的結構
相同的方式新入第二個資料表BakingNotes,欄位結如圖15,ID為主索引鍵,TypeID為外部參考索引鍵。

圖14 - BakingNotes資料表
下面的程式為它的SQL指令。
CREATE TABLE [dbo].[BakingNotes] (
[ID] INT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (50) NULL,
[TypeID] INT NOT NULL,
[Image] IMAGE NULL,
[Description] NVARCHAR (MAX) NULL,
[Owner] NVARCHAR (10) NULL,
[CreateDate] DATETIME NULL,
[Ingredients] NVARCHAR (MAX) NULL,
[Directions] NVARCHAR (MAX) NULL,
PRIMARY KEY CLUSTERED ([ID] ASC),
CONSTRAINT [FK_BakingNotes_ToType] FOREIGN KEY ([TypeID]) REFERENCES [dbo].[BakingType] ([TypeID])
);
Local DB建置完成。
資料庫的EDM模型
專案加入「ADO.NET實體資料模型」取名為BakingNoteModel。

圖15 - 加入ADO.NET實體資料模型
在實體資料模型精靈選取「從資料庫產生」,然後點選「下一步」。

圖16 - 從資料庫產生
然後在連接到資料庫的下拉清單選取「BakingNoteService」。按「下一步」。

圖17 - 連接到BakingNote資料庫
接著勾選「資料表」(這會勾選所有的資料表),以及「將產生的物件名稱複數化或單數化」,之後按「完成」。

圖18 - 勾選要的資料表
完成之後Visual Studio會產生預設的Model程式,這些程式不適合用於WCF Data Service的更新功能,所以要從方案總管將它們全數刪除。

圖19 - 刪除預設產生的程式
在設計介面上按滑鼠右鍵選取「加入程式碼產生項目」,接著在「加入項目」的對話視窗左邊選取「線上」(必須連到Internet),右方窗格找到EF 5x STE Generator for C#,按「新增」即可。(此範本自動會根據Model建立可被追蹤更新狀態的類別程式,它的名稱是Self-Taking Entry簡稱為STE)

圖20 - 加入Self-Tracking Entity classes
然後建置專案。
WCF Data Service
在專案加入新的項目,選擇「Visual C#」>「Web」>「WCF Data Service」,並且取名為BakingNoteDataService。
修改繼承類的的泛型名稱為BakingNoteEntities(建構EDM時所產生的類別,負責存取資料庫及實體化Entity物件的任務)。像是這樣:
public class BakingNoteDataService : DataService<BakingNoteEntities >
{
接著在InitializeService的方法中修改SetEnititySetAccessRule的Entity名稱為「*」代表所有的Entity。
config.SetEntitySetAccessRule("*", EntitySetRights.All);
建置專案。現在完成WCF Data Service的建置。
建立Windows Store App專案
在目前方案加入一個新專案,在新增專案的左邊窗格選取Visual C# àWindows 市集,選取右方窗格的「空白的應用程式」,取名為BakingNoteApp。
加入服務參考
這個App必須連到服務才能存取資料內容,在Visual Studio中這是相當容易的作業,只需要將專案加入服務參考,Visual Studio便會幫我們產生連接到WCF Data Service所需的代理程式,以及存取EDM所需的程式碼。
接著在專案中的參考項目按滑鼠右鍵選取「加入服務參考」[2]。

圖21 - 加入服務參考
輸入命名空間為BakingNoteServic,按「確定」。

圖22 -探索服務
撰寫DataSource,存取WCF Data Service
加入一個資料夾取名為Model,接著加入一個類別取名為BakingNoteDataSource,參考命名空間如下:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Services.Client;
using System.Linq;
using System.Threading.Tasks;
在類別之中宣告一個靜態變數名稱為serviceUri,記錄服務所在之位置:
static string serviceUri = "http://localhost:40170/BakingNoteDataService.svc/";
宣告另一個靜態變數儲存bakinType的集合:
private static ObservableCollection<BakingNoteService.BakingType> bakingTypes =
new ObservableCollection<BakingNoteService.BakingType>();
撰寫一個方法執行查詢名稱是ExcecuteQuery,參數型別是DataServiceQuery,以接收LINQ查詢。此方法必須使用非同步的BeginExecute及EndExecute方法取得查詢結果:
private static async Task<IEnumerable> ExecuteQuery(DataServiceQuery query)
{
var ar = query.BeginExecute(null, null);
await Task.Run(() => { while (!ar.IsCompleted) { } });
return query.EndExecute(ar);
}
GetBakingTypes方法,透過加入服務參考產生的代理程式連接到WCF Data Service,並且利用LINQ語法進行查詢,LINQ語法中有用到Expand方法,是用來指定查詢要帶回的關聯資訊,”BakingNotes/BakingType” BakingNotes是BakingType類別的屬性BakingType則是BaingNotes之中的屬性。查詢取得結果之後利用foreach將內容填入bakingTypes集合。
public static async void GetBakingTypes( )
{
BakingNoteService.BakingNoteEntities dbservice =
new BakingNoteService.BakingNoteEntities(new Uri(serviceUri));
var query = (from type in dbservice.BakingTypes.Expand(@"BakingNotes/BakingType")
select type) as DataServiceQuery;
var result = await ExecuteQuery(query)
as IEnumerable<BakingNoteService.BakingType>;
bakingTypes.Clear();
foreach (var item in result.ToArray ())
{
bakingTypes.Add(item);
}
}
UpdateBakingNote方法是用來更新(新增、刪除、修改)BakingNote實體。有兩個參數commandName及note,commandName是字串用來指定命令名稱(包含:add、edit、delete),note則是要被更新的BakingNote實體。STE的EDM模型是用在分散式的架構,新增實體時使用AddObject方法,修改及刪除則使用AttachTo方法,刪除還要加一個DeleteObject方法。最後要呼叫SaveChanges方法將更新寫回資料來源,此處一樣要使用Begin及End非同步方法更新到服務來源。
public async static void UpdateBakingNote(string commandName, BakingNoteService.BakingNote note )
{
BakingNoteService.BakingNoteEntities service =
new BakingNoteService.BakingNoteEntities(new Uri(serviceUri));
commandName = commandName.ToLower();
try
{
if (commandName == "add")
{
service.AddObject("BakingNotes", note);
BakingType.BakingNotes.Add(note);
}
else if (commandName == "edit")
{
service.AttachTo("BakingNotes", note);
service.UpdateObject(note);
}
else if (commandName == "delete")
{
service.AttachTo("BakingNotes", note);
service.DeleteObject(note);
BakingType.BakingNotes.Remove(note);
}
var s_ar = service.BeginSaveChanges(null, null);
await Task.Run(() => {
while (!s_ar.IsCompleted) { }
});
}
catch (Exception)
{
throw;
}
}
}
接著加入兩個屬性BakingTypes,BakingTypes會包含所有分類,初次使用時必須連接到服務抽取資料,需判斷bakingTypes的集合是否有內容,如果沒有必須呼叫GetBakingTypes方法填入內容。
public static ObservableCollection<BakingNoteService.BakingType> BakingTypes
{
get {
if (bakingTypes.Count <=0)
GetBakingTypes();
return BakingNoteDataSource.bakingTypes; }
}
BaingType屬性,負責記憶目前處理的分類項目。
private static BakingNoteService.BakingType bakingType;
public static BakingNoteService.BakingType BakingType
{
get { return BakingNoteDataSource.bakingType; }
set { BakingNoteDataSource.bakingType = value; }
}
建置專案確認程式沒有問題。
設計頁面、資料繫結、資料範本
接下來是畫面的部分,如果有美編人員可以幫你設計版面或者你有不錯的想法,那麼不妨使用空白頁面自行設計版面。若沒有那也無妨,咱們就直接使用Microsoft幫我們設計好的範本。
如0.2節的App草稿那些畫面其實都是仿照Visual Studio格線應用程式的範本所設計的。
第一個畫面要使用「群組項目頁面」範本,從方案總管加入新項目,選擇Visual C# à Windows 市集,然後選取範本為「群組項目頁面」,並取名為BakingNoteItemsPage.xaml。

圖23 -加入群組項目頁面
接著會出現如下的訊息方塊,加入這類範本[3]需要用到一些額外的程式檔,所以此畫面是提示你必須將這些內容加入才能使用,按「是」會自動將所需檔案加入到專案中。

圖24 -按「是」加入必要的檔案
刪除MainPage.cs。
開啟App.xaml.cs找到OnLaunched方法,將預設巡覽到MainPage頁面的程式碼改成BakingNoteItemsPage。
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
// 部分程式省略
// 將框架放在目前視窗中
Window.Current.Content = rootFrame;
if (!rootFrame.Navigate(typeof(BakingNoteItemsPage), args.Arguments))
{
throw new Exception("Failed to create initial page");
}
}
// 確定目前視窗是作用中
Window.Current.Activate();
}
開啟BakingNoteItemsPage.xaml,找到<x:String x:Key=”AppName”>的區段,修改標題應用程式的名稱。
<x:String x:Key="AppName">我的烘焙記事簿</x:String>
按「F5」執行應用程式,執行結果如下:

圖25 - 第一個畫面及標題名稱
開啟BakingNoteItemPage.xaml,找到LoadState方法,這個方法是頁面載入時並且狀態複原後會執行的方法,在此指定DefaultViewModel集合的“Groups”前面所寫的BakingTypes,你應該還記得我們在這個屬性裡有處理BakingTypes的初始化動作(連結到Service取得資料內容)。
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
// TODO: 將可繫結群組的集合指派給 this.DefaultViewModel["Groups"]
this.DefaultViewModel["Groups"] = BakingNoteDataSource.BakingTypes;
}
開啟BakingNoteItemPage.xaml,找到CollectionViewSource,Template已將它自動繫結來源為DefaultViewModel 的Groups,一個Group相當於一個BakingType,BakingType會是資料群組,BakingType中的BakingNotes則會是項目內容,因此這裡要將ItemsPath指為BakingNotes。
<CollectionViewSource x:Name="groupedItemsViewSource" Source="{Binding Groups}" IsSourceGrouped="true" ItemsPath="BakingNotes"/>
修改GridView顯示的群組標題,預設資料繫結的欄位是Title,但BakingType的分類名稱是TypeName,這裡我們修改這個範本。
使用「文件大綱」,展開pageRootà[Grid]àitemGridView,按滑鼠右鍵選取「編輯GroupStyle」à「編輯產生的標題(HeaderTemplate)」à「編輯目前的項目」。

圖26 -編輯群組標題名稱
現在進入DataTemplate的編輯狀態,找到StackPanel中的第一個TextBlock,在XAML編輯器中找到Binding Title的位置,將Title改成TypeName。

圖27 -編輯GridView的群組標題
按文件大綱下的返回按鈕可以回到頁面的設計狀態。按F5執行及佈署應用程式,會看到群組標題如下;

圖28 -群組標題
修改GridView顯示的項目範本,這個範例的項目與範本項目所要顯示的欄位會不一樣,因此這裡也需要修改要顯示的欄位。
使用「文件大綱」,展開pageRootà[Grid]找到itemGridView,按滑鼠右鍵選取「編輯其他範本」à「編輯產生的項目(ItemTemplate)」à「編輯複本」。

圖29 -複製預設的ItemTemplate
這個動作會複製一份預設的範本,下面的對話盒是問我們要將ItemTemplate放置於那個檔案(「此文件」指的就是目前這個檔案),並為這個範本取一個名稱,這裡取名為DataTemplate1。

圖30 - DataTemplate1
按下「確定」之後設計畫面就會進入範本的編輯模式,直接編修XAML,找到Image元素,修改AutomationProperties.Name屬性的Binding為Name,TextBlock元素將原本Binding為Title的欄位改成Name,Binding為Subtitle的改為Owner,XAML內容如下:
<DataTemplate x:Key="DataTemplate1">
<Grid HorizontalAlignment="Left" Width="250" Height="250">
<Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}">
<Image Source="{Binding Image}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Name}"/>
</Border>
<StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
<TextBlock Text="{Binding Name}" Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}" Style="{StaticResource TitleTextStyle}" Height="60" Margin="15,0,15,0"/>
<TextBlock Text="{Binding Owner}" Foreground="{StaticResource ListViewItemOverlaySecondaryForegroundThemeBrush}" Style="{StaticResource CaptionTextStyle}" TextWrapping="NoWrap" Margin="15,0,15,10"/>
</StackPanel>
</Grid>
</DataTemplate>
按F5執行,結果如下,目前無法看到Image,因為Image無法直接顯示binary的內容,這個部分需要另外處理,這將在下一章進行說明。

圖31 -資料繫結
總結
看完之後,應該有躍躍欲試的想法吧! 別忘了先拿一張白紙將你的想法畫下來,開始時建議你先使用Visual Studio內建的範本,熟悉之後再使用空白或基本範本,應當可以設計出美麗的App。