Tutorialreihe <WPF lernen/>

    • WPF

    Es gibt 37 Antworten in diesem Thema. Der letzte Beitrag () ist von Nofear23m.

      2.1.4.8

      Auf die aktuelle Culture eingehen



      Die WPF biete diverse sinnvolle Mechanismen um z.b. den Text in einer TextBox so darzustellen wie dies der aktuelle Anwendungsfall erfordert.
      Das StringFormat beim Binding ist hier ein sehr gutes Beispiel welches wir bereits unter anderem in Kapitel 2.1.4.7 verwendet hatten.

      Aber wie sieht es eigentlich aus mit der Kultur? Kann ich das selbst steuern? Was ist der Default wert bei einer WPF Anwendung? Wie kann ich steuern ob wie ein Datum angezeigt wird oder welches Währungssymbol hinter einem Betrag steht? Das sind Fragen auf welche wir heute eingehen werden.
      Erstmal sein gesagt das die WPF per Default von en-US als Kultureinstellung ausgeht. Das bedeutet dass in allen Controls (DataGrid, TextBox, Label usw.) das amerikanische Format verwendet wird.
      Um dies zu demonstrieren habe ich mal ein Window erstellt welches dies demonstriert.

      XML-Quellcode

      1. <Window x:Class="MainWindow"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      5. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      6. mc:Ignorable="d"
      7. Title="MainWindow" Height="450" Width="800">
      8. <DockPanel LastChildFill="True">
      9. <Border BorderThickness="2" BorderBrush="Black" DockPanel.Dock="Bottom">
      10. <StackPanel DataContext="{Binding SelectedPerson}">
      11. <TextBox Text="{Binding Firstname}"/>
      12. <TextBox Text="{Binding Lastname}"/>
      13. <TextBox Text="{Binding Birthday}"/>
      14. <TextBox Text="{Binding Credit, StringFormat=\{0:C\}}"/>
      15. </StackPanel>
      16. </Border>
      17. <DataGrid ItemsSource="{Binding Persons}" SelectedItem="{Binding SelectedPerson}"/>
      18. </DockPanel>
      19. </Window>




      Wie man gut erkennen kann wird sowohl für den Geburtstag als auch für das Guthaben der Personen die amerikanische Kultur verwendet. Bis hin zum Dollar-Zeichen.
      Das kann auch zu Problemen führen. Wenn wir und die TextBox für das Guthaben unten genauer ansehen sehen wir das hier das „Tausender-Trennzeichen“ ein Komma ist und das Komma ein Punkt.

      Viele gehen nun her und verwenden schlicht statt dem StringFormat \{0:C\ ein eigenes StringFormat und denken dass damit die Sache erledigt ist. Weit gefehlt.
      Gibt der User nun einen Betrag wie 1493,67 ein wird die WPF in diesem Fall nämlich 149367 als Wert zurückspeichern. Fatal! Man müsste nämlich 1494.67 eingeben.
      Also ist es wichtig dass wir die richtige Kultur (Culture) verwenden. Ob man nun die Culture des Windows Benutzers mittels CultureInfo.CurrentCulture verwendet oder ob man es den Benutzer aussuchen lässt (Beispielsweise über Einstellungen) bleibt einem hier selbst überlassen.

      Am besten setzt man die Kultur in der Application_Startup Methode, kann allerdings natürlich auch sonst wo gesetzt werden wie Beispielsweise im Window_Loaded des Startfensters.

      VB.NET-Quellcode

      1. Dim currentCulture As CultureInfo = CultureInfo.CurrentCulture Thread.CurrentThread.CurrentCulture = currentCulture Thread.CurrentThread.CurrentUICulture = currentCulture LanguageProperty.OverrideMetadata(GetType(FrameworkElement), New FrameworkPropertyMetadata(
      2. XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)))


      Möchte man nicht die Systemkultur verwenden dann kann die erste Zeile wie folgt ersetzt werden:

      VB.NET-Quellcode

      1. Dim currentCulture As CultureInfo = New CultureInfo("de-DE")


      So könnte in den Programmeinstellungen der String für die Kultur hinterlegt werden.
      Hier ein kleiner Einblick was in der Culture so hinterlegt ist:



      Wie wir sehen geht es bei der Kultur nicht einfach nur um die Sprache, sondern auch um Tastaturlayouts, Kalender, Nummernformate usw.!
      Es gibt auch Sprachen welche man von rechts nach links liest wie z.b. Arabisch.
      OK, sehen wir uns mal an wie dies aussieht wenn wir die aktuelle Systemsprache auf das oben bereits gezeigt Fenster anwenden indem wir im Window_Loaded Event die Culture setzen.



      Das sieht schon besser aus. Wir haben sowohl das Deutsche Datumsformat als auch das Euro-Symbol und das korrekte Trennzeichen.
      Geben wird nun einen Betrag mit einem Komma wie 1657,47 ein wird der Wert nun auch korrekt gespeichert.

      Converter
      Als nächstes sehen wir uns an wie dies mit Convertern aussieht. Converter haben wir ja bereits in Kapitel 2.1.4.4 kennengelernt. Nun sehen wir uns an wie wir innerhalb eines Converters auf die aktuelle Kultur eingehen.
      Hierfür erstellen wir einen Konverter mit dem Namen „TestConverter“. Dieser konvertiert im Moment nichts. Er soll uns nur dazu dienen zu sehen welche Culture wir in einem Converter hineingereicht bekommen.

      VB.NET-Quellcode

      1. Public Class TestConverter
      2. Implements IValueConverter
      3. Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
      4. Debug.WriteLine("Aktuelles Währungssymbol: " & culture.NumberFormat.CurrencySymbol)
      5. Return value
      6. End Function
      7. Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
      8. Throw New NotImplementedException()
      9. End Function
      10. End Class


      Wir lassen uns hier schlicht das aktuelle Währungssymbol in die Konsole ausgeben um anschließend einfach den value so wie dieser ist wieder zurückzugeben.
      Der Konverter gibt folgenden Text in die Konsole aus:

      Quellcode

      1. Aktuelles Währungssymbol: €


      Wir sehen also dass hier auch die Systemkultur mit übergeben wird und könnten nun darauf reagieren um Beispielsweise Umwandlungen zu vollziehen.

      Fazit: Die WPF nimmt per Default immer en-US als Culture heran.
      Dies muss vom Entwickler überschrieben werden da sonst immer und überall das amerikanische Format verwendet wird.
      Dies ist wie wir gesehen haben auch sehr wichtig da es ansonsten zu sehr unangenehmen Nebeneffekten kommen kann welche oft gerne mal länger unentdeckt bleiben.


      Natürlich erkläre ich dies alles auch wieder interaktiv in einem Video wo ihr besser sehen könnt wie jeder Schritt funktioniert.
      Ich freue mich auch über jedes Like :thumbup: oder ein Kommentar :whistling: im Video oder dem Kanal.




      Hier die Solution: 2_1_4_8_Culture.zip
      Und wie immer auch das PDF für euer E-Book:
      Tutorialreihe WPF lernen 2_1_4_8.pdf

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:

      Grüße
      Sascha
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      2.1.5.(1) Dependency Properties

      2.1.5

      Dependency Properties



      Jeder der mit der WPF Arbeitet hört früher oder später von Dependency Properties. Sie sind Voraussetzung für Styles, Trigger oder Animationen. Jeder verwendet Dependency Properties da jedes Control diverse Eigenschaften besitzt welche als Dependency Properties implementiert sind. Bei den BuildIn Controls sind dies fast alle Properties.

      2.1.5.1

      Was sind Dependency Properties und wie unterscheiden sie sich von normalen Properties



      Generell wird bei Dependency Properties zwischen zwei Arten unterschieden:
      • Dependency Properties, die durch klassische .Net Properties gekapselt werden. Diese werden auf einfach Dependency Properties genannt.
      • Dependency Properties, welche durch zwei statische Methoden gekapselt werden. Dabei wird die Dependency Property auf Objekte anderer Klassen gesetzt. Diese werden auch als Attached Properties genannt.

      Wir werden klären wann und warum eine Dependency Property implementiert werden sollte bevor wir dann eigene Dependency Properties erstellen und Anwenden. Ich werde auch auf die Metadaten, die Validierung und Logik dahinter eingehen.

      DependencyObject und DependencyProperty

      Die Klasse DependencyProperty definiert im Grunde nur den Schlüssel zu einem Wert einer Dependency Property, später dazu mehr.
      DependencyObject definiert zum Setzen oder Abfragen einer Dependency Property die Methoden GetValue und SetValue. Eine DependencyProperty-Instanz stellt im Grunde lediglich den Schlüssel zum eigentlichen Wert dar.
      Vereinfacht gesehen speichert eine DependencyObject-Instanz die Werte von DependencyProperties intern in einer Art IDictionary-Instanz unter dem entsprechenden Schlüssel ab. Da die Klasse DependencyObject für das Speichern und Verwalten von DependencyProperties zuständig ist können auch nur Objekte welche von DependencyObject ableiten Werte einer Dependency Property speichern.

      Der Wert einer DependencyProperty ist von vielen Faktoren abhängig, daher der Name Dependency Property (Dependency = Abhängigkeit). Der Wert kann durch ein DataBinding oder eine Animation geändert werden. Die Dependency Property kann einen Default-Wert haben und sogar durch ein im Logical Tree (der Elementebaum in der WPF) höher liegendem Element oder weiteren Metadaten beeinflusst werden welche Beispielsweise auch einen Layoutprozess auslösen können. All diese Dinge müssen bedacht und auch Priorisiert werden. Z.b. muss der Wert einer Animation immer Vorrang vor einem lokal gesetzten Wert haben. Diese Logik übernimmt uns die Property Engine. Die Engine ist im Grunde etwas Code innerhalb von DependencyObject welcher uns diese Arbeit abnimmt wodurch einiges in der WPF gerade in Verbindung mit Binding „wie von Geisterhand“ wirkt.
      Wie bereits angesprochen sind fast alle Properties von Elementen der WPF als Dependency Properties implementiert. Auch in eigenen Klassen können wir Dependency Properties implementieren, der größte Vorteil ist das Properties welche als Dependency Properties implementiert sind, bereits über einen internen Benachrichtigungsmechanismus verfügen und somit kein INotifyPropertieChanged implementiert werden und das Event OnPropertieChanged geworfen werden muss. Dadurch ist eine Dependency Property ideal für DataBinding geeignet. Neben diesem Vorteil gibt es noch andere Vorteile wie Default-Werte, oder Metadaten welche eben nur mit Dependency Properties zur Verfügung stehen.

      Nachstehend eine Tabelle in welche ich diese einfach mal als Services bezeichne:

      Service
      Beschreibung
      Animationen
      Die WPF besitzt eine sehr gute Unterstützung von Animationen welche nur von Dependency Properties verwendet werden können.
      Metadaten
      Eine Dependency Property besitzt MetaDaten über welche sehr viel gesteuert werden kann wie Beispielsweise der Default-Wert.
      Expressions
      Expressions ermöglichen es das der Wert eines Dependency Property zur Laufzeit dynamisch ermittelt werden kann. Z.b. sind DataBinding und DynamicResources über Expressions implementiert.
      Data Binding
      DataBinding ist über Expressions implementiert, dabei muss die Ziel-Property ein DependencyProperty sein.
      Styles
      Styles haben wir bereits kennengelernt. In einem Style lassen sich allerdings nur Properties setzen welche als Dependency Property implementiert sind.
      Vererbung
      Der Wert einer Dependency Property kann über den Logical Tree vererbt werden was sich über die Metadaten definieren lässt.


      Im nächten Kaptiel erstellen und Rendern wir eine Ellipse welche einen Text enthält unter Verwendung von Dependency Properties und machen diese sogar auch abhängig zueinander um zu sehen wie sich dies auf deren Werte auswirkt.

      Und wie immer auch das PDF für euer E-Book: 2.1.5.1.pdf

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:

      Grüße
      Sascha
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      2.1.5.2 Wir erstellen und Rendern eine „TextEllipse“ mittels Dependency Properties

      2.1.5.2

      Wir erstellen und Rendern eine "TextEllipse" mittels DependencyProperties



      Um die Funktionsweise und das Zusammenspiel von Dependency Properties in der Praxis kennenzulernen möchte ich ein Simples Beispiel erstellen welches eine Ellipse rendern soll in welcher sich in der Mitte ein Text befindet.
      Dies könnte z.b. so aussehen:


      Um auch Binding und die Manipulation von Dependency Properties kennenzulernen soll man im XAML für diese Ellipse die Eigenschaft Diameter angeben können, welche den Durchmesser beschreibt. Weiters soll man einen Text innerhalb des XAML angeben können.
      Was aber wenn der Text länger wird als der Durchmesser der Ellipse zulässt? In diesem Fall soll der gesetzte Wert des Durchmessers überschrieben werden damit sich die Ellipse automatisch dementsprechend vergrößert. Unabhängig davon was zuvor im XAML als Wert für den Durchmesser angegeben wurde.

      Wir legen uns eine neue Klasse mit dem Namen TextEllipse an.
      Diese Klasse erbt von FrameworkElement (da wir das Element auch Rendern möchten).

      VB.NET-Quellcode

      1. Public Class TextElipse
      2. Inherits FrameworkElement
      3. End Class




      Hinweis: FrameworkElement erbt von UIElement-> UIElement erbt von Visual-> Visual erbt von DependencyObject



      Das ist der Grund warum wir hier auch Dependency Properties verwenden können.




      Wir wissen also dass wir zwei DependecyProperty benötigen. Eines für den Text und eines für Diameter.

      Wir beginnen mit dem Property Text:

      VB.NET-Quellcode

      1. Public Property Text As String
      2. Get
      3. CType(GetValue(TextProperty), String)
      4. End Get
      5. Set(ByVal value As String)
      6. SetValue(TextProperty, value)
      7. End Set
      8. End Property
      9. Public Shared ReadOnly TextProperty As DependencyProperty =
      10. DependencyProperty.Register("Text",
      11. GetType(String), GetType(TextEllipse))


      Sehen wir uns dieses Konstrukt mal genauer an. Das eigentliche Property sieht wie ein normales .Net Property mit einem Getter und einem Setter aus. Ist es auch.
      Normalerweise haben wir beim einem Property mit einem Getter und Setter ein sogenanntes BackingField welches einen Wert in einer privaten Variable speichert welcher im Getter abgerufen und übergeben wird (Return Anweisung) und im Setter neu gesetzt wird. Das ist hier nicht der Fall. Wie im vorigen Kapitel bereits angesprochen wird der Wert eines Dependency Property immer mit den Methoden GetValue und SetValue aus der Klasse DependencyObject abgerufen bzw. gesetzt.

      Das eigentliche Dependency Property befindet sich als Feld darunter und wird per Konvention immer mit dem Namen des Property + „Property“ benamst wie im obigen Beispiel gut zu sehen ist, und ist vom Typ DependencyProperty. Dieses ist außerdem immer Shared ReadOnly und muss Public sein. Dies ist quasi der bereits erwähnte „Schlüssel“ für den eigentlichen Wert.
      Dieses Feld wird initialisiert indem die Statische Methode Register der Klasse DependencyProperty aufgerufen wird.
      Diese Methode verlangt im ersten Konstruktor zumindest den Namen der .Net Eigenschaft (hier „Text“), den Typ der Eigenschaft (hier vom Typ String) und den Typ des Objekts welches die Eigenschaft besitzt. (Hier ist das die Klasse „TextEllipse“).



      Tipp: Visual Studio erstellt automatisch ein Grundgerüst eines Dependency Property. Hierfür besitzt Visual Studio ein Snippet welche mit dem Keyword „wpfdp“ aufgerufen werden kann.Tippen Sie im VisualBasic Editor „wpfdp“ ein und drücken 2x TAB wird ein Grundgerüst eingefügt in welchem nur noch die Platzhalter ausgefüllt werden müssen. Näheres seht ihr im Video zu diesem Kapitel.



      Es gibt aber weitere Überladungen der Register Methode. Hier die Definitionen:

      VB.NET-Quellcode

      1. Register(name As String, propertyType As Type, ownerType As Type) As DependencyProperty
      2. Register(name As String, propertyType As Type, ownerType As Type, typeMetadata As PropertyMetadata) As DependencyProperty
      3. Register(name As String, propertyType As Type, ownerType As Type, typeMetadata As PropertyMetadata, validateValueCallback As ValidateValueCallback) As DependencyProperty


      Die zweite Überladung nimmt auch sogenannte Property-Metadaten an. Metadaten bestimmen Dinge wie Beispielsweise den Standardwert. Ihnen kann auch ein Delegate für den PropertyChangedCallback übergeben werden. Diese Methode wird dann aufgerufen wenn sich der Wert eines Dependency Property ändert.
      Machen wir dies mal Anhand des Beispiels mit dem Text-Property.

      VB.NET-Quellcode

      1. Public Shared ReadOnly TextProperty As DependencyProperty =
      2. DependencyProperty.Register("Text",
      3. GetType(String), GetType(TextElipse),
      4. New PropertyMetadata("", AddressOf OnTextChanged))
      5. Private Shared Sub OnTextChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
      6. End Sub


      Wir übergeben hier an die zweite Überladung von Register eine neue Instanz von PropertyMetadata welche in der vierten Überladung als ersten Parameter einen Default-Value erwartet welchen wir auf einen Leerstring festlegen.
      Als zweiten Parameter erwartet die Überladung einen PropertyChangedCallback.Die Methode welche wir hier angeben wird immer aufgerufen wenn sich der Wert des Property ändert.
      Jetzt fragt sich der ein oder andere warum man denn nicht einfach die Änderung des Wertes im Setter des Property abfängt. Aus dem Grund das dies von „außen“ umgangen werden könnte da man den Wert nicht nur über das Property selbst sondern auch mit der Methode SetValue aus der Klasse DependencyObject überschreiben könnte. Dies würde den Setter „umgehen“.

      Nun Fehlt noch das zweite Dependency Property:

      VB.NET-Quellcode

      1. Public Property Diameter As Double
      2. Get
      3. Return CDbl(GetValue(DiameterProperty))
      4. End Get
      5. Set(ByVal value As Double)
      6. SetValue(DiameterProperty, value)
      7. End Set
      8. End Property
      9. Public Shared ReadOnly DiameterProperty As DependencyProperty =
      10. DependencyProperty.Register("Diameter",
      11. GetType(Double), GetType(TextElipse),
      12. New PropertyMetadata(Double.Parse("40"))


      Um nun das Objekt zu Rendern überschreiben wir die Methode OnRender der Basisklasse:

      VB.NET-Quellcode

      1. Protected Overrides Sub OnRender(drawingContext As DrawingContext)
      2. MyBase.OnRender(drawingContext)
      3. drawingContext.DrawEllipse(New SolidColorBrush(Colors.Black), New Pen(New SolidColorBrush(Colors.Yellow), 2), New Point(Diameter / 2, Diameter / 2), Diameter / 2, Diameter / 2)
      4. Dim formatted = GetFormattedText()
      5. drawingContext.DrawText(formatted, New Point((Diameter - formatted.Width) / 2, (Diameter - formatted.Height) / 2))
      6. End Sub
      7. Public Function GetFormattedText() As FormattedText
      8. Return New FormattedText(Text, CultureInfo.InvariantCulture, FlowDirection.LeftToRight, New Typeface("Arial"), 20, New SolidColorBrush(Colors.Green), 1.25)
      9. End Function


      Der Code inkl. der Hilfsmethode GetFormattedText ist im Moment nicht wichtig, wichtig ist das ihr wisst das man OnRender überschreiben muss um selbst ein FrameworkElement Rendern zu können.

      In unserem Projekt binden wir nun unsere TextEllipse in unserem MainWindow.xaml.
      Haben wir das Projekt nun einmahl erfolgreich kompiliert können wir unsere TextEllipse ganz normal im XAML Editor verwenden und haben sogar Intellisense für unsere selbst erstellen DependecyProperties.

      XML-Quellcode

      1. <local:TextElipse x:Name="MyElipse" Grid.Column="0" Text="Hier steht Text" Diameter="150"/>


      Starten wir das Projekt nun sehen wir genau das was wir erwarten würden. Aber auch bereits zur Designzeit haben wir das Ergebnis gesehen.


      Das Ergebnis sieht genauso aus wie erwartet. Fügen wir nun noch einen Slider und eine TextBox hinzu um die Größe und den Text der Ellipse ändern zu können.

      XML-Quellcode

      1. <Grid>
      2. <Grid.ColumnDefinitions>
      3. <ColumnDefinition Width="*"/>
      4. <ColumnDefinition Width="*"/>
      5. </Grid.ColumnDefinitions>
      6. <local:TextElipse x:Name="MyElipse" Grid.Column="0" Text="Hier steht Text" Diameter="150"/>
      7. <StackPanel Grid.Column="1">
      8. <Slider Minimum="1" Maximum="500" Value="{Binding ElementName=MyElipse,Path=Diameter,Mode=TwoWay}"/>
      9. <TextBox Text="{Binding ElementName=MyElipse,Path=Text,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}/>
      10. </StackPanel>
      11. </Grid>


      Starten wir nun erneut das Projekt werden wir merken das wir in die TextBox schreiben können was wir wollen oder des Slider hin und her bewegen können – es wird sich nichts an unserer Ellipse ändern. Weder verändert sich die Größe noch der Text. Aber warum?
      Das liegt daran das wir unser „Control“ selbst rendern. Wir müssen der WPF nun auch noch bekanntgeben dass sie doch bitte neu Rendern soll wenn wir den Wert eines Property ändern.

      Hierfür übergeben wir statt PropertyMetadata diesmal FrameworkPropertyMetadata als letzten Parameter der RegisterMethode. Die Klasse FrameworkPropertyMetadata besitzt im Gegensatz zu PropertyMetadata die interne Eigenschaft Flags welche in insgesamt vier Überladungen mit übergeben werden kann.

      VB.NET-Quellcode

      1. Public Property Diameter As Double
      2. Get
      3. Return CDbl(GetValue(DiameterProperty))
      4. End Get
      5. Set(ByVal value As Double)
      6. SetValue(DiameterProperty, value)
      7. End Set
      8. End Property
      9. Public Shared ReadOnly DiameterProperty As DependencyProperty =
      10. DependencyProperty.Register("Diameter",
      11. GetType(Double), GetType(TextElipse),
      12. New FrameworkPropertyMetadata(Double.Parse("40"), FrameworkPropertyMetadataOptions.AffectsRender))
      13. Public Property Text As String
      14. Get
      15. Return CType(GetValue(TextProperty), String)
      16. End Get
      17. Set(ByVal value As String)
      18. SetValue(TextProperty, value)
      19. End Set
      20. End Property
      21. Public Shared ReadOnly TextProperty As DependencyProperty =
      22. DependencyProperty.Register("Text",
      23. GetType(String), GetType(TextElipse),
      24. New FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.AffectsRender, AddressOf OnTextChanged))


      Wir teilen somit der WPF mit das neu „gerendert“ werden muss wenn sich der Wert dieses Property`s ändert. Die WPF wird in diesem Fall bei jeder Wertänderung die OnRender Methode aufrufen.

      Als kleine „Fleisaufgabe“ werden wir noch sicherstellen das der eingegebene Text immer in die Ellipse passt indem wir die Textgröße berechnen und den Durchmesser der Ellipse in dem Fall das der Text breite sein sollte als der Durchmesser der Ellipse, diese vergrößern.

      VB.NET-Quellcode

      1. Private Shared Sub OnTextChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
      2. Dim te As TextElipse = DirectCast(d, TextElipse)
      3. If te.GetFormattedText().Width > Double.Parse(CType(te.GetValue(DiameterProperty), String)) Then
      4. te.SetValue(DiameterProperty, te.GetFormattedText().Width + 5)
      5. End If
      6. End Sub




      Wir haben also nun gelernt das man Dependency Properties immer mit GetValue abfragen und mittels SetValue neu setzten soll. Dependecy Properties können über Ihre Metadaten einiges an Logik mitbekommen und können direkt gebunden werden. Im nächsten Kapitel werden wir ein Dependency Property in ein UserControl einbauen um hier von „außen“ das Verhalten dieses Control steuern zu können.

      Und auch heute gibt es wieder ein Video:


      Hier die Solution: 2_1_5_2.zip
      Und wie immer auch das PDF für euer E-Book: 2.1.5.2.pdf

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:

      Grüße
      Sascha
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Nofear23m“ ()

      2.1.5.3 Eigene DependencyProperties in einem UserControl implementieren

      2.1.5.3

      Eigene DependencyProperties in einem UserControl implementieren




      In diesem Kapitel kommen wir dazu dass wir ein Dependency Property in ein UserControl implementieren. Dies ist ein Scenario welches in der Praxis schon um einiges öfter vorkommt.

      In diesem Beispiel gehen wir davon aus das wir ein UserControl haben welches uns Personendaten darstellen soll. Das Control soll aber so flexibel wie möglich sein um es möglichst wiederverwenden zu können.
      Weiters sollen wir die Möglichkeit erhalten das wir die TextBoxen in diesem UserControl auf ReadOnly setzen. Und zwar von außen und via Binding. Klingt jetzt etwas kompliziert aber ihr werdet sehen dass es das gar nicht ist.
      Wir erstellen ein neuen UserControl und nennen dieses ShowOrEditPersonControl und gestalten es mit zwei Labels und zwei TextBoxen.



      Sieht unspektakulär aus, ist es auch. :P

      Zusätzlich, damit es ein wenig klarer wird ob die TextBox nun ReadOnly ist oder nicht habe ich einen Style hinzugefügt welcher bewirkt das der Background einer TextBox Grau wird wenn diese ReadOnly ist.

      XML-Quellcode

      1. <UserControl x:Class="ShowOrEditPersonControl"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      5. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      6. xmlns:local="clr-namespace:DependencyProperties_Demo"
      7. mc:Ignorable="d"
      8. d:DesignHeight="72.541" d:DesignWidth="460.656">
      9. <UserControl.Resources>
      10. <Style TargetType="{x:Type TextBox}">
      11. <Setter Property="Background" Value="White"/>
      12. <Setter Property="Margin" Value="3"/>
      13. <Style.Triggers>
      14. <Trigger Property="IsReadOnly" Value="True">
      15. <Setter Property="Background" Value="LightGray"/>
      16. </Trigger>
      17. </Style.Triggers>
      18. </Style>
      19. </UserControl.Resources>
      20. <Grid>
      21. <Grid.RowDefinitions>
      22. <RowDefinition Height="Auto"/>
      23. <RowDefinition Height="Auto"/>
      24. </Grid.RowDefinitions>
      25. <Grid.ColumnDefinitions>
      26. <ColumnDefinition Width="Auto"/>
      27. <ColumnDefinition Width="*"/>
      28. </Grid.ColumnDefinitions>
      29. <Label Content="Vorname"/>
      30. <Label Content="Nachname" Grid.Row="1"/>
      31. <TextBox Text="Sascha" Grid.Column="1"/>
      32. <TextBox Text="Patschka" Grid.Column="1" Grid.Row="1"/>
      33. </Grid>
      34. </UserControl>


      Im Moment haben wir den Vornamen sowie den Nachnamen noch fix gesetzt. Wie möchten aber dass dieser gebunden wird. Außen z.b. im MainWindow können wir dieses UserControl nun problemlos wie jedes andere UserControl im XAML verwenden.

      <local:ShowOrEditPersonControl/>

      Um nun direkt vom MainWindow aus den Text für den Vornamen und den Nachnamen zu setzen, benötigen wie zwei DependencyProperties in der CodeBehind des UserControls. Eines für den Vornamen und eines für den Nachnamen.

      Unsere ShowOrEditPersonControl.vb sieht nun wie folgt aus:

      VB.NET-Quellcode

      1. Public Class ShowOrEditPersonControl
      2. Public Property FirstName As String
      3. Get
      4. Return CType(GetValue(FirstNameProperty), String)
      5. End Get
      6. Set(ByVal value As String)
      7. SetValue(FirstNameProperty, value)
      8. End Set
      9. End Property
      10. Public Shared ReadOnly FirstNameProperty As DependencyProperty =
      11. DependencyProperty.Register("FirstName",
      12. GetType(String), GetType(ShowOrEditPersonControl),
      13. New PropertyMetadata(Nothing))
      14. Public Property LastName As String
      15. Get
      16. Return CType(GetValue(LastNameProperty), String)
      17. End Get
      18. Set(ByVal value As String)
      19. SetValue(LastNameProperty, value)
      20. End Set
      21. End Property
      22. Public Shared ReadOnly LastNameProperty As DependencyProperty =
      23. DependencyProperty.Register("LastName",
      24. GetType(String), GetType(ShowOrEditPersonControl),
      25. New PropertyMetadata(Nothing))
      26. End Class


      Zwei völlig normale aus dem „Default-Gerüst“ bestehende Dependency Property`s.
      Mehr benötigt es auch schon nicht mehr.

      Wir kompilieren das Projekt und sehen uns unser UserControl mal vom MainWindow aus an.
      Wir merken dass wir nun bereits die Properties zur Verfügung haben:



      Die Abbildung zeigt dass wir nun bereits Intellisenseunterstützung haben. Wir können die Werte bereits angeben und könnten nun auch darauf Binden. Setzen wir mal den Vornamen auf „Max“ und den Nachnamen auf „Mustermann“.

      <local:ShowOrEditPersonControl FirstName="Max" LastName="Mustermann"/>

      Aber wie kommen die Daten welche ich hier angebe oder Binde, in die TextBox?
      Berechtigte Frage, denn wer versucht hier Werte anzugeben wird schnell merken das man dies zwar machen kann, es sich aber an der TextBox nichts ändert. Das liegt daran das wir die TextBoxen selbst auch noch Binden müssen.
      Hierfür gibt es mehrere Wege, ich gehe auf einen ein bei welchem ich der Meinung bin das es der „sicherste“ ist.

      Wir ändern also der XAML im ShowOrEditPersonControl wie folgt:

      XML-Quellcode

      1. <TextBox Text="{Binding FirstName, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ShowOrEditPersonControl}}}" Grid.Column="1"/>
      2. <TextBox Text="{Binding LastName, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ShowOrEditPersonControl}}}"
      3. Grid.Column="1" Grid.Row="1"/>


      Das Binding in diesem Codeblock sucht im ElementTree der WPF nach oben, das nächste Element vom Typ „ShowOrEditPersonControl“. Wird es fündig verwendet es in diesem das Property „LastName bzw. „FirstName“.
      Werfen wir nun (nach einem nochmaligem Buildvorgang) nochmals einen Blick in das MainWindow sehen wir bereits ein Bild welches wir schon eher erwarten würden.



      Toll, das funktioniert und wir können von außen nun Binden und steuern was in den TextBoxen für Inhalt stehen soll.
      Da war ja noch was? Genau, wir wollten das wir steuern können ob sich das UserControl im Editiermodus befindet oder nicht. Fügen wir uns erstmal im MainWindow eine Checkbox ein welche der Editiermodus ein bzw. ausschalten soll.

      XML-Quellcode

      1. <StackPanel>
      2. <CheckBox x:Name="chkEditMode" Content="Im Editiermodus?" Margin="10"/>
      3. <local:ShowOrEditPersonControl FirstName="Max" LastName="Mustermann"/>
      4. </StackPanel>


      Nun benötigen wir ein weiteres Dependency Property innerhalb unseres UserControls:


      VB.NET-Quellcode

      1. Public Property IsInEditMode As Boolean
      2. Get
      3. Return CBool(GetValue(IsInEditModeProperty))
      4. End Get
      5. Set(ByVal value As Boolean)
      6. SetValue(IsInEditModeProperty, value)
      7. End Set
      8. End Property
      9. Public Shared ReadOnly IsInEditModeProperty As DependencyProperty =
      10. DependencyProperty.Register("IsInEditMode",
      11. GetType(Boolean), GetType(ShowOrEditPersonControl),
      12. New PropertyMetadata(False))


      Diesem Property übergeben wir über die Metadaten False als Default-Wert.
      Die Bindung auf die TextBoxen kann nun genauso wie beim Text-Property erfolgen oder – da wir einen Style definiert haben gleich in diesem damit wir uns nicht wiederholen müssen.

      XML-Quellcode

      1. <Style TargetType="{x:Type TextBox}">
      2. <Setter Property="Background" Value="White"/>
      3. <Setter Property="Margin" Value="3"/>
      4. <Setter Property="IsReadOnly" Value="{Binding IsInEditMode, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ShowOrEditPersonControl}}}"/>
      5. <Style.Triggers>
      6. <Trigger Property="IsReadOnly" Value="True">
      7. <Setter Property="Background" Value="LightGray"/>
      8. </Trigger>
      9. </Style.Triggers>
      10. </Style>


      Wenn wir dies nun ausprobieren indem wir im MainWindow wieder das Property setzen werden wir sehen dass sich unser UserControl nicht ganz so verhält wie wir das wollen. Nämlich genau anders rum.



      Wir haben im Kapitel 2.1.1.2 einiges über Trigger gelöst und könnten hiermit das verhalten umkehren. Die schönere Lösung wäre allerdings ein Converter wie in Kapitel 2.1.4.4 gelernt.
      Schon aus dem Grund da dieser wiederverwendbar ist, wobei ich Trigger immer wieder neu runter tippen muss.

      Wir legen uns also einen Converter an und nennen die Klasse BooleanReverseConverter:

      VB.NET-Quellcode

      1. Public Class BooleanReverseConverter
      2. Implements IValueConverter
      3. Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
      4. Return Not Boolean.Parse(value.ToString())
      5. End Function
      6. Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
      7. Return Not Boolean.Parse(value.ToString())
      8. End Function
      9. End Class


      Nun müssen wir diesen Converter im unseren XAML einbinden. Am einfachsten geht dies über das Eigenschaftenfenster, aber auch direkt im Code indem wir den Converter als Resource hinzufügen und einen Key vergeben. Diesen Key verwenden wir anschließend im Binding:

      XML-Quellcode

      1. <UserControl.Resources>
      2. <local:BooleanReverseConverter x:Key="BooleanReverseConverter"/>
      3. <Style TargetType="{x:Type TextBox}">
      4. <Setter Property="Background" Value="White"/>
      5. <Setter Property="Margin" Value="3"/>
      6. <Setter Property="IsReadOnly" Value="{Binding IsInEditMode,
      7. Converter={StaticResource BooleanReverseConverter},
      8. RelativeSource={RelativeSource FindAncestor,
      9. AncestorType={x:Type local:ShowOrEditPersonControl}}}"/>
      10. <Style.Triggers>
      11. <Trigger Property="IsReadOnly" Value="True">
      12. <Setter Property="Background" Value="LightGray"/>
      13. </Trigger>
      14. </Style.Triggers>
      15. </Style>
      16. </UserControl.Resources>


      Nach einem Buildvorgang zurück im MainWindow sehen wir das nun alles so funktioniert wie erwünscht. Nun können wir noch die Checkbox auf unser DependencyProperty binden:

      XML-Quellcode

      1. <StackPanel>
      2. <CheckBox x:Name="chkEditMode" Content="Im Editiermodus?" Margin="10"/>
      3. <local:ShowOrEditPersonControl IsInEditMode="{Binding ElementName=chkEditMode,Path=IsChecked}" FirstName="Max" LastName="Mustermann"/>
      4. </StackPanel>


      Hier nun zwei Abbildungen die zeigen wie unser Binding zur Laufzeit funktioniert:


      Fazit: Dependency Properties sind eine feine Sache wenn man diese Versteht und weis was im Hintergrund abläuft.
      Auf Dependency Properties kann gebunden werden, können einen Standardwert haben und eine gewisse Logik insich implementiert haben.



      Und auch heute gibt es wieder ein Video:


      Hier die Solution: 2.1.5.3 - DependencyProperties-Demo.zip
      Und wie immer auch das PDF für euer E-Book: 2.1.5.3.pdf

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:

      Grüße
      Sascha
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      2.1.6

      Markuperweiterungen (Markup Extensions)




      Markup Extensions nutzen wir jeden Tag in der WPF.
      Bekannte Markup Extensions sind Beispielsweise {StaticResource}, {TemplateBinding} oder {Binding}.
      Wir verwenden diese ständig und sind ein fester Bestandteil der WPF, aber wie sieht es aus mit eigenen Extensions? Können wir eigene Extensions schreiben und was für einen Mehrwert bringen uns diese Extensions?

      Dies will ich in diesem Kapitel erläutern.


      2.1.6.1

      Markuperweiterungen - Theorie




      Ich habe ja bereits einige Markup Extensions erwähnt. Aber es gibt noch einige weitere im XAML Namespace.

      x:Name, x:Static, x:Null sind einige welche wir immer wieder verwenden. Ich will auch nicht zu sehr ins Detail der Theorie gehen da dieses Thema nicht allzu oft Verwendung finden wird.
      Ich finde allerdings das die Markup Extension eine nette Möglichkeit ist wieder etwas Code, welcher immer und immer wieder benötigt wird auszulagern.

      Eigene Markup Extension können wir implementieren indem wir entweder von der Basisklasse MarkupExtension erben, oder das Interface IMarkupExtension in eine Klasse implementieren.
      Die Basisklasse MarkupExtension besitzt eine Methode ProvideValue(System.IServiceProvider) As Object) welche als MustOverride deklariert ist. Diese Methode gilt es zu überschreiben und muss ein Ergebnis zurückliefern.

      Im nächsten Kapitel werden wir Beispiele erstellen und ich werde ein wenig auf besonderheiten eingehen.

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:

      Grüße
      Sascha
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      2.1.6.2 Markuperweiterungen - Beispiel

      2.1.6.2

      Markuperweiterungen - Beispiel




      Als ersten möchten wir anhand eines einfachen Beispiels einfach nur ein „Hallo Welt“ zurückgeben, nur damit wir sehen wie die Implementierung einer Markup Extension von statten geht und was dabei evtl. zu beachten ist.
      Unsere Markup Extension soll den Namen HelloWorld bekommen. Die Namenskonvention erwartet von uns dass die Klasse somit den Namen „HelloWorldExtension“ bekommt. Diese Namenskonvention ist keine Pflicht, wird aber empfohlen. Wir legen also eine neue Klasse mit diesem Namen an und erben von MarkupExtension. Tun wir dies generiert uns Visual Studio automatisch die Methode ProvideValue da diese in der Basisklasse ja mit MustOverride deklariert ist. Außerdem empfehle ich immer solche Extensions genauso wie z.b. Konverter in einen eigenen Namespace zu packen:

      VB.NET-Quellcode

      1. Namespace MyWpfExtensions
      2. Public Class HelloWorldExtension
      3. Inherits Markup.MarkupExtension
      4. Public Overrides Function ProvideValue(serviceProvider As IServiceProvider) As Object
      5. Throw New NotImplementedException()
      6. End Function
      7. End Class
      8. End Namespace


      Dies ist also ein Grundkonstrukt einer Markup Extension.
      In unserem Beispiel möchten wir einfach das unsere Extension schlicht ein „Hallo Welt“ zurückgibt. Also machen wir dies:

      VB.NET-Quellcode

      1. Namespace MyWpfExtensions
      2. Public Class HelloWorldExtension
      3. Inherits Markup.MarkupExtension
      4. Public Overrides Function ProvideValue(serviceProvider As IServiceProvider) As Object
      5. Return "Hallo Welt"
      6. End Function
      7. End Class
      8. End Namespace


      Jetzt geht es darum diese Extension in unseren XAML einzubinden.
      Hierfür muss das Projekt erstmal kompiliert werden damit auch der Designer etwas von unserer neuen Klasse mitbekommt. Anschließend können wir erstmal den Namespace importieren:

      XML-Quellcode

      1. xmlns:ext="clr-namespace:_2_1_6_1_MarkupExtensions.MyWpfExtensions"


      Info: Mein Projekt hat den Namen und somit den Stammnamespace „2_1_6_1_MarkupExtensions“. Dem folgt einfach unser Namespace für die Extensions.

      Nun können wir unsere Extension auch schon in unserem XAML nutzen:



      OK, zugegeben. So etwas hat nun im Moment wenig nutzen.Wie sieht es mit Parametern aus? Ja, machen wir ein Beispiel welches einfach zwei Ziffern Multiplizieren soll.
      Wieder legen wir eine neue Klasse an welche von MarkupExtension erbt und nun aber zusätzlich zwei Eigenschaften besitzen soll. Hierfür implementieren wir neben dem paramterlosem Konstruktor (welcher Pflicht ist) einen weiteren welcher zwei Ziffern entgegennehmen kann:

      VB.NET-Quellcode

      1. Imports System.Windows.Markup
      2. Namespace MyWpfExtensions
      3. Public Class MultiplyExtension
      4. Inherits MarkupExtension
      5. Public Sub New()
      6. End Sub
      7. Public Sub New(value1 As String, value2 As String)
      8. Me.Value1 = value1 : Me.Value2 = value2
      9. End Sub
      10. <ConstructorArgument("value1")>
      11. Public Property Value1 As Object
      12. <ConstructorArgument("value2")>
      13. Public Property Value2 As Object
      14. Public Overrides Function ProvideValue(serviceProvider As IServiceProvider) As Object
      15. Throw New NotImplementedException()
      16. End Function
      17. End Class
      18. End Namespace


      Die Annotation der Eigenschaften ist übrigens nicht Pflicht wird aber empfohlen.
      Nun haben wir einen zweiten Konstruktor um müssen nun die Werte nur noch Multiplizieren:

      VB.NET-Quellcode

      1. Public Overrides Function ProvideValue(serviceProvider As IServiceProvider) As Object
      2. If Value1 Is Nothing Then Value1 = 0
      3. If Value2 Is Nothing Then Value2 = 0
      4. Return Double.Parse(Value1.ToString) * Double.Parse(Value2.ToString)
      5. End Function


      Wenn wir nun unsere Extension in unserer TextBox anwenden sehen wir auch dass diese nun korrekt zwei Ziffern addieren kann.



      Bis jetzt sind wir aber noch nicht Typsicher unterwegs, das soll sich nun aber ändern.
      Die zu überschreibende Methode ProvideValue gibt uns ein IServiceProvider mit auf dem Weg.
      Über diesen Serviceprovider kommen wir an die Information an welches Property das Ergebnis geht und somit auch welchen Datentyp dieses Property besitzt. Somit können wir auch versuchen eine Konvertierung durchzuführen.
      Erstellen wir uns eine Klasse mit dem Namen MultiplyTypesaveExtension.
      Unsere Konstruktoren und die Eigenschaften Value1 und Value2 ändern sich insofern das diese nun nur noch Double als Parameter erwarten. Die zu überschreibende Methode ProvideValue ändern wir wie folgt ab:

      VB.NET-Quellcode

      1. Public Overrides Function ProvideValue(serviceProvider As IServiceProvider) As Object
      2. If serviceProvider Is Nothing Then Return Nothing
      3. Dim valueTarget = DirectCast(serviceProvider.GetService(GetType(IProvideValueTarget)), IProvideValueTarget)
      4. Dim calcResult = Value1 * Value2
      5. If valueTarget.TargetProperty.GetType Is GetType(DependencyProperty) Then
      6. Dim dp = DirectCast(valueTarget.TargetProperty, DependencyProperty)
      7. Return Convert.ChangeType(calcResult, dp.PropertyType)
      8. Else
      9. Dim info = DirectCast(valueTarget.TargetProperty, PropertyInfo)
      10. Return Convert.ChangeType(calcResult, info.PropertyType)
      11. End If
      12. End Function


      Im ersten Schritt holen wir uns das IProvideValueTarget-Service. Dieses gibt uns Informationen über die Eigenschaft zurück welche wir benötigen. Nachdem wir das Ergebnis wie bereits gehabt errechnen müssen wir prüfen ob es sich bei der Eigenschaft um ein Dependency Property oder ein normales Property handelt. Den Unterschied haben wir ja bereits in Kapitel 2.1.5 gelernt.
      Bewaffnet mit unseren Informationen können wir nun in den richtigen Typ Konvertieren.
      Zu guter letzt bringen wir auf Höhe der Klasse selbst noch eine Annotation an. Diese in nicht Plicht aber ich empfehle diese, warum zeige ich gleich noch.

      VB.NET-Quellcode

      1. <MarkupExtensionReturnType(GetType(Object))>
      2. Public Class MultiplyTypesaveExtension
      3. ...
      4. ...


      Ist er nun komplett verrückt? Da arbeiten wir nun Typsicher und dann eine Annotation mit Object als Datentyp? OK, das muss ich erklären.
      Diese Annotation gibt dem Designer der WPF die Möglichkeit zu wissen für was für einen Datentyp diese Extension gültig sein soll.
      Machen wir ein Beispiel. Gebe ich in der Annotation als Datentyp Double an kann ich in einem TextBox.Text Property zwar die Extension nutzen, habe aber keine Intellisens.



      Dafür aber Beispielsweise bei der Eigenschaft Opacity der TextBox, da diese von Typ Double ist.



      Gebe ich in der Annotation aber Object an, habe ich die Intellisens überall zur Verfügung. Da wir die Konvertierung im Code der Extension korrekt durchführen ist dies auch kein Problem.

      Ich hoffe das Kapitel hat euch nun einmal mehr von der Flexibilität der WPF überzeugt, ich bin mir sicher das bei euch im Kopf gerade ein paar Ideen für den Einsatz von MarkupExtensions herumschwirren. ;)
      Ein Typisches Beispiel wäre die Lokalisierung (Übersetzung in die aktuelle Sprache des Benutzers).


      Und auch heute gibt es wieder ein Video:


      Hier die Solution: 2_1_6_1_MarkupExtensions.zip
      Und wie immer auch das PDF für euer E-Book: 2.1.6.pdf

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:

      Grüße
      Sascha
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      2.1.7 - Attached Properties

      2.1.7

      Attached Properties




      Jetzt wo wir über Dependency Properties Bescheid wissen kommen wir zu den Attached Properties. Diese sind eine andere Art von Dependency Properties. Wir benutzen Attached Properties im Grunde täglich während der Arbeit unter WPF. Viele Panels haben Attached Properties wie z.b. das DockPanel. Auch das Grid besitzt Attached Properties.
      Haben wir ein Grid in welchen sich Steuerelemente befinden können wir bestimmen wo diese Steuerelemente befinden sollen (Spalte oder Zeile) indem wir Attached Properties nutzen.

      XML-Quellcode

      1. <Grid>
      2. <Grid.RowDefinitions>
      3. <RowDefinition Height="1*"/>
      4. <RowDefinition Height="3*"/>
      5. </Grid.RowDefinitions>
      6. <Grid.ColumnDefinitions>
      7. <ColumnDefinition Width="*"/>
      8. <ColumnDefinition Width="*"/>
      9. </Grid.ColumnDefinitions>
      10. <Button Content="Test" Grid.Column="1" Grid.Row="0"/>
      11. </Grid>


      Grid.Column ist ein Attached Property welches für Dependency Object implementiert wurde. Das bedeutet das jedes Steuerelement der WPF (wir haben ja gelernt das in der WPF jedes Steuerelement indirekt von DependencyObject ableitet) dieses Attached Property nutzen kann.
      So können wir auf einfache Art und Weise bestimmen wo genau der Button reingepackt wird.

      Info: Der oben stehenden XAML würde wie folgt in Code aussehen.


      VB.NET-Quellcode

      1. Dim btn = TestButton
      2. btn.SetValue(Grid.ColumnProperty, 1)
      3. btn.SetValue(Grid.RowProperty, 0)
      4. 'oder
      5. Grid.SetColumn(btn, 1)
      6. Grid.SetRow(btn, 0)


      Als nächsten gehen wir daran eigene Attached Properties zu implementieren.

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:

      Grüße
      Sascha
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      2.1.7.2 - Eigene Attached Properties

      2.1.7.2

      Eigene Attached Properties




      Kommen wir zur Praxis
      So würde ein Attached Property aussehen:

      VB.NET-Quellcode

      1. Public Class MyFirstAttachedProperty
      2. Inherits DependencyObject
      3. Public Shared Function GetMyFirstProp(ByVal element As DependencyObject) As String
      4. If element Is Nothing Then
      5. Throw New ArgumentNullException("element")
      6. End If
      7. Return CType(element.GetValue(MyFirstPropProperty), String)
      8. End Function
      9. Public Shared Sub SetMyFirstProp(ByVal element As DependencyObject, ByVal value As String)
      10. If element Is Nothing Then
      11. Throw New ArgumentNullException("element")
      12. End If
      13. element.SetValue(MyFirstPropProperty, value)
      14. End Sub
      15. Public Shared ReadOnly MyFirstPropProperty As _
      16. DependencyProperty = DependencyProperty.RegisterAttached("MyFirstProp",
      17. GetType(String), GetType(MyFirstAttachedProperty),
      18. New PropertyMetadata(Nothing))
      19. End Class


      Über die Signatur der Methoden Get und Set kann bestimmt werden für welche Controls ein Attached Property gültig sein soll. In diesem Fall wird bei beiden Methoden ein Element vom Typ Dependency Object erwartet, was bedeutet das dieses Attached Property auf jedes Objekt welches vom Typ DependencyObject ableitet angewandt werden kann. Dieses Argument kann auch angepasst werden um dies Beispielsweise nur für einen Button zu erlauben.
      Erstellen wir mal ein einfaches Beispiel welches auf alle Controls angewandt werden kann:

      Wir wollen zum Testen mal ein Attached Property erstellen welches eine Drehung unterstützt.
      Dies ist in der WPF zwar mit RenderTransform über XAML für jedes ObjeKt verfügbar aber es benötigt einige Zeilen Code.
      Um mit Attached Properties warm zu werden möchten wir dies mal einfacher gestalten. Nachstehend der XAML mit welchem man ein Element normalerweise drehen würde:

      XML-Quellcode

      1. <Button Content="Test" RenderTransformOrigin="0.5,0.5">
      2. <Button.RenderTransform>
      3. <TransformGroup>
      4. <RotateTransform Angle="11"/>
      5. </TransformGroup>
      6. </Button.RenderTransform>
      7. </Button>


      Dieser XAML Rendert einen Button um 11 Grad verdreht wobei der Mittelpunkt der Drehung sowohl horizontal als auch vertikal in der Mitte des Control ist.
      Um dies in Zukunft einfacher zu gestalten werden wir ein Attached Property hierfür erstellen welches wir Angle nennen werden.
      Wir erstellen uns also mit Hilfe des CodeSnippets wpfdpa ein Attached Property welches den Namen Angle besitzt und den Wertetyp Double besitzt.
      Die Klasse habe ich in diesem Fall MyRenderTransformHelper benannt.

      VB.NET-Quellcode

      1. Public Class MyRenderTransformHelper
      2. Inherits DependencyObject
      3. Public Shared Function GetAngle(ByVal element As DependencyObject) As Double
      4. If element Is Nothing Then
      5. Throw New ArgumentNullException("element")
      6. End If
      7. Return CDbl(element.GetValue(AngleProperty))
      8. End Function
      9. Public Shared Sub SetAngle(ByVal element As DependencyObject, ByVal value As Double)
      10. If element Is Nothing Then
      11. Throw New ArgumentNullException("element")
      12. End If
      13. element.SetValue(AngleProperty, value)
      14. End Sub
      15. Public Shared ReadOnly AngleProperty As _
      16. DependencyProperty = DependencyProperty.RegisterAttached("Angle",
      17. GetType(Double), GetType(MyRenderTransformHelper),
      18. New PropertyMetadata(Double.Parse("0"))
      19. End Class


      Kompilieren wir nun unser Projekt werden wir sehen das unser neues Property bereits für jedes Control zur Verfügung steht.



      Aber noch haben wir keine Logik implementiert welche sicherstellen würde dass der Button auch wirklich gedreht wird. Bei den Dependency Properties haben wir bereits gelernt das wir auf Änderungen eines Property reagieren können indem wir einen Callback für das PropertyChangedCallback in den Metadaten des Properties setzen.

      VB.NET-Quellcode

      1. Public Shared ReadOnly AngleProperty As _
      2. DependencyProperty = DependencyProperty.RegisterAttached("Angle",
      3. GetType(Double), GetType(MyRenderTransformHelper),
      4. New PropertyMetadata(Double.Parse("0"), AddressOf AngleProperty_Changed))
      5. Private Shared Sub AngleProperty_Changed(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
      6. Dim elem As UIElement = TryCast(d, UIElement)
      7. If elem IsNot Nothing Then
      8. elem.RenderTransformOrigin = New Point(0.5, 0.5)
      9. elem.RenderTransform = New RotateTransform(CDbl(e.NewValue))
      10. End If
      11. End Sub


      Zurück im Designer – sobald wir kompiliert haben – sehen wir den nun gedrehten Button mit folgendem um einiges kürzeren Code:

      XML-Quellcode

      1. <Button VerticalAlignment="Center"
      2. HorizontalAlignment="Center"
      3. Content="TestButton"
      4. local:MyRenderTransformHelper.Angle="15">
      5. </Button>




      Im nächsten Beispiel möchte ich zum einen zeigen wie wir ein Attached Property nur auf einen gewissen Control-Typ zulassen und zum anderen wie wir von einem Control aus ein anderes Control steuern. Ähnlich der Funktionalität des Grids.

      Situation: Wir möchten von einem UserControl aus den Titel des Fensters in welchem sich dieses befindet setzen. Dies ist oft in MVVM Anwendungen von nöten.
      Ich werde die Klasse WindowHelper nennen und nur auf UserControls zulassen.

      VB.NET-Quellcode

      1. Public Class WindowHelper
      2. Inherits DependencyObject
      3. Public Shared Function GetWindowTitle(obj As UserControl) As String
      4. Return DirectCast(obj.GetValue(WindowTitleProperty), String)
      5. End Function
      6. Public Shared Sub SetWindowTitle(obj As UserControl, value As String)
      7. obj.SetValue(WindowTitleProperty, value)
      8. End Sub
      9. Public Shared ReadOnly WindowTitleProperty As DependencyProperty =
      10. DependencyProperty.RegisterAttached("WindowTitle",
      11. GetType(String),
      12. GetType(WindowHelper),
      13. New UIPropertyMetadata("", AddressOf WindowTitle_Changed))
      14. Private Shared Sub WindowTitle_Changed(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
      15. End Class


      Nun müssen wir nur noch die Methode WindowTitle_Changed mit Leben füllen.
      Im Argument „d“ bekommen wir das Objekt herein welches das Property verwendet. In unserem Fall kann/darf dies nur ein UserControl sein da wir nur dieses zulassen was wir schön an den Argumenten der Methoden SetWindowTitle und GetWindowTitle sehen können. Hier haben wir die Möglichkeit ausschließlich auf UserControls beschränkt.
      Das Argument „e“ stellt und unter anderem die Eigenschaft NewValue zur Verfügung, das kennen wir aber bereits aus dem Kapitel DependencyProperties.

      Nach ein wenig überlegen kommen wir drauf das wir nach laden des UserControl den Fenstertitel überschreiben müssen. Folgender Code steht anschließend in der Methode:

      VB.NET-Quellcode

      1. Private Shared Sub WindowTitle_Changed(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
      2. Dim ctl = TryCast(d, UserControl)
      3. AddHandler ctl.Loaded, Sub(sender, e2)
      4. If Window.GetWindow(ctl) IsNot Nothing Then
      5. DirectCast(Window.GetWindow(ctl), Window).Title = GetWindowTitle(ctl)
      6. End If
      7. End Sub
      8. End Sub


      Nun können wir ein UserControl erstellen und unser neues Attached Property probieren.
      Erstellen wir ein UserControl mit dem Namen TestUserControl.
      In dieses setzen wir zum Testen einfach nur ein Label mit ein wenig Text und setzen unser soeben erstelltes Attached Property.
      Im MainWindow unseres Programms werden wir dieses UserControl mal einbinden und das Projekt starten…



      Wir merken schon dass sich durch Attached Properties einige neue Möglichkeiten ergeben.
      Was mir persönlich gut an Attached Properties gefällt ist das man nicht unbedingt für jede kleine „Änderung“ an einem Control immer gleich ein neues Control erstellen muss sondern einfach ein wenig Logik „einpflanzt“. Ohne das Control selbst zu verändern. Der Vorteil dabei, ich kann die Klassen mit meinen Attached Properties in eine Klassenbibliothek legen und jederzeit in jedem Projekt einbinden und habe diese Funktionalitäten zur Verfügung.
      Ich hoffe das hat euch gefallen. Nachstehend wieder ein Video zu diesem Kapitel, hinterlasst mir doch ein Kommentar was euch gefallen hat oder vielleicht auch was euch nicht gefallen hat.



      Hier die Solution wieder als Download: 2.1.7.2_AttachedProperties.zip
      Hier das PDF für Leute mit EBook:
      2.1.7.2.pdf

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:

      Grüße
      Sascha
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Nofear23m“ ()

      2.1.8.1 Die Input API

      2.1.8.1

      Die Input API




      Die Primäre API für die Eingabe ist in den Basiselementen UIElement, ContentElement, FrameworkElement und FrameworkContentElement enthalten.
      Einige davon haben wir ja bereits ausführlicher kennengelernt.

      Alle diese Klassen bieten Funktionen für Eingabeereignisse die im Zusammenhang mit Tastatureingaben, Maustasten, dem Mausrad, sowie der Mausbewegung stehen.
      Dieses System unterscheidet sich von dem System wie es in WinForms implementiert ist und ermöglicht eine andere Eingabearchitektur so das ein eigenes EreignisRouting-Schema möglich wird, bei dem mehr als ein Element ein Ereignis behandeln kann. In der WPF ist sehr vielen Ereignissen ein Ereignispaar zugeordnet wie Beispielsweise das KeyDown-Ereignis und das PreviewKeyDown-Ereignis. Der Unterschied zwischen diesen Ereignissen liegt darin wie an das Zielelement weitergeleitet wird.

      Vorschauereignisse (mit dem Präfix Preview) „Tunneln“ die Elementstruktur vom Stammelement nach unten wobei „Bubbling“-Ereignisse (ohne dem Präfix Preview) vom Zielelement an das Stammelement übergeben. Der Unterschied zwischen Bubbling und Tunneling wird aber in Kapitel 2.1.8.3 noch ausführlicher erläutert. Es ist auch wichtig dieses System zu verstehen da es hier gerne mal zu Verwirrung kommen kann wenn einem der Unterschied nicht klar ist. Aber gehen wir vorher mal zu Tastatur und Mauseingaben…

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      2.1.8.2 - Tastatur und Mausklassen

      2.1.8.2

      Tastatur und Mausklassen




      Zusätzlich zur Input API in den Basiselement-Klassen stellen die Keyboard-Klasse und die Mouse-Klasse weitere APIs für die Arbeit mit der Tastatur und Maus zur Verfügung.

      Tastatur

      VB.NET-Quellcode

      1. If (Keyboard.GetKeyStates(Key.Return) And KeyStates.Down) > 0 Then
      2. Debug.WriteLine("Return is pressed!")
      3. End If


      Maus

      Dasselbe gilt für die Mouse-Klasse. Im folgenden Beispiel wird ermittelt ob die linke Maustaste gedrückt wurde.

      VB.NET-Quellcode

      1. If Mouse.LeftButton = MouseButtonState.Pressed Then
      2. Debug.WriteLine("Left Mousebutton pressed!")
      3. End If


      Stifteingabe

      Die WPF verfügt über integrierte Unterstützung für Stylus-Eingaben.

      Stylus sind Stifte zur Eingabe an einem Tablet, gerade in letzter Zeit und nicht zuletzt durch die Surface Produkte von Microsoft erfreut sich diese Art der Eingabe einer neuen Beliebtheit. WPF Anwendungen können den Stift mithilfe der Mause-API als Maus behandeln aber auch als Stift-Gesten. Hierfür verfügt die WPF über Stylus-Api. All Stiftbezogenen APIs enthalten „Stylus“ in ihrem Namen.

      Wie Beispielsweise folgende Events: PreviewStylusButtonDown, StylusButtonDown, PreviewStylusInAirMove, StylusInAirMove.
      Wie man gut erkennen kann gibt es auch hier die „Paare“ mit und ohne dem Präfix „Preview“.
      Da der Stift aber auch als Maus fungieren kann, können bestehende Anwendungen welche nur die Mauseingabe unterstützen trotzdem bis zu einem gewissen Teil bedient werden, bestimmte einem Stift vorbehaltene Features können in diesem Fall allerdings nicht genutzt werden.

      Hier das PDF für Leute mit EBook:
      2.1.8.2.pdf

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      2.1.8.3 - Eventrouting

      2.1.8.3

      Eventrouting




      Die ist meiner Meinung nach ein recht wichtiges Thema in der WPF um das Eventhandlich der WPF verstehen zu lernen.

      Ich versuche es möglichst nicht zu Theoretisch zu erklären.
      In der WPF gibt es einen ElementTree. In diesem ElementTree sind die Elemente (Control) enthalten. Für Ereignisse aller Control innerhalb eines Elementrees gibt es mehrere Routingmechanissmen welche in den Metadaten des Events hinterlegt sind.
      • Direct
      • Bubbling
      • Tunneling
      In der WPF kann ein Ereignis nicht nur von einem einzelnen Element „bearbeitet“ werden.
      Ich weis nicht ob „bearbeitet“ das richtige Wort ist, aber entscheide selbst.

      Das Direkte Routing-Ereignis (Direct) wird direkt auf diesem Element ausgelöst und auch nur auf diesem. Wie man dies aus WinForms kennt.Bubbling arbeitet sich die Elementstruktur nach oben. Es Blubbert also nach ob die Elemente durch, indem zuerst das Element benachrichtigt wird, von dem das Ereignis stammt, und dann das übergeordnete Element benachrichtigt, dann wieder eines höher usw.
      Tunneling beginnt am Stamm der Elementstruktur (z.b. UserControl) und Arbeitet sich Element für Element nach unten bis es bei dem Element angelangt ist welches das Ereignis ursprünglich ausgelöst hatte.

      Folgende Abbildung zeigt welche Ereignisse auf welchem Objekt ausgelöste werden wenn auf einen Button geklickt wird welches sich innerhalb eines Stackpanels befindet:



      Per Konvention sind Events welche mit dem Präfix „Preview“ versehen sind Events welche das Tunneling als Routingsstrategie verwenden während Events ohne diesem Präfix immer ein Bubbling als Strategie verwenden.
      Wenn also wie in der Abbildung zu sehen ist auf den Button geklickt wird zuerst am Button das Click-Event geworfen. Anschließend wird auf dem StackPanel das Click-Event geworfen und danach am Grid.
      Schreiben wir eine kleine Demoanwendung welche dies besser veranschaulicht. Ihr werdet überrascht sein.
      Wir erstellen eine neue WPF Anwendung und ersetzen im MainWindow den Code des per Default erstellten Grids gegen folgenden:

      XML-Quellcode

      1. <Window x:Class="MainWindow"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      5. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      6. xmlns:local="clr-namespace:BubblingTunnelingDemo"
      7. mc:Ignorable="d"
      8. Title="MainWindow" Height="260" Width="300">
      9. <Grid x:Name="MainGrid" Margin="10" Background="LightYellow"
      10. PreviewMouseDown="PreviewMouseDown"
      11. PreviewMouseUp="PreviewMouseUp"
      12. MouseDown="MouseDown"
      13. MouseUp="MouseUp">
      14. <StackPanel x:Name="MainStackPanel" Margin="10" Background="LightBlue"
      15. PreviewMouseDown="PreviewMouseDown"
      16. PreviewMouseUp="PreviewMouseUp"
      17. MouseDown="MouseDown"
      18. MouseUp="MouseUp">
      19. <Button x:Name="Button1" Content="Button 1" Margin="10"
      20. Click="ButtonClick"
      21. PreviewMouseDown="PreviewMouseDown"
      22. PreviewMouseUp="PreviewMouseUp"/>
      23. <Button x:Name="Button2" Content="Button 2" Margin="10"
      24. Click="ButtonClick"
      25. PreviewMouseDown="PreviewMouseDown"
      26. PreviewMouseUp="PreviewMouseUp"/>
      27. <Button x:Name="Button3" Content="Button 3" Margin="10"
      28. Click="ButtonClick"
      29. PreviewMouseDown="PreviewMouseDown"
      30. PreviewMouseUp="PreviewMouseUp"/>
      31. </StackPanel>
      32. </Grid>
      33. </Window>


      Vielleicht legen wir noch die Größe des Fensters auf: Height="260" Width="300"



      und fügen folgenden Code in die MainWindow.vb – CodeBehind Datei ein:

      VB.NET-Quellcode

      1. Private Sub ButtonClick(sender As Object, e As RoutedEventArgs)
      2. Debug.WriteLine($"{GetMethodName()} from Element with Name: '{DirectCast(sender, FrameworkElement).Name}'")
      3. End Sub
      4. Private Shadows Sub PreviewMouseDown(sender As Object, e As MouseButtonEventArgs)
      5. Debug.WriteLine($"{GetMethodName()} from Element with Name: '{DirectCast(sender, FrameworkElement).Name}'")
      6. End Sub
      7. Private Shadows Sub PreviewMouseUp(sender As Object, e As MouseButtonEventArgs)
      8. Debug.WriteLine($"{GetMethodName()} from Element with Name: '{DirectCast(sender, FrameworkElement).Name}'")
      9. End Sub
      10. Private Shadows Sub MouseDown(sender As Object, e As MouseButtonEventArgs)
      11. Debug.WriteLine($"{GetMethodName()} from Element with Name: '{DirectCast(sender, FrameworkElement).Name}'")
      12. End Sub
      13. Private Shadows Sub MouseUp(sender As Object, e As MouseButtonEventArgs)
      14. Debug.WriteLine($"{GetMethodName()} from Element with Name: '{DirectCast(sender, FrameworkElement).Name}'")
      15. End Sub
      16. Private Function GetMethodName(<Runtime.CompilerServices.CallerMemberName>
      17. Optional memberName As String = Nothing) As String
      18. Return memberName
      19. End Function


      Starten wir die Applikation und sehen wir uns das ganze mal genauer an und klicken auf den Button 1.
      Hierdurch wird folgender Text in der Ausgabe generiert:

      Quellcode

      1. PreviewMouseDown from Element with Name: 'MainGrid'
      2. PreviewMouseDown from Element with Name: 'MainStackPanel'
      3. PreviewMouseDown from Element with Name: 'Button1'
      4. PreviewMouseUp from Element with Name: 'MainGrid'
      5. PreviewMouseUp from Element with Name: 'MainStackPanel'
      6. PreviewMouseUp from Element with Name: 'Button1'
      7. ButtonClick from Element with Name: 'Button1'


      Wir sehen schön, dass hier auf jedem Element an welchem wir das Event mit dem Preview-Präfix abonniert haben das Tunneling greift und die Events von oben nach unten geworfen werden.
      Erst nachdem auch für den Button das PreviewMouseUp geworfen wurde wird das Click-Event für den Button geworfen. PreviewMouseDown und PreviewMouseUp Tunneln von Oben nach unten zum Zielelement.


      Sehen wir uns nun an was passiert wenn wir einen Klick auf das StackPanel machen:

      Quellcode

      1. PreviewMouseDown from Element with Name: 'MainGrid'
      2. PreviewMouseDown from Element with Name: 'MainStackPanel'
      3. MouseDown from Element with Name: 'MainStackPanel'
      4. MouseDown from Element with Name: 'MainGrid'
      5. PreviewMouseUp from Element with Name: 'MainGrid'
      6. PreviewMouseUp from Element with Name: 'MainStackPanel'
      7. MouseUp from Element with Name: 'MainStackPanel'
      8. MouseUp from Element with Name: 'MainGrid'


      Nun sehen wir schön das die Bubbling-Events MouseUp und MouseDown in die Gegenrichtung ausgelöst werden. Nämlich zuerst am StackPanel und erst dann am Grid.
      Um nun zu verhindern das Events weiter nach oben Bubbeln oder nach unten Tunneln kann das Handled Property des EventArgs auf True gesetzt werden. In diesem Fall gilt das Ereignis als behandelt und geht nicht mehr zum nächsten Element weiter.


      Hier das PDF für Leute mit EBook-Reader: 2.1.8.3.pdf

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      2.1.8.4 - Touch und Multitouch

      2.1.8.4

      Touch und Multitouch




      Auch ein immer wichtiger werdendes Thema heute. Touch und Multitouch-Bildschirme werden immer günstiger und in vielen Geräten befinden sich bereits solche Bildschirme. Auch in der Industrie finden die Bildschirme immer mehr Beliebtheit da auch diese immer Robuster werden und die Bedienung schlichtweg intuitiver ist als mit einer Tastatur.

      Seit .Net 4.0 und Windows 7 bieten die Klassen UIElement, UIElement3D und ContentElement Events an um die Funktionalität von Multitouch-Displays zu nutzen. Die Events werden geworfen wenn der Benutzer mit dem Finger ein WPF Element berührt, den Finger darin bewegt oder loslässt.
      • TouchDown
        Tritt beim berühren des Bildschirms innerhalb eines Elements auf
      • TouchUp
        Tritt auf wenn der Finger den Bildschirm wieder loslässt, wenn dieser vorher innerhalb eines Elements befand
      • TouchLeave
        Tritt beim verlassen des Elements auf, also wenn der Finger aus dem Element herausfährt
      • TouchMove
        Tritt auf wenn der Finger sich innerhalb des Elements bewegt
      Alle Touch-Events sind als Bubbling-Events implementiert, Blubbern also den ElementTree nach oben und verwenden die TouchEventArgs.
      Die Klasse TouchEventArgs besitzt unter anderem zwei interessante Eigenschaften bzw. Methoden. GetTouchPoint() gibt einen TouchPoint zurück welcher wiederum die Eigenschaft Position enthält und gibt die relative Position zum Berührungspunkt zurück.

      Die TouchDevice Eigenschaft vom Typ TouchDevice enthält eine ID vom Typ Integer und enthält einen eindeutigen Wert für den Berührungspunkt. Die ID ist dabei abhängig vom Treiber und dem Betriebssystem. Da es bei einem MultiTouch-Display ja mehrere Berührungspunkte geben kann (auch mehr oder weniger gleichzeitig) ist die Auswertung der ID wichtig um Rückschlüsse auf die „Geste“ zu bekommen, welche der User gerade macht.

      Aber neben den genannten „Low-Level-Touch“ Events bietet die Klasse UIElement auch die sehr interessanten Manipulation-Events.

      Die Manipulation-Events werden zum Skalieren, Rotieren und Verschieben eines Elements genutzt. Damit die Events überhaupt geworfen werden muss die Eigenschaft IsManipulationEnabled des Elements auf True gesetzt werden da diese per Default deaktiviert ist. Das geht sowohl über XAML als auch über die CodeBehind.
      Es gibt für die Manipulation einige Events: ManipulationStarting, ManimulationStarted, ManimulationDelta, ManipulationCompleted.
      Da die MultiTouch Funktionalität hier an dieser Stelle und auch in einem WebCast Video recht schwer zu „zeigen“ ist verzichte ich an dieser Stelle darauf. Für alle die ein MultiTouch-Display zu Verfügung haben können sich mit folgendem Beispiel aus der MSDN ein wenig spielen:

      docs.microsoft.com/en-us/dotne…r-first-touch-application


      Hier das PDF für Leute mit EBook: 2.1.8.4.pdf

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      2.1.8.5 - Focus

      2.1.8.5

      Focus




      Wie jetzt ein eigenes Kapitel über den Fokus? In der Tat. Fokus ist nicht gleich Fokus.
      Es gibt in der WPF zwei Hauptkonzepte. Den Tastaturfokus und den logischen Fokus.
      Es ist wichtig den Unterschied zwischen den beiden Konzepten zu kennen.

      Die beiden Keyplayer sind die Klassen Keyboard und FocusManager. Die Klasse Keyboard betrifft den Tastaturfokus während der FocusManager den logischen Fokus in der Hand hat.

      Tastaturfokus



      Der Tastaturfokus bezieht sich immer auf das Element welches gerade Tastatureingaben empfängt. Es kann am gesamten Desktop nur ein Element geben welches Tastatureingaben empfängt. Innerhalb eine WPF Anwendung ist auf diesem Element die Eigenschaft IsKeyboardFocused in diesem Fall auf True festgelegt. Die statische Eigenschaft FocusedElement der Klasse Keyboard gibt immer das Element zurück welches gerade den Tastaturfokus besitzt.
      Damit ein Element den Tastaturfokus erhalten kann muss die Eigenschaft Focusable auf True festgelegt sein. Dies ist bei Elementen wie der TextBox oder der CheckBox per Default auf True, bei Elementen wie Panels allerdings auf False, wodurch diese keinen Tastaturfokus erhalten können solange die Eigenschaft nicht auf True gesetzt wird.

      Der Benutzer kann den Tastaturfokus auf ein Steuerelement festlegen indem er in dieses Klickt oder die Tabulatortaste benutzt. Aber der Focus kann auch über Code gesteuert werden.

      Folgende Methode setzt nach dem laden des Fensters den KeyboardFocus auf den Button mit dem Namen firstButton.

      VB.NET-Quellcode

      1. Private Sub OnLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
      2. Keyboard.Focus(firstButton)
      3. End Sub


      Mit der Eigenschaft IsKeyboardFocused eines Elements kann abgerufen werden ob dieses gerade den Tastaturfokus besitzt. Interessant ist auch die Eigenschaft IsKeyboardFocusWithin.
      Diese gibt einen Wert zurück welcher aussagt ob ein diesem Element untergeordnetes Element gerade den Focus besitzt.

      Logischer Fokus

      Der Logische Focus wird von der statischen Klasse FocusManager jeweils für einen gewissen Bereich verwaltet. Diese Bereiche nennt man auch Focusbereich (Focus-Scope) und wird definiert indem man das AttachedProperty FocusManager.IsFocusScope eines Elements auf True setzt.

      Elemente wie Window, Menu, Toolbar oder ContextMenü haben diese Eigenschaft per Default auf True gesetzt und definieren damit ihren eigenen FocusScope, also ihren Fokus-Bereich.

      Angenommen es gibt ein Window mit einem Menu welches ein MenuItem enthält und zusätzlich einer TextBox innerhalb des Window. Wechselt nun der Tastaturfokus von der TextBox auf das MenuItem weil der User mit TAB navigiert, verliert die TextBox den Tastaturfokus, behält aber den logischen Fokus innerhalb des Fokusbereichs des Windows. Das MenuItem erhält also den Tastaturfokus UND den logischen Fokus innerhalb des Fokusbereichs des Menu.

      Geht nun der Tastaturfokus zurück zum Window, erhält das Element mit dem logischen Focus innerhalb dieses Fokusbereichs wieder den Tastaturfokus, was in diesem Fall die TextBox ist.
      Das MenuItem verliert also den Tastaturfokus aber behält aber innerhalb des Fokusbereichs des Menu den logischen Fokus.

      Es können also mehrere Elemente im logischen Fokusbereich liegen aber nur ein Element innerhalb des Fokusbereichs den logischen Fokus haben.
      Folgendes Beispiel ruft das fokussierte Element eines Fokusbereichs ab (hier im Fokusbereichs des Menu):

      FocusManager.GetFocusedElement(meineMenuInstanz)
      Falls kein Element in diesem Fokusbereich den logischen Fokus besitzt, gibt die Methode Nothing zurück.

      Fazit:
      Wichtig ist das wir den Unterschied zwischen dem logischen Fokus und dem Tastaturfokus kennen oder einfach wissen das es hier einen Unterschied gibt. Unser Hauptaugenmerk sollte immer auf dem Tastaturfokus liegen da uns der logische Fokus im Normalfall nicht so oft über den Weg läuft und wir uns nur bei komplexeren Szenarien beschäftigen müssen. Kommt es allerdings mal zu einem unerwünschten Verhalten was den Fokus betrifft wissen wir nun dass wir uns den Focus-Scope genauer ansehen müssen um der Sache auf den Grund zu gehen.



      Hier das PDF für Leute mit EBook:
      2.1.8.5.pdf

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      2.1.8.6 - Commands

      2.1.8.6

      Commands




      Commands sind ein sehr wichtiger Mechanismus in der WPF. Der Zeck von Command ist zum einen die Trennung zwischen dem Element welches den Command auslöst und der Logik welche hinter einem Command steht und zum anderen hat ein Command eine Eigenschaft welche bestimmt ob dieser ausgeführt werden kann. Dazu kommen wir aber noch.
      Wenn wir einen EventHandler für einen Button-Klick setzen können wir innerhalb dieses EventHandler Logik unterbringen. Wir können denselben EventHandler allerdings nicht verwenden wenn wir die Logik zu einem anderen Ereignis als einem Klick ausführen möchten.
      Weiters müssen wir uns selbst darum kümmern das ein Button „disabled“ wird wenn die Aktion im Moment nicht ausgeführt werden kann. Ein Beispiel hierfür wäre das Speichern eines Formulars wenn noch nicht alle Pflichtfelder ausgefüllt sind. In diesem Fall darf der User noch nicht auf Speichern klicken können. Wir müssen also immer prüfen ob alle Felder ausgefüllt sind und wenn dies der Fall ist den Button „enablen“. Dies kann uns alles ein Command abnehmen.
      Außerdem können Command sehr einfach eine Tastenkombination zugeordnet werden und so Beispielsweise F5 für das Speichern zu verwenden.

      Ein einfaches Beispiel

      Die einfachste Möglichkeit einen Command zu verwenden ist es einen vordefinierten RoutedCommand zu verwenden. Microsoft war so nett uns eine Reihe von Commands zu definieren welche wir überall Verwenden können.
      Der Command Paste ist einer dieser vordefinierten Commands und befindet sich in der statischen Klasse ApplicationCommands.

      Erstellen wir uns ein neues Projekt und ersetzen das Grid des MainWindow gegen folgenden Code:

      XML-Quellcode

      1. <StackPanel>
      2. <Menu>
      3. <MenuItem Command="ApplicationCommands.Paste" />
      4. </Menu>
      5. <TextBox />
      6. </StackPanel>


      Debuggen wir nun dieses Projekt sehen wir das das MenuItem „Einfügen“ benannt ist obwohl wir diesen Text nicht im XAML definiert haben. Weiters ist das MenuItem Enabled=False.
      Setzen wir nun den Keyboardfokus auf die TextBox wird sofort das MenuItem Enabled=True.

      Haben wir im Moment einen Text in der Zwischenablage können wir diesen Text nun durch einen Klick auf das MenuItem einfach in die Textbox einfügen. OK, das sieht jetzt alles ein wenig nach Magie und Hexenwerk aus, gebe ich zu.
      Das Modell der RoutedCommands kann in vier Hauptkonzepte aufgeteilt werden.
      • Der Befehl (Command) – ist die Aktion die ausgeführt werden soll
      • Die Befehlsquelle (Command-Source) – das Objekt welches den Befehl aufruft
      • Das Befehlsziel (Command-Target) – das Objekt auf das der Befehl ausgeführt wird
      • Die Befehlsbindung (Command-Binding) – das Objekt das den Befehl der Logik zuordnet

      Im vorherigen Beispiel ist Paste der Befehl, MenuItem ist die Befehlsquelle, die TextBox das Befehlsziel und die Befehlsbindung wird von der TextBox bereitgestellt. Ein Command-Objekt implementiert die Schnittstelle ICommand. Die ICommand Schnittstelle hat zwei Methoden. Die Execute und die CanExecute Methode sowie ein Event CanExecuteChanged.
      Wir können es uns bereits denken, Execute führt den Befehl aus und CanExecute bestimmt ob der Befehl ausgeführt werden kann.
      Das Ereignis CanExecutChanged wird ausgeführt wenn der CommandManager der WPF eine Änderung einer Befehls-Quelle erkennt und CanExecute kann somit aktualisiert werden.

      Nun ist es ja schon nicht mehr so viel Magie. Da einem RoutedCommand also die Quelle kennt (im obigem Beispiel das MenuItem) kann es dessen Header auch schreiben wodurch „Einfügen“ als Header eingesetzt wird. Allerdings nur wenn durch den Entwickler kein Header angegeben wurde. Dass das MenuItem automatisch Enabled oder Disabled wird, gibt Methode CanExecute zurück.


      Input-Gestures


      Imput-Gestures (Eingabegesten) können als Befehlsquelle verwendet werden. Es gibt z.b. KeyGesture und MouseGesture um zwei Eingabearten zu nennen.
      Beispielsweise ist dem Command Paste die KeyGesture STRG+V zugeordnet.
      Adaptieren wir das vorherige Beispiel um die Möglichkeit mit der Taste F3 den Einfügebefehl auszuführen.
      Wir ersetzen den Code des StackPanels und fügen stattdessen folgenden Code ein:

      XML-Quellcode

      1. <Window.InputBindings>
      2. <KeyBinding Key="F3"
      3. CommandTarget="{Binding ElementName=txtTest}"
      4. Command="ApplicationCommands.Paste" />
      5. </Window.InputBindings>
      6. <StackPanel>
      7. <Menu>
      8. <MenuItem Command="ApplicationCommands.Paste" />
      9. </Menu>
      10. <TextBox x:Name="txtTest" />
      11. </StackPanel>


      Da der Command nun nicht auf dem Ziel Definiert ist auf welchem es ausgeführt wird, müssen wir über die Eigenschaft CommandTarget das Ziel festlegen was die TextBox sein soll. Das ist auch der Grund warum die TextBox nun einen Namen bekommen hat.
      Starten wir nun das Programm können wir Text aus der Zwischenablage mit F3 einfügen, unabhängig davon ob die TextBox den Fokus hat oder hatte, da wir das Ziel explizit angegeben haben.
      Aber wir können einem vorhandenem Command auch eigene Logik implementieren. Hierfür können wir sogenannte CommandBindings im XAML definieren.

      XML-Quellcode

      1. <Window.CommandBindings>
      2. <CommandBinding Command="ApplicationCommands.Open"
      3. Executed="OpenExecuted"
      4. CanExecute="OpenCanExecute"/>
      5. </Window.CommandBindings>


      Sowohl bei „OpenExecute“ als auch bei „OpenCanExecute“ können wir mit einemRechtsklick -> Gehe zu Definition nun die EventHandler im CodeBehind erstellen lassen.

      Folgende CodeBehind sollten wir nun vor uns finden:

      VB.NET-Quellcode

      1. Class MainWindow
      2. Private Sub OpenExecuted(sender As Object, e As ExecutedRoutedEventArgs)
      3. End Sub
      4. Private Sub OpenCanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
      5. End Sub
      6. End Class


      In diese beiden Methoden können wir nun Logik platzieren welche beim ausführen dieses Command ausgeführt werden soll.
      In OpenExecute möchten wir in diesem Beispiel einfach nur eine MessageBox zeigen in welcher wir uns anzeigen lassen möchten welcher Command auf welchem Objekt ausgeführt wurde um zu sehen was passiert.

      VB.NET-Quellcode

      1. Private Sub OpenExecuted(sender As Object, e As ExecutedRoutedEventArgs)
      2. Dim command = CType(e.Command, RoutedCommand).Name
      3. Dim targetobj = CType(sender, FrameworkElement).Name
      4. MessageBox.Show($"Der Command '{command}' auf dem Objekt '{targetobj}' wurde ausgeführt.")
      5. End Sub


      Über die ExecutedRoutedEventArgs kommen wir unter anderem an den Command welcher ausgeführt wird und über den Parameter Sender können wir den Namen des Objekts abrufen.
      In der OpenCanExecute Methode setzen wir die CanExecute Eigenschaft des Parameters e vom Typ CanExecuteRoutedEventArg auf True um die Ausführung des Commands immer zu erlauben, da wir in diesem Fall keine Abhängigkeiten haben welche die Ausführung behindern nicht erlauben würde.

      VB.NET-Quellcode

      1. Private Sub OpenCanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
      2. e.CanExecute = True
      3. End Sub


      Starten wir das Programm wird nichts passieren, wir haben nämlich weder eine InputBinding definiert noch einem Button, MenuItem oder einem anderen Steuerelement diesen Command zugewiesen. Das werden wir nachholen.

      XML-Quellcode

      1. <Window.CommandBindings>
      2. <CommandBinding Command="ApplicationCommands.Open"
      3. Executed="OpenExecuted"
      4. CanExecute="OpenCanExecute"/>
      5. </Window.CommandBindings>
      6. <Window.InputBindings>
      7. <KeyBinding Key="F3"
      8. CommandTarget="{Binding ElementName=txtTest}"
      9. Command="ApplicationCommands.Paste" />
      10. <KeyBinding Modifiers="Ctrl"
      11. Key="O"
      12. Command="ApplicationCommands.Open"/>
      13. </Window.InputBindings>
      14. <StackPanel>
      15. <Menu>
      16. <MenuItem Command="ApplicationCommands.Paste" />
      17. <MenuItem Command="ApplicationCommands.Open" />
      18. </Menu>
      19. <TextBox x:Name="txtTest" />
      20. </StackPanel>


      Starten wir nun das Programm können wir sowohl über das MenuItem als auch über die Tastenkombination STRG+O das Command ausführen und erhalten die MessageBox:



      Wir haben also gelernt was es mit Commands, CommandBindings und InputBindings auf sich hat. Am besten ist es man spielt ein wenig damit rum, und versucht ein wenig Logik in vorhandene Command zu bekommen. Die vorhandenen ApplicationCommands eigenen sich hervorragend um einen kleinen TextEditor zu basteln.

      Eine Übersicht zu allen vordefinierten Command findet man hier: docs.microsoft.com/de-de/dotne…nds?view=netframework-4.8





      Eigene Commands erstellen

      Um eigene Command zu erstellen gibt es zwei Möglichkeiten. Zum einen kann man eine Klasse erstellen welche die Schnittstelle ICommand implementiert oder, die gängigere Methode, die Erstellung eines RoutedCommand oder eines RoutedUiCommands.

      Da wir die Schnittstelle im nächsten Kapitel „RelayCommands“ kennenlernen werden, gehe ich hier erstmal auf die letztere Möglichkeit ein.
      Wir erstellen in diesem Beispiel einen RoutedCommand welcher bei seiner Ausführung einfach eine MessageBox zeigen soll. Um einen RoutedCommand zu erstellen gehen wir in die CodeBehind des Window und erstellen eine Statische Variable vom Typ RoutedCommand.
      Ich erstelle hierfür ein neues Fenster mit dem Namen CustomRoutedCommand.

      VB.NET-Quellcode

      1. Public Shared ShowMessageCommand As RoutedCommand = New RoutedUICommand


      Im XAML definieren wir nun ein CommandBinding für diesen Command. Da die Variable als Shared gekennzeichnet ist können wir im XAML über x:Static darauf zugreifen.
      Wir kompilieren vorher kurz um im XAML Intellisense zu erhalten.

      XML-Quellcode

      1. <Window.CommandBindings>
      2. <CommandBinding Command="{x:Static local:CustomRoutedCommand.ShowMessageCommand}"
      3. Executed="ShowMessageExecuted"
      4. CanExecute="ShowMessageCanExecute"/>
      5. </Window.CommandBindings>


      Nachdem wir uns die Handler generieren haben lassen, sieht unsere CodeBehind folgendermaßen aus:

      VB.NET-Quellcode

      1. Public Class CustomRoutedCommand
      2. Public Shared ShowMessageCommand As RoutedCommand
      3. Private Sub ShowMessageExecuted(sender As Object, e As ExecutedRoutedEventArgs)
      4. End Sub
      5. Private Sub ShowMessageCanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
      6. End Sub
      7. End Class


      Gehen wir zurück zum XAML, dort möchten wir nun noch ein MenuItem erstellen welches diesen Command ausführen soll.

      XML-Quellcode

      1. <StackPanel>
      2. <Menu>
      3. <MenuItem Header="Zeige Messagebox" Command="{x:Static local:CustomRoutedCommand.ShowMessageCommand}"/>
      4. </Menu>
      5. <TextBox />
      6. </StackPanel>


      Zum Schluss noch den Code in die EventHandler eingetragen:

      VB.NET-Quellcode

      1. Public Class CustomRoutedCommand
      2. Public Shared ShowMessageCommand As RoutedCommand = New RoutedUICommand()
      3. Private Sub ShowMessageExecuted(sender As Object, e As ExecutedRoutedEventArgs)
      4. MessageBox.Show("Hallo aus einem eigenen Command")
      5. End Sub
      6. Private Sub ShowMessageCanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
      7. e.CanExecute = True
      8. End Sub
      9. End Class


      Starten wir nun das Programm bekommen wir die MessageBox nach einem Klick auf das MenuItem zu sehen.



      Wir sehen also dass es nicht schwer ist eigene Commands zu implementieren. Auch Tastenkombinationen und das automatische „Labeln“ des Objekts an welches der Command „gebunden“ ist, ist möglich und durch die CanExecute Methode haben wir sogar Beispielsweise einen Button automatisch Disabled wenn der Command nicht ausgeführt werden kann.





      Hier die Solution wieder als Download: 2.1.8.6_CommandsDemo.zip
      Hier das PDF für Leute mit EBook:
      2.1.8.6.pdf

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      2.1.8.7 - Die RelayCommand-Klasse

      2.1.8.7

      Die RelayCommand-Klasse



      Nun möchten wir uns das ganze etwas einfacher und mit voller Bindingunterstützung machen. Bisher mussten wir die Methoden der CodeBehind im XAML angeben wodurch die View nicht vom Code getrennt war. Aber genau das ist unter WPF ja immer unser Ziel. Wir müssen also eine Möglichkeit schaffen genau dies zu erreichen. Und da fällt einem nach kurzem Nachdenken ein das man doch am besten sowohl die Erstellung als auch das Verbinden von Command und den Methoden für CanExecute und Execute am besten im Code alleine machen sollte. So hat man die volle Kontrolle. Also probieren wir dies mal.Wir haben ja gelernt dass jede Command-Klasse ICommand implementiert. OK, erstellen wir uns eine Klasse und nennen diese RelayCommand. Ich packe solche Klassen gerne in einen eigenen Namespace um Ordnung zu haben. Und da ich meine Ordnerstruktur der Solution immer meinen Namespaces anpasse erstelle ich somit einen Ordner. Für mich hat sich dieses Vorgehen als sehr Praktisch erwiesen.



      Somit sieht die neue Klasse nun erstmal wie folgt bei mir aus:

      VB.NET-Quellcode

      1. Namespace ViewModel
      2. Public Class RelayCommand
      3. End Class
      4. End Namespace


      Nun müssen wie ICommand implementieren. Für die jenigen welche nicht wissen wie man eine Schnittstelle am einfachen implementiert hier ein paar Bilder. Wir schreiben also unter dem Klassennamen „Implements ICommand“ und Drücken ENTER.
      Daraufhin bekommen wir von VisualStudio alle Methoden, Events und Eigenschaften direkt in unsere Klasse implementiert.
      Nun sieht die Klasse wie folgt aus:

      VB.NET-Quellcode

      1. Namespace ViewModel
      2. Public Class RelayCommand
      3. Implements ICommand
      4. Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
      5. Public Sub Execute(parameter As Object) Implements ICommand.Execute
      6. Throw New NotImplementedException()
      7. End Sub
      8. Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
      9. Throw New NotImplementedException()
      10. End Function
      11. End Class
      12. End Namespace


      Nun liegt es an uns die Klasse mit Code zu befüllen. Dafür klären wir mal was wir überhaupt brauchen. Bis jetzt haben wir nur erfahren das wir eine eigene Command-Klasse bauen möchten. Wieso wissen wir, aber wie genau nicht.
      Der Nachteil an bestehenden Commandklassen wie der RoutedCommand-Klasse ist das man dieser keine Methode übergeben kann welche ausgeführt werden soll wenn der Command ausgeführt/angestoßen wird. Dies möchten wir in unserer Klasse besser machen. Mit Hilfe der Schlüsselworte Action oder Predicate sowie ein wenig Lambda (Hoch leben Lambda-Expressions) geht dies auch.

      Wir möchten also einen Konstruktor schaffen welchem wir ein Action(Of Object) übergeben können sowie ein Predicate(Of Object) für die CanExecute Methode. Hier wird ein Predicate benötigt weil die CanExecute Methode einen Rückgabewert (Boolean) besitzt welcher später zurückgibt ob der Command ausgeführt werden kann oder nicht.
      Diese Objekte müssen wir uns auch speichern also benötigen wir zwei Felder. Diese können ReadOnly sein.

      VB.NET-Quellcode

      1. ReadOnly _execute As Action(Of Object)
      2. ReadOnly _canExecute As Predicate(Of Object)


      Und den Konstruktor:

      VB.NET-Quellcode

      1. Public Sub New(execute As Action(Of Object), canExecute As Predicate(Of Object))
      2. If execute Is Nothing Then
      3. Throw New ArgumentNullException("execute")
      4. End If
      5. _execute = execute
      6. _canExecute = canExecute
      7. End Sub


      Da „execute“ auf jeden Fall benötigt wird sollten wir Abfragen ob hier auch ein Objekt hereingereicht wird. Für „canexeute“ benötigen wir es nicht unbedingt da wir später auch gerne darauf verzichten können. Im Falle dass dieses Feld Nothing ist können wir gerne True zurückgeben.
      Wir füllen erstmal unsere zwei Methoden aus welche beim Implementieren der ICommand-Schnittstelle generiert wurden.
      In Execute führen wir im Grunde nur die übergebene Methode (Action) aus und in CanExecute prüfen wir ob unser Feld _canExecute nicht Nothing ist. Ist dies der Fall geben wir die Rückgabe der Methode als Ergebnis zurück.

      So sieht unsere Klasse bis nun aus:

      VB.NET-Quellcode

      1. Namespace ViewModel
      2. Public Class RelayCommand
      3. Implements ICommand
      4. ReadOnly _execute As Action(Of Object)
      5. ReadOnly _canExecute As Predicate(Of Object)
      6. Public Sub New(execute As Action(Of Object), canExecute As Predicate(Of Object))
      7. If execute Is Nothing Then
      8. Throw New ArgumentNullException("execute")
      9. End If
      10. _execute = execute
      11. _canExecute = canExecute
      12. End Sub
      13. Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
      14. Public Sub Execute(parameter As Object) Implements ICommand.Execute
      15. _execute(parameter)
      16. End Sub
      17. Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
      18. Return _canExecute Is Nothing OrElse _canExecute(parameter)
      19. End Function
      20. End Class
      21. End Namespace


      Widmen wir uns nochmals dem CanExecute.Wir haben im Kapitel Commands gelernt das es den CommandManager gibt, welcher dafür zuständig ist das die Oberfläche (z.b. ein Button) weis wenn sich am Status eines Commands etwas ändert. Bei jeder Aktion eines Users soll ja überprüft werden ob der Command ausgeführt werden darf. Der CommandManager ruft also immer und immer wieder sein Event RequerySuggested auf. Dieses Event abonnieren Commands um anschließend intern die Methode CanExecute abermals aufzurufen.
      Dieses Event müssen auch wir in unserer Klasse abonnieren. Da uns die Schnittstelle ICommand auch dieses Event zur Verfügung gestellt hat müssen wir in dieses nun eingreifen. Dies geht unter VB.Net nur indem wir aus diesem Event ein CustomEvent machen.

      Sobald wir ein CustomEvent daraus machen müssen wir AddHandler und RemoveHandler in dieses Event implementieren und anschließend das Event werfen (RaiseEvent).

      VB.NET-Quellcode

      1. Public Custom Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
      2. AddHandler(value As EventHandler)
      3. If _canExecute IsNot Nothing Then
      4. AddHandler CommandManager.RequerySuggested, value
      5. End If
      6. End AddHandler
      7. RemoveHandler(value As EventHandler)
      8. If _canExecute IsNot Nothing Then
      9. RemoveHandler CommandManager.RequerySuggested, value
      10. End If
      11. End RemoveHandler
      12. RaiseEvent(sender As Object, e As EventArgs)
      13. End RaiseEvent
      14. End Event


      Damit ist unsere RelayCommand-Klasse im Grunde fertig und wir können versuchen diese anhand eines kleinen Beispiels auszuprobieren.
      Erstellen wir eine simple Klasse mit einer Eigenschaft „TestCommand“. Diese soll einfach nur eine Konsolenausgebe machen und soll immer ausführbar sein.

      VB.NET-Quellcode

      1. Public Class TestClass
      2. Public Sub New()
      3. End Sub
      4. Public Property TestCommand As ICommand
      5. End Class


      Vielleicht fragt Ihr euch warum ich als Typ für die Eigenschaft ICommand angebe und nicht RelayCommand. Könnte ich auch, richtig. Das es allerdings ja sein könnte das ich mal mehrere verschiedene Commandklassen habe weil ich in eine Klasse evtl. zusätzliche Features implementieren möchte gebe ich den Typ an welchen alle diese Klassen gemeinsam haben, und das ist die Schnittstelle ICommand.

      Im Konstruktor setze ich nun den Command auf eine neue Instanz unserer RelayCommand-Klasse und gebe über den ersten Konstruktor der Klasse die zwei Methodennamen an (welche noch nicht existieren):

      VB.NET-Quellcode

      1. Public Sub New()
      2. TestCommand = New ViewModel.RelayCommand(AddressOf Test_Execute, AddressOf Test_CanExecute)
      3. End Sub


      Da die beiden Methoden noch nicht existieren werden diese vom Compiler angemeckert. Siehe Screenshot:




      Wenn wir nun den Cursor in eine der beiden Methodennamen setzen und STRG + . drücken bekommen wir die richtigen Vorschläge und uns die Methoden von VisualStudio erzeugen zu lassen.
      Anschließend noch ein wenig einfachen Code in die Methoden und nun sieht unsere Klasse wir folgt aus:

      VB.NET-Quellcode

      1. Public Class TestClass
      2. Public Sub New()
      3. TestCommand = New ViewModel.RelayCommand(AddressOf Test_Execute, AddressOf Test_CanExecute)
      4. End Sub
      5. Public Property TestCommand As ICommand
      6. Private Function Test_CanExecute(ByVal obj As Object) As Boolean
      7. Return True
      8. End Function
      9. Private Sub Test_Execute(ByVal obj As Object)
      10. Debug.WriteLine("Der Command wurde ausgeführt!")
      11. End Sub
      12. End Class


      Erstellen wir uns schnell ein Window welches diese Klasse als DatenKontext erhält und drücken auf einen Button welcher auf diesen Command gebunden ist.

      XML-Quellcode

      1. <Window x:Class="MainWindow"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      5. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      6. xmlns:local="clr-namespace:_2._1._8._7_RelayCommand_Demo"
      7. mc:Ignorable="d"
      8. Title="MainWindow" Height="131.818" Width="201.515">
      9. <Window.DataContext>
      10. <local:TestClass/>
      11. </Window.DataContext>
      12. <StackPanel Margin="10">
      13. <Button Content="Klick mich"
      14. Command="{Binding TestCommand}"/>
      15. </StackPanel>
      16. </Window>


      Wenn wir dieses Projekt nun starten und auf den Button Klicken sehen wir folgendes in der Ausgabe von VisualStudio:

      Quellcode

      1. Der Command wurde ausgeführt!


      Ein paar weitere Verbesserungen der Klasse und Tipps findet Ihr in dem Video zu diesem Kapitel. Es lohnt sich.


      Zum schluss könnten wir noch XML Comments und der gleichen einfügen wenn wir das wollen. Hier eine fertige RelayCommand-Klasse welche ich meißt verwende:

      Spoiler anzeigen

      VB.NET-Quellcode

      1. ''' <summary>
      2. ''' Diese Klasse Implementiert das ICommand Interface, so muss man nicht in jeder Klasse eines ViewModel alles selbst implementieren.
      3. ''' <example>
      4. ''' <para>Implementierung könnte folgendermaßen aussehen:</para>
      5. ''' <code>
      6. ''' MyCommand = New RelayCommand(AddressOf MyCommand_Execute, AddressOf MyCommand_CanExecute)
      7. ''' </code>
      8. ''' </example>
      9. ''' </summary>
      10. Public Class RelayCommand : Implements ICommand
      11. #Region " Fields "
      12. Private ReadOnly _execute As Action(Of Object)
      13. Private ReadOnly _canExecute As Predicate(Of Object)
      14. #End Region
      15. #Region " Constructors"
      16. ''' <summary>
      17. ''' Erstellt einen neuen Command welcher CanExecute nicht setzt, somit ist dieser Command dann immer Executeable.
      18. ''' </summary>
      19. ''' <param name="execute">The execution logic.</param>
      20. Public Sub New(execute As Action(Of Object))
      21. Me.New(execute, Nothing)
      22. End Sub
      23. ''' <summary>
      24. ''' Erstellt einen neuen Command welcher sowohl die Execute als auch die CanExecute Logik beinhaltet.
      25. ''' </summary>
      26. ''' <param name="execute">Die Logik für Execute.</param>
      27. ''' <param name="canExecute">Die Logik für CanExecute.</param>
      28. Public Sub New(execute As Action(Of Object), canExecute As Predicate(Of Object))
      29. If execute Is Nothing Then
      30. Throw New ArgumentNullException("execute")
      31. End If
      32. _execute = execute
      33. _canExecute = canExecute
      34. End Sub
      35. #End Region
      36. #Region " ICommand Members "
      37. ''' <summary>
      38. ''' Setzt die CanExecute-Methode des ICommand-Interfaces auf True oder False
      39. ''' </summary>
      40. ''' <param name="parameter"></param>
      41. ''' <returns>Gibt zurück ob die Aktion ausgeführt werden kann oder nicht</returns>
      42. Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
      43. Return _canExecute Is Nothing OrElse _canExecute(parameter)
      44. End Function
      45. ''' <summary>
      46. ''' Event welches geworfen wird wenn die Property CanExecuteChanged sich ändert.
      47. ''' </summary>
      48. Public Custom Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
      49. AddHandler(value As EventHandler)
      50. If _canExecute IsNot Nothing Then
      51. AddHandler CommandManager.RequerySuggested, value
      52. End If
      53. End AddHandler
      54. RemoveHandler(value As EventHandler)
      55. If _canExecute IsNot Nothing Then
      56. RemoveHandler CommandManager.RequerySuggested, value
      57. End If
      58. End RemoveHandler
      59. RaiseEvent(sender As Object, e As EventArgs)
      60. End RaiseEvent
      61. End Event
      62. ''' <summary>
      63. ''' Führt die Prozedur Execute des ICommand.Execute aus
      64. ''' </summary>
      65. ''' <param name="parameter"></param>
      66. ''' <remarks></remarks>
      67. Public Sub Execute(parameter As Object) Implements ICommand.Execute
      68. _execute(parameter)
      69. End Sub
      70. #End Region
      71. End Class


      Und heute gibts wieder ein Video ;)



      Hier die Solution wieder als Download: 2.1.8.7_RelayCommand_Demo.zip
      Hier das PDF für Leute mit EBook:
      2.1.8.7.pdf


      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      2.1.8.8 - CommandBinding und CommandParameter

      2.1.8.8

      CommandBinding und CommandParameter




      Im letzten Unterkapitel verfeinern wir nun unsere Commandkenntnisse. Wie ich im letzten Kapitel bereits angesprochen hatte, gibt es bei Commands auch die Möglichkeit einen Parameter mitzugeben. Auch in unserer RelayCommand-Klasse geben wir diesen Parameter an die Methode weiter.
      Nun möchten wir uns mal anhand eines Praxisbeispiels ansehen wo diese Parameter denn gebrauch finden könnten und wie wir diesen Parameter setzen können um diesen in der Execute Methode auswerten zu können.

      Um dies zu demonstrieren habe ich mal ein MainWorkspace-ViewModel erstellt sowie eine Car Klasse welche Marke, Modell und eine Liste von Ausstattungen als Eigenschaften besitzt.
      Die MainWorkspaceViewModel Klasse besitzt neben der Cars Eigenschaft noch eine Eigenschaft SelectedCar und einen Command DeleteCarCommand.
      Der DeleteCarCommand soll ein Car-Objekt aus der Auflistung entfernen können.

      Hier die Klassen als Diagramm:



      Und der Code der Klassen:
      Spoiler anzeigen

      VB.NET-Quellcode

      1. Imports System.Collections.ObjectModel
      2. Namespace ViewModel
      3. Public Class MainWorkspaceViewModel
      4. Inherits ViewModelBase
      5. Public Sub New()
      6. Cars = New ObservableCollection(Of CarViewModel)
      7. Cars.Add(New CarViewModel("VW", "Golf GTI", 230, New List(Of EquipmentViewModel) From {
      8. New EquipmentViewModel("ACC", "Abstandstempomat", 899),
      9. New EquipmentViewModel("Navigationssystem", "Navigationssystem 'Discovery' mit Livetimeupdate", 1500),
      10. New EquipmentViewModel("LED Scheinwerfer", "Voll LED Scheinwerfer mit automatischer Niveauregulierung", 450)}))
      11. Cars.Add(New CarViewModel("Seat", "Ibiza FR", 110, New List(Of EquipmentViewModel) From {
      12. New EquipmentViewModel("Navigationssystem", "Navigationssystem 'Discovery' mit Livetimeupdate", 499),
      13. New EquipmentViewModel("Sitzheizung", "Sitzheitzung für Fahrer und Beifahrer", 300)}))
      14. End Sub
      15. Dim _cars As ObservableCollection(Of CarViewModel)
      16. Public Property Cars As ObservableCollection(Of CarViewModel)
      17. End Property
      18. Private _selectedCar As CarViewModel
      19. Public Property SelectedCar As CarViewModel
      20. End Property
      21. Private _deleteCarCommand As ICommand
      22. Public Property DeleteCarCommand As ICommand
      23. Get
      24. If _deleteCarCommand Is Nothing Then _deleteCarCommand = New RelayCommand(AddressOf Delete_Execute, AddressOf Delete_CanExecute)
      25. Return _deleteCarCommand
      26. End Get
      27. Set(ByVal value As ICommand)
      28. _deleteCarCommand = Value
      29. RaisePropertyChanged()
      30. End Set
      31. End Property
      32. Private Function Delete_CanExecute(ByVal obj As Object) As Boolean
      33. Return True
      34. End Function
      35. Private Sub Delete_Execute(ByVal obj As Object)
      36. End Sub
      37. End Class
      38. End Namespace


      VB.NET-Quellcode

      1. Imports System.Collections.ObjectModel
      2. Namespace ViewModel
      3. Public Class CarViewModel
      4. Inherits ViewModelBase
      5. Public Sub New()
      6. Me.New("Unknown", "Unknown", 0)
      7. End Sub
      8. Public Sub New(brand As String, modell As String, ps As Double)
      9. Me.New(brand, modell, ps, New List(Of EquipmentViewModel))
      10. End Sub
      11. Public Sub New(brand As String, modell As String, ps As Double, equipments As List(Of EquipmentViewModel))
      12. If equipments Is Nothing Then Throw New ArgumentNullException(NameOf(equipments))
      13. Me.Brand = brand : Me.Modell = modell : Me.PS = ps
      14. Me.Equipments = New ObservableCollection(Of EquipmentViewModel)(equipments)
      15. End Sub
      16. Dim _brand As String
      17. Public Property Brand As String
      18. End Property
      19. Dim _modell As String
      20. Public Property Modell As String
      21. End Property
      22. Dim _pS As Double
      23. Public Property PS As Double
      24. End Property
      25. Dim _equipments As ObservableCollection(Of EquipmentViewModel)
      26. Public Property Equipments As ObservableCollection(Of EquipmentViewModel)
      27. End Property
      28. End Class
      29. End Namespace


      VB.NET-Quellcode

      1. Namespace ViewModel
      2. Public Class EquipmentViewModel
      3. Inherits ViewModelBase
      4. Public Sub New()
      5. Me.New("Untitled", "", 0)
      6. End Sub
      7. Public Sub New(title As String, description As String, price As Decimal)
      8. Me.Title = title : Me.Description = description : Me.Price = price
      9. End Sub
      10. Dim _title As String
      11. Public Property Title As String
      12. End Property
      13. Dim _description As String
      14. Public Property Description As String
      15. End Property
      16. Dim _price As Decimal
      17. Public Property Price As Decimal
      18. End Property
      19. End Class
      20. End Namespace



      Das MainWindow binde ich an die MainWorkspaceViewModel Klasse.

      XML-Quellcode

      1. <Window x:Class="MainWindow"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      5. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      6. xmlns:viewmodel="clr-namespace:_2._1._8._8___CommandParameter.ViewModel"
      7. mc:Ignorable="d"
      8. Title="MainWindow" Height="450" Width="800">
      9. <Window.DataContext>
      10. <viewmodel:MainWorkspaceViewModel/>
      11. </Window.DataContext>
      12. <Grid>
      13. <Grid.ColumnDefinitions>
      14. <ColumnDefinition Width="2*"/>
      15. <ColumnDefinition Width="1*"/>
      16. </Grid.ColumnDefinitions>
      17. <ListBox x:Name="listBox" ItemsSource="{Binding Cars}" HorizontalContentAlignment="Stretch" SelectedItem="{Binding SelectedCar}">
      18. <ListBox.Resources>
      19. <DataTemplate DataType="{x:Type viewmodel:CarViewModel}">
      20. <Grid>
      21. <DockPanel>
      22. <WrapPanel Orientation="Horizontal" DockPanel.Dock="Top">
      23. <Label FontWeight="Bold" Content="{Binding Brand,Mode=OneTime}"/>
      24. <Label FontWeight="Bold" Content="{Binding Modell,Mode=OneTime}"/>
      25. </WrapPanel>
      26. <ItemsControl ItemsSource="{Binding Equipments}" DockPanel.Dock="Bottom">
      27. <ItemsControl.ItemsPanel>
      28. <ItemsPanelTemplate>
      29. <WrapPanel Orientation="Horizontal"/>
      30. </ItemsPanelTemplate>
      31. </ItemsControl.ItemsPanel>
      32. <ItemsControl.Resources>
      33. <DataTemplate DataType="{x:Type viewmodel:EquipmentViewModel}">
      34. <Label Content="{Binding Title,Mode=OneTime}" ToolTip="{Binding Description,Mode=OneTime}"/>
      35. </DataTemplate>
      36. </ItemsControl.Resources>
      37. </ItemsControl>
      38. </DockPanel>
      39. <Button Content="Delete" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="5"/>
      40. </Grid>
      41. </DataTemplate>
      42. </ListBox.Resources>
      43. </ListBox>
      44. </Grid>
      45. </Window>


      Der XAML sieht dann wie folgt im Designer aus:



      Wir sehen den „Delete“ Button innerhalb des Car Items. Wir möchten also das man direkt am jeweiligen Item ein Fahrzeug aus der Auflistung entfernen kann.
      Der ein oder andere denkt nun sicher „Ja, Binding auf den Command und dann das SelectedItem der Auflistung entfernen.“ Gut, probieren wir das mal.

      Wir setzen den Command und führen den Code aus:

      XML-Quellcode

      1. <Button Content="Delete" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="5" Command="{Binding DeleteCarCommand}"/>


      Im Execute des Command löschen wir nun das SelectedItem und lassen den Command immer nur zu wenn die Eigenschaft SelectedCar nicht Nothing ist:

      VB.NET-Quellcode

      1. Private Function Delete_CanExecute(ByVal obj As Object) As Boolean
      2. Return SelectedCar IsNot Nothing
      3. End Function
      4. Private Sub Delete_Execute(ByVal obj As Object)
      5. Cars.Remove(SelectedCar)
      6. End Sub


      Starten wir also nun das Projekt und klicken auf den Button „Delete“ des ersten Eintrags (Golf GTI). Es passiert aber nichts. Warum nicht? Ein Blick in die Ausgabe verrät uns warum.

      System.Windows.Data Error: 40 : BindingExpression path error: 'DeleteCarCommand' property not found on 'object' ''CarViewModel' (HashCode=18700393)'. BindingExpression:Path=DeleteCarCommand; DataItem='CarViewModel' (HashCode=18700393); target element is 'Button' (Name=''); target property is 'Command' (type 'ICommand')

      Da wir den Button innerhalb eines List-Items platziert haben, hat dieser ein Item (CarViewModel) als Datenkontext. Der Command befindet sich allerdings eine Ebene weiter oben im MainWorkspaceViewModel. Wir müssen also zusehen das wir erstmal den richtigen Datenkontext für diesen Command bekommen.
      Wir wissen das die ListBox selbst den richtigen Datenkontext hat. Die Listbox verfügt über einen Namen – wenn auch einen nicht sehr gut gewählten.
      Versuchen wir also mal über die ListBox den Datenkontext zu bekommen um den Button-Command korrekt Binden zu können:

      XML-Quellcode

      1. <Button Content="Delete" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="5" Command="{Binding DataContext.DeleteCarCommand, ElementName=listBox}"/>


      Wir holen uns also vom Element mit dem Namen „listbox“ den DataContext. Dieser ist wie wir wissen vom Typ MainWorkspaceViewModel und innerhalb diesem befindet sich der Command mit dem Namen DeleteCarCommand.

      Also nochmal starten und die Ausgabe beobachten…

      Super keine Fehler in der Ausgabe. Aber komisch ist nun das die Buttons ausgegraut sind. Klicken wir auf ein Element in der Auflistung wird der Button aktiv.
      Probieren wir mal etwas… Wir selektieren das zweite Element (Seat Ibiza) und klicken aber auf den „Delete-Button“ des ersten Elements (Golf GTI).

      Ups, obwohl wir den Delete-Button beim Golf geklickt haben wird der Ibiza gelöscht. Warum das?
      Der Grund hierfür ist das wir das selektierte Objekt löschen, klicken wir auf den Button wird aber kein Element selektiert. Das ist auch der Grund warum die Delete Buttons nach dem Start der Anwendung ausgegraut sind, weil kein Element selektiert wurde. Die Eigenschaft ist Nothing wodurch CanExecute ja False zurückgibt.
      Es muss also eine andere Möglichkeit her. Hier kommt der CommandParameter ins Spiel.
      Versuchen wir doch dem Command das Item in welchem der Button „sitzt“ mitzugeben.

      Hierfür ändern wir den XAML Code erstmal ab:

      XML-Quellcode

      1. <Button Content="Delete" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="5"Command="{Binding DataContext.DeleteCarCommand, ElementName=listBox}"
      2. CommandParameter="{Binding}"/>


      Da die Eigenschaft CommandParameter ein DependencyProperty ist können wir auch hier Binding verwenden. Wir möchten als Parameter das Car-Objekt mitgeben, innerhalb eines ListItems haben wir auch genau dieses Objekt als Datenkontext weshalb wir keine Eigenschaft zusätzlich als Binding mitgeben müssen. Einfach das Binding als solches zu setzen reicht aus.
      Im Code des Commands müssen wir nun diesen Parameter noch auswerten. Bisher hatten wir das selektierte Objekt gelöscht, das möchten wir nun nicht mehr, wir möchten den Parameter auswerten.
      Sowohl in CanExecute als auch in Execute bekommen wir den Parameter als Objekt. Wir müssen also den Parameter auch Casten.

      VB.NET-Quellcode

      1. Private Function Delete_CanExecute(ByVal obj As Object) As Boolean
      2. Return obj IsNot Nothing
      3. End Function
      4. Private Sub Delete_Execute(ByVal obj As Object)
      5. Cars.Remove(CType(obj, CarViewModel))
      6. End Sub


      Nun funktioniert unser Command wie gewollt und es wird genau das Objekt aus der Auflistung gelöscht welches auch wirklich gelöscht werden soll da das Objekt nun mittels Binding mitgegeben wird.
      Dies ist nur ein Einsatzgebiet für CommandParameter, es wird euch noch öfters passieren dass ihr mit Parametern arbeiten müsst. Allerdings bitte nicht verwechseln mit dem Parameter beim Binding selbst, denn dieser ist nicht als DependencyProperty implementiert und kann somit nicht gebunden werden.

      Auch zu diesem Kapitel gibt es wieder ein Video. Viel Spaß :thumbsup:




      Hier die Solution wieder als Download: 2.1.8.8 - CommandParameter.zip
      Hier das PDF für Leute mit EBook:
      2.1.8.8.pdf

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      3.1 - Eine Telefonbuch App und WPF (ohne Binding) - Hauptview erstellen

      3.1

      Eine Telefonbuch App und WPF (ohne Binding)




      Nun haben wir die wichtigsten Dinge kennengelernt um soweit mal die erste Applikation zu erstellen. Am liebsten wäre es und natürlich nun mit Binding eine schöne App zu gestallten. Da ich aber auch die Personen ansprechen möchte welche aus der WinForms Ecke kommen hatte ich mir überlegt das ich aufzeigen möchte das auch mit CodeBehind und dem direkten ansprechen von Controls so einiges möglich ist in der Welt der WPF.
      Dieses Beispiel möchte ich natürlich so klein und einfach wie möglich halten.

      Wenn wir uns das Inhaltsverzeichnis mal ansehen werden wir aber feststellen dass wir die Applikation mehrmals erstellen werden, mit verschiedenen Herangehensweisen.
      Um dieses Kapitel übersichtlich zu halten verwende ich hier nun keinerlei Styles und achte auch nicht auf die Optik, zum Schluss zeige ich allerdings schon wie das Beispiel optisch „aufbereitet“ werden kann wenn man das möchte. Natürlich stelle ich die Solution inkl. Den Styles und den Templates dann auch Online aber der Schwerpunkt soll hier auf der Art und Weise wie man ohne Binding zurechtkommt liegen.


      3.1 Hauptfenster erstellen und Funktionen festlegen

      Welche Funktionen soll die erste Version unseres Telefonbuchs bekommen?
      Der User soll Personen anlegen und deren Daten verwalten können. Dazu zählen Vorname, Nachname, ein Bild der Person usw.
      Zusätzlich soll man natürlich Telefonnummern und andere Kontaktarten festlegen können, dies werden wir mit einer Verknüpfung machen damit wir später flexibel sind.
      Zum Beispiel soll man x Telefonnummern hinzufügen können, für jede Telefonnummer soll man bestimmen können ob dies eine Private Handynummer ist oder vielleicht eine Faxnummer oder eine Mailadresse.
      Weiters soll es mehrere Gruppen geben können und eine Person soll zu den „Favoriten“ hinzugefügt werden können.
      Soweit die Funktionen, wir können uns also ungefähr vorstellen was wir so alles benötigen.
      Erstellen wir uns erstmal eine neue WPF Anwendung unter Visual Studio und öffnen wir das MainWindow. Ich habe mal ein einfaches Design erstellt:

      XML-Quellcode

      1. <Window x:Class="MainWindow"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      5. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      6. xmlns:local="clr-namespace:PhoneBook"
      7. mc:Ignorable="d"
      8. Title="MainWindow" Height="273" Width="462.667">
      9. <Window.Resources>
      10. <ResourceDictionary>
      11. <ResourceDictionary.MergedDictionaries>
      12. <ResourceDictionary Source="Resources\Icons.xaml"/>
      13. </ResourceDictionary.MergedDictionaries>
      14. </ResourceDictionary>
      15. </Window.Resources>
      16. <Grid>
      17. <Grid.RowDefinitions>
      18. <RowDefinition Height="Auto" MinHeight="30"/>
      19. <RowDefinition Height="*"/>
      20. </Grid.RowDefinitions>
      21. <DockPanel Grid.Row="0" LastChildFill="True" Height="35">
      22. <Button Content="{StaticResource IconDotsVertical}" DockPanel.Dock="Right"/>
      23. <Button Content="{StaticResource IconPlus}" DockPanel.Dock="Right"/>
      24. <TextBox x:Name="txtSearch" Text="" DockPanel.Dock="Right" Width="100"
      25. Opacity="1" Margin="5" FontWeight="Bold" FontSize="18"
      26. VerticalContentAlignment="Center" />
      27. <Label FontWeight="Bold" FontSize="18" Content="Titel vom Tab"/>
      28. </DockPanel>
      29. <TabControl Grid.Row="1">
      30. <TabItem Header="Kontakte"/>
      31. <TabItem Header="Gruppen"/>
      32. <TabItem Header="Favoriten"/>
      33. </TabControl>
      34. </Grid>
      35. </Window>


      Soweit ist dies ein recht einfaches View. Wir haben ein Grid mit zwei Zeilen.
      In der oberen Zeile befindet sich ein DockPanel in welchem ein Label und zwei Buttons sowie eine TextBox enthalten sind. Der „Plus“ Button soll später dazu dienen ein neue Person ins Telefonbuch einzutragen, die TextBox soll zum Suchen von Personen dienen und der Button mit den drei Punkten soll vielleicht ein ContextMenu öffnen in welchem man dann mehr Optionen zur Verfügung hat.
      In der zweiten Zeile des Grids haben wir ein TabControl mit drei TabItem`s. Diese TabItem`s haben bis jetzt noch keinen Inhalt.

      Die Icons der Buttons kommen auf einem Resourcenwörterbuch welches ich in den Window-Resources verlinkt habe um Zugriff auf diese zu haben.
      Das Resourcenfile stelle ich natürlich mit der Solution am Ende des Kapitels Online.

      So sieht die Hauptansicht erstmal aus:



      Recht unspektakulär aber um das geht es ja erstmal nicht.
      Machen wir uns erstmal Content in das erste TabItem. Wir möchten eine Liste von Personen anzeigen. Hierfür gibt es mehrere Controls zur Auswahl. Um nur zwei zu nennen wäre hierfür eine ListBox oder ein ItemsControl geeignet. Die beiden Controls unterscheiden sich aber in gewissen Punkten voneinander. Der signifikanteste Punkt ist wohl die eine ListBox die Eigenschaft SelectedItem hat.
      Ein ItemsControl hat diese Eigenschaft nicht. Da wir aber kein „DetailView“ haben können wir auf dieses Feature gerne verzichten, zwar müssen wir einen Klick auf eine Person später anders verarbeiten aber ich möchte gerne aufzeigen wie dies in einem ItemsControl geht da hier viele Probleme bekommen, also versuchen wir es mal mit dem ItemsControl.
      Wir erstellen also ein ItemsControl innerhalb des ersten TabItem`s mit dem Header „Kontakte“.

      XML-Quellcode

      1. <TabControl Grid.Row="1">
      2. <TabItem Header="Kontakte">
      3. <ItemsControl x:Name="ItemsControlContacts"/>
      4. </TabItem>
      5. <TabItem Header="Gruppen"/>
      6. <TabItem Header="Favoriten"/>
      7. </TabControl>


      Ich muss hier einen Namen vergeben um später auf das Control aus der CodeBehind Zugriff zu haben.
      Ich würde meinen das wichtigste um erstmal anzufangen haben wir geschafft.

      Datenmodell erstellen

      Wir benötigen Daten. In welcher Form auch immer. Es gibt zig Möglichkeiten Daten in eine Anwendung zu bekommen. In der Objektorientierten Programmierwelt haben wir Klassen, eine Klasseninstanz repräsentiert ein Objekt, in unserem Fall eine Person.
      Ich habe anfangs dieses Kapitels bereits umrissen welche Objekte wir haben werden, diese werden wir nun erstellen.
      Da eine Person viele „Kontakte“ zugeordnet bekommen können soll benötigen wir eine Klasse Contacts.
      Ein Person Objekt hat anschließend eine Eigenschaft mit einer List(Of Contact) um diese zu halten.

      Contact Klasse

      VB.NET-Quellcode

      1. Namespace Model
      2. Public Class Contact
      3. Inherits ModelBase
      4. Public Sub New()
      5. End Sub
      6. Public Sub New(contactValue As String)
      7. Me.ContactValue = contactValue
      8. End Sub
      9. Public Sub New(contactValue As String, contactType As EnuContactType)
      10. Me.ContactValue = contactValue
      11. Me.ContactType = contactType
      12. End Sub
      13. Public Property ContactValue As String
      14. Public Property ContactType As EnuContactType = 0
      15. End Class
      16. Public Enum EnuContactType
      17. PhoneMobile = 0
      18. PhonePrivate = 1
      19. PhoneBusiness = 2
      20. MailPrivate = 3
      21. MailBusiness = 4
      22. Fax = 5
      23. Facebook = 6
      24. Instagram = 7
      25. Other = 8
      26. End Enum
      27. End Namespace


      Person Klasse

      VB.NET-Quellcode

      1. Namespace Model
      2. Public Class Person
      3. Inherits ModelBase
      4. Public Sub New()
      5. ContactItems = New List(Of Contact)
      6. End Sub
      7. Public Sub New(firstName As String, lastName As String, firstContact As Contact)
      8. Me.New(EnuSalutation.Mister, firstName, lastName, Nothing, Nothing, New List(Of Contact) From {firstContact})
      9. End Sub
      10. Public Sub New(salutation As EnuSalutation, firstName As String, lastName As String, image As String, birthday As Date?, contacts As List(Of Contact))
      11. Me.Salutation = salutation
      12. Me.FirstName = firstName : Me.LastName = lastName
      13. Me.Image = image : Me.Birthday = birthday
      14. ContactItems = New List(Of Contact)
      15. End Sub
      16. Public Property Salutation As EnuSalutation = 0
      17. Public Property FirstName As String
      18. Public Property LastName As String
      19. Public Property Image As String
      20. Public Property Birthday As Date?
      21. Public Property ContactItems As List(Of Contact)
      22. Public Property Notes As String
      23. Public Property IsFavorite As Boolean
      24. Public Property Group As String = "Ungrouped"
      25. End Class
      26. Public Enum EnuSalutation
      27. Mister = 0
      28. Miss = 1
      29. End Enum
      30. End Namespace


      Auch eine Model-Basisklasse habe ich erstellt:

      ModelBase - Basisklasse

      VB.NET-Quellcode

      1. Namespace Model
      2. Public MustInherit Class ModelBase
      3. Public Property ID As Guid()
      4. Public Property CreatedAt As Date = DateTime.Now()
      5. Public Property LastChangeAt As Date = DateTime.Now()
      6. End Class
      7. End Namespace


      Damit haben wir erstmal unser Datenmodell. Dieses ist die Grundlage für alles, ob wir die Daten nun von einem Webservice, einer XML, einer Datenbank oder von mir aus einer Textdatei erhalten ist und im Grunde erstmal egal, wir können egal wie und von wo die Daten kommen jederzeit Person-Objekte daraus machen um diese in unserer App weiterverwenden zu können.

      Erste Daten in die Liste laden

      Um den Code in diesem Beispiel einfach und übersichtlich zu halten lade ich im Moment noch keine Daten sondern erstelle beim Start der Anwendung Beispieldaten.
      Öffnen wir die MainWindow.vb und schreiben in die Window_Loaded Ereignisprozedur folgenden Code:

      VB.NET-Quellcode

      1. Imports PhoneBook.Model
      2. Class MainWindow
      3. Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
      4. Dim persList As New List(Of Person)
      5. Dim pers1 As New Person(EnuSalutation.Mister, "Sascha", "Patschka", Nothing,
      6. New Date(1983, 9, 12), New List(Of Contact))
      7. pers1.ContactItems.Add(New Contact("patschka.sascha@live.com",
      8. EnuContactType.MailPrivate))
      9. pers1.ContactItems.Add(New Contact("+43664 12 34 567",
      10. EnuContactType.PhonePrivate))
      11. persList.Add(pers1)
      12. End Sub
      13. End Class


      Damit haben wir erstmal eine Liste mit einem einzigen Eintrag erstellt. Diese Liste müssen wir nun unserem ItemsControl zuordnen.

      VB.NET-Quellcode

      1. Class MainWindow
      2. Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
      3. ...
      4. ...
      5. persList.Add(pers1)
      6. Me.ItemsControlContacts.ItemsSource = persList
      7. End Sub
      8. End Class


      Starten wir nun die Anwendung sehen wir auch den Eintrag in der Liste:



      Allerdings ist dies nicht wirklich das was wir sehen wollen. Wir haben gelernt das die WPF für Objekte für welche sie kein DataTemplate findet die ToString() Methode aufruft. Um nun überhaupt zu sehen ob das Objekt das korrekte Objekt ist können wir nun die ToString() Methode in der Person-Klasse überschreiben:

      VB.NET-Quellcode

      1. Public Overrides Function ToString() As String
      2. Return $"{FirstName} {LastName}"
      3. End Function


      Starten wir nun die Anwendung nochmals sehen wir bereits mehr:




      OK, die Daten kommen also an. Erstellen wir uns schnell ein DataTemplate um die Daten besser darstellen zu können.
      Um für das DataTemplate den Typ angeben zu können müssen wir den Namespace „Model“ importieren:

      XML-Quellcode

      1. xmlns:model="clr-namespace:PhoneBook.Model"


      Nun können wir das DataTemplate in den ItemsControl-Resourcen implementieren:

      XML-Quellcode

      1. <TabControl Grid.Row="1">
      2. <TabItem Header="Kontakte">
      3. <ItemsControl x:Name="ItemsControlContacts">
      4. <ItemsControl.Resources>
      5. <DataTemplate DataType="{x:Type model:Person}">
      6. <ListBoxItem>
      7. <Grid>
      8. <Grid.ColumnDefinitions>
      9. <ColumnDefinition Width="50"/>
      10. <ColumnDefinition Width="*"/>
      11. <ColumnDefinition Width="Auto"/>
      12. </Grid.ColumnDefinitions>
      13. <Grid.RowDefinitions>
      14. <RowDefinition Height="*"/>
      15. <RowDefinition Height="*"/>
      16. </Grid.RowDefinitions>
      17. <Image Source="{Binding Image}" Grid.RowSpan="2"/>
      18. <WrapPanel Orientation="Horizontal" Grid.RowSpan="2"
      19. Grid.Column="1" VerticalAlignment="Center">
      20. <Label Content="{Binding FirstName,FallbackValue=FirstName}" FontSize="18"/>
      21. <Label Content="{Binding LastName,FallbackValue=LastName}" FontSize="18"/>
      22. </WrapPanel>
      23. <ContentControl Grid.RowSpan="2" Grid.Column="2"
      24. Content="{StaticResource IconStar}"
      25. Height="25" Width="25"/>
      26. </Grid>
      27. </ListBoxItem>
      28. </DataTemplate>
      29. </ItemsControl.Resources>
      30. </ItemsControl>
      31. </TabItem>
      32. <TabItem Header="Gruppen"/>
      33. <TabItem Header="Favoriten"/>
      34. </TabControl>


      Unsere Anwendung sieht zur Laufzeit nun wie folgt aus:



      Tipp: Normalerweise erstellt man den Inhalt nicht direkt im DataTemplate da hierfür keine Vorschau verfügbar ist solange man keinen DesignTime-Support hat. Es empfiehlt sich ein UserControl zu erstellen und dieses in das DataTemplate einzubinden.


      Da wir keinen Bilderpfad angegeben haben wird kein Bild angezeigt, und der Stern ist laut unserem Template immer sichtbar, das müssen wir auch noch ändern.
      Wir beginnen mal mit dem Bild für dieses Objekt. Um das Beispiel einfach zu halten binde ich in diesem Moment kein Bild ein sondern werde schlicht eine URL zum meinem Probilbild von vb-paradise.de angeben.

      VB.NET-Quellcode

      1. Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
      2. Dim persList As New List(Of Person)
      3. Dim pers1ImageUrl As String = "https://www.vb-paradise.de/wcf/images/avatars/01/3128-0124b76125128a33003a968a4d2882f1479dd93f.jpg"
      4. Dim pers1 As New Person(EnuSalutation.Mister, "Sascha", "Patschka", pers1ImageUrl,
      5. New Date(1983, 9, 12), New List(Of Contact))
      6. pers1.ContactItems.Add(New Contact("patschka.sascha@live.com",
      7. EnuContactType.MailPrivate))
      8. pers1.ContactItems.Add(New Contact("+43664 12 34 567",
      9. EnuContactType.PhonePrivate))
      10. persList.Add(pers1)
      11. Me.ItemsControlContacts.ItemsSource = persList
      12. End Sub


      Nun sehen wir bereits auch ein Bild für diese Person:



      Um den Stern nun nur einzublenden gibt es mehrere Möglichkeiten. Wir wissen ja das es unter WPF mehrere Visibility-Zustände gibt. Visible zeigt das Control an, Hidden verbirgt es – reserviert den Platz für dieses aber und Collapse verbirgt es – reserviert den Platz allerdings nicht.

      Entweder wir regelt das Verhalten mittels einem Trigger oder wir schreiben einen Converter, die „schönere“ Methode ist ein Converter. Warum? Trigger erzeugen relativ viel XAML Code und wir haben nicht die Möglichkeit diese zu Debuggen. Trigger funktionieren – bitte nicht falsch verstehen, ich bin aber eher ein Freund von Convertern und in diesem Fall – einem Converter von Boolean (den Datentyp haben wir ja in der Klasse (Eigenschaft IsFavorite)) zu Visibility müssen wir den Converter gar nicht schreiben da uns einen solchen Converter Microsoft bereits implementiert hat welcher sich sogar im selben Namensraum befindet, nämlich in System.Windows.Controls.

      XML-Quellcode

      1. <Window.Resources>
      2. <ResourceDictionary>
      3. <ResourceDictionary.MergedDictionaries>
      4. <ResourceDictionary Source="Resources\Icons.xaml"/>
      5. </ResourceDictionary.MergedDictionaries>
      6. <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
      7. </ResourceDictionary>
      8. </Window.Resources>


      Nachdem wir den Converter initialisiert haben können wir diesem anhand des Keys verwenden:

      XML-Quellcode

      1. <ContentControl Grid.RowSpan="2" Grid.Column="2" Content="{StaticResource IconStar}" Height="25" Width="25" Visibility="{Binding IsFavorite, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay}"/>


      Gebunden ist die Eigenschaft Visibility nun auf die Eigenschaft IsFavorite der Klasse.
      Der Converter sorgt dafür das das ContentControl unsichtbar wird wenn der Wert von IsFavorite False ist und schaltet es sichtbar wenn der Wert True ist.
      Starten wir nun die Anwendung ist der Stern nicht mehr sichtbar. Ändern wir den Code nu,n um die eine Test-Person den Favoriten hinzuzufügen sehen wir den Stern wieder.

      VB.NET-Quellcode

      1. Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
      2. Dim persList As New List(Of Person)
      3. Dim pers1ImageUrl As String = "https://www.vb-paradise.de/wcf/images/avatars/01/3128-0124b76125128a33003a968a4d2882f1479dd93f.jpg"
      4. Dim pers1 As New Person(EnuSalutation.Mister, "Sascha", "Patschka", pers1ImageUrl,
      5. New Date(1983, 9, 12), New List(Of Contact))
      6. pers1.ContactItems.Add(New Contact("patschka.sascha@live.com",
      7. EnuContactType.MailPrivate))
      8. pers1.ContactItems.Add(New Contact("+43664 12 34 567",
      9. EnuContactType.PhonePrivate))
      10. pers1.IsFavorite = True
      11. persList.Add(pers1)
      12. Me.ItemsControlContacts.ItemsSource = persList
      13. End Sub


      Das einzige was nun nicht so schön ist, wäre das das Template nicht den kompletten horizontalen Platz einnimmt. Das ändern wir noch indem wir die Eigenschaft HorizontalContentAlignment auf Stretch setzen:

      XML-Quellcode

      1. <TabControl Grid.Row="1">
      2. <TabItem Header="Kontakte">
      3. <ItemsControl x:Name="ItemsControlContacts"HorizontalContentAlignment="Stretch">
      4. <ItemsControl.Resources>
      5. <DataTemplate DataType="{x:Type model:Person}">
      6. <ListBoxItem>
      7. <Grid>
      8. ...
      9. ...




      Das wars nun mal fürs erste, als nächstes werden wir implementieren das der User über den Plus-Button eine neue Person mit allen Angaben hinzufügen kann.





      Hier die Solution wieder als Download: 3.1_PhonebookDemo.zip
      Hier das PDF für Leute mit EBook:
      3.1 Teil 1.pdf

      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

      3.1.1 - Refactoring der Hauptview

      Neu

      3.1.1

      Refactoring der Hauptview




      Das ist ein kleines Spezialkapitel welches ich nur als Video zu Verfügung stellen werde. Da solche Dinge als Textbeitrag einfach sehr schwer zu erklären ist. Dazu kommt das wir alles was ich hier in diesem Kapitel mache ja bereits gelernt haben, hier aber Anhand eines „lebenden“ Praxisbeispiels nun erstmal Anwenden.

      Was mache ich in dem Video?
      Ich lagere ein paar Controls in eine UserControl aus und gebe einige Controls Default-Styles damit diese einheitliche Werte bekommen wie z.b. die Schriftgröße welche wir in unserer Telefonbuch—Anwendung gerne etwas größer hätten. Außerdem setze ich eine Animation für die Filter-Textbox ein.

      Schaut euch am besten das Video an und versucht zu verstehen was hier passiert und warum. Bei Fragen gibt es ja den SupportThread.

      Viel Spaß mit dem Video:




      Ich freue mich wie immer über Kommentare, Kritik und Vorschläge sowie natürlich auch likes/hilfreich.
      Entweder im Supportthread oder auf meinem YouTube Kanal.
      :thumbsup:
      If _work = worktype.hard Then Me.Drink(Coffee)
      Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

      ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##