Databind ComboBox SelectedValue - c#

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.

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

TreeView item selection vs focus mismatch

So Microsoft has decided to make the process of programmatically selecting items in a TreeView obscenely difficult and malfunctional for some insane reason or other, and the only way to do it (since virtualization ensures that any TreeViewItem you try to select doesn't currently exist) is to create a boolean IsVisible property on whatever you want to use for your source data, which by the way has to implement INotifyPropertyChanged and include an event handler now if it didn't before, and then add an ItemContainerStyle to your TreeView binding the property to the IsVisible property of the TreeViewItem.
What this doesn't do however, is set focus to the selected item, so if for example your goal was to let the user delete tree items with the keyboard and have the focus automatically shift to the deleted item's parent so the user doesn't have to continuously tab through items or click on things to get their place back, this is nearly useless. It somehow doesn't even avoid the virtualization problem, allowing you to set the selection to something that it claims in the next line of code doesn't exist.
Here's my XAML:
and relevant C#:
public partial class MainWindow : Window
{
public ObservableCollection<TreeEdit> Source { get; set; }
public MainWindow()
{
Source = new ObservableCollection<TreeEdit>();
Source.Add(new TreeEdit("Hi"));
DataContext = this;
InitializeComponent();
}
private void KeyNav(object sender, KeyEventArgs e)
{
TreeEdit Selection = (sender as TreeView).SelectedItem as TreeEdit;
ObservableCollection<TreeEdit> TSource = (ObservableCollection<TreeEdit>)(sender as TreeView).ItemsSource;
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
if (e.Key == Key.Left && Selection != null)
{
if (Selection.Parent == null)
{
TSource.Remove(Selection);
}
else
{
Selection.Parent.IsSelected = true;
((TreeViewItem)((sender as TreeView).ItemContainerGenerator.ContainerFromItem(Selection.Parent))).Focus();
Selection.Parent.Remove(Selection);
}
}
}
}
}
public class TagEdit : INotifyPropertyChanged
{
public string Name
{
get
{
return name;
}
set
{
OnPropertyChanged("Name");
name = value;
}
}
private string name;
public bool IsSelected
{
get
{
return selected;
}
set
{
OnPropertyChanged("IsSelected");
selected = value;
}
}
private bool selected;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public TagEdit Parent;
public ObservableCollection<TagEdit> Children { get; set; }
public TagEdit(string n)
{
Name = n;
Children = new ObservableCollection<TagEdit>();
}
public void Remove(TagEdit tag)
{
Children.Remove(tag);
}
}
The idea is that the user can navigate the TreeView normally with the arrow keys, then use Ctrl+Left to delete the selected item and select its parent. (sender as TreeView).ItemContainerGenerator.ContainerFromItem(Selection.Parent) often returns null. Removing it causes the proper item to be selected, but the TreeView loses focus. When it doesn't return null, I get the expected behavior.
Also, despite both the TreeView's KeyboardNavigation.TabNavigation and KeyboardNavigation.ControlTabNavigation being set to the default value of Continue, they behave differently (Tab ignores the TreeView's component elements while Ctrl+Tab steps into them).
I've been trying to get something, anything to work for almost a week now and if my opening paragraph didn't tip you off I've long since worn out my patience. Please don't tell me to try anything unless you have already, personally typed exactly what you're about to tell me to try into VisualStudio and it worked.
Apologies for the harsh tone, but this problem went beyond ridiculous and into obscene some time ago.
The WPF TreeView doesn't have a single ItemContainerGenerator. Every item in a tree view is an ItemsControl and thus has its own ItemContainerGenerator for its child items. What you really need to do is to get the grand parent of the item you are going to delete and use THAT ItemContainerGenerator to call ContainerFromItem.
It's not working because you are trying to use the top level ItemContainerGenerator which only contains the top level items.
P.S. on a friendly side note :), who deletes items with a Ctrl+Left? That's undiscoverable. Why not just do this behavior when they hit Delete?

Associate two classes so a combobox appears with the second class

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

Gridview doesn't change when data changes

I've added an observable data and bound it to my data grid as follows.
private ObservableCollection<Order> _allOrders;
public ObservableCollection<Order> AllOrders
{
get { return _allOrders;}
set { _allOrders = value; OnPropertyChanged(); }
}
public Presenter() { _allOrders = new ObservableCollection<Order>(...); }
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] String propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
When I set breakpoint on the event that is supposed to filter the data, I set the property AllOrder to null. I can verify using the watch that it's set to that. However, the view isn't updated, so I'm guessing that I forgot something. The view model class Presenter implements INotifyPropertyChanged interface, of course.
What's missing?
Edit
The XAML code for the grid looks as follows.
<DataGrid x:Name="dataGrid"
ItemsSource="{Binding AllOrders}"
AutoGeneratingColumn="DataGrid_OnAutoGeneratingColumn" ...>
Assuming that you set DataContext accordingly and AllOrders binding works initially if you want to filter items in the UI, without change collection, it's much easier when you use ListCollectionView with a Filter. WPF does not bind directly to collection but to a view - MSDN.
private readonly ObservableCollection<Order> _allOrders;
private readonly ListCollectionView _filteredOrders;
public ICollectionView FilteredOrders
{
get { return _filteredOrders; }
}
public Presenter()
{
_allOrders = new ObservableCollection<Order>(...);
_filteredOrders = new ListCollectionView(_allOrders);
_filteredOrders.Filter = o => ((Order)o).Active;
}
and in XAML
<DataGrid ... ItemsSource="{Binding FilteredOrders}">
when you want to manually refresh UI just call Refresh
_filteredOrders.Refresh();
Apart from that nothing changes in the view model. You still add/remove items to _allItems and changes should be picked up automatically by UI
Do you set the property AllOrders only in the constructor? If so, then do not set the field _allOrders but the property AllOrders. If you set the field then notification is never raised.

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

Categories

Resources