블로그 뷰어 유니버설 Windows 플랫폼 앱 만들기(C++)
C++ 및 XAML을 사용하여 Windows 10에 배포할 수 있는 UWP(유니버설 Windows 플랫폼) 앱을 개발하는 방법은 다음과 같습니다. 앱은 RSS 2.0 또는 Atom 1.0 피드에서 블로그를 읽습니다.
이 자습서에서는 독자가 C++를 사용하여 첫 Windows 스토어 앱 만들기의 개념에 이미 익숙하다고 가정합니다.
이 앱의 완료된 버전을 연구해 보려면 MSDN 코드 갤러리 웹 사이트에서 다운로드할 수 있습니다.
이 자습서에서는 Visual Studio Community 2015 이상을 사용합니다. 다른 버전의 Visual Studio를 사용하는 경우 메뉴 명령이 약간 다를 수 있습니다.
다른 프로그래밍 언어의 자습서는 다음을 참조하세요.
"Hello World" 앱 만들기(C#/VB)
목표
이 자습서는 여러 페이지로 된 Windows 스토어 앱을 만드는 방법과 Windows 런타임에 대한 코딩 작업을 간소화하기 위해 Visual C++ 구성 요소 확장(C++/CX)을 사용하는 방법 및 시기에 대해 지식을 제공하기 위한 것입니다. 또한 concurrency::task
클래스를 사용하여 비동기 Windows 런타임 API를 이용하는 방법도 안내합니다.
SimpleBlogReader 앱의 기능은 다음과 같습니다.
- 인터넷을 통한 RSS 및 Atom 피드 데이터 액세스
- 피드 목록 및 피드 제목 표시
- 게시물을 웹 페이지나 간단한 텍스트로 읽을 수 있도록 두 가지 방법 제공
- PLM(프로세스 수명 관리)을 지원하며, 포그라운드의 다른 작업으로 인해 시스템에서 앱을 종료하는 경우 해당 상태를 정확히 저장하고 다시 로드
- 서로 다른 창 크기 및 장치 방향(가로 또는 세로)에 맞춰 조정
- 사용자가 피드를 추가 및 제거할 수 있도록 함
1부: 프로젝트 설정
먼저, C++ 빈 앱(유니버설 Windows) 템플릿을 사용하여 프로젝트를 만들어 보겠습니다.
새 프로젝트 만들기
- Visual Studio에서 파일 > 새로 만들기 > 프로젝트를 선택하고, 설치됨 > Visual C++ > Windows > 유니버설을 선택합니다. 가운데 창에서 빈 앱(유니버설 Windows) 템플릿을 선택합니다. 솔루션 이름을 "SimpleBlogReader"로 지정합니다. 자세한 전체 지침이 필요하면 "Hello, world" 앱 만들기(C++)를 참조하세요.
모든 페이지를 추가하는 것부터 시작하겠습니다. 코딩을 시작하면 각 페이지는 #include를 사용하여 이동할 페이지를 포함해야 하므로 이렇게 동시에 모두 수행하는 것이 더 쉽습니다.
Windows 앱 페이지 추가
- 사실은 삭제부터 시작합니다. MainPage.xaml을 마우스 오른쪽 단추로 클릭한 다음 제거를 선택하고, 삭제를 클릭하여 파일과 파일의 코드 숨김 파일을 영구적으로 삭제합니다. 이 유형은 필요한 탐색 지원이 없는 빈 페이지 유형입니다. 이제 프로젝트 노드를 마우스 오른쪽 단추로 클릭하고 추가 > 새 항목을 선택합니다.
- 왼쪽 창에서 XAML을 선택하고 가운데 창에서 항목 페이지를 선택합니다. 이름을 MainPage.xaml로 지정하고 확인을 클릭합니다. 새 파일을 프로젝트에 추가해도 괜찮은지를 묻는 메시지 상자가 표시됩니다. 예를 클릭합니다. 시작 코드에서는 Visual Studio가 새 Common 폴더에 넣는 해당 파일에 정의된 SuspensionManager 및 NavigationHelper 클래스를 참조해야 합니다.
- SplitPage를 추가하고 기본 이름을 수락합니다.
- BasicPage를 추가하고 이름을 WebViewerPage로 지정합니다.
나중에 사용자 인터페이스 요소들을 해당 페이지에 추가하겠습니다.
Phone 앱 페이지 추가
- 솔루션 탐색기에서 Windows Phone 8.1 프로젝트를 확장합니다. MainPage.xaml을 마우스 오른쪽 단추로 클릭하고 제거 > 영구 삭제를 선택합니다.
- 새 XAML 기본 페이지를 추가하고 이름을 MainPage.xaml로 지정합니다. Windows 프로젝트에 대해 했던 것처럼 예를 클릭합니다.
- 다양한 페이지 템플릿이 Phone 프로젝트에서는 더 제한일 수 있습니다. 따라서 이 앱에서는 기본 페이지만 사용합니다. 기본 페이지를 세 개 더 추가하고 이름을 FeedPage, TextViewerPage 및 WebViewerPage로 지정합니다.
2부: 데이터 모델 만들기
Visual Studio 템플릿을 기반으로 하는 스토어 앱은 MVVM 아키텍처를 대략적으로 구현합니다. 작업 중인 앱에서 이 모델은 블로그 피드를 캡슐화하는 클래스로 구성됩니다. 앱에 있는 각 XAML 페이지는 해당 데이터의 특정 보기를 나타내며, 각 페이지 클래스에는 DefaultViewModel이라는 일종의 속성인 자체 보기 모델이 있으며, 이것은 Map<String^,Object^> 유형입니다. 이 맵은 페이지의 XAML 컨트롤이 바인딩하는 데이터를 저장하고 페이지에 대한 데이터 컨텍스트의 역할을 합니다.
이 모델은 세 개의 클래스로 구성됩니다. FeedData 클래스는 블로그 피드에 대한 최상위 URI와 메타데이터를 나타냅니다. https://blogs.windows.com/windows/b/buildingapps/rss.aspx의 피드는 FeedData가 캡슐화하는 내용의 예입니다. 피드에는 FeedItem 개체로 나타내는 블로그 게시물 목록이 있습니다. 각 FeedItem은(는) 하나의 게시물을 나타내며, 제목, 콘텐츠, URI 및 기타 메타데이터가 들어 있습니다. https://blogs.windows.com/windows/b/buildingapps/archive/2014/05/28/using-the-windows-phone-emulator-for-testing-apps-with-geofencing.aspx의 게시물은 FeedItem의 예입니다. 작업 중인 앱의 첫 페이지는 피드 보기이며, 두 번째 페이지는 단일 피드에 대한 FeedItem 보기이고, 마지막 두 페이지는 단일 게시물의 다양한 보기(일반 텍스트 또는 웹 페이지 형식)를 제공합니다.
FeedDataSource 클래스에는 FeedData 항목 컬렉션이 이 항목을 다운로드하기 위한 메서드와 함께 들어 있습니다.
요약:
FeedData은(는) RSS 또는 Atom 피드에 대한 정보를 포함합니다.
FeedItem은(는) 피드의 개별 블로그 게시물에 대한 정보를 포함합니다.
FeedDataSource에는 피드를 다운로드하고 데이터 클래스를 초기화할 메서드가 포함되어 있습니다.
이러한 클래스는 데이터 바인딩을 가능하게 하는 public ref 클래스로 정의하며, XAML 컨트롤은 표준 C++ 클래스와 상호 작용할 수 없습니다. Bindable 특성을 사용하여 이러한 형식의 인스턴스에 동적으로 바인딩하는 XAML 컴파일러를 나타냅니다. 공용 참조 클래스에서 공용 데이터 멤버는 속성으로 표시됩니다. 특별한 논리가 없는 속성에는 사용자 지정 getter 및 setter가 필요하지 않습니다. 컴파일러에서 제공하기 때문입니다. FeedData 클래스에서는, Windows::Foundation::Collections::IVector가 공용 컬렉션 형식을 표시하는 데 사용되는 방식에 주목하세요. 내부적으로 Platform::Collections::Vector 클래스는 IVector을(를) 구현하는 구체적인 형식으로 사용됩니다.
Windows 및 Windows Phone 프로젝트는 모두 동일한 데이터 모델을 사용하게 되므로 클래스는 공유 프로젝트에 넣겠습니다.
사용자 지정 데이터 클래스를 만들려면
솔루션 탐색기의 SimpleBlogReader.Shared 프로젝트 노드에 대한 바로 가기 메뉴에서 추가 > 새 항목을 선택합니다. 헤더 파일(.h) 옵션을 선택하고 이름을 FeedData.h로 지정합니다.
FeedData.h을(를) 열고 나서 다음 코드를 붙여넣습니다. "pch.h"에 대한 #include 지시문을 보세요. 이것은 사전 컴파일된 헤더로서, 전혀 또는 많이 변경되지 않는 시스템 헤더를 배치하는 위치입니다. 기본적으로 pch.h에는 Platform::Collections::Vector 형식에 필요한 collection.h 및 관련 형식에 필요한 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; }; }
Windows 런타임 XAML 클래스는 사용자 인터페이스에 대한 데이터 바인딩을 위해 클래스와 상호 작용해야 하므로 클래스는 ref 클래스입니다. 해당 클래스의 [Bindable] 특성도 데이터 바인딩에 필요합니다. 바인딩 메커니즘은 해당 특성 없이는 이 클래스를 인식하지 않습니다.
3부: 데이터 다운로드
FeedDataSource 클래스에는 피드를 다운로드하는 메서드가 들어 있으며, 다른 도우미 메서드도 포함되어 있습니다. 또한 기본 앱 페이지의 DefaultViewModel에 있는 "Items" 값에 추가되는 다운로드한 피드의 컬렉션도 들어 있습니다. FeedDataSource에서는 Windows::Web::Syndication::SyndicationClient 클래스를 사용하여 다운로드를 수행합니다. 네트워크 작업은 시간이 오래 걸릴 수 있으므로, 이 작업은 비동기 작업입니다. 피드 다운로드가 완료되면 FeedData 개체가 초기화되어 FeedDataSource::Feeds 컬렉션에 추가됩니다. IObservable<T>로서, 이것은 항목이 추가될 때 UI에 대한 알림이 표시되고, 기본 페이지에 표시됨을 의미합니다. 비동기 작업의 경우에는, concurrency:: task 클래스 및 관련 클래스와 ppltasks.h의 메서드를 사용합니다. 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 이벤트가 발생하면, App 개체는 InitDataSource를 호출하여 feedDataSource 인스턴스를 통해 모든 해당 데이터에 대한 다운로드가 시작되게 합니다.
일부 클래스 정의를 더 추가해야 하므로 프로젝트는 아직 빌드되지 않습니다.
4부: 종료에서 다시 시작할 때의 데이터 동기화 처리
앱이 처음 시작될 때, 사용자가 페이지 간을 앞뒤로 이동하는 동안에는 데이터 액세스 동기화가 필요하지 않습니다. 피드는 초기화된 후 첫 번째 페이지에만 표시되며, 다른 페이지는 표시되는 피드를 사용자가 클릭하기 전까지 데이터 액세스를 시도하지 않습니다. 그리고 그 후에는 모든 액세스가 읽기 전용이므로, 원본 데이터를 수정하지 마세요. 그러나 동기화를 필요로 하는 시나리오가 하나 있습니다. 특정 피드를 기반으로 페이지가 활성 상태인 동안 앱이 종료되면, 해당 페이지는 앱이 다시 시작될 때 해당 피드 데이터에 다시 바인딩해야 할 때입니다. 이 경우 아직 존재하지 않는 데이터에 액세스하려고 하는 페이지가 있을 수 있습니다. 따라서 데이터 준비가 완료될 때까지 페이지를 강제로 대기시킬 방법이 있어야 합니다.
다음 함수는 앱이 보고 있던 피드를 기억할 수 있도록 합니다. SetCurrentFeed 메서드는 앱이 메모리를 떠난 후에도 검색할 수 있도록 이 피드를 로컬 설정에 계속 유지시킵니다. 돌아와서 마지막 피드를 다시 로드하려 할 때 해당 피드가 다시 로드되기 전에 동기화하면 안 되므로 GetCurrentFeedAsync 메서드는 흥미로운 메서드입니다. 나중에 이 코드에 대해 설명하겠습니다. App 클래스는 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;
task에 대해 concurrency 네임스페이스, Vector에 대해 Platform::Collections 네임스페이스, ApplicationData에 대해 Windows::Storage 네임스페이스가 필요합니다.
이러한 줄을 다음과 같이 맨 아래에 추가합니다.
///<summary> /// Grabs the URI that the user entered, then inserts it into the in-memory list /// and retrieves the data. Then adds the new feed to the data file so it's /// there the next time the app starts up. ///</summary> void App::AddFeed(String^ feedUri) { auto feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); auto client = ref new Windows::Web::Syndication::SyndicationClient(); // The UI is data-bound to the items collection and will update automatically // after we append to the collection. create_task(feedDataSource->RetrieveFeedAndInitData(feedUri, client)) .then([this, feedUri] { // Add the uri to the roaming data. The API requires an IIterable so we have to // put the uri in a Vector. Vector<String^>^ vec = ref new Vector<String^>(); vec->Append(feedUri); concurrency::create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([vec](StorageFile^ file) { FileIO::AppendLinesAsync(file, vec); }); }); } /// <summary> /// Called when the user chooses to remove some feeds which otherwise /// are valid Urls and currently are displaying in the UI, and are stored in /// the Feeds collection as well as in the feeds.txt file. /// </summary> void App::RemoveFeeds(Vector<FeedData^>^ feedsToDelete) { // Create a new list of feeds, excluding the ones the user selected. auto feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); // If we delete the "last viewed feed" we need to also remove the reference to it // from local settings. ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; String^ lastViewed; if (localSettings->Values->HasKey("LastViewedFeed")) { lastViewed = safe_cast<String^>(localSettings->Values->Lookup("LastViewedFeed")); } // When performance is an issue, consider using Vector::ReplaceAll for (const auto& item : feedsToDelete) { unsigned int index = -1; bool b = feedDataSource->Feeds->IndexOf(item, &index); if (index >= 0) { feedDataSource->Feeds->RemoveAt(index); } // Prevent ourself from trying later to reference // the page we just deleted. if (lastViewed != nullptr && lastViewed == item->Title) { localSettings->Values->Remove("LastViewedFeed"); } } // Re-initialize feeds.txt with the new list of URLs. Vector<String^>^ newFeedList = ref new Vector<String^>(); for (const auto& item : feedDataSource->Feeds) { newFeedList->Append(item->Uri); } // Overwrite the old data file with the new list. create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([newFeedList](StorageFile^ file) { FileIO::WriteLinesAsync(file, newFeedList); }); } ///<summary> /// This function enables the user to back out after /// entering a bad url in the "Add Feed" text box, for example pasting in a /// partial address. This function will also be called if a URL that was previously /// formatted correctly one day starts returning malformed XML when we try to load it. /// In either case, the FeedData was not added to the Feeds collection, and so /// we only need to delete the URL from the data file. /// </summary> void App::DeleteUrlFromFeedFile(Platform::String^ s) { // Overwrite the old data file with the new list. create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([this](StorageFile^ file) { return FileIO::ReadLinesAsync(file); }).then([this, s](IVector<String^>^ lines) { for (unsigned int i = 0; i < lines->Size; ++i) { if (lines->GetAt(i) == s) { lines->RemoveAt(i); } } return lines; }).then([this](IVector<String^>^ lines) { create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([this, lines](StorageFile^ file) { FileIO::WriteLinesAsync(file, lines); }); }); } ///<summary> /// Returns the feed that the user last selected from MainPage. ///<summary> task<FeedData^> App::GetCurrentFeedAsync() { FeedDataSource^ feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); return create_task(feedDataSource->m_LastViewedFeedEvent); } ///<summary> /// So that we can always get the current feed in the same way, we call this // method from ItemsPage when we change the current feed. This way the caller // doesn't care whether we're resuming from termination or new navigating. // The only other place we set the event is in InitDataSource in FeedData.cpp // when resuming from termination. ///</summary> void App::SetCurrentFeed(FeedData^ feed) { // Enable any pages waiting on the FeedData to continue FeedDataSource^ feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); feedDataSource->m_LastViewedFeedEvent = task_completion_event<FeedData^>(); feedDataSource->m_LastViewedFeedEvent.set(feed); // Store the current URI so that we can look up the correct feedData object on resume. ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; auto values = localSettings->Values; values->Insert("LastViewedFeed", dynamic_cast<PropertyValue^>(PropertyValue::CreateString(feed->Uri))); } // We stored the string ID when the app was suspended // because storing the FeedItem itself would have required // more custom serialization code. Here is where we retrieve // the FeedItem based on its string ID. FeedItem^ App::GetFeedItem(FeedData^ fd, String^ uri) { auto items = fd->Items; auto itEnd = end(items); auto it = std::find_if(begin(items), itEnd, [uri](FeedItem^ fi) { return fi->Link->AbsoluteUri == uri; }); if (it != itEnd) return *it; return nullptr; }
5부: 데이터를 사용 가능한 형태로 변환
모든 원시 데이터가 반드시 사용 가능한 형태는 아닙니다. RSS 또는 Atom 피드는 게시 날짜를 RFC 822 숫자 값으로 표현합니다. 사용자에게 적합한 텍스트로 변환할 방법이 있어야 합니다. 이를 위해, IValueConverter를 구현하고 RFC833 값을 날짜의 각 구성 요소를 위한 입력 및 출력 문자열로 허용하는 사용자 지정 클래스를 만들겠습니다. 나중에, 데이터를 표시하는 XAML에서는 원시 데이터 형식 대신 DateConverter 클래스의 출력에 바인딩하겠습니다.
날짜 변환기 추가
공유 프로젝트에서 새 .h 파일을 만들고 이 코드를 추가합니다.
//DateConverter.h #pragma once #include <string> //for wcscmp #include <regex> namespace SimpleBlogReader { namespace WGDTF = Windows::Globalization::DateTimeFormatting; /// <summary> /// Implements IValueConverter so that we can convert the numeric date /// representation to a set of strings. /// </summary> public ref class DateConverter sealed : public Windows::UI::Xaml::Data::IValueConverter { public: virtual Platform::Object^ Convert(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ language) { if (value == nullptr) { throw ref new Platform::InvalidArgumentException(); } auto dt = safe_cast<Windows::Foundation::DateTime>(value); auto param = safe_cast<Platform::String^>(parameter); Platform::String^ result; if (param == nullptr) { auto dtf = WGDTF::DateTimeFormatter::ShortDate::get(); result = dtf->Format(dt); } else if (wcscmp(param->Data(), L"month") == 0) { auto formatter = ref new WGDTF::DateTimeFormatter("{month.abbreviated(3)}"); result = formatter->Format(dt); } else if (wcscmp(param->Data(), L"day") == 0) { auto formatter = ref new WGDTF::DateTimeFormatter("{day.integer(2)}"); result = formatter->Format(dt); } else if (wcscmp(param->Data(), L"year") == 0) { auto formatter = ref new WGDTF::DateTimeFormatter("{year.full}"); auto tempResult = formatter->Format(dt); //e.g. "2014" // Insert a hard return after second digit to get the rendering // effect we want std::wregex r(L"(\\d\\d)(\\d\\d)"); result = ref new Platform::String( std::regex_replace(tempResult->Data(), r, L"$1\n$2").c_str()); } else { // We don't handle other format types currently. throw ref new Platform::InvalidArgumentException(); } return result; } virtual Platform::Object^ ConvertBack(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ language) { // Not needed in SimpleBlogReader. Left as an exercise. throw ref new Platform::NotImplementedException(); } }; }
이제 App.xaml.h에서 #include를 사용하여 포함합니다.
#include "DateConverter.h"
그리고 Application.Resources 노드에 있는 App.xaml에서 인스턴스를 만듭니다.
<local:DateConverter x:Key="dateConverter" />
피드 콘텐츠는 HTML로서, 경우에 따라서는 XML 형식의 텍스트로서 유선을 통해 전달됩니다. RichTextBlock에서 이 콘텐츠를 표시하려면 서식 있는 텍스트로 변환해야 합니다. 다음 클래스에서는 Windows HtmlUtilities 함수를 사용하여 HTML을 구문 분석한 다음, <regex> 함수를 사용하여 서식 있는 텍스트 개체를 만들 수 있도록 HTML을 단락으로 분할합니다. 이 시나리오에서는 데이터 바인딩을 이 사용할 수 없으므로 IValueConverter를 구현하는 클래스는 필요하지 않습니다. 필요한 페이지에서 로컬 인스턴스만 만들겠습니다.
텍스트 변환기 추가
공유 프로젝트에서 새 .h 파일을 추가하고 이름을 TextHelper.h로 지정한 다음 이 코드를 추가합니다.
#pragma once namespace SimpleBlogReader { namespace WFC = Windows::Foundation::Collections; namespace WF = Windows::Foundation; namespace WUIXD = Windows::UI::Xaml::Documents; public ref class TextHelper sealed { public: TextHelper(); WFC::IVector<WUIXD::Paragraph^>^ CreateRichText( Platform::String^ fi, WF::TypedEventHandler < WUIXD::Hyperlink^, WUIXD::HyperlinkClickEventArgs^ > ^ context); Platform::String^ UnescapeText(Platform::String^ inStr); private: std::vector<std::wstring> SplitContentIntoParagraphs(const std::wstring& s, const std::wstring& rgx); std::wstring UnescapeText(const std::wstring& input); // Maps some HTML entities that we'll use to replace the escape sequences // in the call to UnescapeText when we create feed titles and render text. std::map<std::wstring, std::wstring> entities; }; }
이제 TextHelper.cpp를 추가합니다.
#include "pch.h" #include "TextHelper.h" using namespace std; using namespace SimpleBlogReader; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::Data::Html; using namespace Windows::UI::Xaml::Documents; /// <summary> /// Note that in this example we don't map all the possible HTML entities. Feel free to improve this. /// Also note that we initialize the map like this because VS2013 Udpate 3 does not support list /// initializers in a member declaration. /// </summary> TextHelper::TextHelper() : entities( { { L"<", L"<" }, { L">", L">" }, { L"&", L"&" }, { L"¢", L"¢" }, { L"£", L"£" }, { L"¥", L"¥" }, { L"€", L"€" }, { L"€", L"©" }, { L"®", L"®" }, { L"“", L"“" }, { L"”", L"”" }, { L"‘", L"‘" }, { L"’", L"’" }, { L"»", L"»" }, { L"«", L"«" }, { L"‹", L"‹" }, { L"›", L"›" }, { L"•", L"•" }, { L"°", L"°" }, { L"…", L"…" }, { L" ", L" " }, { L""", LR"(")" }, { L"'", L"'" }, { L"<", L"<" }, { L">", L">" }, { L"’", L"’" }, { L" ", L" " }, { L"&", L"&" } }) { } ///<summary> /// Accepts the Content property from a Feed and returns rich text /// paragraphs that can be passed to a RichTextBlock. ///</summary> String^ TextHelper::UnescapeText(String^ inStr) { wstring input(inStr->Data()); wstring result = UnescapeText(input); return ref new Platform::String(result.c_str()); } ///<summary> /// Create a RichText block from the text retrieved by the HtmlUtilies object. /// For a more full-featured app, you could parse the content argument yourself and /// add the page's images to the inlines collection. ///</summary> IVector<Paragraph^>^ TextHelper::CreateRichText(String^ content, TypedEventHandler<Hyperlink^, HyperlinkClickEventArgs^>^ context) { std::vector<Paragraph^> blocks; auto text = HtmlUtilities::ConvertToText(content); auto parts = SplitContentIntoParagraphs(wstring(text->Data()), LR"(\r\n)"); // Add the link at the top. Don't set the NavigateUri property because // that causes the link to open in IE even if the Click event is handled. auto hlink = ref new Hyperlink(); hlink->Click += context; auto linkText = ref new Run(); linkText->Foreground = ref new Windows::UI::Xaml::Media::SolidColorBrush(Windows::UI::Colors::DarkRed); linkText->Text = "Link"; hlink->Inlines->Append(linkText); auto linkPara = ref new Paragraph(); linkPara->Inlines->Append(hlink); blocks.push_back(linkPara); for (auto part : parts) { auto p = ref new Paragraph(); p->TextIndent = 10; p->Margin = (10, 10, 10, 10); auto r = ref new Run(); r->Text = ref new String(part.c_str()); p->Inlines->Append(r); blocks.push_back(p); } return ref new Vector<Paragraph^>(blocks); } ///<summary> /// Split an input string which has been created by HtmlUtilities::ConvertToText /// into paragraphs. The rgx string we use here is LR("\r\n") . If we ever use /// other means to grab the raw text from a feed, then the rgx will have to recognize /// other possible new line formats. ///</summary> vector<wstring> TextHelper::SplitContentIntoParagraphs(const wstring& s, const wstring& rgx) { const wregex r(rgx); vector<wstring> result; // the -1 argument indicates that the text after this match until the next match // is the "capture group". In other words, this is how we match on what is between the tokens. for (wsregex_token_iterator rit(s.begin(), s.end(), r, -1), end; rit != end; ++rit) { if (rit->length() > 0) { result.push_back(*rit); } } return result; } ///<summary> /// This is used to unescape html entities that occur in titles, subtitles, etc. // entities is a map<wstring, wstring> with key-values like this: { L"<", L"<" }, /// CAUTION: we must not unescape any content that gets sent to the webView. ///</summary> wstring TextHelper::UnescapeText(const wstring& input) { wsmatch match; // match L"<" as well as " " const wregex rgx(LR"(&#?\w*?;)"); wstring result; // itrEnd needs to be visible outside the loop wsregex_iterator itrEnd, itrRemainingText; // Iterate over input and build up result as we go along // by first appending what comes before the match, then the // unescaped replacement for the HTML entity which is the match, // then once at the end appending what comes after the last match. for (wsregex_iterator itr(input.cbegin(), input.cend(), rgx); itr != itrEnd; ++itr) { wstring entity = itr->str(); map<wstring, wstring>::const_iterator mit = entities.find(entity); if (mit != end(entities)) { result.append(itr->prefix()); result.append(mit->second); // mit->second is the replacement text itrRemainingText = itr; } else { // we found an entity that we don't explitly map yet so just // render it in raw form. Exercise for the user: add // all legal entities to the entities map. result.append(entity); continue; } } // If we didn't find any entities to escape // then (a) don't try to dereference itrRemainingText // and (b) return input because result is empty! if (itrRemainingText == itrEnd) { return input; } else { // Add any text between the last match and end of input string. result.append(itrRemainingText->suffix()); return result; } }
여기에 사용된 사용자 지정 TextHelper 클래스는 C++/CX 앱 내에서 내부적으로 ISO C++(std:: map, std::regex, std::wstring)를 사용할 수 있는 방법을 몇 가지 보여 줍니다. 이 클래스의 인스턴스는 클래스를 사용하는 페이지에서 로컬로 만들게 됩니다. App.xaml.h에서는 한번 포함해야 합니다.
#include "TextHelper.h"
이제 앱을 빌드하고 실행할 수 있게 되었습니다. 매우 많은 작업을 수행할 것으로 기대하지는 마세요.
6부: 앱 시작, 일시 중단 및 다시 시작
App::OnLaunched
이벤트는, 사용자가 앱 타일을 누르거나 클릭하여 앱을 시작할 때와, 시스템이 다른 앱에 사용할 메모리를 확보하기 위해 앱을 종료한 후 사용자가 다시 앱으로 이동한 후 발생합니다. 두 경우 모두 항상 인터넷으로 가서 이 이벤트에 응답하여 데이터를 다시 로드하게 됩니다. 하지만, 경우에 따라 호출해야 하는 다른 작업이 있습니다. 함수로 전달된 후 적절한 작업을 수행하는 LaunchActivatedEventArgs 인수와 결합하여 rootFrame을 보면 이러한 상태를 추론할 수 있습니다. 다행히 MainPage를 사용하여 자동으로 추가된 SuspensionManager 클래스는 앱이 일시 중단되었다가 다시 시작될 때 앱 상태를 저장 및 복원하기 위한 대부분의 작업을 수행합니다. 해당 메서드를 호출만 하면 됩니다.
SuspensionManager 코드 파일을 Common 폴더의 프로젝트에 추가합니다. SuspensionManager.h를 추가하고 여기에 다음 코드를 복사합니다.
// // SuspensionManager.h // Declaration of the SuspensionManager class // #pragma once namespace SimpleBlogReader { namespace Common { /// <summary> /// SuspensionManager captures global session state to simplify process lifetime management /// for an application. Note that session state will be automatically cleared under a variety /// of conditions and should only be used to store information that would be convenient to /// carry across sessions, but that should be disacarded when an application crashes or is /// upgraded. /// </summary> class SuspensionManager sealed { public: static void RegisterFrame(Windows::UI::Xaml::Controls::Frame^ frame, Platform::String^ sessionStateKey, Platform::String^ sessionBaseKey = nullptr); static void UnregisterFrame(Windows::UI::Xaml::Controls::Frame^ frame); static concurrency::task<void> SaveAsync(); static concurrency::task<void> RestoreAsync(Platform::String^ sessionBaseKey = nullptr); static Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ SessionState(); static Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ SessionStateForFrame( Windows::UI::Xaml::Controls::Frame^ frame); private: static void RestoreFrameNavigationState(Windows::UI::Xaml::Controls::Frame^ frame); static void SaveFrameNavigationState(Windows::UI::Xaml::Controls::Frame^ frame); static Platform::Collections::Map<Platform::String^, Platform::Object^>^ _sessionState; static const wchar_t* sessionStateFilename; static std::vector<Platform::WeakReference> _registeredFrames; static Windows::UI::Xaml::DependencyProperty^ FrameSessionStateKeyProperty; static Windows::UI::Xaml::DependencyProperty^ FrameSessionBaseKeyProperty; static Windows::UI::Xaml::DependencyProperty^ FrameSessionStateProperty; }; } }
SuspensionManager.cpp 코드 파일을 추가하고 여기에 다음 코드를 복사합니다.
// // SuspensionManager.cpp // Implementation of the SuspensionManager class // #include "pch.h" #include "SuspensionManager.h" #include <algorithm> using namespace SimpleBlogReader::Common; using namespace concurrency; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::Storage; using namespace Windows::Storage::FileProperties; using namespace Windows::Storage::Streams; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Interop; Map<String^, Object^>^ SuspensionManager::_sessionState = ref new Map<String^, Object^>(); const wchar_t* SuspensionManager::sessionStateFilename = L"_sessionState.dat"; std::vector<WeakReference> SuspensionManager::_registeredFrames; DependencyProperty^ SuspensionManager::FrameSessionStateKeyProperty = DependencyProperty::RegisterAttached("_FrameSessionStateKeyProperty", TypeName(String::typeid), TypeName(SuspensionManager::typeid), nullptr); DependencyProperty^ SuspensionManager::FrameSessionBaseKeyProperty = DependencyProperty::RegisterAttached("_FrameSessionBaseKeyProperty", TypeName(String::typeid), TypeName(SuspensionManager::typeid), nullptr); DependencyProperty^ SuspensionManager::FrameSessionStateProperty = DependencyProperty::RegisterAttached("_FrameSessionStateProperty", TypeName(IMap<String^, Object^>::typeid), TypeName(SuspensionManager::typeid), nullptr); class ObjectSerializeHelper { public: // Codes used for identifying serialized types enum StreamTypes { NullPtrType = 0, // Supported IPropertyValue types UInt8Type, UInt16Type, UInt32Type, UInt64Type, Int16Type, Int32Type, Int64Type, SingleType, DoubleType, BooleanType, Char16Type, GuidType, StringType, // Additional supported types StringToObjectMapType, // Marker values used to ensure stream integrity MapEndMarker }; static String^ ReadString(DataReader^ reader); static IMap<String^, Object^>^ ReadStringToObjectMap(DataReader^ reader); static Object^ ReadObject(DataReader^ reader); static void WriteString(DataWriter^ writer, String^ string); static void WriteProperty(DataWriter^ writer, IPropertyValue^ propertyValue); static void WriteStringToObjectMap(DataWriter^ writer, IMap<String^, Object^>^ map); static void WriteObject(DataWriter^ writer, Object^ object); }; /// <summary> /// Provides access to global session state for the current session. This state is serialized by /// <see cref="SaveAsync"/> and restored by <see cref="RestoreAsync"/> which require values to be /// one of the following: boxed values including integers, floating-point singles and doubles, /// wide characters, boolean, Strings and Guids, or Map<String^, Object^> where map values are /// subject to the same constraints. Session state should be as compact as possible. /// </summary> IMap<String^, Object^>^ SuspensionManager::SessionState() { return _sessionState; } /// <summary> /// Registers a <see cref="Frame"/> instance to allow its navigation history to be saved to /// and restored from <see cref="SessionState"/>. Frames should be registered once /// immediately after creation if they will participate in session state management. Upon /// registration if state has already been restored for the specified key /// the navigation history will immediately be restored. Subsequent invocations of /// <see cref="RestoreAsync(String)"/> will also restore navigation history. /// </summary> /// <param name="frame">An instance whose navigation history should be managed by /// <see cref="SuspensionManager"/></param> /// <param name="sessionStateKey">A unique key into <see cref="SessionState"/> used to /// store navigation-related information.</param> /// <param name="sessionBaseKey">An optional key that identifies the type of session. /// This can be used to distinguish between multiple application launch scenarios.</param> void SuspensionManager::RegisterFrame(Frame^ frame, String^ sessionStateKey, String^ sessionBaseKey) { if (frame->GetValue(FrameSessionStateKeyProperty) != nullptr) { throw ref new FailureException("Frames can only be registered to one session state key"); } if (frame->GetValue(FrameSessionStateProperty) != nullptr) { throw ref new FailureException("Frames must be either be registered before accessing frame session state, or not registered at all"); } if (sessionBaseKey != nullptr) { frame->SetValue(FrameSessionBaseKeyProperty, sessionBaseKey); sessionStateKey = sessionBaseKey + "_" + sessionStateKey; } // Use a dependency property to associate the session key with a frame, and keep a list of frames whose // navigation state should be managed frame->SetValue(FrameSessionStateKeyProperty, sessionStateKey); _registeredFrames.insert(_registeredFrames.begin(), WeakReference(frame)); // Check to see if navigation state can be restored RestoreFrameNavigationState(frame); } /// <summary> /// Disassociates a <see cref="Frame"/> previously registered by <see cref="RegisterFrame"/> /// from <see cref="SessionState"/>. Any navigation state previously captured will be /// removed. /// </summary> /// <param name="frame">An instance whose navigation history should no longer be /// managed.</param> void SuspensionManager::UnregisterFrame(Frame^ frame) { // Remove session state and remove the frame from the list of frames whose navigation // state will be saved (along with any weak references that are no longer reachable) auto key = safe_cast<String^>(frame->GetValue(FrameSessionStateKeyProperty)); if (SessionState()->HasKey(key)) { SessionState()->Remove(key); } _registeredFrames.erase( std::remove_if(_registeredFrames.begin(), _registeredFrames.end(), [=](WeakReference& e) { auto testFrame = e.Resolve<Frame>(); return testFrame == nullptr || testFrame == frame; }), _registeredFrames.end() ); } /// <summary> /// Provides storage for session state associated with the specified <see cref="Frame"/>. /// Frames that have been previously registered with <see cref="RegisterFrame"/> have /// their session state saved and restored automatically as a part of the global /// <see cref="SessionState"/>. Frames that are not registered have transient state /// that can still be useful when restoring pages that have been discarded from the /// navigation cache. /// </summary> /// <remarks>Apps may choose to rely on <see cref="NavigationHelper"/> to manage /// page-specific state instead of working with frame session state directly.</remarks> /// <param name="frame">The instance for which session state is desired.</param> /// <returns>A collection of state subject to the same serialization mechanism as /// <see cref="SessionState"/>.</returns> IMap<String^, Object^>^ SuspensionManager::SessionStateForFrame(Frame^ frame) { auto frameState = safe_cast<IMap<String^, Object^>^>(frame->GetValue(FrameSessionStateProperty)); if (frameState == nullptr) { auto frameSessionKey = safe_cast<String^>(frame->GetValue(FrameSessionStateKeyProperty)); if (frameSessionKey != nullptr) { // Registered frames reflect the corresponding session state if (!_sessionState->HasKey(frameSessionKey)) { _sessionState->Insert(frameSessionKey, ref new Map<String^, Object^>()); } frameState = safe_cast<IMap<String^, Object^>^>(_sessionState->Lookup(frameSessionKey)); } else { // Frames that aren't registered have transient state frameState = ref new Map<String^, Object^>(); } frame->SetValue(FrameSessionStateProperty, frameState); } return frameState; } void SuspensionManager::RestoreFrameNavigationState(Frame^ frame) { auto frameState = SessionStateForFrame(frame); if (frameState->HasKey("Navigation")) { frame->SetNavigationState(safe_cast<String^>(frameState->Lookup("Navigation"))); } } void SuspensionManager::SaveFrameNavigationState(Frame^ frame) { auto frameState = SessionStateForFrame(frame); frameState->Insert("Navigation", frame->GetNavigationState()); } /// <summary> /// Save the current <see cref="SessionState"/>. Any <see cref="Frame"/> instances /// registered with <see cref="RegisterFrame"/> will also preserve their current /// navigation stack, which in turn gives their active <see cref="Page"/> an opportunity /// to save its state. /// </summary> /// <returns>An asynchronous task that reflects when session state has been saved.</returns> task<void> SuspensionManager::SaveAsync(void) { // Save the navigation state for all registered frames for (auto && weakFrame : _registeredFrames) { auto frame = weakFrame.Resolve<Frame>(); if (frame != nullptr) SaveFrameNavigationState(frame); } // Serialize the session state synchronously to avoid asynchronous access to shared // state auto sessionData = ref new InMemoryRandomAccessStream(); auto sessionDataWriter = ref new DataWriter(sessionData->GetOutputStreamAt(0)); ObjectSerializeHelper::WriteObject(sessionDataWriter, _sessionState); // Once session state has been captured synchronously, begin the asynchronous process // of writing the result to disk return task<unsigned int>(sessionDataWriter->StoreAsync()).then([=](unsigned int) { return ApplicationData::Current->LocalFolder->CreateFileAsync(StringReference(sessionStateFilename), CreationCollisionOption::ReplaceExisting); }) .then([=](StorageFile^ createdFile) { return createdFile->OpenAsync(FileAccessMode::ReadWrite); }) .then([=](IRandomAccessStream^ newStream) { return RandomAccessStream::CopyAsync( sessionData->GetInputStreamAt(0), newStream->GetOutputStreamAt(0)); }) .then([=](UINT64 copiedBytes) { (void) copiedBytes; // Unused parameter return; }); } /// <summary> /// Restores previously saved <see cref="SessionState"/>. Any <see cref="Frame"/> instances /// registered with <see cref="RegisterFrame"/> will also restore their prior navigation /// state, which in turn gives their active <see cref="Page"/> an opportunity restore its /// state. /// </summary> /// <param name="sessionBaseKey">An optional key that identifies the type of session. /// This can be used to distinguish between multiple application launch scenarios.</param> /// <returns>An asynchronous task that reflects when session state has been read. The /// content of <see cref="SessionState"/> should not be relied upon until this task /// completes.</returns> task<void> SuspensionManager::RestoreAsync(String^ sessionBaseKey) { _sessionState->Clear(); task<StorageFile^> getFileTask(ApplicationData::Current->LocalFolder->GetFileAsync(StringReference(sessionStateFilename))); return getFileTask.then([=](StorageFile^ stateFile) { task<BasicProperties^> getBasicPropertiesTask(stateFile->GetBasicPropertiesAsync()); return getBasicPropertiesTask.then([=](BasicProperties^ stateFileProperties) { auto size = unsigned int(stateFileProperties->Size); if (size != stateFileProperties->Size) throw ref new FailureException("Session state larger than 4GB"); task<IRandomAccessStreamWithContentType^> openReadTask(stateFile->OpenReadAsync()); return openReadTask.then([=](IRandomAccessStreamWithContentType^ stateFileStream) { auto stateReader = ref new DataReader(stateFileStream); return task<unsigned int>(stateReader->LoadAsync(size)).then([=](unsigned int bytesRead) { (void) bytesRead; // Unused parameter // Deserialize the Session State Object^ content = ObjectSerializeHelper::ReadObject(stateReader); _sessionState = (Map<String^, Object^>^)content; // Restore any registered frames to their saved state for (auto && weakFrame : _registeredFrames) { auto frame = weakFrame.Resolve<Frame>(); if (frame != nullptr && safe_cast<String^>(frame->GetValue(FrameSessionBaseKeyProperty)) == sessionBaseKey) { frame->ClearValue(FrameSessionStateProperty); RestoreFrameNavigationState(frame); } } }, task_continuation_context::use_current()); }); }); }); } #pragma region Object serialization for a known set of types void ObjectSerializeHelper::WriteString(DataWriter^ writer, String^ string) { writer->WriteByte(StringType); writer->WriteUInt32(writer->MeasureString(string)); writer->WriteString(string); } void ObjectSerializeHelper::WriteProperty(DataWriter^ writer, IPropertyValue^ propertyValue) { switch (propertyValue->Type) { case PropertyType::UInt8: writer->WriteByte(StreamTypes::UInt8Type); writer->WriteByte(propertyValue->GetUInt8()); return; case PropertyType::UInt16: writer->WriteByte(StreamTypes::UInt16Type); writer->WriteUInt16(propertyValue->GetUInt16()); return; case PropertyType::UInt32: writer->WriteByte(StreamTypes::UInt32Type); writer->WriteUInt32(propertyValue->GetUInt32()); return; case PropertyType::UInt64: writer->WriteByte(StreamTypes::UInt64Type); writer->WriteUInt64(propertyValue->GetUInt64()); return; case PropertyType::Int16: writer->WriteByte(StreamTypes::Int16Type); writer->WriteUInt16(propertyValue->GetInt16()); return; case PropertyType::Int32: writer->WriteByte(StreamTypes::Int32Type); writer->WriteUInt32(propertyValue->GetInt32()); return; case PropertyType::Int64: writer->WriteByte(StreamTypes::Int64Type); writer->WriteUInt64(propertyValue->GetInt64()); return; case PropertyType::Single: writer->WriteByte(StreamTypes::SingleType); writer->WriteSingle(propertyValue->GetSingle()); return; case PropertyType::Double: writer->WriteByte(StreamTypes::DoubleType); writer->WriteDouble(propertyValue->GetDouble()); return; case PropertyType::Boolean: writer->WriteByte(StreamTypes::BooleanType); writer->WriteBoolean(propertyValue->GetBoolean()); return; case PropertyType::Char16: writer->WriteByte(StreamTypes::Char16Type); writer->WriteUInt16(propertyValue->GetChar16()); return; case PropertyType::Guid: writer->WriteByte(StreamTypes::GuidType); writer->WriteGuid(propertyValue->GetGuid()); return; case PropertyType::String: WriteString(writer, propertyValue->GetString()); return; default: throw ref new InvalidArgumentException("Unsupported property type"); } } void ObjectSerializeHelper::WriteStringToObjectMap(DataWriter^ writer, IMap<String^, Object^>^ map) { writer->WriteByte(StringToObjectMapType); writer->WriteUInt32(map->Size); for (auto && pair : map) { WriteObject(writer, pair->Key); WriteObject(writer, pair->Value); } writer->WriteByte(MapEndMarker); } void ObjectSerializeHelper::WriteObject(DataWriter^ writer, Object^ object) { if (object == nullptr) { writer->WriteByte(NullPtrType); return; } auto propertyObject = dynamic_cast<IPropertyValue^>(object); if (propertyObject != nullptr) { WriteProperty(writer, propertyObject); return; } auto mapObject = dynamic_cast<IMap<String^, Object^>^>(object); if (mapObject != nullptr) { WriteStringToObjectMap(writer, mapObject); return; } throw ref new InvalidArgumentException("Unsupported data type"); } String^ ObjectSerializeHelper::ReadString(DataReader^ reader) { int length = reader->ReadUInt32(); String^ string = reader->ReadString(length); return string; } IMap<String^, Object^>^ ObjectSerializeHelper::ReadStringToObjectMap(DataReader^ reader) { auto map = ref new Map<String^, Object^>(); auto size = reader->ReadUInt32(); for (unsigned int index = 0; index < size; index++) { auto key = safe_cast<String^>(ReadObject(reader)); auto value = ReadObject(reader); map->Insert(key, value); } if (reader->ReadByte() != StreamTypes::MapEndMarker) { throw ref new InvalidArgumentException("Invalid stream"); } return map; } Object^ ObjectSerializeHelper::ReadObject(DataReader^ reader) { auto type = reader->ReadByte(); switch (type) { case StreamTypes::NullPtrType: return nullptr; case StreamTypes::UInt8Type: return reader->ReadByte(); case StreamTypes::UInt16Type: return reader->ReadUInt16(); case StreamTypes::UInt32Type: return reader->ReadUInt32(); case StreamTypes::UInt64Type: return reader->ReadUInt64(); case StreamTypes::Int16Type: return reader->ReadInt16(); case StreamTypes::Int32Type: return reader->ReadInt32(); case StreamTypes::Int64Type: return reader->ReadInt64(); case StreamTypes::SingleType: return reader->ReadSingle(); case StreamTypes::DoubleType: return reader->ReadDouble(); case StreamTypes::BooleanType: return reader->ReadBoolean(); case StreamTypes::Char16Type: return (char16_t) reader->ReadUInt16(); case StreamTypes::GuidType: return reader->ReadGuid(); case StreamTypes::StringType: return ReadString(reader); case StreamTypes::StringToObjectMapType: return ReadStringToObjectMap(reader); default: throw ref new InvalidArgumentException("Unsupported property type"); } } #pragma endregion
app.xaml.cpp에서 이 include 지시문을 추가합니다.
#include "Common\SuspensionManager.h"
네임스페이스 지시문을 추가합니다.
using namespace SimpleBlogReader::Common;
이제 기존 함수를 다음 코드로 바꿉니다.
void App::OnLaunched(LaunchActivatedEventArgs^ e) { #if _DEBUG if (IsDebuggerPresent()) { DebugSettings->EnableFrameRateCounter = true; } #endif auto rootFrame = dynamic_cast<Frame^>(Window::Current->Content); // Do not repeat app initialization when the Window already has content, // just ensure that the window is active. if (rootFrame == nullptr) { // Create a Frame to act as the navigation context and associate it with // a SuspensionManager key rootFrame = ref new Frame(); SuspensionManager::RegisterFrame(rootFrame, "AppFrame"); // Initialize the Atom and RSS feed objects with data from the web FeedDataSource^ feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); if (feedDataSource->Feeds->Size == 0) { if (e->PreviousExecutionState == ApplicationExecutionState::Terminated) { // On resume FeedDataSource needs to know whether the app was on a // specific FeedData, which will be the unless it was on MainPage // when it was terminated. ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; auto values = localSettings->Values; if (localSettings->Values->HasKey("LastViewedFeed")) { feedDataSource->CurrentFeedUri = safe_cast<String^>(localSettings->Values->Lookup("LastViewedFeed")); } } feedDataSource->InitDataSource(); } // We have 4 pages in the app rootFrame->CacheSize = 4; auto prerequisite = task<void>([](){}); if (e->PreviousExecutionState == ApplicationExecutionState::Terminated) { // Now restore the pages if we are resuming prerequisite = Common::SuspensionManager::RestoreAsync(); } // if we're starting fresh, prerequisite will execute immediately. // if resuming from termination, prerequisite will wait until RestoreAsync() completes. prerequisite.then([=]() { if (rootFrame->Content == nullptr) { if (!rootFrame->Navigate(MainPage::typeid, e->Arguments)) { throw ref new FailureException("Failed to create initial page"); } } // Place the frame in the current Window Window::Current->Content = rootFrame; Window::Current->Activate(); }, task_continuation_context::use_current()); } // There is a frame, but is has no content, so navigate to main page // and activate the window. else if (rootFrame->Content == nullptr) { #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP // Removes the turnstile navigation for startup. if (rootFrame->ContentTransitions != nullptr) { _transitions = ref new TransitionCollection(); for (auto transition : rootFrame->ContentTransitions) { _transitions->Append(transition); } } rootFrame->ContentTransitions = nullptr; _firstNavigatedToken = rootFrame->Navigated += ref new NavigatedEventHandler(this, &App::RootFrame_FirstNavigated); #endif // When the navigation stack isn't restored navigate to the first page, // configuring the new page by passing required information as a navigation // parameter. if (!rootFrame->Navigate(MainPage::typeid, e->Arguments)) { throw ref new FailureException("Failed to create initial page"); } // Ensure the current window is active in this code path. // we also called this inside the task for the other path. Window::Current->Activate(); } }
App 클래스는 공유 프로젝트이므로 여기에서 작업하는 코드는 WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP 매크로가 정의되는 경우를 제외하고 Windows 앱과 Phone 앱 모두에서 실행됩니다.
OnSuspending 처리기는 더 간단합니다. 이 처리기는 사용자가 종료할 때가 아니라 시스템에서 앱을 종료할 때 호출됩니다. 여기에서는 SuspensionManager가 이 작업을 수행하도록 합니다. 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를 보여 주는 Items Page(항목 페이지)입니다. Phone 프로젝트에는 Items Page(항목 페이지)가 없으므로 여기에서는 수동으로 ListView을(를) 추가하는 기본 페이지를 사용합니다. 목록 보기는 장치 방향이 변경되면 자동으로 자체 조정됩니다.
이 페이지와 모든 페이지에는 다음과 같이 일반적으로 수행할 동일한 기본 작업이 있습니다.
- UI를 설명하고 데이터에 바인딩하는 XAML 태그 추가
- 사용자 지정 코드를
LoadState
및SaveState
멤버 함수에 추가 - 이벤트 처리. 이 이벤트 중 하나 이상에는 보통 다음 페이지로 이동하는 코드가 있습니다.
먼저 Windows 프로젝트에서 이 작업을 차례차례 수행하겠습니다.
XAML 태그 추가(MainPage)
기본 페이지에서 GridView 컨트롤에 있는 각 FeedData 개체를 렌더링합니다. 데이터가 어떤 모양이어야 하는지를 설명하려면, 각 항목을 렌더링하는 데 사용될 XAML 트리인 DataTemplate을(를) 만듭니다. 레이아웃, 글꼴, 색상 등의 측면에서 DataTemplates의 가능성은 개발자의 상상력과 스타일 감각에 따라 달라집니다. 이 페이지에서는 렌더링될 때 다음과 같이 표시되는 간단한 템플릿을 사용하겠습니다.
XAML 스타일은 Microsoft Word에서의 스타일과 유사하며, "TargetType" XAML 요소에서 속성 값 집합을 그룹화하는 편리한 방법입니다. 스타일은 다른 스타일을 기반으로 만들어질 수 있습니다. "X:key" 특성은 스타일을 참조하는 데 사용하는 이름을 지정합니다.
이 템플릿과 이 템플릿의 지원 스타일을 MainPage.xaml의 Page.Resources 노드에 배치합니다(Windows 8.1). 이것들은 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
요소를 삭제합니다.Page.Resources 노드에 CollectionViewSource을(를) 추가합니다. 이 개체는 ListView를 데이터 모델에 연결합니다.
<!-- Collection of items displayed by this page --> <CollectionViewSource x:Name="itemsViewSource" Source="{Binding Items}"/>
페이지 요소에 MainPage 클래스에 대해 DefaultViewModel 속성으로 설정된 DataContext 특성이 이미 있습니다. FeedDataSource가 되도록 해당 속성을 설정하므로 CollectionViewSource가 찾은 항목 컬렉션에 대한 것으로 보입니다.
App.xaml에서는 앱에 있는 여러 페이지에서 참조하게 될 일부 추가 리소스와 함께, 앱 이름에 대한 전역 리소스 문자열을 추가하겠습니다. 여기에 리소스를 배치하면 각 페이지에서 별도로 정의할 필요가 없습니다. 다음 요소들을 App.xaml에 있는 리소스 노드에 추가합니다.
<x:String x:Key="AppName">Simple Blog Reader</x:String> <SolidColorBrush x:Key="WindowsBlogBackgroundBrush" Color="#FF0A2562"/> <SolidColorBrush x:Key="GreenBlockBackgroundBrush" Color="#FF6BBD46"/> <Style x:Key="WindowsBlogLayoutRootStyle" TargetType="Panel"> <Setter Property="Background" Value="{StaticResource WindowsBlogBackgroundBrush}"/> </Style> <!-- Green square in all ListViews that displays the date --> <ControlTemplate x:Key="DateBlockTemplate"> <Viewbox Stretch="Fill"> <Canvas Height="86" Width="86" Margin="4,0,4,4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <TextBlock TextTrimming="WordEllipsis" Padding="0,0,0,0" TextWrapping="NoWrap" Width="Auto" Height="Auto" FontSize="32" FontWeight="Bold"> <TextBlock.Text> <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="month"/> </TextBlock.Text> </TextBlock> <TextBlock TextTrimming="WordEllipsis" TextWrapping="Wrap" Width="Auto" Height="Auto" FontSize="32" FontWeight="Bold" Canvas.Top="36"> <TextBlock.Text> <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="day"/> </TextBlock.Text> </TextBlock> <Line Stroke="White" StrokeThickness="2" X1="50" Y1="46" X2="50" Y2="80" /> <TextBlock TextWrapping="Wrap" Height="Auto" FontSize="18" FontWeight="Bold" FontStretch="Condensed" LineHeight="18" LineStackingStrategy="BaselineToBaseline" Canvas.Top="38" Canvas.Left="56"> <TextBlock.Text> <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="year" /> </TextBlock.Text> </TextBlock> </Canvas> </Viewbox> </ControlTemplate> <!-- Describes the layout for items in all ListViews --> <DataTemplate x:Name="ListItemTemplate"> <Grid Margin="5,0,0,0"> <Grid.ColumnDefinitions> <ColumnDefinition Width="72"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition MaxHeight="54"></RowDefinition> </Grid.RowDefinitions> <!-- Green date block --> <Border Background="{StaticResource GreenBlockBackgroundBrush}" VerticalAlignment="Top"> <ContentControl Template="{StaticResource DateBlockTemplate}" /> </Border> <TextBlock Grid.Column="1" Text="{Binding Title}" Margin="10,0,0,0" FontSize="20" TextWrapping="Wrap" MaxHeight="72" Foreground="#FFFE5815" /> </Grid> </DataTemplate>
MainPage에 피드 목록이 표시됩니다. 장치가 가로 방향이면 가로 스크롤을 지원하는 GridView를 사용하겠습니다. 가로 방향에서는 세로 스크롤을 지원하는 ListView를 사용하겠습니다. 사용자가 두 방향에서 앱을 사용할 수 있게 하고 싶습니다. 방향 변경에 대한 지원을 구현하는 것은 비교적 쉽습니다.
- 두 컨트롤을 모두 페이지에 추가하고 ItemSource를 동일한 collectionViewSource로 설정합니다. ListView의 Visibility 속성을 기본적으로 표시되지 않도록 Collapsed로 설정합니다.
- VisualState 개체 두 개로 된 집합을 만듭니다. 하나는 가로 방향에 대한 UI 동작을 설명하고, 다른 하나는 세로 방향에 대한 동작을 설명합니다.
- 방향이 변경되거나 사용자가 창 너비를 좁히거나 넓힐 때 발생되는 Window::SizeChanged 이벤트를 처리합니다. 새 크기의 너비와 높이를 검사합니다. 높이가 너비보다 크다면, 세로 방향에 대한 VisualState를 호출합니다. 그렇지 않으면, 가로에 대한 상태를 호출합니다.
GridView 및 ListView 추가
MainPage.xaml에서 GridView, ListView 및 뒤로 단추와 페이지 제목이 들어 있는 그리드를 추가합니다.
<Grid Style="{StaticResource WindowsBlogLayoutRootStyle}"> <Grid.ChildrenTransitions> <TransitionCollection> <EntranceThemeTransition/> </TransitionCollection> </Grid.ChildrenTransitions> <Grid.RowDefinitions> <RowDefinition Height="140"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- Horizontal scrolling grid --> <GridView x:Name="ItemGridView" AutomationProperties.AutomationId="ItemsGridView" AutomationProperties.Name="Items" TabIndex="1" Grid.RowSpan="2" Padding="116,136,116,46" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" SelectionMode="None" ItemTemplate="{StaticResource DefaultGridItemTemplate}" IsItemClickEnabled="true" IsSwipeEnabled="false" ItemClick="ItemGridView_ItemClick" Margin="0,-10,0,10"> </GridView> <!-- Vertical scrolling list --> <ListView x:Name="ItemListView" Visibility="Collapsed" AutomationProperties.AutomationId="ItemsListView" AutomationProperties.Name="Items" TabIndex="1" Grid.Row="1" Margin="-10,-10,0,0" IsItemClickEnabled="True" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" IsSwipeEnabled="False" ItemClick="ItemGridView_ItemClick" ItemTemplate="{StaticResource ListItemTemplate}"> <ListView.ItemContainerStyle> <Style TargetType="FrameworkElement"> <Setter Property="Margin" Value="2,0,0,2"/> </Style> </ListView.ItemContainerStyle> </ListView> <!-- Back button and page title --> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="120"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Button x:Name="backButton" Margin="39,59,39,0" Command="{Binding NavigationHelper.GoBackCommand, ElementName=pageRoot}" Style="{StaticResource NavigationBackButtonNormalStyle}" VerticalAlignment="Top" AutomationProperties.Name="Back" AutomationProperties.AutomationId="BackButton" AutomationProperties.ItemType="Navigation Button"/> <TextBlock x:Name="pageTitle" Text="{StaticResource AppName}" Style="{StaticResource HeaderTextBlockStyle}" Grid.Column="1" IsHitTestVisible="false" TextWrapping="NoWrap" VerticalAlignment="Bottom" Margin="0,0,30,40"/> </Grid>
두 컨트롤에서 ItemClick 이벤트용의 동일한 멤버 함수를 사용합니다. 이 중 하나에 삽입 지점을 배치하고 f12 키를 눌러 이벤트 처리기 스텁을 자동으로 생성합니다. 나중에 이를 위한 코드를 추가하겠습니다.
루트 그리드 안에 마지막 요소가 있도록 VisualStateGroups 정의에 붙여넣습니다. (그리드 외부에 넣지 마세요. 그렇게 하면 작동하지 않습니다.) 두 가지 상태가 있지만 하나만 명시적으로 정의됩니다. 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 페이지에서 주의해야 하는 두 가지 기본 멤버 함수는 LoadState
및 (경우에 따라) SaveState
입니다. LoadState
에서는 페이지용 데이터를 채우고, SaveState
에서는 일시 중단했다가 다시 시작되는 경우에 대비해 페이지를 다시 채우는 데 필요한 모든 데이터를 저장합니다.
LoadState
구현을, 시작 시 만든 feedDataSource에서 로드한(또는 아직 로드 중인) 피드 데이터를 삽입하고, 데이터를 이 페이지에 대한 해당 ViewModel에 넣는 다음 코드로 바꿉니다.void MainPage::LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e) { auto feedDataSource = safe_cast<FeedDataSource^> (App::Current->Resources->Lookup("feedDataSource")); this->DefaultViewModel->Insert("Items", feedDataSource->Feeds); }
이 페이지에 대해서는 기억할 것이 없으므로 MainPage에 대해
SaveState
을(를) 호출할 필요가 없습니다. 이것은 항상 모든 피드를 표시합니다.
이벤트 처리기(Windows 앱 MainPage)
모든 페이지는 개념상 프레임 안에 상주합니다. 이 프레임은 페이지 간에 앞뒤로 이동하는 데 사용되는 프레임입니다. 탐색 함수 호출에서 두 번째 매개 변수는 데이터를 한 페이지에서 다른 페이지로 전달하는 데 사용됩니다. 여기서 전달하는 모든 개체는 앱이 다시 시작될 때 값을 복원할 수 있도록 앱이 일시 중단될 때마다 SuspensionManager에서 자동으로 저장되고 직렬화됩니다. 기본 SuspensionManager는 기본 제공 형식, 문자열 및 Guid만 지원합니다. 보다 복잡한 일련화가 필요한 경우 사용자 지정 SuspensionManager를 만들 수 있습니다. 여기서는 SplitPage가 현재 피드를 조회하는 데 사용하게 될 문자열을 전달합니다.
항목 클릭 시 이동
사용자가 그리드에 있는 항목을 클릭하면 이벤트 처리기는 클릭된 항목을 가져오고, 앱이 나중에 어느 시점에서 일시 중단되는 경우에 대비해 "현재 피드"로서 설정한 다음, 다음 페이지로 이동합니다. 그 다음, 해당 페이지가 해당 피드에 대한 데이터를 조회할 수 있도록 피드의 제목을 다음 페이지로 전달합니다. 다음은 붙여넣을 코드입니다.
void MainPage::ItemGridView_ItemClick(Object^ sender, ItemClickEventArgs^ e) { // We must manually cast from Object^ to FeedData^. auto feedData = safe_cast<FeedData^>(e->ClickedItem); // Store the feed and tell other pages it's loaded and ready to go. auto app = safe_cast<App^>(App::Current); app->SetCurrentFeed(feedData); // Only navigate if there are items in the feed if (feedData->Items->Size > 0) { // Navigate to SplitPage and pass the title of the selected feed. // SplitPage will receive this in its LoadState method in the // navigationParamter. this->Frame->Navigate(SplitPage::typeid, feedData->Title); } }
이전 코드를 컴파일하려면 현재 파일 MainPage.xaml.cpp(Windows 8.1)의 맨 위에서 #include를 사용하여 SplitPage.xaml.h를 포함해야 합니다.
#include "SplitPage.xaml.h"
Page_SizeChanged 이벤트 처리
MainPage.xaml에서
x:Name="pageRoot"
를 루트 페이지 요소의 특성에 추가하여 루트 요소에 이름을 추가한 다음SizeChanged="pageRoot_SizeChanged"
특성을 추가하여 이벤트 처리기를 만듭니다. cpp 파일에 있는 처리기를 다음 코드로 바꿉니다.void MainPage::pageRoot_SizeChanged(Platform::Object^ sender, SizeChangedEventArgs^ e) { if (e->NewSize.Height / e->NewSize.Width >= 1) { VisualStateManager::GoToState(this, "Portrait", false); } else { VisualStateManager::GoToState(this, "DefaultLayout", false); } }
그런 다음 이 함수의 선언을 MainPage.xaml.h의 MainPage 클래스에 추가합니다.
private: void pageRoot_SizeChanged(Platform::Object^ sender, SizeChangedEventArgs^ e);
코드는 간단합니다. 이제 시뮬레이터에서 앱을 실행하고 장치를 회전하면, GridView와 ListView 간 UI 변화를 보게 됩니다.
XAML 추가(Phone 앱 MainPage)
이제 작동하는 Phone 앱 기본 페이지를 만들어 보겠습니다. 여기서는 공유 프로젝트에 넣었던 모든 코드를 사용할 것이므로 코드량이 훨씬 줄어들 것입니다. 또한 GridView가 제대로 작동하기엔 화면이 너무 작으므로 Phone 앱에서는 GridView 컨트롤을 지원하지 않습니다. 따라서 자동으로 가로 방향으로 조정되고 VisualState 변경은 하지 않아도 되는 ListView를 사용하겠습니다. DataContext 특성을 페이지 요소에 추가하는 것으로 시작하겠습니다. 이 속성은 ItemsPage나 SplitPage에서처럼 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; }; } }
Common 폴더에서 RelayCommand.cpp를 추가하고 여기에 이 코드를 복사합니다.
// // RelayCommand.cpp // Implementation of the RelayCommand and associated classes // #include "pch.h" #include "RelayCommand.h" #include "NavigationHelper.h" using namespace SimpleBlogReader::Common; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::System; using namespace Windows::UI::Core; using namespace Windows::UI::ViewManagement; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Navigation; /// <summary> /// Determines whether this <see cref="RelayCommand"/> can execute in its current state. /// </summary> /// <param name="parameter"> /// Data used by the command. If the command does not require data to be passed, this object can be set to null. /// </param> /// <returns>true if this command can be executed; otherwise, false.</returns> bool RelayCommand::CanExecute(Object^ parameter) { return (_canExecuteCallback) (parameter); } /// <summary> /// Executes the <see cref="RelayCommand"/> on the current command target. /// </summary> /// <param name="parameter"> /// Data used by the command. If the command does not require data to be passed, this object can be set to null. /// </param> void RelayCommand::Execute(Object^ parameter) { (_executeCallback) (parameter); } /// <summary> /// Method used to raise the <see cref="CanExecuteChanged"/> event /// to indicate that the return value of the <see cref="CanExecute"/> /// method has changed. /// </summary> void RelayCommand::RaiseCanExecuteChanged() { CanExecuteChanged(this, nullptr); } /// <summary> /// RelayCommand Class Destructor. /// </summary> RelayCommand::~RelayCommand() { _canExecuteCallback = nullptr; _executeCallback = nullptr; }; /// <summary> /// Creates a new command that can always execute. /// </summary> /// <param name="canExecuteCallback">The execution status logic.</param> /// <param name="executeCallback">The execution logic.</param> RelayCommand::RelayCommand(std::function<bool(Platform::Object^)> canExecuteCallback, std::function<void(Platform::Object^)> executeCallback) : _canExecuteCallback(canExecuteCallback), _executeCallback(executeCallback) { }
Common 폴더에서 NavigationHelper.h 파일을 추가하고 여기에 이 코드를 복사합니다.
// // NavigationHelper.h // Declaration of the NavigationHelper and associated classes // #pragma once #include "RelayCommand.h" namespace SimpleBlogReader { namespace Common { /// <summary> /// Class used to hold the event data required when a page attempts to load state. /// </summary> public ref class LoadStateEventArgs sealed { public: /// <summary> /// The parameter value passed to <see cref="Frame->Navigate(Type, Object)"/> /// when this page was initially requested. /// </summary> property Platform::Object^ NavigationParameter { Platform::Object^ get(); } /// <summary> /// A dictionary of state preserved by this page during an earlier /// session. This will be null the first time a page is visited. /// </summary> property Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ PageState { Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ get(); } internal: LoadStateEventArgs(Platform::Object^ navigationParameter, Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ pageState); private: Platform::Object^ _navigationParameter; Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ _pageState; }; /// <summary> /// Represents the method that will handle the <see cref="NavigationHelper->LoadState"/>event /// </summary> public delegate void LoadStateEventHandler(Platform::Object^ sender, LoadStateEventArgs^ e); /// <summary> /// Class used to hold the event data required when a page attempts to save state. /// </summary> public ref class SaveStateEventArgs sealed { public: /// <summary> /// An empty dictionary to be populated with serializable state. /// </summary> property Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ PageState { Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ get(); } internal: SaveStateEventArgs(Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ pageState); private: Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ _pageState; }; /// <summary> /// Represents the method that will handle the <see cref="NavigationHelper->SaveState"/>event /// </summary> public delegate void SaveStateEventHandler(Platform::Object^ sender, SaveStateEventArgs^ e); /// <summary> /// NavigationHelper aids in navigation between pages. It provides commands used to /// navigate back and forward as well as registers for standard mouse and keyboard /// shortcuts used to go back and forward in Windows and the hardware back button in /// Windows Phone. In addition it integrates SuspensionManger to handle process lifetime /// management and state management when navigating between pages. /// </summary> /// <example> /// To make use of NavigationHelper, follow these two steps or /// start with a BasicPage or any other Page item template other than BlankPage. /// /// 1) Create an instance of the NavigationHelper somewhere such as in the /// constructor for the page and register a callback for the LoadState and /// SaveState events. /// <code> /// MyPage::MyPage() /// { /// InitializeComponent(); /// auto navigationHelper = ref new Common::NavigationHelper(this); /// navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &MyPage::LoadState); /// navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &MyPage::SaveState); /// } /// /// void MyPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) /// { } /// void MyPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e) /// { } /// </code> /// /// 2) Register the page to call into the NavigationHelper whenever the page participates /// in navigation by overriding the <see cref="Windows::UI::Xaml::Controls::Page::OnNavigatedTo"/> /// and <see cref="Windows::UI::Xaml::Controls::Page::OnNavigatedFrom"/> events. /// <code> /// void MyPage::OnNavigatedTo(NavigationEventArgs^ e) /// { /// NavigationHelper->OnNavigatedTo(e); /// } /// /// void MyPage::OnNavigatedFrom(NavigationEventArgs^ e) /// { /// NavigationHelper->OnNavigatedFrom(e); /// } /// </code> /// </example> [Windows::Foundation::Metadata::WebHostHidden] [Windows::UI::Xaml::Data::Bindable] public ref class NavigationHelper sealed { public: /// <summary> /// <see cref="RelayCommand"/> used to bind to the back Button's Command property /// for navigating to the most recent item in back navigation history, if a Frame /// manages its own navigation history. /// /// The <see cref="RelayCommand"/> is set up to use the virtual method <see cref="GoBack"/> /// as the Execute Action and <see cref="CanGoBack"/> for CanExecute. /// </summary> property RelayCommand^ GoBackCommand { RelayCommand^ get(); } /// <summary> /// <see cref="RelayCommand"/> used for navigating to the most recent item in /// the forward navigation history, if a Frame manages its own navigation history. /// /// The <see cref="RelayCommand"/> is set up to use the virtual method <see cref="GoForward"/> /// as the Execute Action and <see cref="CanGoForward"/> for CanExecute. /// </summary> property RelayCommand^ GoForwardCommand { RelayCommand^ get(); } internal: NavigationHelper(Windows::UI::Xaml::Controls::Page^ page, RelayCommand^ goBack = nullptr, RelayCommand^ goForward = nullptr); bool CanGoBack(); void GoBack(); bool CanGoForward(); void GoForward(); void OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e); void OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e); event LoadStateEventHandler^ LoadState; event SaveStateEventHandler^ SaveState; private: Platform::WeakReference _page; RelayCommand^ _goBackCommand; RelayCommand^ _goForwardCommand; #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP Windows::Foundation::EventRegistrationToken _backPressedEventToken; void HardwareButton_BackPressed(Platform::Object^ sender, Windows::Phone::UI::Input::BackPressedEventArgs^ e); #else bool _navigationShortcutsRegistered; Windows::Foundation::EventRegistrationToken _acceleratorKeyEventToken; Windows::Foundation::EventRegistrationToken _pointerPressedEventToken; void CoreDispatcher_AcceleratorKeyActivated(Windows::UI::Core::CoreDispatcher^ sender, Windows::UI::Core::AcceleratorKeyEventArgs^ e); void CoreWindow_PointerPressed(Windows::UI::Core::CoreWindow^ sender, Windows::UI::Core::PointerEventArgs^ e); #endif Platform::String^ _pageKey; Windows::Foundation::EventRegistrationToken _loadedEventToken; Windows::Foundation::EventRegistrationToken _unloadedEventToken; void OnLoaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); void OnUnloaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); ~NavigationHelper(); }; } }
이제 구현 파일인 NavigationHelper.cpp를 다음 코드를 사용하여 동일한 폴더에 추가합니다.
// // NavigationHelper.cpp // Implementation of the NavigationHelper and associated classes // #include "pch.h" #include "NavigationHelper.h" #include "RelayCommand.h" #include "SuspensionManager.h" using namespace SimpleBlogReader::Common; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::System; using namespace Windows::UI::Core; using namespace Windows::UI::ViewManagement; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Interop; using namespace Windows::UI::Xaml::Navigation; #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP using namespace Windows::Phone::UI::Input; #endif /// <summary> /// Initializes a new instance of the <see cref="LoadStateEventArgs"/> class. /// </summary> /// <param name="navigationParameter"> /// The parameter value passed to <see cref="Frame->Navigate(Type, Object)"/> /// when this page was initially requested. /// </param> /// <param name="pageState"> /// A dictionary of state preserved by this page during an earlier /// session. This will be null the first time a page is visited. /// </param> LoadStateEventArgs::LoadStateEventArgs(Object^ navigationParameter, IMap<String^, Object^>^ pageState) { _navigationParameter = navigationParameter; _pageState = pageState; } /// <summary> /// Gets the <see cref="NavigationParameter"/> property of <see cref"LoadStateEventArgs"/> class. /// </summary> Object^ LoadStateEventArgs::NavigationParameter::get() { return _navigationParameter; } /// <summary> /// Gets the <see cref="PageState"/> property of <see cref"LoadStateEventArgs"/> class. /// </summary> IMap<String^, Object^>^ LoadStateEventArgs::PageState::get() { return _pageState; } /// <summary> /// Initializes a new instance of the <see cref="SaveStateEventArgs"/> class. /// </summary> /// <param name="pageState">An empty dictionary to be populated with serializable state.</param> SaveStateEventArgs::SaveStateEventArgs(IMap<String^, Object^>^ pageState) { _pageState = pageState; } /// <summary> /// Gets the <see cref="PageState"/> property of <see cref"SaveStateEventArgs"/> class. /// </summary> IMap<String^, Object^>^ SaveStateEventArgs::PageState::get() { return _pageState; } /// <summary> /// Initializes a new instance of the <see cref="NavigationHelper"/> class. /// </summary> /// <param name="page">A reference to the current page used for navigation. /// This reference allows for frame manipulation and to ensure that keyboard /// navigation requests only occur when the page is occupying the entire window.</param> NavigationHelper::NavigationHelper(Page^ page, RelayCommand^ goBack, RelayCommand^ goForward) : _page(page), _goBackCommand(goBack), _goForwardCommand(goForward) { // When this page is part of the visual tree make two changes: // 1) Map application view state to visual state for the page // 2) Handle hardware navigation requests _loadedEventToken = page->Loaded += ref new RoutedEventHandler(this, &NavigationHelper::OnLoaded); //// Undo the same changes when the page is no longer visible _unloadedEventToken = page->Unloaded += ref new RoutedEventHandler(this, &NavigationHelper::OnUnloaded); } NavigationHelper::~NavigationHelper() { delete _goBackCommand; delete _goForwardCommand; _page = nullptr; } /// <summary> /// Invoked when the page is part of the visual tree /// </summary> /// <param name="sender">Instance that triggered the event.</param> /// <param name="e">Event data describing the conditions that led to the event.</param> void NavigationHelper::OnLoaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP _backPressedEventToken = HardwareButtons::BackPressed += ref new EventHandler<BackPressedEventArgs^>(this, &NavigationHelper::HardwareButton_BackPressed); #else Page ^page = _page.Resolve<Page>(); // Keyboard and mouse navigation only apply when occupying the entire window if (page != nullptr && page->ActualHeight == Window::Current->Bounds.Height && page->ActualWidth == Window::Current->Bounds.Width) { // Listen to the window directly so focus isn't required _acceleratorKeyEventToken = Window::Current->CoreWindow->Dispatcher->AcceleratorKeyActivated += ref new TypedEventHandler<CoreDispatcher^, AcceleratorKeyEventArgs^>(this, &NavigationHelper::CoreDispatcher_AcceleratorKeyActivated); _pointerPressedEventToken = Window::Current->CoreWindow->PointerPressed += ref new TypedEventHandler<CoreWindow^, PointerEventArgs^>(this, &NavigationHelper::CoreWindow_PointerPressed); _navigationShortcutsRegistered = true; } #endif } /// <summary> /// Invoked when the page is removed from visual tree /// </summary> /// <param name="sender">Instance that triggered the event.</param> /// <param name="e">Event data describing the conditions that led to the event.</param> void NavigationHelper::OnUnloaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP HardwareButtons::BackPressed -= _backPressedEventToken; #else if (_navigationShortcutsRegistered) { Window::Current->CoreWindow->Dispatcher->AcceleratorKeyActivated -= _acceleratorKeyEventToken; Window::Current->CoreWindow->PointerPressed -= _pointerPressedEventToken; _navigationShortcutsRegistered = false; } #endif // Remove handler and release the reference to page Page ^page = _page.Resolve<Page>(); if (page != nullptr) { page->Loaded -= _loadedEventToken; page->Unloaded -= _unloadedEventToken; delete _goBackCommand; delete _goForwardCommand; _goForwardCommand = nullptr; _goBackCommand = nullptr; } } #pragma region Navigation support /// <summary> /// Method used by the <see cref="GoBackCommand"/> property /// to determine if the <see cref="Frame"/> can go back. /// </summary> /// <returns> /// true if the <see cref="Frame"/> has at least one entry /// in the back navigation history. /// </returns> bool NavigationHelper::CanGoBack() { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frame = page->Frame; return (frame != nullptr && frame->CanGoBack); } return false; } /// <summary> /// Method used by the <see cref="GoBackCommand"/> property /// to invoke the <see cref="Windows::UI::Xaml::Controls::Frame::GoBack"/> method. /// </summary> void NavigationHelper::GoBack() { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frame = page->Frame; if (frame != nullptr && frame->CanGoBack) { frame->GoBack(); } } } /// <summary> /// Method used by the <see cref="GoForwardCommand"/> property /// to determine if the <see cref="Frame"/> can go forward. /// </summary> /// <returns> /// true if the <see cref="Frame"/> has at least one entry /// in the forward navigation history. /// </returns> bool NavigationHelper::CanGoForward() { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frame = page->Frame; return (frame != nullptr && frame->CanGoForward); } return false; } /// <summary> /// Method used by the <see cref="GoForwardCommand"/> property /// to invoke the <see cref="Windows::UI::Xaml::Controls::Frame::GoBack"/> method. /// </summary> void NavigationHelper::GoForward() { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frame = page->Frame; if (frame != nullptr && frame->CanGoForward) { frame->GoForward(); } } } /// <summary> /// Gets the <see cref="GoBackCommand"/> property of <see cref"NavigationHelper"/> class. /// </summary> RelayCommand^ NavigationHelper::GoBackCommand::get() { if (_goBackCommand == nullptr) { _goBackCommand = ref new RelayCommand( [this](Object^) -> bool { return CanGoBack(); }, [this](Object^) -> void { GoBack(); } ); } return _goBackCommand; } /// <summary> /// Gets the <see cref="GoForwardCommand"/> property of <see cref"NavigationHelper"/> class. /// </summary> RelayCommand^ NavigationHelper::GoForwardCommand::get() { if (_goForwardCommand == nullptr) { _goForwardCommand = ref new RelayCommand( [this](Object^) -> bool { return CanGoForward(); }, [this](Object^) -> void { GoForward(); } ); } return _goForwardCommand; } #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP /// <summary> /// Handles the back button press and navigates through the history of the root frame. /// </summary> void NavigationHelper::HardwareButton_BackPressed(Object^ sender, BackPressedEventArgs^ e) { if (this->GoBackCommand->CanExecute(nullptr)) { e->Handled = true; this->GoBackCommand->Execute(nullptr); } } #else /// <summary> /// Invoked on every keystroke, including system keys such as Alt key combinations, when /// this page is active and occupies the entire window. Used to detect keyboard navigation /// between pages even when the page itself doesn't have focus. /// </summary> /// <param name="sender">Instance that triggered the event.</param> /// <param name="e">Event data describing the conditions that led to the event.</param> void NavigationHelper::CoreDispatcher_AcceleratorKeyActivated(CoreDispatcher^ sender, AcceleratorKeyEventArgs^ e) { sender; // Unused parameter auto virtualKey = e->VirtualKey; // Only investigate further when Left, Right, or the dedicated Previous or Next keys // are pressed if ((e->EventType == CoreAcceleratorKeyEventType::SystemKeyDown || e->EventType == CoreAcceleratorKeyEventType::KeyDown) && (virtualKey == VirtualKey::Left || virtualKey == VirtualKey::Right || virtualKey == VirtualKey::GoBack || virtualKey == VirtualKey::GoForward)) { auto coreWindow = Window::Current->CoreWindow; auto downState = Windows::UI::Core::CoreVirtualKeyStates::Down; bool menuKey = (coreWindow->GetKeyState(VirtualKey::Menu) & downState) == downState; bool controlKey = (coreWindow->GetKeyState(VirtualKey::Control) & downState) == downState; bool shiftKey = (coreWindow->GetKeyState(VirtualKey::Shift) & downState) == downState; bool noModifiers = !menuKey && !controlKey && !shiftKey; bool onlyAlt = menuKey && !controlKey && !shiftKey; if ((virtualKey == VirtualKey::GoBack && noModifiers) || (virtualKey == VirtualKey::Left && onlyAlt)) { // When the previous key or Alt+Left are pressed navigate back e->Handled = true; GoBackCommand->Execute(this); } else if ((virtualKey == VirtualKey::GoForward && noModifiers) || (virtualKey == VirtualKey::Right && onlyAlt)) { // When the next key or Alt+Right are pressed navigate forward e->Handled = true; GoForwardCommand->Execute(this); } } } /// <summary> /// Invoked on every mouse click, touch screen tap, or equivalent interaction when this /// page is active and occupies the entire window. Used to detect browser-style next and /// previous mouse button clicks to navigate between pages. /// </summary> /// <param name="sender">Instance that triggered the event.</param> /// <param name="e">Event data describing the conditions that led to the event.</param> void NavigationHelper::CoreWindow_PointerPressed(CoreWindow^ sender, PointerEventArgs^ e) { auto properties = e->CurrentPoint->Properties; // Ignore button chords with the left, right, and middle buttons if (properties->IsLeftButtonPressed || properties->IsRightButtonPressed || properties->IsMiddleButtonPressed) { return; } // If back or foward are pressed (but not both) navigate appropriately bool backPressed = properties->IsXButton1Pressed; bool forwardPressed = properties->IsXButton2Pressed; if (backPressed ^ forwardPressed) { e->Handled = true; if (backPressed) { if (GoBackCommand->CanExecute(this)) { GoBackCommand->Execute(this); } } else { if (GoForwardCommand->CanExecute(this)) { GoForwardCommand->Execute(this); } } } } #endif #pragma endregion #pragma region Process lifetime management /// <summary> /// Invoked when this page is about to be displayed in a Frame. /// </summary> /// <param name="e">Event data that describes how this page was reached. The Parameter /// property provides the group to be displayed.</param> void NavigationHelper::OnNavigatedTo(NavigationEventArgs^ e) { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frameState = SuspensionManager::SessionStateForFrame(page->Frame); _pageKey = "Page-" + page->Frame->BackStackDepth; if (e->NavigationMode == NavigationMode::New) { // Clear existing state for forward navigation when adding a new page to the // navigation stack auto nextPageKey = _pageKey; int nextPageIndex = page->Frame->BackStackDepth; while (frameState->HasKey(nextPageKey)) { frameState->Remove(nextPageKey); nextPageIndex++; nextPageKey = "Page-" + nextPageIndex; } // Pass the navigation parameter to the new page LoadState(this, ref new LoadStateEventArgs(e->Parameter, nullptr)); } else { // Pass the navigation parameter and preserved page state to the page, using // the same strategy for loading suspended state and recreating pages discarded // from cache LoadState(this, ref new LoadStateEventArgs(e->Parameter, safe_cast<IMap<String^, Object^>^>(frameState->Lookup(_pageKey)))); } } } /// <summary> /// Invoked when this page will no longer be displayed in a Frame. /// </summary> /// <param name="e">Event data that describes how this page was reached. The Parameter /// property provides the group to be displayed.</param> void NavigationHelper::OnNavigatedFrom(NavigationEventArgs^ e) { Page ^page = _page.Resolve<Page>(); if (page != nullptr) { auto frameState = SuspensionManager::SessionStateForFrame(page->Frame); auto pageState = ref new Map<String^, Object^>(); SaveState(this, ref new SaveStateEventArgs(pageState)); frameState->Insert(_pageKey, pageState); } } #pragma endregion
이제 NavigationHelper를 포함시키기 위한 코드를 MainPage.xaml.h 헤더 파일에 추가하고 나중에 필요한 DefaultViewModel 속성도 추가합니다.
// // MainPage.xaml.h // Declaration of the MainPage class // #pragma once #include "MainPage.g.h" #include "Common\NavigationHelper.h" namespace SimpleBlogReader { namespace WFC = Windows::Foundation::Collections; namespace WUIX = Windows::UI::Xaml; namespace WUIXNav = Windows::UI::Xaml::Navigation; namespace WUIXControls = Windows::UI::Xaml::Controls; /// <summary> /// A basic page that provides characteristics common to most applications. /// </summary> [Windows::Foundation::Metadata::WebHostHidden] public ref class MainPage sealed { public: MainPage(); /// <summary> /// Gets the view model for this <see cref="Page"/>. /// This can be changed to a strongly typed view model. /// </summary> property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel { WFC::IObservableMap<Platform::String^, Platform::Object^>^ get(); } /// <summary> /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>. /// </summary> property Common::NavigationHelper^ NavigationHelper { Common::NavigationHelper^ get(); } protected: virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override; virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override; private: void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e); void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e); static WUIX::DependencyProperty^ _defaultViewModelProperty; static WUIX::DependencyProperty^ _navigationHelperProperty; }; }
MainPage.xaml.cpp에서 NavigationHelper의 구현 및 상태 로드와 저장을 위한 스텁, DefaultViewModel 속성을 추가합니다. 또한 필요한 using 네임스페이스 지시문도 추가하므로 최종 코드는 다음과 같습니다.
// // MainPage.xaml.cpp // Implementation of the MainPage class // #include "pch.h" #include "MainPage.xaml.h" using namespace SimpleBlogReader; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Controls::Primitives; using namespace Windows::UI::Xaml::Data; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Media; using namespace Windows::UI::Xaml::Navigation; using namespace Windows::UI::Xaml::Interop; // The Basic Page item template is documented at https://go.microsoft.com/fwlink/?LinkID=390556 MainPage::MainPage() { InitializeComponent(); SetValue(_defaultViewModelProperty, ref new Platform::Collections::Map<String^, Object^>(std::less<String^>())); auto navigationHelper = ref new Common::NavigationHelper(this); SetValue(_navigationHelperProperty, navigationHelper); navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &MainPage::LoadState); navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &MainPage::SaveState); } DependencyProperty^ MainPage::_defaultViewModelProperty = DependencyProperty::Register("DefaultViewModel", TypeName(IObservableMap<String^, Object^>::typeid), TypeName(MainPage::typeid), nullptr); /// <summary> /// Used as a trivial view model. /// </summary> IObservableMap<String^, Object^>^ MainPage::DefaultViewModel::get() { return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty)); } DependencyProperty^ MainPage::_navigationHelperProperty = DependencyProperty::Register("NavigationHelper", TypeName(Common::NavigationHelper::typeid), TypeName(MainPage::typeid), nullptr); /// <summary> /// Gets an implementation of <see cref="NavigationHelper"/> designed to be /// used as a trivial view model. /// </summary> Common::NavigationHelper^ MainPage::NavigationHelper::get() { return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty)); } #pragma region Navigation support /// The methods provided in this section are simply used to allow /// NavigationHelper to respond to the page's navigation methods. /// /// Page specific logic should be placed in event handlers for the /// <see cref="NavigationHelper::LoadState"/> /// and <see cref="NavigationHelper::SaveState"/>. /// The navigation parameter is available in the LoadState method /// in addition to page state preserved during an earlier session. void MainPage::OnNavigatedTo(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedTo(e); } void MainPage::OnNavigatedFrom(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedFrom(e); } #pragma endregion /// <summary> /// Populates the page with content passed during navigation. Any saved state is also /// provided when recreating a page from a prior session. /// </summary> /// <param name="sender"> /// The source of the event; typically <see cref="NavigationHelper"/> /// </param> /// <param name="e">Event data that provides both the navigation parameter passed to /// <see cref="Frame::Navigate(Type, Object)"/> when this page was initially requested and /// a dictionary of state preserved by this page during an earlier /// session. The state will be null the first time a page is visited.</param> void MainPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) { (void) sender; // Unused parameter (void) e; // Unused parameter } /// <summary> /// Preserves state associated with this page in case the application is suspended or the /// page is discarded from the navigation cache. Values must conform to the serialization /// requirements of <see cref="SuspensionManager::SessionState"/>. /// </summary> /// <param name="sender">The source of the event; typically <see cref="NavigationHelper"/></param> /// <param name="e">Event data that provides an empty dictionary to be populated with /// serializable state.</param> void MainPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e) { (void) sender; // Unused parameter (void) e; // Unused parameter }
역시 MainPage.xaml(Windows Phone 8.1)에서, 페이지 아래로 이동하여 "제목 패널" 주석을 찾아 전체 StackPanel을 제거합니다. Phone에서는 블로그 피드를 나열할 화면 공간이 필요합니다.
페이지 아래로 더 내려가면 주석
"TODO: Content should be placed within the following grid"
가 있는 그리드가 나타납니다. 이 ListView를 그 그리드 안에 넣습니다.<!-- Vertical scrolling item list --> <ListView x:Name="itemListView" AutomationProperties.AutomationId="itemListView" AutomationProperties.Name="Items" TabIndex="1" IsItemClickEnabled="True" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" IsSwipeEnabled="False" ItemClick="ItemListView_ItemClick" SelectionMode="Single" ItemTemplate="{StaticResource ListItemTemplate}"> <ListView.ItemContainerStyle> <Style TargetType="FrameworkElement"> <Setter Property="Margin" Value="2,0,0,2"/> </Style> </ListView.ItemContainerStyle> </ListView>
이제 커서를
ItemListView_ItemClick
이벤트에 놓고 F12(정의로 이동) 키를 누릅니다. Visual Studio가 빈 이벤트 처리기 기능을 생성해줍니다. 나중에 여기에 일부 코드를 추가하겠습니다. 지금은 앱이 컴파일되도록 기능을 생성하기만 하면 됩니다.
8부: 게시물 목록 표시 및 선택한 게시물의 텍스트 보기 제공
이 부에서는 Phone 앱에 게시물을 나열하는 페이지와 선택한 게시물의 텍스트 버전을 보여주는 페이지, 이렇게 두 개의 페이지를 추가하겠습니다. Windows 앱에서는 한 쪽에는 목록을, 다른 한 쪽에는 선택한 게시물의 텍스트를 보여 줄 SplitPage라는 단일 페이지를 추가하면 됩니다. 먼저 Phone 페이지입니다.
XAML 태그 추가(Phone 앱 FeedPage)
계속해서 Phone 프로젝트에서 사용자가 선택하는 피드의 게시물을 나열하는 FeedPage에 대해 작업하겠습니다.
FeedPage.xaml(Windows Phone 8.1)에서 데이터 컨텍스트를 페이지 요소에 추가합니다.
DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
이제 시작 페이지 요소 다음에 CollectionViewSource를 추가합니다.
<Page.Resources> <!-- Collection of items displayed by this page --> <CollectionViewSource x:Name="itemsViewSource" Source="{Binding Items}"/> </Page.Resources>
그리드 요소에서 이 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
속성은 코드 숨김(아래 참조)에 있는 LoadState에서 DefaultViewModel 속성에 삽입되는 작업 중인FeedData::Items
속성에 바인딩되는CollectionViewSource
에 바인딩됩니다.ListView에 선언된 ItemClick 이벤트가 있습니다. 그 위에 커서를 놓고 F12 키를 눌러서 코드 숨김에 이벤트 처리기를 생성합니다. 지금은 이것을 비워둡니다.
LoadState 및 SaveState(Phone 앱 FeedPage)
Mainpage에서는, 앱이 어떤 이유로든 시작될 때마다 페이지가 항상 인터넷에서 완전히 다시 초기화되기 때문에 상태를 저장하는 것에 대해 걱정할 필요가 없었습니다. 하지만 다른 페이지는 상태를 기억해야 합니다. 예를 들어 FeedPage가 표시되는 동안 앱이 종료된 경우(메모리에서 언로드), 사용자가 다시 이 앱으로 이동했을 때 앱이 종료되지 않았던 것처럼 보이게 하고 싶습니다. 그러려면 어떤 피드를 선택했었는지 기억해야 합니다. 이 데이터를 저장하는 장소는 로컬 AppData 저장소에 있으며, 저장하기 좋을 때는 MainPage에서 이것을 클릭할 때입니다.
여기에는 한 가지 복잡한 문제가 있습니다. 데이터가 아직 실제로 존재하는가 하는 것입니다. 사용자의 클릭을 통해 MainPage에서 FeedPage로 이동하는 경우 선택한 FeedData 개체가 MainPage에 나타나는 것으로 보아 이미 존재한다는 것을 확실히 알 수 있습니다. 그러나 앱이 다시 시작되면 FeedPage가 마지막으로 본 FeedData 개체에 바인딩을 시도할 때 이 개체가 로드되지 않을 수 있습니다. 따라서 FeedPage(및 다른 페이지)에서는 FeedData를 사용할 수 있을 때를 아는 방법을 필요로 합니다. concurrency::task_completion_event는 이와 같은 상황을 위해 설계되었습니다. 이를 사용하면 MainPage에서 다시 시작하든지 아니면 새로 탐색하든지 상관없이 동일한 코드 경로에서 FeedData 개체를 안전하게 가져올 수 있습니다. 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 클래스 정의가 필요합니다.
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에 "피드" 값이 있게 됨) 현재 피드도 이미 올바로 설정되어 있을 것입니다. 하지만 MainPage에서 앞으로 탐색하거나, 다시 시작한다면, 페이지를 올바른 데이터로 채우기 위해 현재 피드를 가져와야 할 것입니다. 피드 데이터가 다시 시작된 후 도착하기 위해 필요한 경우 GetCurrentFeedAsync가 대기합니다. DefaultViewModel 종속성 속성에 대한 액세스를 시도하기 전에 use_current() 컨텍스트를 지정하여 작업이 UI 스레드로 돌아가게 할 것입니다. XAML 관리 개체는 일반적으로 백그라운드 스레드에서 바로 액세스할 수 없습니다.
페이지가 로드될 때마다 GetCurrentFeedAsync 메서드에서 상태를 가져오므로 이 페이지에서는
SaveState
과(와) 관련하여 아무것도 하지 않습니다.FeedPage.xaml.h 헤더 파일에서 LoadState의 선언을 추가하고, "Common\NavigationHelper.h"에 대한 include 지시문을 추가하고, NavigationHelper 및 DefaultViewModel 속성을 추가합니다.
// // FeedPage.xaml.h // Declaration of the FeedPage class // #pragma once #include "FeedPage.g.h" #include "Common\NavigationHelper.h" namespace SimpleBlogReader { namespace WFC = Windows::Foundation::Collections; namespace WUIX = Windows::UI::Xaml; namespace WUIXNav = Windows::UI::Xaml::Navigation; namespace WUIXControls = Windows::UI::Xaml::Controls; /// <summary> /// A basic page that provides characteristics common to most applications. /// </summary> [Windows::Foundation::Metadata::WebHostHidden] public ref class FeedPage sealed { public: FeedPage(); /// <summary> /// Gets the view model for this <see cref="Page"/>. /// This can be changed to a strongly typed view model. /// </summary> property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel { WFC::IObservableMap<Platform::String^, Platform::Object^>^ get(); } /// <summary> /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>. /// </summary> property Common::NavigationHelper^ NavigationHelper { Common::NavigationHelper^ get(); } protected: virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override; virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override; private: void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e); void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e); static Windows::UI::Xaml::DependencyProperty^ _defaultViewModelProperty; static Windows::UI::Xaml::DependencyProperty^ _navigationHelperProperty; void ItemListView_ItemClick(Platform::Object^ sender, WUIXControls::ItemClickEventArgs^ e); }; }
Feedpage.xaml.cpp에서 이러한 속성의 구현을 추가하면, 이제 다음과 같습니다.
// // FeedPage.xaml.cpp // Implementation of the FeedPage class // #include "pch.h" #include "FeedPage.xaml.h" #include "TextViewerPage.xaml.h" using namespace SimpleBlogReader; using namespace concurrency; using namespace Platform; using namespace Platform::Collections; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::Graphics::Display; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Controls::Primitives; using namespace Windows::UI::Xaml::Data; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Interop; using namespace Windows::UI::Xaml::Media; using namespace Windows::UI::Xaml::Navigation; // The Basic Page item template is documented at https://go.microsoft.com/fwlink/?LinkID=390556 FeedPage::FeedPage() { InitializeComponent(); SetValue(_defaultViewModelProperty, ref new Platform::Collections::Map<String^, Object^>(std::less<String^>())); auto navigationHelper = ref new Common::NavigationHelper(this); SetValue(_navigationHelperProperty, navigationHelper); navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &FeedPage::LoadState); navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &FeedPage::SaveState); } DependencyProperty^ FeedPage::_defaultViewModelProperty = DependencyProperty::Register("DefaultViewModel", TypeName(IObservableMap<String^, Object^>::typeid), TypeName(FeedPage::typeid), nullptr); /// <summary> /// Used as a trivial view model. /// </summary> IObservableMap<String^, Object^>^ FeedPage::DefaultViewModel::get() { return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty)); } DependencyProperty^ FeedPage::_navigationHelperProperty = DependencyProperty::Register("NavigationHelper", TypeName(Common::NavigationHelper::typeid), TypeName(FeedPage::typeid), nullptr); /// <summary> /// Gets an implementation of <see cref="NavigationHelper"/> designed to be /// used as a trivial view model. /// </summary> Common::NavigationHelper^ FeedPage::NavigationHelper::get() { return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty)); } #pragma region Navigation support /// The methods provided in this section are simply used to allow /// NavigationHelper to respond to the page's navigation methods. /// /// Page specific logic should be placed in event handlers for the /// <see cref="NavigationHelper::LoadState"/> /// and <see cref="NavigationHelper::SaveState"/>. /// The navigation parameter is available in the LoadState method /// in addition to page state preserved during an earlier session. void FeedPage::OnNavigatedTo(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedTo(e); } void FeedPage::OnNavigatedFrom(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedFrom(e); } #pragma endregion /// <summary> /// Populates the page with content passed during navigation. Any saved state is also /// provided when recreating a page from a prior session. /// </summary> /// <param name="sender"> /// The source of the event; typically <see cref="NavigationHelper"/> /// </param> /// <param name="e">Event data that provides both the navigation parameter passed to /// <see cref="Frame::Navigate(Type, Object)"/> when this page was initially requested and /// a dictionary of state preserved by this page during an earlier /// session. The state will be null the first time a page is visited.</param> void FeedPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) { (void)sender; // Unused parameter if (!this->DefaultViewModel->HasKey("Feed")) { auto app = safe_cast<App^>(App::Current); app->GetCurrentFeedAsync().then([this, e](FeedData^ fd) { // Insert into the ViewModel for this page to // initialize itemsViewSource->View this->DefaultViewModel->Insert("Feed", fd); this->DefaultViewModel->Insert("Items", fd->Items); }, task_continuation_context::use_current()); } } /// <summary> /// Preserves state associated with this page in case the application is suspended or the /// page is discarded from the navigation cache. Values must conform to the serialization /// requirements of <see cref="SuspensionManager::SessionState"/>. /// </summary> /// <param name="sender">The source of the event; typically <see cref="NavigationHelper"/></param> /// <param name="e">Event data that provides an empty dictionary to be populated with /// serializable state.</param> void FeedPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e) { (void)sender; // Unused parameter }
EventHandlers(Phone 앱 FeedPage)
FeedPage에 대한 한 이벤트인, 사용자가 게시물을 읽을 수 있는 페이지 쪽으로 나아가는 ItemClick 이벤트를 처리하겠습니다. XAML의 이벤트 이름에서 F12 키를 눌렀을 때 이미 스텁 처리기가 만들어졌습니다.
이제 구현을 이 코드로 바꾸겠습니다.
void FeedPage::ItemListView_ItemClick(Platform::Object^ sender, ItemClickEventArgs^ e) { FeedItem^ clickedItem = dynamic_cast<FeedItem^>(e->ClickedItem); this->Frame->Navigate(TextViewerPage::typeid, clickedItem->Link->AbsoluteUri); }
F5 키를 눌러 에뮬레이터에서 Phone 앱을 빌드하고 실행합니다. 이제 MainPage에서 항목을 선택하면 앱이 FeedPage 쪽으로 이동하여 피드 목록을 표시해야 합니다. 다음 단계는 선택한 피드에 대한 텍스트를 표시하는 것입니다.
XAML 태그 추가(Phone 앱 TextViewerPage)
Phone 프로젝트의 TextViewerPage.xaml에서, 콘텐츠에 대한 간단한 텍스트 렌더링과 함께, 앱 이름(드러나지 않게)과 현재 게시물의 제목을 표시할 이 태그로 제목 패널 및 콘텐츠 그리드를 바꿉니다.
<!-- TitlePanel --> <StackPanel Grid.Row="0" Margin="24,17,0,28"> <TextBlock Text="{StaticResource AppName}" Style="{ThemeResource TitleTextBlockStyle}" Typography.Capitals="SmallCaps"/> <TextBlock x:Name="FeedItemTitle" Margin="0,12,0,0" Style="{StaticResource SubheaderTextBlockStyle}" TextWrapping="Wrap"/> </StackPanel> <!--TODO: Content should be placed within the following grid--> <Grid Grid.Row="1" x:Name="ContentRoot"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <ScrollViewer x:Name="itemDetail" AutomationProperties.AutomationId="ItemDetailScrollViewer" Grid.Row="1" Padding="20,20,20,20" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollMode="Disabled" ScrollViewer.VerticalScrollMode="Enabled" ScrollViewer.ZoomMode="Disabled" Margin="4,0,-4,0"> <!--Border enables background color for rich text block--> <Border x:Name="contentViewBorder" BorderBrush="#FFFE5815" Background="AntiqueWhite" BorderThickness="6" Grid.Row="1"> <RichTextBlock x:Name="BlogTextBlock" Foreground="Black" FontFamily="Segoe WP" FontSize="24" Padding="10,10,10,10" VerticalAlignment="Bottom" > </RichTextBlock> </Border> </ScrollViewer> </Grid>
TextViewerPage.xaml.h에서 NavigationHelper 및 DefaultViewItems 속성을 추가한 다음 m_FeedItem 전용 멤버도 추가하여 이전 단계에서 App 클래스에 추가한 GetFeedItem 함수를 사용하여 현재 피드를 처음으로 조회한 후 현재 피드 항목에 대한 참조를 저장합니다.
또한 RichTextHyperlinkClicked 함수도 추가합니다. 이제 TextViewerPage.xaml.h가 다음과 같습니다.
// // TextViewerPage.xaml.h // Declaration of the TextViewerPage class // #pragma once #include "TextViewerPage.g.h" #include "Common\NavigationHelper.h" namespace SimpleBlogReader { namespace WFC = Windows::Foundation::Collections; namespace WUIX = Windows::UI::Xaml; namespace WUIXNav = Windows::UI::Xaml::Navigation; namespace WUIXDoc = Windows::UI::Xaml::Documents; namespace WUIXControls = Windows::UI::Xaml::Controls; /// <summary> /// A basic page that provides characteristics common to most applications. /// </summary> [Windows::Foundation::Metadata::WebHostHidden] public ref class TextViewerPage sealed { public: TextViewerPage(); /// <summary> /// Gets the view model for this <see cref="Page"/>. /// This can be changed to a strongly typed view model. /// </summary> property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel { WFC::IObservableMap<Platform::String^, Platform::Object^>^ get(); } /// <summary> /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>. /// </summary> property Common::NavigationHelper^ NavigationHelper { Common::NavigationHelper^ get(); } protected: virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override; virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override; void RichTextHyperlinkClicked(WUIXDoc::Hyperlink^ link, WUIXDoc::HyperlinkClickEventArgs^ args); private: void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e); void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e); static Windows::UI::Xaml::DependencyProperty^ _defaultViewModelProperty; static Windows::UI::Xaml::DependencyProperty^ _navigationHelperProperty; FeedItem^ m_feedItem; }; }
LoadState 및 SaveState(Phone 앱 TextViewerPage)
TextViewerPage.xaml.cpp에서 이 include 지시문을 추가합니다.
#include "WebViewerPage.xaml.h"
다음의 두 네임스페이스 지시문을 추가합니다.
using namespace concurrency; using namespace Windows::UI::Xaml::Documents;
NavigationHelper 및 DefaultViewModel에 대한 코드를 추가합니다.
TextViewerPage::TextViewerPage() { InitializeComponent(); SetValue(_defaultViewModelProperty, ref new Platform::Collections::Map<String^, Object^>(std::less<String^>())); auto navigationHelper = ref new Common::NavigationHelper(this); SetValue(_navigationHelperProperty, navigationHelper); navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &TextViewerPage::LoadState); navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &TextViewerPage::SaveState); // this->DataContext = DefaultViewModel; } DependencyProperty^ TextViewerPage::_defaultViewModelProperty = DependencyProperty::Register("DefaultViewModel", TypeName(IObservableMap<String^, Object^>::typeid), TypeName(TextViewerPage::typeid), nullptr); /// <summary> /// Used as a trivial view model. /// </summary> IObservableMap<String^, Object^>^ TextViewerPage::DefaultViewModel::get() { return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty)); } DependencyProperty^ TextViewerPage::_navigationHelperProperty = DependencyProperty::Register("NavigationHelper", TypeName(Common::NavigationHelper::typeid), TypeName(TextViewerPage::typeid), nullptr); /// <summary> /// Gets an implementation of <see cref="NavigationHelper"/> designed to be /// used as a trivial view model. /// </summary> Common::NavigationHelper^ TextViewerPage::NavigationHelper::get() { return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty)); } #pragma region Navigation support /// The methods provided in this section are simply used to allow /// NavigationHelper to respond to the page's navigation methods. /// /// Page specific logic should be placed in event handlers for the /// <see cref="NavigationHelper::LoadState"/> /// and <see cref="NavigationHelper::SaveState"/>. /// The navigation parameter is available in the LoadState method /// in addition to page state preserved during an earlier session. void TextViewerPage::OnNavigatedTo(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedTo(e); } void TextViewerPage::OnNavigatedFrom(NavigationEventArgs^ e) { NavigationHelper->OnNavigatedFrom(e); } #pragma endregion
이제
LoadState
및SaveState
의 구현을 다음 코드로 바꿉니다.void TextViewerPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e) { (void)sender; // Unused parameter // (void)e; // Unused parameter auto app = safe_cast<App^>(App::Current); app->GetCurrentFeedAsync().then([this, app, e](FeedData^ fd) { m_feedItem = app->GetFeedItem(fd, safe_cast<String^>(e->NavigationParameter)); FeedItemTitle->Text = m_feedItem->Title; BlogTextBlock->Blocks->Clear(); TextHelper^ helper = ref new TextHelper(); auto blocks = helper-> CreateRichText(m_feedItem->Content, ref new TypedEventHandler<Hyperlink^, HyperlinkClickEventArgs^> (this, &TextViewerPage::RichTextHyperlinkClicked)); for (auto b : blocks) { BlogTextBlock->Blocks->Append(b); } }, task_continuation_context::use_current()); } void TextViewerPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e) { (void)sender; // Unused parameter e->PageState->Insert("Uri", m_feedItem->Link->AbsoluteUri); }
RichTextBlock에 바인딩할 수 없으므로 TextHelper 클래스를 사용하여 수동으로 그 콘텐츠를 만들겠습니다. 간단하게 하기 위해, 피드에서 텍스트만 추출하는 HtmlUtilities::ConvertToText 함수를 사용합니다. 연습으로, 직접 html 또는 xml에 대한 구문 분석을 하고
Blocks
컬렉션에 텍스트는 물론 이미지 링크를 추가해 볼 수 있습니다. SyndicationClient에는 XML 피드 구문 분석을 위한 함수가 있습니다. 일부 피드는 잘 구성된 XML이고 일부는 그렇지 않습니다.
이벤트 처리기(Phone 앱 TextViewerPage)
TextViewerPage에서는 서식 있는 텍스트의 Hyperlink을(를) 사용하여 WebViewerPage로 이동합니다. 이 방법은 페이지 간을 이동하는 일반적인 방법이 아니지만, 이 경우에는 적절한 것 같고, 이 방법을 사용하면 하이퍼링크의 작동 방식을 탐색할 수도 있습니다. 함수 서명은 이미 TextViewerPage.xaml.h에 추가했습니다. 이제 TextViewerPage.xaml.cpp에서 구현을 추가합니다.
///<summary> /// Invoked when the user clicks on the "Link" text at the top of the rich text /// view of the feed. This navigates to the web page. Identical action to using /// the App bar "forward" button. ///</summary> void TextViewerPage::RichTextHyperlinkClicked(Hyperlink^ hyperLink, HyperlinkClickEventArgs^ args) { this->Frame->Navigate(WebViewerPage::typeid, m_feedItem->Link->AbsoluteUri); }
이제 Phone 프로젝트를 시작 프로젝트로 설정하고 F5 키를 누릅니다. 피드 페이지에서 항목을 클릭하고 블로그 게시물을 읽을 수 있는 TextViewerPage로 이동할 수 있어야 합니다. 이 블로그에는 몇 가지 흥미로운 자료가 있습니다!
XAML 추가(Windows 앱 SplitPage)
Windows 앱은 몇 가지 면에서 Phone 앱과 다르게 동작합니다. Windows 프로젝트의 MainPage.xaml이 Phone 앱에서 사용할 수 없는 ItemsPage 템플릿을 어떻게 사용하는지는 이미 살펴보았습니다. 이제 역시 Phone에서 사용할 수 없는 SplitPage를 추가해 보겠습니다. 장치가 가로 방향이면 Windows 앱의 SplitPage에는 오른쪽 및 왼쪽 창이 있습니다. 작업 중인 앱에서 사용자가 이 페이지로 이동하면 왼쪽 창에는 피드 항목의 목록이 표시되고, 오른쪽 창에는 현재 선택한 피드의 텍스트 렌더링이 표시됩니다. 장치가 세로 방향이거나 창이 전체 너비가 아니면, Split Page에서는 VisualStates를 사용하여 두 개의 분리된 페이지인 것처럼 동작합니다. 이것은 코드에서 "논리적 페이지 탐색"이라고 합니다.
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가 설정되어 있습니다.
titlePanel 그리드를 두 열을 포함하도록 조정하겠습니다. 이렇게 하면 피드 제목을 화면의 전체 너비에 표시할 수 있습니다.
<Grid x:Name="titlePanel" Grid.ColumnSpan="2">
이제 이것과 동일한 그리드에서 pageTitle TextBlock을 찾아 바인딩을 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 메서드를 사용하고 있습니다. 이 페이지와 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로 이동합니다. 물론 이 페이지에 아직 콘텐츠는 없습니다. Phone 프로젝트를 따라 잡은 후에 진행하겠습니다.
뒤로 탐색에 대하여
Windows 앱의 SplitPage에서는 부분에 필요한 추가적인 코딩 없이도 다시 MainPage로 돌아가는 뒤로 탐색 단추가 제공됩니다. Phone에서는, 뒤로 단추 기능이 소프트웨어 단추가 아니라 하드웨어 뒤로 단추로 제공됩니다. Phone 뒤로 단추 탐색은 Common 폴더에 있는 NavigationHelper 클래스에 의해 처리됩니다. 관련 코드를 보려면 솔루션에서 "BackPressed"(Ctrl + Shift + F)를 검색합니다. 여기서도 수행해야 할 추가 작업은 없습니다. 그대로 작동합니다.
9부: 선택한 게시물의 웹 보기 추가
앞으로 추가하게 될 마지막 페이지는 원본 웹 페이지에서 블로그 게시물을 표시하는 페이지입니다. 경우에 따라 독자가 그림을 보고 싶어 할 수도 있습니다. 웹 페이지 보기의 단점은 일부 웹 페이지의 경우 모바일 장치용 서식이 지정되어 있지 않아 휴대폰 화면에서 텍스트를 읽기 어려울 수 있다는 것입니다. 때로는 여백이 화면을 벗어나 가로 스크롤을 많이 해야 합니다. WebViewerPage 페이지는 비교적 간단합니다. 페이지에서 WebView 컨트롤을 추가하고 이것이 모든 작업을 수행하도록 하면 됩니다. Phone 프로젝트를 시작하겠습니다.
XAML 추가(Phone 앱 WebViewerPage)
WebViewerPage.xaml에서, 제목 패널과 contentRoot 그리드를 추가합니다.
<!-- TitlePanel --> <StackPanel Grid.Row="0" Margin="10,10,10,10"> <TextBlock Text="{StaticResource AppName}" Style="{ThemeResource TitleTextBlockStyle}" Typography.Capitals="SmallCaps"/> </StackPanel> <!--TODO: Content should be placed within the following grid--> <Grid Grid.Row="1" x:Name="ContentRoot"> <!-- Back button and page title --> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!--This will render while web page is still downloading, indicating that something is happening--> <TextBlock x:Name="pageTitle" Text="{Binding Title}" Grid.Column="1" IsHitTestVisible="false" TextWrapping="WrapWholeWords" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="40,20,40,20"/> </Grid>
LoadState 및 SaveState(Phone 앱 WebViewerPage)
NavigationHelper 및 WebViewerPage.xaml.h 파일의 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 Developer Center에서 애니메이션에 대해 더 알 수 있습니다. 여기서 다시 이 페이지에 도착할 수 있는 두 가지 가능한 방법을 다뤄야 합니다. 아직 알아나가고 있는 단계이므로, 현재 수준을 확인해야 합니다.
정말 간단하죠! F5 키를 누르면 이제 TextViewerPage에서 WebViewerPage로 이동할 수 있습니다!
이제 Windows 프로젝트로 다시 이동합니다. 이 작업은 Phone에 대해 방금 수행한 작업과 매우 유사합니다.
XAML 추가(Windows 앱 WebViewerPage)
WebViewerPage.xaml에서, SizeChanged 이벤트를 페이지 요소에 추가하고 이름을 pageRoot_SizeChanged로 지정합니다. 여기에 삽입 지점을 넣고 F12 키를 눌러 코드 숨김을 생성합니다.
"Back button and page title" 그리드를 찾아 TextBlock을 삭제합니다. 페이지 제목은 웹 페이지에 표시되므로 여기에서 공간을 차지할 필요가 없습니다.
이제 해당 뒤로 단추 그리드의 바로 다음에 WebView가 있는 Border을(를) 추가합니다.
<Border x:Name="contentViewBorder" BorderBrush="Gray" BorderThickness="2" Grid.Row="1" Margin="20,20,20,20"> <WebView x:Name="contentView" ScrollViewer.HorizontalScrollMode="Enabled" ScrollViewer.VerticalScrollMode="Enabled"/> </Border>
WebView 컨트롤은 많은 작업을 자동으로 수행하지만, 몇 가지 면에서 이 컨트롤을 다른 XAML 컨트롤과 차별화하는 특이한 점이 있습니다. 앱에서 이 컨트롤을 광범위하게 사용할 예정이라면, 반드시 이 컨트롤에 대해 많이 알아보세요.
멤버 변수 추가
WebViewerPage.xaml.h에 다음 비공개 선언을 추가합니다.
Platform::String^ m_feedItemUri;
LoadState 및 SaveState(Windows 앱 WebViewerPage)
LoadState
및SaveState
기능을 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와 Phone 모두에서 잘 작동합니다. 하지만 최종 단계로서, 현실적인 작업으로 사용자가 자신이 선택한 피드를 추가 및 삭제할 수 있도록 하겠습니다. 먼저 사용자가 처음 앱을 시작할 때 화면이 비어있지 않도록 기본 피드를 표시한 다음, 피드를 추가하고 삭제할 수 있도록 단추들을 추가하게 됩니다. 물론 사용자가 세션 간에 유지할 수 있도록 사용자 피드 목록을 저장해야 합니다. 지금이 앱 로컬 데이터에 대해 배우기에 적절한 시점입니다.
첫 번째 단계로서, 앱이 처음으로 시작될 때에도 몇 가지 기본 피드를 저장해야 합니다. 하지만, 기본 피드를 하드 코드하는 대신 ResourceLoader에서 찾을 수 있도록 문자열 리소스 파일에 넣을 수 있습니다. 이 리소스를 Windows 및 Phone 앱 모두에 컴파일해야 하므로 공유 프로젝트에서 .resw 파일을 만들겠습니다.
문자열 리소스 추가:
솔루션 탐색기에서 공유 프로젝트를 선택한 다음 마우스 오른쪽 단추를 클릭하고 새 항목을 추가합니다. 왼쪽 창에서 리소스를 선택한 다음 가운데 창에서 리소스 파일(.resw)을 선택합니다. (.rc 파일은 데스크톱 앱 용이므로 선택하지 마세요.) 기본 이름을 그대로 두거나 아무 이름이나 지정합니다. 그런 다음 "추가"를 클릭합니다.
다음 이름-값 쌍을 추가합니다.
- URL_1 http://sxp.microsoft.com/feeds/3.0/devblogs
- URL_2 https://blogs.windows.com/windows/b/bloggingwindows/rss.aspx
- URL_3 https://azure.microsoft.com/blog/feed
작업이 완료되면 리소스 편집기가 다음과 같이 표시되어야 합니다.
피드를 추가 및 제거하기 위한 공유 코드를 추가합니다.
URL을 FeedDataSource 클래스에 로드하기 위한 코드를 추가하겠습니다. feeddata.h에서 이 비공개 멤버 함수를 FeedDataSource에 추가합니다.
concurrency::task<Windows::Foundation::Collections::IVector<Platform::String^>^> GetUserURLsAsync();
다음 구문을 FeedData.cpp에 추가합니다.
using namespace Windows::Storage; using namespace Windows::Storage::Streams;
그런 다음 구현을 추가합니다.
/// <summary> /// The first time the app runs, the default feed URLs are loaded from the local resources /// into a text file that is stored in the app folder. All subsequent additions and lookups /// are against that file. The method has to return a task because the file access is an /// async operation, and the call site needs to be able to continue from it with a .then method. /// </summary> task<IVector<String^>^> FeedDataSource::GetUserURLsAsync() { return create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("Feeds.txt", CreationCollisionOption::OpenIfExists)) .then([](StorageFile^ file) { return FileIO::ReadLinesAsync(file); }).then([](IVector<String^>^ t) { if (t->Size == 0) { // The data file is new, so we'll populate it with the // default URLs that are stored in the apps resources. auto loader = ref new Resources::ResourceLoader(); t->Append(loader->GetString("URL_1\n")); t->Append(loader->GetString("URL_2")); t->Append(loader->GetString("URL_3")); // Before we return the URLs, let's create the new file asynchronously // for use next time. We don't need the result of the operation now // because we already have vec, so we can just kick off the task to // run whenever it gets scheduled. create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([t](StorageFile^ file) { OutputDebugString(L"append lines async\n"); FileIO::AppendLinesAsync(file, t); }); } // Return the URLs return create_task([t]() { OutputDebugString(L"returning t\n"); return safe_cast<IVector<String^>^>(t); }); }); }
GetUserURLsAsync가 feeds.txt 파일이 있는지 확인하게 됩니다. 없으면 이 파일을 만들고 문자열 리소스에서 URL을 추가합니다. 사용자 추가하는 모든 파일은 feeds.txt 파일로 이동됩니다. 모든 파일 쓰기 작업은 비동기적이므로, 파일 데이터에 대한 액세스를 시도하기 전에 비동기 작업이 완료되었는지 확인하기 위해 task와 .then 연속을 사용합니다.
이제 이전 InitDataSource 구현을 GetUerURLsAsync를 호출하는 이 코드로 바꿉니다.
///<summary> /// Retrieve the data for each atom or rss feed and put it into our custom data structures. ///</summary> void FeedDataSource::InitDataSource() { auto urls = GetUserURLsAsync() .then([this](IVector<String^>^ urls) { // Populate the list of feeds. SyndicationClient^ client = ref new SyndicationClient(); for (auto url : urls) { RetrieveFeedAndInitData(url, client); } }); }
피드를 추가 및 제거하는 기능은 Windows와 Phone에서 동일합니다. 따라서 이 기능을 앱 클래스에 넣겠습니다. App.xaml.h에서
이 내부 멤버를 추가합니다.
void AddFeed(Platform::String^ feedUri); void RemoveFeed(Platform::String^ feedUri);
App.xaml.cpp에서 이 네임스페이스를 추가합니다.
using namespace Platform::Collections;
App.xaml.cpp에서,
void App::AddFeed(String^ feedUri) { auto feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); auto client = ref new Windows::Web::Syndication::SyndicationClient(); // The UI is data-bound to the items collection and will update automatically // after we append to the collection. feedDataSource->RetrieveFeedAndInitData(feedUri, client); // Add the uri to the roaming data. The API requires an IIterable so we have to // put the uri in a Vector. Vector<String^>^ vec = ref new Vector<String^>(); vec->Append(feedUri); concurrency::create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([vec](StorageFile^ file) { FileIO::AppendLinesAsync(file, vec); }); } void App::RemoveFeed(Platform::String^ feedTitle) { // Create a new list of feeds, excluding the one the user selected. auto feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource")); int feedListIndex = -1; Vector<String^>^ newFeeds = ref new Vector<String^>(); for (unsigned int i = 0; i < feedDataSource->Feeds->Size; ++i) { if (feedDataSource->Feeds->GetAt(i)->Title == feedTitle) { feedListIndex = i; } else { newFeeds->Append(feedDataSource->Feeds->GetAt(i)->Uri); } } // Delete the selected item from the list view and the Feeds collection. feedDataSource->Feeds->RemoveAt(feedListIndex); // Overwrite the old data file with the new list. create_task(ApplicationData::Current->LocalFolder-> CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists)) .then([newFeeds](StorageFile^ file) { FileIO::WriteLinesAsync(file, newFeeds); }); }
추가 및 제거 단추를 위한 XAML 태그 추가(Windows 8.1)
피드 추가 및 제거를 한 단추는 MainPage에 속합니다. 단추는 Windows 앱의 TopAppBar과(와) Phone 앱의 BottomAppBar에 넣게 됩니다(Phone 앱에는 위쪽 앱 바가 없음). Windows 프로젝트의 MainPage.xaml에서 Page.Resources 노드 바로 뒤에 TopAppBar를 추가합니다.
<Page.TopAppBar> <CommandBar x:Name="cmdBar" IsSticky="False" Padding="10,0,10,0"> <AppBarButton x:Name="addButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Icon="Add"> <Button.Flyout> <Flyout Placement="Top"> <Grid> <StackPanel> <TextBox x:Name="tbNewFeed" Width="400"/> <Button Click="AddFeed_Click">Add feed</Button> </StackPanel> </Grid> </Flyout> </Button.Flyout> </AppBarButton> <AppBarButton x:Name="removeButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Icon="Remove" Click="removeFeed_Click"/> <!--These buttons appear when the user clicks the remove button to signal that they want to remove a feed. Delete removes the feed(s) and returns to the normal visual state and cancel just returns to the normal state. --> <AppBarButton x:Name="deleteButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Visibility="Collapsed" Icon="Delete" Click="deleteButton_Click"/> <AppBarButton x:Name="cancelButton" Height="95" Margin="20,0,20,0" HorizontalAlignment="Right" Visibility="Collapsed" Icon="Cancel" Click="cancelButton_Click"/> </CommandBar> </Page.TopAppBar>
네 개의 Click 이벤트 처리기 이름(add, remove, delete, cancel) 각각에서 처리기 이름에 커서를 놓고 F12 키를 눌러 코드 숨김에서 기능을 생성합니다.
<VisualStateManager.VisualStateGroups> 요소 안에 이 두 번째 VisualStateGroup을 추가합니다.
<VisualStateGroup x:Name="SelectionStates"> <VisualState x:Name="Normal"/> <VisualState x:Name="Checkboxes"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" Storyboard.TargetProperty="SelectionMode"> <DiscreteObjectKeyFrame KeyTime="0" Value="Multiple"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" Storyboard.TargetProperty="IsItemClickEnabled"> <DiscreteObjectKeyFrame KeyTime="0" Value="False"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="cmdBar" Storyboard.TargetProperty="IsSticky"> <DiscreteObjectKeyFrame KeyTime="0" Value="True"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup>
피드 추가 및 제거를 위한 이벤트 처리기(Windows 8.1)를 추가합니다.
MainPage.xaml.cpp에서 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 키를 누릅니다. 이 각 멤버 기능에서는 단추에 대한 표시 유형 속성을 적절한 값으로 설정한 다음, "기본 시각적 상태"로 이동합니다.
추가 및 제거 단추를 위한 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 키를 눌러 코드 숨김을 생성합니다.
전체 VisualStateGroups 노드가 다음과 같이 표시되도록 "Checkboxes" VisualStateGroup을 추가합니다.
<VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="SelectionStates"> <VisualState x:Name="Normal"/> <VisualState x:Name="Checkboxes"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemListView" Storyboard.TargetProperty="SelectionMode"> <DiscreteObjectKeyFrame KeyTime="0" Value="Multiple"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemListView" Storyboard.TargetProperty="IsItemClickEnabled"> <DiscreteObjectKeyFrame KeyTime="0" Value="False"/> </ObjectAnimationUsingKeyFrames> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>
피드 추가 및 제거 단추를 위한 이벤트 처리기 추가(Windows Phone 8.1)
MainPage.xaml.cpp(WIndows Phone 8.1)에서, 방금 만든 스텁 이벤트 처리기를 다음 코드로 바꿉니다.
void MainPage::AddFeed_Click(Platform::Object^ sender, RoutedEventArgs^ e) { if (tbNewFeed->Text->Length() > 9) { auto app = static_cast<App^>(App::Current); app->AddFeed(tbNewFeed->Text); } } void MainPage::removeFeed_Click(Platform::Object^ sender, RoutedEventArgs^ e) { VisualStateManager::GoToState(this, "Checkboxes", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; addButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Visible; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Visible; } void MainPage::deleteButton_Click(Platform::Object^ sender, RoutedEventArgs^ e) { for (auto item : ItemListView->SelectedItems) { // Get the feed the user selected. Object^ proxy = safe_cast<Object^>(item); FeedData^ item = safe_cast<FeedData^>(proxy); // Remove it from the data file and app-wide feed collection auto app = safe_cast<App^>(App::Current); app->RemoveFeed(item->Title); } VisualStateManager::GoToState(this, "Normal", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible; addButton->Visibility = Windows::UI::Xaml::Visibility::Visible; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; } void MainPage::cancelButton_Click(Platform::Object^ sender, RoutedEventArgs^ e) { VisualStateManager::GoToState(this, "Normal", false); removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible; addButton->Visibility = Windows::UI::Xaml::Visibility::Visible; deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; }
F5 키를 누르고 새 단추를 사용하여 피드를 추가하거나 제거합니다! 휴대폰에서 피드를 추가하려면 웹 페이지의 RSS 링크를 클릭한 다음 "저장"을 선택합니다. URL의 이름이 있는 편집 상자를 누른 다음, 복사 아이콘을 누릅니다. 다시 앱으로 이동하고, 편집 상자에서 삽입 지점을 놓고 다시 복사 아이콘을 눌러 URL에 붙여넣습니다. 피드가 피드 목록에 거의 즉시 표시되는 것이 보입니다.
이제 SimpleBlogReader 앱이 충분히 사용 가능한 상태입니다. Windows 장치에 배포할 준비가 되었습니다.
앱을 자신의 휴대폰에 배포하려면 먼저 Windows Phone 등록에서 설명한 대로 등록해야 합니다.
잠금 해제된 Windows Phone에 배포하려면
릴리스 빌드를 만듭니다.
주 메뉴에서 프로젝트 | 스토어 | 앱 패키지 만들기를 선택합니다. 이 연습에서는 스토어에 배포하지 않습니다. 변경할 이유가 없다면 다음 화면에서 기본값을 적용합니다.
패키지가 성공적으로 만들어진 경우 Windows 앱 인증 키트(WACK)를 실행하라는 메시지가 나타납니다. 앱에 스토어의 수락을 허용하지 않을 숨겨진 결함이 없다는 확인하기 위해 이 작업을 수행할 수 있습니다. 하지만 스토어에 배포하지 않을 것이므로 이 단계는 선택 사항입니다.
주 메뉴에서 도구 | Windows Phone 8.1 | 응용 프로그램 배포를 선택합니다. 응용 프로그램 배포 마법사가 표시되고 첫 번째 화면에 대상이 "장치"로 표시됩니다. 찾아보기 단추를 클릭하여 프로젝트 트리에서 Debug 및 Release 폴더와 같은 수준에 있는 AppPackages 폴더로 이동합니다. 해당 폴더에서 최신 패키지를 찾고(둘 이상의 경우), 두 번 클릭한 다음 그 안에 있는 appx 또는 appxbundle 파일을 클릭합니다.
휴대폰이 컴퓨터에 꽂혀 있고 잠금 화면으로 잠겨 있지 않은지 확인합니다. 마법사에서 배포 단추를 누르고 배포가 완료되기를 기다립니다. 몇 초 이내에 "배포 성공" 메시지가 표시됩니다. 휴대폰의 응용 프로그램 목록에서 앱을 찾고 탭하여 앱을 실행합니다.
참고: 처음엔 새 URL을 추가하는 것이 약간 비직관적일 수 있습니다. 추가할 URL을 검색한 다음 링크를 탭합니다. 프롬프트에서 열겠다고 합니다. IE에 파일이 열린 후 표시되는 임시 xml 파일 이름이 아니라, http://feeds.bbci.co.uk/news/world/rss.xml과 같은RSS URL을 복사합니다. IE에서 XML 페이지가 열리는 경우, 다시 이전 IE 화면으로 이동하여 주소 표시줄에서 원하는 URL을 복사해야 합니다. 복사된 후에는 다시 Simple Blog Reader로 이동하고 Add Feed 텍스트 블록에 붙여넣은 다음, "피드 추가" 단추를 누릅니다. 기본 페이지에 완전히 초기화된 피드가 매우 빠르게 표시되는 것을 보게 될 것입니다. 학습자용 연습: 공유 계약이나 다른 수단을 구현하여 새 URL을 SimpleBlogReader에 추가하는 작업을 단순화합니다. 즐거운 정보가 되었기를 바랍니다.
다음 단계
이 자습서에서는 Microsoft Visual Studio Express 2012 for Windows 8의 기본 제공 페이지 템플릿을 사용하여 여러 페이지로 된 앱을 빌드하는 방법과 페이지 간에 이동하고 데이터를 전달하는 방법을 알아보았습니다. 그다음으로는 스타일과 템플릿을 사용하여 앱을 Windows 팀 블로그 웹 사이트의 특징에 맞게 하는 방법도 알아보았습니다. 또한 테마 애니메이션 및 앱 바를 사용하여 앱을 Windows 스토어 앱의 특징에 맞게 만드는 방법도 익혔습니다. 마지막으로 앱이 항상 최상으로 표시되도록 다양한 레이아웃과 방향에 적응시키는 방법을 알아보았습니다.
이제 이 앱은 Windows 스토어에 제출할 준비가 거의 다 되었습니다. Windows 스토어에 앱을 제출하는 방법에 대한 자세한 내용은 다음을 참조하세요.
- 앱 마케팅
- 앱에 액세스할 수 있도록 하는 방법입니다. 자세한 내용은 접근성을 참조하세요.
- 학습 및 참조 리소스 목록: C++로 작성한 Windows 런타임 앱용 로드 맵.