MVVM : 라디오 버튼을 뷰 모델에 바인딩합니까?
편집 : 문제는 .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를 클릭합니다. 버튼 A의 IsChecked속성이 true 로 설정되었다는 메시지 상자가 나타납니다 . 이제 Button B를 선택합니다. Button B의 IsChecked속성이 true 로 설정되었다는 또 다른 메시지 상자가 표시 되지만 Button A의 IsChecked속성이 false 로 설정 되었음을 나타내는 메시지 상자 가 없습니다. 속성이 변경되지 않았습니다.
이제 버튼 A를 다시 클릭하십시오. 창에서 버튼이 선택되지만 메시지 상자는 나타나지 IsChecked않으며 속성이 변경되지 않았습니다. 마지막으로 버튼 B를 다시 클릭하면 같은 결과가 나타납니다. IsChecked버튼을 먼저 클릭 한 후 속성 중 버튼 전혀 업데이트되지 않습니다.
Jason의 제안으로 시작하면 문제는 목록에서 단일 바운드 선택이되어 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에서 작동합니다.
이 질문을 연구하는 모든 사람을 위해 궁극적으로 구현 한 솔루션은 다음과 같습니다. 문제에 대한 최상의 솔루션으로 선택한 John Bowen의 답변을 기반으로합니다.
먼저 라디오 버튼을 항목으로 포함하는 투명한 목록 상자의 스타일을 만들었습니다. 그런 다음 목록 상자에 들어갈 버튼을 만들었습니다. 내 버튼은 데이터로 앱으로 읽어 들이지 않고 고정되어 있으므로 마크 업에 하드 코딩했습니다.
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>
뷰 모델에는 선택된 버튼을 표시하기 위해 ListButtons 열거 형을 사용하는 단일 속성 SelectedButton이 있습니다. 이 속성은 뷰 모델에 사용하는 기본 클래스의 이벤트를 호출하여 이벤트를 발생시킵니다 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");
}
}
}
}
내 프로덕션 앱에서 SelectedButtonsetter는 버튼이 선택 될 때 필요한 작업을 수행 할 서비스 클래스 메서드를 호출합니다.
완료하려면 다음은 기본 클래스입니다.
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
}
}
도움이 되었기를 바랍니다.
한 가지 해결책은 속성 setter에서 라디오 버튼에 대한 ViewModel을 업데이트하는 것입니다. Button A가 True로 설정되면 Button B를 false로 설정합니다.
DataContext의 개체에 바인딩 할 때 또 다른 중요한 요소는 개체가 INotifyPropertyChanged를 구현해야한다는 것입니다. 바인딩 된 속성이 변경되면 이벤트가 시작되고 변경된 속성의 이름이 포함되어야합니다. (간결성을 위해 샘플에서 Null 검사가 생략되었습니다.)
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;
}
}
}
편집하다:
문제는 버튼 B를 처음 클릭 할 때 IsChecked 값이 변경되고 바인딩이 전달되지만 버튼 A는 확인되지 않은 상태를 통해 ButtonAChecked 속성으로 전달되지 않는다는 것입니다. 코드에서 수동으로 업데이트하면 다음에 Button A를 클릭 할 때 ButtonAChecked 속성 설정자가 호출됩니다.
IsChecked 버그에 대해 확실하지 않은 경우 뷰 모델에 만들 수있는 한 가지 가능한 리팩터링 : 뷰에는 일련의 RadioButton으로 표시되는 상호 배타적 인 상태가 여러 개 있으며 지정된 시간에 하나만 선택할 수 있습니다. 뷰 모델에는 가능한 상태를 나타내는 1 개의 속성 (예 : 열거 형)이 있습니다. stateA, stateB 등 모든 개별 ButtonAIsChecked 등이 필요하지 않습니다.
할 수있는 또 다른 방법이 있습니다.
전망:
<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;
});
}
}
John Bowen의 답변에 대한 작은 확장 : 값이 구현되지 않으면 작동하지 않습니다 ToString(). ContentRadioButton을 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에서 RadioButtons를 사용하려는 대부분의 경우 enum의 요소 중에서 선택하는 것입니다 .이를 위해서는 VM 공간 의 bool 속성을 각 버튼에 바인딩하고 이를 사용 하여 실제 선택을 반영 하는 전체 enum 속성 을 설정 해야합니다. 이것은 매우 지루해집니다. 그래서 재사용이 가능하고 구현하기 쉽고 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에서 상속됩니다. 외부 에서 보면 XAML에서 바인딩 할 수있는 'Is', 'IsRed', 'IsBlue'등의 접두사가 붙은 열거 형의 각 요소에 대한 bool 속성을 만듭니다 . 실제 열거 형 값 을 보유하는 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>
다른 누군가가 유용하다고 생각하기를 바랍니다. 왜냐하면 나는 이것이 내 도구 상자에 있다고 생각하기 때문입니다.
I have a very similar problem in VS2015 and .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">
....
As you can see in this code i have a listview, and items in template are radiobuttons that belongs to a groupname.
If I add a new item to the collection with the property Selected set to True it appears checked and the rest of buttons remain checked.
I solve it by getting the checkedbutton first and set it to false manually but this is not the way it's supposed to be done.
code behind:
`....
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>
Below is the code for 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;
}
}
this worked for me. Even value got binded on both view and viewmodel according to the radio button click. True--> Male and False-->Female
참고URL : https://stackoverflow.com/questions/2284752/mvvm-binding-radio-buttons-to-a-view-model
'Program Tip' 카테고리의 다른 글
| hasNext ()가 False인데 hasNextLine ()이 True 인 이유는 무엇입니까? (0) | 2020.12.01 |
|---|---|
| C # 열거 형 : Nullable 또는 'Unknown'값? (0) | 2020.12.01 |
| Php 소멸자 (0) | 2020.12.01 |
| Go 객체의 포인터 값을 어떻게 인쇄합니까? (0) | 2020.12.01 |
| 부트 스트랩의 clearfix 클래스 이해 (0) | 2020.12.01 |