Gestión de la optimización (Parte I): Creando una interfaz gráfica

Andrey Azatskiy | 22 agosto, 2019

Índice

Introducción

El tema del inicio alternativo del terminal MetaTrader ya fue discutido en el artículo de Vladimir Karputov. Además, en el sitio web de MetaTrader, existe una página que describe el orden de trabajo y el método alternativo del inicio del terminal. El presente artículo se basa en la información de estas dos fuentes mencionadas. No obstante, ninguna de ellas contiene la descripción de cómo diseñar una interfaz gráfica para poder trabajar con varios terminales simultáneamente. Mi artículo está destinado para corregir eso.

El resultado del trabajo realizado es una expansión para el terminal que permite iniciar el proceso de la optimización de los Asesores Expertos (EA) en varios terminales instalados en el mismo ordenador. Los artículos posteriores van a desarrollar esta expansión añadiendo una nueva funcionalidad.

El vídeo muestra el trabajo de la versión final de la expansión. Este artículo describe sólo el proceso del diseño de la interfaz gráfica, mientras que la lógica de la expansión demostrada será descrita en la siguiente parte.




Método del inicio de MetaTrader y archivos de configuración

Antes de considerar la extensión creada en detalle, vamos a revisar rápidamente los fundamentos del inicio del terminal (así como cualquier otra aplicación) usando la consola. Este método de trabajo con las aplicaciones puede parecer algo arcaico, pero no es así. Por ejemplo, en los sistemas operativos a base del núcleo Linux, este método se usa con frecuencia para iniciar las aplicaciones, incluso las aplicaciones sin la interfaz gráfica.

Vamos a considerar el inicio del terminal en el ejemplo de un simple programa escrito en C++:

#include <iostream>

using namespace std;

int main()
{
    cout<<"Hello World";

    return 0;
}

Después de la compilación del programa, obtenemos un archivo de ejecución (.exe). Al iniciarlo, el mensaje "Hello World" aparecerá en la consola, lo cual es habitual y conocido. Nótese que la función de inicio main no tiene parámetros de entrada, pero es un caso especial. Si modificamos este programa usando otra sobrecarga de la función main, obtenemos un programa de consola que recibe una serie de parámetros:

#include <iostream>

using namespace std;

int main(int argc, char *argv[])
{
    cout << "Hello World" << endl;

    for(int i = 0; i < argc; i ++)
    {
        cout << argv[i] << endl; 
    }

    return 0;
}

El primer parámetro argc indica en la longitud del array de los arrays del segundo parámetro.

El segundo parámetro representa una lista de las líneas que se pasan al programa durante el inicio. Se puede llamar a este programa desde la consola de la siguiente manera:

./program "My name" "is Andrey"

Donde ./program es la indicación en el nombre de este programa, las demás líneas representan un conjunto de parámetros separados con espacios. Esos parámetros se escriben en el array pasado, y el resultado de su trabajo es el siguiente:

Hello World
./program
My name
is Andrey

Como podemos observar, el primer mensaje es de la versión antigua del programa, mientras que el resto de las cadenas han sido pasadas como parámetros al array de las líneas agrv (nótese que el primer parámetro siempre es el nombre del programa a iniciar). No vamos analizar este ejemplo en detalle, es sólo una ilustración para comprender cómo funciona la ejecución de de la aplicación MetaTrader a través de la consola.

Al trabajar con los parámetros, normalmente se indican así llamadas banderas antes de cada uno de ellos. Estas banderas permiten al programa comprender para qué parámetro exactamente se establece el valor pasado. Para trabajar con las banderas, existe una serie de funciones en el lenguaje C/С++, pero no vamos a centrarnos en ellas. Lo importante es comprender que nuestra simple aplicación, con la extensión del archivo de ejecución (.exe), puede ser iniciada desde la consola con parámetros pasados, dependiendo de los cuales puede cambiar sus propiedades.  

De acuerdo con las instrucciones en el sitio web, existe una serie de banderas y valores para iniciar MetaTrader usando la consola:

Además, se puede combinar las banderas. Por ejemplo, usando la siguiente combinación de banderas, se puede iniciar el terminal en modo portátil con el archivo de configuración especificado:

terminal.exe /config:c:\myconfiguration.ini /partable

A pesar de que las diferencias entre el programa de ejemplo (Hello World) y el terminal real son enormes, el método de su ejecución a través de la consola es idéntico. Vamos a usar esta característica a la hora de desarrollar nuestra extensión.

Hay que prestar una atención especial en el archivo de configuración cuya ruta se especifica mediante la clave /config, porque debido precisamente a este archivo, el terminal entiende qué login/contraseña hay que usar para la ejecución, o en qué modo hay que realizar la ejecución del Simulador de Estrategias, o si es realmente necesario iniciar el Simulador. No vamos a repetir la instrucción respecto al uso de los archivos de configuración aquí. En vez de eso, me gustaría analizar más detalladamente su estructura. Cada archivo de configuración se compone de una serie de secciones que se indican entre los corchetes.

[Tester]

Después de la sección especificada va la lista clave-valor que representa la descripción de los campos que caracterizan los parámetros del inicio del programa. Además, los archivos de configuración pueden contener los comentarios que empiezan con los caracteres ";" o "#". Cabe mencionar que ahora en vez de los archivos de configuración con formato (*.ini) empiezan a usar los archivos que utilizan el marcado XAML, o los archivos json, porque ellos permiten almacenar una cantidad más grande de la información en un archivo. No obstante, los archivos (*.ini) todavía se usan en muchos programas desarrollados anteriormente, y MetaTrader también pertenece a este grupo. WinApi soporta una serie de funciones para el trabajo con los archivos de configuración, que usábamos durante la escritura de las clases del envoltorio para un trabajo conveniente con el formato requerido. Las funciones usadas y el envoltorio para trabajar con los archivos de configuración de MetaTrader serán descritos más detalladamente a continuación, en una de las secciones de este artículo. 

Funcionalidad de la expansión diseñada y tecnologías utilizadas

Para poder trabajar con el proyecto, primero, es necesario instalar Visual Studio IDE (Integrated Development Environment) en el ordenador. Este proyecto fue creado usando la versión Community de 2019. Durante la instalación de Visual Studio, hay que instalar adicionalmente .Net 4.6.1, que se usaba para escribir esta extensión. Además, para que los lectores sin conocimientos suficientes de C# puedan llegar rápidamente al fondo de la cuestión, intentaré describir en detalle algunos momentos específicos de este lenguaje y las técnicas que utilizaba durante la programación.

Puesto que la manera más conveniente de crear una interfaz gráfica es usar el lenguaje C#, y el terminal MetaTrader soporta el método conveniente para aplicar este lenguaje, merece la pena de aprovechar de esta posibilidad.. Recientemente, unos cuantos artículos relacionados con la creación de una interfaz gráfica usando C# fueron publicados en la web. Estos artículos demuestran bien el método de la creación de interfaces gráficas a base de la tecnología Win Forms y la biblioteca dll de conexión que inicia la gráfica usando los mecanismos de reflexión. La solución usada por el autor de estos artículos era bastante buena, pero para este artículo decidí usar una versión más moderna para escribir las interfaces gráficas, es decir, usando la tecnología WPF. Como resultado, conseguí prescindir de la biblioteca de la conexión, colocando todo en la única biblioteca dll. Para solucionar la tarea planteada, necesitamos crear un tipo del proyecto que nos permita almacenar los objetos gráficos descritos con el uso de la tecnología WPF. El proyecto debe compilarse en una biblioteca dinámica (archivo *.dll), que podría ser cargado luego en el terminal. Este tipo del proyecto ya existe: WpfCustomControlLibrary. Este tipo de proyectos fue desarrollado especialmente para crear interfaces gráficas de los objetos. De ejemplo, puede servir la biblioteca que dibuja los gráficos. Nosotros vamos a usarla para nuestros propósitos, a saber, para crear nuestra extensión para el terminal MetaTrader. Para crear este proyecto, es necesario seleccionarlo de la lista de proyectos en IDEVisual Studio, tal como se muestra en la captura de pantalla:

Vamos a llamar nuestro proyecto "OptimisationManagerExtention". Primero, se crea la carpeta con temas Themes en el proyecto. Contiene el archivo (*.xaml), "Generic.xaml" que almacena los estilos que establecen los colores, tamaños iniciales, márgenes de los bordes y otras propiedades de objetos gráficos. Necesitaremos este archivo más tarde, así que vamos a dejarlo por ahora tal como es. Otro archivo generado automáticamente es el archivo que contiene la clase CustomControl1. No vamos a necesitar este archivo, así que lo eliminamos. Puesto que serán escritos más artículos a base de éste, necesitamos preocuparse de la posibilidad de ampliar nuestra extensión. Eso significa que es necesario recurrir a la plantilla de programación MVVM. Si no está familiarizado con ella, por favor, consulte el siguiente artículo donde, según mi opinión, se describe detalladamente la idea de este patrón. Para implementar un código bies estructurado, creamos la carpeta "View", y colocamos nuestra ventana gráfica en ella. Para crear la ventana gráfica, es necesario añadir el elemento Window (WPF) a la carpeta creada, como se muestra a continuación:


Vamos a llamar la ventana creada ExtentionGUI.xaml. Precisamente ella será aquel elemento gráfico que ha sido capturado en el vídeo de arriba. Ahora, es el momento de hablar de los espacios de nombres (namespace). Hemos creado el proyecto y lo hemos llamado OptimisationManagerExtention; después, Studio ha generado automáticamente el espacio de nombres principal "OptimisationManagerExtention". En el lenguaje C#, como en la mayoría de otros lenguajes de programación, los espacios de nombres sirven de una especie de contenedores que contienen nuestros objetos. Las propiedades de los espacios de nombres pueden ser demostradas por el siguiente ejemplo: 

La construcción de abajo es incorrecta, porque ambas clases están declaradas en el mismo espacio de nombres, a saber:

namespace MyNamespace
{
    class Class_A
    {
    }

    class Class_A
    {
    }
}

Mientras que la siguiente división de las clases es aceptable, porque ambas clases se encuentran en los espacios diferentes, a pesar de que tengan el mismo nombre:

namespace MyFirstNamespace
{
    class Class_A
    {
    }
}

namespace MySecondNamespace
{
    class Class_A
    {
    }
}

Además, hay así llamados los espacios de nombres anidados. Cuando ellos se usan, un espacio de nombres contiene una serie de otros dentro. Como resultado, el siguiente código también es válido:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyNamespace
{
    class Class_A
    {
    }

    namespace Second
    {
        class Class_A
        {
        }
    }

    namespace First
    {
        class Class_A
        {
        }
    }
}

Pero como esta forma de escritura es inconveniente, С# soporta una escritura corta, siendo más conveniente para la percepción:

namespace MyNamespace
{
    class Class_A
    {
    }
}

namespace MyNamespace.First
{
    class Class_A
    {
    }
}

namespace MyNamespace.Second
{
    class Class_A
    {
    }
}

El código de dos ejemplos anteriores es idéntico, pero la última versión es más conveniente. Si volvemos de la teoría a nuestra extensión, cabe mencionar que al crear la carpeta View, hemos creado un espacio de nombres anidado, y ahora los objetos ubicados en la carpeta View van a ubicarse en el espacio de nombres "OptimisationManagerExtention.View". Por tanto, nuestra ventana también tiene su espacio de nombres. Para que los estilos que vamos a describir en el archivo Generic.xaml se apliquen con éxito a la ventana entera, necesitamos editar el marcado XAML de este archivo. Lo primero que hay que hacer es eliminar el bloque del código que se empieza con <Style>, porque no lo necesitamos. Lo segundo, hay que añadir una referencia al espacio de nombres de nuestra ventana, eso se hace a través de la propiedad "xmlns:local". Como resultado, obtenemos el siguiente contenido:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:OptimisationManagerExtention.View">

</ResourceDictionary>

Para establecer el tamaño/color u otras propiedades para los objetos de nuestra ventana, hay que describir su estilo. No voy a entrar en detalles respecto a la belleza de la aplicación, describiré simplemente el mínimo necesario. Usted puede añadir cualquier diseño para la ventana, animación, etc. Después de la edición, hemos obtenido el siguiente archivo que describe los estilos, además, todos los estilos se aplican automáticamente a todos los elementos de la ventana. Es bastante conveniente, ¿verdad?

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:OptimisationManagerExtention.View">
    
    <!--Establecemos el color del fondo de la ventana-->
    <Style TargetType="{x:Type local:ExtentionGUI}">
        <Setter Property="Background" Value="WhiteSmoke"/>
    </Style>

    <!--
    Establecemos el color del fondo de la banda divisor al arrastrar la cual 
    cambiamos los rangos de las zonas separadas horizontalmente en la primera pestaña 
    de nuestra ventana
    -->
    <Style TargetType="GridSplitter">
        <Setter Property="Background" Value="Black"/>
    </Style>

    <!--Establecemos el alto de listas desplegables-->
    <Style TargetType="ComboBox">
        <Setter Property="Height" Value="22"/>
    </Style>

    <!--Establecemos el alto de calendarios-->
    <Style TargetType="DatePicker">
        <Setter Property="Height" Value="22"/>
    </Style>

    <!--Establecemos el alto de los cuadros de texto-->
    <Style TargetType="TextBox">
        <Setter Property="Height" Value="22"/>
    </Style>

    <!--Establecemos el alto de botones-->
    <Style TargetType="Button">
        <Setter Property="Height" Value="22"/>
    </Style>

</ResourceDictionary>

Para que los estilos se apliquen a la ventana, es necesario describir la referencia a ellos en el marcado XAML de nuestra ventana: para eso, después de la etiqueta <Window>, se indica la siguiente construcción donde se establece la ruta hacia el archivo con recursos respecto a la posición de la ventana. 

<!--Conectamos los estilos-->
<Window.Resources>
    <ResourceDictionary Source="../Themes/Generic.xaml"/>
</Window.Resources>

Aparte del directorio creado View, creamos algunos directorios más:

Seguramente ya se ha dado cuenta de que la capa responsable de la gráfica se describe exclusivamente en el marcado XALM, sin usar el lenguaje C# directamente. Teniendo creados dos directorios correspondientes, hemos creado 2 espacios de nombres anidados que deben ser añadidos al marcado XAML de nuestra ventana para tener la posibilidad de usarlos. Además, vamos a crear la clase "ExtentionGUI_VM" en el espacio de nombres OptimisationManagerExtention.ViewModel. Esta clase será nuestro objeto de conexión, pero para poder ejecutar funciones requeridas, tiene que ser heredada de la interfaz "INotifyPropertyChanged". Contiene el evento PropertyChanged que transmite el aviso de la parte gráfica sobre el cambio de valores de uno de los campos, y por tanto sobre la necesidad de actualizar la gráfica. La clase creada tiene el siguiente aspecto:

/// <summary>
/// View Model
/// </summary>
class ExtentionGUI_VM : INotifyPropertyChanged
{
    /// <summary>
    /// Evento del cambio de alguna propiedad de ViewModel 
    /// y sus manejadores
    /// </summary>
    #region PropertyChanged Event
    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// Manejador del evento PropertyChanged
    /// </summary>
    /// <param name=»propertyName»Nombre de la variable actualizada</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
}

El marcado XAML, después de la creación de la ventana y adición de todas las referencias, es el siguiente:

<Window x:Class="OptimisationManagerExtention.View.ExtentionGUI"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:OptimisationManagerExtention.ViewModel"
        xmlns:viewExtention="clr-namespace:OptimisationManagerExtention.ViewExtention"
        mc:Ignorable="d"
        Title="ExtentionGUI" Height="450" Width="1100">

    <!--Conectamos estilos-->
    <Window.Resources>
        <ResourceDictionary Source="../Themes/Generic.xaml"/>
    </Window.Resources>
    <!--Conectamos ViewModel-->
    <Window.DataContext>
        <local:ExtentionGUI_VM />
    </Window.DataContext>    

    <Grid>
        

    </Grid>
</Window>

Las principales preparaciones para escribir la gráfica de nuestra aplicación ya están hechas por el momento, y podemos a empezar a rellenar el marcado XAML de nuestra ventana para crear la capa gráfica. Todos los controles van a escribirse dentro del bloque <Grid/>. A todos los que tienen poca experiencia en el trabajo con el marcado XAML, les recomiendo abrirlo desde el principio en Studio y verificar la lectura, para que sea más cómodo seguir el marcado durante la lectura. Los que están familiarizados bien con esta herramienta pueden usar las partes del código disponibles en este artículo, creo que serán suficientes. Si establecemos la analogía entre dos métodos de la creación de interfaces gráficas (WinForms / WPF), se puede decir que, aparte de unas diferencias evidentes, también tienen similitudes. Recordemos las interfaces WinForms, donde todos los elementos gráficos se representan como instancias de las clases y se almacenan en la parte oculta de una clase abstracta (por ejemplo, la clase Button o ComboBox).

Así, resulta que la aplicación gráfica WinForms se compone de un conjunto de instancias de los objetos interconectados. Analizando el marcado WPF, es difícil de imaginar que se basa en el mismo principio, pero es así. Cada elemento del marcado, por ejemplo, la etiqueta ya conocida "Grid", en realidad, es una clase, y si quiere, se puede recrear una aplicación exactamente igual, pero sin usar el marcado XAML, usando sólo las clases del espacio de nombres correspondiente. Sin embargo, va a ser feo y pesado. De hecho, al abrir la etiqueta <Grid>, indicamos que queremos crear una instancia de esta clase. Luego, los mecanismos del compilador analizan el marcado especificado por nosotros y crean las instancias de objetos requeridos. Esta propiedad de las aplicaciones WPF nos permite crear objetos gráficos personalizados, o los objetos que extienden la funcionalidad estándar. Vamos a considerar cómo se implementa eso más tarde.   

Volviendo al proceso de la creación de la gráfica, cabe mencionar que <Grid/> es un bloque de composición, lo que significa que está diseñado para una colocación conveniente de los controles y otros bloques de composición. Como puede observar en el vídeo, al cambiar entre las pestañas Settings y Optimisation Result, la parte inferior (ProgressBar) queda inalterada. Eso se consigue por medio de la división del bloque principal <Grid/> en 2 filas, que contienen el Panel con pestañas principales (TabControll), y otro bloque <Grid/>, que incluye la Barra de estado (Lable), ProgressBar y el botón para iniciar el proceso de la optimización. Este contenedor anidado también está dividido horizontalmente en 3 partes (columnas), cada una de las cuales contiene uno de los controles ( Lable, ProgressBar, Button).

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition Height="27"/>
    </Grid.RowDefinitions>

    <!--Creamos TabControl con dos pestañas-->
    <TabControl>
        <!--Pestaña para configurar el robot e iniciar la optimización o una simulación-->
        <TabItem Header="Settings">
           
        </TabItem>

        <!--Pestaña para ver los resultados de la optimización y el inicio de la simulación por el evento de doble clic-->
        <TabItem Header="Optimisation Result">
          
        </TabItem>
    </TabControl>

    <!--Contenedor con la barra de progreso, estado de la operación ejecutada y botón de inicio-->
    <Grid Grid.Row="1">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150"/>
            <ColumnDefinition/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <!--Estado de la operación ejecutada-->
        <Label Content="{Binding Status, UpdateSourceTrigger=PropertyChanged}"/>
        <!--Barra de progreso-->
        <ProgressBar Grid.Column="1" 
                                     Minimum="0" 
                                     Maximum="100"
                                     Value="{Binding PB_Value, UpdateSourceTrigger=PropertyChanged}"/>
        <!--Botón de inicio-->
        <Button Margin="5,0,5,0" 
                                Grid.Column="2"
                                Content="Start"
                                Command="{Binding Start}"/>
    </Grid>
</Grid>

Antes de profundizarnos en el tema, vamos a considerar las propiedades utilizadas junto con estos controles, a saber, cómo la información se pasa de ViewModel a View. Como será mostrado en adelante, para cada uno de los campos que muestran o sirven para introducir alguna información, en la clase ExtentionGUI_VM (nuestro objeto ViewMpodel), será creado un determinado campo que almacena su valor. Cuando se crean las aplicaciones WPF, y especialmente cuando se usa el patrón MVVM, normalmente, no se accede a los elementos gráficos directamente desde el código, por el nombre. Por eso, hemos usado un proceso más conveniente de transmisión de datos que, además, requiere el mínimo del código. Por ejemplo, la propiedad Value para el elemento gráfico ProgressBar se establece usando la tecnología de enlace, lo que se consigue con la siguiente línea:

 Value="{Binding PB_Value, UpdateSourceTrigger=PropertyChanged}"

La propiedad Binding es seguida por el nombre del campo que almacena la información y está titulado en la clase ViewModel, mientras que la propiedad  UpdateSourceTrigger indica en el modo de la actualización de la información en el elemento gráfico. Cuando establecemos esta propiedad con el parámetro PropertyChanged, informamos a la aplicación de que esta determinada propiedad de este determinado elemento tiene que actualizarse sólo si el evento PropertyChanged ha tenido lugar en la clase ExtentionGUI_VM, y el nombre de la variable ("PB_Value») con la que tenemos el enlace ha sido pasado como uno de los parámetros de este evento. Como puede ver en el marcado XAML, el botón también tiene el enlace de datos. Sin embargo, el botón se vincula con la propiedad Command, que a través de la interfaz ICommand indica en el comando (o para ser más exacto, en el método determinado en la clase ViewModel) que se invoca por el evento del clic en este botón. De esta manera, se realiza el enlace de eventos de la pulsación en el botón, y de otros eventos (por ejemplo, doble clic en la tabla con resultados de optimización). Nuestra parte gráfica ya ha adquirido unas formas principales y ahora tiene la siguiente apariencia:


El siguiente paso en la creación de la interfaz gráfica será la adición de los controles a la pestaña OptimisationResults. Esta pestaña contiene dos listas desplegables (Combobox) para seleccionar el terminal en el que ha sido realizada la optimización y el EA, respectivamente, así como el botón Update Report. Además, esta pestaña contiene un control anidado TabControl con dos pestañas anidadas, en cada una de las cueles se encuentra una tabla (ListView) en la que se muestran los resultados de la optimización. Es la apariencia en el marcado XAML:

  <!--Pestaña para ver los resultados de la optimización y el inicio de la simulación por el evento de doble clic-->
            <TabItem Header="Optimisation Result">
                <Grid Margin="5">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="50"/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>

                    <Grid VerticalAlignment="Center">
                        <WrapPanel>
                            <Label Content="Terminal:"/>
                            <ComboBox Width="250" 
                                  ItemsSource="{Binding TerminalsAfterOptimisation}"
                                  SelectedIndex="{Binding TerminalsAfterOptimisation_Selected, UpdateSourceTrigger=PropertyChanged}"/>
                            <Label Content="Expert"/>
                            <ComboBox Width="100"  
                                  ItemsSource="{Binding BotsAfterOptimisation}"
                                  SelectedIndex="{Binding BotsAfterOptimisation_Selected, UpdateSourceTrigger=PropertyChanged}"/>
                        </WrapPanel>
                        <Button HorizontalAlignment="Right"
                            Content="Update Report"
                            Command="{Binding UpdateOptimisationReport}"/>
                    </Grid>
                    <!--Contenedor con las tablas del resultado de la optimización-->
                    <TabControl 
                        TabStripPlacement="Bottom"
                        Grid.Row="1">
                        <!--Pestaña para mostrar los resultados de la optimización histórica-->
                        <TabItem Header="Backtest">
                            <!--Tabla con resultados de la optimización-->
                            <ListView ItemsSource="{Binding HistoryOptimisationResults}"
                                  viewExtention:ListViewExtention.DoubleClickCommand="{Binding StartTestFromOptimisationResults}"
                                  viewExtention:ListViewExtention.DoubleClickCommandParameter="History"
                                  SelectedIndex="{Binding SelectedHistoryOptimisationRow}" >
                                <ListView.View>
                                    <GridView 
                                    viewExtention:GridViewColumns.ColumnsSource="{Binding OptimisationResultsColumnHeadders}"
                                    viewExtention:GridViewColumns.DisplayMemberMember="DisplayMember"
                                    viewExtention:GridViewColumns.HeaderTextMember="HeaderText"/>
                                </ListView.View>
                            </ListView>
                        </TabItem>
                        <!--Pestaña para mostrar los resultados de los pasos de la 
                    optimización forward-->
                        <TabItem Header="Forvard">
                            <!--Tabla con resultados de la optimización-->
                            <ListView ItemsSource="{Binding ForvardOptimisationResults}"
                                  viewExtention:ListViewExtention.DoubleClickCommand="{Binding StartTestFromOptimisationResults}"
                                  viewExtention:ListViewExtention.DoubleClickCommandParameter="Forvard"
                                  SelectedIndex="{Binding SelectedForvardOptimisationRow}">
                                <ListView.View>
                                    <GridView 
                                   viewExtention:GridViewColumns.ColumnsSource="{Binding OptimisationResultsColumnHeadders}"
                                   viewExtention:GridViewColumns.DisplayMemberMember="DisplayMember"
                                   viewExtention:GridViewColumns.HeaderTextMember="HeaderText"/>
                                </ListView.View>
                            </ListView>
                        </TabItem>
                    </TabControl>
                </Grid>
            </TabItem>

Como ya ha sido mencionado antes, cada etiqueta utilizada en el marcado XAML representa una clase, además, podemos escribir las clases personalizadas que extienden la funcionalidad del marcado estándar o crean los elementos gráficos personalizados. Precisamente en este paso, tenemos la necesidad de ampliar la funcionalidad del marcado existente. Como se puede ver en el vídeo, nuestras tablas con los resultados de los pasos de optimización deben tener un número de columnas diferente y los nombres diferentes: eso será la primera extensión que vamos a crear.

Nuestra segunda extensión es la conversión del evento del doble clic a la interfaz ICommand. Se podría evitar la necesidad de crear la segunda extensión si no usáramos la plantilla de desarrollo MVVM, según la cual ViewModel (y más aun Model) no tiene que estar conectado con la capa View. Eso se hace para poder modificar fácilmente la capa gráfica de la aplicación o incluso rescribir todo desde cero, en caso de necesidad. Como se puede ver desde el método de la llamada a estas extensiones, todas ellas se encuentran en el espacio de nombres anidado ViewExtention. Luego, tras dos puntos, se pone el nombre de la clase con extensiones, y después del operador «punto», va el nombre de la propiedad para la que se establece un valor.

Para una comprensión detallada de estas extensiones, vamos a analizar cada una de ellas, empezando con la extensión que convierte el evento de los clics en la interfaz ICommand. Para crear una extensión que procesa el evento de doble clic, creamos la clase ListViewExtention en la carpeta ViewExtention partial. El modificador del acceso partial significa que se puede dividir la implementación de esta clase entre varios archivos, entonces, todos los métodos/campos y los demás elementos de la clase marcada como partial, pero dividida entre dos o más archivos, van a pertenecer a la misma clase.

using System.Windows;

using ICommand = System.Windows.Input.ICommand;
using ListView = System.Windows.Controls.ListView;

namespace OptimisationManagerExtention.ViewExtention
{
    /// <summary>
    /// La clase de las extensiones para ListView que convierte los eventos en los comandos (ICommand)
    ///La clase está marcada con palabra clave partial, es decir, su implimentación está dividida en varios archivos.
    /// 
    /// En esta clase está implementada la conversión del evento ListView.DoubleClickEvent 
    /// en comando tipo ICommand
    /// </summary>
    partial class ListViewExtention
    {
        #region Command
        /// <summary>
        /// Propiedad dependiente - contiene una referencia a callback del comando
        /// La propiedad se establece a través del marcado XAML del proyecto
        /// </summary>
        public static readonly DependencyProperty DoubleClickCommandProperty =
            DependencyProperty.RegisterAttached("DoubleClickCommand",
                typeof(ICommand), typeof(ListViewExtention),
                new PropertyMetadata(DoubleClickCommandPropertyCallback));

        /// <summary>
        /// Setter для DoubleClickCommandProperty
        /// </summary>
        /// <param name=»obj»>Control</param>
        /// <param name=»value»>Valor para realizar el enlace</param>
        public static void SetDoubleClickCommand(UIElement obj, ICommand value)
        {
            obj.SetValue(DoubleClickCommandProperty, value);
        }
        /// <summary>
        /// Geter para DoubleClickCommandProperty
        /// </summary>
        /// <param name=»obj»>Control</param>
        /// <returns>referencia al comando guardado tipo ICommand</returns>
        public static ICommand GetDoubleClickCommand(UIElement obj)
        {
            return (ICommand)obj.GetValue(DoubleClickCommandProperty);
        }
        /// <summary>
        /// Callback llamado tras establecer la propiedad DoubleClickCommandProperty
        /// </summary>
        /// <param name=»obj»>Control para el que se establece la propiedad del</param>
        /// <param name=»args»>evento precedente a la llamada a callback</param>
        private static void DoubleClickCommandPropertyCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            if (obj is ListView lw)
            {
                if (args.OldValue != null)
                    lw.MouseDoubleClick -= Lw_MouseDoubleClick;

                if (args.NewValue != null)
                    lw.MouseDoubleClick += Lw_MouseDoubleClick;
            }
        }
        /// <summary>
        /// Callback del evento que se convierte en el tipo ICommand
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void Lw_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            if (sender is UIElement element)
            {
                object param = GetDoubleClickCommandParameter(element);
                ICommand cmd = GetDoubleClickCommand(element);
                if (cmd.CanExecute(param))
                    cmd.Execute(param);
            }
        }
        #endregion

        #region CommandParameter
        /// <summary>
        /// Propiedad dependiente - contiene una referencia a los parámetros pasados en callback tipo ICommand
        /// La propiedad se establece a través del marcado XAML del proyecto
        /// </summary>
        public static readonly DependencyProperty DoubleClickCommandParameterProperty =
            DependencyProperty.RegisterAttached("DoubleClickCommandParameter",
                typeof(object), typeof(ListViewExtention));
        /// <summary>
        /// Setter для DoubleClickCommandParameterProperty
        /// </summary>
        /// <param name=»obj»>Control</param>
        /// <param name=»value»>Valor para realizar el enlace</param>
        public static void SetDoubleClickCommandParameter(UIElement obj, object value)
        {
            obj.SetValue(DoubleClickCommandParameterProperty, value);
        }
        /// <summary>
        /// Geter para DoubleClickCommandParameterProperty
        /// </summary>
        /// <param name=»obj»>Control</param>
        /// <returns>parámetro pasado</returns>
        public static object GetDoubleClickCommandParameter(UIElement obj)
        {
            return obj.GetValue(DoubleClickCommandParameterProperty);
        }
        #endregion
    }
}

Cada propiedad de cada clase de WPF de objetos gráficos está vinculada a la clase DependancyProperty. Esta clase permite realizar el enlace de datos entre las capas View y ViewModel. Para crear una instancia de esta clase, hay que usar el método estático DependencyProperty.RegisterAttached que devuelve la clase configurada DependencyProperty. Este método recibe 4 parámetros. Para más información, consulte aquí. Cabe mencionar el hecho de que la propiedad creada tiene que tener obligatoriamente los modificadores de acceso public static readonly (es decir, accesible desde fuera de esta clase, posibilidad de llamar a esta propiedad sin necesidad de crear una instancia de la clase, mientras que el modificador static establece la unidad de esta propiedad dentro de esta aplicación, y readonly hace que la propiedad sea inalterable).

  1. El primer parámetro establece el nombre por el que la propiedad estará visible en el marcado XAML.
  2. El segundo parámetro establece el tipo del elemento con el que se planea realizar el enlace. Los objetos precisamente de este tipo van a guardarse en la instancia creada de la clase DependancyProperty. 
  3. El tercer parámetro establece el tipo de la clase en la que se encuentra esta propiedad. En nuestro caso, es la clase ListViewExtention.
  4. El último parámetro acepta la instancia de la clase PropertyMetadata. En realidad, este parámetro se refiere al manejador del evento invocado cuando se termina la creación de la instancia de la clase DependancyProperty. Este callback es necesario para suscribirse al evento del doble clic.

Para que podamos establecer y obtener correctamente los valores desde esta propiedad, necesitamos crear los métodos, cuyos nombres van a componerse del nombre de la instancia de la clase DependancyProperty pasada durante la creación y de los prefijos Set (para establecer valores) y Get (para obtener valores). Ambos métodos tienen que ser estáticos. En realidad, ellos encapsulan el uso de los métodos existentes SetValue y GetValue.

El callback del evento relacionado con la conclusión de la creación de una propiedad dependiente realiza la suscripción al evento del doble clic en la fila de la tabla y se da de baja del evento suscrito si éste ya existía antes. Dentro del manejador del evento de doble clic, se realiza una llamada consecutiva de los métodos  CanExecute y Execute desde el campo tipo ICommand pasado en View. Así, cuando se dispara un evento de doble clic en alguna fila de la tabla suscrita, llamamos automáticamente al manejador (handle) de este evento, en el que se llaman a los métodos de la lógica que se ejecuta tras la legada de este evento.

La clase creada es, en realidad, una clase intermediaria. Se encarga del procesamiento de eventos y de la llamada a los métodos desde ViewModel, pero no ejecuta personalmente ninguna lógica de negocio. Tal vea, este enfoque parezca más confuso en comparación con una llamada directa al método desde el manejador del evento de doble clic (como es habitual en WinForms), pero hay razones de peso para usar precisamente este enfoque. Es la necesidad de cumplir el patrón MVVM, que dice que View no tiene que saber nada de ViewModel, y viceversa.

Usando esta clase intermediaria, reducimos esta cohesión entre las clases, para lo que se usa la plantilla de programación mencionada. Ahora, podemos editar la clase ViewModel como queramos. Lo único que hace falta es especificar una propiedad determinada tipo ICommand a la que va a acceder nuestra clase intermediaria.

La extensión descrita también tiene implementada la propiedad de la conversión del evento SelectionChanged в ICommand, así como, la clase intermediaria que crea automáticamente las columnas para la tabla basándose en el campo de enlace que almacena una colección de nombres para las columnas. Ambas extensiones del marcado XAML están implementadas usando el método arriba descrito, por eso, no voy a entrar en detalles. Si algo no le queda claro, no dude en preguntar en los comentarios, contestaré con mucho gusto. Ahora, después de implementar el marcado de la pestaña Optimisation Result, nuestra ventana tiene esta apariencia:


El siguiente paso es la implementación de la pestaña Settings. Por motivos de conveniencia, voy a mostrar aquí sólo una parte para esta pestaña que describe los principales objetos gráficos. Puede ver la versión completa en el código fuente que se adjunta al artículo.

<!--Pestaña para configurar el robot e iniciar la optimización o una simulación-->
            <TabItem Header="Settings">
                <!--Contenedor con las configuraciones, etc.-->
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition Height="200"/>
                    </Grid.RowDefinitions>

                    <!--Contenedor con la lista de terminales seleccionados-->
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="30"/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <!--Contenedor con la selección de los terminales que se determinan automáticamente-->
                        <WrapPanel HorizontalAlignment="Right" 
                                       VerticalAlignment="Center">
                            <!--Lista con los terminales-->
                            <ComboBox Width="200" 
                                          ItemsSource="{Binding TerminalsID}"
                                          SelectedIndex="{Binding SelectedTerminal, UpdateSourceTrigger=PropertyChanged}"
                                          IsEnabled="{Binding IsTerminalsLVEnabled, UpdateSourceTrigger=PropertyChanged}"/>
                            <!--Botón para añadir terminal-->
                            <Button Content="Add" Margin="5,0"
                                    Command="{Binding AddTerminal}"
                                    IsEnabled="{Binding IsTerminalsLVEnabled, UpdateSourceTrigger=PropertyChanged}"/>
                        </WrapPanel>
                        <!--Lista con los terminales seleccionados-->
                        <ListView Grid.Row="1"
                                  ItemsSource="{Binding SelectedTerminalsForOptimisation}"
                                  SelectedIndex="{Binding SelectedTerminalIndex, UpdateSourceTrigger=PropertyChanged}"
                                  IsEnabled="{Binding IsTerminalsLVEnabled, UpdateSourceTrigger=PropertyChanged}" >
                            <ListView.View>
                                <GridView>
                                .
                                .
                                .
                                </GridView>
                            </ListView.View>
                        </ListView>
                    </Grid>
                    <!--Contenedor con parámetros para editar y 
                    configurar optimización-->
                    <TabControl
                                Grid.Row="2" 
                                Margin="0,0,0,5"
                                TabStripPlacement="Right">
                        <!--Pestaña de parámetros del robot-->
                        <TabItem Header="Bot params" >
                            <!--Lista con parámetros de robot-->
                            <ListView 
                                    ItemsSource="{Binding BotParams, UpdateSourceTrigger=PropertyChanged}">
                                <ListView.View>
                                    <GridView>
                                    .
                                    .
                                    .
                                    </GridView>
                                </ListView.View>
                            </ListView>
                        </TabItem>
                        <!--Pestaña de configuraciones de la optimización-->
                        <TabItem Header="Settings">
                            <Grid MinWidth="700"
                                          MinHeight="170"
                                          MaxWidth="750"
                                          MaxHeight="170">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition/>
                                    <ColumnDefinition/>
                                    <ColumnDefinition/>
                                </Grid.ColumnDefinitions>
                                <Grid.RowDefinitions>
                                    <RowDefinition/>
                                    <RowDefinition/>
                                    <RowDefinition/>
                                </Grid.RowDefinitions>
                                <!--Login visible para el robot-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center">
                                    <Label Content="Login:"/>
                                    <TextBox Text="{Binding TestLogin, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Tipo de ejecución-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="1"
                                            Grid.Row="1">
                                    <Label Content="Execution:"/>
                                    <ComboBox 
                                            DataContext="{Binding ExecutionList}"
                                            ItemsSource="{Binding ItemSource}"
                                            SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Tipo para mostrar el historial para pruebas-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="2"
                                            Grid.Row="1">
                                    <Label Content="Model:"/>
                                    <ComboBox 
                                            DataContext="{Binding ModelList}"
                                            ItemsSource="{Binding ItemSource}"
                                            SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Criterios de optimización-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="2"
                                            Grid.Row="2">
                                    <Label Content="Optimisation criteria:"/>
                                    <ComboBox DataContext="{Binding OptimisationCriteriaList}"
                                                  ItemsSource="{Binding ItemSource}"
                                                  SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Fecha de inicio del período forward-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="1"
                                            Grid.Row="0">
                                    <Label Content="Forvard date:"/>
                                    <DatePicker SelectedDate="{Binding ForvardDate, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Depósito-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="0"
                                            Grid.Row="1">
                                    <Label Content="Deposit:"/>
                                    <ComboBox DataContext="{Binding Deposit}" 
                                                  ItemsSource="{Binding ItemSource}"
                                                  SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Divisa pare medir beneficio-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="0"
                                            Grid.Row="2">
                                    <Label Content="Currency:"/>
                                    <ComboBox DataContext="{Binding CurrencyList}"
                                                  ItemsSource="{Binding ItemSource}"
                                                  SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Apalancamiento-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="1"
                                            Grid.Row="2">
                                    <Label Content="Laverage:"/>
                                    <ComboBox DataContext="{Binding LaverageList}"
                                                  ItemsSource="{Binding ItemSource}"
                                                  SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--¿Usar el visualizador de la prueba?-->
                                <CheckBox Content="Visual mode"
                                              Margin="2"
                                              VerticalAlignment="Center"
                                              Grid.Column="2"
                                              Grid.Row="0"
                                              IsChecked="{Binding IsVisual, UpdateSourceTrigger=PropertyChanged}"/>
                            </Grid>
                        </TabItem>
                    </TabControl>

                    <!--Banda divisora que permite modificar los tamaños de 
                    un área respecto a otra-->
                    <GridSplitter Height="3" VerticalAlignment="Bottom" HorizontalAlignment="Stretch"/>

                </Grid>
            </TabItem>

En primer lugar, merece la pena de hablar sobre el método de la implementación de las áreas alteradas dinámicamente. Este comportamiento del formulario se implementa mediante la formación de dos líneas en <Grid/> principal y adición del elemento <GridSplitter/>. Le arrastramos precisamente a él para que el área con la lista de los terminales y el área con las demás tablas cambie sus dimensiones. En la primera línea de la tabla formada, se inserta nuevo <Grid/>, que se divide de nuevo en 2 partes. La primera contiene otro elemento de diseño, WrapPanel, que contiene la lista de terminales y el botón para añadir un terminal nuevo. La segunda parte incluye una tabla con la lista de terminales añadidos.

Aparte del texto, en la tabla también se insertan los controles que permiten editar los datos en esta tabla. Gracias a la tecnología del enlace de datos, para editar/añadir los valores en la tabla, no tenemos que escribir ni una sola línea del código, porque la tabla está asociada directamente con la colección de los datos de los controles. La parte inferior del bloque <Grid/> editado contiene TabControl, en el que se encuentran los ajustes del Simulador de Estrategias y la tabla con la lista de parámetros del robót.

Aquí, la formación del envoltorio gráfico de esta extensión se puede considerar terminada. Pero antes de proceder a la descripción de ViewModel, hablaremos un poco sobre el método del enlace de las tables.

Describiremos este aspecto a base del ejemplo de la tabla con los parámetros de robots. Como podemos observar en el Simulador de MetaTrader, tiene que tener los siguientes campos:

Para pasar todos estos parámetros en la tabla, necesitamos crear una clase para almacenar los datos de la fila de esta tabla. En otras palabras, esta clase debe describir todas las columnas de la tabla, mientras que la colección de estas clases va a almacenar la tabla entera. Para la tabla en cuestión, ha sido creada la siguiente clase:

/// <summary>
/// La clase que describe las filas para la tabla con los parámetros del robot antes de la optimización
/// </summary>
class ParamsItem
{
    /// <summary>
    /// Constructor de la clase
    /// </summary>
    /// <param name=»Name»>Nombre de la viariable</param>
    public ParamsItem(string Name) => Variable = Name;
    /// <summary>
    /// Señal si hay que optimizar esta variable del robot
    /// </summary>
    public bool IsOptimize { get; set; }
    /// <summary>
    /// Nombre de la variable
    /// </summary>
    public string Variable { get; }
    /// <summary>
    /// Valor de la variable seleccionado para la prueba
    /// </summary>
    public string Value { get; set; }
    /// <summary>
    /// Inicio del repaso de parámetros
    /// </summary>
    public string Start { get; set; }
    /// <summary>
    /// Paso del repaso de parámetros
    /// </summary>
    public string Step { get; set; }
    /// <summary>
    /// Fin del repaso de parámetros
    /// </summary>
    public string Stop { get; set; }
}

Como podemos ver, cada propiedad de esta clase contiene la información sobre una columna determinada. Ahora, si entramos en detalles, podemos observar cómo cambia el contexto de datos. Durante la creación de la ventana de la aplicación, desde el principio hemos dicho que la fuente de datos para la ventana será la clase ExtentionGUI_VM, siendo la principal DataContext para esta ventana, y debe contener la colección con la que se asocia la tabla. No obstante, para cada determinada fila de esta tabla determinada DataContext se cambia de la clase ExtentionGUI_VM a la clase  ParamsItem. Este detalle es muy importante, porque si, por ejemplo, hay que actualizar alguna celda en esta tabla desde el código del programa, entonces tendremos que llamar al evento PropertyChanged no en la clase ExtentionGUI_VM, sino en la clase del contexto de esta determinada fila.

Pues, hemos terminado la descripción de la creación de la capa gráfica de nuestra aplicación y podemos proceder a la descripción de la clase de la conexión de la aplicación y la lógica del programa.


ViewModel y conector entre MetaTrader y la dll implementada

El siguiente componente del programa es la parte que es responsable de la conexión entre la gráfica considerada antes y la lógica que será considerada a continuación. En la plantilla de programación utilizada por nosotros (Model View ViewModel o MVVM), esta parte se llama ViewModel y se encuentra en el espacio de nombres correspondiente (OptimisationManagerExtention.ViewModel).

En el primer capítulo del artículo, ya hemos creado la clase ExtentionGUI_VM y hemos implementado la interfaz INotifyPropertyChanged —precisamente esta clase nos servirá de conexión entre la gráfica y la lógica. Antes de describir el proceso de su implementación, por favor, nótese que todos los campos de la clase ExtentionGUI_VM, con los que los datos desde View están enlazados, tienen que estar declarados como propiedades (Property), y no como variables. Si no está familiarizado con esta construcción del lenguaje C#, abajo se muestra un ejemplo que explica la diferencia:

class A
{
    /// <summary>
    /// Es un campo simple público para el que se puede establecer valores y leer valores de él
    /// Pero no puede realizar comprobaciones u otras acciones.
    /// </summary>
    public int MyField = 5;
    /// <summary>
    /// Es una propiedad que permite procesar la información antes la escritura o lectura de datos
    /// </summary>
    public int MyGetSetProperty
    {
        get
        {
            MyField++;
            return MyField;
        }
        set
        {
            MyField = value;
        }
    }

    // Esta propiedad sólo para la lectura
    public int GetOnlyProperty => MyField;
    /// <summary>
    // Esta propiedad sólo para la escritura
    /// </summary>
    public int SetOnlyProperty
    {
        set
        {
            if (value != MyField)
                MyField = value;
        }
    }
}

Como se ve en el ejemplo, las propiedades es una especie del híbrido de métodos y campos. Permiten ejecutar algunas acciones antes de devolver el valor o verificar la corrección de los datos a escribir. Además, las propiedades pueden ser sólo para la lectura o sólo para la escritura. Nos referíamos precisamente a estas construcciones del lenguaje C# en View, cuando aplicábamos el enlace de datos.

Durante la implementación de la clase, ExtentionGUI_VM lo dividí en varios bloques (construcciones #reguin #endregion), y a base de ellos voy a considerar el proceso de su creación. Ya que en la clase View, primero hemos considerado la creación de la pestaña Optimisation Result, en esta clase comenzaremos con las propiedades y los métodos creados para esta pestaña. Por motivos de conveniencia, primero mostraré l código responsable de los datos visualizados en esta pestaña, y luego voy a escribir explicaciones.

#region Optimisation Result

/// <summary>
/// Tabla con resultados de la optimización histórica
/// </summary>
public DataTable HistoryOptimisationResults => model.HistoryOptimisationResults;
/// <summary>
/// Tabla con resultados de la optimización forward
/// </summary>
public DataTable ForvardOptimisationResults => model.ForvardOptimisationResults;
/// <summary>
/// Colección observada con lista de columnas de optimización
/// </summary>
public ObservableCollection<ColumnDescriptor> OptimisationResultsColumnHeadders =>
       model.OptimisationResultsColumnHeadders;

#region Start test from optimisation results
/// <summary>
/// Inicio de la prueba para el paso seleccionado de optimización
/// </summary>
public ICommand StartTestFromOptimisationResults { get; }
/// <summary>
/// Método que inicia la prueba con doble clic
/// </summary>
/// <param name="type"></param>
private void StartTestFromOptimisationResultsAction(object type)
{
    ENUM_TableType tableType = (string)type == "History" ?
        ENUM_TableType.History : ENUM_TableType.Forvard;
    int ind = tableType == ENUM_TableType.History ?
        SelectedHistoryOptimisationRow : SelectedForvardOptimisationRow;

    model.StartTest(tableType, ind);
}
#endregion

/// <summary>
/// índice de la fila seleccionada de la tabla de optimizaciones históricas
/// </summary>
public int SelectedHistoryOptimisationRow { get; set; } = 0;
/// <summary>
/// Indice  de la fila seleccionada de la optimización forward
/// </summary>
public int SelectedForvardOptimisationRow { get; set; } = 0;

#region UpdateOptimisationReport

#region TerminalsAfterOptimisation
public ObservableCollection<string> TerminalsAfterOptimisation => model.TerminalsAfterOptimisation;
public int TerminalsAfterOptimisation_Selected
{
    get => model.TerminalsAfterOptimisation_Selected;
    set
    {
        model.TerminalsAfterOptimisation_Selected.SetVarSilently(value);
        if (value > -1)
           model.SelectNewBotsAfterOptimisation_forNewTerminal();
    }
}
        
public ObservableCollection<string> BotsAfterOptimisation => model.BotsAfterOptimisation;
public int BotsAfterOptimisation_Selected
{
    get => model.BotsAfterOptimisation_Selected;
    set => model.BotsAfterOptimisation_Selected.SetVarSilently(value);
}
#endregion
public ICommand UpdateOptimisationReport { get; }

        private void UpdateReportsData(object o)
        {
            model.LoadOptimisations();
        }
        #endregion
        #endregion

Primero, vamos a considerar las fuentes de datos para las tablas con la optimización histórica y forward, así como, la lista de columnas que está conectada con las columnas de ambas tablas a través de la clase intermediaria (GridViewColumns). Como podemos ver en este fragmento del código, cada tabla tiene dos campos únicos: una fuente de datos (tipificada por la clase DataTable) y una propiedad que incluye el índice de la fila seleccionada en la tabla. El índice de la fila seleccionada no es importante para la visualización, pero será necesario para las futuras acciones, a saber, para iniciar el testeo por el doble clic en la fila de la tabla. Tomando en cuenta que la carga de los datos en las tablas y su limpieza se implementa por la lógica del programa -y de acuerdo con los principios de la POO, una determinada clase es responsable de una determinada tarea- en la propiedades que facilitan la información sobre la composición de la tabla, nosotros simplemente nos referimos a las propiedades correspondientes de la clase principal del modelo (ExtemtionGUI_M). El rastreo de los índices seleccionados se ejecuta automáticamente a través de los clics en los bordes de la tabla, y por tanto, estas propiedades no realizan ningunas acciones no verificaciones. En realidad, son similares que los campos de la clase.

También, hay que prestar atención en el tipo de datos utilizado para la propiedad que contiene la lista de columnas (OptimisationResultsColumnHeadders) — ObservableCollection<T>. Es una de las clases estándar del lenguaje С# que almacenan las colecciones alteradas dinámicamente, pero a diferencia de las listas (List<T>), esta clase contiene el evento CollectionChanged que es llamado cada vez cuando los datos de la colección se modifican/eliminan/añaden. De esta manera, después crear una propiedad tipificada por esta clase, obtenemos una notificación automática de View sobre el hecho de que la fuente de datos ha sido alterada. Por tanto, ya no nos hace falta notificar manualmente al gráfico de la necesidad de rescribir los datos visualizados, lo que puede ser bastante conveniente en algunas ocasiones.

Una vez aclarado el asunto de las tablas, hay que prestar atención en las listas desplegables con la selección de los terminales y robots, así como, proceder a la implementación de los manejadores de eventos de la pulsación en los botones y clics en la tabla. El bloque para trabajar con listas desplegables y cargar los resultados de la optimización se encuentra en el área marcada como  #region UpdateOptimisationReport. Primero, vamos a considerar la fuente de datos para la primera lista desplegable que incluye la lista de terminales. Es la lista de ID de terminales, para los cuales se realizará la optimización, y el índice de terminales seleccionados. Puesto que la lista de los terminales se compone por el modelo, simplemente vamos a referirnos al campo correspondiente en el modelo. La selección del índice de un terminal seleccionado es una tarea un poco más complicada, por eso, vamos a aprovechar de la ventaja sobre los campos que ha sido mencionada antes. Una vez seleccionado el terminal de la lista desplegable, se invoca el setter de la propiedad  TerminalsAfterOptimisation_Selected, en el que realizamos una serie de acciones:

  1. Guardamos nuevo índice seleccionado en el modelo
  2. Actualizamos todos los valores de la segunda lista desplegable que almacena la lista de los robots que han pasado la optimización en este terminal.

Es necesario hacerlo, porque la extensión almacena el historial de las pruebas realizadas, agrupándolas por los robots y terminales. Si volvemos a optimizar el mismo robot en el mismo terminal, el historial anterior reescribe. Este método para pasar los eventos de View a ViewModel es el más conveniente. Lamentablemente, no siempre es apropiado.

El siguiente método para pasar los eventos de la capa gráfica a ViewModel consiste en usar los comandos. Algunos elementos gráficos, por ejemplo Button, soportan comando. Cuando usamos los comandos, enlazamos la propiedad command con una propiedad de ViewModel por el tipo parametrizado ICommand. La interfaz ICommand es uno de las interfaces estándar del lenguaje C# y tiene la siguiente apariencia:

public interface ICommand
{
    //
    // Summary:
    //     Occurs when changes occur that affect whether or not the command should execute.
    event EventHandler CanExecuteChanged;
 
    //
    // Summary:
    //     Defines the method that determines whether the command can execute in its current
    //     state.
    //
    // Parameters:
    //   parameter:
    //     Data used by the command. If the command does not require data to be passed,
    //     this object can be set to null.
    //
    // Returns:
    //     true if this command can be executed; otherwise, false.
    bool CanExecute(object parameter);
    //
    // Summary:

    //     Defines the method to be called when the command is invoked.
    //
    // Parameters:
    //   parameter:
    //     Data used by the command. If the command does not require data to be passed,
    //     this object can be set to null.
    void Execute(object parameter);
}

Cuando se pulsa el botón, el evento ConExecute se dispara primero. Si devuelve false, el botón se hace inaccesible. De lo contrario, se invoca el método Execute, que realiza la operación necesaria. Para poder usar esta funcionalidad, es necesario implementar esta interfaz. Durante su implementación, no inventaba nada nuevo, simplemente usé su implementación estándar, ya que me pareció más apropiada.

/// <summary>
/// Implementación de la interfaz ICommand que se usa para
/// enlazar los comandos con los métodos de ViewModel
/// </summary>
class RelayCommand : ICommand
{
    #region Fields 
    /// <summary>
    /// El delegado que ejecuta el evento
    /// </summary>
    readonly Action<object> _execute;
    /// <summary>
    /// El delegado que comprueba la posibilidad de realizar la acción
    /// </summary>
    readonly Predicate<object> _canExecute;
    #endregion // Fields

    /// <summary>
    /// Конструктор
    /// </summary>
    /// <param name=»execute»>El método pasado por el delegado, que es callback</param>
    public RelayCommand(Action<object> execute) : this(execute, null) { }
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="execute">
    /// El método pasado por delegado que es callback
    /// </param>
    /// <param name="canExecute">
    /// El método pasado por delegado que que comprueba la posibilidad de ejecutar el evento
    /// </param>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");
        _execute = execute; _canExecute = canExecute;
    }

    /// <summary>
    /// Verificando la posibilidad de ejecutar el evento
    /// </summary>
    /// <param name=»parameter»>parámetro pasado de View</param>
    /// <returns></returns>
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }
    /// <summary>
    /// El evento que es llamado cada vez que se cambia la posibilidad de ejecutar callback.
    /// Cuando se dispara este evento, el formulario llama al método "CanExecute"
    /// El evento se dispara de ViewModel si es necesario
    /// </summary>
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
    /// <summary>
    /// El método que llama al delegado que, a su vez, ejecuta el evento
    /// </summary>
    /// <param name=»parameter»>parámetro pasado de View</param>
    public void Execute(object parameter) { _execute(parameter); }
}

De acuerdo con esta implementación de la interfaz ICommand, se crean dos campos privados de sólo lectura, que almacenan los delegados, que almacenan los métodos pasados a través de una de las sobrecargas del constructor de la clase RelayCommand. Como resultado, para usar este mecanismo, crea una instancia de la clase RelayCommand en el constructor de la clase ExtentionGUI_VM. Pásele a esta instancia el método que ejecuta algunas acciones. Si hablamos de la propiedad considerada  UpdateOptimisationReport que actualiza la información en las tablas con optimización, eso tiene el siguiente aspecto:

UpdateOptimisationReport = new RelayCommand(UpdateReportsData);

Aquí UpdateReportsData es un método private de la clase ExtentionGUI_VM que llama al método LoadOptimisations() de la clase ExtentionGUI_M (o sea, la clase de nuestro modelo). El enlace entre la propiedad  StartTestFromOptimisationResults y el evento del doble clic en la fila de la tabla seleccionada por el usuario se realiza absolutamente de la misma manera. Pero en este caso, el traspaso del evento del clic no se realiza a través de la propiedad realizada de manera estándar, como con el botón (clase Button), sino a través de la extensión ya creada y descrita " ListV iewExtention.DoubleClickCommand". Como se puede ver de la signatura de los métodos Execute y CanExecute, pueden aceptar los valores tipo Object. En caso con el botón, no pasamos ningún valor; pero en caso con el evento de doble clic, pasamos el nombre de la tabla: puede verlo del método del enlace con estas propiedades en el marcado XAML:  

viewExtention:ListViewExtention.DoubleClickCommand="{Binding StartTestFromOptimisationResults}"
viewExtention:ListViewExtention.DoubleClickCommandParameter="History"

Basándose precisamente en este parámetro, nuestro modelo entiende de qué tabla hay que coger los datos para iniciar el repaso de la optimización.

Ahora vamos a considerar la implementación de las propiedades y callbacks para trabajar con la pestaña Settings, ya que contiene todos los principales de controles. Entre las interesantes peculiaridades de la implementación de la parte de enlace para esta pestaña, la primera a considerar será la implementación de la fuente de datos para la tabla que contiene los terminales seleccionados.

#region SelectedTerminalsForOptimisation && SelectedTerminalIndex (first LV params)
/// <summary>
/// Lista de terminales seleccionados para la optimización mostrados en la tabla de terminales
/// </summary>
public ObservableCollection<TerminalAndBotItem> SelectedTerminalsForOptimisation { get; private set; } =
    new ObservableCollection<TerminalAndBotItem>();
/// <summary>
/// Indice de la fila seleccionada
/// </summary>
private int selectedTerminalIndex = 0;
public int SelectedTerminalIndex
{
    get { return selectedTerminalIndex; }
    set
    {
        // Asignamos el valor al índice nuevo seleccionado
        selectedTerminalIndex = value;

        //((RelayCommand)Start).OnCanExecuteChanged();

        // Llenamos la lista de parámetros para el robot seleccionada en la fila actual
        if (value == -1)
        {
            return;
        }
        TerminalAndBotItem terminal_item = SelectedTerminalsForOptimisation[value];
        if (terminal_item.Experts.Count > 0)
        {
            FillInBotParams(terminal_item.Experts[terminal_item.SelectedExpert],
                terminal_item.TerminalID);
        }
    }
}
        #endregion

Como vemos, la lista de los terminales está presentada como una colección observada, tipificada por la clase TerminalAndBotItem. Esta colección está almacenada en la clase ViewModel. ViewModel también contiene la propiedad para establecer y obtener el índice de la fila seleccionada: eso ha sido hecho para tener la posibilidad de reaccionar al evento de la selección de algún terminal de la lista. Como ha sido mostrado en el vídeo, al hacer clic en la fila, los parámetros del robót seleccionado se cargan dinámicamente. Este comportamiento se implementa en el setter de la propiedad  SelectedTerminalIndex.

Además hay que recordar que las filas en la tabla con terminales seleccionados contienen los controles, por tanto, necesitaremos organizar la clase  TerminalAndBotItem como la clase de contexto de datos. Veamos algunos interesantes detalles de su implementación.

El primer detalle es la manera de eliminar el terminal de la lista de los terminales. Como hemos dicho antes, los datos para la tabla se almacenan en ViewModel, mientras que callback para el botón Delete ubicado en la tabla se puede enlazar sólo con el contexto de los datos de esta fila, es decir, con la clase  TerminalAndBotItem de la que no hay acceso a esta colección. La solución en esta situación puede ser el uso de los delegados. Yo implementé un método para eliminar los datos en la clase ExtentionGUI_VM, luego, lo pasé a la clase TerminalAndBotItem como delegado, a través del constructor. Para mayor claridad, eliminé todas las líneas sobrantes del código de abajo para esta expresión. El traspaso del método para eliminar a sí mismo desde fuera es el siguiente 

class TerminalAndBotItem
{
    
    public TerminalAndBotItem(List<string> botList,
        string TerminalID,
        Action<string, string> FillInBotParams,
        Action<TerminalAndBotItem> DeleteCommand)
    {
        // Llenamos los campos de los delegados
        #region Delegates
        this.FillInBotParams = FillInBotParams;
        this.DeleteCommand = new RelayCommand((object o) => DeleteCommand(this));
        #endregion
    }

    #region Delegates
    /// <summary>
    /// Campo con el delegado de la actualización de los parámetros del robot seleccionado
    /// </summary>
    private readonly Action<string, string> FillInBotParams;
    /// <summary>
    /// Callback para el comando de eliminación del terminal de la lista (botón Delete en la tabla)
    /// </summary>
    public ICommand DeleteCommand { get; }
    #endregion

    /// <summary>
    /// índice del EA seleccionado
    /// </summary>
    private int selectedExpert;
    /// <summary>
    /// Property para el índice del EA seleccionado
    /// </summary>
    public int SelectedExpert
    {
        get { return selectedExpert; }
        set
        {
            selectedExpert = value;
            // Iniciamos el callback de la carga de los parámetros para el EA seleccionado 
            if (Experts.Count > 0)
                FillInBotParams(Experts[selectedExpert], TerminalID);
        }
    }
}

Como puede ver en este fragmento, durante la implementación de la tarea planteada, fue usada otra construcción del lenguaje C#, es decir, la expresión lambda. Si está familiarizado con С++ o C#, este fragmento no le parecerá extraño. A los demás les explicaré que las expresiones lambda pueden considerarse como funciones, pero la diferencia principal de ellas es el hecho de que éstas no tienen una declaración estándar. Estas construcciones se usan ampliamente en C# y Usted puede encontrar más información aquí. El callback es llamado usando ICommand. Otro momento interesante en la implementación de esta clase es la actualización de los parámetros del robót al seleccionar un robot nuevo en la lista desplegable de todos los robots. En primer lugar, el método que actualiza la lista de los parámetros del robot se encuentra en el modelo, mientras que la implementación del envoltorio de esta método para ViewModel —igual como el método para eliminar el terminal— se encuentra en ViewModel. Pues, los delegados vienen en ayuda de nuevo, pero en este caso, en vez de usar ICommand, colocamos la respuesta al evento de la selección de nuevo robot en el setter de la propiedad  SelectedExpert.

El método que actualiza los parámetros de los EAs también tiene su característica específica, a saber, el asincronismo.

private readonly object botParams_locker = new object();
/// <summary>
/// Obtener y rellenar los parámetros del robot
/// </summary>
/// <param name=»fullExpertName»Nombre completo del EA respecto a la carpeta ~/Experts</param>
/// <param name="Terminal">ID del terminal</param>
private async void FillInBotParams(string fullExpertName, string Terminal)
{
    await System.Threading.Tasks.Task.Run(() =>
    {
        lock (botParams_locker)
        {
            model.LoadBotParams(fullExpertName, Terminal, out OptimisationInputData? optimisationData);
            if (!optimisationData.HasValue)
                return;

            IsSaveInModel = false;
            TestLogin = optimisationData.Value.Login;
            IsVisual = optimisationData.Value.IsVisual;
            ForvardDate = optimisationData.Value.ForvardDate;
            CurrencyList.SelectedIndex = optimisationData.Value.CurrencyIndex;
            Deposit.SelectedIndex = optimisationData.Value.DepositIndex;
            ExecutionList.SelectedIndex = optimisationData.Value.ExecutionDelayIndex;
            LaverageList.SelectedIndex = optimisationData.Value.LaverageIndex;
            ModelList.SelectedIndex = optimisationData.Value.ModelIndex;
            OptimisationCriteriaList.SelectedIndex = optimisationData.Value.OptimisationCriteriaIndex;
            IsSaveInModel = true;
        }
    });


    OnPropertyChanged("BotParams");
}

En C# hay un simple modelo de programación asincrónica para la escritura: Async Await, que ha sido aplicado en este caso. El fragmento del código presentado inicia una operación asincrónica y espera la conclusión de su ejecución. Una vez terminada su ejecución, se invoca el evento  OnPropertyChanged que notifica a View sobre el cambio en la tabla con la lista de los parámetros del robot. Para comprender el carácter específico, vamos a considerar el ejemplo de una aplicación asincrónica con el uso de la tecnología Async Await. 

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine($"Main before Method() = {Thread.CurrentThread.ManagedThreadId}");
        Method();
        Console.WriteLine($"Main after Method() = {Thread.CurrentThread.ManagedThreadId}");

        Console.ReadLine();
    }
    private static async void Method()
    {
        Console.WriteLine($"Before Await = {Thread.CurrentThread.ManagedThreadId}");
        await Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"In Avait 1 = {Thread.CurrentThread.ManagedThreadId}"); });
        Console.WriteLine($"After Await 1 = {Thread.CurrentThread.ManagedThreadId}");
      Thread.Sleep(100);

            await Task.Run(() => { Console.WriteLine($"In Avait 2 = {Thread.CurrentThread.ManagedThreadId}"); });
            Console.WriteLine($"After Await 2 = {Thread.CurrentThread.ManagedThreadId}");
        }

    }

El propósito de esta simple aplicación de consola es demostrar el comportamiento de los flujos y hacer una breve digresión en el mundo del asincronismo para los que no trabajaba con él. En el método Main, nosotros primero mostramos en la pantalla ID del flujo en el que el método Main está iniciado, luego iniciamos el método asincrónico, y después de él, mostramos ID del flujo Main de nuevo. En el método asincrónico, volvemos a mostrar ID del flujo en el que este método está iniciado, y luego mostramos uno por uno ID de los flujos asincrónicos y ID del flujo en el que van a ejecutarse las operaciones después del inicio del flujo asincrónico.  Lo más interesante es la visualización de este programa:

Main bofore Method() = 1

Before Await = 1

Main After Method() = 1

In Await 1 = 3

After Await 1 = 3

In Await 2 = 4

After Await 2 = 4

Como se puede ver, ID del flujo Main y ID de la primera salida del método asincrónico Method() son iguales. Eso quiere decir que el método Method() no es asincrónico de todo. Es verdad, el asincronismo de este método comienza sólo después de la llamada a una operación asincrónica a través del método estático Task.Run(). Si el método Method() fuera completamente asíncrónico, el siguiente mensaje, que muestra de nuevo ID del flujo principal, sería llamado después de la visualización de cuatro siguientes mensajes, pero no es el caso. 

Ahora, vamos a considerar las salidas asincrónicas. La primera salida asincrónica devuelve ID = 3, lo que es de esperar. Pero lo más interesante es que la siguiente operación espera la conclusión de la operación asincrónica (gracias al uso de await) y también devuelve ID = 3. Lo mismo sucede con la segunda operación asincrónica. Otro hecho interesante es que, a pesar de un retardo de 100 milisegundos añadido tras la salida de ID del flujo que se usa después de la primera operación asincrónica, el orden no se altera a pesar de que la segunda operación se inicia en el flujo diferente de la primera.

Esas fueron las particularidades del trabajo con el modelo de la programación asincrónica Async Await y con el asincronismo en general. Volviendo a nuestro método, se puede decir que todas las acciones escritas en él se ejecutan en el contexto del flujo secundario y, por tanto, existe la posibilidad de que será llamado dos veces, lo que puede provocar un error. Para eso, se utiliza la construcción lock(locker_object){}. Esta construcción crea algo parecido a una cola de ejecución de las llamadas, como hemos visto en nuestro ejemplo de prueba. Pero a diferencia del ejemplo de prueba, donde la cola se forma independientemente a través de los mecanismos de C#, aquí usamos un recurso compartido que sirve del conmutador. Si se usa en la construcción lock(), cualquier otra llamada a este método se atascará en la fase del bloque del recurso compartido hasta que no sea liberado. De esta manera, evitamos el error de la llamada doble al método.

Otro aspecto digno de ser considerado, es la creación de las fuentes de datos para las configuraciones de los parámetros del optimizador, cuyo código se muestra a continuación:

#region Optimisation and Test settings

/// <summary>
/// Login visible para el robot durante la prueba (es necesario si hay limitaciones por el login)
/// </summary>
private uint? _tertLogin;
public uint? TestLogin
{
    get => _tertLogin;
    set
    {
        _tertLogin = value;

        OnPropertyChanged("TestLogin");
        CB_Action(GetSetActionType.Set_Index);
    }
}
/// <summary>
/// Retardo de ejecución de las órdenes
/// </summary>
public ComboBoxItems<string> ExecutionList { get; }
/// <summary>
/// Tipo de cotizaciones usada (cada  tick OHLC 1M ...)
/// </summary>
public ComboBoxItems<string> ModelList { get; }
/// <summary>
/// Criterio de optimización
/// </summary>
public ComboBoxItems<string> OptimisationCriteriaList { get; }
/// <summary>
/// Depósito
/// </summary>
public ComboBoxItems<int> Deposit { get; }
/// <summary>
/// Moneda del cálculo del beneficio
/// </summary>
public ComboBoxItems<string> CurrencyList { get; }
/// <summary>
/// Apalancamiento
/// </summary>
public ComboBoxItems<string> LaverageList { get; }
/// <summary>
/// Fecha del inicio de la prueba forward
/// </summary>
private DateTime _DTForvard = DateTime.Now;
public DateTime ForvardDate
{
    get => _DTForvard;
    set
    {
        _DTForvard = value;

        OnPropertyChanged("ForvardDate");
        CB_Action(GetSetActionType.Set_Index);
    }
}
/// <summary>
/// Señal del inicio del Simulador en modo gráfico
/// </summary>
private bool _isVisualMode = false;
/// <summary>
/// Señal del inicio del Simulador en modo visual
/// </summary>
public bool IsVisual
{
    get => _isVisualMode;
    set
    {
        _isVisualMode = value;

        OnPropertyChanged("IsVisual");
        CB_Action(GetSetActionType.Set_Index);
    }
}
/// <summary>
/// variable ocultada que almacena el valor de la bandera IsSaveInModel
/// </summary>
private bool isSaveInModel = true;
/// <summary>
/// Recurso compartido para el acceso asincrónico a la propiedad IsSaveInModel
/// </summary>
private readonly object SaveModel_locker = new object();
/// <summary>
/// Bandera; si True - si los parámetros del Simulador se cambian, ellos van a guardarse
/// </summary>
private bool IsSaveInModel
{
    get
    {
        lock (SaveModel_locker)
            return isSaveInModel;
    }
    set
    {
        lock (SaveModel_locker)
            isSaveInModel = value;
    }
}
/// <summary>
/// Callback que guarda los cambios en los parámetros del Simulador
/// </summary>
/// <param name="actionType"></param>
private void CB_Action(GetSetActionType actionType)
{
    if (actionType == GetSetActionType.Set_Index && IsSaveInModel)
    {
        model.UpdateTerminalOptimisationsParams(new OptimisationInputData
        {
            Login = TestLogin,
            IsVisual = IsVisual,
            ForvardDate = ForvardDate,
            CurrencyIndex = CurrencyList.SelectedIndex,
            DepositIndex = Deposit.SelectedIndex,
            ExecutionDelayIndex = ExecutionList.SelectedIndex,
            LaverageIndex = LaverageList.SelectedIndex,
            ModelIndex = ModelList.SelectedIndex,
            OptimisationCriteriaIndex = OptimisationCriteriaList.SelectedIndex,
            Deposit = Deposit.ItemSource[Deposit.SelectedIndex],
            Currency = CurrencyList.ItemSource[CurrencyList.SelectedIndex],
            Laverage = LaverageList.ItemSource[LaverageList.SelectedIndex]
        });
    }
}
#endregion

Otro momento importante de estas configuraciones es la implementación del guardado de los parámetros del optimizador. Adelantándose, cabe mencionar que dentro de este modelo, se guarda su propia instancia de las configuraciones del Simulador de Estrategias para cada robot. Eso permite configurar el Simulador de cada terminal seleccionado de una manera individual. Precisamente para eso ha sido creado el método CB_Action que es llamado en cada setter, y eso provee un guardado instantáneo de los resultados en el modelo después de la introducción de los cambios en alguno de los parámetros. Además, merece la pena considerar la clase  ComboBoxItems<T> que he creado especialmente para almacenar los datos para las listas desplegables. En realidad, representa un contexto de datos para ComboBox con el que está conectado. Esta clase tiene las siguiente implementación simple.

/// <summary>
/// Clase - envoltorio para los datos de las listas ComboBox
/// </summary>
/// <typeparam name=»T»>Tipo de datos almacenados en ComboBox</typeparam>
class ComboBoxItems<T> : INotifyPropertyChanged
{
    /// <summary>
    /// Colección de los elementos de la lista
    /// </summary>
    private List<T> items;
    public List<T> ItemSource
    {
        get
        {
            OnAction(GetSetActionType.Get_Value);
            return items;
        }
        set
        {
            items = value;
            OnAction(GetSetActionType.Set_Value);
        }
    }
    /// <summary>
    /// Indice seleccionado en la lista
    /// </summary>
    int selectedIndex = 0;
    public int SelectedIndex
    {
        get
        {
            OnAction(GetSetActionType.Get_Index);
            return selectedIndex;
        }
        set
        {
            selectedIndex = value;
            OnAction(GetSetActionType.Set_Index);
        }
    }

    public event Action<GetSetActionType> Action;
    public event PropertyChangedEventHandler PropertyChanged;

    private void OnAction(GetSetActionType type)
    {
        switch (type)
        {
            case GetSetActionType.Set_Value:
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ItemSource"));
                break;
            case GetSetActionType.Set_Index:
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("SelectedIndex"));
                break;
        }
        Action?.Invoke(type);
    }
}
enum GetSetActionType
{
    Get_Value,
    Set_Value,
    Get_Index,
    Set_Index
}

No obstante, su particularidad es el evento que se invoca cada vez cuando se edita uno de sus eventos, o bien, cuando se recibe la información sobre su evento. Otra particularidad suya es la notificación automática a View sobre la alteración de alguna de sus propiedades. De esta manera, es capaz de notificar tanto a ViewModel, como a View sobre los cambios. Gracias a esta propiedad, en ViewModel actualizamos la información en el modelo sobre las propiedades alteradas de las configuraciones del optimizador y llamamos al guardado automático. Además, obtenemos la posibilidad de hacer el código más legible, porque añadimos a ViewModel dos propiedades de cada ComboBox (índice del elemento seleccionado y lista de todos los elementos). Sin usar esta clase, la nuestra ExtentionGUI_VM se extendería aún más.  

En conclusión, vamos a considerar el método de la instanciación del modelo de nuestra extensión y el método del inicio de la interfaz gráfica en el terminal MetaTrader 5. La clase del modelo de datos tiene que ser independiente de ViewModel, igual como el propio ViewModel tiene que ser independiente de View. Por esa razón, y para la posibilidad del testeo, vamos a implementar el modelo vía la interfaz IExtentionGUI_M. La estructura de esta interfaz, así como su implementación, será considerada junto con la descripción del propio modelo de datos. Por ahora, es suficiente saber que la clase ExtentionGUI_VM no sabe de la implementación del modelo de datos, en vez de eso, trabaja con la interfaz IExtentionGUI_M, y la instanciación de la clase del modelo se hace de la siguiente manera:

private readonly IExtentionGUI_M model = ModelCreator.Model;

Este proceso de la instanciación utiliza una fábrica estática. La clase ModelCreator es una fábrica y está implementada así:

/// <summary>
/// Fábrica para sustituir el modelo en la interfaz gráfica
/// </summary>
class ModelCreator
{
    /// <summary>
    /// Modelo
    /// </summary>
    private static IExtentionGUI_M testModel;
    /// <summary>
    /// Property devuelve el modelo (si no ha sido sustituido) o el modelo sustituido (para las pruebas)
    /// </summary>
    internal static IExtentionGUI_M Model => testModel ?? new ExtentionGUI_M(new MainTerminalCreator(),
                                                                             new MainConfigCreator(),
                                                                             new MainReportReaderCreator(),
                                                                             new MainSetFileManagerCreator(),
                                                                             new OptimisationExtentionWorkingDirectory("OptimisationManagerExtention"),
                                                                             new MainOptimisatorSettingsManagerCreator(),
                                                                             new TerminalDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MetaQuotes", "Terminal")));

    /// <summary>
    /// El método de la sustitución del modelo sustituye el modelo de prueba para poder tester la gráfica separadamente de la lógica
    /// </summary>
    /// <param name=»model»>modelo de prueba - se sustituye desde fuera</param>
    [System.Diagnostics.Conditional("DEBUG")]
    public static void SetModel(IExtentionGUI_M model)
    {
        testModel = model;
    }
}

Esta clase tiene el campo private tipificado por la interfaz del modelo de datos. Inicialmente este campo es null, de lo que vamos a aprovechar a la hora de escribir la propiedad estática que recibe el modelo solicitado. Como podemos ver en el código, estamos verificando los siguiente: si testModel es igual a null, instanciamos y devolvemos la implementación del modelo que contienen la lógica de negocio; si testModel  no es igual a null (entonces, hemos sustituido el modelo), devolvemos el modelo sustituido, es decir, el que se almacena en testModel. Para sustituir el modelo, se usa el método estático SetModel. Este método está decorado por el atributo  [System.Diagnostics.Conditional( "DEBUG")], lo que prohíbe su uso en la versión Release de la versión de este programa.

El proceso del inicio de la interfaz gráfica parece al proceso del inicio de la gráfica desde dll descrito en el artículo mencionado anteriormente. La clase public MQLConnector fue descrito especialmente para el enlace con MetaTrader. 

/// <summary>
/// Clase para la conexión de la interfaz gráfica con MetaTrader
/// </summary>
public class MQL5Connector
{
    /// <summary>
    /// El campo que almacena el puntero a la interfaz gráfica iniciada
    /// </summary>
    private static View.ExtentionGUI instance;
    /// <summary>
    /// El método que inicia la interfaz. 
    /// El inicio se realiza sólo para una interfaz desde un robot. 
Es decir, durante el inicio, se comprueba si la interfaz ha sido iniciada antes.
    /// Si es así, el inicio de la nueva se rechaza
    /// </summary>
    /// <param name=»pathToTerminal»>Ruta hacia la carpeta cambiante</param>
    public static void Instance(string terminalID)
    {
        // se verifica si la interfaz ha sido iniciada antes
        if (instance == null)
        {
            // Variable del flujo secundario - flujo de la interfaz gráfica (la gráfica se inicia en el segundo flujo)
            // y su instanciación con el traspaso de la expresión lambda que describe el orden del inicio de la gráfica
            Thread t = new Thread(() =>
            {
                // La instanciación de la clase de la interfaz gráfica y su visualización (inicio de la gráfica)
                instance = new View.ExtentionGUI();
                instance.Show();
                // Suscripción al evento del cierre de la ventana de la gráfica - si la ventana está cerrada, 
al campo que almacenaba la referencia a la interfaz gráfica se le asigna el valor null
                instance.Closed += (object o, EventArgs e) => { instance = null; };

                // Inicio del Dispatcher del flujo de la interfaz gráfica
                Dispatcher.Run();
            });
            MainTerminalID = terminalID;		

            // Inicio del segundo flujo
            t.SetApartmentState(System.Threading.ApartmentState.STA);
            t.Start();
        }
    }     
    /// <summary>
    /// Obtiene la información sobre si la ventana está abierta o no
    /// </summary>
    /// <returns>true si está activa, y false si está cerrada</returns>
    public static bool IsWindowActive() => instance != null;
    /// <summary>
    /// Main Terminal ID
    /// </summary>
    internal static string MainTerminalID { get; private set; }
    internal static Dispatcher CurrentDispatcher => ((instance == null) ? Dispatcher.CurrentDispatcher : instance.Dispatcher);
}

Esta clase tiene que ser marcada con el modificador de acceso «public». Eso es necesario para que esté disponible desde el robot en MetaTrader. Además, los métodos a usar en el terminal tienen que ser estáticos y tener el modificador de acceso «public», ya que el terminal permite usar solamente los métodos estáticos. Esta clase también tiene 2 propiedades con el modificador de acceso «Internal». Este modificador de acceso los oculta del terminal, porque ellas sirven para el uso sólo dentro de la dll creada. Como puede ver en la implementación, se supone que nuestra ventana va a almacenarse en un campo estático privado. Eso es necesario para poder acceder a ella desde otras propiedades y métodos. Esta solución también supone que sólo una instancia de esta aplicación podría ser creada en un robot en un terminal.

El método Instance instancia la gráfica y abre la ventana, vamos a analizarlo más detalladamente. Primero, se verifica si esta ventana ha sido instanciada antes; si es así, este método ignora el intento de la instanciación de la ventana. Luego, se crea el flujo secundario para iniciar la gráfica. La separación de los flujos de la gráfica y del programa que la inicia es necesario para que no haya bloqueo en el trabajo del terminal y la interfaz gráfica. Después de escribir la carga de la ventana, realizamos la suscripción al evento del cierre de la ventana en el que le asignamos el valor null; es necesario para un trabajo correcto del esquema de la carga de la ventana. Luego, necesitamos iniciar el dispatcher, de lo contrario, el dispatcher no será lanzado para el flujo en el que llamamos a nuestra gráfica.

La clase Dispatcher fue creado para solucionar los conflictos de flujos múltiples en las aplicaciones WPF. La cosa es que todos los elementos de la ventana gráfica pertenecen al flujo de la ventana gráfica. Cuando intentamos alterar el valor de algunos elementos de la gráfica desde otro flujo, obtenemos el error cross thread exception. La clase Dispatcher inicia la operación que se le pasa a través del delegado en el contexto del flujo de la interfaz gráfica, y así evita el error mencionado. Después de completar la descripción de la expresión lambda para el inicio de la gráfica, tenemos que configurar el flujo como Single Threaded Apartment e iniciarlo, iniciando así la gráfica. Pero antes de eso, guardamos el valor de ID del terminal actual que ha sido pasado.

Ahora, hay que aclarar, ¿para qué necesitamos todo eso? La respuesta será bastante trivial; eso permite depurar la gráfica separadamente de la lógica. Hemos escrito una interfaz gráfica. Sin embargo, para depurarla, necesitamos una clase que representa el modelo. El modelo tiene una serie de sus detalles de implementación, y por eso, es necesario depurarlo separadamente de la gráfica. Ahora, como ya tenemos de un método de la sustitución de los modelos de datos de prueba, somos capaces de implementar una clase del modelo de datos de prueba y sustituirlo en ViewModel a través de la fábrica estática. Como resultado, obtenemos la posibilidad de depurar la gráfica usando los datos de prueba, iniciar la interfaz gráfica y comprobar la reacción de los callback, el diseño y los demás detalles. Lo hice de la siguiente manera. Primero, necesitamos crear una aplicación de consola en Solution actual, para poder iniciar la gráfica directamente desde VisualStudio; eso nos dará el acceso a las herramientas de la depuración.


La llamaremos "Test" y añadimos dentro una referencia a nuestra dll, la cual escribimos para MetaTrader. Como resultado, obtenemos una aplicación de consola que puede usar las clase public de nuestra dll. Sin embargo, hay sólo una clase public en nuestra dll, es decir, la clase MQL5Connector. Pero a parte de ella, necesitamos crear un modelo de datos falso y sustituirlo en ViewModel, como ha sido descrito antes. Para eso, necesitamos acceder a las clases disponibles sólo en dll, también existe una solución. Para eso, es necesario añadir el siguiente atributo en cualquier lugar de nuestra dll:

[assembly: InternalsVisibleTo("Test")]

Hace que todas las clases internas de nuestra dll estén disponibles en build Test (es decir, en nuestra aplicación de consola de prueba). Al final, podemos crear un modelo falso y usarlo para iniciar nuestra aplicación. Como resultado, nuestra aplicación debe tener la siguiente implementación:

 class Program
 {
    static void Main(string[] args)
    {
        ModelCreator.SetModel(new MyTestModel());

        MQL5Connector.Instance("ID основного терминала");
    }
}

class MyTestModel : IExtentionGUI_M
{
    // Implementación de la interfaz IExtentionGUI_M
}

Ahora, podemos iniciar la gráfica separadamente de la lógica, depurarla y simplemente observarla visualmente.

Conclusión y archivos adjuntos

Hemos analizado los momentos más importantes e interesantes en la creación de la capa gráfica de la aplicación y su clase de conexión (ViewModel).  En esta fase, ya tenemos una gráfica impelementada que se puede abrir y pulsar, así como, tenemos creada una clase de conexión que describe las fuentes de datos para la capa gráfica y su comportamiento (reacción a la pulsación de los botones, etc.). A continuación, discutiremos una clase del modelo y sus componentes, donde será descrita la lógica de la extensión y los métodos de la interacción con los archivos, terminal y directorios del ordenador.