Список переключателей с привязкой к данным в WPF

StackOverflow https://stackoverflow.com/questions/805015

  •  03-07-2019
  •  | 
  •  

Вопрос

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

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

Это было полезно?

Решение

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

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

<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, тем больше мне кажется, что это делает тривиальное безумно сложным, а безумно трудное тривиальным.Наслаждайся.-Скотт

Другие советы

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

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

важный Имя_группы="Список радиостанций"

Я сделал это с помощью ValueConverter это преобразует enum к a bool.Передавая значение enum, которое ваш переключатель представляет как 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.

Супер простой, удобный для 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 resources, чтобы ограничить область применения DataTemplate).

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

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

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

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

Однако у меня возникла одна проблема, когда я попытался использовать элементы управления внутри 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, как и ожидалось.

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

Я черпал свое вдохновение из Запись в блоге Джона Бенсона, но изменил свое решение, чтобы использовать перечисления, которые имеют атрибут description.Таким образом, ключевыми частями решения стали:

Счетчик с описаниями

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>

Ваш список в 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");
      }
    }
  }
}

Я жульничал:

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

            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"];

    }

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top