Come chiamare un metodo asincrono da un getter o setter?


223

Quale sarebbe il modo più elegante per chiamare un metodo asincrono da un getter o setter in C #?

Ecco alcuni pseudo-codici per aiutare a spiegarmi.

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

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

4
La mia domanda sarebbe perché. Una proprietà dovrebbe imitare qualcosa di simile a un campo in quanto in genere dovrebbe eseguire poco (o almeno molto veloce) lavoro. Se hai una proprietà di lunga durata è molto meglio scriverla come metodo in modo che il chiamante sappia che è un corpus di lavoro più complesso.
James Michael Hare,

@James: Esatto, e sospetto sia per questo che non è stato esplicitamente supportato nel CTP. Detto questo, puoi sempre rendere la proprietà di tipo Task<T>, che tornerà immediatamente, avere una semantica di proprietà normale e consentire comunque che le cose vengano trattate in modo asincrono secondo necessità.
Reed Copsey,

17
@James Il mio bisogno deriva dall'utilizzo di Mvvm e Silverlight. Voglio essere in grado di legare a una proprietà, in cui il caricamento dei dati avviene pigramente. La classe di estensione ComboBox che sto usando richiede che l'associazione avvenga nella fase InitializeComponent (), tuttavia il caricamento effettivo dei dati avviene molto più tardi. Nel tentativo di ottenere il minor numero di codice possibile, getter e async si sentono la combinazione perfetta.
Doguhan Uluca,


James e Reed, il tuo sembra dimenticare che ci sono sempre casi limite. Nel caso di WCF, volevo verificare che i dati inseriti in una proprietà fossero corretti e che dovevano essere verificati utilizzando la crittografia / decrittografia. Le funzioni che utilizzo per la decrittografia utilizzano una funzione asincrona di un fornitore di terze parti. (NON MOLTO CHE POSSO FARE QUI).
RashadRivera,

Risposte:


211

Non esiste alcun motivo tecnico per cui le asyncproprietà non sono consentite in C #. È stata una decisione progettuale intenzionale, perché "proprietà asincrone" è un ossimoro.

Le proprietà dovrebbero restituire i valori correnti; non dovrebbero iniziare le operazioni in background.

Di solito, quando qualcuno vuole una "proprietà asincrona", quello che vuole veramente è uno di questi:

  1. Un metodo asincrono che restituisce un valore. In questo caso, modificare la proprietà in un asyncmetodo.
  2. Un valore che può essere utilizzato nell'associazione dati ma che deve essere calcolato / recuperato in modo asincrono. In questo caso, utilizzare un asyncmetodo factory per l'oggetto contenitore o utilizzare un async InitAsync()metodo. Il valore associato ai dati sarà default(T)fino a quando il valore non verrà calcolato / recuperato.
  3. Un valore che è costoso da creare, ma deve essere memorizzato nella cache per uso futuro. In questo caso, utilizzare AsyncLazy dal mio blog o dalla libreria AsyncEx . Questo ti darà una awaitproprietà capace.

Aggiornamento: copro le proprietà asincrone in uno dei miei recenti post sul blog "async OOP".


Al punto 2. non ho preso in considerazione lo scenario normale in cui l'impostazione della proprietà dovrebbe inizializzare nuovamente i dati sottostanti (non solo nel costruttore). Esiste un altro modo oltre all'utilizzo di Nito AsyncEx o all'utilizzo Dispatcher.CurrentDispatcher.Invoke(new Action(..)?
Gerard,

@ Gerard: non vedo perché il punto (2) non funzioni in quel caso. Basta implementare INotifyPropertyChangede quindi decidere se si desidera restituire il vecchio valore o default(T)mentre l'aggiornamento asincrono è in volo.
Stephen Cleary,

1
@Stephan: ok, ma quando chiamo il metodo asincrono nel setter ricevo l'avviso CS4014 "non atteso" (o è solo in Framework 4.0?). Consiglieresti di sopprimere quell'avvertimento in tal caso?
Gerard,

@Gerard: la mia prima raccomandazione sarebbe quella di utilizzare il NotifyTaskCompletiondal mio progetto AsyncEx . Oppure puoi costruirne uno tuo; non è così difficile.
Stephen Cleary,

1
@Stephan: ok ci proverò. Forse è in atto un bell'articolo su questo scenario asincrono di visualizzazione di modelli di dati. Ad esempio il legame con {Binding PropName.Result}non è banale per me scoprirlo.
Gerard,

101

Non è possibile chiamarlo in modo asincrono, poiché non esiste un supporto di proprietà asincrono, ma solo metodi asincroni. Come tale, ci sono due opzioni, entrambe che sfruttano il fatto che i metodi asincroni nel CTP sono in realtà solo un metodo che restituisce Task<T>o Task:

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

O:

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

1
Grazie per la vostra risposta. Opzione A: la restituzione di un'attività non è realmente un allenamento a scopo vincolante. Opzione B: .Result, come accennato, blocca il thread dell'interfaccia utente (in Silverlight), richiedendo quindi l'operazione da eseguire su un thread in background. Vedrò se riesco a trovare una soluzione praticabile con questa idea.
Doguhan Uluca,

3
@duluca: potresti anche provare ad avere un metodo simile a private async void SetupList() { MyList = await MyAsyncMethod(); } Questo causerebbe l'impostazione di MyList (e quindi il bind automatico, se implementa INPC) non appena l'operazione di asincrono viene completata ...
Reed Copsey

La proprietà deve trovarsi in un oggetto che avevo dichiarato come risorsa di pagina, quindi avevo davvero bisogno che questa chiamata provenisse dal getter. Si prega di vedere la mia risposta per la soluzione che mi è venuta in mente.
Doguhan Uluca,

1
@duluca: Questo è stato, in effetti, quello che ti stavo suggerendo di fare ... Renditi conto, tuttavia, se accederai a Title più volte rapidamente, la tua soluzione attuale porterà a più chiamate per getTitle()simulatamente ...
Reed Copsey

Ottimo punto Sebbene non sia un problema per il mio caso specifico, un controllo booleano per isLoading risolverebbe il problema.
Doguhan Uluca,

55

Avevo davvero bisogno che la chiamata provenisse dal metodo get, a causa della mia architettura disaccoppiata. Quindi ho pensato alla seguente implementazione.

Utilizzo: il titolo si trova in un ViewModel o in un oggetto che è possibile dichiarare staticamente come risorsa di pagina. Associati ad esso e il valore verrà popolato senza bloccare l'interfaccia utente, quando getTitle () restituisce.

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
aggiornamento dal 18/07/2012 in Win8 RP dovremmo cambiare la chiamata del Dispatcher in: Window.Current.CoreWindow.Dispatcher.RunAsync (CoreDispatcherPriority.Normal, async () => {Title = wait GetTytleAsync (url);});
Anton Sizikov,

7
@ChristopherStevenson, l'ho pensato anch'io, ma non credo che sia così. Poiché il getter viene eseguito come un incendio e dimentica, senza chiamare il setter al termine, l'associazione non verrà aggiornata quando il getter ha terminato l'escissione.
Iain,

3
No, ha e aveva una condizione di gara, ma l'utente non lo vedrà a causa di 'RaisePropertyChanged ("Title")'. Ritorna prima del completamento. Ma, dopo il completamento, stai impostando la proprietà. Ciò genera l'evento PropertyChanged. Binder ottiene nuovamente il valore della proprietà.
Medeni Baykal,

1
Fondamentalmente, il primo getter restituirà un valore null, quindi verrà aggiornato. Nota che se vogliamo che getTitle venga chiamato ogni volta, potrebbe esserci un cattivo ciclo.
Tofutim,

1
Si dovrebbe anche essere consapevoli del fatto che eventuali eccezioni in quella chiamata asincrona verranno completamente ingoiate. Non riescono nemmeno a raggiungere il gestore delle eccezioni non gestite sull'applicazione se ne hai uno.
Philter,

9

Penso che possiamo aspettare che il valore restituisca prima il primo null e poi ottenga il valore reale, quindi nel caso di Pure MVVM (progetto PCL per esempio) penso che la seguente sia la soluzione più elegante:

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
Questo non genera avvisi del compilatoreCS4014: Async method invocation without an await expression
Nick,

6
Sii molto scettico nel seguire questo consiglio. Guarda questo video e prendi una decisione : channel9.msdn.com/Series/Three-Essential-Tips-for-Async/… .
Contango,

1
DOVREBBE evitare l'uso di metodi "async void"!
SuperJMN,

1
ogni grido dovrebbe arrivare con una risposta saggia, vuoi spiegarci @SuperJMN perché?
Juan Pablo Garcia Coello,

1
@Contango Buon video. Dice "Usa async voidsolo per gestori di alto livello e simili". Penso che questo possa qualificarsi come "e loro simili".
HappyNomad,

7

Puoi usare Taskcosì:

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

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

5

Ho pensato .GetAwaiter (). GetResult () era esattamente la soluzione a questo problema, no? per esempio:

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

5
È lo stesso che bloccare con .Result: non è asincrono e può provocare deadlock.
McGuireV10,

è necessario aggiungere IsAsync = True
Alexsandr Ter

Apprezzo il feedback sulla mia risposta; Mi piacerebbe davvero che qualcuno fornisse un esempio in cui questo deadlock si
blocca in

2

Poiché la "proprietà asincrona" si trova in un modello di visualizzazione, è possibile utilizzare AsyncMVVM :

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

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

Si occuperà del contesto di sincronizzazione e della notifica di modifica della proprietà per te.


Essendo una proprietà, deve essere.
Dmitry Shechtman,

Scusa, ma forse ho perso il punto di questo codice allora. Puoi elaborare per favore?
Patrick Hofman,

Le proprietà stanno bloccando per definizione. GetTitleAsync () funge da "async getter" senza zucchero sintattico.
Dmitry Shechtman,

1
@DmitryShechtman: No, non deve essere bloccato. Questo è esattamente ciò a cui servono le notifiche di modifica e le macchine a stati. E non stanno bloccando per definizione. Sono sincroni per definizione. Questo non è lo stesso del blocco. "Blocco" significa che possono svolgere un lavoro pesante e possono impiegare molto tempo per essere eseguiti. Questo a sua volta è esattamente ciò che le proprietà NON DOVREBBERO FARE.
quetzalcoatl,

1

Necromancing.
In .NET Core / NetStandard2, è possibile utilizzare Nito.AsyncEx.AsyncContext.Runinvece di 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"));
    }


}

Se scegli semplicemente System.Threading.Tasks.Task.Runo System.Threading.Tasks.Task<int>.Run, non funzionerebbe.


-1

Penso che il mio esempio di seguito possa seguire l'approccio di @ Stephen-Cleary, ma volevo fare un esempio in codice. Questo è per l'uso in un contesto di associazione dei dati, ad esempio Xamarin.

Il costruttore della classe - o in effetti il ​​setter di un'altra proprietà da cui dipende - può chiamare un vuoto asincrono che popolerà la proprietà al completamento dell'attività senza la necessità di attendere o bloccare. Quando finalmente ottiene un valore, aggiornerà l'interfaccia utente tramite il meccanismo NotifyPropertyChanged.

Non sono sicuro di alcun effetto collaterale di chiamare un vuoto aysnc da un costruttore. Forse un commentatore elaborerà la gestione degli errori ecc.

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

Quando ho riscontrato questo problema, il tentativo di eseguire una sincronicità del metodo asincrono da un setter o da un costruttore mi ha messo in un deadlock sul thread dell'interfaccia utente e l'utilizzo di un gestore di eventi ha richiesto troppe modifiche al progetto generale.
La soluzione era, come spesso accade, scrivere esplicitamente ciò che volevo accadesse implicitamente, ovvero avere un altro thread che gestisse l'operazione e ottenere il thread principale in attesa che finisse:

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

Si potrebbe sostenere che io abuso del framework, ma funziona.


-1

Esamino tutte le risposte ma tutte hanno un problema di prestazioni.

ad esempio 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 = await getTitle ();});

usa il dispatcher che non è una buona risposta.

ma esiste una soluzione semplice, basta farlo:

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

se la tua funzione è asyn usa getTitle (). wait () invece di getTitle ()
Mahdi Rastegari

-4

È possibile modificare la proprietà in Task<IEnumerable>

e fai qualcosa del tipo:

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

e usalo come aspetto MyList;

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.