Benutzer mit den meisten Antworten
2. Thread

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 } } }
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- Bearbeitet Peter Fleischer Montag, 4. Juni 2018 14:25
- Als Antwort vorgeschlagen Ivan DragovMicrosoft contingent staff, Moderator Montag, 11. Juni 2018 05:58
- Als Antwort markiert Ivan DragovMicrosoft contingent staff, Moderator Mittwoch, 20. Juni 2018 11:04
-
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
- Bearbeitet Peter Fleischer Donnerstag, 7. Juni 2018 20:29 Kommentare hinzugefügt
- Als Antwort vorgeschlagen Ivan DragovMicrosoft contingent staff, Moderator Montag, 11. Juni 2018 05:58
- Als Antwort markiert Ivan DragovMicrosoft contingent staff, Moderator Mittwoch, 20. Juni 2018 11:04
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- Bearbeitet Peter Fleischer Montag, 4. Juni 2018 14:25
- Als Antwort vorgeschlagen Ivan DragovMicrosoft contingent staff, Moderator Montag, 11. Juni 2018 05:58
- Als Antwort markiert Ivan DragovMicrosoft contingent staff, Moderator Mittwoch, 20. Juni 2018 11:04
-
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
-
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
- Bearbeitet Peter Fleischer Donnerstag, 7. Juni 2018 20:29 Kommentare hinzugefügt
- Als Antwort vorgeschlagen Ivan DragovMicrosoft contingent staff, Moderator Montag, 11. Juni 2018 05:58
- Als Antwort markiert Ivan DragovMicrosoft contingent staff, Moderator Mittwoch, 20. Juni 2018 11:04
-
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.