Como chamar um método assíncrono de um getter ou setter?


223

Qual seria a maneira mais elegante de chamar um método assíncrono de um getter ou setter em C #?

Aqui estão alguns pseudo-códigos para ajudar a me explicar.

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

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

4
Minha pergunta seria o porquê. Uma propriedade deve imitar algo como um campo, na medida em que normalmente deve executar pouco (ou pelo menos muito rápido) trabalho. Se você tem uma propriedade de longa duração, é muito melhor escrevê-la como um método, para que o chamador saiba que é um corpo de trabalho mais complexo.
James Michael Hare

@ James: Isso é exatamente correto - e suspeito que é por isso que isso não foi explicitamente suportado no CTP. Dito isto, você sempre pode criar a propriedade do tipo Task<T>, que retornará imediatamente, ter semântica de propriedade normal e ainda permitir que as coisas sejam tratadas de forma assíncrona, conforme necessário.
Reed Copsey

17
@ James Minha necessidade surge do uso de Mvvm e Silverlight. Eu quero poder vincular a uma propriedade, onde o carregamento dos dados é feito preguiçosamente. A classe de extensão ComboBox que estou usando requer que a ligação ocorra no estágio InitializeComponent (), no entanto, o carregamento real dos dados ocorre muito mais tarde. Ao tentar realizar com o mínimo de código possível, getter e async parecem a combinação perfeita.
Doguhan Uluca


James e Reed, você parece estar esquecendo que sempre existem casos extremos. No caso do WCF, eu queria verificar se os dados sendo colocados em uma propriedade estão corretos e precisavam ser verificados usando criptografia / descriptografia. As funções que eu uso para descriptografia empregam uma função assíncrona de um fornecedor terceirizado. (NÃO POSSO FAZER AQUI).
RashadRivera 17/08/19

Respostas:


211

Não há razão técnica para que asyncpropriedades não sejam permitidas em C #. Foi uma decisão de projeto proposital, porque "propriedades assíncronas" é um oxímoro.

Propriedades devem retornar valores atuais; eles não devem iniciar operações em segundo plano.

Normalmente, quando alguém deseja uma "propriedade assíncrona", o que realmente deseja é um destes:

  1. Um método assíncrono que retorna um valor. Nesse caso, altere a propriedade para um asyncmétodo
  2. Um valor que pode ser usado na ligação de dados, mas deve ser calculado / recuperado de forma assíncrona. Nesse caso, use um asyncmétodo de fábrica para o objeto que contém ou use um async InitAsync()método. O valor vinculado aos dados será default(T)até que o valor seja calculado / recuperado.
  3. Um valor que é caro para criar, mas deve ser armazenado em cache para uso futuro. Nesse caso, use AsyncLazy no meu blog ou na biblioteca AsyncEx . Isso lhe dará uma awaitpropriedade capaz.

Update: Eu cobrir propriedades assíncronas em um dos meus últimos "assíncronos OOP" posts.


No ponto 2. imho, você não leva em consideração o cenário regular em que a configuração da propriedade deve inicializar novamente os dados subjacentes (não apenas no construtor). Existe outra maneira além de usar o Nito AsyncEx ou usar Dispatcher.CurrentDispatcher.Invoke(new Action(..)?
Gerard

@ Gerard: Não vejo por que o ponto (2) não funcionaria nesse caso. Basta implementar INotifyPropertyChangede decidir se deseja que o valor antigo seja retornado ou default(T)enquanto a atualização assíncrona estiver em andamento.
precisa

1
@ Stephan: ok, mas quando eu chamo o método assíncrono no setter, recebo o aviso CS4014 "não aguardado" (ou isso é apenas no Framework 4.0?). Você aconselha a suprimir esse aviso nesse caso?
Gerard

@ Gerard: Minha primeira recomendação seria usar o NotifyTaskCompletiondo meu projeto AsyncEx . Ou você pode construir o seu próprio; não é tão difícil.
Stephen Cleary

1
@ Stephan: ok vai tentar isso. Talvez um bom artigo sobre esse cenário de conexão de dados assíncrono esteja presente. Por exemplo, vincular a {Binding PropName.Result}não é trivial para eu descobrir.
Gerard

101

Você não pode chamá-lo de forma assíncrona, pois não há suporte a propriedades assíncronas, apenas métodos assíncronos. Como tal, existem duas opções, ambas aproveitando o fato de que os métodos assíncronos no CTP são realmente apenas um método que retorna Task<T>ou Task:

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

Ou:

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

1
Obrigado pela sua resposta. Opção A: O retorno de uma tarefa não funciona realmente para fins vinculativos. Opção B: .Result, como você mencionou, bloqueia o thread da interface do usuário (no Silverlight), exigindo, portanto, que a operação seja executada em um thread em segundo plano. Vou ver se consigo encontrar uma solução viável com essa idéia.
Doguhan Uluca 07/07

3
@duluca: Você também pode tentar ter um método que é como private async void SetupList() { MyList = await MyAsyncMethod(); } Isso faria com que MyList a ser definido (e, então, automaticamente ligam, se ele implementa INPC), logo que os conclui operação assíncrona ...
Reed Copsey

A propriedade precisa estar em um objeto que eu havia declarado como recurso de página, então eu realmente precisava que essa chamada se originasse do getter. Por favor, veja minha resposta para a solução que eu criei.
Doguhan Uluca 07/07

1
@duluca: Isso foi, efetivamente, o que eu estava sugerindo que você ... perceber, porém, isso se você acessar Título várias vezes rapidamente, sua solução atual levará a várias chamadas para getTitle()simulatenously ...
Reed Copsey

Muito bom ponto. Embora não seja um problema para o meu caso específico, uma verificação booleana de isLoading resolveria o problema.
Doguhan Uluca 07/07

55

Eu realmente precisava da chamada para se originar do método get, devido à minha arquitetura desacoplada. Então, eu vim com a seguinte implementação.

Uso: o título está em um ViewModel ou em um objeto que você pode declarar estaticamente como um recurso de página. Associe-se a ele e o valor será preenchido sem bloquear a interface do usuário, quando getTitle () retornar.

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 no Win8 RP, devemos alterar a chamada do Dispatcher para: Window.Current.CoreWindow.Dispatcher.RunAsync (CoreDispatcherPriority.Normal, async () => {Title = aguardar GetTytleAsync (url);});
Anton Sizikov 18/07/2012

7
@ChristopherStevenson, eu também pensava, mas não acredito que seja esse o caso. Como o getter está sendo executado como um incêndio e esqueça, sem chamar o setter após a conclusão, a ligação não será atualizada quando o getter terminar a extração.
Iain

3
Não, ele possui e teve uma condição de corrida, mas o usuário não a verá devido a 'RaisePropertyChanged ("Title")'. Ele retorna antes de ser concluído. Mas, após a conclusão, você está definindo a propriedade. Isso dispara o evento PropertyChanged. Binder obtém o valor da propriedade novamente.
Medeni Baykal

1
Basicamente, o primeiro getter retornará um valor nulo e será atualizado. Observe que, se quisermos que getTitle seja chamado a cada vez, pode haver um loop ruim.
Tofutim

1
Você também deve estar ciente de que quaisquer exceções nessa chamada assíncrona serão completamente engolidas. Eles nem chegam ao seu manipulador de exceções não tratadas no aplicativo, se você tiver um.
Philter

9

Acho que podemos aguardar o valor retornando primeiro nulo e, em seguida, obtendo o valor real, portanto, no caso do Pure MVVM (projeto PCL por exemplo), acho que a seguinte é a solução mais elegante:

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
Isso não gerar avisos do compiladorCS4014: Async method invocation without an await expression
Nick

6
Seja muito cético em seguir este conselho. Assista a este vídeo e decida : channel9.msdn.com/Series/Three-Essential-Tips-for-Async/… .
Contango5 /

1
Você deve evitar o uso de métodos "async void"!
precisa saber é o seguinte

1
cada grito deve vir com uma resposta sábia, o @SuperJMN nos explica o porquê?
Juan Pablo Garcia Coello 26/01

1
@Contango Bom vídeo. Ele diz: "Use async voidapenas para manipuladores de nível superior e afins". Eu acho que isso pode se qualificar como "e seu gosto".
HappyNomad

7

Você pode usar Taskassim:

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

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

5

Eu pensei que. GetAwaiter (). GetResult () era exatamente a solução para este problema, não? por exemplo:

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

5
É o mesmo que apenas bloquear .Result- não é assíncrono e pode resultar em conflitos.
McGuireV10

você precisa adicionar IsAsync = True
Alexsandr Ter

Agradeço o feedback da minha resposta; Eu realmente adoraria que alguém desse um exemplo em que esses impasses possam ser
vistos

2

Como sua "propriedade assíncrona" está em um modelo de exibição, você pode usar o AsyncMVVM :

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

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

Ele cuidará do contexto de sincronização e da notificação de alteração de propriedade para você.


Sendo uma propriedade, tem que ser.
Dmitry Shechtman

Desculpe, mas talvez eu tenha esquecido o objetivo desse código. Você pode elaborar por favor?
Patrick Hofman

As propriedades estão bloqueando por definição. GetTitleAsync () serve como um "get assíncrono" sem o açúcar sintático.
Dmitry Shechtman

1
@DmitryShechtman: Não, não precisa estar bloqueando. É exatamente para isso que servem as notificações de alteração e as máquinas de estado. E eles não estão bloqueando por definição. Eles são síncronos por definição. Isso não é o mesmo que bloquear. "Bloqueio" significa que eles podem fazer um trabalho pesado e consumir um tempo considerável para serem executados. Isso, por sua vez, é exatamente o que as propriedades NÃO DEVEM FAZER.
Quetzalcoatl

1

Necromante.
No .NET Core / NetStandard2, você pode usar em Nito.AsyncEx.AsyncContext.Runvez de 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"));
    }


}

Se você simplesmente escolheu System.Threading.Tasks.Task.Runou System.Threading.Tasks.Task<int>.Run, então não funcionaria.


-1

Acho que meu exemplo abaixo pode seguir a abordagem de Stephen-Cleary, mas eu queria dar um exemplo codificado. Isso é para uso em um contexto de ligação de dados, por exemplo, Xamarin.

O construtor da classe - ou mesmo o criador de outra propriedade da qual depende - pode chamar um vazio assíncrono que preencherá a propriedade após a conclusão da tarefa sem a necessidade de aguardar ou bloquear. Quando finalmente obtiver um valor, atualizará sua interface do usuário por meio do mecanismo NotifyPropertyChanged.

Não tenho certeza sobre nenhum efeito colateral de chamar um aysnc de um construtor. Talvez um comentarista elabore um tratamento de erro, etc.

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

Quando me deparei com esse problema, tentar executar uma sincronicidade de método assíncrono de um setter ou de um construtor me levou a um impasse no encadeamento da interface do usuário, e o uso de um manipulador de eventos exigiu muitas alterações no design geral.
A solução era, como sempre é, escrever explicitamente o que eu queria que acontecesse implicitamente, que era ter outro thread para lidar com a operação e fazer com que o thread principal esperasse o término:

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

Você poderia argumentar que eu abusei da estrutura, mas funciona.


-1

Analiso todas as respostas, mas todas têm um problema de desempenho.

por exemplo em:

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

use despachante que não é uma boa resposta.

mas existe uma solução simples, basta fazê-lo:

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

se sua função é assíncrona, use getTitle (). wait () em vez de getTitle ()
Mahdi Rastegari

-4

Você pode alterar a propriedade para Task<IEnumerable>

e faça algo como:

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

e use-o como aguarde MyList;

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.