Saturday, November 28, 2009

WPF Command

image

In WPF Skinning, I was using old school MenuItem_Click event handling. WPF has a more powerful commanding system. To implement the same functionality with WPF command, first create a command in Window1:

public static readonly RoutedCommand ChangeSkin = new RoutedCommand(
    "ChangeSkin", typeof(Window1));

Bind the menu items to be sources of this command. Each binds to the same command with a different parameter. Now MenuItem_Click event handling can be removed and IsChecked binding can be one-way. I’ll use the command to change skin and only need menu items to display correct check marks.

<MenuItem Header="_Default"
        Command="local:Window1.ChangeSkin" CommandParameter="Default"
        IsChecked="{Binding Mode=OneWay, ElementName=mainWindow, Path=Skin,
              Converter={StaticResource skinEquals},
              ConverterParameter=Default}"/>
<MenuItem Header="_Green"
        Command="local:Window1.ChangeSkin" CommandParameter="Green"
        IsChecked="{Binding Mode=OneWay, ElementName=mainWindow, Path=Skin,
              Converter={StaticResource skinEquals},
              ConverterParameter=Green}"/>
<MenuItem Header="_Red"
        Command="local:Window1.ChangeSkin" CommandParameter="Red"
        IsChecked="{Binding Mode=OneWay, ElementName=mainWindow, Path=Skin,
              Converter={StaticResource skinEquals},
              ConverterParameter=Red}"/>

The command will bubble up until being handled. Create a command binding at the main window:

<Window.CommandBindings>
    <CommandBinding Command="local:Window1.ChangeSkin" Executed="ChangeSkin_Executed" />
</Window.CommandBindings>

Fill the command handling code:

private void ChangeSkin_Executed(object sender, ExecutedRoutedEventArgs e)
{
    Skin = (string)e.Parameter;
    e.Handled = true;
}

Now this should work the same as previous version.

The menu items do not have keyboard short-cuts (KeyGestures) yet. To make keyboard inputs invoke this command, add key bindings:

<Window.InputBindings>
    <KeyBinding Command="local:Window1.ChangeSkin" CommandParameter="Default"
               Key="D" Modifiers="Ctrl" />
    <KeyBinding Command="local:Window1.ChangeSkin" CommandParameter="Green"
               Key="G" Modifiers="Ctrl" />
    <KeyBinding Command="local:Window1.ChangeSkin" CommandParameter="Red"
               Key="R" Modifiers="Ctrl" />
</Window.InputBindings>

This makes the key gestures work, but they don’t show up in the menu items. They need to be specified in menu items, e.g.:

<MenuItem Header="_Green"
         Command="local:Window1.ChangeSkin" CommandParameter="Green"
         InputGestureText="Ctrl+G"
         IsChecked="{Binding Mode=OneWay, ElementName=mainWindow, Path=Skin,
           Converter={StaticResource skinEquals},
           ConverterParameter=Green}"/>

Alternatively, I can create 3 different commands and omit the above InputGestureText and even Header text:

public static readonly RoutedCommand DefaultSkin = new RoutedUICommand(
    "_Default", "Default", typeof(Window1),
    new InputGestureCollection(
        new KeyGesture[] { new KeyGesture(Key.D, ModifierKeys.Control) }));

public static readonly RoutedCommand GreenSkin = new RoutedUICommand(
    "_Green", "Green", typeof(Window1),
    new InputGestureCollection(
        new KeyGesture[] { new KeyGesture(Key.G, ModifierKeys.Control) }));

public static readonly RoutedCommand RedSkin = new RoutedUICommand(
    "_Red", "Red", typeof(Window1),
    new InputGestureCollection(
        new KeyGesture[] { new KeyGesture(Key.R, ModifierKeys.Control) }));

private void ChangeSkin_Executed(object sender, ExecutedRoutedEventArgs e)
{
    Skin = ((RoutedUICommand)e.Command).Name;
    e.Handled = true;
}

This time UI xaml is much cleaner:

<Window.CommandBindings>
    <CommandBinding Command="local:Window1.DefaultSkin" Executed="ChangeSkin_Executed" />
    <CommandBinding Command="local:Window1.GreenSkin" Executed="ChangeSkin_Executed" />
    <CommandBinding Command="local:Window1.RedSkin" Executed="ChangeSkin_Executed" />
</Window.CommandBindings>

<DockPanel x:Name="framePanel" LastChildFill="True">

    <Menu IsMainMenu="True" DockPanel.Dock="Top">
        <MenuItem Header="_Skins">
            <MenuItem Command="local:Window1.DefaultSkin"
                     IsChecked="{Binding Mode=OneWay, ElementName=mainWindow, Path=Skin,
                           Converter={StaticResource skinEquals}, ConverterParameter=Default}"/>
            <MenuItem Command="local:Window1.GreenSkin"
                     IsChecked="{Binding Mode=OneWay, ElementName=mainWindow, Path=Skin,
                           Converter={StaticResource skinEquals}, ConverterParameter=Green}"/>
            <MenuItem Command="local:Window1.RedSkin"
                     IsChecked="{Binding Mode=OneWay, ElementName=mainWindow, Path=Skin,
                           Converter={StaticResource skinEquals}, ConverterParameter=Red}"/>
        </MenuItem>
    </Menu>
   
    <Grid Background="{DynamicResource bgbrush}">
        <TextBlock
           Text="{Binding ElementName=mainWindow, Path=Skin}"
           HorizontalAlignment="Center"
           VerticalAlignment="Center"/>
    </Grid>
</DockPanel>

No comments: