3
votes

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.

3
I should mention this is all .NET 4.5.Ernie S

3 Answers

1
votes

Try changing your command binding to the following code. Listbox item is a visual child of listbox and you need to get your datacontext wired up correctly. I think. Note the Window Type. Change that to whatever your top level datacontext is where the command is declared and the listbox exists. Ie: usercontrol, window, etc...

<MouseBinding Gesture="LeftDoubleClick" Command="{Binding DataQuerySortEdit, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}}" /> 

You might be able to debug this my looking at your output window when you run your app, it will show you binding errors like this.

EDIT 1: Mvvm Light Event to Command

Okay, I totally overlooked the fact that you are suing Mvvm Light. MVVM-Light has a function built in for binding events to commands on your view model.

Add the following to your windows namespaces:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="http://www.galasoft.ch/mvvmlight"

Now change your border inside your listbox item to include the following:

<Border ...>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="LeftDoubleClick">
            <cmd:EventToCommand Command="{Binding DataContext.DataQuerySortEdit, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Border>

Mvvm light has built this in order to facilitate exactly what you are looking for. It lets you take any event and bind a command to it. I use this for form validation through the lostfocus events of controls etc.

Make sure you have the following references in your project:

  • GalaSoft.MvvmLight.WPF45
  • GalaSoft.MvvmLight.Extras.WPF45
  • Microsoft.Expression.Interactions

Hope this helps

EDIT 3:

Just a last ditch effort, what if you use the following:

It adds the Datacontext to the command name.

0
votes

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

0
votes

I spent quite a bit of time fiddling with this problem. Managed to mix some answers together with a result that's working. Inputbindings on a ListBox have a strange behavior. Some work on the ListBox itself, like a MiddleClick, others need to be implemented on the items. J Kings Binding to the DataContext did the trick in the ContentPresenter of the LisBoxItemStyle.

 <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ListBoxItem}">

                    <Border x:Name="Bd" 
                            BorderBrush="{TemplateBinding BorderBrush}" 
                            BorderThickness="{TemplateBinding BorderThickness}" 
                            Background="{TemplateBinding Background}" 
                            Padding="{TemplateBinding Padding}" 
                            SnapsToDevicePixels="true">

                        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                                          SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
                            <ContentPresenter.InputBindings>
                                <MouseBinding Gesture="Ctrl+MiddleClick" 
                                              Command="{Binding  DataContext.MiddleClickCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
                                <MouseBinding Gesture="Ctrl+RightClick" 
                                              Command="{Binding DataContext.CtrlRightClickCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
                                <MouseBinding MouseAction="RightClick" 
                                              Command="{Binding DataContext.RightClickCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
                            </ContentPresenter.InputBindings>
                        </ContentPresenter>
                    </Border>
                    <ControlTemplate.Triggers>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="IsMouseOver" Value="True"/>
                            </MultiTrigger.Conditions>
                            <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.MouseOver.Background}"/>
                            <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.MouseOver.Border}"/>
                        </MultiTrigger>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="Selector.IsSelectionActive" Value="False"/>
                                <Condition Property="IsSelected" Value="True"/>
                            </MultiTrigger.Conditions>
                            <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Background}"/>
                            <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Border}"/>
                        </MultiTrigger>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="Selector.IsSelectionActive" Value="True"/>
                                <Condition Property="IsSelected" Value="True"/>
                            </MultiTrigger.Conditions>
                            <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Background}"/>
                            <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Border}"/>
                        </MultiTrigger>
                        <Trigger Property="IsEnabled" Value="False">
                            <Setter Property="TextElement.Foreground" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                        </Trigger>
                    </ControlTemplate.Triggers>

                </ControlTemplate>
            </Setter.Value>
        </Setter>