Jak wywołać metodę asynchroniczną z gettera lub settera?


223

Jaki byłby najbardziej elegancki sposób wywołania metody asynchronicznej z modułu pobierającego lub ustawiającego w języku C #?

Oto pseudo-kod, który pomoże mi się wyjaśnić.

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

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

4
Moje pytanie brzmi: dlaczego. Właściwość ma naśladować coś w rodzaju pola, ponieważ zazwyczaj powinna wykonywać niewielką (lub przynajmniej bardzo szybką) pracę. Jeśli masz długoterminową właściwość, znacznie lepiej jest napisać ją jako metodę, aby osoba dzwoniąca wiedziała, że ​​jest to bardziej złożony proces.
James Michael Hare,

@James: Dokładnie tak - podejrzewam, że właśnie dlatego nie było to wyraźnie obsługiwane w CTP. To powiedziawszy, zawsze możesz uczynić właściwość typu Task<T>, która natychmiast zwróci, będzie miała normalną semantykę właściwości i nadal pozwoli na traktowanie rzeczy asynchronicznie w razie potrzeby.
Reed Copsey,

17
@James Moja potrzeba wynika z używania Mvvm i Silverlight. Chcę mieć możliwość powiązania z właściwością, w której ładowanie danych odbywa się leniwie. Klasa rozszerzenia ComboBox, której używam, wymaga powiązania na etapie InitializeComponent (), jednak faktyczne ładowanie danych następuje znacznie później. Próbując osiągnąć jak najmniej kodu, getter i asynchronia wydają się idealną kombinacją.
Doguhan Uluca,


James i Reed, zdajesz się zapominać, że zawsze są przypadki skrajne. W przypadku WCF chciałem sprawdzić, czy dane umieszczone we właściwości są poprawne i trzeba je zweryfikować za pomocą szyfrowania / deszyfrowania. Funkcje, których używam do deszyfrowania, wykorzystują funkcję asynchroniczną od zewnętrznego dostawcy. (NIE DUŻO MOGĘ ZROBIĆ TUTAJ).
RashadRivera

Odpowiedzi:


211

Nie ma technicznego powodu, aby asyncwłaściwości nie były dozwolone w języku C #. Była to celowa decyzja projektowa, ponieważ „właściwości asynchroniczne” to oksymoron.

Właściwości powinny zwracać bieżące wartości; nie powinny rozpoczynać operacji w tle.

Zwykle, gdy ktoś chce „właściwości asynchronicznej”, tak naprawdę chce jednej z tych rzeczy:

  1. Metoda asynchroniczna, która zwraca wartość. W takim przypadku zmień właściwość na asyncmetodę.
  2. Wartość, która może zostać użyta w powiązaniu danych, ale musi zostać obliczona / pobrana asynchronicznie. W takim przypadku użyj asyncmetody fabrycznej dla obiektu zawierającego lub użyj async InitAsync()metody. Wartość powiązana z danymi będzie obowiązywać default(T)do momentu obliczenia / pobrania wartości.
  3. Wartość, której utworzenie jest kosztowne, ale należy ją zapisać w pamięci podręcznej do wykorzystania w przyszłości. W takim przypadku skorzystaj AsyncLazy z mojego bloga lub biblioteki AsyncEx . To da ci awaitwłaściwość.

Aktualizacja: I obejmować właściwości asynchronicznych w jednym z moich ostatnich „asynchronicznych OOP” blogach.


W punkcie 2. imho nie bierze się pod uwagę zwykłego scenariusza, w którym ustawienie właściwości powinno ponownie zainicjować podstawowe dane (nie tylko w konstruktorze). Czy jest inny sposób niż używanie Nito AsyncEx lub używanie Dispatcher.CurrentDispatcher.Invoke(new Action(..)?
Gerard,

@ Gerard: Nie rozumiem, dlaczego punkt (2) nie działałby w takim przypadku. Po prostu zaimplementuj INotifyPropertyChanged, a następnie zdecyduj, czy chcesz zwrócić starą wartość, czy default(T)też trwa aktualizacja asynchroniczna.
Stephen Cleary,

1
@Stephan: ok, ale kiedy wywołuję metodę asynchroniczną w seterze, pojawia się ostrzeżenie CS4014 „nie oczekiwane” (czy to tylko w Framework 4.0?). Czy w takim przypadku radzisz ukryć to ostrzeżenie?
Gerard,

@ Gerard: Moim pierwszym zaleceniem byłoby użycie NotifyTaskCompletionz mojego projektu AsyncEx . Lub możesz zbudować własny; to nie jest takie trudne.
Stephen Cleary,

1
@Stephan: ok spróbuje tego. Być może istnieje ciekawy artykuł na temat tego scenariusza asynchronicznego wiązania danych - viewmodel-scenariusza. Np. Wiązanie się z {Binding PropName.Result}nie jest dla mnie trywialne.
Gerard,

101

Nie można go nazwać asynchronicznie, ponieważ nie ma asynchronicznej obsługi właściwości, tylko metody asynchroniczne. Jako takie istnieją dwie opcje, obie wykorzystujące fakt, że metody asynchroniczne w CTP są tak naprawdę tylko metodą zwracającą Task<T>lub Task:

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

Lub:

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

1
Dziękuję za odpowiedź. Opcja A: Zwrócenie zadania tak naprawdę nie jest treningiem do celów wiążących. Opcja B: .Result, jak wspomniałeś, blokuje wątek interfejsu użytkownika (w Silverlight), dlatego wymaga wykonania operacji na wątku w tle. Zobaczę, czy uda mi się wymyślić praktyczne rozwiązanie z tym pomysłem.
Doguhan Uluca,

3
@duluca: Możesz również spróbować metody podobnej do private async void SetupList() { MyList = await MyAsyncMethod(); } tej. Spowodowałoby to ustawienie MyList (a następnie automatyczne powiązanie, jeśli implementuje INPC), jak tylko operacja asynchroniczna zakończy się ...
Reed Copsey

Właściwość musi znajdować się w obiekcie, który zadeklarowałem jako zasób strony, więc naprawdę potrzebowałem tego wywołania pochodzącego z gettera. Proszę zobaczyć moją odpowiedź na rozwiązanie, które wymyśliłem.
Doguhan Uluca,

1
@duluca: Właśnie to sugerowałem, aby zrobić ... Uświadom sobie jednak, że jeśli szybko uzyskasz dostęp do tytułu wiele razy, twoje obecne rozwiązanie doprowadzi do wielu połączeń jednocześnie getTitle()...
Reed Copsey

Bardzo dobra uwaga. Chociaż nie jest to problem w moim konkretnym przypadku, sprawdzenie logiczne dla isLoading rozwiązałoby problem.
Doguhan Uluca,

55

Naprawdę potrzebowałem wywołania, aby pochodziło z metody get, ze względu na moją oddzieloną architekturę. Więc wymyśliłem następującą implementację.

Użycie: Tytuł znajduje się w ViewModel lub obiekcie, który można statycznie zadeklarować jako zasób strony. Powiąż z nim, a wartość zostanie zapełniona bez blokowania interfejsu użytkownika, gdy funkcja getTitle () zwróci.

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
updfrom 18/07/2012 w Win8 RP powinniśmy zmienić wywołanie Dispatchera na: Window.Current.CoreWindow.Dispatcher.RunAsync (CoreDispatcherPriority.Normal, async () => {Title = czekaj na GetTytleAsync (url);});
Anton Sizikov,

7
@ChristopherStevenson, ja też tak myślałem, ale nie sądzę, żeby tak było. Ponieważ getter jest wykonywany jako ogień i zapomina, bez wywołania setera po zakończeniu, wiązanie nie zostanie zaktualizowane, gdy getter zakończy ekscytację.
Iain

3
Nie, ma i miał wyścig, ale użytkownik go nie zobaczy z powodu „RaisePropertyChanged („ Title ”)”. Zwraca przed zakończeniem. Ale po zakończeniu ustawiasz właściwość. To wywołuje zdarzenie PropertyChanged. Binder ponownie otrzymuje wartość nieruchomości.
Medeni Baykal

1
Zasadniczo pierwszy moduł pobierający zwróci wartość zerową, a następnie zostanie zaktualizowany. Zauważ, że jeśli chcemy, aby getTitle był wywoływany za każdym razem, może wystąpić zła pętla.
tofutim

1
Należy również pamiętać, że wszelkie wyjątki w tym wywołaniu asynchronicznym zostaną całkowicie połknięte. Nawet nie docierają do nieobsługiwanego modułu obsługi wyjątków w aplikacji, jeśli go masz.
Philter,

9

Myślę, że możemy poczekać na wartość zwracającą najpierw zero, a następnie uzyskaną prawdziwą wartość, więc w przypadku Pure MVVM (na przykład projekt PCL) uważam, że najbardziej eleganckim rozwiązaniem jest:

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
Czy to nie generuje ostrzeżeń kompilatoraCS4014: Async method invocation without an await expression
Nick

6
Bądź bardzo sceptyczny, jeśli chodzi o przestrzeganie tej rady. Obejrzyj ten film, a następnie podejmij decyzję: channel9.msdn.com/Series/Three-Essential-Tips-for-Async/… .
Contango,

1
NALEŻY UNIKAĆ stosowania metod „asynchronicznej pustki”!
SuperJMN

1
każdy krzyk powinien zawierać mądrą odpowiedź, czy @SuperJMN wyjaśniłbyś nam dlaczego?
Juan Pablo Garcia Coello

1
@Contango Dobry film. Mówi: „Używaj async voidtylko dla osób zajmujących się obsługą najwyższego poziomu i podobnych”. Myślę, że można to zakwalifikować jako „i im podobne”.
HappyNomad,

7

Możesz użyć w Taskten sposób:

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

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

5

Myślałem, że .GetAwaiter (). GetResult () było właśnie rozwiązaniem tego problemu, nie? na przykład:

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

5
To tak samo, jak zwykłe blokowanie .Result- nie jest asynchroniczne i może doprowadzić do impasu.
McGuireV10,

musisz dodać IsAsync = True
Alexsandr Ter

Doceniam informację zwrotną na temat mojej odpowiedzi; Bardzo chciałbym, aby ktoś podał przykład, w którym impas ten pozwala mi zobaczyć go w akcji
bc3tech

2

Ponieważ Twoja „właściwość asynchroniczna” znajduje się w modelu widoku, możesz użyć AsyncMVVM :

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

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

Zadba o kontekst synchronizacji i powiadomienie o zmianie właściwości.


To musi być własność.
Dmitry Shechtman,

Przepraszam, ale może wtedy nie trafiłem w sedno tego kodu. Czy możesz opracować proszę?
Patrick Hofman,

Właściwości są z definicji blokowane. GetTitleAsync () służy jako „getter asynchroniczny” bez cukru syntaktycznego.
Dmitry Shechtman,

1
@DmitryShechtman: Nie, to nie musi być blokowanie. Właśnie do tego służą powiadomienia o zmianach i automaty stanów. I nie blokują z definicji. Są z definicji synchroniczne. To nie to samo, co blokowanie. „Blokowanie” oznacza, że ​​mogą wykonywać ciężką pracę i mogą wymagać dużo czasu na wykonanie. To z kolei jest dokładnie tym, czego NIE POWINNY BYĆ.
quetzalcoatl

1

Nekromancja.
W .NET Core / NetStandard2 możesz użyć Nito.AsyncEx.AsyncContext.Runzamiast 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"));
    }


}

Jeśli po prostu wybierzesz System.Threading.Tasks.Task.Runlub System.Threading.Tasks.Task<int>.Run, to nie zadziała.


-1

Myślę, że mój przykład poniżej może być zgodny z podejściem Stephena-Cleary'ego, ale chciałem podać zakodowany przykład. Jest to używane w kontekście powiązania danych, na przykład Xamarin.

Konstruktor klasy - a nawet ustawiający inną właściwość, od której jest zależny - może wywołać pustkę asynchroniczną, która zapełni właściwość po zakończeniu zadania bez potrzeby oczekiwania lub bloku. Kiedy w końcu uzyska wartość, zaktualizuje interfejs użytkownika za pośrednictwem mechanizmu NotifyPropertyChanged.

Nie jestem pewien co do jakichkolwiek skutków ubocznych wywołania aysnc void od konstruktora. Być może komentator opracuje obsługę błędów itp.

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

Kiedy natknąłem się na ten problem, próba uruchomienia synchronizacji metody asynchronicznej z narzędzia ustawiającego lub konstruktora doprowadziła mnie do impasu w wątku interfejsu użytkownika, a użycie procedury obsługi zdarzeń wymagało zbyt wielu zmian w ogólnym projekcie.
Rozwiązaniem było, jak to często bywa, po prostu jawne napisanie, co chciałem, niejawnie, co miało mieć inny wątek obsługujący operację i sprawienie, aby główny wątek czekał na zakończenie:

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

Można argumentować, że nadużywam frameworka, ale działa.


-1

Sprawdzam wszystkie odpowiedzi, ale wszystkie mają problem z wydajnością.

na przykład w:

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 = czekaj na getTitle ();});

użyj dyspozytora, co nie jest dobrą odpowiedzią.

ale istnieje proste rozwiązanie, po prostu zrób to:

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

jeśli twoja funkcja jest asyn, użyj getTitle (). wait () zamiast getTitle ()
Mahdi Rastegari

-4

Możesz zmienić właściwość na Task<IEnumerable>

i zrób coś takiego:

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

i używaj go tak, jak czekasz na MyList;

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.