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;
}
Related
I have some objects which are loaded from an SQLite database into a separate list. The user can select an item from that list via combobox and edit its data in a subform. I work with Binding so that the list and its item is immediately updated on every change. Furthermore, I have a LastModified field (DateTime) in order to see the time of the last change which is set via my item SaveToDB() method.
Now, I am wondering how to handle the database update as well as the display of updated bound values correctly and have 3 open questions:
How am I able to store the old item values in order to compare those with the final edit (after leaving the edit control, like a TextBox) without reading the database entry again? As all bound variables are immediately changed with the edit I think I would need a copied item object when the object is loaded and saved inbetween but which is not bound itself. How can I create such a copy?
What is the best event to fire the comparison and database update? I think it is Validated() but I am not sure. It definitely is not the Changed() event as here a database update would be triggered after every keystroke, even when the changes are undone again before leaving the edit control (for which I want to use the comparison in point 1 to get rid of unnecessary database updates).
Why is my bound DateTime label not updated when I call the entity SaveToDB() method? Do you need to rebind every control after each code internal change of object properties? Seriously? I find it also a bit stupid to explicitely update the combobox.Text while the updated data is correctly displayed when I dropdown the combobox. Really strange behaviour.
Code:
public class Entity {
public int ID { get; set; }
public string DateTime { get; set; }
public string Name { get; set; }
...
public void SaveToDB() {
DateTime = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
if (DB.Read(QueryRead()) == null) {
DB.Write(QueryAdd());
} else {
DB.Write(QueryUpdate());
}
}
}
public class Entities : SortableBindingList<Entity> {
...
}
public class EntityStore {
public Entities Entities;
public BindingSource Source; // used for combobox.datasource in selector-form
// singleton
...
public EntityStore() {
Entities = new Entities();
Source = new BindingSource() { DataSource = Entities };
ReadAllFromDB(); // fills Entities
}
...
public void Update(Entity entity) {
entity.SaveToDB();
}
}
public partial class FormSelector : Form {
// FormEntity gets the selected entity via its parent form:
FormEntity formEntity;
...
cbxEntity.DataSource = EntityStore.Current.Source;
formEntity.SetEntity(EntityStore.Current.Entities.FirstOrDefault(x => x.ID == ((Entity)((GridViewRowInfo)(cbxEntity.SelectedItem)).DataBoundItem).ID));
...
}
public partial class FormEntity : Form {
Entity entity;
...
public void SetEntity(Entity entity) {
this.entity = entity;
tbxID.DataBindings.Add(new Binding("Text", entity, "ID", false));
lblDateTime.DataBindings.Add(new Binding("Text", entity, "DateTime", false));
tbxName.DataBindings.Add(new Binding("Text", entity, "Name", false));
}
private void tbxName_Validated(object sender, EventArgs e) {
// missing comparison logic
// ...
// I could also directly call entity.SaveToDB(); here but I would like
// to keep the logic flow separated as follows:
// DB <- Entity <- EntityStore <- Form(s)
EntityStore.Current.Update(entity);
}
}
Sidenote/Question: I also stumbled over the INotifyPropertyChanged event but I do not understand if its used in winforms and for what purpose.
To prevent a stack overflow exception by implementing INotifyPropertyChanged you additionally need to use private properties when getting and setting public properties.
I have a method that queries a database using entity framework and places the results in an ICollectionView. The ICollectionView acts as the ItemsSource for a DataGrid. Everything works fine on the first query, but upon querying a second time, the data is not properly sorted, despite the application of the correct SortDescriptions.
Here is my code for trying querying and grouping/sorting the data:
CollectionViewSource cvsRS;
private ObservableCollection<productorder> rs;
public ObservableCollection<productorder> RS
{
get { return rs; }
set
{
if (rs != value)
{
rs = value;
OnPropertyChanged("RS");
}
}
}
private ICollectionView rsView;
public ICollectionView RSView
{
get { return rsView; }
set
{
if (rsView != value)
{
rsView = value;
OnPropertyChanged("RSView");
}
}
}
public void QueryDatabase()
{
RS = new ObservableCollection<productorder>(DatabaseEntities.productorders.Where(o => o.month.id == CurrentMonth.id));
if (RS != null)
{
cvsRS.Source = RS;
RSView = cvsRS.View;
RSView.GroupDescriptions.Clear();
RSView.GroupDescriptions.Add(new PropertyGroupDescription("producttype.productcategory.name"));
RSView.GroupDescriptions.Add(new PropertyGroupDescription("producttype.name"));
RSView.SortDescriptions.Clear();
RSView.SortDescriptions.Add(new SortDescription("producttype.productcategory.sortorder", ListSortDirection.Ascending));
RSView.SortDescriptions.Add(new SortDescription("client.name", ListSortDirection.Ascending));
RSView.Refresh();
CurrentRecord = null;
SelectedRecords = null;
}
}
The grouping works fine, but the groups aren't in the correct order based on the sorting. I've tried a number of possible "fixes" with no success (e.g. adding sort/group descriptions directly to the CollectionViewSource, sorting before grouping, removing some of the sorting/grouping, removing the SortDescriptions per CollectionViewSource does not re-sort on property change).
Does anyone know how to maintain the sort order regardless of how many queries are performed? I'm open to alternative methods of querying displaying the data in the DataGrid if that may work.
Try binding your CollectionViewSource.Source property to your ObservableCollection<T> property. Set up the binding in the viewmodel constructor. Then, just leave it alone. Update the ObservableCollection<T>, replace it, etc. As long as it's an ObservableCollection<T> and its public property raises PropertyChanged whenever you replace it, the whole thing will work.
public MyViewModel()
{
BindCollectionViewSource();
}
protected void BindCollectionViewSource()
{
cvsRS = new CollectionViewSource();
var binding = new Binding
{
Source = this,
Path = new PropertyPath("RS")
};
BindingOperations.SetBinding(cvsRS, CollectionViewSource.SourceProperty, binding);
}
// Since we're not going to be messing with cvsRS or cvsRS.View after the
// constructor finishes, RSView can just be a plain getter. The value it returns
// will never change.
public ICollectionView RSView
{
get { return cvsRS.View; }
}
You can't just assign a binding to Source; there's more to it than that. The Source="{Binding RSView}" stuff you see in XAML may look like an assignment, but some details are being hidden for convenience. The Binding actively does stuff. It needs to know who the target object is.
I did see one funny thing: I gave my test code one PropertyGroupDescription and one SortDescription. When I added items to the collection, it sorted them within the groups. Then when I called RSView.Refresh(), it resorted them without reference to the groups. Not sure I understood what it was doing there.
I'm building an MVVM Light WPF app in Visual Studio 2015 with Entity Framework 6 (EF) providing the data. I have a ComboBox that displays the reasons why someone needs to take a drug test and it looks like this:
<ComboBox ItemsSource="{Binding ReasonsForTest}"
SelectedItem="{Binding Path=ReasonsForTestVm,
UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="Description" />
The ReasonsForTest is of type ReasonForTestViewModel class:
public class ReasonForTestViewModel: ViewModelBase
{
private int _ReasonForTestId;
private string _ReasonForTestAbbr;
private string _description;
public int ReasonForTestId
{
get { return _ReasonForTestId; }
set
{
if (value == _ReasonForTestId) return;
_ReasonForTestId = value;
RaisePropertyChanged();
}
}
public string ReasonForTestAbbr
{
get { return _ReasonForTestAbbr; }
set
{
if (value == _ReasonForTestAbbr) return;
_ReasonForTestAbbr = value;
RaisePropertyChanged();
}
}
public string Description
{
get { return _description; }
set
{
if (value == _description) return;
_description = value;
RaisePropertyChanged();
}
}
}
I have a data service class that contains the following code to fetch the data for the valid values of the ComboBox:
public async Task<ObservableCollection<ReasonForTestViewModel>> GetReasonsForTest()
{
using (var context = new MyEntities())
{
var query = new ObservableCollection<ReasonForTestViewModel>
(from rt in context.ReasonForTests
orderby rt.description
select new ReasonForTestViewModel
{
ReasonForTestId = rt.ReasonForTestID,
ReasonForTestAbbr = rt.ReasonForTestAbbr,
Description = rt.description,
});
return await Task.Run(() => query);
}
}
The view model populates the ComboBox using this:
var dataService = new TestDataService();
ReasonsForTest = await dataService.GetReasonsForTest();
The ComboBox has the correct data; however, it's not selecting the correct value when the app starts -- it's showing blank on load. The SelectedItem (ReasonsForTestVm) is also of that class type ReasonForTestViewModel and gets populated from the database with the one item for this person. I've stepped through the code to ensure ReasonsForTestVm has the correct data, and it does.
Here's the property for ReasonsForTestVm:
public ReasonForTestViewModel ReasonForTestVm
{
get
{
return _reasonForTestVm;
}
set
{
if (Equals(value, _reasonForTestVm)) return;
_reasonForTestVm = value;
RaisePropertyChanged();
}
}
What am I doing wrong here? I'm about to lose my mind!
Update: Sorry for the confusing name in the property above. Fixed.
Any WPF items control that extends Selector (such as ComboBox and ListBox) has two properties that are often used in conjunction: ItemsSource and SelectedItem.
When you bind a collection to ItemsSource, a representation of those items are shown in the UI. Each one of the representations is bound to an instance found within the collection bound to ItemsSource. If, for an example, you're using a DataTemplate to create that representation, you'll find within each that the DataContext will be one of those instances from the collection.
When you select one of these representations, the SelectedItemproperty now holds the instance from the collection that was bound to that representation.
This works perfectly through user interaction with the UI. However, there's one important caveat when interacting with these controls programmatically.
It's a very common pattern to bind these properties to similar properties in your view model.
public class MuhViewModel
{
public MuhItems[] MuhItems {get;} = new[]{ new Item(1), new Item(2) };
// I don't want to show INPC impls in my sample code, kthx
[SuperSlickImplementINotifyPropertyChangedAttribute]
public MuhSelectedItem {get;set;}
}
bound to
<ComboBox ItemsSource="{Binding MuhItems}"
SelectedItem="{Binding MuhSelectedItem}" />
If you try to manually update the selected item this way...
muhViewModel.MuhSelectedItem = new Item(2);
The UI will not change. The Selector sees that ItemsSource has changed, yes, but it doesn't find that instance in the ItemsSource collection. It doesn't know that one instance of Item with a value of 2 is equivalent to any other Item with the same value. So it does nothing. (That's a bit simplistic for what really happens. You can bust out JustDecompile and see for yourself. It gets real convoluted down in there.)
What you should be doing in this situation is updating SelectedItem with an instance found within the collection bound to ItemsSource. In our example,
var derp = muhViewModel.MuhItems.FirstOrDefault(x => x.MuhValue == 2);
muhViewModel.MuhSelectedItem = derp;
Side note, when tracking instances within a debug session, it helps to use Visual Studio's Make Object ID feature.
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);
I am binding a WPF application DataGrid to an ObservableCollection via the DataGrid's "ItemSource". Initially the DataGrid does come up with headings and values, however the upgrades made to the ObservableCollection are not reflected? (i.e. when I come back programmatically and increase the "Total" value) The ObservableCollection I am using is below.
Any ideas why & how to get the grid to dynamically update/bind correctly?
public class SummaryItem
{
public string ProcessName { get; set; }
public long Total { get; set; }
public long Average { get; set; }
public static SummaryItem ObservableCollectionSearch(ObservableCollection<SummaryItem> oc, string procName)
{
foreach (var summaryItem in oc)
{
if (summaryItem.ProcessName == procName) return summaryItem;
}
return null;
}
}
EDIT - Or perhaps an add-on question is whether in this case DataGrid isn't the control I should be using to visualize what is effectively an in-memory table? That is the observableCollection of SummaryItem's is effectively the in-memory table.
If I see it right you are using an ObservableCollection. If you add items to the ObservableCollection these changes should always been reflected by WPF, but if you edit properties on an item (i.e. changing the "Total" value of a SummaryItem) this is no change to the ObservableCollection but to the SummaryItem.
To achieve the desired behaviour your SummaryItems have to implement the INotifyPropertyChanged interface to "notify" WPF when propertys are changed:
// implement the interface
public event PropertyChangedEventHandler PropertyChanged;
// use this for every property
private long _Total;
public long Total {
get {
return _Total;
}
set {
_Total = value;
if(PropertyChanged != null) {
// notifies wpf about the property change
PropertyChanged(this, new PropertyChangedEventArgs("Total"));
}
}
}
you have just ran into the classic problem with ObservableCollection. Only item add and item remove events are fired for OC. That means, if an item changes, you do NOT get an "ItemChanged" event.
ObservableCollection only raise event when you add or remove items, if you need to raise event even if any item inside the collection changes use BindingList.