views:

72

answers:

1

I have a ListBox with radiobutton on the horizontal line. Number of radiobuttons is optional. The text for each radiobutton is taken from a list of the model. Which radiobutton that is selected will be determined by the property SelectedOption. If none is select it shall be set to -1. The problem is that I wish that in addition to the choices that the model provides, I also want there to be a choice "Don’t know" that put SelectedOption to -1. How do I write the XAML for my ListBox to get this?

I would also like to "Don’t know" to have another background color and margin.

Model:

  • IEnumerable<String> Descriptions - Descriptive text for the options available, apart from "Don’t know"
  • Int SelectedOption - Index of selected Description. -1 If "Don’t know" is selected

Example:

---------------------------------------------------------
| () Option1 () Option2 () Option3        () Don’t know |
---------------------------------------------------------

() is a radiobutton
() Don’t know have another background color

+3  A: 

This was an interesting project that required a bit of hacking from time to time. But I managed it mostly with the help of multi-bindings and a couple value converters. This example covers every feature you requested, and has been encapsulated into a single Window for ease of demonstration. First, let's start with the XAML for the window, where most of the magic happens:

<Window x:Class="TestWpfApplication.BoundRadioButtonListBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:TestWpfApplication"
Title="BoundRadioButtonListBox" Height="200" Width="500"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
    <local:ItemContainerToIndexConverter x:Key="ItemContainerToIndexConverter"/>
    <local:IndexMatchToBoolConverter x:Key="IndexMatchToBoolConverter"/>
</Window.Resources>

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    <ListBox ItemsSource="{Binding Models}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <ItemsControl x:Name="DescriptionList" ItemsSource="{Binding Descriptions}">
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <RadioButton Content="{Binding}" Margin="5"
                                             Command="{Binding RelativeSource={RelativeSource FindAncestor,
                                             AncestorType={x:Type ItemsControl}}, Path=DataContext.CheckCommand}"
                                             CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path=Tag}"
                                             GroupName="{Binding RelativeSource={RelativeSource FindAncestor,
                                             AncestorType={x:Type ItemsControl}}, Path=DataContext.GroupName}">
                                    <RadioButton.Tag>
                                        <MultiBinding Converter="{StaticResource ItemContainerToIndexConverter}">
                                            <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}"
                                                     Mode="OneWay"/>
                                            <Binding RelativeSource="{RelativeSource Self}" 
                                                     Path="DataContext"/>
                                        </MultiBinding>
                                    </RadioButton.Tag>
                                    <RadioButton.IsChecked>
                                        <MultiBinding Converter="{StaticResource IndexMatchToBoolConverter}">
                                            <Binding RelativeSource="{RelativeSource Self}" 
                                                     Path="Tag"/>
                                            <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}"
                                                     Path="DataContext.SelectedOption"/>
                                        </MultiBinding>
                                    </RadioButton.IsChecked>
                                </RadioButton>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <StackPanel Orientation="Horizontal"/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                    </ItemsControl>
                    <Border Background="LightGray" Margin="15,5">
                        <RadioButton Content="Don't Know"
                                     Command="{Binding CheckCommand}"
                                     GroupName="{Binding GroupName}">
                            <RadioButton.CommandParameter>
                                <sys:Int32>-1</sys:Int32>
                            </RadioButton.CommandParameter>
                        </RadioButton>
                    </Border>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

    <StackPanel Grid.Row="1">
        <Label>The selected index for each line is shown here:</Label>
        <ItemsControl ItemsSource="{Binding Models}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Label Content="{Binding SelectedOption}"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>
</Grid>

The trick here is that the first ListBox is bound to the top-level models. Each model's ItemTemplate creates another embedded ItemsControl, which we use to display the item descriptions. That is how we can support a dynamic number of descriptions (this works for any number).

Next, let's check out the code-behind for this window:

/// <summary>
/// Interaction logic for BoundRadioButtonListBox.xaml
/// </summary>
public partial class BoundRadioButtonListBox : Window
{
    public ObservableCollection<LineModel> Models
    {
        get;
        private set;
    }

    public BoundRadioButtonListBox()
    {
        Models = new ObservableCollection<LineModel>();

        List<string> descriptions = new List<string>()
        {
            "Option 1", "Option 2", "Option 3"
        };

        LineModel model = new LineModel(descriptions, 2);
        Models.Add(model);

        descriptions = new List<string>()
        {
            "Option A", "Option B", "Option C", "Option D"
        };

        model = new LineModel(descriptions, 1);
        Models.Add(model);

        InitializeComponent();
    }
}

public class LineModel : DependencyObject
{
    public IEnumerable<String> Descriptions
    {
        get;
        private set;
    }

    public static readonly DependencyProperty SelectedOptionProperty =
        DependencyProperty.Register("SelectedOption", typeof(int), typeof(LineModel));

    public int SelectedOption
    {
        get { return (int)GetValue(SelectedOptionProperty); }
        set { SetValue(SelectedOptionProperty, value); }
    }

    public ICommand CheckCommand
    {
        get;
        private set;
    }

    public string GroupName
    {
        get;
        private set;
    }

    private static int Index = 1;

    public LineModel(IEnumerable<String> descriptions, int selected)
    {
        GroupName = String.Format("Group{0}", Index++);
        Descriptions = descriptions;
        SelectedOption = selected;
        CheckCommand = new RelayCommand((index) => SelectedOption = ((int)index));
    }
}

All of this should be very clear. The LineModel class represents the model you described in your question. Thus, it features a collection of string descriptions as well as a SelectedOption property, which has been made a DependencyProperty for automatic change notifications.

Next, the code for the two value converters:

public class ItemContainerToIndexConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length == 2 &&
            values[0] is ItemsControl &&
            values[1] is string)
        {
            ItemsControl control = values[0] as ItemsControl;
            ContentPresenter item = control.ItemContainerGenerator.ContainerFromItem(values[1]) as ContentPresenter;
            return control.ItemContainerGenerator.IndexFromContainer(item);
        }
        return -1;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return null;
    }
}

public class IndexMatchToBoolConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length == 2 && 
            values[0] is int && 
            values[1] is int)
        {
            return (int)values[0] == (int)values[1];
        }
        return false;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return null;
    }
}

The index-matching converter is extremely simple- it just compares two indices and returns true or false. The container to index converter is a bit more complex, and relies on a few ItemContainerGenerator methods.

Now, the finished result, 100% data-bound:

alt text

The radio-buttons are generated on the fly and checking each radio-button results in the SelectedOption property being updated on your model.

Charlie