Menulis peristiwa di C++/WinRT

Topik ini dibangun pada komponen Windows Runtime, dan aplikasi yang mengonsumsi, bahwa komponen Windows Runtime dengan topik C++/WinRT menunjukkan kepada Anda cara membangun.

Berikut adalah fitur baru yang ditambahkan topik ini.

  • Perbarui kelas runtime termometer untuk menaikkan peristiwa ketika suhunya berada di bawah pembekuan.
  • Perbarui Aplikasi Inti yang menggunakan kelas runtime termometer sehingga menangani peristiwa tersebut.

Catatan

Untuk informasi tentang menginstal dan menggunakan C++/WinRT Visual Studio Extension (VSIX) dan paket NuGet (yang bersama-sama menyediakan templat proyek dan dukungan build), lihat Dukungan Visual Studio untuk C++/WinRT.

Penting

Untuk konsep dan istilah penting yang mendukung pemahaman Anda tentang cara menggunakan dan menulis kelas runtime dengan C++/WinRT, lihat Menggunakan API dengan C++/WinRT dan API Penulis dengan C++/WinRT.

Membuat ThermometerWRC dan ThermometerCoreApp

Jika Anda ingin mengikuti pembaruan yang ditampilkan dalam topik ini, sehingga Anda dapat membangun dan menjalankan kode, maka langkah pertama adalah mengikuti panduan di komponen Windows Runtime dengan topik C++/WinRT . Dengan demikian, Anda akan memiliki komponen ThermometerWRC Windows Runtime, dan Aplikasi ThermometerCoreApp Core yang mengonsumsinya.

Memperbarui ThermometerWRC untuk menaikkan peristiwa

Perbarui Thermometer.idl agar terlihat seperti daftar di bawah ini. Ini adalah cara mendeklarasikan peristiwa yang jenis delegasinya adalah EventHandler dengan argumen angka floating-point presisi tunggal.

// Thermometer.idl
namespace ThermometerWRC
{
    runtimeclass Thermometer
    {
        Thermometer();
        void AdjustTemperature(Single deltaFahrenheit);
        event Windows.Foundation.EventHandler<Single> TemperatureIsBelowFreezing;
    };
}

Simpan file. Proyek tidak akan dibangun hingga selesai dalam statusnya saat ini, tetapi lakukan build sekarang dalam hal apa pun untuk menghasilkan versi file stub dan Thermometer.cpp yang diperbarui\ThermometerWRC\ThermometerWRC\Generated Files\sources\Thermometer.h. Di dalam file tersebut, Anda sekarang dapat melihat implementasi stub dari peristiwa TemperatureIsBelowFreezing . Dalam C++/WinRT, peristiwa yang dideklarasikan IDL diimplementasikan sebagai serangkaian fungsi yang kelebihan beban (mirip dengan cara properti diimplementasikan sebagai sepasang fungsi get dan set yang kelebihan beban). Satu kelebihan beban mengambil delegasi untuk didaftarkan, dan mengembalikan token ( winrt::event_token). Yang lain mengambil token, dan mencabut pendaftaran delegasi terkait.

Sekarang buka Thermometer.h dan Thermometer.cpp, dan perbarui implementasi kelas runtime Thermometer . Dalam Thermometer.h, tambahkan dua fungsi TemperatureIsBelowFreezing yang kelebihan beban, serta anggota data peristiwa privat untuk digunakan dalam implementasi fungsi tersebut.

// Thermometer.h
...
namespace winrt::ThermometerWRC::implementation
{
    struct Thermometer : ThermometerT<Thermometer>
    {
        ...
        winrt::event_token TemperatureIsBelowFreezing(Windows::Foundation::EventHandler<float> const& handler);
        void TemperatureIsBelowFreezing(winrt::event_token const& token) noexcept;

    private:
        winrt::event<Windows::Foundation::EventHandler<float>> m_temperatureIsBelowFreezingEvent;
        ...
    };
}
...

Seperti yang Anda lihat di atas, peristiwa diwakili oleh templat winrt::event struct, diparameterkan oleh jenis delegasi tertentu (yang sendiri dapat diparameterkan oleh jenis args).

Dalam Thermometer.cpp, terapkan dua fungsi TemperatureIsBelowFreezing yang kelebihan beban.

// Thermometer.cpp
...
namespace winrt::ThermometerWRC::implementation
{
    winrt::event_token Thermometer::TemperatureIsBelowFreezing(Windows::Foundation::EventHandler<float> const& handler)
    {
        return m_temperatureIsBelowFreezingEvent.add(handler);
    }

    void Thermometer::TemperatureIsBelowFreezing(winrt::event_token const& token) noexcept
    {
        m_temperatureIsBelowFreezingEvent.remove(token);
    }

    void Thermometer::AdjustTemperature(float deltaFahrenheit)
    {
        m_temperatureFahrenheit += deltaFahrenheit;
        if (m_temperatureFahrenheit < 32.f) m_temperatureIsBelowFreezingEvent(*this, m_temperatureFahrenheit);
    }
}

Catatan

Untuk detail tentang apa itu pencabut peristiwa otomatis, lihat Mencabut delegasi terdaftar. Anda mendapatkan implementasi pencabut peristiwa otomatis secara gratis untuk acara Anda. Dengan kata lain, Anda tidak perlu menerapkan kelebihan beban untuk pencabut peristiwa—yang disediakan untuk Anda oleh proyeksi C++/WinRT.

Kelebihan beban lainnya (pendaftaran dan pencabutan manual kelebihan beban) tidak dipanggang ke dalam proyeksi. Itu untuk memberi Anda fleksibilitas untuk mengimplementasikannya secara optimal untuk skenario Anda. Memanggil event::add and event::remove seperti yang ditunjukkan dalam implementasi ini adalah default yang efisien dan konkurensi/thread-safe. Tetapi jika Anda memiliki sejumlah besar peristiwa, maka Anda mungkin tidak ingin bidang peristiwa untuk masing-masing, melainkan memilih semacam implementasi jarang sebagai gantinya.

Anda juga dapat melihat di atas bahwa implementasi fungsi AdjustTemperature telah diperbarui untuk meningkatkan peristiwa TemperatureIsBelowFreezing jika suhu berada di bawah pembekuan.

Perbarui ThermometerCoreApp untuk menangani peristiwa

Dalam proyek ThermometerCoreApp, di App.cpp, buat perubahan berikut pada kode untuk mendaftarkan penanganan aktivitas, lalu menyebabkan suhu berada di bawah pembekuan.

WINRT_ASSERT adalah definisi makro, dan meluas ke _ASSERTE.

struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
    winrt::event_token m_eventToken;
    ...
    
    void Initialize(CoreApplicationView const &)
    {
        m_eventToken = m_thermometer.TemperatureIsBelowFreezing([](const auto &, float temperatureFahrenheit)
        {
            WINRT_ASSERT(temperatureFahrenheit < 32.f); // Put a breakpoint here.
        });
    }
    ...

    void Uninitialize()
    {
        m_thermometer.TemperatureIsBelowFreezing(m_eventToken);
    }
    ...
    
    void OnPointerPressed(IInspectable const &, PointerEventArgs const & args)
    {
        m_thermometer.AdjustTemperature(-1.f);
        ...
    }
    ...
};

Waspadai perubahan pada metode OnPointerPressed . Sekarang, setiap kali Anda mengklik jendela, Anda mengurangi 1 derajat Fahrenheit dari suhu termometer. Dan sekarang, aplikasi menangani peristiwa yang dinaikkan ketika suhu di bawah pembekuan. Untuk menunjukkan bahwa peristiwa sedang dinaikkan seperti yang diharapkan, letakkan titik henti di dalam ekspresi lambda yang menangani peristiwa TemperatureIsBelowFreezing , jalankan aplikasi, dan klik di dalam jendela.

Delegasi berparameter di seluruh ABI

Jika peristiwa Anda harus dapat diakses di seluruh antarmuka biner aplikasi (ABI)—seperti antara komponen dan aplikasi yang menggunakannya—maka peristiwa Anda harus menggunakan jenis delegasi Windows Runtime. Contoh di atas menggunakan jenis delegasi Windows::Foundation::EventHandler<T> Windows Runtime. TypedEventHandler<TSender, TResult> adalah contoh lain dari jenis delegasi Windows Runtime.

Parameter jenis untuk kedua jenis delegasi tersebut harus melintasi ABI, sehingga parameter jenis harus jenis Windows Runtime juga. Itu termasuk kelas runtime Windows, kelas runtime pihak ketiga, dan jenis primitif seperti angka dan string. Pengkompilasi membantu Anda dengan kesalahan "T harus jenis WinRT" jika Anda lupa batasan tersebut.

Di bawah ini adalah contoh dalam bentuk daftar kode. Mulailah dengan proyek ThermometerWRC dan ThermometerCoreApp yang Anda buat sebelumnya dalam topik ini, dan edit kode dalam proyek tersebut agar terlihat seperti kode dalam daftar ini.

Daftar pertama ini untuk proyek ThermometerWRC . Setelah mengedit seperti yang ThermometerWRC.idl ditunjukkan di bawah ini, bangun proyek, lalu salin MyEventArgs.h dan .cpp ke dalam proyek (dari Generated Files folder) seperti yang Anda lakukan sebelumnya dengan Thermometer.h dan .cpp. Ingatlah untuk menghapus static_assert dari kedua file.

// ThermometerWRC.idl
namespace ThermometerWRC
{
    [default_interface]
    runtimeclass MyEventArgs
    {
        Single TemperatureFahrenheit{ get; };
    }

    [default_interface]
    runtimeclass Thermometer
    {
        ...
        event Windows.Foundation.EventHandler<ThermometerWRC.MyEventArgs> TemperatureIsBelowFreezing;
        ...
    };
}

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

namespace winrt::ThermometerWRC::implementation
{
    struct MyEventArgs : MyEventArgsT<MyEventArgs>
    {
        MyEventArgs() = default;
        MyEventArgs(float temperatureFahrenheit);
        float TemperatureFahrenheit();

    private:
        float m_temperatureFahrenheit{ 0.f };
    };
}

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

namespace winrt::ThermometerWRC::implementation
{
    MyEventArgs::MyEventArgs(float temperatureFahrenheit) : m_temperatureFahrenheit(temperatureFahrenheit)
    {
    }

    float MyEventArgs::TemperatureFahrenheit()
    {
        return m_temperatureFahrenheit;
    }
}

// Thermometer.h
...
struct Thermometer : ThermometerT<Thermometer>
{
...
    winrt::event_token TemperatureIsBelowFreezing(Windows::Foundation::EventHandler<ThermometerWRC::MyEventArgs> const& handler);
...
private:
    winrt::event<Windows::Foundation::EventHandler<ThermometerWRC::MyEventArgs>> m_temperatureIsBelowFreezingEvent;
...
}
...

// Thermometer.cpp
#include "MyEventArgs.h"
...
winrt::event_token Thermometer::TemperatureIsBelowFreezing(Windows::Foundation::EventHandler<ThermometerWRC::MyEventArgs> const& handler) { ... }
...
void Thermometer::AdjustTemperature(float deltaFahrenheit)
{
    m_temperatureFahrenheit += deltaFahrenheit;

    if (m_temperatureFahrenheit < 32.f)
    {
        auto args = winrt::make_self<winrt::ThermometerWRC::implementation::MyEventArgs>(m_temperatureFahrenheit);
        m_temperatureIsBelowFreezingEvent(*this, *args);
    }
}
...

Daftar ini untuk proyek ThermometerCoreApp .

// App.cpp
...
void Initialize(CoreApplicationView const&)
{
    m_eventToken = m_thermometer.TemperatureIsBelowFreezing([](const auto&, ThermometerWRC::MyEventArgs args)
    {
        float degrees = args.TemperatureFahrenheit();
        WINRT_ASSERT(degrees < 32.f); // Put a breakpoint here.
    });
}
...

Sinyal sederhana di seluruh ABI

Jika Anda tidak perlu meneruskan parameter atau argumen apa pun dengan peristiwa Anda, maka Anda dapat menentukan jenis delegasi Windows Runtime sederhana Anda sendiri. Contoh di bawah ini menunjukkan versi kelas runtime Thermometer yang lebih sederhana. Ini mendeklarasikan jenis delegasi bernama SignalDelegate dan kemudian menggunakannya untuk menaikkan peristiwa jenis sinyal alih-alih peristiwa dengan parameter.

// ThermometerWRC.idl
namespace ThermometerWRC
{
    delegate void SignalDelegate();

    runtimeclass Thermometer
    {
        Thermometer();
        event ThermometerWRC.SignalDelegate SignalTemperatureIsBelowFreezing;
        void AdjustTemperature(Single value);
    };
}
// Thermometer.h
...
namespace winrt::ThermometerWRC::implementation
{
    struct Thermometer : ThermometerT<Thermometer>
    {
        ...

        winrt::event_token SignalTemperatureIsBelowFreezing(ThermometerWRC::SignalDelegate const& handler);
        void SignalTemperatureIsBelowFreezing(winrt::event_token const& token);
        void AdjustTemperature(float deltaFahrenheit);

    private:
        winrt::event<ThermometerWRC::SignalDelegate> m_signal;
        float m_temperatureFahrenheit{ 0.f };
    };
}
// Thermometer.cpp
...
namespace winrt::ThermometerWRC::implementation
{
    winrt::event_token Thermometer::SignalTemperatureIsBelowFreezing(ThermometerWRC::SignalDelegate const& handler)
    {
        return m_signal.add(handler);
    }

    void Thermometer::SignalTemperatureIsBelowFreezing(winrt::event_token const& token)
    {
        m_signal.remove(token);
    }

    void Thermometer::AdjustTemperature(float deltaFahrenheit)
    {
        m_temperatureFahrenheit += deltaFahrenheit;
        if (m_temperatureFahrenheit < 32.f)
        {
            m_signal();
        }
    }
}
// App.cpp
struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
    ThermometerWRC::Thermometer m_thermometer;
    winrt::event_token m_eventToken;
    ...
    
    void Initialize(CoreApplicationView const &)
    {
        m_eventToken = m_thermometer.SignalTemperatureIsBelowFreezing([] { /* ... */ });
    }
    ...

    void Uninitialize()
    {
        m_thermometer.SignalTemperatureIsBelowFreezing(m_eventToken);
    }
    ...

    void OnPointerPressed(IInspectable const &, PointerEventArgs const & args)
    {
        m_thermometer.AdjustTemperature(-1.f);
        ...
    }
    ...
};

Delegasi berparameter, sinyal sederhana, dan panggilan balik dalam proyek

Jika Anda memerlukan peristiwa yang bersifat internal untuk proyek Visual Studio Anda (tidak di seluruh biner), di mana peristiwa tersebut tidak terbatas pada jenis Windows Runtime, maka Anda masih dapat menggunakan templat kelas Winrt::event<Delegasikan>. Cukup gunakan winrt::d elegate alih-alih jenis delegasi Windows Runtime yang sebenarnya, karena winrt::d elegate juga mendukung parameter Non Windows Runtime.

Contoh di bawah ini pertama-tama menunjukkan tanda tangan delegasi yang tidak mengambil parameter apa pun (pada dasarnya sinyal sederhana), lalu yang mengambil string.

winrt::event<winrt::delegate<>> signal;
signal.add([] { std::wcout << L"Hello, "; });
signal.add([] { std::wcout << L"World!" << std::endl; });
signal();

winrt::event<winrt::delegate<std::wstring>> log;
log.add([](std::wstring const& message) { std::wcout << message.c_str() << std::endl; });
log.add([](std::wstring const& message) { Persist(message); });
log(L"Hello, World!");

Perhatikan bagaimana Anda dapat menambahkan ke acara sebanyak mungkin delegasi yang berlangganan sesuai keinginan Anda. Namun, ada beberapa overhead yang terkait dengan suatu peristiwa. Jika yang Anda butuhkan hanyalah panggilan balik sederhana dengan hanya satu delegasi berlangganan, maka Anda dapat menggunakan winrt::d elegate<... T> sendirian.

winrt::delegate<> signalCallback;
signalCallback = [] { std::wcout << L"Hello, World!" << std::endl; };
signalCallback();

winrt::delegate<std::wstring> logCallback;
logCallback = [](std::wstring const& message) { std::wcout << message.c_str() << std::endl; }f;
logCallback(L"Hello, World!");

Jika Anda melakukan porting dari basis kode C++/CX tempat peristiwa dan delegasi digunakan secara internal dalam proyek, maka winrt::d elegate akan membantu Anda mereplikasi pola tersebut di C++/WinRT.

Peristiwa yang dapat ditangguhkan

Pola umum dalam Windows Runtime adalah peristiwa yang dapat ditangguhkan. Penanganan aktivitas mengambil penahanan dengan memanggil metode GetDeferral argumen peristiwa. Melakukannya menunjukkan kepada sumber kejadian bahwa aktivitas pasca-peristiwa harus ditunda hingga penangguhkan selesai. Hal ini memungkinkan penanganan aktivitas untuk melakukan tindakan asinkron sebagai respons terhadap suatu peristiwa.

Templat struct winrt::d eferrable_event_args adalah kelas pembantu untuk menerapkan (memproduksi) pola deferral Windows Runtime. Berikut adalah contoh.

// Widget.idl
namespace Sample
{
    runtimeclass WidgetStartingEventArgs
    {
        Windows.Foundation.Deferral GetDeferral();
        Boolean Cancel;
    };

    runtimeclass Widget
    {
        event Windows.Foundation.TypedEventHandler<
            Widget, WidgetStartingEventArgs> Starting;
    };
}

// Widget.h
namespace winrt::Sample::implementation
{
    struct Widget : WidgetT<Widget>
    {
        Widget() = default;

        event_token Starting(Windows::Foundation::TypedEventHandler<
            Sample::Widget, Sample::WidgetStartingEventArgs> const& handler)
        {
            return m_starting.add(handler);
        }
        void Starting(event_token const& token) noexcept
        {
            m_starting.remove(token);
        }

    private:
        event<Windows::Foundation::TypedEventHandler<
            Sample::Widget, Sample::WidgetStartingEventArgs>> m_starting;
    };

    struct WidgetStartingEventArgs : WidgetStartingEventArgsT<WidgetStartingEventArgs>,
                                     deferrable_event_args<WidgetStartingEventArgs>
    //                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    {
        bool Cancel() const noexcept { return m_cancel; }
        void Cancel(bool value) noexcept { m_cancel = value; }
        bool m_cancel = false;
    };
}

Berikut adalah cara penerima peristiwa menggunakan pola peristiwa yang dapat ditangguhkan.

// EventRecipient.h
widget.Starting([](auto sender, auto args) -> fire_and_forget
{
    auto deferral = args.GetDeferral();
    if (!co_await CanWidgetStartAsync(sender))
    {
        // Do not allow the widget to start.
        args.Cancel(true);
    }
    deferral.Complete();
});

Sebagai implementor (produser) sumber kejadian, Anda memperoleh kelas event args anda dari winrt::d eferrable_event_args. <deferrable_event_args T> mengimplementasikan T::GetDeferral untuk Anda. Ini juga mengekspos metode pembantu baru deferrable_event_args::wait_for_deferrals, yang selesai ketika semua penundaan yang luar biasa telah selesai (jika tidak ada penundaan yang diambil, maka segera selesai).

// Widget.h
IAsyncOperation<bool> TryStartWidget(Widget const& widget)
{
    auto args = make_self<WidgetStartingEventArgs>();
    // Raise the event to let people know that the widget is starting
    // and give them a chance to prevent it.
    m_starting(widget, *args);
    // Wait for deferrals to complete.
    co_await args->wait_for_deferrals();
    // Use the results.
    bool started = false;
    if (!args->Cancel())
    {
        widget.InsertBattery();
        widget.FlipPowerSwitch();
        started = true;
    }
    co_return started;
}

Pedoman desain

Kami menyarankan agar Anda meneruskan peristiwa, dan bukan delegasi, sebagai parameter fungsi. Fungsi add winrt ::event adalah satu pengecualian, karena Anda harus meneruskan delegasi dalam kasus tersebut. Alasan untuk pedoman ini adalah karena delegasi dapat mengambil formulir yang berbeda di berbagai bahasa Windows Runtime (dalam hal apakah mereka mendukung satu pendaftaran klien, atau beberapa). Peristiwa, dengan model beberapa pelanggan mereka, merupakan opsi yang jauh lebih dapat diprediksi dan konsisten.

Tanda tangan untuk delegasi penanganan aktivitas harus terdiri dari dua parameter: pengirim (IInspectable), dan args (beberapa jenis argumen peristiwa, misalnya RoutedEventArgs).

Perhatikan bahwa panduan ini tidak selalu berlaku jika Anda merancang API internal. Meskipun, API internal sering menjadi publik dari waktu ke waktu.