Phoenix Stoneham
Phoenix Stoneham

Reputation: 147

How do you work around the binding problems with radio button groups

If you create a radio button group inside a control where the data context changes. When you change the data context from an entry where the later defined radio button is true to one where it's false but an earlier defined one is true, the original item has its bound value updated to false.

How do you work around this issue? (While the code is in VB, it will work in any flavour of .net. I used dotnet 4.5.2 for reproduction)

You can find a minimal problem solution on github here https://github.com/PhoenixStoneham/RadioButtonGroupBinding

Main Window

    <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

Upvotes: 2

Views: 1116

Answers (2)

Peregrine
Peregrine

Reputation: 4546

The cleanest way to bind to a radio button group is to define an enum type for each group, and then use a value converter for the binding.

[Sorry my code samples are in C#, but you should be able to convert it to VB.Net easily enough.]

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

Then in the ViewModel, define a property of the enum type

public class MainViewModel: ViewModelBase
{
    private MyEnum _e;

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

and bind in the View using the converter

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

Upvotes: 3

Craig
Craig

Reputation: 2474

You may need to write a replacement control. I know that I ran into binding-related problems with the built-in radio button, and after doing some research, concluded that there were fundamental problems that I could only fix with a replacement.

My xaml looked something like this:

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

Then, in the code-behind, the GroupName, Content, and ToolTip properties are pro forma. The IsChecked property is interesting:

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

I have a dictionary to keep track of groups that are changing, so that I can break out of infinite tail-chasing:

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

IsCheckedChanged is important:

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

The Loaded handler needs to send the value in the right direction depending on the binding:

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

Re the content, I also had to override OnInitialized and set content there.

Finally, handlers for Radio.Checked and Radio.Unchecked are trivial; if the group isn't changing, they set IsChecked appropriately.

I wasn't trying to change the DataContext for my usage. You may need something extra to manage that. Also see what I did in Loaded to handle pushing or pulling the value depending on the binding state.

Upvotes: 1

Related Questions