none
Zugriff von class MainWindow : Window auf class ViewModel

    Frage

  • Hallo,

    über ViewModel vm wird aktuell eine Tabelle dargestellt, welche ich über die externe Klasse ViewModel mit Daten fülle.

        <Window.Resources>
            <local:ViewModel x:Key="vm"/>
        </Window.Resources>


                <TabItem Header="Tabelle">
                    <TabPanel VerticalAlignment="Top" HorizontalAlignment="Left" Height="370" Width="762">
                        <Grid DataContext="{StaticResource vm}" HorizontalAlignment="Right" Width="287">
                            <DataGrid Name="dg" ItemsSource="{Binding View}" IsReadOnly="True" AutoGenerateColumns="False" local:DataGridExtension44.Columns="{Binding Columns}">
                            </DataGrid>
                        </Grid>
                    </TabPanel>
                </TabItem>


    mit Inhalt der Klasse ViewModel

        public class ViewModel
        {
            public ViewModel() => GetData();
    
            private CollectionViewSource cvs = new CollectionViewSource();
            public ICollectionView View
            {
                get
                {
                    return cvs.View;
                }
            }
    
            public ObservableCollection<DataGridColumn> Columns { get { return columns; } }
            private ObservableCollection<DataGridColumn> columns = new ObservableCollection<DataGridColumn>();     
    
            // Datenobjekt
            public class Data
            {
                public string ID { get; set; }
                public List<string> Werte { get; set; }
            }
    
            private string[] zeile1 = {"w11", "w12", "w13", "w14"};  <<< Würde ich gerne von MainWindow aus an GetData übergeben
            private string[] zeile2 = {"w21", "w22", "w23", "w24"};      
            
            private void GetData()
            {
                // Liste der Datenobjekte
                List<Data> liste = new List<Data>();
                // Maximalzahl der Spalten
                int cols = 5;
                // Datenobjekte einlesen und Liste füllen            
                liste.Add(new Data() { ID = "1", Werte = new List<string>(zeile1)});
                liste.Add(new Data() { ID = "2", Werte = new List<string>(zeile2)});            
                // Datenquelle zuweisen
                cvs.Source = liste;
                // Datenobjekte ggf. auffüllen, wenn in Zeilen unterschiedliche Anzahl von Spalten
                foreach (var item in liste)
                    for (int i = item.Werte.Count; i < cols; i++)
                        item.Werte.Add("<leer>");
                // Spalten erzeugen
                columns.Add(new DataGridTextColumn() { Header = $"ID", Binding = new Binding("ID") });
                for (int i = 0; i < cols; i++)
                    columns.Add(new DataGridTextColumn() { Header = $"Spalte {i + 1}", Binding = new Binding($"Werte[{i}]") });
            }        
        }


    Wie gelingt es mir im MainWindow : Window auf class ViewModel zuzugreifen um hier im cvs.Source die Daten zu ändern.

    Funktioniert etwas in der Art wie:

    string[] zeile1 = {"w11", "w12", "w13", "w14"}; 
    string[] zeile2 = {"w21", "w22", "w23", "w24"}; 

    ViewModel vm = New ViewModel();
    vm.GetData(zeile1, zeile2);

    Danke euch und vorarb schonmal ein erholsames Wochenende :)

    Viele Grüße
    Martin



    Freitag, 13. April 2018 08:13

Antworten

  • Hi Martin,
    das MVVM Entwurfsmuster sieht nicht vor, dass von der Anzeige (UI) Schicht auf die darunterliegende Schicht (ViewModel) zugegriffen wird. Die UI holt sich die Daten vom ViewModel, wobei nicht die Daten in die UI kopiert werden, sondern es über Eigenschaftsbindung eine Zwei-Weg-Transport möglich ist. D.h., die UI holt sich einen Wert, zeigt ihn an, der Bediener ändert den Wert und die UI schreibt den geänderten Wert zurück in das ViewModel (in die gleiche Eigenschaft).

    Die ViewModel-Schicht holt sich "fremde" Daten aus der Model-Schicht, nicht von der Oberfläche. Deshalb ist GetData auch eine private Methode innerhalb des ViewModels, die normalerweise die Model-Schicht abfragt und von dort Daten holt. Dabei können Parameter ins Spiel kommen (z.B. Dateiname o.ä.), die über Eigenschaftsbindung durch den bediener in der Oberfläche beeinflusst werden können.

    Du solltest zuerst mal den zu realisierenden Prozess beschreiben:

    1. Welche Daten sollen woher kommen?

    2. Wie sieht die interne Datenpufferung (Datenschema) aus?

    3. Was soll im Ergebnis erreicht werden (Anzeige)?

    4. Welche Datenverarbeitungsschritte sind von 1. zu 3. erforderlich?

    Das ist die übliche Herangehensweise in einem Programm, die üblicherweise in einem PAP (Programmablaufplan) dargestellt wird.


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP)
    Meine Homepage mit Tipps und Tricks



    • Bearbeitet Peter Fleischer Freitag, 13. April 2018 08:27
    • Als Antwort markiert mApO18 Montag, 16. April 2018 11:07
    Freitag, 13. April 2018 08:20

Alle Antworten

  • Hi Martin,
    das MVVM Entwurfsmuster sieht nicht vor, dass von der Anzeige (UI) Schicht auf die darunterliegende Schicht (ViewModel) zugegriffen wird. Die UI holt sich die Daten vom ViewModel, wobei nicht die Daten in die UI kopiert werden, sondern es über Eigenschaftsbindung eine Zwei-Weg-Transport möglich ist. D.h., die UI holt sich einen Wert, zeigt ihn an, der Bediener ändert den Wert und die UI schreibt den geänderten Wert zurück in das ViewModel (in die gleiche Eigenschaft).

    Die ViewModel-Schicht holt sich "fremde" Daten aus der Model-Schicht, nicht von der Oberfläche. Deshalb ist GetData auch eine private Methode innerhalb des ViewModels, die normalerweise die Model-Schicht abfragt und von dort Daten holt. Dabei können Parameter ins Spiel kommen (z.B. Dateiname o.ä.), die über Eigenschaftsbindung durch den bediener in der Oberfläche beeinflusst werden können.

    Du solltest zuerst mal den zu realisierenden Prozess beschreiben:

    1. Welche Daten sollen woher kommen?

    2. Wie sieht die interne Datenpufferung (Datenschema) aus?

    3. Was soll im Ergebnis erreicht werden (Anzeige)?

    4. Welche Datenverarbeitungsschritte sind von 1. zu 3. erforderlich?

    Das ist die übliche Herangehensweise in einem Programm, die üblicherweise in einem PAP (Programmablaufplan) dargestellt wird.


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP)
    Meine Homepage mit Tipps und Tricks



    • Bearbeitet Peter Fleischer Freitag, 13. April 2018 08:27
    • Als Antwort markiert mApO18 Montag, 16. April 2018 11:07
    Freitag, 13. April 2018 08:20
  • Hallo Peter,
    danke dir für die Antwort.

    In der Klasse MainWindow : Window wird über die Routine

    void _spManager_NewSerialDataRecieved(object sender, SerialDataEventArgs e)

    ein Datenstrom einer Hardware eingelesen. Diesen Datenstrom werte ich innerhalb 

    _spManager_NewSerialDataRecieved

    aus und ermittle mir einen Messwert und die AnzeigeAdresse als Parameter.

    Hierbei sind

            // speichert Anzeigenmesswert und Anzeigenadresse
            double Messwert = 0;
            double AnzeigenAdresse = 0;

    Member der Klasse MainWindow : Window. Diese Parameter möchte ich nun an
    public class ViewModel übergeben, damit ich sie anzeigen kann.

    Im Ergebnis sollten diese Parameter einseits im erstellten DataGrid gespeichert werden, andererseits auch in einer Excel Tabelle (anderes Thema).

    Ich hoffe, die Punkte 1 bis 3 konnten einigermaßen angegeben werden.

    Viele Grüße
    Martin


    Freitag, 13. April 2018 13:14
  • Hi Martin,
    unklar ist die Informations-Struktur Deines Projektes. Deine UI sollte keine Daten aus einer externen Datenquelle einlesen können, da es im MVVM in der UI keinen Code (im CodeBehind) gibt. Das Einlesen externer Daten ist Aufgabe des Models. Aus der UI (Dein CodeBehind des MainWindows) Parameterwerte an den ViewModel zu übergeben, ist nicht vereinbar mit dem MVVM Entwurfsmuster. Da musst Du eine andere (eigene von MVVM abweichende) Technologie nutzen.

    Irgendwelche Felder (Messwert und AnzeigenAdresse) haben im CodeBehind der UI nichts zu suchen. Wenn das Werte sind, die der Anwender beeinflussen soll, dann sind diese Werte in Eigenschaften des ViewModels zu kapseln. Diese Eigenschaften können in der UI gebunden werden und beim Mode=TwoWay auch wieder nach Änderungen des Anwenders im ViewModel aktualisiert werden. Wenn der ViewModel dann Daten aus dem Model holen muss und die Methoden des Models diese Werte als Parameter benötigen, können sie aus den eigenen Eigenschaften ausgelesen und beim Aufruf der Model-Methode übergeben werden.

    Im DataGrid kann nichts gespeichert werden. Gespeichert wird in dem vom DataGrid für die Anzeige genutzten Datenpuffer. In MVVM wird ein Verweis auf die Elemente des Datenpuffers vom ViewModel bereitgestellt (gebunden) und das DataGrid holt sich bei Bedarf (Refresh) diese Werte für die Anzeige. Und dieser Datenpuffer kann dann mit einer Excel-Tabelle synchronisiert/exportiert werden.


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP)
    Meine Homepage mit Tipps und Tricks




    Freitag, 13. April 2018 17:41
  • Hallo Peter,

    ich verstehe ansatzweise was wie das System funktioniert. Meinst du ich sollte lieber auf Windows.Forms gehen und nicht mit WPFs in meiner Anwendung arbeiten? Leider kann ich deine Anmerkung nicht umsetzen. Ich benötige ein Beispiel um sie umzusetzen. Nachfolgend nochmal mein Quellcode bisher.

    Danke dir.

    namespace EasyControl
    {
        /// <summary>
        /// Interaktionslogik für MainWindow.xaml
        /// </summary>
        /// 
    
        public class ViewModel
        {
            public ViewModel() => GetData();
    
            private CollectionViewSource cvs = new CollectionViewSource();
            public ICollectionView View
            {
                get
                {
                    //if (cvs.Source == null) cvs.Source = 0;
                    return cvs.View;
                }
            }
    
            public ObservableCollection<DataGridColumn> Columns { get { return columns; } }
            private ObservableCollection<DataGridColumn> columns = new ObservableCollection<DataGridColumn>();     
    
            // Datenobjekt
            public class Data
            {
                public string ID { get; set; }
                public List<string> Werte { get; set; }
            }
    
            private string[] zeile1 = {"w11", "w12", "w13", "w14"};
            private string[] zeile2 = {"w21", "w22", "w23", "w24"};      
            
            private void GetData()
            {
                // Liste der Datenobjekte
                List<Data> liste = new List<Data>();
                // Maximalzahl der Spalten
                int cols = 5;
                // Datenobjekte einlesen und Liste füllen            
                liste.Add(new Data() { ID = "1", Werte = new List<string>(zeile1)});
                liste.Add(new Data() { ID = "2", Werte = new List<string>(zeile2)});            
                // Datenquelle zuweisen
                cvs.Source = liste;
                // Datenobjekte ggf. auffüllen, wenn in Zeilen unterschiedliche Anzahl von Spalten
                foreach (var item in liste)
                    for (int i = item.Werte.Count; i < cols; i++)
                        item.Werte.Add("<leer>");
                // Spalten erzeugen
                columns.Add(new DataGridTextColumn() { Header = $"ID", Binding = new Binding("ID") });
                for (int i = 0; i < cols; i++)
                    columns.Add(new DataGridTextColumn() { Header = $"Spalte {i + 1}", Binding = new Binding($"Werte[{i}]") });
            }        
        }
    
        public static class DataGridExtension44
        {
            public static ObservableCollection<DataGridColumn> GetColumns(DependencyObject obj) =>
              (ObservableCollection<DataGridColumn>)obj.GetValue(ColumnsProperty);
    
            public static void SetColumns(DependencyObject obj, ObservableCollection<DataGridColumn> value) =>
              obj.SetValue(ColumnsProperty, value);
    
            public static readonly DependencyProperty ColumnsProperty =
                   DependencyProperty.RegisterAttached("Columns",
                   typeof(ObservableCollection<DataGridColumn>),
                   typeof(DataGridExtension44),
                   new UIPropertyMetadata(new ObservableCollection<DataGridColumn>(),
                   OnDataGridColumnsPropertyChanged));
    
            private static void OnDataGridColumnsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                if (d.GetType() != typeof(DataGrid)) return;
                DataGrid myGrid = d as DataGrid;
                ObservableCollection<DataGridColumn> Columns = (ObservableCollection<DataGridColumn>)e.NewValue;
                if (Columns == null) return;
                myGrid.Columns.Clear();
                if (Columns != null && Columns.Count > 0)
                    foreach (DataGridColumn dataGridColumn in Columns)
                        myGrid.Columns.Add(dataGridColumn);
                Columns.CollectionChanged += delegate (object sender, NotifyCollectionChangedEventArgs args)
                {
                    if (args.NewItems != null)
                        foreach (DataGridColumn column in args.NewItems)
                            myGrid.Columns.Add(column);
                    if (args.OldItems != null)
                        foreach (DataGridColumn column in args.OldItems)
                            myGrid.Columns.Remove(column);
                };
            }
        }
    
        public partial class MainWindow : Window
        {
    
            public MainWindow()
            {
                InitializeComponent();
                UserInitialization();
                // by default it will create an AutoCad2000 dxf version
                DxfDocument dxf = new DxfDocument();            
            }          
    
            private DxfDocument dxfLoad = new DxfDocument();
            private OpenFileDialog ofd = new OpenFileDialog();
            double Linienstaerke = 0.5;
            double LinienstarkeAbgeblendet = 0.1;
            Point firstPoint = new Point();
            SerialPortManager _spManager;    
    
            // speichert Anzeigenmesswert und Anzeigenadresse
            double Messwert = 0;
            double AnzeigenAdresse = 0;
    
            private void UserInitialization()
            {
                _spManager = new SerialPortManager();
                SerialSettings mySerialSettings = _spManager.CurrentSerialSettings;
                //serialSettingsBindingSource.DataSource = mySerialSettings;
                portNameComboBox.ItemsSource = mySerialSettings.PortNameCollection;
                baudRateComboBox.ItemsSource = mySerialSettings.BaudRateCollection;
                dataBitsComboBox.ItemsSource = mySerialSettings.DataBitsCollection;
                parityComboBox.ItemsSource = Enum.GetValues(typeof(System.IO.Ports.Parity));
                stopBitsComboBox.ItemsSource = Enum.GetValues(typeof(System.IO.Ports.StopBits));
    
    
                _spManager.NewSerialDataRecieved += new EventHandler<SerialDataEventArgs>(_spManager_NewSerialDataRecieved);
                string output = "Value;Display no;Date;Time";
                tbData.AppendText(output + Environment.NewLine);
                tbData.ScrollToEnd();       
           }  
            
            
            
    
            // ermittelt den Messwert aus der Anzeige, nach eingehendem Datenstrom e
            double getMesswert(SerialDataEventArgs e)
            {
                // jedes ankommende Byte in hexadezimale Zeichen umwandeln
                StringBuilder sb = new StringBuilder();
                foreach (byte b in e.Data)
                {
                    sb.Append(string.Format("{0:x2}", b)); // Hexadezimal Format mit 2 Zeichen
                }
                string hexString = sb.ToString();
    
                // Eingang-Signal auswerten:
                int length = hexString.Length;
                int dezimalstelle = Convert.ToInt32(hexString.Substring(7, 1));
                string messwert = hexString.Substring(10, 16);
                messwert = messwert.Remove(0, 1);
                messwert = messwert.Remove(1, 1);
                messwert = messwert.Remove(2, 1);
                messwert = messwert.Remove(3, 1);
                messwert = messwert.Remove(4, 1);
                messwert = messwert.Remove(5, 1);
                messwert = messwert.Remove(6, 1);
                messwert = messwert.Remove(7, 1);
                messwert = messwert.Insert(messwert.Length - dezimalstelle, ",");
                if (hexString.Substring(8, 2) == "2d")
                {
                    messwert = messwert.Insert(0, "-");
                }
                // Datenstring in Double umwandeln          
                return Convert.ToDouble(messwert);
            }
            // ermittelt den Messwert aus der Anzeige, nach eingehendem Datenstrom e
            double getAdressNr(SerialDataEventArgs e)
            {
                // jedes ankommende Byte in hexadezimale Zeichen umwandeln
                StringBuilder sb = new StringBuilder();
                foreach (byte b in e.Data)
                {
                    sb.Append(string.Format("{0:x2}", b)); // Hexadezimal Format mit 2 Zeichen
                }
                string hexString = sb.ToString();
                // Eingang-Signal auswerten:
                string adressNr = hexString.Substring(0, 2);
                // Datenstring in Double umwandeln          
                return Convert.ToDouble(adressNr);
            }
    
            void _spManager_NewSerialDataRecieved(object sender, SerialDataEventArgs e)
            {
                if (!Dispatcher.CheckAccess())
                {
                    // Using this.Invoke causes deadlock when closing serial port, and BeginInvoke is good practice anyway.
                    Dispatcher.BeginInvoke(new EventHandler<SerialDataEventArgs>(_spManager_NewSerialDataRecieved), new object[] { sender, e });
                    return;
                }
    
                // Converting the ASCII characters send by the Willtec Display to text
                // jedes ankommende Byte in hexadezimale Zeichen umwandeln
                StringBuilder sb = new StringBuilder();
                foreach (byte b in e.Data)
                {
                    sb.Append(string.Format("{0:x2}", b)); // Hexadezimal Format mit 2 Zeichen
                }
    
                string hexString = sb.ToString();
                // Checksum auswerten
                string cSum = hexString.Substring(hexString.Length - 2, 2);
    
                // Messwert aus Datenstrom der Anzeige speichern
                Messwert = getMesswert(e);
                AnzeigenAdresse = getAdressNr(e);
    
                //// Anzeigewerte schreiben 
                string NL = Environment.NewLine;
                string output;                               
    
                DateTime moment = DateTime.Now;
                output = Messwert + " ; " + AnzeigenAdresse + " ; " + moment.ToString("d") + " ; " + moment.TimeOfDay.Hours + ":" + moment.TimeOfDay.Minutes;
                tbData.AppendText(output + NL);
                tbData.AppendText("Datenstrom im Hex-Format: " + NL);
                tbData.AppendText(hexString + NL);
                //tbData.AppendText(dadress + NL);   
                tbData.ScrollToEnd();
                string path = System.IO.Directory.GetCurrentDirectory();
                using (System.IO.StreamWriter file =
                new System.IO.StreamWriter((path + @"\\Measurements.csv"), true))
                {
                    file.WriteLine(output);
                }
            }
    
            // Handles the "Start Listening"-buttom click event
            private void btnConnect_Click(object sender, RoutedEventArgs e)
            {
                _spManager.StartListening();
                statusText.Text = _spManager.CurrentSerialSettings.PortStatus;
            }
    
            private void btnDisconnect_Click(object sender, RoutedEventArgs e)
            {
                _spManager.StopListening();
                statusText.Text = _spManager.CurrentSerialSettings.PortStatus;
            }
    
            private void btnRead_Click(object sender, RoutedEventArgs e)
            {
                _spManager.ReadAscii();
            }
        }
    }

    Montag, 16. April 2018 06:55
  • Hallo, ich denke was dir Peter Fleischer sagen möchte ist, das du dir erst einmal Gedanken zu deiner Programm-Struktur machen solltest.

    Das hat nichts damit zu tun, ob du WPF oder WinForms nimmst. (Wobei ich persönlich heute kein Projekt mehr mit WinForms machen würde. Aber das muss jeder für sich entscheiden.)

    Wenn du schon ein ViewModel hast, sieht das so aus, als würdest du das MVVM Entwurfsmuster nutzen. Das macht aus meiner Sicht Sinn. Aber dafür musst du dich schon ein bisschen einlesen.

    Da ich aber anhand deines Codes sehe, das du offensichtlich Daten über eine serielle Schnittstelle von einem externen Messgerät o.ä. einliest und diese dann in eine CSV- Datei schreiben möchtest, wirst du im Internet kein passendes Beispiel finden (ging mir zumindest am Anfang so. Ich habe auch viel mit Messgeräten, Antrieben, Schraubern, Etikettierern, PLC's ... zu tun).

    Daher gehören aus meiner Sicht deine serielle Schnittstelle und deine CSV- Datei in ein Model.
    Das ViewModel holt die Daten ab und stellt sie dem View (UI) zur Verfügung.
    Deine View besteht dann aus Controlls (Button, TextBox, DataGrid.......), welche an Eigenschaften deines ViewModel gebunden sind.

    Ich nutze häufig das "MVVM light toolkit", wobei man schon verstehen sollte, wie das ganze grundlegend funktioniert.

    Freiberufler im Bereich Softwareentwicklung Von der PLC und Robotik zu VB.NET & C#, vorrangig WPF und UWP

    Montag, 16. April 2018 08:30
  • Hi Stefan,

    danke dir für den Denkanstoß. Ich erzeuge mir ne eigene Klasse (Model), die mir die Daten fürs ViewModel liefert.

    Danke euch.

    Folgendes MVVM Tutorial hat mir hierbei noch weitergeholfen.
    http://www.cocktailsandcode.de/2012/04/mvvm-tutorial-part-1-grundlagen/

    Montag, 16. April 2018 11:07
  • Hi Martin,
    meine Empfehlung ist, bau Dir erst einmal eine Klassenbibliothek, die den gesamten Zugriff auf Deine externen Daten in einer "Model"-Klasse kapselt. Der SerialPortManager wird darin instanziiert. Die Klasse hate eine Start-Methode, eine Stop-Methode, eine Read-Methode. Außerdem hat die Klasse ein paar Eigenschaften, die z.B. die Meßwerte und die Adresse liefert. Eine weitere Klasse in der Model-Klassenbibliothek könnte den gesamten Export in eine csv-Datei kapseln.

    Der Projektmappe fügst Du ein Testprojekt hinzu und testest die Methodenaufrufe und auch die Daten in den Eigenschaften der in der Model-Klassenbibliothek vorhandenen Klassen.

    Wenn das funktioniert, dann fügst Du der Projektmappe eine Klassenbibliothek hinzu, die die Klasse des ViewModels enthält. Darin instanziierst Du die Model-Klasse aus der anderen (zuerst angelegtes Projekt = dll). Die ViewModel-Klasse baust Du so auf, dass sie alle Eigenschaften beinhaltet, die von der Oberfläche (UI) benötigt werden = Einzelwerte, Listen, Befehle. Zum Test dieser ViewModel-Klasse fügst Du Deinem Testprojekt ein Testklasse hinzu, deren Testmethoden den ViewModel testet.

    Zuletzt fügst Du der Projektmappe eine WPF Anwendung hinzu. Im MainWindow oder bei Bedarf auch in weiteren Windows entwickelst Du im XAML das Oberflächendesign und bindest die entsprechenden Steuerelemente an die Eigenschaften des ViewModels.


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP)
    Meine Homepage mit Tipps und Tricks

    Montag, 16. April 2018 11:11