This question already has answers here:
.net maui Cannot get updated fields in an Observable collection to update in bound collection view
(3 answers)
Closed 4 months ago.
If I add something to my collection, the UI is updated, so my bindings are in order. If I make an update to the collection, the UI is not updated until I navigate away from the page and back to it.
My collection definition:
public ObservableCollection<Account> Accounts = new();
Account definition:
public class Account
{
public int Id { get; set; }
public string AccountName { get; set; }
public decimal StartingBalance { get; set; }
public override string ToString()
{
return $"Id: {Id}: {AccountName} | {StartingBalance}";
}
}
I make updates like this:
public void EditAccount(string editAccountName, string editAccountStartingBalance, int editAccountId)
{
decimal startingBalance;
Decimal.TryParse(editAccountStartingBalance, out startingBalance);
Account editAccount = Accounts.FirstOrDefault(x => x.Id == editAccountId);
editAccount.AccountName = editAccountName;
editAccount.StartingBalance = startingBalance;
}
As I typed up my question, I found through other questions that I need to do the following:
private ObservableCollection<Account> _accounts = new();
public ObservableCollection<Account> Accounts
{
get { return _accounts; }
set
{
_accounts = value;
RaisePropertyChanged("Accounts");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if(handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
This isn't updating the UI either. I can see via stepping through the code that the collection itself is updated, but it's still not reflected in the UI until I navigate away from the page then back.
Code snippets have been pared down for brevity. I think I included all the needed deets. :) With so many things automagically working in .NET MAUI, I think I'm expecting too much, lol.
So I'm using the CommunityToolkit.MVVM package, and just needed to set [ObservableProperty] on my Account type fields and have it inherit ObservableObject.
public partial class Account : ObservableObject
{
[ObservableProperty]
public int id;
[ObservableProperty]
public string accountName;
[ObservableProperty]
public decimal startingBalance;
public override string ToString()
{
return $"Id: {Id}: {AccountName} | {StartingBalance}";
}
}
Related
I am new to MVVM, and I believe I have successfully built an MVVM app to appreciate it. I soon realised that I could not have properties like below to fire notifications:
public string Status { get; set; }
public bool IsIdle { get; set; }
public ObservableCollection<SpecifiedRecord> FilesCollection { get; set; }
I had to rewrite them as below:
private string _status;
public string Status
{
get { return _status; }
set
{
_status = value;
OnPropertyChanged(nameof(Status));
}
}
private bool _isIdle;
public bool IsIdle
{
get { return _isIdle; }
set
{
_isIdle = value;
OnPropertyChanged(nameof(IsIdle));
}
}
private ObservableCollection<SpecifiedRecord> filesColl = new ObservableCollection<SpecifiedRecord>();
public ObservableCollection<SpecifiedRecord> FilesCollection
{
get { return filesColl; }
set
{
if (value != this.filesColl)
filesColl = value;
OnPropertyChanged(nameof(FilesCollection));
}
}
MVVM has been there for nearly 9 years, and I thought in this day and age, Microsoft would allow OnPropertyChanged events to fire automatically i.e. built-in to Net without us having to write it everytime because if I find it very inefficient to do so.
Alternatively, is there a more simplified way to achieve the same by way of an inherited class?
Sources:
https://github.com/mainroads/SpecifiedRecordsExporter/blob/mvvm/SpecifiedRecordsExporter/MVVM/ViewModels/MainPageViewModel.cs
https://github.com/mainroads/SpecifiedRecordsExporter/blob/mvvm/SpecifiedRecordsExporter/MVVM/ViewModels/ViewModelBase.cs
Thanks,
Michael
You can simplify the PropertyChangedEventHandler alot using the CallerMemberName attribute and using an Inherited Class
public class Core : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void PropChanged([CallerMemberName] string callerName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(callerName));
}
}
Then you use it like so after adding : Core to your class
public class Program : Core
{
private bool _isIdle;
public bool IsIdle
{
get
{
return _isIdle;
}
set
{
//if (_isIdle != value)
//{
_isIdle = value;
PropChanged();
//} reduce number of events (comments)
}
}
}
You can also specify the Caller Name manually to trigger an update for a different property.
This is the most modern way of doing this that I know of, The extra bonus here is you can also use the Core class for anything Static
There's no need anymore for all that boilerplate code.
Using the Source Generators from the MVVM Community Toolkit you can inherit from ObservableObject and then write your properties like this:
public partial class MyViewModel : ObservableObject
{
[ObservableProperty]
string name;
}
This will be used to generate a class that looks similar to this under the hood:
partial class MyViewModel
{
public string Name
{
get => name;
set
{
if(name.Equals(value)) return;
OnPropertyChanging();
name = value;
OnPropertyChanged();
}
}
}
You can also raise notifications for other properties and have commands auto-generated, too:
public partial class MyViewModel : ObservableObject
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName)]
string firstName;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName)]
string lastName;
public string FullName => $"{FirstName} {LastName}";
[RelayCommand]
private void SayHello()
{
Console.WriteLine($"Hello, {FullName}!");
}
}
Note that the backing fields for auto-generated properties must be lowercase, because the generator will create the properties beginning with an uppercase letter.
Commands will have the same name as the method with the "Command" suffix, so SayHello() becomes SayHelloCommand. In case of an async method, e.g. async Task SayHelloAsync(), the Command will still be called SayHelloCommmand, without the "Async" suffix.
I've also written a blog series going into more detail about this.
I don't understand why when I update a object, my bound controls do not update.
The data displays fine initially, but when I want to refresh the data displayed in the UI nothing happens when I update the object. The object updates fine. The ViewModel does use INotifyPropertyChanged on all fields.
However if I update individual items directly, I can update my UI. As commented below.
I guess I've made a school boy error somewhere here?
UPDATE: I've added the model to the question. While I understand the answers, I don't understand how to implement it. Attempted to implement a collection changed event without success. Can I have some pointers please?
public partial class CisArrivalsPanel : UserControl
{
private ApiDataArrivalsDepartures _theArrivalsDepartures;
public CisArrivalsPanel()
{
InitializeComponent();
_theArrivalsDepartures = new ApiDataArrivalsDepartures();
_theArrivalsDepartures = MakeQuery.LiveTrainArrivals("London Kings Cross");
this.DataContext = _theArrivalsDepartures;
ListBoxArr.ItemsSource = _theArrivalsDepartures.StationMovementList;
}
void Reload()
{
//This does not update the UI**
_theArrivalsDepartures = MakeQuery.LiveTrainArrivals("London Paddington");
//However this (when uncommented, and I comment out the above line) does update the UI**
//_theArrivalsDepartures.StationMovementList[0].OriginName = "test";
//_theArrivalsDepartures.StationMovementList[0].Platform = "0";
//_theArrivalsDepartures.StationMovementList[0].BestArrivalEstimateMins = "999";
//_theArrivalsDepartures.StationName = "test";
}
private void StationHeader_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Reload();
Debug.WriteLine(_theArrivalsDepartures.StationName);
foreach (var a in _theArrivalsDepartures.StationMovementList)
{
Debug.WriteLine(a.OriginName);
Debug.WriteLine(a.BestArrivalEstimateMins);
}
}
}
EDIT : Added Model
public class ApiDataArrivalsDepartures : INotifyPropertyChanged
{
private string _stationName;
[JsonProperty(PropertyName = "station_name")]
public string StationName {
get
{
return _stationName;
}
set
{
_stationName = value;
NotifyPropertyChanged("StationName");
}
}
private List<StationListOfMovements> _stationMovementList;
public List<StationListOfMovements> StationMovementList
{
get
{
return _stationMovementList;
}
set
{
_stationMovementList = value;
NotifyPropertyChanged("StationMovementList");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
public class StationListOfMovements : INotifyPropertyChanged
{
private string _originName;
[JsonProperty(PropertyName = "origin_name")]
public string OriginName {
get
{
return _originName;
}
set
{
_originName = value;
NotifyPropertyChanged("OriginName");
}
}
[JsonProperty(PropertyName = "destination_name")]
public string DestinationName { get; set; }
private string _platform;
[JsonProperty(PropertyName = "Platform")]
public string Platform {
get
{
return _platform;
}
set
{
_platform = value;
NotifyPropertyChanged("Platform");
}
}
private string _bestArrivalEstimateMins;
[JsonProperty(PropertyName = "best_arrival_estimate_mins")]
public string BestArrivalEstimateMins {
get
{
return _bestArrivalEstimateMins;
}
set
{
_bestArrivalEstimateMins = value;
NotifyPropertyChanged("BestArrivalEstimateMins");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
There are two pieces here pertaining to your collection (technically three):
If you want a new collection to propagate, the collection property has to raise PropertyChanged (sounds like it does)
If you want add/remove on the collection to propagate, you need to use a collection that implements INotifyCollectionChanged. ObservableCollection is a good choice.
If you want changes to the items in the container to propagate, then those items need to implement INotifyPropertyChanged and raise the PropertyChanged event.
Make sure all those are covered, and the changes should appear on the UI as you expect.
You should update the DataContext and ItemsSource too.
void Reload()
{
//This does not update the UI**
_theArrivalsDepartures = MakeQuery.LiveTrainArrivals("London Paddington");
DataContext = theArrivalsDepartures;
ListBoxArr.ItemsSource = _theArrivalsDepartures.StationMovementList;
}
Use for the collection ObservableCollection , this class notify the ui when change to the collection occurred
your reload function works because the there is PropertyChanged on all the fields include this one
it notify the ui and reload the correct collection
In my XAML code I've got a combo box that is bound to a static property as shown below.
<ComboBox x:Name="DifferentKinds"
ItemsSource="{x:Static local:MainWindow.DifferentKinds}"/>
And the code for the property and its source.
public static Kind[] DifferentKinds
=> (Kind[])Enum.GetValues(typeof(Kind));
public enum Kind { WeeHee, BuuHuu }
I just learned that there'll be more kinds in the future. They won't be created particularly often but it's uncertain how many they might become with time. So, instead of adding new elements to the enum, I'll read in these from the DB.
For the simplicity of the example, let's say we read in those values every time the property is accessed. The solution becomes a private fields that is read in from the DB before the execution of InitializeComponent() starts. Then, I serve those values as a static property still, like so.
public MainWindow()
{
PopulateDifferentKinds();
InitializeComponent();
}
private static IEnumerable<Kind> _allDifferentKinds;
public static IEnumerable<Kind> AllDifferentKinds
=> _allDifferentKinds.Where(element => element.Active);
public class Kind
{
public String Name { get; set; }
public bool Active { get; set; }
public override string ToString() { return Name; }
}
Is this approach creating a huge problem that I miss to see?
Is there a better way to bind the items in the bombo box to the values from DB?
The main problem I see here is that calling the PopulateDifferentKinds method in the view's constructor will create a performance problem. While this method is running and the database is being queried, your UI is being blocked.
This could be improved using a class that loads your data on a background thread and uses a PropertyChanged event to signal that the data has been loaded:
public class Kind
{
public string Name { get; set; }
public bool Active { get; set; }
public int Value { get; set; }
}
public class AppEnumValues : INotifyPropertyChanged
{
private static readonly Lazy<AppEnumValues> current
= new Lazy<AppEnumValues>(() => new AppEnumValues(), LazyThreadSafetyMode.ExecutionAndPublication);
public static AppEnumValues Current
{
get { return current.Value; }
}
public Kind[] AllDifferentKinds { get; private set; }
public bool IsLoaded { get; private set; }
private AppEnumValues()
{
Task.Run(() => this.LoadEnumValuesFromDb())
.ContinueWith(t => this.OnAllPropertiesChanged());
}
protected virtual void OnAllPropertiesChanged()
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(string.Empty));
}
}
private void LoadEnumValuesFromDb()
{
// This simulates some latency
Thread.Sleep(2000);
// Call your data service here and load the values
var kinds = new[]
{
new Kind {Active = true, Name = "WeeHee", Value = 1},
new Kind {Active = true, Name = "BuuHuu", Value = 2}
};
this.AllDifferentKinds = kinds;
this.IsLoaded = true;
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
You could extend this with properties for each extensible "enum" you need in your application. Implementing the Singleton pattern, this would load its data in background the first time it is used. You could bind your ComboBoxes like this:
<ComboBox ItemsSource="{Binding Source={x:Static wpfApplication2:AppEnumValues.Current},Path=AllDifferentKinds}"
IsEnabled="{Binding Source={x:Static wpfApplication2:AppEnumValues.Current},Path=IsLoaded}"
DisplayMemberPath="Name" />
While the data is being loaded, the ComboBox would be disabled.
I would recommend looking into MVVM and Dependency Injection. This will enhance your WPF application architecture and make things like that easy: You wouldn't provide a static property or singleton, which has bad testability and extensibility, but you could use constructor injection to give the AppEnumValues provider into your View Model and then bind your view to it.
I have a WPF application that displays a window with various information in it. In my code I create an instance of a custom class that I created which reads information from RFID card reader. To keep it simple - every now and then someone would swipe their card using the card reader which would generate a string that I successfully capture using my custom class.
The problem that I have is that I need to return that value to the window application so that I can update the information displayed in the window based on the value read. This is not as simple as calling a function in the custom class and returning a value as I don't know when exactly someone would swipe their card.
One solution that I could think of was to make a timer and pool the custom class every second or so to check if someone swiped their card, however, I don't think that's an effective solution.
Since I'm relatively new to WPF I'm assuming that the right way to do it is using INotifyProperyChanged but I'm unsure how to do it. Open to any other suggestions as well, thank you!
Create an event on your CardReader class that you can listen to on your ViewModel.
class CardInfo
{
public string CardDetails { get; set; }
}
class CardSwipedEventArgs
: EventArgs
{
public CardInfo SwipedCard { get; set; }
}
interface ICardReader
{
event EventHandler<CardSwipedEventArgs> CardSwiped;
}
class MyViewModel : INotifyPropertyChanged
{
private ICardReader _cardReader;
private string _lastCardSwiped;
public ICardReader CardReader
{
get
{
return _cardReader;
}
set
{
_cardReader = value;
_cardReader.CardSwiped += OnCardSwiped;
}
}
private void OnCardSwiped(object sender, CardSwipedEventArgs e)
{
LastCardSwiped = e.SwipedCard.CardDetails;
}
public string LastCardSwiped
{
get
{
return _lastCardSwiped;
}
set
{
_lastCardSwiped = value;
this.OnPropertyChanged("LastCardSwiped");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Thank you all for your posts. Using events was definitely the way but it wasn't easy to understand how they worked. Your feedback definitely helped but this article helped me understand how events worked best and how to implement them so I could deal with the issue successfully:
http://www.codeproject.com/Articles/9355/Creating-advanced-C-custom-events
Create an event on the class that reads the data from RFID.
public class CardSweepedEventArgs : EventArgs {
private readonly string _data;
public string Data { get { return _data; } }
public CardSweepedEventArgs(string data) {
_data = data;
}
}
public class YourReadinClass {
public EventHandler<CardSweepedEventArgs> CardSweeped;
// rest of logic.
}
In your class then subscribe to the event and do the necessary.
I'm sorta at a loss to why this doesn't work considering I got it from working code, just added a new level of code, but here's what I have. Basically, when I bind the ViewModel to a list, the binding picks up when Items are added to a collection. However, if an update occurs to the item that is bound, it doesn't get updated. Basically, I have an ObservableCollection that contains a custom class with a string value. When that string value gets updated I need it to update the List.
Right now, when I debug, the list item does get updated correctly, but the UI doesn't reflect the change. If I set the bound item to a member variable and null it out then reset it to the right collection it will work, but not desired behavior.
Here is a mockup of the code, hopefully someone can tell me where I am wrong. Also, I've tried implementing INofityPropertyChanged at every level in the code below.
public class Class1
{
public string ItemName;
}
public class Class2
{
private Class2 _items;
private Class2() //Singleton
{
_items = new ObservableCollection<Class1>();
}
public ObservableCollection<Class1> Items
{
get { return _items; }
internal set
{
_items = value;
}
}
}
public class Class3
{
private Class2 _Class2Instnace;
private Class3()
{
_Class2Instnace = Class2.Instance;
}
public ObservableCollection<Class1> Items2
{
get {return _Class2Instnace.Items; }
}
}
public class MyViewModel : INofityPropertyChanged
{
private Class3 _myClass3;
private MyViewModel()
{
_myClass3 = new Class3();
}
private BindingItems
{
get { return _myClass3.Items2; } // Binds when adding items but not when a Class1.ItemName gets updated.
}
}
The answer to your question is that Class1 needs to implement INotifyPropertyChanged.
public class Class1 : INotifyPropertyChanged
{
private string _ItemName;
public string ItemName
{
get { return _ItemName; }
set
{
_ItemName = value;
NotifyPropertyChanged("ItemName");
}
}
private void NotifyPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
public event PropertyChangedEventHandler PropertyChanged;
}