WPF: Bind to command from ControlTemplate

12,018

Solution 1

I am not sure if this the correct way to do this. It's a bit difficult to read source code in comments, so I write this reply as an answer...

Here is the constructor of MyListView + the command binding methods:

public MyListView()
{        
    showColumnPickerCommand = new RoutedCommand("ShowColumnPickerCommand", typeof(MyListView));

    var binding = new CommandBinding();
    binding.Command = showColumnPickerCommand;
    binding.Executed += ShowColumnPicker;
    binding.CanExecute += ShowColumnPickerCanExecute;

    CommandBindings.Add(binding);
}

private void ShowColumnPicker(object sender, ExecutedRoutedEventArgs e)
{
    MessageBox.Show("Show column picker");          
}

private void ShowColumnPickerCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = true;
}

The bindings are not set up in a static context. The only things that are static are the DependencyProperty for the command, and the command itself:

public static readonly DependencyProperty ShowColumnPickerCommandProperty =
    DependencyProperty.Register("ShowColumnPickerCommand", typeof(RoutedCommand), typeof(MyListView));

private static RoutedCommand showColumnPickerCommand;

public static RoutedCommand ShowColumnPickerCommand
{
    get
    {
        return showColumnPickerCommand;
    }
}

The command needs to be static to able to bind to it from the XAML like this:

<Button Command="{x:Static local:MyListView.ShowColumnPickerCommand}" />

Solution 2

You should start by making the wrapper property for the command static and use

Command={x:Static local:MyListView.MyCustomCommand}

Generally you only want an ICommand property if the command is being set to a different value on each instance (like Button) or if it's something like a DelegateCommand/RelayCommand on a ViewModel. You should also remove all of the extra code in the getter and instead initialize the command either inline or in the static constructor and connect the CommandBinding in the control's instance constructor.

CommandBindings.Add(new CommandBinding(MyCustomCommand, binding_Executed));

**UPDATE

The RoutedCommand itself should be declared as static. ICommand instance properties are good for when an external consumer of your control is passing in a command to execute, which is not what you want here. There is also no need for a DP here and the one you're using is declared incorrectly - to be usable they need to have instance wrapper properties with GetValue/SetValue.

public static RoutedCommand ShowColumnPickerCommand
{
    get; private set;
}

static MyListView()
{        
    ShowColumnPickerCommand = new RoutedCommand("ShowColumnPickerCommand", typeof(MyListView));
}
Share:
12,018
Christian Myksvoll
Author by

Christian Myksvoll

Updated on June 04, 2022

Comments

  • Christian Myksvoll
    Christian Myksvoll almost 2 years

    I am trying to add a button to a custom ListView (MyListView) which triggers a command (MyCustomCommand) defined in MyListView. I have added the button (and a title text) by applying a ControlTemplate. The problem is that I have not found a way to trigger MyCustomCommand when clicking the button. What I eventually want to achieve is to open a Popup or ContextMenu where I can select which columns should be visible in the ListView.

    Here's my template source:

    <Style TargetType="local:MyListView">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:MyListView">
                    <Border Name="Border" BorderThickness="1" BorderBrush="Black">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="30" />
                                <RowDefinition />
                            </Grid.RowDefinitions>
    
                            <Grid Background="LightSteelBlue">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition />
                                    <ColumnDefinition Width="Auto" />
                                </Grid.ColumnDefinitions>
                                <TextBlock Margin="3,3,3,3" Text="{TemplateBinding HeaderTitle}" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Stretch" FontSize="16" />
                                <Button Margin="3,3,3,3" Grid.Column="1" 
                                        VerticalAlignment="Center" HorizontalAlignment="Right" Height="20"
                                        Command="{TemplateBinding MyCustomCommand}">A button</Button>
                            </Grid>
    
                            <ScrollViewer Grid.Row="1" Style="{DynamicResource {x:Static GridView.GridViewScrollViewerStyleKey}}">
                                <ItemsPresenter />
                            </ScrollViewer>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    

    Here is the definition for MyListView:

    public class MyListView : ListView
    {
        public static readonly DependencyProperty MyCustomCommandProperty = 
            DependencyProperty.Register("MyCustomCommand", typeof(ICommand), typeof(MyListView));
    
        private static RoutedCommand myCustomCommand;
    
        public ICommand MyCustomCommand
        {
            get
            {
                if (myCustomCommand == null)
                {
                    myCustomCommand = new RoutedCommand("MyCustomCommand", typeof(MyListView));
    
                    var binding = new CommandBinding();
                    binding.Command = myCustomCommand;
                    binding.Executed += binding_Executed;
    
                    CommandManager.RegisterClassCommandBinding(typeof(MyListView), binding);
                }
                return myCustomCommand;
            }
        }
    
        private static void binding_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            MessageBox.Show("Command Handled!");
        }
    
    
        public static readonly DependencyProperty HeaderTitleProperty =
            DependencyProperty.Register("HeaderTitle", typeof(string), typeof(MyListView));
    
        public string HeaderTitle { get; set; }
    }
    

    And here is the XAML that creates a simple instance of MyListView:

    <local:MyListView VerticalAlignment="Top" HeaderTitle="ListView title">
        <ListView.View>
            <GridView>
                <GridViewColumn Width="70" Header="Column 1" />
                <GridViewColumn Width="70" Header="Column 2" />
                <GridViewColumn Width="70" Header="Column 3" />
            </GridView>
        </ListView.View>
    
        <ListViewItem>1</ListViewItem>
        <ListViewItem>2</ListViewItem>
        <ListViewItem>1</ListViewItem>
        <ListViewItem>2</ListViewItem>
    </local:MyListView>
    

    Notice HeaderTitle which is bound to the DependencyProperty in MyListView. This works as expected. Why doesn't it work the same way with commands? Any clues of how to make this work?

  • Christian Myksvoll
    Christian Myksvoll over 13 years
    Thanks a lot. That solved my case :) Now I can open a Popup when the command executes.
  • Christian Myksvoll
    Christian Myksvoll over 13 years
    I have run into a new problem... The button to trigger the command is only available (enabled) in the first instance of MyListView in a window. Does it have anything to do with the keyword Static in: Command={x:Static local:MyListView.MyCustomCommand}
  • John Bowen
    John Bowen over 13 years
    Buttons with commands get disabled when either the command's CanExecute is false, or the command has no Execute handler attached. Make sure you don't have anything weird happening with CanExecute and that the CommandBinding is being set up on each ListView instance and not in a static context that will just affect the first one.
  • Christian Myksvoll
    Christian Myksvoll over 13 years
    Thanks again. I wrote an answer as a follow-up to your latest comment.
  • Christian Myksvoll
    Christian Myksvoll over 13 years
    After reading your update, I moved this line from the public constructor to the static contructor: ShowColumnPickerCommand = new RoutedCommand("ShowColumnPickerCommand", typeof(MyListView)); Now it works perfectly. Thanks again :)