INotifyPropertyChanged in nested object - c#

I have 2 main classes. The first class represents a Cell that can have values X, O or Empty. I have implemented INotifyPropertyChanged on this.
public class Cell : INotifyPropertyChanged
{
private Symbol state;
public Symbol State
{
get { return state; }
set
{
if (value == Symbol.X || value == Symbol.O)
state = value;
OnPropertyChanged("State");
}
}
public Cell()
{
state = Symbol.Empty;
}
public enum Symbol
{
X, O, Empty
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
The second class contains an object of this class and is also set as the datacontext for my main window.
public class Board
{
private Cell testCell;
public Cell TestCell
{
get { return testCell; }
set { testCell = value; }
}
public Board()
{
TestCell = new Cell();
}
public void Cell_Click(int cellNum)
{
TestCell.State = Cell.Symbol.O;
}
}
In my mainwindow I have set the datacontext as board, and also contains a button_click function.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new Board();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Board board = this.DataContext as Board;
board.Cell_Click(cellNum);
}
}
In my XAML I have bound to Cell object within board using a button style like this:
<Setter Property="Background" Value="{Binding TestCell,
Converter={StaticResource BGConverter}}"/>
BGConverter is a custom converter that accepts a Cell object and converts it into a Colors object. I believe I am indeed directly binding to an object that has INotify implemented, so there's no issue of nested objects. However the binding doesn't reflect changes when I click. When debugging, I found that PropertyChanged event is always null.
The closest answer I found for this is that the event will be subscribed to only if the class Cell is my datacontext. Atleast that's what I understood. How can I correct this problem?
Also I am fresh out of college, currently learning WPF on a new job, so any general recommendations are welcome too.
Thanks

Simply bind to TestCell.State instead of TestCell

I'm pretty new at this myself, but I believe your data context needs to implement INotifyPropertyChanged as well.
That is, your Board class needs to listen to the PropertyChanged event of the cells and fire its own PropertyChanged event when this happens.

Related

Bubbling NotifyPropertyChanged

According to this answer I should not need to bother about NotifyPropertyChanges Bubbling up the hierachie, still I can't get it to work with a (simplified test-) structure like that:
a Data-Holding Class
public class TestNotifyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _Test = "default";
public string Test
{
get
{
return _Test;
}
set
{
if(_Test!=value)
{
_Test = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Test"));
}
}
}
}
A ViewModel that used that Test-Class and Test-Property:
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private TestNotifyChanged tnc = new TestNotifyChanged(); // only to init, otherwise VS screams at me
public ViewModel(TestNotifyChanged tnc)
{
tnc = tnc; // getting an instance of TestNotifyChanged from "Master" passed in, which hopefully will be replaces by a singleton class.
}
private string _Test;
public string Test
{
get
{
return tnc.Test; // this might be the crucial part!?
}
set
{
if (_Test != value) // never hits that, as I would expect, but you never know..
{
_Test = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Test")); // of course also never hit, as expected
}
}
}
}
And finally my MainWindow cs
public partial class MainWindow : Window
{
TestNotifyChanged tnc;
public MainWindow()
{
InitializeComponent();
tnc = new TestNotifyChanged();
DataContext = new ViewModel(tnc); // pass in my Test-Object that has the Values.
}
private void ButtonGet_Click(object sender, RoutedEventArgs e)
{
tnc.Test = "new Value";
MessageBox.Show($"{tnc.Test}"); // got "new Value" here!
}
}
And in xaml I have besides that one button a simple TextBlock that is bound to the ViewModel's Test Property:
<TextBlock x:Name="OutputId" Text="{Binding Path=Test, Mode=OneWay}"/>
What is happening now:
The default value "default" is shown in TextBlock.
When I click the button the messageBox shows the "new Value"
TextBlock is not updating to "new Value"
What I want to achieve:
seems easy: TextBlock should update to "new Value"
I can easily make this work when I directly set the Test Value on the ViewModel - but this doesn't seem right and is far away from what I thought I could structure my app/code. The future goal is to have a Singleton (static won't work I figured out) "RecordStore" that has most of the data (and gets it from an API, from local Database, or just from Memory if any of these are done)
So the question is:
Why is the NotifyPropertyChange not bubbling up to the View/ViewModel?
Or is there another issue I don't see?
I've read INotifyPropertyChanged bubbling in class hierarchy
and
What is a good way to bubble up INotifyPropertyChanged events through ViewModel properties with MVVM?
and
https://learn.microsoft.com/en-us/dotnet/framework/winforms/how-to-implement-the-inotifypropertychanged-interface
and
OnPropertyChange called but not taking any effect on UI
most of those questions are also quite old...
EDIT:
I tried #MineR 's suggestion that way:
// made tnc public in ViewModel
public TestNotifyChanged tnc = new TestNotifyChanged();
// changed Binding directly to that (and it's Property):
<TextBlock x:Name="OutputId" Text="{Binding Path=tnc.Test, Mode=OneWay}"/>
Unfortunately now I don't even get the default, so I must have misunderstood smth.
EDIT2:
I did one thing wrong in the 1st edit:
// this isn't recognized as bindable parameter:
public TestNotifyChanged tnc = new TestNotifyChanged();
// it instead has to be
public TestNotifyChanged tnc { get; }
And I made it TNC, removed the local Test parameter, bound directly to Path=TNC.Test
So I understood, that PropertyChanges do not bubble up the way I hoped/thought, it's better to bind directly down to the nested object.
"Bubbling" is a concept of routed events. A regular event like PropertyChanged doesn't "bubble up".
Besides the apparent bug tnc = tnc; in the ViewModel (which should be this.tnc = tnc;) the Test properties of the two classes are unrelated. In order to update its own Test property, ViewModel must register a PropertyChanged event handler at tnc. And it must update the property of tnc when its own Test property changes.
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private TestNotifyChanged tnc;
public ViewModel(TestNotifyChanged t)
{
tnc = t;
tnc.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(Test) || string.IsNullOrEmpty(e.PropertyName))
{
Test = tnc.Test; // update ViewModel.Test from TestNotifyChanged.Test
}
};
}
private string test;
public string Test
{
get
{
return test; // always return own value
}
set
{
if (test != value)
{
test = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Test)));
tnc.Test = Test; // update TestNotifyChanged.Test from ViewModel.Test
}
}
}
}
Alternatively, drop the Test property's backing field and only operate on tnc.Test:
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private TestNotifyChanged tnc;
public ViewModel(TestNotifyChanged t)
{
tnc = t;
tnc.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(Test) || string.IsNullOrEmpty(e.PropertyName))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Test)));
}
};
}
public string Test
{
get { return tnc.Test; }
set { tnc.Test = Test; }
}
}
Fortunately, it is not necessary at all.
There could instead just be a public Tnc property like
public class ViewModel
{
public TestNotifyChanged Tnc { get; }
public ViewModel(TestNotifyChanged tnc)
{
Tnc = tnc;
}
}
with a Binding like this:
<TextBlock Text="{Binding Tnc.Test}"/>

BindingSource for GridControl on a property holding changing instances

I have an viewmodel object which we call MyVM for now. It holds a list of data objects (let's say of type MyData, it has a List<MyEntry> property ListOfEntries) called AllDataObjects and has a property CurrentSelection returning the currently selected data object out of its list.
The mechanism responsible for changing the selected item works and assigns a different MyData object from AllDataObjects to the CurrentSelection property.
Now I have a DevExpress GridControl that needs a DataBinding referring to MyVM.CurrentSelection.ListOfEntries. However if I set this up, the binding will always point to the first ever assigned element, as when CurrentSelection gets a new MyData object instance assigned, the binding still only knows the reference to the old object.
Is there a way how I can bind my Grid to this nested property without having to reset and reinitialize the same MyData instance stored in CurrentSelecton over and over?
I managed to do this by letting MyVM implement the interface INotifyPropertyChanged and having the UserControl holding my Grid listen to its PropertyChanged event.
public class MyVMClass : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(String propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public List<MyData> AllDataObjects { get; set; }
private MyData CurrentSelection_ { get; set; }
public MyData CurrentSelection
{
get { return CurrentSelection_; }
set
{
CurrentSelection_ = value;
OnPropertyChanged(nameof(CurrentSelection));
}
}
public MyVM(List<MyData> allDataObjects)
{
AllDataObjects = allDataObjects;
}
}
and inside the UserControl's code:
public void Initialize(MyVMClass myVM)
{
MyVM = myVM;
MyVM.PropertyChanged += MyVM_PropertyChanged;
MyVM_PropertyChanged();
}
private void MyVM_PropertyChanged(object sender=null, PropertyChangedEventArgs e=null)
{
if (e == null || e.PropertyName == nameof(MyVMClass.CurrentSelection))
{
MyBindingSource.DataSource = MyVM.CurrentSelection.ListOfEntries;
}
}

Disable ComboBox of a UserControl from another UserControl without MVVM

I have a UserControl Person_UC and Student_UC. There is a ComboBox in Student_UC which I want to disable it from Person_UC.
But its not working. I want to accomplish this without MVVM.
public partial class Person_UC : UserControl
{
public Person_UC()
{
InitializeComponent();
Student_UC su = new Student_UC();
su.myComboBoxName.IsEnabled = false;
}
}
Without MVVM it would be quite hard to solve. You have to manipulate the same instance of Student_UC which is currently used.
Actually, you're instantiating a new Student_UC and disabling its ComboBox, but you're not doing anything with your variable "su". Did you assign it somewhere?
Basically, you should have one ViewModel per UserControl, so a ViewModel for Person_UC and a ViewModel for Student_UC.
Warning, this solution requires you to use a MVVM Framework like MVVM Light (https://mvvmlight.codeplex.com) for sending messages.
One standard way would be sending a message. Bind your Loaded event of your Person_UC to a method in your code-behind like so:
<UserControl x:Class="YourAssembly.Person_UC"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="Person_UC"
Loaded="Loaded">
<Grid Name="RootGrid">
</Grid>
</UserControl>
And:
public partial class Person_UC : UserControl
{
public Person_UC()
{
InitializeComponent();
}
private void Loaded(object sender, RoutedEventArgs e)
{
//Use a Message to notify your Student_UC's ViewModel that you would like to disable its ComboBox
Messenger.Default.Send<ChangeComboBoxEnabilityMessage>(new ChangeComboBoxEnabilityMessage(false));
}
}
Then, when you receive the message within the Student_UC's ViewModel, you have to pass this information to the view. Basically, you can bind IsEnable property of the ComboBox to a property in its ViewModel, that you will set to false when the message is received.
public class Student_UC_ViewModel : INotifyPropertyChanged
{
public Student_UC_ViewModel()
{
//Register your message
Messenger.Default.Register<ChangeComboBoxEnabilityMessage>(message => ComboBoxIsEnabled = message.ComboBoxIsEnabled);
}
private bool _comboBoxIsEnabled;
public bool ComboBoxIsEnabled
{
get
{
return _comboBoxIsEnabled;
}
set
{
_comboBoxIsEnabled = value;
OnPropertyChanged("ComboBoxIsEnabled");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
And your message class:
public class ChangeComboBoxEnabilityMessage : MessageBase
{
public ChangeComboBoxEnabilityMessage(bool comboBoxEnabled)
{
ComboBoxIsEnabled = comboBoxEnabled;
}
public bool ComboBoxIsEnabled
{
get;
set;
}
}
I let you bind your IsEnable property of your ComboBox in your Student_UC xaml to the property of its ViewModel (i.e ComboBoxIsEnabled).
Don't forget to make sure your DataContext is set:
public partial class Student_UC : UserControl
{
public Person_UC()
{
InitializeComponent();
DataContext = new Student_UC_ViewModel();
}
...
}
Also, take care of your binding issues, see your output console in Visual Studio.

Bind ViewModel List<T> to Listbox in C# Windows Universal App

I have a listbox which i want to get updated when the items get added to a list. I understand I need to bind the listbox. I was trying to follow this question/answer.
I have a ViewModel which handles the list:
namespace TESTS
{
public class ViewModel : INotifyPropertyChanged
{
private List<Cars> _listCars;
public List<Cars> listCars
{
get
{
return _listCars;
}
set
{
if (_listCars == value)
{
return;
}
this.RaisePropertyChanged("Message");
_listCars = value;
this.RaisePropertyChanged("Message");
}
}
public ViewModel()
{
listCars = new List<Cars>();
}
protected void RaisePropertyChanged(string propertyName)
{
Debug.WriteLine("Property Changed");
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
Here is the class Cars:
public class Cars: INotifyPropertyChanged
{
public string model{ get; set; }
public string year{ get; set; }
public event PropertyChangedEventHandler PropertyChanged;
}
So I did the binding of listbox to the property path in my Viewmodel which is listCars.
<ListBox .... ItemsSource="{Binding listCars}">
So when in my Main.xaml.cs. I do a button click and add the item. It does not get added to the listbox even though its bind to the list on view model.
public sealed partial class MainPage : Page
{
public static ViewModel vm = new ViewModel();
public MainPage()
{
this.InitializeComponent();
this.DataContext = vm;
}
private void button_Click(object sender, RoutedEventArgs e)
{
Cars x = new Cars();
x.model = "Ford";
x.Year = "1998";
vm.listCars.Add(x);
}
}
I hope I explained what i implemented well enough. Is there something wrong in my implementation of ViewModel. I am new to MVVM. Please help.
Use ObservableCollection<T>, not List<T>. The former is designed to be used with MVVM, the latter is not. You'll get all your notifications automatically. It's doable with List<T>, but you'll have to write much more code and the performance will be much worse, especially with big collections. Just don't do it.
If you create the collection in the constructor, assign it to a read-only property and never change its instance (and this is the way you should do it), you don't even need to implement INPC.
When implementing INPC, you're expected to call RaisePropertyChanged after you've changed the property, once, and with the property name that has been changed, not a random unrelated string.

How to implement DataTable property with INotifyPropertyChanged

I have created WPF MVVM application, and set WPFToolkit DataGrid binding to DataTable so I want to know how to implement DataTable property to notify changed. Currently my code is like below.
public DataTable Test
{
get { return this.testTable; }
set
{
...
...
base.OnPropertyChanged("Test");
}
}
public void X()
{
this.Test.Add(...); // I suppose this line will call to getter first (this.Test = get Test) and then it will call add letter, this mean that setter scope will never fire.
base.OnPropertyChanged("Test"); // my solution is here :) but I hope it has better ways.
}
Is it has another solution for this problem?
There are 2 ways your Table data could change: Either an element could be added/removed from the collection, or some properties from within an element could change.
The first scenario is easy to handle: make your collection an ObservableCollection<T>. Invoking .Add(T item) or .Remove(item) on your table will fire a change notification through to the View for you (and the table will update accordingly)
The second scenario is where you need your T object to implement INotifyPropertyChanged...
Ultimately your code should look something like this:
public class MyViewModel
{
public ObservableCollection<MyObject> MyData { get; set; }
}
public class MyObject : INotifyPropertyChanged
{
public MyObject()
{
}
private string _status;
public string Status
{
get { return _status; }
set
{
if (_status != value)
{
_status = value;
RaisePropertyChanged("Status"); // Pass the name of the changed Property here
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
}
Now set the datacontext of your View to be an instance of your ViewModel, and bind to the collection, like:
<tk:DataGrid
ItemsSource="{Binding Path=MyData}"
... />
Hope this helps :)
Ian

Categories

Resources