Wie rufe ich eine asynchrone Methode von einem Getter oder Setter auf?


223

Was wäre die eleganteste Art, eine asynchrone Methode von einem Getter oder Setter in C # aufzurufen?

Hier ist ein Pseudocode, der mir hilft, mich zu erklären.

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

public IEnumerable MyList
{
    get
    {
         //call MyAsyncMethod() here
    }
}

4
Meine Frage wäre warum. Eine Eigenschaft soll so etwas wie ein Feld imitieren, indem sie normalerweise wenig (oder zumindest sehr schnelle) Arbeit leistet. Wenn Sie eine Eigenschaft mit langer Laufzeit haben, ist es viel besser, sie als Methode zu schreiben, damit der Aufrufer weiß, dass es sich um eine komplexere Arbeit handelt.
James Michael Hare

@ James: Das ist genau richtig - und ich vermute, deshalb wurde dies im CTP ausdrücklich nicht unterstützt. Abgesehen davon können Sie jederzeit die Eigenschaft type festlegen Task<T>, die sofort zurückgegeben wird, eine normale Eigenschaftssemantik aufweist und dennoch die asynchrone Behandlung von Dingen nach Bedarf ermöglicht.
Reed Copsey

17
@ James Mein Bedarf ergibt sich aus der Verwendung von Mvvm und Silverlight. Ich möchte in der Lage sein, an eine Eigenschaft zu binden, bei der das Laden der Daten träge erfolgt. Für die von mir verwendete ComboBox-Erweiterungsklasse muss die Bindung in der Phase InitializeComponent () erfolgen. Das tatsächliche Laden der Daten erfolgt jedoch viel später. Beim Versuch, mit so wenig Code wie möglich zu erreichen, fühlen sich Getter und Async wie die perfekte Kombination an.
Doguhan Uluca


James und Reed, Sie scheinen zu vergessen, dass es immer Randfälle gibt. Im Fall von WCF wollte ich überprüfen, ob die auf einer Eigenschaft platzierten Daten korrekt sind, und sie mussten mithilfe von Verschlüsselung / Entschlüsselung überprüft werden. Die Funktionen, die ich zur Entschlüsselung verwende, verwenden zufällig eine asynchrone Funktion eines Drittanbieters. (Nicht viel, was ich hier tun kann).
RashadRivera

Antworten:


211

Es gibt keinen technischen Grund, warum asyncEigenschaften in C # nicht zulässig sind. Es war eine gezielte Entwurfsentscheidung, da "asynchrone Eigenschaften" ein Oxymoron sind.

Eigenschaften sollten aktuelle Werte zurückgeben. Sie sollten keine Hintergrundoperationen starten.

Wenn jemand eine "asynchrone Eigenschaft" möchte, möchte er normalerweise Folgendes:

  1. Eine asynchrone Methode, die einen Wert zurückgibt. Ändern Sie in diesem Fall die Eigenschaft in eine asyncMethode.
  2. Ein Wert, der für die Datenbindung verwendet werden kann, aber asynchron berechnet / abgerufen werden muss. Verwenden Sie in diesem Fall entweder eine asyncFactory-Methode für das enthaltende Objekt oder eine async InitAsync()Methode. Der datengebundene Wert bleibt so lange bestehen, default(T)bis der Wert berechnet / abgerufen wurde.
  3. Ein Wert, dessen Erstellung teuer ist, der jedoch für die zukünftige Verwendung zwischengespeichert werden sollte. In diesem Fall verwenden Sie AsyncLazy aus meinem Blog oder meiner AsyncEx-Bibliothek . Dies gibt Ihnen eine awaitfähige Eigenschaft.

Update: Ich beschreibe asynchrone Eigenschaften in einem meiner letzten " asynchronen OOP" -Blogposts.


In Punkt 2. imho berücksichtigen Sie nicht das reguläre Szenario, in dem das Festlegen der Eigenschaft die zugrunde liegenden Daten erneut initialisieren sollte (nicht nur im Konstruktor). Gibt es eine andere Möglichkeit als Nito AsyncEx oder Dispatcher.CurrentDispatcher.Invoke(new Action(..)?
Gerard

@Gerard: Ich verstehe nicht, warum Punkt (2) in diesem Fall nicht funktionieren würde. Implementieren INotifyPropertyChangedSie einfach und entscheiden Sie dann, ob der alte Wert zurückgegeben werden soll oder default(T)während das asynchrone Update ausgeführt wird.
Stephen Cleary

1
@Stephan: ok, aber wenn ich die asynchrone Methode im Setter aufrufe, erhalte ich die Warnung CS4014 "nicht erwartet" (oder ist das nur in Framework 4.0?). Raten Sie, diese Warnung in einem solchen Fall zu unterdrücken?
Gerard

@Gerard: Meine erste Empfehlung wäre, das NotifyTaskCompletionaus meinem AsyncEx-Projekt zu verwenden . Oder Sie können Ihre eigenen bauen; es ist nicht so schwer.
Stephen Cleary

1
@Stephan: ok werde das versuchen. Vielleicht ist ein schöner Artikel über dieses asynchrone Datenbindungs-Ansichtsmodell-Szenario vorhanden. ZB das Binden an {Binding PropName.Result}ist für mich nicht trivial, um es herauszufinden.
Gerard

101

Sie können es nicht asynchron aufrufen, da es keine Unterstützung für asynchrone Eigenschaften gibt, sondern nur asynchrone Methoden. Daher gibt es zwei Optionen, die beide die Tatsache ausnutzen, dass asynchrone Methoden im CTP eigentlich nur eine Methode sind, die zurückgibt Task<T>oder Task:

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

Oder:

// Make the property blocking
public IEnumerable MyList
{
    get
    {
         // Block via .Result
         return MyAsyncMethod().Result;
    }
}

1
Danke für Ihre Antwort. Option A: Das Zurückgeben einer Aufgabe funktioniert nicht wirklich zu Bindungszwecken. Option B: .Result blockiert, wie bereits erwähnt, den UI-Thread (in Silverlight) und erfordert daher, dass die Operation auf einem Hintergrund-Thread ausgeführt wird. Ich werde sehen, ob ich mit dieser Idee eine praktikable Lösung finden kann.
Doguhan Uluca

3
@duluca: Sie könnten auch versuchen, eine Methode wie diese zu haben. private async void SetupList() { MyList = await MyAsyncMethod(); } Dies würde dazu führen, dass MyList gesetzt wird (und dann automatisch bindet, wenn INPC implementiert wird), sobald der asynchrone Vorgang abgeschlossen ist ...
Reed Copsey

Die Eigenschaft muss sich in einem Objekt befinden, das ich als Seitenressource deklariert hatte. Daher musste dieser Aufruf wirklich vom Getter stammen. Bitte lesen Sie meine Antwort für die Lösung, die ich gefunden habe.
Doguhan Uluca

1
@duluca: Das war genau das, was ich Ihnen vorgeschlagen habe ... Beachten Sie jedoch, dass Ihre aktuelle Lösung, wenn Sie mehrmals schnell auf Title zugreifen, zu mehreren gleichzeitigen Anrufen führt getTitle()...
Reed Copsey

Sehr guter Punkt. Obwohl dies für meinen speziellen Fall kein Problem darstellt, würde eine boolesche Überprüfung auf isLoading das Problem beheben.
Doguhan Uluca

55

Aufgrund meiner entkoppelten Architektur brauchte ich den Aufruf wirklich, um von der get-Methode zu stammen. Also habe ich mir die folgende Implementierung ausgedacht.

Verwendung: Der Titel befindet sich in einem ViewModel oder einem Objekt, das Sie statisch als Seitenressource deklarieren können. Wenn Sie daran binden, wird der Wert gefüllt, ohne die Benutzeroberfläche zu blockieren, wenn getTitle () zurückkehrt.

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");
        }
    }
}

9
Vom 18.07.2012 in Win8 RP sollten wir den Dispatcher-Aufruf ändern in: Window.Current.CoreWindow.Dispatcher.RunAsync (CoreDispatcherPriority.Normal, async () => {Title = warte auf GetTytleAsync (url);});
Anton Sizikov

7
@ChristopherStevenson, das habe ich auch gedacht, aber ich glaube nicht, dass das der Fall ist. Da der Getter als Feuer ausgeführt wird und vergessen wird, wird die Bindung nicht aktualisiert, wenn der Getter die Ausführung beendet hat, ohne den Setter nach Abschluss aufzurufen.
Iain

3
Nein, es hat und hatte eine Rennbedingung, aber der Benutzer wird es aufgrund von 'RaisePropertyChanged ("Title")' nicht sehen. Es kehrt zurück, bevor es abgeschlossen ist. Nach Abschluss legen Sie die Eigenschaft fest. Das löst das PropertyChanged-Ereignis aus. Binder erhält wieder den Wert des Eigentums.
Medeni Baykal

1
Grundsätzlich gibt der erste Getter einen Nullwert zurück und wird dann aktualisiert. Beachten Sie, dass es eine fehlerhafte Schleife geben kann, wenn getTitle jedes Mal aufgerufen werden soll.
Tofutim

1
Sie sollten sich auch darüber im Klaren sein, dass alle Ausnahmen in diesem asynchronen Aufruf vollständig verschluckt werden. Sie erreichen nicht einmal Ihren nicht behandelten Ausnahmebehandler in der Anwendung, wenn Sie einen haben.
Philter

9

Ich denke, wir können darauf warten, dass der Wert zuerst null zurückgibt und dann den tatsächlichen Wert erhält. Im Fall von Pure MVVM (z. B. PCL-Projekt) halte ich Folgendes für die eleganteste Lösung:

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();
}

3
Erzeugt dies keine Compiler-WarnungenCS4014: Async method invocation without an await expression
Nick

6
Seien Sie sehr skeptisch, wenn Sie diesen Rat befolgen. Sehen Sie sich dieses Video an und entscheiden Sie sich selbst: channel9.msdn.com/Series/Three-Essential-Tips-for-Async/… .
Contango

1
Sie sollten die Verwendung von "async void" -Methoden vermeiden!
SuperJMN

1
Jeder Schrei sollte mit einer klugen Antwort kommen. Würden Sie uns @SuperJMN erklären, warum?
Juan Pablo Garcia Coello

1
@ Contango Gutes Video. Er sagt: " async voidNur für Top-Level-Handler und dergleichen verwenden." Ich denke, dies kann als "und dergleichen" qualifiziert werden.
HappyNomad

7

Sie können Taskwie folgt verwenden:

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

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

5

Ich dachte. GetAwaiter (). GetResult () war genau die Lösung für dieses Problem, nein? z.B:

string _Title;
public string Title
{
    get
    {
        if (_Title == null)
        {   
            _Title = getTitle().GetAwaiter().GetResult();
        }
        return _Title;
    }
    set
    {
        if (value != _Title)
        {
            _Title = value;
            RaisePropertyChanged("Title");
        }
    }
}

5
Das ist das gleiche wie nur das Blockieren mit .Result- es ist nicht asynchron und kann zu Deadlocks führen.
McGuireV10

Sie müssen hinzufügen IsAsync = True
Alexsandr Ter

Ich freue mich über das Feedback zu meiner Antwort. Ich würde es wirklich lieben, wenn jemand ein Beispiel liefert, in dem dies blockiert, damit ich es in Aktion sehen kann
bc3tech

2

Da sich Ihre "asynchrone Eigenschaft" in einem Ansichtsmodell befindet, können Sie AsyncMVVM verwenden :

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

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

Es kümmert sich für Sie um den Synchronisationskontext und die Benachrichtigung über Eigenschaftsänderungen.


Als Eigentum muss es sein.
Dmitry Shechtman

Entschuldigung, aber vielleicht habe ich dann den Punkt dieses Codes verpasst. Können Sie bitte näher darauf eingehen?
Patrick Hofman

Eigenschaften blockieren per Definition. GetTitleAsync () dient als "asynchroner Getter" ohne syntaktischen Zucker.
Dmitry Shechtman

1
@DmitryShechtman: Nein, es muss nicht blockieren. Genau dafür sind Änderungsbenachrichtigungen und Zustandsautomaten gedacht. Und sie blockieren per Definition nicht. Sie sind per Definition synchron. Dies ist nicht dasselbe wie Blockieren. "Blockieren" bedeutet, dass sie schwere Arbeit leisten und viel Zeit für die Ausführung benötigen. Dies ist genau das, was Eigenschaften nicht tun sollten.
Quetzalcoatl

1

Nekromantie.
In .NET Core / NetStandard2 können Sie Nito.AsyncEx.AsyncContext.Runanstelle von 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"));
    }


}

Wenn Sie einfach System.Threading.Tasks.Task.Runoder System.Threading.Tasks.Task<int>.Runwählen würden, würde es nicht funktionieren.


-1

Ich denke, mein Beispiel unten folgt möglicherweise dem Ansatz von @ Stephen-Cleary, aber ich wollte ein codiertes Beispiel geben. Dies dient zur Verwendung in einem Datenbindungskontext, beispielsweise Xamarin.

Der Konstruktor der Klasse - oder tatsächlich der Setter einer anderen Eigenschaft, von der er abhängig ist - kann eine asynchrone Leere aufrufen, die die Eigenschaft nach Abschluss der Aufgabe auffüllt, ohne dass ein Warten oder Blockieren erforderlich ist. Wenn es endlich einen Wert erhält, aktualisiert es Ihre Benutzeroberfläche über den NotifyPropertyChanged-Mechanismus.

Ich bin mir nicht sicher, welche Nebenwirkungen es hat, wenn ein Konstruktor eine aysnc-Leere aufruft. Vielleicht wird ein Kommentator die Fehlerbehandlung usw. näher erläutern.

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();
    }


}

-1

Als ich auf dieses Problem stieß, führte der Versuch, eine asynchrone Methodensynchronität von einem Setter oder einem Konstruktor aus auszuführen, zu einem Deadlock im UI-Thread, und die Verwendung eines Ereignishandlers erforderte zu viele Änderungen im allgemeinen Design.
Die Lösung bestand, wie so oft, darin, explizit zu schreiben, was ich implizit tun wollte, nämlich einen anderen Thread die Operation abwickeln zu lassen und den Haupt-Thread dazu zu bringen, auf den Abschluss zu warten:

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

Sie könnten argumentieren, dass ich das Framework missbrauche, aber es funktioniert.


-1

Ich überprüfe alle Antworten, aber alle haben ein Leistungsproblem.

zum Beispiel in:

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 = warte auf getTitle ();});

Verwenden Sie einen Dispatcher, der keine gute Antwort ist.

Aber es gibt eine einfache Lösung, machen Sie es einfach:

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");
            }
        }
    }

Wenn Ihre Funktion asyn ist, verwenden Sie getTitle (). wait () anstelle von getTitle ()
Mahdi Rastegari

-4

Sie können die Eigenschaft in ändern Task<IEnumerable>

und mache so etwas wie:

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

und benutze es wie auf MyList warten;

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.