The Goal:
I'm trying to create a ListBox bound to an ObservableCollection that features a CheckBox for each item, but only allows one item to be checked at a time.
The Problem:
I'm very close to having this working. The issue that I have is an infinite loop between the CheckBox control itself and the MainWindow code behind that I'm not sure how to resolve.
The XAML:
<ListBox ItemsSource="{Binding Path=p_fighterList}">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<CheckBox IsChecked="{Binding Path=p_selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Checked="NewFighterSelected" />
<TextBlock Text="{Binding Path=p_name}" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The C#:
public partial class MainWindow : Window
{
Controller controller = new Controller();
public MainWindow()
{
InitializeComponent();
DataContext = controller;
}
private void NewFighterSelected(object sender, RoutedEventArgs args)
{
controller.DeselectAll();
if (sender.GetType() == typeof(CheckBox))
{
CheckBox checkBox = (CheckBox)sender;
checkBox.IsChecked = true;
}
}
}
public class Fighter : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string name = "";
public string p_name
{
get { return name; }
set { name = value; }
}
private bool selected = false;
public bool p_selected
{
get { return selected; }
set
{
selected = value;
RaisePropertyChanged("p_selected");
}
}
public Fighter(string newName)
{
p_name = newName;
p_selected = false;
}
public void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
internal class Controller
{
private ObservableCollection<Fighter> fighterList = new ObservableCollection<Fighter>();
public ObservableCollection<Fighter> p_fighterList
{
get { return fighterList; }
set { fighterList = value; }
}
public Controller()
{
p_fighterList.Add(new Fighter("Scorpion"));
p_fighterList.Add(new Fighter("Sub Zero"));
p_fighterList.Add(new Fighter("Liu Kang"));
}
public void DeselectAll()
{
foreach (Fighter fighter in p_fighterList)
{
fighter.p_selected = false;
}
}
}
The bindings work great. When the user selects a second box, though, I want to deselect the first box. I don't see an easy way of being able to tell which box is the newly-checked one and which was the original check.
ATTEMPT 1 (shown above):
My first idea was to deselect all the boxes and then recheck the one box the user had just checked. The DeselectAll() method works perfectly. Checking one of the boxes from inside that event handler, however, causes the Checked event to refire, which recalls NewFighterSelected(), and so on.
ATTEMPT 2:
I tried keeping track of the index of the currently selected fighter in the controller. I thought I could deselect all the check boxes and reselect the newly-checked one from the controller by setting p_selected to true, and that it wouldn't trigger the event in the UI, but I was wrong. Same result.
ATTEMPT 3:
I thought I could set a OneTime binding on the checkbox so it wouldn't refire the event. Then I could manually recreate the binding in code. I changed the binding on the CheckBox to Mode=OneTime in the XAML and changed the event handler to this:
private void NewFighterSelected(object sender, RoutedEventArgs args)
{
controller.DeselectAll();
if (sender.GetType() == typeof(CheckBox))
{
CheckBox checkBox = (CheckBox)sender;
checkBox.IsChecked = true;
Binding binding = new Binding();
binding.Source = controller;
binding.Path = new PropertyPath("p_selected");
binding.Mode = BindingMode.OneTime;
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
BindingOperations.SetBinding(checkBox, CheckBox.IsCheckedProperty, binding);
}
}
The result of the above attempt was that the CheckBox was never rechecked after the DeselectAll(). I believe the reason for this is because the binding was removed, causing the view to not be updated. I think what I need is for the OneTime binding to be on the Checked property and not the IsChecked. However, I'm not sure how to set that up since I've already bound that control to a different data context.
IN CONCLUSION:
Ideally, I only want the Checked event to fire if the user themselves checks the box from the UI (and not if I do it from the code side). Is that possible? I'm open to other ideas as well. Is there another, better way to avoid the loop and subsequent stack overflow?
This was much easier than I made it out to be.
Instead of firing the event on Checked, we can use Click. The click event only fires on actual user input and not on code changes. It also works with keyboard input (navigating to the box with tab and checking with the space bar).
The one caveat is that we now have to check if the user checked or unchecked the box with that click, but it's only one extra line of code.