Подтвердить что ты не робот

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 не обновляется вообще ни для одной из кнопок после первого нажатия кнопки.

4b9b3361

Ответ 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>

Надеюсь, что кто-то найдет это полезным, потому что я считаю, что это происходит в моем ящике инструментов.