I have a MultiColumnComboBox who's datasource is set to a BindingSource.DataSource. The BindingSource.DataSource is set to a BindingList<Person> of objects who's first item is Null. The DropDown displays correctly with the first row being blank. I then bind the MultiColumnComboBox's Value property to Job.Person property. The initial selected row for the MultiColumnComboBox is the Null record. When I select row 2 (Person: John Doe) when the current selection was the Null row, the Job.Person property is not updated. If I change the selection to a row 2 (Person: John Doe) and then row 3 (Person: Peter Paul), Job.Person property is updated.
This is very confusing to the user as it appears that they've updated the field but it hasn't updated the actual Job.Person property. And if the user needs to set Job.Person property back to Null by selecting the first row (the null row), it doesn't update the Job.Person property. Is there a proper way to be able to use a Null row in a MultiColumnComboBox?
9 Answers, 1 is accepted

I fixed the issue by inheriting from RadMultiColumnComboBox and overriding OnSelectedIndexChanged. If the SelectedIndex == 0 then I run:
foreach (Binding binding in this.DataBindings)
{
binding.DataSource.SetValue(binding.BindingMemberInfo.BindingField, null);
}
SetValue() is an extension that is just making reflection simplier
private static readonly Dictionary<
string
, PropertyInfo> PropertyInfos = new Dictionary<
string
, PropertyInfo>();
public static void SetValue(this object source, string propertyName, object value)
{
var type = source.GetType();
var key = $"{type.FullName}{propertyName}";
PropertyInfo oProp;
if (!PropertyInfos.TryGetValue(key, out oProp))
{
oProp = type.GetProperty(propertyName);
PropertyInfos.Add(key, oProp);
}
oProp.SetValue(source, value);
}

Found another small problem. Full Solution:
using System;
using System.Reflection;
using System.Windows.Forms;
namespace Telerik.WinControls.UI
{
public class RadNullableMultiColumnComboBox : RadMultiColumnComboBox
{
private int _prevIndex;
private object _prevSelected;
protected override void OnLostFocus(EventArgs e)
{
base.OnLostFocus(e);
if (this.SelectedValue == null)
this._prevIndex = -1;
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
base.OnSelectedIndexChanged(e);
var newSelected = this.SelectedIndex >= 0
? this.MultiColumnComboBoxElement.Rows[this.SelectedIndex].DataBoundItem
: null;
var e2 = e as CurrentRowChangedEventArgs;
if (e2 != null && e2.OldRow == null && newSelected == null)
{
var newIndex = this.SelectedIndex + (this.SelectedIndex - this._prevIndex);
if (newIndex <
0
|| this.MultiColumnComboBoxElement.Rows.Count <= newIndex) return;
this.SelectedIndex
=
newIndex
;
return;
}
this._prevSelected
=
newSelected
;
this._prevIndex
= this.SelectedIndex;
if (newSelected != null) return;
foreach (Binding binding in this.DataBindings)
{
binding.DataSource.SetValue(binding.BindingMemberInfo.BindingField, null);
}
}
protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
var
newSelected
=
this
.SelectedIndex >= 0
? this.MultiColumnComboBoxElement.Rows[this.SelectedIndex].DataBoundItem
: null;
if (this._prevSelected == null)
{
if (newSelected == null) return;
foreach (Binding binding in this.DataBindings)
{
binding.DataSource.SetValue(binding.BindingMemberInfo.BindingField, newSelected);
}
}
this._prevSelected = newSelected;
this._prevIndex = this.SelectedIndex;
}
}
}
Thank you for writing.
Note that RadMultiColumnComboBox.SelectedValue property depends on the set ValueMember. Hence, if you have a DataSource that represents a collection of Person objects, the Display/ValueMember properties are supposed to be set to some properties of the Person class, e.g. set the ValueMember property to "Id". If you have another object which contains a Person property, you can't bind it directly to the RadMultiColumnComboBox.SelectedValue property because one of the properties will be (Person), and the other will be (int). When the first record in the BindingList is null, the popup RadGridView will create a dummy row that is not attached to the grid actually and its index will be -1. It is not considered as a valid scenario. That is why the RadMultiColumnComboBox.SelectedIndexChanged event is not fired in this case.
The recommended solution is to create an empty Person instance instead of adding null:
Job job;
public
Form1()
{
InitializeComponent();
BindingSource bs =
new
BindingSource();
BindingList<Person> people =
new
BindingList<Person>();
people.Add(
new
Person());
for
(
int
i = 1; i < 5; i++)
{
people.Add(
new
Person(i,
"Person"
+ i));
}
bs.DataSource = people;
this
.radMultiColumnComboBox1.DataSource = bs.DataSource;
this
.radMultiColumnComboBox1.DisplayMember =
"Name"
;
this
.radMultiColumnComboBox1.ValueMember =
"Id"
;
job =
new
Job(people[0]);
this
.radMultiColumnComboBox1.SelectedIndexChanged += radMultiColumnComboBox1_SelectedIndexChanged;
}
private
void
radMultiColumnComboBox1_SelectedIndexChanged(
object
sender, EventArgs e)
{
if
(
this
.radMultiColumnComboBox1.SelectedIndex > -1)
{
job.Person = ((GridViewDataRowInfo)
this
.radMultiColumnComboBox1.SelectedItem).DataBoundItem
as
Person;
}
}
public
class
Job: System.ComponentModel.INotifyPropertyChanged
{
public
Job(Person person)
{
this
._person = person;
}
public
event
PropertyChangedEventHandler PropertyChanged;
protected
virtual
void
OnPropertyChanged(
string
propertyName)
{
if
(PropertyChanged !=
null
)
{
PropertyChanged(
this
,
new
PropertyChangedEventArgs(propertyName));
}
}
private
Person _person;
public
Person Person
{
get
{
return
this
._person;
}
set
{
this
._person = value;
OnPropertyChanged(
"Person"
);
}
}
}
public
class
Person : System.ComponentModel.INotifyPropertyChanged
{
public
Person()
{
}
public
Person(
int
mId,
string
mName)
{
this
.m_id = mId;
this
.m_name = mName;
}
public
event
PropertyChangedEventHandler PropertyChanged;
protected
virtual
void
OnPropertyChanged(
string
propertyName)
{
if
(PropertyChanged !=
null
)
{
PropertyChanged(
this
,
new
PropertyChangedEventArgs(propertyName));
}
}
int
? m_id;
string
m_name;
public
int
? Id
{
get
{
return
this
.m_id;
}
set
{
this
.m_id = value;
OnPropertyChanged(
"Id"
);
}
}
public
string
Name
{
get
{
return
this
.m_name;
}
set
{
this
.m_name = value;
OnPropertyChanged(
"Name"
);
}
}
public
override
string
ToString()
{
return
this
.m_id +
" "
+
this
.m_name;
}
}
private
void
radButton1_Click(
object
sender, EventArgs e)
{
if
(job.Person ==
null
)
{
this
.radLabel1.Text =
"null"
;
}
else
{
this
.radLabel1.Text = job.Person.ToString();
}
}
I hope this information helps. Should you have further questions I would be glad to help.
Regards,
Dess
Telerik by Progress

Dess,
Thank you for your response. Unfortunately, this is the original method I had used to accomplish this which led to many problems. I have the Display/ValueMember set correctly. But if I use an empty Person object as the first row, it will set the value of the Person property the bound Job object to that empty Person object. So when my Business Logic runs, it sees that the Job object has a Person instead of being null. The solution I posted is working great and I posted it for others in case the have the same scenario.
Thank you again for your feedback.
Thank you for writing back.
Although the Job.Person property is not set to null, you can distinguish if the Id property is 0 or some other default value for the empty Person. Null is not a valid Person instance. That is why the popup grid doesn't create an attached data row for this record. Hence, the selection is not changed in this case.
The recommended approach is to create an empty Person instance which will be used for the empty selection.
I hope this information helps. If you have any additional questions, please let me know.
Regards,
Dess
Telerik by Progress

I'm unsure why you keep pushing for your method. I'm using Entity Framework and it cannot distinguish that the Job.Person property should not insert a new Person record into SQL because the Id is 0. I understand why the popup grid doesn't create an attached data row. But the Job.Person property can not be set to a blank object. Entity Framework doesn't work this way.
As I said before, I originally started by using the method you are describing and it is not sufficient. My work around is effective and gets the job done. There is no reason why I should use the method your describing and have to build a bunch of logic elsewhere to try and detect if the property is an empty object and set it to null before Entity Framework saves the Job object and all its navigation properties back to SQL.
In my opinion, the control should allow the user to set a bound property to null. Forcing the grid to have to have an empty object is wrong. It creates limitations on the utilization.
Thank you for writing back.
Feel free to use this approach which suits your requirement best.
However, I will share with the community an alternative solution with a TypeConverter. Since the RadMultiColumnComboBox.ValueMember property is set to Id, we will need to convert the integer value to a valid Person. Thus, when the user hits the Delete key, the RadMultiColumnComboBox.SelectedValue property will be set to null and the Job.Person property will be set to null accordingly in case you use simple data binding:
Job job;
static
BindingList<Person> people =
new
BindingList<Person>();
public
Form1()
{
InitializeComponent();
for
(
int
i = 1; i < 5; i++)
{
people.Add(
new
Person(i,
"Person"
+ i));
}
this
.radMultiColumnComboBox1.DataSource = people;
this
.radMultiColumnComboBox1.DisplayMember =
"Name"
;
this
.radMultiColumnComboBox1.ValueMember =
"Id"
;
job =
new
Job(people[0]);
this
.radMultiColumnComboBox1.DataBindings.Add(
"SelectedValue"
, job,
"Person"
,
true
, DataSourceUpdateMode.OnPropertyChanged);
}
public
class
MyConverter : TypeConverter
{
public
override
bool
CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if
(sourceType ==
typeof
(
int
))
{
return
true
;
}
return
base
.CanConvertFrom(context, sourceType);
}
public
override
object
ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture,
object
value)
{
foreach
(Person p
in
people)
{
if
(p.Id == (
int
)value)
//find teh Person instance that has the same Id
{
return
p;
}
}
return
base
.ConvertFrom(context, culture, value);
}
}
public
class
Job: System.ComponentModel.INotifyPropertyChanged
{
public
Job(Person person)
{
this
._person = person;
}
public
event
PropertyChangedEventHandler PropertyChanged;
protected
virtual
void
OnPropertyChanged(
string
propertyName)
{
if
(PropertyChanged !=
null
)
{
PropertyChanged(
this
,
new
PropertyChangedEventArgs(propertyName));
}
}
private
Person _person;
[TypeConverter(
typeof
(MyConverter))]
public
Person Person
{
get
{
return
this
._person;
}
set
{
this
._person = value;
OnPropertyChanged(
"Person"
);
}
}
}
public
class
Person : System.ComponentModel.INotifyPropertyChanged
{
public
Person()
{
}
public
Person(
int
mId,
string
mName)
{
this
.m_id = mId;
this
.m_name = mName;
}
public
event
PropertyChangedEventHandler PropertyChanged;
protected
virtual
void
OnPropertyChanged(
string
propertyName)
{
if
(PropertyChanged !=
null
)
{
PropertyChanged(
this
,
new
PropertyChangedEventArgs(propertyName));
}
}
int
? m_id;
string
m_name;
public
int
? Id
{
get
{
return
this
.m_id;
}
set
{
this
.m_id = value;
OnPropertyChanged(
"Id"
);
}
}
public
string
Name
{
get
{
return
this
.m_name;
}
set
{
this
.m_name = value;
OnPropertyChanged(
"Name"
);
}
}
public
override
string
ToString()
{
return
this
.m_id +
" "
+
this
.m_name;
}
}
I hope this information helps. If you have any additional questions, please let me know.
Regards,
Dess
Telerik by Progress

This will work so long as I don't have use a new Person object in the MultiColumnComboBox's DataSource; as a new Entity Framework object's Id is 0 until it has been saved to the database. So if I created 10 new Person objects and then loaded the MultiColumnComboBox's DataSource, I'd have 10 Person objects with the same Id. So instead I gave Person a property to reference itself:
public class Person
{
public Person Self { get { return this; } }
public string Name { get; set; )
}
Then set:
this.radMultiColumnComboBox1.DisplayMember = "Name";
this.radMultiColumnComboBox1.ValueMember = "Self";
this.radMultiColumnComboBox1.DataBindings.Add("SelectedValue", job, "Person", true, DataSourceUpdateMode.OnPropertyChanged);
Can I use your type converter method with this configuration?
Thank you for writing back.
Using a TypeConverter for a specific property is a general programming approach for converting one value type to another and vice versa.
I have tested your approach and I think that you don't need a TypeConverter at all in this case. The RadMultiColumnComboBox.SelectedValue will be (Person) and thus you can simple data bind it to the Job.Person property directly. When you change the selection in RadMultiColumnComboBox, the Job. is updated successfully on my end.
I hope this information helps. If you have any additional questions, please let me know.
Regards,
Dess
Telerik by Progress