Это был интересный проект, который время от времени требовал некоторого взлома. Но мне это удалось в основном с помощью мульти-привязок и пары преобразователей значений. Этот пример охватывает все функции, которые вы запрашивали, и для простоты демонстрации был заключен в один Window
. Сначала давайте начнем с XAML для окна, где происходит большая часть магии:
<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>
Хитрость в том, что первый ListBox
привязан к моделям верхнего уровня. ItemTemplate
каждой модели создает еще один встроенный ItemsControl
, который мы используем для отображения описания элементов. Таким образом мы можем поддерживать динамическое количество описаний (это работает для любого числа).
Далее, давайте проверим код для этого окна:
/// <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));
}
}
Все это должно быть очень ясно. Класс LineModel
представляет модель, которую вы описали в своем вопросе. Таким образом, он содержит коллекцию описаний строк, а также свойство SelectedOption
, которое было сделано DependencyProperty
для уведомлений об автоматических изменениях.
Далее код для двух преобразователей:
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;
}
}
Преобразователь сопоставления индексов чрезвычайно прост - он просто сравнивает два индекса и возвращает true или false. Конвертер контейнера в индекс немного сложнее и использует несколько ItemContainerGenerator
методов.
Теперь готовый результат, на 100% привязанный к данным:
альтернативный текст http://img210.imageshack.us/img210/2156/boundradiobuttons.png
Переключатели создаются на лету, и проверка каждого переключателя приводит к обновлению свойства SelectedOption
в вашей модели.