建立部落格閱讀程式通用 Windows 平台 App (C++)
以下說明如何使用 C++ 與 XAML 來開發您可以部署到 Windows 10 之通用 Windows 平台 (UWP) app 的完整程序。App 會從 RSS 2.0 或 Atom 1.0 摘要讀取部落格。
本教學課程假設您已熟悉使用 C++ 建立第一個 Windows 市集 app 中的概念。
如果要研究這個 app 完成後的版本,您可以從 MSDN 程式庫網站下載該 app。
在本教學課程中,我們將使用 Visual Studio Community 2015 或更新版本。如果您正在使用另一個版本的 Visual Studio,功能表命令可能會稍微不同。
如需其他程式設計語言的教學課程,請參閱:
建立 "Hello World" app (C#/VB)
目標
本教學課程旨在協助您了解如何建立多頁面的 Windows 市集應用程式,以及使用 Visual C++ 元件延伸 (C++/CX) 來簡化 Windows 執行階段編碼工作的方法與時機。本教學課程也會教您如何使用 concurrency::task
類別,來使用非同步的 Windows 執行階段 API。
SimpleBlogReader app 具有下列功能:
- 透過網際網路存取 RSS 和 Atom 摘要資料。
- 顯示摘要與摘要標題的清單。
- 提供兩種文章閱讀方式:簡單文字或網頁。
- 支援處理程序生命週期管理 (PLM),且如果系統在另一項工作於前景執行時將它關閉,它可以正確地儲存並重新載入其狀態。
- 可適應不同的視窗大小與裝置方向 (橫向或直向)。
- 讓使用者可以新增及移除摘要。
第一部分:設定專案
一開始,讓我們先使用 C++ 空白 App (通用 Windows) 範本來建立一個專案。
建立新的專案
- 在 Visual Studio 中,選擇 [檔案] > [新增]**** > [專案],選取 [已安裝]**** > [Visual C++] > [Windows] > [通用]****。在中間窗格進行選擇,然後選取 [空白應用程式 (通用 Windows)] 範本。將方案命名為 "SimpleBlogReader"。如需詳細的完整指示,請參閱建立 "Hello, world" app (C++)。
讓我們從新增所有頁面開始。一次建立所有頁面是比較容易的方式,因為當我們開始撰寫每個頁面的程式碼時,我們必須包括 (#include) 它將會瀏覽到的頁面。
新增 Windows app 頁面
- 事實上,我們是以摧毀作為開始。在 MainPage.xaml 上按一下滑鼠右鍵,接著選擇 [移除],然後按一下 [刪除]**** 來永久刪除檔案與其程式碼後置檔案。這是一個空白頁面類型,其中缺少我們需要的瀏覽支援。現在,在專案節點上按一下滑鼠右鍵並選擇 [新增] > [新項目]。
- 在左窗格中選擇 XAML,然後在中間窗格中選擇 [項目頁面]。將它命名為 MainPage.xaml,然後按一下 [確定]。您將會看到一個訊息方塊,詢問您是否要新增一些新檔案到專案。按一下 [是]。在啟始程式碼中,我們需要參照那些檔案中定義的 SuspensionManager 和 NavigationHelper 類別,Visual Studio 將它們放在一個新的 Common 資料夾中。
- 新增一個 SplitPage 並接受預設名稱。
- 新增一個 BasicPage 並將它命名為 WebViewerPage。
我們稍候將會新增使用者介面元素到那些頁面。
新增 Phone 應用程式頁面
- 在 [方案總管] 中展開 Windows Phone 8.1 專案。在 MainPage.xaml 上按一下滑鼠右鍵,選擇 [移除] > [永久刪除]。
- 新增一個新的 XAML [基本頁面]**** 並將它命名為 MainPage.xaml。按一下 [是],就像您為 Windows 專案所做的一樣。
- 您可能會注意到在 Phone 專案中,各種頁面範本會受到較多限制。我們在此應用程式中僅使用基本頁面。請再多新增三個基本頁面,並將它們命名為 FeedPage、TextViewerPage 及 WebViewerPage。
第二部分:建立資料模型
以 Visual Studio 範本為基礎的市集應用程式通常包含 MVVM 架構。在我們的應用程式中,模型是由封裝部落格摘要的類別所組成。應用程式中的每個 XAML 頁面都代表該資料的特定檢視,而且每個頁面類別都有自己的檢視模型 (稱為 DefaultViewModel 的屬性,並且是 Map<String^,Object^> 類型)。此對應會儲存頁面上的 XAML 控制項所繫結的資料,並且它會作為頁面的資料內容來使用。
我們的模型是由三種類別組成。FeedData 類別代表部落格摘要的最上層 URI 和中繼資料。位於 https://blogs.windows.com/windows/b/buildingapps/rss.aspx 的摘要是 FeedData 所封裝之內容的範例。摘要會有一份部落格文章清單,我們以 FeedItem 物件來代表。每個 FeedItem 都代表一篇文章,並包含標題、內容、URI 及其他中繼資料。位於 https://blogs.windows.com/windows/b/buildingapps/archive/2014/05/28/using-the-windows-phone-emulator-for-testing-apps-with-geofencing.aspx 的文章是 FeedItem 的範例。在我們應用程式中的第一個頁面是 Feed 的檢視,第二個頁面是單一摘要之 FeedItem 的檢視,最後兩個頁面則提供單一文章的不同檢視:純文字檢視或網頁檢視。
FeedDataSource 類別包含 FeedData 項目的集合,以及下載它們的方法。
重述重點:
FeedData 包含 RSS 或 Atom 摘要的相關資訊。
FeedItem 包含摘要中與個別部落格文章有關的資訊。
FeedDataSource 包含下載摘要及初始化資料類別的方法。
我們將這些類別定義為公用 ref 類別,以提供資料繫結功能。XAML 控制項無法與標準 C++ 類別互動。我們使用 Bindable 屬性,來向 XAML 編譯器表示我們正動態繫結到這些類型的執行個體。在公用 ref 類別中,會以屬性的形式顯示公開資料成員。沒有特殊邏輯的屬性不需要使用者指定的 getter 和 setter - 編譯器會提供。 在 FeedData 類別中,請注意使用 Windows::Foundation::Collections::IVector 來顯示公用集合類型的方式。我們會在內部使用 Platform::Collections::Vector 類別做為實作 IVector 的具象類型。
Windows 與 Windows Phone 專案將使用相同的資料模型,因此我們將會把類別放在共用專案中。
建立自訂資料類別
在 [方案總管] 中,於 [SimpleBlogReader.Shared]**** 專案節點的捷徑功能表上選擇 [加入] > [新項目]****。選取 [標頭檔 (.h)] 選項,將它命名為 FeedData.h。
開啟 FeedData.h,然後貼入下列程式碼。請注意 "pch.h" 的 #include 指示詞,這是我們先行編譯的標頭,而且它是沒有太大變更或完全沒有變更之系統標頭的放置位置。根據預設,pch.h 包括 collection.h (這對 Platform::Collections::Vector 類型而言是必要的) 和 ppltasks.h (這對 concurrency::task 和相關類型而言是必要的)。這些標頭包含我們的應用程式所需的 <字串> 和 <向量>,所以我們不需要明確包含它們。
//feeddata.h #pragma once #include "pch.h" namespace SimpleBlogReader { namespace WFC = Windows::Foundation::Collections; namespace WF = Windows::Foundation; namespace WUIXD = Windows::UI::Xaml::Documents; namespace WWS = Windows::Web::Syndication; /// <summary> /// To be bindable, a class must be defined within a namespace /// and a bindable attribute needs to be applied. /// A FeedItem represents a single blog post. /// </summary> [Windows::UI::Xaml::Data::Bindable] public ref class FeedItem sealed { public: property Platform::String^ Title; property Platform::String^ Author; property Platform::String^ Content; property Windows::Foundation::DateTime PubDate; property Windows::Foundation::Uri^ Link; private: ~FeedItem(void){} }; /// <summary> /// A FeedData object represents a feed that contains /// one or more FeedItems. /// </summary> [Windows::UI::Xaml::Data::Bindable] public ref class FeedData sealed { public: FeedData(void) { m_items = ref new Platform::Collections::Vector<FeedItem^>(); } // The public members must be Windows Runtime types so that // the XAML controls can bind to them from a separate .winmd. property Platform::String^ Title; property WFC::IVector<FeedItem^>^ Items { WFC::IVector<FeedItem^>^ get() { return m_items; } } property Platform::String^ Description; property Windows::Foundation::DateTime PubDate; property Platform::String^ Uri; private: ~FeedData(void){} Platform::Collections::Vector<FeedItem^>^ m_items; }; }
類別是 ref 類別,因為 Windows 執行階段 XAML 類別必須與它們互動以執行與使用者介面的資料繫結。那些類別上的 [Bindable] 屬性也是資料繫結時的必要屬性。在沒有該屬性的情況下,繫結機制將不會看到它們。
第三部分:下載資料
FeedDataSource 類別包含下載摘要的方法,而且也有一些其他的協助程式方法。它也包含已下載之摘要的集合,這些摘要會新增至主應用程式頁面之 DefaultViewModel 中的 "Items" 值。FeedDataSource 會使用 Windows::Web::Syndication::SyndicationClient 類別來執行下載作業。因為網路作業可能需要一些時間,所以這些作業都是非同步的。當摘要下載完成時,FeedData 物件會初始化並新增到 FeedDataSource::Feeds 集合。這是一個 IObservable<T>,表示在新增項目時,UI 將會獲得通知,且將會在主頁面中顯示項目。對於非同步作業,我們是使用 concurrency:: task 類別與相關類別及來自 ppltasks.h 的方法。create_task 函式是用來包裝 Windows API 中的 IAsyncOperation 和 IAsyncAction 函式呼叫。task:: then 成員函式是用來執行必須等待工作完成之後才能執行的程式碼。
應用程式的一個好用功能是使用者不需要等待所有摘要下載完成。他們可以在摘要顯示時立即按一下摘要,然後移到顯示該摘要之所有項目的新頁面。這是一個「快速且流暢」的使用者介面的範例,可以透過在背景執行緒上執行大量工作來實現此介面。我們將會在我們新增主要的 XAML 頁面之後看到它開始運作。
但是,非同步作業確實會增加複雜度,「快速且流暢」是要付出代價的。如果您已經閱讀先前的教學課程,您就會知道目前不在使用中的應用程式可能是已被系統終止以釋出記憶體,然後會在使用者切換回該應用程式時恢復運作。在我們的應用程式中,我們不會在關機時儲存所有摘要資料,因為那會佔用大量儲存空間,並且最後可能變成儲存了大量失效的資料。我們一律在啟動時下載摘要。這表示我們必須考量應用程式從終止狀態恢復執行並立即嘗試顯示尚未下載完成之 FeedData 物件的情況。我們需要確保我們不會嘗試在資料可用之前顯示資料。在這個案例中,我們不能使用 then 方法,但我們可以使用 task_completed_event。這個事件將會防止任何程式碼在 FeedData 物件完成載入之前嘗試存取該物件。
將 FeedDataSource 類別新增至 FeedData.h,以做為命名空間 SimpleBlogReader 的一部分:
/// <summary> /// A FeedDataSource represents a collection of FeedData objects /// and provides the methods to retrieve the stores URLs and download /// the source data from which FeedData and FeedItem objects are constructed. /// This class is instantiated at startup by this declaration in the /// ResourceDictionary in app.xaml: <local:FeedDataSource x:Key="feedDataSource" /> /// </summary> [Windows::UI::Xaml::Data::Bindable] public ref class FeedDataSource sealed { private: Platform::Collections::Vector<FeedData^>^ m_feeds; FeedData^ GetFeedData(Platform::String^ feedUri, WWS::SyndicationFeed^ feed); concurrency::task<WFC::IVector<Platform::String^>^> GetUserURLsAsync(); void DeleteBadFeedHandler(Windows::UI::Popups::UICommand^ command); public: FeedDataSource(); property Windows::Foundation::Collections::IObservableVector<FeedData^>^ Feeds { Windows::Foundation::Collections::IObservableVector<FeedData^>^ get() { return this->m_feeds; } } property Platform::String^ CurrentFeedUri; void InitDataSource(); internal: // This is used to prevent SplitPage from prematurely loading the last viewed page on resume. concurrency::task_completion_event<FeedData^> m_LastViewedFeedEvent; concurrency::task<void> RetrieveFeedAndInitData(Platform::String^ url, WWS::SyndicationClient^ client); };
現在要在共用專案中建立一個名為 FeedData.cpp 的檔案,並貼到此程式碼中:
#include "pch.h" #include "FeedData.h" using namespace std; using namespace concurrency; using namespace SimpleBlogReader; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::Web::Syndication; using namespace Windows::Storage; using namespace Windows::Storage::Streams; FeedDataSource::FeedDataSource() { m_feeds = ref new Vector<FeedData^>(); CurrentFeedUri = ""; } ///<summary> /// Uses SyndicationClient to get the top-level feed object, then initializes /// the app's data structures. In the case of a bad feed URL, the exception is /// caught and the user can permanently delete the feed. ///</summary> task<void> FeedDataSource::RetrieveFeedAndInitData(String^ url, SyndicationClient^ client) { // Create the async operation. feedOp is an // IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^ auto feedUri = ref new Uri(url); auto feedOp = client->RetrieveFeedAsync(feedUri); // Create the task object and pass it the async operation. // SyndicationFeed^ is the type of the return value that the feedOp // operation will pass to the continuation. The continuation can run // on any thread. return create_task(feedOp).then([this, url](SyndicationFeed^ feed) -> FeedData^ { return GetFeedData(url, feed); }, concurrency::task_continuation_context::use_arbitrary()) // Append the initialized FeedData object to the items collection. // This has to happen on the UI thread. By default, a .then // continuation runs in the same apartment that it was called on. // We can append safely to the Vector from multiple threads // without taking an explicit lock. .then([this, url](FeedData^ fd) { if (fd->Uri == CurrentFeedUri) { // By setting the event we tell the resuming SplitPage the data // is ready to be consumed. m_LastViewedFeedEvent.set(fd); } m_feeds->Append(fd); }) // The last continuation serves as an error handler. // get() will surface any unhandled exceptions in this task chain. .then([this, url](task<void> t) { try { t.get(); } catch (Platform::Exception^ e) { // Sometimes a feed URL changes(I'm talking to you, Windows blogs!) // When that happens, or when the users pastes in an invalid URL or a // URL is valid but the content is malformed somehow, an exception is // thrown in the task chain before the feed is added to the Feeds // collection. The only recourse is to stop trying to read the feed. // That means deleting it from the feeds.txt file in local settings. SyndicationErrorStatus status = SyndicationError::GetStatus(e->HResult); String^ msgString; // Define the action that will occur when the user presses the popup button. auto handler = ref new Windows::UI::Popups::UICommandInvokedHandler( [this, url](Windows::UI::Popups::IUICommand^ command) { auto app = safe_cast<App^>(App::Current); app->DeleteUrlFromFeedFile(url); }); // Display a message that hopefully is helpful. if (status == SyndicationErrorStatus::InvalidXml) { msgString = "There seems to be a problem with the formatting in this feed: "; } if (status == SyndicationErrorStatus::Unknown) { msgString = "I can't load this feed (is the URL correct?): "; } // Show the popup. auto msg = ref new Windows::UI::Popups::MessageDialog( msgString + url); auto cmd = ref new Windows::UI::Popups::UICommand( ref new String(L"Forget this feed."), handler, 1); msg->Commands->Append(cmd); msg->ShowAsync(); } }); //end task chain } ///<summary> /// Retrieve the data for each atom or rss feed and put it into our custom data structures. ///</summary> void FeedDataSource::InitDataSource() { // Hard code some feeds for now. Later in the tutorial we'll improve this. auto urls = ref new Vector<String^>(); urls->Append(L"http://sxp.microsoft.com/feeds/3.0/devblogs"); urls->Append(L"https://blogs.windows.com/windows/b/bloggingwindows/rss.aspx"); urls->Append(L"https://azure.microsoft.com/blog/feed"); // Populate the list of feeds. SyndicationClient^ client = ref new SyndicationClient(); for (auto url : urls) { RetrieveFeedAndInitData(url, client); } } ///<summary> /// Creates our app-specific representation of a FeedData. ///</summary> FeedData^ FeedDataSource::GetFeedData(String^ feedUri, SyndicationFeed^ feed) { FeedData^ feedData = ref new FeedData(); // Store the Uri now in order to map completion_events // when resuming from termination. feedData->Uri = feedUri; // Get the title of the feed (not the individual posts). // auto app = safe_cast<App^>(App::Current); TextHelper^ helper = ref new TextHelper(); feedData->Title = helper->UnescapeText(feed->Title->Text); if (feed->Subtitle != nullptr) { feedData->Description = helper->UnescapeText(feed->Subtitle->Text); } // Occasionally a feed might have no posts, so we guard against that here. if (feed->Items->Size > 0) { // Use the date of the latest post as the last updated date. feedData->PubDate = feed->Items->GetAt(0)->PublishedDate; for (auto item : feed->Items) { FeedItem^ feedItem; feedItem = ref new FeedItem(); feedItem->Title = helper->UnescapeText(item->Title->Text); feedItem->PubDate = item->PublishedDate; //Only get first author in case of multiple entries. item->Authors->Size > 0 ? feedItem->Author = item->Authors->GetAt(0)->Name : feedItem->Author = L""; if (feed->SourceFormat == SyndicationFormat::Atom10) { // Sometimes a post has only the link to the web page if (item->Content != nullptr) { feedItem->Content = helper->UnescapeText(item->Content->Text); } feedItem->Link = ref new Uri(item->Id); } else { feedItem->Content = item->Summary->Text; feedItem->Link = item->Links->GetAt(0)->Uri; } feedData->Items->Append(feedItem); }; } else { feedData->Description = "NO ITEMS AVAILABLE." + feedData->Description; } return feedData; } //end GetFeedData
現在讓我們在 app 中加入 FeedDataSource 執行個體。在 app.xaml.h 中,新增 feedData.h 的 #include 指示詞來顯示類型。
#include "FeedData.h"
在共用專案中,於 App.xaml 中新增一個 Application.Resources 節點,並在其中放入對 FeedDataSource 的參照,現在頁面看起來就像這樣:
<Application x:Class="SimpleBlogReader.App" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:SimpleBlogReader"> <Application.Resources> <local:FeedDataSource x:Key="feedDataSource" /> </Application.Resources> </Application>
這個標記將會在應用程式啟動時促使建立 FeedDataSource 物件,且可以從應用程式中的任何頁面存取該物件。當 OnLaunched 事件被引發時,應用程式物件將會呼叫 InitDataSource 來讓 feedDataSource 執行個體開始下載其所有資料。
此時還不會建置專案,因為我們需要新增一些額外的類別定義。
第四部分:從終止狀態恢復繼續處理資料同步處理
當應用程式第一次啟動時,以及當使用者在頁面之間來回瀏覽時,不需要執行資料存取同步處理。摘要只有在它們初始化之後才會在第一個頁面中顯示,並且在使用者按一下顯示的摘要之前,其他頁面都一律不會嘗試存取資料。而且在那之後,所有存取都會是唯讀的。我們一律不會修改我們的來源資料。但是,有一個情況需要同步處理:如果在一個以特定摘要為基礎的頁面在使用中時終止應用程式,該頁面將必須在應用程式恢復執行時重新繫結至該摘要資料。在這個情況下,頁面可能會嘗試存取尚未存在的資料。因此,我們需要一種方式來強制頁面等待資料準備就緒。
下列函式可讓應用程式記住它之前正在查看的摘要。SetCurrentFeed 方法會繼續將摘要保存到本機設定中,即使在應用程式移出記憶體之後它也可以從中抓取摘要。GetCurrentFeedAsync 方法是一個很有趣的方法,因為我們必須確保當我們回來並且想要重新載入上次的摘要時,我們不會在該摘要重新載入之前嘗試這樣做。我們將在稍候更詳細地討論這個程式碼。我們將程式碼新增到 App 類別,因為我們將會從 Windows app 和 Phone app 呼叫它。
在 app.xaml.h 中新增這些方法簽章。內部協助工具表示它們只能從相同命名空間中的其他 C++ 程式碼取用。
internal: concurrency::task<FeedData^> GetCurrentFeedAsync(); void SetCurrentFeed(FeedData^ feed); FeedItem^ GetFeedItem(FeedData^ fd, Platform::String^ uri); void AddFeed(Platform::String^ feedUri); void RemoveFeeds(Platform::Collections::Vector<FeedData^>^ feedsToDelete); void DeleteUrlFromFeedFile(Platform::String^ s);
接著,在 app.xaml.cpp 中新增下列 using 陳述式:
using namespace concurrency; using namespace Platform::Collections; using namespace Windows::Storage;
您需要適用於工作的並行命名空間、適用於向量的 Platform::Collections 命名空間,以及適用於 ApplicationData 的 Windows::Storage 命名空間。
然後將這幾行新增到底部:
///<summary> /// Grabs the URI that the user entered, then inserts it into the in-memory list /// and retrieves the data. Then adds the new feed to the data file so it's /// there the next time the app starts up. ///</summary> void App::AddFeed(String^ feedUri) { auto feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); auto client = ref new Windows::Web::Syndication::SyndicationClient(); // The UI is data-bound to the items collection and will update automatically // after we append to the collection. create_task(feedDataSource->RetrieveFeedAndInitData(feedUri, client)) .then([this, feedUri] { // Add the uri to the roaming data. The API requires an IIterable so we have to // put the uri in a Vector. Vector<String^>^ vec = ref new Vector<String^>(); vec->Append(feedUri); concurrency::create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([vec](StorageFile^ file) { FileIO::AppendLinesAsync(file, vec); }); }); } /// <summary> /// Called when the user chooses to remove some feeds which otherwise /// are valid Urls and currently are displaying in the UI, and are stored in /// the Feeds collection as well as in the feeds.txt file. /// </summary> void App::RemoveFeeds(Vector<FeedData^>^ feedsToDelete) { // Create a new list of feeds, excluding the ones the user selected. auto feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); // If we delete the "last viewed feed" we need to also remove the reference to it // from local settings. ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; String^ lastViewed; if (localSettings->Values->HasKey("LastViewedFeed")) { lastViewed = safe_cast<String^>(localSettings->Values->Lookup("LastViewedFeed")); } // When performance is an issue, consider using Vector::ReplaceAll for (const auto& item : feedsToDelete) { unsigned int index = -1; bool b = feedDataSource->Feeds->IndexOf(item, &index); if (index >= 0) { feedDataSource->Feeds->RemoveAt(index); } // Prevent ourself from trying later to reference // the page we just deleted. if (lastViewed != nullptr && lastViewed == item->Title) { localSettings->Values->Remove("LastViewedFeed"); } } // Re-initialize feeds.txt with the new list of URLs. Vector<String^>^ newFeedList = ref new Vector<String^>(); for (const auto& item : feedDataSource->Feeds) { newFeedList->Append(item->Uri); } // Overwrite the old data file with the new list. create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([newFeedList](StorageFile^ file) { FileIO::WriteLinesAsync(file, newFeedList); }); } ///<summary> /// This function enables the user to back out after /// entering a bad url in the "Add Feed" text box, for example pasting in a /// partial address. This function will also be called if a URL that was previously /// formatted correctly one day starts returning malformed XML when we try to load it. /// In either case, the FeedData was not added to the Feeds collection, and so /// we only need to delete the URL from the data file. /// </summary> void App::DeleteUrlFromFeedFile(Platform::String^ s) { // Overwrite the old data file with the new list. create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([this](StorageFile^ file) { return FileIO::ReadLinesAsync(file); }).then([this, s](IVector<String^>^ lines) { for (unsigned int i = 0; i < lines->Size; ++i) { if (lines->GetAt(i) == s) { lines->RemoveAt(i); } } return lines; }).then([this](IVector<String^>^ lines) { create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([this, lines](StorageFile^ file) { FileIO::WriteLinesAsync(file, lines); }); }); } ///<summary> /// Returns the feed that the user last selected from MainPage. ///<summary> task<FeedData^> App::GetCurrentFeedAsync() { FeedDataSource^ feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); return create_task(feedDataSource->m_LastViewedFeedEvent); } ///<summary> /// So that we can always get the current feed in the same way, we call this // method from ItemsPage when we change the current feed. This way the caller // doesn't care whether we're resuming from termination or new navigating. // The only other place we set the event is in InitDataSource in FeedData.cpp // when resuming from termination. ///</summary> void App::SetCurrentFeed(FeedData^ feed) { // Enable any pages waiting on the FeedData to continue FeedDataSource^ feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); feedDataSource->m_LastViewedFeedEvent = task_completion_event<FeedData^>(); feedDataSource->m_LastViewedFeedEvent.set(feed); // Store the current URI so that we can look up the correct feedData object on resume. ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; auto values = localSettings->Values; values->Insert("LastViewedFeed", dynamic_cast<PropertyValue^>(PropertyValue::CreateString(feed->Uri))); } // We stored the string ID when the app was suspended // because storing the FeedItem itself would have required // more custom serialization code. Here is where we retrieve // the FeedItem based on its string ID. FeedItem^ App::GetFeedItem(FeedData^ fd, String^ uri) { auto items = fd->Items; auto itEnd = end(items); auto it = std::find_if(begin(items), itEnd, [uri](FeedItem^ fi) { return fi->Link->AbsoluteUri == uri; }); if (it != itEnd) return *it; return nullptr; }
第五部分:將資料轉換成可使用的表單
可使用之表單中的所有未經處理資料並非都是必要資料。RSS 或 Atom 摘要會以 RFC 822 數值方式來表示其發佈日期。我們需要一種方式來將它轉換成使用者可以看懂的文字。為了那樣做,我們將會建立一個自訂類別,該類別可以實作 IValueConverter 並接受 RFC833 值做為每個日期元件的輸入和輸出字串。稍後,在顯示資料的 XAML 中,我們將會繫結至我們的 DateConverter 類別的輸出,而不是繫結至未經處理資料格式。
新增日期轉換器
在共用專案中,建立一個新的.h 檔案並新增此程式碼:
//DateConverter.h #pragma once #include <string> //for wcscmp #include <regex> namespace SimpleBlogReader { namespace WGDTF = Windows::Globalization::DateTimeFormatting; /// <summary> /// Implements IValueConverter so that we can convert the numeric date /// representation to a set of strings. /// </summary> public ref class DateConverter sealed : public Windows::UI::Xaml::Data::IValueConverter { public: virtual Platform::Object^ Convert(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ language) { if (value == nullptr) { throw ref new Platform::InvalidArgumentException(); } auto dt = safe_cast<Windows::Foundation::DateTime>(value); auto param = safe_cast<Platform::String^>(parameter); Platform::String^ result; if (param == nullptr) { auto dtf = WGDTF::DateTimeFormatter::ShortDate::get(); result = dtf->Format(dt); } else if (wcscmp(param->Data(), L"month") == 0) { auto formatter = ref new WGDTF::DateTimeFormatter("{month.abbreviated(3)}"); result = formatter->Format(dt); } else if (wcscmp(param->Data(), L"day") == 0) { auto formatter = ref new WGDTF::DateTimeFormatter("{day.integer(2)}"); result = formatter->Format(dt); } else if (wcscmp(param->Data(), L"year") == 0) { auto formatter = ref new WGDTF::DateTimeFormatter("{year.full}"); auto tempResult = formatter->Format(dt); //e.g. "2014" // Insert a hard return after second digit to get the rendering // effect we want std::wregex r(L"(\\d\\d)(\\d\\d)"); result = ref new Platform::String( std::regex_replace(tempResult->Data(), r, L"$1\n$2").c_str()); } else { // We don't handle other format types currently. throw ref new Platform::InvalidArgumentException(); } return result; } virtual Platform::Object^ ConvertBack(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ language) { // Not needed in SimpleBlogReader. Left as an exercise. throw ref new Platform::NotImplementedException(); } }; }
現在將它包括 (#include) 在 App.xaml.h 中:
#include "DateConverter.h"
並在 Application.Resources 節點內的 App.xaml 中建立其執行個體:
<local:DateConverter x:Key="dateConverter" />
摘要內容會以 HTML 的方式傳送,或在部分情況下以 XML 格式文字的方式傳送。若要在 RichTextBlock 中顯示此內容,我們必須將它轉換成 RTF 格式。下列類別使用 Windows HtmlUtilities 函式來剖析 HTML,然後使用 <regex> 函式來將它分割成多個段落,以方便我們建立 RTF 物件。我們無法在此案例中使用資料繫結,因此類別不需要實作 IValueConverter。我們將只會在需要它的頁面中建立其本機執行個體。
新增文字轉換器
在共用專案中,建立一個新的.h 檔案,接著將它命名為 TextHelper.h,然後新增此程式碼:
#pragma once namespace SimpleBlogReader { namespace WFC = Windows::Foundation::Collections; namespace WF = Windows::Foundation; namespace WUIXD = Windows::UI::Xaml::Documents; public ref class TextHelper sealed { public: TextHelper(); WFC::IVector<WUIXD::Paragraph^>^ CreateRichText( Platform::String^ fi, WF::TypedEventHandler < WUIXD::Hyperlink^, WUIXD::HyperlinkClickEventArgs^ > ^ context); Platform::String^ UnescapeText(Platform::String^ inStr); private: std::vector<std::wstring> SplitContentIntoParagraphs(const std::wstring& s, const std::wstring& rgx); std::wstring UnescapeText(const std::wstring& input); // Maps some HTML entities that we'll use to replace the escape sequences // in the call to UnescapeText when we create feed titles and render text. std::map<std::wstring, std::wstring> entities; }; }
現在請新增 TextHelper.cpp:
#include "pch.h" #include "TextHelper.h" using namespace std; using namespace SimpleBlogReader; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::Data::Html; using namespace Windows::UI::Xaml::Documents; /// <summary> /// Note that in this example we don't map all the possible HTML entities. Feel free to improve this. /// Also note that we initialize the map like this because VS2013 Udpate 3 does not support list /// initializers in a member declaration. /// </summary> TextHelper::TextHelper() : entities( { { L"<", L"<" }, { L">", L">" }, { L"&", L"&" }, { L"¢", L"¢" }, { L"£", L"£" }, { L"¥", L"¥" }, { L"€", L"€" }, { L"€", L"©" }, { L"®", L"®" }, { L"“", L"“" }, { L"”", L"”" }, { L"‘", L"‘" }, { L"’", L"’" }, { L"»", L"»" }, { L"«", L"«" }, { L"‹", L"‹" }, { L"›", L"›" }, { L"•", L"•" }, { L"°", L"°" }, { L"…", L"…" }, { L" ", L" " }, { L""", LR"(")" }, { L"'", L"'" }, { L"<", L"<" }, { L">", L">" }, { L"’", L"’" }, { L" ", L" " }, { L"&", L"&" } }) { } ///<summary> /// Accepts the Content property from a Feed and returns rich text /// paragraphs that can be passed to a RichTextBlock. ///</summary> String^ TextHelper::UnescapeText(String^ inStr) { wstring input(inStr->Data()); wstring result = UnescapeText(input); return ref new Platform::String(result.c_str()); } ///<summary> /// Create a RichText block from the text retrieved by the HtmlUtilies object. /// For a more full-featured app, you could parse the content argument yourself and /// add the page's images to the inlines collection. ///</summary> IVector<Paragraph^>^ TextHelper::CreateRichText(String^ content, TypedEventHandler<Hyperlink^, HyperlinkClickEventArgs^>^ context) { std::vector<Paragraph^> blocks; auto text = HtmlUtilities::ConvertToText(content); auto parts = SplitContentIntoParagraphs(wstring(text->Data()), LR"(\r\n)"); // Add the link at the top. Don't set the NavigateUri property because // that causes the link to open in IE even if the Click event is handled. auto hlink = ref new Hyperlink(); hlink->Click += context; auto linkText = ref new Run(); linkText->Foreground = ref new Windows::UI::Xaml::Media::SolidColorBrush(Windows::UI::Colors::DarkRed); linkText->Text = "Link"; hlink->Inlines->Append(linkText); auto linkPara = ref new Paragraph(); linkPara->Inlines->Append(hlink); blocks.push_back(linkPara); for (auto part : parts) { auto p = ref new Paragraph(); p->TextIndent = 10; p->Margin = (10, 10, 10, 10); auto r = ref new Run(); r->Text = ref new String(part.c_str()); p->Inlines->Append(r); blocks.push_back(p); } return ref new Vector<Paragraph^>(blocks); } ///<summary> /// Split an input string which has been created by HtmlUtilities::ConvertToText /// into paragraphs. The rgx string we use here is LR("\r\n") . If we ever use /// other means to grab the raw text from a feed, then the rgx will have to recognize /// other possible new line formats. ///</summary> vector<wstring> TextHelper::SplitContentIntoParagraphs(const wstring& s, const wstring& rgx) { const wregex r(rgx); vector<wstring> result; // the -1 argument indicates that the text after this match until the next match // is the "capture group". In other words, this is how we match on what is between the tokens. for (wsregex_token_iterator rit(s.begin(), s.end(), r, -1), end; rit != end; ++rit) { if (rit->length() > 0) { result.push_back(*rit); } } return result; } ///<summary> /// This is used to unescape html entities that occur in titles, subtitles, etc. // entities is a map<wstring, wstring> with key-values like this: { L"<", L"<" }, /// CAUTION: we must not unescape any content that gets sent to the webView. ///</summary> wstring TextHelper::UnescapeText(const wstring& input) { wsmatch match; // match L"<" as well as " " const wregex rgx(LR"(&#?\w*?;)"); wstring result; // itrEnd needs to be visible outside the loop wsregex_iterator itrEnd, itrRemainingText; // Iterate over input and build up result as we go along // by first appending what comes before the match, then the // unescaped replacement for the HTML entity which is the match, // then once at the end appending what comes after the last match. for (wsregex_iterator itr(input.cbegin(), input.cend(), rgx); itr != itrEnd; ++itr) { wstring entity = itr->str(); map<wstring, wstring>::const_iterator mit = entities.find(entity); if (mit != end(entities)) { result.append(itr->prefix()); result.append(mit->second); // mit->second is the replacement text itrRemainingText = itr; } else { // we found an entity that we don't explitly map yet so just // render it in raw form. Exercise for the user: add // all legal entities to the entities map. result.append(entity); continue; } } // If we didn't find any entities to escape // then (a) don't try to dereference itrRemainingText // and (b) return input because result is empty! if (itrRemainingText == itrEnd) { return input; } else { // Add any text between the last match and end of input string. result.append(itrRemainingText->suffix()); return result; } }
請注意,我們的自訂 TextHelper 類別會示範一些您可以在 C++/CX 應用程式內部使用 ISO C++ (std:: map、std::regex、std::wstring) 的方法。我們將會在使用此類別的頁面中,於本機建立此類別的執行個體。我們只需要將它包括在 App.xaml.h 中一次:
#include "TextHelper.h"
您現在應該能夠建置並執行 app。但是請不要預期它可以做很多事。
第六部分:啟動、暫停及繼續執行 app
當使用者按下或按一下應用程式磚來啟動應用程式時,以及當使用者在系統已終止應用程式以釋出記憶體供其他應用程式使用之後瀏覽回應用程式時,就會觸發 App::OnLaunched
事件。在任一種情況下,我們一律會移至網際網路,並重新載入資料以回應此事件。但是,這兩種情況還各有需要叫用的其他動作。我們可以透過查看與傳送至函式的 LaunchActivatedEventArgs 引數合併使用的 rootFrame 來減少這些狀態,然後執行正確的工作。幸運的是,使用 MainPage 自動新增的 SuspensionManager 類別會在應用程式暫停及重新啟動時,執行大部分的工作以儲存及還原應用程式狀態。我們只需呼叫它的方法即可。
將 SuspensionManager 程式碼檔案新增到 Common 資料夾中的專案。 新增 SuspensionManager.h,然後將下列程式碼複製到其中:
// // SuspensionManager.h // Declaration of the SuspensionManager class // #pragma once namespace SimpleBlogReader { namespace Common { /// <summary> /// SuspensionManager captures global session state to simplify process lifetime management /// for an application. Note that session state will be automatically cleared under a variety /// of conditions and should only be used to store information that would be convenient to /// carry across sessions, but that should be disacarded when an application crashes or is /// upgraded. /// </summary> class SuspensionManager sealed { public: static void RegisterFrame(Windows::UI::Xaml::Controls::Frame^ frame, Platform::String^ sessionStateKey, Platform::String^ sessionBaseKey = nullptr); static void UnregisterFrame(Windows::UI::Xaml::Controls::Frame^ frame); static concurrency::task<void> SaveAsync(); static concurrency::task<void> RestoreAsync(Platform::String^ sessionBaseKey = nullptr); static Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ SessionState(); static Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ SessionStateForFrame( Windows::UI::Xaml::Controls::Frame^ frame); private: static void RestoreFrameNavigationState(Windows::UI::Xaml::Controls::Frame^ frame); static void SaveFrameNavigationState(Windows::UI::Xaml::Controls::Frame^ frame); static Platform::Collections::Map<Platform::String^, Platform::Object^>^ _sessionState; static const wchar_t* sessionStateFilename; static std::vector<Platform::WeakReference> _registeredFrames; static Windows::UI::Xaml::DependencyProperty^ FrameSessionStateKeyProperty; static Windows::UI::Xaml::DependencyProperty^ FrameSessionBaseKeyProperty; static Windows::UI::Xaml::DependencyProperty^ FrameSessionStateProperty; }; } }
新增 SuspensionManager.cpp 程式碼檔案,然後將下列程式碼複製到其中:
// // SuspensionManager.cpp // Implementation of the SuspensionManager class // #include "pch.h" #include "SuspensionManager.h" #include <algorithm> using namespace SimpleBlogReader::Common; using namespace concurrency; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::Storage; using namespace Windows::Storage::FileProperties; using namespace Windows::Storage::Streams; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Interop; Map<String^, Object^>^ SuspensionManager::_sessionState = ref new Map<String^, Object^>(); const wchar_t* SuspensionManager::sessionStateFilename = L"_sessionState.dat"; std::vector<WeakReference> SuspensionManager::_registeredFrames; DependencyProperty^ SuspensionManager::FrameSessionStateKeyProperty = DependencyProperty::RegisterAttached("_FrameSessionStateKeyProperty", TypeName(String::typeid), TypeName(SuspensionManager::typeid), nullptr); DependencyProperty^ SuspensionManager::FrameSessionBaseKeyProperty = DependencyProperty::RegisterAttached("_FrameSessionBaseKeyProperty", TypeName(String::typeid), TypeName(SuspensionManager::typeid), nullptr); DependencyProperty^ SuspensionManager::FrameSessionStateProperty = DependencyProperty::RegisterAttached("_FrameSessionStateProperty", TypeName(IMap<String^, Object^>::typeid), TypeName(SuspensionManager::typeid), nullptr); class ObjectSerializeHelper { public: // Codes used for identifying serialized types enum StreamTypes { NullPtrType = 0, // Supported IPropertyValue types UInt8Type, UInt16Type, UInt32Type, UInt64Type, Int16Type, Int32Type, Int64Type, SingleType, DoubleType, BooleanType, Char16Type, GuidType, StringType, // Additional supported types StringToObjectMapType, // Marker values used to ensure stream integrity MapEndMarker }; static String^ ReadString(DataReader^ reader); static IMap<String^, Object^>^ ReadStringToObjectMap(DataReader^ reader); static Object^ ReadObject(DataReader^ reader); static void WriteString(DataWriter^ writer, String^ string); static void WriteProperty(DataWriter^ writer, IPropertyValue^ propertyValue); static void WriteStringToObjectMap(DataWriter^ writer, IMap<String^, Object^>^ map); static void WriteObject(DataWriter^ writer, Object^ object); }; /// <summary> /// Provides access to global session state for the current session. This state is serialized by /// <see cref="SaveAsync"/> and restored by <see cref="RestoreAsync"/> which require values to be /// one of the following: boxed values including integers, floating-point singles and doubles, /// wide characters, boolean, Strings and Guids, or Map<String^, Object^> where map values are /// subject to the same constraints. Session state should be as compact as possible. /// </summary> IMap<String^, Object^>^ SuspensionManager::SessionState() { return _sessionState; } /// <summary> /// Registers a <see cref="Frame"/> instance to allow its navigation history to be saved to /// and restored from <see cref="SessionState"/>. Frames should be registered once /// immediately after creation if they will participate in session state management. Upon /// registration if state has already been restored for the specified key /// the navigation history will immediately be restored. Subsequent invocations of /// <see cref="RestoreAsync(String)"/> will also restore navigation history. /// </summary> /// <param name="frame">An instance whose navigation history should be managed by /// <see cref="SuspensionManager"/></param> /// <param name="sessionStateKey">A unique key into <see cref="SessionState"/> used to /// store navigation-related information.</param> /// <param name="sessionBaseKey">An optional key that identifies the type of session. /// This can be used to distinguish between multiple application launch scenarios.</param> void SuspensionManager::RegisterFrame(Frame^ frame, String^ sessionStateKey, String^ sessionBaseKey) { if (frame->GetValue(FrameSessionStateKeyProperty) != nullptr) { throw ref new FailureException("Frames can only be registered to one session state key"); } if (frame->GetValue(FrameSessionStateProperty) != nullptr) { throw ref new FailureException("Frames must be either be registered before accessing frame session state, or not registered at all"); } if (sessionBaseKey != nullptr) { frame->SetValue(FrameSessionBaseKeyProperty, sessionBaseKey); sessionStateKey = sessionBaseKey + "_" + sessionStateKey; } // Use a dependency property to associate the session key with a frame, and keep a list of frames whose // navigation state should be managed frame->SetValue(FrameSessionStateKeyProperty, sessionStateKey); _registeredFrames.insert(_registeredFrames.begin(), WeakReference(frame)); // Check to see if navigation state can be restored RestoreFrameNavigationState(frame); } /// <summary> /// Disassociates a <see cref="Frame"/> previously registered by <see cref="RegisterFrame"/> /// from <see cref="SessionState"/>. Any navigation state previously captured will be /// removed. /// </summary> /// <param name="frame">An instance whose navigation history should no longer be /// managed.</param> void SuspensionManager::UnregisterFrame(Frame^ frame) { // Remove session state and remove the frame from the list of frames whose navigation // state will be saved (along with any weak references that are no longer reachable) auto key = safe_cast<String^>(frame->GetValue(FrameSessionStateKeyProperty)); if (SessionState()->HasKey(key)) { SessionState()->Remove(key); } _registeredFrames.erase( std::remove_if(_registeredFrames.begin(), _registeredFrames.end(), [=](WeakReference& e) { auto testFrame = e.Resolve<Frame>(); return testFrame == nullptr || testFrame == frame; }), _registeredFrames.end() ); } /// <summary> /// Provides storage for session state associated with the specified <see cref="Frame"/>. /// Frames that have been previously registered with <see cref="RegisterFrame"/> have /// their session state saved and restored automatically as a part of the global /// <see cref="SessionState"/>. Frames that are not registered have transient state /// that can still be useful when restoring pages that have been discarded from the /// navigation cache. /// </summary> /// <remarks>Apps may choose to rely on <see cref="NavigationHelper"/> to manage /// page-specific state instead of working with frame session state directly.</remarks> /// <param name="frame">The instance for which session state is desired.</param> /// <returns>A collection of state subject to the same serialization mechanism as /// <see cref="SessionState"/>.</returns> IMap<String^, Object^>^ SuspensionManager::SessionStateForFrame(Frame^ frame) { auto frameState = safe_cast<IMap<String^, Object^>^>(frame->GetValue(FrameSessionStateProperty)); if (frameState == nullptr) { auto frameSessionKey = safe_cast<String^>(frame->GetValue(FrameSessionStateKeyProperty)); if (frameSessionKey != nullptr) { // Registered frames reflect the corresponding session state if (!_sessionState->HasKey(frameSessionKey)) { _sessionState->Insert(frameSessionKey, ref new Map<String^, Object^>()); } frameState = safe_cast<IMap<String^, Object^>^>(_sessionState->Lookup(frameSessionKey)); } else { // Frames that aren't registered have transient state frameState = ref new Map<String^, Object^>(); } frame->SetValue(FrameSessionStateProperty, frameState); } return frameState; } void SuspensionManager::RestoreFrameNavigationState(Frame^ frame) { auto frameState = SessionStateForFrame(frame); if (frameState->HasKey("Navigation")) { frame->SetNavigationState(safe_cast<String^>(frameState->Lookup("Navigation"))); } } void SuspensionManager::SaveFrameNavigationState(Frame^ frame) { auto frameState = SessionStateForFrame(frame); frameState->Insert("Navigation", frame->GetNavigationState()); } /// <summary> /// Save the current <see cref="SessionState"/>. Any <see cref="Frame"/> instances /// registered with <see cref="RegisterFrame"/> will also preserve their current /// navigation stack, which in turn gives their active <see cref="Page"/> an opportunity /// to save its state. /// </summary> /// <returns>An asynchronous task that reflects when session state has been saved.</returns> task<void> SuspensionManager::SaveAsync(void) { // Save the navigation state for all registered frames for (auto && weakFrame : _registeredFrames) { auto frame = weakFrame.Resolve<Frame>(); if (frame != nullptr) SaveFrameNavigationState(frame); } // Serialize the session state synchronously to avoid asynchronous access to shared // state auto sessionData = ref new InMemoryRandomAccessStream(); auto sessionDataWriter = ref new DataWriter(sessionData->GetOutputStreamAt(0)); ObjectSerializeHelper::WriteObject(sessionDataWriter, _sessionState); // Once session state has been captured synchronously, begin the asynchronous process // of writing the result to disk return task<unsigned int>(sessionDataWriter->StoreAsync()).then([=](unsigned int) { return ApplicationData::Current->LocalFolder->CreateFileAsync(StringReference(sessionStateFilename), CreationCollisionOption::ReplaceExisting); }) .then([=](StorageFile^ createdFile) { return createdFile->OpenAsync(FileAccessMode::ReadWrite); }) .then([=](IRandomAccessStream^ newStream) { return RandomAccessStream::CopyAsync( sessionData->GetInputStreamAt(0), newStream->GetOutputStreamAt(0)); }) .then([=](UINT64 copiedBytes) { (void) copiedBytes; // Unused parameter return; }); } /// <summary> /// Restores previously saved <see cref="SessionState"/>. Any <see cref="Frame"/> instances /// registered with <see cref="RegisterFrame"/> will also restore their prior navigation /// state, which in turn gives their active <see cref="Page"/> an opportunity restore its /// state. /// </summary> /// <param name="sessionBaseKey">An optional key that identifies the type of session. /// This can be used to distinguish between multiple application launch scenarios.</param> /// <returns>An asynchronous task that reflects when session state has been read. The /// content of <see cref="SessionState"/> should not be relied upon until this task /// completes.</returns> task<void> SuspensionManager::RestoreAsync(String^ sessionBaseKey) { _sessionState->Clear(); task<StorageFile^> getFileTask(ApplicationData::Current->LocalFolder->GetFileAsync(StringReference(sessionStateFilename))); return getFileTask.then([=](StorageFile^ stateFile) { task<BasicProperties^> getBasicPropertiesTask(stateFile->GetBasicPropertiesAsync()); return getBasicPropertiesTask.then([=](BasicProperties^ stateFileProperties) { auto size = unsigned int(stateFileProperties->Size); if (size != stateFileProperties->Size) throw ref new FailureException("Session state larger than 4GB"); task<IRandomAccessStreamWithContentType^> openReadTask(stateFile->OpenReadAsync()); return openReadTask.then([=](IRandomAccessStreamWithContentType^ stateFileStream) { auto stateReader = ref new DataReader(stateFileStream); return task<unsigned int>(stateReader->LoadAsync(size)).then([=](unsigned int bytesRead) { (void) bytesRead; // Unused parameter // Deserialize the Session State Object^ content = ObjectSerializeHelper::ReadObject(stateReader); _sessionState = (Map<String^, Object^>^)content; // Restore any registered frames to their saved state for (auto && weakFrame : _registeredFrames) { auto frame = weakFrame.Resolve<Frame>(); if (frame != nullptr && safe_cast<String^>(frame->GetValue(FrameSessionBaseKeyProperty)) == sessionBaseKey) { frame->ClearValue(FrameSessionStateProperty); RestoreFrameNavigationState(frame); } } }, task_continuation_context::use_current()); }); }); }); } #pragma region Object serialization for a known set of types void ObjectSerializeHelper::WriteString(DataWriter^ writer, String^ string) { writer->WriteByte(StringType); writer->WriteUInt32(writer->MeasureString(string)); writer->WriteString(string); } void ObjectSerializeHelper::WriteProperty(DataWriter^ writer, IPropertyValue^ propertyValue) { switch (propertyValue->Type) { case PropertyType::UInt8: writer->WriteByte(StreamTypes::UInt8Type); writer->WriteByte(propertyValue->GetUInt8()); return; case PropertyType::UInt16: writer->WriteByte(StreamTypes::UInt16Type); writer->WriteUInt16(propertyValue->GetUInt16()); return; case PropertyType::UInt32: writer->WriteByte(StreamTypes::UInt32Type); writer->WriteUInt32(propertyValue->GetUInt32()); return; case PropertyType::UInt64: writer->WriteByte(StreamTypes::UInt64Type); writer->WriteUInt64(propertyValue->GetUInt64()); return; case PropertyType::Int16: writer->WriteByte(StreamTypes::Int16Type); writer->WriteUInt16(propertyValue->GetInt16()); return; case PropertyType::Int32: writer->WriteByte(StreamTypes::Int32Type); writer->WriteUInt32(propertyValue->GetInt32()); return; case PropertyType::Int64: writer->WriteByte(StreamTypes::Int64Type); writer->WriteUInt64(propertyValue->GetInt64()); return; case PropertyType::Single: writer->WriteByte(StreamTypes::SingleType); writer->WriteSingle(propertyValue->GetSingle()); return; case PropertyType::Double: writer->WriteByte(StreamTypes::DoubleType); writer->WriteDouble(propertyValue->GetDouble()); return; case PropertyType::Boolean: writer->WriteByte(StreamTypes::BooleanType); writer->WriteBoolean(propertyValue->GetBoolean()); return; case PropertyType::Char16: writer->WriteByte(StreamTypes::Char16Type); writer->WriteUInt16(propertyValue->GetChar16()); return; case PropertyType::Guid: writer->WriteByte(StreamTypes::GuidType); writer->WriteGuid(propertyValue->GetGuid()); return; case PropertyType::String: WriteString(writer, propertyValue->GetString()); return; default: throw ref new InvalidArgumentException("Unsupported property type"); } } void ObjectSerializeHelper::WriteStringToObjectMap(DataWriter^ writer, IMap<String^, Object^>^ map) { writer->WriteByte(StringToObjectMapType); writer->WriteUInt32(map->Size); for (auto && pair : map) { WriteObject(writer, pair->Key); WriteObject(writer, pair->Value); } writer->WriteByte(MapEndMarker); } void ObjectSerializeHelper::WriteObject(DataWriter^ writer, Object^ object) { if (object == nullptr) { writer->WriteByte(NullPtrType); return; } auto propertyObject = dynamic_cast<IPropertyValue^>(object); if (propertyObject != nullptr) { WriteProperty(writer, propertyObject); return; } auto mapObject = dynamic_cast<IMap<String^, Object^>^>(object); if (mapObject != nullptr) { WriteStringToObjectMap(writer, mapObject); return; } throw ref new InvalidArgumentException("Unsupported data type"); } String^ ObjectSerializeHelper::ReadString(DataReader^ reader) { int length = reader->ReadUInt32(); String^ string = reader->ReadString(length); return string; } IMap<String^, Object^>^ ObjectSerializeHelper::ReadStringToObjectMap(DataReader^ reader) { auto map = ref new Map<String^, Object^>(); auto size = reader->ReadUInt32(); for (unsigned int index = 0; index < size; index++) { auto key = safe_cast<String^>(ReadObject(reader)); auto value = ReadObject(reader); map->Insert(key, value); } if (reader->ReadByte() != StreamTypes::MapEndMarker) { throw ref new InvalidArgumentException("Invalid stream"); } return map; } Object^ ObjectSerializeHelper::ReadObject(DataReader^ reader) { auto type = reader->ReadByte(); switch (type) { case StreamTypes::NullPtrType: return nullptr; case StreamTypes::UInt8Type: return reader->ReadByte(); case StreamTypes::UInt16Type: return reader->ReadUInt16(); case StreamTypes::UInt32Type: return reader->ReadUInt32(); case StreamTypes::UInt64Type: return reader->ReadUInt64(); case StreamTypes::Int16Type: return reader->ReadInt16(); case StreamTypes::Int32Type: return reader->ReadInt32(); case StreamTypes::Int64Type: return reader->ReadInt64(); case StreamTypes::SingleType: return reader->ReadSingle(); case StreamTypes::DoubleType: return reader->ReadDouble(); case StreamTypes::BooleanType: return reader->ReadBoolean(); case StreamTypes::Char16Type: return (char16_t) reader->ReadUInt16(); case StreamTypes::GuidType: return reader->ReadGuid(); case StreamTypes::StringType: return ReadString(reader); case StreamTypes::StringToObjectMapType: return ReadStringToObjectMap(reader); default: throw ref new InvalidArgumentException("Unsupported property type"); } } #pragma endregion
在 app.xaml.cpp 中新增這個 Include 指示詞:
#include "Common\SuspensionManager.h"
新增 namespace 指示詞:
using namespace SimpleBlogReader::Common;
現在使用此程式碼來取代現有的函式:
void App::OnLaunched(LaunchActivatedEventArgs^ e) { #if _DEBUG if (IsDebuggerPresent()) { DebugSettings->EnableFrameRateCounter = true; } #endif auto rootFrame = dynamic_cast<Frame^>(Window::Current->Content); // Do not repeat app initialization when the Window already has content, // just ensure that the window is active. if (rootFrame == nullptr) { // Create a Frame to act as the navigation context and associate it with // a SuspensionManager key rootFrame = ref new Frame(); SuspensionManager::RegisterFrame(rootFrame, "AppFrame"); // Initialize the Atom and RSS feed objects with data from the web FeedDataSource^ feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); if (feedDataSource->Feeds->Size == 0) { if (e->PreviousExecutionState == ApplicationExecutionState::Terminated) { // On resume FeedDataSource needs to know whether the app was on a // specific FeedData, which will be the unless it was on MainPage // when it was terminated. ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; auto values = localSettings->Values; if (localSettings->Values->HasKey("LastViewedFeed")) { feedDataSource->CurrentFeedUri = safe_cast<String^>(localSettings->Values->Lookup("LastViewedFeed")); } } feedDataSource->InitDataSource(); } // We have 4 pages in the app rootFrame->CacheSize = 4; auto prerequisite = task<void>([](){}); if (e->PreviousExecutionState == ApplicationExecutionState::Terminated) { // Now restore the pages if we are resuming prerequisite = Common::SuspensionManager::RestoreAsync(); } // if we're starting fresh, prerequisite will execute immediately. // if resuming from termination, prerequisite will wait until RestoreAsync() completes. prerequisite.then([=]() { if (rootFrame->Content == nullptr) { if (!rootFrame->Navigate(MainPage::typeid, e->Arguments)) { throw ref new FailureException("Failed to create initial page"); } } // Place the frame in the current Window Window::Current->Content = rootFrame; Window::Current->Activate(); }, task_continuation_context::use_current()); } // There is a frame, but is has no content, so navigate to main page // and activate the window. else if (rootFrame->Content == nullptr) { #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP // Removes the turnstile navigation for startup. if (rootFrame->ContentTransitions != nullptr) { _transitions = ref new TransitionCollection(); for (auto transition : rootFrame->ContentTransitions) { _transitions->Append(transition); } } rootFrame->ContentTransitions = nullptr; _firstNavigatedToken = rootFrame->Navigated += ref new NavigatedEventHandler(this, &App::RootFrame_FirstNavigated); #endif // When the navigation stack isn't restored navigate to the first page, // configuring the new page by passing required information as a navigation // parameter. if (!rootFrame->Navigate(MainPage::typeid, e->Arguments)) { throw ref new FailureException("Failed to create initial page"); } // Ensure the current window is active in this code path. // we also called this inside the task for the other path. Window::Current->Activate(); } }
請注意,App 類別是在共用專案中,因此我們在這裡撰寫的程式碼將在已定義 WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP 巨集以外的 Windows 和 Phone 應用程式上執行。
OnSuspending 處理常式比較簡單。在系統關閉應用程式 (而不是在使用者關閉應用程式) 時,會呼叫該處理常式。這裡我們讓 SuspensionManager 來執行此工作。它將會在應用程式的每個頁面上呼叫
SaveState
事件處理常式,並且將會把我們在每個頁面的 PageState 物件中儲存的物件序列化,然後在應用程式恢復繼續執行時將值還原回頁面中。如果您想要查看程式碼,請查看 SuspensionManager.cpp。請使用此程式碼取代現有的 OnSuspending 函式主體:
void App::OnSuspending(Object^ sender, SuspendingEventArgs^ e) { (void)sender; // Unused parameter (void)e; // Unused parameter // Save application state and stop any background activity auto deferral = e->SuspendingOperation->GetDeferral(); create_task(Common::SuspensionManager::SaveAsync()) .then([deferral]() { deferral->Complete(); }); }
此時我們可以啟動應用程式並下載摘要資料,但是我們無法向使用者顯示資料。讓我們來為此做一些事!
第七部分:新增第一個 UI 頁面,摘要的清單
當應用程式開啟時,我們想要向使用者顯示已下載之所有摘要的最上層集合。他們可以按一下或按下集合中的某個項目,來瀏覽至將包含摘要項目或文章之集合的特定摘要。我們已經新增頁面。在 Windows 應用程式中,它是一個項目頁面,會在裝置處於水平方向時顯示 GridView,並在裝置處於垂直方向時顯示 ListView。Phone 專案沒有項目頁面,因此我們有一個基本頁面,我們將會以手動方式將 ListView 新增至該頁面。當裝置方向改變時,清單檢視本身將會自動調整。
在此頁面和每個頁面上,通常都會有相同的基本工作需要完成:
- 將描述 UI 和與資料繫結的 XAML 標記新增至資料
- 將自訂程式碼新增至
LoadState
和SaveState
成員函式。 - 處理事件,至少處理其中一個通常具有瀏覽到下一個頁面之程式碼的事件
我們將依序完成這些作業,首先在 Windows 專案中:
新增 XAML 標記 (MainPage)
主頁面會轉譯 GridView 控制項中的每個 FeedData 物件。為了描述資料的外觀,我們會建立 DataTemplate,這是一個 XAML 樹狀結構,將用來轉譯每個項目。DataTemplates 在配置、字型、色彩等等方面的可能性,只會受到您自己的想像力和造形設計能力限制。在這個頁面上,我們將會使用一個簡單的範本,在轉譯時它看起來就像這樣:
XAML 樣式就像是 Microsoft Word 中的樣式一樣。它是一種將 XAML 元素 (TargetType) 上的一組屬性值組成群組的便利方式。 樣式可以以另一種樣式為基礎。"x:Key" 屬性可指定我們使用樣式時用來參照樣式的名稱。
將這個範本和其支援的樣式放入 MainPage.xaml (Windows 8.1) 的 Page.Resources 節點中。它們只會在 MainPage 中使用。
<Style x:Key="GridTitleTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}"> <Setter Property="FontSize" Value="26.667"/> <Setter Property="Margin" Value="12,0,12,2"/> </Style> <Style x:Key="GridDescriptionTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}"> <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" Background="{StaticResource BlockBackgroundBrush}" > <StackPanel Margin="0,22,16,0"> <TextBlock Text="{Binding Title}" Style="{StaticResource GridTitleTextStyle}" Margin="10,10,10,10"/> <TextBlock Text="{Binding Description}" Style="{StaticResource GridDescriptionTextStyle}" Margin="10,10,10,10" /> </StackPanel> <Border BorderBrush="DarkRed" BorderThickness="4" VerticalAlignment="Bottom"> <StackPanel VerticalAlignment="Bottom" Orientation="Horizontal" Background="{StaticResource GreenBlockBackgroundBrush}"> <TextBlock Text="Last Updated" FontWeight="Bold" Margin="12,4,0,8" Height="42"/> <TextBlock Text="{Binding PubDate, Converter={StaticResource dateConverter}}" FontWeight="ExtraBold" Margin="4,4,12,8" Height="42" Width="88"/> </StackPanel> </Border> </Grid> </DataTemplate>
您將會在
GreenBlockBackgroundBrush
底下看到一條紅色的曲線,我們將會在幾個步驟中處理這個問題。繼續在 MainPage.xaml (Windows 8.1) 中,刪除頁面本機
AppName
元素,使其不會隱藏我們將在 App 範圍中新增的全域元素。將 CollectionViewSource 新增到 Page.Resources 節點。這個物件會將我們的 ListView 連線到資料模型:
<!-- Collection of items displayed by this page --> <CollectionViewSource x:Name="itemsViewSource" Source="{Binding Items}"/>
請注意,Page 元素已經具有設為 MainPage 類別之 DefaultViewModel 屬性的 DataContext 屬性。我們將該屬性設定為 FeedDataSource,因此 CollectionViewSource 就會在該位置查詢並找出項目集合。
在 App.xaml 中,讓我們為應用程式名稱新增一個全域資源字串,以及新增一些將從應用程式中的多個頁面參照的額外資源。 藉由將資源放在此處,我們就不需要在每個頁面上個別定義它們。將這些元素新增到 App.xaml 中的資源節點:
<x:String x:Key="AppName">Simple Blog Reader</x:String> <SolidColorBrush x:Key="WindowsBlogBackgroundBrush" Color="#FF0A2562"/> <SolidColorBrush x:Key="GreenBlockBackgroundBrush" Color="#FF6BBD46"/> <Style x:Key="WindowsBlogLayoutRootStyle" TargetType="Panel"> <Setter Property="Background" Value="{StaticResource WindowsBlogBackgroundBrush}"/> </Style> <!-- Green square in all ListViews that displays the date --> <ControlTemplate x:Key="DateBlockTemplate"> <Viewbox Stretch="Fill"> <Canvas Height="86" Width="86" Margin="4,0,4,4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <TextBlock TextTrimming="WordEllipsis" Padding="0,0,0,0" TextWrapping="NoWrap" Width="Auto" Height="Auto" FontSize="32" FontWeight="Bold"> <TextBlock.Text> <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="month"/> </TextBlock.Text> </TextBlock> <TextBlock TextTrimming="WordEllipsis" TextWrapping="Wrap" Width="Auto" Height="Auto" FontSize="32" FontWeight="Bold" Canvas.Top="36"> <TextBlock.Text> <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="day"/> </TextBlock.Text> </TextBlock> <Line Stroke="White" StrokeThickness="2" X1="50" Y1="46" X2="50" Y2="80" /> <TextBlock TextWrapping="Wrap" Height="Auto" FontSize="18" FontWeight="Bold" FontStretch="Condensed" LineHeight="18" LineStackingStrategy="BaselineToBaseline" Canvas.Top="38" Canvas.Left="56"> <TextBlock.Text> <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="year" /> </TextBlock.Text> </TextBlock> </Canvas> </Viewbox> </ControlTemplate> <!-- Describes the layout for items in all ListViews --> <DataTemplate x:Name="ListItemTemplate"> <Grid Margin="5,0,0,0"> <Grid.ColumnDefinitions> <ColumnDefinition Width="72"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition MaxHeight="54"></RowDefinition> </Grid.RowDefinitions> <!-- Green date block --> <Border Background="{StaticResource GreenBlockBackgroundBrush}" VerticalAlignment="Top"> <ContentControl Template="{StaticResource DateBlockTemplate}" /> </Border> <TextBlock Grid.Column="1" Text="{Binding Title}" Margin="10,0,0,0" FontSize="20" TextWrapping="Wrap" MaxHeight="72" Foreground="#FFFE5815" /> </Grid> </DataTemplate>
MainPage 會顯示摘要的清單。當裝置處於橫向方向時,我們將會使用支援水平捲動的 GridView。處於直向方向時,我們將會使用支援垂直捲動的 ListView。我們希望使用者在任一方向都能夠使用應用程式。實作對方向變更的支援是相對簡單的:
- 將這兩個控制項新增到頁面,並將 ItemSource 設為相同的 collectionViewSource。將 ListView 上的 Visibility 屬性設定為 Collapsed,以將它預設為不顯示。
- 建立一組兩個 VisualState 物件,一個描述橫向方向的 UI 行為,一個描述直向方向的行為。
- 處理 Window::SizeChanged 事件,此事件會在方向變更或使用者縮小或放大視窗時觸發。檢查新大小的高度與寬度。如果高度大於寬度,就會叫用直向方向的 VisualState。否則會叫用橫向狀態。
新增 GridView 與 ListView
在 MainPage.xaml 中,新增這個 GridView 與 ListView,以及包含返回按鈕與頁面標題的資料格:
<Grid Style="{StaticResource WindowsBlogLayoutRootStyle}"> <Grid.ChildrenTransitions> <TransitionCollection> <EntranceThemeTransition/> </TransitionCollection> </Grid.ChildrenTransitions> <Grid.RowDefinitions> <RowDefinition Height="140"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- Horizontal scrolling grid --> <GridView x:Name="ItemGridView" AutomationProperties.AutomationId="ItemsGridView" AutomationProperties.Name="Items" TabIndex="1" Grid.RowSpan="2" Padding="116,136,116,46" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" SelectionMode="None" ItemTemplate="{StaticResource DefaultGridItemTemplate}" IsItemClickEnabled="true" IsSwipeEnabled="false" ItemClick="ItemGridView_ItemClick" Margin="0,-10,0,10"> </GridView> <!-- Vertical scrolling list --> <ListView x:Name="ItemListView" Visibility="Collapsed" AutomationProperties.AutomationId="ItemsListView" AutomationProperties.Name="Items" TabIndex="1" Grid.Row="1" Margin="-10,-10,0,0" IsItemClickEnabled="True" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" IsSwipeEnabled="False" ItemClick="ItemGridView_ItemClick" ItemTemplate="{StaticResource ListItemTemplate}"> <ListView.ItemContainerStyle> <Style TargetType="FrameworkElement"> <Setter Property="Margin" Value="2,0,0,2"/> </Style> </ListView.ItemContainerStyle> </ListView> <!-- Back button and page title --> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="120"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Button x:Name="backButton" Margin="39,59,39,0" Command="{Binding NavigationHelper.GoBackCommand, ElementName=pageRoot}" Style="{StaticResource NavigationBackButtonNormalStyle}" VerticalAlignment="Top" AutomationProperties.Name="Back" AutomationProperties.AutomationId="BackButton" AutomationProperties.ItemType="Navigation Button"/> <TextBlock x:Name="pageTitle" Text="{StaticResource AppName}" Style="{StaticResource HeaderTextBlockStyle}" Grid.Column="1" IsHitTestVisible="false" TextWrapping="NoWrap" VerticalAlignment="Bottom" Margin="0,0,30,40"/> </Grid>
請注意,對於 ItemClick 事件而言,這兩個控制項是使用相同的成員函式。將插入點放在其中一個控制項上,然後按 F12 來自動產生事件處理常式虛設常式。我們稍後將為它新增程式碼。
貼上 VisualStateGroups 定義,讓它成為根資料格內部的最後一個元素 (請不要將它放在資料格外,否則它將會無法運作)。請注意,狀態有兩種,但只有一種已明確定義。這是因為此頁面的 XAML 中已經描述 DefaultLayout 狀態。
<VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="ViewStates"> <VisualState x:Name="DefaultLayout"/> <VisualState x:Name="Portrait"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" Storyboard.TargetProperty="Visibility"> <DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemGridView" Storyboard.TargetProperty="Visibility"> <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>
現在 UI 已經全部定義完成。我們只需要告知頁面當它被載入時要執行哪些工作。
LoadState 與 SaveState (Windows 應用程式 MainPage)
在任何 XAML 頁面上,我們需要注意的兩個主要成員函式為 LoadState
與 SaveState
(有時候)。 我們會在 LoadState
中填入頁面的資料,然後在 SaveState
中儲存當我們被暫停然後再度啟動時需要重新填入頁面的任何資料。
使用此程式碼取代
LoadState
實作,這會插入已由我們在啟動時建立之 feedDataSource 所載入 (或仍在載入中) 的摘要資料,然後將資料放入此頁面的 ViewModel 中。void MainPage::LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e) { auto feedDataSource = safe_cast<FeedDataSource^> (App::Current->Resources->Lookup("feedDataSource")); this->DefaultViewModel->Insert("Items", feedDataSource->Feeds); }
我們不需要呼叫 MainPage 的
SaveState
,因為此頁面沒有任何項目需要記住。它一律會顯示所有摘要。
事件處理常式 (Windows 應用程式 MainPage)
在概念上,所有頁面都是存在於框架內部。它是我們用來在頁面之間來回瀏覽的框架。瀏覽函式呼叫中的第二個參數是用來將資料從某個頁面傳遞到另一個頁面。我們在這裡傳遞的任何物件,都會在應用程式暫停時由 SuspensionManager 自動儲存及序列化,因此可以在應用程式恢復繼續執行時還原值。預設 SuspensionManager 僅支援內建類型、字串及 GUID。如果您需要更複雜的序列化,您可以建立自訂的 SuspensionManager。這裡我們會傳送字串,SplitPage 將用來查詢目前的摘要。
瀏覽項目點選
當使用者按一下資料格中的某個項目時,事件處理常式會取得按下的項目,將它設為「目前摘要」以防止應用程式在之後的某個時間點暫停,然後再瀏覽至下一個頁面。它會將摘要的標題傳遞到下一個頁面,讓該頁面可以查詢該摘要的資料。以下是要貼上的程式碼:
void MainPage::ItemGridView_ItemClick(Object^ sender, ItemClickEventArgs^ e) { // We must manually cast from Object^ to FeedData^. auto feedData = safe_cast<FeedData^>(e->ClickedItem); // Store the feed and tell other pages it's loaded and ready to go. auto app = safe_cast<App^>(App::Current); app->SetCurrentFeed(feedData); // Only navigate if there are items in the feed if (feedData->Items->Size > 0) { // Navigate to SplitPage and pass the title of the selected feed. // SplitPage will receive this in its LoadState method in the // navigationParamter. this->Frame->Navigate(SplitPage::typeid, feedData->Title); } }
針對之前要編譯的程式碼,我們需要在目前檔案 (MainPage.xaml.cpp (Windows 8.1)) 的最上方包括 (#include) SplitPage.xaml.h:
#include "SplitPage.xaml.h"
處理 Page_SizeChanged 事件
在 MainPage.xaml 中,將
x:Name="pageRoot"
新增至根 Page 元素的屬性,藉以新增根元素的名稱,然後新增SizeChanged="pageRoot_SizeChanged"
屬性來建立事件處理常式。使用此程式碼來取代 cpp 檔案中的處理常式實作:void MainPage::pageRoot_SizeChanged(Platform::Object^ sender, SizeChangedEventArgs^ e) { if (e->NewSize.Height / e->NewSize.Width >= 1) { VisualStateManager::GoToState(this, "Portrait", false); } else { VisualStateManager::GoToState(this, "DefaultLayout", false); } }
然後將這個函式的宣告新增至 MainPage.xaml.h 的 MainPage 類別。
private: void pageRoot_SizeChanged(Platform::Object^ sender, SizeChangedEventArgs^ e);
程式碼很簡單。如果您現在在模擬器中執行應用程式,然後旋轉裝置,您就會看到 GridView 與 ListView 之間的 UI 變更。
新增 XAML (Phone 應用程式 MainPage)
現在我們要讓 Phone 應用程式主頁面運作。這需要的程式碼將會少很多,因為我們將會使用我們放入共用專案中的所有程式碼。此外,Phone 應用程式不支援 GridView 控制項,因為畫面太小使控制項無法妥善運作。所以我們將會使用會自動調整為橫向方向且不需要任何 VisualState 變更的 ListView。我們會從新增 DataContext 屬性到頁面元素開始。這不會在 Phone 基本頁面中自動產生,就像在 ItemsPage 或 SplitPage 中一樣。
若要實作頁面瀏覽,您的頁面需要 NavigationHelper,這依序取決於 RelayCommand。新增項目 RelayCommand.h,然後將此程式碼複製到其中:
// // RelayCommand.h // Declaration of the RelayCommand and associated classes // #pragma once // <summary> // A command whose sole purpose is to relay its functionality // to other objects by invoking delegates. // The default return value for the CanExecute method is 'true'. // <see cref="RaiseCanExecuteChanged"/> needs to be called whenever // <see cref="CanExecute"/> is expected to return a different value. // </summary> namespace SimpleBlogReader { namespace Common { [Windows::Foundation::Metadata::WebHostHidden] public ref class RelayCommand sealed :[Windows::Foundation::Metadata::Default] Windows::UI::Xaml::Input::ICommand { public: virtual event Windows::Foundation::EventHandler<Object^>^ CanExecuteChanged; virtual bool CanExecute(Object^ parameter); virtual void Execute(Object^ parameter); virtual ~RelayCommand(); internal: RelayCommand(std::function<bool(Platform::Object^)> canExecuteCallback, std::function<void(Platform::Object^)> executeCallback); void RaiseCanExecuteChanged(); private: std::function<bool(Platform::Object^)> _canExecuteCallback; std::function<void(Platform::Object^)> _executeCallback; }; } }
在 Common 資料夾中新增 RelayCommand.cpp,並將此程式碼複製到其中:
// // RelayCommand.cpp // Implementation of the RelayCommand and associated classes // #include "pch.h" #include "RelayCommand.h" #include "NavigationHelper.h" using namespace SimpleBlogReader::Common; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::System; using namespace Windows::UI::Core; using namespace Windows::UI::ViewManagement; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Navigation; /// <summary> /// Determines whether this <see cref="RelayCommand"/> can execute in its current state. /// </summary> /// <param name="parameter"> /// Data used by the command. If the command does not require data to be passed, this object can be set to null. /// </param> /// <returns>true if this command can be executed; otherwise, false.</returns> bool RelayCommand::CanExecute(Object^ parameter) { return (_canExecuteCallback) (parameter); } /// <summary> /// Executes the <see cref="RelayCommand"/> on the current command target. /// </summary> /// <param name="parameter"> /// Data used by the command. If the command does not require data to be passed, this object can be set to null. /// </param> void RelayCommand::Execute(Object^ parameter) { (_executeCallback) (parameter); } /// <summary> /// Method used to raise the <see cref="CanExecuteChanged"/> event /// to indicate that the return value of the <see cref="CanExecute"/> /// method has changed. /// </summary> void RelayCommand::RaiseCanExecuteChanged() { CanExecuteChanged(this, nullptr); } /// <summary> /// RelayCommand Class Destructor. /// </summary> RelayCommand::~RelayCommand() { _canExecuteCallback = nullptr; _executeCallback = nullptr; }; /// <summary> /// Creates a new command that can always execute. /// </summary> /// <param name="canExecuteCallback">The execution status logic.</param> /// <param name="executeCallback">The execution logic.</param> RelayCommand::RelayCommand(std::function<bool(Platform::Object^)> canExecuteCallback, std::function<void(Platform::Object^)> executeCallback) : _canExecuteCallback(canExecuteCallback), _executeCallback(executeCallback) { }
在 Common 資料夾中新增 NavigationHelper.h 檔案,並將此程式碼複製到其中:
// // NavigationHelper.h // Declaration of the NavigationHelper and associated classes // #pragma once #include "RelayCommand.h" namespace SimpleBlogReader { namespace Common { /// <summary> /// Class used to hold the event data required when a page attempts to load state. /// </summary> public ref class LoadStateEventArgs sealed { public: /// <summary> /// The parameter value passed to <see cref="Frame->Navigate(Type, Object)"/> /// when this page was initially requested. /// </summary> property Platform::Object^ NavigationParameter { Platform::Object^ get(); } /// <summary> /// A dictionary of state preserved by this page during an earlier /// session. This will be null the first time a page is visited. /// </summary> property Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ PageState { Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ get(); } internal: LoadStateEventArgs(Platform::Object^ navigationParameter, Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ pageState); private: Platform::Object^ _navigationParameter; Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ _pageState; }; /// <summary> /// Represents the method that will handle the <see cref="NavigationHelper->LoadState"/>event /// </summary> public delegate void LoadStateEventHandler(Platform::Object^ sender, LoadStateEventArgs^ e); /// <summary> /// Class used to hold the event data required when a page attempts to save state. /// </summary> public ref class SaveStateEventArgs sealed { public: /// <summary> /// An empty dictionary to be populated with serializable state. /// </summary> property Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ PageState { Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ get(); } internal: SaveStateEventArgs(Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ pageState); private: Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ _pageState; }; /// <summary> /// Represents the method that will handle the <see cref="NavigationHelper->SaveState"/>event /// </summary> public delegate void SaveStateEventHandler(Platform::Object^ sender, SaveStateEventArgs^ e); /// <summary> /// NavigationHelper aids in navigation between pages. It provides commands used to /// navigate back and forward as well as registers for standard mouse and keyboard /// shortcuts used to go back and forward in Windows and the hardware back button in /// Windows Phone. In addition it integrates SuspensionManger to handle process lifetime /// management and state management when navigating between pages. /// </summary> /// <example> /// To make use of NavigationHelper, follow these two steps or /// start with a BasicPage or any other Page item template other than BlankPage. /// /// 1) Create an instance of the NavigationHelper somewhere such as in the /// constructor for the page and register a callback for the LoadState and /// SaveState events. /// <code> /// MyPage::MyPage() /// { /// InitializeComponent(); /// auto navigationHelper = ref new Common::NavigationHelper(this); /// navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &MyPage::LoadState); /// navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &MyPage::SaveState); /// } /// /// void MyPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) /// { } /// void MyPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e) /// { } /// </code> /// /// 2) Register the page to call into the NavigationHelper whenever the page participates /// in navigation by overriding the <see cref="Windows::UI::Xaml::Controls::Page::OnNavigatedTo"/> /// and <see cref="Windows::UI::Xaml::Controls::Page::OnNavigatedFrom"/> events. /// <code> /// void MyPage::OnNavigatedTo(NavigationEventArgs^ e) /// { /// NavigationHelper->OnNavigatedTo(e); /// } /// /// void MyPage::OnNavigatedFrom(NavigationEventArgs^ e) /// { /// NavigationHelper->OnNavigatedFrom(e); /// } /// </code> /// </example> [Windows::Foundation::Metadata::WebHostHidden] [Windows::UI::Xaml::Data::Bindable] public ref class NavigationHelper sealed { public: /// <summary> /// <see cref="RelayCommand"/> used to bind to the back Button's Command property /// for navigating to the most recent item in back navigation history, if a Frame /// manages its own navigation history. /// /// The <see cref="RelayCommand"/> is set up to use the virtual method <see cref="GoBack"/> /// as the Execute Action and <see cref="CanGoBack"/> for CanExecute. /// </summary> property RelayCommand^ GoBackCommand { RelayCommand^ get(); } /// <summary> /// <see cref="RelayCommand"/> used for navigating to the most recent item in /// the forward navigation history, if a Frame manages its own navigation history. /// /// The <see cref="RelayCommand"/> is set up to use the virtual method <see cref="GoForward"/> /// as the Execute Action and <see cref="CanGoForward"/> for CanExecute. /// </summary> property RelayCommand^ GoForwardCommand { RelayCommand^ get(); } internal: NavigationHelper(Windows::UI::Xaml::Controls::Page^ page, RelayCommand^ goBack = nullptr, RelayCommand^ goForward = nullptr); bool CanGoBack(); void GoBack(); bool CanGoForward(); void GoForward(); void OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e); void OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e); event LoadStateEventHandler^ LoadState; event SaveStateEventHandler^ SaveState; private: Platform::WeakReference _page; RelayCommand^ _goBackCommand; RelayCommand^ _goForwardCommand; #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP Windows::Foundation::EventRegistrationToken _backPressedEventToken; void HardwareButton_BackPressed(Platform::Object^ sender, Windows::Phone::UI::Input::BackPressedEventArgs^ e); #else bool _navigationShortcutsRegistered; Windows::Foundation::EventRegistrationToken _acceleratorKeyEventToken; Windows::Foundation::EventRegistrationToken _pointerPressedEventToken; void CoreDispatcher_AcceleratorKeyActivated(Windows::UI::Core::CoreDispatcher^ sender, Windows::UI::Core::AcceleratorKeyEventArgs^ e); void CoreWindow_PointerPressed(Windows::UI::Core::CoreWindow^ sender, Windows::UI::Core::PointerEventArgs^ e); #endif Platform::String^ _pageKey; Windows::Foundation::EventRegistrationToken _loadedEventToken; Windows::Foundation::EventRegistrationToken _unloadedEventToken; void OnLoaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); void OnUnloaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); ~NavigationHelper(); }; } }
現在使用下列程式碼,於相同資料夾中新增實作檔案 NavigationHelper.cpp:
// // NavigationHelper.cpp // Implementation of the NavigationHelper and associated classes // #include "pch.h" #include "NavigationHelper.h" #include "RelayCommand.h" #include "SuspensionManager.h" using namespace SimpleBlogReader::Common; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::System; using namespace Windows::UI::Core; using namespace Windows::UI::ViewManagement; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Interop; using namespace Windows::UI::Xaml::Navigation; #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP using namespace Windows::Phone::UI::Input; #endif /// <summary> /// Initializes a new instance of the <see cref="LoadStateEventArgs"/> class. /// </summary> /// <param name="navigationParameter"> /// The parameter value passed to <see cref="Frame->Navigate(Type, Object)"/> /// when this page was initially requested. /// </param> /// <param name="pageState"> /// A dictionary of state preserved by this page during an earlier /// session. This will be null the first time a page is visited. /// </param> LoadStateEventArgs::LoadStateEventArgs(Object^ navigationParameter, IMap<String^, Object^>^ pageState) { _navigationParameter = navigationParameter; _pageState = pageState; } /// <summary> /// Gets the <see cref="NavigationParameter"/> property of <see cref"LoadStateEventArgs"/> class. /// </summary> Object^ LoadStateEventArgs::NavigationParameter::get() { return _navigationParameter; } /// <summary> /// Gets the <see cref="PageState"/> property of <see cref"LoadStateEventArgs"/> class. /// </summary> IMap<String^, Object^>^ LoadStateEventArgs::PageState::get() { return _pageState; } /// <summary> /// Initializes a new instance of the <see cref="SaveStateEventArgs"/> class. /// </summary> /// <param name="pageState">An empty dictionary to be populated with serializable state.</param> SaveStateEventArgs::SaveStateEventArgs(IMap<String^, Object^>^ pageState) { _pageState = pageState; } /// <summary> /// Gets the <see cref="PageState"/> property of <see cref"SaveStateEventArgs"/> class. /// </summary> IMap<String^, Object^>^ SaveStateEventArgs::PageState::get() { return _pageState; } /// <summary> /// Initializes a new instance of the <see cref="NavigationHelper"/> class. /// </summary> /// <param name="page">A reference to the current page used for navigation. /// This reference allows for frame manipulation and to ensure that keyboard /// navigation requests only occur when the page is occupying the entire window.</param> NavigationHelper::NavigationHelper(Page^ page, RelayCommand^ goBack, RelayCommand^ goForward) : _page(page), _goBackCommand(goBack), _goForwardCommand(goForward) { // When this page is part of the visual tree make two changes: // 1) Map application view state to visual state for the page // 2) Handle hardware navigation requests _loadedEventToken = page->Loaded += ref new RoutedEventHandler(this, &NavigationHelper::OnLoaded); //// Undo the same changes when the page is no longer visible _unloadedEventToken = page->Unloaded += ref new RoutedEventHandler(this, &NavigationHelper::OnUnloaded); } NavigationHelper::~NavigationHelper() { delete _goBackCommand; delete _goForwardCommand; _page = nullptr; } /// <summary> /// Invoked when the page is part of the visual tree /// </summary> /// <param name="sender">Instance that triggered the event.</param> /// <param name="e">Event data describing the conditions that led to the event.</param> void NavigationHelper::OnLoaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP _backPressedEventToken = HardwareButtons::BackPressed += ref new EventHandler<BackPressedEventArgs^>(this, &NavigationHelper::HardwareButton_BackPressed); #else Page ^page = _page.Resolve<Page>(); // Keyboard and mouse navigation only apply when occupying the entire window if (page != nullptr && page->ActualHeight == Window::Current->Bounds.Height && page->ActualWidth == Window::Current->Bounds.Width) { // Listen to the window directly so focus isn't required _acceleratorKeyEventToken = Window::Current->CoreWindow->Dispatcher->AcceleratorKeyActivated += ref new TypedEventHandler<CoreDispatcher^, AcceleratorKeyEventArgs^>(this, &NavigationHelper::CoreDispatcher_AcceleratorKeyActivated); _pointerPressedEventToken = Window::Current->CoreWindow->PointerPressed += ref new TypedEventHandler<CoreWindow^, PointerEventArgs^>(this, &NavigationHelper::CoreWindow_PointerPressed); _navigationShortcutsRegistered = true; } #endif } /// <summary> /// Invoked when the page is removed from visual tree /// </summary> /// <param name="sender">Instance that triggered the event.</param> /// <param name="e">Event data describing the conditions that led to the event.</param> void NavigationHelper::OnUnloaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP HardwareButtons::BackPressed -= _backPressedEventToken; #else if (_navigationShortcutsRegistered) { Window::Current->CoreWindow->Dispatcher->AcceleratorKeyActivated -= _acceleratorKeyEventToken; Window::Current->CoreWindow->PointerPressed -= _pointerPressedEventToken; _navigationShortcutsRegistered = false; } #endif // Remove handler and release the reference to page Page ^page = _page.Resolve<Page>(); if (page != nullptr) { page->Loaded -= _loadedEventToken; page->Unloaded -= _unloadedEventToken; delete _goBackCommand; delete _goForwardCommand; _goForwardCommand = nullptr; _goBackCommand = nullptr; } } #pragma region Navigation support /// <summary> /// Method used by the <see cref="GoBackCommand"/> property /// to determine if the <see cref="Frame"/> can go back. /// </summary> /// <returns> /// true if the <see cref="Frame"/> has at least one entry /// in the back navigation history. /// </returns> bool NavigationHelper::CanGoBack() { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frame = page->Frame; return (frame != nullptr && frame->CanGoBack); } return false; } /// <summary> /// Method used by the <see cref="GoBackCommand"/> property /// to invoke the <see cref="Windows::UI::Xaml::Controls::Frame::GoBack"/> method. /// </summary> void NavigationHelper::GoBack() { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frame = page->Frame; if (frame != nullptr && frame->CanGoBack) { frame->GoBack(); } } } /// <summary> /// Method used by the <see cref="GoForwardCommand"/> property /// to determine if the <see cref="Frame"/> can go forward. /// </summary> /// <returns> /// true if the <see cref="Frame"/> has at least one entry /// in the forward navigation history. /// </returns> bool NavigationHelper::CanGoForward() { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frame = page->Frame; return (frame != nullptr && frame->CanGoForward); } return false; } /// <summary> /// Method used by the <see cref="GoForwardCommand"/> property /// to invoke the <see cref="Windows::UI::Xaml::Controls::Frame::GoBack"/> method. /// </summary> void NavigationHelper::GoForward() { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frame = page->Frame; if (frame != nullptr && frame->CanGoForward) { frame->GoForward(); } } } /// <summary> /// Gets the <see cref="GoBackCommand"/> property of <see cref"NavigationHelper"/> class. /// </summary> RelayCommand^ NavigationHelper::GoBackCommand::get() { if (_goBackCommand == nullptr) { _goBackCommand = ref new RelayCommand( [this](Object^) -> bool { return CanGoBack(); }, [this](Object^) -> void { GoBack(); } ); } return _goBackCommand; } /// <summary> /// Gets the <see cref="GoForwardCommand"/> property of <see cref"NavigationHelper"/> class. /// </summary> RelayCommand^ NavigationHelper::GoForwardCommand::get() { if (_goForwardCommand == nullptr) { _goForwardCommand = ref new RelayCommand( [this](Object^) -> bool { return CanGoForward(); }, [this](Object^) -> void { GoForward(); } ); } return _goForwardCommand; } #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP /// <summary> /// Handles the back button press and navigates through the history of the root frame. /// </summary> void NavigationHelper::HardwareButton_BackPressed(Object^ sender, BackPressedEventArgs^ e) { if (this->GoBackCommand->CanExecute(nullptr)) { e->Handled = true; this->GoBackCommand->Execute(nullptr); } } #else /// <summary> /// Invoked on every keystroke, including system keys such as Alt key combinations, when /// this page is active and occupies the entire window. Used to detect keyboard navigation /// between pages even when the page itself doesn't have focus. /// </summary> /// <param name="sender">Instance that triggered the event.</param> /// <param name="e">Event data describing the conditions that led to the event.</param> void NavigationHelper::CoreDispatcher_AcceleratorKeyActivated(CoreDispatcher^ sender, AcceleratorKeyEventArgs^ e) { sender; // Unused parameter auto virtualKey = e->VirtualKey; // Only investigate further when Left, Right, or the dedicated Previous or Next keys // are pressed if ((e->EventType == CoreAcceleratorKeyEventType::SystemKeyDown || e->EventType == CoreAcceleratorKeyEventType::KeyDown) && (virtualKey == VirtualKey::Left || virtualKey == VirtualKey::Right || virtualKey == VirtualKey::GoBack || virtualKey == VirtualKey::GoForward)) { auto coreWindow = Window::Current->CoreWindow; auto downState = Windows::UI::Core::CoreVirtualKeyStates::Down; bool menuKey = (coreWindow->GetKeyState(VirtualKey::Menu) & downState) == downState; bool controlKey = (coreWindow->GetKeyState(VirtualKey::Control) & downState) == downState; bool shiftKey = (coreWindow->GetKeyState(VirtualKey::Shift) & downState) == downState; bool noModifiers = !menuKey && !controlKey && !shiftKey; bool onlyAlt = menuKey && !controlKey && !shiftKey; if ((virtualKey == VirtualKey::GoBack && noModifiers) || (virtualKey == VirtualKey::Left && onlyAlt)) { // When the previous key or Alt+Left are pressed navigate back e->Handled = true; GoBackCommand->Execute(this); } else if ((virtualKey == VirtualKey::GoForward && noModifiers) || (virtualKey == VirtualKey::Right && onlyAlt)) { // When the next key or Alt+Right are pressed navigate forward e->Handled = true; GoForwardCommand->Execute(this); } } } /// <summary> /// Invoked on every mouse click, touch screen tap, or equivalent interaction when this /// page is active and occupies the entire window. Used to detect browser-style next and /// previous mouse button clicks to navigate between pages. /// </summary> /// <param name="sender">Instance that triggered the event.</param> /// <param name="e">Event data describing the conditions that led to the event.</param> void NavigationHelper::CoreWindow_PointerPressed(CoreWindow^ sender, PointerEventArgs^ e) { auto properties = e->CurrentPoint->Properties; // Ignore button chords with the left, right, and middle buttons if (properties->IsLeftButtonPressed || properties->IsRightButtonPressed || properties->IsMiddleButtonPressed) { return; } // If back or foward are pressed (but not both) navigate appropriately bool backPressed = properties->IsXButton1Pressed; bool forwardPressed = properties->IsXButton2Pressed; if (backPressed ^ forwardPressed) { e->Handled = true; if (backPressed) { if (GoBackCommand->CanExecute(this)) { GoBackCommand->Execute(this); } } else { if (GoForwardCommand->CanExecute(this)) { GoForwardCommand->Execute(this); } } } } #endif #pragma endregion #pragma region Process lifetime management /// <summary> /// Invoked when this page is about to be displayed in a Frame. /// </summary> /// <param name="e">Event data that describes how this page was reached. The Parameter /// property provides the group to be displayed.</param> void NavigationHelper::OnNavigatedTo(NavigationEventArgs^ e) { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frameState = SuspensionManager::SessionStateForFrame(page->Frame); _pageKey = "Page-" + page->Frame->BackStackDepth; if (e->NavigationMode == NavigationMode::New) { // Clear existing state for forward navigation when adding a new page to the // navigation stack auto nextPageKey = _pageKey; int nextPageIndex = page->Frame->BackStackDepth; while (frameState->HasKey(nextPageKey)) { frameState->Remove(nextPageKey); nextPageIndex++; nextPageKey = "Page-" + nextPageIndex; } // Pass the navigation parameter to the new page LoadState(this, ref new LoadStateEventArgs(e->Parameter, nullptr)); } else { // Pass the navigation parameter and preserved page state to the page, using // the same strategy for loading suspended state and recreating pages discarded // from cache LoadState(this, ref new LoadStateEventArgs(e->Parameter, safe_cast<IMap<String^, Object^>^>(frameState->Lookup(_pageKey)))); } } } /// <summary> /// Invoked when this page will no longer be displayed in a Frame. /// </summary> /// <param name="e">Event data that describes how this page was reached. The Parameter /// property provides the group to be displayed.</param> void NavigationHelper::OnNavigatedFrom(NavigationEventArgs^ e) { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frameState = SuspensionManager::SessionStateForFrame(page->Frame); auto pageState = ref new Map<String^, Object^>(); SaveState(this, ref new SaveStateEventArgs(pageState)); frameState->Insert(_pageKey, pageState); } } #pragma endregion
現在將包含 NavigationHelper 的程式碼新增至 MainPage.xaml.h 標頭檔,以及我們稍後需要用到的 DefaultViewModel 屬性。
// // MainPage.xaml.h // Declaration of the MainPage class // #pragma once #include "MainPage.g.h" #include "Common\NavigationHelper.h" namespace SimpleBlogReader { namespace WFC = Windows::Foundation::Collections; namespace WUIX = Windows::UI::Xaml; namespace WUIXNav = Windows::UI::Xaml::Navigation; namespace WUIXControls = Windows::UI::Xaml::Controls; /// <summary> /// A basic page that provides characteristics common to most applications. /// </summary> [Windows::Foundation::Metadata::WebHostHidden] public ref class MainPage sealed { public: MainPage(); /// <summary> /// Gets the view model for this <see cref="Page"/>. /// This can be changed to a strongly typed view model. /// </summary> property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel { WFC::IObservableMap<Platform::String^, Platform::Object^>^ get(); } /// <summary> /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>. /// </summary> property Common::NavigationHelper^ NavigationHelper { Common::NavigationHelper^ get(); } protected: virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override; virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override; private: void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e); void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e); static WUIX::DependencyProperty^ _defaultViewModelProperty; static WUIX::DependencyProperty^ _navigationHelperProperty; }; }
在 MainPage.xaml.cpp 中,新增 NavigationHelper 的實作和用於載入與儲存狀態的虛設常式,以及 DefaultViewModel 屬性。您也會使用 namespace 指示詞來新增必要項,因此,最終程式碼看起來像這樣:
// // MainPage.xaml.cpp // Implementation of the MainPage class // #include "pch.h" #include "MainPage.xaml.h" using namespace SimpleBlogReader; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Controls::Primitives; using namespace Windows::UI::Xaml::Data; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Media; using namespace Windows::UI::Xaml::Navigation; using namespace Windows::UI::Xaml::Interop; // The Basic Page item template is documented at https://go.microsoft.com/fwlink/?LinkID=390556 MainPage::MainPage() { InitializeComponent(); SetValue(_defaultViewModelProperty, ref new Platform::Collections::Map<String^, Object^>(std::less<String^>())); auto navigationHelper = ref new Common::NavigationHelper(this); SetValue(_navigationHelperProperty, navigationHelper); navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &MainPage::LoadState); navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &MainPage::SaveState); } DependencyProperty^ MainPage::_defaultViewModelProperty = DependencyProperty::Register("DefaultViewModel", TypeName(IObservableMap<String^, Object^>::typeid), TypeName(MainPage::typeid), nullptr); /// <summary> /// Used as a trivial view model. /// </summary> IObservableMap<String^, Object^>^ MainPage::DefaultViewModel::get() { return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty)); } DependencyProperty^ MainPage::_navigationHelperProperty = DependencyProperty::Register("NavigationHelper", TypeName(Common::NavigationHelper::typeid), TypeName(MainPage::typeid), nullptr); /// <summary> /// Gets an implementation of <see cref="NavigationHelper"/> designed to be /// used as a trivial view model. /// </summary> Common::NavigationHelper^ MainPage::NavigationHelper::get() { return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty)); } #pragma region Navigation support /// The methods provided in this section are simply used to allow /// NavigationHelper to respond to the page's navigation methods. /// /// Page specific logic should be placed in event handlers for the /// <see cref="NavigationHelper::LoadState"/> /// and <see cref="NavigationHelper::SaveState"/>. /// The navigation parameter is available in the LoadState method /// in addition to page state preserved during an earlier session. void MainPage::OnNavigatedTo(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedTo(e); } void MainPage::OnNavigatedFrom(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedFrom(e); } #pragma endregion /// <summary> /// Populates the page with content passed during navigation. Any saved state is also /// provided when recreating a page from a prior session. /// </summary> /// <param name="sender"> /// The source of the event; typically <see cref="NavigationHelper"/> /// </param> /// <param name="e">Event data that provides both the navigation parameter passed to /// <see cref="Frame::Navigate(Type, Object)"/> when this page was initially requested and /// a dictionary of state preserved by this page during an earlier /// session. The state will be null the first time a page is visited.</param> void MainPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) { (void) sender; // Unused parameter (void) e; // Unused parameter } /// <summary> /// Preserves state associated with this page in case the application is suspended or the /// page is discarded from the navigation cache. Values must conform to the serialization /// requirements of <see cref="SuspensionManager::SessionState"/>. /// </summary> /// <param name="sender">The source of the event; typically <see cref="NavigationHelper"/></param> /// <param name="e">Event data that provides an empty dictionary to be populated with /// serializable state.</param> void MainPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e) { (void) sender; // Unused parameter (void) e; // Unused parameter }
繼續在 MainPage.xaml (Windows Phone 8.1) 中,將頁面往下移動、找出「標題面板」註解,然後移除整個 StackPanel。在手機上,我們需要實際的螢幕空間來列出部落格摘要。
頁面再往下移動您將會看到包含這個註解的資料格:
"TODO: Content should be placed within the following grid"
。請將這個 ListView 放入該資料格內部:<!-- Vertical scrolling item list --> <ListView x:Name="itemListView" AutomationProperties.AutomationId="itemListView" AutomationProperties.Name="Items" TabIndex="1" IsItemClickEnabled="True" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" IsSwipeEnabled="False" ItemClick="ItemListView_ItemClick" SelectionMode="Single" ItemTemplate="{StaticResource ListItemTemplate}"> <ListView.ItemContainerStyle> <Style TargetType="FrameworkElement"> <Setter Property="Margin" Value="2,0,0,2"/> </Style> </ListView.ItemContainerStyle> </ListView>
現在將游標放在
ItemListView_ItemClick
事件上方,並按下 F12 (移至定義)。Visual Studio 將會為我們產生空的事件處理常式函式。我們稍後將會對其新增一些程式碼。現在我們只需要產生函式,就能編譯 app。
第八部分:列出文章及顯示所選文章的文字檢視
在這個部分中,我們將會新增兩個頁面到 Phone 應用程式:列出文章的頁面和顯示所選文章之文字版本的頁面。在 Windows 應用程式中,我們只需要新增一個稱為 SplitPage 的單一頁面,該頁面將會在其中一側顯示清單,在另一側顯示所選文章的文字。首先是 Phone 頁面。
新增 XAML 標記 (Phone 應用程式 FeedPage)
我們繼續停留在 Phone 專案中處理 FeedPage,它會列出使用者選取之摘要的文章。
****
在 FeedPage.xaml (Windows Phone 8.1) 中,新增資料內容至頁面元素:
DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
現在,在開啟的頁面元素後面新增 CollectionViewSource:
<Page.Resources> <!-- Collection of items displayed by this page --> <CollectionViewSource x:Name="itemsViewSource" Source="{Binding Items}"/> </Page.Resources>
在 Grid 元素中,新增這個 StackPanel:
<!-- TitlePanel --> <StackPanel Grid.Row="0" Margin="24,17,0,28"> <TextBlock Text="{StaticResource AppName}" Style="{ThemeResource TitleTextBlockStyle}" Typography.Capitals="SmallCaps"/> </StackPanel>
接下來,在資料格 (就在開啟的元素正後方) 內部新增 ListView:
<!-- Vertical scrolling item list --> <ListView x:Name="itemListView" AutomationProperties.AutomationId="ItemsListView" AutomationProperties.Name="Items" TabIndex="1" Grid.Row="1" Margin="-10,-10,0,0" IsItemClickEnabled="True" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" IsSwipeEnabled="False" ItemClick="ItemListView_ItemClick" ItemTemplate="{StaticResource ListItemTemplate}"> <ListView.ItemContainerStyle> <Style TargetType="FrameworkElement"> <Setter Property="Margin" Value="2,0,0,2"/> </Style> </ListView.ItemContainerStyle> </ListView>
請注意,ListView
ItemsSource
屬性會繫結至CollectionViewSource
,它會繫結至我們在程式碼後置的 LoadState 中 DefaultViewModel 屬性內插入的FeedData::Items
屬性 (如下所示)。有一個已在 ListView 中宣告的 ItemClick 事件。將游標移到它的上方,然後按 F12 來於程式碼後置中產生事件處理常式。我們現在將它保持空白。
LoadState 與 SaveState (Phone 應用程式 FeedPage)
在 MainPage 中,我們不需要擔心儲存狀態,因為只要在應用程式因為任何原因而啟動時,頁面一律會從網際網路執行完全重新初始化。其他頁面則需要記住其狀態。例如,如果應用程式在顯示 FeedPage 時被終止 (從記憶體卸載),當使用者瀏覽回該應用程式時,我們希望應用程式看起來就像從未終止一樣。因此,我們需要記住有哪些摘要已經被選取。儲存此資料的位置是在本機 AppData 存放區中,而當使用者在 MainPage 中按一下該資料時就是儲存此資料的好機會。
這裡只有一件事比較複雜,那就是資料是否已經實際存在?如果我們正在透過使用者的按一下動作從 MainPage 瀏覽至 FeedPage,我們就可以確定選取的 FeedData 物件已經存在,因為它如果不存在就不會顯示在 MainPage 清單中。但是,如果應用程式是從暫停或終止狀態恢復繼續執行,當 FeedPage 嘗試繫結至上次檢視的 FeedData 物件時,該物件可能尚未載入。因此,FeedPage (與其他頁面) 需要一種方式來知道 FeedData 何時可用。Concurrency::task_completion_event 就是專門針對這類型情況所設計。無論我們是從暫停或終止狀態恢復繼續執行,或是從 MainPage 重新瀏覽,都可以使用它,於相同的程式碼路徑中安全地取得 FeedData 物件。我們一律透過呼叫 GetCurrentFeedAsync,從 FeedPage 取得摘要。如果是從 MainPage 瀏覽,當使用者按一下某個摘要時,事件已經設定,因此方法將會立即傳回摘要。如果我們是從暫停狀態恢復繼續執行,事件會在 FeedDataSource::InitDataSource 函式中設定,並且在那樣的情況下,FeedPage 可能必須稍微等待摘要重新載入。在此情況下,等待會比當機好。這個小小的複雜性問題是 FeedData.cpp 和 App.xaml.cpp 中出現許多複雜難懂的非同步程式碼的原因,但是如果您仔細查看該程式碼,就會發現其實可能沒那麼複雜。
在 FeedPage.xaml.cpp 中,新增此命名空間來將工作物件納入範圍中:
using namespace concurrency;
TextViewerPage.xaml.h 的 #include 指示詞:
#include "TextViewerPage.xaml.h"
在對 Navigate 的呼叫中,需有 TextViewerPage 類別定義,如下所示。
請使用此程式碼取代
LoadState
方法:void FeedPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) { (void)sender; // Unused parameter if (!this->DefaultViewModel->HasKey("Feed")) { auto app = safe_cast<App^>(App::Current); app->GetCurrentFeedAsync().then([this, e](FeedData^ fd) { // Insert into the ViewModel for this page to // initialize itemsViewSource->View this->DefaultViewModel->Insert("Feed", fd); this->DefaultViewModel->Insert("Items", fd->Items); }, task_continuation_context::use_current()); } }
如果我們是從頁面堆疊中較上方的頁面瀏覽回 FeedPage,那麼頁面將會是已初始化的狀態 (也就是 DefaultViewModel 將會有 "Feed" 的值),而且目前的摘要將會是已正確設定的狀態。但是,如果我們是從 MainPage 繼續向前瀏覽,或是從暫停或終止狀態恢復繼續執行,那麼我們將會需要取得目前的摘要,以在頁面中填入正確的資料。如有需要,GetCurrentFeedAsync 將會在恢復繼續執行之後等待摘要資料到達。我們會指定 use_current() 內容來告知工作先返回 UI 執行緒,然後再嘗試存取 DefaultViewModel 相依性屬性。通常 XAML 相關物件無法從背景執行緒直接存取。
我們在此頁面中不使用
SaveState
執行任何動作,因為在載入頁面時,會從 GetCurrentFeedAsync 方法取得我們的狀態。在標頭檔 FeedPage.xaml.h 中新增 LoadState 的宣告、新增 "Common\NavigationHelper.h" 的 include 指示詞,然後新增 NavigationHelper 和 DefaultViewModel 屬性。
// // FeedPage.xaml.h // Declaration of the FeedPage class // #pragma once #include "FeedPage.g.h" #include "Common\NavigationHelper.h" namespace SimpleBlogReader { namespace WFC = Windows::Foundation::Collections; namespace WUIX = Windows::UI::Xaml; namespace WUIXNav = Windows::UI::Xaml::Navigation; namespace WUIXControls = Windows::UI::Xaml::Controls; /// <summary> /// A basic page that provides characteristics common to most applications. /// </summary> [Windows::Foundation::Metadata::WebHostHidden] public ref class FeedPage sealed { public: FeedPage(); /// <summary> /// Gets the view model for this <see cref="Page"/>. /// This can be changed to a strongly typed view model. /// </summary> property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel { WFC::IObservableMap<Platform::String^, Platform::Object^>^ get(); } /// <summary> /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>. /// </summary> property Common::NavigationHelper^ NavigationHelper { Common::NavigationHelper^ get(); } protected: virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override; virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override; private: void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e); void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e); static Windows::UI::Xaml::DependencyProperty^ _defaultViewModelProperty; static Windows::UI::Xaml::DependencyProperty^ _navigationHelperProperty; void ItemListView_ItemClick(Platform::Object^ sender, WUIXControls::ItemClickEventArgs^ e); }; }
在 FeedPage.xaml.cpp 中新增這些屬性的實作,現在看起來像這樣:
// // FeedPage.xaml.cpp // Implementation of the FeedPage class // #include "pch.h" #include "FeedPage.xaml.h" #include "TextViewerPage.xaml.h" using namespace SimpleBlogReader; using namespace concurrency; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::Graphics::Display; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Controls::Primitives; using namespace Windows::UI::Xaml::Data; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Interop; using namespace Windows::UI::Xaml::Media; using namespace Windows::UI::Xaml::Navigation; // The Basic Page item template is documented at https://go.microsoft.com/fwlink/?LinkID=390556 FeedPage::FeedPage() { InitializeComponent(); SetValue(_defaultViewModelProperty, ref new Platform::Collections::Map<String^, Object^>(std::less<String^>())); auto navigationHelper = ref new Common::NavigationHelper(this); SetValue(_navigationHelperProperty, navigationHelper); navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &FeedPage::LoadState); navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &FeedPage::SaveState); } DependencyProperty^ FeedPage::_defaultViewModelProperty = DependencyProperty::Register("DefaultViewModel", TypeName(IObservableMap<String^, Object^>::typeid), TypeName(FeedPage::typeid), nullptr); /// <summary> /// Used as a trivial view model. /// </summary> IObservableMap<String^, Object^>^ FeedPage::DefaultViewModel::get() { return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty)); } DependencyProperty^ FeedPage::_navigationHelperProperty = DependencyProperty::Register("NavigationHelper", TypeName(Common::NavigationHelper::typeid), TypeName(FeedPage::typeid), nullptr); /// <summary> /// Gets an implementation of <see cref="NavigationHelper"/> designed to be /// used as a trivial view model. /// </summary> Common::NavigationHelper^ FeedPage::NavigationHelper::get() { return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty)); } #pragma region Navigation support /// The methods provided in this section are simply used to allow /// NavigationHelper to respond to the page's navigation methods. /// /// Page specific logic should be placed in event handlers for the /// <see cref="NavigationHelper::LoadState"/> /// and <see cref="NavigationHelper::SaveState"/>. /// The navigation parameter is available in the LoadState method /// in addition to page state preserved during an earlier session. void FeedPage::OnNavigatedTo(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedTo(e); } void FeedPage::OnNavigatedFrom(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedFrom(e); } #pragma endregion /// <summary> /// Populates the page with content passed during navigation. Any saved state is also /// provided when recreating a page from a prior session. /// </summary> /// <param name="sender"> /// The source of the event; typically <see cref="NavigationHelper"/> /// </param> /// <param name="e">Event data that provides both the navigation parameter passed to /// <see cref="Frame::Navigate(Type, Object)"/> when this page was initially requested and /// a dictionary of state preserved by this page during an earlier /// session. The state will be null the first time a page is visited.</param> void FeedPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) { (void)sender; // Unused parameter if (!this->DefaultViewModel->HasKey("Feed")) { auto app = safe_cast<App^>(App::Current); app->GetCurrentFeedAsync().then([this, e](FeedData^ fd) { // Insert into the ViewModel for this page to // initialize itemsViewSource->View this->DefaultViewModel->Insert("Feed", fd); this->DefaultViewModel->Insert("Items", fd->Items); }, task_continuation_context::use_current()); } } /// <summary> /// Preserves state associated with this page in case the application is suspended or the /// page is discarded from the navigation cache. Values must conform to the serialization /// requirements of <see cref="SuspensionManager::SessionState"/>. /// </summary> /// <param name="sender">The source of the event; typically <see cref="NavigationHelper"/></param> /// <param name="e">Event data that provides an empty dictionary to be populated with /// serializable state.</param> void FeedPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e) { (void)sender; // Unused parameter }
EventHandlers (Phone app FeedPage)
我們會在 FeedPage 上處理一個事件,也就是會向前瀏覽至使用者可讀取文章之所在頁面的 ItemClick 事件。當您在 XAML 中的事件名稱上按下 F12 時,您已經建立一個虛設常式處理常式。
現在讓我們使用此程式碼來取代實作。
void FeedPage::ItemListView_ItemClick(Platform::Object^ sender, ItemClickEventArgs^ e) { FeedItem^ clickedItem = dynamic_cast<FeedItem^>(e->ClickedItem); this->Frame->Navigate(TextViewerPage::typeid, clickedItem->Link->AbsoluteUri); }
按下 F5 來於模擬器中建置並執行 Phone 應用程式。現在,當您從 MainPage 選取項目時,應用程式應該會瀏覽到 FeedPage 並顯示摘要的清單。下一個步驟是顯示所選摘要的文字。
新增 XAML 標記 (Phone 應用程式 TextViewerPage)
在 Phone 專案中,於 TextViewerPage.xaml 中使用此標記來取代標題面板和內容資料格,將會顯示 app 名稱 (以低調的方式) 與目前文章的標題,以及顯示內容的簡單文字:
<!-- TitlePanel --> <StackPanel Grid.Row="0" Margin="24,17,0,28"> <TextBlock Text="{StaticResource AppName}" Style="{ThemeResource TitleTextBlockStyle}" Typography.Capitals="SmallCaps"/> <TextBlock x:Name="FeedItemTitle" Margin="0,12,0,0" Style="{StaticResource SubheaderTextBlockStyle}" TextWrapping="Wrap"/> </StackPanel> <!--TODO: Content should be placed within the following grid--> <Grid Grid.Row="1" x:Name="ContentRoot"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <ScrollViewer x:Name="itemDetail" AutomationProperties.AutomationId="ItemDetailScrollViewer" Grid.Row="1" Padding="20,20,20,20" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollMode="Disabled" ScrollViewer.VerticalScrollMode="Enabled" ScrollViewer.ZoomMode="Disabled" Margin="4,0,-4,0"> <!--Border enables background color for rich text block--> <Border x:Name="contentViewBorder" BorderBrush="#FFFE5815" Background="AntiqueWhite" BorderThickness="6" Grid.Row="1"> <RichTextBlock x:Name="BlogTextBlock" Foreground="Black" FontFamily="Segoe WP" FontSize="24" Padding="10,10,10,10" VerticalAlignment="Bottom" > </RichTextBlock> </Border> </ScrollViewer> </Grid>
在 TextViewerPage.xaml.h 中,新增 NavigationHelper 和 DefaultViewItems 屬性,而且在我們第一次使用 GetFeedItem 函式查閱目前的摘要項目之後,還會新增 Private 成員 m_FeedItem 來儲存對它的參考,而我們已在上一個步驟中將此函式新增到 App 類別。
此外,新增 RichTextHyperlinkClicked 函式。 TextViewerPage.xaml.h 現在看起來應該就像這樣:
// // TextViewerPage.xaml.h // Declaration of the TextViewerPage class // #pragma once #include "TextViewerPage.g.h" #include "Common\NavigationHelper.h" namespace SimpleBlogReader { namespace WFC = Windows::Foundation::Collections; namespace WUIX = Windows::UI::Xaml; namespace WUIXNav = Windows::UI::Xaml::Navigation; namespace WUIXDoc = Windows::UI::Xaml::Documents; namespace WUIXControls = Windows::UI::Xaml::Controls; /// <summary> /// A basic page that provides characteristics common to most applications. /// </summary> [Windows::Foundation::Metadata::WebHostHidden] public ref class TextViewerPage sealed { public: TextViewerPage(); /// <summary> /// Gets the view model for this <see cref="Page"/>. /// This can be changed to a strongly typed view model. /// </summary> property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel { WFC::IObservableMap<Platform::String^, Platform::Object^>^ get(); } /// <summary> /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>. /// </summary> property Common::NavigationHelper^ NavigationHelper { Common::NavigationHelper^ get(); } protected: virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override; virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override; void RichTextHyperlinkClicked(WUIXDoc::Hyperlink^ link, WUIXDoc::HyperlinkClickEventArgs^ args); private: void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e); void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e); static Windows::UI::Xaml::DependencyProperty^ _defaultViewModelProperty; static Windows::UI::Xaml::DependencyProperty^ _navigationHelperProperty; FeedItem^ m_feedItem; }; }
LoadState 與 SaveState (Phone app TextViewerPage)
在 TextViewerPage.xaml.cpp 中新增這個 Include 指示詞:
#include "WebViewerPage.xaml.h"
新增這兩個 namespace 指示詞:
using namespace concurrency; using namespace Windows::UI::Xaml::Documents;
新增 NavigationHelper 及 DefaultViewModel 的程式碼。
TextViewerPage::TextViewerPage() { InitializeComponent(); SetValue(_defaultViewModelProperty, ref new Platform::Collections::Map<String^, Object^>(std::less<String^>())); auto navigationHelper = ref new Common::NavigationHelper(this); SetValue(_navigationHelperProperty, navigationHelper); navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &TextViewerPage::LoadState); navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &TextViewerPage::SaveState); // this->DataContext = DefaultViewModel; } DependencyProperty^ TextViewerPage::_defaultViewModelProperty = DependencyProperty::Register("DefaultViewModel", TypeName(IObservableMap<String^, Object^>::typeid), TypeName(TextViewerPage::typeid), nullptr); /// <summary> /// Used as a trivial view model. /// </summary> IObservableMap<String^, Object^>^ TextViewerPage::DefaultViewModel::get() { return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty)); } DependencyProperty^ TextViewerPage::_navigationHelperProperty = DependencyProperty::Register("NavigationHelper", TypeName(Common::NavigationHelper::typeid), TypeName(TextViewerPage::typeid), nullptr); /// <summary> /// Gets an implementation of <see cref="NavigationHelper"/> designed to be /// used as a trivial view model. /// </summary> Common::NavigationHelper^ TextViewerPage::NavigationHelper::get() { return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty)); } #pragma region Navigation support /// The methods provided in this section are simply used to allow /// NavigationHelper to respond to the page's navigation methods. /// /// Page specific logic should be placed in event handlers for the /// <see cref="NavigationHelper::LoadState"/> /// and <see cref="NavigationHelper::SaveState"/>. /// The navigation parameter is available in the LoadState method /// in addition to page state preserved during an earlier session. void TextViewerPage::OnNavigatedTo(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedTo(e); } void TextViewerPage::OnNavigatedFrom(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedFrom(e); } #pragma endregion
現在,請使用此程式碼來取代
LoadState
和SaveState
的實作:void TextViewerPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) { (void)sender; // Unused parameter // (void)e; // Unused parameter auto app = safe_cast<App^>(App::Current); app->GetCurrentFeedAsync().then([this, app, e](FeedData^ fd) { m_feedItem = app->GetFeedItem(fd, safe_cast<String^>(e->NavigationParameter)); FeedItemTitle->Text = m_feedItem->Title; BlogTextBlock->Blocks->Clear(); TextHelper^ helper = ref new TextHelper(); auto blocks = helper-> CreateRichText(m_feedItem->Content, ref new TypedEventHandler<Hyperlink^, HyperlinkClickEventArgs^> (this, &TextViewerPage::RichTextHyperlinkClicked)); for (auto b : blocks) { BlogTextBlock->Blocks->Append(b); } }, task_continuation_context::use_current()); } void TextViewerPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e) { (void)sender; // Unused parameter e->PageState->Insert("Uri", m_feedItem->Link->AbsoluteUri); }
我們不能繫結到 RichTextBlock,所以我們會使用 TextHelper 類別手動建構其內容。為了簡單起見,我們使用 HtmlUtilities::ConvertToText 函式,它只會從摘要擷取文字。您可以嘗試自行剖析 HTML 或 XML,以及嘗試將影像連結與文字附加到
Blocks
集合。SyndicationClient 擁有可用於剖析 XML 摘要的函式。部分摘要的 XML 格式是正確的,部分是不正確的。
事件處理常式 (Phone 應用程式 TextViewerPage)
在 TextViewerPage 中,我們會藉由 RTF 中的 Hyperlink 瀏覽到 WebViewerPage。這通常不是在頁面之間瀏覽的方式,但是在此案例中,這似乎是適當的方式,而且這可以讓我們探索超連結如何運作。我們已經將函式簽章新增至 TextViewerPage.xaml.h。現在請在 TextViewerPage.xaml.cpp 中新增實作:
///<summary> /// Invoked when the user clicks on the "Link" text at the top of the rich text /// view of the feed. This navigates to the web page. Identical action to using /// the App bar "forward" button. ///</summary> void TextViewerPage::RichTextHyperlinkClicked(Hyperlink^ hyperLink, HyperlinkClickEventArgs^ args) { this->Frame->Navigate(WebViewerPage::typeid, m_feedItem->Link->AbsoluteUri); }
現在請將 Phone 專案設定為起始專案,然後按下 F5。您應該能夠按一下 [摘要] 頁面中的項目,並瀏覽到您可以在其中閱讀部落格文章的 TextViewerPage。那些部落格中有一些有趣的東西!
新增 XAML (Windows 應用程式 SplitPage)
Windows 應用程式的行為與 Phone 應用程式的行為有幾項差異。我們已經看過 MainPage.xaml 如何在 Windows 專案中使用 ItemsPage 範本,該範本無法在 Phone 應用程式中使用。現在我們即將新增一個 SplitPage,它也無法在 Phone 中使用。當裝置處於橫向方向時,Windows 應用程式中的 SplitPage 會有右側與左側窗格。當使用者瀏覽至我們應用程式中的頁面時,他們將會在左側窗格中看到摘要項目清單,並在右側窗格中看到目前所選摘要的文字轉譯。當裝置處於直向方向,或視窗不是以完整寬度方式顯示時,SplitPage 會使用 VisualStates 來表現,就像有兩個個別頁面一樣。這在程式碼中稱為「邏輯頁面巡覽」。
開始使用下列程式碼,這是適用於基本分割頁面的 xaml,此頁面是 Windows 8 專案中的預設範本。
<Page x:Name="pageRoot" x:Class="SimpleBlogReader.SplitPage" DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:SimpleBlogReader" xmlns:common="using:SimpleBlogReader.Common" xmlns:d="https://schemas.microsoft.com/expression/blend/2008" xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Page.Resources> <!-- Collection of items displayed by this page --> <CollectionViewSource x:Name="itemsViewSource" Source="{Binding Items}"/> </Page.Resources> <Page.TopAppBar> <AppBar Padding="10,0,10,0"> <Grid> <AppBarButton x:Name="fwdButton" Height="95" Margin="150,46,0,0" Command="{Binding NavigationHelper.GoForwardCommand, ElementName=pageRoot}" AutomationProperties.Name="Forward" AutomationProperties.AutomationId="ForwardButton" AutomationProperties.ItemType="Navigation Button" HorizontalAlignment="Right" Icon="Forward" Click="fwdButton_Click"/> </Grid> </AppBar> </Page.TopAppBar> <!-- This grid acts as a root panel for the page that defines two rows: * Row 0 contains the back button and page title * Row 1 contains the rest of the page layout --> <Grid Style="{StaticResource WindowsBlogLayoutRootStyle}"> <Grid.ChildrenTransitions> <TransitionCollection> <EntranceThemeTransition/> </TransitionCollection> </Grid.ChildrenTransitions> <Grid.RowDefinitions> <RowDefinition Height="140"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition x:Name="primaryColumn" Width="420"/> <ColumnDefinition x:Name="secondaryColumn" Width="*"/> </Grid.ColumnDefinitions> <!-- Back button and page title --> <Grid x:Name="titlePanel" Grid.ColumnSpan="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="120"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Button x:Name="backButton" Margin="39,59,39,0" Command="{Binding NavigationHelper.GoBackCommand, ElementName=pageRoot}" Style="{StaticResource NavigationBackButtonNormalStyle}" VerticalAlignment="Top" AutomationProperties.Name="Back" AutomationProperties.AutomationId="BackButton" AutomationProperties.ItemType="Navigation Button"/> <TextBlock x:Name="pageTitle" Grid.Column="1" Text="{Binding Title}" Style="{StaticResource HeaderTextBlockStyle}" IsHitTestVisible="false" TextWrapping="NoWrap" VerticalAlignment="Bottom" Padding="10,10,10,10" Margin="0,0,30,40"> <TextBlock.Transitions> <TransitionCollection> <ContentThemeTransition/> </TransitionCollection> </TextBlock.Transitions> </TextBlock> </Grid> <!-- Vertical scrolling item list --> <ListView x:Name="itemListView" AutomationProperties.AutomationId="ItemsListView" AutomationProperties.Name="Items" TabIndex="1" Grid.Row="1" Margin="-10,-10,0,0" Padding="120,0,0,60" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" IsSwipeEnabled="False" SelectionChanged="ItemListView_SelectionChanged"> <ListView.ItemTemplate> <DataTemplate> <Grid Margin="6"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Border Background="{ThemeResource ListViewItemPlaceholderBackgroundThemeBrush}" Width="60" Height="60"> <Image Source="{Binding ImagePath}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/> </Border> <StackPanel Grid.Column="1" Margin="10,0,0,0"> <TextBlock Text="{Binding Title}" Style="{StaticResource TitleTextBlockStyle}" TextWrapping="NoWrap" MaxHeight="40"/> <TextBlock Text="{Binding Subtitle}" Style="{StaticResource CaptionTextBlockStyle}" TextWrapping="NoWrap"/> </StackPanel> </Grid> </DataTemplate> </ListView.ItemTemplate> <ListView.ItemContainerStyle> <Style TargetType="FrameworkElement"> <Setter Property="Margin" Value="0,0,0,10"/> </Style> </ListView.ItemContainerStyle> </ListView> <!-- Details for selected item --> <ScrollViewer x:Name="itemDetail" AutomationProperties.AutomationId="ItemDetailScrollViewer" Grid.Column="1" Grid.RowSpan="2" Padding="60,0,66,0" DataContext="{Binding SelectedItem, ElementName=itemListView}" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollMode="Disabled" ScrollViewer.VerticalScrollMode="Enabled" ScrollViewer.ZoomMode="Disabled"> <Grid x:Name="itemDetailGrid" Margin="0,60,0,50"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Image Grid.Row="1" Margin="0,0,20,0" Width="180" Height="180" Source="{Binding ImagePath}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/> <StackPanel x:Name="itemDetailTitlePanel" Grid.Row="1" Grid.Column="1"> <TextBlock x:Name="itemTitle" Margin="0,-10,0,0" Text="{Binding Title}" Style="{StaticResource SubheaderTextBlockStyle}"/> <TextBlock x:Name="itemSubtitle" Margin="0,0,0,20" Text="{Binding Subtitle}" Style="{StaticResource SubtitleTextBlockStyle}"/> </StackPanel> <TextBlock Grid.Row="2" Grid.ColumnSpan="2" Margin="0,20,0,0" Text="{Binding Content}" Style="{StaticResource BodyTextBlockStyle}"/> </Grid> </ScrollViewer> <VisualStateManager.VisualStateGroups> <!-- Visual states reflect the application's view state --> <VisualStateGroup x:Name="ViewStates"> <VisualState x:Name="PrimaryView" /> <VisualState x:Name="SinglePane"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="primaryColumn" Storyboard.TargetProperty="Width"> <DiscreteObjectKeyFrame KeyTime="0" Value="*"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="secondaryColumn" Storyboard.TargetProperty="Width"> <DiscreteObjectKeyFrame KeyTime="0" Value="0"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemDetail" Storyboard.TargetProperty="Visibility"> <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemListView" Storyboard.TargetProperty="Padding"> <DiscreteObjectKeyFrame KeyTime="0" Value="120,0,90,60"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> <!-- When an item is selected and only one pane is shown the details display requires more extensive changes: * Hide the master list and the column it was in * Move item details down a row to make room for the title * Move the title directly above the details * Adjust padding for details --> <VisualState x:Name="SinglePane_Detail"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="primaryColumn" Storyboard.TargetProperty="Width"> <DiscreteObjectKeyFrame KeyTime="0" Value="0"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="secondaryColumn" Storyboard.TargetProperty="Width"> <DiscreteObjectKeyFrame KeyTime="0" Value="*"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemListView" Storyboard.TargetProperty="Visibility"> <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="titlePanel" Storyboard.TargetProperty="(Grid.Column)"> <DiscreteObjectKeyFrame KeyTime="0" Value="0"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemDetail" Storyboard.TargetProperty="Padding"> <DiscreteObjectKeyFrame KeyTime="0" Value="10,0,10,0"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> </Grid> </Page>
預設的頁面已經有其資料內容,而且已經設定 CollectionViewSource。
讓我們來調整 titlePanel 資料格,讓它跨越兩個欄位。這將會讓摘要標題以螢幕完整寬度的方式顯示:
<Grid x:Name="titlePanel" Grid.ColumnSpan="2">
現在請在這個相同的資料格中查詢 pageTitle TextBlock,然後將 Binding 從 Title 變更為 Feed.Title。
Text="{Binding Feed.Title}"
現在請查詢「垂直捲動項目清單」註解,並使用此註解取代預設的 ListView:
<!-- Vertical scrolling item list --> <ListView x:Name="itemListView" AutomationProperties.AutomationId="ItemsListView" AutomationProperties.Name="Items" TabIndex="1" Grid.Row="1" Margin="10,10,0,0" Padding="10,0,0,60" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" IsSwipeEnabled="False" SelectionChanged="ItemListView_SelectionChanged" ItemTemplate="{StaticResource ListItemTemplate}"> <ListView.ItemContainerStyle> <Style TargetType="FrameworkElement"> <Setter Property="Margin" Value="0,0,0,10"/> </Style> </ListView.ItemContainerStyle> </ListView>
SplitPage 的詳細資料窗格可以保留您想要的任何項目。在這個應用程式中,我們將會在應用程式中放入 RichTextBlock,並顯示部落格文章的簡單文字版本。我們可以使用 Windows API 所提供的公用程式函式來從 FeedItem 剖析 HTML 並傳回 Platform::String,然後我們將會使用我們自己的公用程式類別,來將傳回的字串分割成多個段落並建立 RTF 元素。這個檢視將不會顯示任何影像,但是它載入的速度很快,而且如果您想要擴充此應用程式,您可以在之後新增一個選項來讓使用者調整字型和字型大小。
在「選取的項目詳細資料」註解下方尋找 ScrollViewer 元素並將它刪除。然後貼到這個標記中:
<!-- Details for selected item --> <Grid x:Name="itemDetailGrid" Grid.Row="1" Grid.Column="1" Margin="10,10,10,10"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock x:Name="itemTitle" Margin="10,10,10,10" DataContext="{Binding SelectedItem, ElementName=itemListView}" Text="{Binding Title}" Style="{StaticResource SubheaderTextBlockStyle}"/> <ScrollViewer x:Name="itemDetail" AutomationProperties.AutomationId="ItemDetailScrollViewer" Grid.Row="1" Padding="20,20,20,20" DataContext="{Binding SelectedItem, ElementName=itemListView}" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollMode="Disabled" ScrollViewer.VerticalScrollMode="Enabled" ScrollViewer.ZoomMode="Disabled" Margin="4,0,-4,0"> <Border x:Name="contentViewBorder" BorderBrush="#FFFE5815" Background="Honeydew" BorderThickness="5" Grid.Row="1"> <RichTextBlock x:Name="BlogTextBlock" Foreground="Black" FontFamily="Lucida Sans" FontSize="32" Margin="20,20,20,20"> </RichTextBlock> </Border> </ScrollViewer> </Grid>
LoadState 與 SaveState (Windows app SplitPage)
使用下列程式碼來取代您建立的 SplitPage 頁面。
SplitPage.xaml.h 看起來應該就像這樣:
// // SplitPage.xaml.h // Declaration of the SplitPage class // #pragma once #include "SplitPage.g.h" #include "Common\NavigationHelper.h" namespace SimpleBlogReader { namespace WFC = Windows::Foundation::Collections; namespace WUIX = Windows::UI::Xaml; namespace WUIXNav = Windows::UI::Xaml::Navigation; namespace WUIXDoc = Windows::UI::Xaml::Documents; namespace WUIXControls = Windows::UI::Xaml::Controls; /// <summary> /// A page that displays a group title, a list of items within the group, and details for the /// currently selected item. /// </summary> [Windows::Foundation::Metadata::WebHostHidden] public ref class SplitPage sealed { public: SplitPage(); /// <summary> /// This can be changed to a strongly typed view model. /// </summary> property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel { WFC::IObservableMap<Platform::String^, Platform::Object^>^ get(); } /// <summary> /// NavigationHelper is used on each page to aid in navigation and /// process lifetime management /// </summary> property Common::NavigationHelper^ NavigationHelper { Common::NavigationHelper^ get(); } protected: virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override; virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override; private: void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e); void SaveState(Object^ sender, Common::SaveStateEventArgs^ e); bool CanGoBack(); void GoBack(); #pragma region Logical page navigation // The split page isdesigned so that when the Window does have enough space to show // both the list and the dteails, only one pane will be shown at at time. // // This is all implemented with a single physical page that can represent two logical // pages. The code below achieves this goal without making the user aware of the // distinction. void Window_SizeChanged(Platform::Object^ sender, Windows::UI::Core::WindowSizeChangedEventArgs^ e); void ItemListView_SelectionChanged(Platform::Object^ sender, WUIXControls::SelectionChangedEventArgs^ e); bool UsingLogicalPageNavigation(); void InvalidateVisualState(); Platform::String^ DetermineVisualState(); #pragma endregion static Windows::UI::Xaml::DependencyProperty^ _defaultViewModelProperty; static Windows::UI::Xaml::DependencyProperty^ _navigationHelperProperty; static const int MinimumWidthForSupportingTwoPanes = 768; void fwdButton_Click(Platform::Object^ sender, WUIX::RoutedEventArgs^ e); void pageRoot_SizeChanged(Platform::Object^ sender, WUIX::SizeChangedEventArgs^ e); }; }
針對 SplitPage.xaml.cpp,使用下列程式碼做為起點。這會實作基本的分割頁面,並新增 NavigationHelper 和 SuspensionManager 支援 (與您新增到其他頁面的一樣),以及 SizeChanged 事件處理常式 (與前一個頁面一樣)。
// // SplitPage.xaml.cpp // Implementation of the SplitPage class // #include "pch.h" #include "SplitPage.xaml.h" using namespace SimpleBlogReader; using namespace SimpleBlogReader::Common; using namespace Platform; using namespace Platform::Collections; using namespace concurrency; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::UI::Core; using namespace Windows::UI::ViewManagement; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Controls::Primitives; using namespace Windows::UI::Xaml::Data; using namespace Windows::UI::Xaml::Documents; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Interop; using namespace Windows::UI::Xaml::Media; using namespace Windows::UI::Xaml::Navigation; // The Split Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234234 SplitPage::SplitPage() { InitializeComponent(); SetValue(_defaultViewModelProperty, ref new Map<String^, Object^>(std::less<String^>())); auto navigationHelper = ref new Common::NavigationHelper(this, ref new Common::RelayCommand( [this](Object^) -> bool { return CanGoBack(); }, [this](Object^) -> void { GoBack(); } ) ); SetValue(_navigationHelperProperty, navigationHelper); navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &SplitPage::LoadState); navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &SplitPage::SaveState); ItemListView->SelectionChanged += ref new SelectionChangedEventHandler(this, &SplitPage::ItemListView_SelectionChanged); Window::Current->SizeChanged += ref new WindowSizeChangedEventHandler(this, &SplitPage::Window_SizeChanged); InvalidateVisualState(); } DependencyProperty^ SplitPage::_defaultViewModelProperty = DependencyProperty::Register("DefaultViewModel", TypeName(IObservableMap<String^, Object^>::typeid), TypeName(SplitPage::typeid), nullptr); /// <summary> /// used as a trivial view model. /// </summary> IObservableMap<String^, Object^>^ SplitPage::DefaultViewModel::get() { return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty)); } DependencyProperty^ SplitPage::_navigationHelperProperty = DependencyProperty::Register("NavigationHelper", TypeName(Common::NavigationHelper::typeid), TypeName(SplitPage::typeid), nullptr); /// <summary> /// Gets an implementation of <see cref="NavigationHelper"/> designed to be /// used as a trivial view model. /// </summary> Common::NavigationHelper^ SplitPage::NavigationHelper::get() { // return _navigationHelper; return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty)); } #pragma region Page state management /// <summary> /// Populates the page with content passed during navigation. Any saved state is also /// provided when recreating a page from a prior session. /// </summary> /// <param name="navigationParameter">The parameter value passed to /// <see cref="Frame::Navigate(Type, Object)"/> when this page was initially requested. /// </param> /// <param name="pageState">A map of state preserved by this page during an earlier /// session. This will be null the first time a page is visited.</param> void SplitPage::LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e) { if (!this->DefaultViewModel->HasKey("Feed")) { auto app = safe_cast<App^>(App::Current); app->GetCurrentFeedAsync().then([this, app, e](FeedData^ fd) { // Insert into the ViewModel for this page to initialize itemsViewSource->View this->DefaultViewModel->Insert("Feed", fd); this->DefaultViewModel->Insert("Items", fd->Items); if (e->PageState == nullptr) { // When this is a new page, select the first item automatically // unless logical page navigation is being used (see the logical // page navigation #region below). if (!UsingLogicalPageNavigation() && itemsViewSource->View != nullptr) { this->itemsViewSource->View->MoveCurrentToFirst(); } else { this->itemsViewSource->View->MoveCurrentToPosition(-1); } } else { auto itemUri = safe_cast<String^>(e->PageState->Lookup("SelectedItemUri")); auto app = safe_cast<App^>(App::Current); auto selectedItem = app->GetFeedItem(fd, itemUri); if (selectedItem != nullptr) { this->itemsViewSource->View->MoveCurrentTo(selectedItem); } } }, task_continuation_context::use_current()); } } /// <summary> /// Preserves state associated with this page in case the application is suspended or the /// page is discarded from the navigation cache. Values must conform to the serialization /// requirements of <see cref="SuspensionManager::SessionState"/>. /// </summary> /// <param name="sender">The source of the event; typically <see cref="NavigationHelper"/></param> /// <param name="e">Event data that provides an empty dictionary to be populated with /// serializable state.</param> void SplitPage::SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e) { if (itemsViewSource->View != nullptr) { auto selectedItem = itemsViewSource->View->CurrentItem; if (selectedItem != nullptr) { auto feedItem = safe_cast<FeedItem^>(selectedItem); e->PageState->Insert("SelectedItemUri", feedItem->Link->AbsoluteUri); } } } #pragma endregion #pragma region Logical page navigation // Visual state management typically reflects the four application view states directly (full // screen landscape and portrait plus snapped and filled views.) The split page is designed so // that the snapped and portrait view states each have two distinct sub-states: either the item // list or the details are displayed, but not both at the same time. // // This is all implemented with a single physical page that can represent two logical pages. // The code below achieves this goal without making the user aware of the distinction. /// <summary> /// Invoked to determine whether the page should act as one logical page or two. /// </summary> /// <returns>True when the current view state is portrait or snapped, false /// otherwise.</returns> bool SplitPage::CanGoBack() { if (UsingLogicalPageNavigation() && ItemListView->SelectedItem != nullptr) { return true; } else { return NavigationHelper->CanGoBack(); } } void SplitPage::GoBack() { if (UsingLogicalPageNavigation() && ItemListView->SelectedItem != nullptr) { // When logical page navigation is in effect and there's a selected item that // item's details are currently displayed. Clearing the selection will return to // the item list. From the user's point of view this is a logical backward // navigation. ItemListView->SelectedItem = nullptr; } else { NavigationHelper->GoBack(); } } /// <summary> /// Invoked with the Window changes size /// </summary> /// <param name="sender">The current Window</param> /// <param name="e">Event data that describes the new size of the Window</param> void SplitPage::Window_SizeChanged(Platform::Object^ sender, WindowSizeChangedEventArgs^ e) { InvalidateVisualState(); } /// <summary> /// Invoked when an item within the list is selected. /// </summary> /// <param name="sender">The GridView displaying the selected item.</param> /// <param name="e">Event data that describes how the selection was changed.</param> void SplitPage::ItemListView_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e) { if (UsingLogicalPageNavigation()) { InvalidateVisualState(); } } /// <summary> /// Invoked to determine whether the page should act as one logical page or two. /// </summary> /// <returns>True if the window should show act as one logical page, false /// otherwise.</returns> bool SplitPage::UsingLogicalPageNavigation() { return Window::Current->Bounds.Width <= MinimumWidthForSupportingTwoPanes; } void SplitPage::InvalidateVisualState() { auto visualState = DetermineVisualState(); VisualStateManager::GoToState(this, visualState, false); NavigationHelper->GoBackCommand->RaiseCanExecuteChanged(); } /// <summary> /// Invoked to determine the name of the visual state that corresponds to an application /// view state. /// </summary> /// <returns>The name of the desired visual state. This is the same as the name of the /// view state except when there is a selected item in portrait and snapped views where /// this additional logical page is represented by adding a suffix of _Detail.</returns> Platform::String^ SplitPage::DetermineVisualState() { if (!UsingLogicalPageNavigation()) return "PrimaryView"; // Update the back button's enabled state when the view state changes auto logicalPageBack = UsingLogicalPageNavigation() && ItemListView->SelectedItem != nullptr; return logicalPageBack ? "SinglePane_Detail" : "SinglePane"; } #pragma endregion #pragma region Navigation support /// The methods provided in this section are simply used to allow /// NavigationHelper to respond to the page's navigation methods. /// /// Page specific logic should be placed in event handlers for the /// <see cref="NavigationHelper::LoadState"/> /// and <see cref="NavigationHelper::SaveState"/>. /// The navigation parameter is available in the LoadState method /// in addition to page state preserved during an earlier session. void SplitPage::OnNavigatedTo(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedTo(e); } void SplitPage::OnNavigatedFrom(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedFrom(e); } #pragma endregion void SimpleBlogReader::SplitPage::fwdButton_Click(Platform::Object^ sender, RoutedEventArgs^ e) { // Navigate to the appropriate destination page, and configure the new page // by passing required information as a navigation parameter. auto selectedItem = dynamic_cast<FeedItem^>(this->ItemListView->SelectedItem); // selectedItem will be nullptr if the user invokes the app bar // and clicks on "view web page" without selecting an item. if (this->Frame != nullptr && selectedItem != nullptr) { auto itemUri = safe_cast<String^>(selectedItem->Link->AbsoluteUri); this->Frame->Navigate(WebViewerPage::typeid, itemUri); } } /// <summary> /// /// /// </summary> void SimpleBlogReader::SplitPage::pageRoot_SizeChanged( Platform::Object^ sender, SizeChangedEventArgs^ e) { if (e->NewSize.Height / e->NewSize.Width >= 1) { VisualStateManager::GoToState(this, "SinglePane", true); } else { VisualStateManager::GoToState(this, "PrimaryView", true); } }
在 SplitPage.xaml.cpp 中,新增這個 using 指示詞:
using namespace Windows::UI::Xaml::Documents;
現在請使用此程式碼取代
LoadState
和SaveState
:void SplitPage::LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e) { if (!this->DefaultViewModel->HasKey("Feed")) { auto app = safe_cast<App^>(App::Current); app->GetCurrentFeedAsync().then([this, app, e](FeedData^ fd) { // Insert into the ViewModel for this page to initialize itemsViewSource->View this->DefaultViewModel->Insert("Feed", fd); this->DefaultViewModel->Insert("Items", fd->Items); if (e->PageState == nullptr) { // When this is a new page, select the first item automatically unless logical page // navigation is being used (see the logical page navigation #region below). if (!UsingLogicalPageNavigation() && itemsViewSource->View != nullptr) { this->itemsViewSource->View->MoveCurrentToFirst(); } else { this->itemsViewSource->View->MoveCurrentToPosition(-1); } } else { auto itemUri = safe_cast<String^>(e->PageState->Lookup("SelectedItemUri")); auto app = safe_cast<App^>(App::Current); auto selectedItem = GetFeedItem(fd, itemUri); if (selectedItem != nullptr) { this->itemsViewSource->View->MoveCurrentTo(selectedItem); } } }, task_continuation_context::use_current()); } } /// <summary> /// Preserves state associated with this page in case the application is suspended or the /// page is discarded from the navigation cache. Values must conform to the serialization /// requirements of <see cref="SuspensionManager::SessionState"/>. /// </summary> /// <param name="sender">The source of the event; typically <see cref="NavigationHelper"/></param> /// <param name="e">Event data that provides an empty dictionary to be populated with /// serializable state.</param> void SplitPage::SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e) { if (itemsViewSource->View != nullptr) { auto selectedItem = itemsViewSource->View->CurrentItem; if (selectedItem != nullptr) { auto feedItem = safe_cast<FeedItem^>(selectedItem); e->PageState->Insert("SelectedItemUri", feedItem->Link->AbsoluteUri); } } }
請注意,我們現在是使用我們之前新增到共用專案的 GetCurrentFeedAsync 方法。這個頁面與 Phone 頁面之間的一項差異是,現在我們會持續追蹤選取的項目。在
SaveState
中,我們會將目前選取的項目插入 PageState 物件,SuspensionManager 就會在需要時保留該項目,之後在呼叫LoadState
時,我們就可以再度於 PageState 物件中使用該項目。我們將需要該字串來於目前的 Feed 中查詢目前的 FeedItem。
事件處理常式 (Windows 應用程式 SplitePage)
選取的項目變更時,詳細資料窗格將使用 TextHelper
類別來轉譯文字。
在 SplitPage.xaml.cpp 中,新增這些 #include 指示詞:
#include "TextHelper.h" #include "WebViewerPage.xaml.h"
以下列程式碼取代預設的 SelectionChanged 事件處理常式虛設常式:
void SimpleBlogReader::SplitPage::ItemListView_SelectionChanged( Platform::Object^ sender, SelectionChangedEventArgs^ e) { if (UsingLogicalPageNavigation()) { InvalidateVisualState(); } // Sometimes there is no selected item, e.g. when navigating back // from detail in logical page navigation. auto fi = dynamic_cast<FeedItem^>(itemListView->SelectedItem); if (fi != nullptr) { BlogTextBlock->Blocks->Clear(); TextHelper^ helper = ref new TextHelper(); auto blocks = helper->CreateRichText(fi->Content, ref new TypedEventHandler<Hyperlink^, HyperlinkClickEventArgs^>(this, &SplitPage::RichTextHyperlinkClicked)); for (auto b : blocks) { BlogTextBlock->Blocks->Append(b); } } }
此函式指定一個回呼,回呼將傳送至以 RTF 文字建立的 Hyperlink。
在 SplitPage.xaml.h 中新增此私用成員函式:
void RichTextHyperlinkClicked(Windows::UI::Xaml::Documents::Hyperlink^ link, Windows::UI::Xaml::Documents::HyperlinkClickEventArgs^ args);
並在 SplitPage.xaml.cpp 中新增此實作:
/// <summary> /// Navigate to the appropriate destination page, and configure the new page /// by passing required information as a navigation parameter. /// </summary> void SplitPage::RichTextHyperlinkClicked( Hyperlink^ hyperLink, HyperlinkClickEventArgs^ args) { auto selectedItem = dynamic_cast<FeedItem^>(this->itemListView->SelectedItem); // selectedItem will be nullptr if the user invokes the app bar // and clicks on "view web page" without selecting an item. if (this->Frame != nullptr && selectedItem != nullptr) { auto itemUri = safe_cast<String^>(selectedItem->Link->AbsoluteUri); this->Frame->Navigate(WebViewerPage::typeid, itemUri); } }
此函式將參考巡覽堆疊中的下一頁。現在按 F5 便能看到選取變更導致的文字更新。在模擬器中執行,並旋轉虛擬裝置,查看預設 VisualState 物件是否確實如預期般處理直向和橫向方向顯示。按一下部落格文字中的連結文字,並瀏覽至 WebViewerPage。該處目前當然尚無內容;在討論此主題前,我們將先介紹手機專案。
關於返回瀏覽
您可能已經發現,在 Windows 應用程式中,SplitPage 提供返回瀏覽按鈕,可帶您回到 MainPage,而您不需要撰寫額外的程式碼。在手機上,返回按鈕功能是由硬體返回按鈕所提供,而非軟體按鈕。手機返回按鈕瀏覽是由 Common 資料夾中的 NavigationHelper 類別所處理。在方案中搜尋 "BackPressed" (Ctrl + Shift + F),以查看相關程式碼。在此,您仍舊無需進行任何動作。一切都已就緒!
第九部分:新增選取文章的 Web 檢視。
我們將新增的最後一頁,會以原始網頁形式顯示部落格文章。有時候,讀者也會想要看到圖片!檢視網頁的缺點便是手機螢幕上很難閱讀文字,而且並非所有網頁的格式都適用行動裝置。有時候,邊界會超出螢幕,便必須頻繁進行水平捲動。我們的 WebViewerPage 頁面則相當簡單。我們只要在頁面中新增 WebView 控制項,它便能完成所有工作。我們將從手機專案開始:
新增 XAML (Phone 應用程式 WebViewerPage)
在 WebViewerPage.xaml 中,新增標題面板與 contentRoot 格線:
<!-- TitlePanel --> <StackPanel Grid.Row="0" Margin="10,10,10,10"> <TextBlock Text="{StaticResource AppName}" Style="{ThemeResource TitleTextBlockStyle}" Typography.Capitals="SmallCaps"/> </StackPanel> <!--TODO: Content should be placed within the following grid--> <Grid Grid.Row="1" x:Name="ContentRoot"> <!-- Back button and page title --> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!--This will render while web page is still downloading, indicating that something is happening--> <TextBlock x:Name="pageTitle" Text="{Binding Title}" Grid.Column="1" IsHitTestVisible="false" TextWrapping="WrapWholeWords" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="40,20,40,20"/> </Grid>
LoadState 與 SaveState (Phone app WebViewerPage)
就像所有其他頁面一樣,藉由在 WebViewerPage.xaml.h 檔案中提供 NavigationHelper 和 DefaultItems 支援以及 WebViewerPage.xaml.cpp 中的實作來開始 WebViewerPage。
WebViewerPage.xaml.h 應該以如下的方式開始:
// // WebViewerPage.xaml.h // Declaration of the WebViewerPage class // #pragma once #include "WebViewerPage.g.h" #include "Common\NavigationHelper.h" namespace SimpleBlogReader { namespace WFC = Windows::Foundation::Collections; namespace WUIX = Windows::UI::Xaml; namespace WUIXNav = Windows::UI::Xaml::Navigation; namespace WUIXControls = Windows::UI::Xaml::Controls; /// <summary> /// A basic page that provides characteristics common to most applications. /// </summary> [Windows::Foundation::Metadata::WebHostHidden] public ref class WebViewerPage sealed { public: WebViewerPage(); /// <summary> /// This can be changed to a strongly typed view model. /// </summary> property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel { WFC::IObservableMap<Platform::String^, Platform::Object^>^ get(); } /// <summary> /// NavigationHelper is used on each page to aid in navigation and /// process lifetime management /// </summary> property Common::NavigationHelper^ NavigationHelper { Common::NavigationHelper^ get(); } protected: virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override; virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override; private: void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e); void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e); static Windows::UI::Xaml::DependencyProperty^ _defaultViewModelProperty; static Windows::UI::Xaml::DependencyProperty^ _navigationHelperProperty; }; }
WebViewerPage.xaml.cpp 應該以如下的方式開始:
// // WebViewerPage.xaml.cpp // Implementation of the WebViewerPage class // #include "pch.h" #include "WebViewerPage.xaml.h" using namespace SimpleBlogReader; using namespace concurrency; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Controls::Primitives; using namespace Windows::UI::Xaml::Data; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Interop; using namespace Windows::UI::Xaml::Media; using namespace Windows::UI::Xaml::Media::Animation; using namespace Windows::UI::Xaml::Navigation; // The Basic Page item template is documented at // https://go.microsoft.com/fwlink/?LinkId=234237 WebViewerPage::WebViewerPage() { InitializeComponent(); SetValue(_defaultViewModelProperty, ref new Map<String^, Object^>(std::less<String^>())); auto navigationHelper = ref new Common::NavigationHelper(this); SetValue(_navigationHelperProperty, navigationHelper); navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &WebViewerPage::LoadState); navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &WebViewerPage::SaveState); } DependencyProperty^ WebViewerPage::_defaultViewModelProperty = DependencyProperty::Register("DefaultViewModel", TypeName(IObservableMap<String^, Object^>::typeid), TypeName(WebViewerPage::typeid), nullptr); /// <summary> /// used as a trivial view model. /// </summary> IObservableMap<String^, Object^>^ WebViewerPage::DefaultViewModel::get() { return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty)); } DependencyProperty^ WebViewerPage::_navigationHelperProperty = DependencyProperty::Register("NavigationHelper", TypeName(Common::NavigationHelper::typeid), TypeName(WebViewerPage::typeid), nullptr); /// <summary> /// Gets an implementation of <see cref="NavigationHelper"/> designed to be /// used as a trivial view model. /// </summary> Common::NavigationHelper^ WebViewerPage::NavigationHelper::get() { return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty)); } #pragma region Navigation support /// The methods provided in this section are simply used to allow /// NavigationHelper to respond to the page's navigation methods. /// /// Page specific logic should be placed in event handlers for the /// <see cref="NavigationHelper::LoadState"/> /// and <see cref="NavigationHelper::SaveState"/>. /// The navigation parameter is available in the LoadState method /// in addition to page state preserved during an earlier session. void WebViewerPage::OnNavigatedTo(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedTo(e); } void WebViewerPage::OnNavigatedFrom(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedFrom(e); } #pragma endregion
在 WebViewerPage.xaml.h 中,新增這個 Private 成員變數:
Windows::Foundation::Uri^ m_feedItemUri;
在 WebViewerPage.xaml.cpp 中,以下列程式碼取代
LoadState
與SaveState
:void WebViewerPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) { (void)sender; // Unused parameter // Run the PopInThemeAnimation. Storyboard^ sb = dynamic_cast<Storyboard^>(this->FindName("PopInStoryboard")); if (sb != nullptr) { sb->Begin(); } if (e->PageState == nullptr) { m_feedItemUri = safe_cast<String^>(e->NavigationParameter); contentView->Navigate(ref new Uri(m_feedItemUri)); } // We are resuming from suspension: else { m_feedItemUri = safe_cast<String^>(e->PageState->Lookup("FeedItemUri")); contentView->Navigate(ref new Uri(m_feedItemUri)); } } void WebViewerPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e) { (void)sender; // Unused parameter (void)e; // Unused parameter e->PageState->Insert("FeedItemUri", m_feedItemUri); }
請注意函式一開頭的無意義動畫。您可以在 Windows 開發人員中心了解有關動畫的詳細資訊。請注意,在此我們仍舊必須處理到達此頁面的兩種可能方式。如果是喚醒,則必須查詢狀態。
這樣就完成了!按 F5,您現在就可以從 TextViewerPage 瀏覽至 WebViewerPage!
現在回到 Windows 專案。接下來的步驟與先前為手機執行的步驟十分類似。
新增 XAML (Windows 應用程式 WebViewerPage)
在 WebViewerPage.xaml 中,將 SizeChanged 事件新增至 Page 元素,並將之稱為 pageRoot_SizeChanged。將插入點置於其上,並按 F12 以產生程式碼後置。
尋找「返回按鈕與頁面標題」資料格,並刪除 TextBlock。頁面標題會顯示在網頁上,因此不需要在此佔用空間。
現在,緊接在返回按鈕格線之後,新增含 WebView 的 Border:
<Border x:Name="contentViewBorder" BorderBrush="Gray" BorderThickness="2" Grid.Row="1" Margin="20,20,20,20"> <WebView x:Name="contentView" ScrollViewer.HorizontalScrollMode="Enabled" ScrollViewer.VerticalScrollMode="Enabled"/> </Border>
WebView 控制項可以免費執行許多工作,但是它有特異之處,使它不同於其他 XAML 控制項。若您要在應用程式中大量使用此控制項,請務必先深入研究。
新增成員變數
在 WebViewerPage.xaml.h 中新增下列私用宣告:
Platform::String^ m_feedItemUri;
LoadState 與 SaveState (Windows 應用程式 WebViewerPage)
使用這個與手機頁面十分相似的程式碼,來取代
LoadState
與SaveState
函式:void WebViewerPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) { (void)sender; // Unused parameter // Run the PopInThemeAnimation. auto sb = dynamic_cast<Storyboard^>(this->FindName("PopInStoryboard")); if (sb != nullptr) { sb->Begin(); } // We are navigating forward from SplitPage if (e->PageState == nullptr) { m_feedItemUri = safe_cast<String^>(e->NavigationParameter); contentView->Navigate(ref new Uri(m_feedItemUri)); } // We are resuming from suspension: else { contentView->Navigate( ref new Uri(safe_cast<String^>(e->PageState->Lookup("FeedItemUri"))) ); } } void WebViewerPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e) { (void)sender; // Unused parameter // Store the info needed to reconstruct the page on back navigation, // or in case we are terminated. e->PageState->Insert("FeedItemUri", m_feedItemUri); }
\
將 Windows 專案設為啟始專案並按 F5。當您按一下 TextViewerPage 上的連結時,應該會前往 WebViewerPage;當您按一下 WebViewerPage 返回按鈕時,應該會回到 TextViewerPage。
第十部分:新增和移除摘要
應用程式現在在 Windows 與 Phone 上皆運作良好,前提當然是使用者只滿足閱讀我們寫入程式碼中的三篇摘要。在最後的步驟中,我們將面對現實,讓使用者能夠新增與刪除自行選擇的摘要。我們將會向使用者顯示一些預設摘要,使用者首次啟動應用程式時,才不會看到空白畫面。接著我們將新增一些按鈕,讓使用者可以新增與刪除摘要。當然,我們必須儲存使用者摘要的清單,以便持續存在於工作階段之間。現在就是了解應用程式本機資料的好時機。
首先,我們必須儲存一些預設摘要,以供應用程式首次啟動之用。但是我們不會在程式碼中寫入這些摘要,而是將之置於 ResourceLoader 可找到的字串資源檔。這些資源必須編譯到 Windows 與 Phone 應用程式,因此我們將在共用專案中建立 .resw 檔案。
新增字串資源:
在方案總管中,選取共用專案,然後按一下滑鼠右鍵並新增新的項目。在左邊窗格中選擇 [資源],然後在中間窗格中選擇 [資源檔案 (.resw)]。(請勿選擇 .rc 檔案,因為這是供傳統型應用程式之用。)使用預設名稱,或指定其他名稱。然後按一下 [新增]。
新增下列「名稱-值」組:
- URL_1 http://sxp.microsoft.com/feeds/3.0/devblogs
- URL_2 https://blogs.windows.com/windows/b/bloggingwindows/rss.aspx
- URL_3 https://azure.microsoft.com/blog/feed
完成後,資源編輯器應如下所示。
新增用於新增與移除摘要的共用程式碼
我們將新增程式碼,以載入 URL 至 FeedDataSource 類別。 在 feeddata.h 中,新增此私用成員函式 FeedDataSource:
concurrency::task<Windows::Foundation::Collections::IVector<Platform::String^>^> GetUserURLsAsync();
新增這些陳述式至 FeedData.cpp
using namespace Windows::Storage; using namespace Windows::Storage::Streams;
然後新增實作:
/// <summary> /// The first time the app runs, the default feed URLs are loaded from the local resources /// into a text file that is stored in the app folder. All subsequent additions and lookups /// are against that file. The method has to return a task because the file access is an /// async operation, and the call site needs to be able to continue from it with a .then method. /// </summary> task<IVector<String^>^> FeedDataSource::GetUserURLsAsync() { return create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("Feeds.txt", CreationCollisionOption::OpenIfExists)) .then([](StorageFile^ file) { return FileIO::ReadLinesAsync(file); }).then([](IVector<String^>^ t) { if (t->Size == 0) { // The data file is new, so we'll populate it with the // default URLs that are stored in the apps resources. auto loader = ref new Resources::ResourceLoader(); t->Append(loader->GetString("URL_1\n")); t->Append(loader->GetString("URL_2")); t->Append(loader->GetString("URL_3")); // Before we return the URLs, let's create the new file asynchronously // for use next time. We don't need the result of the operation now // because we already have vec, so we can just kick off the task to // run whenever it gets scheduled. create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([t](StorageFile^ file) { OutputDebugString(L"append lines async\n"); FileIO::AppendLinesAsync(file, t); }); } // Return the URLs return create_task([t]() { OutputDebugString(L"returning t\n"); return safe_cast<IVector<String^>^>(t); }); }); }
GetUserURLsAsync 將查看 feeds.txt 檔案是否存在。若不存在,將會建立新檔案,並從字串資源新增 URL。使用者新增的檔案將會進到 feeds.txt 檔案。因為所有檔案寫入作業皆為非同步作業,因此我們使用工作及 .then 接續,確保完成非同步工作之後,才嘗試存取檔案資料。
現在,使用呼叫 GetUerURLsAsync 的此程式碼,取代原先的 InitDataSource 實作:
///<summary> /// Retrieve the data for each atom or rss feed and put it into our custom data structures. ///</summary> void FeedDataSource::InitDataSource() { auto urls = GetUserURLsAsync() .then([this](IVector<String^>^ urls) { // Populate the list of feeds. SyndicationClient^ client = ref new SyndicationClient(); for (auto url : urls) { RetrieveFeedAndInitData(url, client); } }); }
新增與移除摘要的函式在 Windows 與 Phone 上皆相同,因此我們將之置於 App 類別中。 在 App.xaml.h 中,
新增這些內部成員:
void AddFeed(Platform::String^ feedUri); void RemoveFeed(Platform::String^ feedUri);
在 App.xaml.cpp 中,新增此命名空間:
using namespace Platform::Collections;
在 App.xaml.cpp 中:
void App::AddFeed(String^ feedUri) { auto feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); auto client = ref new Windows::Web::Syndication::SyndicationClient(); // The UI is data-bound to the items collection and will update automatically // after we append to the collection. feedDataSource->RetrieveFeedAndInitData(feedUri, client); // Add the uri to the roaming data. The API requires an IIterable so we have to // put the uri in a Vector. Vector<String^>^ vec = ref new Vector<String^>(); vec->Append(feedUri); concurrency::create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([vec](StorageFile^ file) { FileIO::AppendLinesAsync(file, vec); }); } void App::RemoveFeed(Platform::String^ feedTitle) { // Create a new list of feeds, excluding the one the user selected. auto feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); int feedListIndex = -1; Vector<String^>^ newFeeds = ref new Vector<String^>(); for (unsigned int i = 0; i < feedDataSource->Feeds->Size; ++i) { if (feedDataSource->Feeds->GetAt(i)->Title == feedTitle) { feedListIndex = i; } else { newFeeds->Append(feedDataSource->Feeds->GetAt(i)->Uri); } } // Delete the selected item from the list view and the Feeds collection. feedDataSource->Feeds->RemoveAt(feedListIndex); // Overwrite the old data file with the new list. create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([newFeeds](StorageFile^ file) { FileIO::WriteLinesAsync(file, newFeeds); }); }
為新增與移除按鈕新增 XAML 標記 (Windows 8.1)
新增與移除摘要的按鈕屬於 MainPage。我們會將按鈕置於 Windows 應用程式的 TopAppBar,以及 Phone 應用程式的 BottomAppBar (Phone 應用程式沒有頂端應用程式列)。 在 Windows 專案的 MainPage.xaml 中,緊接在 Page.Resources 節點之後新增 TopAppBar:
<Page.TopAppBar> <CommandBar x:Name="cmdBar" IsSticky="False" Padding="10,0,10,0"> <AppBarButton x:Name="addButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Icon="Add"> <Button.Flyout> <Flyout Placement="Top"> <Grid> <StackPanel> <TextBox x:Name="tbNewFeed" Width="400"/> <Button Click="AddFeed_Click">Add feed</Button> </StackPanel> </Grid> </Flyout> </Button.Flyout> </AppBarButton> <AppBarButton x:Name="removeButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Icon="Remove" Click="removeFeed_Click"/> <!--These buttons appear when the user clicks the remove button to signal that they want to remove a feed. Delete removes the feed(s) and returns to the normal visual state and cancel just returns to the normal state. --> <AppBarButton x:Name="deleteButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Visibility="Collapsed" Icon="Delete" Click="deleteButton_Click"/> <AppBarButton x:Name="cancelButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Visibility="Collapsed" Icon="Cancel" Click="cancelButton_Click"/> </CommandBar> </Page.TopAppBar>
針對四個 Click 事件處理常式名稱 (新增、移除、刪除、取消),將游標置於每個處理常式名稱上並按 F12,以便在程式碼後置中產生函式。
在 <VisualStateManager.VisualStateGroups> 元素內新增第二個 VisualStateGroup:
<VisualStateGroup x:Name="SelectionStates"> <VisualState x:Name="Normal"/> <VisualState x:Name="Checkboxes"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" Storyboard.TargetProperty="SelectionMode"> <DiscreteObjectKeyFrame KeyTime="0" Value="Multiple"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" Storyboard.TargetProperty="IsItemClickEnabled"> <DiscreteObjectKeyFrame KeyTime="0" Value="False"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="cmdBar" Storyboard.TargetProperty="IsSticky"> <DiscreteObjectKeyFrame KeyTime="0" Value="True"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup>
為新增與移除摘要新增事件處理常式 (Windows 8.1):
在 MainPage.xaml.cpp 中,使用此程式碼取代四個事件處理常式虛設常式:
/// <summary> /// Invoked when the user clicks the "add" button to add a new feed. /// Retrieves the feed data, updates the UI, adds the feed to the ListView /// and appends it to the data file. /// </summary> void MainPage::AddFeed_Click(Object^ sender, RoutedEventArgs^ e) { auto app = safe_cast<App^>(App::Current); app->AddFeed(tbNewFeed->Text); } /// <summary> /// Invoked when the user clicks the remove button. This changes the grid or list /// to multi-select so that clicking on an item adds a check mark to it without /// any navigation action. This method also makes the "delete" and "cancel" buttons /// visible so that the user can delete selected items, or cancel the operation. /// </summary> void MainPage::removeFeed_Click(Object^ sender, RoutedEventArgs^ e) { VisualStateManager::GoToState(this, "Checkboxes", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; addButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Visible; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Visible; } ///<summary> /// Invoked when the user presses the "trash can" delete button on the app bar. ///</summary> void SimpleBlogReader::MainPage::deleteButton_Click(Object^ sender, RoutedEventArgs^ e) { // Determine whether listview or gridview is active IVector<Object^>^ itemsToDelete; if (itemListView->ActualHeight > 0) { itemsToDelete = itemListView->SelectedItems; } else { itemsToDelete = itemGridView->SelectedItems; } for (auto item : itemsToDelete) { // Get the feed the user selected. Object^ proxy = safe_cast<Object^>(item); FeedData^ item = safe_cast<FeedData^>(proxy); // Remove it from the data file and app-wide feed collection auto app = safe_cast<App^>(App::Current); app->RemoveFeed(item->Title); } VisualStateManager::GoToState(this, "Normal", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible; addButton->Visibility = Windows::UI::Xaml::Visibility::Visible; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; } ///<summary> /// Invoked when the user presses the "X" cancel button on the app bar. Returns the app /// to the state where clicking on an item causes navigation to the feed. ///</summary> void MainPage::cancelButton_Click(Object^ sender, RoutedEventArgs^ e) { VisualStateManager::GoToState(this, "Normal", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible; addButton->Visibility = Windows::UI::Xaml::Visibility::Visible; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; }
以 Windows 專案為啟始專案並按 F5 。您可看到每一個這些成員函式會將按鈕的可見性屬性設為適當的值,然後進入「一般視覺狀態」。
為新增與移除按鈕新增 XAML 標記 (Windows Phone 8.1)
在 Page.Resources 節點之後,新增含按鈕的底部應用程式列:
<Page.BottomAppBar> <CommandBar x:Name="cmdBar" Padding="10,0,10,0"> <AppBarButton x:Name="addButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Icon="Add" > <Button.Flyout> <Flyout Placement="Top"> <Grid Background="Black"> <StackPanel> <TextBox x:Name="tbNewFeed" Width="400"/> <Button Click="AddFeed_Click">Add feed</Button> </StackPanel> </Grid> </Flyout> </Button.Flyout> </AppBarButton> <AppBarButton x:Name="removeButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Icon="Remove" Click="removeFeed_Click"/> <!--These buttons appear when the user clicks the remove button to signal that they want to remove a feed. Delete removes the feed(s) and returns to the normal visual state. Cancel just returns to the normal state. --> <AppBarButton x:Name="deleteButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Visibility="Collapsed" Icon="Delete" Click="deleteButton_Click"/> <AppBarButton x:Name="cancelButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Visibility="Collapsed" Icon="Cancel" Click="cancelButton_Click"/> </CommandBar> </Page.BottomAppBar>
在每一個 Click 事件名稱上按 F12,以產生程式碼後置。
新增 "Checkboxes" VisualStateGroup,讓整個 VisualStateGroups 節點有如下列所示:
<VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="SelectionStates"> <VisualState x:Name="Normal"/> <VisualState x:Name="Checkboxes"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemListView" Storyboard.TargetProperty="SelectionMode"> <DiscreteObjectKeyFrame KeyTime="0" Value="Multiple"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemListView" Storyboard.TargetProperty="IsItemClickEnabled"> <DiscreteObjectKeyFrame KeyTime="0" Value="False"/> </ObjectAnimationUsingKeyFrames> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>
為新增與移除摘要按鈕新增事件處理常式 (Windows Phone 8.1)
在 MainPage.xaml.cpp (WIndows Phone 8.1) 中,使用下列程式碼取代您剛建立的虛設常式事件處理常式:
void MainPage::AddFeed_Click(Platform::Object^ sender, RoutedEventArgs^ e) { if (tbNewFeed->Text->Length() > 9) { auto app = static_cast<App^>(App::Current); app->AddFeed(tbNewFeed->Text); } } void MainPage::removeFeed_Click(Platform::Object^ sender, RoutedEventArgs^ e) { VisualStateManager::GoToState(this, "Checkboxes", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; addButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Visible; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Visible; } void MainPage::deleteButton_Click(Platform::Object^ sender, RoutedEventArgs^ e) { for (auto item : ItemListView->SelectedItems) { // Get the feed the user selected. Object^ proxy = safe_cast<Object^>(item); FeedData^ item = safe_cast<FeedData^>(proxy); // Remove it from the data file and app-wide feed collection auto app = safe_cast<App^>(App::Current); app->RemoveFeed(item->Title); } VisualStateManager::GoToState(this, "Normal", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible; addButton->Visibility = Windows::UI::Xaml::Visibility::Visible; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; } void MainPage::cancelButton_Click(Platform::Object^ sender, RoutedEventArgs^ e) { VisualStateManager::GoToState(this, "Normal", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible; addButton->Visibility = Windows::UI::Xaml::Visibility::Visible; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; }
按 F5 並嘗試使用新按鈕來新增與移除摘要!若要在手機上新增摘要,請按一下網頁上的 RSS 連結,然後選擇 [儲存]。然後按具有 URL 的名稱之編輯方塊,然後按複製圖示。瀏覽回到應用程式,將插入點置於編輯方塊中,然後再次按複製圖示以貼上 URL。您應該可以立即看到摘要出現在摘要清單中。
SimpleBlogReader 應用程式現在已達可正常使用的狀態。它已經可以部署在您的 Windows 裝置上。
若要將應用程式部署在您的手機上,首先必須要註冊,如註冊您的 Windows Phone 裝置以供開發中所述。
部署在未鎖定的 Windows Phone
建立發行組建。
從主功能表選取 [專案 | 市集 | 建立應用程式套件]。在此練習中,您不需要部署至市集。除非您有變更設定的理由,否則在下一個畫面中請接受預設值。
若順利建立套件,將會提示您執行 Windows 應用程式認證套件 (WACK)。執行此動作可確保應用程式沒有隱藏的問題,不會被市集拒絕。不過,因為我們不會部署至市集,因此這是選擇性步驟。
從主功能表選取 [工具 | Windows Phone 8.1 | 應用程式部署]。接著會顯示「應用程式部署」精靈,在第一個畫面中 [目標] 應為 [裝置]。按一下 [瀏覽] 按鈕,瀏覽至專案樹狀結構中的 AppPackages 資料夾,與 Debug 及 Release 資料夾位於同一層。在該資料夾中找出最新的套件 (若資料夾中有多個套件) 並按兩下該套件,然後按一下其中的 appx 或 appxbundle 檔案。
確認手機已連接電腦,且沒有被鎖定畫面鎖住。按精靈中的 [部署] 按鈕,並等待部署完成。應該只需等幾秒鐘,便會看到「部署成功」訊息。在手機上應用程式清單中找出應用程式,點選以執行該應用程式。
注意:新增新的 URL 一開始在操作上可能略為複雜。搜尋要新增的 URL,然後點選連結。在提示中選擇要開啟。複製 RSS URL,例如 http://feeds.bbci.co.uk/news/world/rss.xml,而不是 IE 開啟檔案後顯示的暫時 xml 檔案名稱。若 XML 頁面在 IE 中開啟,您必須往回瀏覽到前一個 IE 畫面,以便從位址列擷取您要的 URL。複製後,往回瀏覽至 Simple Blog Reader,並在 Add Feed 文字區塊中貼上,然後按 [新增摘要] 按鈕。您很快便會在主頁面上看到完全初始化的摘要。供讀者的練習:實作共用合約或其他方法,以簡化新增新的 URL 至 SimpleBlogReader 的步驟。祝閱讀愉快!
下一步
本教學課程說明如何使用 Microsoft Visual Studio Express 2012 for Windows 8 內建的頁面範本來建置多頁面應用程式,以及如何在頁面之間瀏覽和傳送資料。我們學會如何使用樣式和範本,使應用程式符合 Windows 小組部落格網站的特質。我們也學會如何使用主題動畫和應用程式列,使我們的應用程式符合 Windows 市集應用程式的特質。 最後,我們學會如何讓應用程式適應各種配置和方向,讓它的外觀保持最佳狀態。
我們的應用程式已準備好提交到 Windows 市集。如需如何將應用程式送出到 Windows 市集的詳細資訊,請參閱:
- 在市場上發佈您的應用程式
- 如何將您的應用程式設計成無障礙應用程式。如需詳細資訊,請參閱協助工具。
- 學習和參考資源清單:使用 C++ 建立 Windows 執行階段應用程式的藍圖。