Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF Mutually Exclusive Listboxes

Tags:

binding

wpf

xaml

I have an app that has a ListBox of ListBoxes. I would like to make the InnerList boxes mutually exclusive. My ViewModel has a collection Foos which have a description, an IsSelected property and a collections Bars which have a name and IsSelected property.

public class MyViewModel : INotifyPropertyChanged
{
     public ObservableCollection<Foo> Foos { /* code removed for brevity */ }
}

public class Foo : INotifyPropertyChanged
{
     public string Description { /* code removed for brevity */ }
     public ObservableCollection<Bar> Bars { /* code removed for brevity */ }
     public bool IsSelected { /* code removed for brevity */ }
}

public class Bar : INotifyPropertyChanged
{
     public string Name { /* code removed for brevity */ }
     public bool IsSelected { /* code removed for brevity */ }
}

Below is a part of my MainWindow whose DataContext is set to MyViewModel. This ListBox's ItemsSource property is bound using ItemsSource={Binding Path=Foos} and in the template for this ListBox is an inner ListBox which is bound using ItemsSource="{Binding Path=Bars}". A, B, and C are Foos' Descriptions. The items contained in them are Bar's Names.

|--------------------------|
| A |--------------------| |
|   | Item 1             | |
|   | Item 2             | |
|   | Item 3             | |
|   |--------------------| |
|                          |
| B |--------------------| |
|   | Item X             | |
|   | Item Y             | |
|   | Item Z             | |
|   |--------------------| |
|                          |
| C |--------------------| |
|   | Item l             | |
|   | Item m             | |
|   |--------------------| |
|--------------------------|

I need to make it so a user can only select a single item out of any of the Bars. So, if the user selects Item 1 from Foo A then selects Item X from Foo B then Item 1 should be deselected.

I also need to bind the selected item to a TextBox control elsewhere on the window, but 1 thing a time I suppose.

Doing this in code and selection changed events is not an option. I'd prefer to keep this using XAML only.

Thanks in advance.

UPDATE
Following Moonshield's advice I've come up with this, but it still isn't completely working.

public class MyViewModel
{
     private Bar _selectedBar;

     public ObservableCollection<Foo> Foos { /* code removed for brevity */ }
     public Bar SelectedBar 
     { 
          get { return _selectedBar; }
          set 
          {
              _selectedBar = null;
              NotifyPropertyChanged("SelectedBar");

              _selectedBar = value;
              NotifyPropertyChanged("SelectedBar");
          }
     }    
}
<ListBox x:Name="lbFoos" ItemsSource="{Binding Path=Foos}" SelectedItem="{Binding Path=SelectedBar}">
    <ListBox.ItemContainerStyle>
        <Style TargetType="{x:Type ListBoxItem}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                        <StackPanel>
                            <TextBlock Text="Foo: " />
                            <TextBlock Text="{Binding Path=Description}" />
                            <ListBox ItemsSource="{Binding Path=Bars}" SelectedItem="{Binding Path=SelectedItem RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type ListBox}}}">
                                <ListBox.ItemContainerStyle>
                                    <Style TargetType="ListBoxItem">
                                        <Setter Property="Template">
                                            <Setter.Value>
                                                <ControlTemplate TargetType="{x:Type ListBoxItem}">
                                                    <TextBlock Text="{Binding Path=Name}" />
                                                </ControlTemplate>
                                            </Setter.Value>
                                        </Setter>
                                        <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=OneWayToSource}" />
                                    </Style>
                                </ListBox.ItemContainerStyle>
                            </ListBox>
                        </StackPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>
like image 965
Adam Avatar asked Nov 27 '10 18:11

Adam


2 Answers

The easiest way to do this is probably to add a SelectedBar property to your MyViewModel class and bind the SelectedItem property of the listboxes to that. This only allows one item to be selected at once, and provides you with something to bind your textbox to later.

You can then set a binding (OneWayToSource) on the IsSelected property of each ListBoxItem (via ItemContainerStyle perhaps) to update the IsSelected property of each bar. To update the IsSelected property of the Foo objects, set a binding to the listbox's SelectedItem with a valueconverter to check if it is null.

Edit:

SelectedItem Property (implementing Dan's fix):

protected Bar selectedItem;
public Bar SelectedItem{
    get
    {
        return selectedItem;
    }
    set
    {
        selectedItem = null;
        NotifyPropertyChanged("SelectedItem");

        selectedItem = value;
        NotifyPropertyChanged("SelectedItem");
    }

ListBoxItem with Binding (assuming ListBoxItem DataContext is Bar viewmodel):

<ListBoxItem IsSelected="{Binding Path=IsSelected, Mode=OneWayToSource}" />

Edit - fixes to your code:

I managed to get your code working. Two issues I found:

  1. The reason items weren't appearing to select was that you'd re-templated the ListBoxItems populated with Bar objects, so there was no highlight style when an item was selected - fixed this by setting the ItemTemplate instead, which templates the content of the item rather than overriding the whole template.

  2. Instead of binding the SelectedItem of one of the nested ListBoxes to the SelectedItem index of the parent and then binding that to the viewmodel, I changed the binding to bind directly to the viewmodel, which fixed the multiple-selection issue.

    <ListBox x:Name="lbFoos" ItemsSource="{Binding Path=Foos}"> <!--Removed SelectedItem binding.-->
        <ListBox.ItemContainerStyle>
            <Style TargetType="{x:Type ListBoxItem}">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type ListBoxItem}">
                            <StackPanel>
                                <TextBlock Text="Foo: " />
                                <TextBlock Text="{Binding Path=Description}" />
                                <ListBox ItemsSource="{Binding Path=Bars}" SelectionChanged="ListBox_SelectionChanged" SelectedItem="{Binding Path=DataContext.SelectedBar, RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type ListBox}}}"><!--Changed binding to bind directly to ViewModel-->
                                    <ListBox.ItemTemplate><!--Set ItemTemplated rather than ControlTemplate-->
                                        <DataTemplate>
                                            <TextBlock Text="{Binding Path=Name}" />
                                        </DataTemplate> 
                                    </ListBox.ItemTemplate>
                                    <ListBox.ItemContainerStyle>
                                        <Style TargetType="ListBoxItem">
                                            <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=OneWayToSource}" />
                                        </Style>
                                    </ListBox.ItemContainerStyle>
                                </ListBox>
                            </StackPanel>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </ListBox.ItemContainerStyle>
    </ListBox>
    
like image 134
Moonshield Avatar answered Oct 11 '22 08:10

Moonshield


If you only want one item to be selected at any time then having an IsSelected property is pointless. Instead you should have a container property which holds the currently selected item (as per Moonshield's suggestion). This model implies that only one can be selected, whereas your existing model implies that many can be selected. Ultimately, the individual instances of Foo probably don't need to know that they've been selected anyway.

like image 23
OJ. Avatar answered Oct 11 '22 08:10

OJ.