开发者

WPF RadioButton Change Not Updating UI MVVM

开发者 https://www.devze.com 2023-02-06 01:15 出处:网络
I have two radio buttons working as radioButton List in UI using MVVM. When the user control is loaded first time, one of the radio button is selected and the related controls are shown in UI... Now w

I have two radio buttons working as radioButton List in UI using MVVM. When the user control is loaded first time, one of the radio button is selected and the related controls are shown in UI... Now when I change the radio button, UI is not getting updated.

Below is the sample XAML:

<Label Grid.Column="0" Grid.Row="3" Content="Exchange Details:" Margin="3" VerticalContentAlignment="Center" Style="{StaticResource NormalLabelStyle}"></Label>
 <Grid Grid.Column="1" Grid.Row="3" Width="200">
  <Grid.ColumnDefinitions>
   <ColumnDefinition Width="Auto"/>
   <ColumnDefinition Width="20"/>
  <ColumnDefinition Width="Auto"/>
 </Grid.ColumnDefinitions>
<RadioButton GroupName="rdoExchange" Content="Basic" IsChecked="{Binding Path=ExchangeDetailsBasic}"  Grid.Column="0" VerticalContentAlignment="Center" VerticalAlignment="Center"></RadioButton>
<RadioButton GroupName="rdoExchange" Content="Advanced" IsChecked="{Binding Path=ExchangeDetailsAdvanced}" Grid.Column="2" VerticalContentAlignment="Center" VerticalAlignment="Center"></RadioButton
 </Grid> 

 <Label Grid.Column="3" Grid.Row="0" Content="Number of Mailbox Profiles:" VerticalContentAlignment="Center" Style="{StaticResource NormalLabelStyle}" Visibility="{Binding Path=IsAdvanced}" ></Label>
 <telerik:RadNumericUpDown Grid.Column="4" Grid.Row="0" Margin="3" Value="{Binding Path=NumberofMailboxProfiles}" IsInteger="True" Minimum="1" Maximum="4"  HorizontalAlignment="Left" Visibility="{Binding Path=IsAdvanced}">< /telerik:RadNumericUpDown>

Below is my ViewModel code:

 private enum ExchangeDetails{
        Basic,
        Advanced
 }

 private bool isBasicMode = true;

 public bool ExchangeDetailsBasic {
         get {
            return this.isBasicMode;
        }

        set {
            if (value) {
                this.applicationSpecificRequirements[ExchangeDetailsKey] = ExchangeDetails.Basic.ToString();
                if (!this.isBasicMode) {
                    this.CheckBasicOrAdvancedSelecteAndDisplayView();
                }
            }
        }
    }

 public bool ExchangeDetailsAdvanced {
        get {
            return !this.isBasicMode;
        }

        set {
            if (value) {
                this.applicationSpecificRequirements[ExchangeDetailsKey] = ExchangeDetails.Advanced.ToString();
                this.CheckBasicOrAdvancedSelecteAndDisplayView();
            }
        }
    }

    public Visibility IsAdvanced { get; private set; }

    private void CheckBasicOrAdvancedSelecteAndDisplayView() {
        this.isBasicMode = this.applicationSpecificRequirements.ContainsKey(ExchangeDetailsKey) ? (this.applicationSpecificRequirements[ExchangeDetailsKey].Equals(ExchangeDetails.Basic.ToString()) ? true : false) : 开发者_StackOverflowtrue;
        this.IsAdvanced = this.isBasicMode ? Visibility.Collapsed : Visibility.Visible;
    }


Radio buttons, groups, and binding don't mix. This is, amazingly, by design.

There are three ways to change the value of a bound control in the UI. One is that the user can do it himself with a mouse click or keypress. The second is that code can change the value of the data source, and binding will update the value in the UI.

The third way is to set the value explicitly in code. If you do this, the binding on the control you've just set is disabled.

This is a little counter-intuitive. You'd expect the new value to get pushed to the data source. The design assumption is that if you wanted the value to get changed in the data source, you'd change it in the data source, and that your code is manipulating the UI because you don't want it to be bound anymore. This gives you a simple way of manually overriding binding - just set the value of the control in code - that doesn't compel you to find the Binding object and manipulate it explicitly. This makes a certain amount of sense. I guess.

But it creates problems with radio buttons. Because grouped radio buttons change each others' values in code. If you have three radio buttons in a group, and one gets checked, the radio button finds the other buttons in the group and unchecks them. You can see this if you look at the code in Reflector.

So what happens is exactly what you're observing: you click on radio buttons and binding gets disabled.

Here's what you do about it - and this actually makes a considerable amount of sense. Don't use groups. You can use radio buttons, but only for their visual style. Disregard their grouping functionality.

Instead, implement the logic that makes the bound boolean properties mutually exclusive in your view model, e.g.:

public bool Option1
{
   set
   {
      _Option1 = value;
      if (value)
      {
         Option2 = false;
         Option3 = false;
      }
      OnPropertyChanged("Option1");
   }
}

If you think about it, this logic really shouldn't be in the view anyway. Because it's logic, and that's what the view model is for. So while it's something of a pain, you can console yourself with the thought that architecturally it's the right thing to do.


I guess you are missing the implementation of INotifyPropertyChanged for the view model class. If you have used two way data binding and you are raising the property changed event when the selection changes everything should work fine. @Zamboni has explained it with the code example.


If you implement INotifyPropertyChanged in your view model and you set Binding Mode=TwoWay in your XAML, you can let the binding take care of the rest for you.

Here is sample using some of your code:

<Grid >
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <RadioButton GroupName="rdoExchange" Content="Basic" 
                 IsChecked="{Binding Path=ExchangeDetailsBasic, Mode=TwoWay}"  
                 Grid.Column="0" 
                 VerticalContentAlignment="Center" 
                 VerticalAlignment="Center"/>
    <RadioButton GroupName="rdoExchange" Content="Advanced" 
                 IsChecked="{Binding Path=ExchangeDetailsAdvanced, Mode=TwoWay}" 
                 Grid.Column="1" 
                 VerticalContentAlignment="Center" 
                 VerticalAlignment="Center"/>
    <Label Grid.Column="0" Grid.Row="1" Grid.RowSpan="2" 
           Content="Number of Mailbox Profiles:" 
           VerticalContentAlignment="Center" 
           Visibility="{Binding Path=IsAdvanced, Mode=TwoWay}" />
</Grid>

Here is the ViewModel:

public class MainViewModel : ViewModelBase
{
  public MainViewModel()
  {
  }

  private bool _isBasicMode = true;
  public bool ExchangeDetailsBasic
  {
    get
    {
      return this._isBasicMode;
    }

    set
    {
      this._isBasicMode = value;
      if (value)
      {
        ExchangeDetailsAdvanced = false;
        IsAdvanced = Visibility.Collapsed;
      }
      this.OnPropertyChanged("ExchangeDetailsBasic");
    }
  }

  private bool _isAdvancedMode = false;
  public bool ExchangeDetailsAdvanced
  {
    get
    {
      return this._isAdvancedMode;
    }

    set
    {
      _isAdvancedMode = value;
      if (value)
      {
        ExchangeDetailsBasic = false;
        IsAdvanced = Visibility.Visible;
      }
      this.OnPropertyChanged("ExchangeDetailsAdvanced");
    }
  }

  private Visibility _isAdvanced = Visibility.Collapsed;
  public Visibility IsAdvanced
  {
    get
    {
      return _isAdvanced;
    }
    set
    {
      _isAdvanced = value;
      this.OnPropertyChanged("IsAdvanced");
    }
  }
}

Here is the base class that implements INotifyPropertyChanged.

public abstract class ViewModelBase : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;

  protected void OnPropertyChanged(string propertyName)
  {
     PropertyChangedEventHandler handler = PropertyChanged;

     if (handler != null)
     {
        handler(this, new PropertyChangedEventArgs(propertyName));
     }
  }
}


Robert Rossney's answer is great, but I still think that radio buttons should behave like radio buttons and let the VM handle more important logic.

Here is my solution: an attached property that toggles the IsChecked property of all buttons in the same group. Works on my machine :-)

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;

namespace Elca.MvvmHelpers {

    public class RadioButtonHelper : DependencyObject {

        private static readonly Dictionary<string, List<RadioButton>> s_group2ButtonsMap = new Dictionary<string, List<RadioButton>>();
        private static readonly List<RadioButton> s_knownButtons = new List<RadioButton>(); 

        private static void OnRadioButtonChecked(object sender, RoutedEventArgs e) {
            RadioButton rb = (RadioButton)sender;
            UncheckOtherButtonsInGroup(rb);
        }

        public static bool? GetIsChecked(RadioButton d) {
            return (bool?) d.GetValue(IsCheckedProperty);
        }

        public static void SetIsChecked(RadioButton d, bool? value) {
            d.SetValue(IsCheckedProperty, value);
        }

        public static readonly DependencyProperty IsCheckedProperty =
            DependencyProperty.RegisterAttached("IsChecked", 
                                                typeof(bool?), 
                                                typeof(RadioButtonHelper),
                                                new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Journal |
                                                                                     FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                                                              IsCheckedChanged));

        public static void IsCheckedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {

            var rb = d as RadioButton;
            if (rb == null) {
                throw new Exception("IsChecked attached property only works on a FrameworkElement type");
            }

            RememberRadioButton(rb);

            if ((bool) e.NewValue) {

                rb.IsChecked = true; // this triggers OnRadioButtonChecked => other buttons in the same group will be unchecked
            }
        }

        private static void RememberRadioButton(RadioButton rb) {
            var groupName = GetGroupName(rb);

            // if this button is unknown, add it to the right list, based on its group name
            if (s_knownButtons.Contains(rb)) {
                return;
            }

            s_knownButtons.Add(rb);

            List<RadioButton> existingButtons;
            if (! s_group2ButtonsMap.TryGetValue(groupName, out existingButtons)) {
                // unknown group
                s_group2ButtonsMap[groupName] = new List<RadioButton> {rb};
                RegisterButtonEvents(rb);
            } else {
                if (! existingButtons.Contains(rb)) {
                    existingButtons.Add(rb);
                    RegisterButtonEvents(rb);
                }
            }
        }

        private static void RegisterButtonEvents(RadioButton rb) {
            rb.Unloaded += OnButtonUnloaded;
            rb.Checked += OnRadioButtonChecked;
        }

        private static void OnButtonUnloaded(object sender, RoutedEventArgs e) {
            RadioButton rb = (RadioButton) sender;

            ForgetRadioButton(rb);
        }

        private static void ForgetRadioButton(RadioButton rb) {

            List<RadioButton> existingButtons = s_group2ButtonsMap[GetGroupName(rb)];
            existingButtons.Remove(rb);
            s_knownButtons.Remove(rb);

            UnregisterButtonEvents(rb);
        }

        private static void UnregisterButtonEvents(RadioButton rb) {
            rb.Unloaded -= OnButtonUnloaded;
            rb.Checked -= OnRadioButtonChecked;
        }

        private static void UncheckOtherButtonsInGroup(RadioButton rb) {

            List<RadioButton> existingButtons = s_group2ButtonsMap[GetGroupName(rb)];

            foreach (RadioButton other in existingButtons) {

                if (other != rb) {
                    SetIsChecked(other, false);
                }
            }
            SetIsChecked(rb, true);

        }

        private static string GetGroupName(RadioButton elt) {
            string groupName = elt.GroupName;
            if (String.IsNullOrEmpty(groupName)) {
                groupName = "none"; // any value will do
            }
            return groupName;
        }
    }
}

In the view, for each button:

<RadioButton MvvmHelpers:RadioButtonHelper.IsChecked="{Binding IsExplicitFileSelected, Mode=TwoWay}">
...
</RadioButton>

The VM has a boolean property for each radio button. One must assign a value to each such property to start the listening process of the attached property.

All buttons without a group name are considered to be part of the same group.

0

精彩评论

暂无评论...
验证码 换一张
取 消