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


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.