¿Cómo llamar a un método asíncrono desde un getter o setter?

223

¿Cuál sería la forma más elegante de llamar a un método asíncrono desde un getter o setter en C #?

Aquí hay un pseudocódigo para ayudar a explicarme.

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

public IEnumerable MyList
{
    get
    {
         //call MyAsyncMethod() here
    }
}
Doguhan Uluca
fuente
44
Mi pregunta sería por qué. Se supone que una propiedad imita algo como un campo en el sentido de que normalmente debería realizar un trabajo pequeño (o al menos muy rápido). Si tiene una propiedad de larga duración, es mucho mejor escribirla como método para que la persona que llama sepa que es un trabajo más complejo.
James Michael Hare
@James: Eso es exactamente correcto, y sospecho que es por eso que explícitamente no fue compatible con el CTP. Dicho esto, siempre puede hacer que la propiedad de tipo Task<T>, que regresará de inmediato, tenga una semántica de propiedad normal y aún permita que las cosas se traten de forma asincrónica según sea necesario.
Reed Copsey
17
@ James Mi necesidad surge del uso de Mvvm y Silverlight. Quiero poder vincularme a una propiedad, donde la carga de los datos se realiza de forma perezosa. La clase de extensión ComboBox que estoy usando requiere que el enlace ocurra en la etapa InitializeComponent (), sin embargo, la carga de datos real ocurre mucho más tarde. Al tratar de lograr el menor código posible, getter y async se sienten como la combinación perfecta.
Doguhan Uluca
relacionado: stackoverflow.com/a/33942013/11635
Ruben Bartelink
James y Reed, parece que estás olvidando que siempre hay casos extremos. En el caso de WCF, quería verificar que los datos que se colocan en una propiedad sean correctos y que se hayan verificado mediante cifrado / descifrado. Las funciones que uso para descifrar emplean una función asíncrona de un proveedor externo. (NO MUCHO QUE PUEDO HACER AQUÍ).
RashadRivera

Respuestas:

211

No hay ninguna razón técnica para que las asyncpropiedades no estén permitidas en C #. Fue una decisión de diseño intencional, porque las "propiedades asincrónicas" son un oxímoron.

Las propiedades deben devolver los valores actuales; no deberían estar iniciando operaciones en segundo plano.

Por lo general, cuando alguien quiere una "propiedad asincrónica", lo que realmente quiere es uno de estos:

  1. Un método asincrónico que devuelve un valor. En este caso, cambie la propiedad a un asyncmétodo.
  2. Un valor que se puede usar en el enlace de datos pero que se debe calcular / recuperar de forma asincrónica. En este caso, use un asyncmétodo de fábrica para el objeto que lo contiene o use un async InitAsync()método. El valor vinculado a los datos será default(T)hasta que se calcule / recupere el valor.
  3. Un valor costoso de crear, pero que debe almacenarse en caché para su uso futuro. En este caso, utilícelo AsyncLazy desde mi blog o biblioteca AsyncEx . Esto le dará una awaitpropiedad capaz.

Actualización: cubro las propiedades asincrónicas en una de mis publicaciones de blog recientes "async OOP".

Stephen Cleary
fuente
En el punto 2. en mi opinión, no tiene en cuenta el escenario habitual en el que la configuración de la propiedad debería inicializar nuevamente los datos subyacentes (no solo en el constructor). ¿Hay alguna otra forma además de usar Nito AsyncEx o usar Dispatcher.CurrentDispatcher.Invoke(new Action(..)?
Gerard
@Gerard: No veo por qué el punto (2) no funcionaría en ese caso. Simplemente implemente INotifyPropertyChangedy luego decida si desea que se devuelva el valor anterior o default(T)mientras la actualización asincrónica está en curso.
Stephen Cleary
1
@Stephan: ok, pero cuando llamo al método asíncrono en el setter recibo la advertencia CS4014 "no esperado" (¿o es solo en Framework 4.0?). ¿Aconseja suprimir esa advertencia en tal caso?
Gerard
@Gerard: Mi primera recomendación sería usar el NotifyTaskCompletionde mi proyecto AsyncEx . O puedes construir el tuyo; no es tan dificil.
Stephen Cleary
1
@Stephan: ok lo intentaremos. Tal vez hay un buen artículo sobre este escenario de modelo de vista de enlace asíncrono. Por ejemplo, vincularme {Binding PropName.Result}no es trivial para mí descubrirlo.
Gerard
101

No puede llamarlo de forma asincrónica, ya que no hay soporte de propiedad asincrónica, solo métodos asincrónicos. Como tal, hay dos opciones, ambas aprovechando el hecho de que los métodos asincrónicos en el CTP son realmente solo un método que devuelve Task<T>o Task:

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

O:

// Make the property blocking
public IEnumerable MyList
{
    get
    {
         // Block via .Result
         return MyAsyncMethod().Result;
    }
}
Reed Copsey
fuente
1
Gracias por su respuesta. Opción A: la devolución de una tarea realmente no se realiza con fines vinculantes. Opción B: .Resultar, como usted menciona, bloquea el hilo de la interfaz de usuario (en Silverlight), por lo tanto, requiere que la operación se ejecute en un hilo de fondo. Veré si puedo encontrar una solución viable con esta idea.
Doguhan Uluca
3
@duluca: También podría intentar tener un método como private async void SetupList() { MyList = await MyAsyncMethod(); } este que haría que MyList se establezca (y luego se vincule automáticamente, si implementa INPC) tan pronto como se complete la operación asíncrona ...
Reed Copsey
La propiedad debe estar en un objeto que haya declarado como recurso de página, por lo que realmente necesitaba que esta llamada se originara en el captador. Por favor, vea mi respuesta para la solución que se me ocurrió.
Doguhan Uluca
1
@duluca: Eso fue, efectivamente, lo que te estaba sugiriendo que hicieras ... Ten en cuenta, sin embargo, que si accedes a Título varias veces rápidamente, tu solución actual generará múltiples llamadas de forma getTitle()simultánea ...
Reed Copsey
Muy buen punto. Aunque no es un problema para mi caso específico, una comprobación booleana para isLoading solucionaría el problema.
Doguhan Uluca
55

Realmente necesitaba que la llamada se originara en el método get, debido a mi arquitectura desacoplada. Entonces se me ocurrió la siguiente implementación.

Uso: El título está en un ViewModel o un objeto que podría declarar estáticamente como un recurso de página. Vincúlelo y el valor se completará sin bloquear la interfaz de usuario, cuando getTitle () regrese.

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");
        }
    }
}
Doguhan Uluca
fuente
99
updfrom 18/07/2012 en Win8 RP debemos cambiar la llamada de Dispatcher a: Window.Current.CoreWindow.Dispatcher.RunAsync (CoreDispatcherPriority.Normal, async () => {Title = await GetTytleAsync (url);});
Anton Sizikov
77
@ ChristopherStevenson, yo también lo pensé, pero no creo que ese sea el caso. Debido a que el getter se está ejecutando como un fuego y se olvida, sin llamar al setter al finalizar, el enlace no se actualizará cuando el getter haya terminado de excitarse.
Iain
3
No, tiene y tuvo una condición de carrera, pero el usuario no lo verá debido a 'RaisePropertyChanged ("Title")'. Regresa antes de que se complete. Pero, una vez completado, está configurando la propiedad. Eso activa el evento PropertyChanged. Binder obtiene el valor de la propiedad nuevamente.
Medeni Baykal
1
Básicamente, el primer getter devolverá un valor nulo, luego se actualizará. Tenga en cuenta que si queremos que se llame a getTitle cada vez, podría haber un bucle defectuoso.
tofutim
1
También debe tener en cuenta que cualquier excepción en esa llamada asincrónica se tragará por completo. Ni siquiera llegan a su controlador de excepciones no manejado en la aplicación si tiene uno.
Philter
9

Creo que podemos esperar que el valor devuelva primero nulo y luego obtener el valor real, por lo que en el caso de Pure MVVM (proyecto PCL, por ejemplo), creo que la siguiente es la solución más 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();
}
Juan Pablo García Coello
fuente
3
¿No genera esto advertencias del compiladorCS4014: Async method invocation without an await expression
Nick
66
muy escéptico de seguir este consejo. Mire este video y luego decídase : channel9.msdn.com/Series/Three-Essential-Tips-for-Async/… .
Contango
1
¡DEBE evitar usar métodos de "vacío asíncrono"!
SuperJMN
1
cada grito debería venir con una sabia respuesta, ¿podría @SuperJMN explicarnos por qué?
Juan Pablo Garcia Coello
1
@Contango Buen video. Él dice: "Úselo async voidsolo para manejadores de nivel superior y similares". Creo que esto puede calificar como "y sus similares".
HappyNomad
7

Puedes usar Taskasí:

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

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

Pensé .GetAwaiter (). GetResult () era exactamente la solución a este problema, ¿no? p.ej:

string _Title;
public string Title
{
    get
    {
        if (_Title == null)
        {   
            _Title = getTitle().GetAwaiter().GetResult();
        }
        return _Title;
    }
    set
    {
        if (value != _Title)
        {
            _Title = value;
            RaisePropertyChanged("Title");
        }
    }
}
bc3tech
fuente
55
Es lo mismo que simplemente bloquear con .Result: no es asíncrono y puede provocar puntos muertos.
McGuireV10
necesitas agregar IsAsync = True
Alexsandr Ter
Agradezco los comentarios sobre mi respuesta; Realmente me encantaría que alguien brinde un ejemplo donde este punto muerto pueda verlo en acción
bc3tech
2

Como su "propiedad asíncrona" está en un modelo de vista, puede usar AsyncMVVM :

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

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

Se encargará del contexto de sincronización y la notificación de cambio de propiedad.

Dmitry Shechtman
fuente
Siendo una propiedad, tiene que ser.
Dmitry Shechtman
Lo siento, pero tal vez me perdí el punto de este código. ¿Puedes elaborar por favor?
Patrick Hofman, el
Las propiedades están bloqueadas por definición. GetTitleAsync () sirve como un "captador asíncrono" sin el azúcar sintáctico.
Dmitry Shechtman
1
@DmitryShechtman: No, no tiene que estar bloqueando. Esto es exactamente para lo que son las notificaciones de cambio y las máquinas de estado. Y no están bloqueando por definición. Son sincrónicos por definición. Esto no es lo mismo que bloquear. "Bloquear" significa que pueden hacer un trabajo pesado y consumir un tiempo considerable para ejecutar. Esto a su vez es exactamente lo que NO DEBEN HACER las propiedades.
quetzalcoatl
1

Nigromancia
En .NET Core / NetStandard2, puede usar en Nito.AsyncEx.AsyncContext.Runlugar 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"));
    }


}

Si simplemente elige System.Threading.Tasks.Task.Runo System.Threading.Tasks.Task<int>.Run, entonces no funcionaría.

Stefan Steiger
fuente
-1

Creo que mi ejemplo a continuación puede seguir el enfoque de @ Stephen-Cleary, pero quería dar un ejemplo codificado. Esto es para usar en un contexto de enlace de datos, por ejemplo Xamarin.

El constructor de la clase, o el configurador de otra propiedad de la que depende, puede llamar a un vacío asíncrono que llenará la propiedad al completar la tarea sin la necesidad de esperar o bloquear. Cuando finalmente obtenga un valor, actualizará su interfaz de usuario a través del mecanismo NotifyPropertyChanged.

No estoy seguro de los efectos secundarios de llamar a un vacío de Aysnc desde un constructor. Tal vez un comentarista elaborará el manejo de errores, 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();
    }


}
Craig.C
fuente
-1

Cuando me encontré con este problema, intentar ejecutar una sincronía de método asíncrono desde un instalador o un constructor me metió en un punto muerto en el hilo de la interfaz de usuario, y el uso de un controlador de eventos requirió demasiados cambios en el diseño general.
La solución era, como suele ser, simplemente escribir explícitamente lo que quería que sucediera implícitamente, que era que otro hilo manejara la operación y que el hilo principal esperara a que terminara:

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

Se podría argumentar que abusé del marco, pero funciona.

Yuval Perelman
fuente
-1

Reviso todas las respuestas pero todas tienen un problema de rendimiento.

por ejemplo en:

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

use el despachador que no es una buena respuesta.

pero hay una solución simple, solo hazlo:

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");
            }
        }
    }
Mahdi Rastegari
fuente
si su función es asyn, use getTitle (). wait () en lugar de getTitle ()
Mahdi Rastegari
-4

Puedes cambiar la proactividad a Task<IEnumerable>

y hacer algo como:

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

y úsalo como espera MyList;

Alejandro Guardiola
fuente