I have a form which has two ComboBoxes. I'd like to use the second (child) ComboBox to display a list of child objects based on user selection of an item from the first.
When the form is instantiated, I databind both controls to private List<Widget>s like so:
private List<ParentWidget> _parentList;
private List<ChildWidget> _childList;
public FormExample()
{
InitializeComponent();
_parentList = GetParentWidgets();
_childList = new List<ChildWidget>();
cmbParent.DisplayMember = "WidgetName";
cmbParent.ValueMember = "ID";
cmbParent.DataSource = _parentList;
cmbChild.DisplayMember = "WidgetName";
cmbChild.ValueMember = "ID";
cmbChild.DataSource = _childList;
}
When the parent selected index is changed, I then populate _childList with the appropriate objects.
The problem is the child ComboBox never shows any of the objects in the collection. It works if I populate the collection with at least one ChildWidget before databinding it, but I'd like it to start empty.
If I understand correctly from another answer, this is failing because the empty list does not contain any properties to bind to. However I am binding to a specific class (Widget) rather than a generic object. Is this not sufficient for the databinding?
When using binding, you should better use the BindingList<> instead, your problem is the List<> does not support notifying changes, so when the data is changed, the control does not know about it and update accordingly. You can use the BindingList<> instead like this:
private BindingList<ParentWidget> _parentList;
private BindingList<ChildWidget> _childList;
That means you have to change the return type of the method GetParentWidgets() to BindingList<ParentWidget>, or you can also use the constructor of a BindingList<> like this:
_parentList = new BindingList<ParentWidget>(GetParentWidgets());
What I would do is listening to the valueChanged event of the parent comboBox and when the event fire I would do the databinding on the child combo.
cmbParent.SelectedValueChanged += OnParentSelectedValueChanged;
private void OnParentSelectedValueChanged(object sender, EventArgs e)
{
this.UpdateChildList(); // Update the data depending on the value in the parent combo
cmbChild.DisplayMember = "WidgetName"; // I guess you can still do this in the constructor
cmbChild.ValueMember = "ID"; // I guess you can still do this in the constructor
cmbChild.DataSource = _childList;
}
Related
The context is as below:
A ComboBox is binded to a BindingSource.
That BindingSource is then binded to a DataModel.
That ComboBox has a DataSource of a list of objects. Yet, this list maybe set to
null when under some conditions, and maybe restored under some other conditions.
My problem is on the last step of the below scenario:
At the begining, ComboBox's dataSource is a List of 3 items (eg. item1, item2, item3)
Now comboBox show item1 as the default selected item
User pick item 3 on comboBox
Now comboBox show item3 as the selected item
...some other event happened and cause comboBox's dataSource become null
Now comboBox has no selected item (which selectedIndex is -1)
...some other event happened and cause comboBox's dataSource become the original list
8. Now comboBox's selected item is back to item3, but NOT item1
I would like to understand why will it be so, as i don't want the prvious value being preserved after the dataSource has been null.
Here is how I setup the code, with 1 comboBox binded to the a bindingSource and data-model, and 2 buttons to simulate change of comboBox's dataSource.
BindingModel model = new BindingModel();
public Form1()
{
InitializeComponent();
// BindingSource <--> model
cbxBindingSource.DataSource = typeof(BindingModel);
cbxBindingSource.Add(model);
// ComboxBox <-SelectedValue-> BindingSource <--> model.Result
comboBox.DataBindings.Add(new Binding("SelectedValue", cbxBindingSource, "Result", true, DataSourceUpdateMode.OnPropertyChanged));
}
// Simulate a re-usable list
List<BinderHelper<string>> list = new List<BinderHelper<string>>
{
new BinderHelper<string>("Item1", "1"),
new BinderHelper<string>("Item2", "2"),
new BinderHelper<string>("Item3", "3"),
new BinderHelper<string>("Item4", "4"),
};
private void button2_Click(object sender, EventArgs e)
{
// Simulate triggers that will result in a well-populated data-source
comboBox.DisplayMember = "DisplayMember";
comboBox.ValueMember = "ValueMember";
comboBox.DataSource = list;
}
private void button3_Click(object sender, EventArgs e)
{
// Simulate triggers that will result in a empty data-source
comboBox.DataSource = null;
}
private class BindingModel
{
// Simple model to bind to the selected value of the comboBox
public string Result { get; set; }
}
private class BinderHelper<T>
{
// Use for binding purpose
public string DisplayMember { get; set; }
public T ValueMember { get;set; }
public BinderHelper(string display, T value)
{
DisplayMember = display;
ValueMember = value;
}
}
However, if i create a new list, with new items everytime, then the issue will not be seen.
However, it is not possible in my actual code as the list is always the same instance, just a matter of put it on datasource or not.
Sample as below:
private void button2_Click(object sender, EventArgs e)
{
// Create new list everytime
List<BinderHelper<string>> list = new List<BinderHelper<string>>
{
new BinderHelper<string>("Item1", "1"),
new BinderHelper<string>("Item2", "2"),
new BinderHelper<string>("Item3", "3"),
new BinderHelper<string>("Item4", "4"),
};
// Simulate triggers that will result in a well-populated data-source
comboBox.DisplayMember = "DisplayMember";
comboBox.ValueMember = "ValueMember";
comboBox.DataSource = list;
}
A workaround i did is to check the current dataSource:
If the current dataSource is null, and the new dataSource is not,
Then set SelectedIndex = 0
But i would really want to avoid this manual intervention other than changing the datasource.
Appreciate anyone have a solution on that, thanks in adv!
Updated the working code with suggestions from Ivan:
public Form1()
{
InitializeComponent();
model = new BindingModel();
// BindingSource <--> model
cbxToModelBindingSource.DataSource = typeof (BindingModel);
cbxToModelBindingSource.Add(model);
// New dedicated source to provide the list in combox
// BindingSource <--> comboBox
listToCbxBindingSource = new BindingSource(components);
listToCbxBindingSource.DataSource = typeof (BinderHelper<string>);
comboBox.DataSource = listToCbxBindingSource;
// ComboxBox <--SelectedValue--> BindingSource <--> model.Result
comboBox.DataBindings.Add(new Binding("SelectedValue", cbxToModelBindingSource, "Result", true, DataSourceUpdateMode.OnPropertyChanged));
}
// Simulate triggers that will result in a well-populated data-source
private void button2_Click(object sender, EventArgs e)
{
listToCbxBindingSource.DataSource = list;
}
// Simulate triggers that will result in a empty data-source
private void button3_Click(object sender, EventArgs e)
{
listToCbxBindingSource.DataSource = null;
}
// Additional button to test two-way-bindings
private void button4_Click(object sender, EventArgs e)
{
model.Result = "4";
}
The described behavior is caused by the WinForms data binding infrastructure, in particular the BindingContext class. For each unique data source object it maintains a corresponding BindingManageBase object, which along with other things provides Position and Current property for the data sources - something that they normally don't have - consider IList for example. For list type data source it's an instance of a CurrencyManager class and is created the first time binding manager is requested for a data source object. The important part related to your issue is that there is no mechanism of removing the already created binding managers. Think of it like a dictionary with data source as a key, binding manager as a value created the first time you request a non existing key.
ComboBox control is one of the controls that uses and modifies the Position property when you data bind the list portion of it. When you assign an list to the DataSource property, it synchronizes the selection with the Position, which for newly created binding manger for non empty list is 0. Then when you select an item in the combo box, the Position is updated accordingly. When you set DataSource to null, the combo box items are cleared, but the existing binding manager with the last set position remains inside the BindingContext. The next time you set the same list as data source, it gets the existing binding manager with the last set position, hence the behavior you are getting.
To get rid of this behavior, you should use intermediate BindingSource component. Along with other things, it provides it's own Position, Current property/management and dedicated associated CurrencyManager which allows it do be used a regular data source for controls, at the same time hiding the actual data source from them (and in particular from BindingContext).
Here is how I see the solution. Add another BindingSource component (similar to yours cbxBindingSource), let call it for instance listBindingSource and statically bind the combo box to it:
public Form1()
{
InitializeComponent();
// Make properties of the expected object type available for binding
listBindingSource.DataSource = typeof(BinderHelper<string>);
comboBox.DisplayMember = "DisplayMember";
comboBox.ValueMember = "ValueMember";
comboBox.DataSource = listBindingSource;
// the rest ...
}
Now all you need is to manipulate the DataSource property of the binding source instead of the bound control(s).
private void button2_Click(object sender, EventArgs e)
{
// Simulate triggers that will result in a well-populated data-source
listBindingSource.DataSource = list;
}
private void button3_Click(object sender, EventArgs e)
{
// Simulate triggers that will result in a empty data-source
listBindingSource.DataSource = null;
}
You can simply handle the issue by verifying item.count , so if is equal to zero do nothing
if (Combo1.item.count==0)
{
return;
}
keep above code where ever you would like
Let me know if it helps
So I've found a lot of questions similar to this tho nothing really solve my problem..
I have a combobx that is bounded by a datasource
cmbProduct.DataSource = this.masterDataSet.Product.Where(x => x.Location == getLocation).AsDataView();
cmbProduct.DisplayMember = "Product";
cmbProduct.ValueMember = "Product";
But whenever i update the source, the combobox items does not update automatically. I still need to close then reopen the form.
is there a method to refresh/reload/or update the combobox?
Solution 1
You can use an implementation of IBindingList as DataSource to view changes of data source in the bound list control (complex two-way data binding). The most suitable implementation is System.ComponentModel.BindingList<T>.
Then when you add items to the binding list, or remove item from it you will see changes immediately in the control.
Solution 2
But as a more simple solution with less changes for you, you can reset the databinding of your cmbProduct this way when you need; for example after a change, call RefreshBindings();:
public void RefreshBindings()
{
var list = put your updated list here;
this.cmbProduct.DataSource = null;
this.cmbProduct.DataSource = list;
this.cmbProduct.DisplayMember = "set the display member here";
this.cmbProduct.ValueMember = "set the value member here";
}
You could implement an event that fires whenever the DataSet changes. The event could reset the Datasource and rebind it.
Somewhere in your code:
yourDataController.DataChanged += OnDataChanged;
and the implementation
public void OnDataChanged(object sender, EventArgs e)
{
cmbProduct.Items.Clear();
cmbProduct.DataSource = this.masterDataSet.Product.Where(x => x.Location == getLocation).AsDataView();
cmbProduct.DisplayMember = "Product";
cmbProduct.ValueMember = "Product";
}
Edit: Of course you need to manually implement the event and cause it to fire every time your data changes.
I have two DataGrids above one another, and selecting a row in the top grid, OrdersGrid, displays some details in the bottom grid, DetailsGrid, about the selected row from OrdersGrid.
I would like the NewItemPlaceholderPosition to be AtBeginning for both grids. This is easy enough for OrdersGrid because I can just set it in my UserControl subclass constructor:
((IEditableCollectionView)OrdersGrid.Items).NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtBeginning;
But the problem is that the ItemsSource for DetailsGrid is a member of the currently selected item in OrdersGrid.
If I set NewItemPlaceholderPosition for DetailsGrid as above then it works until I click on a new row in OrdersGrid when it goes back to the default of being at the bottom since DetailsGrid reloads from its new ItemsSource.
The ItemsSource for OrdersGrid is an ObservableCollection called Orders, which contains Objects of type Order, and the ItemsSource for DetailsGrid is Order.Details, also an ObservableCollection, for the current Order.
I'm thinking I want something like an ItemsSourceChanged event for DetailsGrid, but I'm not sure if this is the correct approach or even how to go about this. Please help!
I was able to solve this problem by adding the following to my UserControl subclass constructor:
((IEditableCollectionView)DetailsGrid.Items).NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtBeginning;
var dpd = DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof(DataGrid));
if (dpd != null)
{
dpd.AddValueChanged(DetailsGrid, DetailsSourceChanged);
}
with the DetailsSourceChanged method defined as followed:
private void DetailsSourceChanged(object sender, EventArgs e)
{
if (DetailsGrid.Items.Count > 0)
{
((IEditableCollectionView)DetailsGrid.Items).NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtBeginning;
}
}
I am trying to add some data inside my DataGrid.
I added some columns with the designer. Now I want to add rows with data inside the DataGrid.
Here's my code so far:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var dataContext = new PurchaseOrderDataContext();
var purchaseOrderTable = dataContext.GetTable<PurchaseOrder>();
var query = from a in purchaseOrderTable
select a;
var purchaseOrders = query;
foreach (var purchaseOrder in purchaseOrders)
{
// I believe that this method is the right one, but what do I pass to it?
// dataGrid1.Items.Add(test);
}
}
All I want to know is: What kind of object do I need to use to add something in the DataGrid, and what kind of object do I need to pass to that last method? Also how do I add, let's say, text to a certain column of a row I added?
Thanks a lot!
In general, you would bind the ItemsSource of the grid to a collection that supports change notification (IObservableCollection is idea) and just add to the collection. If the collection supports change notification, the grid will automatically display the new row.
Try this:
dataGrid1.ItemsSource = query;
I have a List and a Button. When the Lists Count == 0, I would like the button Visibility to = false.
How do I do this using Data Binding?
Thanks in advance,
Added
I have asked this so that I can try to avoid checking the Count on the list in code every time I add or remove an item to or from the list. But if there is no solution then I will continue to do it that way.
Create a DTO (Data Transfer Object) that exposes all your data that you intend to bind to UI elements. Create a property in the DTO (with an appropriate name):
public bool ButtonVisible
{
get { return myListCount != 0; }
}
Add a BindingSource to your form and set it's DataSource to your DTO type.
Click on the Button, goto Properties. Expand the DataBindings node, and click Advanced.
Scroll down the list in the left hand pane, and select Visible. Set it's binding to your property exposed vis the BindingSource..
The General Answer
Write an event handler and register it with your list-control's bindings object
A Specific Example
class MyForm : Form {
protected Button myButton;
BindingSource myBindingSource;
DataGridView dgv;
public MyForm(List someList) {
myBindingSource = new BindingSource();
dgv = new DataGridView();
this.myButton = new Button();
this.Controls.Add(myButton);
this.Controls.Add(dgv);
myBindingSource.DataSource = someList;
dgv.DataSource = myBindingSource;
dgv.DataSource.ListChanged += new ListChangedEventHandler (ListEmptyDisableButton);
}
protected void ListEmptyDisableButton (object sender, ListChangedEventArgs e) {
this.myButton.Visible = this.dgv.RowCount <= 0 ? false : true;
}
}
PS - I'd vote down the favorite answer. A Data Transfer Object (DTO) misses the whole point and functionality of .NET Binding architechture
As the question is currently worded, it doesn't sound like it has anything to do w/ DataBind.
If we have a list -- doesn't matter whether it's populated via code or bound to a data source -- we can set the button's visibility based on the count.
e.g.
List<string> somelist = new List<string>();
somelist.Add("string1");
somelist.Add("string2");
Button1.Visible = somelist.Count > 0 ? true : false;
I think you want to be using the CurrencyManager and the BindingContext of the control.
http://www.akadia.com/services/dotnet_databinding.html#CurrencyManager