Skip to content

CppWinRT authoring helpers

Alexander Sklar edited this page Oct 24, 2023 · 1 revision

WIL includes C++/WinRT authoring helpers that aid with implementing WinRT classes using C++/WinRT.

For example, if you have a class in an IDL file like this:

runtimeclass SomeViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged {
    String MyStr;
    Int32 MyInt;

    event Windows.Foundation.EventHandler<String> MyEvent;

    void DoSomething();
}

You can use these helpers to implement your SomeViewModel:

struct SomeViewModel : SomeViewModelT<SomeViewModel>,
                       wil::notify_property_changed_base<SomeViewModel>
{
    WIL_NOTIFYING_PROPERTY(winrt::hstring, MyStr, L"Hello from WIL!");
    WIL_NOTIFYING_PROPERTY(int32_t, MyInt, 42);

    wil::untyped_event<winrt::hstring> MyEvent;

    void DoSomething()
    {
        // Automatically fires a property-changed event
        MyStr(L"This fires a property-changed event!");

        // You can also fire events manually
        m_MyInt = 123; // no event
        RaisePropertyChanged(L"MyInt");

        // Fire custom events
        MyEvent.invoke(*this, L"Fire events!");
    }
};

Versus implementing all of that manually:

(expand to see full implementation)
struct SomeViewModel : SomeViewModelT<SomeViewModel>
{
    // Windows.UI.Xaml.Data.INotifyPropertyChanged
    winrt::event_token PropertyChanged(winrt::Windows::UI::Xaml::Data::PropertyChangedEventHandler const& value)
    {
        return m_propertyChanged.add(value);
    }
    void PropertyChanged(const winrt::event_token& token)
    {
        m_propertyChanged.remove(token);
    }

    // MyStr
    winrt::hstring m_MyStr = L"Hello from WIL!";
    winrt::hstring MyStr() {
        return m_MyStr;
    }
    void MyStr(winrt::hstring v) {
        m_MyStr = v;
        m_propertyChanged(*this, { L"MyStr" });
    }

    // MyInt
    int32_t m_MyInt = 42;
    int32_t MyInt() {
        return m_MyInt;
    }
    void MyInt(int32_t v) {
        m_MyInt = v;
        m_propertyChanged(*this, { L"MyInt" });
    }

    // MyEvent
    winrt::event_token MyEvent(winrt::Windows::Foundation::EventHandler<winrt::hstring> const& value)
    {
        return m_myEvent.add(value);
    }
    void MyEvent(const winrt::event_token& token)
    {
        m_myEvent.remove(token);
    }

    void DoSomething()
    {
        // Basically unchanged.
    }

private:
    winrt::event<Windows::UI::Xaml::Data::PropertyChangedEventHandler> m_propertyChanged;
    winrt::event<winrt::hstring> m_myEvent;
};

Usage

Import wil/cppwinrt_authoring.h:

#include <wil/cppwinrt_authoring.h>

Properties API

WinRT read? WinRT write? Can fire property changed? Example IDL Notes
wil::single_threaded_property<T> ❌ (internal writes only*) String MyStr{ get; };
wil::single_threaded_rw_property<T> String MyStr;
WIL_NOTIFYING_PROPERTY(T, Name, defaultValue) String MyStr; in a class that inherits from INotifyPropertyChanged
  • Import winrt/Microsoft.UI.Xaml.Data.h (for WinUI 3 projects) or winrt/Windows.UI.Xaml.Data.h (for UWP projects)
  • The base class probably should inherit from wil::notify_property_changed_based<CRTP>
  • See Notifying properties below
wil::single_threaded_notifying_property<T> String MyStr; in a class that inherits from INotifyPropertyChanged Same as above + also, must be initialized. See Notifying properties below

* wil::single_threaded_property does not expose set, so it can only be used for {get;}; properties. It does this by not implementing void Name(T const& value), which is how C++/WinRT exposes settable properties to WinRT. However, it still exposes a public void operator=(T const& value) C++ function, so any code with access to the direct implementation class (including the implementation class itself) can still set the value.

Examples

Simple properties

// MyComponent.idl
namespace MyNamespace
{
    runtimeclass MyComponent
    {
        String ReadWriteProperty; // Equivalent: `ReadWriteProperty{get;set;};`
        Boolean ReadableProperty{ get; };
        SomeOtherComponent AnObjectProperty;

        void DoStuff();
    }

    runtimeclass SomeOtherComponent {
        // ...
    }
}
// MyComponent.h
namespace winrt::MyNamespace::implementation
{
    struct MyComponent : MyComponentT<MyComponent>
    {
        // ...

        wil::single_threaded_rw_property<winrt::hstring> ReadWriteProperty = L"Default value";

        // Can default-initialize, too:
        wil::single_threaded_property<bool> ReadableProperty{};

        // Make sure to use the projection type, not the implementation type.
        //
        // If we used `<SomeOtherComponent>`, we'd get the implementation type,
        // `winrt::MyNamespace::implementation::SomeOtherComponent`, which
        // wouldn't compile.
        wil::single_threaded_rw_property<winrt::MyNamespace::SomeOtherComponent> AnObjectProperty{ nullptr };

        void DoStuff()
        {
            // Set the properties
            ReadWriteProperty = L"A new value";
            ReadableProperty = true;
            AnObjectProperty = winrt::make<SomeOtherComponent>();

            // Only the rw properties expose the C++/WinRT "setter" syntax
            ReadWriteProperty(L"A newer value");
            // ReadableProperty(false); // won't compile
            AnObjectProperty(winrt::make<SomeOtherComponent>());

            // Read the properties
            if (ReadableProperty == false) {}
            if (ReadWriteProperty != L"Foo") {}
            AnObjectProperty.SomeMethod();

            // C++/WinRT "getter" syntax
            if (ReadableProperty() == false) {}
            if (ReadWriteProperty() != L"Foo") {}
            AnObjectProperty().SomeMethod();
        }
    };
}
// Other code

// Other code can use these properties as exposed via the IDL

const auto instance = winrt::make<MyComponent>();

// Read
const auto val = instance.ReadWriteProperty();
const bool anotherVal = instance.ReadableProperty();
const auto objVal = instance.AnObjectProperty();

// Write - can only write to rw exposed via the IDL.
//
// NOTE: if your IDL specifies just `{get;}` (read-only), then external code
// will not be able to set the property, even if you implemented the property
// with `wil::single_threaded_rw_property<T>`. Externally writeable properties
// must be settable in the IDL.
instance.ReadWriteProperty(L"Hello!");
// instance.ReadableProperty(false); // fails to compile
instance.AnObjectProperty(nullptr);

// No other methods are exposed externally for these properties.

Notifying properties

// Bar.idl
runtimeclass Bar : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
{
    // 2 properties to demonstrate the 2 syntaxes for declaring notifying properties:
    String MacroThingy;
    String PropertyThingy;

    void DoStuff();
}
// Bar.h

// Inherit from wil::notify_property_changed_base<T> to automatically implement
// INotifyPropertyChanged and add the RaisePropertyChanged(winrt::hstring const&) helper.
struct Bar : BarT<Bar>, wil::notify_property_changed_base<Bar>
{
    // One-liner macro, no initialization required:
    WIL_NOTIFYING_PROPERTY(hstring, MacroThingy, L"Hello!");

    // Must be initialized in the constructor:
    wil::single_threaded_notifying_property<hstring> PropertyThingy;
    Bar() : INIT_NOTIFYING_PROPERTY(PropertyThingy, L"This works too")
    {}

    void DoStuff()
    {
        // Fires change events automatically
        MacroThingy(L"A new value");
        PropertyThingy(L"Change is the only constant");

        // wil::single_threaded_notifying_property has operator=
        // that also fires change events
        PropertyThingy = L"So powerful, so majestic";
        // MacroThingy = L"This won't compile";

        // You can set the value without firing events
        m_MacroThingy = L"Ssh, be vewy quiet";
        //PropertyThingy.m_value = L"Nobody's gonna know"; // Does not work

        // And you can always fire events manually
        RaisePropertyChanged(L"MacroThingy");
        RaisePropertyChanged(PropertyThingy.Name());
    }
}

Notifying properties (INotifyPropertyChanged)

In C++/WinRT, classes and ViewModels often implement observable properties via the INotifyPropertyChanged interface. Simply put, the INotifyPropertyChanged interface specifies an event called PropertyChanged, which you should fire whenever any property in your class changes. Other code (including XAML x:Bind) can listen to this event and update UI accordingly.

Thus, implementing observable properties requires 2 distinct steps:

  1. Your class must inherit from Microsoft.UI.Xaml.Data.INotifyPropertyChanged (for WinUI 3 apps) or Windows.UI.Xaml.Data.INotifyPropertyChanged (for UWP apps) and implement the PropertyChanged event.
  2. Whenever you change a property, you must fire PropertyChanged.

WIL includes helpers for both steps.

Note: You must import winrt/Microsoft.UI.Xaml.Data.INotifyPropertyChanged.h or winrt/Windows.UI.Xaml.Data.INotifyPropertyChanged.h to have access to these helpers.

  • If you are writing a WinUI 3 project (eg, if your controls are in the Microsoft namespace), use winrt/Microsoft.UI.Xaml.Data.INotifyPropertyChanged.h
  • If you are writing a UWP project (eg, if your controls are in the Windows namespace), use winrt/Windows.UI.Xaml.Data.INotifyPropertyChanged.h

You will get weird compilation errors otherwise.

Implement INotifyPropertyChanged with wil::notify_property_changed_base<T>

wil::notify_property_changed_base<T> is a base class that automatically implements INotifyPropertyChanged, exposing the internal method RaisePropertyChanged(winrt::hstring const& property).

For example, if you have a runtimeclass in IDL:

runtimeclass Foo : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
{
    // ...
}

You can inherit from wil::notify_property_changed_base<T> (where T is the class itself, see CRTP) to automatically implement that interface:

struct Foo : FooT<Foo>, wil::notify_property_changed_base<Foo>
{
    // ... I can call RaisePropertyChanged(L"Bar") in my class ...
}

Firing PropertyChanged with WIL_NOTIFYING_PROPERTY and wil::single_threaded_notifying_property<T>

Once you inherit from wil::notify_proprety_changed_base<T>, you have multiple options for implementing notifying properties:

// 0. Call `RaisePropertyChanged(winrt::hstring const&)` manually
m_somePropertyValue = L"changed";
RaisePropertyChanged(L"SomePropertyValue");

// 1. Declare your properties with WIL_NOTIFYING_PROPERTY
// in h:
WIL_NOTIFYING_PROPERTY(winrt::hstring, MyCoolMacroProperty, L"Default value");
// in cpp:
MyCoolMacroProperty = L"New value"; // automatically fires property changed

// 2. Declare your properties with wil::single_threaded_notifying_property<T>
// in h:
wil::single_threaded_notifying_property<winrt::hstring> MyCoolObjectProperty;
MyConstructor() : INIT_NOTIFYING_PROPERTY(MyCoolObjectProperty, L"This works too")
{} // Must be initialized in the constructor
// in cpp
MyCoolObjectProperty = L"Another value"; // automatically fires property changed

See examples above. See also XAML controls; bind to a C++/WinRT property for a plain C++ implementation.

Events API

WIL also offers handlers for implementing events.

Handler type Example IDL
wil::untyped_event<T> Windows::Foundation::EventHandler<T> event Windows.Foundation.EventHandler<String> UriFetched;
wil::typed_event<TSender, TArgs> Windows::Foundation::TypedEventHandler<TSender, TArgs> event Windows.Foundation.TypedEventHandler<ModalPage, String> OkClicked;

See also Author events in C++/WinRT.

Note that other event types are not supported at this time (e.g. Windows.UI.Xaml.Data.CurrentChangingEventHandler). See https://github.com/microsoft/wil/issues/354.

Examples

// EventComponent.idl
runtimeclass ComplexEventArgs {
    // ...
}

runtimeclass EventComponent
{
    event Windows.Foundation.EventHandler<String> SimpleEvent;
    event Windows.Foundation.TypedEventHandler<EventComponent, ComplexEventArgs> ComplexEvent;

    void DoStuff();
}
// EventComponent.h
struct EventComponent : EventComponentT<EventComponent>
{
    wil::untyped_event<winrt::hstring> SimpleEvent;
    wil::typed_event<winrt::MyNamespace::EventComponent, winrt::MyNamespace::ComplexEventArgs> ComplexEvent;

    void DoStuff()
    {
        // Fire the events!
        SimpleEvent.invoke(*this, L"Args");

        const auto args = winrt::make<ComplexEventArgs>();
        ComplexEvent.invoke(*this, args);
    }
}
// Other code

// Other code can register for events, just like any other C++/WinRT code
// See https://learn.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/handle-events

const auto instance = winrt::make<EventComponent>(); // Or external code can simply call `EventComponent{}`

// Register & unregister
const auto token = instance.SimpleEvent(
    [](winrt::Windows::Foundation::IInspectable const& sender, winrt::hstring const& args)
    {});
instance.SimpleEvent(token);

const auto token2 = instance.ComplexEvent(
    [](winrt::MyNamespace::EventComponent const& sender, winrt::MyNamespace::ComplexEventArgs const& args)
    {});
instance.ComplexEvent(token2);

// Auto-revoker
const auto autoRevoker = instance.SimpleEvent(winrt::auto_revoke,
    [](winrt::Windows::Foundation::IInspectable const& sender, winrt::hstring const& args) {});
const auto autoRevoker = instance.ComplexEvent(winrt::auto_revoke, [](auto sender, auto args) {});