Binding DataSource of one ComboBox to SelectedValue of another - c#

I have two ComboBoxes envelopeList and datasetList. Consider the following line of code:
envelopeList.DataBindings.Add("DataSource", datasetList, "SelectedValue");
The intended functionality is to update envelopeList.DataSource to be datasetList.SelectedValue whenever the selection is changed. However, if datasetList is empty this throws an ArgumentException saying "Additional information: Complex DataBinding accepts as a data source either an IList or an IListSource."
I don't understand why this happens. When datasetList is empty datasetList.SelectedValue returns null and envelopeList.DataSource = null does not throw any exception. This doesn't throw any exceptions either: envelopeList.DataSource = datasetList.SelectedValue;, nor does this: envelopeList.DataSource = new BindingSource(datasetList, "SelectedValue");, even when datasetList is empty.
Doing the binding after datasetList has at least one item works as intended, until it becomes empty in which case envelopeList.DataSource isn't updated. The DataSourceChanged event isn't even fired. (Though in my case that noticed by the user since the DataSource will be emptied when the item in datasetList is deleted).
To make this work I have to execute the following code after datasetList has been populated for the first time:
if(doonce && !(doonce = false))
envelopeList.DataBindings.Add("DataSource", datasetList, "SelectedValue");
It's a very ugly way to do it and I would much rather be able to do this during initialization.
Some potentially important information.
Both ComboBoxes are actually my own inheriting type AdvancedComboBox. This is the relevant functionality within:
public class AdvancedComboBox : ComboBox, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected override void OnSelectedValueChanged(EventArgs e)
{
base.OnSelectedValueChanged(e);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("SelectedValue")); //I don't know why, but it works even if I remove this line.
}
}
(I have other uses for the PropertyChanged event, even if I apparently don't actually need it for the SelectedValue property.)
datasetList.DataSource is bound to an IBindingList containing DatasetPresenter objects.
DatasetPresenter has a property Areas that return an IBindingList with the objects that I want envelopeList to show.
I run datasetList.ValueMember = "Areas" before doing the binding.
The question
How do I make envelopeList.DataBindings.Add("DataSource", datasetList, "SelectedValue"); work even when datasetList is empty or achive a similar result?
I prefer solutions that I only have to execute during initialization of the ComboBoxes and/or code that I can put inside the AdvancedComboBox class so that it remains self-containing.
Bonus: Why doesn't it work when datasetList is empty? Even though datasetList.SelectedValue returns null and envelopeList.DataSource = null is okay.

Well, I found a solution, but it's not pretty.
I created this class specifically for this purpose:
private class PlaceHolder
{
public string Name { get; }//This property is the DisplayMember of both ComboBoxes.
public List<PlaceHolder> Areas { get { return new List<PlaceHolder>(0); } }//This property is the ValueMember of datasetList.
}
And during initialization I run this code.
datasetList.DataSource = new List<PlaceHolder>() { new PlaceHolder() };
envelopeList.DataBindings.Add("DataSource", datasetList, "SelectedValue");
datasetList.DataSource = myController.Datasets;//This is the intended data source.
It's ugly and I'm still looking for better solutions.

Related

Text Box values chabged but the binding see it as null [duplicate]

I am working on some data collection forms in WinForms/C#. When the form loads, I am looping through a configuration and adding a new Binding to each of the TextBox controls; mapping the Text property of each TextBox control to specific string property on my POCO object.
public void BindTextBoxControls(dynamic entity, List<TextBoxConfig> textBoxConfig)
{
foreach (var config in textBoxConfig)
config.Control.DataBindings.Add(new Binding("Text", entity, config.PropertyName));
}
Everything has been working as expected, new records properly saving new values entered into the corresponding TextBox controls, TextBoxes populating with the correct values when reopened a previously entered records with the form, and updates to values in TextBoxes of previously entered records are getting the updated values set on the underlying POCO.
However, I started to layer in some business rules onto the form specifically to gray out/disable and clear out previously entered values in the TextBox based on other user input/activity on the form - things are not working as expected.
In a contrived example; a rule like if a Checkbox_1 is checked then TextBox #5 should not be valued (clear out any previously entered value and disable it from input). On my Checkbox_1 event handler for CheckedChanged, I specifically check if the Checkbox_1 is checked and if so, set TextBox_1.Text == null and TextBox_1.Enabled = false. This works as expected and on the form, I see any previously entered value cleared from the TextBox_1 and it becomes enabled.
private void chkCheckBox1_CheckedChanged(object sender, EventArgs e)
{
if(!chkCheckBox1.Checked)
{
txtBox5.Text = string.Empty;
}
}
However, when I debug and break on the save and inspect the underlying POCO's property that the underlying control is bound to after the method is called; the old value still persists on the object's property which the text box is bound to, despite the textbox having not value appearing on the form. When I reopen the form for that record, the old cleared out value is re-populated in the disabled TextBox. However, manually clearing out the value in the same TextBox or updating a value and inspecting the object shows the updated value after those operations are performed.
It seems like changing the Text value of a TextBox control (e.g. the Text property of a TextBox) in code maybe somehow be "bypassing" the DataBinding? I'm actually seeing the same/similar behavior when applying similar rules to "uncheck" TextBoxes programmatically within event handler methods - the CheckBox controls are also using DataBinding to boolean properties on the POCO.
When you setup databinding by this overload: Binding(String, Object, String), then the value of DataSourceUpdateMode will be OnValidation, which means when you modify the value of control's property using code or through UI, the binding will push the new value to data source only after Validating event happens for the control.
To fix the problem, use either of the following options:
Use another overload and set the DataSourceUpdateMode to OnProperetyChanged
OR, after setting the Value of the TextBox.Text call ValidateChildren method of the form.
Example - Set the DataSourceUpdateMode to OnProperetyChanged
public class Person
{
public string Name { get; set; }
public string LegalCode { get; set; }
public bool IsRealPerson { get; set; }
}
Person person;
private void Form1_Load(object sender, EventArgs e)
{
person = new Person() {
Name = "My Company", LegalCode = "1234567890", IsRealPerson = false };
NameTextBox.DataBindings.Add(nameof(TextBox.Text), person,
nameof(Person.Name), true, DataSourceUpdateMode.OnPropertyChanged);
LegalCodeTextBox.DataBindings.Add(nameof(TextBox.Text), person,
nameof(Person.LegalCode), true, DataSourceUpdateMode.OnPropertyChanged);
IsRealPersonCheckBox.DataBindings.Add(nameof(CheckBox.Checked), person,
nameof(Person.IsRealPerson), true, DataSourceUpdateMode.OnPropertyChanged);
IsRealPersonCheckBox.CheckedChanged += (obj, args) =>
{
if (IsRealPersonCheckBox.Checked)
{
LegalCodeTextBox.Text = null;
LegalCodeTextBox.Enabled = false;
}
};
}
Note - You can put the logic inside the model
Another solution (Which needs more effort and more changes in your code) is implementing INotifyPropertyChanged in your model class. Then when PropertyChanged event raises for your boolean property, you can check if it's false then you can set the string property to null.
In this approach you don't need to handle UI events. Also right after updating the model property, the UI will be updated; in fact implementing INotifyPropertyChanged enables two-way databinding for your model class.

ComboBox has DataSource set but is not displaying any items

I have a windows form with a ComboBox DisplayBox. In my ViewModel I now have a Property BindingList<MyObject> ObjectBindingList that I want to bind to the DisplayBox.
When I load the form, the DisplayBox does not show any text.
The property DataSource is set and holds a List of MyObjects when checking in the debug modus after the data download.
The property items always has a count of zero.
My code works as following:
On startup I set the databindings in the form class to a still empty List ObjectBindingList.
displayBox.DataSource = ObjectBindingList;
The DisplayMember and ValueMember were set in the ComboBox Properties in the GUI Designer.
Asynchrously the controller downloads some data (MyDataObjects) async. Then sets the BindingList<MyObject> ObjectBindingList in the ViewModel to the downloaded Objects through adding them.
Since I don't see all of the relevant code, I can only assume what's happening.
Probably, you don't see the data in the ComboBox, because you are creating a new BindingList when loading the data. But the ComboBox is still attached to the old empty list.
You initialize the data source with an empty list like this:
// Property
BindingList<MyObject> ObjectBindingList { get; set; }
Somewhere else
// Initializes data source with an empty `BindingList<MyObject>`.
ObjectBindingList = new BindingList<MyObject>();
displayBox.DataSource = ObjectBindingList;
Later, you load the data and replace the list:
ObjectBindingList = LoadData();
Now, you have two lists: the initial empty list assigned to displayBox.DataSource and a new filled one assigned to the property ObjectBindingList. Note that displayBox.DataSource does not have a reference to the property itself, therefore it does not see the new value of the property.
For a BindingList<T> to work as intended, you must add the items with
var records = LoadData();
foreach (var data in records) {
ObjectBindingList.Add(data);
}
I.e., keep the original BindingList<MyObject> assigned to the data source.
See also: How can I improve performance of an AddRange method on a custom BindingList?
To avoid the problem, I would be advisasble to make the property read-only (using C# 9.0's Target-typed new expressions).
BindingList<MyObject> ObjectBindingList { get; } = new();
It seems like when trying to update the ComboBox from a different thread than the main forms thread, the update did not reach the control.
I am now using the Invoke Method together with a BindingSource Object in between the Binding List and the control.
private void SetBindingSourceDataSource( BindingList<MyObject> myBindingList)
{
if (InvokeRequired)
{
Invoke(new Action<BindingList<MyObject>>(SetBindingSourceDataSource), myBindingList);
}
else {
this.BindingSource.DataSource = myBindingList;
}
}
I am expeciall calling the above function on a PropertyChanged event, that I trigger at the end of every call of the download Function.

Why does my sort behavior fire with only one item in the list, and only the first time the collection is added to?

This question is a result of the fix to this problem. After getting the sort behavior to properly sort the ObservableCollection, I now notice that the first time the collection is added to, the CustomSorter handler fires with only the first item in it, and that same item is passed in as both parameters to the method. That is producing a duplicate item in the list.
Here are the pertinent parts of the view model code:
public ObservableCollection<PermissionItemViewModel> PermissionItems { get; private set; }
private void FetchRoleData()
{
PermissionItems.Clear();
if (SelectedRole != null)
{
using (var context = new myDataContext(new myDbFactory().GetConnectionString()))
{
foreach (PermissionsEnum permission in Enum.GetValues(typeof(PermissionsEnum)))
PermissionItems.Add(new PermissionItemViewModel(permission, SelectedRole[permission]));
}
}
}
All subsequent manipulations of that collection do not do this...it only happens the first time through the FetchRoleData method. Why?
EDIT:
Some additional information. The CustomSort property is set when the CollectionViewSource fires its Filter event (the only event it has AFAIK). I couldn't come up with any better trigger to get it set. The OnAttached override is too soon, as the View member of the CollectionViewSource is not set yet by that point. Catch 22, I guess. That is happening immediately after that first collection item is added. If this is due to the order in which things are being set, then are there any recommendations for a change?
I don't know how or where you're setting up the filter handler. Here's an example of how to set a custom sort on a CollectionViewSource when its View property changes. That's when you want to do it. This assumes that it's in the resources for a Window (or at least someplace where the Window can find it). But once you have cvs, wherever it comes from and however you got your mitts on it, the rest is the same.
public MainWindow()
{
InitializeComponent();
var cvs = FindResource("MyCollectionViewSource1") as CollectionViewSource;
var dpdView = DependencyPropertyDescriptor.FromProperty(
CollectionViewSource.ViewProperty, typeof(CollectionViewSource));
dpdView.AddValueChanged(cvs, (s, e) =>
{
if (cvs.View is ListCollectionView lvc)
{
lvc.CustomSort = new CustomSorter();
}
});
}
I'm baffled by your claim that the first item in the collection is being duplicated. No code you've shown, and no code I've given you, could have that effect. You'll have to share code that demonstrates that issue.

WPF - setting properties to themselves fails to fire changed event

Here's a curious piece of behaviour. We recently built some code in a WPF MVVM application that looked a bit like this:
foreach (var mA in Preferences.Where(itm => itm.Preference == "Y"))
{
Member m = _members.FirstOrDefault(itm => itm.MemberID == mA.MemberAvertedID);
if (m != null)
{
m.Selected = true;
}
}
Members = _members;
So, FirstOrDefault fetches a reference to a Member, which is updated. Members and _members are effectively the same - the former is a property, wrapping the latter as a private variable, with an event fire:
public ObservableCollection<Member> Members
{
get
{return _members;}
set
{
_members = value;
OnPropertyChanged("Members");
}
}
The purpose of setting Members to _members was simply to get the event to fire - but it didn't work. As you stepped through, the OnPropertyChanged event fired, but the application didn't respond. This, however, does work:
foreach (var mA in Preferences.Where(itm => itm.Preference == "Y"))
{
Members m = _members.FirstOrDefault(itm => itm.MemberID == mA.MemberAvertedID);
if (m != null)
{
mtc.Selected = true;
}
}
var mem = new ObservableCollection<Members>(_members);
Members = mem;
I'm assuming that what's going on here is that because setting Members to _members is effectively the property setting itself, the code "presumes" that nothing has changed, and skips the event. But I'm not really satisfied with that explanation. Can anyone elucidate further as to what's going on here?
Bindings to ObservableCollections will not rebind the list unless the collection reference differs, hence the last piece of code is working. That said, unless you are actually adding or removing items from the underlying _members list you shouldn't have to rebind the whole list.
So (I assume) if your goal is to refresh the state of the Selected Member, you are probably lacking a OnPropertyChanged("Selected") raised from within the Selected property.
In summary: the property that is changing must be named in the PropertyChanged event. And the event must come from the object that owns the property. In this case, notifying WPF of a change to the Selected property requires raising the event on the Member instance.

Testing Custom Control derived from ComboBox

I've created a control derived from ComboBox, and wish to unit test its behaviour.
However, it appears to be behaving differently in my unit test to how it behaves in the real application.
In the real application, the Combobox.DataSource property and the .Items sync up - in other words when I change the Combobox.DataSource the .Items list immediately and automatically updates to show an item for each element of the DataSource.
In my test, I construct a ComboBox, assign a datasource to it, but the .Items list doesn't get updated at all, remaining at 0 items. Thus, when I try to update the .SelectedIndex to 0 in the test to select the first item, I recieve an ArgumentOutOfRangeException.
Is this because I don't have an Application.Run in my unit test starting an event loop, or is this a bit of a red herring?
EDIT: More detail on the first test:
[SetUp]
public void SetUp()
{
mECB = new EnhancedComboBox();
mECB.FormattingEnabled = true;
mECB.Location = new System.Drawing.Point( 45, 4 );
mECB.Name = "cboFind";
mECB.Size = new System.Drawing.Size( 121, 21 );
mECB.TabIndex = 3;
mECB.AddObserver( this );
mTestItems = new List<TestItem>();
mTestItems.Add( new TestItem() { Value = "Billy" } );
mTestItems.Add( new TestItem() { Value = "Bob" } );
mTestItems.Add( new TestItem() { Value = "Blues" } );
mECB.DataSource = mTestItems;
mECB.Reset();
mObservedValue = null;
}
[Test]
public void Test01_UpdateObserver()
{
mECB.SelectedIndex = 0;
Assert.AreEqual( "Billy", mObservedValue.Value );
}
The test fails on the first line, when trying to set the SelectedIndex to 0. On debugging, this appears to be because when the .DataSource is changed, the .Items collection is not updated to reflect this. However, on debugging the real application, the .Items collection is always updated when the .DataSource changes.
Surely I don't have to actually render the ComboBox in the test, I don't even have any drawing surfaces set up to render on to! Maybe the only answer I need is "How do I make the ComboBox update in the same way as when it is drawn, in a unit test scenario where I don't actually need to draw the box?"
Since you're simply calling the constructor, a lot of functionality of the combobox will not work. For example, the items will be filled when the ComboBox is drawn on screen, on a form. This does not happen when constructing it in a unit test.
Why do you want to write a unit test on that combobox?
Can't you seperate the logic which now is in the custom control? For example put this in a controller, and test that?
Why don't you test on the DataSource property instead of the Items collection?
I'm sure that Application.Run absence cannot affects any control's behavior
I'm having the same problem with a combo box where the items are data bound. My current solution is to create a Form in the test, add the combo box to the Controls collection, and then show the form in my test. Kind of ugly. All my combo box really does is list a bunch of TimeSpan objects, sorted, and with custom formatting of the TimeSpan values. It also has special behavior on keypress events. I tried extracting all the data and logic to a separate class but couldn't figure it out. There probably is a better solution but what I'm doing seems satisfactory.
To make testing easier, I created these classes in my test code:
class TestCombo : DurationComboBox {
public void SimulateKeyUp(Keys keys) { base.OnKeyUp(new KeyEventArgs(keys)); }
public DataView DataView { get { return DataSource as DataView; } }
public IEnumerable<DataRowView> Rows() { return (DataView as IEnumerable).Cast<DataRowView>(); }
public IEnumerable<int> Minutes() { return Rows().Select(row => (int)row["Minutes"]); }
}
class Target {
public TestCombo Combo { get; private set; }
public Form Form { get; private set; }
public Target() {
Combo = new TestCombo();
Form = new Form();
Form.Controls.Add(Combo);
Form.Show();
}
}
Here is a sample test:
[TestMethod()]
public void ConstructorCreatesEmptyList() {
Target t = new Target();
Assert.AreEqual<int>(0, t.Combo.DataView.Count);
Assert.AreEqual<int>(-1, t.Combo.SelectedMinutes);
Assert.IsNull(t.Combo.SelectedItem);
}
This solve some problems if target is ComboBox or any other control:
target.CreateControl();
but I was unable to set SelectedValue it has null value, my test working with two data sources for combo box, one as data source and second is binded to selevted value. With other controls everithing working fine. In the begining I was also creating form in tests, but there is problem when form on created on our build server while tests are executed.
I did a little hack to allow this in my custom derived combobox:
public class EnhancedComboBox : ComboBox
{
[... the implementation]
public void DoRefreshItems()
{
SetItemsCore(DataSource as IList);
}
}
The SetItemsCore function instructs the base combobox to load internal items with the provided list, it's what uses internally after the datasource changes.
This function never gets called when the control is not on a form, because there are lots of checks for CurrencyManagers and BindingContexts that are failing because this components, I believe, are provided by the parent form somehow.
Anyway, in the test, you have to call mECB.DoRefreshItems() just after the mECB.DataSource = mTestItems and everything should be fine if you only depend on the SelectedIndex and the Items property. Any other behavior like databinding is probably still not functional.

Categories