React on object changed in ObservableCollection - c#

I have an ObservableCollection<Person> in my viewmodel. This is bound as an ItemsSource to a DataGrid in the view. The class Person only has threeProperties:
public class Person : ViewModelBase
{
private Guid id;
public Guid Id
{
get { return this.id; }
set
{
this.id = value;
OnPropertyChanged("Id");
}
}
private string firstname;
public string Firstname
{
get { return this.firstname; }
set
{
this.firstname = value;
OnPropertyChanged("Firstname");
}
}
private string lastname;
public string Lastname
{
get { return this.lastname; }
set
{
this.lastname = value;
OnPropertyChanged("Lastname");
}
}
}
The class ViewModelBase implements INotifyPropertyChanged.
The items in the collection are updated perfect if I add or remove an entry in the dategrid. The item is then also removed in the collection.
My problem is that the content of an person-item is updated, but I don't know how I can react on this.
Do I have to add an event or something else to the person-class to get informed or is there another way to do this?

Implement INotifyPropertyChanged interface on your class Person so that any change in Person properties gets reflected back on UI.
Sample -
public class Person : INotifyPropertyChanged
{
private Guid id;
public Guid Id
{
get { return id; }
private set
{
if(id != value)
{
id = value;
NotifyPropertyChanged("Id");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}

Related

DataBinding on Getter Only Property That Concats 2 variables

I have the following classes
public abstract class Contact
{
public abstract string FullName { get; }
public abstract string FullName_LastNameFirst { get; }
}
public class PersonContact
{
private string firstName;
private string lastName;
public override string FullName => firstName + " " + lastName;
public override string FullName_LastNameFirst => lastName + ", " + firstName;
}
public class BusinessContact
{
private string name;
public override string FullName => name;
public override string FullName_LastNameFirst => name;
}
These classes extend INotifyPropertyChanged (not shown) and include public properties wrapping the private variables that trigger OnPropertyChanged.
The question is if I bind in WPF to the FullName or FullName_LastNameFirst properties how can I have them update when either of the properties that they are wrapping are changed.
When you change underlying private fields (firstName, lastName or name in BusinessContact) - call
OnPropertyChanged("FullName");
OnPropertyChanged("FullName_LastNameFirst");
WPF data binding will subscribe to PropertyChanged event of your object and will listen to change notifications of corresponding property. Since your property has no setter in which you can call OnPropertyChanged - you need to call it explicitly when any underlying data changes.
You surely have properties for your Person. If you have a ForeName property you can implement it like:
private string _ForeName;
public string ForeName
{
get
{ return _ForeName; }
set
{
if (_ForeName != value)
{
_ForeName = value;
OnPropertyChanged(nameof(this.ForeName));
OnPropertyChanged(nameof(this.FullName));
}
}
}
As you can see, a PropertyChanged event will be fired, if the value of ForeName is changed. You can make the LastName property similar.
An other solution is, if the person class listens to its own PropertyChanged:
public Person()
{
this.PropertyChanged += this.Person_PropertyChanged;
}
private void Person_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(LastName))
{
this.OnPropertyChanged(nameof(FullName));
}
if (e.PropertyName == nameof(ForeName))
{
this.OnPropertyChanged(nameof(FullName));
}
}
This second solution is useful if you have generated classes, and you cannot change the property setters. Then you can make a partial class, and handle the property changes.
The answers from Evk and lvoros are technically correct.
However, you can save yourself from having to write all this boilerplate code by using the Fody ProperyChanged library (available via Nuget).
A class written as
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string GivenNames { get; set; }
public string FamilyName { get; set; }
public string FullName => $"{GivenNames} {FamilyName}";
}
Will actually be compiled as
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
string givenNames;
public string GivenNames
{
get => givenNames;
set
{
if (value != givenNames)
{
givenNames = value;
OnPropertyChanged("GivenNames");
OnPropertyChanged("FullName");
}
}
}
string familyName;
public string FamilyName
{
get => familyName;
set
{
if (value != familyName)
{
familyName = value;
OnPropertyChanged("FamilyName");
OnPropertyChanged("FullName");
}
}
}
public string FullName => $"{GivenNames} {FamilyName}";
public virtual void OnPropertyChanged(string propertyName)
{
var propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Fody will automatically generate OnPropertyChanged() calls for any dependent calculated properties in addition to the direct property wrappers.
I'm not normally a fan of programs working by "magic", but Fody is my one expception.

XAML Gridview not rebinding on RaisePropertyChanged

EDIT: This is a Windows Store (8.1) application
I have a person class as shown below which I am using as a model
public class Person : BaseModel
{
private string _FirstName;
public string FirstName
{
get { return _FirstName; }
set
{
if (_FirstName == value)
return;
_FirstName = value;
RaisePropertyChanged("FirstName");
}
}
private string _MiddleName;
public string MiddleName
{
get { return _MiddleName; }
set
{
if (_MiddleName == value)
return;
_MiddleName = value;
RaisePropertyChanged("MiddleName");
}
}
private string _LastName;
public string LastName
{
get { return _LastName; }
set
{
if (_LastName == value)
return;
_LastName = value;
RaisePropertyChanged("LastName");
}
}
}
where BaseModel is defined as below
using GalaSoft.MvvmLight;
public class BaseModel: ObservableObject
{
}
I am using the Model in a ViewModel class as shown below.
using GalaSoft.MvvmLight;
public class PersonViewModel : ViewModelBase
{
/// <summary>
/// List of searched People
/// </summary>
private ObservableCollection<Person> _People;
public ObservableCollection<Person> People
{
get { return _People; }
set
{
if (_People== value)
return;
_People= value;
RaisePropertyChanged("People");
}
}
}
I am binding the People collection to a GridView as shown below.
<GridView
x:Name="PeopleSearchResultsGridView"
ItemsSource="{Binding People}">
</GridView>
When the search completes I get back a list of people which I add to the list as follows.
var list = await p.SearchPeople();
People = new ObservableCollection<Person>(list);
I see that the setter for the People collection is firing and the RaisePropertyChanged("People") event is also firing however that is not updating the GridView. Can anyone tell me what is wrong here ?

Is there a way to trigger some kind of `OnPropertyChanged` event for a computed property that uses a property of a "child entity" in a collection?

Is there a way to trigger some kind of OnPropertyChanged event for a computed property that uses a property of a "child entity" in a collection?
A small example:
I have a simple WPF application with a DataGrid showing Customer properties.
I am using Entity Framework 5, CodeFirst approach, so I wrote my classes manually with my own INotifyPropertyChanged implementation.
public partial class Customer : INotifyPropertyChanged
{
private string _firstName;
public virtual string FirstName
{
get { return _firstName; }
set
{
_firstName = value;
OnPropertyChanged("FirstName");
OnPropertyChanged("FullName");
}
}
private string _lastName;
public virtual string LastName
{
get { return _lastName; }
set
{
_lastName = value;
OnPropertyChanged("LastName");
OnPropertyChanged("FullName");
}
}
public virtual ICollection<Car> Cars { get; set; }
public virtual ICollection<Invoice> Invoices { get; set; }
...
}
Now in that same class I created 3 computed properties:
public string FullName
{
get { return (FirstName + " " + LastName).Trim(); }
}
public int TotalCars
{
get
{
return Cars.Count();
}
}
public int TotalOpenInvoices
{
get
{
if (Invoices != null)
return (from i in Invoices
where i.PayedInFull == false
select i).Count();
else return 0;
}
}
The FullName is automatically updated in the DataGrid because I'm calling OnPropertyChanged("FullName");
I found an example of the INotifyCollectionChanged implementation that I can probably use to auto update the TotalCars when something is added to or removed from the ICollection:
http://www.dotnetfunda.com/articles/article886-change-notification-for-objects-and-collections.aspx
But what is the best approach to trigger the OnPropertyChange("TotalOpenInvoices") when a property (PayedInFull) inside the ICollection (Invoices) changes?
Doing something like OnPropertyChanged("Customer.TotalOpenInvoices"); in the Invoice class doesn't seem to do the trick... :)
Do you have control of what goes inside your collections? If you do, you can create a Parent property on your Invoice object and when it is added to the collection, set the parent to your Customer. Then when PaidInFull gets set, run your Customer.OnPropertyChanged("TotalOpenInvoices") or call a method on the Customer object to recalculate your invoices.
Enforcing parent-child relationship in C# and .Net
This assumes that Invoice and Customer both implement IPropertyChanged. Simply change your collection to an ObservableCollection, and watch the CollectionChanged property.
When new Invoices are added, hook up an event handler to the PropertyChanged event of that Invoice. When an item is removed from the collection, remove that event handler.
Then, just call your NotifyPropertyChanged function on your TotalOpenInvoices property.
For example (not completely tested, but it should be close):
Invoice
public class Invoice : INotifyPropertyChanged
{
private bool payedInFull = false;
public bool PayedInFull
{
get { return payedInFull; }
set
{
payedInFull = value;
NotifyPropertyChanged("PayedInFull");
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
Customer
public class Customer : INotifyPropertyChanged
{
public Customer()
{
this.Invoices.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Invoices_CollectionChanged);
}
ObservableCollection<Invoice> Invoices
{
get;
set;
}
public int TotalOpenInvoices
{
get
{
if (Invoices != null)
return (from i in Invoices
where i.PayedInFull == false
select i).Count();
else return 0;
}
}
void Invoices_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach (Invoice invoice in e.NewItems)
{
invoice.PropertyChanged += new PropertyChangedEventHandler(invoice_PropertyChanged);
NotifyPropertyChanged("TotalOpenInvoices");
}
foreach (Invoice invoice in e.OldItems)
{
invoice.PropertyChanged -= new PropertyChangedEventHandler(invoice_PropertyChanged);
NotifyPropertyChanged("TotalOpenInvoices");
}
}
void invoice_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "PayedInFull")
NotifyPropertyChanged("TotalOpenInvoices");
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
Note that each class could be abstracted to a single class that implements INotifyPropertyChanged, such as a ViewModelBase class, but that is not necessarily required for this example.

Data binding to custom object

I have a simple custom class (Person), which I want to bind to a label as a whole (not to separate properties of this class). The label should just present whatever the Person.ToString() returns (in this case FirstName + LastName).
How do I properly bind it using the person as a Source.
How do I make sure that any change in one of the properties of the Person will be reflected in the label?
public class Person : INotifyPropertyChanged {
private string firstName;
public string FirstName {
get {
return firstName;
}
set {
firstName = value;
OnPropertyChanged("FirstName");
}
}
private string lastName;
public string LastName {
get {
return lastName;
}
set {
lastName = value;
OnPropertyChanged("LastName");
}
}
public override string ToString() {
return FirstName + " " + LastName;
}
private void OnPropertyChanged(string name) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
public Window1() {
myPerson = new Person() {
FirstName = "AAA",
LastName = "BBB"};
InitializeComponent();
}
public Person MyPerson {
get {
return myPerson;
}
set {
myPerson = value;
}
}
Label Content="{Binding Source=MyPerson}"
Create a new property FullName which returns the full name and raise PropertyChanged for FullName in the setters of FirstName and LastName as well. You should never bind to the object itself.

My listbox doesn't update

I have a listbox to which I'm binding HistoryItems , where HistoryItems is a ObservableCollection of History.
Here is the listbox declaration :
<ListBox x:Name="RecentListBox" SelectionChanged="RecentListBox_SelectionChanged" ItemsSource="{Binding HistoryItems,Converter={StaticResource HistoryValueConverter} }" ItemsPanel="{StaticResource ItemsPanelTemplate1_Wrap}" ItemTemplate="{StaticResource RecentViewModelTemplate}">
Here is the History class :
public class History : INotifyPropertyChanged
{
public History() { }
int _id;
string _date;
string _url;
string _name;
public History(int id, string date,string url,string name)
{
this.id = id;
this.date = date;
this.url = url;
this.name = name;
}
public int id
{
get
{
return _id;
}
set
{
if (value != _id)
{
_id = value;
NotifyPropertyChanged("id");
}
}
}
public string date
{
get
{
return _date;
}
set
{
if (!value.Equals(_date))
{
_date = value;
NotifyPropertyChanged("string");
}
}
}
public string url
{
get
{
return _url;
}
set
{
if (!value.Equals(_url))
{
_url = value;
NotifyPropertyChanged("url");
}
}
}
public string name
{
get
{
return _name;
}
set
{
if (!value.Equals(_name))
{
_name = value;
NotifyPropertyChanged("name");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
// App.viewModel.HistoryItems = (App.Current as App).dataHandler.retrieveHistory_DB();
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
When I start the app the list gets populated, but after I do some modifs in some pivot children, and go back to the main panorama view, I try to update the HistoryItems in OnNavigatedTo :
App.ViewModel.HistoryItems = (App.Current as App).dataHandler.retrieveHistory_DB();
but the listbox doesn't get updated (and the function returns the correct data). What could the problem be? History is INotifyPropertyChanged and the HistoryItems is a ObservableCollection<History> so there should be no problem.. What is causing the list to not update?
Since you are replacing HistoryItems when you refresh it doesn't matter that it's an ObservableCollection.
You can either clear the HistoryItems and then add the new items when you refresh. Or the ViewModel should implement INotifyPropertyChanged and the HistoryItems setter should raise the event.
Rewrite the setter for ViewModel.HistoryItems so that instead of doing this
_historyItems = value;
it does this
if (_historyItems == null)
_historyItems = new ObservableCollection<HistoryItem>();
_historyItems.Clear();
foreach (var hi in value)
_historyItems.Add(hi);
You need NotifyPropertyChanged for App.ViewModel in the Setter of HistoryItems

Categories