Как вызвать асинхронный метод из метода получения или установки?


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 (), однако фактическая загрузка данных происходит намного позже. Пытаясь выполнить как можно меньше кода, getter и async чувствуют себя как идеальная комбинация.
— Догухан Улука


Джеймс и Рид, Вы, кажется, забываете, что всегда есть крайние случаи. В случае WCF я хотел убедиться, что данные, размещаемые в свойстве, являются правильными, и это нужно было проверить с использованием шифрования / дешифрования. Функции, которые я использую для расшифровки, используют асинхронную функцию от стороннего поставщика. (Не очень я могу сделать здесь).
— RashadRivera

Ответы:


211

Нет технической причиныasync свойства не разрешены в C #. Это было целенаправленное дизайнерское решение, потому что «асинхронные свойства» - это оксюморон.

Свойства должны возвращать текущие значения; они не должны начинать фоновые операции.

Обычно, когда кто-то хочет «асинхронное свойство», он действительно хочет получить один из них:

  1. Асинхронный метод, который возвращает значение. В этом случае измените свойство на asyncметод.
  2. Значение, которое может использоваться в привязке данных, но должно вычисляться / извлекаться асинхронно. В этом случае либо используйте asyncфабричный метод для содержащего объекта, либо используйте async InitAsync()метод. Значение, привязанное к данным, будет default(T)до тех пор, пока значение не будет вычислено / получено.
  3. Значение, которое дорого создать, но должно быть кэшировано для будущего использования. В этом случае используйте AsyncLazy из моего блога или библиотеки AsyncEx . Это даст вам awaitспособную собственность.

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


В пункте 2. imho вы не учитываете обычный сценарий, при котором установка свойства должна снова инициализировать базовые данные (не только в конструкторе). Есть ли другой способ, кроме использования Nito AsyncEx или использования Dispatcher.CurrentDispatcher.Invoke(new Action(..)?
— Джерард

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

1
@Stephan: хорошо, но когда я вызываю асинхронный метод в установщике, я получаю предупреждение CS4014 «не ожидается» (или это только в Framework 4.0?). Вы советуете подавить это предупреждение в таком случае?
— Джерард

@Gerard: Моей первой рекомендацией будет использование NotifyTaskCompletionмоего проекта AsyncEx . Или вы можете построить свой собственный; это не так сложно.
— Стивен Клири

1
@ Стефан: хорошо попробую. Возможно, есть хорошая статья об этом асинхронном сценарии привязки данных-viewmodel-script. Например, привязка к {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: Возврат Задачи на самом деле не работает для обязательной цели. Вариант Б: .Результат, как вы упомянули, блокирует поток пользовательского интерфейса (в Silverlight), поэтому требуется выполнение операции в фоновом потоке. Я посмотрю, смогу ли я найти работоспособное решение с этой идеей.
— Догухан Улука

3
@duluca: Вы также можете попробовать использовать метод, подобный private async void SetupList() { MyList = await MyAsyncMethod(); } этому. Это заставит MyList быть установленным (и затем автоматически связываться, если он реализует INPC), как только асинхронная операция завершится ...
— Рид Копси,

Свойство должно быть в объекте, который я объявил как ресурс страницы, поэтому мне действительно нужно, чтобы этот вызов исходил от геттера. Пожалуйста, смотрите мой ответ для решения, которое я придумал.
— Догухан Улука

1
@duluca: По сути, это было то, что я и предлагал вам сделать ... Поймите, однако, что если вы получите быстрый доступ к Title несколько раз, ваше текущее решение приведет к одновременным одновременным вызовам getTitle()...
— Reed Copsey

Очень хороший момент. Хотя это не проблема для моего конкретного случая, логическая проверка isLoading решит проблему.
— Догухан Улука

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
upd 18/07/2012 в Win8 RP мы должны изменить вызов Dispatcher на: Window.Current.CoreWindow.Dispatcher.RunAsync (CoreDispatcherPriority.Normal, async () => {Title = await GetTytleAsync (url);});
— Антон Сизиков

7
@ChristopherStevenson, я тоже так думал, но я не верю, что это так. Поскольку метод получения выполняется как запуск и забыть, без вызова метода установки по завершении привязка не будет обновлена, когда метод получения завершит исключение.
— Иэн

3
Нет, он имел и имел состояние гонки, но пользователь не увидит его из-за 'RaisePropertyChanged ("Title") ". Он возвращается до завершения. Но после завершения вы устанавливаете свойство. Это запускает событие PropertyChanged. Binder снова получает значение свойства.
— Медени Байкал

1
По сути, первый получатель вернет нулевое значение, а затем обновится. Обратите внимание, что если мы хотим, чтобы getTitle вызывался каждый раз, это может быть плохой цикл.
— тофутим

1
Вы также должны знать, что любые исключения в этом асинхронном вызове будут полностью проглочены. Они даже не доходят до вашего обработчика необработанных исключений в приложении, если он у вас есть.
— Филтер

9

Я думаю, что мы можем подождать, пока значение вернет сначала значение null, а затем получит реальное значение, поэтому в случае 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
— Ник

6
Будьте очень скептичны в следовании этому совету. Посмотрите это видео и решите сами: channel9.msdn.com/Series/Three-Essential-Tips-for-Async/… .
— Контанго

1
Вы ДОЛЖНЫ избегать использования методов "async void"!
— 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 = True
— Александр Тер

Я ценю отзывы о моем ответе; Я бы очень хотел, чтобы кто-нибудь
— привел

2

Поскольку ваше «свойство async» находится в модели представления, вы можете использовать AsyncMVVM :

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

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

Он позаботится об уведомлении о контексте синхронизации и уведомлении об изменении свойства.


Будучи собственностью, это должно быть.
— Дмитрий Шехтман

Извините, но, возможно, я упустил смысл этого кода тогда. Можете ли вы уточнить, пожалуйста?
— Патрик Хофман

Свойства блокируются по определению. GetTitleAsync () служит «асинхронным геттером» без синтаксического сахара.
— Дмитрий Шехтман

1
@DmitryShechtman: Нет, это не должно блокировать. Это именно то, для чего нужны уведомления об изменениях и конечные автоматы. И они не блокируют по определению. Они синхронны по определению. Это не то же самое, что блокировка. «Блокировка» означает, что они могут выполнять тяжелую работу и могут требовать значительного времени для выполнения. Это, в свою очередь, именно то, что свойства не должны делать.
— Кецалькоатль

1

Necromancing.
В .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

Я думаю, что мой пример ниже может следовать подходу @ Stephen-Cleary, но я хотел бы привести закодированный пример. Это для использования в контексте привязки данных, например, Xamarin.

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

Я не уверен в каких-либо побочных эффектах вызова aysnc void от конструктора. Возможно, комментатор остановится на обработке ошибок и т. Д.

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 () => {Title = await 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.