Optimierungsmanagement (Teil I): Erstellen einer GUI

Andrey Azatskiy | 21 August, 2019

Inhaltsverzeichnis

Einführung

Die alternative Methode, das MetaTrader-Terminal zu starten, wurde bereits in einem Artikel von Vladimir Karputov diskutiert. Außerdem sind die Schritte zum Starten des Terminals und eine zusätzliche alternative Methode in der entsprechenden Dokumentation beschrieben. Daten aus diesen beiden Quellen wurden in diesem Artikel verwendet, aber keine der Quellen enthält eine Beschreibung, wie man eine komfortable GUI für den gleichzeitigen Betrieb mehrerer Terminals erstellen könnte. Dieses Thema wird in dem vorliegenden Artikel behandelt.

Basierend auf den entsprechenden Recherchen habe ich eine Erweiterung für das Terminal erstellt, die es ermöglicht, den Optimierungsprozess von Expert Advisors auf mehreren Terminals auf ein und demselben Computer zu starten. Weitere Artikelversionen werden die Möglichkeiten dieser Erweiterung durch neue Funktionen erweitern.

Die Bedienung der resultierenden Version kann im Video eingesehen werden. Dieser Artikel enthält nur die Beschreibung des GUI-Erstellungsprozesses, während die Logik der Erweiterung im nächsten Teil demonstriert wird.




Startmethoden und Konfigurationsdateien des MetaTraders

Bevor wir die erstellte Erweiterung im Detail betrachten, lassen Sie uns kurz den Start des Terminals (sowie anderer Anwendungen) über die Befehlszeile betrachten. Diese Methode mag etwas archaisch erscheinen, wird aber oft verwendet, z.B. in Linux-basierten Betriebssystemen, oder sie wird auch zum Starten von Anwendungen ohne grafische Oberfläche verwendet.

Betrachten wir den Terminalstart am Beispiel eines einfachen, in C++ geschriebenen Programms:

#include <iostream>

using namespace std;

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

    return 0;
}

Nach dem Kompilieren des Programms erhalten wir eine .exe-Datei. Führen Sie die Datei aus und die Meldung "Hello World" erscheint in der Konsole, was das übliche Verhalten ist. Beachten Sie, dass die Startfunktion "main" keine Argumente hat, aber dies ist ein Sonderfall. Wenn wir dieses Programm mit einer anderen Überladung der Hauptfunktion ändern, erstellen wir eine Konsolenanwendung, die eine Reihe von Parametern erhält:

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

Der erste Parameter 'argc' gibt die Länge des Arrays von Arrays des zweiten Parameters an.

Der zweite Parameter ist die Liste der Zeichenketten, die beim Start dem Programm übergeben werden. Dieses Programm kann bereits von der Konsole aus wie folgt aufgerufen werden:

./program "My name" "is Andrey"

wobei ./program der Programmnamen darstellt und die anderen Zeichenketten seine durch Leerzeichen getrennte Parameter sind. Diese Parameter werden in das übergebene Array geschrieben. Das Ergebnis der Programmausführung ist unten dargestellt:

Hello World
./program
My name
is Andrey


Die erste Nachricht bleibt aus dem vorherigen Programm erhalten, während alle anderen Zeichenketten als Parameter an das Array 'argv' von Zeichenketten übergeben wurden (beachten Sie, dass der erste Parameter immer der Name der Anwendung ist, die Sie starten). Wir werden dieses Beispiel nicht im Detail analysieren, obwohl es nur ein Beispiel dafür ist, wie die MetaTrader-Anwendung über die Befehlszeile gestartet werden kann.

Bei der Arbeit mit Parametern werden in der Regel sogenannte Flags vor jedem Parameter angezeigt, damit das Programm versteht, auf welchen Parameter der übertragene Wert eingestellt ist. Die Sprache C/C++ verfügt über eine Reihe von Funktionen für die Arbeit mit Flags. Das bedeutet, dass die einfache Anwendung mit der Erweiterung der ausführbaren Datei (.exe) von der Konsole aus mit den übergebenen Parametern gestartet werden kann, die die Anwendungseigenschaften ändern können.  

Laut der offiziellen Dokumentation gibt es spezielle Flags und Werte für die Ausführung von MetaTrader über die Befehlszeile:

Die Flags können kombiniert werden. Wenn Sie beispielsweise eine Kombination von Flags verwenden, können Sie das Terminal im portablen Modus mit der angegebenen Konfigurationsdatei starten:

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

Obwohl die Unterschiede zwischen dem Beispiel "Hello World" und dem Terminal sehr groß sind, sind die Methoden zum Starten über die Befehlszeile identisch. Wir werden diese Funktion bei der Entwicklung unseres Add-ons nutzen.

Achten Sie besonders auf die Konfigurationsdatei, deren Pfad mit dem Schlüsselwert /config angegeben wird: Aufgrund dieser Datei weiß das Terminal, welches Login/Passwort beim Start verwendet werden soll, ebenso wie den Startmodus des Testers oder generell die Notwendigkeit, den Tester auszuführen. Ich werde die Konfigurationsdatei mit den Anweisungen hier nicht kopieren. Lassen Sie uns jedoch die Struktur dieser Dateien betrachten. Jede Konfigurationsdatei besteht aus einer Reihe von Abschnitten, die in eckigen Klammern angegeben sind.

[Tester]

Dem Abschnitt folgt eine Schlüsselwertliste mit der Beschreibung von Feldern, die die Programmstartparameter charakterisieren. Konfigurationsdateien können auch Kommentare enthalten, die mit den Zeichen ";" oder "#" beginnen. Neben *.ini, die XAML-Markup- oder Json-Dateien verwenden und das Speichern einer größeren Datenmenge in einer Datei ermöglichen, stehen nun auch neue Konfigurationsdateiformate zur Verfügung. Der MetaTrader verwendet jedoch nur *.ini-Dateien. WinApi unterstützt Funktionen für Operationen mit Konfigurationsdateien, die bei der Entwicklung einer Wrapperklasse für die komfortable Bedienung mit dem gewünschten Format verwendet wurden. Die verwendeten Funktionen und der Wrapper für die Arbeit mit den MetaTrader-Konfigurationsdateien. 

Die Funktionsvielfalt des gewünschten Add-ons und der verwendeten Technologien

Um mit dem Projekt arbeiten zu können, sollten Sie Visual Studio IDE (Integrierte Entwicklungsumgebung) installieren. Dieses Projekt wurde mit der Community 2019 Version erstellt. Während der Installation von Visual Studio sollten Sie auch .Net 4.6.1 installieren, das bei der Entwicklung dieses Add-ons verwendet wurde. Um den Lesern, die keine fundierten Kenntnisse von C# haben, beim Verständnis der Idee zu helfen, werde ich detaillierte Beschreibungen spezifischer Sprachprobleme und der Techniken, die ich bei der Programmierung verwendet habe, geben.

Da die bequemste Methode zur Erstellung einer grafischen Benutzeroberfläche die Verwendung der Sprache C# ist und das MetaTrader-Terminal eine bequeme Methode zur Anwendung dieser Sprache unterstützt, werden wir die bereitgestellten Möglichkeiten nutzen. Vor kurzem wurden auf dieser Website einige Artikel veröffentlicht, die sich auf die Erstellung von GUIs mit C# beziehen. Diese Artikel zeigen GUI-Erstellungsmethoden, die auf der Technologie von Win Forms basieren, und eine Verbindungs-DLL, die Grafiken mit Reflexionsmechanismen startet. Die vom Artikelautor verwendete Lösung ist gut genug, aber für den aktuellen Artikel habe ich mich für eine modernere GUI-Entwicklungsmethode entschieden: die Verwendung der WPF-Technologie. Dadurch ist es mir gelungen, die Verbindungsbibliothek zu vermeiden und gleichzeitig alles Notwendige in einer einzigen DLL zu implementieren. Um die Hauptaufgabe zu lösen, müssen wir den Typ des Projekts erstellen, wodurch wir grafische Objekte speichern können, die mit der WPF-Technologie beschrieben werden. Das Projekt sollte in die dynamische Bibliothek (*.dll-Datei) kompiliert werden, die dann in das Terminal geladen werden kann. Diese Projektart ist vorhanden: WpfCustomControlLibrary. Dieser Typ wurde speziell für die Erstellung von benutzerdefinierten Grafikobjekten entwickelt. Ein Beispiel dafür ist eine Bibliothek, die Diagramme darstellt. Wir werden diesen Typ für unseren speziellen Zweck verwenden, d.h. für die Erstellung eines Add-ons für das MetaTrader-Terminal. Um diesen Projekttyp zu erstellen, wählen Sie ihn aus der Liste der Projekte in IDEVisual Studio, wie im folgenden Screenshot gezeigt:

Nennen wir unser Projekt "OptimisationManagerExtention". Der Ordner Themes wird zunächst im Projekt erstellt. Es enthält eine (*.xaml) Datei "Generic.xaml": Diese Datei speichert die Stile, die Farben, Anfangsgrößen, Einrückungen und ähnliche Eigenschaften von Grafikobjekten festlegen. Wir werden diese Datei später benötigen, also lassen wir sie so, wie sie ist. Eine weitere, automatisch generierte Datei ist die mit der Klasse CustomControl1. Wir werden diese Datei nicht benötigen, also löschen wir sie. Da weitere Artikel auf Basis dieses Artikels geschrieben werden, müssen wir die Möglichkeit bieten, unser Add-on zu erweitern. Das bedeutet, dass wir die MVVM-Programmiervorlage verwenden müssen. Wenn Sie mit dem Muster nicht vertraut sind, lesen Sie bitte die Erklärung unter diesem Link. Um einen gut strukturierten Code zu implementieren, erstellen wir den Ordner "View" und fügen ihm unser Grafikfenster hinzu. Um das Grafikfenster zu erstellen, müssen wir das Element Window (WPF) zum erstellten Ordner hinzufügen (wie im folgenden Screenshot gezeigt):


Rufen wir das Fenster ExtentionGUI.xaml auf — das ist das grafische Element, das im obigen Fenster dargestellt wird. Betrachten wir die Namensräume. Wir haben das Projekt erstellt und es OptimisationManagerExtention genannt; danach hat Studio automatisch den Hauptnamensraum generiert: "OptimierungManagerErweiterung". In C# dienen die Namensräume, wie in vielen anderen Programmiersprachen, als Container, in denen unsere Objekte enthalten sind. Die Namensraum-Eigenschaften können durch das folgende Beispiel veranschaulicht werden: 

Die folgende Konstruktion ist falsch, da beide Klassen im gleichen Namensraum deklariert sind:

namespace MyNamespace
{
    class Class_A
    {
    }

    class Class_A
    {
    }
}

Die folgende Verwendung von Klassen ist zulässig, da sich beide Klassen trotz des gleichen Namens in unterschiedlichen Namensräumen befinden:

namespace MyFirstNamespace
{
    class Class_A
    {
    }
}

namespace MySecondNamespace
{
    class Class_A
    {
    }
}

 Es gibt auch sogenannte verschachtelte Namensräume. Wenn diese verwendet werden, enthält ein Namensraum eine Reihe weiterer Namensräume. In diesem Fall gilt auch der folgende Code:

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

Da diese Form der Aufnahme jedoch umständlich ist, unterstützt C# verkürzte Datensätze, die für die Darstellung leichter verständlich sind:

namespace MyNamespace
{
    class Class_A
    {
    }
}

namespace MyNamespace.First
{
    class Class_A
    {
    }
}

namespace MyNamespace.Second
{
    class Class_A
    {
    }
}

Die in den beiden vorherigen Beispielen vorgestellten Codevarianten sind identisch, aber die zweite ist leichter verständlich. Nachdem wir den View-Ordner erstellt haben, haben wir den verschachtelten Namensraum erstellt und so werden die dem View-Ordner hinzugefügten Objekte dem Namensraum "OptimisationManagerExtention.View" hinzugefügt. Dementsprechend hat unser Fenster auch diesen Namensraum. Um die Anwendung von Styles, die wir in der Datei Generic.xaml beschreiben, auf das gesamte Fenster zu ermöglichen, müssen wir das XAML-Markup für diese Datei bearbeiten. Zuerst müssen wir den Codeblock löschen, der mit dem Tag <Style> beginnt, weil wir ihn nicht benötigen. Zweitens müssen wir einen Link zum Namensraum unseres Fensters hinzufügen — dies geschieht über die Eigenschaft "xmlns:local". Als Ergebnis erhalten wir die folgenden Inhalte:

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

Um die Größe/Farbe oder andere Eigenschaften für unser Fenster festzulegen, müssen wir deren Stil beschreiben. Ich werde hier nicht die Details über die Schönheit der Anwendung hinzufügen, sondern nur das notwendige Minimum beschreiben. Sie können jedes beliebige Design, jede beliebige Animation oder andere Funktionen hinzufügen. Nach der Bearbeitung erhalten wir eine Datei, die Stile festlegt, während alle Stile automatisch auf alle Elemente des Fensters angewendet werden. Das ist doch sehr praktisch, nicht wahr?

<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">
    
    <!--Set the window background color-->
    <Style TargetType="{x:Type local:ExtentionGUI}">
        <Setter Property="Background" Value="WhiteSmoke"/>
    </Style>

    <!--
    Set the background color for the dividing strip, by dragging which 
    we change ranges of horizontally divided zones in the first tab 
    of our window
    -->
    <Style TargetType="GridSplitter">
        <Setter Property="Background" Value="Black"/>
    </Style>

    <!--Set the height of drop-down lists-->
    <Style TargetType="ComboBox">
        <Setter Property="Height" Value="22"/>
    </Style>

    <!--Set the height of calendars-->
    <Style TargetType="DatePicker">
        <Setter Property="Height" Value="22"/>
    </Style>

    <!--Set the height of text boxes-->
    <Style TargetType="TextBox">
        <Setter Property="Height" Value="22"/>
    </Style>

    <!--Set the height of buttons-->
    <Style TargetType="Button">
        <Setter Property="Height" Value="22"/>
    </Style>

</ResourceDictionary>


Damit die Stile auf das Fenster angewendet werden können, legen Sie den Link zu ihnen im XAML-Markup unseres Fensters fest: Nach dem Öffnungs-Tag <Window> geben Sie die folgende Konstruktion an, die den Pfad zur Datei mit Ressourcen relativ zur Fensterposition setzt. 

<!--Connect styles-->
<Window.Resources>
    <ResourceDictionary Source="../Themes/Generic.xaml"/>
</Window.Resources>

Erstellen Sie zusätzlich zum erstellten Verzeichnis View ein paar weitere Verzeichnisse:

Wie Sie vielleicht schon vermuten, wird die für die Grafik der Anwendung verantwortliche Ebene ausschließlich im XAML-Markup beschrieben, ohne die Sprache C# direkt zu verwenden. Nachdem wir die entsprechenden Verzeichnisse erstellt haben, haben wir 2 weitere verschachtelte Namensräume erstellt, die dem XAML-Markup unseres Fensters hinzugefügt werden sollten, um sie nutzen zu können. Lassen Sie uns auch die Klasse "ExtentionGUI_VM" im Namensraum OptimisationManagerExtention.ViewModel anlegen. Diese Klasse wird unser Verbindungsobjekt sein. Um jedoch benötigte Funktionen ausführen zu können, sollte sie von der Schnittstelle "INotifyPropertyChanged" geerbt werden. Es enthält das Ereignis PropertyChanged, über das der grafische Teil über eine Wertänderung eines der Felder und damit über die Notwendigkeit einer Aktualisierung der Grafik informiert wird. Die erstellte Datei sieht wie folgt aus:

/// <summary>
/// View Model
/// </summary>
class ExtentionGUI_VM : INotifyPropertyChanged
{
    /// <summary>
    /// The event of a change in any of the ViewModel properties 
    /// and its handlers
    /// </summary>
    #region PropertyChanged Event
    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// The PropertyChanged event handler
    /// </summary>
    /// <param name="propertyName">Updated variable name</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
}

Das XAML-Markup nach der Erstellung des Fensters und dem Hinzufügen aller Links sieht wie folgt aus:

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

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

    <Grid>
        

    </Grid>
</Window>


Die Hauptvorbereitungen für die Entwicklung der GUI für unsere Anwendung sind abgeschlossen und so können wir mit dem XAML-Markup unseres Fensters zur Erstellung der Grafikschicht fortfahren. Alle Steuerelemente werden in den <Grid/> Block geschrieben. Für diejenigen, die nicht genügend Erfahrung im Umgang mit dem XAML-Markup haben, empfehle ich, es direkt aus dem Studio zu öffnen und die Lesbarkeit zu überprüfen. Diejenigen, die mit diesem Tool vertraut sind, können die in diesem Artikel verfügbaren Codestücke verwenden. Vergleicht man die beiden GUI-Erstellungsmethoden (WinForms / WPF), so haben sie neben den offensichtlichen Unterschieden auch Gemeinsamkeiten. Denken Sie an Schnittstellen WinForms, in denen alle grafischen Elemente als Klasseninstanzen dargestellt und im versteckten Teil einer abstrakten Klasse (z.B. Button oder ComboBox) abgelegt werden.

So stellt sich heraus, dass die gesamte grafische Anwendung von WinForms aus einer Reihe von miteinander verbundenen Objektinstanzen besteht. Durch die Analyse des WPF-Markups ist es schwer vorstellbar, dass es auf dem gleichen Prinzip basiert. Jedes Markup-Element, z.B. das "Grid"-Tag, ist eigentlich eine Klasse, so dass Sie genau die gleiche Anwendung ohne XAML-Markup wiederherstellen können, während Sie nur Klassen aus dem entsprechenden Namensraum verwenden. Das wäre jedoch hässlich und sperrig. Tatsächlich geben wir durch das Öffnen des <Grid>-Tags an, dass wir die Klasseninstanz erstellen wollen. Dann analysieren die Compiler-Mechanismen das von uns spezifizierte Markup und erzeugen Instanzen von benötigten Objekten. Diese Eigenschaft von WPF-Anwendungen ermöglicht das Erstellen von benutzerdefinierten grafischen Objekten oder Objekten, die die Standardfunktionalität erweitern. Weiterhin werden wir prüfen, wie man zusätzliche Funktionen implementieren kann.    

Was den Prozess der Grafikerstellung betrifft, so ist zu beachten, dass <Grid/> ein Layoutblock ist, d.h. er ist für die bequeme Platzierung von Steuerelementen und anderen Designblöcken ausgelegt. Wie Sie im Video sehen können, bleibt beim Wechsel zwischen den Registerkarten Einstellungen und der Registerkarte Optimierungsergebnis der untere Teil (ProgressBar) unverändert. Dies wird erreicht, indem die Hauptblöcke <Grid/> in 2 Zeilen aufgeteilt werden, in denen das Panel mit den Hauptregisterkarten (TabControll) platziert wird, sowie ein weiterer Block <Grid/>, der die Statuszeile (Label), ProgressBar und den Optimierungsstart-Button enthält. Aber jetzt ist es horizontal in drei Spalten unterteilt, die jeweils eines der Steuerelemente enthalten ( Lable, ProgressBar, Button).

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

    <!--Create TabControl with two tabs-->
    <TabControl>
        <!--The tab with robot settings and optimization or single test launch options-->
        <TabItem Header="Settings">
           
        </TabItem>

        <!--Tab for viewing optimization results and launching a test upon a double-click event-->
        <TabItem Header="Optimisation Result">
          
        </TabItem>
    </TabControl>

    <!--Container with a progress bar, operation status and a launch button-->
    <Grid Grid.Row="1">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150"/>
            <ColumnDefinition/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <!--Status of a running operation-->
        <Label Content="{Binding Status, UpdateSourceTrigger=PropertyChanged}"/>
        <!--Progress bar-->
        <ProgressBar Grid.Column="1" 
                                     Minimum="0" 
                                     Maximum="100"
                                     Value="{Binding PB_Value, UpdateSourceTrigger=PropertyChanged}"/>
        <!--Start button-->
        <Button Margin="5,0,5,0" 
                                Grid.Column="2"
                                Content="Start"
                                Command="{Binding Start}"/>
    </Grid>
</Grid>

Betrachten wir auch die Eigenschaften, die zusammen mit diesen Steuerelementen verwendet wurden, nämlich wie Daten aus ViewModel in View übergeben werden. Für jedes der Felder, die Daten anzeigen oder Daten eingeben können, wird in der Klasse ExtentionGUI_VM (unser ViewMpodel-Objekt) ein eigenes Feld angelegt, das seinen Wert speichert. Beim Erstellen von WPF-Anwendungen, und insbesondere bei der Verwendung des MVVM-Musters, werden Grafikelemente in der Regel nicht direkt angesprochen, weshalb wir einen komfortableren Wertübergabeprozess verwenden, der ein Minimum an Code erfordert. Beispielsweise wird die Value-Eigenschaft für das Grafikelement ProgressBar mit Hilfe der Datenverknüpfungstechnologie eingestellt, was in der folgenden Zeile erfolgt:

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

Auf die Binding-Eigenschaft folgt der Name des Feldes, in dem die Daten gespeichert werden, während die Eigenschaft UpdateSourceTrigger die Methode zur Aktualisierung der Daten im Grafikelement angibt. Durch das Setzen dieser Eigenschaft mit dem Parameter PropertyChanged teilen wir der Anwendung mit, dass diese spezielle Eigenschaft dieses speziellen Elements nur dann aktualisiert werden muss, wenn das PropertyChanged-Ereignis in der Klasse ExtentionGUI_VM ausgelöst wurde und der Name der Variablen, mit der sie verknüpft war, als einer der Parameter dieses Ereignisses übergeben wurde, nämlich "PB_Value". Wie Sie am XAML-Markup sehen können, hat auch die Schaltfläche eine Datenverknüpfung, jedoch wird für die Verknüpfung der Schaltfläche die Command-Eigenschaft verwendet, die über die ICommand-Schnittstelle auf den Befehl (oder besser gesagt auf eine in der ViewModel-Klasse definierte Methode) zeigt, der auf das Klick-Ereignis der Schaltfläche aufgerufen wird. Dies ist die Verknüpfung des Klick-Ereignisses der Schaltfläche und anderer Ereignisse (z.B. ein Doppelklick auf die Optimierungsergebnistabelle). Nun sieht unser grafischer Teil wie folgt aus:


Der nächste Schritt zur GUI-Erstellung ist das Hinzufügen von Steuerelementen auf der Registerkarte OptimisationResults. Diese Registerkarte enthält zwei Comboboxen zur Auswahl des Terminals, in dem die Optimierung durchgeführt wurde, und des Expert Advisor, sowie die Schaltfläche Update Report. Diese Registerkarte enthält auch eine verschachtelte TabControl mit zwei verschachtelten Registerkarten, die jeweils eine Tabelle (ListView) mit den Optimierungsergebnissen enthalten. Hier ist das passende XAML-Markup:

  <!--Tab for viewing optimization results and launching a test upon a double-click event-->
            <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>
                    <!--Container with the optimization result tables-->
                    <TabControl 
                        TabStripPlacement="Bottom"
                        Grid.Row="1">
                        <!--A tab in which the historic optimization results are shown-->
                        <TabItem Header="Backtest">
                            <!--Table with optimization results-->
                            <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>
                        <!--A tab in which the results of forward optimization 
                    passes are shown-->
                        <TabItem Header="Forvard">
                            <!--Table with optimization results-->
                            <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>

Wie bereits erwähnt, ist jedes Tag, das im XAML-Markup verwendet wird, eine Klasse. Wir können auch unsere eigenen Klassen schreiben, die die Funktionsvielfalt von Standard-Markups erweitern oder benutzerdefinierte grafische Elemente erstellen. Im gegenwärtigen Stadium mussten wir die Funktionsweise des bestehenden Markups erweitern. Die Tabellen mit den Ergebnissen der Optimierungsdurchläufe sollten eine unterschiedliche Anzahl von Spalten und unterschiedliche Namen haben: Das wird unsere erste Erweiterung sein.

Die zweite Erweiterung ist die Umwandlung eines Doppelklicks in eine ICommand-Schnittstelle. Wir konnten die Notwendigkeit vermeiden, die zweite Erweiterung zu erstellen, wenn wir nicht die MVVM-Entwicklungsvorlage verwenden würden, nach der ViewModel und Model nicht mit der View-Schicht verbunden sein dürfen. Dies geschieht, um bei Bedarf eine einfache Änderung oder Neuschreibung der Grafikschicht der Anwendung zu ermöglichen. Wie aus den Methoden des Erweiterungsaufrufs ersichtlich ist, befinden sie sich alle im verschachtelten Namensraum von ViewExtention, gefolgt von einem Doppelpunkt und dem Namen der Klasse, die die Erweiterungen enthält. Nach dem Operator "point" folgt der Name der Eigenschaft, auf die wir den Wert setzen wollen.

Betrachten wir jede der Erweiterungen, beginnend mit derjenigen, die Klickereignisse in die ICommand-Schnittstelle umwandelt. Um eine Erweiterung zu erstellen, die Ereignisse mit Doppelklick verarbeitet, erstellen Sie Teilklasse ListViewExtention im Ordner ViewExtention. Der Teilzugriffsmodifikator zeigt an, dass die Klassenimplementierung auf mehrere Dateien aufgeteilt werden kann, während alle Methoden/Felder und andere Komponenten der Klasse, die als "teilweise" markiert ist, aber auf zwei oder mehr Dateien aufgeteilt ist, derselben Klasse angehören.

using System.Windows;

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

namespace OptimisationManagerExtention.ViewExtention
{
    /// <summary>
    /// The class of extensions for ListView, which translates events to commands (ICommand)
    /// the class is marked with keyword 'partial', i.e. its implementation is divided into several files.
    /// 
    /// In this class ListView.DoubleClickEvent is translated 
    /// into the ICommand type command
    /// </summary>
    partial class ListViewExtention
    {
        #region Command
        /// <summary>
        /// Dependent property - containing a reference to the command callback
        /// The property is set via View in the XAML markup of the project
        /// </summary>
        public static readonly DependencyProperty DoubleClickCommandProperty =
            DependencyProperty.RegisterAttached("DoubleClickCommand",
                typeof(ICommand), typeof(ListViewExtention),
                new PropertyMetadata(DoubleClickCommandPropertyCallback));

        /// <summary>
        /// Setter for DoubleClickCommandProperty
        /// </summary>
        /// <param name="obj">Control</param>
        /// <param name="value">The value to link with</param>
        public static void SetDoubleClickCommand(UIElement obj, ICommand value)
        {
            obj.SetValue(DoubleClickCommandProperty, value);
        }
        /// <summary>
        /// Getter for DoubleClickCommandProperty
        /// </summary>
        /// <param name="obj">Control</param>
        /// <returns>a link to the saved command of type ICommand</returns>
        public static ICommand GetDoubleClickCommand(UIElement obj)
        {
            return (ICommand)obj.GetValue(DoubleClickCommandProperty);
        }
        /// <summary>
        /// Callback which is called after setting property DoubleClickCommandProperty
        /// </summary>
        /// <param name="obj">Control for which the property</param>
        /// <param name="args">events preceding callback</param>
        private static void DoubleClickCommandPropertyCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            if (obj is ListView lw)
            {
                if (args.OldValue != null)
                    lw.MouseDoubleClick -= Lw_MouseDoubleClick;

                if (args.NewValue != null)
                    lw.MouseDoubleClick += Lw_MouseDoubleClick;
            }
        }
        /// <summary>
        /// Callback of the event which is translated to the ICommand type
        /// </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>
        /// Dependent property - containing a reference to parameters passed to the callback of type ICommand
        /// The property is set via View in the XAML markup of the project
        /// </summary>
        public static readonly DependencyProperty DoubleClickCommandParameterProperty =
            DependencyProperty.RegisterAttached("DoubleClickCommandParameter",
                typeof(object), typeof(ListViewExtention));
        /// <summary>
        /// Setter for DoubleClickCommandParameterProperty
        /// </summary>
        /// <param name="obj">Control</param>
        /// <param name="value">The value to link with</param>
        public static void SetDoubleClickCommandParameter(UIElement obj, object value)
        {
            obj.SetValue(DoubleClickCommandParameterProperty, value);
        }
        /// <summary>
        /// Getter for DoubleClickCommandParameterProperty
        /// </summary>
        /// <param name="obj">Control</param>
        /// <returns>passed parameter</returns>
        public static object GetDoubleClickCommandParameter(UIElement obj)
        {
            return obj.GetValue(DoubleClickCommandParameterProperty);
        }
        #endregion
    }
}


Jede Eigenschaft jeder Klasse aus WPF-Grafikobjekten ist mit der Klasse DependancyProperty verknüpft. Diese Klasse ermöglicht die Datenbindung zwischen den Ebenen View und ViewModel. Um die Klasseninstanz zu erzeugen, verwenden Sie die Methode DependencyProperty.RegisterAttached, die die konfigurierte Klasse DependencyProperty zurückgibt. Das Verfahren akzeptiert 4 Parameter. Für Details siehe hier. Beachten Sie, dass die erstellte Eigenschaft die Zugriffsmodifikatoren 'public static readonly' haben muss (d.h. Zugriff von außerhalb der Klasse, Möglichkeit, diese Eigenschaft aufzurufen, ohne eine Klasseninstanz erstellen zu müssen, während der Modifikator 'static' die Einheit dieser Eigenschaft innerhalb dieser spezifischen Anwendung setzt und 'readonly' die Eigenschaft unveränderlich macht).

  1. Der erste Parameter legt den Namen fest, unter dem die Eigenschaft im XAML-Markup sichtbar ist.
  2. Der zweite Parameter legt den Typ des Elements fest, mit dem die Bindung durchgeführt wird. Die Objekte dieses Typs werden in der erstellten Instanz der Klasse DependancyProperty gespeichert. 
  3. Der dritte Parameter legt den Typ der Klasse fest, in der sich die Eigenschaft befindet. In unserem Fall ist die Klasse ListViewExtention.
  4. Der letzte Parameter akzeptiert die Instanz der Klasse PropertyMetadata — dieser Parameter bezieht sich auf den Behandlung des Ereignisses, die nach dem Anlegen der Instanz der Klasse DependancyProperty aufgerufen wird. Dieser Rückruf wird benötigt, um das Doppelklick-Ereignis zu abonnieren.

Um Werte dieser Eigenschaft korrekt setzen und abrufen zu können, lassen erstellen wir Methoden mit den Namen, die aus dem Namen bestehen, der bei der Instanzerstellung der DependancyProperty-Klasse übergeben wurde, und dem Präfix Set (um Werte zu setzen oder Get für den Abruf der Werte). Beide Methoden müssen 'static' sein. Im Wesentlichen kapseln sie die Verwendung bereits vorhandener Methoden SetValue und GetValue.

Der Rückruf des Ereignisses im Zusammenhang mit dem Abschluss der Erstellung von abhängigen Eigenschaften, realisiert das Abonnement des Ereignisses eines Doppelklicks auf eine Tabellenzeile und die Abmeldung vom zuvor abonnierten Ereignis, falls vorhanden. Innerhalb der Behandlung von Doppelklick-Ereignissen werden die Methoden CanExecute und Execute aus dem ICommand-Feld, das an View übergeben wird, sequentiell aufgerufen. Wenn das Ereignis eines Doppelklicks auf eine der Zeilen der abonnierten Tabelle auslöst, rufen wir automatisch die Ereignisbehandlung auf, der Aufrufe der Methoden der Logik enthält, die nach dem Auftreten dieses Ereignisses ausgeführt werden.

Die angelegte Klasse ist eigentlich eine Zwischenklasse. Sie verarbeitet Ereignisse und ruft Methoden aus ViewModel auf, führt aber keine Geschäftslogik aus. Dieser Ansatz mag verwirrender erscheinen als ein direkter Aufruf einer Methode aus einem Doppelklick-Event-Handler (wie in WinForms implementiert), aber es gibt Gründe, diesen Ansatz zu verwenden: Wir müssen das MVVM-Muster beobachten, das besagt, dass View nichts über ViewModel wissen sollte und umgekehrt.

Durch die Verwendung der Zwischenklasse reduzieren wir die Konnektivität zwischen den Klassen, für die wir das erwähnte Programmiermuster verwenden. Jetzt können wir die Klasse ViewModel bearbeiten. Es ist jedoch notwendig, einen bestimmten Eigenschaftstyp von ICommand anzugeben, auf die die Zwischenklasse zugreifen wird.

Die Erweiterung enthält auch eine Implementierung der Eigenschaft, die das Ereignis SelectionChanged in ICommand umwandelt, sowie eine Zwischenklasse, die automatisch Spalten für die Tabellen basierend auf der Datei Binded erzeugt, das die Sammlung von Spaltennamen speichert. Diese beiden Erweiterungen von XAML-Markup sind wie oben beschrieben implementiert, daher werde ich nicht weiter darauf eingehen. Wenn Sie Fragen haben, stellen Sie diese bitte im Kommentarteil zu diesem Artikel. Nachdem wir nun die Registerkarte Markup Optimierungsergebnis implementiert haben, sieht unser Fenster wie folgt aus:


Der nächste Schritt ist die Implementierung der Registerkarte Settings (Einstellungen). Der Einfachheit halber werde ich hier nur den Teil zeigen, der die grundlegenden Grafikobjekte beschreibt, anstatt die Vollversion des XAML-Markups für diese Registerkarte hinzuzufügen. Der vollständige Code ist unten angehängt.

<!--The tab with robot settings and optimization or single test launch options-->
            <TabItem Header="Settings">
                <!--Container with settings and other items-->
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition Height="200"/>
                    </Grid.RowDefinitions>

                    <!--Container with the list of selected terminals-->
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="30"/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <!--Container with the selection of terminals which are determined automatically-->
                        <WrapPanel HorizontalAlignment="Right" 
                                       VerticalAlignment="Center">
                            <!--List with terminals-->
                            <ComboBox Width="200" 
                                          ItemsSource="{Binding TerminalsID}"
                                          SelectedIndex="{Binding SelectedTerminal, UpdateSourceTrigger=PropertyChanged}"
                                          IsEnabled="{Binding IsTerminalsLVEnabled, UpdateSourceTrigger=PropertyChanged}"/>
                            <!--Terminal adding button-->
                            <Button Content="Add" Margin="5,0"
                                    Command="{Binding AddTerminal}"
                                    IsEnabled="{Binding IsTerminalsLVEnabled, UpdateSourceTrigger=PropertyChanged}"/>
                        </WrapPanel>
                        <!--List of selected terminals-->
                        <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>
                    <!--Container with parameters for editing and 
                    optimization settings-->
                    <TabControl
                                Grid.Row="2" 
                                Margin="0,0,0,5"
                                TabStripPlacement="Right">
                        <!--Robot parameters tab-->
                        <TabItem Header="Bot params" >
                            <!--List with robot parameters-->
                            <ListView 
                                    ItemsSource="{Binding BotParams, UpdateSourceTrigger=PropertyChanged}">
                                <ListView.View>
                                    <GridView>
                                    .
                                    .
                                    .
                                    </GridView>
                                </ListView.View>
                            </ListView>
                        </TabItem>
                        <!--Optimization settings tab-->
                        <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 seen by the robot-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center">
                                    <Label Content="Login:"/>
                                    <TextBox Text="{Binding TestLogin, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Execution type-->
                                <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>
                                <!--Type of history passing for tests-->
                                <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>
                                <!--Optimization criteria-->
                                <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>
                                <!--Forward period start date-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="1"
                                            Grid.Row="0">
                                    <Label Content="Forward date:"/>
                                    <DatePicker SelectedDate="{Binding ForvardDate, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Deposit-->
                                <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>
                                <!--Profit calculation currency-->
                                <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>
                                <!--Leverage-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="1"
                                            Grid.Row="2">
                                    <Label Content="Leverage:"/>
                                    <ComboBox DataContext="{Binding LaverageList}"
                                                  ItemsSource="{Binding ItemSource}"
                                                  SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Whether to use test visualizer-->
                                <CheckBox Content="Visual mode"
                                              Margin="2"
                                              VerticalAlignment="Center"
                                              Grid.Column="2"
                                              Grid.Row="0"
                                              IsChecked="{Binding IsVisual, UpdateSourceTrigger=PropertyChanged}"/>
                            </Grid>
                        </TabItem>
                    </TabControl>

                    <!--Separator line which allows resizing 
                    one area relative to the other one-->
                    <GridSplitter Height="3" VerticalAlignment="Bottom" HorizontalAlignment="Stretch"/>

                </Grid>
            </TabItem>

Lassen Sie uns zunächst die Implementierung von dynamisch editierbaren Bereichen betrachten. Dieses Formularverhalten wird implementiert, indem zwei Zeilen im Hauptmenü <Grid/> bildet und das Element <GridSplitter/> hinzugefügt wird. Um die Größe des Bereichs mit der Liste der Terminals und des Bereichs mit anderen Tabellen zu ändern, ziehen wir es. Fügen Sie in die erste Zeile der generierten Tabelle das neue <Grid/> ein, das wir wiederum in 2 Teile teilen. Der erste Teil enthält ein weiteres Layoutelement — WrapPanel, das die Liste der Terminals und eine Schaltfläche zum Hinzufügen eines neuen Terminals enthält. Der zweite Teil enthält eine Tabelle mit der Liste der hinzugefügten Terminals.

Die Tabelle enthält neben dem Text auch Steuerelemente, mit denen die Daten in der Tabelle geändert werden können. Dank der Datenbindungstechnologie zum Ändern/Ergänzen von Werten in die Tabelle müssen wir keinen zusätzlichen Code schreiben, da die Tabelle direkt mit einer Sammlung von Steuerdaten verknüpft ist. Der untere Teil des editierbaren <Grid/>-Blocks enthält TabControl, der die Testereinstellungen und eine Tabelle mit der Liste der Roboterparameter enthält.

Daher haben wir die Generierung der grafischen Oberfläche für diese Erweiterung durchgeführt. Bevor wir mit der Beschreibung von ViewModel fortfahren, lassen Sie uns die Tabellenbindungsmethode betrachten.

Hier ist die Beschreibung am Beispiel einer Tabelle mit Roboterparametern, die die folgenden Felder haben sollte:

Um alle diese Parameter an die Tabelle zu übergeben, müssen wir eine Speicherklasse anlegen, in der die Daten der Tabellenzeilen gespeichert werden. Mit anderen Worten, diese Klasse sollte alle Tabellenspalten beschreiben, und die Sammlung dieser Klassen speichert die gesamte Tabelle. Die folgende Klasse wurde für unsere Tabelle erstellt:

/// <summary>
/// The class describing rows for the table with the robot parameter settings before optimization
/// </summary>
class ParamsItem
{
    /// <summary>
    /// Class constructor
    /// </summary>
    /// <param name="Name">The name of the variable</param>
    public ParamsItem(string Name) => Variable = Name;
    /// <summary>
    /// The flag showing whether this robot variable needs to be optimized
    /// </summary>
    public bool IsOptimize { get; set; }
    /// <summary>
    /// Variable name
    /// </summary>
    public string Variable { get; }
    /// <summary>
    /// The value of the variable selected for the test
    /// </summary>
    public string Value { get; set; }
    /// <summary>
    /// Parameters enumeration start
    /// </summary>
    public string Start { get; set; }
    /// <summary>
    /// Parameters enumeration step
    /// </summary>
    public string Step { get; set; }
    /// <summary>
    /// Parameters enumeration end
    /// </summary>
    public string Stop { get; set; }
}

Jede Eigenschaft dieser Klasse enthält Informationen zu einer bestimmten Spalte. Lassen Sie uns nun sehen, wie sich der Datenkontext verändert. Beim Erstellen des Anwendungsfensters haben wir gleich zu Beginn darauf hingewiesen, dass die Datenquelle für das Fenster die Klasse ExtentionGUI_VM sein würde, die der Hauptdatenkontext für dieses Fenster ist und die die Sammlung enthalten sollte, der die Tabelle zugeordnet ist. Für jede bestimmte Zeile dieser spezifischen Tabelle wird jedoch der Datenkontext von der Klasse ExtentionGUI_VM auf ParamsItem geändert. Dies ist ein wichtiger Punkt, wenn Sie also eine Zelle dieser Tabelle aus dem Programmcode aktualisieren müssen, dann müssen Sie das Ereignis PropertyChanged nicht in der Klasse ExtentionGUI_VM, sondern in der Kontextklasse dieser bestimmten Zeile aufrufen.

Damit haben wir die Beschreibung des Prozesses zur Erstellung der grafischen Ebene abgeschlossen und können mit der Beschreibung der Klasse fortfahren, die die Anwendung mit der Programmlogik verbindet.


ViewModel und eine Verbindung zwischen MetaTrader und der implementierten DLL.

Die nächste Komponente des Programms ist der Teil, der für die Verbindung der oben genannten Grafiken und der Logik verantwortlich ist, die als nächstes besprochen werden wird. In der verwendeten Programmiervorlage (Model View ViewModel oder MVVM) heißt dieser Teil ViewModel und befindet sich im entsprechenden Namensraum (OptimisationManagerExtention.ViewModel).

Im ersten Kapitel dieses Artikels haben wir bereits die Klasse ExtentionGUI_VM erstellt und die Schnittstelle INotifyPropertyChanged implementiert — diese Klasse verbindet die Grafik und Logik. Bitte beachten Sie, dass alle Felder der Klasse ExtentionGUI_VM, mit denen Daten aus der View verknüpft sind, als Property und nicht als Variablen deklariert werden müssen. Wenn Sie mit diesem Sprachkonstrukt C# nicht gut vertraut sind, lesen Sie bitte den folgenden Code mit Erläuterungen:

class A
{
    /// <summary>
    /// This is a simple public field to which you can set values or read values from it 
    /// But there is no possibility to perform a check or other actions.
    /// </summary>
    public int MyField = 5;
    /// <summary>
    /// This property allows processing data before reading or writing
    /// </summary>
    public int MyGetSetProperty
    {
        get
        {
            MyField++;
            return MyField;
        }
        set
        {
            MyField = value;
        }
    }

    // This is a read-only property
    public int GetOnlyProperty => MyField;
    /// <summary>
    // This is a write-only property
    /// </summary>
    public int SetOnlyProperty
    {
        set
        {
            if (value != MyField)
                MyField = value;
        }
    }
}

Wie Sie am Beispiel sehen können, sind Eigenschaften eine Art Hybrid aus Methoden und Feldern. Sie erlauben es, spezifische Aktionen durchzuführen, bevor sie den Wert zurückgeben oder die aufgezeichneten Daten verifizieren. Auch Eigenschaften können nur zum Lesen oder nur zum Schreiben sein. Wir haben bei der Implementierung der Datenbindung auf diese C#-Konstrukte in View verwiesen.

Bei der Implementierung der Klasse ExtentionGUI_VM habe ich sie in Blöcke aufgeteilt (konstruiert #region #endregion). In View haben wir mit der Erstellung des Optimierungsergebnisses begonnen und betrachten nun Eigenschaften und Methoden zur Erstellung dieser Registerkarte. Der Einfachheit halber werde ich zunächst den Code angeben, der für die auf dieser Registerkarte angezeigten Daten verantwortlich ist, und danach werde ich Erklärungen hinzufügen.

#region Optimisation Result

/// <summary>
/// Table with historical optimization results
/// </summary>
public DataTable HistoryOptimisationResults => model.HistoryOptimisationResults;
/// <summary>
/// Table with forward optimization results
/// </summary>
public DataTable ForvardOptimisationResults => model.ForvardOptimisationResults;
/// <summary>
/// Observable collection with a list of optimization columns
/// </summary>
public ObservableCollection<ColumnDescriptor> OptimisationResultsColumnHeadders =>
       model.OptimisationResultsColumnHeadders;

#region Start test from optimisation results
/// <summary>
/// Run the test for the selected optimization process
/// </summary>
public ICommand StartTestFromOptimisationResults { get; }
/// <summary>
/// The method that starts a test upon a double-click
/// </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>
/// Index of the selected row from the historical optimization table
/// </summary>
public int SelectedHistoryOptimisationRow { get; set; } = 0;
/// <summary>
/// Index of the selected row from the forward optimization
/// </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

Betrachten wir die Datenquellen für die Optimierungstabellen historisch und vorwärts sowie die Liste der Spalten, die über die Zwischenklasse (GridViewColumns) mit den Spalten der beiden Tabellen verbunden ist. Jede Tabelle hat zwei eindeutige Felder: die Datenquelle (Typ durch die DataTable) und die Eigenschaft, die den Index der ausgewählten Zeile in der Tabelle enthält. Der Index der ausgewählten Tabellenzeile ist für die Darstellung nicht wichtig, aber wir benötigen ihn für weitere Aktionen, wie z.B. den Start von Testläufen durch einen Doppelklick auf die Tabellenzeile. Das Laden von Daten in Tabellen und das Löschen dieser Daten wird durch die Programmlogik implementiert; und nach den OOP-Prinzipien sollte eine bestimmte Klasse für eine bestimmte Aufgabe verantwortlich sein, dann verweisen wir in den Eigenschaften, die Daten über die Tabellenzusammensetzung liefern, einfach auf die entsprechenden Eigenschaften aus der Hauptklasse des Modells (ExtemtionGUI_M). Die Verfolgung der ausgewählten Indizes erfolgt automatisch durch Mausklicks auf die Tabellenfelder, so dass diese Eigenschaften keine Aktionen oder Prüfungen durchführen. Sie ähneln den Klassenfeldern.

Beachten Sie auch den verwendeten Datentyp für die Eigenschaft, die die Liste der Spalten enthält (OptimisationResultsColumnHeadders) — ObservableCollection<T>. Dies ist eine der Standard-C#-Klassen, die dynamisch veränderbare Kollektionen speichert. Im Gegensatz zu Listen (List<T>) enthält diese Klasse jedoch das Ereignis CollectionChanged, das jedes Mal aufgerufen wird, wenn Daten in der Sammlung geändert, gelöscht oder hinzugefügt werden. Nachdem wir mit dieser Klasse einen Eigenschaftstyp erstellt haben, erhalten wir eine automatische Benachrichtigung von View über eine Änderung in der Datenquelle. Dadurch entfällt die Notwendigkeit, die Grafiken manuell über die Notwendigkeit zu informieren, die angezeigten Daten neu zu schreiben. 

Beachten Sie nun die Auswahllisten mit der Auswahl von Terminals und Robotern, sowie die Implementierung der Ereignisbehandlung von Tastendruck und Tabellenklick. Der Block für die Arbeit mit Auswahllisten und das Laden der Optimierungsergebnisse befindet sich im Bereich #region UpdateOptimisationReport. Betrachten Sie zunächst eine Datenquelle für die erste Dropdown-Liste, die eine Liste von Terminals enthält. Dies ist die Liste der Terminal-IDs, für die eine Optimierung durchgeführt wurde, und der Index der ausgewählten Terminals. Die Liste der Terminals wird vom Modell erstellt, daher können wir uns einfach auf das entsprechende Feld im Modell beziehen. Die Auswahl des ausgewählten Terminalindexes ist eine etwas komplexere Aufgabe. Nutzen wir den Vorteil der Eigenschaften gegenüber den bereits erwähnten Feldern. Nach Auswahl eines Terminals aus der Auswahlliste wird der TerminalsAfterOptimisation_Selected zum Eintragen der Eigenschaften aufgerufen, in dem die folgenden Aktionen ausgeführt werden:

  1. Speichern eines ausgewählten Index im Modell
  2. Aktualisierung des Wertes der zweiten Auswahlliste, die die Liste der Roboter speichert, die in diesem Terminal optimiert wurden.

Die Erweiterung speichert die Historie der durchgeführten Tests und gruppiert sie nach Robotern und Terminals. Wenn Sie den gleichen Roboter im gleichen Terminal neu optimieren, wird die bisherige Historie neu geschrieben. Diese Methode zur Übergabe von Ereignissen von View an ViewModel ist die bequemste. Es ist jedoch nicht immer geeignet.

Die nächste Methode zur Übergabe von Ereignissen von der Grafikschicht an ViewModel ist die Verwendung von Befehlen. Einige grafische Elemente wie z.B. Schaltflächen unterstützen Befehle. Wenn Sie Befehle verwenden, verknüpfen wir die Eigenschaft 'command' mit einer Eigenschaft aus ViewModel durch einen parametrisierten ICommand-Typ. Die ICommand-Schnittstelle ist eine der Standardschnittstellen der C#-Sprache und sieht so aus:

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

Wenn auf eine Schaltfläche geklickt wird, wird zuerst das Ereignis ConExecute ausgelöst, und wenn es false zurückgibt, wird die Schaltfläche unzugänglich, andernfalls wird die Methode Execute aufgerufen, die die gewünschte Operation ausführt. Wir müssen diese Schnittstelle implementieren, um diese Funktionalität nutzen zu können. Ich habe bei der Implementierung der Schnittstelle nichts Neues erfunden und einfach ihre Standardimplementierung verwendet.

/// <summary>
/// Implementation of the ICommand interface, used for
/// binding commands with methods from ViewModel
/// </summary>
class RelayCommand : ICommand
{
    #region Fields 
    /// <summary>
    /// Delegate directly performing the action
    /// </summary>
    readonly Action<object> _execute;
    /// <summary>
    /// Delegate checking for the possibility of performing an action
    /// </summary>
    readonly Predicate<object> _canExecute;
    #endregion // Fields

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="execute">The method passed for the delegate, which is a callback</param>
    public RelayCommand(Action<object> execute) : this(execute, null) { }
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="execute">
    /// The method passed for the delegate, which is a callback
    /// </param>
    /// <param name="canExecute">
    /// The method passed for the delegate, which checks the possibilities to perform an action
    /// </param>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");
        _execute = execute; _canExecute = canExecute;
    }

    /// <summary>
    /// Checking the possibility to perform an action
    /// </summary>
    /// <param name="parameter">parameter passed from View</param>
    /// <returns></returns>
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }
    /// <summary>
    /// Event - called whenever the callback execution ability changes.
    /// When this event is triggered, the form calls the "CanExecute" method again
    /// The event is triggered from ViewModel when needed
    /// </summary>
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
    /// <summary>
    /// The method calling a delegate which performs the action
    /// </summary>
    /// <param name="parameter">parameter passed from View</param>
    public void Execute(object parameter) { _execute(parameter); }
}

Gemäß dieser ICommand-Schnittstellenimplementierung werden zwei private, schreibgeschützte Felder angelegt, die delegates speichern, die wiederum Methoden speichern, die ihnen durch eine der Überladungen des Klassenkonstruktors RelayCommand übergeben wurden. Um diesen Mechanismus zu nutzen, erstellen wir Sie die Klasseninstanz RelayCommand im Klassenkonstruktor ExtentionGUI_VM. Übergeben Sie an diese Instanz eine Methode, die einige Aktionen ausführt. Die Eigenschaft UpdateOptimisationReport, die Informationen in Optimierungstabellen aktualisiert, sieht wie folgt aus:

UpdateOptimisationReport = new RelayCommand(UpdateReportsData);

Hier ist UpdateReportsData die 'private' Methode aus der Klasse ExtentionGUI_VM, die die Methode LoadOptimisations() aus der Klasse ExtentionGUI_M (d.h. aus unserer Modellklasse) aufruft. Ebenso ist die Eigenschaft StartTestFromOptimizationResults mit dem Ereignis eines Doppelklicks auf die vom Benutzer ausgewählte Tabellenzeile verknüpft. In diesem Fall wird das Doppelklick-Ereignis jedoch nicht über die Standardeigenschaft (wie in einer Schaltfläche, der Button-Klasse), sondern über die zuvor beschriebene und implementierte Lösung "ListViewExtention.DoubleClickCommand" übergeben. Wie aus der Signatur der Methoden Execute und CanExecute ersichtlich ist, können sie den Wert vom Typ 'Object' akzeptieren. Im Falle der Schaltfläche übergeben wir keine Werte, im Falle des Doppelklick-Ereignisses übergeben wir den Tabellennamen: Sie können ihn von der Bindungsmethode mit diesen Eigenschaften im XAML-Markup sehen:    

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

Basierend auf diesem Parameter versteht unser Modell, aus welcher Tabelle es Daten zur Durchführung des Optimierungs-Testdurchlaufs übernehmen soll.

Betrachten wir nun die Implementierung von Properties und Callbacks für die Arbeit mit der Registerkarte Settings, in der sich die wichtigsten Controls befinden. Beginnen wir mit der Implementierung der Datenquelle für die Tabelle der ausgewählten Terminals.

#region SelectedTerminalsForOptimisation && SelectedTerminalIndex (first LV params)
/// <summary>
/// The list of terminals selected for optimization, which is displayed in the terminals table
/// </summary>
public ObservableCollection<TerminalAndBotItem> SelectedTerminalsForOptimisation { get; private set; } =
    new ObservableCollection<TerminalAndBotItem>();
/// <summary>
/// The index of the selected row
/// </summary>
private int selectedTerminalIndex = 0;
public int SelectedTerminalIndex
{
    get { return selectedTerminalIndex; }
    set
    {
        // Assign the value of the newly selected index
        selectedTerminalIndex = value;

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

        // Fill in the list of parameters of the robot selected in the current row
        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

Die Liste der Terminals wird als eine beobachtete Kollektion dargestellt, die von der Klasse TerminalAndBotItem typisiert wird. Die Sammlung wird in der Klasse ViewModel gespeichert. Das ViewModel enthält auch eine Eigenschaft zum Setzen und Abrufen des Index der ausgewählten Zeile: Dies geschieht, um auf ein Terminalauswahlverfahren reagieren zu können. Wie im Video gezeigt, werden beim Anklicken einer Zeile die ausgewählten Roboterparameter dynamisch geladen. Dieses Verhalten ist in SelectedTerminalIndex, das die Eigenschaften einträgt, implementiert.

Denken Sie auch daran, dass die Zeilen in der Tabelle mit den ausgewählten Terminals Steuerelemente enthalten, und entsprechend müssen wir das TerminalAndBotItem als Datenkontextklasse organisieren.

Zuerst löschen wir das Terminal aus der Liste der Terminals. Wie bereits erwähnt, werden die Daten für die Tabelle in ViewModel gespeichert, während der Rückruf für die Schaltfläche Delete in der Tabelle nur an den Kontext der Zeilendaten gebunden werden kann, d.h. an die Klasse TerminalAndBotItem, aus der diese Kollektion nicht zugänglich ist. Die Lösung in diesem Fall ist die Verwendung von Delegierten. Ich habe in der ExtentionGUI_VM eine Methode zum Löschen von Daten implementiert und diese dann über einen Konstruktor als Delegierter an die Klasse TerminalAndBotItem übergeben. Aus Gründen der Übersichtlichkeit habe ich im folgenden Code alle Extrazeilen gelöscht. Die Übergabe einer Methode zum Löschen von sich selbst von außen sieht wie folgt aus.

class TerminalAndBotItem
{
    
    public TerminalAndBotItem(List<string> botList,
        string TerminalID,
        Action<string, string> FillInBotParams,
        Action<TerminalAndBotItem> DeleteCommand)
    {
        // Fill in the delegate fields
        #region Delegates
        this.FillInBotParams = FillInBotParams;
        this.DeleteCommand = new RelayCommand((object o) => DeleteCommand(this));
        #endregion
    }

    #region Delegates
    /// <summary>
    /// Field with the delegate to update selected robot parameters
    /// </summary>
    private readonly Action<string, string> FillInBotParams;
    /// <summary>
    /// Callback for a command to delete a terminal from the list (Delete button in the table)
    /// </summary>
    public ICommand DeleteCommand { get; }
    #endregion

    /// <summary>
    /// index of the selected EA
    /// </summary>
    private int selectedExpert;
    /// <summary>
    /// Property for the index of the selected EA
    /// </summary>
    public int SelectedExpert
    {
        get { return selectedExpert; }
        set
        {
            selectedExpert = value;
            // Run the callback to load parameters for the selected EA 
            if (Experts.Count > 0)
                FillInBotParams(Experts[selectedExpert], TerminalID);
        }
    }
}

Wie aus diesem Fragment ersichtlich ist, wurde bei der Umsetzung dieser Aufgabe eine weitere C#-Sprachkonstruktion verwendet: Lambda-Ausdrücke. Wenn Sie mit C++ oder C# vertraut sind, wird dieser Codeteil nicht seltsam vorkommen. Lambda-Ausdrücke können als gleiche Funktionen betrachtet werden, aber der Hauptunterschied zu ihnen besteht darin, dass sie keine traditionelle Deklaration haben. Diese Konstruktionen sind in C# weit verbreitet und Sie können hier mehr über sie lesen. Der Rückruf erfolgt über ICommand. Der nächste interessante Punkt in der Klassenimplementierung ist die Aktualisierung der Roboterparameter bei der Auswahl eines neuen Roboters aus der Dropdown-Liste aller Roboter. Die Methode, die die Roboterparameter aktualisiert, befindet sich im Modell, während die Implementierung dieses Methoden-Wrappers für ViewModel innerhalb von ViewModel erfolgt (die Methode zum Löschen von Terminals ist ebenfalls vorhanden). Auch hier verwenden wir Delegierte, aber anstatt ICommand zu verwenden, platzieren Sie die Antwort auf ein neues Roboterauswahlereignis in SelectedExpert, das die Eigenschaften einträgt.

Die Methode zum Aktualisieren von EA-Parametern hat auch spezifische Merkmale, nämlich: Sie ist asynchron.

private readonly object botParams_locker = new object();
/// <summary>
/// Get and fill robot parameters
/// </summary>
/// <param name="fullExpertName"> Full EA name in relation to folder ~/Experts</param>
/// <param name="Terminal">ID of the 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");
}

C# hat ein einfach zu schreibendes asynchrones Programmiermodell: Async Await, die wir in diesem Fall verwendet haben. Der präsentierte Codeausschnitt startet eine asynchrone Operation und wartet dann auf den Abschluss der Implementierung. Nach Abschluss der Operation wird das Ereignis Onpropertychanged aufgerufen, das View über eine Änderung in der Tabelle mit einer Liste von Roboterparametern benachrichtigt. Um das spezifische Merkmal zu verstehen, betrachten wir ein Beispiel für eine asynchrone Anwendung der Technologie 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}");
        }

    }

Der Zweck dieser einfachen Konsolenanwendung ist es, das Verhalten von Threads zu demonstrieren und eine kurze Erklärung der Asynchronität zu geben. In der Main-Methode zeigen wir zuerst die ID des Threads an, in dem die Main-Methode läuft, und dann starten wir die asynchrone Methode und zeigen ID des Main-Threads erneut an. In der asynchronen Methode zeigen wir wieder ID des Threads an, in dem diese Methode läuft, und drucken dann nacheinander die IDs von asynchronen Threads und die ID des Threads, in dem Operationen ausgeführt werden, nachdem der asynchrone Thread gestartet wurde. Der interessanteste Ausdruck dieses Programms:

Main before 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

Wie oben zu sehen ist, haben der Hauptthread und der allererste Ausdruck der asynchronen Method() die gleichen IDs. Das bedeutet, dass Method() nicht vollständig asynchron ist. Die Asynchronität dieser Methode beginnt nach dem Aufruf der asynchronen Operation mit der 'static' Methode Task.Run(). Wenn die Method() vollständig synchron wäre, würde das nächste Ereignis, das erneut die ID des Hauptthreads anzeigt, nach der Ausgabe der nächsten vier Nachrichten aufgerufen. 

Betrachten wir nun die asynchronen Ausgaben. Die erste asynchrone Ausgabe liefert ID = 3, was zu erwarten war. Aber die nächste Operation wartet auf den Abschluss der asynchronen Operation (durch die Verwendung von 'await') und gibt auch ID = 3 zurück. Das gleiche Bild wird bei der zweiten asynchronen Operation beobachtet. Auch trotz der Verzögerung von 100 Millisekunden, die nach der Ausgabe der nach dem ersten asynchronen Betrieb verwendeten Thread-ID hinzugefügt wurde, ändert sich die Reihenfolge nicht, obwohl die zweite Operation auf dem anderen Thread beginnt, der unabhängig vom ersten ist.

Dies sind die spezifischen Merkmale des Modells Async Await und der Asynchronität im Allgemeinen. Alle Aktionen in unserer Methode werden im Zusammenhang mit dem Sekundär-Thread durchgeführt und es besteht die Möglichkeit, dass die Methode zweimal aufgerufen wird, was zu einem Fehler führen kann. Zu diesem Zweck wird das Konstrukt lock(locker_object){} verwendet. Dieses Design erstellt ähnlich wie im Beispiel eine Warteschlange für die Ausführung von Aufrufen. Im Gegensatz zum Testbeispiel, bei dem die Warteschlange unabhängig durch C#-Mechanismen gebildet wird, verwenden wir hier eine gemeinsame Ressource, die als Switch dient. Wenn es im lock()-Konstrukt verwendet wird, dann bleibt jeder andere Methodenaufruf in der gemeinsamen Ressourcenphase stecken, bis er freigegeben wird. So vermeiden wir den Fehler eines doppelten Aufrufs der Methode.

Betrachten wir nun die Erstellung von Datenquellen für die Einstellungen der Optimierungsparameter. Der Code wird unten gezeigt:

#region Optimization and Test settings

/// <summary>
/// The login visible to the robot during tests (it is required if there is limitation by login)
/// </summary>
private uint? _tertLogin;
public uint? TestLogin
{
    get => _tertLogin;
    set
    {
        _tertLogin = value;

        OnPropertyChanged("TestLogin");
        CB_Action(GetSetActionType.Set_Index);
    }
}
/// <summary>
/// Order execution delay
/// </summary>
public ComboBoxItems<string> ExecutionList { get; }
/// <summary>
/// Type of used quotes (every tick, OHLC, 1M ...)
/// </summary>
public ComboBoxItems<string> ModelList { get; }
/// <summary>
/// Optimization criterion
/// </summary>
public ComboBoxItems<string> OptimisationCriteriaList { get; }
/// <summary>
/// Deposits
/// </summary>
public ComboBoxItems<int> Deposit { get; }
/// <summary>
/// Profit calculation currency
/// </summary>
public ComboBoxItems<string> CurrencyList { get; }
/// <summary>
/// Leverage
/// </summary>
public ComboBoxItems<string> LaverageList { get; }
/// <summary>
/// Forward test start date
/// </summary>
private DateTime _DTForvard = DateTime.Now;
public DateTime ForvardDate
{
    get => _DTForvard;
    set
    {
        _DTForvard = value;

        OnPropertyChanged("ForvardDate");
        CB_Action(GetSetActionType.Set_Index);
    }
}
/// <summary>
/// Indication of tester start in the graphical mode
/// </summary>
private bool _isVisualMode = false;
/// <summary>
/// Indication of tester start in the visual mode
/// </summary>
public bool IsVisual
{
    get => _isVisualMode;
    set
    {
        _isVisualMode = value;

        OnPropertyChanged("IsVisual");
        CB_Action(GetSetActionType.Set_Index);
    }
}
/// <summary>
/// a hidden variable which stores the IsSaveInModel flag value
/// </summary>
private bool isSaveInModel = true;
/// <summary>
/// Shared resource for asynchronous access to the IsSaveInModel property
/// </summary>
private readonly object SaveModel_locker = new object();
/// <summary>
/// Flag; if True - if tester parameters are changed, they will be saved
/// </summary>
private bool IsSaveInModel
{
    get
    {
        lock (SaveModel_locker)
            return isSaveInModel;
    }
    set
    {
        lock (SaveModel_locker)
            isSaveInModel = value;
    }
}
/// <summary>
/// Callback saving changes in tester parameters
/// </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

Eine weitere, wichtige Sache ist die Implementierung der Optimierungsparameter. In diesem Modell wird für jeden Roboter eine individuelle Instanz der Einstellungen des Testers gespeichert. Dies ermöglicht eine individuelle Konfiguration des Testers für jedes ausgewählte Terminal. Die entsprechende CB_Action-Methode wird in jedem "Setter" aufgerufen und ermöglicht so die sofortige Speicherung der Ergebnisse im Modell bei Änderungen der Parameter. Ich habe auch die Klasse ComboBoxItems<T> erstellt, um Daten für die Auswahlliste zu speichern. Es ist eigentlich ein Kontext für die ComboBox, mit dem sie verbunden ist. Hier ist die Klassenimplementierung:

/// <summary>
/// Class - a wrapper for ComboBox list data
/// </summary>
/// <typeparam name="T">Data type stored in ComboBox</typeparam>
class ComboBoxItems<T> : INotifyPropertyChanged
{
    /// <summary>
    /// Collection of list items
    /// </summary>
    private List<T> items;
    public List<T> ItemSource
    {
        get
        {
            OnAction(GetSetActionType.Get_Value);
            return items;
        }
        set
        {
            items = value;
            OnAction(GetSetActionType.Set_Value);
        }
    }
    /// <summary>
    /// Selected index in the list
    /// </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
}

 Ihre Besonderheit ist das Ereignis, das jedes Mal aufgerufen wird, wenn eines seiner Ereignisse bearbeitet wird oder Daten in seinen Ereignissen empfangen werden. Eine weitere Eigenschaft ist das automatisierte Update von View über seine Eigenschaftsänderung. So ist es in der Lage, sowohl ViewModel als auch View über eine Änderung seiner Eigenschaften zu informieren. So aktualisieren wir in ViewModel die Daten im Modell, die die geänderten Eigenschaften der Optimierungseinstellungen betreffen, und rufen die automatische Speicherung auf. Dies macht den Code auch leichter lesbar, da wir ViewModel zwei Eigenschaften jeder ComboBox hinzufügen (den Index des ausgewählten Elements und die Liste aller Elemente). Ohne diese Klasse wäre der Klassencode der ExtentionGUI_VM noch größer.  

Abschließend schauen wir, wie man das Modell unseres Add-ons instanziiert und wie man die GUI im MetaTrader 5 Terminal ausführt. Die Datenmodellklasse muss unabhängig von ViewModel sein, und das ViewModel ist unabhängig von View. Dies, für die Testmöglichkeit werden wir das Modell über die Schnittstelle IExtentionGUI_M implementieren. Der Aufbau und die Implementierung dieser Schnittstelle werden zusammen mit der Beschreibung des Datenmodells besprochen. Beachten Sie nun, dass die Klasse ExtentionGUI_VM nichts über die spezifische Implementierung des Datenmodells weiß, sondern mit der Schnittstelle IExtentionGUI_M arbeitet und die Modellklasse wie folgt instanziiert wird:

private readonly IExtentionGUI_M model = ModelCreator.Model;

Dieser Instanziierungsprozess verwendet eine 'static factory'. Die Klasse ModelCreator ist eine 'factory' und wird wie folgt implementiert:

/// <summary>
/// Factory for substituting a model in a graphical interface
/// </summary>
class ModelCreator
{
    /// <summary>
    /// Model
    /// </summary>
    private static IExtentionGUI_M testModel;
    /// <summary>
    /// Property returning either a model (if it has not been substitutes) or a substitutes model (for tests)
    /// </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>
    /// Model substitution method substitutes a test model so that you can test the graphics separately from the logic
    /// </summary>
    /// <param name="model">test model - substituted from the outside</param>
    [System.Diagnostics.Conditional("DEBUG")]
    public static void SetModel(IExtentionGUI_M model)
    {
        testModel = model;
    }
}

Diese Klasse hat ein 'private' Feld, das über die Schnittstelle des Datenmodell ausgefüllt wird. Das Feld ist zunächst Null. Wir haben diese Funktion verwendet, um eine 'static' Eigenschaft zu schreiben, die das gewünschte Modell erhalten hat. Eine Überprüfung wird im obigen Code durchgeführt: testModel ist nun gleich Null, Instanziieren und Rückgabe der Implementierung des Modells, das die Betriebslogik enthält; wenn testModel nicht Null ist (das Modell wurde ersetzt), geben wir das substituierte Modell zurück, dasjenige, das in testModel gespeichert ist. Die 'static' Methode SetModel wird verwendet, um das Modell zu ersetzen. Diese Methode ist durch das Attribut [System.Diagnostics.Conditional("DEBUG")]] gekennzeichnet, das die Verwendung in der Release-Version dieses Programms verbietet.

Der GUI-Startprozess ähnelt dem Ausführen von Grafiken aus einer DLL, was im obengenannten Artikel beschrieben wurde. Die öffentliche Klasse MQLConnector wurde geschrieben, um die Verbindung mit MetaTrader zu implementieren. 

/// <summary>
/// Class for connecting the graphical interface with MetaTrader
/// </summary>
public class MQL5Connector
{
    /// <summary>
    /// Field containing a pointer to a running graphical interface
    /// </summary>
    private static View.ExtentionGUI instance;
    /// <summary>
    /// Method that launches the graphical interface. 
    /// Only one interface is launched from one robot. 
    /// During launch a check is performed if the GUI has already been started. 
    /// If yes, the new one is not started
    /// </summary>
    /// <param name="pathToTerminal">Path to the terminal's mutable folder</param>
    public static void Instance(string terminalID)
    {
        // check if the GUI has already been started
        if (instance == null)
        {
            // Variable of the secondary thread - the GUI thread (graphics are launched in the secondary thread)
            // Its instantiation and passing a lambda expression describing the order of graphics start
            Thread t = new Thread(() =>
            {
                // Instantiation of the GUI class and its display (launch of graphics)
                instance = new View.ExtentionGUI();
                instance.Show();
                // Subscribe to the graphics window closing event - if the window is closed then 
                // the field in which the link to the HUI was stored is assigned the null value
                instance.Closed += (object o, EventArgs e) => { instance = null; };

                // Launch GUI thread dispatcher
                Dispatcher.Run();
            });
            MainTerminalID = terminalID;		

            // Start secondary thread
            t.SetApartmentState(System.Threading.ApartmentState.STA);
            t.Start();
        }
    }     
    /// <summary>
    /// Gets data on whether the window is active
    /// </summary>
    /// <returns>true if active and false if closed</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);
}

Diese Klasse muss mit dem Zugriffsmodifikator 'public' definiert werden - so ist sie von einem Roboter aus im MetaTrader zugänglich. Außerdem müssen die im Terminal zu verwendenden Methoden 'static' sein und einen Zugriffsmodifikator 'public' haben, da das Terminal die Verwendung nur statischer Methoden erlaubt. Diese Klasse hat auch 2 Eigenschaften mit einem internen Zugriffsmodifikator. Dieser Zugriffsmodifikator verbirgt sie vor dem Terminal, da sie nur für die Verwendung innerhalb der erstellten DLL vorgesehen sind. Wie Sie an der Implementierung sehen können, soll unser Fenster in einem 'private static' Feld gespeichert werden. Dadurch ist es von anderen Eigenschaften und Methoden aus zugänglich. Diese Lösung stellt auch sicher, dass auf diesem Terminal nur eine Anwendungsinstanz in einem Roboter erstellt werden kann. Die Methode Instance instanziiert Grafiken und öffnet ein Fenster. Zuerst wird ein Check durchgeführt, ob das Fenster früher instanziiert wurde. Wenn ja, sollte der Versuch ignoriert werden. Anschließend wird der Sekundär-Thread für die Ausführung der Grafik erstellt. Die Trennung der Threads für die Grafik und das laufende Programm wird verwendet, um ein Einfrieren im Terminal und in der grafischen Oberfläche zu vermeiden. Nachdem wir das Laden der Fenster geschrieben haben, subskribieren wir das Ereignis für das Schließen des Fensters und weisen ihm für das ordnungsgemäße Laden des Fensterschemas Null zu. Dann müssen wir den Dispatcher starten, sonst wird der Dispatcher nicht für den Thread gestartet, in dem die Grafik aufgerufen wird. Die Dispatcher-Klasse wurde entwickelt, um Multithreading-Probleme in WPF-Anwendungen zu lösen. Tatsache ist, dass alle Elemente des Grafikfensters zum Thread des Grafikfensters gehören. Wenn wir versuchen, den Wert eines der Grafikelemente aus einem anderen Thread zu ändern, erhalten wir den Fehler 'cross thread exception'. Die Dispatcher-Klasse startet den an sie übergebenen Vorgang über einen Delegierten im Thread der grafischen Benutzeroberfläche und vermeidet so den Fehler. Nachdem wir die Beschreibung des Lambda-Ausdrucks für den Grafikstart abgeschlossen haben, müssen wir den Thread als "Single Threaded Apartment" konfigurieren und ihn ausführen, um die Grafik auszuführen. Zuvor ist es notwendig, den Wert der übergebenen aktuellen Terminal-ID zu speichern.

Warum brauchen wir das? Dies ermöglicht es uns, Grafiken getrennt von der Logik zu debuggen. Wir haben eine grafische Oberfläche erstellt. Um sie jedoch zu debuggen, benötigen wir eine Klasse, die das Modell repräsentiert. Das Modell hat eine Reihe von spezifischen Implementierungsmerkmalen und sollte daher separat von der Grafik debuggt werden. Nun, da wir eine Methode zum Ersetzen eines Testdatenmodells haben, können wir eine Testdatenmodellklasse implementieren und diese im ViewModel durch eine 'static factory' ersetzen. Dadurch haben wir die Möglichkeit, die Grafik anhand der Testdaten zu debuggen, die GUI auszuführen und die Reaktion von Rückrufen, Design und anderen Nuancen zu überprüfen. Ich habe es wie folgt gemacht. Zunächst müssen wir eine Konsolenanwendung in der aktuellen Lösung erstellen, um Grafiken direkt aus VisualStudio heraus auszuführen: Dies ermöglicht den Zugriff auf Debugging-Tools.


Nennen wir es "Test" und fügen einen Link zu unserer DLL hinzu, die wir für MetaTrader schreiben. Als Ergebnis erhalten wir eine Konsolenanwendung, die die öffentlichen Klassen unserer DLL verwenden kann. Es gibt jedoch nur eine 'public' Klasse in unserer DLL, nämlich die Klasse MQL5Connector. Zusätzlich müssen wir jedoch ein Pseudo-Datenmodell erstellen und wie zuvor beschrieben im ViewModel ersetzen. Um dies zu tun, müssen wir auf Klassen zugreifen, die nur innerhalb der DLL verfügbar sind. Dafür gibt es eine Lösung. Um dies zu tun, weisen wir das folgende Attribut an beliebiger Stelle unserer DLL an:

[assembly: InternalsVisibleTo("Test")]

Es stellt alle internen Klassen unserer DLL im Test-Build (d.h. in unserer Testkonsolenanwendung) zur Verfügung. So können wir ein Pseudo-Modell erstellen und es zum Starten unserer Anwendung verwenden. Infolgedessen sollte unsere Konsolenanwendung die folgende Implementierung haben:

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

        MQL5Connector.Instance("ID of the main terminal");
    }
}

class MyTestModel : IExtentionGUI_M
{
    // Implementation of the IExtentionGUI_M interface
}

Jetzt können wir die Grafiken getrennt von der Logik ausführen, debuggen und visuell analysieren.

Schlussfolgerung und Anlagen

Wir haben die wichtigsten und interessantesten Punkte bei der Erstellung der grafischen Anwendungsschicht und ihrer Verbindungsklasse (ViewModel) besprochen. In dieser Phase haben wir die Grafiken implementiert, die geöffnet und angeklickt werden können, sowie eine Verknüpfungsklasse erstellt, die Datenquellen für die Grafikschicht und ihr Verhalten (Reaktion auf Tastendrucke, etc.) beschreibt. Weiterhin werden wir die Modellklasse und ihre Komponenten besprechen, die die Logik des Add-ons und die Methoden der Interaktion mit den Dateien, Terminals und Computerverzeichnissen beschreiben.