I have the following simple class;
public class MyObject
{
public int Id {get;set;}
public string Name {get;set;}
}
List<MyObject> oList = new List<MyObject>();
My list is populated with some items. I then populate my BindingSource with the list like;
MyBindingSource.DataSource = oList; //contains some items in a list
My BindingSource is linked to a DataGridView (which doesn't really matter in this example), but depending on the selected row in the DataGridView, I then have the following method for my datagrid view clicked button;
private void MyDataGrid_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
if (e.ColumnIndex == btnRemove.Index)
{
MyBindingSource.RemoveCurrent();
}
}
The call
MyBindingSource.RemoveCurrent()
removes the items from the DataGridView, but how do I remove the item from the underlying list which is oList.
I thought that assigning MyBindingSource.DataSource = oList, means the list shown in MyBindingSource.DataSource is actually pointing to oList ?
List<T> isn't smart enough to know things have changed, so try using a BindingList<T> from System.ComponentModel instead:
BindingList<MyObject> oList = new BindingList<MyObject>();
Related
How to extract a selected item from Listbox as an object. Currently, I'm taking all records from DB and I add them to the Listbox in the following way:
var allEmployees = GetAll();
foreach (var emp in allEmployees)
{
var empFull = $"{emp.Id} - {emp.Name} {emp.Surname} - {emp.Email}";
listBoxViewEmp.Items.Add(empFull);
}
However, I find it challenging to make the listBoxViewEmp.SelectedItem to object since I'm taking the entire string that contains id, name, surname, and email.
private void btnUpdate_Click(object sender, EventArgs e)
{
if(listBoxViewEmp.SelectedItem != null)
{
var newForm = new UpdateForm(listBoxViewEmp.SelectedItem);
newForm.Show();
}
else
{
MessageBox.Show("Please select employee first");
}
}
What is the best way to handle this kind of issue?
Update:
Here is my attempt, but I got only objects now in the listbox
var allEmployees = GetAll();
foreach (var emp in allEmployees)
{
var empFull = (Employee)emp;
listBoxViewEmp.Items.Add(empFull);
}
enter image description here
The string that appears for any object added as an item to a ListBox control will be the return value of the ToString method of that object. In your case, you added a string directly to the list. So there's (likely) no way to recover the original object from the formatted string you created for that object.
Instead, override ToString for your Employee object (if appropriate to do so).
Alternatively, you can create a wrapper class whose job it is to remember the original object and to provide a custom ToString implementation for the remembered object. Such a generic formatter class might look like:
public class Formatter<T>
{
public Formatter(T obj, Func<T, string> fnFormat)
{
this.obj = obj;
this.fnFormat = fnFormat;
}
public T obj;
public Func<T, string> fnFormat;
public override string ToString() => fnFormat(obj);
}
Then, when you add items to the list box, add a Formatter wrapping that item instead. Its ToString method will be called, which will call the custom formatter for that item.
foreach (var employee in employees)
{
this.listBox1.Items.Add(new Formatter<Employee>(employee,
emp => $"{emp.Id} - {emp.Name} {emp.Surname} - {emp.Email}"));
}
And to access the original Employee, case the SelectedItem back to the Formatter<Employee> and access its remembered object field obj. Now you have the original Employee object back (or DB record or entity or whatever it was originally.) Example usage:
private void button1_Click(object sender, EventArgs e)
{
Employee employee = (this.listBox1.SelectedItem as Formatter<Employee>)?.obj;
this.label1.Text = employee?.Id.ToString();
}
You can use databinding for this. Make a new readonly property which will return computed string you want. Than ListBox has some properties you can use:
Datasource - source of data which will be displayed. You can set here just simple List<Employee>.
DisplayMember - you set the name of the property, which value you want to see in the ListBox.
ValueMember - you set the name of the property which value you want to get for selected item using ListBox's SelectedValue property.
So you can do it this way:
// You should set these two properties in form's designer.
listBoxViewEmp.DisplayMember = "Info"; // "Info" is that new readonly property.
listBoxViewEmp.ValueMember = "Id";
var allEmployees = GetAll().ToList();
listBoxViewEmp.DataSource = allEmployees;
That's it. Now:
You will see in the listbox what you want to see there.
listBox.SelectedItem returns whole your Eployee object.
listBox.SelectedValue returns the Id value of selected employee.
If you have access to the Employee class, do that :
Main code:
var allEmployees = GetAll();
foreach (var emp in allEmployees)
{
var empFull = (Employee)emp;
listBoxViewEmp.Items.Add(empFull);
}
In class code :
class Employee
{
...
public override string ToString()
{
return $"{this.Id} - {this.Name} {this.Surname} - {this.Email}";
}
}
Now you are able to get the items from the listbox as Employee and it will also replace "JustTest.Models.Employee" in the ToString above.
I have DataGrid with DataTable ItemSource.
In DataTable cells there are myClass objects with displayField property for display data and sorting.
Here I try to set correct SortMemberPath value:
private void profileStat_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
e.Column.SortMemberPath = "displayField";
e.Column.CanUserSort = true;
}
Now I am getting an error:
The type "" does not have a property named "displayField", therefore no sorting of the data family is possible. '
You should show us that myClass class code. Are you sure the field displayField is a property?
it should look like this:
public string displayField { get; set; }
Note that it must be public.
I found this solution
private void DataGrid_Sorting(object sender, DataGridSortingEventArgs e)
{
DataView data = ((DataView)((DataGrid)sender).ItemsSource).Table.ApplySort((r1, r2) =>{ //sort realization });
((DataGrid)sender).ItemsSource = data;
}
Thanks to:
https://learn.microsoft.com/en-us/dotnet/api/system.data.dataview.sort?view=netframework-4.8
https://stackoverflow.com/a/582499/5261588
I have been trying to create a small form application and I wanted to try out binding a DataGridView directly to a collection of objects.
I created the following classes
public class MyClassRepository
{
public List<MyClass> MyClassList { get; set; } = new List<MyClass> { new MyClass { Name = "Test" } };
}
public class MyClass
{
public string Name { get; set; }
}
and I added the following code to a form to test. I based this off of the code in the designer after setting the BindingSource through the UI (while following this walk through https://msdn.microsoft.com/en-us/library/ms171892.aspx)
var tmp = new BindingSource();
tmp.DataMember = "MyClassList";
tmp.DataSource = typeof(MyClassRepository);
When this didn't work I started running through the code behind BindingSource to see what was happening. The setter calls ResetList which tries to create a dataSourceInstance by calling ListBindingHelper.GetListFromType. This call ultimately calls SecurityUtils.SecureCreateInstance(Type) where type is a BindingList<MyClassRepository>. This passes null to args which is passed Activator.CreateInstance which returns an empty collection.
After this ListBindingHelper.GetList(dataSourceInstance, this.dataMember) is called. This method calls ListBindingHelper.GetListItemProperties which results in a PropertyDescriptor for my MyClassList property and assigns it to dmProp.
At this point GetList calls GetFirstItemByEnumerable(dataSource as IEnumerable) where dataSource is the previously created (and empty) instance of BindingList<MyClassRepository> and returns (currentItem == null) ? null : dmProp.GetValue(currentItem);.
The value of dmProp/MyClassList is never accessed and the BindingSource is never populated with the instance I created. Am I doing something wrong? If not is there a bug in the source code? It seems to me like either SecureCreateInstance(Type type, object[] args) should be called and MyClassList should be passed via args instead of the existing call to SecureCreateInstance(Type type) or the value of dmProp should be used regardless?
If that is not correct how do I make the Designers automatically generated code set the DataSource to an instance of the object? Or do I have to inherit from BindingSource? If the latter why does it give you the option to choose a class that does not inherit from BindingSource?
As Reza Aghaei points out, in the designer, setting the BindingSource.DataSource to the “MyClassRepository” may work, however you still need to initialize (create a new) MyClassRepository object. I do not see this line of code anywhere in the posted code: MyClassRepository myRepositiory = new MyClassRepository(); The data source is empty because you have not created an instance of “MyClassRepository” and as Reza points out, this is usually done in the forms Load event.
To keep it simple, remove the DataSource for the BindingSource in the designer and simply set up the BindingSource’s data source in the form load event like below. First, create a new “instance” of the MyClassRepository then use its MyClassList property as a data source to the BindingSource. I hope this helps.
MyClassRepository repOfMyClass;
public Form1() {
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e) {
repOfMyClass = new MyClassRepository();
bindingSource1.DataSource = repOfMyClass.MyClassList;
dataGridView1.DataSource = bindingSource1;
}
Edit-----
After further review… I agree that you should be able to do as you describe. I was able to get it working as expected with the code below.
BindingSource bindingSource1;
private void Form1_Load(object sender, EventArgs e) {
bindingSource1 = new BindingSource();
bindingSource1.DataSource = typeof(MyClassRepository);
bindingSource1.DataMember = "MyClassList";
dataGridView1.DataSource = bindingSource1;
}
I followed the same steps in the “designer” and it worked as expected. Is there something else I am missing? As you stated… using MyClassRepository mcr = new MyClassRepository() appears to be unnecessary. In addition, if you cannot get it to work using one of the two ways above… then something else is going on. If it does not work as above, what happens?
Edit 2
Without creating a “new” MyClassRepository, object was unexpected and I did not realize that new items added to the list/grid were going into the bindingSource1. The main point, is that without instantiating a “new” MyClassRepository object, the constructor will never run. This means that the property List<MyClass> MyClassList will never get instantiated. Nor will the default values get set.
Therefore, the MyClassList variable will be inaccessible in this context. Example in this particular case, if rows are added to the grid, then bindingSource1.Count property would return the correct number of rows. Not only will the row count be zero (0) in MyClassList but also more importantly… is “how” would you even access the MyClassList property without first instantiating a “new” MyClassRepository object? Because of this inaccessibility, MyClassList will never be used.
Edit 3 ---
What you are trying to achieve can be done in a myriad number of ways. Using your code and my posted code will not work if you want MyClassList to contain the real time changes made in the grid by the user. Example, in your code and mine… if the user adds a row to the grid, it will add that item to the “bindingSource” but it will NOT add it to MyClassList. I can only guess this is not what you want. Otherwise, what is the purpose of MyClassList. The code below “will” use MyClassList as expected. If you drop the “designer” perspective… you can do the same thing in three (3) lines of code... IF you fix the broken MyClassRepository class and create a new one on the form load event. IMHO this is much easier than fiddling with the designer.
Changes to MyClassRepository… added a constructor, added a size property and a method as an example.
class MyClassRepository {
public List<MyClass> MyClassList { get; set; }
public int MaxSize { get; set; }
public MyClassRepository() {
MyClassList = new List<MyClass>();
MaxSize = 1000;
}
public void MyClassListSize() {
MessageBox.Show("MyClassList.Count: " + MyClassList.Count);
}
// other list manager methods....
}
Changes to MyClass… added a property as an example.
class MyClass {
public string Name { get; set; }
public string Age { get; set; }
}
Finaly, the form load event to create a new MyClassRepository, set up the binding source to point to MyClassList and lastly set the binding source as a data source to the grid. NOTE: making myClassRepository and gridBindingSource global variables is unnecessary and is set this way to check that MyClassList is updated in real time with what the user does in the grid. This is done in the button click event below.
MyClassRepository myClassRepository;
BindingSource gridBindingSource;
public Form1() {
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e) {
try {
myClassRepository = new MyClassRepository();
gridBindingSource = new BindingSource(myClassRepository.MyClassList, "");
dataGridView1.DataSource = gridBindingSource;
}
catch (Exception ex) {
MessageBox.Show("Error: " + ex.Message);
}
}
private void button1_Click_1(object sender, EventArgs e) {
MessageBox.Show("Binding source count:" + gridBindingSource.Count + Environment.NewLine +
"MyClassList count: " + myClassRepository.MyClassList.Count);
}
I hope this makes sense. ;-)
Designer sets DataSource = typeof(Something) for design-time support, for example to let you choose DataMember from a dropdown or to let you choose the data source property from dropdown while setting up data-bindings.
How do I make the Designers automatically generated code set the
DataSource to an instance of the object?
Forcing the designer to do that doesn't make much sense, because the designer doesn't have any idea about what the real data source you are going to use to load data. It can be a web service, a WCF service, a business logic layer class.
So at run-time you need to assign an instance of your list to DataSource. For example in Load event of the form.
After a combobox index changes, another combobox has to be populated with List<string> values. How am I able to do this?
For example:
Form (This is the way I'm having it now, incorrect though):
private void cbSelectEditFunction_SelectedIndexChanged(object sender, EventArgs e)
{
cbSelectEditName.Items.Add(emp.FindEmployeeinFunction(cbSelectEditFunction.Text));
}
Class method:
public List<string> FindEmployeeinFunction(string aFunction)
{
List<string> EmployeeListFunction = new List<string>();
foreach (Employee TempEmployee in EmployeeList)
{
if(TempEmployee.Function == aFunction)
{
EmployeeListFunction.Add(TempEmployee.Username);
}
}
return EmployeeListFunction;
}
Hope it's understandable this way. Let me know if I've forgotten something!
I think AddRange is the method you're looking for
//Assuming you don't want to continually add new items use Clear()
cbSelectEditName.Items.Clear();
//Use AddRange to add the list. ToArray() is used to convert List<> to string[]
cbSelectEditName.Items.AddRange(emp.FindEmployeeinFunction(cbSelectEditFunction.Text).ToArray());
I am trying to bind several ListBoxs to a List. When a ListBox on one form is updated, I want it to update the other ListBox, too.
The problem I am running into is that it doesn't seem to update the view on the ListBox when I update the underlying List. If I look at the ListBox.Items in debug, I can see that all the items I add are there, but are not being displayed. Additionally, when I open another form that displays the List on a ListBox, it does correctly display whatever items had already been added.
private List<String> _list;
public Form1()
{
InitializeComponent();
_list = StaticInstanceOfList.GetInstance();
listbox1.DataSource = _list;
}
public void AddStringToList(string value)
{
if (!_list.Contains(value))
{
_list.Add(value);
}
}
Try to use a BindingList<T> to store your items and then assign this list to both listboxes via the DataSource property.
Use a bindingSource and bind both listBoxes to that.