您如何解决单选按钮组的绑定问题

时间:2020-05-19 11:58:49

标签: .net wpf vb.net data-binding

如果在控件中创建一个单选按钮组,在该控件中数据上下文会发生变化。当您将数据上下文从稍后定义的单选按钮为true的条目更改为false,但是将较早定义的单选按钮为true的条目更改时,原始项目的绑定值将更新为false。

您如何解决此问题? (虽然代码在VB中,但是它可以在任何.net风格下工作。我使用dotnet 4.5.2进行复制)

您可以在https://github.com/PhoenixStoneham/RadioButtonGroupBinding

的github上找到最小的问题解决方案

主窗口

    <Window x:Class="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"
        xmlns:local="clr-namespace:WPFRadioButtonGroupBinding"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <ListBox ItemsSource="{Binding Durations}" DisplayMemberPath="Name" SelectedItem="{Binding SelectedDuration}"/>
        <Grid Grid.Column="1" DataContext="{Binding SelectedDuration}">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Label Content="Name"/>
            <TextBlock Text="{Binding Name}" Grid.Column="1"/>
            <Label Content="Duration" Grid.Row="1"/>
            <TextBox Text="{Binding Frequency}" Grid.Row="1" Grid.Column="1"/>
            <RadioButton GroupName="DurationType" IsChecked="{Binding Hourly}" Grid.Row="2" Grid.Column="1" Content="Hours"/>
            <RadioButton GroupName="DurationType" IsChecked="{Binding Daily}" Grid.Row="3" Grid.Column="1" Content="Days"/>
            <RadioButton GroupName="DurationType" IsChecked="{Binding Weekly}" Grid.Row="4" Grid.Column="1" Content="Weeks"/>
            <RadioButton GroupName="DurationType" IsChecked="{Binding Monthly}" Grid.Row="5" Grid.Column="1" Content="Months"/>
        </Grid>
    </Grid>
</Window>

MainWindowViewModel

    Imports System.Collections.ObjectModel
Imports System.ComponentModel

Public Class MainWindowViewModel
    Implements INotifyPropertyChanged

    Public ReadOnly Property Durations As ObservableCollection(Of DurationViewModel)
    Public Sub New()
        Durations = New ObservableCollection(Of DurationViewModel)
        Durations.Add(New DurationViewModel("Daily", 1, False, True, False, False))
        Durations.Add(New DurationViewModel("Weekly", 1, False, False, True, False))
        Durations.Add(New DurationViewModel("Fortnightly", 1, False, False, True, False))
        Durations.Add(New DurationViewModel("Monthly", 1, False, False, False, True))
        Durations.Add(New DurationViewModel("1/2 yearly", 6, False, False, False, True))
        Durations.Add(New DurationViewModel("Other Days", 2, False, True, False, False))
        Durations.Add(New DurationViewModel("Take Over", 1, True, False, False, False))
        Durations.Add(New DurationViewModel("1/2 Day Takeover", 12, True, False, False, False))

    End Sub
    Private _SelectedDuration As DurationViewModel
    Public Property SelectedDuration As DurationViewModel
        Get
            Return _SelectedDuration
        End Get
        Set(value As DurationViewModel)
            _SelectedDuration = value
            DoPropertyChanged("SelectedDuration")
        End Set
    End Property

    Public Sub DoPropertyChanged(name As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))
    End Sub
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
End Class

DurationViewModel

Imports System.ComponentModel

Public Class DurationViewModel
    Implements INotifyPropertyChanged

    Private _Name As String
    Public Property Name As String
        Get
            Return _Name
        End Get
        Set(value As String)
            _Name = value
            DoPropertyChanged("Name")
        End Set
    End Property
    Private _Hourly As Boolean
    Public Property Hourly As Boolean
        Get
            Return _Hourly
        End Get
        Set(value As Boolean)
            _Hourly = value
            DoPropertyChanged("Hourly")
        End Set
    End Property

    Private _Daily As Boolean
    Public Property Daily As Boolean
        Get
            Return _Daily
        End Get
        Set(value As Boolean)
            _Daily = value
            DoPropertyChanged("Daily")
        End Set
    End Property
    Private _Weekly As Boolean
    Public Property Weekly As Boolean
        Get
            Return _Weekly
        End Get
        Set(value As Boolean)
            _Weekly = value
            DoPropertyChanged("Weekly")
        End Set
    End Property
    Private _Monthly As Boolean
    Public Property Monthly As Boolean
        Get
            Return _Monthly
        End Get
        Set(value As Boolean)
            _Monthly = value
            DoPropertyChanged("Monthly")
        End Set
    End Property

    Public Sub New(name As String, frequency As Integer, hourly As Boolean, daily As Boolean, weekly As Boolean, monthly As Boolean)
        Me.Name = name
        Me.Frequency = frequency
        Me.Hourly = hourly
        Me.Daily = daily
        Me.Weekly = weekly
        Me.Monthly = monthly
    End Sub
    Private _Frequency As Integer
    Public Property Frequency As Integer
        Get
            Return _Frequency
        End Get
        Set(value As Integer)
            _Frequency = value
            DoPropertyChanged("Frequency")
        End Set
    End Property
    Public Sub DoPropertyChanged(name As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))
    End Sub
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
End Class

2 个答案:

答案 0 :(得分:1)

您可能需要编写替换控件。我知道我在使用内置单选按钮时遇到了与绑定有关的问题,经过一些研究后得出的结论是,有些基本问题我只能用替换来解决。

我的xaml看起来像这样:

<UserControl x:Class="MyRadioButton"> <!-- remainder of declaration is pro forma -->
  <Grid x:Name="Grid">
    <RadioButton x:Name="Radio"
                 GroupName="{Binding Path=GroupName, Mode=OneWay, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type local:MyRadioButton}, AncestorLevel=1}}" />
  <!-- ToolTip omitted for brevity.  IsChecked is NOT a passthrough. -->
  </Grid>
</UserControl>

然后,在后面的代码中,GroupName,Content和ToolTip属性为形式。 IsChecked属性很有趣:

Public Shared ReadOnly IsCheckedProperty As DependencyProperty =
    DependencyProperty.Register("IsChecked", GetType(Boolean?), GetType(MyRadioButton), New FrameworkPopertyMetadata(False, FrameworkPropertyMetadataOptions.Journal Or FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, AddressOf IsCheckedChanged)

我有一个词典来跟踪正在变化的组,以便可以摆脱无限追尾的局面:

Private Shared _groupChanging As New Dictionary(Of String, Boolean)

IsCheckedChanged很重要:

Public Shared IsCheckedChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
    Dim instance = DirectCast(d, MyRadioButton)

    Try
        _groupChanging(instance.GroupName) = True
        instance.Radio.IsChecked = CBool(e.NewValue)
    Finally
        _groupChanging(instance.GroupName) = false
    End Try
End Sub

Loaded处理程序需要根据绑定在正确的方向上发送值:

Private Sub MyRadioButton_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs) Handles Me.Loaded
    If DependencyPropertyHelper.GetValueSource(Me, IsCheckedProperty) <> BaseValueSource.Default Then
        Try
            _groupChanging(GroupName) = True
            Radio.IsChecked = IsChecked
        Finally
            _groupChanging(GroupName) = False
        End Try
    Else
        SetCurrentValue(IsCheckedProperty, Radio.IsChecked)
    End If

    'Making content a pass-through binding didn't work for some reason
    Radio.Content = Content
End Sub

重新显示内容,我还必须覆盖OnInitialized并在那里设置内容。

最后,Radio.CheckedRadio.Unchecked的处理程序很简单;如果组没有变化,则会适当设置IsChecked

我没有尝试更改DataContext以供使用。您可能需要一些额外的东西来进行管理。另请参阅我在Loaded中为根据绑定状态推入或拉出值所做的操作。

答案 1 :(得分:1)

绑定到单选按钮组的最简单方法是为每个组定义一个枚举类型,然后使用值转换器进行绑定。

[对不起,我的代码示例在C#中,但是您应该能够轻松地将其转换为VB.Net。]

public enum MyEnum
{
    A,
    B,
    C
}

public class EnumToBoolConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return parameter != null && parameter.Equals(value);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return value != null && value.Equals(true) ? parameter : DependencyProperty.UnsetValue;
    }
} 

然后在ViewModel中,定义枚举类型的属性

public class MainViewModel: ViewModelBase
{
    private MyEnum _e;

    public MyEnum E
    {
        get => _e;
        set => Set(nameof(E), ref _e, value);
    }
}

并使用转换器绑定到视图中

<Window ...>
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>

    <Window.Resources>
        <local:EnumToBoolConverter x:Key="EnumConverter"/>
    </Window.Resources>

    <StackPanel>
        <RadioButton 
            Content="A"
            IsChecked="{Binding Path=E, Converter={StaticResource EnumConverter}, ConverterParameter={x:Static local:MyEnum.A}}" />

        <RadioButton 
            Content="B"
            IsChecked="{Binding Path=E, Converter={StaticResource EnumConverter}, ConverterParameter={x:Static local:MyEnum.B}}" />

        <RadioButton 
            Content="C"
            IsChecked="{Binding Path=E, Converter={StaticResource EnumConverter}, ConverterParameter={x:Static local:MyEnum.C}}" />

        <TextBlock Text="{Binding E}"/>
    </StackPanel>
</Window>