Gerenciando otimizações (Parte I): Criando uma interface gráfica do usuário

Andrey Azatskiy | 27 agosto, 2019

Sumário

Introdução

A questão da inicialização alternativa do terminal MetaTrader já foi levantada no artigo de Vladímir Karputov, bem como no site do MetaTrader (existe uma pagina descrevendo o procedimento e uma maneira alternativa de iniciar o terminal). Ambas as fontes mencionadas formam a base deste artigo, no entanto, nenhuma delas descreve como criar uma interface gráfica conveniente para trabalhar com vários terminais simultaneamente: meu artigo é concebido para tratar disso.

O resultado deste trabalho foi uma extensão - para o terminal - que permite iniciar o processo de otimização de EAs em vários terminais instalados num só computador. Mais artigos vão desenvolver este complemento, adicionando novas funcionalidades a ele.

No vídeo, pode-se ver o trabalho da versão final desse complemento. O artigo atual descreve apenas o processo de criação de uma interface gráfica, já a lógica do complemento apresentado será descrita na próxima parte.




Como iniciar o MetaTrader e os arquivos de configuração

Antes de considerar detalhadamente o complemento a ser criado, vale a pena uma breve análise dos princípios básicos de inicialização do terminal (e de quaisquer outros aplicativos) por meio do console. Esta maneira de trabalhar com aplicativos pode parecer algo arcaico, mas esse não é o caso, por exemplo, quanto a sistemas operacionais baseados em Linux, uma inicialização similar de aplicativos, bem como aplicativos sem uma interface gráfica são comuns e amplamente usados.

Consideremos iniciar um terminal com um programa simples escrito em C++:

#include <iostream>

using namespace std;

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

    return 0;
}

Compilado este programa, obteremos um arquivo executável (.exe) que ao ser iniciado no console mostrará a conhecida mensagem "Hello World". Vale a pena prestar atenção ao fato de que a função de inicialização main não possui parâmetros de entrada, no entanto este é apenas um caso especial. Se mudarmos este programa usando outra sobrecarga da função main, obteremos um programa de console que usa uma série 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;
}

O primeiro parâmetro argc indica o comprimento do array de arrays do segundo parâmetro.

O segundo parâmetro é uma lista de strings passadas para o programa na inicialização. Já é possível chamar este programa do console da seguinte maneira:

./program "My name" "is Andrey"

Onde ./program é uma indicação do nome deste programa, enquanto as strings restantes são seu conjunto de parâmetro separados por espaços em branco. Estes parâmetros são registrados no array passado e o resultado de sua operação é o seguinte:

Hello World
./program
My name
is Andrey

Como podemos ver, a primeira mensagem é deixada pela versão antiga do programa, mas o resto das strings foram passadas como parâmetros para o array de strings argv (vale a pena observar que o primeiro parâmetro é sempre o nome do programa a ser executado). Não vamos analisar este exemplo em detalhe, pois é apenas uma ilustração para entender como funciona a inicialização do aplicativo MetaTrader através do console.

Antes de cada parâmetro, são indicados sinalizadores que permitem ao programa entender para qual parâmetro é definido o valor passado. Para trabalhar com sinalizadores, existe uma série de funções em linguagem C/C++, mas não nos debruçamos sobre elas. A principal coisa a entender é que nosso aplicativo (com extensão .exe) é simples e pode ser iniciado através do console com os parâmetros passados que podem mudar suas propriedades.  

Segundo as instruções no site, para iniciar o MetaTrader através do console, há vários sinalizadores e valores que podem ser passados:

Também são possíveis combinações de sinalizadores, por exemplo, a inicialização do terminal com a seguinte combinação irá iniciá-lo com o arquivo de configuração especificado no modo portátil:

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

Apesar do fato de que as diferenças entre o programa de exemplo (Hello World) e o terminal real são impressionantes, a forma como eles são iniciados através do console é idêntica, o que usaremos no processo de escrita do complemento em questão.

Deve-se prestar atenção especial ao arquivo de configuração, cujo caminho é especificado usando a chave/config, porque é graças a ele que o terminal entende com qual login/senha iniciar, em que modo vale a pena iniciar o testador e se vale a pena iniciar o testador. Nós não vamos duplicar novamente as instruções de trabalho com arquivos de configuração, em vez disso, gostaria de considerar com mais detalhes sua estrutura. Cada arquivo de configuração consiste em várias seções, indicadas entre colchetes.

[Tester]

Depois desta seção, há uma lista de valores-chave que fornece uma descrição dos campos que caracterizam os parâmetros de inicialização do programa. Além disso, os arquivos de configuração podem conter comentários que começam com os caracteres ";" ou "#". Vale a pena dizer que agora os arquivos de configuração de formato *.ini são substituídos por arquivos usando o esquema XAML ou o arquivos json, já que eles permitem armazenar muitas mais informações num arquivo, entretanto, em muitos programas desenvolvidos, incluindo MetaTrader, ainda são usados arquivos com esse formato. O WinApi suporta várias funções para trabalhar com arquivos de configuração que usamos ao escrever a classe wrapper para trabalhar convenientemente com o formato de arquivo necessário. Funções e wrapper para trabalhar com arquivos de configuração do MetaTrader serão descritos em mais detalhes numa das seções a seguir deste artigo. 

Recursos do complemento criado e tecnologias utilizadas

Para começar, vale a pena decidir que, para trabalhar com o projeto, será necessário instalar o IDE (Integrated Development Environment) do Visual Studio. Este projeto foi criado usando a versão Community do 2019. Além disso, durante a instalação do estúdio, é preciso instalar o .Net 4.6.1, pois este complemento é escrito com ajuda dele. Também vale a pena dizer que, para que os leitores que não são bem versados em C# possam entender rapidamente a essência da questão, tentarei descrever em detalhes alguns pontos específicos desta linguagem e as técnicas que usei durante a programação.

Como é mais conveniente criar a interface gráfica usando a linguagem C# e o terminal MetaTrader suporta uma maneira amigável de encaixar com essa linguagem, vale a pena usar essa possibilidade. Além disso, há pouco, no fórum apareceram vários artigos sobre a criação de interfaces gráficas usando C#. Esses artigos apresentam uma boa maneira de criar interfaces gráficas baseadas na tecnologia Win Forms e uma biblioteca dll de encaixe que inicia gráficos através de mecanismos de reflexão. A solução usada pelo autor destes artigos é muito boa, mas para o atual eu decidi usar uma versão mais moderna de escrever interfaces gráficas, nomeadamente através da tecnologia WPF. Como resultado, conseguimos, sem uma biblioteca de encaixe, amoldar tudo numa única biblioteca dll. Para resolver nossa tarefa, precisamos criar um tipo de projeto que nos permita armazenar os objetos gráficos descritos usando a tecnologia WPF e compilar numa biblioteca dinâmica (arquivo *.dll) que pode ser carregada posteriormente no terminal. Este tipo de projeto existe, ele é WpfCustomControlLibrary. Esse tipo de projeto foi projetado especificamente para criar objetos gráficos personalizados, por exemplo, para criar uma biblioteca que desenha gráficos. Vamos usá-lo para nossos próprios propósitos, particularmente para criar nossa extensão para o terminal MetaTrader. Para criar este projeto, precisamos selecioná-lo na lista de projetos no IDEVisual Studio, como mostra a imagem:

Vamos nomear o projeto a ser criado "OptimisationManagerExtention". Inicialmente, no projeto é criada a pasta com temas Themes, com o arquivo anexado "Generic.xaml" (este é um arquivo proposto para armazenar estilos especificando cores, tamanhos iniciais, recuos ao longo das bordas e propriedades semelhantes de objetos gráficos). Ainda precisaremos deste arquivo em mais detalhes, por isso, vamos deixá-lo por enquanto. Outro arquivo gerado automaticamente é o que contém a classe CustomControl1 (ele não será necessário para nossos propósitos, portanto, vamos exclui-lo). Como vários outros artigos serão escritos com base neste artigo, vale a pena inicialmente cuidar da extensibilidade do complemento criado, o que significa que se deve recorrer ao modelo de programação MVVM. Para aqueles que não estão familiarizados com ele, deixarei um link para um artigo que é bom o suficiente, nele, são descritos em detalhes a minha opinião e a ideia por trás deste padrão de programação. Para tornar o código mais estruturado, criamos a pasta "View", para colocarmos nossa janela gráfica. Para criar a janela gráfica, precisamos adicionar o elemento Window (WPF) à pasta criada, como na imagem abaixo:


Vamos chamar a janela criada ExtentionGUI.xaml (será o mesmo elemento gráfico que foi capturado no vídeo acima). Agora vale a pena falar sobre namespaces. Criado o projeto OptimizationManagerExtention, o estúdio gera automaticamente o namespace principal — "OptimisationManagerExtention". Em C#, como na maioria das outras linguagens de programação, os namespaces servem como contêineres nos quais são empacotados nossos objetos. O seguinte exemplo mostra mais facilmente as propriedades do namespace: 

Este tipo de constructo é errôneo, já que ambas as classes são declaradas no mesmo namespace:

namespace MyNamespace
{
    class Class_A
    {
    }

    class Class_A
    {
    }
}

Tal partição de classes é permissível, já que ambas as classes, apesar do mesmo nome, estão num namespace diferente:

namespace MyFirstNamespace
{
    class Class_A
    {
    }
}

namespace MySecondNamespace
{
    class Class_A
    {
    }
}

 Há também os chamados namespaces aninhados, que dentro de um namespace contêm vários outros namespaces, assim, como resultado, o código descrito é válido novamente:

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
        {
        }
    }
}

Mas como essa forma de registro é inconveniente, em C# há um registro abreviado, mais fácil de entender:

namespace MyNamespace
{
    class Class_A
    {
    }
}

namespace MyNamespace.First
{
    class Class_A
    {
    }
}

namespace MyNamespace.Second
{
    class Class_A
    {
    }
}

O código apresentado nos dois exemplos anteriores é idêntico, mas a última variante de código é mais conveniente. Voltando da teoria para nosso complemento, vale a pena notar que ao criar a pasta View, criamos um namespace aninhado, e agora os objetos colocados nessa pasta serão colocados no namespace "OptimisationManagerExtention.View". Assim, nossa janela também possui este namespace. Para que os estilos que descreveremos no arquivo Generic.xaml sejam aplicados com êxito à janela inteira, precisamos editar o esquema XAML deste arquivo. A primeira coisa a fazer é remover o bloco de código que começa com a tag <Style>, uma vez que ele não é necessário. A segunda coisa é adicionar um link para o namespace da nossa janela, o que é feito através da propriedade "xmlns: local". Como resultado, obtemos o seguinte conteúdo:

<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 definir o tamanho/cor ou outra coisa, os objetos da nossa janela precisam descrever seu estilo. Não prestarei atenção especial à aparência do aplicativo, em vez disso, descreverei apenas o mínimo necessário. Você pode adicionar qualquer design, animação e afins à janela. Após todas as edições, consegui o seguinte arquivo descrevendo os estilos, além disso, todos os estilos são aplicados automaticamente a todos os elementos da janela. Conveniente, não é?

<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">
    
    <!--Definimos a cor da tela de fundo-->
    <Style TargetType="{x:Type local:ExtentionGUI}">
        <Setter Property="Background" Value="WhiteSmoke"/>
    </Style>

    <!--
    Definimos a cor da tela de fundo da faixa divisória que ao ser arrastada 
    altera os intervalos de zonas divididas horizontalmente na primeira guia 
    de nossa janela
    -->
    <Style TargetType="GridSplitter">
        <Setter Property="Background" Value="Black"/>
    </Style>

    <!--Definimos a altura das listas suspensas-->
    <Style TargetType="ComboBox">
        <Setter Property="Height" Value="22"/>
    </Style>

    <!--Definimos a altura dos calendários-->
    <Style TargetType="DatePicker">
        <Setter Property="Height" Value="22"/>
    </Style>

    <!--Definimos a altura do texto dos blocos-->
    <Style TargetType="TextBox">
        <Setter Property="Height" Value="22"/>
    </Style>

    <!--Definimos a altura dos botões-->
    <Style TargetType="Button">
        <Setter Property="Height" Value="22"/>
    </Style>

</ResourceDictionary>

Para que os estilos se apliquem à janela, no esquema XAML da nossa janela, deve-se descrever um link para eles. Para fazer isso, imediatamente após a tag <Window> aberta é indicado o constructo a seguir, constructo esse em que é especificado o caminho para o arquivo com recursos relativos ao local da janela. 

<!--Inserimos os estilo-->
<Window.Resources>
    <ResourceDictionary Source="../Themes/Generic.xaml"/>
</Window.Resources>

Além do diretório View criado, criaremos outros:

Como você provavelmente já adivinhou, a camada responsável pelos gráficos do aplicativo é descrita exclusivamente no esquema XAML, sem usar C# diretamente. Após gerados os diretórios correspondentes, criamos mais dois namespaces aninhados, que, para poder usá-los, devem ser adicionados ao esquema XAML de nossa janela. Também criamos a classe "ExtentionGUI_VM" no namespace OptimisationManagerExtention.ViewModel. Essa classe se tornará nosso objeto de encaixe, mas para executar as funções necessárias, ela deve ser herdada da interface "INotifyPropertyChanged", uma vez que ela contém o evento PropertyChanged, através do qual a parte gráfica é notificada sobre a alteração nos valores dos campos e, consequentemente, sobre a necessidade de atualizar os gráficos. A classe criada fica assim:

/// <summary>
/// View Model
/// </summary>
class ExtentionGUI_VM : INotifyPropertyChanged
{
    /// <summary>
    /// Evento de alteração de propriedade do ViewModel 
    /// e seus manipuladores
    /// </summary>
    #region PropertyChanged Event
    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// Manipulador de eventos PropertyChanged
    /// </summary>
    /// <param name="propertyName">Nome da variável atualizável</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
}

O esquema XAML após a janela criada e a adição de todos os links fica assim:

<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">

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

    <Grid>
        

    </Grid>
</Window>

Como os principais preparativos para escrever os gráficos da nossa aplicação estão acabados, podemos começar a preencher o esquema XAML da nossa janela para criar uma camada gráfica. Todos os controladores serão registrados dentro do bloco <Grid/>. Para aqueles que não conhecem muito o esquema XAML, recomendo abri-lo imediatamente no estúdio e verificar a leitura para que seja mais conveniente seguir o esquema durante o processo de leitura. Acho que para aqueles que estão familiarizados com essa ferramenta devem ser suficientes os pedaços de código que eu inserirei aqui no artigo. Fazendo uma analogia entre os dois métodos de criação de interfaces gráficas (WinForms/WPF), podemos dizer que, além das diferenças óbvias, eles também têm semelhanças. Lembremos as interfaces do WinForms, nelas todos os elementos gráficos são representados como instâncias de classes e armazenados na parte oculta de uma classe abstrata (classe Button ou ComboBox).

Assim, verifica-se que todo o aplicativo gráfico do WinForms consiste num conjunto de instâncias de objetos interconectados. Olhando para o esquema WPF, é difícil imaginar que seja baseado no mesmo princípio, mas é. Cada elemento do esquema, por exemplo, a tag "Grid" que já é familiar para nós, é na verdade uma classe e, se desejado, pode-se recriar exatamente o mesmo aplicativo, mas sem usar o esquema XAML, isto é, usando apenas classes do namespace correspondente, porém, isto ficará feio e excessivamente volumoso. Essencialmente, ao abrir a tag <Grid>, nós indicamos que queremos criar uma instância desta classe, assim, os próprios mecanismos de compilação analisam o esquema que especificamos e criam instâncias dos objetos requeridos. Esta propriedade dos aplicativos WPF nos permite criar nossos próprios objetos gráficos ou objetos que estendem a funcionalidade padrão. Além disso, no artigo, vamos considerar como é implementado isso.    

Voltando ao processo de criação de gráficos, vale notar que o bloco <Grid/> é projetado para colocação conveniente de controladores e outros blocos de construção. Como se pode ver no vídeo, ao alterar guias, entre as abas Settings e Optimisation Result, a parte inferior (ProgressBar) permanece inalterada, isso é conseguido dividindo o bloco principal <Grid/> em duas linhas contendo o Painel com as guias principais (TabControll) e outro bloco <Grid/> que contém String de status (Lable), ProgressBar e o botão de inicialização do processo de otimização. Dado contêiner aninhado também é dividido, mas apenas horizontalmente e em 3 partes (colunas), cada uma das colunas alocadas contém um dos controles ( Lable, ProgressBar, Button)

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

    <!--Criamos TabControl com duas guias-->
    <TabControl>
        <!--Guia de configurações do robô e de inicialização da otimização ou do teste único-->
        <TabItem Header="Settings">
           
        </TabItem>

        <!--Guia de visualização do resultado de otimização e da inicialização do teste de acordo como o evento de clique duplo-->
        <TabItem Header="Optimisation Result">
          
        </TabItem>
    </TabControl>

    <!--Contêiner com faixa de carregamento, com status de execução de operação e com botão de inicialização-->
    <Grid Grid.Row="1">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150"/>
            <ColumnDefinition/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <!--Status da operação em andamento-->
        <Label Content="{Binding Status, UpdateSourceTrigger=PropertyChanged}"/>
        <!--Faixa de carregamento-->
        <ProgressBar Grid.Column="1" 
                                     Minimum="0" 
                                     Maximum="100"
                                     Value="{Binding PB_Value, UpdateSourceTrigger=PropertyChanged}"/>
        <!--Botão de inicialização-->
        <Button Margin="5,0,5,0" 
                                Grid.Column="2"
                                Content="Start"
                                Command="{Binding Start}"/>
    </Grid>
</Grid>

Por enquanto, como ainda não avançamos em profundidade no nosso texto, vale a pena considerar as propriedades usadas juntamente com este controle, nomeadamente com a maneira de transferir informações do ViewModel para View. Como mostrado mais adiante, para cada um dos campos que exibem ou servem para inserir algumas informações, na classe ExtentionGUI_VM (nosso objeto ViewMpodel) será criado um campo determinado que armazenará seu valor. Ao criar aplicativos WPF e, mais ainda, ao usar o padrão MVVM, não é habitual segundo o nome do código acessar diretamente os elementos gráficos, portanto, usamos um processo mais conveniente de passar valores, o que também requer um mínimo de código. Por exemplo, a propriedade Value para o elemento gráfico ProgressBar é definido usando a tecnologia de vinculação de dados, pela qual é responsável o seguinte registro:

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

Após a propriedade Binding, é especificado o nome do campo que armazena as informações e é nomeado na classe ViewModel, enquanto a propriedade UpdateSourceTrigger indica a maneira de atualizar informações num elemento gráfico. Ao definir esta propriedade com o parâmetro PropertyChanged, informamos ao aplicativo que esta propriedade específica deste elemento específico precisa ser atualizada apenas se for acionado o evento PropertyChanged na classe ExtentionGUI_VM e se o nome da variável com a qual foi vinculada (PB_Value) for transmitido como um dos parâmetros deste evento. Como se pode ver no esquema XAML, os botões também possuem uma vinculação de dados, porém, no botão a ligação é realizada com a propriedade Command, que indica através da interface ICommand um comando (ou melhor, um método definido na classe ViewModel) que é chamado quando o botão é clicado. Desta forma, são vinculados eventos para clicar em botões, bem como outros eventos (por exemplo, clicar duas vezes numa tabela com resultados de otimização). Nesta etapa, nossa parte gráfica já adquiriu os principais recursos:


A próxima etapa na criação da interface gráfica aborda o preenchimento das guias OptimisationResults com controles. Esta guia contém duas listas suspensas (Combobox) para selecionar o terminal no qual é realizada a otimização e o EA, respectivamente, bem como o botão Update Report. Essa guia também contém um controle aninhado Tabcontrol com duas sub-guias, cada uma com uma tabela (Listview) que reflete os resultados das otimizações. No esquema XAML, fica assim:

  <!--Guia de visualização do resultado de otimização e da inicialização do teste de acordo como o evento de clique duplo-->
            <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>
                    <!--Contêiner com as tabelas do resultado da otimização-->
                    <TabControl 
                        TabStripPlacement="Bottom"
                        Grid.Row="1">
                        <!--Guia em que são exibidos os resultados da otimização histórica-->
                        <TabItem Header="Backtest">
                            <!--Tabela com os resultados de otimização-->
                            <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>
                        <!--Guia em que são exibidos os resultados da otimização para frente 
                    de execuções-->
                        <TabItem Header="Forvard">
                            <!--Tabela com os resultados de otimização-->
                            <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 mencionado anteriormente, cada tag usada no esquema XAML é uma classe. Também podemos escrever nossas próprias classes para estender as funcionalidades da marcação padrão ou para criar elementos gráficos personalizados. No estágio atual, precisamos apenas expandir as funcionalidades do esquema atual. Nossas tabelas, que, como pode ser visto no vídeo, refletem os resultados das otimizações, devem ter nomes diferentes e um número diferente de colunas: isso é a primeira extensão que vamos criar.

A segunda extensão é a conversão de eventos de duplo clique para a interface ICommand. A segunda extensão não poderia ter sido criada se não tivéssemos usado o modelo de desenvolvimento MVVM, de acordo com o qual o ViewModel, e especialmente o Model, não deveria ser vinculado à camada View de nenhuma maneira, pois isso é feito para que seja possível alterar facilmente a camada gráfica do aplicativo ou até mesmo reescrevê-la a partir do zero. Como se pode ver nos métodos para chamar estas extensões, elas estão todas no namespace ViewExtention aninhado, em seguida, dois pontos seguidos pelo nome da classe na qual as extensões estão localizadas e depois do operador dot, isto é, o nome da propriedade na qual definimos o valor.

Para uma compreensão mais detalhada do que são essas extensões, analisamos cada uma delas e abordamos a expansão do evento que converte os cliques na interface ICommand. Para criar uma extensão que processa um evento de clique duplo, na pasta ViewExtention criamos uma classe parcial ListViewExtention. O modificador de acesso partial indica que a implementação dessa classe pode ser dividida entre vários arquivos, e todos os métodos/campos e outros componentes da classe, marcados como partial, mas divididos entre dois ou mais arquivos, pertencerão à mesma classe.

using System.Windows;

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

namespace OptimisationManagerExtention.ViewExtention
{
    /// <summary>
    /// Classe de extensão para ListView que converte eventos em comandos (ICommand)
    /// a classe está marcada com a palavra-chave partial, sua implementação é dividida em vários arquivos.
    /// 
    /// Especificamente nesta classe é implementada a conversão do evento ListView.DoubleClickEvent 
    /// no comando do tipo ICommand
    /// </summary>
    partial class ListViewExtention
    {
        #region Command
        /// <summary>
        /// Propriedade dependente contendo uma referência ao retorno de chamada do comando
        /// A propriedade é definida via View no esquema do projeto XAML
        /// </summary>
        public static readonly DependencyProperty DoubleClickCommandProperty =
            DependencyProperty.RegisterAttached("DoubleClickCommand",
                typeof(ICommand), typeof(ListViewExtention),
                new PropertyMetadata(DoubleClickCommandPropertyCallback));

        /// <summary>
        /// Setter para DoubleClickCommandProperty
        /// </summary>
        /// <param name="obj">Elemento de gerenciamento</param>
        /// <param name="value">Valor com o qual é realizado o vínculo</param>
        public static void SetDoubleClickCommand(UIElement obj, ICommand value)
        {
            obj.SetValue(DoubleClickCommandProperty, value);
        }
        /// <summary>
        /// Geter para DoubleClickCommandProperty
        /// </summary>
        /// <param name="obj">Elemento de gerenciamento</param>
        /// <returns>referência para um comando salvo do tipo ICommand</returns>
        public static ICommand GetDoubleClickCommand(UIElement obj)
        {
            return (ICommand)obj.GetValue(DoubleClickCommandProperty);
        }
        /// <summary>
        /// Chamada de retorno depois de configurar a propriedade DoubleClickCommandProperty
        /// </summary>
        /// <param name="obj"> Controle para o qual é definida a propriedade</param>
        /// <param name="args">eventos antes de invocar a chamada de retorno</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>
        /// Chamada de retorno de um evento que se converte no 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>
        /// Propriedade dependente contendo uma referência ao retorno de chamada do comando do tipo ICommand
        /// A propriedade é definida via View no esquema do projeto XAML
        /// </summary>
        public static readonly DependencyProperty DoubleClickCommandParameterProperty =
            DependencyProperty.RegisterAttached("DoubleClickCommandParameter",
                typeof(object), typeof(ListViewExtention));
        /// <summary>
        /// Setter para DoubleClickCommandParameterProperty
        /// </summary>
        /// <param name="obj">Elemento de gerenciamento</param>
        /// <param name="value">Valor com o qual é realizado o vínculo</param>
        public static void SetDoubleClickCommandParameter(UIElement obj, object value)
        {
            obj.SetValue(DoubleClickCommandParameterProperty, value);
        }
        /// <summary>
        /// Geter para DoubleClickCommandParameterProperty
        /// </summary>
        /// <param name="obj">Elemento de gerenciamento</param>
        /// <returns>parâmetro passado</returns>
        public static object GetDoubleClickCommandParameter(UIElement obj)
        {
            return obj.GetValue(DoubleClickCommandParameterProperty);
        }
        #endregion
    }
}

Cada propriedade de cada classe de objetos gráficos do WPF é vinculada na classe DependancyProperty. Esta classe permite vincular dados entre as camadas View e ViewModel. Para criar uma instância desta classe, é preciso usar o método estático DependencyProperty.RegisterAttached, que retorna a classe DependencyProperty configurada. Dado método assume 4 parâmetros, saiba mais sobre eles aqui. Vale a pena mencionar o fato de que a propriedade criada deve ter modificadores de acesso public static readonly (ou seja, acessíveis fora dessa classe, com chamada dessa propriedade sem a necessidade de criar uma instância da classe, além disso, o modificador estático define a unidade dessa propriedade neste aplicativo específico, enquanto readonly torna a propriedade imutável).

  1. O primeiro parâmetro define o nome pelo qual essa propriedade ficará visível no esquema XAML.
  2. O segundo parâmetro define o tipo de elemento com o qual é planejado realizar a ligação. Objetos desse tipo específico serão armazenados na instância criada da classe DependancyProperty. 
  3. O terceiro parâmetro define o tipo da classe na qual está localizada esta propriedade, no nosso caso, a classe ListViewExtention.
  4. O último parâmetro aceita uma instância da classe PropertyMetadata - este parâmetro está ligado essencialmente ao manipulador de eventos chamado quando instanciada a classe DependancyProperty. Precisamos desse retorno de chamada para assinar o evento de duplo clique.

Para que possamos definir e receber valores corretamente a partir desta propriedade, precisamos criar métodos cujo nome consistirá no nome passado ao criar a instância da classe DependancyProperty e nos prefixos Set (para definir valores Get e para obter valores). Ambos os métodos também devem ser estáticos. Essencialmente, eles encapsulam o uso de métodos pré-existentes. Setvaluee Getvalue.

O retorno de chamada do evento de criação da propriedade dependente realiza a assinatura do evento de clique duplo de uma linha da tabela e cancela a inscrição de um evento previamente assinado, se existir. Dentro do manipulador do evento de clique duplo, é realizada chamada sequencial de métodos CanExecute e Execute de um campo do tipo ICommand passado para View. Dessa forma, quando um evento de clique duplo é acionado em qualquer uma das linhas da tabela assinada, chamamos automaticamente o manipulador deste evento, no qual são chamados os métodos da lógica executada após a ocorrência desse evento.

A classe que criamos é essencialmente uma classe intermediária que assume o papel de manipular eventos e chamar métodos do ViewModel, mas não executa nenhuma lógica de negociação em si. Talvez essa abordagem pareça mais confusa do que chamar diretamente um método de um manipulador de eventos de clique duplo (como é habitual nos WinForms), mas há boas razões para usá-la, pois é necessário observar o padrão MVVM que afirma que o View não deve saber de nada sobre ViewModel e vice-versa.

Usando a classe intermediária, reduzimos a conectividade entre as classes, o que é feito pelo modelo de programação mencionado. Agora podemos alterar arbitrariamente a classe ViewModel, a única coisa que precisamos é especificar uma propriedade específica do tipo ICommand que será acessada pela classe intermediária.

Esse complemento também implementa a propriedade de transformar o evento SelectionChanged em ICommand, bem como uma classe intermediária que cria automaticamente colunas para uma tabela com base num campo vinculado que armazena uma coleção de nomes de colunas. Ambas as extensões do esquema XAML são implementadas da maneira descrita acima e, portanto, não chamarei atenção para elas se algo não estiver claro, terei prazer em responder a todas as perguntas nos comentários do artigo. Agora que implementamos o esquema da guia Optimisation Result, nossa janela fica assim:


O próximo passo é implementar a guia Settings. Por conveniência, não apresentarei apenas a parte do esquema XAML que descreve os principais objetos gráficos. A versão completa do esquema pode ser vista no código fonte do artigo.

<!--Guia de configurações do robô e de inicialização da otimização ou do teste único-->
            <TabItem Header="Settings">
                <!--Contêiner com as configurações e etc.-->
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition Height="200"/>
                    </Grid.RowDefinitions>

                    <!--Contêiner contendo a lista de terminais selecionados-->
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="30"/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <!--Contêiner com seleção de terminais que são definidos automaticamente-->
                        <WrapPanel HorizontalAlignment="Right" 
                                       VerticalAlignment="Center">
                            <!--Lista com terminais-->
                            <ComboBox Width="200" 
                                          ItemsSource="{Binding TerminalsID}"
                                          SelectedIndex="{Binding SelectedTerminal, UpdateSourceTrigger=PropertyChanged}"
                                          IsEnabled="{Binding IsTerminalsLVEnabled, UpdateSourceTrigger=PropertyChanged}"/>
                            <!--Botão de adição de terminal-->
                            <Button Content="Add" Margin="5,0"
                                    Command="{Binding AddTerminal}"
                                    IsEnabled="{Binding IsTerminalsLVEnabled, UpdateSourceTrigger=PropertyChanged}"/>
                        </WrapPanel>
                        <!--Lista com os terminais selecionados-->
                        <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>
                    <!--Contêiner contendo opções para edição e 
                    configurações de otimização-->
                    <TabControl
                                Grid.Row="2" 
                                Margin="0,0,0,5"
                                TabStripPlacement="Right">
                        <!--Guia de parâmetros do robô-->
                        <TabItem Header="Bot params" >
                            <!--Lista com parâmetros do robô-->
                            <ListView 
                                    ItemsSource="{Binding BotParams, UpdateSourceTrigger=PropertyChanged}">
                                <ListView.View>
                                    <GridView>
                                    .
                                    .
                                    .
                                    </GridView>
                                </ListView.View>
                            </ListView>
                        </TabItem>
                        <!--Guia de configurações de otimização-->
                        <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 que vê o robô-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center">
                                    <Label Content="Login:"/>
                                    <TextBox Text="{Binding TestLogin, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Tipo de execução-->
                                <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 de entrega do histórico para testos-->
                                <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>
                                <!--Critérios de otimização-->
                                <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>
                                <!--Data de início do 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>
                                <!--Moeda de medição do lucro-->
                                <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>
                                <!--Alavancagem-->
                                <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 o visualizador de teste?-->
                                <CheckBox Content="Visual mode"
                                              Margin="2"
                                              VerticalAlignment="Center"
                                              Grid.Column="2"
                                              Grid.Row="0"
                                              IsChecked="{Binding IsVisual, UpdateSourceTrigger=PropertyChanged}"/>
                            </Grid>
                        </TabItem>
                    </TabControl>

                    <!--Barra de separação para alterar o tamanho 
                    de uma área em relação a outra ->
                    <GridSplitter Height="3" VerticalAlignment="Bottom" HorizontalAlignment="Stretch"/>

                </Grid>
            </TabItem>

Em primeiro lugar, vale a pena mencionar como implementar áreas que mudam dinamicamente. Este comportamento de formulário é implementado formando duas linhas <Grid/> e adicionando o elemento <GridSplitter/>. Somos nós que o ajustamos, para que as áreas (lista de terminais e o resto das tabelas) mudem de tamanho. Na primeira linha da tabela gerada, inserimos um novo <Grid/>, que novamente dividimos em duas partes: a primeira contém outro elemento ( Wrappanel) que possui uma lista de terminais e um botão para adicionar um novo terminal, já a segunda parte contém uma tabela com uma lista dos terminais adicionados.

Além do texto, à tabela também são adicionados os controles que permitem alterar seus dados. Graças à metodologia de vinculação de dados para alterar/adicionar valores à tabela, não precisaremos escrever uma única linha de código, pois a tabela estará diretamente associada a uma coleção de elementos. A parte inferior do bloco variável <Grid/> contém o Tabcontrol que, por sua vez, encerra as configurações do testador e a tabela com a lista de parâmetros do robô.

Neste ponto, o shell gráfico deste complemento pode ser considerado como concluído. Mas antes de passar para a descrição de ViewModel, vale a pena dizer algumas palavras sobre a maneira como as tabelas são vinculadas.

Vamos descrever este aspecto exemplificando com uma tabela contendo os parâmetros dos robôs, como pode ser visto no testador MetaTrader, ele deve ter os seguintes campos:

Para transferir todos esses parâmetros para uma tabela, precisamos criar uma classe armazenando os dados da linha desta tabela. Em outras palavras, esta classe deve descrever todas as colunas que a tabela possui, enquanto a coleção destas classes armazenará a tabela inteira. Para a tabela descrita, foi criada a seguinte classe:

/// <summary>
/// Classe descrevendo as strings para a tabela com as configurações dos parâmetros do robô antes da otimização
/// </summary>
class ParamsItem
{
    /// <summary>
    /// Construtor da classe
    /// </summary>
    /// <param name="Name">Nome da variável</param>
    public ParamsItem(string Name) => Variable = Name;
    /// <summary>
    /// Sinal para otimizar esta variável do robô
    /// </summary>
    public bool IsOptimize { get; set; }
    /// <summary>
    /// Nome da variável
    /// </summary>
    public string Variable { get; }
    /// <summary>
    /// Valor da variável selecionada para o teste
    /// </summary>
    public string Value { get; set; }
    /// <summary>
    /// Início da iteração de parâmetros
    /// </summary>
    public string Start { get; set; }
    /// <summary>
    /// Etapa da iteração de parâmetro
    /// </summary>
    public string Step { get; set; }
    /// <summary>
    /// Fim da iteração de parâmetros
    /// </summary>
    public string Stop { get; set; }
}

Como pode ser visto a partir de sua estrutura, cada propriedade desta classe contém informações sobre uma coluna específica. Agora, se aprofundarmos, podemos perceber como muda o contexto dos dados. Ao criar a janela do aplicativo, indicamos logo no início que a fonte de dados para a janela é a classe ExtentionGUI_VM, pois ela é o DataContext principal para esta janela, e é ela que deve conter a coleção à qual a tabela está associada. No entanto, para cada linha específica desta tabela específica, DataContext muda da classe ExtentionGUI_VM para a classe ParamsItem. Esta nuance é bastante importante, sendo assim, se por exemplo nós tivermos que atualizar qualquer célula desta tabela no código do programa, nós já teremos que chamar o evento PropertyChanged não na classe ExtentionGUI_VM, mas, sim, na classe de contexto de esta linha em particular.

Nesta fase, podemos concluir a descrição de como criar a camada gráfica de nosso aplicativo e, assim, prosseguir para a descrição da classe de acoplamento entre o aplicativo e a lógica do programa em si.


ViewModel e conector juntando o MetaTrader e a dll implementada

O próximo componente do programa é a parte responsável pelo acoplamento entre os gráficos discutidos acima e a lógica, que será discutida mais adiante. Em nosso modelo de programação (Model View ViewModel ou MVVM), essa parte é chamada ViewModel e está localizada no namespace correspondente (OptimisationManagerExtention.ViewModel).

No primeiro capítulo deste artigo, criamos a classe ExtentionGUI_VM e implementamos a interface INotifyPropertyChanged, pois, é essa classe específica que servirá como juntura entre gráficos e lógica. Antes de descrever seu processo de implementação, vale a pena enfatizar que todos os campos da classe ExtentionGUI_VM, com os quais estão vinculados os dados do View, devem ser declarados precisamente como propriedades e não como variáveis. Para aqueles que são novos neste constructo da linguagem C#, explicaremos como eles diferem exemplificando com o código abaixo:

class A
{
    /// <summary>
    /// Este é um campo público simples (que pode ser definido) a partir do qual podem ser lidos os valores 
    /// Mas não há como realizar a verificação ou outras ações.
    /// </summary>
    public int MyField = 5;
    /// <summary>
    /// Esta é uma propriedade que permite processar informações antes de escrever ou ler dados
    /// </summary>
    public int MyGetSetProperty
    {
        get
        {
            MyField++;
            return MyField;
        }
        set
        {
            MyField = value;
        }
    }

    // Esta propriedade é somente para leitura
    public int GetOnlyProperty => MyField;
    /// <summary>
    /// Esta propriedade é somente para registro
    /// </summary>
    public int SetOnlyProperty
    {
        set
        {
            if (value != MyField)
                MyField = value;
        }
    }
}

Como se pode ver no exemplo, as propriedades são um tipo de híbrido entre métodos e campos. Eles permitem executar qualquer ação antes de retornar o valor ou verificar a fidelidade dos dados registrados. Além disso, as propriedades podem ser somente leitura ou apenas para registro. Foram precisamente aos dados de constructo da linguagem C# que nos referimos em View quando aplicamos a vinculação de dados.

Durante a implementação da classe ExtentionGUI_VM, eu a dividi numa série de blocos (constructos #reguin #endregion), e é através deles que eu considerarei seu processo de criação. Na parte do View, primeiro, consideramos a criação da guia Optimization Result, já nesta classe começaremos abordando as propriedades e métodos criados para esta guia. Por conveniência, primeiro, mostrarei o código responsável pelos dados exibidos em esta guia e, depois disso, escreverei explicações.

#region Optimisation Result

/// <summary>
/// Tabela com resultados históricos de otimização
/// </summary>
public DataTable HistoryOptimisationResults => model.HistoryOptimisationResults;
/// <summary>
/// Tabela com resultados de otimização para frente
/// </summary>
public DataTable ForvardOptimisationResults => model.ForvardOptimisationResults;
/// <summary>
/// Coleção observável com uma lista de colunas de otimização
/// </summary>
public ObservableCollection<ColumnDescriptor> OptimisationResultsColumnHeadders =>
       model.OptimisationResultsColumnHeadders;

#region Start test from optimisation results
/// <summary>
/// Inicialização do teste para o processo de otimização selecionado
/// </summary>
public ICommand StartTestFromOptimisationResults { get; }
/// <summary>
/// Método que inicia um teste após duplo clique
/// </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 da linha selecionada da tabela de otimizações históricas
/// </summary>
public int SelectedHistoryOptimisationRow { get; set; } = 0;
/// <summary>
/// Índice da linha selecionada da otimização para frente
/// </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

A primeira coisa a considerar é fontes de dados para tabelas com otimizações histórica e para frente, bem como lista de colunas vinculadas através da classe intermediária GridViewColumns com as colunas de ambas as tabelas. Como pode ser visto no fragmento de código apresentado, cada tabela possui dois campos exclusivos: a própria fonte de dados (digitada pela classe DataTable) e a propriedade que contém o índice da linha selecionada na tabela. O índice da linha selecionada não desempenha um papel na exibição, no entanto, precisaremos dela para outras ações, isto é, para executar testes clicando duas vezes na linha da tabela. Considerando o fato de que a lógica do programa deve se encarregar do carregamento de dados em tabelas e da sua limpeza, e segundo os princípios da POO, uma classe específica deve ser responsável por uma tarefa específica, então nas propriedades que fornecem informações sobre a composição da tabela, simplesmente nos referimos às propriedades correspondentes da classe principal do modelo (ExtemtionGUI_M). O rastreamento dos índices selecionados é realizado automaticamente através de cliques do mouse nos campos da tabela e, portanto, essas propriedades não executam nenhuma ação ou verificação. Em essência, eles são semelhantes aos campos de classe.

Também vale a pena prestar atenção ao tipo de dados usado para a propriedade que contém uma lista de colunas (OptimisationResultsColumnHeadders) — ObservableCollection<T>. Esta classe é uma das classes de linguagem C# padrão que armazena coleções dinamicamente alteráveis, mas, ao contrário de listas (List<T>), esta classe contém o evento CollectionChanged, que é chamado toda vez que os dados são alterados/excluídos/adicionados à coleção. Assim, ao criar uma propriedade digitada por esta classe, recebemos uma notificação automática do View sobre o fato de que a fonte de dados foi alterada e, portanto, não precisamos mais notificar manualmente o gráfico sobre a necessidade de sobrescrever os dados exibidos, o que é bastante conveniente em alguns casos. 

Apos abordar as tabelas, vale a pena prestar atenção às listas suspensas com a escolha de terminais e de robôs, bem como analisar a implementação de manipuladores de eventos para clicar em botões e clicar em tabelas. O bloco para trabalhar com listas suspensas e os resultados de otimização de carregamento estão contidos na área marcada como #region UpdateOptimisationReport. Primeiro, consideraremos uma fonte de dados para a primeira lista suspensa que contém uma lista de terminais. Estes são a lista de IDs de terminal que foram otimizados e o índice de terminais selecionados. Como é o modelo que deverá novamente se encarregar de compilar a lista de terminais, simplesmente nos referiremos ao campo correspondente no modelo. A escolha do índice do terminal é um pouco mais complicada, neste caso foi utilizada a vantagem das propriedades sobre os campos mencionados acima. Após selecionar um terminal na lista suspensa, é acessado o setter da propriedade TerminalsAfterOptimisation_Selected em que realizamos uma série de ações, isto é:

  1. Salvamos o novo índice selecionado no modelo
  2. Atualizamos todos os valores da segunda lista suspensa, onde é armazenada a lista de robôs que foram submetidos a otimização neste terminal.

Isso é necessário porque o complemento armazena o histórico de testes, classificando por robô e por terminal, se novamente otimizarmos o mesmo robô no mesmo terminal, será sobrescrito o histórico anterior. Este método de transferir eventos de View para ViewModel é o mais conveniente, no entanto, infelizmente, nem sempre é adequado.

A próxima maneira de passar eventos da camada de gráficos para o ViewModel é usar comandos. Alguns elementos gráficos, como Button, suportam os chamados comandos. Ao usar comandos, vinculamos a propriedade de comando à propriedade do ViewModel com o tipo parametrizado ICommand. A interface ICommand em si é uma das interfaces padrão de C# e tem a seguinte aparência:

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

Quando clicado um botão, primeiro é acionado o evento ConExecute e, se retornar false, o botão ficará inacessível, caso contrário, será chamado o método Execute, o que executa a operação necessária. Para usar essa funcionalidade, é necessário implementar esta interface. No processo de implementação da interface, eu não inventei nada de novo e simplesmente usei sua implementação padrão, já que me pareceu a mais ideal.

/// <summary>
/// Implementação da interface ICommand usada para
/// relação de comandos com métodos do ViewModel
/// </summary>
class RelayCommand : ICommand
{
    #region Fields 
    /// <summary>
    /// Delegado executando a ação diretamente
    /// </summary>
    readonly Action<object> _execute;
    /// <summary>
    /// Delegado verificando a possibilidade de executar uma ação
    /// </summary>
    readonly Predicate<object> _canExecute;
    #endregion // Fields

    /// <summary>
    /// Construtor
    /// </summary>
    /// <param name="execute">Método (passado segundo delegado) que é a chamada de retorno</param>
    public RelayCommand(Action<object> execute) : this(execute, null) { }
    /// <summary>
    /// Construtor
    /// </summary>
    /// <param name="execute">
    /// Método (passado segundo delegado) que é a chamada de retorno
    /// </param>
    /// <param name="canExecute">
    /// Método (passado segundo delegado) verificando se uma ação pode ser executada
    /// </param>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");
        _execute = execute; _canExecute = canExecute;
    }

    /// <summary>
    /// Verificação da possibilidade de executar uma ação
    /// </summary>
    /// <param name="parameter">parâmetro passado do View</param>
    /// <returns></returns>
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }
    /// <summary>
    /// Evento chamado sempre que é alterada a possibilidade de execução de retorno de chamada.
    /// Quando este evento é acionado, o formulário chama o método "CanExecute" novamente
    /// O evento é iniciado do ViewModel conforme necessário
    /// </summary>
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
    /// <summary>
    /// Método chamando o delegado que por sua vez executa a ação
    /// </summary>
    /// <param name="parameter">parâmetro passado do View</param>
    public void Execute(object parameter) { _execute(parameter); }
}

De acordo com esta implementação da interface ICommand, são criados dois campos privados somente leitura que armazenam em si delegados que, por sua vez, armazenam os métodos passados para eles através de uma das sobrecargas do construtor de classe Relaycommand. Como resultado, para usar este mecanismo, criamos uma instância da classe RelayCommand no construtor da classe ExtentionGUI_VM e passamos - para ela - um método que executa determinadas ações. Se falamos diretamente sobre a propriedade considerada UpdateOptimisationReport que atualiza as informações em tabelas com otimizações, ela fica assim:

UpdateOptimisationReport = new RelayCommand(UpdateReportsData);

OndeUpdateReportsData é um método privado da classe ExtentionGUI_VM que chama o método LoadOptimisations() da classe ExtentionGUI_M (isto é, a classe do nosso modelo). De uma maneira completamente semelhante, é realizada a junção entre a propriedade StartTestFromOptimisationResults e o evento de duplo clique na linha - selecionada pelo usuário - da tabela. Mas somente neste caso, a transmissão do evento de clique é realizada não através de uma propriedade implementada como um botão (classe Button), mas, sim, através da extensão "ListV iewExtention.DoubleClickCommand" já descrita e criada por nós. Como pode ser visto na assinatura dos métodos Execute e CanExecute, eles podem assumir um valor do tipo Object. No caso do botão, não passamos nenhum valor para eles, mas no caso do evento de clique duplo, passamos o nome da tabela para eles, o que pode ser visto a partir do método de ligação com essas propriedades no esquema XAML:    

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

Com base neste parâmetro, nosso modelo entende em qual tabela vale a pena pegar os dados para executar o teste de otimização.

O próximo passo ao considerar esta classe é prestar atenção à implementação de propriedades e de chamadas de retorno para trabalhar com a guia Settings, é nela que estão localizados todos os controles principais. Entre as particularidades interessantes da implementação da parte de encaixe para esta guia, a primeira coisa a considerar é a implementação da fonte de dados para a tabela que contém os terminais selecionados.

#region SelectedTerminalsForOptimisation && SelectedTerminalIndex (first LV params)
/// <summary>
/// Lista de terminais selecionados para otimização e exibidos na tabela de terminais
/// </summary>
public ObservableCollection<TerminalAndBotItem> SelectedTerminalsForOptimisation { get; private set; } =
    new ObservableCollection<TerminalAndBotItem>();
/// <summary>
/// Índice da linha selecionada
/// </summary>
private int selectedTerminalIndex = 0;
public int SelectedTerminalIndex
{
    get { return selectedTerminalIndex; }
    set
    {
        // Atribuímos o valor do índice recém-selecionado
        selectedTerminalIndex = value;

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

        // Preenchemos a lista de parâmetros do robô selecionado na linha atual
        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 você pode ver, a lista de terminais é apresentada como uma coleção observável digitada pela classe TerminalAndBotItem. Esta coleção é armazenada na própria classe ViewModel. Também no ViewModel foi feita a propriedade de trabalho e obtenção do índice da linha selecionada, isso foi feito para poder responder ao evento de seleção de terminal da lista. Como visto no vídeo, ao clicar numa linha, os parâmetros do robô são carregados dinamicamente. Dado comportamento é implementado apenas no setter da propriedade SelectedTerminalIndex.

Também vale a pena lembrar que as linhas na tabela com os terminais selecionados contêm controles e, por esse motivo, precisamos criar uma classe TerminalAndBotItem como uma classe-contexto sendo um contexto de dados, consideramos detalhes interessantes de sua implementação.

O primeiro desses detalhes é como removemos um terminal da lista de terminais. Acontece que, como mencionado acima, os dados para a tabela são armazenados no ViewModel, enquanto o retorno de chamada para o botão Delete, que está na tabela, pode ser desvinculado apenas com o contexto de dados dessa linha, ou seja, com a classe TerminalAndBotItem a partir da qual não há acesso a esta coleção. Podemos contornar esta situação com a ajuda de delegados. Eu implementei um método que exclui dados na classe ExtentionGUI_VM e, em seguida, passa-o como um delegado através do construtor para a classe TerminalAndBotItem. No código abaixo, para maior clareza, foram excluídas todas as linhas desnecessárias para esta descrição. A entrega do método para se apagar do exterior fica assim:

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

    #region Delegates
    /// <summary>
    /// Campo com o delegado para atualizar os parâmetros do robô selecionado
    /// </summary>
    private readonly Action<string, string> FillInBotParams;
    /// <summary>
    /// Retorno de chamada para o comando que exclui um terminal da lista (na tabela, botão Delete)
    /// </summary>
    public ICommand DeleteCommand { get; }
    #endregion

    /// <summary>
    /// índice do EA selecionado
    /// </summary>
    private int selectedExpert;
    /// <summary>
    /// Property para o índice do EA selecionado
    /// </summary>
    public int SelectedExpert
    {
        get { return selectedExpert; }
        set
        {
            selectedExpert = value;
            // Executamos o retorno de chamada do carregamento de parâmetros para o EA selecionado 
            if (Experts.Count > 0)
                FillInBotParams(Experts[selectedExpert], TerminalID);
        }
    }
}

Como visto neste trecho, no processo de implementação da tarefa dada, foi usadõ outro constructo da linguagem C#, nomeadamente expressões lambda. Para aqueles que estão familiarizados com C++ ou C#, este trecho de código não parecerá estranho, para o resto eu explicarei que expressões lambda podem ser consideradas como as funções em si, mas a principal diferença delas é o fato de não terem uma declaração tradicional . Estes constructos são amplamente usados em C# e você pode ler sobre eles aqui. O retorno de chamada em si é chamado usando o ICommand. Outro momento interessante na implementação desta classe é a atualização dos parâmetros do robô ao escolher um novo robô na lista suspensa que contém todos os robôs. Em primeiro lugar, o método que atualiza a lista de parâmetros do robô está no modelo, enquanto a implementação do wrapper para este método para o ViewModel - assim como o método para excluir o terminal - está no ViewModel. Novamente, os delegados vêm ao resgate, no entanto, neste caso, em vez de usar o ICommand, colocamos uma resposta ao evento de seleção de um novo robô no setter da propriedade SelectedExpert.

O método em si, que atualiza os parâmetros dos EA, também apresenta algumas nuances, nomeadamente assincronia.

private readonly object botParams_locker = new object();
/// <summary>
/// Obter e preencher parâmetros do robô
/// </summary>
/// <param name="fullExpertName">Nome completo do EA em relação à pasta ~/Experts</param>
/// <param name="Terminal">ID do 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");
}

Em C#, há um modelo de programação assíncrona fácil de escrever - Async Await - que aplicamos neste caso. O trecho de código apresentado inicia uma operação assíncrona e aguarda que sua implementação seja concluída. Após a conclusão da operação, é gerado um evento Onpropertychanged, que notifica o View de uma alteração na tabela que contém a lista de parâmetros do robô. Para entender qual é a particularidade, vale a pena considerar um modelo de um aplicativo assíncrono usando a tecnologia 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}");
        }

    }

O objetivo deste aplicativo de console simples é mostrar o comportamento dos threads e fazer uma breve excursão ao mundo da assincronia para aqueles que não trabalharam com ela. Como se pode ver, no método Main, primeiro exibimos na tela o ID do thread no qual está sendo executado o método Main, em seguida, inciamos o método assíncrono e depois exibimos o ID do thread Main novamente. No método assíncrono, novamente exibimos o ID do thread no qual está sendo executado este método e imprimimos um por um o ID dos threads assíncronos e o ID do thread no qual serão executadas as operações após a inicialização do thread assíncrono. A conclusão mais interessante deste 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 pode ser visto na saída deste programa, os IDs do thread Main e os da primeira saída do método assíncrono Method() são os mesmos. Isso nos diz que o método Method() não é completamente assíncrono e, de fato, a assincronia desse método começa somente depois que a operação assíncrona é chamada usando o método estático Task.Run(). Se o método() estiver completamente sincronizado, a próxima mensagem novamente exibindo o ID do thread principal seria chamada depois que as próximas quatro mensagens forem exibidas, no entanto, esse não é o caso. 

Agora vamos ver saídas assíncronas. Primeira saída assíncrona retorna ID = 3, o que é esperado, mas o mais interessante é que a operação seguinte, após aguardar a conclusão da operação assíncrona (graças a await), também retorna ID = 3. A mesma imagem é observada com a segunda operação assíncrona. Também é um fato interessante que, apesar do atraso de 100 milissegundos adicionados após a saída do ID do thread usado após a primeira operação assíncrona, a ordem de prioridade não muda, apesar do fato de que a segundo operação começa num thread diferente do primeiro.

Tratavam-se de particularidades ao trabalhar com o modelo assíncrono de programação Async Await e assincronia em geral. Voltando ao nosso método, podemos dizer que todas as ações nele escritas são realizadas no contexto do thread secundário e, portanto, existe a chance de ele ser chamado duas vezes, o que pode levar a erros. Para fazer isso, é usado o constructor lock(locker_object){}. Este constructo cria algo como uma fila de chamadas, como vimos em nosso modelo de teste, mas apenas em contraste com esse modelo de teste em que a fila é formada independentemente por mecanismos C#, aqui usamos um recurso compartilhado que serve como um comutador. Se estiver envolvido no constructo lock(), qualquer outra chamada para este método ficará presa - no estágio de bloqueio do recurso compartilhado - até ser liberada. Assim, evitamos o erro de chamada de método dupla, sem esperar que seja concluído.

Outro aspecto que vale a pena considerar é a criação de fontes de dados para configurações do otimizador (seu código é dado abaixo):

#region Optimisation and Test settings

/// <summary>
/// Login que o robô vê durante o teste (necessário se houver restrições no login)
/// </summary>
private uint? _tertLogin;
public uint? TestLogin
{
    get => _tertLogin;
    set
    {
        _tertLogin = value;

        OnPropertyChanged("TestLogin");
        CB_Action(GetSetActionType.Set_Index);
    }
}
/// <summary>
/// Atraso na execução de ordens
/// </summary>
public ComboBoxItems<string> ExecutionList { get; }
/// <summary>
/// Tipo de cotações utilizadas (cada tick OHLC 1M ...)
/// </summary>
public ComboBoxItems<string> ModelList { get; }
/// <summary>
/// Critério de otimização
/// </summary>
public ComboBoxItems<string> OptimisationCriteriaList { get; }
/// <summary>
/// Depósito
/// </summary>
public ComboBoxItems<int> Deposit { get; }
/// <summary>
/// Moeda de cálculo de lucro
/// </summary>
public ComboBoxItems<string> CurrencyList { get; }
/// <summary>
/// Alavancagem
/// </summary>
public ComboBoxItems<string> LaverageList { get; }
/// <summary>
/// Data de início do teste para frente
/// </summary>
private DateTime _DTForvard = DateTime.Now;
public DateTime ForvardDate
{
    get => _DTForvard;
    set
    {
        _DTForvard = value;

        OnPropertyChanged("ForvardDate");
        CB_Action(GetSetActionType.Set_Index);
    }
}
/// <summary>
/// Sinal de inicialização do testador no modo gráfico
/// </summary>
private bool _isVisualMode = false;
/// <summary>
/// Sinal de inicialização do testador no modo visual
/// </summary>
public bool IsVisual
{
    get => _isVisualMode;
    set
    {
        _isVisualMode = value;

        OnPropertyChanged("IsVisual");
        CB_Action(GetSetActionType.Set_Index);
    }
}
/// <summary>
/// variável oculta armazenando o valor do sinalizador IsSaveInModel
/// </summary>
private bool isSaveInModel = true;
/// <summary>
/// Recurso compartilhado para acesso assíncrono à propriedade IsSaveInModel
/// </summary>
private readonly object SaveModel_locker = new object();
/// <summary>
/// Se o sinalizador for True, ao alterar os parâmetros do testador, eles serão salvos
/// </summary>
private bool IsSaveInModel
{
    get
    {
        lock (SaveModel_locker)
            return isSaveInModel;
    }
    set
    {
        lock (SaveModel_locker)
            isSaveInModel = value;
    }
}
/// <summary>
/// Chamada de retorno salvando mudanças nos parâmetros do testador
/// </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

Um ponto importante destas configurações é como é implementado o armazenamento de parâmetros do otimizador. Falando um pouco antes, vale dizer que o modelo para cada robô tem sua própria instância de configurações de testador, o permite configurar o testador de cada um dos terminais selecionados à sua maneira. Para isso, foi criado o método CB_Action que é chamado em cada setter, o que garante um armazenamento instantâneo de resultados no modelo após fazer alterações em qualquer um dos parâmetros. Também vale a pena considerar a classe ComboBoxItems<T> que eu criei especificamente para armazenar dados de listas suspensas. Em essência, é o contexto de dados para o ComboBox com o qual está vinculado. A classe de dados tem a seguinte implementação sem complicações:

/// <summary>
/// Classe embrulho para dados da lista ComboBox
/// </summary>
/// <typeparam name="T">Tipo de dados armazenados no ComboBox</typeparam>
class ComboBoxItems<T> : INotifyPropertyChanged
{
    /// <summary>
    /// Coleção de itens da 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>
    /// Índice selecionado na 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 entanto, sua característica é o evento chamado a cada vez ao editar seus eventos ou ao receber informações de seus eventos. Sua próxima característica é a notificação automática View sobre a alteração de qualquer uma das suas propriedades. Assim, é capaz de notificar ViewModel e View sobre alterações em suas propriedades, graças a esta propriedade, no ViewModel, atualizamos o modelo com informações sobre as propriedades alteradas das configurações do otimizador e chamada de salvamento automático, além disso, conseguimos tornar o código mais legível, pois renderizamos suas propriedades para cada ComboBox em ViewModel 2 (índice do elemento selecionado e uma lista de todos os elementos). Se não usássemos essa classe, nossa classe ExtentionGUI_VM aumentaria ainda mais.  

Em conclusão, vale a pena considerar como instanciar o modelo de nosso complemento e como executar gráficos escritos no terminal MetaTrader 5. A classe do modelo de dados também deve ser independente do ViewModel, bem como do ViewModel do View, portanto, para teste, faremos o modelo através da interface IExtentionGUI_M. Abordaremos a estrutura desta interface e sua implementação quando descrevermos o próprio modelo de dados, agora só precisamos saber que a classe ExtentionGUI_VM não sabe sobre a implementação específica do modelo de dados, em vez disso, trabalha com a interface IExtentionGUI_M, enquanto a classe model é instanciada da seguinte maneira:

private readonly IExtentionGUI_M model = ModelCreator.Model;

Este processo de instanciação usa uma fábrica estática. A classe ModelCreator é uma fábrica e é implementada da seguinte forma:

/// <summary>
/// Fábrica para substituir um modelo na interface gráfica
/// </summary>
class ModelCreator
{
    /// <summary>
    /// Modelo
    /// </summary>
    private static IExtentionGUI_M testModel;
    /// <summary>
    /// Property retornando um modelo (se não foi substituído) ou um modelo trocado (para testes)
    /// </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>
    /// Método de substituição de modelo. substitui o modelo de teste para poder testar os gráficos separadamente da lógica
    /// </summary>
    /// <param name="model"> modelo de teste - substituído do lado de fora </param>
    [System.Diagnostics.Conditional("DEBUG")]
    public static void SetModel(IExtentionGUI_M model)
    {
        testModel = model;
    }
}

Esta classe tem campo private digitado pela interface do modelo de dados. Inicialmente, este campo é null, o qual usamos ao escrever a propriedade estática recebendo o modelo solicitado. Como se pode ver no código, nós verificamos (se testModel é agora null, instanciamos e retornamos uma implementação do modelo contendo a lógica de negociação; se testModel não é null - o que significa que nós mudamos o modelo -, retornamos o modelo substituído) aquele que é armazenado em testModel. Para substituir o modelo, é usado o método estático SetModel. Este método é decorado com o atributo [System.Diagnostics.Conditional( "DEBUG")], que proíbe seu uso na versão Release deste programa.

O processo de inicialização da GUI é semelhante ao processo de inicialização da dll sugerido no artigo mencionado anteriormente. Especialmente para o acoplamento com o MetaTrader, foi escrita a classe pública MQLConnector. 

/// <summary>
/// Classe para vincular a interface gráfica com o MetaTrader
/// </summary>
public class MQL5Connector
{
    /// <summary>
    /// Campo contendo um ponteiro para uma interface gráfica em execução
    /// </summary>
    private static View.ExtentionGUI instance;
    /// <summary>
    /// Método que inicia a interface gráfica. 
    /// Além disso, a inicialização é realizada apenas para a interface de um robô. 
    /// quer dizer, ao iniciar, verifica-se se a interface gráfica foi iniciada anteriormente 
    /// se sim, é rejeitada a inicialização de um novo
    /// </summary>
    /// <param name="pathToTerminal">Caminho para a pasta do terminal a ser modificada</param>
    public static void Instance(string terminalID)
    {
        // verifica se a interface gráfica foi iniciada anteriormente
        if (instance == null)
        {
            // Variável de fluxo secundário - o fluxo da interface gráfica (os gráficos são iniciados no fluxo secundário)
            // e sua instanciação com a passagem de uma expressão lambda descrevendo a ordem de inicialização dos gráficos
            Thread t = new Thread(() =>
            {
                // Instância da classe de interface gráfica e sua exibição (inicialização de gráfico)
                instance = new View.ExtentionGUI();
                instance.Show();
                // Assinatura do evento de fechar a janela de gráficos, se a janela estiver fechada, 
                // o campo onde o link para a interface gráfica foi armazenado é definido como null
                instance.Closed += (object o, EventArgs e) => { instance = null; };

                // Inicialização do gerenciador de threads da GUI
                Dispatcher.Run();
            });
            MainTerminalID = terminalID;		

            // Inicialização do fluxo secundário
            t.SetApartmentState(System.Threading.ApartmentState.STA);
            t.Start();
        }
    }     
    /// <summary>
    /// Obtém informações de se a janela está ativa
    /// </summary>
    /// <returns>true se ativa e false se fechada</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 classe deve ser marcada com o modificador de acesso public. Isso é necessário para que seja acessível a partir do robô no MetaTrader. Além disso, os métodos que devem ser usados no terminal devem ser estáticos e ter um modificador de acesso público, porque o terminal permite usar apenas métodos estáticos. Esta classe também tem 2 propriedades com o modificador de acesso Internal — este modificador de acesso esconde sua visibilidade do terminal, uma vez que elas são destinadas apenas para uso dentro da dll criada. Como você pode ver na implementação, nossa janela deve ser armazenada no campo estático privado, pois isso é necessário para acessá-lo de outras propriedades e métodos, bem como para criar apenas uma instância dessa aplicação no terminal num robô.

O método Instance instancia o gráfico e abre uma janela, vamos analisá-lo em mais detalhes. Primeiro é verificado se a janela foi instanciada anteriormente, nesse caso, esse método ignorará a tentativa de instanciar a janela. Em seguida, é criado um thread secundário para executar gráficos. A separação de threads do gráfico e do seu programa em execução é necessária para que não haja atrasos na operação do terminal e da interface gráfica. No final do carregamento da janela, realizamos a assinatura do evento de fechamento da janela, no qual atribuímos o valor null. Isto é necessário para o correto funcionamento do esquema concebido para carregar a janela. Em seguida, precisamos executar o dispatcher se isso não for feito, o dispatcher não será iniciado para o thread em que chamamos nosso gráfico.

A classe Dispatcher foi criada para resolver conflitos de multithreading em aplicativos WPF. Acontece que todos os elementos da janela gráfica pertencem ao thread da janela gráfica, quando tentamos alterar o valor de qualquer um dos elementos gráficos de outro thread, obtemos o erro cross thread exception. A classe Dispatcher, no entanto, inicia a operação passada para ela por meio do delegado no contexto do thread da GUI, portanto, não fazendo o erro mencionado. Depois de completar a descrição da expressão lambda para executar os gráficos, devemos configurar o thread como Single Threaded Apartment e iniciá-la, iniciando assim os gráficos, mas antes disso, salvamos o valor do ID do terminal atual transmitido.

Agora vale a pena analisar por que todos precisamos disso? A resposta será bastante trivial: isso é necessário para permitir a depuração gráfica, separadamente da lógica. Nós escrevemos uma interface gráfica, no entanto, para depurá-la, precisamos de uma classe que represente o modelo. O modelo tem uma série de nuances de implementação próprias e, portanto, deve ser depurado separadamente dos gráficos. Agora que temos uma maneira de substituir um modelo de dados de teste, podemos implementar uma classe de modelo de dados de teste e substituí-la no ViewModel por meio de uma fábrica estática. Como resultado, podemos depurar os gráficos com base nos dados de teste, executar a interface gráfica e verificar a reação de retornos de chamada, design e outras nuances. Eu fiz da seguinte maneira. Primeiro, precisamos criar um aplicativo de console na solução atual para executar gráficos diretamente do VisualStudio, o que dará acesso às ferramentas de depuração.


Chamamos de "Test" e adicionamos nele um link para a nossa dll, que escrevemos para o MetaTrader. Como resultado, obtemos um aplicativo de console que pode usar as classes públicas de nossa dll. No entanto, existe apenas uma classe pública em nossa dll (classe MQL5Connector), mas além dela precisamos criar um modelo de dados falso e substituí-la no ViewModel como descrito anteriormente. Para isso precisamos acessar classes que estão disponíveis apenas dentro da dll - também há uma solução para isso. Para fazer isso, adicionamos o seguinte atributo em qualquer lugar de nossa dll:

[assembly: InternalsVisibleTo("Test")]

Ele disponibiliza na compilação Test (ou seja, em nosso aplicativo de console de teste) todas as classes internas de nossa dll. Como resultado, podemos criar um modelo falso e usá-lo para iniciar nosso aplicativo. Como resultado, nosso aplicativo de console deve ter a seguinte implementação:

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

        MQL5Connector.Instance("ID do terminal principal");
    }
}

class MyTestModel : IExtentionGUI_M
{
    // Aqui está a implementação da interface IExtentionGUI_M
}

e agora conseguimos de executar os gráficos separadamente da lógica, depurá-los e ver como ficam visualmente.

Conclusão e arquivos anexados

Examinamos os pontos mais importantes e interessantes na criação da camada gráfica do aplicativo e sua classe de vinculação (ViewModel). Neste estágio, já existe um gráfico que pode ser aberto e clicado, bem como uma classe de vinculação que descreve as fontes de dados para a camada gráfica e alguns de seus comportamentos (reação ao botão pressionado e afins). Mais adiante, passaremos a considerar a classe de modelo e seus componentes, que descreverão a lógica do complemento que será criado e os métodos de interação com arquivos, terminal e computador.