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