Элементы управления XAML; привязка к свойству C++/WinRT

Свойство, которое может быть эффективно привязано к элементу управления XAML, называется наблюдаемым свойством. Эта идея основана на шаблоне проектирования программного обеспечения, известном как шаблон наблюдателя. В этом разделе показано, как реализовать наблюдаемые свойства в C++/WinRT и как привязать элементы управления XAML к ним (для фоновой информации см. привязку данных).

Это важно

Основные понятия и термины, которые поддерживают понимание использования и разработки классов среды выполнения с помощью C++/WinRT, см. в статье "Использование API с помощью C++/WinRT и разработки API с помощью C++/WinRT".

Что такое наблюдаемое значение для свойства?

Предположим, что класс среды выполнения с именемBookSku имеет свойство Title. Если BookSku вызывает событие INotifyPropertyChanged::P ropertyChanged при каждом изменении значения Title , то это означает, что Title является наблюдаемым свойством. Именно поведение BookSku (генерирует событие или нет) определяет, какие из его свойств, если вообще какие-либо, являются наблюдаемыми.

Текстовый элемент XAML или элемент управления может привязаться к этим событиям и обрабатывать их. Такой элемент или элемент управления обрабатывает событие, извлекая обновленные значения, а затем обновляя его, чтобы отобразить новое значение.

Note

Сведения об установке и использовании расширения C++/WinRT Visual Studio (VSIX) и пакета NuGet (которые вместе предоставляют поддержку шаблона проекта и сборки), см. Visual Studio поддержку C++/WinRT.

Создайте пустое приложение («Книжный магазин»)

Начните с создания проекта в Microsoft Visual Studio. Создайте проект C++ Пустое упакованное приложение (WinUI 3 на рабочем столе) и назовите его Bookstore. Убедитесь, что флажок «Разместить решение и проект в одном каталоге» снят. Нацелимся на последнюю общедоступную версию (т. е. не предварительную) пакета SDK Windows.

Мы создадим новый класс для представления книги с наблюдаемым свойством заголовка. Мы создадим и потребляем класс в одном модуле компиляции. Но мы хотим иметь возможность привязаться к этому классу из XAML, и по этой причине он будет классом среды выполнения. И мы будем использовать C++/WinRT как для его создания, так и для его использования.

Первым шагом в создании нового класса среды выполнения является добавление нового элемента Midl File (IDL) в проект. Присвойте новому элементу имя BookSku.idl. Удалите содержимое BookSku.idl по умолчанию и вставьте следующее объявление класса среды выполнения.

// BookSku.idl
namespace Bookstore
{
    runtimeclass BookSku : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
    {
        BookSku(String title);
        String Title;
    }
}

Note

Классы модели представления ( на самом деле, любой класс среды выполнения, объявленный в приложении), не должны быть производными от базового класса. Класс BookSku , объявленный выше, является примером этого. Он реализует интерфейс, но не является производным от базового класса.

Любой класс среды выполнения, объявленный в приложении, который является производным от базового класса, называется составным классом. Существуют ограничения для составных классов. Чтобы приложение успешно проходило тесты приложение для Windows Certification Kit, используемые Visual Studio и Microsoft Store для проверки отправляемых приложений (а значит, чтобы приложение могло быть успешно принято в Microsoft Store), компонуемый класс в конечном итоге должен наследоваться от базового класса Windows. Это означает, что класс в самом корне иерархии наследования должен быть типом, определённым в пространстве имён Windows.* или Microsoft.*. Если вам действительно нужно унаследовать класс среды выполнения от базового класса — например, чтобы реализовать класс BindableBase, от которого будут наследоваться все ваши модели представления, — то можно унаследовать его от Microsoft.UI.Xaml.DependencyObject.

Модель представления — это абстракция представления, поэтому она привязана непосредственно к представлению (разметка XAML). Модель данных — это абстракция данных, и она используется только из моделей представления и не привязана непосредственно к XAML. Таким образом, вы можете объявить модели данных не как классы среды выполнения, а как структуры или классы C++. Они не должны быть объявлены в MIDL, и вы можете использовать любую иерархию наследования, которую вы хотите.

Сохраните файл и создайте проект. Сборка пока не завершится (полностью) успешно, но выполнит для нас несколько необходимых действий. В частности, во время сборки midl.exe средство запускается для создания файла метаданных среда выполнения Windows, описывающего класс среды выполнения (файл помещается на диск по адресу\Bookstore\Debug\Bookstore\Unmerged\BookSku.winmd). Затем запускается средство cppwinrt.exe для создания файлов исходного кода, чтобы помочь вам при создании и использовании класса среды выполнения. Эти файлы содержат заготовки, которые помогут вам приступить к реализации класса среды выполнения BookSku, который вы объявили в IDL. Мы через мгновение найдем их на диске, а эти заглушки — \Bookstore\Bookstore\Generated Files\sources\BookSku.h и BookSku.cpp.

Теперь щелкните правой кнопкой мыши узел проекта в Visual Studio и щелкните "Открыть папку" в проводнике. Откроется папка проекта в Проводнике. Теперь вы должны посмотреть содержимое \Bookstore\Bookstore\ папки. Затем перейдите в папку \Generated Files\sources\ и скопируйте файлы-заглушки BookSku.h и BookSku.cpp в буфер обмена. Вернитесь в папку проекта (\Bookstore\Bookstore\) и вставьте два скопированных файла. Наконец, выберите узел проекта в Обозревателе решений и убедитесь, что переключатель Показать все файлы включен. Щелкните правой кнопкой мыши по скопированным файлам заглушек, затем выберите Include In Project.

Реализуйте BookSku

Теперь давайте открываем \Bookstore\Bookstore\BookSku.h и BookSku.cpp реализуем класс среды выполнения. Во-первых, вы увидите static_assert в верхней части BookSku.h и BookSku.cpp, который нужно удалить.

Затем в BookSku.h внесите эти изменения.

  • В конструкторе по умолчанию замените = default на = delete. Это связано с тем, что мы не хотим конструктор по умолчанию.
  • Добавьте частный член для хранения строки заголовка. Обратите внимание, что у нас есть конструктор, который принимает значение winrt::hstring . Это значение является строкой заголовка.
  • Добавьте еще один закрытый член для события, которое мы создадим при изменении заголовка.

После внесения этих изменений ваш BookSku.h будет выглядеть следующим образом.

// BookSku.h
#pragma once
#include "BookSku.g.h"

namespace winrt::Bookstore::implementation
{
    struct BookSku : BookSkuT<BookSku>
    {
        BookSku() = delete;
        BookSku(winrt::hstring const& title);

        winrt::hstring Title();
        void Title(winrt::hstring const& value);
        winrt::event_token PropertyChanged(Microsoft::UI::Xaml::Data::PropertyChangedEventHandler const& value);
        void PropertyChanged(winrt::event_token const& token);
    
    private:
        winrt::hstring m_title;
        winrt::event<Microsoft::UI::Xaml::Data::PropertyChangedEventHandler> m_propertyChanged;
    };
}
namespace winrt::Bookstore::factory_implementation
{
    struct BookSku : BookSkuT<BookSku, implementation::BookSku>
    {
    };
}

В BookSku.cpp реализуйте функции следующим образом.

// BookSku.cpp
#include "pch.h"
#include "BookSku.h"
#include "BookSku.g.cpp"

namespace winrt::Bookstore::implementation
{
    BookSku::BookSku(winrt::hstring const& title) : m_title{ title }
    {
    }

    winrt::hstring BookSku::Title()
    {
        return m_title;
    }

    void BookSku::Title(winrt::hstring const& value)
    {
        if (m_title != value)
        {
            m_title = value;
            m_propertyChanged(*this, Microsoft::UI::Xaml::Data::PropertyChangedEventArgs{ L"Title" });
        }
    }

    winrt::event_token BookSku::PropertyChanged(Microsoft::UI::Xaml::Data::PropertyChangedEventHandler const& handler)
    {
        return m_propertyChanged.add(handler);
    }

    void BookSku::PropertyChanged(winrt::event_token const& token)
    {
        m_propertyChanged.remove(token);
    }
}

В функции мутатора title мы проверяем, задано ли значение, отличное от текущего значения. И если да, то обновим заголовок, а также создадим событие INotifyPropertyChanged::P ropertyChanged с аргументом, равным имени свойства, которое изменилось. Таким образом, пользовательский интерфейс будет знать, какое значение свойства следует повторно запрашивать.

Проект будет построен снова, если вы хотите проверить это.

Объявите и реализуйте BookstoreViewModel

Основная страница XAML будет привязана к основной модели представления. И эта модель представления будет иметь несколько свойств, включая один из типов BookSku. На этом шаге мы объявим и реализуем класс среды выполнения модели основного представления.

Добавьте новый элемент Midl File (.idl) с именем BookstoreViewModel.idl. См. также Выделение классов среды выполнения в файлы MIDL (.idl).

// BookstoreViewModel.idl
import "BookSku.idl";

namespace Bookstore
{
    runtimeclass BookstoreViewModel
    {
        BookstoreViewModel();
        BookSku BookSku{ get; };
    }
}

Сохраните и выполните сборку (сборка пока не завершится полностью успешно, но мы запускаем её, чтобы повторно сгенерировать файлы-заглушки).

Скопируйте BookstoreViewModel.h и BookstoreViewModel.cpp из Generated Files\sources папки в папку проекта и добавьте их в проект. Откройте эти файлы (удалив static_assert снова) и реализуйте класс среды выполнения, как показано ниже. Обратите внимание, что в BookstoreViewModel.h мы подключаем BookSku.h, в котором объявлен тип реализации для BookSku (то есть winrt::Bookstore::implementation::BookSku). И мы удаляем = default из конструктора по умолчанию.

Note

В приведённых ниже листингах для BookstoreViewModel.h и BookstoreViewModel.cpp код иллюстрирует стандартный способ инициализации члена данных m_bookSku. Это метод, который используется с первого выпуска C++/WinRT, и с этим паттерном полезно хотя бы ознакомиться. В C++/WinRT версии 2.0 и более поздних версиях есть оптимизированная форма строительства, доступная для вас как единообразная конструкция (см. новости и изменения в C++/WinRT 2.0). Далее в этом разделе мы рассмотрим пример единообразного строительства.

// BookstoreViewModel.h
#pragma once
#include "BookstoreViewModel.g.h"
#include "BookSku.h"

namespace winrt::Bookstore::implementation
{
    struct BookstoreViewModel : BookstoreViewModelT<BookstoreViewModel>
    {
        BookstoreViewModel();

        Bookstore::BookSku BookSku();

    private:
        Bookstore::BookSku m_bookSku{ nullptr };
    };
}
namespace winrt::Bookstore::factory_implementation
{
    struct BookstoreViewModel : BookstoreViewModelT<BookstoreViewModel, implementation::BookstoreViewModel>
    {
    };
}
// BookstoreViewModel.cpp
#include "pch.h"
#include "BookstoreViewModel.h"
#include "BookstoreViewModel.g.cpp"

namespace winrt::Bookstore::implementation
{
    BookstoreViewModel::BookstoreViewModel()
    {
        m_bookSku = winrt::make<Bookstore::implementation::BookSku>(L"Atticus");
    }

    Bookstore::BookSku BookstoreViewModel::BookSku()
    {
        return m_bookSku;
    }
}

Note

Типом m_bookSku является проецируемый тип (winrt::Bookstore::BookSku), а параметр шаблона, используемый с winrt::make , является типом реализации (winrt::Bookstore::implementation::BookSku). Тем не менее, make возвращает экземпляр проецируемого типа.

Проект будет построен снова.

Добавление свойства типа BookstoreViewModel в MainPage

Open MainPage.idl, который объявляет класс среды выполнения, представляющий главную страницу пользовательского интерфейса.

  • Добавьте директиву import для импорта BookstoreViewModel.idl.
  • Добавьте свойство только для чтения с именем MainViewModel типа BookstoreViewModel.
  • Удалите свойство MyProperty .
// MainPage.idl
import "BookstoreViewModel.idl";

namespace Bookstore
{
    runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
    {
        MainPage();
        BookstoreViewModel MainViewModel{ get; };
    }
}

Сохраните файл. Проект еще не будет полностью успешным в построении, но теперь сборка является полезной задачей, так как она повторно создает файлы исходного кода, в которых реализуется класс среды выполнения MainPage (\Bookstore\Bookstore\Generated Files\sources\MainPage.h и MainPage.cpp). Так что приступайте к созданию прямо сейчас. Ошибка сборки, которую можно увидеть на этом этапе, — MainViewModel: не является членом winrt::Bookstore::implementation::MainPage.

Если опустить директиву include для BookstoreViewModel.idl (см. листинг MainPage.idl выше), то появится сообщение об ошибке ожидается < рядом с "MainViewModel". Еще один совет: размещайте все типы в одном и том же пространстве имён — в том, которое показано в листингах кода.

Чтобы устранить ожидаемую ошибку, теперь вам потребуется скопировать заглушки методов доступа для свойства MainViewModel из сгенерированных файлов (\Bookstore\Bookstore\Generated Files\sources\MainPage.h и MainPage.cpp) в \Bookstore\Bookstore\MainPage.h и MainPage.cpp. Действия, необходимые для этого, описаны ниже.

В \Bookstore\Bookstore\MainPage.h выполните следующие действия.

  • Включите BookstoreViewModel.h, который объявляет тип реализации для BookstoreViewModel (который является winrt::Bookstore::implementation::BookstoreViewModel).
  • Добавьте частный член для хранения модели представления. Обратите внимание, что функция доступа к свойству (и член m_mainViewModel) реализована через проецируемый тип для BookstoreViewModel (то есть Bookstore::BookstoreViewModel).
  • Тип реализации находится в том же проекте (единице компиляции), что и приложение, поэтому мы создадим m_mainViewModel через перегрузку конструктора, которая принимает std::nullptr_t.
  • Удалите свойство MyProperty .

Note

В паре листингов ниже для MainPage.h и MainPage.cpp код иллюстрирует способ создания по умолчанию члена данных m_mainViewModel. В следующем разделе мы покажем версию, которая использует единообразную конструкцию.

// MainPage.h
...
#include "BookstoreViewModel.h"
...
namespace winrt::Bookstore::implementation
{
    struct MainPage : MainPageT<MainPage>
    {
        MainPage();

        Bookstore::BookstoreViewModel MainViewModel();

        void ClickHandler(Windows::Foundation::IInspectable const&, Microsoft::UI::Xaml::RoutedEventArgs const&);

    private:
        Bookstore::BookstoreViewModel m_mainViewModel{ nullptr };
    };
}
...

В \Bookstore\Bookstore\MainPage.cppприведенном ниже списке внесите следующие изменения.

  • Вызовите winrt::make (с использованием типа реализации BookstoreViewModel), чтобы назначить m_mainViewModel новый экземпляр спроецированного типа BookstoreViewModel. Как мы видели выше, конструктор BookstoreViewModel создает новый объект BookSku в качестве члена частных данных, задав его название изначально L"Atticus".
  • В обработчике событий кнопки (ClickHandler) обновите название книги до его опубликованного названия.
  • Реализуйте аксессор для свойства MainViewModel.
  • Удалите свойство MyProperty .
// MainPage.cpp
#include "pch.h"
#include "MainPage.h"
#include "MainPage.g.cpp"

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::Bookstore::implementation
{
    MainPage::MainPage()
    {
        m_mainViewModel = winrt::make<Bookstore::implementation::BookstoreViewModel>();
        InitializeComponent();
    }

    void MainPage::ClickHandler(Windows::Foundation::IInspectable const& /* sender */, Microsoft::UI::Xaml::RoutedEventArgs const& /* args */)
    {
        MainViewModel().BookSku().Title(L"To Kill a Mockingbird");
    }

    Bookstore::BookstoreViewModel MainPage::MainViewModel()
    {
        return m_mainViewModel;
    }
}

Единообразная конструкция

Чтобы использовать унифицированную инициализацию вместо winrt::make, в MainPage.h объявите и инициализируйте m_mainViewModel за один шаг, как показано ниже.

// MainPage.h
...
#include "BookstoreViewModel.h"
...
struct MainPage : MainPageT<MainPage>
{
    ...
private:
    Bookstore::BookstoreViewModel m_mainViewModel;
};
...

А затем в конструкторе MainPage.cpp нет необходимости в кодеm_mainViewModel = winrt::make<Bookstore::implementation::BookstoreViewModel>();.

Дополнительные сведения о единообразном построении и примерах кода см. в разделе "Согласие на единое строительство" и прямой доступ к реализации.

Привязка кнопки к свойству Title

Откройте файл MainPage.xaml, который содержит разметку XAML для главной страницы пользовательского интерфейса. Как показано в приведенном ниже описании, удалите имя из кнопки и измените его значение свойства Content с литерала на выражение привязки. Обратите внимание на свойство Mode=OneWay у выражения привязки (односторонняя привязка от модели представления к пользовательскому интерфейсу). Без этого свойства пользовательский интерфейс не будет реагировать на события изменения свойства.

<Button Click="ClickHandler" Content="{x:Bind MainViewModel.BookSku.Title, Mode=OneWay}"/>

Теперь создайте и запустите проект. Нажмите кнопку, чтобы выполнить обработчик событий Click . Этот обработчик вызывает функцию мутатора названия книги; этот мутатор вызывает событие, чтобы сообщить пользовательскому интерфейсу, что свойство Title изменилось; и кнопка повторно запрашивает значение этого свойства для обновления собственного значения содержимого .

Использование расширения разметки {Binding} с C++/WinRT

Для текущей выпущенной версии C++/WinRT, чтобы иметь возможность использовать расширение разметки {Binding}, необходимо реализовать интерфейсы ICustomPropertyProvider и ICustomProperty.

Привязка элемента к элементу

Свойство одного элемента XAML можно привязать к свойству другого элемента XAML. Ниже приведен пример того, как это выглядит в разметке.

<TextBox x:Name="myTextBox" />
<TextBlock Text="{x:Bind myTextBox.Text, Mode=OneWay}" />

Необходимо объявить именованную сущность myTextBox XAML как свойство только для чтения в файле Midl (IDL).

// MainPage.idl
runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
{
    MainPage();
    Microsoft.UI.Xaml.Controls.TextBox myTextBox{ get; };
}

Вот причина этой необходимости. Все типы, необходимые компилятору XAML для проверки (включая те, которые используются в {x:Bind}), считываются из Windows метаданных (WinMD). Все, что необходимо сделать, — добавить свойство только для чтения в файл Midl. Не реализуйте его, так как автоматически созданный код XAML обеспечивает реализацию.

Использование объектов, заданных в XAML-разметке

Все сущности, к которым выполняется обращение с помощью расширения разметки XAML {x:Bind}, должны быть объявлены как общедоступные в IDL. Кроме того, если разметка XAML содержит ссылку на другой элемент, который также находится в разметке, то метод получения этой разметки должен присутствовать в IDL.

<Page x:Name="MyPage">
    <StackPanel>
        <CheckBox x:Name="UseCustomColorCheckBox" Content="Use custom color"
             Click="UseCustomColorCheckBox_Click" />
        <Button x:Name="ChangeColorButton" Content="Change color"
            Click="{x:Bind ChangeColorButton_OnClick}"
            IsEnabled="{x:Bind UseCustomColorCheckBox.IsChecked.Value, Mode=OneWay}"/>
    </StackPanel>
</Page>

Элемент ChangeColorButton ссылается на элемент UseCustomColorCheckBox через привязку. Поэтому IDL для этой страницы должен объявить свойство, доступное только для чтения, с именем UseCustomColorCheckBox , чтобы оно было доступно для привязки.

Делегат обработчика событий щелчка для UseCustomColorCheckBox использует классический синтаксис делегата XAML, поэтому не требует записи в IDL; Он должен быть общедоступным в классе реализации. С другой стороны, у ChangeColorButton также есть обработчик события click {x:Bind}, который также должен быть включён в IDL.

runtimeclass MyPage : Microsoft.UI.Xaml.Controls.Page
{
    MyPage();

    // These members are consumed by binding.
    void ChangeColorButton_OnClick();
    Microsoft.UI.Xaml.Controls.CheckBox UseCustomColorCheckBox{ get; };
}

Не нужно предоставлять реализацию для свойства UseCustomColorCheckBox . Генератор кода XAML делает это для вас.

Привязка к логическому значению

Это можно сделать в режиме диагностики:

<TextBlock Text="{Binding CanPair}"/>

Это отображается true или false в C++/CX, но отображается Windows.Foundation.IReference`1<Boolean> в C++/WinRT.

Вместо этого используйте x:Bind при привязке к логическому значению.

<TextBlock Text="{x:Bind CanPair}"/>

Использование библиотек реализации Windows (WIL)

Библиотеки реализации Windows (WIL) предоставляют вспомогательные средства для упрощения написания привязываемых свойств. См. «Уведомляющие свойства» в документации WIL.

Важные API