8

I am using MVVM and am displaying two listboxes on one window. I am binding from both of those listboxes simultaneously to different fields call them A and B. A and B are then both modifying C. To make this work, I want to only have one item from the two listboxes IsSelected at once, so that A does not override C when B IsSelected. How can I restrict this?

Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
user1722960
  • 103
  • 1
  • 5
  • 1
    You want selection to be cleared in one ListBox when a selection is made in another ListBox? I find your question difficult to understand. – Simon Brydon Oct 06 '12 at 12:31
  • I basically want to know how I can bind something like a IsFocused/IsSelected Listbox property to a bool in my view model. – user1722960 Oct 06 '12 at 14:01
  • 2
    You can bind the SelectedIndex of a ListBox to a property in your view model. So for example when SelectedIndexA of ListBoxA is set, you could set SelectedIndexB of ListBoxB to -1 to clear ListBoxB's selection(s) and vice-versa. Is that your intention? – Simon Brydon Oct 06 '12 at 14:17
  • This should do the trick, thanks! – user1722960 Oct 06 '12 at 16:21
  • 1
    Simon gave you a great answer, it's rude not to accept it... and you should also give it a vote up. Be nice to other users and they will be nice in turn. – Hannish Mar 24 '13 at 00:01

2 Answers2

12

I can think of a couple of ways to do this.

One way is you could bind the ListBox.SelectedIndex of your 2 ListBoxes to change-notifying ViewModel properties.

For example in your View:

<ListBox SelectedIndex="{Binding SelectedIndexA}">
     <ListBoxItem Content="Item 1"/>
     <ListBoxItem Content="Item 2"/>
</ListBox>
<ListBox SelectedIndex="{Binding SelectedIndexB}">
     <ListBoxItem Content="Item 1"/>
     <ListBoxItem Content="Item 2"/>
</ListBox>

And in your ViewModel:

public int SelectedIndexA
{
    get { return _selectedIndexA; }
    set
    {
        _selectedIndexA = value;
        _selectedIndexB = -1;
        OnPropertyChanged("SelectedIndexB");
    }
}

public int SelectedIndexB
{
    get { return _selectedIndexB; }
    set
    {
        _selectedIndexB = value;
        _selectedIndexA = -1;
        OnPropertyChanged("SelectedIndexA");
    }
}

Another way would be with an attached property like 'GroupName' where you can group Selectors (ListBox inherits from Selector) to ensure only one Selector in the group has a selected item at any one time.

For example:

public static class SingleSelectionGroup
{
    public static readonly DependencyProperty GroupNameProperty =
        DependencyProperty.RegisterAttached("GroupName", typeof(string), typeof(SingleSelectionGroup),
                                            new UIPropertyMetadata(OnGroupNameChanged));

    public static string GetGroupname(Selector selector)
    {
        return (string) selector.GetValue(GroupNameProperty);
    }

    public static void SetGroupName(Selector selector, string value)
    {
        selector.SetValue(GroupNameProperty, value);
    }

    private static void OnGroupNameChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var selector = (Selector) dependencyObject;

        if (e.OldValue != null)
            selector.SelectionChanged -= SelectorOnSelectionChanged;
        if (e.NewValue != null)
            selector.SelectionChanged += SelectorOnSelectionChanged;
    }

    private static void SelectorOnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (e.AddedItems.Count == 0)
            return;

        var selector = (Selector) sender;
        var groupName = (string) selector.GetValue(GroupNameProperty);
        var groupSelectors = GetGroupSelectors(selector, groupName);

        foreach (var groupSelector in groupSelectors.Where(gs => !gs.Equals(sender)))
        {
            groupSelector.SelectedIndex = -1;
        }
    }

    private static IEnumerable<Selector> GetGroupSelectors(DependencyObject selector, string groupName)
    {
        var selectors = new Collection<Selector>();
        var parent = GetParent(selector);
        GetGroupSelectors(parent, selectors, groupName);
        return selectors;
    }

    private static DependencyObject GetParent(DependencyObject depObj)
    {
        var parent = VisualTreeHelper.GetParent(depObj);
        return parent == null ? depObj : GetParent(parent);
    }

    private static void GetGroupSelectors(DependencyObject parent, Collection<Selector> selectors, string groupName)
    {
        var childrenCount = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < childrenCount; i++)
        {
            var child = VisualTreeHelper.GetChild(parent, i);
            var selector = child as Selector;
            if (selector != null && (string) selector.GetValue(GroupNameProperty) == groupName)
                selectors.Add(selector);

            GetGroupSelectors(child, selectors, groupName);
        }
    }
}

And in your View:

<ListBox my:SingleSelectionGroup.GroupName="Group A">
    <ListBoxItem Content="Item 1 (Group A)"/>
    <ListBoxItem Content="Item 2 (Group A)"/>
</ListBox>
<ListBox my:SingleSelectionGroup.GroupName="Group A">
    <ListBoxItem Content="Item 1 (Group A)"/>
    <ListBoxItem Content="Item 2 (Group A)"/>
</ListBox>

<ListBox my:SingleSelectionGroup.GroupName="Group B">
    <ListBoxItem Content="Item 1 (Group B)"/>
    <ListBoxItem Content="Item 2 (Group B)"/>
</ListBox>
<ListBox my:SingleSelectionGroup.GroupName="Group B">
    <ListBoxItem Content="Item 1 (Group B)"/>
    <ListBoxItem Content="Item 2 (Group B)"/>
</ListBox>

If you have to click an item twice before it is highlighted you can use a quick workaround like this:

<Style TargetType="ListBoxItem">
    <Style.Triggers>
        <EventTrigger RoutedEvent="GotKeyboardFocus">
            <BeginStoryboard>
                <Storyboard>
                    <BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="(ListBoxItem.IsSelected)">
                        <DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="True" />
                    </BooleanAnimationUsingKeyFrames>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Style.Triggers>
</Style>
Simon Brydon
  • 985
  • 8
  • 12
  • great answer!!! I have made some correction look at the answer below. Still I have a small problem. If you can try to solve it then it will be very nice of you. If you become free then please take a look at my problem here : http://stackoverflow.com/questions/22209877/movefocus-from-one-listbox-to-another – Vishal Jul 31 '14 at 17:49
6

If anybody has used ListBox/ListView inside ItemsControl and has the following problem after using the above answer:

  1. When you click on any Item in listbox1 the item is selected.
  2. When you click on any item in listbox2 the selected item of listbox1 is unselected but no item in listbox2 is selected.
  3. When you click again on any item in listbox2 the item is selected.

Just add below xaml to the style of your ListBoxItem:

<Style  TargetType="ListBoxItem">
    <Style.Triggers>
        <Trigger Property="IsKeyboardFocusWithin" Value="True">
             <Setter Property="IsSelected" Value="True"></Setter>
        </Trigger>
    </Style.Triggers>
</Style>

Also, If anybody is getting the following error after using code in above answer:

GroupName is already registered by Selector

Please change the third parameter typeof(......) in dependency property declaration to Name of your class.

Vishal
  • 6,238
  • 10
  • 82
  • 158
  • Thanks for looking into that. I hadn't actually needed the attached property until today so spotted the issue with selected items not being highlighted. As a quick fix I used an EventTrigger instead of the above because regular triggers reset when the value is false, meaning selection is cleared when the item loses keyboard focus. This was unwanted in my situation. – Simon Brydon Aug 19 '14 at 22:13
  • @Simon Thanks for the new quick fix. – Vishal Aug 19 '14 at 22:15