Pergunta

Eu tenho uma lista de opções em um objeto de dados, e eu quero fazer o equivalente a uma lista de botão de rádio para permitir que o usuário selecione um e apenas um deles. Funcionalidade semelhante a uma caixa de combinação de ligação de dados, mas em formato de botão de rádio.

Silly me, eu pensei que este seria construído em, mas não. Como você faz isso?

Foi útil?

Solução

Basicamente, depois de analisar os resultados do Google, eu comecei com a informação de uma linha de discussão MSDN onde o Dr. WPF forneceu uma resposta, que fala sobre estilo de uma caixa de listagem para olhar direito. No entanto, quando a caixa de listagem está desativada, o fundo era uma cor irritante que eu não poderia se livrar de para a vida de mim, até que eu li o exemplo MSDN do ListBox ControlTemplate, que mostra o elemento Border segredo que estava chutando minha bunda fundo.

Assim, a resposta final aqui foi esse estilo:

<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>

que fornece um ControlTemplate para e estilos, a caixa de listagem e os itens. E ele se acostuma com isto:

<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}"/>

Quanto mais eu passar tempo com WPF, mais eu acho que faz o trivial insanamente difícil ea insanamente difícil trivial. Aproveitar. -Scott

Outras dicas

Bind a caixa de listagem para o ItemsSource de uma caixa de listagem com uma lista de objetos que têm uma propriedade Name (Esta mudança pode)

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

importante GroupName = "radioList"

Eu fiz isso através de um ValueConverter que converte um enum a um bool. Ao passar o valor de enumeração que o botão de opção representa como o ConverterParameter, o conversor retorna se este botão de rádio deve ser verificado ou não.

<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 é definido da seguinte forma:

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;
        }
    }

Note que eu não vincular à lista de enums para gerar os botões de rádio, eu fiz-los à mão. Se você queria para preencher a lista de botões de rádio através de uma ligação, eu acho que você vai precisar alterar a ligação a uma IsChecked que se liga a ambos o valor atual eo valor de enumeração do rádio, porque você não pode usar uma ligação em MultiBinding ConverterParameter.

Super Simples, MVVM amigáveis, aproveitando DataTemplates para tipos. 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>

Model View, etc:

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; }
}

Se você quebrar a sua opção em um tipo específico (ou provavelmente já é). Você pode apenas definir um DataTemplate para esse tipo, WPF irá usá-lo automaticamente. (Define DataTemplate em recursos de caixa de listagem de limitar o escopo de onde o DataTemplate irá ser aplicado).

Além disso, use o nome do grupo no DataTemplate para definir o grupo se quiser.

Este é muito mais simples do que mudar o modelo de controle, no entanto isso não significa que você começa uma linha azul em itens selecionados. (Mais uma vez, nada que um pouco de estilo não pode correção).

WPF é simples quando você sabe como.

Desculpe, eu gostaria de colocar esta resposta ao post de Scott S como um comentário em seu post, mas eu ainda não têm a reputação de fazer isso. Eu realmente gostei de sua resposta como era uma solução única de estilo e, portanto, não requer qualquer código-behind adicionado ou a criação de uma custom-controle, etc.

No entanto, eu tinha um problema quando Então eu fui tentar usar controles dentro dos ListBoxItems. Quando eu usar este estilo que eu sou incapaz de se concentrar qualquer dos controlos contidos devido a esta linha:

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

necessidades o botão de opção para desligar Focusable e IsHitTestVisible para o IsChecked vinculativa para trabalhar corretamente. Para contornar esta situação, eu mudei o IsChecked de um TemplateBinding a uma ligação regular, o que me permitiu fazer uma ligação de duas vias. Removendo as configurações ofender me deu esta linha:

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

O que agora me permite concentrar todos os controlos contidos no ListBoxItems como esperado.

Espero que isso ajude.

Eu levei a minha inspiração blog de Jon Benson entrada, mas modificou sua solução para usar enumerações que têm um atributo de descrição. Assim, as peças-chave da solução tornou-se:

Enumerator com descrições

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

Código para ler descrições e retornando pares de chave / valor para a ligação.

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();
    }
}

O seu fornecedor de dados do objeto em XAML

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

O seu ListBox em 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>

A propriedade (por exemplo, no seu modelo de vista) que você está ligando para

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

Eu enganei:

Minha solução foi ligar a caixa de listagem programaticly uma vez que é tudo o que parecia estar a trabalhar para mim:

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

Os olhares XAML como este:

                        <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>

E no meu caso para ler o valor do botão de rádio como ele está selecionado parece com isso:

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

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

    }

Espero que ajude alguém.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top