使用 C# 或 Visual Basic 創建你的第一個 Metro 風格應用


http://msdn.microsoft.com/zh-cn/library/windows/apps/br211380


使用 C# 或 Visual Basic 創建你的第一個 Metro 風格應用

10(共 11)對本文的評價是有幫助 - 評價此主題

[本文檔尚屬初步文檔,可能會隨時發生更改。]

我們將向你介紹使用 C# 或 Microsoft Visual Basic 創建 Metro 風格應用所需的基本代碼和概念。你將使用可擴展標記語言 (XAML) 來定義 UI 並選擇編寫應用邏輯的語言。

如果你希望使用其他編程語言,請參閱:

路線圖:此主題與其他主題有何關聯?請參閱:使用 C# 或 Visual Basic 的 Metro 風格應用的路線圖 

在本教程中,我們將快速瀏覽構建 Metro 風格應用程序時使用的功能。在創建一個簡單博客閱讀器應用的過程中,我們會介紹使用 XAML 進行開發時的重要概念,包括佈局、控件、模板和數據綁定。我們將學習使用編譯到 Microsoft Visual Studio 11 Express Beta for Windows 8 中的頁面模板和導航,以快速開始我們的應用開發。然後,我們會學習如何使用自定義樣式來修改應用的外觀以及如何將 UI 應用到各種不同的佈局和視圖。最後,我們會簡單討論如何將我們的應用與 Windows 8 Consumer Preview 相集成並將其發佈到 Windows 應用商店。學完本教程後,你便可以開始構建你自己的 Metro 風格應用了。

閱讀本教程大約需要 30 分鐘,如果你還要做練習,則要花更長的時間。

Hello World

在使用 C# 或 Visual Basic 創建 Metro 風格應用時,通常會使用 XAML 定義 UI,並用選定的語言在關聯的代碼隱藏文件中編寫應用邏輯。使用 C# 或 Visual Basic 編寫的 Metro 風格應用的 XAML UI 框架位於 Windows 運行時 的 Windows.UI.Xaml.* 命名空間中。如果你使用 Windows Presentation Foundation (WPF)、Silverlight 或 Silverlight for Windows Phone 編寫過應用,那麼你應該已經熟悉此編程模型了,並且能夠利用這一經驗使用 C++、C# 或 Visual Basic 來創建你的 Metro 風格應用。

此處的示例顯示了定義簡單的 Hello World 應用的 UI 及其關聯的代碼隱藏頁面的 XAML。即使這一簡單的示例,也顯示了若干個對基於 XAML 的編程模型而言非常重要的概念,包括部分類、佈局、控件、屬性和事件。

<Page
    x:Class="HelloWorld.BlankPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:HelloWorld"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
        <StackPanel>
            <Button Content="Click Me" Click="HelloButton_Click" />
            <TextBlock x:Name="DisplayText" FontSize="48" />
        </StackPanel>
    </Grid>
</Page>

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

namespace HelloWorld
{
    public sealed partial class BlankPage : Page
    {
        public BlankPage()
        {
            InitializeComponent();
        }

        private void HelloButton_Click(object sender, RoutedEventArgs e)
        {
            DisplayText.Text = "Hello World";
        }
    }
}

Hello World 應用是很好的入門示例。但它還不足以把我們帶向贏利之路,因此,我們在探索如何構建用於 Windows 8 的 Metro 風格應用時將採用一條不同的路徑。我們用一個簡單的博客閱讀器作爲入門示例應用,它可以下載和顯示 RSS 2.0 或 Atom 1.0 源中的數據。Windows 團隊博客站點中的信息提要似乎正適合使用。Developing for Windows 博客就是其中之一。

Developing for Windows 博客。

在 Visual Studio 中創建 Windows Metro 風格應用

在本部分中,你將學習如何:

  • 在 Visual Studio 11 Express Beta for Windows 8 中創建新的 Windows Metro 風格項目。

Visual Studio 是一個用於開發 Windows 應用的功能強大的集成開發環境 (IDE)。它可提供下列功能:源文件管理;集成式生成、部署和啓動支持;XAML、Visual Basic、C#、C++、圖形和清單編輯;以及調試等。Visual Studio 有好幾個版本,但我們將使用的是 Visual Studio 11 Express Beta for Windows 8。你可以隨用於 Metro 風格應用的 Windows 軟件開發工具包 (SDK) 免費下載 Visual Studio,以便獲得構建、封裝和部署你的 Metro 風格應用所需的一切。

要開始創建應用,你需要使用 C# 或 Visual Basic 創建一個新的 Windows Metro 風格項目。Visual Studio 11 Express Beta for Windows 8 包含多個用於 Windows Metro 風格項目的模板,這些模板爲你提供瞭如何創建使用各種佈局的應用的良好開端。空白應用程序項目模板提供最少的創建所有 Windows Metro 風格應用所需的文件。

BR211380.wedge(zh-cn,WIN.10).gif創建新的 Windows Metro 風格項目

  1. 安裝 Visual Studio 11 Express Beta for Windows 8 
  2. 選擇“文件”>“新建項目”。“新建項目”對話框即會打開。
  3. 在“已安裝”窗格中,展開“Visual C#”或“Visual Basic”。
  4. 選擇“Windows Metro 風格”模板類型。
  5. 在中心窗格中,選擇“空白應用程序”。
  6. 輸入項目名稱。

    這是一個在 Visual Studio 11 Express Beta for Windows 8 中創建的新項目。

    Visual Studio“新建項目”對話框。
  7. 單擊“確定”。你的項目文件已經創建完畢。

你創建項目時,Visual Studio 創建項目文件並在“解決方案資源管理器”中顯示這些文件。讓我們來看看空白應用程序模板所創建的文件。

文件名稱 描述
Properties/AssemblyInfo (.vb or .cs) 包含嵌入到所生成的集合中的名稱和版本元數據。
Package.appxmanifest 包含描述你的應用的元數據,包括顯示名稱、說明、徽標和功能。
Assets/* 你可以替換的默認徽標和初始屏幕圖像。
Common/LayoutAwarePage 具有能夠適應不同佈局和視圖的頁面功能的基類。
Common/RichTextColumns 具有用於在列中部署的多格式文本的功能的基類。
Common/StandardStyles.xaml 包含應用的默認樣式和模板。
App.xaml,App.xaml.* (.vb, .cs) 這些文件指定應用級邏輯。顯示用戶界面需要使用應用類。
BlankPage.xaml 用於創建用戶界面的默認起始頁。
BlankPage.xaml.* (.vb, .cs) 包含默認起始頁的邏輯的代碼隱藏文件。

若要了解有關這些文件和模板的詳細信息,請參閱使用模板快速開始你的 Metro 風格應用(C#、C++、Visual Basic) 

指定應用功能

在本部分中,你將學習如何:

  • 在應用程序清單設計器中指定應用的功能。

Metro 風格應用在安全容器中運行,對文件系統、網絡資源和硬件具有有限的訪問權限。 無論用戶何時從 Windows 應用商店安裝應用,Windows 都會查看文件中的元數據Package.appxmanifest以確定應用需要執行哪些功能。例如,某個應用可能需要訪問 Internet 中的數據、用戶文檔庫中的文檔,或用戶的攝像頭和麥克風。當應用安裝完成後,它會向用戶顯示所需的功能,而用戶必須授予相應的權限才能讓它訪問這些資源。如果應用沒有請求並接收所需要的某個資源的訪問權限,則當用戶運行它時,系統將禁止其訪問該資源。

下面列出了一些常見的功能:

功能 名稱 描述
文檔庫 documentsLibrary 允許應用訪問用戶的文檔庫,以及添加、更改或刪除文件。你的應用只能訪問已在清單中聲明的文件類型,不能訪問家庭組計算機上的文檔庫。
企業身份驗證 enterpriseAuthentication 允許應用連接至需要域憑據的 Intranet 資源。
Internet(客戶端和服務器) internetClientServer 允許你的應用訪問 Internet 和公用網絡,允許通過 Internet 連接到你的應用。對重要端口的入站訪問始終會被阻止。這是 Internet(客戶端)功能的一個超集。你不用同時聲明兩者。
Internet(客戶端) internetClient 允許你的應用訪問 Internet 和公用網絡。大部分需要 Internet 訪問的應用都應使用此功能。
位置 location 允許你的應用訪問用戶的當前位置。
麥克風 microphone 允許你的應用訪問用戶的麥克風。
音樂庫 musicLibrary 允許你的應用訪問用戶的音樂庫,並允許添加、更改或刪除文件。還允許訪問家庭組計算機上的音樂庫以及本地連接的媒體服務器上的音樂文件類型。
圖片庫 picturesLibrary 允許你的應用訪問用戶的圖片庫,並允許添加、更改或刪除文件。還允許訪問家庭組計算機上的圖片庫,以及本地連接的媒體服務器上的圖片文件類型。
專用網絡(客戶端和服務器) privateNetworkClientServer 允許通過你的應用對用戶信任的網絡(例如家庭或企業網絡)進行入站和出站訪問。對重要端口的入站訪問始終會被阻止。
近程 proximity 允許你的應用訪問用戶的接近現場通信 (NFC) 設備。
可移動存儲 removableStorage 允許你的應用訪問可移動存儲設備,例如外部硬盤驅動器或 USB 閃存驅動器,並允許添加、更改或刪除文件。你的應用只能訪問已在清單中聲明的文件類型。你的應用不能訪問家庭組計算機上的可移動存儲設備。
共享用戶證書 sharedUserCertificates 允許你的應用訪問軟件和硬件證書,例如智能卡證書。
文本消息傳遞 sms 允許你的應用訪問文本消息傳遞功能。
視頻庫 videosLibrary 允許你的應用訪問用戶的視頻庫,並允許添加、更改或刪除文件。還允許訪問家庭組計算機上的視頻庫,以及本地連接的媒體服務器上的視頻文件類型。
網絡攝像機 webcam 允許你的應用訪問用戶的照相機。

嚮應用添加功能

  1. 在“解決方案資源管理器”中,雙擊 Package.appxmanifest。此時將在“應用程序清單設計器”中打開該文件。
  2. 在“應用程序清單設計器”中,選擇“功能”選項卡。
  3. 選中你的應用所需的每項功能旁邊的複選框。(“Internet(客戶端)”默認處於選中狀態。這是我們使用博客閱讀器應用所全部需要的。)
  4. 保存並關閉文件。

指定某項功能時,該功能即會列在 Capabilities 元素下的 Package.appxmanifest.xml 文件中。如我們剛纔看到的一樣,你通常是在應用程序清單設計器中設置功能,但如果你右鍵單擊文件,選擇“打開方式…”,並在 XML 編輯器中打開該文件,則可以在 XML 中看到此 Capabilities 元素。

<Capabilities>    
    <Capability Name="internetClient" />
</Capabilities>

將數據獲取到應用

在本部分中,你將學習如何:

  • 創建自定義數據類
  • 異步檢索 RSS 或 Atom 數據信息提要。

既然我們的應用可以從 Internet 下載數據了,我們便可以編寫代碼以將博客信息提要置於其中了。Windows 團隊博客以 RSS 和 Atom 兩種形式展示了文章的完整文本。我們希望在閱讀器應用中顯示的博客數據爲每篇最新博客文章的標題、作者、日期和內容。

首先,我們需要下載每篇文章的數據。幸運的是,Windows 運行時包含一組類,這些類可以爲我們執行處理信息提要數據的許多工作。我們可以在 Windows.Web.Syndication 命名空間中可以找到這些類。可以直接使用這些類顯示 UI 中的數據。但在我們的博客閱讀器中,我們將創建屬於自己的數據類。這賦予了我們更多的靈活性,還可以讓我們以相同的方式處理 RSS 和 Atom 信息提要。

向項目添加新的類文件

  1. 選擇“項目”>“添加類”。“新增項目”對話框即會打開。
  2. 輸入類文件的名稱。在此示例中,我們使用 FeedData
  3. 單擊“添加”。此時,新類文件便已創建完畢。

在我們的博客閱讀器應用中,我們使用 3 個類來容納和檢索信息提要數據。我們將所有 3 個類都放在一個名爲 FeedData(.cs 或 .vb)的文件中。FeedData 類容納有關 RSS 或 Atom 信息提要的信息。FeedItem 類容納有關信息提要所包含的單個博客文章的信息。FeedDataSource 類包含信息提要的機會以及從網絡檢索信息提要的方法。以下是這些類的代碼。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Windows.Web.Syndication;

namespace WindowsBlogReader
{
    // FeedData
    // Holds info for a single blog feed, including a list of blog posts (FeedItem)
    public class FeedData
    {
        public string Title { get; set; }
        public string Description { get; set; }
        public DateTime PubDate { get; set; }

        private List<FeedItem> _Items = new List<FeedItem>();
        public List<FeedItem> Items
        {
            get
            {
                return this._Items;
            }
        }
    }

    // FeedItem
    // Holds info for a single blog post
    public class FeedItem
    {
        public string Title { get; set; }
        public string Author { get; set; }
        public string Content { get; set; }
        public DateTime PubDate { get; set; }
        public Uri Link { get; set; }
    }

    // FeedDataSource
    // Holds a collection of blog feeds (FeedData), and contains methods needed to
    // retreive the feeds.
    public class FeedDataSource
    {
        private ObservableCollection<FeedData> _Feeds = new ObservableCollection<FeedData>();
        public ObservableCollection<FeedData> Feeds
        {
            get
            {
                return this._Feeds;
            }
        }

        public async Task GetFeedsAsync()
        {
            Task<FeedData> feed1 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/developers/atom.aspx");
            Task<FeedData> feed2 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/windowsexperience/atom.aspx");
            Task<FeedData> feed3 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/extremewindows/atom.aspx");
            Task<FeedData> feed4 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/business/atom.aspx");
            Task<FeedData> feed5 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/bloggingwindows/atom.aspx");
            Task<FeedData> feed6 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/windowssecurity/atom.aspx");
            Task<FeedData> feed7 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/springboard/atom.aspx");
            Task<FeedData> feed8 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/windowshomeserver/atom.aspx");
            // There is no Atom feed for this blog, so we use the RSS feed.
            Task<FeedData> feed9 = 
                GetFeedAsync("http://windowsteamblog.com/windows_live/b/windowslive/rss.aspx");
            Task<FeedData> feed10 = 
                GetFeedAsync("http://windowsteamblog.com/windows_live/b/developer/atom.aspx");
            Task<FeedData> feed11 = 
                GetFeedAsync("http://windowsteamblog.com/ie/b/ie/atom.aspx");
            Task<FeedData> feed12 = 
                GetFeedAsync("http://windowsteamblog.com/windows_phone/b/wpdev/atom.aspx");
            Task<FeedData> feed13 = 
                GetFeedAsync("http://windowsteamblog.com/windows_phone/b/wmdev/atom.aspx");

            this.Feeds.Add(await feed1);
            this.Feeds.Add(await feed2);
            this.Feeds.Add(await feed3);
            this.Feeds.Add(await feed4);
            this.Feeds.Add(await feed5);
            this.Feeds.Add(await feed6);
            this.Feeds.Add(await feed7);
            this.Feeds.Add(await feed8);
            this.Feeds.Add(await feed9);
            this.Feeds.Add(await feed10);
            this.Feeds.Add(await feed11);
            this.Feeds.Add(await feed12);
            this.Feeds.Add(await feed13);
        }

        private async Task<FeedData> GetFeedAsync(string feedUriString)
        {
            // using Windows.Web.Syndication;
            SyndicationClient client = new SyndicationClient();
            Uri feedUri = new Uri(feedUriString);

            try
            {
                SyndicationFeed feed = await client.RetrieveFeedAsync(feedUri);

                // This code is executed after RetrieveFeedAsync returns the SyndicationFeed.
                // Process it and copy the data we want into our FeedData and FeedItem classes.
                FeedData feedData = new FeedData();

                feedData.Title = feed.Title.Text;
                if (feed.Subtitle.Text != null)
                {
                    feedData.Description = feed.Subtitle.Text;
                }
                // Use the date of the latest post as the last updated date.
                feedData.PubDate = feed.Items[0].PublishedDate.DateTime;

                foreach (SyndicationItem item in feed.Items)
                {
                    FeedItem feedItem = new FeedItem();
                    feedItem.Title = item.Title.Text;
                    feedItem.PubDate = item.PublishedDate.DateTime;
                    feedItem.Author = item.Authors[0].Name.ToString();
                    // Handle the differences between RSS and Atom feeds.
                    if (feed.SourceFormat == SyndicationFormat.Atom10)
                    {
                        feedItem.Content = item.Content.Text;
                        feedItem.Link = new Uri("http://windowsteamblog.com" + item.Id);
                    }
                    else if (feed.SourceFormat == SyndicationFormat.Rss20)
                    {
                        feedItem.Content = item.Summary.Text;
                        feedItem.Link = item.Links[0].Uri;
                    }
                    feedData.Items.Add(feedItem);
                }
                return feedData;
            }
            catch (Exception)
            {
                return null;
            }
        }
    }
}


檢索信息提要數據

在準備好用於容納我們的數據的數據類後,我們回過頭來下載這些博客信息提要。Windows.Web.Syndication.SyndicationClient 類可檢索完全解析的 RSS 或 Atom 信息提要,因此,我們不用擔心解析 XML 的問題,而可以繼續構建應用中更加有趣的部分。 SyndicationClient類只提供一種檢索信息提要的方法,並且是異步的。異步編程模型在 Windows 運行時中常常用於幫助應用保持響應。幸運的是,程序已經爲我們處理好了在使用異步方法時可能會遇到的許多複雜問題。

在 C# 和 Visual Basic 中使用 await

如果在 C# 和 Visual Basic 中使用await關鍵字,則以異步方式檢索信息提要的代碼將與以同步方式檢索信息提要時所使用的代碼相似。我們來看一下。

在 GetFeedsAsync 方法中,我們針對每個我們想要檢索的博客信息提要調用 GetFeedAsync。只要可能,我們便會在 URL 中對 Atom 信息提要進行傳遞,因爲它包含我們想要顯示的作者數據。當每個博客信息提要返回時,我們將其添加到 FeedDataSource.Feeds 集合。

public async Task GetFeedsAsync()
{
    Task<FeedData> feed1 = 
        GetFeedAsync("http://windowsteamblog.com/windows/b/developers/atom.aspx");
    ...
    this.Feeds.Add(await feed1);
    ...
}

現在,讓我們更詳細地來看看 GetFeedAsync 方法,以瞭解 await 關鍵字是如何爲我們提供幫助的。第一個要注意的是:我們將 async 關鍵字添加到方法簽名中。

private async Task<FeedData> GetFeedAsync(string feedUriString)
{
...
}

你只能在被定義爲async的方法中使用await關鍵字。我們將返回類型指定爲 Task<FeedData> 。這是告訴編譯器生成表示方法檢索的 FeedData 對象的 Task 

在方法內,我們實例化一個 SyndicationClient 並調用其 RetrieveFeedAsync 方法來獲取包含我們需要的 RSS 或 Atom 信息的 SyndicationFeed 

SyndicationFeed feed = await client.RetrieveFeedAsync(feedUri);

此處的await關鍵字會告訴編譯器在後臺自動爲我們執行多種處理。編譯器會將該方法中位於此調用之後的其餘部分作爲此調用返回後將要執行的回調。它會緊接着將控制返回給調用會話(通常是 UI 會話),以使應用保持響應。此時,會將表示此方法的最終輸出的 Task (一個FeedData 對象)返回給調用者。

當 RetrieveFeedAsync 返回包含我們所需數據的 SyndicationFeed 時,將執行我們的方法中其餘的代碼。重要的是,系統在我們從中進行原始調用的相同會話上下文(UI 會話)中執行這些代碼,因此當我們需要在此代碼中更新 UI 時不必擔心使用調度程序。檢索到 SyndicationFeed 後,我們將需要的部分複製到 FeedData 和 FeedItem 數據類中。

// This code is executed after RetrieveFeedAsync returns the SyndicationFeed.
// Process it and copy the data we want into our FeedData and FeedItem classes.
FeedData feedData = new FeedData();

feedData.Title = feed.Title.Text;
if (feed.Subtitle.Text != null)
{
    feedData.Description = feed.Subtitle.Text;
}
// Use the date of the latest post as the last updated date.
feedData.PubDate = feed.Items[0].PublishedDate.DateTime;

foreach (SyndicationItem item in feed.Items)
{
    FeedItem feedItem = new FeedItem();
    feedItem.Title = item.Title.Text;
    feedItem.PubDate = item.PublishedDate.DateTime;
    feedItem.Author = item.Authors[0].Name.ToString();
    // Handle the differences between RSS and Atom feeds.
    if (feed.SourceFormat == SyndicationFormat.Atom10)
    {
        feedItem.Content = item.Content.Text;
        feedItem.Link = new Uri("http://windowsteamblog.com" + item.Id);
    }
    else if (feed.SourceFormat == SyndicationFormat.Rss20)
    {
        feedItem.Content = item.Summary.Text;
        feedItem.Link = item.Links[0].Uri;
    }
    feedData.Items.Add(feedItem);
}
return feedData;

當我們執行到 return 語句時,我們並未真正返回 FeedData 對象。請記住,當方法緊隨 await 語句返回給調用程序時,會返回一個表示該方法的最終輸出結果的 Task 。現在,我們就已經最終獲得想要的結果了。行 return feedData; 將作爲方法結果的 FeedData 對象提供給正在等待該對象的 Task

GetFeedsAsync 方法中的該行在等待 Task 

this.Feeds.Add(await feed1);

當 Task 獲得正在等待的 FeedData 結果後,代碼執行將繼續,FeedData 將被添加到 FeedDataSource.Feeds 集合中。

使用應用中的數據

爲了使用我們的應用中的數據,我們在 App.xaml.cs/vb 中創建了數據源的一個靜態實例。我們將實例命名爲 DataSource

sealed partial class App : Application
{
    // Add a static instance of FeedDataSource.
    public static FeedDataSource DataSource;

    public App()
    {
        this.InitializeComponent();

        // Instantiate the data source.
        DataSource = new FeedDataSource();
    }

   ...

}

頁面模板已經在它的代碼隱藏文件中包含一個 OnNavigatedTo 方法的替代。我們將代碼置於此方法中,以獲得應用的 FeedDataSource 實例並獲得源。首先,我們將 async 關鍵字添加到方法聲明,因爲我們在方法中使用 await 關鍵字。導航到頁面時,我們檢查以查看 FeedDataSource 是否已包含源。如果未包含,我們調用 FeedDataSource.GetFeedsAsync 方法。然後,將頁面的 DataContext 設置爲第一個源。 以下是 BlankPage.xaml.cs/vb 的相關代碼。

        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            FeedDataSource _feedDataSource = App.DataSource;

            if (_feedDataSource.Feeds.Count == 0)
            {
                await _feedDataSource.GetFeedsAsync();
            }

            this.DataContext = (_feedDataSource.Feeds).First();
        }

在 XAML 中定義應用佈局

注意  在接下來的在 XAML 中定義應用佈局添加控件和內容顯示數據 3 個部分中,我們將學習在 XAML 中創建用戶界面的基本知識。爲了學習這些基本知識,我們創建了一個簡單的單頁博客閱讀器,以顯示單個博客信息提要的文章。如果你已經有使用 XAML 的經驗並且熟悉 XAML 佈局、控件和數據綁定,則可以跳過這些部分且不用完成練習。但不要跳過爲日期轉換器添加代碼的部分,稍後在我們的應用中會用到它。讓我們回到添加頁面和導航部分中的創建完整的 Metro 風格應用。

在本部分中,我們將學習:

  • 可以在 XAML 中使用哪些面板來定義佈局
  • 如何在 Grid 中定義行和列
  • 如何使用 StackPanel 

應用佈局用於指定應用中每個對象的大小和位置。要定位視覺對象,你必須將其置於某個Panel 控件或其他容器對象中。XAML 佈局系統提供了各種 Panel 控件,例如用作你在其中排列控件的 Grid Canvas 和 StackPanel 

XAML 佈局系統既支持絕對佈局,也支持動態佈局。在絕對佈局中,使用顯式的 x 和 y 座標(例如,使用 Canvas )來定位控件。在動態佈局中,當應用重新調整大小時,佈局容器和控件會隨之自動改變大小和位置(例如使用StackPanel Grid 的情況)。在實際過程中,通常通過結合使用絕對佈局和動態佈局的方式,以及將面板嵌入到其他面板的方式來定義應用的佈局。

博客閱讀器應用的典型佈局如下:頂部爲標題,左側是文章列表,右側是所選文章的內容。

佈局的示例。

默認情況下,空白應用模板僅包含一個空白 Grid ,它是我們的 UI 的根元素。爲了指定我們的佈局,我們將Grid劃分爲兩行。首行顯示博客標題。在第二行中,我們嵌入另一個Grid,將它分爲兩列,然後添加其他一些佈局容器以顯示博客內容。

    <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="140" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <!-- Title -->

        <!-- Content -->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="2*" MinWidth="320" />
                <ColumnDefinition Width="3*" />
            </Grid.ColumnDefinitions>

            <!-- Left column -->
            <ListView x:Name="ItemListView" />

            <!-- Right column -->
            <Grid Grid.Column="1" >
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>
            </Grid>
        </Grid>
    </Grid> 


我們更詳細地看看此 XAML 可以做什麼。若要在 Grid 中定義行,你需要在 Grid.RowDefinitions 集合中添加 RowDefinition 對象。你可以在 RowDefinition 中設置屬性,以指定行的外觀。添加列的方法是相同的,只不過要使用 ColumnDefinition 對象和 Grid.ColumnDefinitions 集合。

在 XAML 中,行定義的外觀如下:

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


第一個行定義(行 0)上的 Height="140" 屬性設置將頂部的行設置爲 140 個與設備無關的像素的絕對高度。無論行的內容或應用的大小如何變化,此高度都不會改變。第二個行定義(行 1)上的 Height="*" 設置告訴底部行佔用行 0 佔用後的所有空間。這通常稱爲星形比例縮放。我們在第二個Grid 的列定義中也使用了比例縮放。寬度設置Width="2*"Width="3*"要求Grid將自身分爲 5 個相等的部分。兩個部分用於第一列,三個部分用於第二列。

要在Grid 中定位某個元素,你需要設置該元素的附加屬性Grid.Row 和 Grid.Column 。行和列編號是從零開始的。這些屬性的默認值是 0,因此如果未設置任何內容,則該元素將位於第一行第一列。

<Grid Grid.Row="1"> 元素在根 Grid 的底部行嵌入一個 Grid。該Grid被劃分爲兩列。

元素<ListView x:Name="ItemListView">將一個ListView 添加到底部的左側列中Grid 。元素<Grid Grid.Column="1">將另一個Grid添加到底部的右側列中Grid。我們將該Grid劃分爲兩行。設置Height="Auto"要求頂行儘可能地調整高度以適合其內容。底行則佔用剩下的所有空間。

我們的 UI 中需要佈局面板的最後一個部分是博客文章列表。在該列表中,我們需要按如下所示排列標題、作者和日期。

StackPanel 的示例。

當你需要在頁面 UI 的一個小的子部分中自動排列連續元素時,通常使用一個 StackPanel 。 StackPanel是一種簡單的佈局面板,可以將子元素按水平或垂直方向排列到單行中。你可以使用StackPanel.Orientation 屬性來指定子元素的方向。 Orientation屬性的默認值是 Orientation.Vertical 。我們使用StackPanel來排列博客文章列表中的項。我們看到在使用模板設置數據格式會用到它。StackPanel 的 XAML 如下所示。

<StackPanel>
    <TextBlock Text="{Binding Path=Title}" FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap" />
    <TextBlock Text="{Binding Path=Author}" FontSize="16" Margin="15,0,0,0"/>
    <TextBlock Text="{Binding Path=PubDate}" FontSize="16" Margin="15,0,0,0"/>
</StackPanel>

添加控件和內容

在本部分中,你將學習如何:

  • 嚮應用添加控件

佈局面板雖然至關重要,但如果其中沒有內容,勢必索然無味。 通過添加按鈕、列表、文本、圖形和圖像等控件創建應用的 UI。你所使用的元素取決於你的應用要完成的功能。有關可以在我們的 Metro 風格應用中使用的控件的列表,請參閱控件列表 

看看我們的博客閱讀器 UI 的草稿,可以清楚地看到,我們需要顯示一行文本(博客和文章標題)、多行文本(文章內容)和博客文章列表。我們通過添加TextBlock 控件以顯示標題,並且用一個ListView 控件來顯示博客文章列表。初看之下,我們似乎可以使用一個多行的TextBlockRichTextBlock 來顯示文章內容。但是,當我們更深入瞭解時,我們發現包含文章內容的字符串不是純文本,而是 HTML 字符串。我們不想顯示一堆 HTML 標記,但如果我們將字符串放在一個 TextBlock 中便會發生這種情況,因此我們使用 WebView 控件來顯示 HTML。

添加控件後,我們的 UI 所用的 XAML 現在看起來類似於以下所示。

<Page
    x:Class="WindowsBlogReader.BlankPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WindowsBlogReader"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="140" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <!-- Title -->
        <TextBlock x:Name="TitleText" Text="{Binding Title}"
                   VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0"/>

        <!-- Content -->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="2*" MinWidth="320" />
                <ColumnDefinition Width="3*" />
            </Grid.ColumnDefinitions>

            <!-- Left column -->
            <!-- The default value of Grid.Column is 0, so we do not need to set it   
                 to make the ListView show up in the first column. -->
            <ListView x:Name="ItemListView"  
                      ItemsSource="{Binding Items}"
                      Margin="60,0,0,10">
            </ListView>

            <!-- Right column -->
            <!-- We use a Grid here instead of a StackPanel so that the WebView sizes correctly. -->
            <Grid DataContext="{Binding ElementName=ItemListView, Path=SelectedItem}"
                  Grid.Column="1" Margin="25,0,0,0">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>
                <TextBlock x:Name="PostTitleText" Text="{Binding Title}" FontSize="24"/>
                <WebView x:Name="ContentView" Grid.Row="1" Margin="0,5,20,20"/>
            </Grid>
        </Grid>
    </Grid>
</Page>

在本部分中,你將學習如何:

  • 將數據綁定到 UI
  • 使用模板設置數據格式。

將數據綁定到 UI

在我們最初創建的 Hello World 應用中,我們通過在此按鈕單擊事件處理程序中設置 TextBlock 的 Text 屬性,更新了 UI 中的文本。

private void HelloButton_Click(object sender, RoutedEventArgs e)
{
    DisplayText.Text = "Hello World";
}

有時像這樣在代碼中設置Text 屬性即可奏效。但要顯示數據,你通常需要使用數據綁定將數據源連接到 UI。建立綁定時以及數據源發生變化時,綁定到數據源的 UI 元素會自動反映變化。類似地,用戶在 UI 元素中所做的更改也會反映在數據源中。例如,如果用戶編輯了TextBox 中的值,則綁定引擎將自動更新基礎數據源以反映該變化。

在博客閱讀器應用中,我們將標題TextBlock Text 屬性綁定到某個源對象的Title屬性,以此來顯示博客標題。

<TextBlock x:Name="TitleText" Text="{Binding Title}"
           VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0" />


我們採用同樣的方式顯示所選博客文章的標題。

<TextBlock x:Name="PostTitleText" Text="{Binding Title}" FontSize="24"/>

且慢!如果兩個TextBlock 都綁定到同一個源對象,那它們如何顯示不同的標題呢?答案就在每個TextBlock所綁定的DataContext 中。使用 DataContext 屬性可設置整個 UI 元素的默認綁定,包括其所有子元素。有時你爲整個頁面設置DataContext屬性,而有時你需要爲頁面上的個別元素設置該屬性。每個 XAML 級別的 DataContext 設置都會替代更高一級的所有設置。此外,你還可以通過設置個別綁定的Source 屬性來替代針對該綁定的任何已生效的DataContext設置。

在博客閱讀器應用中,我們爲代碼隱藏的整個頁面設置 DataContext 。注意,在檢索到數據信息提要之後,我們將使用這行代碼設置DataContext 。

        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            ...
            this.DataContext = (_feedDataSource.Feeds).First();
        }

第一個 TextBlock 的上下文是 FeedData 對象,因此它顯示 FeedData.Title 屬性。

第二個TextBlock 如何顯示所選博客文章的標題?如下所示,第二個TextBlock位於一個Grid 中。

<Grid DataContext="{Binding ElementName=ItemListView, Path=SelectedItem}" 
      Grid.Column="1" Margin="25,0,0,0">
      ...
      <TextBlock x:Name="PostTitleText" Text="{Binding Title}" FontSize="24"/>
      ...
</Grid>

Grid DataContext ListView SelectedItem 屬性綁定。因此綁定引擎將再次爲我們執行後臺操作。只要ListView中的選擇發生變化,StackPanel DataContext就會自動更新爲所選文章。 GridDataContext會替代頁面的DataContext,因此第二個TextBlock 將顯示所選博客文章的FeedItem.Title屬性。

每個綁定都會有一個 Mode 屬性來規定更新數據的方式和時間。

綁定模式 描述
OneTime   僅當首次創建綁定時,纔會在目標上設置值,之後再也不會更新。
OneWay   如果源發生變化,目標將更新。
TwoWay   如果源或目標之一發生變化,則目標和源都會更新。
如果你使用 OneWay 或 TwoWay 綁定,要使綁定收到源對象變化的通知,必須實現 INotifyPropertyChanged 接口。有關數據綁定的詳細信息,請參閱快速入門: 

使用數據模板設置數據格式

比起單純地設置綁定,在列表視圖中顯示我們需要的數據則稍微複雜一點。我們將 ListView 綁定到 FeedData 對象的 Items 屬性,因此,正確的數據已準備就緒。但如果我們就這樣運行該應用, ListView並不知道要顯示什麼內容,因此它只會調用自己所綁定對象中的ToString 。這爲我們提供了一個如下所示的“WindowsBlogReader.FeedItem”字符串列表。很顯然,這不是我們想要顯示的內容。

未使用模板的綁定列表。

我們可以通過設置ListView 中的DisplayMemberPath 屬性對它做一點改進。此操作指示列表視圖調用綁定對象的指定屬性(而不是對象本身)上的 ToString。如果我們設置 DisplayMemberPath=Title, ListView將顯示博客文章標題的列表。

<ListView x:Name="ItemListView"  
                  ItemsSource="{Binding Items}"
                  DisplayMemberPath="Title"
                  Margin="60,0,0,10">
</ListView>

這就更接近我們要顯示的內容了。

一個顯示了標題的綁定列表。

但我們實際想要顯示的是列表中每篇文章的標題、作者和發佈日期。爲此,我們定義一個模板,它會確切地告訴ListView 我們希望如何顯示數據。用於查看數據項集合的控件是由ItemsControl 類派生的。這些控件具有ItemTemplate 屬性,我們可以用某個DataTemplate 爲其賦值。 DataTemplate定義了數據的顯示方式。

要點  你不能同時使用一個 DisplayMemberPath 和一個 ItemTemplate 。向 ListView 添加 ItemTemplate 時,請確保刪除 DisplayMemberPath 設置。

以下是使用 DataTemplate 定義的級聯(而不是 DisplayMemberPath )的 ListView 的 XAML。

<ListView x:Name="ItemListView"  
          ItemsSource="{Binding Items}"
          Margin="60,0,0,10">
    <ListView.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding Title}"  
                           FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap" />
                <TextBlock Text="{Binding Author}" 
                           FontSize="16" Margin="15,0,0,0"/>
                <TextBlock Text="{Binding PubDate}" 
                           FontSize="16" Margin="15,0,0,0"/>
            </StackPanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

在 DataTemplate 內定義 UI 的位與定義任何其他 UI 的方式相同。我們使用一個StackPanel 依次垂直堆放 3 個TextBlock 。然後,我們將 TextBlock 的 Text 屬性綁定到 TitleAuthor 和 PubDate 屬性。這些綁定的默認 DataContext 是顯示在 ListView 中的對象,它是一個 FeedItem

以下是當我們使用模板定義列表外觀時的樣子。

一個使用了數據模板的綁定列表。

使用值轉換器設置數據格式

在 ItemListView 的DataTemplate 中,我們將PubDate屬性(是一個DateTime )綁定到TextBlock.Text 屬性。綁定引擎會自動將 PubDate 從一個 DateTime 轉換爲一個字符串。但自動轉換會同時顯示日期和時間,而我們只想顯示日期。要修復此問題,我們可以創建自己的值轉換器來將DateTime轉換爲字符串,並且可以在其中將字符串設置爲任何需要的格式。

要創建值轉換器,我們先創建一個用於實現IValueConverter 接口的類,然後實現Convert ConvertBack 方法。轉換器可以將數據從一種類型更改爲另一種類型,根據文化背景轉換數據,或者修改數據呈現方式的其他方面。在這裏,我們創建一個日期轉換器,它將轉換傳入的日期值並設置其格式,從而只顯示日、月和年。

注意  要向項目添加新類,請選擇“項目”>“添加類”。將類命名爲 DateConverter(.cs 或 .vb)。

Convert ConvertBack 方法還允許你傳入一個參數,以便通過不同的選項使用該轉換器的同一個實例。在此示例中,我們包含了一個格式設置轉換器,它可以根據輸入的參數生成不同格式的日期。你可以使用Binding 類的ConverterParameter ConvertConvertBack方法中傳遞一個參數。稍後,我們將修改列表視圖來演示上述內容。

using System;
using Windows.Globalization.DateTimeFormatting;

namespace WindowsBlogReader
{
    public class DateConverter : Windows.UI.Xaml.Data.IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string culture)
        {
            if (value == null)
                throw new ArgumentNullException("value", "Value cannot be null.");

            if (!typeof(DateTime).Equals(value.GetType()))
                throw new ArgumentException("Value must be of type DateTime.", "value");
            
            DateTime dt = (DateTime)value;

            if (parameter == null)
            {
                // Date "7/27/2011 9:30:59 AM" returns "7/27/2011"
                return DateTimeFormatter.ShortDate.Format(dt);
            }           
            else if ((string)parameter == "day")
            {
                // Date "7/27/2011 9:30:59 AM" returns "27"
                DateTimeFormatter dateFormatter = new DateTimeFormatter("{day.integer(2)}");
                return dateFormatter.Format(dt);
            }
            else if ((string)parameter == "month")
            {
                // Date "7/27/2011 9:30:59 AM" returns "JUL"
                DateTimeFormatter dateFormatter = new DateTimeFormatter("{month.abbreviated(3)}");
                return dateFormatter.Format(dt).ToUpper();
            }
            else if ((string)parameter == "year")
            {
                // Date "7/27/2011 9:30:59 AM" returns "2011"
                DateTimeFormatter dateFormatter = new DateTimeFormatter("{year.full}");
                return dateFormatter.Format(dt);
            }
            else
            {
                // Requested format is unknown. Return in the original format.
                return dt.ToString();
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, string culture)
        {
            string strValue = value as string;
            DateTime resultDateTime;
            if (DateTime.TryParse(strValue, out resultDateTime))
            {
                return resultDateTime;
            }
            return Windows.UI.Xaml.DependencyProperty.UnsetValue;
        }
    }
}


使用 DateConverter 類之前,必須在我們的 XAML 中聲明一個該類的實例。我們將關鍵字 dateConverter 作爲 App.xaml 中應用資源來聲明實例。在此處聲明實例後,在應用的每個頁面都可以使用它。

    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary>
                    <local:FeedDataSource x:Key="feedDataSource"/>
               
                    <!-- Add the DateConverter here. -->
                    <local:DateConverter x:Key="dateConverter" />

                </ResourceDictionary>
                ...
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>


現在,我們便可在綁定中使用 DateConverter 了。以下是來自 ItemListView 的 DataTemplate 中更新後的 PubDate 綁定。

<TextBlock Text="{Binding Path=PubDate, Converter={StaticResource dateConverter}}" 
           FontSize="16" Margin="15,0,0,0"/>

通過此 XAML,綁定引擎使用我們的自定義 DateConverter 將 DateTime 更改爲字符串。它返回的字符串已設置爲我們需要的格式,只含有日、月和年。

在 WebView 中顯示 HTML

在頁面中顯示博客文章的最後一步是,獲取文章數據以在 WebView 控件中顯示。我們已經在我們的 UI 中添加了這一控件 ContentView

<WebView x:Name="ContentView" Grid.Row="1" Margin="0,5,20,20"/>

WebView 控件爲我們提供了一種在應用內承載 HTML 數據的方法。但如果我們看看它的 Source 屬性,就會發現它採用 Web 頁面的 Uri 進行顯示。我們的 HTML 數據只不過是HTML 的字符串。它沒有包含可以綁定到 Source 屬性的 Uri。幸運的是,我們可以通過一種 NavigateToString 方法來傳遞我們的 HTML 字符串。

要實現該功能,我們需要處理ListView SelectionChanged 事件。以下是添加了 SelectionChanged 事件的 ListView 的 XAML。

<ListView x:Name="ItemListView"  
                  ItemsSource="{Binding Items}"
                  Margin="60,0,0,10"
                  SelectionChanged="ItemListView_SelectionChanged">
    ...

我們向 BlankPage.xaml.cs/vb 代碼隱藏頁添加事件處理程序。在事件處理程序中,我們將所選項目轉換爲一個 FeedItem,並從 Content 屬性獲取 HTML 的字符串。然後,我們將該字符串傳遞給 NavigateToString 方法。

private void ItemListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    FeedItem feedItem = e.AddedItems[0] as FeedItem;
    if (feedItem != null)
    {
        // Navigate the WebView to the blog post content HTML string.
        ContentView.NavigateToString(feedItem.Content);
    }
}

以下是該應用運行時的情況。

這是已完成的博客閱讀器應用。

這是一個不錯的基本頁面,在大多數情況下可以正常運行。但是 Metro 風格應用需要在所有情況下都能夠運行良好。它必須適應各種設備上不同的分辨率、方向和視圖。例如,以下是縱向查看時頁面的顯示情況。

BR211380.xaml_SimpleBlogReaderPortrait(zh-cn,WIN.10).png

你可以看到,在此方式下我們基本頁面的效果不如之前那麼好。我們希望我們的應用看起來美觀,並且能更好地反映 Windows 團隊博客的風格。爲了實現這些目標,我們使用 Microsoft Visual Studio 中提供的頁面模板作爲開始。然後,我們使用在創建此頁面的過程中學到的知識來修改模板並創建我們的完整應用。

添加頁面和導航

爲了使我們的博客閱讀器能夠適用於所有 Windows 團隊博客,我們必須嚮應用添加更多的頁面並處理如何在這些頁面之間進行導航。首先,我們需要一個能夠列出 Windows 團隊所有博客的頁面。當閱讀器從該頁面中選擇某個博客時,我們將加載該博客的文章列表。我們已創建的分頁閱讀器也可以完成此功能,但我們希望對它做一點改進。最後,我們需要添加一個詳細信息頁面,以便閱讀單個博客文章,而不至於讓列表視圖佔用空間。

幸好,我們不需要從空白模板開始創建每個頁面。Visual Studio 11 Express Beta for Windows 8 附帶了一個頁面模板的集合,這些模板對於各種情形都很有用。以下是可用的頁面模板。

頁面類型 描述
組詳細信息頁面 顯示單個組的詳細信息以及組中每個項目的預覽。
分組項目頁面 顯示分組的集合。
項目詳細信息頁面 詳細顯示一個項目,並允許導航到相鄰的項目。
項目頁面 顯示項目的集合。
拆分頁面 顯示項目的列表以及所選項目的詳細信息。
基本頁面 可以適應不同方向和視圖的空白頁面,並且包含一個標題和返回按鈕。
空白頁面 用於 Metro 風格應用的空白頁面。

嚮應用添加頁面。

  1. 選擇“項目”>“添加新項”。“添加新項”對話框即會打開。
  2. 在“已安裝”窗格中,展開“Visual C#”或“Visual Basic”。
  3. 選擇“Windows Metro 風格”模板類型。
  4. 在中心窗格中,選擇要添加到項目中的頁面類型。
  5. 爲該頁面輸入一個名稱。
  6. 單擊“添加”。XAML 和你的頁面的代碼隱藏文件即被添加到項目中。

以下是“添加新項”對話框。

Visual Studio“添加新項”對話框。

針對我們的博客閱讀器,我們將添加一個項目頁面來顯示 Windows 團隊博客的列表。我們將此頁面命名爲 ItemsPage。 我們添加一個拆分頁面來顯示每個博客的文章。我們將此頁面命名爲 SplitPage拆分頁面模板與我們爲簡單博客閱讀器應用創建的頁面相似,但更加精煉。我們針對詳細信息頁面(將其命名爲 DetailPage)使用基本頁面模板。它只有返回按鈕、頁面標題和一個用於顯示文章內容的 WebView 控件。但並不是像我們在拆分頁面中那樣將來自 HTML 字符串的文章內容加載到 WebView,我們導航到文章的 URL 並顯示實際的 Web 頁面。完成後的應用頁面看起來如下所示:

三個頁面的導航示例。

當我們將頁面模板添加到項目並瀏覽 XAML 和代碼隱藏後,看起來這些頁面模板幫我們做了許多工作。但實際上很容易迷失,因此,讓我們更進一步瞭解這些頁面模板並看看它們都包含什麼內容。Metro 風格應用的所有頁面模板都具有相同的格式。

頁面模板的 XAML 包含 4 個主要部分:

資源 “資源”部分定義頁面的樣式和數據模板。我們將在使用樣式創建一致性外觀部分對此進行更詳細的討論。
應用欄 可在應用欄定義應用命令。我們將在添加應用欄部分對此進行更詳細的討論。
應用內容 在根佈局面板中定義組成應用 UI 的控件和內容。
Visual State Manager 在 Visual State Manager (VSM) 中定義使應用適用於不同佈局和方向的動畫和過渡。我們將在適應不同的佈局部分對此進行更詳細的討論。

模板頁面全部從 LayoutAwarePage 類派生而來,默認情況下,這些模板頁面能夠比我們使用的初始 BlankPage執行更多的功能。LayoutAwarePage 是 Page 的一個實現,爲 Metro 風格應用開發啓用了重要的功能:

  • 應用程序視圖狀態到視覺狀態的映射使頁面能夠適應不同的分辨率、方向和視圖。
  • GoBack 和 GoHome 事件處理程序支持基本的導航。
  • 默認的視圖模型爲你提供了一個簡單的可綁定數據源。

頁面模板還使用 StandardStyles.xaml 中的樣式和模板,這些樣式和模板應用 Metro 風格應用的設計指南。我們將使用其中一些樣式作爲開始,並修改它們的副本來自定義應用的外觀。

在頁面之間導航

XAMLUI 框架提供了使用 Frame 和 Page 的內置導航模型,其工作方式與在 Web 瀏覽器中的導航方式非常相似。Frame控件可託管 Page,並且具有導航歷史記錄,你可以通過該歷史記錄在訪問過的頁面中前進和後退。在導航時,你可以在頁面之間傳遞數據。

在 Visual Studio 項目模板中,名爲 rootFrame 的 Frame 被設置爲應用窗口的內容。我們來看看 App.xaml.cs/vb 中的代碼。

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
    // Create a Frame to act as navigation context and navigate to the first page
    var rootFrame = new Frame();
    rootFrame.Navigate(typeof(BlankPage));

    // Place the frame in the current Window and ensure that it is active
    Window.Current.Content = rootFrame;
    Window.Current.Activate();
}

這些代碼用於創建框架 ,將其設置爲 Window 的內容,並導航到 BlankPage。由於我們的完整應用的第一頁是 ItemsPage,因此我們將調用更改爲導航 方法並在此處所示的 ItemsPage 中進行傳遞。

rootFrame.Navigate(typeof(ItemsPage));


加載 ItemsPage 時,我們需要獲得數據源的一個實例,並檢索要顯示的源數據,就像我們在使用應用中的數據部分中使用 BlankPage 一樣。我們將代碼置於頁面模板中包括的 OnNavigatedTo 方法替代中,如果尚未檢索源,我們將調用 FeedDataSource.GetFeedsAsync 方法。以下是 ItemsPage.xaml.cs/vb 的相關代碼。

protected override async void OnNavigatedTo(NavigationEventArgs e)
{
    FeedDataSource _feedDataSource = App.DataSource;

    if (_feedDataSource != null)
    {
        if (_feedDataSource.Feeds.Count == 0)
        {
            await _feedDataSource.GetFeedsAsync();
        }

        this.DefaultViewModel["Items"] = _feedDataSource.Feeds;
    }
}

當用戶從集合中選取博客時,我們從項目頁導航到拆分頁。爲了執行此導航,我們希望 GridView 項目響應單擊(如按鈕)操作,而不是被選定。爲了使 GridView 項目可單擊,我們按如下所示設置 SelectionMode 和 IsItemClickEnabled 屬性。然後我們爲 GridView 的 ItemClick 事件添加一個事件處理程序。以下是 ItemsPage.xaml 中用於 GridView 的 XAML,其中已設置屬性,並已添加 ItemClick 事件。

<GridView
    x:Name="itemGridView"
    AutomationProperties.AutomationId="ItemsGridView"
    AutomationProperties.Name="Items"
    Margin="116,0,116,46"
    ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
    ItemTemplate="{StaticResource Standard250x250ItemTemplate}"
    SelectionMode="None"
    IsItemClickEnabled="True"
    ItemClick="ItemView_ItemClick" />

項目頁面還包含一個名爲 itemListView 的列表視圖,如果“調整”了應用,則會顯示該列表視圖來代替網格。.我們將在適應不同的佈局部分中對此進行更詳細的討論。目前,我們只需對 ListView 進行與對 GridView 所做更改相同的更改,以確保它們的行爲相同。

<ListView
    x:Name="itemListView"
    AutomationProperties.AutomationId="ItemsListView"
    AutomationProperties.Name="Items"
    Margin="10,0,0,60"
    ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
    ItemTemplate="{StaticResource Standard80ItemTemplate}"
    SelectionMode="None"
    IsItemClickEnabled="True"
    ItemClick="ItemView_ItemClick"/>

我們向 ItemsPage.xaml.cs/vb 代碼隱藏頁添加事件處理程序。 在處理程序中,我們將導航到拆分頁並傳遞所選信息提要的數據。

        private void itemView_ItemClick(object sender, ItemClickEventArgs e)
        {
            // Navigate to the split page, configuring the new page
            // by passing the clicked item (FeedItem) as a navigation parameter
            this.Frame.Navigate(typeof(SplitPage), e.ClickedItem);
        }

若要在頁面之間導航,你可以使用 Frame 控件的 Navigate GoForward 和 GoBack 方法。通過 Navigate(TypeName, Object) 方法可以導航並將數據對象傳遞到新頁面。我們將使用此方法在我們的頁面之間傳遞數據。第一個參數 typeof(BlankPage) 是我們將要導航到的頁面的 Type 。第二個參數是我們傳遞給將要導航到的頁面的數據對象。在本例中,我們傳遞 clicked 項。

在 SplitPage.xaml.cs/vb 代碼隱藏頁面中,我們需要使用剛剛從項目頁面傳遞的 FeedData 對象執行某些操作。爲此,我們將覆蓋Page OnNavigatedTo 方法。該方法已添加到頁面模板代碼中,因此我們只需要對其進行修改以便與我們的數據關聯。模板頁面包含一個名爲 DefaultViewModel 的內置視圖模型,我們可以將數據與之關聯。NavigationEventArgs.Parameter 屬性包含從項目頁面傳遞的數據對象。 我們將其轉換回 FeedData 對象,並將信息提要數據添加至具有關鍵字 Feed 的 DefaultViewModel,將 FeedData.Items 屬性添加至具有關鍵字 Items 的 DefaultViewModel。以下是更新的 OnNavigatedTo 方法。

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // TODO: Assign a bindable group to this.DefaultViewModel["Group"]
    // TODO: Assign a collection of bindable items to this.DefaultViewModel["Items"]
 
    FeedData feedData = e.Parameter as FeedData;
    if (feedData != null)
    {
        this.DefaultViewModel["Feed"] = feedData;
        this.DefaultViewModel["Items"] = feedData.Items;

        // Select the first item automatically unless logical page navigation is
        // being used (see the logical page navigation #region below.)
        if (!this.UsingLogicalPageNavigation()) this.itemsViewSource.View.MoveCurrentToFirst();
    }
}

在 Visual Studio 頁面模板中,TODO 註釋表示我們從何處將我們的數據對象添加至具有關鍵字 Group 的 DefaultViewModel。由於我們使用的是關鍵字 Feed,因此我們需要更改頁面標題中的綁定,以綁定到 Feed 屬性,而不是 Group。在 SplitPage.xaml 中,更改名爲 pageTitle 的 TextBlock 的 Text 綁定以綁定到 Feed.Title,如下所示:Text="{Binding Feed.Title}"

<TextBlock x:Name="pageTitle" Grid.Column="1" Text="{Binding Feed.Title}" 
           Style="{StaticResource PageHeaderTextStyle}"/>

要導航回項目頁面,Visual Studio 頁面模板包含相應的代碼來處理 BackButton 的 Click 事件,並調用 Frame.GoBack 方法。

我們需要再做一些更改才能完成向我們添加到組中的新頁面添加功能的操作。將這些代碼添加到應用中後,我們便可以繼續對我們的應用進行風格和動畫設置。

在 ItemsPage.xaml 中,頁面標題綁定到具有關鍵字 AppName 的靜態資源。將此資源中的文本更新到 Windows 團隊博客 中,如下所示。

<x:String x:Key="AppName">Windows Team Blogs</x:String>

在 SplitPage.xaml 中,將名爲 titlePanel 的網格更改爲跨 2 個列。

<!-- Back button and page title -->
<Grid x:Name="titlePanel" Grid.ColumnSpan="2">

同樣在 SplitPage.xaml 中,我們需要更改用來顯示所選博客文章的標題和內容的佈局。要執行此操作,需要將名爲 itemDetail 的 ScrollViewer 替換爲此 ScrollViewer 佈局。

        <!-- Details for selected item -->
        <ScrollViewer
            x:Name="itemDetail"
            AutomationProperties.AutomationId="ItemDetailScrollViewer"
            Grid.Column="1"
            Grid.Row="1"
            Padding="70,0,120,0"
            DataContext="{Binding SelectedItem, ElementName=itemListView}"
            Style="{StaticResource VerticalScrollViewerStyle}">
        
            <Grid x:Name="itemDetailGrid">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>
        
                <TextBlock x:Name="itemTitle" Text="{Binding Title}" 
                           Style="{StaticResource SubheaderTextStyle}"/>
                <Border x:Name="contentViewBorder" BorderBrush="Gray" BorderThickness="2" 
                        Grid.Row="1" Margin="0,15,0,20">
                    <Grid>
                        <WebView x:Name="contentView" />
                        <Rectangle x:Name="contentViewRect" />
                    </Grid>    
                </Border>
            </Grid>
        </ScrollViewer>

在 SplitPage.xaml.cs/vb 中,向 ItemListView_SelectionChanged 事件處理程序添加代碼,使用所選博客文章的內容填充 WebView ,如此處所示。此事件處理程序位於代碼的邏輯頁導航區域中。

        void ItemListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // Invalidate the view state when logical page navigation is in effect, as a change
            // in selection may cause a corresponding change in the current logical page.  When
            // an item is selected this has the effect of changing from displaying the item list
            // to showing the selected item's details.  When the selection is cleared this has the
            // opposite effect.
            if (this.UsingLogicalPageNavigation())
            {
                this.InvalidateVisualState();
            }
                
            // Add this code to populate the web view
            //  with the content of the selected blog post.
            Selector list = sender as Selector;
            FeedItem selectedItem = list.SelectedItem as FeedItem;
            if (selectedItem != null)
            {
                this.contentView.NavigateToString(selectedItem.Content);
            }        
        }

在 DetailPage.xaml 中,我們需要將標題文本綁定到博客文章標題,並添加一個 WebView 控件以顯示博客頁面。要執行此操作,需要將包含返回按鈕和頁面標題的 Grid 替換爲此 Grid 和 WebView

        <!-- 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}"/>
            <TextBlock x:Name="pageTitle" Grid.Column="1" 
                       Text="{Binding Title}" 
                       Style="{StaticResource PageHeaderTextStyle}"/>
        </Grid>
        <Border x:Name="contentViewBorder" BorderBrush="Gray" BorderThickness="2" 
                Grid.Row="1" Margin="120,15,20,20">
            <WebView x:Name="contentView" />
        </Border>

在 DetailPage.xaml.cs/vb 中,將代碼添加到 OnNavigatedTo 方法改寫中,以導航到博客文章並設置頁面的 DataContext 。此處顯示更新後的 OnNavigatedTo 方法。

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // Add this code to navigate the web view to the selected blog post.
    FeedItem feedItem = e.Parameter as FeedItem;
    if (feedItem != null)
    {
        this.contentView.Navigate(feedItem.Link);
        this.DataContext = feedItem;
    }
}

添加應用欄

博客閱讀器應用中的但部分導航都發生在用戶在 UI 中選取項目時。但在拆分頁面上,我們必須提供一種方法,讓用戶轉到博客文章的詳細信息視圖。我們可以在頁面的某個位置放置一個按鈕,但這會影響核心應用體驗,即閱讀。我們轉而將按鈕放在應用欄中,只有當用戶需要時纔會顯示應用欄。

應用欄是 UI 的一部分,默認情況下處於隱藏狀態,當用戶從屏幕邊緣輕掃或與應用交互時,可以顯示或隱藏屏幕欄。它可以向用戶顯示導航、命令和工具。應用欄可以顯示在頁面頂部、頁面底部,也可同時顯示在頁面頂部和底部。我們建議你將導航放在頂部應用欄中,將工具和命令放在底部應用欄中。

要在 XAML 中添加應用欄,我們需要將一個 AppBar 控件指定給 Page 的 TopAppBar 或 BottomAppBar 屬性。 我們將添加一個頂部應用欄,其中包含一個按鈕可以導航到詳細信息頁面。StandardStyles.xaml 文件包含適用於常見場景的各種應用欄按鈕樣式。我們將使用這些樣式作爲創建按鈕樣式的指南。我們將樣式放置在 SplitPage.xaml 的 UserControl.Resources 部分,並按如下所示將 Page.TopAppBar xaml 添加到資源部分之後。

<UserControl.Resources>
...
    <Style x:Key="WebViewAppBarButtonStyle" TargetType="Button" 
           BasedOn="{StaticResource AppBarButtonStyle}">
        <Setter Property="AutomationProperties.AutomationId" Value="WebViewAppBarButton"/>
        <Setter Property="AutomationProperties.Name" Value="View Web Page"/>
        <Setter Property="Content" Value=""/>
    </Style>
</UserControl.Resources>

<Page.TopAppBar>
   <AppBar Padding="10,0,10,0">
        <Grid>

            <Button Click="ViewDetail_Click" HorizontalAlignment="Right" 
                    Style="{StaticResource WebViewAppBarButtonStyle}"/>
        </Grid>
    </AppBar>
</Page.TopAppBar>

通過設置 IsSticky 和 IsOpen 屬性可以控制應用欄顯示和消失的方式和時間。還可以通過處理 Opened 和 Closed 事件響應打開或隱藏的應用欄。

爲處理詳細信息頁面的導航,我們將此代碼添加到 SplitPage.xaml.cs/vb 中。

private void ViewDetail_Click(object sender, RoutedEventArgs e)
{
    FeedItem selectedItem = this.itemListView.SelectedItem as FeedItem;
    if (selectedItem != null && this.Frame != null)
    {
        this.Frame.Navigate(typeof(DetailPage), selectedItem);
    }
}


添加動畫和過渡

當我們談論動畫時,通常會聯想到屏幕上蹦蹦跳跳的物體。但在 XAML 中,動畫基本上只是一種更改對象的屬性值的一種方法。這讓動畫具有多種用途,而不僅僅是一堆跳動的球。在我們的博客閱讀器應用中,我們使用動畫使 UI 適用於不同的佈局和方向。我們將在下一部分對此進行深入探討,但首先,我們需要了解動畫的工作方式。

要使用動畫,我們需要將它放在一個Storyboard 中。當 Storyboard 運行時,屬性會按照動畫規定發生變化。Storyboard 中可以包含一個或多個動畫。每個動畫會指定一個目標對象、該對象上要更改的屬性,以及該屬性的新值。

在我們的博客閱讀器應用中,我們有一個名爲 itemListView 的 ListView 。以下是一個當 Storyboard 運行時將 itemListView 的 Visibility 屬性更改爲 Visible 的動畫。

<Storyboard>
    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" 
                                   Storyboard.TargetProperty="Visibility" >
        <DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
    </ObjectAnimationUsingKeyFrames>
</Storyboard>


添加主題動畫

Windows 8 使用動畫和過渡來改進用戶的 UI 體驗。我們想在應用中具有相同的體驗,以符合 Windows 8 的風格。所幸的是,我們可以在應用中使用內置的主題動畫主題過渡,以與 Windows 8 中的主題動畫和主題過渡相匹配。我們可以在 Windows.UI.Xaml.Media.Animation 命名空間中找到它們。

主題動畫是一個預定義的動畫,我們可以將其放在一個 Storyboard 中。 PopInThemeAnimation 在頁面加載時使 Web 視圖從右到左滑入。增加 FromHorizontalOffset 屬性的值會使效果更好。在此,我們將 PopInThemeAnimation 放入 Storyboard,並使其成爲 DetailPage.xaml 中的資源。因爲返回按鈕和標題在各個頁面中均位於相同的位置,我們並不需要將它們彈入,所以我們將動畫的目標設置爲圍繞在我們的 Web 內容周圍的 Border 。這樣便會使 Border 和其中的所有內容具有動畫效果。

<UserControl.Resources>
    <Storyboard x:Name="PopInStoryboard">
        <PopInThemeAnimation  Storyboard.TargetName="contentViewBorder" 
                              FromHorizontalOffset="400"/>
    </Storyboard>
</UserControl.Resources>


在代碼隱藏頁面中,我們在OnNavigatedTo 方法中啓動 Storyboard ,這樣當用戶導航到詳細信息頁面時便可將彈入動畫應用於 Border 。以下是更新後的 OnNavigatedTo 方法。

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // Run the PopInThemeAnimation 
    Windows.UI.Xaml.Media.Animation.Storyboard sb = 
        this.FindName("PopInStoryboard") as Windows.UI.Xaml.Media.Animation.Storyboard;
    if (sb != null) sb.Begin();

    // Add this code to navigate the web view to the selected blog post.
    FeedItem feedItem = e.Parameter as FeedItem;
    if (feedItem != null)
    {
        this.contentView.Navigate(feedItem.Link);
        this.DataContext = feedItem;
    }
}


要使用主題動畫,我們仍然必須將其放入某個Storyboard 中,然後在發生特定事件時對該Storyboard進行控制。有時,我們可以改用主題過渡來爲 UI 元素設置動畫。主題過渡是一個完整的動畫集,還是一個可以附加到 UI 元素的預封裝行爲的 Storyboard。此處,我們使用一個 ContentThemeTransition 來爲 SplitPage.xaml 中的博客文章標題文本設置動畫。 ContentThemeTransitionContentControl 一起使用,並且會在控件內容發生更改時自動觸發。

        <TextBlock x:Name="itemTitle" Text="{Binding Title}" 
                   Style="{StaticResource SubheaderTextStyle}">
            <TextBlock.Transitions>
                <TransitionCollection>
                    <ContentThemeTransition />
                </TransitionCollection>
            </TextBlock.Transitions>
        </TextBlock>

我們將ContentThemeTransition 添加到TransitionCollection 中,然後將後者設置爲TextBlock Transitions 屬性值。當 TextBlock 的內容發生更改時, ContentThemeTransition將自動觸發並運行。動畫是預定義的,無需我們執行任何操作即可運行。我們只需將其附加到TextBlock 中即可。

有關詳細信息以及主題動畫和過渡的完整列表,請參閱快速入門:動畫 

使用樣式創建一致性外觀

我們希望讓博客閱讀器應用的外觀和感覺類似於 Windows 團隊博客網站。我們希望用戶在該網站和我們的應用之間切換時能夠擁有無縫的使用體驗。Windows Metro 風格 UI 的默認黑色主題與 Windows 團隊博客網站不太匹配。這在詳細信息頁面上尤爲明顯,在該頁面上我們會將實際的博客頁面加載到一個WebView 中,如下所示:

採用深色主題的詳細信息頁面。

爲了給我們的應用提供一個一致的外觀以便在需要時進行更新,我們使用畫筆和樣式。 Brush 可以讓我們在一個位置定義一種外觀,然後將它用於任何需要的地方。 Style 可以讓我們設置控件的各屬性值,然後在整個應用中重用這些設置。

在深入瞭解詳細信息之前,我們先來看看如何使用畫筆在我們的應用頁面中設置背景色。應用的每個頁面都有一個設置了 Background 屬性的 Grid ,以定義頁面的背景色。我們可以單個設置每個頁面的背景,如下所示。

<Grid Background="Blue">

但有一種更好的方法是將一個 Brush 定義爲資源,並用它來定義我們所有頁面的背景色。我們在 Visual Studio 模板中就是這麼做的,如我們在此處看到的一樣。

<Grid Background="{StaticResource ApplicationPageBackgroundBrush}">

我們將對象和值定義爲資源,以使它們可重用。若要將對象或值用作資源,我們必須設置其 x:Key 屬性。我們使用此關鍵字來指代來自 XAML 的資源。此處,背景被設置爲具有鍵 ApplicationPageBackgroundBrush 的資源,該鍵是定義 SolidColorBrush 的系統。

若要更改頁面的背景,我們需要使用 App.xaml 中的鍵 WindowsBlogBackgroundBrush 定義一個新的 SolidColorBrush 。針對這一新畫筆,我們設置了 Color #FF0A2562,這是一種與 http://windowsteamblog.com 站點上的顏色很搭配的好看的藍色。

// Add this brush to the resource dictionary in App.xaml.
<SolidColorBrush x:Key="WindowsBlogBackgroundBrush" Color="#FF0A2562"/>


在應用的每個頁面中,我們使用此新畫筆更改背景。

// Change the Background of the root Grid in each xaml page in the app.
<Grid Background="{StaticResource WindowsBlogBackgroundBrush}">


你可以在以下位置定義資源:單個頁面的 XAML 文件中、App.xaml 文件中,或者單獨的資源詞典 XAML 文件中,例如 StandardStyles.xaml。定義資源時的位置確定了資源的使用範圍。Visual Studio 將 StandardStyles.xaml 文件創建爲項目模板的一部分,並將其放在 Common 文件夾中。它是一個資源詞典,包含 Visual Studio 頁面模板中所使用的值、樣式和數據模板。可以在應用之間共享資源詞典 XAML 文件,並且可以將多個資源詞典合併到單個應用中。

在我們的博客閱讀器應用中,我們在 App.xaml 中定義資源,從而使它們在整個應用中均可用。我們還在單個頁面的 XAML 文件中定義了一些資源。這些資源只在定義了它們的頁面中可用。如果在 App.xaml 和頁面中同時定義了具有相同關鍵字的資源,頁面中的資源將覆蓋 App.xaml 中的資源。同樣,在 App.xaml 中定義的資源將覆蓋在單獨的資源詞典文件中定義的具有相同關鍵字的資源。有關詳細信息,請參閱快速入門:設置控件樣式 

現在,讓我們來看一個在我們的應用中使用 Style 的示例。我們的博客閱讀器 UI 中的大部分文本都是相似的,只是大小不同而已。文本的默認外觀由 StandardStyles.xaml 中的該 Style 定義。

...
<SolidColorBrush x:Key="ApplicationTextBrush" Color="#DEFFFFFF"/>
...
<x:Double x:Key="ContentFontSize">14.667</x:Double>
<FontFamily x:Key="ContentFontFamily">Segoe UI</FontFamily>
...
<Style x:Key="BasicTextStyle" TargetType="TextBlock">
    <Setter Property="Foreground" Value="{StaticResource ApplicationTextBrush}"/>
    <Setter Property="FontSize" Value="{StaticResource ContentFontSize}"/>
    <Setter Property="FontFamily" Value="{StaticResource ContentFontFamily}"/>
    <Setter Property="TextTrimming" Value="WordEllipsis"/>
    <Setter Property="TextWrapping" Value="Wrap"/>
    <Setter Property="Typography.StylisticSet20" Value="True"/>
    <Setter Property="Typography.DiscretionaryLigatures" Value="True"/>
</Style>

在 Style 定義中,我們需要一個 TargetType 屬性和由一個或多個 Setter 組成的集合。我們將 TargetType 設置爲指定 Style 將應用到的類型,在此例中爲 TextBlock 。如果你試圖將某個Style應用到與TargetType屬性不匹配的控件,就會發生異常。每個Setter元素都需要一個Property 和一個 Value 。這些屬性設置用於指示該設置將應用於哪個控件屬性,以及爲該屬性設置的值。

在 ItemsPage.xaml 中,我們希望網格框中的文本具有默認的外觀,但我們希望 FontSize 更大一些。我們可以通過在本地進行設置來改寫 FontSize,如下所示:<TextBlock Text="{Binding Title}" Style="{StaticResource BasicTextStyle}" FontSize="26.667" />。但還有一種更好的方法:創建一個基於 BasicTextStyle 的新 Style ,並更改其中的 FontSize。此處介紹了我們如何爲網格標題文本定義新樣式。

<!-- This XAML gets added to the app with the other grid styles later. -->
<Style x:Key="GridTitleTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BasicTextStyle}">
    <Setter Property="FontSize" Value="26.667"/>
    <Setter Property="Margin" Value="12,0,12,2"/>
</Style>

BasedOn="{StaticResource BasicTextStyle}" 行指示新的 Style 從 BasicTextStyle 繼承我們沒有明確設置的所有屬性。我們的新 Style 與默認文本樣式一樣,只是大一些,我們可以將其用於所有網格標題,如下所示。

<!-- This XAML is an exerpt from the DefaultGridItemTemplate that we add later. -->
<TextBlock Text="{Binding Title}" Style="{StaticResource GridTitleTextStyle}" />

爲了讓我們的應用具有與 Windows 團隊博客網站相同的外觀和感覺,除了 Brush 和 Style 之外,我們還使用了自定義數據模板。我們在顯示數據部分討論了數據模板。下面顯示了我們爲自定義應用的外觀而添加的數據模板和樣式。

在 App.xaml 中,我們添加了定義用於顯示日期的方塊的 ControlTemplate 。我們在 App.xaml 中對它進行定義,以便在 ItemsPage.xaml 和 SplitPage.xaml 中都能使用。

    <Application.Resources>
        <ResourceDictionary>
                                                                    ...
            <ControlTemplate x:Key="DateBlockTemplate">
                <Canvas Height="86" Width="86"  Margin="8,8,0,8" HorizontalAlignment="Left" VerticalAlignment="Top">
                    <TextBlock TextTrimming="WordEllipsis" TextWrapping="NoWrap" 
                     Width="Auto" Height="Auto" Margin="8,0,4,0" FontSize="32" FontWeight="Bold">
                          <TextBlock.Text>
                              <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="month"  />
                          </TextBlock.Text>
                    </TextBlock>

                    <TextBlock TextTrimming="WordEllipsis" TextWrapping="Wrap" 
                     Width="40" Height="Auto" Margin="8,0,0,0" FontSize="34" FontWeight="Bold" Canvas.Top="36">
                        <TextBlock.Text>
                            <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="day"  />
                        </TextBlock.Text>
                    </TextBlock>
                    <Line Stroke="White" StrokeThickness="2" X1="54" Y1="46" X2="54" Y2="80" />

                    <TextBlock TextWrapping="Wrap" 
                     Width="20" Height="Auto" FontSize="{StaticResource ContentFontSize}" Canvas.Top="42" Canvas.Left="60">
                        <TextBlock.Text>
                            <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="year"  />
                        </TextBlock.Text>
                    </TextBlock>
                </Canvas>
            </ControlTemplate>
            ...
    </Application.Resources>

在 ItemsPage.xaml 中,我們添加了以下資源以定義默認視圖中的網格項的外觀。

<UserControl.Resources>

...
    <!-- light blue -->
    <SolidColorBrush x:Key="BlockBackgroundBrush" Color="#FF557EB9"/>
    
    <!-- Grid Styles -->
    <Style x:Key="GridTitleTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BasicTextStyle}">
        <Setter Property="FontSize" Value="26.667"/>
        <Setter Property="Margin" Value="12,0,12,2"/>
    </Style>

    <Style x:Key="GridDescriptionTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BasicTextStyle}">
        <Setter Property="VerticalAlignment" Value="Bottom"/>
        <Setter Property="Margin" Value="12,0,12,60"/>
    </Style>

    <DataTemplate x:Key="DefaultGridItemTemplate">
        <Grid HorizontalAlignment="Left" Width="250" Height="250">
            <Border Background="{StaticResource BlockBackgroundBrush}" />
            <TextBlock Text="{Binding Title}" Style="{StaticResource GridTitleTextStyle}"/>
            <TextBlock Text="{Binding Description}" Style="{StaticResource GridDescriptionTextStyle}" />
            <StackPanel VerticalAlignment="Bottom" Orientation="Horizontal"
                        Background="{StaticResource ListViewItemOverlayBackgroundBrush}">
                <TextBlock Text="Last Updated" Margin="12,4,0,8" Height="42"/>
                <TextBlock Text="{Binding PubDate, Converter={StaticResource dateConverter}}" Margin="12,4,12,8" />
            </StackPanel>
        </Grid>
    </DataTemplate>
</UserControl.Resources>

在 ItemsPage.xaml 中,還必須更新 itemGridView 的 ItemTemplate 屬性,以使用我們的 DefaultGridItemTemplate 資源而不是 Standard250x250ItemTemplate,後者是 StandardStyles.xaml 中定義的默認模板。此處顯示的是更新後的 itemGridView 的 XAML。

            <GridView
                x:Name="itemGridView"
                AutomationProperties.AutomationId="ItemsGridView"
                AutomationProperties.Name="Items"
                Margin="116,0,116,46"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"

                ItemTemplate="{StaticResource DefaultGridItemTemplate}"

                SelectionMode="None"
                IsItemClickEnabled="True"
                ItemClick="ItemView_ItemClick"/>

在 SplitPage.xaml 中,我們添加了以下資源來定義默認視圖中列表項的外觀。

<UserControl.Resources>
                                ...
    <!-- green -->
    <SolidColorBrush x:Key="BlockBackgroundBrush" Color="#FF6BBD46"/>

    <DataTemplate x:Key="DefaultListItemTemplate">
        <Grid HorizontalAlignment="Stretch" Width="Auto" Height="110" Margin="10,10,10,0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <!-- Green date block -->
            <Border Background="{StaticResource BlockBackgroundBrush}" Width="110" Height="110" />
            <ContentControl Template="{StaticResource DateBlockTemplate}" />
            <StackPanel Grid.Column="1"  HorizontalAlignment="Left" Margin="12,8,0,0">
                <TextBlock Text="{Binding Title}" FontSize="26.667" TextWrapping="Wrap"
                           MaxHeight="72" Foreground="#FFFE5815" />
                <TextBlock Text="{Binding Author}" FontSize="18.667" />
            </StackPanel>
        </Grid>
    </DataTemplate>
    ...
</UserControl.Resources>

在 SplitPage.xaml 中,我們還更新了 itemListView 中的 ItemTemplate 屬性,以使用我們的 DefaultListItemTemplate 資源而不是使用默認模板 Standard130ItemTemplate。此處顯示的是更新後的 itemListView 的 XAML。

            <ListView
                x:Name="itemListView"
                AutomationProperties.AutomationId="ItemsListView"
                AutomationProperties.Name="Items"
                Margin="120,0,0,60"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                SelectionChanged="ItemListView_SelectionChanged"

                ItemTemplate="{StaticResource DefaultListItemTemplate}"/>

在應用了我們的樣式後,該應用就非常符合 Windows 團隊博客網站的外觀和感覺了:

設置樣式後的集合頁面。
設置樣式後的拆分頁面。
設置樣式後的詳細信息頁面。

通過使用樣式和在其他樣式基礎上新建樣式,我們可以爲自己的應用快速定義和應用各種外觀。在接下來的部分中,我們將結合使用所學到的動畫和樣式知識,使我們的應用在運行過程中能夠動態地適應各種佈局和方向。

適應不同的佈局

通常,應用會設計爲以全屏幕方式橫向查看。但 Metro 風格 UI 必須能夠適應各種不同的方向和佈局。具體來說,它必須對縱向和橫向都支持。在橫向方位中,它必須支持“全屏”、“填充”和“鋪屏”佈局。我們發現,當我們從一個空白模板創建博客閱讀器頁面時,頁面縱向看起來不太好看。在本部分中,我們來看看怎樣讓我們的應用無論以什麼分辨率或無論從哪個方向看起來效果都不錯。

Visual Studio 模板包括用於處理對視圖狀態所做的變更的代碼。這些代碼位於 LayoutAwarePage.cs/vb 文件中,將我們的應用狀態與 XAML 中定義的視覺狀態相對應。由於已經提供了頁面佈局邏輯,因此我們只需要提供要針對每個頁面的視覺狀態使用的視圖。

要使用 XAML 在不同的視圖之間進行轉換,我們需要使用 VisualStateManger 來爲應用定義不同的 VisualState 。此處,我們在 ItemsPage.xaml 中定義了一個 VisualStateGroup 。該組有 4 個 VisualState,分別名爲:FullScreenLandscapeFilledFullScreenPortrait 和 Snapped。不能同時使用來自同一個VisualStateGroup的不同VisualState 。 每個 VisualState 都使用動畫來指示應用需要在爲 UI 的 XAML 中指定的基準項中進行哪些更改。

<!--App Orientation States-->
  <VisualStateManager.VisualStateGroups>
     <VisualStateGroup>
        <VisualState x:Name="FullScreenLandscape" />
        <VisualState x:Name="Filled"> ... </VisualState>
        <VisualState x:Name="FullScreenPortrait"> ... </VisualState>
        <VisualState x:Name="Snapped"> ... </VisualState>
    </VisualStateGroup>
 </VisualStateManager.VisualStateGroups>

當應用處於全屏橫向模式時,我們使用 FullScreenLandscape 狀態。因爲我們正是針對此視圖設計了默認的 UI,所以無需進行任何更改,這只是一個空的VisualState 

當用戶有另外一個鋪排到屏幕一側的應用時,我們使用 Filled 主題。在本例中,項目視圖頁面只需移到上面,而無需進行任何更改。這也只是一個空的VisualState 

當應用從橫向旋轉爲縱向時,使用 FullScreenPortrait 狀態。在該視覺狀態中,我們有兩個動畫。一個用於更改返回按鈕所用的樣式,另一個用於更改 itemGridView 的頁邊距,以便所有內容顯示都更好地與屏幕相吻合。

<!-- The entire page respects the narrower 100-pixel margin convention for portrait -->
<VisualState x:Name="FullScreenPortrait">
    <Storyboard>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="backButton" 
                                       Storyboard.TargetProperty="Style">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PortraitBackButtonStyle}"/>
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemGridView" 
                                       Storyboard.TargetProperty="Margin">
            <DiscreteObjectKeyFrame KeyTime="0" Value="96,0,86,56"/>
        </ObjectAnimationUsingKeyFrames>
    </Storyboard>
</VisualState>


當用戶顯示兩個應用,而我們的應用是其中較窄的一個時,使用 Snapped 狀態。在這種狀態下,我們的應用只有 320 DIP 的寬度,因而需要對佈局進行更大的變更。在項頁面 UI 的 XAML 中,定義了一個 GridView 和一個 ListView 並將其綁定到數據集合。默認情況下,會顯示 itemGridViewScroller,而 itemListViewScroller 處於摺疊狀態。在 Snapped 狀態中,包含 4 個動畫,用於摺疊 itemListViewScroller、顯示 itemListViewScroller 和更改“後退”按鈕和頁面標題的 Style 以使其更小。

<!--
    The back button and title have different styles when snapped, and the list representation is substituted
    for the grid displayed in all other view states
-->
<VisualState x:Name="Snapped">
    <Storyboard>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="backButton" 
                                       Storyboard.TargetProperty="Style">
            <DiscreteObjectKeyFrame KeyTime="0" 
                                    Value="{StaticResource SnappedBackButtonStyle}"/>
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="pageTitle" 
                                       Storyboard.TargetProperty="Style">
            <DiscreteObjectKeyFrame KeyTime="0" 
                                    Value="{StaticResource SnappedPageHeaderTextStyle}"/>
        </ObjectAnimationUsingKeyFrames>

        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListScrollViewer" 
                                       Storyboard.TargetProperty="Visibility">
            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemGridScrollViewer"
                                       Storyboard.TargetProperty="Visibility">
            <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
        </ObjectAnimationUsingKeyFrames>
    </Storyboard>
</VisualState>

在本教程的使用樣式創建一致性外觀部分中,我們創建了樣式和模板來使我們的應用具有自定義的外觀。默認橫向視圖使用這些樣式和模板。爲了使客戶能以不同的視圖進行查看,我們還需要爲這些視圖創建自定義的樣式和模板。

在 ItemsPage.xaml 中,我們爲網格項創建了新的數據模板。我們還需要爲以 Snapped 視圖顯示的列表項提供新的數據模板。我們將此模板命名爲 NarrowListItemTemplate,並將其添加到 ItemsPage.xaml 的資源部分,緊隨 DefaultGridItemTemplate 資源之後。

<UserControl.Resources>
...
    <!-- Used in Snapped view -->
    <DataTemplate x:Key="NarrowListItemTemplate">
        <Grid Height="80">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Border Background="{StaticResource BlockBackgroundBrush}" Width="80" Height="80" />
            <ContentControl Template="{StaticResource DateBlockTemplate}" Margin="-12,-12,0,0"/>
            <StackPanel Grid.Column="1" HorizontalAlignment="Left" Margin="12,8,0,0">
                <TextBlock Text="{Binding Title}" MaxHeight="56" TextWrapping="Wrap"/>
            </StackPanel>
        </Grid>
    </DataTemplate>
</UserControl.Resources>

爲了讓 ListView 顯示我們的新數據模板,我們需要更新 itemListView 的 ItemTemplate 屬性,以使用我們的 NarrowListItemTemplate 資源而不是 Standard80ItemTemplate,後者是 StandardStyles.xaml 中定義的默認視圖。此處顯示的是更新後的 itemListView 的 XAML。

            <ListView
                x:Name="itemListView"
                AutomationProperties.AutomationId="ItemsListView"
                AutomationProperties.Name="Items"
                Margin="10,0,0,60"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"

                ItemTemplate="{StaticResource NarrowListItemTemplate}"

                SelectionMode="None"
                IsItemClickEnabled="True"
                ItemClick="ItemView_ItemClick"/>

在 SplitPage.xaml 中,我們創建了一個與在下列視圖中所用的模板相似的 ListView 模板:Filled 和 Snapped 視圖,以及當屏幕寬度小於 1366 DIP 時的 FullScreenLandscape 視圖。我們也將此模板命名爲 NarrowListItemTemplate,並將其添加到 SplitPage.xaml 資源部分,緊隨 DefaultListItemTemplate 資源之後。

<UserControl.Resources>
...
    <!-- Used in Filled and Snapped views -->
    <DataTemplate x:Key="NarrowListItemTemplate">
        <Grid Height="80">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Border Background="{StaticResource BlockBackgroundBrush}" Width="80" Height="80"/>
            <ContentControl Template="{StaticResource DateBlockTemplate}" Margin="-12,-12,0,0"/>
            <StackPanel Grid.Column="1" HorizontalAlignment="Left" Margin="12,8,0,0">
                <TextBlock Text="{Binding Title}" MaxHeight="56" Foreground="#FFFE5815" TextWrapping="Wrap"/>
                <TextBlock Text="{Binding Author}" FontSize="12" />
            </StackPanel>
        </Grid>
    </DataTemplate>
...
</UserControl.Resources>

要使用此數據模板,我們需要更新使用該模板的視覺狀態。在 Snapped 和 Filled 視覺狀態的 XAML 中,我們發現了針對 itemListView 的 ItemTemplate 屬性的動畫。接着,我們更改了該值,以使用 NarrowListItemTemplate 資源而不使用默認的 Standard80ItemTemplate 資源。此處顯示的是更新後的動畫 XAML。

<VisualState x:Name="Filled">
    <Storyboard>
    ....
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" Storyboard.TargetProperty="ItemTemplate">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource NarrowListItemTemplate}"/>
        </ObjectAnimationUsingKeyFrames>
    ....
    </Storyboard>
</VisualState>
...
<VisualState x:Name="Snapped">
    <Storyboard>
    ....
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" Storyboard.TargetProperty="ItemTemplate">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource NarrowListItemTemplate}"/>
        </ObjectAnimationUsingKeyFrames>
    ....
    </Storyboard>
</VisualState>

我們還將拆分頁面的項目詳細信息部分替換爲我們自己的、使用 WebView 的詳細信息部分。由於我們進行了此更改,因此 Snapped_Detail 視覺狀態目標元素中的某些動畫將不再存在。這些動畫在我們使用此視覺狀態時會導致錯誤,因此我們需要將其刪除。在 SplitPage.xaml 中,我們從 Snapped_Detail 視覺狀態中刪除了這些動畫。

<VisualState x:Name="Snapped_Detail">
    <Storyboard>
    ...
        <!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemDetailTitlePanel" Storyboard.TargetProperty="(Grid.Row)">
                <DiscreteObjectKeyFrame KeyTime="0" Value="0"/>
            </ObjectAnimationUsingKeyFrames>
            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemDetailTitlePanel" Storyboard.TargetProperty="(Grid.Column)">
                <DiscreteObjectKeyFrame KeyTime="0" Value="0"/>
            </ObjectAnimationUsingKeyFrames>-->
...
        <!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemSubtitle" Storyboard.TargetProperty="Style">
                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource CaptionTextStyle}"/>
            </ObjectAnimationUsingKeyFrames>-->
    </Storyboard>
</VisualState>

在 DetailPage.xaml 中,我們只需要在 Snapped 視圖中調整我們的 WebView 頁邊距,即可使用所有可用空間。在 Snapped 視覺狀態的 XAML 中,我們添加了一個動畫來更改 contentViewBorder 上的 Margin 屬性,如下所示。

<VisualState x:Name="Snapped">
    <Storyboard>
    ...
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="contentViewBorder" Storyboard.TargetProperty="Margin">
            <DiscreteObjectKeyFrame KeyTime="0" Value="20,5,20,20"/>
        </ObjectAnimationUsingKeyFrames>

    </Storyboard>
</VisualState>

添加初始屏幕和徽標

我們的應用帶給用戶的第一印象來自於徽標和初始屏幕。徽標顯示在 Windows 應用商店和“開始”屏幕上。初始屏幕在用戶啓動應用時立即顯示,並在應用初始化其資源時爲用戶提供即時反饋。當應用的第一個頁面準備就緒可以顯示時,它就會關閉。

初始屏幕由一種背景色和一個 620 x 300 像素的圖像組成。我們在 Package.appxmanifest 文件中設置這些值。雙擊此文件在清單編輯器中打開它。在清單編輯器的“應用程序 UI”選項卡中,我們設置了初始屏幕圖像的路徑和背景色。項目模板提供了一個名爲 SplashScreen.png 的默認空白圖像。我們將該圖像替換爲我們自己的初始屏幕圖像,以清晰地標識我們的應用並立即將用戶吸引到我們的應用中來。我們還可以用自己的徽標文件替換模板徽標文件來指定徽標。以下是我們的博客閱讀器的初始屏幕。

初始屏幕圖像。

基本初始屏幕可以適用於我們的博客閱讀器,但你也可以使用 SplashScreen 類的屬性和方法擴展該初始屏幕。你可以使用 SplashScreen 類獲取初始屏幕的座標,然後利用這些座標定位該應用的第一個頁面。還可以掌握初始屏幕消失的時間,以確定啓動應用的任何內容進入動畫的時機。

我們學習瞭如何使用 Visual Studio 11 Express Beta for Windows 8 中的內置頁面模板構建完整的多頁面應用,還學習瞭如何在頁面之間導航和傳遞數據。我們學習瞭如何使用樣式和模板以使我們的應用符合 Windows 團隊博客網站的風格。我們還學習瞭如何使用主題動畫、應用欄和初始屏幕來使應用更適合 Windows 8 的個性化內容。 最後,我們學習瞭如何根據各種佈局和方向來調整我們的應用,從而讓它始終保持美觀。

現在,我們基本上可以將我們的應用提交到 Windows 應用商店了。下面提供了一些鏈接,以便你瞭解將應用提交到 Windows 應用商店需要執行的後續步驟的詳細信息:

使用 C# 或 Visual Basic 的 Metro 風格應用的路線圖
使用 Visual Studio 11 開發 Metro 風格應用
發佈了51 篇原創文章 · 獲贊 17 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章