Список связанных радиосвязей в WPF

У меня есть список параметров в объекте данных, и я хочу сделать эквивалент списка переключателей, чтобы позволить пользователю выбрать один и только один из них. Функциональность похожа на поле со списком данных, но в формате радиокнопки.

Глупо мне, я думал, что это будет встроено, но нет. Как вы это делаете?

Ответы

Ответ 1

В принципе, после просмотра результатов Google я начал с информации из темы обсуждения MSDN, где Dr. WPF предоставил ответ, в котором говорится о стилизации ListBox, чтобы выглядеть правильно. Однако, когда список отключен, фон был раздражающим цветом, который я не мог избавиться от жизни, пока не прочитаю пример MSDN ListBox ControlTemplate, в котором показан секретный элемент Border, который пинал мой фоновой прикладом.

Итак, окончательным ответом здесь был этот стиль:

    <Style x:Key="RadioButtonList" TargetType="{x:Type ListBox}">
    <!-- ControlTemplate taken from MSDN http://msdn.microsoft.com/en-us/library/ms754242.aspx -->
    <Setter Property="SnapsToDevicePixels" Value="true"/>
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
    <Setter Property="MinWidth" Value="120"/>
    <Setter Property="MinHeight" Value="95"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListBox">
                <Border 
      Name="Border" 
      Background="Transparent"
      BorderBrush="Transparent"
      BorderThickness="0"
      CornerRadius="2">
                    <ScrollViewer 
        Margin="0"
        Focusable="false">
                        <StackPanel Margin="2" IsItemsHost="True" />
                    </ScrollViewer>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter TargetName="Border" Property="Background"
                Value="Transparent" />
                        <Setter TargetName="Border" Property="BorderBrush"
                Value="Transparent" />
                    </Trigger>
                    <Trigger Property="IsGrouping" Value="true">
                        <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="ItemContainerStyle">
        <Setter.Value>
            <Style TargetType="{x:Type ListBoxItem}" >
                <Setter Property="Margin" Value="2" />
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type ListBoxItem}">
                            <Border Name="theBorder" Background="Transparent">
                                <RadioButton Focusable="False"
                    IsHitTestVisible="False"
                    IsChecked="{TemplateBinding IsSelected}">
                                    <ContentPresenter />
                                </RadioButton>
                            </Border>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </Setter.Value>
    </Setter>
</Style>

который предоставляет ControlTemplate для и стили ListBox и элементы. И он используется следующим образом:

            <ListBox Grid.Column="1" Grid.Row="0" x:Name="TurnChargeBasedOnSelector" Background="Transparent"
            IsEnabled="{Binding Path=IsEditing}"
            Style="{StaticResource RadioButtonList}"
            ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:MainForm}}, Path=DataContext.RampTurnsBasedOnList}"
            DisplayMemberPath="Description" SelectedValuePath="RampTurnsBasedOnID"
            SelectedValue="{Binding Path=RampTurnsBasedOnID, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"

                                     />

Чем больше я провожу время с WPF, тем больше я думаю, что это делает тривиальным безумно сложным и безумно сложным тривиальным. Наслаждаться. -Скотт

Ответ 2

Привязать список к элементу ItemsSource ListBox со списком объектов, у которых есть свойство Name (это может измениться)

<ListBox Name="RadioButtonList">
   <ListBox.ItemTemplate >
        <DataTemplate >
             <RadioButton GroupName="radioList" Tag="{Binding}" Content="{Binding Name}"/>
         </DataTemplate>
                                                    </ListBox.ItemTemplate>
                                                </ListBox>

important GroupName = "radioList"

Ответ 3

Я сделал это через ValueConverter, который преобразует enum в bool. Передавая значение перечисления, которое ваша радиокнопка представляет как ConverterParameter, преобразователь возвращает, должен ли этот переключатель переключаться или нет.

<Window.Resources>
    <Converters:EnumConverter x:Key="EnumConverter" />
</Window.Resources>

<RadioButton IsChecked="{Binding Path=MyEnum, Mode=TwoWay, 
                                 Converter={StaticResource EnumConverter}, 
                                 ConverterParameter=Enum1}"}
             Content="Enum 1" />
<RadioButton IsChecked="{Binding Path=MyEnum, Mode=TwoWay, 
                                 Converter={StaticResource EnumConverter}, 
                                 ConverterParameter=Enum2}"}
             Content="Enum 2" />

EnumConverter определяется следующим образом:

public class EnumConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (targetType.IsAssignableFrom(typeof(Boolean)) && targetType.IsAssignableFrom(typeof(String)))
                throw new ArgumentException("EnumConverter can only convert to boolean or string.");
            if (targetType == typeof(String))
                return value.ToString();

            return String.Compare(value.ToString(), (String)parameter, StringComparison.InvariantCultureIgnoreCase) == 0;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (targetType.IsAssignableFrom(typeof(Boolean)) && targetType.IsAssignableFrom(typeof(String)))
                throw new ArgumentException("EnumConverter can only convert back value from a string or a boolean.");
            if (!targetType.IsEnum)
                throw new ArgumentException("EnumConverter can only convert value to an Enum Type.");

            if (value.GetType() == typeof(String))
            {
                return Enum.Parse(targetType, (String)value, true);
            }

            //We have a boolean, as for binding to a checkbox. we use parameter
            if ((Boolean)value)
                return Enum.Parse(targetType, (String)parameter, true);

            return null;
        }
    }

Обратите внимание, что я не привязываюсь к списку перечислений для создания переключателей, я сделал их вручную. Если вы хотите заполнить список переключателей через привязку, я думаю, вам нужно будет заменить привязку IsChecked на MultiBinding, которая связывается как с текущим значением, так и с номером радионутом, потому что вы не можете использовать привязка к ConverterParameter.

Ответ 4

Извините, я бы хотел поместить этот отзыв в сообщение Скотта О как комментарий к его сообщению, но у меня пока нет такой репутации. Мне очень понравился его ответ, поскольку это было решение только для стилей и, следовательно, не требовало добавления кода или создания настраиваемого элемента управления и т.д.

Однако у меня была одна проблема, когда я пошел попробовать использовать элементы управления внутри ListBoxItems. Когда я использую этот стиль, я не могу сфокусировать любой из содержащихся элементов управления из-за этой строки:

<RadioButton Focusable="False" IsHitTestVisible="False" IsChecked="{TemplateBinding IsSelected}">

Для правильной работы переключателя необходимо отключить функцию Focusable и IsHitTestVisible для привязки IsChecked. Чтобы обойти это, я изменил IsChecked из TemplateBinding на регулярную привязку, что позволило мне сделать двустороннюю привязку. Удаление устаревших настроек дало мне следующую строку:

<RadioButton IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsSelected, Mode=TwoWay}">

Что теперь позволяет мне сфокусировать любые элементы управления, содержащиеся в ListBoxItems, как и ожидалось.

Надеюсь, что это поможет.

Ответ 5

Super Simple, MVVM дружественный, используя DataTemplates для типов. XAML:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication1"
    Title="MainWindow" Height="350" Width="525">

<Window.Resources>
    <DataTemplate DataType="{x:Type local:Option}">
        <RadioButton Focusable="False"
                IsHitTestVisible="False"
                Content="{Binding Display}"
                IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListBoxItem}}">
        </RadioButton>
    </DataTemplate>
</Window.Resources>

<Grid>
    <ListBox ItemsSource="{Binding Options}" SelectedItem="{Binding SelectedOption}"/>
</Grid>

Показать модель и т.д.:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new Vm();
    }
}

public class Vm
{
    public Option[] Options { get { return new Option[] { 
        new Option() { Display = "A" }, 
        new Option() { Display = "B" }, 
        new Option() { Display = "C" } }; } }
    public Option SelectedOption { get; set; }
}

public class Option
{
    public string Display { get; set; }
}

Если вы привяжете свой вариант к определенному типу (или, скорее всего, это уже). Вы можете просто установить DataTemplate для этого типа, WPF автоматически его использует. (Определите DataTemplate в ресурсах ListBox, чтобы ограничить область применения DataTemplate).

Также используйте имя группы в DataTemplate для установки группы, если хотите.

Это намного проще, чем изменение шаблона управления, однако это означает, что вы получаете синюю линию для выбранных элементов. (Опять же, ничто не похоже на стилистику).

WPF прост, когда вы знаете, как это сделать.

Ответ 6

Я черпал вдохновение из записи в блоге Jon Benson, но модифицировал его решение, чтобы использовать перечисления с атрибутом описания. Таким образом, ключевые части решения стали:

Перечислитель с описаниями

public enum AgeRange {
  [Description("0 - 18 years")]
  Youth,
  [Description("18 - 65 years")]
  Adult,
  [Description("65+ years")]
  Senior,
}

Код для чтения описаний и возврата пар ключ/значение для привязки.

public static class EnumHelper
{
    public static string ToDescriptionString(this Enum val)
    {
        var attribute =
            (DescriptionAttribute)
            val.GetType().GetField(val.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false).
                SingleOrDefault();
        return attribute == default(DescriptionAttribute) ? val.ToString() : attribute.Description;
    }

    public static List<KeyValuePair<string,string>> GetEnumValueDescriptionPairs(Type enumType)
    {
        return Enum.GetValues(enumType)
            .Cast<Enum>()
            .Select(e => new KeyValuePair<string, string>(e.ToString(), e.ToDescriptionString()))
            .ToList();
    }
}

Поставщик данных объекта в XAML

<ObjectDataProvider
    ObjectType="{x:Type local:EnumHelper}"
    MethodName="GetEnumValueDescriptionPairs"
    x:Key="AgeRanges">
    <ObjectDataProvider.MethodParameters>
        <x:Type TypeName="local:AgeRange" />
    </ObjectDataProvider.MethodParameters>
</ObjectDataProvider>

Ваш ListBox в XAML

<ListBox 
    ItemsSource="{Binding Source={StaticResource AgeRanges}}"
    SelectedValue="{Binding SelectedAgeRange}"
    SelectedValuePath="Key">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <RadioButton 
                IsChecked="{Binding IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}"
                Content="{Binding Value}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Свойство (например, в вашей модели просмотра), которое вы связываете с

public class YourViewModel : INotifyPropertyChanged
{
  private AgeRange _selectedAgeRange;
  public AgeRange SelectedAgeRange
  {
    get { return _selectedAgeRange; }
    set 
    {
      if (value != _selectedAgeRange)
      {
        _selectedAgeRange = value;
        OnPropertyChanged("SelectedAgeRange");
      }
    }
  }
}

Ответ 7

Я обманул:

Мое решение состояло в том, чтобы программно связать блок списка, так как это все, что, казалось, сработало для меня:

            if (mUdData.Telephony.PhoneLst != null)
            {
                lbPhone.ItemsSource = mUdData.Telephony.PhoneLst;
                lbPhone.SelectedValuePath = "ID";
                lbPhone.SelectedValue = mUdData.Telephony.PrimaryFaxID;
            }

XAML выглядит следующим образом:

                        <ListBox.ItemTemplate >

                        <DataTemplate >
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition></RowDefinition>
                                </Grid.RowDefinitions>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto"></ColumnDefinition>
                                    <ColumnDefinition Width="Auto"></ColumnDefinition>
                                </Grid.ColumnDefinitions>

                                <RadioButton 
                                    IsChecked="{Binding Path=PrimaryPhoneID}" 
                                    GroupName="Phone" 
                                    x:Name="rbPhone"
                                    Content="{Binding Path=PrimaryPhoneID}"
                                    Checked="rbPhone_Checked"/>

                                <CheckBox Grid.Column="2" IsEnabled="False" IsChecked="{Binding Path=Active}" Content="{Binding Path=Number}" ></CheckBox>

                            </Grid>
                        </DataTemplate>
                    </ListBox.ItemTemplate>

И в моем случае, чтобы прочитать значение переключателя, как оно выбрано, выглядит так:

    private void rbPhone_Checked(object sender, RoutedEventArgs e)
    {
        DataRowView dvFromControl = null;
        dvFromControl = (DataRowView)((RadioButton)sender).DataContext;

        BindData.Telephony.PrimaryPhoneID = (int)dvFromControl["ID"];

    }

Надеюсь, что это поможет кому-то.