Pages

Tuesday, March 22, 2011

Issue with RadioButtons and Binding for MVVM

Well, if you are working with WPF or silverlight, and in VS 2008, I think you would have definitely found this issue or will find it sooner.

Most of us when dealing with WPF applications must have been using MVVM pattern where you want to completely separate the presentation layer into a View Models. Well, it would be hard to create MVP or MVVM pattern yourself in other applications, but WPF has inbuilt support of MVVM with Command interfaces and Binding.  Binding is the concept which lets you to update the control whenever the underlying data object is modified and vice versa.

Most of the controls works great with Binding and hence can easily be used with MVVM pattern, but RadioButton has serious issue with it. In this post I will describe the problem with Radios and define some of the ways to solve it.




public class MainModel : INotifyPropertyChanged
    {

        private string _radio1 = "Radio1";
        public string Radio1
        {
            get { return this._radio1; }
            set
            {
                this._radio1 = value;
                this.OnPropertyChanged("Radio1");
            }
        }

        private bool _radio1IsCheck;
        public bool Radio1IsCheck
        {
            get { return this._radio1IsCheck; }
            set
            {
                this._radio1IsCheck = value;
                this.OnPropertyChanged("Radio1IsCheck");
                this.OnPropertyChanged("TextValue");
            }
        }

        private bool _radio2IsCheck;
        public bool Radio2IsCheck
        {
            get { return this._radio2IsCheck; }
            set
            {
                this._radio2IsCheck = value;
                this.OnPropertyChanged("Radio1IsCheck");
                this.OnPropertyChanged("TextValue");
            }
        }

        private string _radio2 = "Radio2";
        public string Radio2
        {
            get { return this._radio2; }
            set
            {
                this._radio2 = value;
                this.OnPropertyChanged("Radio2");
            }
        }

        public string TextValue
        {
            get
            {
                string selected = this.Radio1IsCheck ? this.Radio1 : this.Radio2;
                return string.Format("You have selected {0}", selected);
            }
        }
        #region INotifyPropertyChanged Members

        private void OnPropertyChanged(string propName)
        {
            if (this.PropertyChanged != null)
                this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion
    }

This represents the ViewModel. Now lets bind the elements in XAML.

<Window x:Class="TestRadioBug.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TestRadioBug"
    Title="Window1" Height="300" Width="300">
    <Window.DataContext>
        <local:MainModel />
    </Window.DataContext>
    <Grid >
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <RadioButton Content="{Binding Radio1}"
                     IsChecked="{Binding Radio1IsCheck}"
                     GroupName="grp"
                     Grid.Row="0"
                     Grid.Column="0"/>
        <RadioButton Content="{Binding Radio2}"
                     IsChecked="{Binding Radio2IsCheck}"
                     GroupName="grp"
                     Grid.Row="0"
                     Grid.Column="1"/>
        <TextBlock Text="{Binding TextValue}" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"/>

    </Grid>
</Window>

Now the above code displays two radiobuttons with a common group. The common group will automatically unselect one element when another is selected. Each of the RadioButton is bound to their individual properties, so that when one button is UnChecked the underlying property will be set to false.

Now there is a serious issue with this in WPF 3.5. Its a known bug in the system, which removes the Bindings altogether whenever RadioButton is used with common groups.

Just run this code in WPF 3.5 you will see the problem. The problem actually happens as the control is going to literally set the IsChecked property of all other RadioButtons in the same group when you click on one RadioButton. If you look the RadioButton class in Reflector, you will get the position where the RadioButton.IsCheck is set. And hence removes the existing Binding on the Control.


Inside the RadioButton class, the OnChecked event is handled check every other control which is on the same group. Thus you can see in the above code, the control Traverses the Visual Tree to find all other RadioButtons and Uncheck each of them individually.

This is the reason why Binding is getting removed from the RadioButton.

To solve this issue there are a number of options available. Lets name a few in this post:

1. Use it as Checkbox : 

Yes, This seems to be my first preference. When there is a problem with the existing code, it is better to create this of your own. Try not to set the GroupName for the RadioButton, this will ensure that UpdateRadioButtonGroup does not find any RadioButton on the same group to update, and you can eventually set all RadioButton IsChecked property yourself from Model.

private bool _radio1IsCheck;
public bool Radio1IsCheck
{
    get { return this._radio1IsCheck; }
    set
    {
         this.SetOtherRadioToFalse();
         this._radio1IsCheck = value;
         this.OnPropertyChanged("Radio1IsCheck");
         this.OnPropertyChanged("TextValue");
              
    }
}

2. Overriding OnChecked and OnToggle

Yes the second approach should be to create a new derived control from RadioButton and override these two methods. As I have already shown you that RadioButton resets the other radios on the group only from OnChecked and OnToggle methods, overriding it to your own will stop such behavior. In this way your button will not respond to groupnames.

public class GreatRadioButton : RadioButton
{
     protected override void OnChecked(RoutedEventArgs e)
     {
          //No code Required
     }

     protected override void OnToggle()
     {
          //No code Required
     }
}

3. Use RadioButton as List

Well, as a 3rd option, you can use your RadioButton inside a ListBox. A RadioButton List can be used to handle the ListBox. Yes, you can use binding to Bind IsSelected property of the ListBoxItem to the IsChecked property of the RadioButton, and it will work great.

<ControlTemplate TargetType="ListBoxItem"> 
    <Grid Margin="2"> 
         <Grid.ColumnDefinitions> 
           <ColumnDefinition Width="Auto" /> 
            <ColumnDefinition /> 
          </Grid.ColumnDefinitions> 
            <RadioButton IsChecked="{Binding IsSelected, 
                         RelativeSource={RelativeSource TemplatedParent}, 
                         Mode=TwoWay}" /> 
           <ContentPresenter Grid.Column="1" Margin="2,0,0,0" /> 
    </Grid> 
</ControlTemplate>

Check this implementation.

Well there are other options too like this or this, which you can also use if you like.


Finally,
I must conclude, the bug is fixed in VS 2010, so if you are using 2010, you should be less bothered with this bug. Thanks a ton to MS team for fixing this issue.

No comments:

Post a Comment

Please make sure that the question you ask is somehow related to the post you choose. Otherwise you post your general question in Forum section.