MVVM: привязка переключателей к модели представления?
EDIT: проблема была исправлена в .NET 4.0.
я пытался привязать группу переключателей к модели представления с помощью . После просмотра других сообщений, похоже, что IsChecked
свойство просто не работает. Я собрал короткую демонстрацию, которая воспроизводит проблему, которую я включил ниже.
вот мой вопрос: есть ли простой и надежный способ привязки переключателей с помощью MVVM? Спасибо.
дополнительная информация: The IsChecked
собственность не работает по двум причинам:
когда кнопка выбрана, свойства IsChecked других кнопок в группе не получают значение ложные.
при выборе кнопки ее собственное свойство IsChecked не устанавливается после первого выбора кнопки. Я предполагаю, что привязка становится разбитой WPF на первый щелчок.
демо-проекта: вот код и разметка для простой пример, который воспроизводит проблему. Создайте проект WPF и замените разметку в Window1.код на следующий:
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300" Loaded="Window_Loaded">
<StackPanel>
<RadioButton Content="Button A" IsChecked="{Binding Path=ButtonAIsChecked, Mode=TwoWay}" />
<RadioButton Content="Button B" IsChecked="{Binding Path=ButtonBIsChecked, Mode=TwoWay}" />
</StackPanel>
</Window>
замените код в Window1.код XAML.cs со следующим кодом (взлом), который устанавливает модель представления:
using System.Windows;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = new Window1ViewModel();
}
}
}
теперь добавьте следующий код в проект как Window1ViewModel.cs
:
using System.Windows;
namespace WpfApplication1
{
public class Window1ViewModel
{
private bool p_ButtonAIsChecked;
/// <summary>
/// Summary
/// </summary>
public bool ButtonAIsChecked
{
get { return p_ButtonAIsChecked; }
set
{
p_ButtonAIsChecked = value;
MessageBox.Show(string.Format("Button A is checked: {0}", value));
}
}
private bool p_ButtonBIsChecked;
/// <summary>
/// Summary
/// </summary>
public bool ButtonBIsChecked
{
get { return p_ButtonBIsChecked; }
set
{
p_ButtonBIsChecked = value;
MessageBox.Show(string.Format("Button B is checked: {0}", value));
}
}
}
}
для воспроизведения проблема, запустите приложение и нажмите кнопку A. появится окно сообщения, в котором говорится, что кнопка a IsChecked
свойство было установлено в правда. Теперь выберите кнопку B. появится другое окно сообщения, в котором говорится, что кнопка B IsChecked
свойство было установлено в правда, но нет никакого окна сообщения, указывающего, что кнопка a IsChecked
свойство было установлено в ложные--свойство не было изменено.
Теперь нажмите кнопку A еще раз. Кнопка будет выбрано в окне, но окно сообщения не появится--the IsChecked
свойство не было изменено. Наконец, снова нажмите кнопку B-тот же результат. Элемент IsChecked
свойство не обновляется вообще для любой кнопки после первого нажатия кнопки.
11 ответов:
если вы начнете с предложения Джейсона, то проблема станет одним связанным выбором из списка, который очень хорошо переводится в
ListBox
. В этот момент тривиально применять стиль кListBox
управление так, что он отображается какRadioButton
список.<ListBox ItemsSource="{Binding ...}" SelectedItem="{Binding ...}"> <ListBox.ItemContainerStyle> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBoxItem}"> <RadioButton Content="{TemplateBinding Content}" IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsSelected}"/> </ControlTemplate> </Setter.Value> </Setter> </Style> </ListBox.ItemContainerStyle> </ListBox>
похоже, они исправили привязку к
IsChecked
свойство в .NET 4. Проект, который был сломан в VS2008 работает в VS2010.
для тех, кто исследует этот вопрос в будущем, вот решение, которое я в конечном итоге реализовал. Он основан на ответе Джона Боуэна, который я выбрал как лучшее решение проблемы.
во-первых, я создал стиль для прозрачного списка, содержащего переключатели в качестве элементов. Затем я создал кнопки для перехода в поле списка-мои кнопки фиксированы, а не считываются в приложение как данные, поэтому я жестко закодировал их в разметку.
я использую перечисление называется
ListButtons
в модели представления для представления кнопок в списке, и я использую каждую кнопкуTag
свойство для передачи строкового значения перечисления, которое будет использоваться для этой кнопки. ЭлементListBox.SelectedValuePath
свойство позволяет мне указатьTag
свойство в качестве источника для выбранного значения, которое я привязываю к модели представления с помощьюSelectedValue
собственность. Я думал, что мне понадобится конвертер значений для преобразования между строкой и ее значением enum, но встроенные конвертеры WPF обрабатывали преобразование без проблем.вот полная разметка для файл window1.xaml:
<Window x:Class="RadioButtonMvvmDemo.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300"> <!-- Resources --> <Window.Resources> <Style x:Key="RadioButtonList" TargetType="{x:Type ListBox}"> <Setter Property="Background" Value="Transparent"/> <Setter Property="ItemContainerStyle"> <Setter.Value> <Style TargetType="{x:Type ListBoxItem}" > <Setter Property="Margin" Value="5" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBoxItem}"> <Border BorderThickness="0" Background="Transparent"> <RadioButton Focusable="False" IsHitTestVisible="False" IsChecked="{TemplateBinding IsSelected}"> <ContentPresenter /> </RadioButton> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </Setter.Value> </Setter> <Setter Property="Control.Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBox}"> <Border BorderThickness="0" Padding="0" BorderBrush="Transparent" Background="Transparent" Name="Bd" SnapsToDevicePixels="True"> <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" /> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </Window.Resources> <!-- Layout --> <Grid> <!-- Note that we use SelectedValue, instead of SelectedItem. This allows us to specify the property to take the value from, using SelectedValuePath. --> <ListBox Style="{StaticResource RadioButtonList}" SelectedValuePath="Tag" SelectedValue="{Binding Path=SelectedButton}"> <ListBoxItem Tag="ButtonA">Button A</ListBoxItem> <ListBoxItem Tag="ButtonB">Button B</ListBoxItem> </ListBox> </Grid> </Window>
модель представления имеет одно свойство SelectedButton, которое использует перечисление ListButtons, чтобы показать, какая кнопка выбрана. Свойство вызывает событие в базовом классе, который я использую для моделей представления, что вызывает
PropertyChanged
событие:namespace RadioButtonMvvmDemo { public enum ListButtons {ButtonA, ButtonB} public class Window1ViewModel : ViewModelBase { private ListButtons p_SelectedButton; public Window1ViewModel() { SelectedButton = ListButtons.ButtonB; } /// <summary> /// The button selected by the user. /// </summary> public ListButtons SelectedButton { get { return p_SelectedButton; } set { p_SelectedButton = value; base.RaisePropertyChangedEvent("SelectedButton"); } } } }
в моем приложении производства
SelectedButton
setter вызовет метод класса обслуживания, который будет выполнять необходимые действия, когда a кнопка выбрана.и чтобы быть полным, вот базовый класс:
using System.ComponentModel; namespace RadioButtonMvvmDemo { public abstract class ViewModelBase : INotifyPropertyChanged { #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion #region Protected Methods /// <summary> /// Raises the PropertyChanged event. /// </summary> /// <param name="propertyName">The name of the changed property.</param> protected void RaisePropertyChangedEvent(string propertyName) { if (PropertyChanged != null) { PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName); PropertyChanged(this, e); } } #endregion } }
надеюсь, что это поможет!
одним из решений является обновление ViewModel для переключателей в сеттере свойств. Когда кнопка A установлена в True, установите кнопку B в false.
еще одним важным фактором при привязке к объекту в DataContext является то, что объект должен реализовать INotifyPropertyChanged. Когда любое связанное свойство изменяется, событие должно быть запущено и включать имя измененного свойства. (Нулевая проверка опущена в примере для краткость.)
public class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected bool _ButtonAChecked = true; public bool ButtonAChecked { get { return _ButtonAChecked; } set { _ButtonAChecked = value; PropertyChanged(this, new PropertyChangedEventArgs("ButtonAChecked")); if (value) ButtonBChecked = false; } } protected bool _ButtonBChecked; public bool ButtonBChecked { get { return _ButtonBChecked; } set { _ButtonBChecked = value; PropertyChanged(this, new PropertyChangedEventArgs("ButtonBChecked")); if (value) ButtonAChecked = false; } } }
Edit:
проблема в том, что при первом нажатии на кнопку B значение IsChecked изменяется и привязка проходит, но кнопка A не пропускает свое непроверенное состояние в свойство ButtonAChecked. При обновлении вручную в коде ButtonAChecked сеттер собственность будет вызван в следующий раз, когда кнопка нажата.
Не уверен в каких-либо исправленных ошибках, один возможный рефактор, который вы могли бы сделать для своей viewmodel:представление имеет ряд взаимоисключающих состояний, представленных серией радиокнопок, только одна из которых в любой момент времени может быть выбрана. В модели представления просто есть 1 свойство( например, перечисление), которое представляет возможные состояния: stateA, stateB и т. д. Таким образом, вам не понадобятся все отдельные кнопки и т. д.
вот еще один способ вы можете сделать это
VIEW:
<StackPanel Margin="90,328,965,389" Orientation="Horizontal"> <RadioButton Content="Mr" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/> <RadioButton Content="Mrs" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/> <RadioButton Content="Ms" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/> <RadioButton Content="Other" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}}" GroupName="Title"/> <TextBlock Text="{Binding SelectedTitle, Mode=TwoWay}"/> </StackPanel>
ViewModel:
private string selectedTitle; public string SelectedTitle { get { return selectedTitle; } set { SetProperty(ref selectedTitle, value); } } public RelayCommand TitleCommand { get { return new RelayCommand((p) => { selectedTitle = (string)p; }); } }
небольшое расширение к Джону Боуэну ответ: он не работает, когда значения не реализуют
ToString()
. Что вам нужно вместо установкиContent
из RadioButton к TemplateBinding, просто поставитьContentPresenter
в нем, как это:<ListBox ItemsSource="{Binding ...}" SelectedItem="{Binding ...}"> <ListBox.ItemContainerStyle> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBoxItem}"> <RadioButton IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsSelected}"> <ContentPresenter/> </RadioButton> </ControlTemplate> </Setter.Value> </Setter> </Style> </ListBox.ItemContainerStyle> </ListBox>
таким образом, вы можете дополнительно использовать
DisplayMemberPath
илиItemTemplate
по мере необходимости. RadioButton просто "обертывает" элементы, обеспечивая выбор.
вы должны добавить имя группы радиокнопок
<StackPanel> <RadioButton Content="Button A" IsChecked="{Binding Path=ButtonAIsChecked, Mode=TwoWay}" GroupName="groupName" /> <RadioButton Content="Button B" IsChecked="{Binding Path=ButtonBIsChecked, Mode=TwoWay}" GroupName="groupName" /> </StackPanel>
Я знаю, что это старый вопрос, и исходная проблема была решена в .NET 4. и, честно говоря, это немного не по теме.
в большинстве случаев, когда я хотел использовать радиокнопки в MVVM, это выбор между элементами перечисление, это требует привязки a буль свойство в пространстве виртуальной машины для каждой кнопки и с их помощью установить общий перечисление свойство, которое отражает реальный выбор, это становится очень утомительным очень быстро. Так Что Я придумал решение, которое можно использовать повторно и очень легко реализовать, и не требует ValueConverters.
вид в значительной степени то же самое, но как только у вас есть перечисление на месте сторона VM может быть выполнена с одним свойством.
MainWindowVM
using System.ComponentModel; namespace EnumSelectorTest { public class MainWindowVM : INotifyPropertyChanged { public EnumSelectorVM Selector { get; set; } private string _colorName; public string ColorName { get { return _colorName; } set { if (_colorName == value) return; _colorName = value; RaisePropertyChanged("ColorName"); } } public MainWindowVM() { Selector = new EnumSelectorVM ( typeof(MyColors), MyColors.Red, false, val => ColorName = "The color is " + ((MyColors)val).ToString() ); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void RaisePropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } }
класс, который выполняет всю работу наследует от DynamicObject. При взгляде снаружи он создает буль свойство для каждого элемента перечисление с префиксом "Is", "IsRed", "IsBlue" и т. д. это может быть связано с XAML. Вместе со свойством Value, которое содержит фактическое перечисление значение.
public enum MyColors { Red, Magenta, Green, Cyan, Blue, Yellow }
EnumSelectorVM
using System; using System.ComponentModel; using System.Dynamic; using System.Linq; namespace EnumSelectorTest { public class EnumSelectorVM : DynamicObject, INotifyPropertyChanged { //------------------------------------------------------------------------------------------------------------------------------------------ #region Fields private readonly Action<object> _action; private readonly Type _enumType; private readonly string[] _enumNames; private readonly bool _notifyAll; #endregion Fields //------------------------------------------------------------------------------------------------------------------------------------------ #region Properties private object _value; public object Value { get { return _value; } set { if (_value == value) return; _value = value; RaisePropertyChanged("Value"); _action?.Invoke(_value); } } #endregion Properties //------------------------------------------------------------------------------------------------------------------------------------------ #region Constructor public EnumSelectorVM(Type enumType, object initialValue, bool notifyAll = false, Action<object> action = null) { if (!enumType.IsEnum) throw new ArgumentException("enumType must be of Type: Enum"); _enumType = enumType; _enumNames = enumType.GetEnumNames(); _notifyAll = notifyAll; _action = action; //do last so notification fires and action is executed Value = initialValue; } #endregion Constructor //------------------------------------------------------------------------------------------------------------------------------------------ #region Methods //--------------------------------------------------------------------- #region Public Methods public override bool TryGetMember(GetMemberBinder binder, out object result) { string elementName; if (!TryGetEnumElemntName(binder.Name, out elementName)) { result = null; return false; } try { result = Value.Equals(Enum.Parse(_enumType, elementName)); } catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || ex is OverflowException) { result = null; return false; } return true; } public override bool TrySetMember(SetMemberBinder binder, object newValue) { if (!(newValue is bool)) return false; string elementName; if (!TryGetEnumElemntName(binder.Name, out elementName)) return false; try { if((bool) newValue) Value = Enum.Parse(_enumType, elementName); } catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || ex is OverflowException) { return false; } if (_notifyAll) foreach (var name in _enumNames) RaisePropertyChanged("Is" + name); else RaisePropertyChanged("Is" + elementName); return true; } #endregion Public Methods //--------------------------------------------------------------------- #region Private Methods private bool TryGetEnumElemntName(string bindingName, out string elementName) { elementName = ""; if (bindingName.IndexOf("Is", StringComparison.Ordinal) != 0) return false; var name = bindingName.Remove(0, 2); // remove first 2 chars "Is" if (!_enumNames.Contains(name)) return false; elementName = name; return true; } #endregion Private Methods #endregion Methods //------------------------------------------------------------------------------------------------------------------------------------------ #region Events public event PropertyChangedEventHandler PropertyChanged; protected virtual void RaisePropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } #endregion Events } }
чтобы ответить на изменения, вы можете подписаться на событие NotifyPropertyChanged или передать анонимный метод конструктору, как это было сделано выше.
и, наконец, файл MainWindow.xaml
<Window x:Class="EnumSelectorTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Grid> <StackPanel> <RadioButton IsChecked="{Binding Selector.IsRed}">Red</RadioButton> <RadioButton IsChecked="{Binding Selector.IsMagenta}">Magenta</RadioButton> <RadioButton IsChecked="{Binding Selector.IsBlue}">Blue</RadioButton> <RadioButton IsChecked="{Binding Selector.IsCyan}">Cyan</RadioButton> <RadioButton IsChecked="{Binding Selector.IsGreen}">Green</RadioButton> <RadioButton IsChecked="{Binding Selector.IsYellow}">Yellow</RadioButton> <TextBlock Text="{Binding ColorName}"/> </StackPanel> </Grid> </Window>
надеюсь, кто-то еще найдет это полезным, потому что я считаю эти идут в мой ящик с инструментами.
у меня очень похожая проблема в VS2015 и .NET 4.5.1
XAML:
<ListView.ItemsPanel> <ItemsPanelTemplate> <UniformGrid Columns="6" Rows="1"/> </ItemsPanelTemplate> </ListView.ItemsPanel> <ListView.ItemTemplate> <DataTemplate > <RadioButton GroupName="callGroup" Style="{StaticResource itemListViewToggle}" Click="calls_ItemClick" Margin="1" IsChecked="{Binding Path=Selected,Mode=TwoWay}" Unchecked="callGroup_Checked" Checked="callGroup_Checked">
....
Как вы можете видеть в этом коде у меня есть listview, а элементы в шаблоне-это радиокнопки, которые принадлежат имени группы.
Если я добавляю новый элемент в коллекцию с выбранным свойством, установленным в True, он отображается проверенным, а остальные кнопки остаются проверенными.
Я решаю это, получая сначала checkedbutton и устанавливая его в false вручную но это не так, как это должно быть сделано.
код:
`.... lstInCallList.ItemsSource = ContactCallList AddHandler ContactCallList.CollectionChanged, AddressOf collectionInCall_change ..... Public Sub collectionInCall_change(sender As Object, e As NotifyCollectionChangedEventArgs) 'Whenever collection change we must test if there is no selection and autoselect first. If e.Action = NotifyCollectionChangedAction.Add Then 'The solution is this, but this shouldn't be necessary 'Dim seleccionado As RadioButton = getCheckedRB(lstInCallList) 'If seleccionado IsNot Nothing Then ' seleccionado.IsChecked = False 'End If DirectCast(e.NewItems(0), PhoneCall).Selected = True ..... End sub
'
<RadioButton IsChecked="{Binding customer.isMaleFemale}">Male</RadioButton> <RadioButton IsChecked="{Binding customer.isMaleFemale,Converter= {StaticResource GenderConvertor}}">Female</RadioButton>
Ниже приведен код для IValueConverter
public class GenderConvertor : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return !(bool)value; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return !(bool)value; } }
это работает для меня. Даже значение было привязано как к view, так и к viewmodel в соответствии с щелчком переключателя. Правда-- > мужчина и ложь-- > женщина