I have a ListView bound to a collection, and I want the ListView to automatically update when an item is added to the collection. I managed to get it working using an ObservableCollection, but I'd rather to use INotifyPropertyChanged instead. Maybe you can give me a hint what I am doing wrong?
First, here is the (relevant part of) XAML:
<StackPanel DataContext="{Binding Family}"> <!-- DataContext is of type Family -->
<TextBlock Text="{Binding LastName}"/>
<ListView ItemsSource="{Binding Members}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FirstName}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
Here are the relevant classes:
public class Family : INotifyPropertyChanged
{
public string LastName { get; private set; }
private readonly IList<Member> _members;
public IEnumerable<Member> Members { get => _members; }
public Family(string lastName, IEnumerable<Member> members)
{
LastName = lastName;
_members = members.ToList();
}
public void AddMember(string name)
{
var member = new Member { FirstName = name };
_members.Add(member);
OnPropertyChanged(nameof(Members));
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
public class Member
{
public string FirstName { get; set; }
}
If I use this code and call AddMember somewhere, it will not update the ListView GUI. I don't see why not, because AddMember calls OnPropertyChanged(nameof(Members)), and Members is what the ListView is bound to. So it should get notified about the change.
So what am I doing wrong?
If I change IList<Member> _members into ObservableCollection<Member> _members and _members = members.ToList() into _members = new ObservableCollection<Member>(members) accordingly, it works as expected.
After adding an item to the _members collection, the reference returned by Members is still the same. The Equals method of collections will usually compare references, not items. Consequently, the binding will not detect a change and does not reevaluate the property.
If you want to get this to work, you could do one of the following:
Assign null temporarily, raise property changed, reassign the collection and raise property changed again, so the binding detects a changed reference (thanks to #Ash).
public void AddMember(string name)
{
var member = new Member { FirstName = name };
_members.Add(member);
var members = _members;
_members = null;
OnPropertyChanged(nameof(Members));
_members = members;
OnPropertyChanged(nameof(Members));
}
Naive approach, recreate the collection when you add a member, e.g:
public void AddMember(string name)
{
var member = new Member { FirstName = name };
_members = _members.ToList();
_members.Add(member);
OnPropertyChanged(nameof(Members));
}
This is costly due to lots of unnecessary allocations, don't do it.
As you can see, both approaches have their downsides, either firing additional property changed notifications or unnecessary allocations which will additionally cause the ListView to remove and recreate all of its items each time. This is why there is an ObservableCollection<T> type that implements INotifyCollectionChanged, which allows notifying added and removed items specifically, as well as other operations.
First of all, it is good that you decided that a Family is not an ObservableCollection. After all, you can do a lot of things with ObservableCollections that can't be done in Families. For instance: what would Replace(Member) mean in the context of a family?
The problem is, that you forgot to implement INotifyCollectionChanged.
With this interface you can notify others that you added an element (and moved, and deleted, etc.)
public class Family : INotifyPropertyChanged, INotifyCollectionChanged
{
...
Because you also have to notify if elements are moved / deleted / etc. This will cost some development effort if your family can do more than just Add.
Therefore it might be a good idea to change your
private readonly IList<Member> _members;
into an ObservableCollection<Member>, and implement the interface via this ObservableCollection.
class Family : INotifyPropertyChanged, INotifyCollectionChanged
{
public string LastName { get; private set; }
private readonly ObservableCollection<Member> members;
public IEnumerable<Member> Members => this.members;
public event NotifyCollectionChangedEventHandler CollectionChanged
{
add => this.members.NotifyCollectionChangedEventHandler.CollectinChanged += value;
remove => this.members.NotifyCollectionChangedEventHandler.CollectinChanged -= value;
}
Now your Add / Remove / Replace / Move / etc methods will be one-liners; the appropriate event will be raised.
public void Add(Member member)
{
this.members.Add(member);
}
public void Remove(Member member)
{
this.members.Remove(member);
}
Not sure if you need methods to Move and Replace family Members, but even if you need to, they will also be one-liner calls to the corresponding ObservableCollection method
You have completely hidden that you are using an ObservableCollection<Member>, so if in future you need to completely get rid of the ObservableCollection, your users won't have to change, as long as you promise to implement the interfaces.
Related
The question may be dumb but I am a newbie in WPF, so I am sorry.
I found a lot of similar questions here, but none of them helped.
I have a form that gets some object with a Location property as input.
I want this Location property to be selected in the ComboBox when the form is shown.
I also want to make this property changeable during the lifecycle of the form.
I madea binding in this way:
<ComboBox Name="CB_Location" ItemsSource="{Binding Locations}" SelectedItem="{Binding SelectedLocation}" DisplayMemberPath="LocationName" SelectedValuePath="LocationID" SelectionChanged="CB_Location_SelectionChanged"/>
public class Location
{
public int LocationID { get; set; }
public string LocationName { get; set; }
}
public Form(object _obj)
{
InitializeComponent();
lctrl = new LocationController();
Locations = lctrl.GetAllLocations();
SelectedLocation = lctrl.GetLocationById(_obj.LocationID);
DataContext = this;
}
public List<Location> Locations { get; set; }
public Location SelectedLocation; { get; set; }
The ComboBox is populated with objects correctly, but I cannot set the SelectedItem property correctly.
The issue why the selected item is not set is because SelectedLocation is a field, which you cannot bind. For more information about binding sources, you can refer to the documentation.
You can bind to public properties, sub-properties, as well as indexers, of any common language runtime (CLR) object. The binding engine uses CLR reflection to get the values of the properties. Alternatively, objects that implement ICustomTypeDescriptor or have a registered TypeDescriptionProvider also work with the binding engine.
In order to make your current code work, just make SelectedLocation a public property.
public Location SelectedLocation { get; set; }
Apart from that, if you only want to bind the selected item, setting a SelectedValuePath is useless.
<ComboBox Name="CB_Location"
ItemsSource="{Binding Locations}"
SelectedItem="{Binding SelectedLocation}"
DisplayMemberPath="LocationName"
SelectionChanged="CB_Location_SelectionChanged"/>
If you wanted to bind the SelectedValue where the SelectedValuePath is applicable instead, you would have to expose a property that matches the type of the selected value path, here int.
public int SelectedLocationID { get; set; }
Then you can bind this the SelectedValue with value path LocationID (without SelectedItem).
<ComboBox Name="CB_Location"
ItemsSource="{Binding Locations}"
DisplayMemberPath="LocationName"
SelectedValuePath="LocationID"
SelectedValue="{Binding SelectedLocationID}"
SelectionChanged="CB_Location_SelectionChanged"/>
Another note on updating properties. It seems that you do not implement the INotifyPropertyChanged interface. If you set Location for instance, the user interface (here the ComboBox) will not reflect the change, as it does not get notified. Therefore, if you intend to change the Location or other properties bound in your form, you have to implement INotifyPropertyChanged, e.g.:
public class YourViewModel : INotifyPropertyChanged
{
private Location _selectedLocation;
public Location SelectedLocation
{
get => _selectedLocation;
set
{
if (_selectedLocation == value)
return;
_selectedLocation = value;
OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// ...other code.
}
So Im trying to creat a simple app like a shopping app. so I have categories and multiple items for each category, and when you get to choose an item then you will have the posibility to increase how many you need or delete the item. For exemple I chosed three items, so my cart have 3 items where each one have an Add button and a delete button. When I hit the add button the number of the items shown should increase and so on.
so what I've done so far is creating a JSON file that having all my categories, and once I hit a category I get to deserialize another JSON file that have all my items, so the items shown depends on the category I chosed of course.
Now each time i choose an item it get added to the cart and shown on the bottom page with a + and - buttons and so on.
so I created a category class to deserialize my json, and an objets class to deserialize my Item's json. I implememted the INotifyChangedProperty in the objets class so that I can keep showin whenever the number of a chosen item get increased, so basicly thats my ViewModel, but I guess that it's like that I need a ViewModel of each created item ? so I guess what I really need to use is the ObservableCollection ..
I hope I explained everything well, and waiting for your feedbacks about if Im doing it right or wrong and how should i proceed to get what I want. thank you so much
the problems is that to set the bindingcontext to my "Objets" Class I have to put the arguments in it, and then my Label well get a precised value ... what should I do ?
I do one sample about your model, you can take a look:
<ContentPage.Content>
<StackLayout>
<Label x:Name="label1" />
<Button
x:Name="btn1"
Clicked="Btn1_Clicked"
Text="change value" />
</StackLayout>
</ContentPage.Content>
public partial class Page15 : ContentPage
{
public Objets model { get; set; }
public Page15()
{
InitializeComponent();
model= new Objets("test 1", 1.001f, " test11111", 12);
this.BindingContext = model;
label1.SetBinding(Label.TextProperty, "nbr_objet");
}
private void Btn1_Clicked(object sender, EventArgs e)
{
model.nbr_objet = 20;
}
}
public class Objets : INotifyPropertyChanged
{
public string Designation { get; set; }
public float Prix { get; set; }
public string imageUrl { get; set; }
private int Nbr_Objet;
public int nbr_objet
{
get { return Nbr_Objet; }
set
{
Nbr_Objet = value;
RaisePropertyChanged("nbr_objet");
}
}
public Objets(string Designation, float Prix, string imageUrl, int Nbr_Objet)
{
this.Designation = Designation;
this.Prix = Prix;
this.imageUrl = imageUrl;
this.Nbr_Objet = Nbr_Objet;
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Update:
but I guess that it's like that I need a ViewModel of each created item ? so I guess what I really need to use is the ObservableCollection ..
You said that you have three categories, and each category have many items, If you display these in ListView, category is used as Group header, and I suggest you can use the same model for different item for different categories, then add in Observablecollection, because it have implemented INotifyPropertyChanged interface.
About ListView group, you can take a look:
https://github.com/xamarin/xamarin-forms-samples/tree/master/UserInterface/ListView/Grouping
If you still have another question, I suggest you can create new thread to ask, because this thread is very long.
Please remember to mark the helpful reply as answer, thanks.
to set a binding programatically
// set the BindingContext for the page
this.BindingContext = new MyViewModel();
// Title is a public property on MyViewModel
myLabel.SetBinding(Label.TextProperty, "Title");
in order for the UI to update when the VM is changed, the VM needs to implement INotifyPropertyChanged
This is some guidance that might help with your problem. Your code is messy and I think that is causing your confusion (you have several things named very similarly).
int Nbr_Objet;
public int nbr_objet { get{...} set {...}}
this.Nbr_Objet= Nbr_Objet;
this shows me that you are setting your member variable Nbr_Objet directly, when you do that the property change notification doesn't fire - you need to assign the value through the public nbr_objet for that to happen.
I'd suggest you define the binding in XAML, and make sure you bind to the property nbr_objet, not the private member variable (field) Nbr_Objet.
If you want to avoid confusion, follow the C# coding standard and name your member variable _nbrObjet, and camel case your property name public int NbrObjet { get {....
I experiment with the order of setting the DataContext property in the default constructor in WPF.
<StackPanel>
<ListBox ItemsSource="{Binding MyItems, PresentationTraceSources.TraceLevel=High}"></ListBox>
<TextBlock Text="{Binding SomeText}"></TextBlock>
<TextBlock Text="{Binding SomeNum}"></TextBlock>
<TextBlock Text="{Binding Path=Person.Name}"></TextBlock>
<ListBox ItemsSource="{Binding Path=PersonList}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
1) With DataContext set before the InitializeComponent method
public partial class MainWindow : Window, INotifyPropertyChanged
{
private string someText = "Default text";
public List<string> MyItems { get; set; }
public List<Person> PersonList { get; set; }
public Person Person { get; set; }
public int SomeNum { get; set; }
public string SomeText
{
get
{
return someText;
}
set
{
someText = value;
OnPropertyChanged("SomeText");
}
}
public MainWindow()
{
this.DataContext = this;
MyItems = new List<string>();
PersonList = new List<Person>();
Person = new Person();
InitializeComponent();
/*These changes are not reflected in the UI*/
SomeNum = 7;
Person.Name = "Andy";
/*Changes reflected with a help of INotifyPropertyChanged*/
SomeText = "Modified Text";
/* Changes to the Lists are reflected in the UI */
MyItems.Add("Red");
MyItems.Add("Blue");
MyItems.Add("Green");
MyItems[0] = "Golden";
PersonList.Add(new Person() { Name = "Xavier" });
PersonList.Add(new Person() { Name = "Scott" });
PersonList[0].Name = "Jean";
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
public class Person
{
public string Name { get; set; } = "Default Name";
}
After the call to the InitializeComponent method changes to the values of properties are not reflected in the UI except for those properties which use INotifyPropertyChanged. Everything is clear so far.
However I noticed that changes to the list items are also reflected in the UI. How come?
I always thought that in order to reflect adding/removing from the collection I need ObservableCollection and to implement INotifyPropertyChanged on list object to detect modifications of these objects. What is the meaning of this?
2) With DataContext set after the InitializeComponent method
Why setting a DataContext property after the InitializeComponent is a bad practice with MVVM? Could you describe it more thoroughly or give a simple code example?
I always thought that in order to reflect adding/removing from the collection I need ObservableCollection<T> and to implement INotifyPropertyChanged on list object to detect modifications of these objects.
You do, if you want reliable updating of the UI during changes in the view model.
What is the meaning of this?
The "meaning" is that in your particular scenario, you are making assumptions that aren't valid. WPF components go through a variety of initialization steps, only some of which occur as part of the InitializeComponent() method.
If, for example, you were to move the code for your value updates into a handler for the Loaded event, you'd find some of the updates reflected in the UI, but not all.
If you move that same code into a method invoked via Dispatcher.InvokeAsync() using a priority of DispatcherPriority.SystemIdle, you'd find that none of the updates would be observed, except for the one backed by INotifyPropertyChanged. In that case, you're explicitly waiting until every aspect of initialization has completed, and there are no longer opportunities for the initialization code to observe your updated values.
It's all about timing. Any code that sets a value before the UI winds up observing it, can do so successfully without INotifyPropertyChanged or equivalent. But you're entirely at the mercy of the current implementation of the framework in that case. Different parts of the initialization happen at different times, and these are not all documented, so you're relying on undocumented behavior. It probably won't change, but you have no way to know for sure.
Why setting a DataContext property after the InitializeComponent is a bad practice with MVVM?
It's not. Don't believe everything you read, even (or especially!) on the Internet.
If you want to forego implementation of INotifyPropertyChanged, then it will be important that you initialize all of your view model data before assigning the DataContext. But, even if you assign the DataContext after calling InitializeComponent, that assignment will be observed (because DataContext is a dependency property and so provides property changed notification to the framework), and the UI will retrieve all of the bound data from your view model data.
What's important is that the view model data be initialized before the assignment of DataContext. Where that happens relative to InitializeComponent() is not important.
When a view model property does not fire the PropertyChanged event, its value must of course be set before assigning the view model instance to the view's DataContext.
It does however not matter if you assign the DataContext before or after calling InitializeComponent:
Given a Binding like
<TextBlock Text="{Binding SomeText}"/>
these two sequence will both result in showing the property value in the view:
DataContext = new { SomeText = "Hello, World." };
InitializeComponent();
and
InitializeComponent();
DataContext = new { SomeText = "Hello, World." };
I'm using mvvm-light and I noticed this strange behavior about the RaisePropertyChanged.
xaml:
<ListBox ItemsSource="{Binding Collection}"/>
<TextBlock Text="{Binding Text}"/>
Observable class:
public class A : ObservableObject
{
private string _b;
public string B
{
get { return this._b; }
set
{
this._b = value;
this.RaisePropertyChanged("B");
}
}
}
vm:
public MainViewModel(IDataService dataService) { this.Collection = new List<A>(...); }
public RelayCommand Command1
{
get
{
return this._command1 ?? (this._command1= new RelayCommand(() =>
{
this.Collection.Add(new A());
this.Collection[2].B = "updated";
this.RaisePropertyChanged("Collection");
this.RaisePropertyChanged("Text");
}));
}
}
public RelayCommand Command2
{
get { return this._command2?? (this._command2 = new RelayCommand(() => { this.Text++; })); }
}
public List<A> Collection { get; set; }
public int Text { get; set; }
So, RaisePropertyChanged("Collection") doesn't update the binding while RaisePropertyChanged("Text") do. I can see it by executing the Command2 several times and the Command1 after that. If the Collection is an ObservableCollection then new element shows in a view, but updated item isn't, which means an internal mechanism of an ObservableCollection works, but not the RaisePropertyChanged.
First, an explanation of the issue:
On Windows Phone, when setting a value for a dependency property, the framework internally check if the new value is different from the old one (for optimization purpose maybe). When you raise the PropertyChanged event or directly re-assign your collection to the ItemsSource property (which is just a wrapper around the ItemsControl.ItemsSourceProperty dependency property), the framework detects that the value actually didn't change and doesn't update the property. Therefore, the ListBox is never notified of your changes, and isn't updated.
The ObservableCollection works because it uses a whole different mechanism: the ListBox directly subscribes to the CollectionChanged event of your collection, and thus isn't hindered by the limitations of the dependency properties.
Now, how to get around this limitation? The only workarounds I can think of are:
Use an ObservableCollection instead of a List
Assign null to the ItemsSource property of your ListBox, then re-assign your collection
Bind the ListBox to a property that will return a different collection every time it's called:
public List<A> CollectionCopy
{
get
{
return this.Collection.ToList();
}
}
I hope the title is understandable, but anyhow I will try to describe the problem as best i can.
I have an ObservableCollection GroupList which contains objects of the type Group. The class Group is very simple:
public class Group
{
public Group()
{
Members = new ObservableCollection<Member>();
}
public Group(string name)
{
Members = new ObservableCollection<Member>();
GroupName = name;
}
public ObservableCollection<Member> Members { get; set; }
public string GroupName { get; set; }
}
The GroupList is the ItemsSource of a ComboBox I have in my View, determining which ObservableCollection of Members to show in a ListBox. The properties of the SelectedItem in the ListBox is then bound to some TextBoxes to show Name, Age etc for each Member.
My problem is that I have a Group with Name="All members" and other groups where the Members are also present. Group A contains Member A+B and Group B contains Member B+C, but Group All members contains A+B+C. However, if I change the properties of say Member A in Group A, it is not reflected in Group All members. I have spent days trying to figure out what is wrong, since it works like a charm if I test it in a new project (written off memory):
ObservableCollection<Member> MembersA = new ObservableCollection<Member>;
ObservableCollection<Member> MembersA = new ObservableCollection<Member>;
Member newMember = new Member("TestMember",1986);
MembersA.Add(newMember);
MembersB.Add(newMember);
MembersA.First().Name="TestMemberChanged";
This updates the member in both OC's but not in my project and i can't figure out what is wrong. Do I need to implement an interface like INotifyPropertyChanged for Member and Group as well? (It is already implemented for GroupList in the VM). Or do I need to manually update all OC's in the events OnPropertyChanged or CollectionChanged?
private ObservableCollection<Group> _groupList;
public ObservableCollection<Group> GroupList
{
get
{
return _groupList;
}
set
{
_groupList = value;
Notify("GroupList");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void Notify(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
I realise that my architechture might be inefficient, so any suggestions how to improve it, are very welcome.
Yes, you said already the answer: you need to implement INotifyPropertyChanged also for the Member class and raise that event when a property of Member like 'Name' has changed it's value. This is because an ObservableCollection notifies the user interface just when items are added/removed/reordered from the list but not when a property value of one item in the list has changed.