Question

I have an ObservableCollection in my Model, displayed in a ListBox in my View. Each ListBoxItem displays a radio button which should allow the user to choose one of the items. A property of the ViewModel should record this choice by holding a reference to the chosen item.

How can I set up a two-way binding to do this?

An IValueConverter should allow me to bind RadioButton.IsChecked to the VM property, with the actual item in which the radio button occurs passed to the converter either as a parameter or a value in an IMultiValueConverter. This way I can:

  1. Return true / false for Convert() based on comparison of the VM property and the item the radio button is associated with.
  2. Return the item if IsChecked==true and Binding.DoNothing otherwise for ConvertBack().

However, ValueConverter parameters cannot be bound to because they aren’t dependency properties (so I can't bind to the item to use as a ValueConverter parameter) and I cannot use the MultiConverter because although Convert() will receive both values of interest, ConvertBack() will only receive the value of IsChecked.

Notes

  1. The built in ListBox selection mechanism is already in use for other purposes.
  2. The collection of interest is nested in another collection and presented in a containing ListBox.
  3. The ListBox is bound to a collection in the Model. I am hoping not to implement events in the Model collections that tell the VM how to record events in the View, for obvious reasons.
Was it helpful?

Solution

I managed to solve this:

  1. Include a converter on the resources node of the ItemsPanel which wraps each ListBoxItem (or somewhere else within the repeated part of the tree), so there is one converter instance per item.
  2. Add a property (Host in my example) to the converter for the item each converter attaches to and initialise it somehow* as each ListBoxItem is created.
  3. Bind IsChecked to the ViewModel property you need to have populated using the converter.

*I tried the Host property as a DP with binding to the DataContext (which should be one member of the collection the ListBox is bound to at the point at which the converter is located), but I simply could not get this to work for my nested ListBoxes. I resorted to the Initialized event of the containing element and some code behind (which did allow Host to revert to an ordinary property; the DP was overkill but had been necessary for binding).

The DataTemplate:

<StackPanel Initialized="StackPanel_Initialized">
    <StackPanel.Resources>
        <!-- ExlcusionRadioConverter.Host is initialised in code behind -->
        <local:ExclusionRadioConverter x:Key="ExclusionRadio" />
    </StackPanel.Resources>
    <RadioButton
        GroupName="Exclusion"
        IsChecked="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}, Mode=FindAncestor}, Converter={StaticResource ExclusionRadio}, Path=DataContext.ExclusionCriterion}" />
    <Label Content="{Binding Description}" />
</StackPanel>

The Panel Initialized event (NB this answer originally had this code in the Loaded event handler, but this was causing some problems):

private void StackPanel_Initialized(object sender, EventArgs e)
{
    StackPanel panel = (StackPanel)sender;
    ExclusionRadioConverter converter = (ExclusionRadioConverter)panel.FindResource("ExclusionRadio");
    converter.Host = panel.DataContext as OptionListMember;
}

The Converter:

[ValueConversion(typeof(object), typeof(bool))]
public class ExclusionRadioConverter : IValueConverter
{
    public OptionListMember Host { get; set; }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return ReferenceEquals(value, Host);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return ((bool)value) ? Host : Binding.DoNothing;
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top