创建博客阅读器通用 Windows 平台应用 (C++)
下面将详细介绍如何使用 C++ 和 XAML 开发 可以部署到 Windows 10 的通用 Windows 平台 (UWP) 应用。该应用阅读来自 RSS 2.0 或 Atom 1.0 源的博客。
该教程假定你已熟悉使用 C++ 创建你的首个 Windows 应用商店应用中的概念。
要研究该应用的已完成版本,你可以 从 MSDN 代码库网站下载它。
在本教程中,我们将使用 Visual Studio Community 2015 或更高版本。如果你使用的是 Visual Studio 的另一个版本,则菜单命令可能稍有不同。
有关采用其他编程语言的教程,请参阅:
创建“Hello World”应用 (C#/VB)
目标
本教程旨在帮助你了解有关以下知识:如何创建多页 Windows 应用商店应用,如何及何时使用 Visual C++ 组件扩展 (C++/CX) 简化面向 Windows 运行时的编码工作。本教程还将教你如何使用 concurrency::task
类来使用异步 Windows 运行时 API。
SimpleBlogReader 应用具有以下功能:
- 通过 Internet 访问 RSS 和 Atom 源数据。
- 显示源和数据源标题的列表。
- 以简单的文本或网页形式提供用于阅读文章的两种方式 。
- 如果系统在前台存在其他任务时将其关闭,它会支持进程生命周期管理 (PLM) 并能正确保存和重新加载其状态。
- 能够适应不同的窗口大小和设备方向(横向或纵向)。
- 使用户能够添加和删除源。
第 1 部分:设置项目
首先,让我们使用 C++“空白应用(通用 Windows)”模板创建一个项目。
创建一个新项目
- 在 Visual Studio 中,依次选择“文件”>“新建”>“项目”、“已安装”>“Visual C++”>“Windows”>“通用”****。在中间窗格中,选择“空白应用(通用 Windows)”模板。将解决方案命名为“SimpleBlogReader”。有关更多完整说明,请参阅创建“Hello World”应用 (C++)。
让我们从添加所有页面开始。因为当我们开始编码时,每个页面都必须对它导航到的页面执行 #include 指令,所以以这种方式一次性执行所有操作更为容易。
添加 Windows 应用页面
- 实际上,我们从破坏旧项开始。右键单击 MainPage.xaml、选择“删除”,然后单击“删除”****以永久删除该文件及其代码隐藏文件。这是一种缺少所需的导航支持的空白页类型。现在,右键单击项目节点,然后依次选择“添加”>“新建项”。
- 在左侧窗格中,选择 XAML,然后在中间窗格中选择“项页”。将它命名为 MainPage.xaml,然后单击“确定”。你将看到一个消息框,询问是否可以向该项目中添加一些新的文件。单击“是”。在启动代码中,我们需要引用在这些文件中定义的 SuspensionManager 和 NavigationHelper 类(Visual Studio 将其放置在新的“常用”文件夹中)。
- 添加 SplitPage 并接受默认名称。
- 添加 BasicPage 并将它命名为 WebViewerPage。
我们将在以后向这些页面添加用户界面元素。
添加手机应用页面
- 在“解决方案资源管理器”中,展开 Windows Phone 8.1 项目。右键单击 MainPage.xaml,然后选择“删除”>“永久删除”。
- 添加一个新的 XAML“基本页”****并将它命名为 MainPage.xaml。单击“是”(就像针对 Windows 项目所执行的操作)。
- 你可能注意到在手机项目中各种页面模板更为受限;在此应用中,我们仅使用基本页。再添加三个基本页并将其命名为 FeedPage、TextViewerPage 和 WebViewerPage。
第 2 部分:创建数据模型
基于 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 的一个示例。应用中的第一个页面是源的视图、第二个页面是单个源的 FeedItem 的视图、最后两个页面提供一篇文章的不同视图:纯文本形式或网页形式。
FeedDataSource 类包含一个 FeedData 项的集合以及下载它们的方法。
概括:
FeedData 包含有关 RSS 或 Atom 源的信息。
FeedItem 包含有关源中的各篇博客文章的信息。
FeedDataSource 包含下载源和初始化我们的数据类所用的方法。
我们将这些类定义为公共引用类以启用数据绑定;XAML 控件不能与标准 C++ 类交互。我们使用 Bindable 属性指示动态绑定到这些类型的实例的 XAML 编译器。在公共引用类中,将公共数据成员公开为属性。没有特殊逻辑的属性不需要用户指定的 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 和相关类型所必需的)。这些标头包含我们的应用需要的 <string> 和 <vector>,因此我们无需显式包含它们。
//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; }; }
这些类是引用类,因为 Windows 运行时 XAML 类需要与之进行交互以将数据绑定到用户界面。这些类上的 [Bindable] 属性也是数据绑定所必需的。在没有该属性的情况下,绑定机制不会看到它们。
第 3 部分:下载数据
FeedDataSource 类包含用于下载源的方法,还有一些其他的 Helper 方法。它还包含下载源的集合,该集合将添加到主应用页面的 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
现在,让我们将 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 事件时,App 对象将调用 InitDataSource 以使 feedDataSource 实例开始下载其所有数据。
尚不会构建项目,因为我们需要添加一些其他类定义。
第 4 部分:处理从终止恢复时的数据同步
当应用首次启动并且用户同时在页面之间来回导航时,不需要任何数据访问同步。源初始化后仅在第一个页面中显示,并且在用户单击可见源之前,其他页面永不尝试访问数据。在这之后,所有访问都是只读的;我们永不修改源数据。但是,在以下这一种情况下需要进行同步:如果应用在基于特定源的页面处于活动状态时被终止,则该页面将需要在应用恢复时重新绑定到该源数据。在此情况下,页面可以尝试访问尚不存在的数据。因此,我们需要一种用于强制页面等待数据做好准备的方法。
以下函数使应用能够记住它之前在查看哪个源。SetCurrentFeed 方法仅使源保留到本地设置,即使应用内存不足,也可以从中检索源。GetCurrentFeedAsync 方法是一个有趣的方法,因为我们必须确保当我们返回并且想要重新加载最后一个源时,我们不会在该源已重新加载之前尝试执行此操作。稍后我们将详细讨论此代码。我们会将该代码添加到 App 类,因为我们要从 Windows 应用和手机应用中调用它。
在 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;
你需要 Task 的并发命名空间、Vector 的 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; }
第 5 部分:将数据转换为可用的形式
并非所有原始数据都必须采用可用的形式。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(); } }; }
现在,在 App.xaml.h 中对其执行 #include 指令:
#include "DateConverter.h"
然后,在 Application.Resources 节点的 App.xaml 中创建它的一个实例:
<local:DateConverter x:Key="dateConverter" />
源内容通过网络以 HTML 形式(或在某些情况下以 XML 格式的文本形式)传入。若要在 RichTextBlock 中显示此内容,我们必须将其转换为富文本。下面的类使用 Windows HtmlUtilities 函数来分析 HTML,然后使用 <regex> 函数将其拆分为段落,以便我们可以构建富文本对象。我们无法在此方案中使用数据绑定,因此该类无需实现 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"
现在,你应该能够生成和运行应用了。只是不要期待它执行太多操作。
第 6 部分:启动、暂停和恢复应用
当用户通过按下或单击应用的应用磁贴启动该应用时,以及在系统已终止应用以为其他应用释放内存,然后用户导航回该应用之后,都将引发 App::OnLaunched
事件。在任一情况下,我们始终转到 Internet 并重新加载数据以响应此事件。但是,有些其他操作只需在这两种情况之一下进行调用。我们可以通过查看 rootFrame 以及传递给该函数的 LaunchActivatedEventArgs 参数来推断这些状态,然后再执行正确操作。幸运的是,当应用暂停并重新启动时,已与 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"
添加命名空间指令:
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 类位于共享项目中,因此我们在此处编写的代码将在 Windows 和手机应用上运行(定义 WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP 宏的位置除外)。
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(); }); }
此时,我们可以启动应用并下载源数据,但是我们无法向用户显示它。让我们对此采取一些措施!
第 7 部分:添加第一个 UI 页面,即源列表
当应用打开时,我们想要向用户显示已下载的所有源的顶级集合。用户可以单击或按下该集合中的项以导航到特定源,该源中将包含源项或文章的集合。我们已添加页面。在 Windows 应用中,它是项页,它在设备处于水平状态时显示 GridView,在设备处于垂直状态时显示 ListView。手机项目没有项页,因此我们具有一个将向其手动添加 ListView 的基本页。当设备方向发生更改时,列表视图将自动调整自身。
在此页以及每个页面上,要完成的基本任务一般都相同:
- 添加用于描述 UI 并对数据进行数据绑定的 XAML 标记
- 将自定义代码添加到
LoadState
和SaveState
成员函数。 - 处理事件,其中至少一个事件通常具有导航到下一页的代码
我们将按顺序完成这些任务,首先在 Windows 项目中:
添加 XAML 标记 (MainPage)
主页将每个 FeedData 对象呈现在 GridView 控件中。为了描述数据外观,我们创建一个 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
元素,以便它不会隐藏我们要在“应用”作用域内添加的全局元素。将 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)
在概念上,所有页面都驻留在框架内。它是我们用于在页面之间来回导航的框架。Navigate 函数调用中的第二个参数用于将数据从一个页面传递到另一个页面。每当应用暂停时,我们在此处传递的任何对象都由 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 顶部的 SplitPage.xaml.h 执行 #include 指令 (Windows 8.1):
#include "SplitPage.xaml.h"
处理 Page_SizeChanged 事件
在 MainPage.xaml 中,通过将
x:Name="pageRoot"
添加到根页面元素的属性来向根元素添加名称,然后添加属性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(手机应用 MainPage)
现在,让我们开始使手机应用主页正常工作。这将使用很少的代码,因为我们将使用已放置到共享项目中的所有代码。此外,手机应用不支持 GridView 控件,因为屏幕太小而难以使其很好地工作。因此,我们将使用 ListView,它将自动调整为横向方向,并且不需要任何 VisualState 更改。我们首先将 DataContext 属性添加到 Page 元素。这不是在手机基本页中自动生成的(如在 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
现在添加代码,以使 MainPage.xaml.h 头文件中包含 NavigationHelper 以及我们稍后需要的 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 属性。还要添加所需的 using 命名空间指令,因此最终代码如下所示:
// // 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 将为我们生成一个空的事件处理程序函数。我们稍后将向其中添加一些代码。目前,我们只需要生成函数,以便应用可以编译。
第 8 部分:列出文章并显示所选文章的文本视图
在这一部分中,我们将向手机应用中添加两个页面:列出文章的页面和显示所选文章的文本版本的页面。在 Windows 应用中,我们只需要添加一个名为 SplitPage 的页面,它将在一侧显示列表,在另一侧显示所选文章的文本。第一个是手机页面。
添加 XAML 标记(手机应用 FeedPage)
让我们停留在手机项目中,并对 FeedPage(列出了用户选择的源的文章)进行操作。
****
在 FeedPage.xaml (Windows Phone 8.1) 中,将数据上下文添加到 Page 元素:
DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
现在,在起始 Page 元素后添加 CollectionViewSource:
<Page.Resources> <!-- Collection of items displayed by this page --> <CollectionViewSource x:Name="itemsViewSource" Source="{Binding Items}"/> </Page.Resources>
在网格元素中,添加以下 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
,后者绑定到我们的FeedData::Items
属性,我们将该属性插入到代码隐藏的 LoadState 中的 DefaultViewModel 属性中(如下所示)。在 ListView 中声明了一个 ItemClick 事件。将光标放在该事件上,然后按 F12 以在代码隐藏中生成事件处理程序。我们暂时将其保留为空。
LoadState 和 SaveState(手机应用 FeedPage)
在 MainPage 中,我们不必担心存储状态,因为每当应用出于任何原因启动时,页面始终都从 Internet 执行完全重新初始化。其他页面需要记住其状态。例如,如果应用在 FeedPage 显示时终止(从内存中卸载),则当用户导航回该应用时,我们希望看起来像从未移除过该应用一样。因此,我们需要记住所选的源。存储此数据的位置是在本地 AppData 存储中,当用户在 MainPage 中单击它时是存储它的好时机。
此处只有一个问题,即数据实际上是否存在?如果我们要通过用户单击从 MainPage 导航到 FeedPage,则我们可以确定所选的 FeedData 对象已存在,否则它不会出现在 MainPage 列表中。但是,如果应用正在恢复,则当 FeedPage 尝试绑定到 FeedData 对象时,可能还尚未加载上次查看过的这一对象。因此,FeedPage(以及其他页面)需要一种方法来知道 FeedData 何时可用。concurrency::task_completion_event 专为这种情况而设计。通过使用它,我们可以安全地获取相同代码路径中的 FeedData 对象,而不管我们是要从 MainPage 重新恢复还是导航。在 FeedPage 中,我们始终通过调用 GetCurrentFeedAsync 获取我们的源。如果要从 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 将等待源数据到达。在尝试访问 DefaultViewModel 依赖属性之前,我们指定 use_current() 上下文以指示任务返回到 UI 线程上。一般情况下,不能直接从后台线程访问与 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(手机应用 FeedPage)
我们在 FeedPage 上处理一个事件,即向前导航到用户可以从中阅读文章的页面的 ItemClick 事件。当你按下“F12”时,你已经在 XAML 中的事件名称上创建了一个存根处理程序。
现在,让我们将实现替换为此代码。
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”****以在仿真器中构建并运行手机应用。现在,当从 MainPage 中选择某个项时,该应用应导航到 FeedPage 并显示源列表。下一步是显示选定源的文本。
添加 XAML 标记(手机应用 TextViewerPage)
在手机项目的 TextViewerPage.xaml 中,将标题面板和内容网格替换为此标记,该标记将显示应用名称(后台)和当前文章的标题,以及内容的简单文本呈现:
<!-- 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 属性,并且同时添加私有成员 m_FeedItem,以便可以在使用 GetFeedItem 函数(已在上一步中将其添加到 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(手机应用 TextViewerPage)
在 TextViewerPage.xaml.cpp 中,添加以下 include 指令:
#include "WebViewerPage.xaml.h"
添加以下两个命名空间指令:
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,而某些不是。
事件处理程序(手机应用 TextViewerPage)
在 TextViewerPage 中,我们通过 RichText 中的 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); }
现在,将手机项目设置为启动项目,然后按“F5”。你应当能够单击源页面中的某个项并导航到可以从中阅读博客文章的 TextViewerPage。这些博客中还有一些有趣的内容!
添加 XAML(Windows 应用 SplitPage)
Windows 应用与手机应用在某些方面的行为方式有所不同。我们已看到 Windows 项目中的 MainPage.xaml 如何使用 ItemsPage 模板,它在手机应用中不可用。现在,我们将要添加 SplitPage,它在手机上也不可用。当设备处于横向方向时,Windows 应用中的 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 并将绑定从 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,然后我们将使用我们自己的实用程序类来将返回的字符串拆分为段落并构建富文本元素。此视图不会显示任何图像但可以快速加载,并且如果你想要扩展此应用,则可以稍后添加一个选项,以允许用户调整字体和字体大小。
在“选定项的详细信息”注释下方查找 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 应用 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 方法。此页面和手机页面之间的一个区别在于,现在我们跟踪选定的项。在
SaveState
中,我们将当前选定的项插入到 PageState 对象中,以便 SuspensionManager 根据需要保留它,并且当调用LoadState
时,它可供我们在 PageState 对象中再次使用。我们将需要该字符串以在当前源中查找当前 FeedItem。
事件处理程序(Windows 应用 MainPage)
当选定的项发生更改时,详细信息窗格将使用 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); } } }
此函数指定将传递给我们在富文本中创建的超链接的回调。
在 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,而无需你进行任何额外编码。在手机上,返回按钮功能由硬件返回按钮(而非软件按钮)提供。手机返回按钮导航由“常用”文件夹中的 NavigationHelper 类处理。在你的解决方案中搜索“BackPressed”(Ctrl + Shift + F) 以查看相关代码。同样,你无需在此处执行任何额外操作。使用简单!
第 9 部分:添加选定文章的 Web 视图。
我们将添加的最后一个页面是一个将在其原始网页中显示博客文章的页面。有时,阅读器也可能需要查看图片!查看网页的缺点是文本在手机屏幕上可能难以阅读,而且并非所有网页都针对移动设备进行了良好的格式设置。有时,边缘会超出屏幕的一侧,并且需要大量的水平滚动。我们的 WebViewerPage 页面相对简单。我们仅在页面中添加 WebView 控件,并让它完成所有工作。我们将从手机项目开始:
添加 XAML(手机应用 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(手机应用 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 中,添加以下私有成员变量:
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。
第 10 部分:添加和删除源
现在,应用在 Windows 和手机上都运行良好(假定用户永远不会想要阅读除我们已经硬编码到应用中的三个源之外的任何内容)。但作为最后一步,我们应从实际出发,使用户能够添加和删除他们自己选择的源。我们将向他们显示某些默认源,以便当他们首次启动应用时,屏幕不是空白的。然后,我们将添加一些按钮,使他们可以添加和删除源。当然,我们将需要存储用户源列表,以使其在会话之间持续。这是了解应用本地数据的好时机。
作为第一步,在应用首次启动时,我们仍然需要存储一些默认源。但是不是对其进行硬编码,而是将其放在 ResourceLoader 可以从中找到它们的字符串资源文件中。我们需要这些资源来编译到 Windows 和手机应用中,因此我们将在共享项目中创建 .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
完成后,资源编辑器的外观应如下所示。
添加用于添加和删除源的共享代码
我们将向 FeedDataSource 类添加用于加载 URL 的代码。 在 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 延续来确保异步工作在我们尝试访问文件数据之前完成。
现在,将旧的 InitDataSource 实现替换为以下用于调用 GetUerURLsAsync 的代码:
///<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 和手机上是相同的,因此我们会将它们放在 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 中以及手机应用的 BottomAppBar 中(手机应用没有顶部应用栏)。 在 Windows 项目的 MainPage.xaml 中,添加 TopAppBar,紧跟在 Page.Resources 节点的后面:
<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; }
按“F5”,此时 Windows 项目为启动项目。你可以看到每个成员函数都将按钮上的 visibility 属性设置为适当的值,然后转到普通视觉状态。
添加用于添加和删除按钮的 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”以生成代码隐藏。
添加“复选框”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 (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 文件夹,该文件夹与“调试和发布”文件夹位于相同的级别上。在该文件夹中查找最新的程序包(如果存在多个)、双击该程序包,然后单击其内部的 appx 或 appxbundle 文件。
请确保你的手机已插入计算机,并且锁屏界面未锁定该手机。按向导中的“部署”按钮,然后等待部署完成。应仅在几秒钟后即可看到“部署成功”消息。在手机的应用程序列表中添加该应用,然后点击它以运行该应用。
注意:添加新的 URL 起初可能不太直观。搜索想要添加的 URL,然后点击该链接。在提示下,假设你想要打开它。复制 RSS url(例如 http://feeds.bbci.co.uk/news/world/rss.xml)而不是在 IE 打开该文件后出现的临时 xml 文件名。如果 XML 页面在 IE 中打开,则你将需要导航回以前的 IE 屏幕,以从地址栏中获取所需的 URL。复制它之后,再导航回“简单博客阅读器”并将其粘贴到“添加源”文本块中,然后按“添加源”按钮。你将看到完全初始化的源非常迅速地出现在你的主页中。留给读者的练习:实现共享合约或其他用于简化向 SimpleBlogReader 添加新 URL 的方法。祝阅读愉快!
总结
本教程介绍如何使用 Microsoft Visual Studio Express 2012 for Windows 8 中的内置页面模板构建多个页面应用,以及如何在页面之间导航和传递数据。我们了解了如何使用样式和模板以使我们的应用符合 Windows 团队博客网站的风格。我们还学习了如何使用主题动画和应用栏来使应用符合 Windows 8 应用商店应用的风格。 最后,我们了解了如何根据各种布局和方向来调整应用,从而让它始终保持美观。
我们的应用现在已基本就绪,可以提交到 Windows 应用商店了。有关如何将应用提交至 Windows 应用商店的详细信息,请参阅:
- 将你的应用投入市场
- 如何使你的应用可被访问。有关详细信息,请参阅辅助功能。
- 学习和参考资源列表:使用 C++ 的 Windows 运行时应用的路线图。