【问题标题】:How do you work around the binding problems with radio button groups您如何解决单选按钮组的绑定问题
【发布时间】:2020-05-19 11:58:49
【问题描述】:

如果您在数据上下文更改的控件内创建单选按钮组。当您将数据上下文从稍后定义的单选按钮为真的条目更改为它为假但之前定义的单选按钮为真的条目时,原始项目的绑定值将更新为假。

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

你可以在这里https://github.com/PhoenixStoneham/RadioButtonGroupBindinghttps://github.com/PhoenixStoneham/RadioButtonGroupBindinggithub上找到一个最小的问题解决方案

主窗口

    <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>

主窗口视图模型

    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

持续时间视图模型

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

【问题讨论】:

  • 我怀疑你需要为它写一个替换控件;我知道这是我需要做的,以解决内置单选按钮控件中的绑定更新行为问题。我制作了一个包含单选按钮的新用户控件。不幸的是,我没有在我的代码中评论我遇到的确切问题,但它看起来主要是围绕加载控件时的行为(值被推送到应该被拉入的绑定点,或者反之亦然)。
  • 您是否使用了 GroupName 功能?
  • 是的,我使用的是 GroupName。我在用户控件上实现了 GroupName、Content、ToolTip 和 IsChecked。
  • 你会碰巧有那个用户控件的副本吗?如果我偷了它你介意吗?
  • 我不确定,但这个问题可能会有所帮助:github.com/dotnet/wpf/issues/2995

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


【解决方案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>

【讨论】:

  • 这适用于我放入 OP 中的情况吗?正如我注意到的那样,您的示例只有一个级别的绑定,而仅当您更改数据上下文时才会明确出现问题。单选按钮背后的组名功能似乎领先于数据上下文更新。因此,这意味着它在 C 上的 ischecked 设置为 false,因为 B 已经检查了此项,而 C 已检查了最后一项。
  • 是的,它可以正常工作,因为没有要绑定的布尔属性,只有一个枚举值,因此将设置与该枚举值对应的任何单选按钮。
  • 不用担心c#,我可以很好地阅读它,我们只是使用VB,因为它的学习曲线较低,因为它更像英语的语法。
【解决方案2】:

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

我的 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 中所做的,以根据绑定状态处理推送或拉取值。

【讨论】:

  • 我会看看这是怎么回事,如果它适用于我的场景,我会将其标记为答案
  • 我在转录 xaml 时有几个拼写错误,我想它们现在已经修复了。
  • @PhoenixStoneham 请注意刚才的更新,我无意中从 xaml 中省略了 AncestorLevel=1
猜你喜欢
  • 2011-10-17
  • 2015-03-31
  • 2014-06-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-17
  • 1970-01-01
相关资源
最近更新 更多