Добавить дополнительную строку в ListBox с XAML - PullRequest
0 голосов
/ 14 апреля 2010

У меня есть ListBox с радиокнопкой на горизонтальной линии. Количество радиокнопок необязательно. Текст для каждой радиокнопки взят из списка модели. Какая выбранная радиокнопка будет определяться свойством SelectedOption. Если ничего не выбрано, должно быть установлено -1. Проблема состоит в том, что я хочу, чтобы в дополнение к вариантам, которые предоставляет модель, я также хотел, чтобы был выбор «Не знаю», который устанавливает SelectedOption в -1. Как мне написать XAML для моего ListBox, чтобы получить это?

Я также хотел бы, чтобы "Не знаю", чтобы был другой цвет фона и поля.

Модель:

  • IEnumerable<String> Descriptions - Описательный текст для доступных вариантов, кроме «Не знаю»
  • Int SelectedOption - указатель выбранного описания. -1 Если выбрано «Не знаю»

Пример:

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

() - это радиокнопка
() Don’t know есть другой цвет фона

1 Ответ

3 голосов
/ 15 апреля 2010

Это был интересный проект, который время от времени требовал некоторого взлома. Но мне это удалось в основном с помощью мульти-привязок и пары преобразователей значений. Этот пример охватывает все функции, которые вы запрашивали, и для простоты демонстрации был заключен в один 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 в вашей модели.

...