MVVM: Привязка переключателей к модели представления?

Править: Проблема была решена в.NET 4.0.

Я пытался связать группу переключателей к использованию модели представления IsChecked кнопка. После рассмотрения других сообщений кажется что IsChecked свойство просто не работает. Я соединил короткую демонстрацию, которая воспроизводит проблему, которую я включал ниже.

Вот мой вопрос: существует ли простой и надежный способ связать переключатели с помощью MVVM?Спасибо.

Дополнительная информация: IsChecked свойство не работает по двум причинам:

  1. Когда кнопка нажимается, свойства IsChecked других кнопок в группе не становятся установленными на ложь.

  2. Когда кнопка нажимается, ее собственное свойство 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 свойство не обновляется вообще ни для одной кнопки после того, как кнопка будет сначала нажата.

61
задан CZoellner 31 October 2016 в 12:50
поделиться

5 ответов

Если вы начнете с предложения Джейсона, тогда проблема станет выделением одной границы из списка, который очень хорошо трансформируется в 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>
53
ответ дан 24 November 2019 в 17:20
поделиться

Одним из решений является обновление 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.

3
ответ дан 24 November 2019 в 17:20
поделиться

Не уверен в каких-либо ошибках IsChecked. Один из возможных рефакторингов, который вы могли бы внести в свою модель представления: представление имеет ряд взаимоисключающих состояний, представленных серией RadioButton , только один из которых может быть выбран в любой момент времени. В модели представления просто имейте 1 свойство (например, перечисление), которое представляет возможные состояния: stateA, stateB и т. Д. Таким образом вам не понадобятся все отдельные ButtonAIsChecked и т. Д.

2
ответ дан 24 November 2019 в 17:20
поделиться

Для пользы тех, кто будет изучать этот вопрос в дальнейшем, вот решение, которое я в итоге реализовал. Оно основано на ответе Джона Боуэна, который я выбрал как лучшее решение проблемы.

Сначала я создал стиль для прозрачного поля списка, содержащего радиокнопки в качестве элементов. Затем я создал кнопки в поле списка - мои кнопки фиксированы, а не считываются в приложение как данные, поэтому я жестко закодировал их в разметке.

Я использую перечисление 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
    }
}

Надеюсь, это поможет!

10
ответ дан 24 November 2019 в 17:20
поделиться

Похоже, они исправили привязку к свойству IsChecked в .NET 4. Проект, который был нарушен в VS2008, работает в VS2010.

19
ответ дан 24 November 2019 в 17:20
поделиться
Другие вопросы по тегам:

Похожие вопросы: