WPF: How to bind a command to the ListBoxItem using MVVM?

78,831

Solution 1

Unfortunately, only ButtonBase derived controls have the possibility for binding ICommand objects to their Command properties (for the Click event).

However, you can use an API provided by Blend to map an event (like in your case MouseDoubleClick on the ListBox) to an ICommand object.

<ListBox>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction Command="{Binding YourCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ListBox>

You'll have to define: xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" and have a reference to System.Windows.Interactivity.dll.

-- EDIT -- This is part of WPF4, but u can use Microsoft.Windows.Interactivity if you're not using WPF4. This dll is from Blend SDK, which doesn't require Blend, from here: http://www.microsoft.com/downloads/en/details.aspx?FamilyID=f1ae9a30-4928-411d-970b-e682ab179e17&displaylang=en

Update: I found something that should help you. check this link on MVVM Light Toolkit which contains a walkthrough on how to do this, along with a link to the needed libraries. MVVM Light Toolkit is a very interesting framework for applying MVVM with Silverlight, WPF, and WP7.

Hope this helps :)

Solution 2

This is made tricky because of the DoubleClick event. There are a few ways to do this:

  1. Handle the double-click event in code behind, and then manually invoke a command/method on your ViewModel
  2. Use an attached behavior to route the DoubleClick event to your Command
  3. Use a Blend Behavior to map the DoubleClick event to your command

2 and 3 might be more pure, but frankly, 1 is easier, less complex, and not the worst thing in the world. For a one-off case, I'd probably use approach #1.

Now, if you changed your requirements to use, say, a hyperlink on each item, it would be easier. Start out by naming the root element in your XAML - e.g., for a Window:

<Window .... Name="This">

Now, in the DataTemplate for your ListBox items, use something like this:

<ListBox ...>
  <ListBox.ItemTemplate>
    <DataTemplate>
      <Hyperlink 
        Command="{Binding ElementName=This, Path=DataContext.OpenEntryCmd}"
        Text="{Binding Path=Name}" 
        />

The ElementName binding lets you resolve the OpenEntryCmd from the context of your ViewModel, rather than the specific data item.

Solution 3

This is a bit of a hack, but it works well and allows you to use commands and avoid code behind. This also has the added benefit of not triggering the command when you double-click (or whatever your trigger is) in the empty ScrollView area assuming your ListBoxItems don't fill the entire container.

Basically, just create a DataTemplate for your ListBox that is composed of a TextBlock and bind the width of the TextBlock to the width of the ListBox, set the margins and padding to 0, and disable horizontal scrolling (because the TextBlock will bleed beyond the visible bounds of the ScrollView triggering the horizontal scroll bar otherwise). The only bug I've found is that the command won't fire if the user clicks precisely on the border of the ListBoxItem, which I can live with.

Here is an example:

<ListBox
    x:Name="listBox"
    Width="400"
    Height="150"
    ScrollViewer.HorizontalScrollBarVisibility="Hidden"
    ItemsSource="{Binding ItemsSourceProperty}"
    SelectedItem="{Binding SelectedItemProperty}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Padding="0" 
                        Margin="0" 
                        Text="{Binding DisplayTextProperty}" 
                        Width="{Binding ElementName=listBox, Path=Width}">
                <TextBlock.InputBindings>
                    <MouseBinding 
                        Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBox}}, Path=DataContext.SelectProjectCommand}" 
                                    Gesture="LeftDoubleClick" />
                </TextBlock.InputBindings>
            </TextBlock>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Solution 4

I find the best way to do this is to create a simple user control wrapper for my content, with dependency properties for the command and parameter.

The reason I did this was due to the Button not bubbling the click event to my ListBox which prevented it from selecting the ListBoxItem.

CommandControl.xaml.cs:

public partial class CommandControl : UserControl
{
    public CommandControl()
    {
        MouseLeftButtonDown += OnMouseLeftButtonDown;
        InitializeComponent();
    }

    private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
    {
        if (Command != null)
        {
            if (Command.CanExecute(CommandParameter))
            {
                Command.Execute(CommandParameter);
            }
        }
    }

    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register("Command", typeof(ICommand),
            typeof(CommandControl),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));

    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    public static readonly DependencyProperty CommandParameterProperty =
        DependencyProperty.Register("CommandParameter", typeof(object),
            typeof(CommandControl),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));

    public object CommandParameter
    {
        get { return (object)GetValue(CommandParameterProperty); }
        set { SetValue(CommandParameterProperty, value); }
    }
}

CommandControl.xaml:

<UserControl x:Class="WpfApp.UserControls.CommandControl"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300"
         Background="Transparent">
</UserControl>

Usage:

<ListBoxItem>
    <uc:CommandControl Command="{Binding LoadPageCommand}"
                       CommandParameter="{Binding HomePageViewModel}">
        <TextBlock Text="Home" Margin="0,0,0,5" VerticalAlignment="Center"
                   Foreground="White" FontSize="24" />
    </uc:CommandControl>
</ListBoxItem>

The Content can be whatever, and when the control is clicked, it will execute the command.

EDIT: Added Background="Transparent" to UserControl to enable click events on the entire area of the control.

Share:
78,831
Boris
Author by

Boris

Updated on May 28, 2021

Comments

  • Boris
    Boris almost 3 years

    I have just started learning MVVM. I've made the application from scratch by following this MVVM tutorial (I highly recommend it to all MVVM beginners out there). Basically, what I have created so far is a couple of text boxes where user adds his or her data, a button to save that data which subsequently populates the ListBox with all entries made.

    Here's where I got stuck: I want to be able to double-click on a ListBoxItem and to trigger a command that I have created and added to my ViewModel. I don't know how to finish the XAML side, i.e. I don't know how to bind that command to the ListBox(Item).

    Here's XAML:

    ...
    <ListBox 
        Name="EntriesListBox" 
        Width="228" 
        Height="208" 
        Margin="138,12,0,0" 
        HorizontalAlignment="Left" 
        VerticalAlignment="Top" 
        ItemsSource="{Binding Entries}" />
    ...
    

    Here's ViewModel:

    public class MainWindowViewModel : DependencyObject
    {
        ...
        public IEntriesProvider Entries
        {
            get { return entries; }
        }
    
        private IEntriesProvider entries;
        public OpenEntryCommand OpenEntryCmd { get; set; }
    
        public MainWindowViewModel(IEntriesProvider source)
        {
            this.entries = source;
            ...
            this.OpenEntryCmd = new OpenEntryCommand(this);
        }
        ...
    }
    

    And finally, here's the OpenEntryCommand that I want to be executed once the user double-clicks the item in the EntriesListBox:

    public class OpenEntryCommand : ICommand
    {
        private MainWindowViewModel viewModel;
    
        public OpenEntryCommand(MainWindowViewModel viewModel)
        {
            this.viewModel = viewModel;
        }
    
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
    
        public bool CanExecute(object parameter)
        {
            return parameter is Entry;
        }
    
        public void Execute(object parameter)
        {
            string messageFormat = "Subject: {0}\nStart: {1}\nEnd: {2}";
            Entry entry = parameter as Entry;
            string message = string.Format(messageFormat, 
                                           entry.Subject, 
                                           entry.StartDate.ToShortDateString(), 
                                           entry.EndDate.ToShortDateString());
    
            MessageBox.Show(message, "Appointment");
        }
    }
    

    Please help, I'd appreciate it.