ブログ リーダー ユニバーサル Windows プラットフォーム アプリの作成 (C++)
ここでは、C++ と XAML を使って、Windows 10 に展開できるユニバーサル Windows プラットフォーム (UWP) アプリを開発する方法を紹介します。アプリは、RSS 2.0 フィードまたは Atom 1.0 フィードからブログを読み取ります。
このチュートリアルは、「C++ を使った初めての Windows ストア アプリの作成」で説明した概念を十分に理解していることを前提としています。
学習用にこのアプリの完成版は、MSDN コード ギャラリー Web サイトから ダウンロード できます。
このチュートリアルでは、Visual Studio Community 2015 以降を使います。他のエディションの Visual Studio を使っている場合は、メニュー コマンドが若干異なる可能性があります。
他のプログラミング言語のチュートリアルについては、次のトピックをご覧ください。
"Hello World" アプリを作成する (C#/VB)
目標
このチュートリアルは、マルチページ Windows ストア アプリの作成方法を理解するために役立ちます。また、Visual C++ コンポーネント拡張機能 (C++/CX) をいつどのように使うことで Windows ランタイムに対するコーディング作業が簡単になるかを理解するための手助けにもなります。さらに、concurrency::task
クラスを使って非同期 Windows ランタイム API を利用する方法についても説明します。
SimpleBlogReader アプリには、次の特徴があります。
- インターネットを経由して RSS フィードと Atom フィードのデータにアクセスする。
- フィードとフィード タイトルの一覧を表示する。
- 投稿を単純なテキストまたは Web ページとして読み取るための 2 つの方法を提供する。
- プロセス ライフタイム管理 (PLM) をサポートし、フォアグランドで別のタスクが実行されているときにシステムによってシャットダウンされた場合に状態を正確に保存および再読み込みする。
- さまざまなウィンドウ サイズやデバイスの向き (横または縦) に対応する。
- ユーザーがフィードの追加と削除を実行できるようにする。
パート 1: プロジェクトをセットアップする
最初に、C++ の空のアプリ (ユニバーサル Windows) テンプレートを使ってプロジェクトを作ってみましょう。
新しいプロジェクトを作成するには
- Visual Studio で、[ファイル]、[新規]、[プロジェクト] の順にクリックします。次に、[インストール済み]、[Visual C++]、[Windows]、[ユニバーサル] の順にクリックします。中央のウィンドウで [空白のアプリ (ユニバーサル Windows)] テンプレートをクリックし、ソリューションに "SimpleBlogReader" という名前を付けます。手順について詳しくは、「"Hello, world" アプリを作成する (C++)」をご覧ください。
最初に、すべてのページを追加します。ページを 1 つずつコーディングする場合、初めに、各ページの移動先となるページについて #include を指定する必要があるため、すべてのページをまとめて追加する方法の方が簡単に実行できます。
Windows アプリ ページを追加する
- 実際には、破棄の作業から開始します。MainPage.xaml を右クリックし、[削除] をクリックします。次に [削除] をクリックして、ファイルとその分離コード ファイルを完全に削除します。このページ (MainPage.xaml) は空白のページであり、必要なナビゲーション サポートは含まれていません。次に、プロジェクト ノードを右クリックし、[追加]、[新しい項目] の順にクリックします。
- 左側のウィンドウで [XAML] をクリックし、中央のウィンドウで [項目ページ] をクリックします。このページを MainPage.xaml とします。[OK] をクリックします。プロジェクトに新しいファイルを追加するかどうかを確認するメッセージ ボックスが表示されます。[はい] をクリックします。このスタートアップ コードでは SuspensionManager クラス NavigationHelper クラスを参照する必要があります。これらのクラスは、Visual Studio によって新しい共通フォルダーに配置されるファイル内に定義されています。
- SplitPage を追加して、既定の名前を使います。
- BasicPage を追加し、このページを WebViewerPage とします。
後で、これらのページにユーザー インターフェイス要素を追加します。
Windows Phone のアプリ ページを追加する
- ソリューション エクスプローラーで、Windows Phone 8.1 プロジェクトを展開します。MainPage.xaml を右クリックし、[削除]、[完全に削除] の順にクリックします。
- 新しい XAML の [基本ページ] を追加し、このページを MainPage.xaml とします。Windows プロジェクトの場合と同様に、[はい] をクリックします。
- ご存知のように、Windows Phone プロジェクトではさまざまなページ テンプレートが制限されています。このアプリでは、基本ページのみを使います。基本ページをさらに 3 つ追加します。それらのページを、FeedPage、TextViewerPage、WebViewerPage とします。
パート 2: データ モデルを作る
Visual Studio テンプレートに基づくストア アプリは、厳密な状態ではありませんが、MVVM アーキテクチャを実現します。このアプリでは、ブログ フィードをカプセル化するクラスで構成されるモデルを採用します。アプリの各 XAML ページはデータの特定のビューを表し、各ページ クラスには独自のビュー モデルがあります。このビュー モデルは DefaultViewModel と呼ばれるプロパティであり、ビュー モデルの種類は Map<String^,Object^> です。このマップは、ページ上の XAML コントロールがバインドされるデータを保管し、ページのデータ コンテキストとして機能します。
このモデルは 3 つのクラスで構成されます。FeedData クラスは、ブログ フィードの最上位の URI とメタデータを表します。FeedData によるカプセル化の例として、https://blogs.windows.com/windows/b/buildingapps/rss.aspx のフィードご覧ください。フィードには、ブログ投稿の一覧が含まれています。また、ブログ投稿は FeedItem オブジェクトで表されます。各 FeedItem は 1 つの投稿を表し、タイトル、コンテンツ、URI などのメタデータが含まれています。FeedItem の例として、https://blogs.windows.com/windows/b/buildingapps/archive/2014/05/28/using-the-windows-phone-emulator-for-testing-apps-with-geofencing.aspx の投稿をご覧ください。このアプリの最初のページはフィードのビューであり、2 番目のページは 1 つのフィードに対する FeedItem のビューです。また、最後の 2 つのページでは 1 つの投稿に関する異なるビュー (プレーンテキストまたは Web ページ) が提供されます。
FeedDataSource クラスには、FeedData 項目のコレクションとそれらの項目をダウンロードするためのメソッドが含まれています。
まとめ:
FeedData は、RSS フィードまたは Atom フィードに関する情報を保持します。
FeedItem は、フィードに含まれる個々のブログ投稿に関する情報を保持します。
FeedDataSource は、フィードをダウンロードしてデータ クラスを初期化するためのメソッドを含みます。
これらのクラスをパブリック ref クラスとして定義して、データ バインディングを有効にします。XAML コントロールでは、標準の C++ クラスを操作できません。Bindable 属性を使って、これらの型のインスタンスに動的にバインドすることを XAML コンパイラに示します。public ref class では、パブリック データ メンバーはプロパティとして公開されます。特別なロジックを持たないプロパティには、ユーザーが指定した getter と setter は必要ありません。これらはコンパイラが提供します。 FeedData クラスでは、Windows::Foundation::Collections::IVector を使ってパブリック コレクション型を公開する方法に注目してください。また、IVector を実装する具象型として Platform::Collections::Vector クラスを内部で使っています。
Windows プロジェクトと Windows Phone プロジェクトの両方で同じデータ モデルを使います。このため、クラスは共有プロジェクト内に配置します。
カスタム データ クラスを作成するには
ソリューション エクスプローラーで、[SimpleBlogReader.Shared] プロジェクト ノードのショートカット メニューを開き、[追加]、[新しい項目] の順にクリックします。[ヘッダー ファイル (.h)] オプションをクリックし、FeedData.h という名前を付けます。
FeedData.h を開き、次のコードを貼り付けます。"pch.h" の #include ディレクティブに注目してください。"pch.h" はプリコンパイル済みヘッダーであり、ほとんどあるいはまったく変更されないシステム ヘッダーが含まれます。既定では、pch.h には、Platform::Collections::Vector 型で必要となる collection.h、および concurrency::task とその関連する型で必要となる ppltasks.h が含まれています。これらのヘッダーには、アプリで必要となる <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; }; }
クラスは ref クラスです。これは、Windows ランタイム XAML クラスがユーザー インターフェイスにデータ バインディングするためには、ref クラスを操作する必要があるためです。また、これらのクラスの [Bindable] 属性も、データ バインディングで必要となります。この属性を使わないと、バインディングのメカニズムではこれらのクラスを参照できません。
パート 3: データをダウンロードする
FeedDataSource クラスには、フィードをダウンロードするメソッドと他のヘルパー メソッドが含まれています。また、ダウンロードされたフィードのコレクションも含まれています。このコレクションは、アプリのメイン ページの DefaultViewModel にある "Items" 値に追加されます。FeedDataSource では、Windows::Web::Syndication::SyndicationClient クラスを使ってダウンロードを実行します。ネットワーク操作には時間がかかるため、これらの操作は非同期的に実行されます。フィードのダウンロードが完了すると、FeedData オブジェクトが初期化され、FeedDataSource::Feeds コレクションに追加されます。これは、IObservable<T> です。つまり、項目が追加されたときに UI が通知を受け取り、その項目がメイン ページに表示されます。非同期操作では、ppltasks.h から concurrency::task クラス、関連クラス、およびメソッドを使います。create_task 関数は、IAsyncOperation 関数呼び出しと IAsyncAction 関数呼び出しを Windows API にラップするために使われます。task::then メンバー関数は、タスクが完了するまで待機する必要があるコードを実行するために使われます。
このアプリの便利な点は、ユーザーはすべてのフィードがダウンロードされるのを待機する必要がないことです。フィードが表示されたらすぐにクリックすることで、そのフィードのすべての項目が表示される新しいページに移動できます。これは、"軽快かつ柔軟" なユーザー インターフェイスの一例です。こうしたユーザー インターフェイスは、バックグラウンド スレッドで多くの処理を実行することによって実現されます。メインの XAML ページを追加した後で、このユーザー インターフェイスが動作するようすを確認します。
ただし、非同期操作によって複雑さが増しています。"軽快かつ柔軟" なユーザー インターフェイスは "簡単に" 実現することはできません。前のチュートリアルをお読みであればご理解いただけますが、現在アクティブではないアプリは、メモリを解放するためにシステムによって終了される場合があり、ユーザーがそのアプリに切り替えたときに復元されます。このアプリでは、シャットダウン時にすべてのフィード データを保存しません。これを行うと、多くのストレージが使われ、古いデータが残ってしまう可能性があるためです。フィードのダウンロードは起動するたびに実行されます。ただしこれは、アプリが終了状態から再開されたら、ダウンロードがまだ完了していなかった FeedData オブジェクトを直ちに表示するというシナリオに対応する必要があることを意味しています。データを表示するには、データが利用可能になってから行う必要があります。この場合、then メソッドを使うことはできませんが、task_completed_event を使うことができます。このイベントによって、FeedData オブジェクトの読み込みが完了するまで、コードはこのオブジェクトにアクセスできなくなります。
FeedDataSource クラスを、名前空間 SimpleBlogReader の一部として FeedData.h に追加します。
/// <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 イベントが発生と、アプリ オブジェクトによって InitDataSource が呼び出され、feedDataSource インスタンスがすべてのデータのダウンロードを開始します。
プロジェクトはまだ構築されません。これは、クラス定義をいくつか追加する必要があるためです。
パート 4: 終了状態から再開するときにデータの同期を処理する
アプリを初めて起動したとき、ユーザーがページ間を移動している間は、データ アクセスの同期は必要ではありません。フィードは、フィードが初期化された後で最初のページにのみ表示されます。他のページは、表示されるフィードをユーザーがクリックするまでデータにアクセスしません。また、すべてのアクセスが読み取り専用である場合は、ソース データを変更しません。ただし、同期が必要となるシナリオがあります。そのシナリオでは、特定のフィードに基づくページがアクティブになっているときに、アプリが終了された場合、アプリが再開されたときには、ページをそのフィード データに再バインドする必要があります。この場合、ページはまだ存在していないデータへのアクセスを試行できます。そのため、データが利用可能になるまでページを強制的に待機させる方法が必要になります。
次の関数を使うと、参照していたフィードをアプリで保存することができます。SetCurrentFeed メソッドは、アプリケーションがメモリから解放された後でもフィードを取得できるローカルの設定場所に、フィードを保持します。GetCurrentFeedAsync メソッドは特徴的なメソッドです。このメソッドを使った場合、アプリが再開して最後に読み込んだフィードをもう一度表示する必要があるとき、フィードの再読み込みが完了するまでは、フィードの表示を実行しません。このコードについては、後で説明します。ここでは、App クラスにコードを追加します。これは、Windows アプリと Windows Phone アプリの両方からコードを呼び出すためです。
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;
タスクには concurrency 名前空間、ベクターには 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> 関数を使って HTML を段落に分割します。これにより、リッチ テキスト オブジェクトを作成できます。このシナリオではデータ バインディングを使うことはできません。そのため、クラスで IValueConverter を実装する必要はありません。ここでは、IValueConverter が必要となるページに 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 クラスは、ISO C++ (std::map、std::regex、std::wstring) を C++/CX アプリ内で使うための方法の一部を示すものです。このクラスのインスタンスを、クラスを使うページにローカルで作成します。App.xaml.h 内に、このクラスを 1 回含めるだけです。
#include "TextHelper.h"
これでアプリをビルドして実行できます。このアプリの機能は豊富ではないことに注意してください。
パート 6: アプリの起動、中断、再開を行う
ユーザーがアプリのタイルを押すかクリックすると、App::OnLaunched
イベントが発生します。このイベントは、アプリがシステムによって終了され、他のアプリのためにメモリが解放された後でアプリに戻った場合にも発生します。どちらの場合も、インターネットに必ずアクセスし、このイベントに応じてデータを再度読み込みます。ただし、いずれかの場合にのみ呼び出す必要がある他のアクションがあります。関数に渡される LaunchActivatedEventArgs 引数と組み合わせて rootFrame を調べることにより、このようなアクションが必要になる状態を推測でき、適切な処置を実行できます。MainPage で自動的に追加された SuspensionManager クラスは、アプリの中断と再起動が行われる際に、アプリの状態を保存し復元するためのほとんどの処理を実行します。ここでは、そのメソッドを呼び出すだけです。
SuspensionManager コード ファイルを共通フォルダーのプロジェクトに追加します。 SuspensionManager.h を追加し、次のコードをそれにコピーします。
// // SuspensionManager.h // Declaration of the SuspensionManager class // #pragma once namespace SimpleBlogReader { namespace Common { /// <summary> /// SuspensionManager captures global session state to simplify process lifetime management /// for an application. Note that session state will be automatically cleared under a variety /// of conditions and should only be used to store information that would be convenient to /// carry across sessions, but that should be disacarded when an application crashes or is /// upgraded. /// </summary> class SuspensionManager sealed { public: static void RegisterFrame(Windows::UI::Xaml::Controls::Frame^ frame, Platform::String^ sessionStateKey, Platform::String^ sessionBaseKey = nullptr); static void UnregisterFrame(Windows::UI::Xaml::Controls::Frame^ frame); static concurrency::task<void> SaveAsync(); static concurrency::task<void> RestoreAsync(Platform::String^ sessionBaseKey = nullptr); static Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ SessionState(); static Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ SessionStateForFrame( Windows::UI::Xaml::Controls::Frame^ frame); private: static void RestoreFrameNavigationState(Windows::UI::Xaml::Controls::Frame^ frame); static void SaveFrameNavigationState(Windows::UI::Xaml::Controls::Frame^ frame); static Platform::Collections::Map<Platform::String^, Platform::Object^>^ _sessionState; static const wchar_t* sessionStateFilename; static std::vector<Platform::WeakReference> _registeredFrames; static Windows::UI::Xaml::DependencyProperty^ FrameSessionStateKeyProperty; static Windows::UI::Xaml::DependencyProperty^ FrameSessionBaseKeyProperty; static Windows::UI::Xaml::DependencyProperty^ FrameSessionStateProperty; }; } }
SuspensionManager.cpp コード ファイルを追加して、次のコードをそのファイルにコピーします。
// // SuspensionManager.cpp // Implementation of the SuspensionManager class // #include "pch.h" #include "SuspensionManager.h" #include <algorithm> using namespace SimpleBlogReader::Common; using namespace concurrency; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::Storage; using namespace Windows::Storage::FileProperties; using namespace Windows::Storage::Streams; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Interop; Map<String^, Object^>^ SuspensionManager::_sessionState = ref new Map<String^, Object^>(); const wchar_t* SuspensionManager::sessionStateFilename = L"_sessionState.dat"; std::vector<WeakReference> SuspensionManager::_registeredFrames; DependencyProperty^ SuspensionManager::FrameSessionStateKeyProperty = DependencyProperty::RegisterAttached("_FrameSessionStateKeyProperty", TypeName(String::typeid), TypeName(SuspensionManager::typeid), nullptr); DependencyProperty^ SuspensionManager::FrameSessionBaseKeyProperty = DependencyProperty::RegisterAttached("_FrameSessionBaseKeyProperty", TypeName(String::typeid), TypeName(SuspensionManager::typeid), nullptr); DependencyProperty^ SuspensionManager::FrameSessionStateProperty = DependencyProperty::RegisterAttached("_FrameSessionStateProperty", TypeName(IMap<String^, Object^>::typeid), TypeName(SuspensionManager::typeid), nullptr); class ObjectSerializeHelper { public: // Codes used for identifying serialized types enum StreamTypes { NullPtrType = 0, // Supported IPropertyValue types UInt8Type, UInt16Type, UInt32Type, UInt64Type, Int16Type, Int32Type, Int64Type, SingleType, DoubleType, BooleanType, Char16Type, GuidType, StringType, // Additional supported types StringToObjectMapType, // Marker values used to ensure stream integrity MapEndMarker }; static String^ ReadString(DataReader^ reader); static IMap<String^, Object^>^ ReadStringToObjectMap(DataReader^ reader); static Object^ ReadObject(DataReader^ reader); static void WriteString(DataWriter^ writer, String^ string); static void WriteProperty(DataWriter^ writer, IPropertyValue^ propertyValue); static void WriteStringToObjectMap(DataWriter^ writer, IMap<String^, Object^>^ map); static void WriteObject(DataWriter^ writer, Object^ object); }; /// <summary> /// Provides access to global session state for the current session. This state is serialized by /// <see cref="SaveAsync"/> and restored by <see cref="RestoreAsync"/> which require values to be /// one of the following: boxed values including integers, floating-point singles and doubles, /// wide characters, boolean, Strings and Guids, or Map<String^, Object^> where map values are /// subject to the same constraints. Session state should be as compact as possible. /// </summary> IMap<String^, Object^>^ SuspensionManager::SessionState() { return _sessionState; } /// <summary> /// Registers a <see cref="Frame"/> instance to allow its navigation history to be saved to /// and restored from <see cref="SessionState"/>. Frames should be registered once /// immediately after creation if they will participate in session state management. Upon /// registration if state has already been restored for the specified key /// the navigation history will immediately be restored. Subsequent invocations of /// <see cref="RestoreAsync(String)"/> will also restore navigation history. /// </summary> /// <param name="frame">An instance whose navigation history should be managed by /// <see cref="SuspensionManager"/></param> /// <param name="sessionStateKey">A unique key into <see cref="SessionState"/> used to /// store navigation-related information.</param> /// <param name="sessionBaseKey">An optional key that identifies the type of session. /// This can be used to distinguish between multiple application launch scenarios.</param> void SuspensionManager::RegisterFrame(Frame^ frame, String^ sessionStateKey, String^ sessionBaseKey) { if (frame->GetValue(FrameSessionStateKeyProperty) != nullptr) { throw ref new FailureException("Frames can only be registered to one session state key"); } if (frame->GetValue(FrameSessionStateProperty) != nullptr) { throw ref new FailureException("Frames must be either be registered before accessing frame session state, or not registered at all"); } if (sessionBaseKey != nullptr) { frame->SetValue(FrameSessionBaseKeyProperty, sessionBaseKey); sessionStateKey = sessionBaseKey + "_" + sessionStateKey; } // Use a dependency property to associate the session key with a frame, and keep a list of frames whose // navigation state should be managed frame->SetValue(FrameSessionStateKeyProperty, sessionStateKey); _registeredFrames.insert(_registeredFrames.begin(), WeakReference(frame)); // Check to see if navigation state can be restored RestoreFrameNavigationState(frame); } /// <summary> /// Disassociates a <see cref="Frame"/> previously registered by <see cref="RegisterFrame"/> /// from <see cref="SessionState"/>. Any navigation state previously captured will be /// removed. /// </summary> /// <param name="frame">An instance whose navigation history should no longer be /// managed.</param> void SuspensionManager::UnregisterFrame(Frame^ frame) { // Remove session state and remove the frame from the list of frames whose navigation // state will be saved (along with any weak references that are no longer reachable) auto key = safe_cast<String^>(frame->GetValue(FrameSessionStateKeyProperty)); if (SessionState()->HasKey(key)) { SessionState()->Remove(key); } _registeredFrames.erase( std::remove_if(_registeredFrames.begin(), _registeredFrames.end(), [=](WeakReference& e) { auto testFrame = e.Resolve<Frame>(); return testFrame == nullptr || testFrame == frame; }), _registeredFrames.end() ); } /// <summary> /// Provides storage for session state associated with the specified <see cref="Frame"/>. /// Frames that have been previously registered with <see cref="RegisterFrame"/> have /// their session state saved and restored automatically as a part of the global /// <see cref="SessionState"/>. Frames that are not registered have transient state /// that can still be useful when restoring pages that have been discarded from the /// navigation cache. /// </summary> /// <remarks>Apps may choose to rely on <see cref="NavigationHelper"/> to manage /// page-specific state instead of working with frame session state directly.</remarks> /// <param name="frame">The instance for which session state is desired.</param> /// <returns>A collection of state subject to the same serialization mechanism as /// <see cref="SessionState"/>.</returns> IMap<String^, Object^>^ SuspensionManager::SessionStateForFrame(Frame^ frame) { auto frameState = safe_cast<IMap<String^, Object^>^>(frame->GetValue(FrameSessionStateProperty)); if (frameState == nullptr) { auto frameSessionKey = safe_cast<String^>(frame->GetValue(FrameSessionStateKeyProperty)); if (frameSessionKey != nullptr) { // Registered frames reflect the corresponding session state if (!_sessionState->HasKey(frameSessionKey)) { _sessionState->Insert(frameSessionKey, ref new Map<String^, Object^>()); } frameState = safe_cast<IMap<String^, Object^>^>(_sessionState->Lookup(frameSessionKey)); } else { // Frames that aren't registered have transient state frameState = ref new Map<String^, Object^>(); } frame->SetValue(FrameSessionStateProperty, frameState); } return frameState; } void SuspensionManager::RestoreFrameNavigationState(Frame^ frame) { auto frameState = SessionStateForFrame(frame); if (frameState->HasKey("Navigation")) { frame->SetNavigationState(safe_cast<String^>(frameState->Lookup("Navigation"))); } } void SuspensionManager::SaveFrameNavigationState(Frame^ frame) { auto frameState = SessionStateForFrame(frame); frameState->Insert("Navigation", frame->GetNavigationState()); } /// <summary> /// Save the current <see cref="SessionState"/>. Any <see cref="Frame"/> instances /// registered with <see cref="RegisterFrame"/> will also preserve their current /// navigation stack, which in turn gives their active <see cref="Page"/> an opportunity /// to save its state. /// </summary> /// <returns>An asynchronous task that reflects when session state has been saved.</returns> task<void> SuspensionManager::SaveAsync(void) { // Save the navigation state for all registered frames for (auto && weakFrame : _registeredFrames) { auto frame = weakFrame.Resolve<Frame>(); if (frame != nullptr) SaveFrameNavigationState(frame); } // Serialize the session state synchronously to avoid asynchronous access to shared // state auto sessionData = ref new InMemoryRandomAccessStream(); auto sessionDataWriter = ref new DataWriter(sessionData->GetOutputStreamAt(0)); ObjectSerializeHelper::WriteObject(sessionDataWriter, _sessionState); // Once session state has been captured synchronously, begin the asynchronous process // of writing the result to disk return task<unsigned int>(sessionDataWriter->StoreAsync()).then([=](unsigned int) { return ApplicationData::Current->LocalFolder->CreateFileAsync(StringReference(sessionStateFilename), CreationCollisionOption::ReplaceExisting); }) .then([=](StorageFile^ createdFile) { return createdFile->OpenAsync(FileAccessMode::ReadWrite); }) .then([=](IRandomAccessStream^ newStream) { return RandomAccessStream::CopyAsync( sessionData->GetInputStreamAt(0), newStream->GetOutputStreamAt(0)); }) .then([=](UINT64 copiedBytes) { (void) copiedBytes; // Unused parameter return; }); } /// <summary> /// Restores previously saved <see cref="SessionState"/>. Any <see cref="Frame"/> instances /// registered with <see cref="RegisterFrame"/> will also restore their prior navigation /// state, which in turn gives their active <see cref="Page"/> an opportunity restore its /// state. /// </summary> /// <param name="sessionBaseKey">An optional key that identifies the type of session. /// This can be used to distinguish between multiple application launch scenarios.</param> /// <returns>An asynchronous task that reflects when session state has been read. The /// content of <see cref="SessionState"/> should not be relied upon until this task /// completes.</returns> task<void> SuspensionManager::RestoreAsync(String^ sessionBaseKey) { _sessionState->Clear(); task<StorageFile^> getFileTask(ApplicationData::Current->LocalFolder->GetFileAsync(StringReference(sessionStateFilename))); return getFileTask.then([=](StorageFile^ stateFile) { task<BasicProperties^> getBasicPropertiesTask(stateFile->GetBasicPropertiesAsync()); return getBasicPropertiesTask.then([=](BasicProperties^ stateFileProperties) { auto size = unsigned int(stateFileProperties->Size); if (size != stateFileProperties->Size) throw ref new FailureException("Session state larger than 4GB"); task<IRandomAccessStreamWithContentType^> openReadTask(stateFile->OpenReadAsync()); return openReadTask.then([=](IRandomAccessStreamWithContentType^ stateFileStream) { auto stateReader = ref new DataReader(stateFileStream); return task<unsigned int>(stateReader->LoadAsync(size)).then([=](unsigned int bytesRead) { (void) bytesRead; // Unused parameter // Deserialize the Session State Object^ content = ObjectSerializeHelper::ReadObject(stateReader); _sessionState = (Map<String^, Object^>^)content; // Restore any registered frames to their saved state for (auto && weakFrame : _registeredFrames) { auto frame = weakFrame.Resolve<Frame>(); if (frame != nullptr && safe_cast<String^>(frame->GetValue(FrameSessionBaseKeyProperty)) == sessionBaseKey) { frame->ClearValue(FrameSessionStateProperty); RestoreFrameNavigationState(frame); } } }, task_continuation_context::use_current()); }); }); }); } #pragma region Object serialization for a known set of types void ObjectSerializeHelper::WriteString(DataWriter^ writer, String^ string) { writer->WriteByte(StringType); writer->WriteUInt32(writer->MeasureString(string)); writer->WriteString(string); } void ObjectSerializeHelper::WriteProperty(DataWriter^ writer, IPropertyValue^ propertyValue) { switch (propertyValue->Type) { case PropertyType::UInt8: writer->WriteByte(StreamTypes::UInt8Type); writer->WriteByte(propertyValue->GetUInt8()); return; case PropertyType::UInt16: writer->WriteByte(StreamTypes::UInt16Type); writer->WriteUInt16(propertyValue->GetUInt16()); return; case PropertyType::UInt32: writer->WriteByte(StreamTypes::UInt32Type); writer->WriteUInt32(propertyValue->GetUInt32()); return; case PropertyType::UInt64: writer->WriteByte(StreamTypes::UInt64Type); writer->WriteUInt64(propertyValue->GetUInt64()); return; case PropertyType::Int16: writer->WriteByte(StreamTypes::Int16Type); writer->WriteUInt16(propertyValue->GetInt16()); return; case PropertyType::Int32: writer->WriteByte(StreamTypes::Int32Type); writer->WriteUInt32(propertyValue->GetInt32()); return; case PropertyType::Int64: writer->WriteByte(StreamTypes::Int64Type); writer->WriteUInt64(propertyValue->GetInt64()); return; case PropertyType::Single: writer->WriteByte(StreamTypes::SingleType); writer->WriteSingle(propertyValue->GetSingle()); return; case PropertyType::Double: writer->WriteByte(StreamTypes::DoubleType); writer->WriteDouble(propertyValue->GetDouble()); return; case PropertyType::Boolean: writer->WriteByte(StreamTypes::BooleanType); writer->WriteBoolean(propertyValue->GetBoolean()); return; case PropertyType::Char16: writer->WriteByte(StreamTypes::Char16Type); writer->WriteUInt16(propertyValue->GetChar16()); return; case PropertyType::Guid: writer->WriteByte(StreamTypes::GuidType); writer->WriteGuid(propertyValue->GetGuid()); return; case PropertyType::String: WriteString(writer, propertyValue->GetString()); return; default: throw ref new InvalidArgumentException("Unsupported property type"); } } void ObjectSerializeHelper::WriteStringToObjectMap(DataWriter^ writer, IMap<String^, Object^>^ map) { writer->WriteByte(StringToObjectMapType); writer->WriteUInt32(map->Size); for (auto && pair : map) { WriteObject(writer, pair->Key); WriteObject(writer, pair->Value); } writer->WriteByte(MapEndMarker); } void ObjectSerializeHelper::WriteObject(DataWriter^ writer, Object^ object) { if (object == nullptr) { writer->WriteByte(NullPtrType); return; } auto propertyObject = dynamic_cast<IPropertyValue^>(object); if (propertyObject != nullptr) { WriteProperty(writer, propertyObject); return; } auto mapObject = dynamic_cast<IMap<String^, Object^>^>(object); if (mapObject != nullptr) { WriteStringToObjectMap(writer, mapObject); return; } throw ref new InvalidArgumentException("Unsupported data type"); } String^ ObjectSerializeHelper::ReadString(DataReader^ reader) { int length = reader->ReadUInt32(); String^ string = reader->ReadString(length); return string; } IMap<String^, Object^>^ ObjectSerializeHelper::ReadStringToObjectMap(DataReader^ reader) { auto map = ref new Map<String^, Object^>(); auto size = reader->ReadUInt32(); for (unsigned int index = 0; index < size; index++) { auto key = safe_cast<String^>(ReadObject(reader)); auto value = ReadObject(reader); map->Insert(key, value); } if (reader->ReadByte() != StreamTypes::MapEndMarker) { throw ref new InvalidArgumentException("Invalid stream"); } return map; } Object^ ObjectSerializeHelper::ReadObject(DataReader^ reader) { auto type = reader->ReadByte(); switch (type) { case StreamTypes::NullPtrType: return nullptr; case StreamTypes::UInt8Type: return reader->ReadByte(); case StreamTypes::UInt16Type: return reader->ReadUInt16(); case StreamTypes::UInt32Type: return reader->ReadUInt32(); case StreamTypes::UInt64Type: return reader->ReadUInt64(); case StreamTypes::Int16Type: return reader->ReadInt16(); case StreamTypes::Int32Type: return reader->ReadInt32(); case StreamTypes::Int64Type: return reader->ReadInt64(); case StreamTypes::SingleType: return reader->ReadSingle(); case StreamTypes::DoubleType: return reader->ReadDouble(); case StreamTypes::BooleanType: return reader->ReadBoolean(); case StreamTypes::Char16Type: return (char16_t) reader->ReadUInt16(); case StreamTypes::GuidType: return reader->ReadGuid(); case StreamTypes::StringType: return ReadString(reader); case StreamTypes::StringToObjectMapType: return ReadStringToObjectMap(reader); default: throw ref new InvalidArgumentException("Unsupported property type"); } } #pragma endregion
app.xaml.cpp に、次の #include ディレクティブを追加します。
#include "Common\SuspensionManager.h"
namespace ディレクティブを追加します。
using namespace SimpleBlogReader::Common;
既にある関数を次のコードに置き換えます。
void App::OnLaunched(LaunchActivatedEventArgs^ e) { #if _DEBUG if (IsDebuggerPresent()) { DebugSettings->EnableFrameRateCounter = true; } #endif auto rootFrame = dynamic_cast<Frame^>(Window::Current->Content); // Do not repeat app initialization when the Window already has content, // just ensure that the window is active. if (rootFrame == nullptr) { // Create a Frame to act as the navigation context and associate it with // a SuspensionManager key rootFrame = ref new Frame(); SuspensionManager::RegisterFrame(rootFrame, "AppFrame"); // Initialize the Atom and RSS feed objects with data from the web FeedDataSource^ feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); if (feedDataSource->Feeds->Size == 0) { if (e->PreviousExecutionState == ApplicationExecutionState::Terminated) { // On resume FeedDataSource needs to know whether the app was on a // specific FeedData, which will be the unless it was on MainPage // when it was terminated. ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; auto values = localSettings->Values; if (localSettings->Values->HasKey("LastViewedFeed")) { feedDataSource->CurrentFeedUri = safe_cast<String^>(localSettings->Values->Lookup("LastViewedFeed")); } } feedDataSource->InitDataSource(); } // We have 4 pages in the app rootFrame->CacheSize = 4; auto prerequisite = task<void>([](){}); if (e->PreviousExecutionState == ApplicationExecutionState::Terminated) { // Now restore the pages if we are resuming prerequisite = Common::SuspensionManager::RestoreAsync(); } // if we're starting fresh, prerequisite will execute immediately. // if resuming from termination, prerequisite will wait until RestoreAsync() completes. prerequisite.then([=]() { if (rootFrame->Content == nullptr) { if (!rootFrame->Navigate(MainPage::typeid, e->Arguments)) { throw ref new FailureException("Failed to create initial page"); } } // Place the frame in the current Window Window::Current->Content = rootFrame; Window::Current->Activate(); }, task_continuation_context::use_current()); } // There is a frame, but is has no content, so navigate to main page // and activate the window. else if (rootFrame->Content == nullptr) { #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP // Removes the turnstile navigation for startup. if (rootFrame->ContentTransitions != nullptr) { _transitions = ref new TransitionCollection(); for (auto transition : rootFrame->ContentTransitions) { _transitions->Append(transition); } } rootFrame->ContentTransitions = nullptr; _firstNavigatedToken = rootFrame->Navigated += ref new NavigatedEventHandler(this, &App::RootFrame_FirstNavigated); #endif // When the navigation stack isn't restored navigate to the first page, // configuring the new page by passing required information as a navigation // parameter. if (!rootFrame->Navigate(MainPage::typeid, e->Arguments)) { throw ref new FailureException("Failed to create initial page"); } // Ensure the current window is active in this code path. // we also called this inside the task for the other path. Window::Current->Activate(); } }
App クラスは共有プロジェクト内にあるため、ここに記述されているコードは Windows アプリと Windows Phone アプリの両方で動作します (ただし、WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP マクロが定義されている部分を除く)。
OnSuspending ハンドラーは単純です。このハンドラーが呼び出されるのは、ユーザーがアプリを終了したときではなく、システムによってアプリがシャットダウンされたときです。ここでは、SuspensionManager を使って処理を実行するように指定します。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 が表示されます。Windows Phone プロジェクトの場合は項目ページがありません。そのため、ListView を手動で追加する基本ページが用意されています。デバイスの向きが変わると、リスト ビューではその表示状態が自動的に調整されます。
通常、最初の UI ページだけでなくすべてのページで、次のような同じ基本タスクを実行する必要があります。
- UI の記述とデータへのバインディングを行う XAML マークアップを追加する。
- カスタム コードを
LoadState
メンバー関数とSaveState
メンバー関数に追加する。 - イベントを処理する。通常、それらのイベントの少なくとも 1 つは、次のページに移動するコードを保持しているイベントです。
これらのタスクを順に実行します。最初に、Windows プロジェクトで行います。
XAML マークアップを追加する (MainPage)
メイン ページでは、GridView コントロール内の各 FeedData オブジェクトがレンダリングされます。データの外観を記述するために、DataTemplate を作成します。これは、各項目のレンダリングに使われる XAML ツリーです。レイアウト、フォント、色などに対して DataTemplates をどのように使うかは、アプリの作成者が持つ想像力とスタイルのセンスに依存します。このページでは、レンダリング時に次のようなシンプルなテンプレートを使います。
XAML スタイルは Microsoft Word でのスタイルに類似しています。XAML 要素のプロパティ値 "TargetType" のセットをグループ化すると便利です。 スタイルを、他のスタイルに基づいて作成することもできます。"x:Key" 属性は、スタイルを使うときにそのスタイルを参照するための名前を指定します。
このテンプレートとサポートされているスタイルを、MainPage.xaml (Windows 8.1) の Page.Resources ノードに設定します。これらのテンプレートやスタイルは、MainPage でのみ使われます。
<Style x:Key="GridTitleTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}"> <Setter Property="FontSize" Value="26.667"/> <Setter Property="Margin" Value="12,0,12,2"/> </Style> <Style x:Key="GridDescriptionTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}"> <Setter Property="VerticalAlignment" Value="Bottom"/> <Setter Property="Margin" Value="12,0,12,60"/> </Style> <DataTemplate x:Key="DefaultGridItemTemplate"> <Grid HorizontalAlignment="Left" Width="250" Height="250" Background="{StaticResource BlockBackgroundBrush}" > <StackPanel Margin="0,22,16,0"> <TextBlock Text="{Binding Title}" Style="{StaticResource GridTitleTextStyle}" Margin="10,10,10,10"/> <TextBlock Text="{Binding Description}" Style="{StaticResource GridDescriptionTextStyle}" Margin="10,10,10,10" /> </StackPanel> <Border BorderBrush="DarkRed" BorderThickness="4" VerticalAlignment="Bottom"> <StackPanel VerticalAlignment="Bottom" Orientation="Horizontal" Background="{StaticResource GreenBlockBackgroundBrush}"> <TextBlock Text="Last Updated" FontWeight="Bold" Margin="12,4,0,8" Height="42"/> <TextBlock Text="{Binding PubDate, Converter={StaticResource dateConverter}}" FontWeight="ExtraBold" Margin="4,4,12,8" Height="42" Width="88"/> </StackPanel> </Border> </Grid> </DataTemplate>
GreenBlockBackgroundBrush
の下に赤色の波線が表示されます。これについては、いくつかの手順を実行して対応します。MainPage.xaml (Windows 8.1) で、ページ ローカルの
AppName
要素を削除し、App スコープで追加する予定のグローバル要素が非表示にならないようにします。CollectionViewSource を Page.Resources ノードに追加します。このオブジェクトによって、ListView がデータ モデルに接続されます。
<!-- Collection of items displayed by this page --> <CollectionViewSource x:Name="itemsViewSource" Source="{Binding Items}"/>
Page 要素では、DataContext 属性が MainPage クラスの DefaultViewModel プロパティに既に設定されていることに注意してください。ここでは、そのプロパティが FeedDataSource になるように設定します。そのため、CollectionViewSource は FeedDataSource に基づいて検出可能な Items コレクションを探します。
App.xaml に、アプリ名のグローバル リソース文字列と、アプリの複数のページから参照されるその他のリソースを追加します。 ここにリソースを設定するので、各ページで個別にリソースを定義する必要はありません。App.xaml の Resources ノードに、次の要素を追加します。
<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 に設定します。これにより、既定では ListView が表示されなくなります。
- 2 つの VisualState から成るオブジェクトのセットを作成します。一方の VisualState では横向きでの UI の動作を記述し、もう一方の VisualState では縦向きでの UI の動作を記述します。
- Window::SizeChanged イベントを処理します。このイベントは、デバイスの向きが変わった場合、またはユーザーがウィンドウのサイズを小さくしたり大きくしたりする場合に発生します。新しいサイズの高さと幅を調べます。高さが幅より大きい場合、縦向きの VisualState を呼び出します。それ以外の場合は、横向きの 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 の定義を貼り付けます。これはルート グリッド内の最後の要素となります (この定義は Grid の外部には設定しないでください。設定すると動作しません)。2 つの状態がありますが、明示的に定義するのは 1 つだけであることに注意してください。これは、DefaultLayout の状態がこのページの XAML で既に記述されているためです。
<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 ページで注意する必要がある 2 つの主要なメンバー関数は、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 関数呼び出しの 2 番目のパラメーターは、あるページから別のページへデータを渡す場合に使われます。ここで渡すすべてのオブジェクトは、SuspensionManager によって自動的に保存されシリアル化されます。この保存とシリアル化は、アプリが中断されると必ず実行されます。これにより、アプリが再開されるときには、値を復元することができます。既定では、SuspensionManager は組み込み型 (String および Guid) のみをサポートします。より高度なシリアル化が必要な場合は、カスタムの SuspensionManager を作成できます。ここでは、現在のフィードを検索するために SplitPage で使われる String 型のデータを渡します。
項目をクリックしたきに移動するには
ユーザーがグリッド内の項目をクリックすると、イベント ハンドラーはクリックされた項目を取得し、今後アプリが任意の時点で中断された場合に備えて、その項目を "現在のフィード" として設定します。その後で、次のページに移動します。フィードのタイトルは次のページに渡されます。これにより、次のページはそのフィードのデータを検索できます。貼り付けるコードを次に示します。
void MainPage::ItemGridView_ItemClick(Object^ sender, ItemClickEventArgs^ e) { // We must manually cast from Object^ to FeedData^. auto feedData = safe_cast<FeedData^>(e->ClickedItem); // Store the feed and tell other pages it's loaded and ready to go. auto app = safe_cast<App^>(App::Current); app->SetCurrentFeed(feedData); // Only navigate if there are items in the feed if (feedData->Items->Size > 0) { // Navigate to SplitPage and pass the title of the selected feed. // SplitPage will receive this in its LoadState method in the // navigationParamter. this->Frame->Navigate(SplitPage::typeid, feedData->Title); } }
上のコードをコンパイルするために、現在のファイル (MainPage.xaml.cpp (Windows 8.1)) の最上部に SplitPage.xaml.h の #include ディレクティブを指定する必要があります。
#include "SplitPage.xaml.h"
Page_SizeChanged イベントを処理するには
MainPage.xaml で、
x:Name="pageRoot"
をルート Page 要素の属性に追加することで、名前をルート要素に追加し、属性SizeChanged="pageRoot_SizeChanged"
を追加してイベント ハンドラーを作成します。cpp ファイル内のハンドラーの実装は、次のコードに置き換えます。void MainPage::pageRoot_SizeChanged(Platform::Object^ sender, SizeChangedEventArgs^ e) { if (e->NewSize.Height / e->NewSize.Width >= 1) { VisualStateManager::GoToState(this, "Portrait", false); } else { VisualStateManager::GoToState(this, "DefaultLayout", false); } }
その後、この関数の宣言を MainPage.xaml.h の MainPage クラスに追加します。
private: void pageRoot_SizeChanged(Platform::Object^ sender, SizeChangedEventArgs^ e);
コードは単純です。シミュレーターでアプリを実行し、デバイスを回転させると、GridView と ListView での UI の変化を確認できます。
XAML を追加する (Windows Phone アプリの MainPage)
次に、Windows Phone アプリのメイン ページを動作させるための作業を行います。この場合、コードの量は少なくなります。これは、共有プロジェクトに保存したすべてのコードを使うためです。また、Windows Phone アプリでは GridView コントロールがサポートされません。これは、このコントロールが適切に動作するには、画面が小さすぎるためです。ここでは、横向きに自動的に調整され、VisualState の変更を必要としない ListView を使います。最初に、DataContext 属性を Page 要素に追加します。この属性は、ItemsPage や SplitPage のように、Windows Phone の基本ページでは自動生成されません。
ページ ナビゲーションを実装するために、ページには 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; }; } }
共通フォルダーに 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) { }
共通フォルダーに、NavigationHelper.h ファイルを追加し、次のコードをこのファイルにコピーします。
// // NavigationHelper.h // Declaration of the NavigationHelper and associated classes // #pragma once #include "RelayCommand.h" namespace SimpleBlogReader { namespace Common { /// <summary> /// Class used to hold the event data required when a page attempts to load state. /// </summary> public ref class LoadStateEventArgs sealed { public: /// <summary> /// The parameter value passed to <see cref="Frame->Navigate(Type, Object)"/> /// when this page was initially requested. /// </summary> property Platform::Object^ NavigationParameter { Platform::Object^ get(); } /// <summary> /// A dictionary of state preserved by this page during an earlier /// session. This will be null the first time a page is visited. /// </summary> property Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ PageState { Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ get(); } internal: LoadStateEventArgs(Platform::Object^ navigationParameter, Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ pageState); private: Platform::Object^ _navigationParameter; Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ _pageState; }; /// <summary> /// Represents the method that will handle the <see cref="NavigationHelper->LoadState"/>event /// </summary> public delegate void LoadStateEventHandler(Platform::Object^ sender, LoadStateEventArgs^ e); /// <summary> /// Class used to hold the event data required when a page attempts to save state. /// </summary> public ref class SaveStateEventArgs sealed { public: /// <summary> /// An empty dictionary to be populated with serializable state. /// </summary> property Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ PageState { Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ get(); } internal: SaveStateEventArgs(Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ pageState); private: Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ _pageState; }; /// <summary> /// Represents the method that will handle the <see cref="NavigationHelper->SaveState"/>event /// </summary> public delegate void SaveStateEventHandler(Platform::Object^ sender, SaveStateEventArgs^ e); /// <summary> /// NavigationHelper aids in navigation between pages. It provides commands used to /// navigate back and forward as well as registers for standard mouse and keyboard /// shortcuts used to go back and forward in Windows and the hardware back button in /// Windows Phone. In addition it integrates SuspensionManger to handle process lifetime /// management and state management when navigating between pages. /// </summary> /// <example> /// To make use of NavigationHelper, follow these two steps or /// start with a BasicPage or any other Page item template other than BlankPage. /// /// 1) Create an instance of the NavigationHelper somewhere such as in the /// constructor for the page and register a callback for the LoadState and /// SaveState events. /// <code> /// MyPage::MyPage() /// { /// InitializeComponent(); /// auto navigationHelper = ref new Common::NavigationHelper(this); /// navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &MyPage::LoadState); /// navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &MyPage::SaveState); /// } /// /// void MyPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) /// { } /// void MyPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e) /// { } /// </code> /// /// 2) Register the page to call into the NavigationHelper whenever the page participates /// in navigation by overriding the <see cref="Windows::UI::Xaml::Controls::Page::OnNavigatedTo"/> /// and <see cref="Windows::UI::Xaml::Controls::Page::OnNavigatedFrom"/> events. /// <code> /// void MyPage::OnNavigatedTo(NavigationEventArgs^ e) /// { /// NavigationHelper->OnNavigatedTo(e); /// } /// /// void MyPage::OnNavigatedFrom(NavigationEventArgs^ e) /// { /// NavigationHelper->OnNavigatedFrom(e); /// } /// </code> /// </example> [Windows::Foundation::Metadata::WebHostHidden] [Windows::UI::Xaml::Data::Bindable] public ref class NavigationHelper sealed { public: /// <summary> /// <see cref="RelayCommand"/> used to bind to the back Button's Command property /// for navigating to the most recent item in back navigation history, if a Frame /// manages its own navigation history. /// /// The <see cref="RelayCommand"/> is set up to use the virtual method <see cref="GoBack"/> /// as the Execute Action and <see cref="CanGoBack"/> for CanExecute. /// </summary> property RelayCommand^ GoBackCommand { RelayCommand^ get(); } /// <summary> /// <see cref="RelayCommand"/> used for navigating to the most recent item in /// the forward navigation history, if a Frame manages its own navigation history. /// /// The <see cref="RelayCommand"/> is set up to use the virtual method <see cref="GoForward"/> /// as the Execute Action and <see cref="CanGoForward"/> for CanExecute. /// </summary> property RelayCommand^ GoForwardCommand { RelayCommand^ get(); } internal: NavigationHelper(Windows::UI::Xaml::Controls::Page^ page, RelayCommand^ goBack = nullptr, RelayCommand^ goForward = nullptr); bool CanGoBack(); void GoBack(); bool CanGoForward(); void GoForward(); void OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e); void OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e); event LoadStateEventHandler^ LoadState; event SaveStateEventHandler^ SaveState; private: Platform::WeakReference _page; RelayCommand^ _goBackCommand; RelayCommand^ _goForwardCommand; #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP Windows::Foundation::EventRegistrationToken _backPressedEventToken; void HardwareButton_BackPressed(Platform::Object^ sender, Windows::Phone::UI::Input::BackPressedEventArgs^ e); #else bool _navigationShortcutsRegistered; Windows::Foundation::EventRegistrationToken _acceleratorKeyEventToken; Windows::Foundation::EventRegistrationToken _pointerPressedEventToken; void CoreDispatcher_AcceleratorKeyActivated(Windows::UI::Core::CoreDispatcher^ sender, Windows::UI::Core::AcceleratorKeyEventArgs^ e); void CoreWindow_PointerPressed(Windows::UI::Core::CoreWindow^ sender, Windows::UI::Core::PointerEventArgs^ e); #endif Platform::String^ _pageKey; Windows::Foundation::EventRegistrationToken _loadedEventToken; Windows::Foundation::EventRegistrationToken _unloadedEventToken; void OnLoaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); void OnUnloaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); ~NavigationHelper(); }; } }
ここで、次のコードを使用して、同じフォルダーに実装ファイル NavigationHelper.cpp を追加します。
// // NavigationHelper.cpp // Implementation of the NavigationHelper and associated classes // #include "pch.h" #include "NavigationHelper.h" #include "RelayCommand.h" #include "SuspensionManager.h" using namespace SimpleBlogReader::Common; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::System; using namespace Windows::UI::Core; using namespace Windows::UI::ViewManagement; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Interop; using namespace Windows::UI::Xaml::Navigation; #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP using namespace Windows::Phone::UI::Input; #endif /// <summary> /// Initializes a new instance of the <see cref="LoadStateEventArgs"/> class. /// </summary> /// <param name="navigationParameter"> /// The parameter value passed to <see cref="Frame->Navigate(Type, Object)"/> /// when this page was initially requested. /// </param> /// <param name="pageState"> /// A dictionary of state preserved by this page during an earlier /// session. This will be null the first time a page is visited. /// </param> LoadStateEventArgs::LoadStateEventArgs(Object^ navigationParameter, IMap<String^, Object^>^ pageState) { _navigationParameter = navigationParameter; _pageState = pageState; } /// <summary> /// Gets the <see cref="NavigationParameter"/> property of <see cref"LoadStateEventArgs"/> class. /// </summary> Object^ LoadStateEventArgs::NavigationParameter::get() { return _navigationParameter; } /// <summary> /// Gets the <see cref="PageState"/> property of <see cref"LoadStateEventArgs"/> class. /// </summary> IMap<String^, Object^>^ LoadStateEventArgs::PageState::get() { return _pageState; } /// <summary> /// Initializes a new instance of the <see cref="SaveStateEventArgs"/> class. /// </summary> /// <param name="pageState">An empty dictionary to be populated with serializable state.</param> SaveStateEventArgs::SaveStateEventArgs(IMap<String^, Object^>^ pageState) { _pageState = pageState; } /// <summary> /// Gets the <see cref="PageState"/> property of <see cref"SaveStateEventArgs"/> class. /// </summary> IMap<String^, Object^>^ SaveStateEventArgs::PageState::get() { return _pageState; } /// <summary> /// Initializes a new instance of the <see cref="NavigationHelper"/> class. /// </summary> /// <param name="page">A reference to the current page used for navigation. /// This reference allows for frame manipulation and to ensure that keyboard /// navigation requests only occur when the page is occupying the entire window.</param> NavigationHelper::NavigationHelper(Page^ page, RelayCommand^ goBack, RelayCommand^ goForward) : _page(page), _goBackCommand(goBack), _goForwardCommand(goForward) { // When this page is part of the visual tree make two changes: // 1) Map application view state to visual state for the page // 2) Handle hardware navigation requests _loadedEventToken = page->Loaded += ref new RoutedEventHandler(this, &NavigationHelper::OnLoaded); //// Undo the same changes when the page is no longer visible _unloadedEventToken = page->Unloaded += ref new RoutedEventHandler(this, &NavigationHelper::OnUnloaded); } NavigationHelper::~NavigationHelper() { delete _goBackCommand; delete _goForwardCommand; _page = nullptr; } /// <summary> /// Invoked when the page is part of the visual tree /// </summary> /// <param name="sender">Instance that triggered the event.</param> /// <param name="e">Event data describing the conditions that led to the event.</param> void NavigationHelper::OnLoaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP _backPressedEventToken = HardwareButtons::BackPressed += ref new EventHandler<BackPressedEventArgs^>(this, &NavigationHelper::HardwareButton_BackPressed); #else Page ^page = _page.Resolve<Page>(); // Keyboard and mouse navigation only apply when occupying the entire window if (page != nullptr && page->ActualHeight == Window::Current->Bounds.Height && page->ActualWidth == Window::Current->Bounds.Width) { // Listen to the window directly so focus isn't required _acceleratorKeyEventToken = Window::Current->CoreWindow->Dispatcher->AcceleratorKeyActivated += ref new TypedEventHandler<CoreDispatcher^, AcceleratorKeyEventArgs^>(this, &NavigationHelper::CoreDispatcher_AcceleratorKeyActivated); _pointerPressedEventToken = Window::Current->CoreWindow->PointerPressed += ref new TypedEventHandler<CoreWindow^, PointerEventArgs^>(this, &NavigationHelper::CoreWindow_PointerPressed); _navigationShortcutsRegistered = true; } #endif } /// <summary> /// Invoked when the page is removed from visual tree /// </summary> /// <param name="sender">Instance that triggered the event.</param> /// <param name="e">Event data describing the conditions that led to the event.</param> void NavigationHelper::OnUnloaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP HardwareButtons::BackPressed -= _backPressedEventToken; #else if (_navigationShortcutsRegistered) { Window::Current->CoreWindow->Dispatcher->AcceleratorKeyActivated -= _acceleratorKeyEventToken; Window::Current->CoreWindow->PointerPressed -= _pointerPressedEventToken; _navigationShortcutsRegistered = false; } #endif // Remove handler and release the reference to page Page ^page = _page.Resolve<Page>(); if (page != nullptr) { page->Loaded -= _loadedEventToken; page->Unloaded -= _unloadedEventToken; delete _goBackCommand; delete _goForwardCommand; _goForwardCommand = nullptr; _goBackCommand = nullptr; } } #pragma region Navigation support /// <summary> /// Method used by the <see cref="GoBackCommand"/> property /// to determine if the <see cref="Frame"/> can go back. /// </summary> /// <returns> /// true if the <see cref="Frame"/> has at least one entry /// in the back navigation history. /// </returns> bool NavigationHelper::CanGoBack() { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frame = page->Frame; return (frame != nullptr && frame->CanGoBack); } return false; } /// <summary> /// Method used by the <see cref="GoBackCommand"/> property /// to invoke the <see cref="Windows::UI::Xaml::Controls::Frame::GoBack"/> method. /// </summary> void NavigationHelper::GoBack() { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frame = page->Frame; if (frame != nullptr && frame->CanGoBack) { frame->GoBack(); } } } /// <summary> /// Method used by the <see cref="GoForwardCommand"/> property /// to determine if the <see cref="Frame"/> can go forward. /// </summary> /// <returns> /// true if the <see cref="Frame"/> has at least one entry /// in the forward navigation history. /// </returns> bool NavigationHelper::CanGoForward() { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frame = page->Frame; return (frame != nullptr && frame->CanGoForward); } return false; } /// <summary> /// Method used by the <see cref="GoForwardCommand"/> property /// to invoke the <see cref="Windows::UI::Xaml::Controls::Frame::GoBack"/> method. /// </summary> void NavigationHelper::GoForward() { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frame = page->Frame; if (frame != nullptr && frame->CanGoForward) { frame->GoForward(); } } } /// <summary> /// Gets the <see cref="GoBackCommand"/> property of <see cref"NavigationHelper"/> class. /// </summary> RelayCommand^ NavigationHelper::GoBackCommand::get() { if (_goBackCommand == nullptr) { _goBackCommand = ref new RelayCommand( [this](Object^) -> bool { return CanGoBack(); }, [this](Object^) -> void { GoBack(); } ); } return _goBackCommand; } /// <summary> /// Gets the <see cref="GoForwardCommand"/> property of <see cref"NavigationHelper"/> class. /// </summary> RelayCommand^ NavigationHelper::GoForwardCommand::get() { if (_goForwardCommand == nullptr) { _goForwardCommand = ref new RelayCommand( [this](Object^) -> bool { return CanGoForward(); }, [this](Object^) -> void { GoForward(); } ); } return _goForwardCommand; } #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP /// <summary> /// Handles the back button press and navigates through the history of the root frame. /// </summary> void NavigationHelper::HardwareButton_BackPressed(Object^ sender, BackPressedEventArgs^ e) { if (this->GoBackCommand->CanExecute(nullptr)) { e->Handled = true; this->GoBackCommand->Execute(nullptr); } } #else /// <summary> /// Invoked on every keystroke, including system keys such as Alt key combinations, when /// this page is active and occupies the entire window. Used to detect keyboard navigation /// between pages even when the page itself doesn't have focus. /// </summary> /// <param name="sender">Instance that triggered the event.</param> /// <param name="e">Event data describing the conditions that led to the event.</param> void NavigationHelper::CoreDispatcher_AcceleratorKeyActivated(CoreDispatcher^ sender, AcceleratorKeyEventArgs^ e) { sender; // Unused parameter auto virtualKey = e->VirtualKey; // Only investigate further when Left, Right, or the dedicated Previous or Next keys // are pressed if ((e->EventType == CoreAcceleratorKeyEventType::SystemKeyDown || e->EventType == CoreAcceleratorKeyEventType::KeyDown) && (virtualKey == VirtualKey::Left || virtualKey == VirtualKey::Right || virtualKey == VirtualKey::GoBack || virtualKey == VirtualKey::GoForward)) { auto coreWindow = Window::Current->CoreWindow; auto downState = Windows::UI::Core::CoreVirtualKeyStates::Down; bool menuKey = (coreWindow->GetKeyState(VirtualKey::Menu) & downState) == downState; bool controlKey = (coreWindow->GetKeyState(VirtualKey::Control) & downState) == downState; bool shiftKey = (coreWindow->GetKeyState(VirtualKey::Shift) & downState) == downState; bool noModifiers = !menuKey && !controlKey && !shiftKey; bool onlyAlt = menuKey && !controlKey && !shiftKey; if ((virtualKey == VirtualKey::GoBack && noModifiers) || (virtualKey == VirtualKey::Left && onlyAlt)) { // When the previous key or Alt+Left are pressed navigate back e->Handled = true; GoBackCommand->Execute(this); } else if ((virtualKey == VirtualKey::GoForward && noModifiers) || (virtualKey == VirtualKey::Right && onlyAlt)) { // When the next key or Alt+Right are pressed navigate forward e->Handled = true; GoForwardCommand->Execute(this); } } } /// <summary> /// Invoked on every mouse click, touch screen tap, or equivalent interaction when this /// page is active and occupies the entire window. Used to detect browser-style next and /// previous mouse button clicks to navigate between pages. /// </summary> /// <param name="sender">Instance that triggered the event.</param> /// <param name="e">Event data describing the conditions that led to the event.</param> void NavigationHelper::CoreWindow_PointerPressed(CoreWindow^ sender, PointerEventArgs^ e) { auto properties = e->CurrentPoint->Properties; // Ignore button chords with the left, right, and middle buttons if (properties->IsLeftButtonPressed || properties->IsRightButtonPressed || properties->IsMiddleButtonPressed) { return; } // If back or foward are pressed (but not both) navigate appropriately bool backPressed = properties->IsXButton1Pressed; bool forwardPressed = properties->IsXButton2Pressed; if (backPressed ^ forwardPressed) { e->Handled = true; if (backPressed) { if (GoBackCommand->CanExecute(this)) { GoBackCommand->Execute(this); } } else { if (GoForwardCommand->CanExecute(this)) { GoForwardCommand->Execute(this); } } } } #endif #pragma endregion #pragma region Process lifetime management /// <summary> /// Invoked when this page is about to be displayed in a Frame. /// </summary> /// <param name="e">Event data that describes how this page was reached. The Parameter /// property provides the group to be displayed.</param> void NavigationHelper::OnNavigatedTo(NavigationEventArgs^ e) { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frameState = SuspensionManager::SessionStateForFrame(page->Frame); _pageKey = "Page-" + page->Frame->BackStackDepth; if (e->NavigationMode == NavigationMode::New) { // Clear existing state for forward navigation when adding a new page to the // navigation stack auto nextPageKey = _pageKey; int nextPageIndex = page->Frame->BackStackDepth; while (frameState->HasKey(nextPageKey)) { frameState->Remove(nextPageKey); nextPageIndex++; nextPageKey = "Page-" + nextPageIndex; } // Pass the navigation parameter to the new page LoadState(this, ref new LoadStateEventArgs(e->Parameter, nullptr)); } else { // Pass the navigation parameter and preserved page state to the page, using // the same strategy for loading suspended state and recreating pages discarded // from cache LoadState(this, ref new LoadStateEventArgs(e->Parameter, safe_cast<IMap<String^, Object^>^>(frameState->Lookup(_pageKey)))); } } } /// <summary> /// Invoked when this page will no longer be displayed in a Frame. /// </summary> /// <param name="e">Event data that describes how this page was reached. The Parameter /// property provides the group to be displayed.</param> void NavigationHelper::OnNavigatedFrom(NavigationEventArgs^ e) { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frameState = SuspensionManager::SessionStateForFrame(page->Frame); auto pageState = ref new Map<String^, Object^>(); SaveState(this, ref new SaveStateEventArgs(pageState)); frameState->Insert(_pageKey, pageState); } } #pragma endregion
ここで、NavigationHelper を含めるためのコードを MainPage.xaml.h ヘッダー ファイルに追加します。また、後で必要になる DefaultViewModel プロパティを追加します。
// // MainPage.xaml.h // Declaration of the MainPage class // #pragma once #include "MainPage.g.h" #include "Common\NavigationHelper.h" namespace SimpleBlogReader { namespace WFC = Windows::Foundation::Collections; namespace WUIX = Windows::UI::Xaml; namespace WUIXNav = Windows::UI::Xaml::Navigation; namespace WUIXControls = Windows::UI::Xaml::Controls; /// <summary> /// A basic page that provides characteristics common to most applications. /// </summary> [Windows::Foundation::Metadata::WebHostHidden] public ref class MainPage sealed { public: MainPage(); /// <summary> /// Gets the view model for this <see cref="Page"/>. /// This can be changed to a strongly typed view model. /// </summary> property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel { WFC::IObservableMap<Platform::String^, Platform::Object^>^ get(); } /// <summary> /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>. /// </summary> property Common::NavigationHelper^ NavigationHelper { Common::NavigationHelper^ get(); } protected: virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override; virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override; private: void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e); void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e); static WUIX::DependencyProperty^ _defaultViewModelProperty; static WUIX::DependencyProperty^ _navigationHelperProperty; }; }
MainPage.xaml.cpp に、NavigationHelper の実装と読み込み/保存状態のスタブ、および DefaultViewModel プロパティを追加します。また、必要な 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) で、ページの下方向へ移動して、"Title Panel" コメントを探し、StackPanel 全体を削除します。Windows Phone の場合、ブログ フィードを一覧表示するための画面領域が必要になります。
ページをさらに下へ移動すると、コメント
"TODO: Content should be placed within the following grid"
が指定された Grid があります。次の ListView をその Grid 内に設定します。<!-- 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: 投稿の一覧と、選んだ投稿のテキスト ビューを表示する
ここでは、2 つのページを Windows Phone アプリに追加します。投稿の一覧を表示するページと、選んだ投稿のテキスト バージョンを表示するページです。Windows アプリでは、SplitPage という 1 つのページを追加する必要があります。このページでは、一方の側に一覧が表示され、他方の側に選んだ投稿のテキストが表示されます。最初に Windows Phone のページを取り上げます。
XAML マークアップを追加する (Windows Phone アプリの FeedPage)
Windows Phone プロジェクトで、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>
Grid 要素には、次の StackPanel を追加します。
<!-- TitlePanel --> <StackPanel Grid.Row="0" Margin="24,17,0,28"> <TextBlock Text="{StaticResource AppName}" Style="{ThemeResource TitleTextBlockStyle}" Typography.Capitals="SmallCaps"/> </StackPanel>
次に、グリッド内に ListView を追加します (開始要素の直後)。
<!-- Vertical scrolling item list --> <ListView x:Name="itemListView" AutomationProperties.AutomationId="ItemsListView" AutomationProperties.Name="Items" TabIndex="1" Grid.Row="1" Margin="-10,-10,0,0" IsItemClickEnabled="True" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" IsSwipeEnabled="False" ItemClick="ItemListView_ItemClick" ItemTemplate="{StaticResource ListItemTemplate}"> <ListView.ItemContainerStyle> <Style TargetType="FrameworkElement"> <Setter Property="Margin" Value="2,0,0,2"/> </Style> </ListView.ItemContainerStyle> </ListView>
ListView の
ItemsSource
プロパティはCollectionViewSource
にバインドされます。この CollectionViewSource は、分離コードの LoadState の DefaultViewModel プロパティに挿入されるFeedData::Items
プロパティにバインドされます (以下をご覧ください)。ListView には、ItemClick イベントが宣言されています。このイベントの上にカーソルを置き、F12 キーを押して、分離コードでイベント ハンドラーを生成します。現時点では、このイベント ハンドラーの内容は空にしておきます。
LoadState と SaveState (Windows Phone アプリの FeedPage)
MainPage では、状態の保存について考慮する必要はありませんでした。このページでは、どのような理由でアプリが起動された場合でも、インターネットを経由した完全な再初期化が必ず実行されるためです。他のページでは、ページの状態を保存しておく必要があります。たとえば、FeedPage が表示されているときにアプリが終了した (メモリからアンロードされた) 場合、ユーザーが FeedPage に戻ろうとしたとき、アプリがそのページから移動していないことが必要になります。そのため、どのフィードが選ばれていたかという情報を保持しておく必要があります。このデータの保存場所は、ローカルの AppData ストレージです。またデータを保存する適切なタイミングは、ユーザーが MainPage でフィードをクリックしたときです。
ここで、データがまだ実際に存在しているかという問題が考えられます。ユーザーによるクリックに基づいて MainPage から FeedPage に移動する場合、選ばれた FeedData オブジェクトは既に存在することが明確になっています。それは、このオブジェクトが存在しなければ、フィードは MainPage に表示されないからです。ただし、アプリが再開された場合、FeedPage が FeedData オブジェクトへのバインドを試行しているときには、最後に表示されていた FeedData オブジェクトがまだ読み込まれていない可能性があります。そのため、FeedPage (および他のページ) では、FeedData が利用可能になったタイミングを把握するための方法が必要になります。concurrency::task_completion_event は、このような状況に対応するように設計されています。これを使うことで、アプリを再開するのか、MainPage から移動してきたのかに関係なく、同じコード パスで FeedData オブジェクトを安全に取得できます。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"
後で示しますが、TextViewerPage のクラス定義は Navigate の呼び出しで必要になります。
LoadState
メソッドを次のコードに置き換えます。void FeedPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) { (void)sender; // Unused parameter if (!this->DefaultViewModel->HasKey("Feed")) { auto app = safe_cast<App^>(App::Current); app->GetCurrentFeedAsync().then([this, e](FeedData^ fd) { // Insert into the ViewModel for this page to // initialize itemsViewSource->View this->DefaultViewModel->Insert("Feed", fd); this->DefaultViewModel->Insert("Items", fd->Items); }, task_continuation_context::use_current()); } }
ページ スタックのより上位にあるページから FeedPage に戻る場合、そのページは既に初期化されています (つまり、DefaultViewModel には "Feed" の値があります)。また、現在のフィードは既に正しく設定されています。ただし、MainPage から先に進む場合やアプリを再開する場合は、正しいデータでページを設定するために、現在のフィードを取得する必要があります。GetCurrentFeedAsync は、再開後にフィード データが到着するまで、必要に応じて待機します。ここでは、use_current() context を指定して、DefaultViewModel の依存関係プロパティにアクセスする前に 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 }
イベント ハンドラー (Windows Phone アプリの FeedPage)
ここでは、FeedPage で ItemClick イベントを処理します。このイベントによって、ユーザーが投稿を読み取ることができるようになった場合に、該当するページに移動します。XAML でこのイベント名に対して F12 キーを押したので、スタブ ハンドラーは既に作成されています。
このイベントの実装を次のコードに置き換えます。
void FeedPage::ItemListView_ItemClick(Platform::Object^ sender, ItemClickEventArgs^ e) { FeedItem^ clickedItem = dynamic_cast<FeedItem^>(e->ClickedItem); this->Frame->Navigate(TextViewerPage::typeid, clickedItem->Link->AbsoluteUri); }
F5 キーを押し、Windows Phone アプリをビルドしてエミュレーターで実行します。MainPage で項目を選ぶと、アプリでは FeedPage に移動し、フィードの一覧が表示されます。次の手順では、選んだフィードのテキストを表示します。
XAML マークアップを追加する (Windows Phone アプリの TextViewerPage)
Windows Phone プロジェクトの 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 プロパティを追加します。また、前の手順で App クラスに追加した GetFeedItem 関数を使って最初にフィードを検索した後に、参照を格納するためのプライベート メンバー m_FeedItem を現在のフィード項目に追加します。
また、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 (Windows Phone アプリの TextViewerPage)
TextViewerPage.xaml.cpp に、次の #include ディレクティブを追加します。
#include "WebViewerPage.xaml.h"
次の 2 つの namespace ディレクティブを追加します。
using namespace concurrency; using namespace Windows::UI::Xaml::Documents;
NavigationHelper と DefaultViewModel のコードを追加します。
TextViewerPage::TextViewerPage() { InitializeComponent(); SetValue(_defaultViewModelProperty, ref new Platform::Collections::Map<String^, Object^>(std::less<String^>())); auto navigationHelper = ref new Common::NavigationHelper(this); SetValue(_navigationHelperProperty, navigationHelper); navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &TextViewerPage::LoadState); navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &TextViewerPage::SaveState); // this->DataContext = DefaultViewModel; } DependencyProperty^ TextViewerPage::_defaultViewModelProperty = DependencyProperty::Register("DefaultViewModel", TypeName(IObservableMap<String^, Object^>::typeid), TypeName(TextViewerPage::typeid), nullptr); /// <summary> /// Used as a trivial view model. /// </summary> IObservableMap<String^, Object^>^ TextViewerPage::DefaultViewModel::get() { return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty)); } DependencyProperty^ TextViewerPage::_navigationHelperProperty = DependencyProperty::Register("NavigationHelper", TypeName(Common::NavigationHelper::typeid), TypeName(TextViewerPage::typeid), nullptr); /// <summary> /// Gets an implementation of <see cref="NavigationHelper"/> designed to be /// used as a trivial view model. /// </summary> Common::NavigationHelper^ TextViewerPage::NavigationHelper::get() { return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty)); } #pragma region Navigation support /// The methods provided in this section are simply used to allow /// NavigationHelper to respond to the page's navigation methods. /// /// Page specific logic should be placed in event handlers for the /// <see cref="NavigationHelper::LoadState"/> /// and <see cref="NavigationHelper::SaveState"/>. /// The navigation parameter is available in the LoadState method /// in addition to page state preserved during an earlier session. void TextViewerPage::OnNavigatedTo(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedTo(e); } void TextViewerPage::OnNavigatedFrom(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedFrom(e); } #pragma endregion
LoadState
とSaveState
の実装を次のコードに置き換えます。void TextViewerPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) { (void)sender; // Unused parameter // (void)e; // Unused parameter auto app = safe_cast<App^>(App::Current); app->GetCurrentFeedAsync().then([this, app, e](FeedData^ fd) { m_feedItem = app->GetFeedItem(fd, safe_cast<String^>(e->NavigationParameter)); FeedItemTitle->Text = m_feedItem->Title; BlogTextBlock->Blocks->Clear(); TextHelper^ helper = ref new TextHelper(); auto blocks = helper-> CreateRichText(m_feedItem->Content, ref new TypedEventHandler<Hyperlink^, HyperlinkClickEventArgs^> (this, &TextViewerPage::RichTextHyperlinkClicked)); for (auto b : blocks) { BlogTextBlock->Blocks->Append(b); } }, task_continuation_context::use_current()); } void TextViewerPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e) { (void)sender; // Unused parameter e->PageState->Insert("Uri", m_feedItem->Link->AbsoluteUri); }
RichTextBlock にバインドすることができないため、その内容を TextHelper クラスを使って手動で作成します。簡潔にするために、フィードからテキストのみを抽出する HtmlUtilities::ConvertToText 関数を使います。練習として、html または xml を自分で解析し、画像形式のリンクとテキストを
Blocks
コレクションに追加してみてください。SyndicationClient には、XML フィードを解析するための関数があります。一部のフィードは整形式の XML ですが、整形式の XML ではないフィードもあります。
イベント ハンドラー (Windows Phone アプリの 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); }
Windows Phone プロジェクトをスタートアップ プロジェクトとして設定し、F5 キーを押します。フィードのページで項目をクリックし、ブログ投稿を読むことができる TextViewerPage に移動できます。これらのブログには、興味を引く投稿が掲載されています。
XAML を追加する (Windows アプリの SplitPage)
Windows アプリは、いくつかの点で Windows Phone アプリとは異なる動作をします。Windows プロジェクトの MainPage.xaml で ItemsPage テンプレートを使う方法は既に確認しました。このテンプレートは Windows Phone アプリでは使うことができません。ここでは、SplitPage を追加します。このページも Windows Phone アプリでは使うことができません。デバイスが横向きのとき、Windows アプリの SplitPage では、右のウィンドウと左のウィンドウが表示されます。このアプリでユーザーがそのページに移動すると、左のウィンドウにはフィード項目の一覧が表示され、右のウィンドウには現在選んでいるフィードのテキスト レンダリングが表示されます。デバイスが縦向きのとき、またはウィンドウが最大幅になっていないとき、SplitPage では VisualStates を使うことで、2 つの個別のページが存在するように動作します。これは、コードの "論理ページ ナビゲーション" と呼ばれます。
次のコードで開始します。これは、Windows 8 プロジェクトの既定のテンプレートであった基本分割ページの xaml です。
<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 が既に設定されています。
2 つの列にまたがるように titlePanel グリッドを調整します。これにより、フィード タイトルを画面の幅全体を対象として表示することができます。
<Grid x:Name="titlePanel" Grid.ColumnSpan="2">
同じグリッドで pageTitle の TextBlock を探し、Binding を Title から Feed.Title に変更します。
Text="{Binding Feed.Title}"
"Vertical scrolling item list" のコメントを探し、既定の 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 を返すことができます。その後で、独自のユーティリティ クラスを使って、返された文字列を段落に分割し、リッチ テキスト要素を作成します。このビューには画像は表示されませんが、ビューの読み込みは高速です。このアプリを拡張する場合、後で、ユーザーがフォントやフォント サイズを調整できるオプションを追加できます。
"Details for selected item" のコメントの下にある 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 メソッドを使っています。このページと Windows Phone ページとの相違点は、選ばれた項目を追跡することです。
SaveState
で、現在選ばれている項目を PageState オブジェクトに挿入します。これにより、SuspensionManager は必要に応じてその項目を保持するので、LoadState
が呼び出されたときに PageState オブジェクトでその項目を使うことができます。現在のフィードにある現在の FeedItem を検索する際に、その項目の文字列が必要になります。
イベント ハンドラー (Windows アプリの SplitPage)
選ばれた項目が変更されると、詳細ウィンドウでは 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 Phone プロジェクトを確認した後で説明します。
"戻る" ナビゲーションについて
Windows アプリの場合、SplitPage には "戻る" ナビゲーションのボタンが用意されています。このボタンによって MainPage に戻ることができます。追加のコーディングは必要ありません。Windows Phone の場合、戻るボタンの機能は、ソフトウェア ボタンではなくハードウェアの戻るボタンによって実現されます。Windows Phone の戻るボタンによるナビゲーションは、共通フォルダーの NavigationHelper クラスによって処理されます。関連するコードを確認するには、ソリューションで "BackPressed" (Ctrl + Shift + F) を検索してください。繰り返しになりますが、ここでは追加の作業を行う必要はありません。追加のコーディングをしなくても、"戻る" ナビゲーションはそのまま動作します。
パート 9: 選んだ投稿の Web ビューを追加する
最後に、ブログ投稿をオリジナルの Web ページの形式で表示するページを追加します。場合によっては、画像の表示を希望する読者もいます。Web ページを表示する際の欠点は、そのテキストを電話の画面で読み取るのが難しいということです。また、すべての Web ページがモバイル デバイス向けに適切に書式設定されているわけではありません。Web ページによっては、余白が画面の端よりも外側にあり、水平スクロールを何回も実行する必要があります。ここで使う WebViewerPage ページは比較的シンプルです。ここでは、ページに WebView コントロールを追加するだけで、すべての機能を実行します。最初に、Windows Phone プロジェクトについて説明します。
XAML を追加する (Windows Phone アプリの WebViewerPage)
WebViewerPage.xaml に、タイトル パネルと contentRoot グリッドを追加します。
<!-- TitlePanel --> <StackPanel Grid.Row="0" Margin="10,10,10,10"> <TextBlock Text="{StaticResource AppName}" Style="{ThemeResource TitleTextBlockStyle}" Typography.Capitals="SmallCaps"/> </StackPanel> <!--TODO: Content should be placed within the following grid--> <Grid Grid.Row="1" x:Name="ContentRoot"> <!-- Back button and page title --> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!--This will render while web page is still downloading, indicating that something is happening--> <TextBlock x:Name="pageTitle" Text="{Binding Title}" Grid.Column="1" IsHitTestVisible="false" TextWrapping="WrapWholeWords" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="40,20,40,20"/> </Grid>
LoadState と SaveState (Windows Phone アプリの 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 デベロッパー センターで確認してください。繰り返しになりますが、この Web ページを表示する場合も、2 つの方法を扱う必要があります。前にも説明したように、状態を調べることに注意してください。
以上で作業は終了です。F5 キーを押すと、TextViewerPage から WebViewerPage に移動できます。
次に、Windows プロジェクトについて説明します。Windows プロジェクトでの作業は、Windows Phone プロジェクトで行った作業と類似しています。
XAML を追加する (Windows アプリの WebViewerPage)
WebViewerPage.xaml で、SizeChanged イベントを Page 要素に追加し、pageRoot_SizeChanged という名前を指定します。このイベント上に挿入ポイントを置き、F12 キーを押して分離コードを生成します。
"Back button and page title" グリッドを探し、TextBlock を削除します。ページ タイトルは Web ページ上に表示されるので、そのための領域をここで設定する必要はありません。
戻るボタンのグリッドの直後に、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
関数を次のコードに置き換えます。このコードは、Windows Phone のページとよく似ています。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 と Windows Phone の両方で適切に機能します。ただし、ユーザーはハード コードされている 3 つのフィード以外は読むことができないという前提があります。最後の手順では、現実の利用を考慮し、ユーザーが独自に選んだフィードの追加と削除を実行できるようにします。ここでは、ユーザーがアプリを初めて起動したときに画面が空白にならないように、ユーザーに対して既定のフィードをいくつか示します。次に、フィードの追加と削除を実行するためのボタンを追加します。もちろん、セッション間で保持できるように、ユーザー フィードの一覧を保存する必要があります。この作業は、アプリのローカル データについての学習に適しています。
最初の手順では、アプリを初めて起動したときのために、既定のフィードをいくつか保存する必要があります。ただし、ハード コーディングするのではなく、ResourceLoader がフィードを検索できるように、フィードを文字列リソース ファイルに設定します。Windows アプリや Windows Phone アプリにコンパイルする場合にこれらのリソースが必要になります。そのため、共有プロジェクト内に .resw ファイルを作成します。
文字列リソースを追加する
ソリューション エクスプローラーで共有プロジェクトを選び、右クリックして、新しい項目を追加します。左側のウィンドウでリソースを選び、中央のウィンドウでリソース ファイル (.resw) を選びます (.rc ファイルは選ばないでください。これはデスクトップ アプリ用のファイルです)。既定の名前をそのまま使うか、任意の名前を指定します。[追加] をクリックします。
次の名前と値のペアを追加します。
- URL_1 http://sxp.microsoft.com/feeds/3.0/devblogs
- URL_2 https://blogs.windows.com/windows/b/bloggingwindows/rss.aspx
- URL_3 https://azure.microsoft.com/blog/feed
追加すると、リソース エディターは次のように表示されます。
フィードの追加と削除を行うための共有コードを追加する
URL を FeedDataSource クラスに読み込むためのコードを追加します。 feeddata.h で、次のプライベート メンバー関数を FeedDataSource に追加します。
concurrency::task<Windows::Foundation::Collections::IVector<Platform::String^>^> GetUserURLsAsync();
次のステートメントを FeedData.cpp に追加します。
using namespace Windows::Storage; using namespace Windows::Storage::Streams;
次の実装を追加します。
/// <summary> /// The first time the app runs, the default feed URLs are loaded from the local resources /// into a text file that is stored in the app folder. All subsequent additions and lookups /// are against that file. The method has to return a task because the file access is an /// async operation, and the call site needs to be able to continue from it with a .then method. /// </summary> task<IVector<String^>^> FeedDataSource::GetUserURLsAsync() { return create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("Feeds.txt", CreationCollisionOption::OpenIfExists)) .then([](StorageFile^ file) { return FileIO::ReadLinesAsync(file); }).then([](IVector<String^>^ t) { if (t->Size == 0) { // The data file is new, so we'll populate it with the // default URLs that are stored in the apps resources. auto loader = ref new Resources::ResourceLoader(); t->Append(loader->GetString("URL_1\n")); t->Append(loader->GetString("URL_2")); t->Append(loader->GetString("URL_3")); // Before we return the URLs, let's create the new file asynchronously // for use next time. We don't need the result of the operation now // because we already have vec, so we can just kick off the task to // run whenever it gets scheduled. create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([t](StorageFile^ file) { OutputDebugString(L"append lines async\n"); FileIO::AppendLinesAsync(file, t); }); } // Return the URLs return create_task([t]() { OutputDebugString(L"returning t\n"); return safe_cast<IVector<String^>^>(t); }); }); }
GetUserURLsAsync では、feeds.txt ファイルが存在するかどうかを確認します。存在しない場合、このファイルを作成し、文字列リソースから URL を追加します。ユーザーが追加したすべてファイルは、feeds.txt ファイルに取り込まれます。すべてのファイル書き込み操作は非同期であるため、task と .then の継続を使って、ファイル データにアクセスする前に非同期処理が必ず実行されるようにします。
以前の InitDataSource 実装を、GetUserURLsAsync を呼び出す次のコードに置き換えます。
///<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 でも Windows Phone でも同じです。そのため、これらの関数は App クラスに設定します。 App.xaml.h で次の操作を行います。
次の内部メンバーを追加します。
void AddFeed(Platform::String^ feedUri); void RemoveFeed(Platform::String^ feedUri);
App.xaml.cpp に、次の namespace ディレクティブを追加します。
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、Windows Phone アプリの場合は BottomAppBar です (Windows Phone アプリには上部のアプリ バーがありません)。 Windows プロジェクトでは、MainPage.xaml の Page.Resources ノードの直後に TopAppBar を追加します。
<Page.TopAppBar> <CommandBar x:Name="cmdBar" IsSticky="False" Padding="10,0,10,0"> <AppBarButton x:Name="addButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Icon="Add"> <Button.Flyout> <Flyout Placement="Top"> <Grid> <StackPanel> <TextBox x:Name="tbNewFeed" Width="400"/> <Button Click="AddFeed_Click">Add feed</Button> </StackPanel> </Grid> </Flyout> </Button.Flyout> </AppBarButton> <AppBarButton x:Name="removeButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Icon="Remove" Click="removeFeed_Click"/> <!--These buttons appear when the user clicks the remove button to signal that they want to remove a feed. Delete removes the feed(s) and returns to the normal visual state and cancel just returns to the normal state. --> <AppBarButton x:Name="deleteButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Visibility="Collapsed" Icon="Delete" Click="deleteButton_Click"/> <AppBarButton x:Name="cancelButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Visibility="Collapsed" Icon="Cancel" Click="cancelButton_Click"/> </CommandBar> </Page.TopAppBar>
4 つある Click イベント ハンドラー (add、remove、delete、cancel) のそれぞれで、ハンドラー名の上にカーソルを置き、F12 キーを押して分離コードで関数を生成します。
<VisualStateManager.VisualStateGroups> 要素内に、次の 2 つ目の 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 で、4 つのイベント ハンドラー スタブを次のコードに置き換えます。
/// <summary> /// Invoked when the user clicks the "add" button to add a new feed. /// Retrieves the feed data, updates the UI, adds the feed to the ListView /// and appends it to the data file. /// </summary> void MainPage::AddFeed_Click(Object^ sender, RoutedEventArgs^ e) { auto app = safe_cast<App^>(App::Current); app->AddFeed(tbNewFeed->Text); } /// <summary> /// Invoked when the user clicks the remove button. This changes the grid or list /// to multi-select so that clicking on an item adds a check mark to it without /// any navigation action. This method also makes the "delete" and "cancel" buttons /// visible so that the user can delete selected items, or cancel the operation. /// </summary> void MainPage::removeFeed_Click(Object^ sender, RoutedEventArgs^ e) { VisualStateManager::GoToState(this, "Checkboxes", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; addButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Visible; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Visible; } ///<summary> /// Invoked when the user presses the "trash can" delete button on the app bar. ///</summary> void SimpleBlogReader::MainPage::deleteButton_Click(Object^ sender, RoutedEventArgs^ e) { // Determine whether listview or gridview is active IVector<Object^>^ itemsToDelete; if (itemListView->ActualHeight > 0) { itemsToDelete = itemListView->SelectedItems; } else { itemsToDelete = itemGridView->SelectedItems; } for (auto item : itemsToDelete) { // Get the feed the user selected. Object^ proxy = safe_cast<Object^>(item); FeedData^ item = safe_cast<FeedData^>(proxy); // Remove it from the data file and app-wide feed collection auto app = safe_cast<App^>(App::Current); app->RemoveFeed(item->Title); } VisualStateManager::GoToState(this, "Normal", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible; addButton->Visibility = Windows::UI::Xaml::Visibility::Visible; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; } ///<summary> /// Invoked when the user presses the "X" cancel button on the app bar. Returns the app /// to the state where clicking on an item causes navigation to the feed. ///</summary> void MainPage::cancelButton_Click(Object^ sender, RoutedEventArgs^ e) { VisualStateManager::GoToState(this, "Normal", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible; addButton->Visibility = Windows::UI::Xaml::Visibility::Visible; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; }
スタートアップ プロジェクトとして設定した Windows プロジェクトで、F5 キーを押します。各メンバー関数によって、ボタンの 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 キーを押し、分離コードを生成します。
"Checkboxes" VisualStateGroup を追加します。VisualStateGroups ノード全体は次のようになります。
<VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="SelectionStates"> <VisualState x:Name="Normal"/> <VisualState x:Name="Checkboxes"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemListView" Storyboard.TargetProperty="SelectionMode"> <DiscreteObjectKeyFrame KeyTime="0" Value="Multiple"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemListView" Storyboard.TargetProperty="IsItemClickEnabled"> <DiscreteObjectKeyFrame KeyTime="0" Value="False"/> </ObjectAnimationUsingKeyFrames> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>
フィールドの追加と削除ボタン用のイベント ハンドラーを追加する (Windows Phone 8.1)
MainPage.xaml.cpp (WIndows Phone 8.1) で、作成したスタブ イベント ハンドラーを次のコードに置き換えます。
void MainPage::AddFeed_Click(Platform::Object^ sender, RoutedEventArgs^ e) { if (tbNewFeed->Text->Length() > 9) { auto app = static_cast<App^>(App::Current); app->AddFeed(tbNewFeed->Text); } } void MainPage::removeFeed_Click(Platform::Object^ sender, RoutedEventArgs^ e) { VisualStateManager::GoToState(this, "Checkboxes", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; addButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Visible; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Visible; } void MainPage::deleteButton_Click(Platform::Object^ sender, RoutedEventArgs^ e) { for (auto item : ItemListView->SelectedItems) { // Get the feed the user selected. Object^ proxy = safe_cast<Object^>(item); FeedData^ item = safe_cast<FeedData^>(proxy); // Remove it from the data file and app-wide feed collection auto app = safe_cast<App^>(App::Current); app->RemoveFeed(item->Title); } VisualStateManager::GoToState(this, "Normal", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible; addButton->Visibility = Windows::UI::Xaml::Visibility::Visible; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; } void MainPage::cancelButton_Click(Platform::Object^ sender, RoutedEventArgs^ e) { VisualStateManager::GoToState(this, "Normal", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible; addButton->Visibility = Windows::UI::Xaml::Visibility::Visible; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; }
F5 キーを押し、新しいボタンを使ってフィードの追加や削除を行ってみてください。Windows Phone でフィードを追加するには、Web ページの RSS リンクをクリックしてから [保存] をクリックします。次に、URL の名前が付いている編集ボックスを押し、コピー アイコンを押します。アプリに戻って、挿入ポイントを編集ボックスに設定し、コピー アイコンをもう一度押して URL を貼り付けます。フィードがフィードの一覧にすぐに表示されます。
これで、SimpleBlogReader アプリは適切に使うことができる状態になりました。このアプリを Windows デバイスに展開することができます。
アプリを自分の電話に展開するには、最初に、「Windows Phone の登録」の説明に従って電話を登録する必要があります。
ロック解除された Windows Phone に展開するには
リリース ビルドを作成します。
メイン メニューで [プロジェクト]、[ストア]、[アプリ パッケージの作成] の順にクリックします。この作業では、ストアに展開する必要はありません。変更する必要がなければ、次の画面の既定値をそのまま使います。
パッケージが正常に作成されると、Windows アプリ認定キット (WACK) を実行するよう求められます。場合によっては、ストアでの受け入れが拒否されるような欠陥がアプリに隠されていないことを確認するために、この操作を行う必要があります。ただしストアには展開しないため、この手順は省略可能です。
メイン メニューで [ツール]、[Windows Phone 8.1]、[アプリケーションの配置] の順にクリックします。アプリケーションの配置ウィザードが表示され、最初の画面では、[ターゲット] に "デバイス" と示されます。[参照] ボタンをクリックして、プロジェクト ツリーの AppPackages フォルダーに移動します。このフォルダーは、Debug フォルダーや Release フォルダーと同じレベルにあります。そのフォルダーで最新のパッケージを探します (複数ある場合)。次に、そのパッケージをダブルクリックして、パッケージに含まれている appx ファイルまたは appxbundle ファイルをクリックします。
電話がコンピューターに接続されており、ロック画面でロックされていないことを確認します。ウィザードの [配置] ボタンを押し、アプリの展開が完了するのを待機します。"展開の成功" を示すメッセージが表示されるまで、数秒かかります。電話に表示されるアプリケーションの一覧で該当のアプリを探し、そのアプリをタップして実行します。
注: 新しい URL の追加は、最初は直感的に操作できない場合があります。追加する URL を検索し、リンクをタップしてください。プロンプトで、その URL を開くとします。RSS の URL (http://feeds.bbci.co.uk/news/world/rss.xml など) をコピーします。IE でファイルを開いた後で表示される一時的な xml ファイルの名前ではありません。XML ページを IE で開いた場合は、IE の前の画面に戻って、アドレス バーから必要な URL を取得します。URL をコピーした後で、"Simple Blog Reader" に戻り、その URL を [Add Feed] (フィードの追加) テキスト ブロックに貼り付けて、[Add Feed] (フィードの追加) ボタンをクリックします。メイン ページには、完全に初期化されたフィードがすぐに表示されます。読者のための練習: SimpleBlogReader への新しい URL の追加を簡素化するために、共有コントラクトやその他の方法を実装してみてください。楽しいブログ閲覧を!
次にすること
このチュートリアルでは、Microsoft Visual Studio Express 2012 for Windows 8 の組み込みページ テンプレートを使ってマルチページ アプリを作成する方法と、ページ間を移動してデータを渡す方法について説明しました。スタイルとテンプレートを使ってアプリを Windows チーム ブログの Web サイトの雰囲気に調和させる方法についても説明しました。さらに、テーマ アニメーションとアプリ バーを使って、アプリの外観を Windows ストア アプリの雰囲気に合わせる方法についても説明しました。 最後に、アプリをさまざまなレイアウトや向きに対応させて常に最善の表示を得る方法について説明しました。
この段階で、アプリを Windows ストアに提出する準備がほぼ整いました。Windows ストアにアプリを提出する方法について詳しくは、以下をご覧ください。
- 市場でのアプリの公開
- アプリをアクセシビリティ対応にする方法。詳しくは、「アクセシビリティ」をご覧ください。
- 学習リソースやリファレンス リソース: C++ を使った Windows ランタイム アプリのためのロードマップ