Laden...

Forenbeiträge von GeneVorph Ingesamt 180 Beiträge

31.12.2023 - 12:33 Uhr

Ah, sorry, ich wollte eigentlich noch den Code posten, falls mal jemand anderes ein ähnliches Problem hat und da nicht weiter kommt.

var colorItems = typeof(Colors).GetProperties();

cmbFontColors.ItemsSource = colorItems;

var defaultColor = colorItems.Where(c => c.Name == "Black").FirstOrDefault();

cmbFontColors.SelectedItem = defaultColor;

Wie Palladin es bereits erwähnt hat: beim Lesen des Codes wird nicht gleich ersichtlich warum, wieso, weshalb.

31.12.2023 - 12:28 Uhr

@Palladin007 Herzlichen Dank für deine Antwort und die Mühe die du dir gemacht hast, auch bezüglich der Erklärungen. Sehr cool - dadurch hatte ich beim Lesen das ein oder andere "Aha"-Erlebnis! Auf jeden Fall weiß ich jetzt, was "under the hood" passiert, und damit ist das Ganze schon etwas demystifiziert.

Bleibt bei meiner Lösung einzig ein kleiner "Schönheitsfehler": wenn ich einen Default setzen möchte (z. B. cmbFontColors.SelectedItem = [...]) scheitere ich mit dem Ausdruck hinter dem Gleichheitszeichen - so wie ich das jetzt verstanden habe müsste da ja auch ein PropertyInfo-Item hin. Das ließe sich über Zwischenschrite bewerkstelligen, hat aber den großen Nachteil, dass es sehr "unsexy" ist - da man Farben eher nicht mit PropertyInfos in Verbindung bringt.

Jetzt bin ich zwar reiner Hobby-Coder, aber ich frage mich, ob ich in zwei, drei Jahren direkt Blicke, warum da kein Brush drinne ist. Ich merke, ich entwickle gerade große Sympathie für die Zwischenklassenlösung 😃

31.12.2023 - 00:26 Uhr

Also: die gute Nachricht ist, dass ich über Umwege zu einer Lösung gekommen bin. Verstehen tu ich's nicht, aber das steht ja auf einem anderen Blatt.

Ich weiß auch nicht, ob es wirklich eine Lösung oder letztlich ein Hack ist - also bitte nicht gleich schlagen, falls ich hier Wurgs-Code geschrieben habe!

Da der Compiler ja eine InvalidCastException wirft, dachte ich zuerst, ich caste zum falschen Type - so hatte ich die Fehlermeldung ursprünglich verstanden. Man kann es wohl auch als "Aufruf" zum Casten in den Type PropertyInfo vestehen. Was ich jetzt letztlich getan habe. Darüber erhalte ich den Farbnamen - als string. Und das funktioniert. Warum auch immer. Hier der Code:

var propertyInfo = (PropertyInfo)e.AddedItems[0];

var editValue = propertyInfo.Name; //editValue ist hier effektiv ein string

ApplyPropertyValueToSelectedText(TextElement.ForegroundProperty, editValue);

Weil ich es erst nicht glauben konnte, habe ich editValue durch "Red" ersetzt - auch das funktioniert.

Leute, vielen Dank erstmal! Im neuen Jahr nehme ich dann die übrigen Fragen zum Thema Texteditor in Angriff - jetzt bin ich erstmal froh, dass ich prinzipiell Schriftart, Schriftgröße und Schriftfarbe einstellen kann. Die Details dann im neuen Jahr.

Ich wünsche euch einen guten Rutsch - man sieht sich!

Viele Grüße

Vorph

30.12.2023 - 23:50 Uhr

Hallo Hans,

ja, das weiß ich schon - das waren lediglich verzweifelte Versuche, die freilich von vornherein zum Scheitern verurteilt waren. Entweder ich stehe voll auf dem Schlauch oder... Keine Idee?

Benutze ich den falschen Type? Oder stimmt generell an meinem Vorgehen etwas nicht?

Ich habe versuchsweise im ViewModel mal ein Property mit dem Namen DefaultColor erstellt, das ich an die Foreground-Property der RichTextBox gebunden habe:

DefaultColor = new SolidColorBrush(Colors.Red);

Die App lädt - die Schrift ist rot. Voila.

Ich versteh's einfach nicht, warum das mit dem Color-Picker nicht geht. Ich fülle den mit

cmbFontColors.ItemsSource = typeof(Colors).GetProperties();

Liegt hier vielleicht erkennbar ein Fehler? Ich meine, ich bekomme die Farben wunderbar angezeigt in meiner ComboBox, aber das mit dem SelectedItem funktioniert nicht. Zumindest nicht im Codebehind. Aber ja anscheinend auch im ViewModel nicht richtig, zumindest nicht, solange das SelectedItem der ComboBox im Spiel ist.

30.12.2023 - 20:56 Uhr

Hm...das ist mehr als seltsam:

private void cmbFontColors_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var dim = "dim";

    try
    {
        //Brushes editValue = (Brushes)e.AddedItems[0];
        //ApplyPropertyValueToSelectedText(TextElement.ForegroundProperty, editValue);

        ApplyPropertyValueToSelectedText(TextElement.ForegroundProperty, Brushes.Red);

    }
    catch (Exception) { }

So funktioniert es, meine Methode wird aufgerufen, die Schriftfarbe ist Rot. Wie du siehst hatte ich vorher versucht das e.AddedItems-Objekt zu Brushes zu casten - und wieder passiert nichts. Zur Runtime kann ich zwar eine beliebige Farbe aus der Combo wählen. Untersuche ich aber meinen Code mittels Breakpoint, bekomme ich an genau dieser Stelle eine Fehlermeldung. Steh' ich auf dem Schlauch? Es müsste doch so eigentlich funktionieren, oder?

Übrigens: trotz Focus(); in der anderen Methode bleibt es dabei: zuerst muss ich was schreiben - wenn ich dann die Farbe ändere klappt es. Wenn ich im Konstruktor Focus() verwende scheint das nichts zu ändern. Erst schreiben, dann lassen sich Schriftgröße und Schriftart ändern. Aber das betrachtet ich als gesondertes Problem - ich versuche erstmal das mit der FontColor in den Griff zu bekommen. Dennoch vielen Dank!

EDIT: Habe es gerade noch mit e.AddedItems[0] as Brushes (Brush, Colors. SolidColorBrush...) probiert - dann ist editValue einfach null^^ Ebenso, wenn man statt e.AddedItems direkt cmbFontColors anspricht. Lässt sich einfach nicht casten!

30.12.2023 - 14:56 Uhr

Hallo,

ich bin gerade dabei mir einen einfachen Texteditor in einer WPF-Anwendung (.Net 6.0)  zusammenzustellen. Es hat viele Beispiele und Tutorials, die sich oft lediglich mit dem Befüllen der UI-Elemente beschäftigen (Comboboxes etc.), aber die wesentlichen Details aussparen. Ich habe zu dem Thema generell ein paar Fragen, möchte mich aber hier speziell folgendem Problem widmen:

Für den Texteditor benötige ich die Möglichkeit den Text in unterschiedlichen Farben darzustellen. Dazu habe ich eine Combobox mit den System-Colors befüllt. Da ich hier wirklich exclusiv mit der View befasst bin, habe ich mich entschlossen hier im Codebehind zu bleiben. Dazu folgender Code

public partial class TextEditorView : UserControl
{    
    public TextEditorView()
    {
        InitializeComponent();
              
        cmbFontColors.ItemsSource = typeof(Colors).GetProperties();       
    }
}

Mein XAML-Code für die ComboBox (meinen Color-Picker):

<ComboBox Name="cmbFontColors" Height="23" Width="80" Focusable="False" 
          SelectionChanged="cmbFontColors_SelectionChanged">
    <ComboBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <Rectangle Fill="{Binding Name}" Width="16" Height="16" Margin="0,2,5,2" />
                <TextBlock Text="{Binding Name}"/>
            </StackPanel>
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

Um die selektierte Farbe auf den Text anwenden zu können, habe ich einen Ereignishandler im Codebehind, der das SelectionChanged-Ereignis behandeln soll (immer wenn einen neue Farbe ausgewählt wird, soll diese auf den Text übertragen werden).

Hier mein Code dazu:

private void cmbFontColors_SelectionChanged(object sender, SelectionChangedEventArgs e)
{    
    try
    {
        Color editValue = (Color)e.AddedItems[0];
        ApplyPropertyValueToSelectedText(TextElement.ForegroundProperty, editValue);
    }
    catch (Exception) { }
}

private void ApplyPropertyValueToSelectedText(DependencyProperty formattingProperty, object value)
{
    if (value == null)
        return;

    if (rtbEditor != null)
    {
        rtbEditor.Selection.ApplyPropertyValue(formattingProperty, value);
        rtbEditor.Focus();
    }
}

Während der Compile-Time erhalte ich keine Fehlermeldung. Wenn ich die App starte kann ich zwar verschiedene Farben auswählen, diese werden allerdings nicht auf den Text übertragen.

Ich habe dazu einen Breakpoint in der SelectionChanged-Methode gesetzt. Ich erhalte eine Fehlermeldung in dieser Code-Zeile:

ApplyPropertyValueToSelectedText(TextElement.ForegroundProperty, editValue);

Fehlermeldung:

System.InvalidCastException: "Unable to cast object of type 'System.Reflection.RuntimePropertyInfo' to type 'System.Windows.Media.Color'."

Diese Ausnahme wurde ursprünglich von dieser Aufrufliste ausgelöst:
SoloAdventureCreator.View.TextEditorView.cmbFontColors_SelectionChanged(object, System.Windows.Controls.SelectionChangedEventArgs) in TextEditorView.xaml.cs

Offensichtlich ist 'Color' nicht der richtige Type. Ich habe es nun bereits mit SolidColorBrush und Brush probiert, aber auch damit taucht diese Exception auf.

Ich entnehme der Fehlermeldung zwar, dass es ein Problem beim Casten gibt, und weiß ja auch wo das Problem sitzt, weiß mir aber momentan keinen Rat, wie ich das beheben könnte. Laut Dokumentation müsste es ein Brush sein; das Property ist bindable.

Viele Grüße

Vorph

P.S: ich habe noch ein paar Folgefragen Richtung Texteditor: beim App-Start das Caret in der Richtextbox platzieren, Schriftarten ad hoc anwenden (klappt bei mir erst, wenn man in die Richtextbox klickt und schon was geschrieben hat) - neuer Thread oder hier posten? Möchte ja nicht das Forum mit meinen Texteditor-Fragen spammen, aber unter diesem Topic findets auch keiner?

20.12.2023 - 19:58 Uhr

Vielen Dank, Th69! MemoryStream, .rtf, TextRange, Encoding - mein C#-Wortschatz hat Zuwachs erhalten. Bleibt zu hoffen, dass ich über die Feiertage bissl zum Coden komme 😉

19.12.2023 - 21:10 Uhr

Hi,

eines vorweg: da meine Frage u. a. auch darauf abzielt zu erfahren, welche Begriffe mein Problem eingrenzen, bzw. am besten beschreiben, kann es gut sein, dass ich hier im falschen Forenbereich poste. Ich entschuldige mich daher an dieser Stelle, sollte ich im falschen Bereich gelandet sein - gerne verschieben!

Ich entwickle derzeit eine WPF-App unter .Net 6.0. Teil der App ist ein einfacher Texteditor. Der Editor selbst ist lediglich eine Richtextbox, bei der ich die Möglichkeit habe einfachste Formatierungen vorzunehmen, z.B. Textfarbe, Schriftart, Style (bold, italic, underline). Im Prinzip geht es darum, dass der User mit Hilfe des Texteditors einzelne Kapitel schreiben kann (Chapter), die später zu einem kompletten (formatierten) Text in beliebiger Reihenfolge kombiniert werden können. Mein Model dazu schaut folgendermaßen aus:

public class Chapter
{
	public int Id {get;set;}
	public int Index {get;set;}
	public string TextBody {get;set;} // 'string' ist hier mit Sicherheit der falsche/ungeeignete Type?
}

Das ist jetzt erstmal ein rudimentärer und naiver Entwurf der Klasse. Die Crux dürfte bei string TextBody liegen. Denn ich möchte ja keine Zeichenfolge in dem Sinn, sondern einen kompletten, formatierten Text.

Lösungsansätze?:

Eine nachvollziehbare aber bestimmt "unschöne" Lösung wäre es, jedes Chapter als .rtf-Datei zu speichern, nur um dann beim Erstellen der finalen Datei alle zu den jeweiligen Chaptern gehörigen .rtf-Dateien in der gewünschten Reihenfolge auszulesen und in einer neuen .rtf-Datei zusammenzuführen.

Wahrscheinlich gibt es auch eine xaml-Lösung, wobei das im Grunde in der Praxis dann ähnlich ablaufen dürfte wie Lösung 1?

FileStream?

Mir fehlen hier ganz ehrlich die Begrifflichkeiten (was ich bei meiner Google-Suche gemerkt habe: ich lande immer wieder bei Suchergebnissen, die zeigen wie man String.Format() benutzt...) - von daher wäre mir schon sehr mit Schubsern in die richtige Richtung geholfen in Bezug auf die korrekten Termini. Super wäre es, wenn ich die einzelnen Chapter in einer SQLite-Datenbank speichern könnte, aber das ist kein absolutes Muss: ich bin offen für Vorschläge.

Besten Dank im Voraus,

Gruß

Gene

19.12.2023 - 18:40 Uhr

Vielen Dank euch beiden - beide Versionen führen (natürlich) zum gewünschten Ergebnis! Auch wenn es peinlich ist: ich glaube, ich habe mich so sehr von der Tatsache täuschen lassen, dass in der TextEditorView im Designer die Images zu sehen waren, dass ich lange nicht auf den Slash gekommen wäre. Vielen Dank nochmal!

18.12.2023 - 21:13 Uhr

Hallo,

ich arbeite derzeit an einem wpf-Projekt, bei dem ein simpler Texteditor benötgt wird. Meine Idee ist, den Texteditor in ein eigenes Projekt auszulagern, so dass meine Projektmappe nun zwei Projekte beinhaltet:

  • Main-App
  • TextEditor

Im TextEditor-Projekt habe ich einen Ordner mit Namen "Images". Bis jetzt handelt es sich beim Inhalt lediglich um drei Bilddateien. I

Ich habe TextEditor in Main-App referenziert. Bisher habe ich eine MainView im Main-App-Projekt und eine TextEditorView im TextEditor-Projekt. Die TextEditorView schaut so aus:

<UserControl x:Class="TextEditor.Views.TextEditorView"
            <!-- die üblichen Namespaces wurden der Übersichtlichkeit wegen hier weggelassen-->            
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid Background="AliceBlue">
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition/>           
        </Grid.RowDefinitions>

        <DockPanel Grid.Row="0">
            <ToolBar>
                <Button x:Name="btnBold" Width="20" Height="20">
                    <Image Source="TextEditor;component/Images/bold.png"/>
                </Button>
                <Button x:Name="btnItalic" Width="20" Height="20">
                    <Image Source="TextEditor;component/Images/italic.png"/>
                </Button>
                <Button x:Name="btnUnderline" Width="20" Height="20">
                    <Image Source="TextEditor;component/Images/underline.png"/>
                </Button>
            </ToolBar>            
        </DockPanel>            
    </Grid>
</UserControl>

Dieses UserControl soll in der MainView der Main-App angezeigt werden. Hier der Code:

<Window x:Class="SoloAdventurer.Views.AppMainView"
        <!-- die üblichen Namespaces wurden zur Vereinfachung weggelassen, bis auf den folgenden: --
        xmlns:textEditorViews ="clr-namespace:TextEditor.Views;assembly=TextEditor"
        mc:Ignorable="d"
        Title="AppMainView" Height="450" Width="800">
    <Grid>
        <textEditorViews:TextEditorView/>
    </Grid>
</Window>

Das Problem: der Designer zeigt mir bei TextEditorView noch die Images an, in AppMainView jedoch nicht. Wenn ich einen build erstelle kann ich zwar die Buttons sehen, die Images werden aber scheinbar nicht geladen.

Habe ich etwas Wesentliches übersehen?

Ich arbeite zum ersten Mal mit zwei Projekten innerhalb einer Solution.

Vielen Dank,

Gruß

Gene

17.08.2023 - 17:22 Uhr

Sch.. - Anfängerfehler 😦

Danke, Abt.

16.08.2023 - 10:28 Uhr

Hallo,

ich beschäftige mich derzeit mit der Implementierung von Callbacks, bzw. Anwendungsmöglichkeiten. Es gibt keinen konkreten Anwendungsgrund - ich bin einfach über das Thema "gestolpert" und versuche es zu verstehen.

Allerorten liest man, Callbacks seien Pointer zu Methoden (Function-Pointer), beruhend auf Delegaten (C#). Soweit - so gut.

Ich habe mir also eine Klasse gebastelt, die einen Callback an eine andere Klasse übergibt. Der Code funktioniert, ich verstehe allerdings einen ganz bestimmten Aspekt nicht, auf den ich gleich näher eingehe. Zunächst mein Code in einer Konsolen-App:

public class ClassA
    {
        public int CallMeAnytime(int x)
        {
            x = 100; 

            return x;
        }
    }
    
     public class ClassB
    {
        public int IdoSomeWorkHere(int a, int b, Func<int, int> callback)
        {
            int c = a + b;

            callback(c);

            c = c + c;

            return c;
        }
    }
    
    //Programm.cs
    using Callbacks;

ClassA a = new ClassA();
ClassB b = new ClassB();

var result = b.IdoSomeWorkHere(5, 5, a.CallMeAnytime);

Console.WriteLine(result);

//Output = 20 

Was mich irritiert ist folgendes:

  • Zum Zeitpunkt, zu dem callback(c) aufgerufen wird, ist c = 10. Danach verändert sich c im Code der Klasse B, wird aber dennoch "aktualisiert". Wie erklärt sich das? Hat das mit der "Art" von Pointern zu tun?

Anders gesagt: heißt das, so lange der Wert, der durch den Callback zurückgegeben wird, sich verändert, wird die entsprechende Methode in Klasse A ständig "aktualisiert"? Das würde zumindest insofern Sinn machen, da Callbacks ja auch für Status-Updates verwendet werden, z. B. den Fortschritt bei einem langen Ladevorgang etc. Liege ich da in etwa richtig? Eventuell ergeben sich noch Folgefragen.

Einstweilen vielen Dank,

Vorph

24.10.2022 - 10:03 Uhr

Hallo Palladin007,

vielen Dank für deine ausführliche Antwort! Deine Mühe (zu nachtschlafener Zeit) hat sich absolut gelohnt - heute morgen fallen mir gleich reihenweise die Schuppen von den Augen! Alles, was du unter "ein paar Anmerkungen" geschrieben hast, hilft mir weit über die eigentliche Fragestellung hinaus weiter - besonders hinsichtlich der Planung für kommende Projekte.

Exakt das ist dein Problem.
Das ist aber nichts hinter den Kulissen, sondern im Code "right in your face"

Ich würde jetzt gerne sagen "Ich wusste es!", aber ich wusste ehrlich nicht genau wo 😉 Und das beruhte auf der einen "kleinen" Fehlannahme (die du glücklicherweise auch herausgestellt hast), dass

dann gehört dazu die RedView, bzw. umgekehrt

dies zutreffend ist. Tatsächlich hatte ich


                 <ContentControl>
                    <viewModels:RedViewModel/>
                </ContentControl>

auch schon gegen


                 <ContentControl>
                    <views:RedView/>
                </ContentControl>

getauscht. In beiden Fällen wird die jeweilige View korrekt im Designer angezeigt. Nachträglich möchte ich mich an dieser Stelle selbst ohrfeigen, denn hätte ich richtig geschaltet, hätte ich bemerkt, dass nun der OnPropertyChanged nicht mehr ausgelöst wird! Damit hätte ich eine erste Spur gehabt, dass das was ich sehe und das, was der Code tut nicht korreliert. Und dann hätte mich auch die Tatsache stutzig machen müssen, dass diese "kleine" Änderung plötzlich dazu führt, dass View und ViewModel sich plötzlich nicht mehr finden --> ergo muss das mit dem "umgekehrt" falsch sein und es ist an der Stelle einleuchtend, dass meine Vermutung (ein neues VM würde instanziiert) wahrscheinlich an genau der Stelle passiert 😭

Tatsächlich waren die Tutorials mehrere mit dem Thema ViewModel-Navigation: und da funktioniert es ja auch prächtig. Meine Rückschlüsse waren die Falschen! Denn beim Binding auf Properties läuft es ja auch nach dem Schema {Binding <PropertyName>} - und Halleluja, da habe ich zwei ViewModel-Properties und vermassele das Binding 1x1!

Wie gesagt: 1000-Dank für deine Antwort, das hat mir sehr weitergeholfen. Manchmal muss man eben einfach seine Annahmen kritisch hinterfragen^^
Übrigens:

Besser wäre, wenn Du etwas anderes hast, was den Fluss steuert, also eine Art Backend oder Business-Layer oder in der MVVM-Terminologie das Model.

Genau so hatte ich es vor, daher eine kleine Anschlussfrage: ich hatte vor diese Klasse "Mediator" oder meinetwegen auch "ViewModelMediator" zu nennen. Nun sehe ich gerade, da gibt's das Mediatorpattern und ich bin noch nicht ganz durch. Also a) entweder ist das genau das worüber wir hier reden oder b) es ist doch waa anderes. Und nein, ich bin nicht in einem Team und werde wohl nie in die Verlegenheit kommen, einem Team anzugehören - bin wirklich nur Hobbycoder. Aber: ist der Name "Mediator" i. d. Fall geschickt gewählt oder gibt das Verwirrung bezüglich Mediator-Pattern?

Viele Grüße
Vorph

23.10.2022 - 23:47 Uhr

Hallo,
nach etlichen Versuchen, Tutorials und der oft nicht ganz einfachen MSDN wähnte ich mich bereits am Ziel, mit dem simpelst vorstellbaren Ziel: Ein Hauptfenster, mit zwei verschiedenen GUI-Bereichen, jedes mit eigener View (usercontrol) und eigenem VIewModel. Eines enthält eine Textbox, das andere einen Textblock - was ich in die Textbox eingebe, soll im Textblock zu sehen sein.
Mein Code verwendet keinen Code behind. Ich habe versucht nach meinem Kenntnis- und Wissensstand (gefühlt immer noch Anfänger...) MVVM umzusetzen.

[WPF app, .NET 6]

Ich weiß, meine Frage ist vermutlich ein alter Hund, aber es würde mir sehr helfen, wenn mir jemand aufzeigen könnte an welchem Punkt und warum mein Code sich nicht so verhält, wie von mir beabsichtigt. Ich werde außerdem am Ende kurz meine Gedanken zu einzelnen Codesegmenten darlegen, in der Hoffnung, dass mein Fehler evtl. auf Missverständnissen oder Fehlinterpretationen beruht.

Ohne weitere Worte - hier der Code

  1. MainWindowView

<Window x:Class="LearnViewModelsAndViews.Views.MainWindowView"
        [...]
    <Window.DataContext>
        <viewModels:MainWindowViewModel/>
    </Window.DataContext>
    
    <Grid>
        <Grid Margin="10">
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>

            <Grid Grid.Row="0">
                <ContentControl>
                    <viewModels:RedViewModel/>
                </ContentControl>
            </Grid>

            <Grid Grid.Row="1">
                <ContentControl>
                    <viewModels:BlueViewModel/>
                </ContentControl>
            </Grid>
        </Grid>
    </Grid>
</Window>

Und das zugehörige MainWindowViewModel


 public class MainWindowViewModel: NotifyPropertyChangedBase
    {
        private RedViewModel _redVM;
        public RedViewModel RedVM
        {
            get { return _redVM; }
            set { OnPropertyChanged(ref _redVM, value); }
        }

        private BlueViewModel _blueVM;
        public BlueViewModel BlueVM
        {
            get { return _blueVM; }
            set { OnPropertyChanged(ref _blueVM, value); }
        }


        public MainWindowViewModel()
        {
            RedVM = new RedViewModel();
            BlueVM = new BlueViewModel();

            SetEvents();
        }

        private void SetEvents()
        {
            RedVM.OnInputTextChanged += BlueVM.HandleInputTextChanged;
        }
    }
}

XAML von RedView


<UserControl x:Class="LearnViewModelsAndViews.Views.RedView"
          [...]
    <Grid Background="LightCoral">       
        <Grid>
            <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
                <TextBox Width="220" Height="60" Text="{Binding InputText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" FontSize="18"/>
            </StackPanel>
        </Grid>            
    </Grid>
</UserControl>

Und das RedViewModel


public class RedViewModel: NotifyPropertyChangedBase
    {
		public event EventHandler OnInputTextChanged;

		private string _inputText;
		public string InputText
		{
			get { return _inputText; }
			set 
			{ 
				OnPropertyChanged(ref _inputText, value); 
				OnInputTextChanged?.Invoke(value, EventArgs.Empty);
			}
		}

		public RedViewModel()
		{
		}
	}

Hier die BlueView.xaml


<UserControl x:Class="LearnViewModelsAndViews.Views.BlueView"
             [...]
    <Grid Background="LightBlue">
        <Grid>
            <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
                <TextBlock Width="220" Height="60" FontSize="18" Foreground="Red" Text="{Binding DisplayText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>                
            </StackPanel>                        
        </Grid>
            
    </Grid>
</UserControl>

Und das zugehörige ViewModel


 public class BlueViewModel: NotifyPropertyChangedBase
    {
		private string _displayText;
		public string DisplayText
		{
			get { return _displayText; }
			set { OnPropertyChanged(ref _displayText, value); }
		}
		
		public BlueViewModel()
		{
		}

		public void HandleInputTextChanged(object sender, EventArgs e)
		{
			DisplayText = sender.ToString();
		}

	}

Last but not least meine App.xaml


<Application x:Class="LearnViewModelsAndViews.App"
           [...]
             StartupUri="Views/MainWindowView.xaml">
    <Application.Resources>

        <DataTemplate DataType="{x:Type viewModels:RedViewModel}">
            <views:RedView/>
        </DataTemplate>

        <DataTemplate DataType="{x:Type viewModels:BlueViewModel}">
            <views:BlueView/>
        </DataTemplate>

    </Application.Resources>
</Application>

OK: mein Projekt wird wie vorgesehen erstellt und ich bekomme ein Projektfenster angezeigt mit meinem RedView im oberen Bereich und meiner BlueView im unteren Bereich. Soweit alles paletti.
Gebe ich nun Text in die Textbox der RedView ein, wird das OnPropertyChanged() zwar gefeuert, der Event darunter (OnInputTextChanged) ist jedoch null.

Meine Fragen soweit: wo ist der Denkfehler? Was muss ich ändern?

Die Tatsache, dass der event null ist, interpretiere ich so, dass mein BlueVM nicht das ViewModel ist, das ich anzusprechen beabsichtige. Hier kommt nun (mit ziemlicher Sicherheit) Halbwissen ins Spiel:

  • mit den DataTemplates in der App.xaml sage ich doch nichts anderes als, dass wenn das ViewModel vom Typ RedViewModel ist, dann gehört dazu die RedView, bzw. umgekehrt. Sehr vereinfacht und vermutlich ungelenk formuliert. Aber auch völlig falsch?
  • ich instanziiere ein RedViewModel (RedVM) und ein BlueViewModel (BlueVM) auf meinem MainViewModel. Da ich die Werteveränderung von InputText per event weiterleiten möchte, platziere ich hier meine Subscription ( RedVM.OnInputTextChanged += BlueVM.HandleInputTextChanged; ) Allerdings werden diese ViewModels vermutlich gar nicht angesprochen: irgendwo "hinter den Kulissen" erstelle ich wohl (unbeabsichtigt) eine andere Instanz von RedViewModel und BlueViewModel - und dann ist klar, dass der Compiler nicht wissen kann welche Instanz jetzt implizit gemeint ist.

Wie gesagt: für konkrete Hinweise bin ich euch sehr dankbar. Ich möchte mir nur ungern ein neues Hobby suchen 😉

Gruß
Vorph

13.03.2021 - 13:06 Uhr

Hallo Wilfried, danke für deinen Beitrag.

Ich habe mir noch einmal die Dokumentation zu Gemüte geführt (hier und hier) - was aber nur mehr zur Verwirrung beiträgt. Ich kann mir zwar übersetzen was da steht, aber was inhaltlich der Unterschied zwischen "relates a universal serial bus (USB) controller and the CIM_LogicalDevice instance connected to it" und "manages the capabilities of a universal serial bus (USB) controller" ist, bleibt mir damit ein Rätsel (für die Zustände eines Controllers ist eine andere Klasse zuständig als für die Kommunikation zwischen controller und device? Worin liegt dann der Unterschied zwischen Controller und Device?).

Ich hab bei mir auf dem PC grade mal geschaut, da gibt es weder bei Win32_USBControllerDevice noch bei Win32_USBController das Property "ProductName".

Stimmt, das habe ich nach dem Post von Th69 ebenfalls herausgefunden. Wie gesagt, das hatte ich in einem Post gefunden und (ganz unschuldig) statt DeviceId eingesetzt.

Tatsächlich funktioniert für meinen ursprünglichen Code "Antecedent", wenn ich die Win32_USBControllerDevice-Klasse ansteuere.

Aber ich bin ein Schrittchen weiter:
Mit ManagementObjectSearcher(@"Select * From Win32_USBControllerDevice")) und cmbDevices.Items.Add((string)device.GetPropertyValue("Antecedent")) bekomme ich 37 Einträge in meiner ComboBox angezeigt (gebs jetzt direkt in der ComboBox aus).
Beispiel (erster Eintrag):
\ARBEITS-PC\root\cimv2:Win32_USBController.DeviceID="PCI\VEN_1033&DEV_0194&SUBSYS_84131043&REV_04\4&DDEC341&0&00E1"

Mit ManagementObjectSearcher(@"Select * From Win32_USBController")) und cmbDevices.Items.Add((string)device.GetPropertyValue("DeviceId")) bekomme ich 4 in meiner ComboBox angezeigt.
Beispiel (erster Eintrag):
PCI\VEN_1033&DEV_0194&SUBSYS_84131043&REV_04\4&DDEC341&0&00E1

Tatsächlich weiß ich jetzt überhaupt nicht, was ich da angezeigt bekomme^^

Erwartet hätte ich z. B. im zweiten Fall die Id's der aktuell angeschlossenen Devices (Keyboard, Mouse) erwartet - ich bekomme aber 4 Einträge. Wenn ich außerdem zusätzlich einen USB-Stick anschließe sind es immer noch 4 Einträge, und zwar genau dieselben wie zuvor.

Die 37 Einträge aus dem ersten Fall erschließen sich mir hingegen überhaupt nicht: sind das nun Geräte, Ports, Geräte und Ports und wenn ja, wie komme ich dann um Himmelswillen auf 37??

BTW: Danke für den Topp zum WMI-CodeCreator - sobald ich kann, werde ich mir den mal genauer ansehen.

13.03.2021 - 12:16 Uhr

Wie kommst du auf "ProductName"?

Zuerst hatte ich DeviceID, dann ProductName, nachdem ich es bei stackoverflow in einem anderen thread gefunden hatte. Beides funktionierte nicht.

Ich habe jetzt folgendes verändert:
aus


var searcher = new ManagementObjectSearcher(@"Select * From Win32_USBControllerDevice"))

wurde jetzt


var searcher = new ManagementObjectSearcher(@"Select * From Win32_USBController"))

Bei "ProductName" stürzt die App weiterhin ab.

Wenn ich nun "Name" eingebe, läuft die Schleife ohne Abbruch durch. Leider ist das Problem damit nicht gelöst (s. Screenshot):
Diesmal finden sich in der devicesCollection nur noch 4 Einträge (ehemals 35, wenn man "ProductName" eingibt): wenn ich nach der Schleife einen BreakPoint setze, dann sieht man unter Ergebnisansicht der devicesCollection dieselbe Fehlermeldung wie zuvor: "Fehler = Die Funktionsauswertung wurde deaktiviert, weil bei einer vorhergehenden Funktionsauswertung das Timeout überschritten wurde."

Also nach meinem Verständnis bedeutet das:

  • bei Win32_USBControllerDevice findet das Programm vor der Schleife 35 Einträge; ich kann jedoch nicht feststellen, was genau, denn beim ersten Schleifendurchlauf stürzt die App ab.
  • bei Win32_USBController wird die Schleife durchlaufen - 4 Einträge werden gefunden, aber ich kann dennoch nicht feststellen von was, denn ich finde keine Möglichkeit auf die einzelnen Items zuzugreifen.

Kurz zu WMI-Tool: ich hatte gestern abend noch WbemTest unter Windows 10 gestartet - ich meine mich zu erinnern, dass z. B. "DeviceID" im USBControllerDevice-Namespace zu finden war: trotzdem der Absturz. Das eigentiche Problem dürfte m. E. vor der Schleife zu finden sein. Ich werde dennoch mit SimpleWMIView nochmal mein Glück versuchen.

Vielen Dank soweit.

EDIT: im screenshot ist der breakpoint noch vor einstieg in die schleife - unter ergebnisansicht steht "Zeitüberschreitung Funktionsevaluierug". Setzt man den BP nach der Schleife, kommt die Meldung von oben.

13.03.2021 - 01:28 Uhr

Hallöchen,

vorweg: ich bin mir nicht sicher, ob mein Post hier im richtigen Teil des Forums gelandet ist! Falls nicht: sorry und bitte ggf. verschieben! Danke.

Ich möchte in einer WindowsForms-App alle USB-Geräte in einer Collection auflisten. Dabei habe ich mich an diesem Beispiel orientiert. Meinen Code habe ich abgeändert, so dass er wie folgt ausschaut:


private void frmMainDisplay_Load(object sender, EventArgs e)
        {
            List<string> devices = new List<string>();
            ManagementObjectCollection devicesCollection;

            using (var searcher = new ManagementObjectSearcher(@"Select * From Win32_USBControllerDevice"))
                devicesCollection = searcher.Get();

            foreach (var device in devicesCollection)
            {
                devices.Add((string)device.GetPropertyValue("ProductName"));
            }

            devicesCollection.Dispose();
        }

Da ich an den Devices und nicht an den Hubs interessiert bin, suche ich in Win32_USBControllerDevices.

Wenn ich meinen Code ausführe, stürzt er in der foreach-Schleife ab. Ich erhalte folgende Fehlermeldung:

Fehlermeldung:
System.Management.ManagementException: "nicht gefunden"
Diese Ausnahme wurde ursprünglich von dieser Aufrufliste ausgelöst:
[Externer Code]
VGACaptureDevice.frmMainDisplay.frmMainDisplay_Load(object, System.EventArgs) in Form1.cs
[Externer Code]

Beim Debugging habe ich festgestellt, dass im Ausgagbefenster folgendes angezeigt wird:

Ausnahme ausgelöst: "System.Management.ManagementException" in System.Management.dll
Ein Ausnahmefehler des Typs "System.Management.ManagementException" ist in System.Management.dll aufgetreten.
Nicht gefunden

Durch setzen eines Break-Points habe ich außerdem festgestellt, dass devicesCollection vor Ausführen der foreach-Schleife bei den Properties unter Count = 35 aufweist (ich nehme an, das sind sämtliche USB-devices plus hubs plus BlueTooth plus ?), ich diese 35 Items aber nicht näher Anschauen kann: unter Ergebnisansicht steht: "Fehler = Die Funktionsauswertung wurde deaktiviert, weil bei einer vorhergehenden Funktionsauswertung das Timeout überschritten wurde. Sie müssen die Ausführung fortsetzen, um die Funktionsauswertung wieder zu aktivieren." Tue ich das, stürzt die App allerdings wie zuvor beschrieben ab.

Um wirklich sämtliche Fehlerquellen auszuschließen, hier mal mein Vorgehen bis zu diesem Punkt: ich musste nämlich feststellen, dass es nicht genügt den Namespace System.Management per using hinzuzufügen; man muss auch händisch einen Verweis setzen. Dies habe ich getan mit

  • Rechtsklick auf mein Projekt --> Hinzufügen --> Verweis --> Häkchen gesetzt bei System.Management und System.Management.Instrumentation
  • OK geklickt.

Eine Google-Suche nach dem Fehler brachte zu Tage, dass möglicherweise das WMI-Repository von Windows beschädigt sein könnte. Ich habe dazu in der Powershell winmgmt /verifyrepository ausgeführt. "Leider" war das Ergebnis "Das Repository ist konsistent".

Damit gehen mir leider derzeit die Ideen aus - mein Code besteht lediglich aus den paar Zeilen weiter oben und die Fehlermeldung lässt mich leider auch im Regen stehen. Hat jemand eine Idee, was da falsch laufen könnte?

Vielen Dank.
Gruß
vorph

13.03.2021 - 01:02 Uhr

Ich bin kein Anwalt, aber nach meinem Verständnis begehst Du damit ein Lizenzvergehen.

Yieks - das sei mir fern 🙂 !

Danke für die ausführlichen Erläuterungen, Abt; und wieder ein bisschen schlauer 😉

Statisches Linken ist einfach nicht erlaubt.

Na ja, so gesehen...passt auch irgendwie nicht zum dem d in dll, oder?

[...]dazu mußt du den Ordner nur in der app.config bekannt machen, s. z.B. Loading .NET Assemblies out of Seperate Folders (Stichwort: privatePath).

Genau so habe ich es jetzt auch gemacht. Vielen Dank.

Gut, dann ist meine Frage somit beantwortet. Vielleicht noch ergänzend der ausdrückliche Hinweis, dass ich mir über die lizenzrechtlichen Bestimmungen diesbezüglich nicht bewusst gewesen bin - nicht, dass meine Frage hier als Hilfe zum Brechen von Lizenzrecht missverstanden wird 😉

viele Grüße
Vorph

12.03.2021 - 12:50 Uhr

Hallo,

ich habe eine Windows-Forms-App mit mehreren AForge-Libararies erstellt und möchte diese nun zu einer .exe kompilieren. Das klappt auch problemlos, allerdings erhielt ich beim Start die Fehlermeldung:

Fehlermeldung:
Microsoft .NET Framework
Unbehandelte Ausnahme in Anwendung. Klicken Sie auf "Weiter" um den Fehler zu ignorieren und die Anwendung fortzusetzen. Wenn Sie auf "Beenden" klicken, wird die Anwendung sofort beendet.
Die Datei oder Assembly "AForge Video.DirectShow Version=2.2.5.0.Culture=neutral PublicKey Token=61ea4348d43881b7" oder eine Abhängigkeit davon wurde nicht gefunden. Das System kann die angegebene Datei nicht finden.

Was ich dazu per Google gefunden habe war folgendes: offensichtlich müssen die Assembly-Dateien im selben Ordner abgelegt werden, wie die .exe auch.
Das habe ich nun getan und damit läuft die App auch.

Meine Frage ist folgende: gibt es eine Möglichkeit, diese Dateien in die .exe mit einzubinden?

Es sind nämlich insgesamt 19 Dateien und ich kann sie auch nicht einfach in einen Sub-Folder der Anwendung unterbringen - das führt dann nur wieder zu selben Fehlermeldung.
Was ich sonst dazu gefunden habe ist, dass man die Dateien ins .exe-Verzeichnis kopieren muss, oder eine Setup-Datei schreiben, die die Assembly aus einem zip-Ordner extrahiert oder ähnliches. Wie gesagt: gibt es keine Möglichkeiten die in der .exe unterzubringen?

Vielen Dank,
Gruß
vorph

02.03.2021 - 22:44 Uhr

Du kannst dem Template nicht null zuweisen, aber wo genau das auftritt, sollte dir die Fehlermeldung sagen.

Die komplette Fehlermeldung ist ja mit angegeben (s. Post 1) - expliziter wird es leider nicht 🙁

Ansonsten scheint es in deinem Code noch mehrere andere Merkwürdigkeiten zu geben. Wo defininierst du z.B. das Template für "ExpanderHeaderCollapsedType"

In UserControl.Resources (mir schleierhaft warum ich das vergessen hatte - sorry)


<DataTemplate x:Key="ExpanderHeaderCollapsedType">
            <StackPanel>
                <TextBlock Text="{Binding Path=DataContext.Name, RelativeSource={RelativeSource AncestorType=Expander}}"
                           FontSize="16" FontWeight="Bold"/>
                <TextBlock>
                    <TextBlock.Style>
                        <Style TargetType="TextBlock">
                            <Setter Property="Text">
                                <Setter.Value>
                                    <MultiBinding Mode="OneWay" Converter="{StaticResource AverageValueConverter}" ConverterParameter="Type">
                                        <Binding  Path="DataContext" RelativeSource="{RelativeSource FindAncestor, AncestorType=Expander, AncestorLevel=1}"/>
                                        <Binding  RelativeSource="{RelativeSource FindAncestor, AncestorType=DataGrid, AncestorLevel=1}"
                                                  Path="DataContext.AllGradesCV.View.Groups"></Binding>
                                    </MultiBinding>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </TextBlock.Style>
                </TextBlock>
            </StackPanel>
        </DataTemplate>

... und warum gibst du als TargetNullValue für einen String-Wert ein "true" an?

Gute Frage - weil ich schlicht keine Ahnung hatte, was ich mit dem Fehler anfangen soll, und beim googeln dann auf diese Monstrosität gestoßen bin. Im übrigen ist es egal ob ich true oder PaulePanter eingebe - das ändert schlicht nüscht.

Dürfte ich meine Eingangsfrage vlt. umformulieren? Ich denke mit diesem "Null-Type-Value" verrenne ich mich in eine ganz falsche Richtung. Nachdem ich es jetzt tagelang probiert habe, scheint mir, die eigentliche Frage müsste lauten:
wie erreiche ich es, dass beim Gruppieren der jeweilige Expander abhängig von der angeklickten DataGridColumn UND dem Zustand von ExpandedState eine bestimmte Funktion über einen ValueConverter auslöst?

Hier ein funktionierendes Beispiel (eine andere View in meinem Projekt, die aber fast dieselbe Funktionalität bereitstellt):
Hier wird im ValueConverter lediglich festgestellt, ob ein bestimmtes Objekt (Eintrag im DataGrid) gerade das aktuelle Objekt ist. Wenn ja, werden alle GroupItems collapsed, bis auf dasjenige, zu dem das akutelle Objekt gehört (konkret: du hast Schüler verschiedener Klassen. Aus der Klasse A1 ist Paule Panter angeklickt. Beim Gruppieren werden nun alle Klassen gruppiert und in einem kollabierten Expander zusammengefasst, außer die Klasse, in der Paule Panter ist):


<UserControl.Resources>
       <!-- Hier wird der ValueConverter deklariert. Dieser ist eine .cs-Datei in meinem Projekt (s. unten) -->
        <conv:ExpandConverter x:Key="ExpanderStateConverter"/>

<!-- Ein enum (ExpandedState) entscheidet darüber, wie der Expander sich verhalten soll (collapsed, expanded, alle collapsed, bis auf das GroupItem, das den aktuell im DataGrid ausgewählten Eintrag erhält. Da im ExpanderHeader unterschiedliche Infos je nach Zustand angezeigt werden, gibt es zwei verschiedene Templates-->

        <DataTemplate x:Key="ExpanderHeaderExpanded">
            <StackPanel>
                <TextBlock Text="{Binding Path=DataContext.Name, RelativeSource={RelativeSource AncestorType=Expander}}"
                           FontSize="16" FontWeight="Bold"/>
                <TextBlock Text="{Binding Path=DataContext.ItemCount, StringFormat=Schüler: {0}, RelativeSource={RelativeSource AncestorType=Expander}}"/>
            </StackPanel>
        </DataTemplate>

        <DataTemplate x:Key="ExpanderHeaderCollapsed">
            <StackPanel>
                <TextBlock Text="{Binding Path=DataContext.Name, RelativeSource={RelativeSource AncestorType=Expander}}"
                           FontSize="16" FontWeight="Bold"/>
                <TextBlock Text="{Binding Path=DataContext.ItemCount, StringFormat=Schüler: {0}, RelativeSource={RelativeSource AncestorType=Expander}}"
                           FontSize="12" FontWeight="Bold"/>
            </StackPanel>
        </DataTemplate>
       
<!-- GroupItem-Style für den Expander. Je nachdem, ob das ExpanderHeader expanded oder collapsed ist wird hier das Header-Template geladen-->
        <Style x:Key="GroupItemStyle" TargetType="Expander">           

            <Setter Property="Background" Value="#FF5CB9EE"/>
            <Setter Property="ExpandDirection" Value="Down"/>
            <Style.Triggers>
                <Trigger Property="IsExpanded" Value="False">
                    <Setter Property="HeaderTemplate" Value="{StaticResource ExpanderHeaderCollapsed}"/>
                </Trigger>
                <Trigger Property="IsExpanded" Value="True">
                    <Setter Property="HeaderTemplate" Value="{StaticResource ExpanderHeaderExpanded}"/>
                </Trigger>

<!-- GroupItem-DAtaTrigger: Hier wird an den enum ExpandedState im ViewModel gebunden... -->
                <DataTrigger  Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Path=DataContext.ExpandedState}" Value="expandAll">
<!-- ...und wenn dessen Value "IsExpanded ist, dieser Code ausgeführt: -->
                    <Setter Property="IsExpanded">
                        <Setter.Value>
                            <MultiBinding Mode="OneWay" Converter="{StaticResource ExpanderStateConverter}" ConverterParameter="expandAll">
                                <Binding  Path="DataContext" RelativeSource="{RelativeSource Self}"/>
                                <Binding  RelativeSource="{RelativeSource FindAncestor, AncestorType=DataGrid, AncestorLevel=1}"
                Path="DataContext.SelectedStudent"></Binding>
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>

<!-- ...wenn der Wert "collapseAll" ist, wird dieser Code ausgeführt: -->
                <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Path=DataContext.ExpandedState}" Value="collapseAll">
                    <Setter Property="IsExpanded">
                        <Setter.Value>
                            <MultiBinding Mode="OneWay" Converter="{StaticResource ExpanderStateConverter}" ConverterParameter="collapseAll">
                                <Binding  Path="DataContext" RelativeSource="{RelativeSource Self}"/>
                                <Binding
                RelativeSource="{RelativeSource FindAncestor, AncestorType=DataGrid, AncestorLevel=1}"
                Path="DataContext.SelectedStudent"></Binding>
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>

<!-- ...und wenn der Wert "collapseAllButSelected" ist, wird dieser Code ausgeführt: -->
                <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Path=DataContext.ExpandedState}" Value="collapseAllButSelected">
                    <Setter Property="IsExpanded">
                        <Setter.Value>
                            <MultiBinding Mode="OneWay" Converter="{StaticResource ExpanderStateConverter}" ConverterParameter="collapseAllButSelected">
                                <Binding  Path="DataContext" RelativeSource="{RelativeSource Self}"/>
                                <Binding
                RelativeSource="{RelativeSource FindAncestor, AncestorType=DataGrid, AncestorLevel=1}"
                Path="DataContext.SelectedStudent"></Binding>
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>

            </Style.Triggers>
            
        </Style>
        
    </UserControl.Resources>

Im DataTrigger findet ja so eine Art if/then/else Abfrage statt (wenn ExpandedState == collapseAll -> then...).
Alles was ich jetzt mit diesem Post erreichen wollte ist eigentlich: wie mache ich das, wenn nun noch eine Condition hinzukommt? Also:
if (ExpandedState == collapseAllButSelected && DataGrid.ColumnName == "xyu")?
Dieser Gedankengang hat mich überhaupt erst auf MultiDataTrigger gebracht.

Der Vollständigkeit halber hier mal noch der ValueConverter:


public class ExpandConverter : IMultiValueConverter
    {
        private bool ItemInFilterGroup(object[] value)
        {
            CollectionViewGroup oc;
            Student x = new Student();

            //CollectionViewGroup 
            //1. Evalute type on [0]
            if (value[0] is CollectionViewGroup)
            {
                oc = value[0] as CollectionViewGroup;
            }
            else
            {
                oc = null;
            }

            if (value[1] is Student)
            {
                x = value[1] as Student;
            }

            if (oc != null && x != null)
            {
                if (oc.Items.Contains(x))
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }

            return false;
            
        }

        public object Convert(object[] value, Type targetType, object parameter, CultureInfo culture)
        {
            var x = parameter as string;

            if (x == "expandAll")
            {                
                return true;
            }
            else if (x== "collapseAll")
            {
                return false;
            }
            else if (x == "collapseAllButSelected")
            {
                return ItemInFilterGroup(value);
            }

            return false;
        }       

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

26.02.2021 - 12:17 Uhr

Hallo,
folgendes Problem versuche ich derzeit zu lösen:

Ich gruppiere Daten in einem DataGrid. Dazu verwende ich einen Expander. In meinem ViewModel ist ein enum, der drei Expander-Zustände deklariert:

  • collapseAll
  • expandAll
  • collapseAllButSelected

collapseAll: alle GroupItems werden kollabiert, d.h. die Expander aller GroupItems sind collapsed
expandAll: alle GroupItems sind geöffnet, d.h. die Expander aller GroupItems sind expanded
collapseAllButSelected: alle GroupItems sind kollabiert, es sei denn, ein Item aus der Gruppe wurde ausgewählt.

Ich habe dazu im XAML code bereitgestellt, der immer dann, wenn man auf einen ColumnHeader des DAtaGrids klickt ein Command auslöst.
Das Command initiiert einen Zähler, der je nach Anzahl der Klicks den ExpanderState verändert:
Beim ersten Klick auf den ColumnHeader collapseAll, beim zweiten Klick expandAll, beim dritten Klick collapseAllButSelected.
Diese Funktionalität habe ich bereits bereitgestellt – das funktioniert also.

Was ich nun vorhabe ist folgendes:1. Wenn die Elemente im DataGrid gruppiert werden…
a) frage den ExpanderState ab
b) prüfe, welche Column angeklickt wurde (welcher Name hat der Header)
c) je nachdem welche Bedingung zutrifft wähle ein entsprechendes Expander-Header-Template
Das könnte dann also so aussehen:
Wenn der ExpanderState collapseAll ist (a) UND die Column mit dem Header „Fach“ (b) angeklickt wurde lade ExpanderHeader-Template x (c).
Wenn der ExpanderState collapseAll ist (a) UND die Column mit dem Header „Note“ angeklickt wrude lade ExpanderHeader-Template y (c).
Wenn der ExpanderState expandAll ist (a) UND die Column mit dem Header „Fach“ (b) angeklickt wurde lade ExpanderHeader-Template z (c).
Wenn der ExpanderState expandAll ist (a) UND die Column mit dem Header „Note“ angeklickt wrude lade ExpanderHeader-Template u (c). … usw. usf.

Natürlich wollte ich „einfach“ anfangen und erstmal einen Prüffall durchspielen.
In meinem xaml-code sieht das so aus:


<!-- DataTemplates for the EXPANDER HEADERs -->
        <DataTemplate x:Key="ExpanderHeaderExpanded">
            <StackPanel>
                <TextBlock Text="{Binding Path=DataContext.Name, RelativeSource={RelativeSource AncestorType=Expander}}"
                           FontSize="16" FontWeight="Bold"/>
                <TextBlock Text=""/>
            </StackPanel>
        </DataTemplate>

        <DataTemplate x:Key="ExpanderHeaderCollapsed">
            <StackPanel>
                <TextBlock Text="{Binding Path=DataContext.Name, RelativeSource={RelativeSource AncestorType=Expander}}"
                           FontSize="16" FontWeight="Bold"/>
                <TextBlock>
                    <TextBlock.Style>                        
                        <Style TargetType="TextBlock">
                            <Setter Property="Text">
                                <Setter.Value>
                                    <MultiBinding Mode="OneWay" Converter="{StaticResource AverageValueConverter}" ConverterParameter="Subject">
                                        <Binding  Path="DataContext" RelativeSource="{RelativeSource FindAncestor, AncestorType=Expander, AncestorLevel=1}"/>
                                        <Binding  RelativeSource="{RelativeSource FindAncestor, AncestorType=DataGrid, AncestorLevel=1}"
                                                  Path="DataContext.AllGradesCV.View.Groups"></Binding>
                                    </MultiBinding>
                                </Setter.Value>
                            </Setter>                            
                        </Style>
                    </TextBlock.Style>
                </TextBlock>
            </StackPanel>
        </DataTemplate>


<!-- Style for GroupItem Behaviour of EXPANDER -->
        <Style x:Key="GroupItemStyle" TargetType="Expander">
            <Setter Property="Background" Value="#FF5CB9EE"/>
            <Setter Property="ExpandDirection" Value="Down"/>
            <Style.Triggers>                  
                <Trigger Property="IsExpanded" Value="True">
                    <Setter Property="HeaderTemplate" Value="{StaticResource ExpanderHeaderExpanded}"/>
                </Trigger>
                <!-- Diesen Trigger gegen MultiDataTrigger ersetzen?= -->
                <!--<Trigger Property="IsExpanded" Value="False">
                    <Setter Property="HeaderTemplate" Value="{StaticResource ExpanderHeaderCollapsed}"/>
                </Trigger>-->

                <MultiDataTrigger>
                    <MultiDataTrigger.Conditions>
                        <Condition Property="IsExpanded" Value="False"/>
                        <Condition Binding="{Binding TargetNullValue=true, RelativeSource={RelativeSource AncestorType={x:Type DataGridTemplateColumn}}, Path=Header}" Value="Fach"/>
                    </MultiDataTrigger.Conditions>
                    <MultiDataTrigger.Setters>
                        <Setter Property="HeaderTemplate" Value="{StaticResource ExpanderHeaderCollapsedType}"/>
                    </MultiDataTrigger.Setters>
                </MultiDataTrigger>

Der Part mit dem MultiDataTrigger ist das Problem: wenn ich die App starte wird folgende Exception ausgegeben:> Fehlermeldung:

System.Windows.Markup.XamlParseException: "Nicht-NULL-Wert für "Binding" erforderlich."

Innere Ausnahme:
InvalidOperationException: Nicht-Null-Wert für “Binding” erforderlich.

Ich vermute es fehlt ein Nicht-Null-Wert – was auch immer das bedeuten mag.
Ich bin mir nicht mal sicher, ob ich mit dem MultiDataTriffer für mein Vorhaben da auf dem richtigen Weg bin.
Daher hier noch kurz der Versuch einer Konkretisierung:

  • ich möchte im Style mit dem Target-Type “Expander” auf die Property “IsExpanded” des Expanders binden (prüfen, ob der Value false ist”
  • gleichzeitig auf das Property des DataGrids binden (Column --> Header--> Bezeichnung) und wenn letzterer den Value “Fach aufweist” im Setter das Property “HeaderTemplate” des Expanders setzen.
    Gruß
    Vorph
16.02.2021 - 20:34 Uhr

Sodale - vielen Dankf für die Hilfe(n) - ich habe mich einige Zeit jetzt mit Logging beschäftigt; mit Sicherheit nicht umsonst. Ich kapier zwar immer noch nicht alles, aber so hab ich wenigstens den Fehler schnell gefunden.

Die Lösung ist mal wieder banal - auch wenn ich sie mir nicht erklären kann:

  • soweit ich weiß, wird bei der von mir verwendeten Schreibweise "FileName =./Test_DataBase00.sqlite" die Datenbank immer relativ zum Projektordner erstellt, und zwar unter Projekt-->bin-->Debug-->netcoreapp3.1

Dort fand ich auch bei mir die Test_DataBase00.sqlite

Nur, dass sie bei mir leer war, obwohl scheinbar soweit alles in Ordnung war. War es auch. Allerdings wurde eine identische Datei unter 'Projekt' erstellt. Und diese enthielt alle Daten.

Ich habe die Datenbank jetzt schon umbenannt, gelöscht, ausgetauscht - egal: nach der ersten Migration (erst nach 'update-database') habe ich die Datenbankdatei einmal unter Debug, und einmal direkt im primären Ordner.

Soll das so sein? Ich hatte bisher Datenbanken in der Konstellation EFcore und sqlite nur in Verbindung mit wpf-Projekten genutzt. Sollte das bei einer Konsolen-App anders sein? Wohl kaum, oder?

Jedenfalls - jetzt, da ich mit SQLite-Studio die richtige Datei öffne, klappt auch alles.

Seit wann unterstützt der Connection String den führenden Punkt?

optionsBuilder.UseSqlite(connectionString: "FileName =./Test_DataBase00.sqlite");

Und der Space wird meines wissens auch nicht gemocht.

Also, bei meinen wpf-Projekten hatte ich das immer so gemacht, seit ich das mal in Zusammenhang mit einem wpf-Tutorial so gesehen hatte - da gab es bislang keinen Stress. Es stünde natürlich zu vermuten, dass hier ein Grund für den Fehler liegt - aber das wäre schon strange, wenn die Art des Projekts (Konsole vs. wpf) Auswirkungen auf die Erstellung der DB haben sollte...
Wie gesagt, ich kenne keine andere Form: wie wäre es denn besser, wenn man möchte, dass die DB sich immer in einem Ordner relativ zur App befindet?

Ansonsten nochmal vielen Dank für den Logging Hinweis - wieder was gelernt 🙂

15.02.2021 - 15:07 Uhr

Yes - ich hatte schon befürchtet, dass es nichts Offensichtliches ist^^

@Exception handling --> setzt das nicht voraus, dass überhaupt eine Exception geworfen wird?

Das mit dem Logging - keine Ahnung was das ist oder was es bedeutet; meintest du sowas Logging in .NET Core and ASP.NET Core

15.02.2021 - 11:48 Uhr

Verwendetes Datenbanksystem: EntityFrameworkCore 5 w/ SQLite

Hallo,
ich habe mir ein Test-Projekt (Konsolenanwendung) mit einer Test-Datenbank angelegt. Das Problem besteht darin, dass


using (var context = new DataBaseContext())
{
    context.MidiItems.Add(mI);
    context.SaveChanges();
}

NICHT dazu führt, dass der Wert in die Datenbank geschrieben wird.

Hier das Model


public class MidiItem
{
public int ItemID {get;set;}
public string MyValue {get;set;}
}

Mein DataBaseContext:


public class DataBaseContext: DbContext
    {
        public DbSet<MidiItem> MidiItems{ get; set; }
      
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite(connectionString: "FileName =./Test_DataBase00.sqlite");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.ApplyConfiguration(new MidiItemEntityConfiguration());
         }
    }

Meine MidiItemEntityConfiguration


public class MidiItemEntityConfiguration : IEntityTypeConfiguration<MidiItem>
    {
        public void Configure(EntityTypeBuilder<MidiItem> builder)
        {
            builder.HasKey(s => s.ItemID);         
        }
    }

Und mein Program.cs-Code


static void Main(string[] args)
  {
       // var context = new DataBaseContext(); --> mein erster Versuch...
       using (var context = new DataBaseContext())
       {
           context.DataBase.EnsureCreated();
       }

       MidiItem mI = new MidiItem();
       mI.MyValue = "Test";

     // context.MidiItems.Add(mI);
     // context.SaveChanges(); --> erster Versuch....
      using (var context = new DataBaseContext())
      {
           context.MidiItems.Add(mI);
           context.SaveChanges();    
      }
       
  }

Mehr Code habe ich bisher nicht.

Der Code läuft ohne Fehler durch - wenn ich einen Breakpoint setze, kann ich sehen, dass der Eintrag unter context-->local-->MidiItems auftaucht.
Leider landet der Wert nicht in der Datenbank.
Ich kann zwar die Struktur in SQLite-Studio sehen (ItemID | MyValue), aber das item wird nicht hinzugefügt.

Ich stehe gerade vollkommen auf dem Schlauch - in meinen wpf-Projekten habe ich das im Prinzip auch nicht anders gelöst - warum sollte es bei einer Konsolenanwendung nicht klappen.

Wenn ich Migrations anwende, sehe ich, dass die Struktur der Datenbank so wiedergespiegelt wird, wie sie auch intendiert ist. Habe ich evtl. eine Library nicht implementiert? Bisher:

  • Microsoft.EntityFrameworkCore.SQLite 5.0.3
  • Microsoft.EntityFrameworkCore.SQLite.Design 1.1.6
  • Microsoft.EntityFrameworkCore.Tools 5.0.3

Gruß
vorph

10.02.2021 - 14:29 Uhr

Sorry, das ist eine absolute Quatschargumentation.
Mir ein Rätsel, wie man das unterschlagen / abwimmeln kann.

Heute wieder viel Code zum Weinen gesehen? 🙂

Ich denke, wer sich meine Posts in Ruhe durchgelesen hat, der wird feststellen, dass ich lediglich meinen Beweggrund dargelet habe, indem ich auf tribs Frage geantwortet habe. Ich dachte, das kann man so machen - jetzt weiß ich es besser. Ich fände es schade, wenn der Eindruck aufkommen würde, ich täte etwas unterschlagen/abwimmeln - ich bin hier, um mich in meinem Hobby zu verbessern und nehme diese Ratschläge auch gerne an. Um mich mal selbst zu zitieren "Ich war davon ausgegangen..." --> Präteritum, und alles is jut 😉

Vielen Dank für eure Hilfe!

09.02.2021 - 11:02 Uhr

@trib: na ja, es gibt genau eine Stelle in der App, an der man ein Datum "eingeben" kann - nämlich beim Anlegen eines neuen Schülers. Und dort auch nur per DatePicker, der in XAML so angelegt wurde, dass man keine händische Eingabe vornehmen kann. Normalerweise mag ich diese User-Gängelung nicht, aber es handelt sich hier um einen überschaubaren Personenkreis (außer mir vlt. noch vier, fünf Kollegen), die später einmal diese App nutzen. Da hielt ich es für vertretbar, das eingegebene Datum direkt in einen String umzuwandeln, bevor es in die Datenbank geschrieben wird.
Tatsächlich wird beim Auslesen der Daten der String nochmal in DateTime konvertiert, um das aktuelle Alter der Schüler zu berechnen. Ich glaub', das waren vier Zeilen Code...

@Abt: stimmt. Ich glaube, das hattest du mir schon mal empfohlen. Auch hier war ich einfach davon ausgegangen, dass - solange keine anspruchsvollen Datumsberechnungen vorliegen - der alte DateTime reicht.

08.02.2021 - 09:40 Uhr

Das macht sogar sehr viel Sinn, du erbst ja vom selben Interface 🙂

🙂 Ja - ich meinte eher im echten Leben. Vom Code her klar. Die Idee mit der überladenen Methode gefällt mir.

08.02.2021 - 09:36 Uhr

wobei das installierte .NET Framework als Restriktion wohl auch beachtet werden will

Welche Einschränkungen wären das im Besonderen?

diverse Ergebnisse unter nuget.org

Danke für die Links - hast du Erfahrungen mit einem dieser Packages? Eventuell eines, das dokumentiert ist? USB.Net sieht erst mal gut aus (nicht, weil es das erste in der Liste ist, sondern weil es scheinbar regelmäßig aktualisiert wird), scheint aber keine Dokumentation zu haben? Ansonsten werde ich mir diese einfach schon mal ansehen.

Wie sieht es auf der technischen Seite aus? Gibt es da etwas spezielles zu beachten?

Gruß
Vorph

07.02.2021 - 20:27 Uhr

Hallo,

Ich arbeite seit über 10 Jahren mit einem digitalen Mischpult, das einen eingebauten 10"-Screen hat. Damals gab es von einem Drittanbieter eine Adapter-Karte zu kaufen, die einen VGA-Ausgang und einen USB-Ausgang hat.
Mit dem VGA-Ausgang konnte man sich die Anzeige des Digitalpults auf einem externen Monitor ansehen.
Den USB-Anschluss konnte man mit einem PC verbinden und mittels einer kostenlosen App vom Hersteller konnte man sich den Screen des Pults in einem eigenen Fenster in der App ansehen.

Leider ist die Software total veraltet, das Pult wird schon ewig nicht mehr hergestellt (läuft hier bei mir aber noch tadellos in einer Win10-Umgebung).

Mal eine Frage (und man verzeihe mir bitte, wenn sie vielleicht furchtbar naiv klingt, aber ich bin lediglich Hobbyist und dann ist Hardware auch nicht unbedingt meine Stärke):

Wenn ich mir nun selbst ein Programm schreiben wollte, das lediglich in einem eignen Fenster einfach das ausgeben soll, was auch auf dem 10"-Screen zu sehen ist, mit welchen Techniken müsste ich mich dann beschäftigen?

Wahrscheinlich bräuchte ich zunächst eine Library oder ein Package, mit dem ich mit c# USB-Schnittstellen ansprechen kann - oder ist das schon in System.IO?

Ich vermute, dass über USB einfach der VGA-Stream (Stream? Ist das in diesem Zusammenhang der richtige Begriff?) übertragen wird. Gibt es hier spezielle Stolpersteine zu beachten?

Gruß
Vorph

07.02.2021 - 20:09 Uhr

So, hier also die versprochene Rückmeldung. Zunächst - es hat für meine Zwecke super geklappt, tut was es soll; und dabei, ist es auch noch schön praktisch 🙂 Vielen Dank also an alle Hilfesteller!

Ohne mich groß zu wiederholen, hier kommt der Code:

Die Klasse Student wurde also um eine List<> ergänzt, in der sowohl Ziffern, als auch Verbalzbenotungen untergebracht werden können. Dazu hab ich folgendes Interface erstellt


   public interface IGrading 
     {
        int GradeId {get; set;}
        string DateOfGrading {get;set;}
     }

Da nun zwei verschiedene Benotungsweisen unter einen Hut zu bringen waren, brauchte es natürlich zwei entsprechende Klassen:


   public class RegularGrading : IGrading 
     {
        public int GradeId {get; set;}
        public string DateOfGrading {get;set;}

        public string GradeAsWord {get;set;}
        public string GradeWithLeadingZero {get;set;}
     }


   public class SpecialGrading : IGrading
     {
        public int GradeId {get;set;}
        public string DateOfGrading {get;set;}
        public string VerbalGrade [get;set;}
      }

Dementsprechend sieht meine Schüler-Klasse nun so aus:


   public class Student  
     {
        //...andere Properties s. oben
        public List<IGrading> Grading {get;set;}
     }

Tja, und das war es dann tatsächlich auch schon.

Und wie wird das Ganze nun genutzt? Nun, wenn eine neue Bewertung erstellt werden soll, sieht das Ganze in etwa so aus:


//Methode im ViewModel   
     public void CreateGrading(Student selectedStudent) 
     {
        if (selectedStudent.StudentType == StudentType.Regular)
	  {
	      RegularGrading r = new RegularGrading();
              r = CalculateValues(points);
	      
	      selectedStudent.Grading.Add(r);
          }
	 else
	  {
	      SpecialGrading s = new SpecialGrading();
	      s = GetVerbalGrade(textBody);    

              selectedStudent.Grading.Add(s);
           }

Wie gesagt, das funktioniert und wirkt auf mich recht elegant gelöst.

Der einzige Nachteil ist: you gotta get your sh** together!
Denn folgendes ist im Code möglich, macht aber von der Logik her keinen Sinn:


   public void UnLogicalMethod() 
     {
        RegularGrading r = new RegularGrading();
        SpecialGrading s = new SpecialGrading();

        r = FillRGWithSomeValues();
        s = FillSGWithAValue();

        selectedStudent.Grading.Add(r);
        selectedStudent.Grading.Add(s); //funktioniert beides, macht aber keinen Sinn, da ein Schüler nur die eine Art der Bewertung erhält ODER die andere!
     }

@Witte:
Danke für die Hinweise.

Du denkst nicht objektorientiert - man sieht nur Daten, es wird aber kein Verhalten gezeigt.

OK, meinst du damit man sieht nicht, wie die Objekte im Code genutzt werden? Falls ja, habe ich vielleicht zu kurz gedacht, weil ich eigentlich einen ganz anderen Weg eingeschlagen hatte und mein Thread einen ganz anderen Titel hatte. So wie es sich jetzt liest, klingt es wirklich ein bisschen knapp, vor allem das, was ich im ersten Post geschrieben habe. Ansonsten erläutere bitte, was du mit "nicht objektorientiert denken" meinst, ich lerne gerne dazu und vielleicht gehe ich ja grundsätzlich nicht so an die Sache heran, wie es für objektorientiert angebracht wäre.

Grüße
Vorph

04.02.2021 - 18:21 Uhr

Danke für den Einwurf bezgl. Basisklasse - ich hatte das kurz in Erwägung gezogen, aber es macht IMHO hier nicht wirklich Sinn, weil die Bewertungen wirklich grundverschieden sind. Ich versuche das nochmal ganz kurz zu skizzieren - ich weiß, die Thematik ist sehr speziell.

Regelschüler = R
Förderschüler = F

Grundsätzlich erhalten beide ein Zeugnis. Bei R sind das Ziffernnoten von 15 bis 00. Bei bestimmten F (z. B. geistige Behinderung) besteht das Zeugnis nur aus einer verbalen Beurteilung - keine Ziffernnoten. Die Krux: an einer Förderschule können Schüler aus verschiedenen Bildungsgängen dieselbe Klasse besuchen, z. B. jemand der den Hauptschulabschluss macht und jemand, der im Bereich geistige Behinderung unterrichtet wird. Formal unterschieden sich die Schülertypen nur in der Bewertung (eine Schülerakte wäre also völlig identisch aufgebaut, würde sich aber in diesem Punkt fundamental unterscheiden).

Daraus ergibt sich, dass ich in meiner App zwei verschiedene Typen Schüler in ein und derselben Klasse unterbringen möchte. Ich hätte gleich zwei Typen von Schülern anlegen können, dabei aber jede Menge Code-Duplikate produziert, was sehr unschön gewesen wäre, da sie sich - formal - nur im Bewertungsteil so substantiell unterscheiden.

Daher bin ich auch von der Basisklasse abgerückt - es gibt in diesem Punkt keinen wirklich gemeinsamen Nenner.
Aber: ich glaube, die Idee von Palladin gestattet mir eine elegante Lösung, die zumindest praktikabel gehandhabt werden kann. Wie gesagt: für diesen Part existiert noch kein konkreter Code, ich kann mich erst übermorgen ausgiebig damit beschäftigen.

Und lieber Klasse als Interface bei diesem "is-a", vorallem wenn es mal in eine Datenbank soll.

Könntest dur mir erläutern, wo genau du da Probleme siehst? Ja, später soll es in eine Datenbank (defacto entwickle ich meine DB parallel), aber ich sehe jetzt beim Interface eigentlich nur den Nachteil (wenn man das so nennen will), dass ich mehr Code zu schreiben habe. Ansonsten sehe ich einen Benefit in Sachen Wartbarkeit, Erweiterung und ease-of-use. Bin aber nur Hobby-Coder von daher kann es natürlich gut sein, dass ich offensichtliche Schwachstellen nicht sehe/bedacht habe.

Grüße
vorph

04.02.2021 - 10:05 Uhr

Hallo Palladin - sorry für die späte Rückantwort! Wo fange ich an? Erstmal vielen Dank für deine Vorschläge und die Richtigstellungen - meine Verwendung des Begriffs 'boxing' war natürlich falsch - und ich wusste es nicht mal 😉

Gestern Abend kam ich dazu ein bisschen zu coden und ich muss sagen: die Idee mit den Interfaces gefällt mir ziemlich gut. Ich weiß nicht, ob ich das alles richtig verstanden habe, aber über's Wochenende dürfte ich etwas ausgiebiger uzm Programmieren kommen und dann werde ich meine Lösung mal hier posten.

Wenn man "GradeWord" weg lässt, sind die beiden Klassen auch gar nicht mehr so verschieden [...]

Dein ganzes Problem löst sich mit ein bisschen Umdenken also einfach in Luft auf

Ganz so einfach ist es dann doch nicht 🙂 Wenn man versucht, die Realität abzubilden, braucht man für ein Regelschulzeugnis zwei Werte: Note als Punktwert mit vorangestellter Null plus Notenname (GradeWord), z. B.: '08' befriedigend.
Wohingegen eine verbale Beurteilung einfach ein Text ist, der Aufschluss über den Entwicklungsstand von Förderschülern in bestimmten Bildungsgängen geben soll, die prinzipiell nicht durch Ziffernnoten beurteilt werden, sozusagen eine Beurteilung im Fließtext. Beides zählt aber als 'Benotung', wenn es auch faktisch zwei verschiedene Dinge sind. So kommt man dann über's Programmieren zu der Erkenntnis, dass das derzeitige Bildungssystem erhebliche Schwachstellen haben muss 😉

OK, wie gesagt, die Lösung werde ich hier noch posten, sobald ich weiß, dass mein Code tut was er soll.

Gruß
Vorph

P.S:
Ich stelle fest, dass meine Überschrift geändert wurde !? Fände ich gut, wenn man da eine automatisierte Info per Mail bekäme - ich habe jetzt erstmal im Forum gesucht und war schon verblüfft, weil ich dachte mein Thread sei weg!
Außerdem: das mit dem Festlegen der Klasse hat sich ja nun eigentlich erst aus dem Lösungsvorschlag von Palladin ergeben - grundsätzlich ging es ja schon erst mal um die Feststellung, dass ein Property da ist, dessen Typ zur Compile-Time noch nicht feststeht. Finde ich sehr verwirrend.

02.02.2021 - 22:51 Uhr

Hallo,

beim Frickeln habe ich ein Problem entdeckt, dessen Lösung mir gerade einiges Kopfzerbrechen bereitet. Alleine schon ein anschauliches Beispiel zu finden mit einem praktischen Bezug ist nicht so einfach - mal wieder taugt aber ein Schul-Besipiel.

Es existiert nur Pseudo-Code; ich suche nicht nach konkretem Code, eher einem Konzept, einer Roadmap sozusagen.

Folgendes Szenario: stellt euch eine Schule vor. Es gibt (Schul-)Klassen und Schüler. Jeder Schüler bekommt Noten.

Das könnte also so aussehen:


public class SClass
{
     List<Student> Students {get;set;}
}

public class Student
{
     List<Grading> Grades {get;set;}
}

Wie euch sicher aufgefallen ist, habe ich die Klasse Grading (Bewertung) weggelassen, denn sie stellt den Kern des Problems dar.
Es gibt nämlich Bewertungen rein in Ziffernform (Grundschule ab Kl.3, Sekundarstufe) und rein verbale Bewertungen (z. B. Förderschüler - nicht alle, aber bestimmte Gruppen).

Ich brauche also zwei bestimmte Bewertungs-Klassen:


public class RegularGrading
{
     //Regelschulbewertung
     public string GradeWord {get;set;}
     public string Grade {get;set;}
}

public class SpecialGrading
{
     //Förderschulbewertung
     public string VerbalGrade {get;set;}
}

Der Typ der Bewertung hängt ab vom Schülertyp (Regel-/Förderschüler).
Meine Idee war, dass die Bewertung ein generischer Typ ist. Dann würde das so aussehen:


public class Student<T>
{
     List<T> Grades {get;set;}   
}

Da kein Schüler-Objekt (im späteren Code) erstellt werden kann, ohne dass der Schülertyp festgelegt wurde, wäre das eigentlich schon die Lösung, es bräuchte lediglich


     Student<RegularGrading> NewStudent = new Student<RegularGrading>();

und die Sache wäre geritzt. Zwar müssten bei Zuweisungen im Code vorher immer noch mal der Typ geprüft werden (typeOf()), aber das wäre zu verschmerzen.
Das große Problem, das sich abzeichnet ist folgendes: die Schulklasse enthält eine Liste mit Schüler-Objekten. Diese sind nun aber generische Klassen, was wiederum bedeutet, dass auch die Schulklasse einen generischen Typ <T> bereithalten muss:


public class SClass<T>
{
     List<Student<T>> Students {get;set;}
}

Damit erklärt sich das Problem womöglich von selbst: ich kann nun keine Schulklassen mehr instanziieren, ohne explizit den Typ der Bewertung für Student angeben zu müssen. Das kann ich aber gar nicht, weil es
a) zum Zeitpunkt der Klassenerstellung noch gar keine Schüler geben kann (zuerst braucht man eine Schulklasse, und dann Schüler, die ihr zugeteilt werden können) und
b) jeder Schüler nur eines von zwei möglichen Bewertungsschemata haben kann.

Da es keine generischen Properties gibt, muss ich hier einen anderen Weg einschlagen - spontan fiel mir nur ein, statt das Grading generisch zu halten, vielleicht einfach eine List<object> in den Grading-Klassen anzulegen. Prinzipiell natürlich eine Lösung, hat jedoch zwei Schönheitsfehler:
a) jede Menge boxing und unboxing im Code
b)


//Gradings unter der Annahme, dass die Notenlisten den Typ <object> entgegennehmen:
RegularGrading g = new RegularGrading();
SpecialGrading s = new SpecialGrading();

MyStudent.Grades.Add(g);
MyStudent.Grades.Add(s); //da die Liste Items vom Typ object entgegennimmt funktioniert das zwar, kann aber zu Fehlern führen, da ein Schüler stets nur 1 einem Bewertungsschema zugeordnet werden kann.

Wie könnte ich dieses Problem angehen? Müsste ich konzeptuell etwas verändern?

Gruß
Vorph

28.01.2021 - 09:26 Uhr

Werde Berater wie ich; siehst viel Code, der Dir Kopfschmerzen macht und Du eigentlich lieber Schafshirte geworden wärst 😃

Hehe - der ist gut 😉 Auf 'nem T-Shirt würde ich den Spruch sofort kaufen 😄

26.01.2021 - 21:01 Uhr

So, ich konnte mich jetzt die letzten Tage noch einmal eingehender mit der Thematik beschäftigen. Vielen Dank, Abt, ich kann mittlerweile tatsächlich Erfolge verweisen - die Datenbank (das DB-Schema, das erstellt wird) sieht wirklich so aus, wie ich es konfiguriert habe und verhält sich auch wunschgemäß.

Super vielen Dank hierfür 😉

Eine letzte Frage sei mir vielleicht noch gestattet: es ist das erste Mal, dass ich bewusst auf DataAnnotations verzichtet habe und mich ganz bewusst für die sauberere Implementierung und das "mehr" an Power der Fluent Api entschieden habe. Leider bekomme ich zu 99,9% nur meinen eigenen Code zu Gesicht und so fällt es schwer, vergleiche zu ziehen. Daher muss ich jetzt bei einer Sache noch einmal nachhaken:

Entitites sind Datenmodelle. Aber Datenmodelle sind nicht gleich Entities:
Entities sind (bei relationalen Datenbanken) die genaue Darstellung einer Tabellenstruktur.

Ich glaube, das habe ich verstanden - aber ich möchte sichergehen, dass ich nicht völlig auf dem Holzweg bin!

Mein ViewModel --> geschenkt!
Meine Business-Logik --> geschenkt! Ich habe eine Klasse Student, eine Klasse SchoolClass, eine Klasse Person...usw., die Properties und Methoden beinhalten, die die jeweilige Klasse definieren (i. Sinne von: welche Eigenschaften das Objekt haben soll) und Funktionalität bereitstellen (Methoden).

Dann habe ich noch das, was ich mein Data-Model nenne: Klassen, die nur die Properties der Models implementieren, also als kurzes Beispiel:


public class Student
{
    public int StudentId {get;set;}
    public string FullName {get;set;\
    //...noch ein paar Properties

    private int CalculateAlge() //Methode zum Berechnen des Alters
    private void ChangeStudentType () // Methode, die den Typ des Schülers ändert
}


public class StudentEntity
{
    public int StudentId {get;set;}
    public string FullName {get;set;}
    //...und die restlichen Properties
}

Soweit, so gut. (Übrigens: ich hatte meine Klassen in <MeineKlasse>Entity umbenannt - auch das hat vieles übersichtlicher gemacht.)

Um das Datenbank-Schema zu erstellen (erinnere dich kurz an meinen ersten Post) war es notwendig, dass die One-To-Many-Relation zw. Student und SchoolClass deklariert wird: Eine Klasse kann viele Schüler haben, ein Schüler immer nur eine Klasse.

Im Code oben hatte ich ja eine Liste List<Students> Students in der SchoolClass-Klasse (ist jetzt ein HashSet). Gleichzeitig habe ich jetzt meine StudentEntity so abgeändert:


public class StudentEntity
{
    public int StudentId {get;set;}
    public string FullName {get;set;}
    //...und die restlichen Properties, ergänzt um:
    
    public int SchoolClassId {get;set;}
    public SchoolClass StudentClass {get;set;}

}

Dadurch ließ sich nun im DataContext die Relation zwischen SchoolClass und Student darstellen (nur vorweg - ich hab's im Code mit einer Klasse, die IEntityTypeConfiguration<StudentEntity> implementiert gelöst, hier also nur der kürze wegen so:)


  protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<SchoolClassEntity>()
                              .HasKey(s => s.SchoolClassId);

           modelBuilder.Entity<SchoolClassEntity>()
                             .HasMany<StudentEntity>(s => s.Students)
                             .WithOne(s => s.StudentClass)
                             .HasForeignKey(s => s.StudentId);

Was mich verunsichert ist folgendes:

Im Code, übergebe ich in einer Methode Daten an mein Daten-Objekt (StudentEntity), in etwa so:


       newStudentEntity.Person.FirstName = FirstName; 
       newStudentEntity.Person.LastName = LastName;
//...

Jedesmal, wenn ich die Instanz newStudentEntity von StudentEntity eintippe, zeigt mir Intellisense nun natürlich auch das StudentClass Property vom Typ SchoolClass. Theoretisch könnte ich hier also versehentlich richtig Murks machen, denn im Code (in der BusinessLogik?) sollte Student überhaupt nicht auf SchoolClass zugreifen können - im Prinzip braucht Student von SchoolClass gar nichts zu wissen! Allerdings brauche ich das Property ja hier, um es im DataContext als Navigation-Property nutzen zu können.

Wie gesagt: vielleicht mache ich mir jetzt Gedanken um Sachen, die völlig irrelevant sind.

Oder ich habe ebend doch noch etwas nicht verstanden?

Gruß
Vorph

EDIT: NNNGH! Jetzt, wo ich gerade zum x-ten Mal die Klassen durchgehe und Code optimiere, fällt mir ein, dass ich ja in meiner ModelBuilder-Klasse (z. B. StudentEntity) einen Type T definiere! Damit hätte sich ja die Frage geklärt:

  • ich schreibe Entities, die sich nur darum "kümmern", wie das Datanbankschema aussehen wird
  • Meine Daten übergebe ich nich an die Entities - sondern an DataModels (was bei mir mal StudentData war und ich nun in StudentEntity umbenannt habe)
    Ich hatte einfach den Begriff DataModels falsch verstanden (OK, zumindest zu eng gefasst), weil ich dachte, dass sind nur die Klassen, die Daten für die Datenbank entgegennehmen.

Wenn das Edit stimmt, verstehe ich auch völlig was

Entitites sind Datenmodelle. Aber Datenmodelle sind nicht gleich Entities

bedeuten soll. (Ich kreuze mal die Finger...)

26.01.2021 - 08:46 Uhr

Hallo Thron,

könntest du deine Problematik etwas genauer beschreiben?

Da SQLite eher ein Client-based Datenbanksystem ist, würde ich dir EntityFrameworkCore vorschlagen (es entällt die clientseitige Installation eines kompletten Datenbanksystems, aber das möchtest du ja eh nicht, da du SQLite verwendest).

Die Herangehensweie ist prinzipiell dieselbe:
Du erstellst deine Data-Models und erstellst deine Entities anhand dieser Models. Außerdem benötigst du eine Klasse die von DbContext erbt - das wird dein DataContext.

Das ist jetzt sehr, sehr sporadisch umrissen, aber es lohnt sich eigentlich erst dann in die Tiefe zu gehen, wenn wir das konkrete Problem kennen.

Gruß
GeneVorph

23.01.2021 - 19:56 Uhr

Hallo Abt,

erstmal vielen Dank für die ausführlichen Erklärungen, und das du so viel Mühe in meinen Code gesteckt hast!

Bis ins kleinste Detail ist mir noch nicht klar, was da passiert, aber hier vlt. eine Frage, mit der ich etwas Licht ins Dunkel bekomme:

sind die Daten-Models etwas anderes als die Entity-Models? Oder blöd gefragt: in meiner Anwendung stelle ich z. B. Informationen in Textboxen bereit, die übergebe ich an die entsprechenden Properties der entsprechenden Data-Models (z. B. SchoolClass in meinem Fall), dieses übergibt dan diese Daten mit Hilfe eines DataService an die Datenbank weiter. Aber dieses Data-Model ist nochmal getrennt von den Models für die Entities, oder?

Weil es auch falsch ist. Du wirst die Methode auch nirgends in der Doku finden.

Das will ich dir auch gerne glauben, aber zumindest hier wird die Methode beschrieben/gezeigt. Und auch in msnd.

Nein. EF ist der FK hier im Endeffekt egal; der Datenbank aber nicht, weil Du durch das Schema Regeln erzeugt hast, dass der FK gesetzt sein muss.
Das prüft hier EF aber nicht automatisch und auch bei Queries spielt das für EF keine Rolle.

Ah - Danke! Auch das hab ich noch nirgendwo so gelesen, aber es holt einige Fragezeichen von meiner Liste! Jetzt verstehe ich auch, warum es EFC bis zur Migration völlig egal ist (und eigentlich auch darüber hinaus), außer beim PK. Ich dachte mir ja schon, dass da keine Magie im Spiel ist 😉

23.01.2021 - 12:53 Uhr

verwendetes Datenbanksystem: SQLite
betrifft: EntityFrameworkCore

Hallo,

ich versuche gerade komplett von DataAnnotations wegzukommen und meine Datenbank mit Hilfe von fluent Api zu konfigurieren.

Ich habe mich jetzt hauptsächlich auf entityframeworkcore.com, www.entityframeworktutorial.net und www.learnentityframeworkcore.com gestürzt.

Am besten ich zeige euch erst mal mein Data-Model und dann meine Fragen.

Ich kürze es ein wenig, weil es im Wesentlichen um das Verständnis geht (die Beispiele findet man so auch in etwa auf den vorgenannten Sites):

nehmen wir mal eine Schulklasse und einen Schüler:


public class SchoolClass
{
public int SchoolClassId {get;set;}

// ... noch ein paar Properties

public List<Students> Students {get;set;}
}

public class Student
{
public int StudentId {get;set;}
public Person PersonData {get;set;}
public Address AddressData {get;set;}
public Education EducationData {get;set,}
}

public class Person
{
public int PersonId {get;set;}

//...Properties wie Vorname, Nachname, Geburtsdatum, Geburtsort...etc.
}

public class Address
{
public int AddressId {get;set;}

//...Properties wie Straße, Hausnummer, Wohnort, Postleitzahl, etc.
}

public class Education
{
public int EducationId {get;set;}

//...Properties wie Schulform, Datum Schuleintritt, Klassenstufe, etc.
}

Nichts spannendes soweit. Wenn ich nun meinen DataContext so belasse...


public class DataBaseContext : DbContext
    {
        public DbSet<SchoolClass> SchoolClasses { get; set; }        
        public DbSet<Student> Students { get; set; }
        public DbSet<Person> Persons { get; set; }
        public DbSet<Address> Addresses { get; set; }
        public DbSet<Education> Educations { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            //optionsBuilder.UseSqlite("Data Source=Test_lite.sqlite");
            optionsBuilder.UseSqlite(connectionString: "FileName =./Test_DataBase.sqlite");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {                   
                        
        }
    }

...dann erstellt mir Entitiframeworkcore eine Datenbank mit den Tables SchoolClasses, Students, Persons, Addresses und Educations. In jedem Table werden außerdem Columns für die ForeignKeys erstellt, um die Objekt-Relationen abzubilden. Soweit so gut.

Nun möchte ich wie gesagt einige Eigenschaften konkretisieren. Ich möchte z. B. meiner Student-Klasse noch drei Properties vom Typ int hinzufügen:
public int PersonKey
public int AddressKey
public int EducationKey
Diese möchte ich im Code auslesen können, um z. B. ganz gezielt Informationen aus der Datenbank abfragen zu können - dazu müsste ich doch im ModelBuilder deklarieren, dass der ForeignKey z. B. PersonKey sein soll, oder? Nur


 protected override void OnModelCreating(ModelBuilder modelBuilder)
        {                   
            modelBuilder.Entity<Student>()
                              .HasForeignKey()  //<-- funktioniert bei mir nicht, diesen Eintrag bekomme ich                    
                                                                durch Intellisense nicht vorgeschlagen, bzw. wir als Fehler 
                                                                 angezeigt.

        }

Ich bin mir nicht sicher, warum das nicht funktioniert - wenn ich mir die Beispiele auf den erwähnten Sites so ansehe, kommt mir allerdings der Verdacht, dass ich ein Navigation-Property benötige.
Dessen Funktion ist mir im Prinzip klar, allerdings verstehe ich nicht, wie sich das auf meinen Code auswirkt, denn:



public class Student
{
public int StudentId {get;set;}

public int SchoolClassId {get;set;}
public SchoolClass AssignedSchoolClass {get;set;}
}

//Beispiel hier anhand der Person-Klasse
public class Person
{
public int PersonId {get;set;}

public int StudentId{get;set;}
public Student AssignedStudent {get;set;}
}

  • jetzt hätte meine Student-Klasse ein SchoolClass-Objekt (Navigation-Property) um eine One-to-Many-Relation herzustellen (Eine Klass kann viele Schüler enthalten <-> ein Schüler gehört immer nur einer Klasse an), bzw meine Person-Klasse ein Student-Objekt und nach den EFC-Konventionen hätten wir eine One-to-One-Relationship (zumindest möchte ich das abbilden: Ein Schüler hat genau ein ihm zugeordnetes Personen-Objekt mit seinen Stammdaten <->ein Personen-Objekt mit seinen spezifischen Stammdaten gehört zu genau einem Schüler)

Was ich jetzt nicht so ganz kapiere: in meinem Daten-Model ist jetzt in der Person-Klasse plötzlich ein Objekt vom Typ Schüler - und dieses besitzt ja wiederum ein Person-Property und dieses ... immer so weiter. Ebenso in der Student-Klasse: ein SchoolClass-Objekt, das ja wiederum eine List<Student> enthält. Soll das so sein?

Im Prinzip müsste es doch so sein, dass mein Navigation-Property im Code gar nicht auftaucht (denn dort wird es ja auch nicht gebraucht) - aber andererseits muss es ja da sein, damit ich es als Navigation-Property nutzen kann (denn für den DataContext wird es ja gebraucht).
So ganz bin ich mir noch nicht sicher, wie das läuft - ich hoffe, ihr könnt mir das etwas einfacher erklären 😃

Gruß
Vorph

20.01.2021 - 11:04 Uhr

Hallo ToXit,

ich trage jetzt zwar Säulen nach Athen (denn die einzig richtige Antwort hat FZelle schon gegeben), aber ich sehe hier schweren "System-Missbrauch"!

Was ihr umsetzen möchtet ist das Hinzufügen eines Eintrages unter XAML, der bei Verwendung von MVVM eine reine Routineeinstellung ist - durch Code-behind und das direkte ansprechen von Controls aber zum Ding der Unmöglichkeit wird!

Aber im Prinzip ließe sich das noch einfach umsetzen, indem du/ihr zuerst eine View (das eigentliche UserControl oder Window) erstellt und dann ein Model für diese View (ein ViewModel) --> das ist dann eine .cs-Datei, die sich um die Logik der Darstellung kümmert.

Um überhaupt mal ein Gespür dafür zu bekommen, würde ich vorschlagen sich mal in Grundzügen mit dem Bezug zwischen View und ViewModel auseinander zu setzen. Ich habe hier schon oft gelesen, das sei schwer, umständlich, kompliziert, aufwendig u. ä. Dem kann ich mich nicht anschließen.

Daher hier ein kleiner Kick-Start:

  1. Lege in deinem Projekt eine Datei an nach dem Schema (MyControlName)ViewModel --> z. B. MainWindowViewModel

  2. Dein ViewModel muss INotifyPropertyChanged implementieren, z.B. so:

public class MainWindowViewModel: INotifyPropertyChanged

  1. Im XAML-Code deiner View (z. B. MainWindow.xaml) fügst du folgenden Code hinzu:
<Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>

Fertig 😃 Du hast nun ein ViewModel, das mit deiner View verbunden ist.

Für deinen Fall erstellst du nun eine ObservableCollection<Sources> MyCollection. Und im DataGrid deiner View legst du diese Collection als ItemsSource fest:

<DataGrid ItemsSource="{Binding MyCollection}"

Und wir sind da...

Kleiner Tipp noch: Sources ist bei dir eine Liste von Listen. Kann man machen, wird aber sehr unübersichtlich; mir ist nicht ganz klar, wie alle diese Infos in ein und dasselbe DataGrid sollen. Es könnte also durchaus Sinn machen entweder das Sources-Objekt anders zu konzipieren (denkt auch an entsprechende Properties) oder mehrere Collections anzulegen, etwa für ComboBoxZellen im Grid.

Ich habe bisher herausgefunden, dass die ItemsSource nicht bearbeitet werden kann.

Wo immer du das herausgefunden hast - das ist komplett falsch! Genau für diesen Fall hast du die ItemsSource und DataBinding...und MVVM.

Gruß
Vorph

17.01.2021 - 13:27 Uhr

Es ist kein übliches Vorgehen, daß sich einzelne Einträge ändern (sondern meistens ändert man ja die komplette Liste und setzt dann die Auswahl neu).

Das stimmt natürlich Aber nur, weil es unüblich ist, heißt das ja nicht, dass man nicht eben genau diesen Verhalten im Projekt benötigt:-D

Persönlich würde es mich gar nicht mal stören, weil ich ja weiß, dass der Wert übernommen wurde. Aber ich denke, einen Benutzer würde es irritieren, wenn er z. B. ein Bauteil in einer Liste umbenennt und dann aber den alten Namen angezeigt bekommt.

Aber gut: dann bin ich beruhigt. Kein falsches Binding, kein Denkfehler - einfach nur nicht vorgesehen.

Ich denke, ich bleibe dann beim SelectionChanged. Mercie beaucoup!

Gruß
Vorph

16.01.2021 - 17:30 Uhr

IsEditable ist jetzt false - damit kann ich zwar den Bereich nicht mehr beschreiben, aber das eigentliche Problem ist dadurch nicht gelöst: es geht ja darum, dass wenn der Wert in diesem Bereich sich ändert, dies erst angezeigt wird, wenn man vorher ein anderes Item auswählt.

Im DropDown-Bereich wird bereits der neue Wert angezeigt - da das ja ber das aktuelle Item ist, passiert natürlich erst mal nichts, wenn ich diesen sofort auswähle (logischerweise wird in diesem Fall das SelectionChanged-event nicht gefeuert).

15.01.2021 - 00:10 Uhr

Danke Witte.

Allerdings gibt es da ein Problem: ich habe aus Testzwecken meiner View eine ListView, bzw. ein Label hinzugefügt. Die ListView zeigt alle Elemente der CollectionViewSource an und zwar NUR deren FullName-Property.

Das Label zeigt nur das FullName-Property der SelectedPerson.

Wie oben bereits beschrieben: SelectedPerson ist mein ViewModel-Property für das SelectedItem der ComboBox.

Klicke ich den Button werden die Werte für FullName in der ListView und auch im Label sofort geändert - nur nicht im Textfeld der ComboBox.

Ich denke somit wäre die Implementierung von INotifyPropertyChanged wohl auch nur ein Hack, oder? Funktioniert die ComboBox möglicherweise iwie anders?

14.01.2021 - 19:28 Uhr

Hallo,

wahrscheinlich ein einfacher Sachverhalt:

Ich habe eine ComboBox, eine TextBox und einen Button.

Die ComboBox enthält Personen-Objecte, die sie aus einer CollectionViewSource erhält. Angezeigt wird jeweils der volle Name einer Person. Die CollectionViewSource wiederum hat als Source eine ObservableCollection<Person>.

Wählt man in der ComboBox eine Person aus, so wird deren Name in der TextBox angezeigt. Der Benutzer kann nun z.B. einen neuen Namen eingeben. Dieser soll jedoch die 'FullName'-Eigenschaft der ausgewählten Person überschreiben, also ein Update vornehmen.

Dazu ist im übrigen der Button, der ein Command (UpdateCommand) feuert.

Nehmen wir an, ich habe "Müller, Peter" ausgewählt.

Ändere ich den Eintrag in der TextBox jetzt ab in "Antoinette, Marie" und betätige den Button, scheint es zunächst, als sei nichts passiert, denn

  • das aktuelle Objekt (SelectedItem) wird zwar im ViewModel in der Command-Methode auf den neuen Wert geupdated, aber in der View ist davon nichts zu sehen.

  • klicke ich auf die ComboBox, so dass sich das DropDown-Feld öffnet sehe ich den alten Eintrag (Müller, Peter) nicht mehr, sondern den neuen (Antoinette, Marie) - gleichzeitig wird aber immer noch im Display der ComboBox 'Müller, Peter' angezeigt.

Meine "LÖsung" bisher: ich setze den Index der ComboBox im ViewModel auf -1 und dann direkt auf den Index des SelectedItem; so erweckt es den Anschein, als sei das Objekt geupdated worden.

Hier mal mein XAML:


<ComboBox ItemsSource="{Binding AllPersonsCV.View}" DisplayMemberPath="FullName" Width="120" 
                          SelectedItem="{Binding SelectedPerson, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" SelectedIndex="{Binding MyIndex}"/>

Und der relevante Code im ViewModel; zuerst für das SelectedItem-Property:

 private Person _selectedPerson;

        public Person SelectedPerson
        {
            get { return _selectedPerson; }
            set
            {
                OnPropertyChanged(ref _selectedPerson, value);
            }

Und die Command-Methode, die das eigentliche Update/Überschreiben vornimmt:



 private void UpdatePerson_Execute(object parameter)
        {
            UpdatePerson();
        }

private void UpdatePerson()
        {
            //Der in die TextBox eingegebene string wird an das Property fullName übergeben und hierdann
            //der FullName-Eigenschaft des SelectedItems der ComboBox übergeben.
            SelectedPerson.FullName = fullName;

           //CollectionViewSource-Refresh
            AllPersonsCV.View.Refresh();
         }
               

Und hier noch mein PersonName-Property, welches an den Text der TextBox gebunden ist:


private string _personName;

public string PersonName
{
   get {return _personName; }
   set
      {
          OnPropertyChanged(ref _personName, value);
      }
}

Soweit habe ich das Problem dann im Debugger schon aufgedröselt:

  • die Werte werden übergeben, d.h. mein SelectedPerson.FullName-Property wird in der UpdatePerson-Methode überschrieben.

  • auch in der AllPersons-ObservableCollection wird dadurch die entsprechende Property des entsprechenden Person-Objekts geändert.

  • da für die View aber meine CollectionViewSource (in der im Debugger auch schon der neue Wert zu sehen is) zuständig ist, refreshe ich die CollectionViewSource.

Trotzdem zeigt das Display-Feld der ComboBox immer noch den alten Wert an. Erst, wenn ich ein anderes Element aus der ComboBox auswähle, ist der alte Wert endgültig verschwunden.

Was mache ich da falsch?

Grüße
Vorph

13.01.2021 - 13:42 Uhr

[LÖSUNG]
Heureka – ich hab’s!

Ich weiß nicht, ob meine Frage seltendämlich, bockschwer oder schlecht gestellt war – daher hier eine kurze Zusammenfassung und eine Lösung step-by-step.

Problemstellung:

Ich möchte in meinem DataGrid auf den Header der Column ‚Klasse‘ klicken und alle Schüler-Objekte nach ihren Klassen sortiert bekommen.

Dabei sollen alle Schüler-Objekte unter einem Expander gruppiert werden.
Beim ersten Klick auf die Column ‚Klasse‘ werden alle Gruppen ‚expanded‘ angezeigt.
Beim zweiten Klick auf die Column ‚Klasse‘ werden alle Gruppen ‚collapsed‘ angezeigt.
Beim dritten Klick auf die Column ‚Klasse‘ werden alle Gruppen ‚collapsed‘, bis auf die Gruppe, die das aktuell ausgewählte Schüler-Objekt enthält.

Oh ja, und die Lösung sollte natürlich MVVM-kompatibel sein…

Im Prinzip war die Lösung – wie so oft – ziemlich einfach, wenngleich nicht unbedingt intuitiv!

Hier also die Lösung:
Wir benötigen:

  • Eine Klasse die die IMultiValueConverter-Schnittstelle implementiert
  • Einen enum, genannt ExpanderStatesEnum
  • Einen Zähler und ein Command im ViewModel
  • Eine CollectionViewSource die dem DataGrid als ItemsSource dient
  • Und ein VieModel-Property vom Typ Student, das in meinem Fall einfach SelectedStudent heißt

Auf die beiden zuletzt genannten muss ich hier denke ich nicht näher eingehen.

Zuerst das Offensichtliche: wir legen uns einen enum an, der die gewünschten States repräsentiert


public enum EpanderStatesEnum
{
expandAll,
collapseAll,
collapseAllButSelected
}

expandAll wird später im ViewModel gesetzt, wann immer alle Expander ‚expanded‘ dargestellt werden sollen. Analog dazu ‚collapseAll‘.
collapseAllButSelected soll gesetzt werden, wann immer alle Expander ‚collapsed‘ werden sollen, bis auf denjenigen, zu dem das aktuell ausgewählte Schüler-Objekt gehört (sofern eines ausgewählt wurde).

Als nächstes widmen wir uns dem ViewModel. Hier benötigen wir zuerst ein Command:


public ICommand GroupSortCommand { get; set; }
//Und im Constructor
public MyViewModel()
        {
//do Stuff
GroupSortCommand = new CommandDelegateBase(GroupSort_Execute,    GroupSort_CanExecute);
        }
 

Die Methoden GroupSort_Execute und GroupSort_CanExecute können wir eigentlich gleich schon anlegen – zuvor benötigen wir im ViewModel einen einfachen Zähler…


private int _simpleCounter;

…der mitzählt, wie oft der Header bereits angeklickt wurde (zur Erinnerung: wir wollten ja verschiedenes Grouping-Verhalten beim 1. Klick, 2. Klick, etc.).
Nun noch schnell ein bool-Property, das darüber Auskunft gibt, ob überhaupt gruppiert werden soll (true) oder eben nicht (false)
Das sieht dann so aus:


private bool _isGroupSortRequested = true;

_isGroupSourtRequested wird mit true initiiert, damit man die Funktionalität beim Starten des Programms gleich benutzen kann.

Nun aber wirklich zu GroupSort_Execute und GroupSourt_CanExecute, damit unser Command auch eine Logik verabreicht bekommt:


private bool GroupSort_CanExecute(object paramerter)
        {
            return true;
        }
        
        private void GroupSort_Execute(object parameter)
        {
//Zunächst prüfen, ob wir nicht schon Schüler-Objekte gruppiert haben
            if (_isGroupSortRequested)
            {
//falls ja, kümmern wir uns um den Zustand des Counters – dieser kann die Werte 1, 2 und 3 //aufweisen.
                switch (_simpleCounter)
                {
//Beim 1. Klick
                    case 1:
//Wir setzen unseren enum auf den gewünschten Status
                        ExpandedState = ExpandedStatesEnum.expandAll;
//Wir übermitteln unserer CollectionViewSource die gewünschte GroupDescription…
                        AllStudentsCV.GroupDescriptions.Add(new PropertyGroupDescription("StudentEducational.ClassLabel"));
//…und zählen den Zähler eins hoch
                        _simpleCounter++;
                        break;
                        //Beim 2. Klick
                    case 2:
//hier muss nur der gewünschte enum-Status übermittelt werden – die GroupDescription ist noch //aktiv
                        ExpandedState = ExpandedStatesEnum.collapseAll;
//Auch hier den Zähler eins weiter zählen
                        _simpleCounter++;
                        break;
//Beim 3. Klick…
                    case 3:
//Wieder den enum auf den gewünschten Status setzen
                        ExpandedState = ExpandedStatesEnum.collapseAllButSelected;
//Und jetzt ganz wichtig – den Zähler auf den Wert 1 zurücksetzen!
                        _simpleCounter = 1;
//Und _isGroupSortRequested auf false setzen – dies bewirkt, dass wir beim 4. Klick auf den Header //im else-Teil dieser Schleife landen
                        _isGroupSortRequested = false;
                        break;
                }
            }
            else
            {
//wenn _isGroupSortRequested false ist, landen wir hier
//Alle GroupDescriptions zurücksetzen
                AllStudentsCV.GroupDescriptions.Clear();
//und die Variable _isGroupSortRequested wieder initiieren, damit wir beim nächsten Klick auf den //Header wieder im if-Teil der Schleife landen.
                _isGroupSortRequested = true;
            }            
        }

Wo benötigen wir dieses Command? Im ViewModel – nämlich immer dann, wenn der Header der Column ‚Klasse‘ angeklickt wird. Das erledigen wir natürlich im XAML der betreffenden View. Da es sich bei mir um ein DataGrid handelt, in dem ich das Command einbinden möchte, kommt der Code als Style in die DataGrid.Resources:


<DataGrid x:Name="MyGrid" ItemsSource="{Binding AllStudentsCV.View}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" HeadersVisibility="Column"
                      IsSynchronizedWithCurrentItem="True" SelectedItem="{Binding SelectedStudent}">

                <DataGrid.Resources>
                    <Style x:Key="GroupHeaderStyle" TargetType="DataGridColumnHeader">
                        <Setter Property="Command" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=DataContext.GroupSortCommand}"/>                        
                    </Style>
                   
                </DataGrid.Resources>

Ich sollte vielleicht hinzufügen, dass die Property HeadersVisibility="Column" unbedingt dazugehört, wenn ihr den Header-Button-Style wollt – sonst gibt es kein visuelles Feedback (blau hinterlegter background).

Als nächstes kümmern wir uns um die visuelle Darstellung unserer Daten – die Schüler, die im DataGrid angezeigt werden, sollen ja später gruppiert und mit Hilfe von Expandern dargestellt werden. Dazu erstellen wir uns erst mal einen Style für den Expander und zwar in den Resources der View (in meinem Fall UserControl):

 
<!--dazu komme ich noch -->
<conv:ExpandConverter x:Key="ExpanderStateConverter"/>
<!—hier hübsche ich nur den Header des Expanders auf… -->
        <DataTemplate x:Key="ExpanderHeader">
            <ContentPresenter 
                Content="{Binding}"
                TextBlock.FontSize="14"
                TextBlock.FontWeight="Bold"/>
        </DataTemplate>
                
        <Style x:Key="GroupItemStyle" TargetType="Expander">
            <Setter Property="Header" Value="{Binding Name}"/>
            <Setter Property="HeaderTemplate" Value="{StaticResource ExpanderHeader}"/>
            <Setter Property="Background" Value="#FF5CB9EE"/>
            <Setter Property="ExpandDirection" Value="Down"/>
<!-- Ab HIER wird es interessant -->
            <Style.Triggers>
                <DataTrigger  Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Path=DataContext.ExpandedState}" Value="expandAll">
                    <Setter Property="IsExpanded">
                        <Setter.Value>
                            <MultiBinding Mode="OneWay" Converter="{StaticResource ExpanderStateConverter}" ConverterParameter="expandAll">
                                <Binding  Path="DataContext" RelativeSource="{RelativeSource Self}"/>
                                <Binding
                RelativeSource="{RelativeSource FindAncestor, AncestorType=DataGrid, AncestorLevel=1}"
                Path="DataContext.SelectedStudent"></Binding>
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>

                <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Path=DataContext.ExpandedState}" Value="collapseAll">
                    <Setter Property="IsExpanded">
                        <Setter.Value>
                            <MultiBinding Mode="OneWay" Converter="{StaticResource ExpanderStateConverter}" ConverterParameter="collapseAll">
                                <Binding  Path="DataContext" RelativeSource="{RelativeSource Self}"/>
                                <Binding
                RelativeSource="{RelativeSource FindAncestor, AncestorType=DataGrid, AncestorLevel=1}"
                Path="DataContext.SelectedStudent"></Binding>
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>

                <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Path=DataContext.ExpandedState}" Value="collapseAllButSelected">
                    <Setter Property="IsExpanded">
                        <Setter.Value>
                            <MultiBinding Mode="OneWay" Converter="{StaticResource ExpanderStateConverter}" ConverterParameter="collapseAllButSelected">
                                <Binding  Path="DataContext" RelativeSource="{RelativeSource Self}"/>
                                <Binding
                RelativeSource="{RelativeSource FindAncestor, AncestorType=DataGrid, AncestorLevel=1}"
                Path="DataContext.SelectedStudent"></Binding>
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>

            </Style.Triggers>
            
        </Style> 

Der „Schlüssel“ zum Erfolg liegt letztlich in den DataTriggern des Expanders. Daher werde ich hier etwas ausführlicher sein:

 <DataTrigger  Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Path=DataContext.ExpandedState}" Value="expandAll">                </DataTrigger> 

Wir binden hier an das ExpandedState-Property des ViewModels und geben gleich den Wert an, auf den der DataTrigger hier reagiren soll: expandAll. Wenn also der ExpandedState = expandAll ist, dann soll der Trigger…


                   <Setter Property="IsExpanded">
                        <Setter.Value>
                            <MultiBinding Mode="OneWay" Converter="{StaticResource ExpanderStateConverter}" ConverterParameter="expandAll">
                                <Binding  Path="DataContext" RelativeSource="{RelativeSource Self}"/>
                                <Binding
                RelativeSource="{RelativeSource FindAncestor, AncestorType=DataGrid, AncestorLevel=1}"
                Path="DataContext.SelectedStudent"></Binding>
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
 

…den Wert für die IsExpanded-Property abhängig von den Werten der Multibindings setzen. Dazu müssen wir das im DataGrid ausgewählte SelectedItem und einen ValueConverter referenzieren.

Keine Sorge – gleich ist es geschafft! 😃

Für den Converter lege ich im Projekt folgende Klasse an:

 public class ExpandConverter : IMultiValueConverter
    {
//Diese Methode benötigen wir, wenn ausgewertet werden soll, ob das SelectedItem im Datagrid in //der Gruppierung enthalten ist.
        private bool ItemInFilterGroup(object[] value)
        {
            CollectionViewGroup oc;
            Student x = new Student();
            //CollectionViewGroup 
            //Es wird ein Array mit 2 objects übergeben; einmal ein weiteres Array mit der //CollectionViewGroup, das ist die Collection, die unsere gruppierten Items enthält und dann noch //ein Object entsprechend des Typs des SelectedItem (hier: Student)
//Wahrscheinlich i. d. Fall nicht nötig, aber ich prüfe in beiden Fällen den Typ und setze die Variablen
            if (value[0] is CollectionViewGroup)
            {
                oc = value[0] as CollectionViewGroup;
            }
            else
            {
                oc = null;
            }

            if (value[1] is Student)
            {
                x = value[1] as Student;
            }

//Sind beide Typen korrekt…
            if (oc != null && x != null)
            {
//Schauen wir nach, ob das SelectedItem (x) in der GroupItem-Collection enthalten ist.
                if (oc.Items.Contains(x))
                {
//Wenn ja geben wir true zurück, was dafür sorgt, dass auch der Expander mit dieser GroupItem bei //IsExpanded auf true gesetzt wird.
                    return true;
                }
                else
                {
                    return false;
                }
            }

            return false;
            
        }

        public object Convert(object[] value, Type targetType, object parameter, CultureInfo culture)
        {
//Im XAML übergeben wir per ConverterParameter den Status des ExpandedState enums
            var x = parameter as string;
//Im nächsten Schritt prüfen wir, welcher Status vorliegt…
            if (x == "expandAll")
            {                
                return true;
            }
            else if (x== "collapseAll")
            {
                return false;
            }
            else if (x == "collapseAllButSelected")
            {
//Und nur bei diesem Status (wenn alle Expander collapsed werden sollen, außer derjenige mit dem //gerade gewählten SelectedItem des DataGrids, rufen wir die private Methode auf
                return ItemInFilterGroup(value);
            }

            return false;
        }       
//Hier nicht nötig
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }        

Wie man weiter oben, in den Resources der View sieht, wird der Converter dort bereits referenziert (ich erwähne es nur der Vollständigkeit halber: der Converter muss VOR dem Expander-Style deklariert werden…)

Und damit läuft unser Programm:

Klickt man auf den Header ‚Klasse‘ des DataGrids wird das GroupSortCommand gefeuert. Diese ruft letztlich die Methode GroupSort_Execute auf, in der geprüft wird, ob bereits gruppiert wurde, und die dann gemäß unserem Zähler auswertet, ob es sich um den 1., 2. oder 3. Klick auf den Header handelt.

Dadurch wird der Status unseres ExpandedStateEnums verändert, und zwar je nach Klick auf expandAll, collapseAll oder collapseAllButSelected.

Dies wiederum bewirkt, dass unser DataTrigger, äh, getriggert wird und mit Hilfe des Converters auswertet, welcher Fall denn nun vorliegt. Der Converter wertet aus und meldet der View zurück welchen Wert IsExpanded annehmen soll – true oder false.

11.01.2021 - 11:58 Uhr

Ich habe es jetzt nicht probiert, aber so wie ich dein XAML lese setzt zu lediglich den ImageBrush mit dem Verweis auf das Image.
Wie gesagt, wenn ich nicht komplett irre, müsstest du erst das Image-Tag setzen mit der Source und darin den ImageBrush (könnte sogar durchaus sein, dass dann darin die Source noch einmal refernziert werden muss/kann).

Ich kreuze mal die Finger 😉

08.01.2021 - 14:23 Uhr

es sollte klar sein, das bei einer Abfrage A und B beide ein Datum haben müssen und C keines
und umgekehrt...

Wie soll dann diese Anweisung funktionieren?

... WHERE datum > 'DateA' AND < 'DateB
... WHERE datum = 'DateC'

Denn wenn deine Daten hier keinen Wert haben, dann erhälst du eine Null-Exception. Oder sind deine DateTimes nullable? Aber dann wäre die ganze Abfrage ja Kokolores...

Ich denke mal, du wirst eine List<DateTime> benutzen, in der alle in Frage kommenden Daten enthalten sind. Dann hättest du so etwas wie

var resultDate = DateList.Where(d => d.Date > DateA && d.Date < DateB).FirstOrDefault(); 

Was ich absolut nicht verstehe ist, wie DatumC da ins Spiel kommt. Oder brauchst du hier einen zweiten Wert? Dann würdest du im Prinzip den obigen Linq-Code mit der Lambda-Expression eben noch mal anwenden:
[

var dateC = DateList.Where(d => d.Date == DateC).FirstOrDefault();

]

Kleine Anmerkung: FirstOrDefault macht hier natürlich nur Sinn, wenn du EIN bestimmtes Datum suchst, denn der Ausdruck wird dir genau das erste DAtum in der Liste, auf den die Lambdas verweisen zurückliefern. Ohne FirstOrDefault wirds ein IEnumerabel.

Hoffe, das hilft dir weiter,
Gruß
vorph

07.01.2021 - 15:40 Uhr

Nur um sicher zu gehen, dass ich das richtig verstanden habe:

Du hast 3 DatePicker: Von (nennen wir jetzt A), bis (B) und Datum (C).

A, B, und C verweisen auf denselben Datensatz in der Datenbank?

Konkretes Beispiel:
A (01.01.2021) , B (01.01.2021) , C (01.01.2021)

Ist das so gemeint?

Falls ja, dann mal folgende Fragen:

  • darf der Benutzer ein in A eingegebenes Datum gar nicht erst in B und C auswählen?
  • dürfen A und C identisch sein?
  • düfen B und C identisch sein?
  • darf weder A, B, noch C mit irgendeinem anderen identisch sein?

So ganz habe ich jetzt nicht verstanden, was du genau erreichen möchtest. Kannst du das was du vorhast genauer beschreiben?

06.01.2021 - 12:33 Uhr

Hallo zusammen,

ich zerbreche mir gerade den Kopf, wie ich folgendes Vorhaben umsetzen könnte:

Ein Datagrid zeigt verschiedene Angaben zu einem Schüler-Objekt an: Name, Alter, Klasse, Klassenstufe, Bildungsgang.

Ich möchte nun, dass wenn man auf den Header „Klasse“ klickt zunächst alle Schüler entsprechend ihrer Klassenzugehörigkeit gruppiert werden. Dazu sollen sie in einem Expander-Element angezeigt werden.

Das funktioniert bereits.

Dazu verwende ich folgenden XAML-Code


<DataGrid x:Name="MyGrid" ItemsSource="{Binding AllStudentsCV.View}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" HeadersVisibility="Column" IsSynchronizedWithCurrentItem="True">
                
<DataGrid.Resources>
                    <Style x:Key="GroupHeaderStyle" TargetType="DataGridColumnHeader">
                        <Setter Property="Command" Value="{Binding RelativeSource={RelativeSource 
                                                    AncestorType={x:Type DataGrid}},
                                                    Path=DataContext.GroupSortCommand}"/>
                    </Style>                  
</DataGrid.Resources>

                <DataGrid.GroupStyle>
                    <GroupStyle>
                        <GroupStyle.ContainerStyle>
                            <Style TargetType="{x:Type GroupItem}">
                                <Setter Property="Template">
                                    <Setter.Value>
                                        <ControlTemplate TargetType="{x:Type GroupItem}">
                                            <StackPanel>
                                                <Expander Header="{Binding Name}"
                                                          FontWeight="Bold" FontSize="14" 
                                                          Background="#FF5CB9EE" ExpandDirection="Down"                                                    
                                                    <StackPanel>
                                                        <StackPanel Orientation="Horizontal">
                                                          <TextBlock Text="{Binding ItemCount,  
                                                                            StringFormat=Schüler: {0}}" Margin="30,0,0,0" />
                                                        </StackPanel>
                                                        <ItemsPresenter/>                                                        
                                                    </StackPanel>                                                    
                                                </Expander>                                                
                                            </StackPanel>
                                        </ControlTemplate>
                                    </Setter.Value>
                                </Setter>
                            </Style>
                        </GroupStyle.ContainerStyle>
                    </GroupStyle>
                </DataGrid.GroupStyle>

                <DataGrid.Columns>
                    
<!-- Hier nur das DataGridTemplateColumn für die Klasse-Column -->
                    <DataGridTemplateColumn Header="Klasse" CanUserSort="True"
                                                 SortMemberPath="StudentPersonal.LastName" >                        
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>                                
                                <TextBlock Text="{Binding StudentEducational.ClassLabel}" 
                                                  HorizontalAlignment="Center" Margin="5"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
             
                </DataGrid.Columns>
            </DataGrid>

Nun kommt der schwierige Teil, bzw. meine Frage:

Wie könnte ich es einrichten, dass

a) beim ersten Klick auf den Header „Klasse“ alle Schüler ihrer jeweiligen Klasse zugeordnet und in einem eigenen Expander angezeigt werden [Expander sollen alle „expanded“ sein – dieses Verhalten habe ich ja jetzt bereits, a) wäre also quasi erledigt] In diesem Zustand können alles Expander also individuell „expanded“ oder „collapsed“ werden.

b) beim zweiten Klick auf den Header alle Expander geschlossen werden (alle Expander „collapsed“), bis (!) auf den, in dem ein Schüler ausgewählt wurde (sozusagen, das aktuelle GroupItem).

c) beim dritten Klick auf den Header sollen dann schließlich alle (!) Expander geschlossen (collapsed) werden, unabhängig davon, ob nun ein Element ausgewählt ist oder nicht.

Wie man an meinem XAML vielleicht schon erkennt, habe ich ein Command installiert, das dem Header „Klasse“ zugeordnet ist und immer dann gefeuert wird, wenn ich auf den Header klicke.

Ich benutze das MVVM-Pattern, d. h. meine View hat ein entsprechendes ViewModel.
Das DataGrid benutzt als ItemsSource eine CollectionViewSource (AllStudentsCV). Deren Source-Property ist meine ObservableCollection<Student>AllStudents.

Ich habe die GroupDescription im XAML-Code, weil dieser ja für die visuelle Präsentation meiner Daten zuständig ist. Das Grouping gehört da ja dazu. Das ist zumindest mein bisheriger Lösungsansatz.

Überlegungen zur Lösung:

Die Logik, um die verschiedenen Schaltzyklen zu managen bekomme ich – denke ich - hin: ich werde im ViewModel einen Zähler einbauen, der bei jedem Klick auf den Header um eins hochzählt und danach ausgewertet wird (if (counter == 1) {//do stuff} else if (counter == 2){…}) und der nach Auswerten von seinem Maximalwert zurückgesetzt wird.

Schwieriger gestaltet sich die Umsetzung des Verhaltens der Expander der einzelnen GroupItems:

Idee: Im DataTemplate des Expanders wollte ich einen DataTrigger hinterlegen, der auf einen Enum reagiert (defaultBehavior, allCollapsedButSelected, AllCollapsed), der der View übergeben wird.
Jedes GroupItem muss jedoch individuell darauf reagieren. Spätestens, wenn es darum geht zu überprüfen, ob das GroupItem das aktuell angewählte ist, dessen Expander sich ja im Fall b) (s. oben) eben nicht schließen soll, sondern als einziges geöffnet bleiben soll, bin ich überfragt.

Wie könnte ich das umsetzten? Sollte ich das Grouping komplett vom ViewModel aus betreiben?
Für Vorschläge bereits jetzt vielen Dank!

Gruß
vorph

28.12.2020 - 22:00 Uhr

Sorry, die Feiertage hielten mich davon ab mich näher mit dem Problem zu beschäftigen. Gestern Abend hatte ich aber eine Stunde Zeit gefunden und tatsächlich den Fehler gefunden!

Nochmal zu der Klasse, die den Fehler auswarf:


public static void DeleteSchoolClass(SchoolClassData _schoolClass)
        {
            using (var context = new DataBaseContext())
            {
                //Die nun folgende Zeile löst den Fehler aus
                var entity = context.SchoolClasses.Remove(_schoolClass);

                entity.State = Microsoft.EntityFrameworkCore.EntityState.Deleted;

                context.SaveChanges();
            }
        }

Das _schoolClass-Objekt konnte bei der Übergabe mit einem vollständigen Datensatz aufwarten - trotzdem dann der unerwartete Fehler. Und genau darin lag das Problem:

Die Fehlermeldung brachte mich auf eine vollkommen falsche Fährte: alles deutete auf einen Logik-Fehler oder eine Fehlkonfiguration der Datenbank oder einer Fehlkonfiguration durch EFC. Am meisten störte mich, dass ich anhand der Migrationen sehen konnte, dass die Datenbank so erstellt wurde, wie sie meiner Meinung nach hätte beschaffen sein müssen!
Die Lösung fand ich jetzt durch Zufall: vor der Übergabe erstelle ich ein neues SchoolClass-Objekt, das alle Daten enthält, die an die DB übergeben werden sollen. Die SchoolClassId war 0, die SchoolClassInternalsId war 0. Als ich dann gestern händisch ein Objekt in die DB eingegeben hatte, stellte ich fest, dass die vergebenen Ids aber jeweils 1 waren.
Wo lag der Fehler? Ich hatte das Datenobjekt an der falschen Stelle übergeben, so dass die Ids (die laut Deklaration ja nicht null sein durften) auf den Wert 0 gesetzt wurden. Diesen Verweis gab es in der DB natürlich nicht.

Mit anderen Worten: es wurde versucht ein Objekt aus der DB zu löschen, dessen Ids nicht vorhanden waren.

Aber ich finde die Annotation-Attribute auch selten dämlich, daher verwende ich sie nie; aber ist meine persönliche Meinung.
Mit den Model Buildern hat man einfach einen viel besseren Überblick und eine viel bessere Kontrolle.

Das kann ich - jetzt - vollkommen verstehen und nachvollziehen! Ich werde das in der finalen Version auch dementsprechend ändern - die Annotations habe ich erst vor kurzem gefunden, als ich erfuhr, dass es ansonsten Probleme bei der Performance geben kann. So wird für ein string-Property z. B. der maximale Speicherbedarf für string reserviert, was zwar für meine kleine App völlig unerheblich ist, aber ich finde, es ist schon wichtig das zu wissen.

Danke, für den Support, Abt! Vielleicht ist es ja auch für dich eine Neuigkeit, dass diese Fehlermeldung durch nicht vorhandene Ids geworfen werden kann?

viele Grüße und schon mal alles Gute für's neue Jahr!

25.12.2020 - 18:51 Uhr

Also, ich hänge total mit diesem Fehler^^

Nur, um noch mal auf die Fehlermeldung einzugehen:

The property 'SchoolClassData.SchoolClassDataId' has a temporary value while attempting to change the entity's state to 'Deleted'.

Was heißt 'temporary value'? Ich meine, ich kann das übersetzen, aber was soll das letztlich bedeuten? Mein Wert kommt ja in der Datenbank an und hat eine eindeutig zugewiesene ID, die laut SQLite Studio not null, unique, und Primary Key ist.

Auch den nächsten Part verstehe ich nicht

Either set a permanent value explicitly, or ensure that the database is configured to generate values for this property.

Was sollte hier ein permanenter Wert bringen? Gerade das will man doch nicht, oder? Und den zweiten Teil (...that the databse is configured to generate...) lese ich so, dass AutoIncrement 'true' sein muss. Auch das ist der Fall.

Aber es wird noch seltsamer:
Die erste Version meines Projekts hatte noch versucht ohne DataAccess-Layer auszukommen; dabei hatte der DataContext freien Zugriff auf die Models. Aber --> in dieser Version kann ich Daten speichern, löschen und updaten.
Dann habe ich Daten-Models angelegt, die nun an stelle der Models im DataContext sind. Und seit dem kommt dieser Fehler.

Ich habe seit Eröffnung dieses Posts noch diese Maßnahme ergirffen:

  • DataContext, Models und DataModels Zeile um Zeile mit dem Vorgänger-Modell verglichen aber keine Unterschiede gefunden. Einziger offensichtlicher Unterschied: es gab in Version 1 keine DataModels. Diese sind aber komplett getrennt von den Models, bzw. dem DataContext. Der DC weiß nicht einmal, dass andere Models außer den DataModels existieren!

  • zwei weitere TestProjekte angelegt, die dann aber letztlich alle denselben Fehler warfen. Dabei hatte ich die Datenbank, bzw. Datenbankobjekte komplett vereinfacht;

Bit es noch irgendeine Datei, einen Verweis o.ä. den ich sichten könnte? Nach meinem derzeitigen Kenntnisstand verlangt der Compiler einen nicht-temporären ID-Wert, den er aber bekommt. Zumindest hat mein Objekt eine eindeutige ID, wenn ich im Debug-Modus das Objekt unmittelbar vor der Ausführung von

var entity = context.SchoolClassesDM.Remove(_schoolClass)

ansehe.