Laden...

WPF Combobox SelectionChanged wirft Cast-Exception (Items = Colors)

Letzter Beitrag vor einem Jahr 13 Posts 686 Views
WPF Combobox SelectionChanged wirft Cast-Exception (Items = Colors)

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?

Laut How to use RichTextBox in WPF? sollte dein Code mit einem Brush funktionieren. Probiere es zuersteinmal mit einem festen Wert (wie im Link mit Brushes.Red). Es sollte aber selbstverständlich auch mit new SolidColorBrush(color) fehlerfrei laufen.

Außerdem wird dort TextBlock.ForegroundProperty verwendet.

Und zum Aktivieren der RichTextBox rufe auch im Konstruktor rtbEditor.Focus() auf (dazu muß dein TextEditorView-Objekt selbstverständlich auch im übergeordneten Element aktiviert worden sein).

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!

Du weißt aber schon, dass die statischen Eigenschaften der statischenColors Klasse vom Typ Color sind?

Du weißt aber schon, dass die statischen Eigenschaften der statischenBrushes Klasse vom Typ Brush(bzw. konkret SolidColorBrush) sind?

Das casten in eine statische Klasse (z.B. Colors, Brushes) ist idR. immer falsch.

Hat die Blume einen Knick, war der Schmetterling zu dick.

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.

[vorläufige Lösung] gelöst ja, verstanden nein

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

Das ursprüngliches Problem ist (war), wie Du schon vermutet hast, folgende Zeile:

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

Die GetProperties-Methode gibt dir nicht die einzelnen Farben (Color-Werte) zurück, sondern PropertyInfo-Objekte, die Metadaten zur jeweiligen Property enthalten. Konkret bekommst Du RuntimePropertyInfo-Objekte zurück, das ist eine Ableitung von PropertyInfo und wird von .NET intern benutzt. Diese Metadaten sind dann z.B. der Typ oder der Name der Property. Recherchier mal zu Reflection, dann wird es vielleicht klarer.

Dein Layout (das {Binding Name}) funktioniert trotzdem, weil die PropertyInfo auch eine Name-Property hat. Microsoft hat in diesem Fall die ganzen Properties in der Colors-Klasse so benannt, wie auch die Farben heißen, deshalb wird der richtige Farb-Name dargestellt.

Und deine vorläufige Lösung funktioniert, weil WPF einen ValueConverter für die ForegroundProperty hat, der automatisch in einen Brush konvertieren kann. Das ist nötig, damit Du im XAML einfach nur den Namen der Farbe eintragen kannst, der ValueConverter sucht diesen Namen dann in den bekannten Farben und konvertiert automatisch. Und dass die Namen der Properties in der Colors-Klasse identisch zu den bekannten Farb-Namen sind, hatte ich ja schon erwähnt.


Probier mal folgendes:

cmbFontColors.ItemsSource = typeof(Colors).GetProperties().Select(p => p.GetValue(null));

Damit sollten die einzelnen Items dann vom Typ Color sein, dann kannst Du in der cmbFontColors_SelectionChanged-Methode auch darauf casten.

Allerdings wirst Du damit dein Ziel auch nicht erreichen, aus zwei Gründen:

  • Die ForegroundProperty ist nicht vom Typ Color, sondern vom Typ Brush. Der ValueConverter konvertiert aber nur String (Farb-Name) und Brush hin und her, nicht aber die Color-Werte. Gleiches gilt für die Fill-Property von Rectangle, das funktioniert dann auch nicht mehr.
  • Die Color-Werte haben keine Name-Property, dein Binding auf den Namen funktioniert also nicht.

Daher ist deine (vorläufige) Lösung sogar genau richtig so.

Du könntest aber auch einen Umweg über eine Zwischen-Klasse gehen. Dafür musst Du dann wie gehabt die PropertyInfo-Objekte abrufen und pro Item dann eine Instanz der Zwischen-Klasse erstellen:

cmbFontColors.ItemsSource = typeof(Colors).GetProperties()
    .Select(p => new ColorInfo()
    {
        Name = p.Name,
        Brush = new SolidColorBrush((Color)p.GetValue(null))
    });

Im XAML dann:

<Rectangle Fill="{Binding Brush}" Width="16" Height="16" Margin="0,2,5,2" />
<TextBlock Text="{Binding Name}"/>

Und in der cmbFontColors_SelectionChanged-Methode kannst Du dann auf deine Zwischen-Klasse casten und die Brush-Property verwenden:

ApplyPropertyValueToSelectedText(TextElement.ForegroundProperty, ((ColorInfo)e.AddedItems[0]).Brush);

Diese Lösung hätte den Vorteil, dass Du beliebig viele eigene Farben und die volle Bandbreite der Möglichkeiten der Brush-Klasse nutzen kannst, da gibt's nicht nur SolidColorBrush. Oder Du möchtest die Farben lokalisiert darstellen, dabei würde dir diese Zwischen-Klasse auch in die Hände spielen.

Welche Lösung jetzt besser ist, da kann man sicher hervorragend drüber streiten und hängt auch von deinen Anforderungen ab.
Bedenke aber auch, dass deine Lösung nur dann verständlich ist, wenn man weiß, wie WPF bei dem Thema tickt, das hast Du ja auch schon gemerkt.
Insofern ist der Weg über die Zwischen-Klasse definitiv leichter verständlich.


Abgesehen davon:

Ein try mit leerem catch macht man nicht, das würde ich dir beim Code-Review um die Ohren hauen 😉
Das macht man nur, wenn man auch einen sehr guten Grund dafür hat und dieser Grund gehört dann auch kommentiert.


@Th69:

Zitat von Th69

Außerdem wird dort TextBlock.ForegroundProperty verwendet.

Das ist identisch. In der TextBlock-Klasse wird TextElement.ForegroundProperty.AddOwner(typeof(TextBlock)) verwendet, was wiederum this zurück gibt, siehe hier und hier.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

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

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.

wenn ich einen Default setzen möchte

Die ComboBox hat einen SelectedValuePath-Property, kann sein, dass die dein Problem löst, wenn Du dort "Name" einträgst.
Ich bin mir aber nicht sicher, was dann in e.AddedItems steht, kann sein, dass da dann auch der Name steht, das müsstest Du prüfen.

Oder Du nutzt Folgendes:

var defaultItem = typeof(Colors).GetProperty(nameof(Colors.Blue));

Das liefert dir das selbe (ganz wichtig) PropertyInfo-Objekt, sodass Du das einfach als ausgewähltes Item in der ComboBox nutzen kannst.

ob ich in zwei, drei Jahren direkt Blicke, warum da kein Brush drinne ist

Das typeof(Colors) ist nicht die Colors-Klasse, sondern ein Type-Objekt der Colors-Klasse, was Typ-Informationen bereithält - ganz wichtiger Unterschied ^^

Die Type-Klasse ist dafür gedacht, Metadaten aller Typen (Klassen, Structs, Delegates, Generics, etc.) verfügbar zu machen und genau das tut sie. Und die GetProperties- und GetProperty-Methoden liefern dir das Gegenstück für eine einzelne Properties. Mit dem Type-Objekt kannst Du eine Instanz des Typs erzeugen (bzw. mit der Activator-Klasse) und das PropertyInfo-Objekt kann den Wert abrufen oder setzen - vorausgesetzt der Typ bzw. die Property lassen das zu. Aber es bleiben Metadaten, nicht mehr und nicht weniger.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Zitat von GeneVorph

[…] warum da kein Brush drinne ist. […]

Wenn du Colors bemühst und die Eigenschaften vom Typ Color sind (macht ja auch Sinn) wieso erwartest du dann den Typ Brush?

Wenn du das haben möchtest dann nimm statt Colors die Klasse Brushes, denn die hat (wie der Name schon verkündet) Eigenschaften vom Typ Brush (also den du auch haben möchtest).

Wenn man in die Apfelkiste greift und sich wundert dass da keine Bananen drin sind. 🤔

Hat die Blume einen Knick, war der Schmetterling zu dick.

Stimmt, daran hatte ich gar nicht gedacht - einfach Brushes nehmen 😄

cmbFontColors.ItemsSource = typeof(Brushes).GetProperties()
    .Select(p => new BrushInfo()
    {
        Name = p.Name,
        Brush = (Brush)p.GetValue(null)
    });

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.