Як викликати метод асинхронізації від геттера чи сетера?


223

Що може бути найелегантнішим способом викликати метод асинхрування від геттера чи сетера в C #?

Ось псевдо-код, який допоможе пояснити себе.

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

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

4
Моє питання було б чому. Властивість повинна імітувати щось на зразок поля в тому, що воно, як правило, повинно виконувати невелику (або принаймні дуже швидку) роботу. Якщо у вас є довготривала властивість, набагато краще записати це як метод, щоб абонент знав, що це складніший склад роботи.
— Джеймс Майкл Заєць

@James: Це абсолютно правильно - і я підозрюю, що тому явно це не підтримувалося в CTP. При цьому, ви завжди можете зробити властивість типу Task<T>, яка повернеться негайно, мати нормальну семантику властивостей і все ж дозволити обробляти речі асинхронно за потребою.
— Рід Копсей

17
@James Моя потреба виникає у використанні Mvvm та Silverlight. Я хочу мати змогу прив’язатись до властивості, де завантаження даних здійснюється ліниво. Клас розширення ComboBox, який я використовую, вимагає, щоб прив'язка відбулася на етапі InitializeComponent (), однак фактичне завантаження даних відбувається набагато пізніше. Намагаючись виконати якомога менше коду, геттер та асинхрон відчувають себе ідеальним поєднанням.
— Doguhan Uluca


Джеймс і Рід, ви, здається, забуваєте, що завжди є випадки. У випадку WCF я хотів переконатися, що дані, розміщені у властивості, є правильними та їх потрібно було перевірити за допомогою шифрування / дешифрування. Функції, які я використовую для розшифровки, використовують функцію асинхронізації від сторонніх постачальників. (НЕ МНОГО Я можу зробити ТУТ).
— RashadRivera

Відповіді:


211

Немає технічної причини, що asyncвластивості заборонено в C #. Це було цілеспрямоване дизайнерське рішення, адже "асинхронні властивості" - це оксиморон.

Властивості повинні повертати поточні значення; вони не повинні починати фонові операції.

Зазвичай, коли хтось хоче "асинхронну властивість", те, що вони дійсно хочуть, є одним із таких:

  1. Асинхронний метод, який повертає значення. У цьому випадку змініть властивість на asyncметод.
  2. Значення, яке може бути використане для прив'язки даних, але повинно бути обчислено / отримано асинхронно. У цьому випадку або використовуйте asyncзаводський метод, що містить об'єкт, або використовуйте async InitAsync()метод. Значення, пов'язане з даними, буде default(T)до тих пір, поки значення не буде обчислено / отримано.
  3. Значення, яке дорого створити, але його слід кешувати для подальшого використання. У цьому випадку використовуйте AsyncLazy з мого блогу чи бібліотеки AsyncEx . Це дасть вам awaitможливість власності.

Оновлення: я висвітлюю асинхронні властивості в одному з моїх останніх публікацій блогу "async OOP".


У пункті 2. Якщо ви не враховуєте звичайний сценарій, коли встановлення властивості має знову ініціалізувати основні дані (не тільки в конструкторі). Чи є інший спосіб, крім використання Nito AsyncEx або використання Dispatcher.CurrentDispatcher.Invoke(new Action(..)?
— Джерард

@Gerard: Я не розумію, чому точка (2) не працювала б у такому випадку. Просто реалізуйте INotifyPropertyChanged, а потім вирішіть, чи потрібно повернути старе значення або default(T)під час польоту асинхронного оновлення.
— Стівен Клірі

1
@Stephan: гаразд, але коли я викликаю метод асинхронізації в сетері, я отримую CS4014 попередження "не чекаю" (чи це лише у Framework 4.0?). Чи радите ви придушити це попередження в такому випадку?
— Джерард

@Gerard: Першою моєю рекомендацією буде використання NotifyTaskCompletionмого проекту AsyncEx . Або ви можете побудувати свій власний; це не так складно.
— Стівен Клірі

1
@Stephan: гаразд, спробуй це. Можливо, приємна стаття про цей асинхронний сценарій прив'язки даних-viewmodel є в наявності. Наприклад, прив'язка до {Binding PropName.Result}мене не банальна для того, щоб дізнатися.
— Джерард

101

Ви не можете назвати це асинхронно, оскільки не існує асинхронної підтримки властивостей, а лише асинхронних методів. Таким чином, є два варіанти, обидва користуються тим, що асинхронні методи в CTP - це дійсно просто метод, який повертає Task<T>або Task:

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

Або:

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

1
Спасибі за вашу відповідь. Варіант A: Повернення Завдання насправді не тренується з обов'язковою метою. Варіант B: .Результат, як ви вже згадували, блокує потік інтерфейсу (у Silverlight), тому вимагає виконання операції на фоновому потоці. Я побачу, чи зможу я придумати ефективне рішення з цією ідеєю.
— Doguhan Uluca

3
@duluca: Ви також можете спробувати такий метод, як private async void SetupList() { MyList = await MyAsyncMethod(); } це. Це призведе до встановлення MyList (а потім автоматично прив'язування, якщо він реалізує INPC), як тільки операція асинхронізації завершиться ...
— Reed Copsey

Власність повинна знаходитись в об'єкті, який я оголосив як ресурс сторінки, тому мені справді потрібен цей виклик від джерела. Будь ласка, дивіться мою відповідь щодо рішення, яке я придумав.
— Doguhan Uluca

1
@duluca: Це було, фактично, те, що я пропонував вам зробити ... Хоча, зрозумійте, це, якщо ви отримаєте доступ до заголовку кілька разів швидко, ваше поточне рішення призведе до безлічі дзвінків, щоб getTitle()симультанно ...
— Рід Копсей

Дуже хороший момент. Хоча це не проблема для мого конкретного випадку, булева перевірка на isLoading вирішить проблему.
— Doguhan Uluca

55

Мені дуже потрібен дзвінок, що походить від методу get, завдяки моїй роз'єднаній архітектурі. Тому я придумав таку реалізацію.

Використання: Назва знаходиться у ViewModel або об'єкті, який ви можете статично оголосити ресурсом сторінки. Прив’яжіть до нього, і значення заповниться без блокування інтерфейсу користувача, коли getTitle () повернеться.

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 у Win8 RP, нам слід змінити виклик диспетчера на: Window.Current.CoreWindow.Dispatcher.RunAsync (CoreDispatcherPriority.Normal, async () => {Назва = очікувати GetTytleAsync (url);});
— Антон Сизіков

7
@ChristopherStevenson, я теж думав, але я не вірю, що це так. Оскільки геттер виконується як пожежа та забудьте, не викликаючи сетер після завершення, прив'язка не буде оновлена, коли геттер закінчить видалення.
— Iain

3
Ні, у нього був і стан перегонів, але користувач не побачить його через 'RaisePropertyChanged ("Title") ". Він повертається до завершення. Але після завершення ви встановлюєте властивість. Це спричиняє подію PropertyChanged. Біндер знову отримує вартість майна.
— Медені Байкал

1
В основному, перший отримувач поверне нульове значення, потім оновиться. Зауважте, що якщо ми хочемо, щоб getTitle викликали кожен раз, може виникнути поганий цикл.
— tofutim

1
Ви також повинні знати, що будь-які винятки в цьому виклику асинхронізації будуть повністю проковтовані. Вони навіть не звертаються до вашого необробленого обробника винятків у програмі, якщо він у вас є.
— Філтер

9

Я думаю, що ми можемо чекати, коли значення просто поверне нуль, а потім отримає реальне значення, тож у випадку Pure MVVM (наприклад, PCL-проект), я думаю, що наступне - це найелегантніше рішення:

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
Чи не породжує це попередження компілятораCS4014: Async method invocation without an await expression
— Nick

6
Будьте дуже скептично поставлені до цієї поради. Перегляньте це відео, тоді передумайте: channel9.msdn.com/Series/Three-Essential-Tips-for-Async/… .
— Контанго

1
Ви ДОЛЖЕНО уникати використання методів "асинхронізації недійсних"!
— SuperJMN

1
на кожен крик мусить відповісти мудра відповідь, ти б @SuperJMN пояснив нам чому?
— Хуан Пабло Гарсія Коелло

1
@Contango Гарне відео. Він каже: "Використовуйте async voidлише для обробників вищого рівня та їм подібних". Я думаю, це може бути кваліфіковано як "і їм подібне".
— HappyNomad

7

Ви можете використовувати Taskтак:

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

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

5

Я думав, що саме GetAwaiter (). GetResult () було саме вирішенням цієї проблеми, ні? наприклад:

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

5
Це те саме, що просто блокувати .Result- це не асинхронізація, і це може призвести до тупиків.
— McGuireV10

вам потрібно додати IsAsync = Істинно
— Alexsandr Ter

Я вдячний за відгук про свою відповідь; Я б дуже хотів, щоб хтось
— наводив

2

Оскільки ваше "властивість асинхронізації" перебуває у моделі перегляду, ви можете використовувати AsyncMVVM :

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

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

Він подбає про контекст синхронізації та повідомлення про зміну властивості для вас.


Будучи власністю, це має бути.
— Дмитро Шехтман

Вибачте, але, можливо, тоді я пропустив точку цього коду. Чи можете ви докладно уточнити?
— Патрік Хофман

Властивості блокуються за визначенням. GetTitleAsync () виконує функцію "асинхронного одержувача", не використовуючи синтаксичний цукор.
— Дмитро Шехтман

1
@DmitryShechtman: Ні, його не потрібно блокувати. Саме для цього призначено сповіщення про зміни та стан машини. І вони не блокуються за визначенням. Вони є синхронними за визначенням. Це не те саме, що блокування. "Блокування" означає, що вони можуть робити важку роботу і можуть зайняти значну кількість часу для виконання. Це, в свою чергу, саме те, якими властивостями НЕ БУДЕ БУТИ ВЗАЄМО
— quetzalcoatl

1

Некромантування.
У .NET Core / NetStandard2 ви можете використовувати Nito.AsyncEx.AsyncContext.Runзамість 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"));
    }


}

Якщо ви просто вибрали System.Threading.Tasks.Task.Runабо System.Threading.Tasks.Task<int>.Run, то це не вийде.


-1

Я думаю, що мій приклад нижче може слідувати підходу Стефана-Клірі, але я хотів навести закодований приклад. Це для використання в контексті зв'язування даних, наприклад, Xamarin.

Конструктор класу - або взагалі встановник іншого властивості, від якого він залежить - може викликати асинхронічну порожнечу, яка заповнить властивість після завершення завдання без необхідності очікування чи блоку. Коли він нарешті отримає значення, він оновить ваш інтерфейс користувача через механізм NotifyPropertyChanged.

Я не впевнений у будь-яких побічних ефектах виклику айсн-пустоти від конструктора. Можливо, коментатор розробить питання щодо помилок тощо.

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

Коли я зіткнувся з цією проблемою, намагаючись запустити синхронність методу асинхронізації або від сеттера, або від конструктора, мене ввели в глухий кут на потоці інтерфейсу користувача, а використання обробника подій вимагало занадто багато змін у загальному дизайні.
Рішення полягало в тому, що якраз явно пишу явно те, що я хотів, щоб це відбулося неявно, а це - мати ще одну нитку для обробки операції та змусити основну нитку дочекатися її завершення:

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

Ви можете заперечити, що я зловживаю рамкою, але вона працює.


-1

Я переглядаю всі відповіді, але у всіх є питання щодо ефективності.

наприклад у:

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 () => {Заголовок = очікуємо getTitle ();});

використовувати диспетчер, що не є гарною відповіддю.

але є просте рішення, просто зробіть це:

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

якщо ваша функція - асин, використовуйте getTitle (). wait () замість getTitle ()
— Махді Растегарі

-4

Ви можете змінити властивість на Task<IEnumerable>

і робити щось на кшталт:

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

і використовувати його, як очікувати MyList;

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.