Controles de XAML; enlazar a una propiedad de C++/WinRT

Una propiedad que se puede enlazar de forma eficaz a un control de XAML se conoce como una propiedad observable. Esta idea se basa en el patrón de diseño de software conocido como patrón observador. En este tema se muestra cómo implementar propiedades observables en C++/WinRT y cómo enlazar controles de elementos XAML a dichas propiedades (para obtener información general, consulta Enlace de datos).

Importante

Para conocer los conceptos y términos esenciales que te ayuden a entender cómo consumir y crear clases en tiempo de ejecución con C++/WinRT, consulta Consumir API con C++/WinRT y Crear API con C++/WinRT .

¿Qué significa observable con respecto a una propiedad?

Supongamos que una clase en tiempo de ejecución denominada BookSku tiene una propiedad denominada Title. Si BookSku genera el evento INotifyPropertyChanged::PropertyChanged siempre que el valor de Title cambia, significa que la propiedad Title será de tipo observable. Es el comportamiento de BookSku (generar o no generar el evento) que determina cuáles de sus propiedades, si las hay, son observables.

Un control o elemento de texto XAML puede enlazarse a estos eventos y controlarlos. Dicho elemento o control administra el evento. Para ello, recupera los valores actualizados y, luego, se actualiza automáticamente para mostrar el nuevo valor.

Nota

Para más información sobre cómo instalar y usar la Extensión de Visual Studio (VSIX) para C++/WinRT y el paquete de NuGet (que juntos proporcionan la plantilla de proyecto y compatibilidad de la compilación), consulta Compatibilidad de Visual Studio para C++/WinRT.

Crear una aplicación en blanco (Bookstore)

Para empezar, crea un proyecto en Microsoft Visual Studio. Crea un proyecto Aplicación en blanco (C++/WinRT) y asígnale el nombre Bookstore. Asegúrese de que la opción Colocar la solución y el proyecto en el mismo directorio esté desactivada. Elija como destino la versión más reciente disponible de manera general (es decir, no en versión preliminar) de Windows SDK.

Vamos a crear una nueva clase para representar un libro que tenga una propiedad de título observable. Vamos a crear y consumir la clase dentro de la misma unidad de compilación. Pero queremos poder enlazar a esta clase desde XAML y para ello tiene que ser una clase en tiempo de ejecución. Y vamos a usar C++/WinRT tanto para crearla como para consumirla.

El primer paso para crear una clase en tiempo de ejecución es agregar un nuevo elemento Archivo MIDL (.idl) al proyecto. Asigne el nombre BookSku.idl al nuevo elemento. Elimina el contenido predeterminado de BookSku.idl y pégalo en esta declaración de clase en tiempo de ejecución.

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

Nota

Las clases del modelo de vista, en realidad cualquier clase en tiempo de ejecución que declares en la aplicación, no deben derivar de una clase base. La clase BookSku declarada anteriormente es un ejemplo de ello. Implementa una interfaz, pero no deriva de ninguna clase base.

Cualquier clase en tiempo de ejecución que declares en la aplicación y que derive de una clase base se conoce como una clase que admite composición. Las clases que admiten composición tienen restricciones. Para que una aplicación pase las pruebas del kit de certificación de aplicaciones en Windows que Visual Studio y Microsoft Store utilizan para validar los envíos (y para que la aplicación se incorpore correctamente a Microsoft Store), una clase que admite composición debe derivar en última instancia de una clase base de Windows. Eso significa que, en la raíz misma de la jerarquía de herencia, la clase debe ser un tipo que se origina en un espacio de nombres Windows.*. Si necesita derivar una clase en tiempo de ejecución de una clase base, por ejemplo para implementar una clase BindableBase para todos los modelos de vista de los que puede derivar, puede derivar de Windows.UI.Xaml.DependencyObject.

Un modelo de vista es una abstracción de una vista y, por lo tanto, está enlazado directamente a la vista (el marcado XAML). Un modelo de datos es una abstracción de datos, solo lo consumen los modelos de vista y no se enlazan directamente a XAML. Por lo tanto, en lugar de declarar los modelos de datos como clases en tiempo de ejecución, puede hacerlo como estructuras o clases de C++. No es necesario declararlos en MIDL y tienes la libertad de usar cualquier jerarquía de herencia que prefieras.

Guarde el archivo y compile el proyecto. La compilación aún no se completará correctamente (del todo), pero realizará algunas acciones que necesitamos. En concreto, durante el proceso de compilación, la herramienta midl.exe se ejecuta para crear un archivo de metadatos de Windows Runtime que describe la clase en tiempo de ejecución (el archivo se sitúa en el disco en \Bookstore\Debug\Bookstore\Unmerged\BookSku.winmd). Después se ejecutará la herramienta cppwinrt.exe para generar archivos de código fuente y ayudarte a crear y consumir tu clase en tiempo de ejecución. Estos archivos incluyen códigos auxiliares para que puedas empezar a implementar la clase en tiempo de ejecución BookSku que declaraste en tu archivo IDL. Los encontraremos en el disco en un momento, pero esos códigos auxiliares son \Bookstore\Bookstore\Generated Files\sources\BookSku.h y BookSku.cpp.

Por lo tanto, haga clic con el botón derecho en el nodo del proyecto en Visual Studio y haga clic en Abrir carpeta en Explorador de archivos. Se abrirá la carpeta del proyecto en el Explorador de archivos. Ahora debería examinar el contenido de la carpeta \Bookstore\Bookstore\. Desde allí, vaya a la carpeta \Generated Files\sources\ y copie los archivos de código auxiliar BookSku.h y BookSku.cpp en el Portapapeles. Vuelva a la carpeta del proyecto (\Bookstore\Bookstore\) y pegue los dos archivos que acaba de copiar. Por último, en el Explorador de soluciones con el nodo de proyecto seleccionado, asegúrate de que Mostrar todos los archivos esté activado. Haz clic con el botón derecho en los archivos de código auxiliar que has copiado y luego haz clic en Incluir en el proyecto.

Implementar BookSku

Ahora, vamos a abrir \Bookstore\Bookstore\BookSku.h y BookSku.cpp e implementar nuestra clase en tiempo de ejecución. Primero, verá static_assert en la parte superior de BookSku.h y BookSku.cpp y deberá quitarlo.

A continuación, en BookSku.h, haga estos cambios.

  • En el constructor predeterminado, cambie = default por = delete. El motivo de ello es que no queremos un constructor predeterminado.
  • Agregue un miembro privado para almacenar la cadena de título. Tenga en cuenta que tenemos un constructor que toma el valor winrt::hstring. Ese valor es la cadena de título.
  • Agregue otro miembro privado para el evento que se generará cuando el título cambie.

Después de hacer estos cambios, tu BookSku.h tendrá este aspecto.

// 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(Windows::UI::Xaml::Data::PropertyChangedEventHandler const& value);
        void PropertyChanged(winrt::event_token const& token);
    
    private:
        winrt::hstring m_title;
        winrt::event<Windows::UI::Xaml::Data::PropertyChangedEventHandler> m_propertyChanged;
    };
}
namespace winrt::Bookstore::factory_implementation
{
    struct BookSku : BookSkuT<BookSku, implementation::BookSku>
    {
    };
}

Implementa las funciones en BookSku.cpp de este modo.

// 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, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"Title" });
        }
    }

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

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

En la función de mutación Title, comprobamos si se ha establecido un valor diferente del valor actual. Y, si es el caso, actualizamos el título y también generamos el evento INotifyPropertyChanged::PropertyChanged con un argumento igual que el nombre de la propiedad que cambió. Esto sirve para que la interfaz de usuario sepa qué valor de propiedad tiene que volver a consultar.

El proyecto se volverá a compilar ahora, en caso de que quiera comprobarlo.

Declarar e implementar BookstoreViewModel

Nuestra página principal de XAML se enlazará a un modelo de vista principal. Y ese modelo de vista tendrá varias propiedades, incluida una de tipo BookSku . En este paso, declararemos e implementaremos la clase en tiempo de ejecución del modelo de vista principal.

Agrega un nuevo elemento Midl File (.idl) denominado BookstoreViewModel.idl. Pero consulta también Factorizar clases en tiempo de ejecución en archivos Midl (.idl).

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

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

Guárdelo y compílelo (la compilación no se completará correctamente, pero la realizamos para volver a generar los archivos de código auxiliar).

Copia BookstoreViewModel.h y BookstoreViewModel.cpp desde la carpeta Generated Files\sources en la carpeta del proyecto e inclúyelos en el proyecto. Abra estos archivos (para ello, vuelva a eliminar static_assert) e implemente la clase en tiempo de ejecución como se indica a continuación. Observa cómo, en BookstoreViewModel.h, hemos incluido BookSku.h, que declara el tipo de implementación para BookSku (que es winrt::Bookstore::implementation::BookSku). Y hemos quitado = default del constructor predeterminado.

Nota

En los siguientes listados para BookstoreViewModel.h y BookstoreViewModel.cpp, el código muestra la forma predeterminada de construir el miembro de datos m_bookSku. Este es el método que se creó en la primera versión de C++/WinRT, y se recomienda al menos estar familiarizado con el patrón. Con la versión 2.0 de C++ o WinRT y las posteriores, dispone de una forma de construcción optimizada que se conoce como construcción uniforme (consulte Novedades y cambios de C++/WinRT 2.0). Más adelante en este tema, mostraremos un ejemplo de construcción uniforme.

// 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;
    }
}

Nota

El tipo de m_bookSku es el tipo proyectado (winrt::Bookstore::BookSku) y el parámetro de plantilla que usas con winrt::make es el tipo de implementación (winrt::Bookstore::implementation::BookSku). Aun así, make devuelve una instancia del tipo proyectado.

A continuación, se volverá a compilar el proyecto.

Agrega una propiedad de tipo BookstoreViewModel a MainPage

Abre MainPage.idl, que declara la clase en tiempo de ejecución que representa nuestra página de interfaz de usuario principal.

  • Agregue una directiva import para importar BookstoreViewModel.idl.
  • Agregue una propiedad de solo lectura denominada MainViewModel de tipo BookstoreViewModel.
  • Quite la propiedad MyProperty.
// MainPage.idl
import "BookstoreViewModel.idl";

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

Guarde el archivo. El proyecto no se compilará correctamente por completo en este momento, pero realizar la compilación ahora resulta útil, ya que se generan los archivos de código fuente en los que se implementará la clase en tiempo de ejecución MainPage (\Bookstore\Bookstore\Generated Files\sources\MainPage.h y MainPage.cpp). Así que compila ahora. En esta fase, el error de compilación que verás es "MainViewModel": no es miembro de "winrt::Bookstore::implementation::MainPage" .

Si omites la inclusión de BookstoreViewModel.idl (consulta la lista anterior de MainPage.idl), verás el error se espera < cerca de "MainViewModel" . Otra sugerencia es asegurarse de que deja todos los tipos en el mismo espacio de nombres, que se muestra en los listados de código.

Para resolver el error que esperamos ver, tendrás que copiar el código auxiliar del descriptor de acceso de la propiedad MainViewModel fuera de los archivos generados (\Bookstore\Bookstore\Generated Files\sources\MainPage.h y MainPage.cpp) y en \Bookstore\Bookstore\MainPage.h y MainPage.cpp. Estos son los pasos para hacerlo.

En \Bookstore\Bookstore\MainPage.h, siga estos pasos.

  • Incluya BookstoreViewModel.h, que declara el tipo de implementación para BookstoreViewModel (que es winrt::Bookstore::implementation::BookstoreViewModel).
  • Agrega un miembro privado para almacenar el modelo de vista. Tenga en cuenta que la función de descriptor de acceso de propiedad (y el miembro m_mainViewModel) se implementan en términos del tipo proyectado para BookstoreViewModel (que es Bookstore::BookstoreViewModel).
  • El tipo de implementación está en el mismo proyecto (unidad de compilación) que la aplicación, por lo que construimos m_mainViewModel a través de la sobrecarga del constructor que toma std::nullptr_t.
  • Quite la propiedad MyProperty.

Nota

En el par de listados siguiente para MainPage.h y MainPage.cpp, el código muestra la forma predeterminada de construir el miembro de datos m_mainViewModel. En la siguiente sección, mostraremos una versión que utiliza la construcción uniforme en su lugar.

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

        Bookstore::BookstoreViewModel MainViewModel();

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

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

En \Bookstore\Bookstore\MainPage.cpp, como se muestra en la lista siguiente, realice los cambios siguientes.

  • Llame a winrt::make (con el tipo de implementación BookstoreViewModel) para asignar una nueva instancia del tipo BookstoreViewModel proyectado a m_mainViewModel. Como hemos visto anteriormente, el constructor BookstoreViewModel crea un nuevo objeto BookSku como miembro de datos privado, estableciendo su título inicialmente en L"Atticus".
  • En el controlador de eventos del botón (ClickHandler), actualice el título del libro a su título publicado.
  • Implemente el descriptor de acceso para la propiedad MainViewModel.
  • Quite la propiedad MyProperty.
// MainPage.cpp
#include "pch.h"
#include "MainPage.h"
#include "MainPage.g.cpp"

using namespace winrt;
using namespace Windows::UI::Xaml;

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

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

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

Construcción uniforme

Para usar la construcción uniforme en lugar de winrt::make, en MainPage.h declare e inicialice m_mainViewModel en un solo paso, como se muestra a continuación.

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

A continuación, en el constructor MainPage de MainPage.cpp, no se requiere el código m_mainViewModel = winrt::make<Bookstore::implementation::BookstoreViewModel>();.

Para obtener más información sobre la construcción uniforme y ejemplos de código, consulte Participación en la construcción uniforme y acceso de implementación directa.

Enlaza el botón a la propiedad Title

Abre MainPage.xaml, que contiene el marcado XAML de nuestra página principal de la interfaz de usuario. Tal y como muestra el listado siguiente, quita el nombre del el botón y cambia el valor de la propiedad Content de un literal a una expresión de enlace. Ten en cuenta la propiedad Mode=OneWay en la expresión de enlace (unidireccional desde el modelo de vista a la interfaz de usuario). Sin dicha propiedad, la interfaz de usuario no responderá a los eventos cambiados de propiedad.

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

Ahora compila y ejecuta el proyecto. Haz clic en el botón para ejecutar el controlador de eventos Clic. Este controlador llama la función de mutación de título del libro; la mutación genera un evento para que la interfaz de usuario sepa que la propiedad Title ha cambiado; y el botón vuelve a consultar el valor de dicha propiedad para actualizar su propio valor Content.

Uso de la extensión de marcado {Binding} con C++/WinRT

En la versión actualmente publicada de C++/WinRT, para poder usar la extensión de marcado {Binding} se deben implementar las interfaces ICustomPropertyProvider y ICustomProperty.

Enlace de elemento a elemento

Se puede enlazar la propiedad de un elemento XAML a la propiedad de otro elemento XAML. En este ejemplo se muestra cómo queda en marcado.

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

Debes declarar la entidad XAML con nombre myTextBox como una propiedad de solo lectura en el archivo Midl (.idl).

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

Este es el motivo de tal necesidad. Todos los tipos que el compilador XAML debe validar (incluidas aquellas utilizadas en {x: Bind}) se leen desde Metadatos de Windows (WinMD). Lo único que necesita hacer es agregar la propiedad de solo lectura al archivo Midl. No la implementes, porque el código XAML subyacente generado automáticamente proporciona la implementación por ti.

Consumir objetos a partir del marcado XAML

Todas las entidades consumidas por la extensión de marcado {x:Bind} de XAML deben exponerse públicamente en IDL. Además, si el marcado XAML contiene una referencia a otro elemento que también está en el marcado, el captador de dicho marcado debe estar presente en 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>

El elemento ChangeColorButton hace referencia al elemento UseCustomColorCheckBox a través del enlace. Por lo tanto, el IDL de esta página debe declarar una propiedad de solo lectura denominada UseCustomColorCheckBox para que sea accesible para el enlace.

El delegado del controlador de eventos click para UseCustomColorCheckBox usa la sintaxis de delegado de XAML clásica, de modo que no necesita una entrada en el IDL; tan solo tiene que ser público en la clase de la implementación. Por otro lado, ChangeColorButton también tiene un controlador de eventos click {x:Bind}, que también debe entrar en el IDL.

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

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

No es necesario que proporciones una implementación para la propiedad UseCustomColorCheckBox. El generador de código XAML lo hace automáticamente.

Enlazar a un valor booleano

Puede hacerlo en modo de diagnóstico:

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

Se muestra true o false en C++/CX, pero se muestra Windows.Foundation.IReference`1<Boolean> en C++/WinRT.

En su lugar, use x:Bind al enlazar a un valor booleano.

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

Mediante las bibliotecas de implementación de Windows (WIL).

Las bibliotecas de implementación de Windows (WIL) proporcionan asistentes para facilitar la escritura de propiedades enlazables. Consulte Notificar propiedades en la documentación de WIL.

API importantes