Danke für den Hinweis - so auf den ersten Blick vermute ich, dass ein Großteil der Funktionen, diie ich implementiert habe, dann wegfallen:
Die Eingabe von "," führt in meinem Fall z. B. dazu, dass "0,00 €" abgezeigt werden, das Caret aber an der richtigen Stelle (nach dem Komma) steht. Das macht die TextBox nach der Umwandlung von selbst.
"-", "-0", "-,", "-0,0" werden korrekt als "-0,00 €" interpretiert (mit dem Caret an der Position nach dem Komma, bzw. nach der ersten Null.
Auch das Löschen der Eingaben funktioniert wie intendiert.
Lediglich bei 99 Miliarden 999 Millionen 999 Tausend 999 ist Schluss - wenn ich jetzt noch eine Ziffer eingebe, springt das Caret hinter das Euro-Zeichen. Sonst bleibt es immer vor dem Komma^^
Ich tendiere langsam Richtung Bug: wie bereits erwähnt - im Converter wird sowohl in der Convert-, als auch der ConvertBack-Methode jeweils der korrekte Wert returniert - danach gibt es keine von mir selbst geschriebene Logik mehr, zumal schon gar keine, die die Position des Carets beeinflusst.
Meine CurrencyConverter-Klasse (heißt hier ThirdConverter --> Gründe s. oben)
public static decimal? ConvertCurrencyStringToDecimal(string? _input)
{
StringBuilder sb = new StringBuilder();
sb.Append(_input.Replace(" ", string.Empty));
return decimal.Parse(sb.ToString());
}
public static string? ConvertDecimalValueToCurrencyString(decimal? _input)
{
var regularValueResult = FinalValueConversion(_input);
return regularValueResult;
}
//Helfer-Methoden
private static string? FinalValueConversion(decimal? value)
{
//Define string builder for output string
StringBuilder preString = new StringBuilder();
StringBuilder arrangedOutput = new StringBuilder();
decimal x = Convert.ToDecimal(value);
preString.Append(x.ToString("0.00"));
if (preString.ToString().Contains(","))
{
arrangedOutput.Append(ArrangeOutputString(preString.ToString()));
}
return arrangedOutput.ToString();
}
private static string TrimOutputString(decimal? value)
{
if (value == null)
{ return null; }
StringBuilder final_pre = new StringBuilder();
StringBuilder final_post = new StringBuilder();
StringBuilder final = new StringBuilder();
decimal _value = Convert.ToDecimal(value); // convet decimal? to decimal...
final.Append(_value.ToString("#.00")); //...make it a string
var vsArray = final.ToString().Split(',');
if (vsArray.Length > 1)
{
if (vsArray[1].Length > 2)
{
final.Clear();
final_post.Append(vsArray[1].Remove(2));
final_pre.Append(vsArray[0]);
final_pre.Append(",");
final.Append(final_post.ToString());
}
}
return final.ToString();
}
#region Arrangement of output string methods
private static string ArrangeOutputString(string _preFinalString)
{
StringBuilder sb = new StringBuilder();
string[] dummyArray = _preFinalString.Split(',');
string preCommaValue = dummyArray[0];
string postCommaValue = dummyArray[1];
var dummyList = ConsolidateDigitGroups(preCommaValue);
string finalString = AssembleGroupsOfThree(dummyList);
sb.Append(finalString).Append(",").Append(postCommaValue);
return sb.ToString();
}
private static List<string> ConsolidateDigitGroups(string _unconsolidatedString) //neueste
{
StringBuilder sb = new StringBuilder();
List<string> tmpList = new List<string>();
int numberOfDigits = _unconsolidatedString.Count();
while (numberOfDigits > 3)
{
//IMPORTANT! Add the LAST 3 digits first. E.g.: 1566 - 566 must be truncated first in order to get the final 1 566.
tmpList.Add(_unconsolidatedString.Substring(_unconsolidatedString.Length - 3));
//Remove the digits that were truncated from the original input.
_unconsolidatedString = _unconsolidatedString.Remove(_unconsolidatedString.Length - 3);
//Lower the number of remaining digits by 3 (i.e. the amount of digits that just were truncated)
numberOfDigits -= 3;
}
//If after subtracting 3 each time the while-loop is true, a rest greater than zero remains, add the remaining strings to the list.
if (numberOfDigits > 0)
{
tmpList.Add(_unconsolidatedString);
}
return tmpList;
}
private static string AssembleGroupsOfThree(List<string> stringChunks)
{
StringBuilder sb = new StringBuilder();
//Define an output string.
string outputString = string.Empty;
//Define a limit for the following for-loop - it is equal to the number of entries in the list.
int limit = stringChunks.Count;
//Define an indexer that will access the entries in the stringChunks-List.
//The indexer will start with the integer which represents the maximum number of entries.
int indexer = limit;
for (int i = 1; i <= limit; i++)
{
//Append the LEAST index of the stringChunks-List. [The the list is 0-based index, the actual indexer is indexer - 1]
//With each iteration the indexer will get closer to 0. --> MUST end with zero!
//outputString += stringChunks[indexer - 1];
sb.Append(stringChunks[indexer - 1]);
//Lower the indexer by 1
indexer--;
//Check, wheter the current iteration is less than the limit. As long as the expression is true, add a grouping placeholder (" ").
if (i < limit)
{
//outputString += " ";
sb.Append(" ");
}
}
//return outputString;
return sb.ToString();
}
#endregion
Hier mein Value-Converter
public class TestConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// Convert a decimal to a string
var resultString = ThirdConverter.ConvertDecimalValueToCurrencyString((decimal)value);
return resultString;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
// Convert a currencs string to a decimal
var result = ThirdConverter.ConvertCurrencyStringToDecimal(value.ToString());
return result;
}
}
Und mein xaml-code für die View
UserControl x:Class="ValidateAndConvert_Views.TestView"
[...]
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<conversion:TestConverter x:Key="testConverter"/>
</UserControl.Resources>
<Grid Background="LightGray">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<!-- Content -->
<Grid Grid.Row="1">
<TextBox Text="{Binding UserInput, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource testConverter}, ConverterParameter=true}">
</TextBox>
</Grid>
</Grid>
</UserControl>
Lösungsversuche bisher:
Ich habe beim Debuggen Haltepunkte gesetzt (im ValueConverter): dort wird der decimal 1234567891 zu "1 234 567 891,00" konvertiert. Hier springt nun aber das Caret ans Ende. Bei allen vorherigen Ziffern - nicht.
--> Wenn ich die Formatierung weglasse (also das arrangieren in 3-er-Gruppen und das Hinzufügen der Dezimalstellen), dann kann ich natürlich so viel eingeben, wie ich möchte. Trotzdem verstehe ich nicht, wieso es nach Eingabe von 9 Zeichen abrupt nicht mehr funktioniert, zumal weiterhin korrekt formatiert ausgegeben wird, wenn ich das Caret manuell an die richtige Stelle setze.
Hilfen:
Vorab vielen Dank!
viele Grüße
Vorph
Hallo,
ich hänge gerade an einem Problem, das mir einige Rätsel aufgibt. Ich versuche es so einfach wie möglich zu beschreiben:
Ich habe eine TextBox, in die nur die Ziffern von 0-9, sowie ein ',' eingegeben werden können. Ich möchte letztendlich einen Währungsbetrag angeben können. Aus der DB wird ein in meinem ViewModel ein decimal gelesen und der wird von einem ValueConverter in der View als string ausgegeben.
Im ValueConverter rufe ich eine CurrencyConversion-Klasse (im Code untern "ThirdConverter" --> der ungünstige Name steht da, weil ich seit Tagen Fehlersuche betreibe und dies der 3. Konverter ist, mit dem ich versuche das Problem zu lösen...), die aus dem decimal einen string im gewünschten Währungsformat ausgiebt. Ich weiß, es gibt buil-in-Lösungen, aber
Meine Methoden liefern das gewünschte Resultat: ich gebe einen Betrag ein, z. B. 1425,36 und bekomme in der Textbox "1 425,36 €" angezeigt.
Wichtig ist hierbei: nach jeder Eingabe befindet sich das Caret in der Textbox immer an der richigen Stelle - da musste ich auch gar nix machen, das regelt die Textbox selbst.
PROBLEM: Zumindest dachte ich, dass die Textbox selbst regelt. Denn aus unerfindlichen Gründen springt das Caret bei der Eingabe von 1234567891 (bei Eingabe der letzten 1) ganz ans Ende der Selection. Ich kann dann zwar das Caret hinter der 1 positionieren und eine weitere Eingabe machen, ab dann wird es aber immer ganz ans Ende springen (die Formatierung wird dabei aber weiterhin tadellos in 3-er-Gruppen angezeigt).
Nachfolgend mein Code.
Zur Beachtung: er ist aus meinem Test-Projekt eingefügt, d.h. ich habe ihn so einfach wie nur irgend möglich gehalten. Man kann in dieser Version nur Ziffern eingeben, andere Zeichen crashen die Anwendung. Das "springende" Caret ist ja das Problem.
HI Paladin,
vielen Dank für deine Hilfe - so funktioniert es nun 😃
Den Satz hab ich leider erst übersehen.
Was heißt denn "funktioniert nicht"?
Ehrlich gesagt: ich bin selbst am Rätseln! Ich hätte beinahe zurückgeschrieben, dass es tatsächlich nicht funktioniert - und zwar hatte ich mein xaml-Code entsprechend deinen Ausführungen abgeändert, trotzdem wurde die View nicht angezeigt. Keine Fehlermeldung kein gar nichts - nur die View wurde nicht angezeigt. Ich habe dann allerdings - einfach versuchsweise - meine anderen beiden Views mal eingesetzt, z. B. so:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles/ColorPaletteStyles.xaml"/>
<!-- ... -->
<ResourceDictionary>
<DataTemplate DataType="{x:Type viewModels:OverviewViewViewModel}">
<views:OverviewVieww/>
</DataTemplate>
<!-- ... -->
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
--> und es hat auf Anhieb funktioniert! Ich konnte und kann mir nicht erklären, warum es mit der DefaultView einfach nicht klappen wollte. Ich weiß aber noch, dass ich gestern beim Erstellen der DefaultView eine Fehlermeldung bekommen habe, gleich nach dem Erstellen: ich kann den Inhalt leider nicht wiedergeben - ich wurde lediglich im DEsigner aufgefordert die Projektmappe neu zu erstellen. Nachdem der Inhalt der xaml im Designer einwandfrei dargestellt wurde und auch sonst alles lief, dachte ich, der Fehler hätte sich mit dem neu erstellen des Projekts erledigt. Ich kann nicht die Hand dafür ins Feuer legen, dass das tatsächlich der Grund ist, aber: ich habe den xaml-Code der DefaultView einfach kopiert, die xaml-Datei gelöscht und erneut eine DefaultView erstellt. Keine Fehlermeldung diesmal - alles klappt jetzt einwandfrei!
Deine Hilfe hat mir viele Kopfschmerzen hier erspart: ich hätte den Fehler sonstwo gesucht.
Gruß
Vorph
Hallo,
nach langer Pause, versuche ich mich derzeit wieder an einer WPF-App. Da ich auf meiner MainView einen Content-Bereich habe, mit dem ich zu den einzelnen Views navigieren kann, benutze ich die ApplicationResources, um dort DataTemplates für meine Views zu hinterlegen. Das sieht dann so aus:
<Application x:Class="AppStart.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:Views;assembly=Views"
xmlns:viewModels="clr-namespace:MainViewModels;assembly=ViewModels"
xmlns:local="clr-namespace:AppStart"
StartupUri="pack://application:,,,/Views;component/MainView.xaml">
<Application.Resources>
<DataTemplate DataType="{x:Type viewModels:DefaultViewViewModel}">
<views:DefaultView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:OverviewViewModel}">
<views:OverviewView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:DetailViewModel}">
<views:DetailView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:SummaryViewViewModel}">
<views:SummaryView/>
</DataTemplate>
</Application.Resources>
</Application>
Nun möchte ich allerdings Styles implementieren, auf die in der ganzen App zugegriffen werden soll (für Textboxen, Textblocks, Comboboxen, etc.). Wenn ich das so in meine App.xaml verpacke...
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!--Order is important-->
<ResourceDictionary Source="Styles/ColorPaletteStyles.xaml"/>
<ResourceDictionary Source="Styles/ButtonStyles.xaml"/>
<ResourceDictionary Source="Styles/CheckBoxStyles.xaml"/>
<ResourceDictionary Source="Styles/ComboBoxStyles.xaml"/>
<ResourceDictionary Source="Styles/RadioButtonStyles.xaml"/>
<ResourceDictionary Source="Styles/TextBlockStyles.xaml"/>
<ResourceDictionary Source="Styles/TextBoxStyles.xaml"/>
<ResourceDictionary Source="Styles/MenuStyle.xaml"/>
<ResourceDictionary Source="Styles/TabControlStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
...funktioniert das auch einwandfrei. Allerdings - meine DataTemplates, die ich für die View-Navigation brauche kann ich nun nicht mehr wie bisher hinzufügen. Also folgendes funktioniert nicht:
<Application.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type viewModels:DefaultViewViewModel}">
<views:DefaultView/>
</DataTemplate>
<ResourceDictionary.MergedDictionaries>
<!--Order is important-->
<ResourceDictionary Source="Styles/ColorPaletteStyles.xaml"/>
<ResourceDictionary Source="Styles/ButtonStyles.xaml"/>
[...]
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
Auch das hier funktioniert nicht:
<Application.Resources>
<DataTemplate DataType="{x:Type viewModels:DefaultViewViewModel}">
<views:DefaultView/>
</DataTemplate>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!--Order is important-->
<ResourceDictionary Source="Styles/ColorPaletteStyles.xaml"/>
<ResourceDictionary Source="Styles/ButtonStyles.xaml"/>
[...]
Hier bekomme ich wenigstens einen Hinweis:
Jeder Verzeichniseintrag muss einen zugewiesenen Schlüssel aufweisen.
Auch damit komme ich nicht weiter: zwar verschwindet der Hinweis, wenn ich einen x:Key hinzufüge, allerdings müsste ich dann jedesmal wenn ich die Dienste des DataTemplates benötige das jeweilige Template explizit mit Key ansprechen, was aber beim Navigieren eher nicht geht (bzw. ich weiß nicht ob es überhaupt einen Weg gibt, das so zu bewerkstelligen...).
Die Essenz der langen Vorrede: ich bin sicher, es gibt einen einfachen Weg, damit meine DataTemplates und die ResourceDictionaries in der App.xaml friedlich koexistieren können. Genau hier brauche ich Hilfe: wie bewerkstellige ich das?
Vielen Dank,
beste Grüße
Vorph
--> ich habe bereits versucht die DataTemplates in ein REsourceDictionary auszulagern - leider funktioniert auch das scheinbar nicht.
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.
@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 😃
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
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.
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!
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?
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 😉
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
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!
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:
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
Sch.. - Anfängerfehler 😦
Danke, Abt.
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:
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
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
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
<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:
Wie gesagt: für konkrete Hinweise bin ich euch sehr dankbar. Ich möchte mir nur ungern ein neues Hobby suchen 😉
Gruß
Vorph
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.
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:
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.
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
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
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
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
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();
}
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: 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:
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:
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 🙂
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
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:
Gruß
vorph
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!
@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.
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.
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
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
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
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
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.
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
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 😄
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:
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...)
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
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 😉
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;}
}
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
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:
Lege in deinem Projekt eine Datei an nach dem Schema (MyControlName)ViewModel --> z. B. MainWindowViewModel
Dein ViewModel muss INotifyPropertyChanged implementieren, z.B. so:
public class MainWindowViewModel: INotifyPropertyChanged
<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
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
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).
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?
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
[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:
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.
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 😉