¿Alguna vez has tenido la necesidad de que cuando haces la instancia de una clase ya se carguen datos de forma asíncrona pero usarlos de forma síncrona? Si tu respuesta es SI y no has sabido como resolverlo, aqui te traigo una posible solución. Como siempre no tiene porque ser la única, ni la mejor, pero es una opción.
Lazy al rescate
Imagino que habrás oido hablar del Lazy Loading, bien pues la calse Lazy es la que nos ofrece esta funcionalidad. Lazy básicamente encapcula los datos qeu necesitamos cargar para luego poderlos pedir cuando los necesitemos. Esta clase se encarga de pedir esos datos al servicio que nosotros le hemos ofrecido, y luego los deja ya almacenados para no estar siemrpe pudiéndoloe.
Es decir. con Lazy podemos pedir los datos, éstos se pediran de forma asíncrona (o ssíncrona dependendiendo de como el servicio que le configuramos), y luego ya los tendremos disponibles siempre que los necesitmos.
Manos a la obra
Bien vamos a comenzar con la tarea. Lo que tenemos que hacer son basicamente 4 cosas:
1. Declarar un campo que declare un objeto Lazy<[tipo de dato]>
2. Declarar un objecto Lock para asegurarnos de que el proceso asíncrono ha terminado y que no afecte a otros hilos de ejecución
3. Tener un método que cargue los datos de froma asíncrona que devuelva nuestros datos [tipo de dato].
4. Inicializar nuestro campo Lazy<[tipo de dato]> pasándole el método que va a realizar la carga asíncrona.
La teoría muchas veces no se entiende bien, pero como lo vamos a ver con un ejemplo no hay mucho problema.
Codificando
Para este ejemplo vamos a suponer que necesitamos los datos como un Dictionary.
class MiClase
{
private readonly Lazy> Datasource;
private readonly object InitializationLock = new object();
public MiClase()
{
Datasource = new Lazy>(LoadData);
}
private Dictionary LoadData()
{
Dictionary result = new Dictionary();
lock(InitializationLock)
{
//Ejemplo con un HTTP
using HttpClient client = new HttpClient();
result = client.GetFromJsonAsync>("api/datos").Result;
}
return result;
}
}
Que estemos inicializando en el constructor el campo Datasource, no quiere decir que ya se esté realmante ejecutando. Cuando necesites usar los datos por primera vez, es cuando se ejecuta el método LoadData que realizara la llamada asíncrona, pero esperando a que termine, y después ya se queda con esos datos para que los podamos usar de forma normal. Sin tener que volver a esperar para optenerlos.
Usando los datos
Ahora dentro de tu clase cada vez que necesites los datos sólo tendrás que utilizar la propiedad Value de tu objeto Datasource y ya. Como parte del ejemplo te propongo un indizador, que es algo muy útil que que realmente solemos utilizar muy poco. Además un ejemplo de sobrecarga del método ToString(), que también se usa poco y es muy útil. Aqui el código
public string this[int index]
{
get
{
return Datasource.Value.TryGetValue(index, out string valor) ? valor : null;
}
set
{
Selected = index;
if(Datasource.Value.TryGetValue(index, out string valor))
Datasource.Value[index] = valor;
else
Datasource.Value.Add(index, valor);
}
}
private int SelectedBk;
public int Selected
{
get { return SelectedBk; }
set { SelectedBk = value; }
}
public override string ToString()
{
if(Selected >= 0 && Selected < Datasource.Value.Count)
{
return Datasource.Value[Selected];
}
return base.ToString();
}
Como puedes observar hago uso directo de Datasource.Value para acceder a los datos. Si es la primera vez que se usa, realizará el proceso asíncrono, convertido en síncrono, por lo que el objeto espera, y luego ya hago uso de los datos, agregando, o modificando.
Consideraciones
Como estamos convirtiendo un proceso asíncrono en uno síncrono debes de recordar controlar bien las posible excepciones y tener algún tipo de "timeout" para que no bloquees a la aplicación de forma indefinida.
Luego si quieres, tambien puedes hacer uso de la propiedad IsValueCreated de la calse Lazy, es decir, de nuestro objeto Datasource.IsValueCreated, que nos devolverá un false si aun no está creado o un true si ya tiene valores. Esto es útil si neceistas controlar que no se produzcan NULL REFERENCE al intentar acceder a los datos. O para hacer Dispose() del objeto si fuera necesarrio. Es decir, si el objeta necesita Dispose(), pero nunca fue creado, entonces no hace falta, ya que si intentaramos acceder al objeto para hacer el Dispose(), será en ese momento cuando se cree para luego hacer Dispose() por lo que es una perdida de tiempo y de recursos innecesaria.
Conclusiones
Y eso es todo amigos, espero que esto les ayuda en alguna ocasión. Se es es algo un poco raro, pero que seguro que puede que en algun momento necesites de hacerlo.
Happy codding