C++/WinRT（非ストアアプリ）でコレクションのデータバインディング

[前回](https://qiita.com/cubeundcube/items/50ff7949aeb1f6146489 "C++/WinRT（非ストアアプリ）でデータバインディング")の最後に、コレクションもバインディングしたいと述べた。
さぞかし難航するんだろうなーと思ってたら、すんなり出来た。

中に入れる要素は前回のようにきちんと作らないといけないが、ガワのコレクションは`IObservableVector`をそのまま使えば良かった。
ただしコレクションの初期化は`single_threaded_observable_vector`でやる必要がある。

非ストアアプリにおいて、コレクションのデータバインディング条件をまとめるとこう。

1. バインディング可能な要素を`IObservableVector`で包んでコレクションを宣言
1. 専用の関数テンプレート`single_threaded_observable_vector`でコレクションを初期化

これだけ。

![binding_collection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1109951/598f230e-d9f4-0148-50e7-ac24c425a8b9.png)

サンプルアプリは、右上のボタンをクリックするたびにリストの項目が増えていくというものにした。

##サンプルコード

[前回の記事](https://qiita.com/cubeundcube/items/50ff7949aeb1f6146489 "C++/WinRT（非ストアアプリ）でデータバインディング")と異なるのは「pch.h」「main.xaml」「main.cpp」だけなので、その3つだけ記載。
手抜きで申し訳無い。

```cpp:pch.h
#include <windows.h>
#include <windows.ui.xaml.hosting.desktopwindowxamlsource.h>    //DesktopWindowXamlSource

#pragma push_macro("GetCurrentTime")
#pragma push_macro("TRY")
#undef GetCurrentTime
#undef TRY

#include <winrt/windows.ui.xaml.hosting.h>  //WindowsXamlManager
#include <winrt/windows.ui.xaml.markup.h>   //XamlReader
#include <winrt/windows.ui.xaml.controls.h>
#include <winrt/windows.ui.xaml.controls.primitives.h>  //Click
#include <winrt/windows.ui.xaml.data.h>    //Binding
#include <winrt/windows.ui.xaml.interop.h>	//xaml_typename
#include <winrt/windows.foundation.collections.h>	//IObservableVector
#include <winrt/windows.security.cryptography.h>    //CryptographicBuffer

#pragma pop_macro("TRY")
#pragma pop_macro("GetCurrentTime")
```

`IObservableVector`を使うので`windows.foundation.collections.h`のインクルードが増えたが、それにより上位の`windows.foundation.h`はここでインクルードする必要が無くなった。

```xml:main.xaml
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
	<Grid.RowDefinitions>
		<RowDefinition Height="Auto"/>
		<RowDefinition Height="*"/>
	</Grid.RowDefinitions>
	<Button Name="button1" Content="ボタン" HorizontalAlignment="Right"/>
    <ListView Name="list1" Grid.Row="1">
        <ListView.ItemTemplate>
			<DataTemplate>
				<StackPanel VerticalAlignment="Center">
					<TextBlock Text="{Binding Name}" FontSize="16"/>
				</StackPanel>
			</DataTemplate>
		</ListView.ItemTemplate>
    </ListView>
</Grid>
```

今回はルートを`<StackPanel>`じゃなくて`<Grid>`にしてみた。
（ListViewをスクロールさせたかったため。）
基本的には最上位は`<Grid>`の方が良さそう。

```cpp:main.cpp
#include "pch.h"
#include "Person.h"

#include "resource.h"

using namespace winrt;
using namespace Windows::UI::Xaml;  //XamlRoot
using namespace Windows::UI::Xaml::Hosting;  //WindowsXamlManager
using namespace Windows::UI::Xaml::Markup;   //XamlReader
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::Foundation;    //IInspectable
using namespace Windows::Foundation::Collections;	//IObservableVector
using namespace Windows::Security::Cryptography;    //CryptographicBuffer
using namespace CppBind;    //Person

//名前からコントロールを特定するテンプレート
template<typename T> T Element(const wchar_t *name) {
	return _xamlroot.Content().as<Panel>().FindName(name).as<T>();
};

LRESULT CALLBACK WindowProc(HWND, UINT, WPARAM, LPARAM);
void OnButtonClick(IInspectable const &, RoutedEventArgs const &);

HWND _hWnd;
HWND _hWndXamlIsland = nullptr;
XamlRoot _xamlroot = { nullptr };

//コレクション宣言
IObservableVector<Person> _persons;

int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd) {
    //ウインドウクラス登録
    const wchar_t szWindowClass[] = L"Win32DesktopApp";
    WNDCLASSEX windowClass = {};
    windowClass.cbSize = sizeof(WNDCLASSEX);
    windowClass.lpfnWndProc = WindowProc;
    windowClass.hInstance = hInstance;
    windowClass.lpszClassName = szWindowClass;
    windowClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    windowClass.hIconSm = ::LoadIcon(nullptr, IDI_APPLICATION);
    if (::RegisterClassEx(&windowClass) == 0) {
        ::MessageBox(nullptr, L"Windows registration failed!", L"Error", MB_OK);
        return 0;
    }

    //ウインドウ作成
    _hWnd = ::CreateWindow(
        szWindowClass,
        L"Windows c++ Win32 Desktop App",
        WS_OVERLAPPEDWINDOW | WS_VISIBLE,
        CW_USEDEFAULT, CW_USEDEFAULT, 320, 640,
        nullptr,
        nullptr,
        hInstance,
        nullptr
    );
    if (_hWnd == nullptr) {
        ::MessageBox(nullptr, L"Call to CreateWindow failed!", L"Error", MB_OK);
        return 0;
    }

    //C++/WinRTのおまじない
    init_apartment(apartment_type::single_threaded);
    WindowsXamlManager winxamlmanager = WindowsXamlManager::InitializeForCurrentThread();

    //XAMLホスト準備
    DesktopWindowXamlSource desktopSource;
    auto interop = desktopSource.as<IDesktopWindowXamlSourceNative>();
    check_hresult(interop->AttachToWindow(_hWnd));
    interop->get_WindowHandle(&_hWndXamlIsland);

    //XAMLホストのサイズをメインウインドウに合わせる
    RECT rcClient;
    ::GetClientRect(_hWnd, &rcClient);
    ::SetWindowPos(_hWndXamlIsland, nullptr, 0, 0, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top, SWP_SHOWWINDOW);

    //リソースからXAMLコードを取り出してXAMLホストにセット
    auto hResInfo = ::FindResource(hInstance, MAKEINTRESOURCE(IDR_XAML_MAIN), L"XAMLFILE");
    auto res_size = ::SizeofResource(hInstance, hResInfo);
    std::vector<uint8_t> xaml_array(res_size, 0);
    auto hGlobal = ::LoadResource(hInstance, hResInfo);
    char *xaml_bin = (char *)::LockResource(hGlobal);
    ::memcpy_s(xaml_array.data(), res_size, xaml_bin, res_size);
    auto xaml_buf = CryptographicBuffer::CreateFromByteArray(xaml_array);
    auto xamlstr = CryptographicBuffer::ConvertBinaryToString(BinaryStringEncoding::Utf8, xaml_buf);
    auto xamlContainer = XamlReader::Load(xamlstr.data()).as<Grid>();
    desktopSource.Content(xamlContainer);
    _xamlroot = xamlContainer.XamlRoot();

    //コレクション初期化
    _persons = single_threaded_observable_vector<Person>();

    Element<ListView>(L"list1").ItemsSource(_persons);

    Person person;
    person.Name(L"Cube Cube");
    _persons.Append(person);

    //ボタンにイベントハンドラ登録
    Element<Button>(L"button1").Click({ &OnButtonClick });

    //メインウインドウ表示
    ::ShowWindow(_hWnd, nShowCmd);
    ::UpdateWindow(_hWnd);

    //メッセージループ
    MSG msg = {};
    while (::GetMessage(&msg, nullptr, 0, 0)) {
        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
    }

    return 0;
}

//ウインドウプロシージャ
LRESULT CALLBACK WindowProc(HWND hWnd, UINT messageCode, WPARAM wParam, LPARAM lParam) {
    RECT rcClient;

    switch (messageCode) {
        case WM_CREATE:
            //return 0;
            break;

        case WM_DESTROY:
            ::PostQuitMessage(0);
            break;

        case WM_SIZE:   //メインウインドウのサイズが変わったらXAMLホストも追従
            ::GetClientRect(hWnd, &rcClient);
            ::SetWindowPos(_hWndXamlIsland, nullptr, 0, 0, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top, SWP_SHOWWINDOW);

            return 0;
            break;

        default:
            return ::DefWindowProc(hWnd, messageCode, wParam, lParam);
            break;
    }

    return 0;
}

//ボタンクリックのイベントハンドラ
void OnButtonClick(IInspectable const &sender, RoutedEventArgs const &e) {
    Person person;
    person.Name(L"Mr. Append");
    _persons.Append(person);
}
```

気を付けるのは、コレクション（`IObservableVector`）を`single_threaded_observable_vector<T>()`で初期化するのを忘れないこと。

コレクションの中の任意の要素の任意のプロパティを変更してみたら、ちゃんと`ListView`の方にも反映されたので一安心。