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


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.