Search code examples
wpfdata-bindingtelerikinotifypropertychanged

RadComboBox PropertyChanged not reflected in Text


I have xaml like so:

<controls:RadComboBoxEx ItemsSource="{Binding Projects}" 
                        SelectedItem="{Binding SelectedProject}" 
                        DisplayMemberPath="Title"/>

<TextBox Text="{Binding SelectedProject.Title}" />

RadComboBoxEx is a local class that extends RadComboBox from Telerik and overrides the default properties with company-wide defaults:

this.IsEditable = true;
this.IsReadOnly = false;
this.OpenDropDownOnFocus = true;
this.CanAutocompleteSelectItems = false;
this.StaysOpenOnEdit = true;
this.IsFilteringEnabled = true;
this.TextSearchMode = TextSearchMode.Contains;

... and does some additional event handling.

When my application starts up and Projects is populated, this works like a charm. The user can start typing in the box, see what they're typing, and get a filtered list of items to select. They select an item and the full title is displayed even if they'd only typed part of the title.

Now... the user selects Refresh (UI element and method not shown). The background code goes to the database and picks up a new Project Title. It keeps the same object, just updates the object properties that changed. The PropertyChanged events fire, the TextBox displays updates, the items in the combobox dropdown display the update, but the selected item text does not update.

First I tried adding the Text property and binding it to the SelectedProject.Title. Two way binding allowed the text to refresh, but caused all sorts of issues as the values of the objects were being unintentionally changed. You'd expect OneWay binding to work, but it did not take any updates from the DataContext unless UpdateSourceTrigger=LostFocus, which isn't really sufficient behavior.

I then discovered that if I set IsEditable="False", the selected item display would update without any Text property binding. But then the user couldn't see what they were typing when trying to filter the list. I considered changing TextSearchMode.Contains to TextSearchMode.StartsWith, but that's only going to be suitable for some of the combo boxes.

I tried forcing SelectedItem to TwoWay binding and raising an PropertyChanged event saying SelectedProject had changed even when it had not, and there was no change in behavior.

Obviously the TextBox that is displayed when IsEditable is true is not (by default, unless you stupidly bind two way to the Text property^) bound directly to the item selected, as you want user typing to connect to the filter, not the selected item. It does however seem to correctly fill in the full string when an item is selected, so there must be some way to trigger the event when the selection changes. (Despite my prior failed attempt to force this with a PropertyChanged event, it must be so.)

So I brute forced it in my refresh method.

Project temp = this.SelectedProject;
this.SelectedProject = null;
this.SelectedProject = temp;

This worked! So somewhere in the event chain inside the control, there was a no change -> no action break in the chain, and it never looked at the sub-properties changing for updating the Text property.

So I have a hack that works for this particular circumstance, but I have to do this in several places, and I'd rather not leave this trap behind for the next dev in our team. Is there anything I can do in our RadComboBoxEx to get the correct event flow going for this type of change?


Solution

  • Official solution to the issue

    This issue has been reported in the Telerik forums before. The official solution to get this to work is by binding the Text property of the ComboBox in TwoWay mode. You did almost the same, but you used the same property for binding the display member and the Text property. This will not work for your requirements, you have to create separate properties.

    private Project _selectedProject;
    private string _selectedProjectTitle;
    
    public Project SelectedProject
    {
       get => _selectedProject;
       set
       {
          if (_selectedProject != value)
          {
             _selectedProject = value;
             OnPropertyChanged();
          }
       }
    }
    
    public string SelectedProjectTitle
    {
       get => _selectedProjectTitle;
       set
       {
          if (_selectedProjectTitle!= value)
          {
             _selectedProjectTitle= value;
             OnPropertyChanged();
          }
       }
    }
    

    In your XAML code, bind the SelectedItem and the Text and set the DisplayMemberPath.

    <ComboBox ItemsSource="{Binding Projects}" 
              SelectedItem="{Binding SelectedProject}" 
              DisplayMemberPath="Title"
              Text="{Binding  SelectedProjectTitle, Mode=TwoWay}"
              .../>
    

    Whenever you fetch the new data from the database, and your selected item or one of its properties changes, assign the new Title to the SelectedProjectTitle property. This property is only used to make the updates possible, it does not change any data on your Project view models, so you will not run into the issue of unintentional changes.

    Root cause of the issue

    The root cause of this issue is the IsEditable property. Internally, there is an Update method like this, which behaves differently depending on the IsEditable state.

    private void Update()
    {
       if (this.IsEditable)
          this.UpdateEditableTextBox();
       else
          this.UpdateSelectionBoxItem();
    }
    

    The display member of the selected item is displayed by a TextBox. If the ComboBox is not editable, the text box displays the property defined by DisplayMemberPath on the SelectedItem. It is updated in the UpdateSelectionBoxItem method, but not in UpdateEditableTextBox.

    Consequently, if the ComboBox is editable, the DisplayMemberPath is disregarded on updates, so the text will stay the same, there will not be any updates from the properties. However, the official solution of course works, as the Text property is bound TwoWay which will trigger updates.

    There is also a reason, why resetting the SelectedItem works for updating the text. There are a few triggers that cause an update of the SelectedItem, like assigning another item or null. This will call a method, which will update the Text property explicitly and then call Update.

    Can I override the no change -> no event pattern in a child class of RadComboBox?

    All the update related methods are private or internal, but not virtual. You will not be able to override the behavior in a subclass, unless you start at dependency property changed handlers, which themselves contain lots of logic, so this apprach is doomed to fail sooner or later.

    I think the best way to go from here is to use the official solution, as it is documented and matches the behavior of WPF's own ComboBox. In the long run this will most likely save you a ton of maintenance and bug fixes. The key is that you communicate this intended behavior openly and document it for your own derived control, so that all your clients are aware of it.