web-dev-qa-db-fra.com

Comment "attendre" la création d'un événement EventHandler

Parfois, le modèle d’événement est utilisé pour générer des événements dans les applications MVVM à l’aide d’un modèle de vue enfant afin d’envoyer un message à son modèle de vue parent de manière plus ou moins couplée. 

Parent ViewModel

searchWidgetViewModel.SearchRequest += (s,e) => 
{
    SearchOrders(searchWidgitViewModel.SearchCriteria);
};

SearchWidget ViewModel

public event EventHandler SearchRequest;

SearchCommand = new RelayCommand(() => {

    IsSearching = true;
    if (SearchRequest != null) 
    {
        SearchRequest(this, EventArgs.Empty);
    }
    IsSearching = false;
});

En refactant mon application pour .NET4.5, je fais autant que possible du code pour utiliser async et await. Cependant ce qui suit ne marche pas (enfin je ne m'y attendais vraiment pas)

 await SearchRequest(this, EventArgs.Empty);

Le framework fait vraiment cela pour appeler des gestionnaires d’événements comme celui-ci , mais je ne suis pas sûr de savoir comment il le fait?

private async void button1_Click(object sender, RoutedEventArgs e)
{
   textBlock1.Text = "Click Started";
   await DoWork();
   textBlock2.Text = "Click Finished";
}

Tout ce que j'ai trouvé sur le sujet de la création d'événements de manière asynchrone estancien mais je ne trouve rien dans le cadre qui appuie cela.

Comment puis-je await appeler un événement mais rester sur le thread d'interface utilisateur.

37
Simon_Weaver

Edit: Cela ne fonctionne pas bien pour plusieurs abonnés, donc à moins que vous n'en ayez qu'un, je ne le recommanderais pas.


C'est un peu hacky, mais je n'ai jamais rien trouvé de mieux:

Déclarez un délégué. Ceci est identique à EventHandler mais renvoie une tâche au lieu de void

public delegate Task AsyncEventHandler(object sender, EventArgs e);

Vous pouvez ensuite exécuter les opérations suivantes et tant que le gestionnaire déclaré dans le parent utilise correctement les variables async et await, il s'exécutera de manière asynchrone:

if (SearchRequest != null) 
{
    Debug.WriteLine("Starting...");
    await SearchRequest(this, EventArgs.Empty);
    Debug.WriteLine("Completed");
}

Gestionnaire d'échantillons:

 // declare handler for search request
 myViewModel.SearchRequest += async (s, e) =>
 {                    
     await SearchOrders();
 };

Remarque: je n'ai jamais testé cela avec plusieurs abonnés et je ne sais pas comment cela va fonctionner. Si vous avez besoin de plusieurs abonnés, assurez-vous de le tester soigneusement.

27
Simon_Weaver

Sur la base de la réponse de Simon_Weaver, j'ai créé une classe d'assistance capable de gérer plusieurs abonnés, avec une syntaxe similaire à celle des événements c #.

public class AsyncEvent<TEventArgs> where TEventArgs : EventArgs
{
    private readonly List<Func<object, TEventArgs, Task>> invocationList;
    private readonly object locker;

    private AsyncEvent()
    {
        invocationList = new List<Func<object, TEventArgs, Task>>();
        locker = new object();
    }

    public static AsyncEvent<TEventArgs> operator +(
        AsyncEvent<TEventArgs> e, Func<object, TEventArgs, Task> callback)
    {
        if (callback == null) throw new NullReferenceException("callback is null");

        //Note: Thread safety issue- if two threads register to the same event (on the first time, i.e when it is null)
        //they could get a different instance, so whoever was first will be overridden.
        //A solution for that would be to switch to a public constructor and use it, but then we'll 'lose' the similar syntax to c# events             
        if (e == null) e = new AsyncEvent<TEventArgs>();

        lock (e.locker)
        {
            e.invocationList.Add(callback);
        }
        return e;
    }

    public static AsyncEvent<TEventArgs> operator -(
        AsyncEvent<TEventArgs> e, Func<object, TEventArgs, Task> callback)
    {
        if (callback == null) throw new NullReferenceException("callback is null");
        if (e == null) return null;

        lock (e.locker)
        {
            e.invocationList.Remove(callback);
        }
        return e;
    }

    public async Task InvokeAsync(object sender, TEventArgs eventArgs)
    {
        List<Func<object, TEventArgs, Task>> tmpInvocationList;
        lock (locker)
        {
            tmpInvocationList = new List<Func<object, TEventArgs, Task>>(invocationList);
        }

        foreach (var callback in tmpInvocationList)
        {
            //Assuming we want a serial invocation, for a parallel invocation we can use Task.WhenAll instead
            await callback(sender, eventArgs);
        }
    }
}

Pour l'utiliser, vous le déclarez dans votre classe, par exemple:

public AsyncEvent<EventArgs> SearchRequest;

Pour abonner un gestionnaire d'événements, vous utiliserez la syntaxe habituelle (la même que dans la réponse de Simon_Weaver):

myViewModel.SearchRequest += async (s, e) =>
{                    
   await SearchOrders();
};

Pour appeler l'événement, utilisez le même modèle que nous utilisons pour les événements c # (uniquement avec InvokeAsync):

var eventTmp = SearchRequest;
if (eventTmp != null)
{
   await eventTmp.InvokeAsync(sender, eventArgs);
}

Si vous utilisez c # 6, vous devriez pouvoir utiliser l'opérateur conditionnel null et écrire ceci à la place:

await (SearchRequest?.InvokeAsync(sender, eventArgs) ?? Task.CompletedTask);
19
tzachs

Pour répondre à la question directe: je ne pense pas que EventHandler permette aux implémentations de communiquer suffisamment en retour à l'appelant pour permettre une attente correcte. Vous pourrez peut-être exécuter des astuces avec un contexte de synchronisation personnalisé, mais si vous souhaitez attendre les gestionnaires, il est préférable que ceux-ci puissent renvoyer leur Task à l'invocateur. En faisant cette partie de la signature du délégué, il est plus clair que le délégué sera awaited.

Je suggère d’utiliser l’approche Delgate.GetInvocationList() décrite dans la réponse d’Ariel avec des idées tirées de la réponse de tzachs . Définissez votre propre délégué AsyncEventHandler<TEventArgs> qui retourne une Task. Utilisez ensuite une méthode d’extension pour masquer la complexité de son invocation correcte. Je pense que ce modèle est logique si vous souhaitez exécuter un ensemble de gestionnaires d'événements asynchrones et attendre leurs résultats.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

public delegate Task AsyncEventHandler<TEventArgs>(
    object sender,
    TEventArgs e)
    where TEventArgs : EventArgs;

public static class AsyncEventHandlerExtensions
{
    public static IEnumerable<AsyncEventHandler<TEventArgs>> GetHandlers<TEventArgs>(
        this AsyncEventHandler<TEventArgs> handler)
        where TEventArgs : EventArgs
        => handler.GetInvocationList().Cast<AsyncEventHandler<TEventArgs>>();

    public static Task InvokeAllAsync<TEventArgs>(
        this AsyncEventHandler<TEventArgs> handler,
        object sender,
        TEventArgs e)
        where TEventArgs : EventArgs
        => Task.WhenAll(
            handler.GetHandlers()
            .Select(handleAsync => handleAsync(sender, e)));
}

Cela vous permet de créer une event de style .net normale. Abonnez-vous comme vous le feriez normalement.

public event AsyncEventHandler<EventArgs> SomethingHappened;

public void SubscribeToMyOwnEventsForNoReason()
{
    SomethingHappened += async (sender, e) =>
    {
        SomethingSynchronous();
        // Safe to touch e here.
        await SomethingAsynchronousAsync();
        // No longer safe to touch e here (please understand
        // SynchronizationContext well before trying fancy things).
        SomeContinuation();
    };
}

Ensuite, rappelez-vous simplement d'utiliser les méthodes d'extension pour appeler l'événement plutôt que de les appeler directement. Si vous voulez plus de contrôle dans votre invocation, vous pouvez utiliser l'extension GetHandlers(). Pour le cas le plus courant d'attendre que tous les gestionnaires se terminent, utilisez simplement la commodité InvokeAllAsync(). Dans de nombreux modèles, les événements ne produisent rien de ce qui intéresse l’appelant ou bien ils lui communiquent en modifiant la valeur passée dans EventArgs. (Remarque: si vous pouvez supposer un contexte de synchronisation avec une sérialisation de style dispatcher, vos gestionnaires d’événements peuvent muter la variable EventArgs au sein de leurs blocs synchrones, car les continuations seront marshalées sur le fil du répartiteur. vous invoquez et await l'événement à partir d'un thread d'interface utilisateur dans winforms ou WPF, sinon vous devrez peut-être utiliser le verrouillage lors de la mutation de EventArgs au cas où l'une de vos mutations se produirait dans une continuation exécutée sur le pool de threads).

public async Task Run(string[] args)
{
    if (SomethingHappened != null)
        await SomethingHappened.InvokeAllAsync(this, EventArgs.Empty);
}

Cela vous rapproche de quelque chose qui ressemble à une invocation d'événement normale, sauf que vous devez utiliser .InvokeAllAsync(). Et, bien sûr, vous avez toujours les problèmes normaux liés à des événements tels que la nécessité de protéger les invocations pour des événements sans abonné afin d'éviter une variable NullArgumentException.

Notez que je ne suis pas en utilisant await SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty) car await explose sur null. Vous pouvez utiliser le modèle d'appel suivant si vous le souhaitez, mais vous pouvez faire valoir que les parens sont laids et que le style if est généralement meilleur pour diverses raisons:

await (SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty) ?? Task.CompletedTask);
5
binki

Puisque les délégués (et les événements sont des délégués) implémentent le modèle de programmation asynchrone (APM), vous pouvez utiliser la méthode TaskFactory.FromAsync . (Voir aussi Tâches et modèle de programmation asynchrone (APM) .)

public event EventHandler SearchRequest;

public async Task SearchCommandAsync()
{
    IsSearching = true;
    if (SearchRequest != null)
    {
        await Task.Factory.FromAsync(SearchRequest.BeginInvoke, SearchRequest.EndInvoke, this, EventArgs.Empty, null);
    }
    IsSearching = false;
}

Cependant, le code ci-dessus invoquera l'événement sur un thread de pool de threads, c'est-à-dire qu'il ne capturera pas le contexte de synchronisation actuel. Si cela pose un problème, vous pouvez le modifier comme suit:

public event EventHandler SearchRequest;

private delegate void OnSearchRequestDelegate(SynchronizationContext context);

private void OnSearchRequest(SynchronizationContext context)
{
    context.Send(state => SearchRequest(this, EventArgs.Empty), null);
}

public async Task SearchCommandAsync()
{
    IsSearching = true;
    if (SearchRequest != null)
    {
        var search = new OnSearchRequestDelegate(OnSearchRequest);
        await Task.Factory.FromAsync(search.BeginInvoke, search.EndInvoke, SynchronizationContext.Current, null);
    }
    IsSearching = false;
}
2
Scott

Je ne comprends pas bien ce que vous entendez par "Comment puis-je await appeler un événement mais rester sur le fil de l'interface utilisateur". Voulez-vous que le gestionnaire d'événements soit exécuté sur le thread d'interface utilisateur? Si c'est le cas, vous pouvez faire quelque chose comme ceci:

var h = SomeEvent;
if (h != null)
{
    await Task.Factory.StartNew(() => h(this, EventArgs.Empty),
        Task.Factory.CancellationToken,
        Task.Factory.CreationOptions,
        TaskScheduler.FromCurrentSynchronizationContext());
}

Ce qui enveloppe l'invocation du gestionnaire dans un objet Task afin que vous puissiez utiliser await, car vous ne pouvez pas utiliser await avec une méthode void - d'où provient l'erreur de compilation.

Mais, je ne suis pas sûr de l’avantage que vous espérez en retirer.

Je pense qu'il y a un problème fondamental de conception. C'est bien de commencer un travail de fond sur un événement click et vous pouvez implémenter quelque chose qui supporte await. Mais, quel est l'effet sur la façon dont l'interface utilisateur peut être utilisée? par exemple. si vous avez un gestionnaire Click qui lance une opération qui prend 2 secondes, voulez-vous que l'utilisateur puisse cliquer sur ce bouton pendant que l'opération est en attente? L'annulation et le délai d'attente sont des complexités supplémentaires. Je pense que beaucoup plus de compréhension des aspects de l'utilisabilité doit être faite ici.

2
Peter Ritchie

Pour continuer sur la réponse de Simon Weaver , j’ai essayé ce qui suit

        if (SearchRequest != null)
        {
            foreach (AsyncEventHandler onSearchRequest in SearchRequest.GetInvocationList())
            {
                await onSearchRequest(null, EventArgs.Empty);
            }
        }

Cela semble faire l'affaire.

0
Ariel Steiner
public static class FileProcessEventHandlerExtensions
{
    public static Task InvokeAsync(this FileProcessEventHandler handler, object sender, FileProcessStatusEventArgs args)
     => Task.WhenAll(handler.GetInvocationList()
                            .Cast<FileProcessEventHandler>()
                            .Select(h => h(sender, args))
                            .ToArray());
}
0
Andrii

Si vous utilisez des gestionnaires d'événements personnalisés, vous voudrez peut-être jeter un coup d'œil au DeferredEvents , car cela vous permettra de lever et d'attendre les gestionnaires d'un événement, comme ceci:

await MyEvent.InvokeAsync(sender, DeferredEventArgs.Empty);

Le gestionnaire d'événement fera quelque chose comme ceci:

public async void OnMyEvent(object sender, DeferredEventArgs e)
{
    var deferral = e.GetDeferral();

    await DoSomethingAsync();

    deferral.Complete();
}

Alternativement, vous pouvez utiliser le modèle using comme ceci:

public async void OnMyEvent(object sender, DeferredEventArgs e)
{
    using (e.GetDeferral())
    {
        await DoSomethingAsync();
    }
}

Vous pouvez lire sur les événements différés ici .

0
Pedro Lamas