wpfmvvmmouseeventmvvm-lightinputbinding

ListBoxItem MouseBinding in ControlTemplate InputBinding not Working MVVM


I have what I hope is an obvious problem to someone. I want an Edit command to fire when a user double clicks a ListBoxItem inside a ListBox. I have done this before in user controls but want to do it directly in a VIEW since it is a simple enough ListBox. But it will not wire up.

Here is the list box:

<ListBox SelectedItem="{Binding DataQuerySortSelected}"
         ItemContainerStyle="{StaticResource SortListItemStyle}"
         ItemsSource="{Binding DataQueryHolder.DataQuerySorts}">

    <ListBox.InputBindings>
        <KeyBinding Key="Delete" Command="{Binding DataQuerySortDelete}" />
    </ListBox.InputBindings>

    <ListBox.Resources>
        <Style TargetType="{x:Type ListBoxItem}">
            <Setter Property="AllowDrop" Value="True" />
            <EventSetter Event="PreviewMouseMove" Handler="DragDropListBoxItem_PreviewMouseMoveEvent" />
            <EventSetter Event="Drop" Handler="DragDropListBoxItem_Drop" />
        </Style>

    </ListBox.Resources>
</ListBox>

Note the Delete Key binding at the top level works just fine. Here is the referenced style (brought in as a separate ResourceDictionary but putting the style inline made no difference):

<Style x:Key="SortListItemStyle" TargetType="ListBoxItem">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListBoxItem">
                <Border Name="MainBorder">
                    <ContentPresenter>
                        <ContentPresenter.InputBindings>
                            <MouseBinding Gesture="LeftDoubleClick" Command="{Binding DataQuerySortEdit}" />
                        </ContentPresenter.InputBindings>
                    </ContentPresenter>

                    <Border.InputBindings>
                        <MouseBinding Gesture="LeftDoubleClick" Command="{Binding DataQuerySortEdit}" />
                    </Border.InputBindings>
                </Border>

                <ControlTemplate.Triggers>
                    <Trigger Property="IsSelected" Value="True">
                        <Setter TargetName="MainBorder" Value="Yellow" Property="Background" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

I put the mouse binding in two places just to see if it made a difference but it does not. There just isnt any wiring going on there. Everything else works as expected. If I create a plain button in the View and point it at the DataQuerySortEdit Command it works as expected.

Am I missing something? Thanks for any help.


EDIT: Just adding some more info in response to J's response. I gave the Border of the ControlTemplate a binding relative to the nearest listbox and gave the listbox a name so the output would confirm it would find it. This was the output window:

System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''DataQuerySort' (HashCode=7641038)'. BindingExpression:Path=DataQuerySortEdit; DataItem='DataQuerySort' (HashCode=7641038); target element is 'MouseBinding' (HashCode=65119131); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''DataQuerySort' (HashCode=50439840)'. BindingExpression:Path=DataQuerySortEdit; DataItem='DataQuerySort' (HashCode=50439840); target element is 'MouseBinding' (HashCode=3649016); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''DataQuerySort' (HashCode=65588106)'. BindingExpression:Path=DataQuerySortEdit; DataItem='DataQuerySort' (HashCode=65588106); target element is 'MouseBinding' (HashCode=35717517); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''DataQuerySort' (HashCode=32836053)'. BindingExpression:Path=DataQuerySortEdit; DataItem='DataQuerySort' (HashCode=32836053); target element is 'MouseBinding' (HashCode=66172851); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''ListBox' (Name='SortListBox')'. BindingExpression:Path=DataQuerySortEdit; DataItem='ListBox' (Name='SortListBox'); target element is 'MouseBinding' (HashCode=28263486); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''ListBox' (Name='SortListBox')'. BindingExpression:Path=DataQuerySortEdit; DataItem='ListBox' (Name='SortListBox'); target element is 'MouseBinding' (HashCode=27134857); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''ListBox' (Name='SortListBox')'. BindingExpression:Path=DataQuerySortEdit; DataItem='ListBox' (Name='SortListBox'); target element is 'MouseBinding' (HashCode=7437765); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''ListBox' (Name='SortListBox')'. BindingExpression:Path=DataQuerySortEdit; DataItem='ListBox' (Name='SortListBox'); target element is 'MouseBinding' (HashCode=58400697); target property is 'Command' (type 'ICommand')

So the second attempt to bind (I am guessing the one in the Border InputBinding) does fine the proper listbox but still cannot find the ICommand. I tried doing a relative find to the window, to the Gird containing the list, etc. and still cant get it to wire. I also tried as J mentions to put the relative search directly in the MouseBindings and they result in the same errors.


EDIT2: Here are the Command and properties in the ViewModel, using MVVMLight

public DataQuerySort DataQuerySortSelected
{
    get { return _DataQuerySortSelected; }
    set { NotifySetProperty(ref _DataQuerySortSelected, value, () => DataQuerySortSelected); }
}
private DataQuerySort _DataQuerySortSelected;


public RelayCommand DataQuerySortEdit
{
    get { return new RelayCommand(_DataQuerySortEdit, CanDataQuerySortEdit); }
}
private void _DataQuerySortEdit()
{
    DataQuerySortHolder = DataQuerySortSelected.Copy();
    DataQuerySortEditMode = EditMode.Edit;
}    
private bool CanDataQuerySortEdit() 
{ 
    return DataQuerySortSelected != null; 
}

Taking out the CanDataQuerySortEdit makes not difference. I know everything works because if I create a button and point at it it works. If I also create an inputbinding in the ListBox for the mouse like the Delete key that works - as long as I click outside the ListBoxItems of course.

EDIT3: Here is Part of the View itself including the class, datacontext, and resources. I have tried doing relative bindings to be "{x:Type Window}" and "{x:Type l:ToolkitWindowBase}". The ToolkitWindowBase extends Window directly. The frmDataBrowserViewModel extends a class called ToolkitViewModelBase which extends ViewModelBase from MVVMLight:

<l:ToolkitWindowBase x:Class="GISToolkit.frmDataBrowser"  x:Name="mainWindow" Icon="Images/favicon.ico" 
    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"
    xmlns:l="clr-namespace:GISToolkit;assembly="
    xmlns:lc="clr-namespace:GISToolkit.Controls;assembly="
    xmlns:ls="clr-namespace:GISToolkit.Settings;assembly="
    xmlns:system="clr-namespace:System;assembly=mscorlib"
    xmlns:xctk="clr-namespace:Xceed.Wpf.Toolkit;assembly=Xceed.Wpf.Toolkit"
    xmlns:xctkp="clr-namespace:Xceed.Wpf.Toolkit.Primitives;assembly=Xceed.Wpf.Toolkit"
    Title="Solutions GIS Toolkit - Setup"  
    ResizeMode="CanResizeWithGrip" Foreground="White"
    l:ControlBox.HasMaximizeButton="False" l:ControlBox.HasMinimizeButton="False" l:ControlBox.HasCloseButton="False"
    Height="{Binding RunTimeHeight, Mode=TwoWay}" 
    Width="{Binding RunTimeWidth, Mode=TwoWay}" IsSettingsDirty="{Binding IsCurrentSettingsDirty}" IsEnabled="True">

    <Window.DataContext>
        <l:frmDataBrowserViewModel />
    </Window.DataContext>

    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Themes/DataBrowser.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>

    ..................
<l:ToolkitWindowBase />

EDIT4: Just in case someone out there is still listing, do me a favor, create a new WPF project called "WpfMvvmApplication1" with a single Window called "BindingTestWindow" and a viewmodel called "MainWindowViewModel" Then for the window put (should be simple cut/paste unless you used different names for the files/project):

<Window x:Class="WpfMvvmApplication1.BindingTestWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WpfMvvmApplication1"
        Title="BindingTestWindow" Height="300" Width="300">

    <Window.DataContext>
        <l:BindingTestViewModel />
    </Window.DataContext>

    <Grid>
        <GroupBox Header="Sorting:" >
            <Grid>
                <ListBox Background="White" Name="SortListBox" ItemsSource="{Binding TestCollection}">

                    <ListBox.InputBindings>
                        <KeyBinding Key="Delete" Command="{Binding TestCommand}" />
                    </ListBox.InputBindings>

                    <ListBox.Resources>
                        <Style TargetType="{x:Type ListBoxItem}">
                            <Setter Property="AllowDrop" Value="True" />
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="ListBoxItem">
                                        <Border Name="MainBorder" Padding="0" Margin="0">
                                            <ContentPresenter />

                                            <Border.InputBindings>
                                                <MouseBinding Gesture="LeftDoubleClick" Command="{Binding TestCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
                                            </Border.InputBindings>
                                        </Border>

                                        <ControlTemplate.Triggers>
                                            <Trigger Property="IsSelected" Value="True">
                                                <Setter TargetName="MainBorder" Value="Yellow" Property="Background" />
                                            </Trigger>
                                        </ControlTemplate.Triggers>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </ListBox.Resources>
                </ListBox>
            </Grid>
        </GroupBox>
    </Grid>
</Window>

and for the VIEWMODEL:

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Windows.Input;

namespace WpfMvvmApplication1
{
    public class BindingTestViewModel : NotificationObject
    {
        public BindingTestViewModel()
        {
            TestCollection = new ObservableCollection<string>();
            for (int i = 0; i < 10; i++ )
                TestCollection.Add("test" + i);
        }

        public ICommand TestCommand { get { return new DelegateCommand(_TestCommand); } }

        private void _TestCommand() { System.Diagnostics.Debugger.Break(); }

        public ObservableCollection<string> TestCollection
        {
            get { return _TestCollection; }
            set 
            {
                _TestCollection = value;
                RaisePropertyChanged(() => TestCollection);
            }
        }
        private ObservableCollection<string> _TestCollection;
    }

    public class DelegateCommand : ICommand
    {
        private readonly Action _command;
        private readonly Func<bool> _canExecute;
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public DelegateCommand(Action command, Func<bool> canExecute = null)
        {
            if (command == null)
                throw new ArgumentNullException();
            _canExecute = canExecute;
            _command = command;
        }

        public void Execute(object parameter)
        {
            _command();
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null || _canExecute();
        }

    }

    public class NotificationObject : INotifyPropertyChanged
    {
        protected void RaisePropertyChanged<T>(Expression<Func<T>> action)
        {
            var propertyName = GetPropertyName(action);
            RaisePropertyChanged(propertyName);
        }

        private static string GetPropertyName<T>(Expression<Func<T>> action)
        {
            var expression = (MemberExpression)action.Body;
            var propertyName = expression.Member.Name;
            return propertyName;
        }

        private void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

This has nothing else in it. It gives me the binding error when the inputbinding is inside the listboxtiem but not when in, say, the listbox itself. Seems like it should work since the output says it does find the window in the FindAncestor.


Solution

  • Big thanks to J King for hanging in there. But it seems that doing in XAML does not work. I ended up doing this in the code behind of the VIEW (hope it can help someone):

    public BindingTestWindow()
    {
        InitializeComponent();
    
        SortListBox.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
    }
    
    private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
    {
        if (SortListBox.ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
        {
            BindingTestViewModel vm = (BindingTestViewModel)this.DataContext;
    
            for(int i = 0; i < SortListBox.Items.Count; i++)
            {
                ListBoxItem lbi = (ListBoxItem)SortListBox.ItemContainerGenerator.ContainerFromIndex(i);
                lbi.InputBindings.Clear();
                lbi.InputBindings.Add(new InputBinding(vm.TestCommand, new MouseGesture(MouseAction.LeftDoubleClick)));
            }
        }
    }  
    

    In order to manipulate the actual listboxitems (and not their content through ListBox.Items) we have to use the ItemContainerGenerator. But in order to do that, you have to wait for it to be completely generated hence the need for the event handler. Its not pretty but it works.

    Ernie