Gestire eventi mediante i delegati in C++/WinRT

Questo argomento illustra come registrare e revocare delegati per la gestione degli eventi con C++/WinRT. Puoi gestire un evento con qualsiasi oggetto di tipo funzione C++ standard.

Nota

Per informazioni sull'installazione e sull'uso dell'Estensione C++/WinRT per Visual Studio (VSIX) e del pacchetto NuGet, che insieme forniscono il modello di progetto e il supporto della compilazione, vedi Supporto di Visual Studio per C++/WinRT.

Uso di Visual Studio per aggiungere un gestore eventi

Un modo pratico per aggiungere un gestore eventi al progetto consiste nell'usare l'interfaccia di utente (UI) della finestra di progettazione XAML in Visual Studio. Con la pagina XAML aperta nella finestra di progettazione XAML, seleziona il controllo di cui vuoi gestire l'evento. Nella pagina delle proprietà del controllo, fai clic sull'icona del lampo per elencare tutti gli eventi che hanno origine da tale controllo. Fai quindi doppio clic sull'evento che vuoi gestire, ad esempio OnClicked.

La finestra di progettazione XAML aggiunge il prototipo di funzione del gestore eventi appropriato (e un'implementazione dello stub) per i file di origine, che puoi sostituire subito con la tua implementazione.

Nota

In genere non è necessario che i gestori eventi siano descritti nel file Midl (.idl). Pertanto, la finestra di progettazione XAML non aggiunge prototipi di funzione del gestore eventi nel file Midl. Li aggiunge solo nei file .h e .cpp.

Registrare un delegato per gestire un evento

Un esempio molto semplice consiste nella gestione dell'evento Click di un pulsante. Capita spesso di usare markup XAML per registrare una funzione membro allo scopo di gestire un evento come quello che segue.

// MainPage.xaml
<Button x:Name="myButton" Click="ClickHandler">Click Me</Button>
// MainPage.h
void ClickHandler(
    winrt::Windows::Foundation::IInspectable const& sender,
    winrt::Windows::UI::Xaml::RoutedEventArgs const& args);

// MainPage.cpp
void MainPage::ClickHandler(
    IInspectable const& /* sender */,
    RoutedEventArgs const& /* args */)
{
    myButton().Content(box_value(L"Clicked"));
}

Il codice precedente è tratto dal progetto App vuota (C++/WinRT) in Visual Studio. Il codice myButton() chiama una funzione di accesso generata, che restituisce l'elemento Button denominato MyButton. Se si modifica x:Name nell'elemento Button, viene modificato anche il nome della funzione di accesso generata.

Nota

In questo caso, l'origine evento, ovvero l'oggetto che genera l'evento, è l'elemento Button denominato myButton. E il destinatario dell'evento, ovvero l'oggetto che gestisce l'evento, è un'istanza di MainPage. Più avanti in questo argomento sono disponibili altre informazioni sulla gestione della durata delle origini eventi e dei destinatari degli eventi.

Anziché procedere in modo dichiarativo nel markup, puoi registrare in modo imperativo una funzione membro per gestire un evento. Potrebbe non essere evidente dal codice riportato di seguito, ma l'argomento per la chiamata ButtonBase::Click è un'istanza del delegato RoutedEventHandler. In questo caso viene usato l'overload del costruttore RoutedEventHandler che accetta un oggetto e un puntatore alla funzione membro.

// MainPage.cpp
MainPage::MainPage()
{
    InitializeComponent();

    myButton().Click({ this, &MainPage::ClickHandler });
}

Importante

Quando registri il delegato, l'esempio di codice precedente passa un puntatore non elaborato this che punta all'oggetto corrente. Per informazioni su come definire un riferimento sicuro o debole all'oggetto corrente, vedi Se usi una funzione membro come delegato.

Di seguito è riportato un esempio che usa una funzione membro statica. Si noti la sintassi più semplice.

// MainPage.h
static void ClickHandler(
    winrt::Windows::Foundation::IInspectable const& sender,
    winrt::Windows::UI::Xaml::RoutedEventArgs const& args);

// MainPage.cpp
MainPage::MainPage()
{
    InitializeComponent();

    myButton().Click( MainPage::ClickHandler );
}
void MainPage::ClickHandler(
    IInspectable const& /* sender */,
    RoutedEventArgs const& /* args */) { ... }

Esistono tuttavia altri modi per costruire un RoutedEventHandler. Di seguito è riportato il blocco di sintassi estratto dall'argomento della documentazione per RoutedEventHandler (scegli C++/WinRT dall'elenco a discesa Linguaggio nell'angolo in alto a destra della pagina Web). Osserva i vari costruttori: uno accetta una funzione lambda, un altro una funzione libera, un altro ancora (quello utilizzato sopra) un oggetto e un puntatore alla funzione membro.

struct RoutedEventHandler : winrt::Windows::Foundation::IUnknown
{
    RoutedEventHandler(std::nullptr_t = nullptr) noexcept;
    template <typename L> RoutedEventHandler(L lambda);
    template <typename F> RoutedEventHandler(F* function);
    template <typename O, typename M> RoutedEventHandler(O* object, M method);
    /* ... other constructors ... */
    void operator()(winrt::Windows::Foundation::IInspectable const& sender,
        winrt::Windows::UI::Xaml::RoutedEventArgs const& e) const;
};

È utile vedere anche la sintassi dell'operatore di chiamata di funzione, che indica quali debbano essere i parametri del delegato. Come appare evidente, in questo caso la sintassi dell'operatore di chiamata di funzione corrisponde ai parametri del nostro MainPage::ClickHandler.

Nota

Per qualsiasi evento, per determinare i dettagli del relativo delegato e i parametri del delegato, vedi prima l'argomento della documentazione per l'evento stesso. Vediamo come esempio l'evento UIElement.KeyDown. Accedi all'argomento e scegli C++/WinRT nell'elenco a discesa Linguaggio. Nel blocco di sintassi all'inizio dell'argomento saranno visualizzate queste informazioni.

// Register
event_token KeyDown(KeyEventHandler const& handler) const;

Le informazioni indicano che l'evento UIElement.KeyDown, l'argomento corrente, ha il tipo delegato KeyEventHandler, dal momento che quello è il tipo passato quando si registra un delegato con questo tipo di evento. Ora fai clic sul collegamento relativo al tipo delegato KeyEventHandler. Qui il blocco di sintassi contiene un operatore di chiamata di funzione. Inoltre, come menzionato sopra, esso indica quali debbano essere i parametri del delegato.

void operator()(
  winrt::Windows::Foundation::IInspectable const& sender,
  winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) const;

Come puoi notare, il delegato deve essere dichiarato in modo tale che accetti un IInspectable come mittente e un'istanza della classe KeyRoutedEventArgs come argomenti.

Per un altro esempio, esaminiamo l'evento Popup.Closed. Il suo tipo di delegato è EventHandler<IInspectable>. Pertanto il delegato accetterà un IInspectable come mittente e un altro IInspectable (perché questo è il parametro di tipo di EventHandler) come argomenti.

Se non esegui molte attività nel gestore eventi, puoi usare una funzione lambda anziché una funzione membro. Anche in questo caso potrebbe non risultare ovvio dall'esempio di codice seguente, ma un delegato RoutedEventHandler viene creato da una funzione lambda che, di nuovo, deve corrispondere alla sintassi dell'operatore di chiamata di funzione trattato sopra.

MainPage::MainPage()
{
    InitializeComponent();

    myButton().Click([this](IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
    {
        myButton().Content(box_value(L"Clicked"));
    });
}

Quando crei il delegato, puoi scegliere di essere un po' più esplicito. Ad esempio, se desideri passarlo o usarlo più volte.

MainPage::MainPage()
{
    InitializeComponent();

    auto click_handler = [](IInspectable const& sender, RoutedEventArgs const& /* args */)
    {
        sender.as<winrt::Windows::UI::Xaml::Controls::Button>().Content(box_value(L"Clicked"));
    };
    myButton().Click(click_handler);
    AnotherButton().Click(click_handler);
}

Revocare un delegato registrato

Quando registri un delegato, ti viene generalmente restituito un token. Successivamente puoi usare tale token per revocare il delegato. Questo significa che la registrazione del delegato all'evento viene annullata e che, anche qualora venisse generato nuovamente l'evento, il delegato non verrà chiamato.

Per semplicità, nessuno degli esempi di codice precedenti illustra come eseguire questa operazione. Il prossimo esempio di codice invece archivia il token nel membro di dati privati dello struct e revoca il relativo gestore nel distruttore.

struct Example : ExampleT<Example>
{
    Example(winrt::Windows::UI::Xaml::Controls::Button const& button) : m_button(button)
    {
        m_token = m_button.Click([this](IInspectable const&, RoutedEventArgs const&)
        {
            // ...
        });
    }
    ~Example()
    {
        m_button.Click(m_token);
    }

private:
    winrt::Windows::UI::Xaml::Controls::Button m_button;
    winrt::event_token m_token;
};

Invece di un riferimento sicuro, come nell'esempio precedente, puoi archiviare un riferimento debole al pulsante (vedi Riferimenti sicuri e deboli in C++/WinRT).

Nota

Quando un'origine genera gli eventi in modo sincrono, puoi revocare il gestore e avere la certezza che non riceverai altri eventi. Per gli eventi asincroni, tuttavia, anche dopo la revoca (e in particolare quando la revoca viene eseguita all'interno del distruttore), è possibile che un evento in corso raggiunga l'oggetto dopo l'avvio del processo di distruzione. L'identificazione di una posizione in cui annullare la sottoscrizione prima della distruzione può attenuare il problema, ma vedi Accesso sicuro al puntatore this con un delegato di gestione degli eventi per una soluzione solida e affidabile.

In alternativa, quando registri un delegato, puoi specificare winrt::auto_revoke, ovvero un valore di tipo winrt::auto_revoke_t, per richiedere un revocatore di eventi (di tipo winrt::event_revoker). Il revocatore di eventi mantiene un riferimento debole all'origine dell'evento (l'oggetto che genera l'evento) automaticamente. Puoi eseguire la revoca manualmente chiamando la funzione membro event_revoker::revoke, tuttavia il revocatore eventi chiama automaticamente la funzione quando esce dall'ambito. La funzione revoke verifica se l'origine evento esiste e, in caso affermativo, revoca il delegato. In questo esempio non c'è alcuna necessità di archiviare l'origine evento né di richiedere un distruttore.

struct Example : ExampleT<Example>
{
    Example(winrt::Windows::UI::Xaml::Controls::Button button)
    {
        m_event_revoker = button.Click(
            winrt::auto_revoke,
            [this](IInspectable const& /* sender */,
            RoutedEventArgs const& /* args */)
        {
            // ...
        });
    }

private:
    winrt::Windows::UI::Xaml::Controls::Button::Click_revoker m_event_revoker;
};

Di seguito è riportato il blocco di sintassi estratto dall'argomento della documentazione per l'evento ButtonBase::Click. Mostra le tre diverse funzioni di registrazione e revoca. Puoi vedere esattamente il tipo di revocatore di eventi che devi dichiarare dal terzo overload. Inoltre, è possibile passare gli stessi tipi di delegati agli overload register e revoke with event_revoker.

// Register
winrt::event_token Click(winrt::Windows::UI::Xaml::RoutedEventHandler const& handler) const;

// Revoke with event_token
void Click(winrt::event_token const& token) const;

// Revoke with event_revoker
Button::Click_revoker Click(winrt::auto_revoke_t,
    winrt::Windows::UI::Xaml::RoutedEventHandler const& handler) const;

Nota

Nell'esempio di codice precedente Button::Click_revoker è un alias di tipo per winrt::event_revoker<winrt::Windows::UI::Xaml::Controls::Primitives::IButtonBase>. Un modello simile si applica a tutti gli eventi C++/WinRT. Ogni evento di Windows Runtime ha un overload della funzione revoke che restituisce un revocatore di eventi e il tipo del revocatore è un membro dell'origine evento. Per un altro esempio, l'evento CoreWindow::SizeChanged ha un overload di funzione di registrazione che restituisce un valore di tipo CoreWindow::SizeChanged_revoker.

Puoi valutare la possibilità di revocare gestori durante lo spostamento su una pagina. Se esplori a più riprese una pagina e torni indietro, puoi revocare eventuali gestori al momento dell'uscita. In alternativa, se riutilizzi la stessa istanza di pagina, controlla il valore del tuo token ed esegui la registrazione solo se non è ancora stato impostato (if (!m_token){ ... }). Una terza opzione consiste nell'archiviare un revocatore di eventi nella pagina come membro dati. Una quarta opzione, come descritto più avanti in questo argomento, consiste nell'acquisire un riferimento sicuro o debole all'oggetto this nella funzione lambda.

Se la registrazione del delegato di revoca automatica ha esito negativo

Se provi a specificare winrt::auto_revoke durante la registrazione di un delegato e viene generata un'eccezione winrt::hresult_no_interface, ciò significa in genere che l'origine dell'evento non supporta riferimenti deboli. Questa è una situazione comune, ad esempio nello spazio dei nomi Windows.UI.Composition. In una situazione di questo tipo non puoi usare la funzionalità di revoca automatica. Dovrai eseguire il fallback e revocare manualmente i gestori eventi.

Tipi di delegati per operazioni e azioni asincrone

Gli esempi precedenti usano il tipo di delegato RoutedEventHandler, ma esistono molti altri tipi di delegati. Ad esempio, operazioni e azioni asincrone (con e senza stato) hanno eventi di completamento e/o avanzamento che prevedono delegati del tipo corrispondente. Ad esempio, l'evento di avanzamento di un'operazione asincrona con stato, ovvero tutto ciò che implementa IAsyncOperationWithProgress, richiede un delegato di tipo AsyncOperationProgressHandler. Di seguito è riportato un esempio di codice per la creazione di un delegato di tale tipo mediante una funzione lambda. L'esempio illustra anche come creare un delegato AsyncOperationWithProgressCompletedHandler.

#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;

    auto async_op_with_progress = syndicationClient.RetrieveFeedAsync(rssFeedUri);

    async_op_with_progress.Progress(
        [](
            IAsyncOperationWithProgress<SyndicationFeed,
            RetrievalProgress> const& /* sender */,
            RetrievalProgress const& args)
        {
            uint32_t bytes_retrieved = args.BytesRetrieved;
            // use bytes_retrieved;
        });

    async_op_with_progress.Completed(
        [](
            IAsyncOperationWithProgress<SyndicationFeed,
            RetrievalProgress> const& sender,
            AsyncStatus const /* asyncStatus */)
        {
            SyndicationFeed syndicationFeed = sender.GetResults();
            // use syndicationFeed;
        });

    // or (but this function must then be a coroutine, and return IAsyncAction)
    // SyndicationFeed syndicationFeed{ co_await async_op_with_progress };
}

Come suggerisce il commento "coroutine" precedente, anziché usare un delegato con gli eventi di completamento di operazioni e azioni asincrone, potresti trovare più naturale usare coroutine. Per informazioni dettagliate ed esempi di codice, vedi Concorrenza e operazioni asincrone con C++/WinRT.

Nota

Non è corretto implementare più di un gestore del completamento per un'operazione o un'azione asincrona. Puoi avere un solo delegato per l'evento completato oppure puoi usare co_await. In presenza di entrambi, il secondo avrà esito negativo.

Se preferisci continuare a usare i delegati anziché una coroutine, puoi scegliere una sintassi più semplice.

async_op_with_progress.Completed(
    [](auto&& /*sender*/, AsyncStatus const /* args */)
{
    // ...
});

Tipi di delegato che restituiscono un valore

Alcuni tipi di delegato devono restituire un valore. ListViewItemToKeyHandler, ad esempio, restituisce una stringa. Di seguito è riportato un esempio di creazione di un delegato di quel tipo. Nota che la funzione lambda restituisca un valore.

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

winrt::hstring f(ListView listview)
{
    return ListViewPersistenceHelper::GetRelativeScrollPosition(listview, [](IInspectable const& item)
    {
        return L"key for item goes here";
    });
}

Accesso sicuro al puntatore this con un delegato di gestione degli eventi

Ma se gestisci un evento con una funzione membro di un oggetto o dall'interno di una funzione lambda all'interno della funzione membro di un oggetto, devi considerare le durate relative del destinatario dell'evento (l'oggetto che gestisce l'evento) e dell'origine dell'evento (l'oggetto che genera l'evento). Per altre informazioni ed esempi di codice, vedi Riferimenti sicuri e deboli in C++/WinRT.

API importanti