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>