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

6

31

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.

Portable answered 4/4, 2011 at 22:47 Comment(0)
I
71

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 :)

Isolt answered 4/4, 2011 at 23:9 Comment(12)
I can't find System.Windows.Interactivity.dll. My guess is that it is required to have Blend's API. Could you please point me to the appropriate location? Thanks.Portable
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: microsoft.com/downloads/en/…Haywire
I downloaded and installed Microsoft Expression Blend 4 SDK. When I run VS2010 afterwards, I was not offered "schemas.microsoft.com/expression/2010/interactivity" namespace in the intellisense dropdown while naming a new namespace for the Window. What should I do to have it there?Portable
@Boris: you have to add a reference to the library. Click "Add Reference" on your project and look for "System.Windows.Interactivity"Isolt
Aaaaah! I forgot to add a reference to the assembly first! My bad!Portable
@AbdouMoumen, I can't just place the <i:EventTrigger> inside the <ListBox> tag. What should I wrap the <i:EventTrigger> with in order to make it work? I tried <ListBox.Triggers><i:EventTrigger>, but it doesn't work. Please help?Portable
Great, I am able to compile now, but the command doesn't get executed. It must be the binding or something like that. Either way, AbdouMoumen thank you for helping me out! I honestly appreciate it. (Thanks go to Elad as well)Portable
I've tested the code snippet this time ;) and it works just fine. just make sure you set the ViewModel to be the DataContext of your control, and also make sure the class is "public" (I lost half of yesterday debugging a binding problem and found out that it's caused by the class not being "public!)Isolt
In the end it turned out that my error was caused by omitting the command parameter. Oh, I am such a newbie! :DPortable
-1: @AbdouMoumen: While this answer is mostly OK, it has one tricky problem: it binds the mouse2click to the ListBox, not ListBoxItem. This means that if the ListBox displays its internal scrollbars, doubleclicking on such scrollbars will trigger this command. Usually, that's highly unwantedTriform
Unfortunately, adding a reference to System.Windiows.Interaction.dll was not sufficient. Only a post-build-action with copy /Y "%ProgramFiles%\Microsoft SDKs\Expression\Blend\.NETFramework\v4.0\Libraries\System.Windows.Interactivity.*" "$(TargetDir)" did the trick.Saucier
xmlns:i="schemas.microsoft.com/xaml/behaviors" works.Harrow
A
10

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.

Aliciaalick answered 4/4, 2011 at 23:2 Comment(2)
Paul, I agree that no 1 is the simplest solution and not the worst thing in the world, as you've put it. However, since the purpose of my question is education, I'll stick with the other two solutions you have provided. I am clueless when it comes to the second alternative - I don't get the provided answer at all. The third one makes the most sense to me, but I'll wait for AbdouMoumen's reply for a while, because his solution avoids creating additional class (ExecuteCommandAction). Thanks for all the help!Portable
Abdou's higher-scored answer has a nontrivial problem. There's no point in copying comments from there, I'll just say that binding a command to LIST is completely different than bindig it to each of the ITEMs. Such change has important consequences that are not visible from "just handle the click" point of view. The datacontext change and "doubleclick on scrollbar" are probably the easiest to spot. On the other hand, all three approaches described by Paul are free of that problems, because they bind commands to proper targets. In short, they are correct, Abdou's are not.Triform
I
5

EDIT: I wrote this post as an inexperienced WPF developer, nowadays I'd either use a framework that provides event to command binding, or simply use a button and restyle it. Of course for maximum flexibility this is maybe better.

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.

Imprimatur answered 7/3, 2017 at 14:30 Comment(3)
thanks! Runs perfectly and seems the most clean solution with the View Viewmodel layering.Badtempered
I had to bind a custom Event from my UserControl to a Command in my main View. Interactivity did not work with custom Events and I was struggling to deal with it. Creating a custom Command instead of an Event is the smartest and, indeed, the most elegant solution for me. For the time being at least. Thank you!Sparteine
My pleasure! I recommend using an MVVM framework, then you won't have to do any of this. For example in Caliburn.Micro, you can use Actions to handle XAML events in your ViewModel, for example: <Button cal:Message.Attach="[Event Click] = [Action OnClick]" /> where OnClick is a method in your ViewModel. A lot more elegant than this :-)Imprimatur
S
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>
Saito answered 30/1, 2019 at 3:44 Comment(0)
F
1

I recently needed to trigger an ICommand upon double clicking a ListBoxItem as well.

Personally, I don't like the DataTemplate method as it is binding to the content inside the ListBoxItem container, and not the container itself. I've opted to use an Attached Property to assign an InputBinding on the container. It takes a little more elbow grease, but it works well.

First, we need to create an attached property class. I've created mine a little more generically towards any class that derives from FrameworkElement, just in case I run into this again with a different visual.

public class FrameworkElementAttachedProperties : DependencyObject
{
    public static readonly DependencyProperty DoubleClickProperty = DependencyProperty.RegisterAttached("DoubleClick", typeof(InputBinding),
        typeof(FrameworkElementAttachedProperties), new PropertyMetadata(null, OnDoubleClickChanged));

    public static void SetDoubleClick(FrameworkElement element, InputBinding value)
    {
        element.SetValue(DoubleClickProperty, value);
    }

    public static InputBinding GetDoubleClick(FrameworkElement element)
    {
        return (InputBinding)element.GetValue(DoubleClickProperty);
    }

    private static void OnDoubleClickChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement element = obj as FrameworkElement;
        
        /// Potentially throw an exception if an object is not a FrameworkElement (is null).
        
        if(e.NewValue != null)
        {
            element.InputBindings.Add(e.NewValue as InputBinding);
        }
        if(e.OldValue != null)
        {
            element.InputBindings.Remove(e.OldValue as InputBinding);
        }
    }
}

Then the final step is to override the base container style for the ListBoxItem.

<ListBox.ItemContainerStyle>
    <Style TargetType="{x:Type ListBoxItem}"
        BasedOn="{StaticResource ListBoxItem}">
        <Setter Property="local:FrameworkElementAttachedProperties.DoubleClick">
            <Setter.Value>
                <MouseBinding Command="{Binding OnListBoxItemDoubleClickCommand}"
                    MouseAction="LeftDoubleClick"/>
            </Setter.Value>
        </Setter>
    </Style>
</ListBox.ItemContainerStyle>

Now, anytime a ListBoxItem is double clicked, it will fire our OnListBoxItemDoubleClickCommand.

Fasano answered 28/5, 2021 at 4:16 Comment(0)
I
0

If you're looking for a nice simple solution that uses interactions instead of mucking about with user controls, code behind, input bindings, custom attached properties, etc.

And you want something that works at the ListBoxItem level, i.e. not ListBox level as per the (incorrectly) accepted solution.

Then here's a snippet for a simple 'button like' click action..

<ListBox>
  <ListBox.ItemTemplate>
    <DataTemplate>
      <Grid Background="Transparent">
        <!-- insert your visuals here -->
        
        <b:Interaction.Triggers>
          <b:EventTrigger EventName="MouseUp">
            <b:InvokeCommandAction Command="{Binding YourCommand}" />
          </b:EventTrigger>
        </b:Interaction.Triggers>
      </Grid>
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>

Note, background="Transparent" is required to ensure the entire Grid is clickable and not just the contents inside.

Inodorous answered 14/5, 2021 at 13:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.