none
2. Thread RRS feed

  • Frage

  • Hallo!

    Ich möchte die SelectedValue-Werte zweier ComBoxen innerhalb einer Methode im Grafik-Thread setzen.

    Dazu starte ich zwei separate Threads (CBAThread und CBBThread) und setze in ihnen die SelectedIndex-Eigenschaft(en) der Combobox(en):

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
        zServer = "Server3"; zDB = "DBB";       // Ziel-Werte einstellen
                
        // Änderung ComboBox Auswahl
        System.Threading.ThreadStart CBAThread = delegate()
        {
            Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
                new Action<string>(SetCBA), zServer);
        };
        new System.Threading.Thread(CBAThread).Start();   // Thread starten
    
        System.Threading.ThreadStart CBBThread = delegate()
        {
            Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
                new Action<string>(SetCBB), zDB);
        };
        new System.Threading.Thread(CBBThread).Start();   // Thread starten
    }


    void SetCBA(string Server)
    {
        cbA.SelectedIndex = liServer.FindIndex(item => item == Server);  // (übergebenen) DB-Server aktivieren -> ComboBox Change-Event wird ausgelöst
    }
    
    void SetCBB(string DB)
    {
        cbB.SelectedIndex = liDB.FindIndex(item => item == DB);  // (übergebene) Datenbank aktivieren -> ComboBox Change-Event wird ausgelöst
    }
    


    Durch das Setzen des Indexes werden die SelectionChanged-Event ausgelöst und die Anzeige(n) aktualisiert.

    Mein Problem ist, dass nach dem Ausführen des Setzens des Indexes der ComboBox-A im CBAThread, der CBBThread nicht mehr ausgeführt wird. Auch ein unmittelbares aufrufen des CBBThreads wird nicht mehr ausgeführt:

    void SetCBA(string Server)
    {
        cbA.SelectedIndex = liServer.FindIndex(item => item == Server);  // (übergebenen) DB-Server aktivieren -> ComboBox Change-Event wird ausgelöst
        SetCBB(zDB);  // Wird Nie ausgeführt !!!
    }


    Woran könnte das liegen???

    -------------------------------------------------------------------------------------------------------------------------------------------

    Ich habe mal ein kleines Demo erstellt:

    XAML:

    <Window x:Name="wnd" x:Class="ComBox.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="ComboBox" Height="250" Width="325" WindowStartupLocation="CenterScreen">
        <Grid>
            <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" >
                <ComboBox Name="cbA" ItemsSource="{Binding}" SelectionChanged="cbA_SelectionChanged" />
                <ComboBox Name="cbB" ItemsSource="{Binding}" SelectionChanged="cbB_SelectionChanged" Margin="0,20,0,0" />
                <Button Name="btnStart" Content="Start" Click="btnStart_Click" Margin="0,30,0,0" />
            </StackPanel>        
        </Grid>
    </Window>

    CodeBehind:

    using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    
    namespace ComBox
    {
        /// <summary>
        /// Interaktionslogik für MainWindow.xaml
        /// </summary>
        public partial class MainWindow : Window
        {
            List<string> liServer = new List<string>();
            List<string> liDB = new List<string>();
            string zServer = "Server1";
            string zDB = "DB1";
    
    
            public MainWindow()
            {
                InitializeComponent();
                liServer = getServer();
                liDB = getDB();
                cbA.DataContext = liServer; cbA.SelectedIndex = 0;
                cbB.DataContext = liDB; cbB.SelectedIndex = 0;
            }
    
            private List<string> getServer()
            {
                System.Threading.Thread.Sleep(1000);    // Datenabruf simulieren
                return new List<string>(new string[] { "Server1", "Server2", "Server3" });
            }
    
            private List<string> getDB()
            {
                System.Threading.Thread.Sleep(1000);    // Datenabruf simulieren
                return new List<string>(new string[] { "DB1", "DB2", "DB3" });
            }
    
            private List<string> getDB2()
            {
                System.Threading.Thread.Sleep(1000);    // Datenabruf simulieren
                return new List<string>(new string[] { "DBA", "DBB", "DBC" });
            }
    
    
            private async void cbA_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                if (cbA.SelectedValue != null)                      // Wurde ein Server ausgewählt?
                {
                    wnd.Opacity = 0.5;                              // Hauptfenster "abdunkeln"
                    cbA.IsEnabled = false;                          // ComboBox sperren
    
                    zServer = (string)cbA.SelectedValue;            // Server aktualisieren
                    liDB = await Task.Run(() => getDB2());          // Datenbanken des aktuellen SQL-Servers auslesen und anzeigen
                    cbB.DataContext = liDB;
                    if (cbB.Items.Count > 0)
                    {
                        cbB.SelectedIndex = 0;                      // 1. (eingelesene) DB anzeigen
                        cbB_SelectionChanged(null, null);           // DB-Änderung immer aktivieren (da SelectedIndex beim neuen DB-Server den gleichen Wert haben kann!)
                    }
                    wnd.Opacity = 1;                                // Hauptfenster "normal" anzeigen
                    cbA.IsEnabled = true;                           // ComboBox freigeben
                }
            }
    
            private async void cbB_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                liDB = await Task.Run(() => getDB2());              // Dummy, damit cbB_SelectionChanges asynchron ausgeführt werden kann
                zDB = (string)cbB.SelectedValue;
            }
    
            private void btnStart_Click(object sender, RoutedEventArgs e)
            {
                btnStart.IsEnabled = false;             // Schaltfläche sperren            
                zServer = "Server3"; zDB = "DBB";       // Ziel-Werte einstellen
                
                // Änderung ComboBox Auswahl
                System.Threading.ThreadStart CBAThread = delegate()
                {
                    Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
                      new Action<string>(SetCBA), zServer);
                };
                new System.Threading.Thread(CBAThread).Start();   // Thread starten
    
                System.Threading.ThreadStart CBBThread = delegate()
                {
                    Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
                      new Action<string>(SetCBB), zDB);
                };
                new System.Threading.Thread(CBBThread).Start();   // Thread starten
    
                btnStart.IsEnabled = true;              // Schaltfläche freigeben
            }
    
            void SetCBA(string Server)
            {
                cbA.SelectedIndex = liServer.FindIndex(item => item == Server);  // (übergebenen) DB-Server aktivieren -> ComboBox Change-Event wird ausgelöst
                SetCBB(zDB);
            }
    
            void SetCBB(string DB)
            {
                cbB.SelectedIndex = liDB.FindIndex(item => item == DB);  // (übergebene) Datenbank aktivieren -> ComboBox Change-Event wird ausgelöst
            }
    
    
        }
    }

    Montag, 4. Juni 2018 13:26

Antworten

  • Hi Fred,
    ich liebe MVVM :-)

    Du vermischst in Deinem Code Oberfläche und Logik und verlierst damit den Überblick. Würdest Du beides entkoppeln, wäre es einfacher, die Programmlogik zu verfolgen und auch zu testen.

    Mit einem Klick startest Du btnStart_Click.

    Darin rufst Du asynchron SetCBA und danach asynchron SetCBB auf.

    In SetCBA setzt Du cbA.SelectedIndex und rufst danach in gleichen Thread SetCBB auf.

    Im Ergebnis con cbA.SelectedIndex wird cbA_SelectionChanged ausgelöst.

    In cbA_SelectionChanged setzt Du den DataContext der 2. Combobox neu: cbB.DataContext = liDB.

    In SetCBB setzt Du cbB.SelectedIndex. Und da SetCBB zwei Mal aufgerufen wird, entsteht für mich Chaos.

    Hinzu kommt, dass in cbB_SelectionChanged ein neuer Verweis auf liDB gesetzt wird. Ob dieser neue Verweis dann in SetCBB wirksam wird, ist wegen der asynchronen Arbeitsweise und wegen möglicher mehrerer Kerne unbestimmt. Beim Debuggen kann es mal funktionieren, beim Ausführen funktioniert es mit hoher Wahrscheinlichkeit nicht.

    Was soll dieses asynchrone Durcheinander bringen?


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


    Montag, 4. Juni 2018 14:23
  • Hi Fred,
    ich habe mal aus Deinen Codestücken und der Beschreibung eine Demo erzeugt. Ich habe aber auf Kommentare erst einmal verzichtet. Das Fenster wird gestartet (geöffnet) und mit dem Klick auf die Befehlsschaltfläche werden die Daten geladen, zuerst die Server-Liste, die dann auf den 2. Eintrag positioniert wird und mit dieser Positionierung wird die Datenbank-Liste geladen. Wenn Du einen anderen Servereintrag auswählst, dann wird entsprechend die Datenbank-Liste dazu geladen. Das asynchrone Laden ist in den Model ausgelagert. Es wird nur das Laden gestartet und mit dem Abschluss des Ladens wird ein Ereignis ausgelöst, welches anzeigt, was geladen wurde (Typ) und auch das Ergebnis (Liste) enthält. Das Ereignis wird über Post des SynchronizationContext in den Ausgangsthread "umgeleitet", damit es keinen threadübergreifenden Fehler gibt.

    Mein Beispiel ist ein möglicher Lösungsweg. Wenn die genauen Randbedingungen bekannt sind, kann auch eine bessere (optimale) Lösung möglich sein.

    XAML:

    <Window x:Class="WpfApp1CS.Window60"
            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:WpfApp1CS"
            mc:Ignorable="d"
            Title="Window60" Height="450" Width="800">
     <Grid>
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" >
          <ComboBox Name="cbA" 
                    ItemsSource="{Binding ViewA}" DisplayMemberPath="Info"
                    SelectedItem="{Binding SelectedItemA}" />
          <ComboBox Name="cbB" 
                    ItemsSource="{Binding ViewB}" DisplayMemberPath="Info"
                    SelectedItem="{Binding SelectedItemB}" Margin="0,20,0,0" />
          <Button Name="btnStart" Content="Start" Margin="0,30,0,0" Click="btnStart_Click" IsEnabled="{Binding IsEnabled}" />
        </StackPanel>
      </Grid>
    </Window>

    Dazu alle Code-Klassen:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using System.Threading;
    using System.Windows;
    using System.Windows.Data;
    
    namespace WpfApp1CS
    {
      /// <summary>
      /// Interaction logic for Window60.xaml
      /// </summary>
      public partial class Window60 : Window
      {
        /// <summary>
        /// Window-Contructor
        /// </summary>
        public Window60()
        {
          InitializeComponent();
          // ViewModel-Instanz für Fenster als DataContext
          this.DataContext = vm;
        }
        // ViewModel-Instanz beim Instanziieren des Fensters erzeugen
        Window60VM vm = new Window60VM();
        // Ereignisroutine zum Button-Klick zuweisen
        private void btnStart_Click(object sender, RoutedEventArgs e) => vm.StartLoadData();
      }
    
      /// <summary>
      /// ViewModel-Klasse
      /// </summary>
      public class Window60VM : INotifyPropertyChanged
      {
        // Model-Instanz beim Instanziieren des ViewModleserzeugen
        Window60Model model = new Window60Model();
    
        /// <summary>
        /// Constructor mit Zuweisen der Ereignisroutine, 
        /// mit der vom Model das Ende des Ladnes der Daten gemeldet wird
        /// </summary>
        public Window60VM() => model.DataArrived += Model_DataArrived;
    
        #region Master-Bereich
        // Quelle für die Sicht auf die Liste (für Server-Liste)
        CollectionViewSource cvsA = new CollectionViewSource();
        // Sicht-Eigenschaft zur Bindung an die ComboBox (Listenbereich)
        public ICollectionView ViewA { get => cvsA.View; }
        // Eigenschaft für das ausgewählte (selektierte) Datenobjekt in der ComboBox
        public Window60Data SelectedItemA { get; set; }
        #endregion
    
        #region Child-Bereich
        // Quelle für die Sicht auf die Liste (für DB-Liste)
        CollectionViewSource cvsB = new CollectionViewSource();
        // Sicht-Eigenschaft zur Bindung an die ComboBox (Listenbereich)
        public ICollectionView ViewB { get => cvsB.View; }
        // Eigenschaft für das ausgewählte (selektierte) Datenobjekt in der ComboBox
        public Window60Data SelectedItemB { get; set; }
        #endregion
    
        // Änderungen erlauben, wenn Model nicht genutzt wird
        public bool IsEnabled { get => model.Used == 0; }
    
        // Ereignisroutine beim Button-Klick, die im Model das Laden der Server-Liste startet
        public void StartLoadData() => model.GetServerBegin();
    
        // Ereignisroutine, wenn das Model eine Lade-Ende meldet
        private void Model_DataArrived(object sender, Window60ModelEventArgs e)
        {
          // Auswählen nach Typ des Ladevorganges
          switch (e.Typ)
          {
            // es wurde Server-Liste geladen
            case Window60Typ.Server:
              setA(e.Liste);
              break;
            // es wurde DB-Liste geladen
            case Window60Typ.DB:
              setB(e.Liste);
              break;
            default:
              break;
          }
          // der Oberfläche mitteilen, dass Änderungen erlaubt bzw. nicht erlaubt sind 
          OnPropertyChanged(nameof(IsEnabled));
        }
    
        // Demo-Werte für das spätere Selektieren in den ComboBoxen
        string zServer = "Server2";
        string zDB = "DB22";
    
        // die geladene Server-Liste (Master-Liste) verarbeiten
        private void setA(List<Window60Data> liste)
        {
          // Liste der Quelle zuweisen
          cvsA.Source = liste;
          // Ereignisroutine zuweisen, die die Auswahl eines anderen Eintrages 
          // in der gebundenen ComboBox verarbeitet
          cvsA.View.CurrentChanged += ViewA_CurrentChanged;
          // der Oberfläche mitteilen, dass sich der Inhalt der Sicht geändert hat
          OnPropertyChanged(nameof(ViewA));
          // das Datenobjekt zuweisen, welches selektiert werden soll
          SelectedItemA = (from item in liste where item.Info == zServer select item).FirstOrDefault();
          // der Oberfläche mitteilen, dass es ein neues selektiertes Datenobjekt (Zeile) gibt
          OnPropertyChanged(nameof(SelectedItemA));
        }
    
        // ein Eintrag in der Liste der Sicht der Server wurde neu ausgewählt
        private void ViewA_CurrentChanged(object sender, EventArgs e)
        {
          // das neue Datenobjekt ermitteln
          Window60Data item = (Window60Data)cvsA.View.CurrentItem;
          // Laden der DB-Liste starten
          model.GetDBBegin(item.ID);
        }
    
        // die geladene DB-Liste (Child-Liste) verarbeiten
        private void setB(List<Window60Data> liste)
        {
          // Liste der Quelle zuweisen
          cvsB.Source = liste;
          // der Oberfläche mitteilen, dass sich der Inhalt der Sicht geändert hat
          OnPropertyChanged(nameof(ViewB));
          // das Datenobjekt ermitteln, welches selektiert werden soll
          var db = (from item in liste where item.Info == zDB select item).FirstOrDefault();
          // wenn es nicht ermittelt wurde, dann das erste Datenobjekt nehmen
          if (db == null) db = liste.FirstOrDefault();
          // zu selektierendes Objekt zuweisen
          SelectedItemB = db;
          // der Oberfläche mitteilen, dass es ein neues selektiertes Datenobjekt (Zeile) gibt
          OnPropertyChanged(nameof(SelectedItemB));
        }
    
        #region  OnPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] string propName = "") =>
          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
        #endregion
      }
    
      // Model-Klasse, mit welcher die Daten geladen und bereitgestelt werden
      public class Window60Model
      {
        // Ereignis, mit dem das Ende des Laden gemeldet wird
        public event EventHandler<Window60ModelEventArgs> DataArrived;
        // Context des Threads, in welchem die Klasse instanziiert wird (UI-Thread)
        SynchronizationContext sc = null;
        // Delegates für die asynchronden Arbeistweisen
        public delegate List<Window60Data> AsyncDelegateServer();
        public delegate List<Window60Data> AsyncDelegateDB(int id);
    
        // Constructor, in welchem der SynchronizationContext 
        // zum Zeitpunkt der Instanziierung gemerkt wird
        public Window60Model() => sc = SynchronizationContext.Current;
    
        // Eigenschaft, in der die Anzahl der Nutzungen gemerkt wird
        public int Used { get; set; } = 0;
    
        // Server-Liste beginnen zu laden
        public void GetServerBegin()
        {
          // Nutzungszähler hochzählen
          Used++;
          // Delegate für folgende asynchrone Arbeitsweise zuweisen
          AsyncDelegateServer dServer = new AsyncDelegateServer(getServer);
          // Asynchrone (in anderem Thread) Verarbeitung starten
          dServer.BeginInvoke(new AsyncCallback(cbServer), dServer);
        }
    
        // Methode, die zum Ende des Laden ausgeführt wird (CallBack)
        private void cbServer(IAsyncResult ar)
        {
          // den Delegate aus dem State holen, um die Liste zu erhalten
          AsyncDelegateServer dServer = (AsyncDelegateServer)ar.AsyncState;
          // das Ende in den Context des UI-Threads umleiten
          sc.Post(new SendOrPostCallback(cbServerEnd), dServer.EndInvoke(ar));
        }
    
        // Ende des Ladens mit einem Ereigni smitteilen
        private void cbServerEnd(object state)
        {
          // Nutzungszähler runterzählen
          Used--;
          // Ereignis auslösen, um dem ViewModel das Ladeende und Ergebnis (Liste) mitzuteilen
          DataArrived?.Invoke(this, new Window60ModelEventArgs()
          { Typ = Window60Typ.Server, Liste = (List<Window60Data>)state });
        }
    
        // DB-Liste beginnen zu laden
        public void GetDBBegin(int db)
        {
          // Nutzungszähler hochzählen
          Used++;
          // Delegate für folgende asynchrone Arbeitsweise zuweisen
          AsyncDelegateDB dDB = new AsyncDelegateDB(getDB);
          // Asynchrone (in anderem Thread) Verarbeitung starten
          dDB.BeginInvoke(db, new AsyncCallback(cbDB), dDB);
        }
    
        // Methode, die zum Ende des Laden ausgeführt wird (CallBack)
        private void cbDB(IAsyncResult ar)
        {
          // den Delegate aus dem State holen, um die Liste zu erhalten
          AsyncDelegateDB dlgt = (AsyncDelegateDB)ar.AsyncState;
          // das Ende in den Context des UI-Threads umleiten
          sc.Post(new SendOrPostCallback(cbDBEnd), dlgt.EndInvoke(ar));
        }
    
        // Ende des Ladens mit einem Ereignis mitteilen
        private void cbDBEnd(object state)
        {
          // Nutzungszähler runterzählen
          Used--;
          // Ereignis auslösen, um dem ViewModel das Ladeende und Ergebnis (Liste) mitzuteilen
          DataArrived?.Invoke(this, new Window60ModelEventArgs()
          { Typ = Window60Typ.DB, Liste = (List<Window60Data>)state });
        }
    
        // eigentliche Laderoutine für die Server-Liste mit Demo-Daten
        private List<Window60Data> getServer()
        {
          System.Threading.Thread.Sleep(1000);    // Datenabruf simulieren
          return new List<Window60Data>(new Window60Data[]
          { new Window60Data() { ID = 1, Info = "Server1" },
            new Window60Data() { ID = 2, Info = "Server2" },
            new Window60Data() { ID = 3, Info = "Server3" } });
        }
    
        // eigentliche Laderoutine für die DB-Liste mit Demo-Daten
        private List<Window60Data> getDB(int id)
        {
          System.Threading.Thread.Sleep(1000);    // Datenabruf simulieren
          return new List<Window60Data>(new Window60Data[]
          { new Window60Data() { ID = 1, Info = $"DB1{id}" },
            new Window60Data() { ID = 2, Info = $"DB2{id}" },
            new Window60Data() { ID = 3, Info = $"DB3{id}" } });
        }
      }
    
      // Klasse für ein Datenobjekte (Server- und auch DB-Daten)
      public class Window60Data
      {
        // Identifikator eines Datenobjektes
        public int ID { get; set; }
        // Bezeichnung für die Anzeige
        public string Info { get; set; }
      }
    
      // Klasse für Ereignisargumente, die den Typ und auch die Daten beinhaltet
      public class Window60ModelEventArgs : EventArgs
      {
        // Typ der Daten, die zum Ereignis geführt haben
        public Window60Typ Typ { get; set; }
        // Liste mit den geladenen Daten
        public List<Window60Data> Liste { get; set; }
      }
    
      // Aufzählung für die Typen, die beim Ende des Ladens der Daten möglich sind
      public enum Window60Typ
      {
        Server,
        DB
      }
    }

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




    Donnerstag, 7. Juni 2018 12:46

Alle Antworten

  • Hi Fred,
    ich liebe MVVM :-)

    Du vermischst in Deinem Code Oberfläche und Logik und verlierst damit den Überblick. Würdest Du beides entkoppeln, wäre es einfacher, die Programmlogik zu verfolgen und auch zu testen.

    Mit einem Klick startest Du btnStart_Click.

    Darin rufst Du asynchron SetCBA und danach asynchron SetCBB auf.

    In SetCBA setzt Du cbA.SelectedIndex und rufst danach in gleichen Thread SetCBB auf.

    Im Ergebnis con cbA.SelectedIndex wird cbA_SelectionChanged ausgelöst.

    In cbA_SelectionChanged setzt Du den DataContext der 2. Combobox neu: cbB.DataContext = liDB.

    In SetCBB setzt Du cbB.SelectedIndex. Und da SetCBB zwei Mal aufgerufen wird, entsteht für mich Chaos.

    Hinzu kommt, dass in cbB_SelectionChanged ein neuer Verweis auf liDB gesetzt wird. Ob dieser neue Verweis dann in SetCBB wirksam wird, ist wegen der asynchronen Arbeitsweise und wegen möglicher mehrerer Kerne unbestimmt. Beim Debuggen kann es mal funktionieren, beim Ausführen funktioniert es mit hoher Wahrscheinlichkeit nicht.

    Was soll dieses asynchrone Durcheinander bringen?


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


    Montag, 4. Juni 2018 14:23
  • Hallo Peter!

    Du hast ja recht, ist ein ganz schönes Kuddelmuddel :-)  (ist aber historisch so gewachsen)

    Die SelectionChanged-Eventroutinen der ComboBoxen sind asynchron, weil im Produktivsystem die Abfragen Zeit beötigen und ich dies grafisch durch abdunkeln des Fensters anzeigen möchte. Die ComboBoxen beeinflussen sich aber gegenseitig. Bei Änderung der selectedValue Eigenschaft der Combobox-A = Datenbank-Server muss der Inhalt der Combobox B = Datenbanken des (selektierten) Datenbankservers neu eingelesen werden, da diese ja unterschiedlich sind.

    Die einzelnen SelectedChange-Eventroutinen für sich arbeiten korrekt.
    DB-Server Changed:  (ComboBox-A)
    Der Benutzer wählt einen DB-Server. Der ausgewählte Server ist selektiert. Die zugehörigen Datenbanken werden ausgelesen. Die 1. DB des selektierten DB-Servers wird selektiert.
    Die zentrale Eigenschaft zServer entspricht dem selektierten Datenbank-Server.

    DB Changed: (ComboBox-B)
    Der Benutzer wählt eine DB. Die ausgewählte DB ist selektiert. Die zentrale Eigenschaft zDB entspricht der selektierten Datenbank.

    Da ich diese Werte (DB-Server + Datenbank) jetzt aber abspeichere, möchte ich beim Einlesen genau diesen Zustand: Gespeicherten DB-Server + Datenbank, beide sind selektiert, (innerhalb meiner Button-Click-Methode) wieder herstellen. (=Zielwerte)

    Deshalb war es für mich das logischste, innerhalb der Button-Click-Methode Threads zu starten (damit die grafischen Änderungen auch ausgeführt werden können) und in ihnen (Threads) die SelectedIndex-Eigenschaften der ComboBoxen zu setzen. 1. Server (DB's werden eingelesen) und danach die DB noch einmal mit der Ziel-DB zu überschreiben. Durch die vielen asynchronen Vorgänge arbeitet es aber nicht wie gewünscht :-(

    Wie könnte ich das MVVM gerecht trennen? Die Liste der Server wird dynamisch abgefragt ebenso die zugeordnenten (enthaltenen) Datenbanken. Der Inhalt der Combobox B ist von Combobox A abhängig.

    Sollte die Kombination (Liste) DB-Server zugeordnete (Liste) Datenbanken in eine Datenklasse? Es wäre nett, wenn du das richtige Verfahren einmal (grob) beschreiben könntest.

    Vielen Dank für deine Antwort!

    Fred.





    • Bearbeitet perlfred Dienstag, 5. Juni 2018 13:37
    Dienstag, 5. Juni 2018 13:23
  • Hi Fred,
    ich habe mal aus Deinen Codestücken und der Beschreibung eine Demo erzeugt. Ich habe aber auf Kommentare erst einmal verzichtet. Das Fenster wird gestartet (geöffnet) und mit dem Klick auf die Befehlsschaltfläche werden die Daten geladen, zuerst die Server-Liste, die dann auf den 2. Eintrag positioniert wird und mit dieser Positionierung wird die Datenbank-Liste geladen. Wenn Du einen anderen Servereintrag auswählst, dann wird entsprechend die Datenbank-Liste dazu geladen. Das asynchrone Laden ist in den Model ausgelagert. Es wird nur das Laden gestartet und mit dem Abschluss des Ladens wird ein Ereignis ausgelöst, welches anzeigt, was geladen wurde (Typ) und auch das Ergebnis (Liste) enthält. Das Ereignis wird über Post des SynchronizationContext in den Ausgangsthread "umgeleitet", damit es keinen threadübergreifenden Fehler gibt.

    Mein Beispiel ist ein möglicher Lösungsweg. Wenn die genauen Randbedingungen bekannt sind, kann auch eine bessere (optimale) Lösung möglich sein.

    XAML:

    <Window x:Class="WpfApp1CS.Window60"
            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:WpfApp1CS"
            mc:Ignorable="d"
            Title="Window60" Height="450" Width="800">
     <Grid>
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" >
          <ComboBox Name="cbA" 
                    ItemsSource="{Binding ViewA}" DisplayMemberPath="Info"
                    SelectedItem="{Binding SelectedItemA}" />
          <ComboBox Name="cbB" 
                    ItemsSource="{Binding ViewB}" DisplayMemberPath="Info"
                    SelectedItem="{Binding SelectedItemB}" Margin="0,20,0,0" />
          <Button Name="btnStart" Content="Start" Margin="0,30,0,0" Click="btnStart_Click" IsEnabled="{Binding IsEnabled}" />
        </StackPanel>
      </Grid>
    </Window>

    Dazu alle Code-Klassen:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using System.Threading;
    using System.Windows;
    using System.Windows.Data;
    
    namespace WpfApp1CS
    {
      /// <summary>
      /// Interaction logic for Window60.xaml
      /// </summary>
      public partial class Window60 : Window
      {
        /// <summary>
        /// Window-Contructor
        /// </summary>
        public Window60()
        {
          InitializeComponent();
          // ViewModel-Instanz für Fenster als DataContext
          this.DataContext = vm;
        }
        // ViewModel-Instanz beim Instanziieren des Fensters erzeugen
        Window60VM vm = new Window60VM();
        // Ereignisroutine zum Button-Klick zuweisen
        private void btnStart_Click(object sender, RoutedEventArgs e) => vm.StartLoadData();
      }
    
      /// <summary>
      /// ViewModel-Klasse
      /// </summary>
      public class Window60VM : INotifyPropertyChanged
      {
        // Model-Instanz beim Instanziieren des ViewModleserzeugen
        Window60Model model = new Window60Model();
    
        /// <summary>
        /// Constructor mit Zuweisen der Ereignisroutine, 
        /// mit der vom Model das Ende des Ladnes der Daten gemeldet wird
        /// </summary>
        public Window60VM() => model.DataArrived += Model_DataArrived;
    
        #region Master-Bereich
        // Quelle für die Sicht auf die Liste (für Server-Liste)
        CollectionViewSource cvsA = new CollectionViewSource();
        // Sicht-Eigenschaft zur Bindung an die ComboBox (Listenbereich)
        public ICollectionView ViewA { get => cvsA.View; }
        // Eigenschaft für das ausgewählte (selektierte) Datenobjekt in der ComboBox
        public Window60Data SelectedItemA { get; set; }
        #endregion
    
        #region Child-Bereich
        // Quelle für die Sicht auf die Liste (für DB-Liste)
        CollectionViewSource cvsB = new CollectionViewSource();
        // Sicht-Eigenschaft zur Bindung an die ComboBox (Listenbereich)
        public ICollectionView ViewB { get => cvsB.View; }
        // Eigenschaft für das ausgewählte (selektierte) Datenobjekt in der ComboBox
        public Window60Data SelectedItemB { get; set; }
        #endregion
    
        // Änderungen erlauben, wenn Model nicht genutzt wird
        public bool IsEnabled { get => model.Used == 0; }
    
        // Ereignisroutine beim Button-Klick, die im Model das Laden der Server-Liste startet
        public void StartLoadData() => model.GetServerBegin();
    
        // Ereignisroutine, wenn das Model eine Lade-Ende meldet
        private void Model_DataArrived(object sender, Window60ModelEventArgs e)
        {
          // Auswählen nach Typ des Ladevorganges
          switch (e.Typ)
          {
            // es wurde Server-Liste geladen
            case Window60Typ.Server:
              setA(e.Liste);
              break;
            // es wurde DB-Liste geladen
            case Window60Typ.DB:
              setB(e.Liste);
              break;
            default:
              break;
          }
          // der Oberfläche mitteilen, dass Änderungen erlaubt bzw. nicht erlaubt sind 
          OnPropertyChanged(nameof(IsEnabled));
        }
    
        // Demo-Werte für das spätere Selektieren in den ComboBoxen
        string zServer = "Server2";
        string zDB = "DB22";
    
        // die geladene Server-Liste (Master-Liste) verarbeiten
        private void setA(List<Window60Data> liste)
        {
          // Liste der Quelle zuweisen
          cvsA.Source = liste;
          // Ereignisroutine zuweisen, die die Auswahl eines anderen Eintrages 
          // in der gebundenen ComboBox verarbeitet
          cvsA.View.CurrentChanged += ViewA_CurrentChanged;
          // der Oberfläche mitteilen, dass sich der Inhalt der Sicht geändert hat
          OnPropertyChanged(nameof(ViewA));
          // das Datenobjekt zuweisen, welches selektiert werden soll
          SelectedItemA = (from item in liste where item.Info == zServer select item).FirstOrDefault();
          // der Oberfläche mitteilen, dass es ein neues selektiertes Datenobjekt (Zeile) gibt
          OnPropertyChanged(nameof(SelectedItemA));
        }
    
        // ein Eintrag in der Liste der Sicht der Server wurde neu ausgewählt
        private void ViewA_CurrentChanged(object sender, EventArgs e)
        {
          // das neue Datenobjekt ermitteln
          Window60Data item = (Window60Data)cvsA.View.CurrentItem;
          // Laden der DB-Liste starten
          model.GetDBBegin(item.ID);
        }
    
        // die geladene DB-Liste (Child-Liste) verarbeiten
        private void setB(List<Window60Data> liste)
        {
          // Liste der Quelle zuweisen
          cvsB.Source = liste;
          // der Oberfläche mitteilen, dass sich der Inhalt der Sicht geändert hat
          OnPropertyChanged(nameof(ViewB));
          // das Datenobjekt ermitteln, welches selektiert werden soll
          var db = (from item in liste where item.Info == zDB select item).FirstOrDefault();
          // wenn es nicht ermittelt wurde, dann das erste Datenobjekt nehmen
          if (db == null) db = liste.FirstOrDefault();
          // zu selektierendes Objekt zuweisen
          SelectedItemB = db;
          // der Oberfläche mitteilen, dass es ein neues selektiertes Datenobjekt (Zeile) gibt
          OnPropertyChanged(nameof(SelectedItemB));
        }
    
        #region  OnPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] string propName = "") =>
          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
        #endregion
      }
    
      // Model-Klasse, mit welcher die Daten geladen und bereitgestelt werden
      public class Window60Model
      {
        // Ereignis, mit dem das Ende des Laden gemeldet wird
        public event EventHandler<Window60ModelEventArgs> DataArrived;
        // Context des Threads, in welchem die Klasse instanziiert wird (UI-Thread)
        SynchronizationContext sc = null;
        // Delegates für die asynchronden Arbeistweisen
        public delegate List<Window60Data> AsyncDelegateServer();
        public delegate List<Window60Data> AsyncDelegateDB(int id);
    
        // Constructor, in welchem der SynchronizationContext 
        // zum Zeitpunkt der Instanziierung gemerkt wird
        public Window60Model() => sc = SynchronizationContext.Current;
    
        // Eigenschaft, in der die Anzahl der Nutzungen gemerkt wird
        public int Used { get; set; } = 0;
    
        // Server-Liste beginnen zu laden
        public void GetServerBegin()
        {
          // Nutzungszähler hochzählen
          Used++;
          // Delegate für folgende asynchrone Arbeitsweise zuweisen
          AsyncDelegateServer dServer = new AsyncDelegateServer(getServer);
          // Asynchrone (in anderem Thread) Verarbeitung starten
          dServer.BeginInvoke(new AsyncCallback(cbServer), dServer);
        }
    
        // Methode, die zum Ende des Laden ausgeführt wird (CallBack)
        private void cbServer(IAsyncResult ar)
        {
          // den Delegate aus dem State holen, um die Liste zu erhalten
          AsyncDelegateServer dServer = (AsyncDelegateServer)ar.AsyncState;
          // das Ende in den Context des UI-Threads umleiten
          sc.Post(new SendOrPostCallback(cbServerEnd), dServer.EndInvoke(ar));
        }
    
        // Ende des Ladens mit einem Ereigni smitteilen
        private void cbServerEnd(object state)
        {
          // Nutzungszähler runterzählen
          Used--;
          // Ereignis auslösen, um dem ViewModel das Ladeende und Ergebnis (Liste) mitzuteilen
          DataArrived?.Invoke(this, new Window60ModelEventArgs()
          { Typ = Window60Typ.Server, Liste = (List<Window60Data>)state });
        }
    
        // DB-Liste beginnen zu laden
        public void GetDBBegin(int db)
        {
          // Nutzungszähler hochzählen
          Used++;
          // Delegate für folgende asynchrone Arbeitsweise zuweisen
          AsyncDelegateDB dDB = new AsyncDelegateDB(getDB);
          // Asynchrone (in anderem Thread) Verarbeitung starten
          dDB.BeginInvoke(db, new AsyncCallback(cbDB), dDB);
        }
    
        // Methode, die zum Ende des Laden ausgeführt wird (CallBack)
        private void cbDB(IAsyncResult ar)
        {
          // den Delegate aus dem State holen, um die Liste zu erhalten
          AsyncDelegateDB dlgt = (AsyncDelegateDB)ar.AsyncState;
          // das Ende in den Context des UI-Threads umleiten
          sc.Post(new SendOrPostCallback(cbDBEnd), dlgt.EndInvoke(ar));
        }
    
        // Ende des Ladens mit einem Ereignis mitteilen
        private void cbDBEnd(object state)
        {
          // Nutzungszähler runterzählen
          Used--;
          // Ereignis auslösen, um dem ViewModel das Ladeende und Ergebnis (Liste) mitzuteilen
          DataArrived?.Invoke(this, new Window60ModelEventArgs()
          { Typ = Window60Typ.DB, Liste = (List<Window60Data>)state });
        }
    
        // eigentliche Laderoutine für die Server-Liste mit Demo-Daten
        private List<Window60Data> getServer()
        {
          System.Threading.Thread.Sleep(1000);    // Datenabruf simulieren
          return new List<Window60Data>(new Window60Data[]
          { new Window60Data() { ID = 1, Info = "Server1" },
            new Window60Data() { ID = 2, Info = "Server2" },
            new Window60Data() { ID = 3, Info = "Server3" } });
        }
    
        // eigentliche Laderoutine für die DB-Liste mit Demo-Daten
        private List<Window60Data> getDB(int id)
        {
          System.Threading.Thread.Sleep(1000);    // Datenabruf simulieren
          return new List<Window60Data>(new Window60Data[]
          { new Window60Data() { ID = 1, Info = $"DB1{id}" },
            new Window60Data() { ID = 2, Info = $"DB2{id}" },
            new Window60Data() { ID = 3, Info = $"DB3{id}" } });
        }
      }
    
      // Klasse für ein Datenobjekte (Server- und auch DB-Daten)
      public class Window60Data
      {
        // Identifikator eines Datenobjektes
        public int ID { get; set; }
        // Bezeichnung für die Anzeige
        public string Info { get; set; }
      }
    
      // Klasse für Ereignisargumente, die den Typ und auch die Daten beinhaltet
      public class Window60ModelEventArgs : EventArgs
      {
        // Typ der Daten, die zum Ereignis geführt haben
        public Window60Typ Typ { get; set; }
        // Liste mit den geladenen Daten
        public List<Window60Data> Liste { get; set; }
      }
    
      // Aufzählung für die Typen, die beim Ende des Ladens der Daten möglich sind
      public enum Window60Typ
      {
        Server,
        DB
      }
    }

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




    Donnerstag, 7. Juni 2018 12:46
  • Hallo Peter!

    Puh, das muss ich mir erst einmal in Ruhe ansehen!!!  Morgen haben wir allerdings einen Betriebsausflug, so dass es noch ein kleines Stück dauern kann.

    Vielen!!! Dank für deine Mühe, wieder ein Beispielprojekt anzufertigen. Du kannst dir aber sicher sein, ich arbeite es auch Punkt für Punkt durch, bis ich die Wirkungsweise verstanden habe. Ganz so trivial war die Anforderung dann doch nicht, wie ich dem ersten Überfliegen deines Codes entnehmen kann. MVVM: Da bin ich richtig gespannt!

    Ich hatte mich zwischenzeitlich mit der Synchronisation von Threads beschäftigt. Das dürfte sich jetzt dank deiner Lösung erledigt haben.

    Nochmals vielen Dank!

    Fred.

    Donnerstag, 7. Juni 2018 16:17