Flexibles Menü-System mit Enums

    • WPF

      Flexibles Menü-System mit Enums

      Mir ging es auf die Nerven, im Viewmodel immer viele RelayCommand-Instanzen als Properties bereitzustellen, die die Klicks dann auf verschiedene Methoden leiten.
      Ich will doch nur Klickse empfangen, und unterscheiden können, was die bedeuten sollen.
      Gesagt getan:

      VB.NET-Quellcode

      1. Public Class aCommandContainer
      2. Public Enum CommandEnm : DoThis : DoThat : Pause : [Continue] : GetReady : End Enum
      3. Public Property CmdACommand As New RelayCommand(Of CommandEnm)(AddressOf ExecuteACommand)
      4. Private Sub ExecuteACommand(cmd As CommandEnm)
      5. Select Case cmd
      6. Case CommandEnm.DoThis : MessageBox.Show(cmd.ToString)
      7. Case CommandEnm.DoThat : MessageBox.Show(cmd.ToString)
      8. Case CommandEnm.Pause : MessageBox.Show(cmd.ToString)
      9. Case CommandEnm.Continue : MessageBox.Show(cmd.ToString)
      10. Case CommandEnm.GetReady : MessageBox.Show(cmd.ToString)
      11. End Select
      12. End Sub
      13. Public Property CommandsRaw As CommandEnm() = DirectCast([Enum].GetValues(GetType(CommandEnm)), CommandEnm())
      14. Public Property Commands As CommandEnm() = CommandEnm.DoThis.EnumGetValues
      15. Public Property CommandSelection As CommandEnm() = CommandEnm.DoThis.EnumGetValues.Skip(1).Take(3).ToArray
      16. Public Property AnotherSelection As CommandEnm() = {CommandEnm.DoThis, CommandEnm.GetReady}
      17. End Class
      Zunächstmal werden die Optionen als Enum definiert (#3). Das ist fein erweiterbar - kann ja jederzeit weitere hinzufügen.
      Dann gibts ein (jawohl: nur noch ein!) RelayCommand (#5), was die Klickse empfängt, und es empfängt auch einen CommandEnm-Argument, und leitet das weiter an die Execute-Methode (#7).
      Von dort aus könnte jetzt im Select Case an wirklich verarbeitende Methoden verteilt werden, aber weil das hier nur ein Sample ist, verarbeitet der Select Case die Eingabe gschwind selbst.

      Nu wirds bischen interessanter, es wird nämlich ein Array von Enum-Werten publik gemacht, damit im View ein Button solch einen Wert sich nehmen kann, und dem RelayCommand zuschicken. Da hat man nun verschiedene Möglichkeiten:
      #17: Framework-Bordmittel ist die Enum.GetValues-Methode, die leider an Umständlichkeit und Unintuitivität kaum noch zu überbieten ist.
      #18 Nutzt eine Extension, die dasselbe doch eleganter zu formulieren hilft. (Die Extension geht nicht von einem System.Type aus, sondern von einer konkreten Instanz eines beliebigen Enum-Wertes - den Type ruft sie dann davon ab.)
      #19 hängt 3 Linq-Kommandos dran, um zu zeigen, wie man die im View verfügbaren Command-Optionen auch gezielt ausgewählt bereitstellt.
      #20 ist ebenso eine Command-Auswahl, diesmal direkt als Array der ausgewählten Optionen notiert.

      Also statt der Zeilen #17 - 20 reichte auch eine Zeile - ich wollte nur Variationen aufzeigen.

      Gut - binden wir das Xaml dran:

      XML-Quellcode

      1. <Window x:Class="wndCommander"
      2. Title="wndCommander"
      3. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      4. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      5. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      6. mc:Ignorable="d" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      7. Height="300" Width="200"
      8. xmlns:my="clr-namespace:EnumCommander"
      9. DataContext="{Binding Mode=OneWay, Source={StaticResource aCommandContainer}}">
      Zunächst mal stelle ich das Window vor, um zu zeigen, dass dessen DataContext auf ein obiges Viewmodel gesetzt ist. Ich habe das hier gemacht, indem ich an eine Resources binde, die inne Application.Xaml instanziert ist.

      5 Variationen, Knöppe da nun anzubinden
      1. XML-Quellcode

        1. <UniformGrid Margin="2">
        2. <Button Content="{Binding Commands[0]}" Command="{Binding CmdACommand}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"/>
        3. <Button Content="{Binding Commands[1]}" Command="{Binding CmdACommand}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"/>
        4. <Button Content="{Binding Commands[2]}" Command="{Binding CmdACommand}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"/>
        5. <Button Content="{Binding Commands[3]}" Command="{Binding CmdACommand}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"/>
        6. <Button Content="{Binding Commands[4]}" Command="{Binding CmdACommand}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"/>
        7. </UniformGrid>
        Hier habe ich ein UniformGrid genommen, und 5 Buttons reingepackt, die alle fast identisch sind: Content, Command, CommandParameter - alles kann auf einheitliche Weise gebunden werden.
        Bemerkenswert ist zum einen, wie der Content indiziert an das Enum-Array des Viewmodels bindet. Dadurch brauche ich mir auch keine eigene Beschriftung auszudenken - das Enum ist die Beschriftung.
        Bemerkenswert zum anderen der CommandParameter - da bindet der Button an sich selbst, an eben selbigen Content nämlich.
        Und so ist gewährleistet, dass der Enum-Wert sowohl als Beschriftung angezeigt wird, als auch als Parameter ans RelayCommand geschickt.

      2. Ist natürlich schlechter Stil, 5 Buttons zu machen, und 5 mal dieselben Bindings hinzuschreiben, Also ab in einen Style damit:

        XML-Quellcode

        1. <UniformGrid Margin="2">
        2. <FrameworkElement.Resources>
        3. <Style TargetType="{x:Type Button}">
        4. <Style.Setters>
        5. <Setter Property="Command" Value="{Binding CmdACommand}"/>
        6. <Setter Property="CommandParameter" Value="{Binding Content, RelativeSource={RelativeSource Self}}"/>
        7. </Style.Setters>
        8. </Style>
        9. </FrameworkElement.Resources>
        10. <Button Content="{Binding Commands[1]}"/>
        11. <Button Content="{Binding Commands[2]}" />
        12. <Button Content="{Binding Commands[3]}" />
        13. <Button Content="{Binding Commands[4]}" />
        14. </UniformGrid>

        So kommt das Wesentliche doch gleich viel wesentlicher rüber, odr? ;)
        Hier wird auch eine Auswahl getroffen: die erste Enum-Option ist weggelassen

      3. Aber es sind immer noch 4 händisch im Xaml zu formulierende Button - dynamisch geht noch bischen anders:

        XML-Quellcode

        1. <ItemsControl Margin="2" ItemsSource="{Binding CommandSelection}">
        2. <ItemsControl.ItemTemplate>
        3. <DataTemplate>
        4. <Button Content="{Binding Mode=OneWay}"
        5. Command="{Binding DataContext.CmdACommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}"
        6. CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"/>
        7. </DataTemplate>
        8. </ItemsControl.ItemTemplate>
        9. <ItemsControl.ItemsPanel>
        10. <ItemsPanelTemplate>
        11. <WrapPanel/>
        12. </ItemsPanelTemplate>
        13. </ItemsControl.ItemsPanel>
        14. </ItemsControl>
        Jetzt sind die Button in einem ItemsControl, dessen ItemsSource an CommandSelection gebunden ist. Also die Auswahl trifft jetzt das Viewmodel, und das View präsentiert sie dann. Als ItemsPanel hab ich jetzt mal ein WrapPanel genommen, weil immer UniformGrid wird ja mal langweilig ;)
        Command-Binding ist nun ein mittlerer Bandwurm, weil im lokalen DataContext des einzelnen ItemControl-Items ist kein Command, daher muss über den DataContext des beinhaltenden ItemsControl gegangen werden.
        Aber einmal hinbosseln, und dann funzt das.
      4. Aber es gibt ja noch andere ItemsControls - für Knöppe zu organisieren ist ja eiglich das Menu erfunden worden:

        XML-Quellcode

        1. <Menu Margin="2" ItemsSource="{Binding CommandSelection}">
        2. <ItemsControl.ItemContainerStyle>
        3. <Style TargetType="MenuItem">
        4. <Style.Setters>
        5. <Setter Property="Command" Value="{Binding DataContext.CmdACommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Menu}}}"/>
        6. <Setter Property="CommandParameter" Value="{Binding Header, RelativeSource={RelativeSource Self}}"/>
        7. </Style.Setters>
        8. </Style>
        9. </ItemsControl.ItemContainerStyle>
        10. </Menu>
        Hier finden die Command-Bindereien nicht innerhalb eines DataTemplates statt, sondern über den ItemContainerStyle, der auf die MenuItems angewendet wird.
        Interessanterweise muss man MenuItem.Header gar nicht binden - das wird wohl automatisch verknüpft, wenn man dem Menu die ItemsSource setzt.
        Also diese Konstruktion etabliert die MenuItems auf oberster Ebene des Menus

      5. Und ebensogut kann man die MenuItems auch einschachteln, als Submenu:

        XML-Quellcode

        1. <Menu Margin="2">
        2. <MenuItem Header="Commands" ItemsSource="{Binding CommandSelection}">
        3. <ItemsControl.ItemContainerStyle>
        4. <Style TargetType="MenuItem">
        5. <Style.Setters>
        6. <Setter Property="Command" Value="{Binding DataContext.CmdACommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Menu}}}"/>
        7. <Setter Property="CommandParameter" Value="{Binding Header, RelativeSource={RelativeSource Self}}"/>
        8. </Style.Setters>
        9. </Style>
        10. </ItemsControl.ItemContainerStyle>
        11. </MenuItem>
        12. </Menu>


      Jo, soweit - nu viel Spass mit der Sample-Solution.
      Zu der ist noch zu sagen, sie enthält auch ein stark abgespecktes, aber immer noch urs fettes Helpers-Projekt.
      Daraus benötigt werden das RelayCommand, die Viewmodel-Basisklasse und für die eingangs erwähnte Enum-Extension.
      Also wenn man ein (anderes) MVVM-Toolkit eingebunden hat, kann man mit bischen umbauen auch ohne meine Helpers auskommen, und die Enum-Extension ist ja auch nur ein Einzeiler.

      Edit: Eine Weiterentwicklung des Konzepts findet sich (quasi nebenbei) in: [OpenSource] Sync - Tool für Dateisystem-Backups und -Synchronisierungen
      Dort können auch verschachtelte Menüs im Viewmodel definiert werden, und AccessKeys, und Icons (funzt ebenso mit Buttons). Allerdings bedarf es dafür des Zusammenwirkens mehrerer Klassen.
      Dateien
      • EnumCommander.zip

        (27,57 kB, 260 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()