MVVM: привязка переключателей к модели просмотра?
EDIT: Проблема была исправлена в .NET 4.0.
Я пытаюсь связать группу переключателей с моделью просмотра с помощью кнопки IsChecked
. После просмотра других сообщений кажется, что свойство IsChecked
просто не работает. Я собрал короткую демоверсию, которая воспроизводит проблему, которую я включил ниже.
Вот мой вопрос: есть ли простой и надежный способ привязки переключателей с помощью MVVM? Спасибо.
Дополнительная информация: Свойство IsChecked
не работает по двум причинам:
-
Если выбрана кнопка, свойства IsChecked других кнопок в группе не будут установлены в false.
-
Когда выбрана кнопка, ее собственное свойство IsChecked не устанавливается после первого выбора кнопки. Я предполагаю, что привязка будет разбита WPF при первом щелчке.
Демо-проект: Вот код и разметка для простой демонстрации, которая воспроизводит проблему. Создайте проект WPF и замените разметку в Window1.xaml следующим образом:
<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. Появится окно с сообщением о том, что для свойства Button A IsChecked
установлено значение true. Теперь выберите Button B. Появится другое окно сообщения, в котором указано, что для свойства Button B IsChecked
установлено значение true, но нет окна сообщения, указывающего, что для свойства Button A IsChecked
установлено значение false - свойство hasn ' t изменено.
Теперь снова нажмите кнопку A. Кнопка будет выбрана в окне, но не появится окно сообщения - свойство IsChecked
не было изменено. Наконец, снова нажмите кнопку B - тот же результат. Свойство IsChecked
не обновляется вообще ни для одной из кнопок после первого нажатия кнопки.
Ответы
Ответ 1
Если вы начинаете с предложения Джейсона, тогда проблема становится единственным выделенным выбором из списка, который очень хорошо переводится в 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>
Ответ 2
Похоже, они зафиксировали привязку к свойству IsChecked
в .NET 4. Проект, который был нарушен в VS2008, работает в VS2010.
Ответ 3
В интересах любого, кто исследует этот вопрос в будущем, вот решение, которое я в конечном итоге выполнил. Он основывается на ответе Джона Боуэна, который я выбрал как лучшее решение проблемы.
Сначала я создал стиль для прозрачного списка, содержащего переключатели в качестве элементов. Затем я создал кнопки в списке - мои кнопки фиксированы, а не читаются в приложении в качестве данных, поэтому я жестко закодировал их в разметке.
Я использую перечисление под названием ListButtons
в модели представления для представления кнопок в списке, и я использую каждое свойство Tag
для передачи строкового значения значения перечисления, которое будет использоваться для этой кнопки. Свойство ListBox.SelectedValuePath
позволяет мне указать свойство Tag
в качестве источника для выбранного значения, которое я привязываю к модели представления с использованием свойства SelectedValue
. Я думал, что мне понадобится конвертер значений для преобразования между строкой и ее значением перечисления, но встроенные преобразователи 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
вызовет метод класса сервиса, который выполнит действие, требуемое при выборе кнопки.
И чтобы быть полным, вот базовый класс:
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
}
}
Надеюсь, что это поможет!
Ответ 4
Одним из решений является обновление 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 будет вызван при следующем нажатии кнопки A.
Ответ 5
Не уверен в каких-либо ошибках IsChecked, одном из возможных рефакторе, который вы можете внести в свою модель просмотра: представление имеет ряд взаимоисключающих состояний, представленных рядом с RadioButtons, только один из которых в любой момент времени может быть выбран. В модели представления просто есть 1 свойство (например, перечисление), которое представляет возможные состояния: stateA, stateB и т.д. Таким образом вам не понадобится вся отдельная ButtonAIsChecked и т.д.
Ответ 6
Вот еще один способ сделать это
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;
});
}
}
Ответ 7
Небольшое расширение для John Bowen answer: оно не работает, если значения не реализуются 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 просто "обертывает" элементы, предоставляя выбор.
Ответ 8
Вам нужно добавить название группы для кнопки "Радио"
<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>
Ответ 9
У меня очень похожая проблема в 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, а элементы в шаблоне - это радиообъекты, принадлежащие имени группы.
Если я добавлю новый элемент в коллекцию с выбранным свойством "Выбранный набор" в "Истина", появится флажок, а остальные кнопки будут отмечены.
Я решаю его, сначала беря проверочную кнопку и устанавливая ее в 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
`
Ответ 10
<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;
}
}
это сработало для меня. Даже значение получилось привязанным как к виду, так и к viewmodel в соответствии с щелчком переключателя. True → Мужской и False → Женский
Ответ 11
Я знаю, что это старый вопрос, и исходная проблема была решена в .NET 4. и, честно говоря, это немного не по теме.
В большинстве случаев, когда я хотел использовать RadioButtons в MVVM для выбора между элементами перечисления, для этого требуется привязать свойство bool в пространстве VM к каждой кнопке и использовать их для установки общего свойства перечисления, которое отражает фактический выбор, это очень утомительно очень быстро. Поэтому я придумал решение, которое можно использовать повторно и очень легко реализовать, и не требует 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. При просмотре со стороны он создает свойство bool для каждого элемента в перечислении с префиксом "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>
Надеюсь, что кто-то найдет это полезным, потому что я считаю, что это происходит в моем ящике инструментов.