Associate two classes so a combobox appears with the second class - c#

I have a Winforms application. There are two classes. One class stores the people that contribute to a book. The other class is a list of possible contribution types (like editor, reviewer, and so on).
The BookContributors class is the datasource for my datagridview and that works just fine. Now, I want to have the BookContributorTypes class be a combobox that feeds the BookContributors class.
So far in my reading, it looks like I have a number of options, including forcing a combobox into the datagridview, using class attributes, creating a 1-to-many relationship between classes.
Since my code will have many of these types of situations, I want to do this right. I think this means showing the relationship between the two classes somehow so the datagridview knows to just display the combobox, but I am not sure how to go about doing this.
BookContributors is populated by a person filling out a contributor. Example:
BookContributor = "John Smith"; BookContributorFileAs="Smith, John"; BookContributorType = "rev".
public class BookContributors : INotifyPropertyChanged
{
private string _bookContributor;
[DescriptionLocalized(typeof(ResourcesClassBooks), "BookContributorComment")]
[DisplayNameLocalized(typeof(ResourcesClassBooks), "BookContributorDisplayName")]
public string BookContributor
{
get { return _bookContributor; }
set
{
if (SetField(ref _bookContributor, value, "BookContributor"))
{
// When the user types an author name, add the sorted name to the sorted field.
// ex Name = William E Raymond
// ex File As = Raymond, William E
var name = _bookContributor.Split(' ');
if (name.Length >= 2)
{
string fileAsName = (name[name.Length - 1] + ",");
for (int i = 0; i <= (name.Length - 2); i++)
{
fileAsName = fileAsName + " " + name[i];
}
BookContributorFileAs = fileAsName;
}
}
}
}
private string _bookContributorFileAs;
[DescriptionLocalized(typeof(ResourcesClassBooks), "BookContributorFileAsComment")]
[DisplayNameLocalized(typeof(ResourcesClassBooks), "BookContributorFileAsDisplayName")]
public string BookContributorFileAs { get { return _bookContributorFileAs; } set { SetField(ref _bookContributorFileAs, value, "BookContributorFileAs"); } }
private string _bookContributorType;
[DescriptionLocalized(typeof(ResourcesClassBooks), "BookContributorTypeComment")]
[DisplayNameLocalized(typeof(ResourcesClassBooks), "BookContributorTypeDisplayName")]
public string BookContributorType { get { return _bookContributorType; } set { SetField(ref _bookContributorType, value, "BookContributorType"); } }
#region handle property changes
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetField<T>(ref T field, T value, string propertyName)
{
//if the value did not change, do nothing.
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
//the value did change, so make the modification.
field = value;
return true;
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
BookContributorTypes (populated with a list of bookContributor types from an XML file). Example Book contributor type ID's: "rev", "edt". Example Book contributor type descriptions: "Reviewer", "Editor".
class BookContributorTypes : INotifyPropertyChanged
{
private string _bookContributorTypeId;
[DescriptionLocalized(typeof(ResourcesClassBooks), "BookContributorTypeIdComment_lkp")]
[DisplayNameLocalized(typeof(ResourcesClassBooks), "BookContributorTypeIdDisplayName_lkp")]
public string BookContributorTypeId { get { return _bookContributorTypeId; } set { SetField(ref _bookContributorTypeId, value, "BookContributorTypeId"); } }
private string _bookContributorTypeDescription;
[DescriptionLocalized(typeof(ResourcesClassBooks), "BookContributorTypeComment_lkp")]
[DisplayNameLocalized(typeof(ResourcesClassBooks), "BookContributorTypeDisplayName_lkp")]
public string BookContributorTypeDescription { get { return _bookContributorTypeDescription; } set { SetField(ref _bookContributorTypeDescription, value, "BookContributorTypeDescription"); } }
#region handle property changes
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetField<T>(ref T field, T value, string propertyName)
{
//if the value did not change, do nothing.
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
//the value did change, so make the modification.
field = value;
return true;
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
All I have working right now, without the combo box, because I do not know how to implement a combobox to show the selection options from the BookContributorTypes class:
dataGridView1.DataSource = bookContributors;
Thanks for ay help you can provide.

This ended up being a lot more difficult to research than I originally thought. It turns out, if you want to place a column in a DataGridView with a combobox and have that combobox interact with another class, it is not a quick one-liner.
My example has two classes:
BookContributors (bookContributors) - A list of people that have contributed to a book. For example, this stores a person's name and the type of contribution they made.
BookContributorTypes (bookContributorTypes) - A list of possible types of contributions to a book, like Editor or Contributor.
I want the ContributorTypes class to be a combobox and store the user's selection into the BookContributors class, specifically the BookContributorType property.
Here is the code I came up with, along with some additional notes I found along the way. To the best of my knowledge, the information I provide is accurate :-)
gvContributors.DataSource = bookContributors; //set the datgridview's datasource so it displays the class you want.
gvContributors.Columns["BookContributorType"].Visible = false; //hide the column (property) you want to replace with a combobox.
DataGridViewComboBoxColumn contribType = new DataGridViewComboBoxColumn(); //create a combobox object.
contribType.HeaderText = "My Column"; //the text to display in the column header.
contribType.Name = "BookContributorType"; //name of the class property you set to Visible=false. Note sure if this is needed.
contribType.DataSource = bookContributorTypes; //name of the class that provides the combobox data.
contribType.DisplayMember = "BookContributorTypeDescription"; //data the user will see when clicking the combobox.
contribType.ValueMember = "BookContributorTypeId"; //data to store in the class property.
contribType.DataPropertyName = "BookContributorType"; //the class property you are binding to in order to store the data.
gvContributors.Columns.Add(contribType); //add the new combobox to the datagridview.
What you have now is the following:
A DataGridView with a datasource. In my case, the datasource is a class.
A hidden (Visible=false) column that you want to make a combobox.
A DataGridViewComboBox that links to another datasource so the user will see a list of values. By using the DataPropertyName as the same name as the hidden column, you are now binding to the DataGridView's DataSource.
Another answer on this post suggests you Remove the column you are replacing with a combobox but Visible=false seems to work for me.
When you run the solution, you will find the user has to click twice on the combobox. I have not tried this code yet, but think this post will resolve that problem: Open dropdown(in a datagrid view) items on a single click

I'm working with WinForms DataGridView for several years. AFAIK the DataGridView is not as smart
as you expect. You have to setup comboBox columns manually,
i.e. on event DataBindingComplete
create new DataGridViewComboBoxColumn
set its DataPropertyName property to "BookContributorType"
add it to DataGridViewColumnCollection
set its DataSource property to a collection of BookContributorTypes
set its ValueMember property to "BookContributorTypeId"
set its DisplayMember property to "BookContributorTypeDescription"
adopt properties from auto-generated column corresponding BookContributorTypes like
DisplayIndex
remove auto-generated column
I'm using wrapper classes around DataGridView to get a smarter control. In this way I've reduced
source code everywhere I'm using DataGridView.
Here is a code snippet to replace an auto-generated column with a comboBox column:
DataGridViewColumn auto // = ... auto-generated column to be replaced
DataGridViewComboBoxColumn combo = new DataGridViewComboBoxColumn();
combo.DataPropertyName = auto.DataPropertyName;
combo.Name = auto.Name;
DataGridView dgv = auto.DataGridView;
dgv.Columns.Add(combo);
combo.DataSource = GetBookContributorTypes; // collection of comboBox entries
combo.ValueMember = "BookContributorTypeId";
combo.DisplayMember = "BookContributorTypeDescription";
// adopt further properties if required
combo.Frozen = auto.Frozen;
combo.DisplayIndex = auto.DisplayIndex;
combo.Visible = auto.Visible;
combo.ReadOnly = auto.ReadOnly;
combo.HeaderText = auto.HeaderText;
combo.HeaderCell.ToolTipText = auto.HeaderCell.ToolTipText;
combo.SortMode = auto.SortMode;
combo.Width = auto.Width;
dgv.Columns.Remove(auto);

Related

Mode=TwoWay return Null

After a lot of hours I finally found what is the problem that cause the bug. Before to show the code that present the problem I need to explain the situation.
Binding and properties structure
In my application there is a ComboBox that bind as ItemSource a list of Rounds and as SelectedItem the Round selected from the list by the user.
The ComboBox have this structure:
<ComboBox ItemsSource="{Binding Rounds}" DisplayMemberPath="RoundName" SelectedItem="{Binding SelectedRound, Mode=TwoWay}" />
as you can see I've as modality TwoWay this allow me to update the property SelectedRound automatically when the user change the Item selected.
This is the class Round:
public class Round
{
public int Id { get; set; }
public string Link { get; set; }
public bool Selected { get; set; }
public string RoundName { get; set; }
}
and this is the properties used by the ComboBox:
//List of rounds available
private List<Round> _rounds;
public List<Round> Rounds
{
get { return _rounds; }
set
{
_rounds = value;
OnPropertyChanged();
}
}
//Selected round on ComboBox
private Round _selectedRound;
public Round SelectedRound
{
get { return _selectedRound; }
set
{
_selectedRound = value;
OnPropertyChanged();
}
}
both properties implement the OnPropertyChanged().
How the properties valorization works
In the app there is a method called LoadRounds() that is called each time the user press a button, this method have the following instruction:
public void LoadRounds(Team team)
{
//Fill the source of ComboBox with the rounds of the new team
Rounds = team.Rounds.ToList(); //<- Create a copy, so no reference
//Get the selected round
SelectedRound = Rounds?.FirstOrDefault(x => x.Id == team.CurrentRound.Id);
}
the SelectedRound is taken from a team property called CurrentRound, in particular each team have a round, so for a practice example:
[Rounds id available in Rounds property]
37487
38406
38405
37488
37486
...
[CurrentRound id of team]
38405
so the SelectedRound will contain the Round with Id 38405, and the linq query working well.
The problem
I set a breakpoint on _selectedRound = value;, the first firing time the value is a Round item (38405), but there is also a second firing time (that shouldn't be) that have as value null.
After a lot of hours spended on pc to understand why this situation happen I figure out.
Seems that the ComboBox (the TwoWay mode) doesn't know how to map the SelectedRound from the ItemSource, so essentially:
1. [Item Source updated with new Rounds]
2. [SelectedRound updated from the new `Rounds` available]
3. [SelectedRound setter called again with a null value]
I used also the stack call window for see if there is any method that call the setter property another time, but there is no external method that call the setter, so I guess is the TwoWay mode that fire the setter again.
How can I fix this situation? I know that this post is a bit complicated, I'm available to answer to all questions, and for provided more details if needed.
Thanks to all, have a good day.
UPDATE #1
This is my INotifyPropertyChanged implementation:
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
UPDATE #2
The method LoadRounds is called when the user change the selection on a DataGrid, the DataGrid contains all teams, so I get the team selected by the user on the DataGrid, and then call the method LoadRounds.
All the teams are contained in a DataGrid, the ItemSource is a List<Team>.
At the end of the method LoadRounds I save the current Round of the Team on a property called SelectedRoundSaved, simply doing:
SelectedRoundSaved = Clone(SelectedRound);
in this way I prevent to reload the Rounds if the SelectedRoundSaved is equal to SelectedRound.
the Clone method allow me to clone the object, and have this structure:
public T Clone<T>(T source)
{
if (ReferenceEquals(source, null))
{
return default(T);
}
var deserializeSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace };
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source), deserializeSettings);
}
it use the NewtonSoft.Json library.
This information isn't necessary at all, but as I said I'll add all info asked from you, thanks for the attention.
Are you sure this order is correct?
1. [Item Source updated with new Rounds]
2. [SelectedRound updated from the new `Rounds` available]
3. [SelectedRound setter called again with a null value]
After the combo box is initially bound I would expect the order to be (swapped the order of #2 and #3)
1. [Item Source updated with new Rounds]
2. [SelectedRound setter called again with a null value]
3. [SelectedRound updated from the new `Rounds` available]
This behavior follows what I would expect of a combo box.
When you update the ItemSource the ComboBox dumps its items and reloads with the new collection. Because the ComboBox is a Selector, it must then check its SelectedItem. If its SelectedItem is not found in the new collection it updates its SelectedItem to be null. All of this happens just because of the OnPropertyChanged(); call in the Rounds setter.
(Note: you will only see this behavior after a combo box has been loaded and bound)
Now there are many ways you can go about handling this, but IMO the simplest is merely to change the order of operations:
public void LoadRounds(Team team)
{
//Fill the source of ComboBox with the rounds of the new team
var newRounds = team.Rounds.ToList(); //<- Create a copy, so no reference
//Get the selected round
SelectedRound = newRounds.FirstOrDefault(x => x.Id == team.CurrentRound.Id);
Rounds = newRounds;
}

Databind ComboBox SelectedValue

Currently I am setting my comboboxes datasource as such:
comboBox1.DataSource = _team;
comboBox1.DisplayMember = "Name";
comboBox1.ValueMember = "ID";
Team is a generic List of type ListItemModel which looks like:
public class ListItemModel
{
private string _name;
private short _id;
public string Name
{
get
{
return this._name;
}
}
public short ID
{
get
{
return this._id;
}
}
public ListItemModel(string name, short id)
{
this._name = name;
this._id = id;
}
}
I am then trying to databing it as such:
comboBox1.DataBindings.Add("SelectedValue", _person.Person, "TeamId", true, DataSourceUpdateMode.OnPropertyChanged);
My datasource (model) is set up:
public class PersonViewModel : INotifyPropertyChanged
{
public Person Person { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = PropertyChanged;
if (handler != null) handler(this, e);
}
}
I am able to populate the combobox correctly, the OnPropertyChanged event is actually working as I can see the data changing correctly.
The issue is two fold:
1) On initial load/set up the comboboxes selectedvalue isn't set, even though I have databound it.
2) If I change the selection in the combobox and then lose focus it doesn't retain the selected value and just shows nothing selected. The OnPropertyChanged is working as I can see the TeamId changing correctly.
I am wondering what I have missed out when it comes to databinding comboboxes
After spending all day on this and digging into how combo boxes bind data it ended up being a simple solution.
I ended up changing the public class ListItemModel Id property from short to int and it binds and selects the value correctly.
The databinding would fire, and set the SelectedValue property correctly, as I noted the databinding was always correct. After this the DisplayMember and ValueMember bindings would fire, checking the SelectedValue again. The ValueMember is of type short, not int, because of this it would find no match so set it to null.
Its weird how there was no exception or error and it just silently go to null.
Hopefully this is useful to anyone else coming up with this issue.

DataGridView doesn't update Enum in BindingList

The DataSource of my DataGridView is a BindingList<Filter> which contains an enum. The gridview contains only two columns: a regular textbox column for a string, and a combobox (drop down) column for the enum. If I bind the combobox column to the enum variable of my object I get an error. Here is the code for my objects:
public class FilterProfile
{
public FilterProfile()
{
filters = new BindingList<Filter>(); // the list that gets bound to gridview
}
public string name { get; set; }
public BindingList<Filter> filters { get; set; }
}
public class Filter
{
public string keyword { get; set; }
public FilterType type { get; set; } // the enum in question
}
public enum FilterType : int
{
SESSION = 1,
ORDER = 2,
SHIPMENT = 3
}
I have a form where the user selects a FilterProfile from a dropdown menu and then I find the appropriate FilterProfile from a global list and bind it:
foreach (PlvFilterProfile filterProfile in _filterProfiles)
{
// find the correct filter profile
if (filterProfile.name.Equals(lstFilterProfiles.Text))
{
// bind it
grdFilters.DataSource = filterProfile.filters;
break;
}
}
In order for the changes made in the DataGridView to be reflected in filterProfile.filters I need to set the DataPropertyName attribute of both columns to their respective variable (either keyword or type). This works correctly for the keyword string, but not with the type enum.
If I keep the line colFilterType.DataPropertyName = "type"; I get the error below whenever a new row is created or whenever I put my mouse over the dropdown. If I get rid of it, the type of every newly created Filter is set to 0 and never updated.
I'm not sure what causes the DataError event so don't know how to handle it or where to breakpoint.
The problem is when you focus on the new row (prepare to add a new row), a new object is required in the underlying list, this object is default by null, that value is bound to the new row and of course the ComboBoxCell can't accept that null value, causing the exception as you encountered. The solution is very simple indeed, we just need to handle the event AddingNew of the BindingList, set the default new object in there to a valid value and then it works just fine:
public FilterProfile()
{
filters = new BindingList<Filter>(); // the list that gets bound to gridview
filters.AddingNew += (s,e) => {
//the default value of FilterType is up to you.
e.NewObject = new Filter {type = FilterType.SESSION };
};
}

My dataGrid doesn't clear and doesn't refresh

I have two DataGrid, each binding in a dataSource like this :
ItemsSource="{Binding Data, ElementName=EmpSource, Mode=TwoWay}"
The first DataGrid(dgJob), contains Job and the second(dgEmp), the employee linked to the job.
I want to keep all the employees in the EmpSource, and display in the dataGrid, only those who are linked to the selected job in my first datagrid.
So I am doing this in the dgJob selectionChanged event :
dgEmp.ItemsSource = null;
var lstEmp = EmpSource.DataView.OfType<Emp>().Where(ores => ores.IdJob == itmJobSelect.IdJob).ToList();
dgEmp.ItemsSource = lstEmp;
The problem is, the dataGrid is not clearing when I change the selected line in my datagrid with the jobs, so for every job, I display every Employees in the dgEmp, while I should only display those who are connected to the job.
I can delete the line in the xaml, that determine the dataSource, but if I do this, I must refresh the dataGrid when there is a change in the dataSource.
But I don't found how to refresh it(at least for the first time) unless I write the 3 lines each time after a change in dataSource.
Can somebody help me find a solution to my problem?
Thank you.
I recommend you to use MVVM design pattern. You should load your data in view model class and store it in collection which implements INotifyCollectionChanged interface. View model should also implement INotifyPropertyChanged interface.
When your employee collection changes, you should filter second collection as in following code:
Jobs.CollectionChanged += (sender, args) =>
{
Employees = AllEmployees.Where(c=> c.IdJob == SelectedJob.IdJob);
}
You should also do same thing when SelectedJob changes and DataGrid will be refreshed.
This will work only when you will have implemented property changed notifications and correct binding was specified.
Here's example of property changed implementation which you should write:
public class ViewModel : INotifyPropertyChanged
{
public IEnumerable<Emp> Employees
{
get { return _employees; }
set
{
if (_employees != value)
{
_employees = value;
OnPropertyChanged("Employees");
}
}
}
/* ... */
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
You should also assign your view model instance to DataContext for make binding works. For example in code behind file constructor:
public void Page()
{
DataContext = new ViewModel();
InitializeComponent();
}

Create a simple DataGridView with List of objects and checkbox column (C#)

I have looked at a lot of places and I'm struggling to do this supposedly simple thing. I have a Windows form on which I've to show a simple DataGridView in this form:
| (CheckBoxColumn) | FilePath | FileState |
Now, there are a couple of problems. The data I need to bind to them is in a List of objects like this:
class FileObject
{
string filePath;
string fileState;
}
//Now here's the list of these objects which I populate somehow.
List<FileObject> listFiles;
Is there any efficient way to bind this directly to the DataGridView
so that different members of Object in the list are bound to
different columns, and for each there's checkbox?
I saw the examples of adding checkbox column to a datagrid, but none of them showed how
to add it to the 'header' row as well, so that it can behave as a 'check all'/'uncheck all' box.
Any help in how to achieve this would be great! I do write simple applications in C# but never had to work with datagrids etc. :(
Thanks!
The DataGridView control has a feature to automatically generate columns that can be set by the AutoGenerateColumns property. This property defaults to true - that is columns are by default auto generated.
However, columns are only automatically generated for public properties of the object you bind to the grid - fields do not show up.
Auto generation also works for check box columns when there is a public boolean property on the bound object.
So the simplest way to achieve your first two requirements is to change your FileObject class to this:
public class FileObject
{
public string FilePath { get; set; }
public string FileState { get; set; }
public bool Selected { get; set; }
}
If you cannot modify that class then next best would be the create a wrapper object that holds a file object:
public class FileObjectWrapper
{
private FileObject fileObject_;
FileObjectWrapper()
{
fileObject_ = new FileObject();
}
FileObjectWrapper(FileObject fo)
{
fileObject_ = fo;
}
public string FilePath
{
get { return fileObject_.filePath; }
set { fileObject_.filePath = value; }
}
public string FileState
{
get { return fileObject_.fileState; }
set { fileObject_.fileState= value; }
}
public bool Selected { get; set; }
}
You can then create your list to bind to (a BindingList is usually best) doing something like:
var fowList = new BindingList<FileObjectWrapper>();
foreach (FileObject fo in // here you have your list of file objects! )
{
fowList.Add(new FileObjectWrapper(fo));
}
dataGridView1.DataSource = fowList;
There are many ways to do the above but that is a general idea.
You can also add an unbound DataGridViewCheckBoxColumn to the grid, though I find it easier to have in the the bound list. Here is how if you do need to:
DataGridViewCheckBoxColumn c = new DataGridViewCheckBoxColumn();
c.Name = "Selected";
dataGridView1.Columns.Add(c);
Finally, for having a "SelectedAll" option in the header you will need to use custom code.
The article on CodeProject that Umesh linked to (CheckBox Header Column for DataGridView) looks quite easy to implement - they create a custom DataGridViewHeaderCell overriding the Paint and OnMouseClick methods.
Please refer the the below example, showing exactly what you are looking for
http://www.codeproject.com/Articles/20165/CheckBox-Header-Column-For-DataGridView

Categories

Resources