Править: Проблема была решена в.NET 4.0.
Я пытался связать группу переключателей к использованию модели представления IsChecked
кнопка. После рассмотрения других сообщений кажется что IsChecked
свойство просто не работает. Я соединил короткую демонстрацию, которая воспроизводит проблему, которую я включал ниже.
Вот мой вопрос: существует ли простой и надежный способ связать переключатели с помощью MVVM?Спасибо.
Дополнительная информация: IsChecked
свойство не работает по двум причинам:
Когда кнопка нажимается, свойства IsChecked других кнопок в группе не становятся установленными на ложь.
Когда кнопка нажимается, ее собственное свойство 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 Кнопки IsChecked
свойство имело значение true. Теперь выберите Кнопку B. Другое окно сообщения появится, говоря тот B Кнопки IsChecked
свойство имело значение true, но нет никакого окна сообщения, указывающего на тот A Кнопки IsChecked
свойство имело значение false - свойство не было изменено.
Теперь нажмите кнопку снова. Кнопка будет нажата в окне, но никакое окно сообщения не появится - IsChecked
свойство не было изменено. Наконец, нажмите на Button B снова - тот же результат. IsChecked
свойство не обновляется вообще ни для одной кнопки после того, как кнопка будет сначала нажата.
Если вы начнете с предложения Джейсона, тогда проблема станет выделением одной границы из списка, который очень хорошо трансформируется в 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>
Одним из решений является обновление 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.
Не уверен в каких-либо ошибках IsChecked. Один из возможных рефакторингов, который вы могли бы внести в свою модель представления: представление имеет ряд взаимоисключающих состояний, представленных серией RadioButton , только один из которых может быть выбран в любой момент времени. В модели представления просто имейте 1 свойство (например, перечисление), которое представляет возможные состояния: stateA, stateB и т. Д. Таким образом вам не понадобятся все отдельные ButtonAIsChecked и т. Д.
Для пользы тех, кто будет изучать этот вопрос в дальнейшем, вот решение, которое я в итоге реализовал. Оно основано на ответе Джона Боуэна, который я выбрал как лучшее решение проблемы.
Сначала я создал стиль для прозрачного поля списка, содержащего радиокнопки в качестве элементов. Затем я создал кнопки в поле списка - мои кнопки фиксированы, а не считываются в приложение как данные, поэтому я жестко закодировал их в разметке.
Я использую перечисление 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
}
}
Надеюсь, это поможет!
Похоже, они исправили привязку к свойству IsChecked
в .NET 4. Проект, который был нарушен в VS2008, работает в VS2010.