Comment appeler une méthode asynchrone depuis un getter ou un setter?

223

Quelle serait la façon la plus élégante d'appeler une méthode asynchrone à partir d'un getter ou d'un setter en C #?

Voici un pseudo-code pour m'expliquer.

async Task<IEnumerable> MyAsyncMethod()
{
    return await DoSomethingAsync();
}

public IEnumerable MyList
{
    get
    {
         //call MyAsyncMethod() here
    }
}
Doguhan Uluca
la source
4
Ma question serait de savoir pourquoi. Une propriété est censée imiter quelque chose comme un champ dans la mesure où elle doit généralement effectuer peu (ou du moins très rapidement) de travail. Si vous avez une propriété de longue durée, il est préférable de l'écrire en tant que méthode afin que l'appelant sache que c'est un travail plus complexe.
James Michael Hare
@James: C'est tout à fait vrai - et je soupçonne que c'est pourquoi cela n'était explicitement pas pris en charge dans le CTP. Cela étant dit, vous pouvez toujours créer la propriété de type Task<T>, qui reviendra immédiatement, aura une sémantique de propriété normale et permettra toujours aux choses d'être traitées de manière asynchrone selon les besoins.
Reed Copsey
17
@James Mon besoin provient de l'utilisation de Mvvm et Silverlight. Je veux pouvoir me lier à une propriété, où le chargement des données se fait paresseusement. La classe d'extension ComboBox que j'utilise nécessite que la liaison se produise à l'étape InitializeComponent (), mais le chargement réel des données se produit beaucoup plus tard. En essayant d'accomplir avec le moins de code possible, getter et async se sentent comme la combinaison parfaite.
Doguhan Uluca
en relation: stackoverflow.com/a/33942013/11635
Ruben Bartelink
James et Reed, vous semblez oublier qu'il y a toujours des cas extrêmes. Dans le cas de WCF, je voulais vérifier que les données placées sur une propriété sont correctes et elles devaient être vérifiées à l'aide du chiffrement / déchiffrement. Il se trouve que les fonctions que j'utilise pour le déchiffrement utilisent une fonction asynchrone d'un fournisseur tiers. (PAS BEAUCOUP QUE JE PEUX FAIRE ICI).
RashadRivera

Réponses:

211

Il n'y a aucune raison techniqueasync propriétés ne sont pas autorisées en C #. C'était une décision de conception délibérée, car les «propriétés asynchrones» sont un oxymore.

Les propriétés doivent renvoyer les valeurs actuelles; ils ne devraient pas lancer d'opérations en arrière-plan.

Habituellement, quand quelqu'un veut une «propriété asynchrone», ce qu'il veut vraiment, c'est l'un d'entre eux:

  1. Une méthode asynchrone qui renvoie une valeur. Dans ce cas, changez la propriété enasync méthode.
  2. Une valeur qui peut être utilisée dans la liaison de données mais doit être calculée / récupérée de manière asynchrone. Dans ce cas, utilisez une asyncméthode d'usine pour l'objet conteneur ou utilisez une async InitAsync()méthode. La valeur liée aux données seradefault(T) jusqu'à ce que la valeur soit calculée / récupérée.
  3. Une valeur coûteuse à créer, mais qui doit être mise en cache pour une utilisation future. Dans ce cas, utilisez à AsyncLazy partir de mon blog ou de la bibliothèque AsyncEx . Cela vous donnera une awaitpropriété en mesure.

Mise à jour: je couvre les propriétés asynchrones dans l'un de mes récents articles de blog "async OOP".

Stephen Cleary
la source
Au point 2. à mon humble avis, vous ne prenez pas en compte le scénario normal dans lequel la définition de la propriété devrait à nouveau initialiser les données sous-jacentes (pas seulement dans le constructeur). Y a-t-il un autre moyen que d'utiliser Nito AsyncEx ou d'utiliser Dispatcher.CurrentDispatcher.Invoke(new Action(..)?
Gerard
@Gerard: Je ne vois pas pourquoi le point (2) ne fonctionnerait pas dans ce cas. Il vous suffit de l'implémenter INotifyPropertyChanged, puis de décider si vous souhaitez que l'ancienne valeur soit renvoyée ou default(T)pendant que la mise à jour asynchrone est en cours.
Stephen Cleary
1
@Stephan: ok, mais lorsque j'appelle la méthode async dans le setter, j'obtiens l'avertissement CS4014 "non attendu" (ou est-ce uniquement dans Framework 4.0?). Recommandez-vous de supprimer cet avertissement dans un tel cas?
Gerard
@Gerard: Ma première recommandation serait d'utiliser le NotifyTaskCompletionde mon projet AsyncEx . Ou vous pouvez construire le vôtre; ce n'est pas si dur.
Stephen Cleary
1
@Stephan: ok va essayer ça. Peut-être qu'un bel article sur ce scénario asynchrone de liaison de données-vue-modèle est en place. Par exemple, la liaison avec {Binding PropName.Result}n'est pas triviale pour moi de le découvrir.
Gerard
101

Vous ne pouvez pas l'appeler de manière asynchrone, car il n'y a pas de prise en charge de propriété asynchrone, uniquement des méthodes asynchrones. En tant que tel, il existe deux options, les deux profitant du fait que les méthodes asynchrones dans le CTP ne sont en réalité qu'une méthode qui renvoie Task<T>ou Task:

// Make the property return a Task<T>
public Task<IEnumerable> MyList
{
    get
    {
         // Just call the method
         return MyAsyncMethod();
    }
}

Ou:

// Make the property blocking
public IEnumerable MyList
{
    get
    {
         // Block via .Result
         return MyAsyncMethod().Result;
    }
}
Reed Copsey
la source
1
Merci pour votre réponse. Option A: le retour d'une tâche ne fonctionne pas vraiment à des fins de liaison. Option B:. Le résultat, comme vous le mentionnez, bloque le thread d'interface utilisateur (dans Silverlight), ce qui nécessite l'exécution de l'opération sur un thread d'arrière-plan. Je vais voir si je peux trouver une solution viable avec cette idée.
Doguhan Uluca
3
@duluca: Vous pouvez également essayer d'avoir une méthode comme celle- private async void SetupList() { MyList = await MyAsyncMethod(); } ci qui entraînerait la définition de MyList (puis la liaison automatique, si elle implémente INPC) dès que l'opération asynchrone est terminée ...
Reed Copsey
La propriété doit se trouver dans un objet que j'ai déclaré comme ressource de page, donc j'avais vraiment besoin que cet appel provienne du getter. Veuillez voir ma réponse pour la solution que j'ai trouvée.
Doguhan Uluca
1
@duluca: C'était effectivement ce que je vous proposais de faire ... Sachez cependant que si vous accédez rapidement et plusieurs fois au titre, votre solution actuelle conduira à plusieurs appels getTitle()simultanés ...
Reed Copsey
Très bon point. Bien que ce ne soit pas un problème pour mon cas spécifique, une vérification booléenne pour isLoading résoudrait le problème.
Doguhan Uluca
55

J'avais vraiment besoin que l'appel provienne de la méthode get, en raison de mon architecture découplée. J'ai donc proposé l'implémentation suivante.

Utilisation: le titre est dans un ViewModel ou un objet que vous pouvez déclarer statiquement comme ressource de page. Liez-le et la valeur sera remplie sans bloquer l'interface utilisateur, lorsque getTitle () reviendra.

string _Title;
public string Title
{
    get
    {
        if (_Title == null)
        {   
            Deployment.Current.Dispatcher.InvokeAsync(async () => { Title = await getTitle(); });
        }
        return _Title;
    }
    set
    {
        if (value != _Title)
        {
            _Title = value;
            RaisePropertyChanged("Title");
        }
    }
}
Doguhan Uluca
la source
9
à partir du 18/07/2012 dans Win8 RP, nous devons remplacer l'appel Dispatcher par: Window.Current.CoreWindow.Dispatcher.RunAsync (CoreDispatcherPriority.Normal, async () => {Title = attendre GetTytleAsync (url);});
Anton Sizikov
7
@ChristopherStevenson, je le pensais aussi, mais je ne pense pas que ce soit le cas. Parce que le getter est exécuté comme un feu et oubliez, sans appeler le setter à la fin, la liaison ne sera pas mise à jour lorsque le getter aura fini de s'excuter.
Iain
3
Non, il a et avait une condition de concurrence, mais l'utilisateur ne la verra pas à cause de 'RaisePropertyChanged ("Title")'. Il revient avant qu'il ne se termine. Mais, une fois terminé, vous définissez la propriété. Cela déclenche l'événement PropertyChanged. Le classeur obtient à nouveau la valeur de la propriété.
Medeni Baykal
1
Fondamentalement, le premier getter renverra une valeur nulle, puis il sera mis à jour. Notez que si nous voulons que getTitle soit appelé à chaque fois, il pourrait y avoir une mauvaise boucle.
tofutim
1
Vous devez également savoir que toute exception dans cet appel asynchrone sera complètement avalée. Ils n'atteignent même pas votre gestionnaire d'exceptions non géré sur l'application si vous en avez un.
Philter
9

Je pense que nous pouvons attendre que la valeur retourne d'abord null puis obtenir la valeur réelle, donc dans le cas de Pure MVVM (projet PCL par exemple), je pense que la solution la plus élégante est la suivante:

private IEnumerable myList;
public IEnumerable MyList
{
  get
    { 
      if(myList == null)
         InitializeMyList();
      return myList;
     }
  set
     {
        myList = value;
        NotifyPropertyChanged();
     }
}

private async void InitializeMyList()
{
   MyList = await AzureService.GetMyList();
}
Juan Pablo Garcia Coello
la source
3
Cela ne génère-t-il pas des avertissements du compilateurCS4014: Async method invocation without an await expression
Nick
6
Soyez très sceptique quant à suivre ces conseils. Regardez cette vidéo puis décidez- vous: channel9.msdn.com/Series/Three-Essential-Tips-for-Async/… .
Contango
1
Vous DEVEZ éviter d'utiliser des méthodes "async void"!
SuperJMN
1
chaque cri devrait venir avec une réponse sage, pourriez-vous @SuperJMN nous expliquer pourquoi?
Juan Pablo Garcia Coello du
1
@Contango Bonne vidéo. Il dit: "Utiliser async voiduniquement pour les gestionnaires de haut niveau et similaires". Je pense que cela peut être qualifié de "et leurs semblables".
HappyNomad
7

Vous pouvez utiliser Taskcomme ceci:

public int SelectedTab
        {
            get => selected_tab;
            set
            {
                selected_tab = value;

                new Task(async () =>
                {
                    await newTab.ScaleTo(0.8);
                }).Start();
            }
        }
MohsenB
la source
5

Je pensais que .GetAwaiter (). GetResult () était exactement la solution à ce problème, non? par exemple:

string _Title;
public string Title
{
    get
    {
        if (_Title == null)
        {   
            _Title = getTitle().GetAwaiter().GetResult();
        }
        return _Title;
    }
    set
    {
        if (value != _Title)
        {
            _Title = value;
            RaisePropertyChanged("Title");
        }
    }
}
bc3tech
la source
5
C'est la même chose que de bloquer avec .Result- ce n'est pas asynchrone et cela peut entraîner des blocages.
McGuireV10
vous devez ajouter IsAsync = True
Alexsandr Ter
J'apprécie les commentaires sur ma réponse; J'aimerais vraiment que quelqu'un fournisse un exemple où cela se bloque afin que je puisse le voir en action
bc3tech
2

Puisque votre "propriété async" est dans un modèle de vue, vous pouvez utiliser AsyncMVVM :

class MyViewModel : AsyncBindableBase
{
    public string Title
    {
        get
        {
            return Property.Get(GetTitleAsync);
        }
    }

    private async Task<string> GetTitleAsync()
    {
        //...
    }
}

Il s'occupera du contexte de synchronisation et de la notification de changement de propriété pour vous.

Dmitry Shechtman
la source
Être une propriété doit l'être.
Dmitry Shechtman
Désolé, mais j'ai peut-être raté le point de ce code alors. Pouvez-vous élaborer s'il vous plaît?
Patrick Hofman
Les propriétés bloquent par définition. GetTitleAsync () sert de "getter async" sans le sucre syntaxique.
Dmitry Shechtman
1
@DmitryShechtman: Non, il ne doit pas nécessairement être bloqué. C'est exactement à cela que servent les notifications de changement et les machines d'état. Et ils ne bloquent pas par définition. Ils sont synchrones par définition. Ce n'est pas la même chose que le blocage. Le «blocage» signifie qu'ils peuvent faire un travail lourd et peuvent prendre un temps considérable à exécuter. C'est à son tour exactement ce que les propriétés NE DEVRAIENT PAS FAIRE.
quetzalcoatl
1

Nécromancement.
Dans .NET Core / NetStandard2, vous pouvez utiliser à la Nito.AsyncEx.AsyncContext.Runplace de System.Windows.Threading.Dispatcher.InvokeAsync:

class AsyncPropertyTest
{

    private static async System.Threading.Tasks.Task<int> GetInt(string text)
    {
        await System.Threading.Tasks.Task.Delay(2000);
        System.Threading.Thread.Sleep(2000);
        return int.Parse(text);
    }


    public static int MyProperty
    {
        get
        {
            int x = 0;

            // /programming/6602244/how-to-call-an-async-method-from-a-getter-or-setter
            // /programming/41748335/net-dispatcher-for-net-core
            // https://github.com/StephenCleary/AsyncEx
            Nito.AsyncEx.AsyncContext.Run(async delegate ()
            {
                x = await GetInt("123");
            });

            return x;
        }
    }


    public static void Test()
    {
        System.Console.WriteLine(System.DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss.fff"));
        System.Console.WriteLine(MyProperty);
        System.Console.WriteLine(System.DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss.fff"));
    }


}

Si vous avez simplement choisi System.Threading.Tasks.Task.Runou System.Threading.Tasks.Task<int>.Run, cela ne fonctionnerait pas.

Stefan Steiger
la source
-1

Je pense que mon exemple ci-dessous peut suivre l'approche de @ Stephen-Cleary mais je voulais donner un exemple codé. Ceci est destiné à être utilisé dans un contexte de liaison de données, par exemple Xamarin.

Le constructeur de la classe - ou bien le poseur d'une autre propriété dont il dépend - peut appeler un vide asynchrone qui remplira la propriété à la fin de la tâche sans avoir besoin d'attendre ou de bloquer. Lorsqu'il obtient enfin une valeur, il met à jour votre interface utilisateur via le mécanisme NotifyPropertyChanged.

Je ne suis pas certain des effets secondaires de l'appel d'un vide aysnc à partir d'un constructeur. Peut-être qu'un intervenant développera la gestion des erreurs, etc.

class MainPageViewModel : INotifyPropertyChanged
{
    IEnumerable myList;

    public event PropertyChangedEventHandler PropertyChanged;

    public MainPageViewModel()
    {

        MyAsyncMethod()

    }

    public IEnumerable MyList
    {
        set
        {
            if (myList != value)
            {
                myList = value;

                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("MyList"));
                }
            }
        }
        get
        {
            return myList;
        }
    }

    async void MyAsyncMethod()
    {
        MyList = await DoSomethingAsync();
    }


}
Craig.C
la source
-1

Lorsque j'ai rencontré ce problème, essayer d'exécuter une synchronicité de méthode asynchrone à partir d'un setter ou d'un constructeur m'a mis dans une impasse sur le thread d'interface utilisateur, et l'utilisation d'un gestionnaire d'événements a nécessité trop de changements dans la conception générale.
La solution était, comme souvent, d'écrire explicitement ce que je voulais implicitement, c'est-à-dire d'avoir un autre thread pour gérer l'opération et pour que le thread principal attende qu'il se termine:

string someValue=null;
var t = new Thread(() =>someValue = SomeAsyncMethod().Result);
t.Start();
t.Join();

Vous pourriez dire que j'abuse du cadre, mais cela fonctionne.

Yuval Perelman
la source
-1

J'examine toutes les réponses, mais toutes ont un problème de performances.

par exemple dans:

string _Title;
public string Title
{
    get
    {
        if (_Title == null)
        {   
            Deployment.Current.Dispatcher.InvokeAsync(async () => { Title = await getTitle(); });
        }
        return _Title;
    }
    set
    {
        if (value != _Title)
        {
            _Title = value;
            RaisePropertyChanged("Title");
        }
    }
}

Deployment.Current.Dispatcher.InvokeAsync (async () => {Title = attendre getTitle ();});

utilisez dispatcher qui n'est pas une bonne réponse.

mais il existe une solution simple, faites-le:

string _Title;
    public string Title
    {
        get
        {
            if (_Title == null)
            {   
                Task.Run(()=> 
                {
                    _Title = getTitle();
                    RaisePropertyChanged("Title");
                });        
                return;
            }
            return _Title;
        }
        set
        {
            if (value != _Title)
            {
                _Title = value;
                RaisePropertyChanged("Title");
            }
        }
    }
Mahdi Rastegari
la source
si votre fonction est asyn, utilisez getTitle (). wait () au lieu de getTitle ()
Mahdi Rastegari
-4

Vous pouvez changer la propriété en Task<IEnumerable>

et faites quelque chose comme:

get
{
    Task<IEnumerable>.Run(async()=>{
       return await getMyList();
    });
}

et l'utiliser comme attendre MyList;

Alejandro Guardiola
la source