Selecting Null Item in DropDown

10 posts, 0 answers
  1. Paul
    Paul avatar
    26 posts
    Member since:
    Aug 2015

    Posted 17 Aug Link to this post

    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?

  2. Paul
    Paul avatar
    26 posts
    Member since:
    Aug 2015

    Posted 18 Aug Link to this post

    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);
    }

  3. UI for WinForms is Visual Studio 2017 Ready
  4. Paul
    Paul avatar
    26 posts
    Member since:
    Aug 2015

    Posted 18 Aug Link to this post

    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;
            }
        }
    }

  5. Dess
    Admin
    Dess avatar
    1609 posts

    Posted 19 Aug Link to this post

    Hello Paul,

    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 typeof(Person), and the other will be typeof(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
    Check out the Windows Forms project converter, which aids the conversion process from standard Windows Forms applications written in C# or VB to Telerik UI for WinForms.For more information check out this blog post and share your thoughts.
  6. Paul
    Paul avatar
    26 posts
    Member since:
    Aug 2015

    Posted 22 Aug in reply to Dess Link to this post

    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.

  7. Dess
    Admin
    Dess avatar
    1609 posts

    Posted 22 Aug Link to this post

    Hello Paul, 

    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
    Check out the Windows Forms project converter, which aids the conversion process from standard Windows Forms applications written in C# or VB to Telerik UI for WinForms.For more information check out this blog post and share your thoughts.
  8. Paul
    Paul avatar
    26 posts
    Member since:
    Aug 2015

    Posted 22 Aug Link to this post

    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.

  9. Dess
    Admin
    Dess avatar
    1609 posts

    Posted 23 Aug Link to this post

    Hello Paul, 

    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
    Check out the Windows Forms project converter, which aids the conversion process from standard Windows Forms applications written in C# or VB to Telerik UI for WinForms.For more information check out this blog post and share your thoughts.
  10. Paul
    Paul avatar
    26 posts
    Member since:
    Aug 2015

    Posted 23 Aug Link to this post

    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?

  11. Dess
    Admin
    Dess avatar
    1609 posts

    Posted 24 Aug Link to this post

    Hello Paul, 

    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 typeof(Person) and thus you can simple data bind it to the Job.Person property directly. When you change the selection in RadMultiColumnComboBox, the Job.Person 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
    Check out the Windows Forms project converter, which aids the conversion process from standard Windows Forms applications written in C# or VB to Telerik UI for WinForms.For more information check out this blog post and share your thoughts.
Back to Top
UI for WinForms is Visual Studio 2017 Ready