wpf mvvm radio-button ivalueconverter

WPF MVVM Radio botones en ItemsControl



radio-button ivalueconverter (4)

He enlazado las enumeraciones a los botones de opción antes, y generalmente entiendo cómo funciona. Utilicé la implementación alternativa de esta pregunta: ¿Cómo enlazar RadioButtons a una enumeración?

En lugar de enumeraciones, me gustaría generar un conjunto enumerado en tiempo de ejecución de un tipo personalizado y presentarlos como un conjunto de botones de opción. He obtenido una vista que funciona contra un conjunto enumerado en tiempo de ejecución con un ListView , ItemsSource a las propiedades ItemsSource y SelectedItem , por lo que mi ViewModel está conectado correctamente. Ahora estoy tratando de cambiar de un ListView a un ItemsControl con botones de radio.

Aquí está hasta donde he llegado:

<Window.Resources> <vm:InstanceToBooleanConverter x:Key="InstanceToBooleanConverter" /> </Window.Resources> <!-- ... --> <ItemsControl ItemsSource="{Binding ItemSelections}"> <ItemsControl.ItemTemplate> <DataTemplate DataType="{x:Type vm:ISomeType}"> <RadioButton Content="{Binding Name}" IsChecked="{Binding Path=SelectedItem, Converter={StaticResource InstanceToBooleanConverter}, ConverterParameter={Binding}}" Grid.Column="0" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>

InstanceToBooleanConverter tiene la misma implementación que EnumToBooleanConverter de esa otra pregunta. Esto parece correcto, ya que parece que simplemente invoca el método Equals :

public class InstanceToBooleanConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return value.Equals(parameter); } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return value.Equals(true) ? parameter : Binding.DoNothing; } }

El problema que tengo ahora es que no puedo averiguar cómo enviar un valor de tiempo de ejecución como el ConverterParameter . Cuando lo intento (con el código de arriba), recibo este error:

Un ''Enlace'' no se puede establecer en la propiedad ''ConverterParameter'' de tipo ''Enlace''. Un ''Enlace'' solo se puede establecer en una propiedad de dependencia de un objeto de dependencia.

¿Hay una manera de enlazar a la instancia del elemento y pasarlo al IValueConverter ?


Ahora que sé sobre x: Shared (gracias a su otra pregunta ), renuncio a mi respuesta anterior y digo que un MultiBinding es el camino a seguir después de todo.

El XAML:

<StackPanel> <TextBlock Text="{Binding SelectedChoice}" /> <ItemsControl ItemsSource="{Binding Choices}"> <ItemsControl.Resources> <local:MyConverter x:Key="myConverter" x:Shared="false" /> </ItemsControl.Resources> <ItemsControl.ItemTemplate> <DataTemplate> <RadioButton> <RadioButton.IsChecked> <MultiBinding Converter="{StaticResource myConverter}" > <Binding Path="DataContext.SelectedChoice" RelativeSource="{RelativeSource AncestorType=UserControl}" /> <Binding Path="DataContext" RelativeSource="{RelativeSource Mode=Self}" /> </MultiBinding> </RadioButton.IsChecked> <TextBlock Text="{Binding}" /> </RadioButton> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </StackPanel>

El modelo de visualización:

class Viewmodel : INPC { public Viewmodel() { Choices = new List<string>() { "one", "two", "three" }; SelectedChoice = Choices[0]; } public List<string> Choices { get; set; } string selectedChoice; public string SelectedChoice { get { return selectedChoice; } set { if (selectedChoice != value) { selectedChoice = value; OnPropertyChanged("SelectedChoice"); } } } }

El convertidor:

public class MyConverter : IMultiValueConverter { object selectedValue; object myValue; public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) { selectedValue = values[0]; myValue = values[1]; return selectedValue == myValue; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { if ((bool)value) { return new object[] { myValue, Binding.DoNothing }; } else { return new object[] { Binding.DoNothing, Binding.DoNothing }; } } }


Estás muy cerca. Cuando necesite dos enlaces para un convertidor, necesitará un MultiBinding y un IMultiValueConverter . La sintaxis es un poco más detallada pero no más difícil.

Editar:

Aquí hay un pequeño código para comenzar.

La Unión:

<RadioButton Content="{Binding Name}" Grid.Column="0"> <RadioButton.IsChecked> <MultiBinding Converter="{StaticResource EqualsConverter}"> <Binding Path="SelectedItem"/> <Binding Path="Name"/> </MultiBinding> </RadioButton.IsChecked> </RadioButton>

y el convertidor:

public class EqualsConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { return values[0].Equals(values[1]); } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }

Segunda Edición:

El enfoque anterior no es útil para implementar la vinculación bidireccional utilizando la técnica vinculada en la pregunta porque la información necesaria no está disponible al volver a convertir.

Creo que la solución correcta es MVVM directa: codifique el modelo de vista para que coincida con las necesidades de la vista. La cantidad de código es bastante pequeña y evita la necesidad de cualquier convertidor o enlaces o trucos divertidos.

Aquí está el XAML;

<Grid> <ItemsControl ItemsSource="{Binding}"> <ItemsControl.ItemTemplate> <DataTemplate> <RadioButton GroupName="Value" Content="{Binding Description}" IsChecked="{Binding IsChecked, Mode=TwoWay}"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Grid>

y código subyacente para simular el modelo de vista:

DataContext = new CheckBoxValueCollection(new[] { "Foo", "Bar", "Baz" });

y alguna infraestructura de modelo de vista:

public class CheckBoxValue : INotifyPropertyChanged { private string description; private bool isChecked; public string Description { get { return description; } set { description = value; OnPropertyChanged("Description"); } } public bool IsChecked { get { return isChecked; } set { isChecked = value; OnPropertyChanged("IsChecked"); } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } public class CheckBoxValueCollection : ObservableCollection<CheckBoxValue> { public CheckBoxValueCollection(IEnumerable<string> values) { foreach (var value in values) this.Add(new CheckBoxValue { Description = value }); this[0].IsChecked = true; } public string SelectedItem { get { return this.First(item => item.IsChecked).Description; } } }


Que yo sepa, no hay una buena manera de hacer esto con un MultiBinding , aunque inicialmente piensas que lo habrá. Como no puede enlazar el ConverterParameter , su implementación de ConvertBack no tiene la información que necesita.

Lo que he hecho es crear una clase EnumModel separada únicamente con el propósito de vincular una enumeración a los botones de radio. Use un convertidor en la propiedad ItemsSource y luego EnumModel a un EnumModel . El EnumModel es solo un objeto reenviador para hacer posible la vinculación. Contiene un valor posible de la enumeración y una referencia al modelo de vista para que pueda traducir una propiedad en el modelo de vista hacia y desde un valor booleano.

Aquí hay una versión no probada pero genérica:

<ItemsControl ItemsSource="{Binding Converter={StaticResource theConverter} ConverterParameter="SomeEnumProperty"}"> <ItemsControl.ItemTemplate> <DataTemplate> <RadioButton IsChecked="{Binding IsChecked}"> <TextBlock Text="{Binding Name}" /> </RadioButton> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>

El convertidor:

public class ToEnumModelsConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { var viewmodel = value; var prop = viewmodel.GetType().GetProperty(parameter as string); List<EnumModel> enumModels = new List<EnumModel>(); foreach(var enumValue in Enum.GetValues(prop.PropertyType)) { var enumModel = new EnumModel(enumValue, viewmodel, prop); enumModels.Add(enumModel); } return enumModels; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }

El EnumModel:

public class EnumModel : INPC { object enumValue; INotifyPropertyChanged viewmodel; PropertyInfo property; public EnumModel(object enumValue, object viewmodel, PropertyInfo property) { this.enumValue = enumValue; this.viewmodel = viewmodel as INotifyPropertyChanged; this.property = property; this.viewmodel.PropertyChanged += new PropertyChangedEventHandler(viewmodel_PropertyChanged); } void viewmodel_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == property.Name) { OnPropertyChanged("IsChecked"); } } public bool IsChecked { get { return property.GetValue(viewmodel, null).Equals(enumValue); } set { if (value) { property.SetValue(viewmodel, enumValue, null); } } } }

Para un ejemplo de código que sé que funciona (pero aún no está pulido - ¡WIP!), Puede ver http://code.google.com/p/pdx/source/browse/trunk/PDX/PDX/Toolkit/EnumControl.xaml.cs Esto solo funciona dentro del contexto de mi biblioteca, pero demuestra la configuración del Nombre de EnumModel basado en el Atributo de DescriptionAttribute , que puede ser útil para usted.


Resulta que es mucho más sencillo abandonar el uso de ItemsControl y, en cambio, ir con ListBox .

Puede ser más pesado, pero eso se debe principalmente a que está haciendo el trabajo pesado para usted. Es realmente fácil hacer un enlace bidireccional entre RadioButton.IsChecked y ListBoxItem.IsSelected . Con la plantilla de control adecuada para ListBoxItem , puede deshacerse fácilmente de toda la selección visual.

<ListBox ItemsSource="{Binding Properties}" SelectedItem="{Binding SelectedItem}"> <ListBox.ItemContainerStyle> <!-- Style to get rid of the selection visual --> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBoxItem}"> <ContentPresenter /> </ControlTemplate> </Setter.Value> </Setter> </Style> </ListBox.ItemContainerStyle> <ListBox.ItemTemplate> <DataTemplate DataType="{x:Type local:SomeClass}"> <RadioButton Content="{Binding Name}" GroupName="Properties"> <!-- Binding IsChecked to IsSelected requires no support code --> <RadioButton.IsChecked> <Binding Path="IsSelected" RelativeSource="{RelativeSource AncestorType=ListBoxItem}" Mode="TwoWay" /> </RadioButton.IsChecked> </RadioButton> </DataTemplate> </ListBox.ItemTemplate> </ListBox>