I keep running into this issue in MAUI, which I suspect is from some fundamental misunderstanding of MVVM.
Say I have:
public class Student : ObservableObject {
[ObservableProperty] string _name;
[ObservableProperty] double _GPA;
[ObservableProperty] bool _isPassing;
}
I made a custom control (StudentControl
) to reuse my student view around the app. My gut says I should create a BindableProperty
in the control that is of type Student
. Bind the student to the control, and then the control looks up the fields it needs to display, and binds to them itself. My thinking is that it's the StudentControl
's job to decide what it wants to display about the student. If I want to change what gets displayed later, or add/remove a field, I can go there to do it.
<controls:StudentControl Student={Binding SomeBindingToAStudentObject}>
However, This doesn't seem to be the way I'm meant to do it in MAUI. When I pass the Student
directly to the control, binding seems to break, since it's the properties that change, and not the Student
object itself. So although the properties notify correctly, since the control is bound to Student
(rather than the individual properties), the control doesn't change even when the properties change.
I have a similar problem with converters, where I want to pass the parent Student object in, and then decide how to convert it, but again, this breaks the property change notifications in binding, since its link is to Student, not the changing properties themselves.
I know I can fix this by passing the fields individually into the control in a MultiBinding, But then if I add a field in the future, I need to go to every use of the control and add the field to it, rather than adjusting just the control.
Perhaps an important note: I usually have my Student objects in List, rather than a direct reference, and am consuming them through bindabale layouts or collection views etc.
Any suggestions to improve this situation? Or am I stuck passing individual properties to my controls/converters?
EDIT: Sounds like I need to give more context about the views.
This is a really complex problem, so I'm trying to simplify it a bit.
I have a ClassViewModel
that is backing a ClassView
, This is meant to display a ClassList : ObservableObject
ClassList
has an
[ObservableProperty] BindingList<Student> _students;
So in my ClassView, I have something like this:
<StackLayout BindableLayout.ItemsSource="{Binding ClassList.Students}">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="{x:Type models:Student}" >
<controls:StudentControl
Student="{Binding .}"/>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
But the control doesn't update when Student.Name
changes. Same for the other properties. It works when I make 3 bindable properties (one for each field), and pass the bindings in as Student.Name
, Student.GPA
etc.
<!--This Works -->
<StackLayout BindableLayout.ItemsSource="{Binding ClassList.Students}">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="{x:Type models:Student}" >
<controls:StudentControl
Name="{Binding Student.Name}"
GPA="{Binding Student.GPA}"
IsPassing="{Binding Student.IsPassing}"/>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
In the StudentControl
, I have a label that's bound to Student.Name
, and I set BindingContext to This
in the XAML
in header: x:Name="This"
below:
<VerticalStackLayout BindingContext="{x:Reference This}"
Note that the view DOES change, if I swap out a completely different student, but not when an existing student is edited.
EDIT: Solution. Refer to comments to @Stephen Quan's answer.
Added this extension method. Based mostly on https://stackoverflow.com/a/3478581/4163879
public static class ObservableCollectionExtensions
{
public static void AddItemChangedListener<T>(this ObservableCollection<T> collection, PropertyChangedEventHandler h)
{
collection.CollectionChanged += (sender, e) =>
{
AddHandlerToItems(sender, e, h);
};
}
static void AddHandlerToItems(object _, NotifyCollectionChangedEventArgs e, PropertyChangedEventHandler h)
{
if (e.OldItems != null)
{
foreach (INotifyPropertyChanged item in e.OldItems)
item.PropertyChanged -= h;
}
if (e.NewItems != null)
{
foreach (INotifyPropertyChanged item in e.NewItems)
item.PropertyChanged += h;
}
}
}
In ClassList
public ClassList() {
Students.AddItemChangedListener((_, _) => {
OnPropertyChanged(nameof(Students));
});
}
public ObservableCollection<Student> Students {get;} = new();
I think if the ClassList is getting created and destroyed a lot you'd probably want to modify this to be able to remove the listener too.
Firstly, the collection should not use the [ObservableProperty]
attribute but use ObservableCollection
. The best practice for collection is not to change the collection, but, change members in the collection, so, this is why we will not be expecting INotifyPropertyChanged events coming from the collection.
public ObservableCollection<Student> Students { get; } = new();
Next, I would expect if your control needs to update Student, then, the various bindings you have need to be changed to TwoWay, e.g.
<controls:StudentControl
Name="{Binding Student.Name, Mode=TwoWay}"
GPA="{Binding Student.GPA, Mode=TwoWay}"
IsPassing="{Binding Student.IsPassing, Mode=TwoWay}"/>