none
Imitar el comportamiento de TransactionScope RRS feed

  • Pregunta

  • Muy buenas a todos.  Creo que les tengo una pregunta muy, muy interesante, o al menos creo yo.

    Tengo un objetivo:  Crear una clase que se comporte de la misma manera que TransactionScope:  Una clase IDisposable cuyos objetos, al momento de crearse, se registran en una pila y son válidos únicamente para el código que se escribe dentro del bloque using.  El código que se escribe dentro del bloque using no recibe en ningún momento una referencia directa al objeto creado.

    Pongamos un pseudo código:

    public sealed class Objective : IDisposable
    {
        //N propiedades, todas anulables.  Con cada objeto que se agrega a la pila,
        //los valores no-nulos sobreescriben los valores anteriores (nulos o no);
        //y los valores nulos "heredan" los valores del objeto anterior en la pila.
    }

    Inicialmente pensé:  Esto es fácil pues solamente necesito usar TLS (Thread Local Storage) y listo, y .Net tiene el atributo ThreadStaticAttribute para esto.  Así los códigos que corren en distintos hilos de ejecución simplemente ni cuenta se dan de las pilas de otros hilos ya que .Net cambiaría el valor de mi variable de pila (estática) cuando ejecuta distintos hilos.

    Peeero, y he aquí mi pregunta:  ¿Qué pasa si hay await dentro del bloque using?  En aplicaciones de consola, async/await es multi hilo y por lo tanto, al menos creo yo, que no necesitaría una implementación especial pues TLS cubriría este caso particular.  Sin embargo, si el proyecto no es de consola, await simplemente encapsulará el resto del código, lo pondrá en cola de ejecución y saltará fuera del bloque de código que actualmente está en ejecución.

    Entonces hasta aquí he llegado:  Como no puedo valerme de la identidad de CurrentThread pues será siempre la misma, lo que necesito es tener un almacén de múltiples versiones de la pila de objetos para cada objeto tipo Objective que se cree bajo la modalidad de async/await.  Bien.  ¿pero entonces cómo logro detectar el retorno al contexto actual para así obtener la versión correcta de la pila de objetos?  No tengo la menor idea.

    Entonces decidí ver el código fuente de TransactionScope (pueden verlo aquí).  Logré verificar que mi idea de usar TLS es buena pues TransactionScope utiliza el método (¡yay! jeje), pero la parte de async/await está por encima de mis habilidades.  No la entiendo.  Ah, asegúrense de ver el código de la versión de .Net 4.5 o superior, pues antes de esta versión la clase no soportaba el uso de async/await.

    Entonces, después de toda la explicación de mi tragedia, les pregunto:  ¿Alguien puede explicarme cómo puedo identificar en qué contexto se encuentra el objeto para así determinar qué versión de pila debe usarse?

    Si no han entendido el escenario completamente, les muestro un ejemplo del código que se escribiría con esta clase Objective.

    int result = 0;
    int result2 = 0;
    //Nótese cómo no se pasa una referencia directa de "o" u "o2" al objeto helper.
    using (Objective o = new Objective() { Prop1 = "A", PropB = new SomeService() })
    {
        var helper = new DependantObject(); //Un objeto que depende de las propiedades apiladas usando la clase objective.
        result = helper.DoSomething();
        using (Objective o2 = new Objective() { Prop1 = "B" })
        {
            //Aquí obtengo un resultado diferente.
            result2 = helper.DoSomething();
        }
    }


    Jose R. MCP
    My GIT Repositories | Mis Repositorios GIT



    domingo, 24 de junio de 2018 10:13
    Moderador

Respuestas

  • Bueno, creo que ya estoy satisfecho con mis pruebas.  Les explico para los que se interesen en el tema.

    La clase Objective que mencioné en la pregunta sería la siguiente, llamada SomeOptions.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.Remoting.Messaging;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace CallContextTest
    {
        public class SomeOptions : IDisposable
        {
            #region Static Section
            private static readonly string CallContextDataKey = nameof(SomeOptions) + Guid.NewGuid().ToString();
    
            public static SomeOptions GlobalOptions { get; } = new SomeOptions(true);
    
            public static SomeOptions CurrentOptions
            {
                get
                {
                    SomeOptions options = (SomeOptions)CallContext.LogicalGetData(CallContextDataKey);
                    return options ?? GlobalOptions;
                }
            }
    
            private static void CallContextPushOptions(SomeOptions options)
            {
                CallContext.LogicalSetData(CallContextDataKey, options);
            }
           #endregion
    
            #region Properties
            public string PropA { get; set; }
            public int PropB { get; set; }
    
            private SomeOptions SavedOptions { get; set; }
            #endregion
    
            #region Constructors, Destructor & IDisposable
            private SomeOptions(bool isGlobalOptions)
            { }
    
            public SomeOptions()
            {
                PushOptions();
            }
    
            protected virtual void Dispose(bool disposing)
            {
                PopOptions();
            }
    
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }
            #endregion
    
            #region Methods
            private void PushOptions()
            {
                SomeOptions options = CurrentOptions;
                if (!Object.ReferenceEquals(options, GlobalOptions) && options != null)
                {
                    SavedOptions = options;
                }
                CallContextPushOptions(this);
            }
    
            private void PopOptions()
            {
                CallContextPushOptions(SavedOptions);
            }
            #endregion
        }
    }
    

    Lo que hace es registrarse como el objeto actual proveedor de valores cuando se crea la instancia y luego cuando se destruye elimina su registro y restaura cualquier registro que hubiera antes de este objeto.

    El objeto que utiliza estos valores es OptionsReader:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace CallContextTest
    {
        public class OptionsReader
        {
            private Random r = new Random();
            private Task RandomDelay(int upperLimit = 7000)
            {
                return Task.Delay(r.Next(1500, upperLimit));
            }
    
            public string ReadStringOption()
            {
                return SomeOptions.CurrentOptions.PropA;
            }
    
            public int ReadNumberOption()
            {
                return SomeOptions.CurrentOptions.PropB;
            }
    
            public async Task<string> ReadStringOptionAsync()
            {
                await RandomDelay();
                return SomeOptions.CurrentOptions.PropA;
            }
    
            public async Task<int> ReadNumberOptionAsync(bool withDelay)
            {
                if (withDelay)
                {
                    await RandomDelay(2500);
                }
                return SomeOptions.CurrentOptions.PropB;
            }
        }
    }
    

    Con esto puedo escribir código como el del ViewModel que muestro a continuación:

    using CallContextTest;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.Remoting.Messaging;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Input;
    using wj.DataBinding;
    
    namespace CallContextLabWPF.ViewModels
    {
        public class MainViewModel : NotifyPropertyChanged
        {
            public class DataSet : NotifyPropertyChanged
            {
                private static readonly Random random = new Random();
    
                private string m_data;
                public string Data
                {
                    get { return m_data; }
                    set { SaveAndNotify(ref m_data, value); }
                }
    
                private int m_numData;
                public int NumData
                {
                    get { return m_numData; }
                    set { SaveAndNotify(ref m_numData, value); }
                }
    
                public DataSet(bool fillWithRandomData = true)
                {
                    if (fillWithRandomData)
                    {
                        Data = Guid.NewGuid().ToString();
                        NumData = random.Next(1, 111);
                    }
                }
            }
    
            private const string MainLogicalData = nameof(MainLogicalData);
    
            private DataSet m_globalData = new DataSet();
            public DataSet GlobalData
            {
                get { return m_globalData; }
                set { SaveAndNotify(ref m_globalData, value); }
            }
    
            private DataSet m_upperData = new DataSet();
            public DataSet UpperData
            {
                get { return m_upperData; }
                set { SaveAndNotify(ref m_upperData, value); }
            }
    
            private DataSet m_data1 = new DataSet();
            public DataSet Data1
            {
                get { return m_data1; }
                set { SaveAndNotify(ref m_data1, value); }
            }
    
            private DataSet m_data2 = new DataSet();
            public DataSet Data2
            {
                get { return m_data2; }
                set { SaveAndNotify(ref m_data2, value); }
            }
    
            private DataSet m_globalResult = new DataSet(false);
            public DataSet GlobalResult
            {
                get { return m_globalResult; }
                set { SaveAndNotify(ref m_globalResult, value); }
            }
    
            private DataSet m_upperResult = new DataSet(false);
            public DataSet UpperResult
            {
                get { return m_upperResult; }
                set { SaveAndNotify(ref m_upperResult, value); }
            }
    
            private DataSet m_result1 = new DataSet(false);
            public DataSet Result1
            {
                get { return m_result1; }
                set { SaveAndNotify(ref m_result1, value); }
            }
    
            private DataSet m_result2 = new DataSet(false);
            public DataSet Result2
            {
                get { return m_result2; }
                set { SaveAndNotify(ref m_result2, value); }
            }
    
            private ICommand m_goCommandAsync;
            public ICommand GoCommandAsync
            {
                get { return m_goCommandAsync; }
                set { SaveAndNotify(ref m_goCommandAsync, value); }
            }
    
            private Cursor m_cursor;
            public Cursor Cursor
            {
                get { return m_cursor; }
                set { SaveAndNotify(ref m_cursor, value); }
            }
    
            public MainViewModel()
            {
                GoCommandAsync = new RelayCommand(() => 
                {
                    GlobalResult = new DataSet(false);
                    UpperResult = new DataSet(false);
                    Result1 = new DataSet(false);
                    Result2 = new DataSet(false);
                    Cursor = Cursors.AppStarting;
                    TasksQueue tq = new TasksQueue();
                    tq.AllTasksProcessed += (o, e) => { Cursor = null; };
                    SomeOptions.GlobalOptions.PropA = GlobalData.Data;
                    SomeOptions.GlobalOptions.PropB = GlobalData.NumData;
                    OptionsReader or = new OptionsReader();
                    ExecuteGoAsync(tq);
                    Task<string> stringTask;
                    Task<int> intTask;
                    using (SomeOptions so = new SomeOptions())
                    {
                        so.PropA = UpperData.Data;
                        so.PropB = UpperData.NumData;
                        UpperResult.NumData = or.ReadNumberOption();
                        stringTask = or.ReadStringOptionAsync();
                        tq.AddTask(stringTask, t => UpperResult.Data = t.Result);
                    }
                    stringTask = or.ReadStringOptionAsync();
                    tq.AddTask(stringTask, t => GlobalResult.Data = t.Result);
                    intTask = or.ReadNumberOptionAsync(true);
                    tq.AddTask(intTask, t => GlobalResult.NumData = t.Result);
                    tq.AwaitTasksAsync();
                });
            }
    
            private async void ExecuteGoAsync(TasksQueue tq)
            {
                Task<string> t1;
                Task<int> t2;
                using (SomeOptions so = new SomeOptions())
                {
                    OptionsReader or = new OptionsReader();
                    so.PropA = Data1.Data;
                    so.PropB = Data1.NumData;
                    t1 = or.ReadStringOptionAsync();
                    tq.AddTask(t1, t => Result1.Data = t.Result);
                    t2 = or.ReadNumberOptionAsync(true);
                    tq.AddTask(t2, t => Result1.NumData = t.Result);
                    using (SomeOptions so2 = new SomeOptions())
                    {
                        so2.PropA = Data2.Data;
                        so2.PropB = Data2.NumData;
                        t1 = or.ReadStringOptionAsync();
                        tq.AddTask(t1, t => Result2.Data = t.Result);
                        t2 = or.ReadNumberOptionAsync(false);
                        tq.AddTask(t2, t => Result2.NumData = t.Result);
                    }
                }
            }
        }
    }
    

    Según la interfaz gráfica que corresponde a ese ViewModel, todas las lecturas de todos los datos se da correctamente.  Supongo entonces que la estructura de SomeOptions tal como se muestra aquí es suficiente para lograr mi objetivo.

    Me disculpan si el código está un poco desordenado.  No es código que yo pretenda publicar en otra parte.  Es simplemente el "Proof of Concept".

    Si alguien gusta de proveer su opinión al respecto, estaré muy dispuesto a leerla.  ¡Gracias!


    Jose R. MCP
    My GIT Repositories | Mis Repositorios GIT

    martes, 26 de junio de 2018 5:26
    Moderador

Todas las respuestas

  • Bueno, para quien esté interesado, les cuento que ya ando próximo a tener una solución completa.

    Definitivamente tuve que ponerme a leer sobre cosas que nunca he tocado y he llegado al punto de crear unos pequeños proyectos de prueba (consola y WPF) que parecen confirmar lo que he aprendido.

    La clase System.Runtime.Remoting.Messaging.CallContext es una clase con métodos estáticos que permiten almacenar datos arbitrarios en algo parecido a un diccionario que es cambiante según el hilo de ejecución.  Parece tener 2 tipos de almacén:  "Físico" y "Lógico".  El físico no parece servirme según pruebas preliminares pues parece estar ligado verdaderamente al hilo de ejecución (según la prueba en consola; no probé en WPF).  Este físico se accede con los métodos GetData() y SetData().

    El lógico, sin embargo, sí parece diferenciar entre saltos de await.  Este se accede con LogicalGetData() y LogicalSetData().  Mi prueba consiste en tener una función async que espera una cantidad aleatoria de tiempo y luego lee el dato que coloco con LogicalSetData() y lo devuelve como resultado.  Como es async, pues puedo llamarle e inmediatamente continuar con otras cosas.  La otra cosa que hago es cambiar el valor con LogicalGetData() y volver a llamar a la función de lectura.  Luego espero el resultado de ambos y los imprimo conforme van llegando (a veces los obtengo en el orden en que los pedí y a veces no).  En todos los casos obtuve el valor correcto.

    Este es el código de prueba de consola:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.Remoting.Messaging;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace CallContextLab
    {
        class Program
        {
            private const string MainData = nameof(MainData);
            private const string MainLogicalData = nameof(MainLogicalData);
    
            static void Main(string[] args)
            {
                CallContext.LogicalSetData(MainLogicalData, "LogicalA");
                var t1 = GetDataAsync(MainLogicalData);
                CallContext.LogicalSetData(MainLogicalData, "LogicalB");
                var t2 = GetDataAsync(MainLogicalData);
                int readyTask = Task.WaitAny(t1, t2);
                var t = readyTask == 0 ? t1 : t2;
                Console.WriteLine($"Task {readyTask}: {t.Result}");
                t = readyTask == 0 ? t2 : t1;
                Console.WriteLine($"Remaining task:  {t.Result}");
            }
    
            private static async Task<string> GetDataAsync(string name)
            {
                bool isLogical = name.Contains("Logical");
                await Task.Delay(new Random().Next(2000, 7000));
                return await Task.Run<string>(() => isLogical ? CallContext.LogicalGetData(name)?.ToString() : CallContext.GetData(name)?.ToString());
            }
        }
    }
    

    Pueden ignorar/simplificar cualquier código referente a GetData()/SetData() y MainData.

    Un par de resultados de ejemplo:

    Task 1: LogicalB
    Remaining task:  LogicalA
    --------------------------
    Task 0: LogicalA
    Remaining task:  LogicalB

    El primer resultado muestra que la segunda llamada resolvió primero.

    Lo que me resta por entender de la implementación de TransactionScope es el uso de ConditionalWeakTable y por qué en esta tabla se guarda únicamente el ContextKey y no directamente el objeto de contexto.  No sé por qué el nivel de indirección adicional.


    Jose R. MCP
    My GIT Repositories | Mis Repositorios GIT

    lunes, 25 de junio de 2018 10:24
    Moderador
  • Hola webJose:

    Intentando entender tu código y tu lógica, me surgen las preguntas, que desde el poco conocimiento de mi lado, a lo mejor complementan al tuyo. Esto creo que te resultará interesante.

    Para tu laboratorio de pruebas, deberías de generar, algunas tareas que no dependan del procesamiento de la maquina, por ejemplo llamar a algún procedimiento almacenado, que tiene un retardo x, y también hacer otras, que conlleven un stress grande. Aquí entra las peticiones de cancelación.

    Un saludo

    lunes, 25 de junio de 2018 13:20
  • Bueno, creo que ya estoy satisfecho con mis pruebas.  Les explico para los que se interesen en el tema.

    La clase Objective que mencioné en la pregunta sería la siguiente, llamada SomeOptions.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.Remoting.Messaging;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace CallContextTest
    {
        public class SomeOptions : IDisposable
        {
            #region Static Section
            private static readonly string CallContextDataKey = nameof(SomeOptions) + Guid.NewGuid().ToString();
    
            public static SomeOptions GlobalOptions { get; } = new SomeOptions(true);
    
            public static SomeOptions CurrentOptions
            {
                get
                {
                    SomeOptions options = (SomeOptions)CallContext.LogicalGetData(CallContextDataKey);
                    return options ?? GlobalOptions;
                }
            }
    
            private static void CallContextPushOptions(SomeOptions options)
            {
                CallContext.LogicalSetData(CallContextDataKey, options);
            }
           #endregion
    
            #region Properties
            public string PropA { get; set; }
            public int PropB { get; set; }
    
            private SomeOptions SavedOptions { get; set; }
            #endregion
    
            #region Constructors, Destructor & IDisposable
            private SomeOptions(bool isGlobalOptions)
            { }
    
            public SomeOptions()
            {
                PushOptions();
            }
    
            protected virtual void Dispose(bool disposing)
            {
                PopOptions();
            }
    
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }
            #endregion
    
            #region Methods
            private void PushOptions()
            {
                SomeOptions options = CurrentOptions;
                if (!Object.ReferenceEquals(options, GlobalOptions) && options != null)
                {
                    SavedOptions = options;
                }
                CallContextPushOptions(this);
            }
    
            private void PopOptions()
            {
                CallContextPushOptions(SavedOptions);
            }
            #endregion
        }
    }
    

    Lo que hace es registrarse como el objeto actual proveedor de valores cuando se crea la instancia y luego cuando se destruye elimina su registro y restaura cualquier registro que hubiera antes de este objeto.

    El objeto que utiliza estos valores es OptionsReader:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace CallContextTest
    {
        public class OptionsReader
        {
            private Random r = new Random();
            private Task RandomDelay(int upperLimit = 7000)
            {
                return Task.Delay(r.Next(1500, upperLimit));
            }
    
            public string ReadStringOption()
            {
                return SomeOptions.CurrentOptions.PropA;
            }
    
            public int ReadNumberOption()
            {
                return SomeOptions.CurrentOptions.PropB;
            }
    
            public async Task<string> ReadStringOptionAsync()
            {
                await RandomDelay();
                return SomeOptions.CurrentOptions.PropA;
            }
    
            public async Task<int> ReadNumberOptionAsync(bool withDelay)
            {
                if (withDelay)
                {
                    await RandomDelay(2500);
                }
                return SomeOptions.CurrentOptions.PropB;
            }
        }
    }
    

    Con esto puedo escribir código como el del ViewModel que muestro a continuación:

    using CallContextTest;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.Remoting.Messaging;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Input;
    using wj.DataBinding;
    
    namespace CallContextLabWPF.ViewModels
    {
        public class MainViewModel : NotifyPropertyChanged
        {
            public class DataSet : NotifyPropertyChanged
            {
                private static readonly Random random = new Random();
    
                private string m_data;
                public string Data
                {
                    get { return m_data; }
                    set { SaveAndNotify(ref m_data, value); }
                }
    
                private int m_numData;
                public int NumData
                {
                    get { return m_numData; }
                    set { SaveAndNotify(ref m_numData, value); }
                }
    
                public DataSet(bool fillWithRandomData = true)
                {
                    if (fillWithRandomData)
                    {
                        Data = Guid.NewGuid().ToString();
                        NumData = random.Next(1, 111);
                    }
                }
            }
    
            private const string MainLogicalData = nameof(MainLogicalData);
    
            private DataSet m_globalData = new DataSet();
            public DataSet GlobalData
            {
                get { return m_globalData; }
                set { SaveAndNotify(ref m_globalData, value); }
            }
    
            private DataSet m_upperData = new DataSet();
            public DataSet UpperData
            {
                get { return m_upperData; }
                set { SaveAndNotify(ref m_upperData, value); }
            }
    
            private DataSet m_data1 = new DataSet();
            public DataSet Data1
            {
                get { return m_data1; }
                set { SaveAndNotify(ref m_data1, value); }
            }
    
            private DataSet m_data2 = new DataSet();
            public DataSet Data2
            {
                get { return m_data2; }
                set { SaveAndNotify(ref m_data2, value); }
            }
    
            private DataSet m_globalResult = new DataSet(false);
            public DataSet GlobalResult
            {
                get { return m_globalResult; }
                set { SaveAndNotify(ref m_globalResult, value); }
            }
    
            private DataSet m_upperResult = new DataSet(false);
            public DataSet UpperResult
            {
                get { return m_upperResult; }
                set { SaveAndNotify(ref m_upperResult, value); }
            }
    
            private DataSet m_result1 = new DataSet(false);
            public DataSet Result1
            {
                get { return m_result1; }
                set { SaveAndNotify(ref m_result1, value); }
            }
    
            private DataSet m_result2 = new DataSet(false);
            public DataSet Result2
            {
                get { return m_result2; }
                set { SaveAndNotify(ref m_result2, value); }
            }
    
            private ICommand m_goCommandAsync;
            public ICommand GoCommandAsync
            {
                get { return m_goCommandAsync; }
                set { SaveAndNotify(ref m_goCommandAsync, value); }
            }
    
            private Cursor m_cursor;
            public Cursor Cursor
            {
                get { return m_cursor; }
                set { SaveAndNotify(ref m_cursor, value); }
            }
    
            public MainViewModel()
            {
                GoCommandAsync = new RelayCommand(() => 
                {
                    GlobalResult = new DataSet(false);
                    UpperResult = new DataSet(false);
                    Result1 = new DataSet(false);
                    Result2 = new DataSet(false);
                    Cursor = Cursors.AppStarting;
                    TasksQueue tq = new TasksQueue();
                    tq.AllTasksProcessed += (o, e) => { Cursor = null; };
                    SomeOptions.GlobalOptions.PropA = GlobalData.Data;
                    SomeOptions.GlobalOptions.PropB = GlobalData.NumData;
                    OptionsReader or = new OptionsReader();
                    ExecuteGoAsync(tq);
                    Task<string> stringTask;
                    Task<int> intTask;
                    using (SomeOptions so = new SomeOptions())
                    {
                        so.PropA = UpperData.Data;
                        so.PropB = UpperData.NumData;
                        UpperResult.NumData = or.ReadNumberOption();
                        stringTask = or.ReadStringOptionAsync();
                        tq.AddTask(stringTask, t => UpperResult.Data = t.Result);
                    }
                    stringTask = or.ReadStringOptionAsync();
                    tq.AddTask(stringTask, t => GlobalResult.Data = t.Result);
                    intTask = or.ReadNumberOptionAsync(true);
                    tq.AddTask(intTask, t => GlobalResult.NumData = t.Result);
                    tq.AwaitTasksAsync();
                });
            }
    
            private async void ExecuteGoAsync(TasksQueue tq)
            {
                Task<string> t1;
                Task<int> t2;
                using (SomeOptions so = new SomeOptions())
                {
                    OptionsReader or = new OptionsReader();
                    so.PropA = Data1.Data;
                    so.PropB = Data1.NumData;
                    t1 = or.ReadStringOptionAsync();
                    tq.AddTask(t1, t => Result1.Data = t.Result);
                    t2 = or.ReadNumberOptionAsync(true);
                    tq.AddTask(t2, t => Result1.NumData = t.Result);
                    using (SomeOptions so2 = new SomeOptions())
                    {
                        so2.PropA = Data2.Data;
                        so2.PropB = Data2.NumData;
                        t1 = or.ReadStringOptionAsync();
                        tq.AddTask(t1, t => Result2.Data = t.Result);
                        t2 = or.ReadNumberOptionAsync(false);
                        tq.AddTask(t2, t => Result2.NumData = t.Result);
                    }
                }
            }
        }
    }
    

    Según la interfaz gráfica que corresponde a ese ViewModel, todas las lecturas de todos los datos se da correctamente.  Supongo entonces que la estructura de SomeOptions tal como se muestra aquí es suficiente para lograr mi objetivo.

    Me disculpan si el código está un poco desordenado.  No es código que yo pretenda publicar en otra parte.  Es simplemente el "Proof of Concept".

    Si alguien gusta de proveer su opinión al respecto, estaré muy dispuesto a leerla.  ¡Gracias!


    Jose R. MCP
    My GIT Repositories | Mis Repositorios GIT

    martes, 26 de junio de 2018 5:26
    Moderador