WPF MVVM Радио кнопки на ItemsControl
Я связал перечисления с переключателями раньше, и я обычно понимаю, как это работает. Я использовал альтернативную реализацию из этого вопроса: Как связать RadioButtons с перечислением?
Вместо перечислений, я хотел бы создать набор времени выполнения, определенный пользователем, и представить их как набор радиокнопок. Я получил представление, работающее против набора с включенным временем выполнения с ListView
, привязанным к свойствам ItemsSource
и SelectedItem
, поэтому мой ViewModel
подключен правильно. Теперь я пытаюсь переключиться с ListView
на ItemsControl
с помощью переключателей.
Здесь, насколько я понял:
<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
имеет ту же реализацию, что и EnumToBooleanConverter
из этого другого вопроса. Это кажется правильным, так как кажется, что он просто вызывает метод 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;
}
}
Проблема, которую я получаю сейчас, заключается в том, что я не могу понять, как отправить значение времени выполнения в качестве ConverterParameter
. Когда я пытаюсь (с кодом выше), я получаю эту ошибку:
"Связывание" не может быть установлено в свойстве "ConverterParameter" типа "Связывание" . "Связывание" может быть установлено только в DependencyProperty объекта DependencyObject.
Есть ли способ привязки к экземпляру элемента и передать его в IValueConverter
?
Ответы
Ответ 1
Оказывается, гораздо проще отказаться от использования ItemsControl
и вместо этого перейти с ListBox
.
Он может быть более тяжелым, но в основном потому, что он делает тяжелый подъем для вас. Очень просто сделать двустороннюю привязку между RadioButton.IsChecked
и ListBoxItem.IsSelected
. С помощью правильного шаблона управления для ListBoxItem
вы можете легко избавиться от всего визуального выделения.
<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>
Ответ 2
Насколько я знаю, нет хорошего способа сделать это с помощью MultiBinding
, хотя изначально вы думаете, что это будет. Поскольку вы не можете привязать ConverterParameter
, ваша реализация ConvertBack
не имеет необходимой информации.
То, что я сделал, создано отдельным классом EnumModel
исключительно для того, чтобы связать перечислимое число с переключателями. Используйте преобразователь в свойстве ItemsSource
, а затем вы привязываетесь к EnumModel
. EnumModel
- это просто объект пересылки, чтобы сделать возможной привязку. Он содержит одно возможное значение перечисления и ссылку на viewmodel, чтобы он мог преобразовать свойство в viewmodel в и из логического.
Здесь непроверенная, но общая версия:
<ItemsControl ItemsSource="{Binding Converter={StaticResource theConverter} ConverterParameter="SomeEnumProperty"}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton IsChecked="{Binding IsChecked}">
<TextBlock Text="{Binding Name}" />
</RadioButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Конвертер:
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();
}
}
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);
}
}
}
}
Для образца кода, который я знаю, работает (но он по-прежнему довольно неполирован - WIP!), вы можете видеть http://code.google.com/p/pdx/source/browse/trunk/PDX/PDX/Toolkit/EnumControl.xaml.cs. Это работает только в контексте моей библиотеки, но демонстрирует установку имени EnumModel на основе DescriptionAttribute
, который может быть вам полезен.
Ответ 3
Ты так близко. Если вам нужны две привязки для одного конвертера, вам нужны MultiBinding
и IMultiValueConverter
! Синтаксис немного более подробный, но не сложнее.
Edit:
Вот немного кода, чтобы вы начали.
Связывание:
<RadioButton Content="{Binding Name}"
Grid.Column="0">
<RadioButton.IsChecked>
<MultiBinding Converter="{StaticResource EqualsConverter}">
<Binding Path="SelectedItem"/>
<Binding Path="Name"/>
</MultiBinding>
</RadioButton.IsChecked>
</RadioButton>
и преобразователь:
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();
}
}
Второе редактирование:
Вышеупомянутый подход нецелесообразно реализовать двустороннюю привязку, используя технику, связанную с вопросом, потому что необходимая информация недоступна при обращении.
Правильное решение, на мой взгляд, является прямым MVVM: скопируйте модель представления в соответствии с потребностями представления. Объем кода довольно мал и устраняет необходимость в каких-либо конвертерах или смешных привязках или трюках.
Вот XAML;
<Grid>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton
GroupName="Value"
Content="{Binding Description}"
IsChecked="{Binding IsChecked, Mode=TwoWay}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
и код-код для моделирования модели представления:
DataContext = new CheckBoxValueCollection(new[] { "Foo", "Bar", "Baz" });
и некоторой инфраструктуры представлений:
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; }
}
}
Ответ 4
Теперь, когда я знаю об x: Shared (спасибо вашему другому вопросу), я отказываюсь от своего предыдущего ответа и говорю, что MultiBinding
- это способ пойти в конце концов.
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>
Модель просмотра:
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");
}
}
}
}
Конвертер:
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 };
}
}
}