I have a Person class:
public class Person : INotifyPropertyChanged
{
private string _name;
public string Name{
get { return _name; }
set {
if ( _name != value ) {
_name = value;
OnPropertyChanged( "Name" );
}
}
private Address _primaryAddress;
public Address PrimaryAddress {
get { return _primaryAddress; }
set {
if ( _primaryAddress != value ) {
_primaryAddress = value;
OnPropertyChanged( "PrimaryAddress" );
}
}
//OnPropertyChanged code goes here
}
I have an Address class:
public class Address : INotifyPropertyChanged
{
private string _streetone;
public string StreetOne{
get { return _streetone; }
set {
if ( _streetone != value ) {
_streetone = value;
OnPropertyChanged( "StreetOne" );
}
}
//Other fields here
//OnPropertyChanged code goes here
}
I have a ViewModel:
public class MyViewModel
{
//constructor and other stuff here
private Person _person;
public Person Person{
get { return _person; }
set {
if ( _person != value ) {
_person = value;
OnPropertyChanged( "Person" );
}
}
}
I have a View which has the following lines:
<TextBox Text="{Binding Person.Name, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged />
<TextBox Text="{Binding Person.Address.StreetOne, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged />
Both values show up in the text box ok when the view loads.
Changes to the first text box will fire OnPropertyChanged( "Person" ) in MyViewModel. Great.
Changes to the second text box ("Person.Address.StreetOne") does NOT fire OnPropertyChanged( "Person" ) inside MyViewModel. Meaning it doesn't call the Person object's SET method. Not great. Interestingly the SET method of StreetOne inside the Address class is called.
How do I get the SET method of the Person object inside the ViewModel to be called when Person.Address.StreetOne is changed???
Do I need to flatten my data so SteetOne is inside Person and not Address??
Thanks!
While adding 'pass-through' properties to your ViewModel is a fine solution, it can quickly become untenable. The standard alternative is to propagate changes as below:
public Address PrimaryAddress {
get => _primaryAddress;
set {
if ( _primaryAddress != value )
{
//Clean-up old event handler:
if(_primaryAddress != null)
_primaryAddress.PropertyChanged -= AddressChanged;
_primaryAddress = value;
if (_primaryAddress != null)
_primaryAddress.PropertyChanged += AddressChanged;
OnPropertyChanged( "PrimaryAddress" );
}
void AddressChanged(object sender, PropertyChangedEventArgs args)
=> OnPropertyChanged("PrimaryAddress");
}
}
Now change notifications are propagated from Address to person.
Edit: Moved handler to c# 7 local function.
if you want the viewmodel SET to be called you could create a street property
public class MyViewModel
{
//constructor and other stuff here
public string Street{
get { return this.Person.PrimaryAddress.StreetOne; }
set {
if ( this.Person.PrimaryAddress.StreetOne!= value ) {
this.Person.PrimaryAddress.StreetOne = value;
OnPropertyChanged( "Street" );
}
}
}
xaml
<TextBox Text="{Binding Street, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged />
but this solution has its drawbacks. i go with Reeds answer in my projects
How do I get the SET method of the Person object inside the ViewModel to be called when Person.Address.StreetOne is changed???
Why do you want to do this? It should not be required - you only need the StreetOne property changed event to fire.
Do I need to flatten my data so SteetOne is inside Person and not Address??
If you want to actually cause this to trigger, you don't need to flatten it (though that is an option). You can subscribe to the Address's PropertyChanged event within your Person class, and raise the event for "Address" within Person when it changes. This shouldn't be necessary, however.
Since I wasn't able to find a ready-to-use solution, I've done a custom implementation based on Pieters (and Marks) suggestions (thanks!).
Using the classes, you will be notified about any change in a deep object tree, this works for any INotifyPropertyChanged implementing Types and INotifyCollectionChanged* implementing collections (Obviously, I'm using the ObservableCollection for that).
I hope this turned out to be a quite clean and elegant solution, it's not fully tested though and there is room for enhancements. It's pretty easy to use, just create an instance of ChangeListener using it's static Create method and passing your INotifyPropertyChanged:
var listener = ChangeListener.Create(myViewModel);
listener.PropertyChanged +=
new PropertyChangedEventHandler(listener_PropertyChanged);
the PropertyChangedEventArgs provide a PropertyName which will be always the full "path" of your Objects. For example, if you change your Persons's "BestFriend" Name, the PropertyName will be "BestFriend.Name", if the BestFriend has a collection of Children and you change it's Age, the value will be "BestFriend.Children[].Age" and so on. Don't forget to Dispose when your object is destroyed, then it will (hopefully) completely unsubscribe from all event listeners.
It compiles in .NET (Tested in 4) and Silverlight (Tested in 4). Because the code in seperated in three classes, I've posted the code to gist 705450 where you can grab it all: https://gist.github.com/705450 **
*) One reason that the code is working is that the ObservableCollection also implements INotifyPropertyChanged, else it wouldn't work as desired, this is a known caveat
**) Use for free, released under MIT License
There is a spelling mistake in your property change notification:
OnPropertyChanged( "SteetOne" );
should be
OnPropertyChanged( "StreetOne" );
Related
I have some trouble with the data binding while using MVVM in Xamarin. Let me explain my architecture:
I have a Manager-class, which contains a ObservableCollection with classes of type t, t is my model class. The Manager-class contains also a attribute called activeT, which is the current selected object of my model class. There are 2 UIs, one which shows the current data of t. The viewModel is bound to the attribute t of my Manager-class like that:
public t CurrentT
{
get
{
return _mgr.CurrentT;
}
set
{
_mgr.CurrentT = value;
OnPropertyChanged();
}
}
_mgr is my singleton Manager-Object.
Now there is the other view, which is able to choose the current t out of a combobox. The viewModel of that view is bound to the ObservableCollection of the manager. If I change the selected object, I do it like with the same code like above. The Property of the manager is the following code:
public t CurrentT
{
get
{
return _currentT;
}
set
{
_currentT= value;
OnPropertyChanged());
}
}
The problem is now, that the first view to view the current selected t does not refresh, though I can see in the debugger, that the current t is changed by the other view.
Can someone help me?
Edit:
I provide some more Code:
The Manager-Class:
public class Manager : INotifyPropertyChanged
{
private t _currentConstructionSite;
private ObservableCollection<t> _constructionSites = null;
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(name));
}
public t CurrentConstructionSite
{
get
{
return _currentConstructionSite;
}
set
{
_currentConstructionSite = value;
OnPropertyChanged("CurrentConstructionSite");
}
}
public ObservableCollection<t> ConstructionSites
{
get
{
return _constructionSites;
}
set
{
_constructionSites = value;
}
}
private Manager()
{
ConstructionSites = DataRepository.GenConstructionSites();
_currentConstructionSite = ConstructionSites[0];
}
}
The ViewModels Class A (This is the viewmodel of the view, which shows some data):
public class DashboardViewModel : ViewModelBase
{
private Manager _mgr;
public t CurrentConstructionSite
{
get
{
return _mgr.CurrentConstructionSite;
}
set
{
_mgr.CurrentConstructionSite = value;
OnPropertyChanged();
}
}
public DashboardViewModel()
{
_mgr = Manager.getInstance();
}
}
The View A to show some data:
Binding Setup from XAML:
<ContentPage.BindingContext>
<local:DashboardViewModel x:Name="viewModel"/>
</ContentPage.BindingContext>
Binding a Label to show data:
<Label Text="{Binding CurrentConstructionSite.ConstructionSiteName, Mode=TwoWay}" HorizontalOptions="Center" Font="Bold" FontSize="Large"/>
ViewModel B to choose the current t:
public class ChooseConstructionSiteViewModel : ViewModelBase
{
Manager _mgr = null;
public ObservableCollection<t> ConstructionSites
{
get
{
return _mgr.ConstructionSites;
}
}
public t CurrentConstructionSite
{
get
{
return _mgr.CurrentConstructionSite;
}
set
{
_mgr.CurrentConstructionSite = value;
OnPropertyChanged();
}
}
public ChooseConstructionSiteViewModel()
{
_mgr = Manager.getInstance();
}
}
The View to choose the current t:
<combobox:SfComboBox x:Name="combobox" Grid.Row="0" Grid.Column="1" Margin="8,0,20,0" VerticalOptions="Center" HeightRequest="40" DataSource="{Binding ConstructionSites}" DisplayMemberPath="ConstructionSiteName" SelectionChanged="Handle_SelectionChanged"/>
And if the selection from the combobox changed:
void Handle_SelectionChanged(object sender, Syncfusion.XForms.ComboBox.SelectionChangedEventArgs e)
{
t selectedItem = e.Value as t;
_viewModel.CurrentConstructionSite = selectedItem;
}
The two views are contained as contetPages in a tabbedPage. It works in general, but the changing the selected t in the view B does not update the data in view A. I can see in the debugger that the value of t is changed via view B but when I go back to view A there is the old value. In the debugger I can see that the value is updated.
BTW: ViewModelBase is the class which implements INotifyPropertyChanged
From you second viewmodel, you need to "notify" the first viewmodel that the data has changed.
One of the ways to do that, would be to use a Messenger (MvvmLight has one, so does MvvmCross). You can then use the messenger from your Manager, to notify all the viewmodels that need the info, that CurrentT changed.
In the messenger subscription in your viewmodels, simply call a RaiseNotifyPropertyChanged of your property, and you should be good to go
I have a usercontrol bound to a VM. This VM contains a collection property, lets call it "MyCollection" and several regular properties lets call one of them "SomeProperty". As you can see, the get and set logic for this property references the collection in the VM.
The problem is, when I make a change to "MyCollection", this obviously has an impact on the values that are shown in the UI (as they are calculated based on it). However, my UI doesn't appear to be smart enough to update itself whenever "MyCollection" changes.
Here is the VM my usercontrol is bound to:
public class MyVM
{
private ObservableCollection<SomeOtherVM> _myCollection = new ObservableCollection<SomeOtherVM>();
public MyVM()
{
}
public ObservableCollection<SomeOtherVM> MyCollection
{
get { return _myCollection; }
[Notify]
set
{
_myCollection = value;
}
}
public virtual string SomeProperty
{
get
{
if (_myCollection.Count == 1)
return _myCollection[0].SomeProperty;
else
return "More than one "SomeOtherVM" has been selected";
}
[Notify]
set
{
foreach (SomeOtherVM s in _myCollection)
{
s.SomeProperty = value;
}
}
}
}
}
Note that nothing in my usercontrol is directly bound to the collection, it is only bound to other properties that reference the collection in its get; set; methods.
Is there anything I could do in the VM to force the UI to update whenever "MyCollection" is changed? I want to avoid having to put anything in the code behind for the user control.
Subscribe to CollectionChanged of MyCollection and fire PropertyChanged-notifications for the other properties there (no fancy attribute usage for you).
I am having an issue where the PropertyChanged event from the generated Ria Services entities is not raised for all the properties.
When I look at the generated code (client-side), I can see that my entities are deriving from the Entity object which implements INotifyPropertyChanged. I can also see that some properties, like the Id property, is raising the PropertyChanged event but some are not.
I did not use any T4 templates so the default were used.
So, my question is:
Is there an option/attribute that I can set so that the PropertyChanged event be raised for any properties of the generated client-side entities?
Any help would be appreciated.
EDIT:
Here's an example of a property, in the auto-generated client-side file, which doesn't raise the PropertyChanged event:
[DataMember()]
[Required()]
[StringLength(50)]
public string FirstName
{
get
{
return this._firstName;
}
set
{
if ((this._firstName != value))
{
this.OnFirstNameChanging(value);
this.RaiseDataMemberChanging("FirstName");
this.ValidateProperty("FirstName", value);
this._firstName = value;
this.RaiseDataMemberChanged("FirstName");
this.OnFirstNameChanged();
}
}
}
And this is what was defined in the model server-side:
[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
[DataMemberAttribute()]
public global::System.String FirstName
{
get
{
return _FirstName;
}
set
{
OnFirstNameChanging(value);
ReportPropertyChanging("FirstName");
_FirstName = StructuralObject.SetValidValue(value, false);
ReportPropertyChanged("FirstName");
OnFirstNameChanged();
}
}
Here's an example of a property, in the auto-generated client-side file, which does raise the PropertyChanged event:
[DataMember()]
[Editable(false, AllowInitialValue=true)]
[Key()]
[RoundtripOriginal()]
public Guid Id
{
get
{
return this._id;
}
set
{
if ((this._id != value))
{
this.OnIdChanging(value);
this.ValidateProperty("Id", value);
this._id = value;
this.RaisePropertyChanged("Id");
this.OnIdChanged();
}
}
}
And this is what was defined in the model server-side:
[EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
[DataMemberAttribute()]
public global::System.Guid Id
{
get
{
return _Id;
}
set
{
if (_Id != value)
{
OnIdChanging(value);
ReportPropertyChanging("Id");
_Id = StructuralObject.SetValidValue(value);
ReportPropertyChanged("Id");
OnIdChanged();
}
}
}
The PropertyChanged event is encapsulated in the RaiseDataMemberChanged(). You can check this with any Decompiler (.NET Reflector, etc.).
The RaiseDataMemberChanged and RaiseDataMemberChanging methods are used to notify the framework about changes for change tracking and state transitions.
These two properties are diffrent, cause the Id property is decorated with the Key-Attribute. I think properties with this attribute get special handling, due to associations with other entities, and a diffrent auto-generated implementation, cause there is not need to track changes or transistions on change.
So in your case, i think there is another problem, why your PropertyChanged event is (maybe) not fired.
I'd like to know how to set a custom property of a custom user control by calling a method that would serve as "data provider" for that property.
What I mean is I'd like to write something like this:
<CatsUserControl Cats={FindAllCats} />,
where CatsUserControl is a User Control I created (which has a Property named Cats), and FindAllCats() is a method written in C# that returns a list of Cat objects.
Is this possible?
I appreciate any ideas. Thanks!
EDIT:
The whole thing began because I wanted to be able to assign a list to a property in XAML, the same way you do <Button Content="Value"/> (except, in my case, Value is a more complex type than a string, it's a List<KeyValuePair<String, Boolean>>). Because I didn't find any way to do that, I thought it might be possible to call a method that returns this list and assign the returned value to the property instead.
Hope that makes things clearer.
It seems like what you are trying to do is really just databinding?
In other words, you would approach it like this:
<CatsUserControl ItemsSource="{Binding AllCats}" />
Binding to a collection
If you want to bind specifically to a property called Cats you need to create an object that implements INotifyPropertyChanged, I think. (semi-pseudo code follows):
//binding to this public property in the CatsUserControl
public List<Cat> Cats;
public class BindingToCats : INotifyPropertyChanged
{
private List<Cat> allCats;
public List<Cat> AllCats
{
get { return allCats; }
set
{
allCats = value;
OnPropertyChanged("AllCats");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
and then bind to it with your custom usercontrol:
<CatsUserControl Cats="{Binding AllCats, Source={StaticResource BindingToCats}}" />
What you are probably looking for is a TypeConverter.
Here is an example of how you can set a List< KeyValuePair< String, Boolean>> using a string in attribute syntax in xaml. The Xaml will end up looking something like:
<c:CustomButton Values="Hello World,true; Foo,false; Bar,true" />
The exact syntax of how you want to represent the list of key/value pairs is up to you.
Here is the code necessary to do this:
public class CustomButton : Button
{
[TypeConverter(typeof(ListOfStringAndBoolPairsTypeConverter))]
public List<KeyValuePair<String, Boolean>> Values { get; set; }
}
public class ListOfStringAndBoolPairsTypeConverter : TypeConverter
{
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
//TODO: Should add better error handling here.
var stringValue = (string)value;
var pairs = stringValue.Split(';').Select(ps => ParsePair(ps));
var result = new List<KeyValuePair<String, Boolean>>();
result.AddRange(pairs);
return result;
}
private KeyValuePair<String, Boolean> ParsePair(string pairStringValue)
{
var splitString = pairStringValue.Split(',');
var key = splitString[0];
var value = Boolean.Parse(splitString[1].Trim());
return new KeyValuePair<string, bool>(key, value);
}
}
You ought to be able to do what you're asking by subscribing to events instead of binding directly from the XAML.
Also, you could use a converter, which is described here.
How can I implement cancelation of editing an object using MVVM.
For example: I have a list of customers. I choose one customer an click the button "Edit", a dialog window(DataContext is binded to CustomerViewModel) opens and I start editing customer's fields. And then I decide to cancel editing, but the fields of the customer have been already changed, so how can I return a customer to its previous state in MVVM?
Check out the IEditableObject interface. Your Customer class should implement that, and your commands can execute BeginEdit / CancelEdit / EndEdit as appropriate.
You can use binding with UpdateSourceTrigger=Explicit. Here you can find more information how this can be implemented.
One super easy way, if your object is already serializable, such as if you are using WCF. You can serialize your original object into an internal field. If, your object isn't serializable, then just use AutoMapper to create a copy of your object with one line of code.
Order backup = Mapper.Map<Order, Order>(order);
When you handle your CancelCommand, just call AutoMapper in reverse. Since your properties already have a change notification everything just works. Its possible you could combine these techniques with IEditableObject, if you need and want to write the extra code.
In this article, Raul just reload the object from the DB. I guess it's less trouble than the solution Kent proposes.
internal void Cancel(CustomerWorkspaceViewModel cvm)
{
Mainardi.Model.ObjectMapping.Individual dc = cvm.DataContext
as Mainardi.Model.ObjectMapping.Individual;
int index = 0;
if (dc.ContactID > 0 && dc.CustomerID > 0)
{
index = _customerCollectionViewModel.List.IndexOf(dc);
_customerCollectionViewModel.List[index] =
_customerBAL.GetCustomerById(dc.CustomerID);
}
Collection.Remove(cvm);
}
Based on Камен Великов's answer:
You can mark your bindings as to be updated manually by defining
<TextBox Name="yourTextBox" Text="{BindingPath=YourBinding, UpdateSourceTrigger=Explicit}" />
in your view (XAML). Then, you have to write the changes from your UI in ViewModel by calling
yourTextBox.GetBindingExpression(TextBox.TextProperty).UpdateSource();
when Save is clicked.
Please note, if there are updated to the binding source triggered from anything else, they are still shown directly in the UI.
I had this problem too. I solved it using "The Memento Pattern Design". With this pattern you could easy save a copy of your original object and, in selectedIndexChange (of a control) or in the Cancel button, you could restore easy the prior version of your object.
An example of use of this pattern is available at How is the Memento Pattern implemented in C#4?
An example of code:
If we have a class User with properties UserName Password and NombrePersona we need to add methods CreateMemento and SetMemento:
public class Usuario : INotifyPropertyChanged
{
#region "Implementación InotifyPropertyChanged"
internal void RaisePropertyChanged(string prop)
{
if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(prop)); }
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
private String _UserName = "Capture su UserName";
public String UserName
{
get { return _UserName; }
set { _UserName = value; RaisePropertyChanged("UserName"); }
}
private String _Password = "Capture su contraseña";
public String Password
{
get { return _Password; }
set { _Password = value; RaisePropertyChanged("Password"); }
}
private String _NombrePersona = "Capture su nombre";
public String NombrePersona
{
get { return _NombrePersona; }
set { _NombrePersona = value; RaisePropertyChanged("NombrePersona"); }
}
// Creates memento
public Memento CreateMemento()
{
return (new Memento(this));
}
// Restores original state
public void SetMemento(Memento memento)
{
this.UserName memento.State.UserName ;
this.Password = memento.State.Password ;
this.NombrePersona = memento.State.NombrePersona;
}
Then, we need a class Memento that will contain the "copy" of our object like this:
/// <summary>
/// The 'Memento' class
/// </summary>
public class Memento
{
//private Usuario _UsuarioMemento;
private Usuario UsuarioMemento { get; set; }
// Constructor
public Memento(Usuario state)
{
this.UsuarioMemento = new Usuario();
this.State.UserName = state.UserName ;
this.State.Password = state.Password ;
this.State.NombrePersona = state.NombrePersona ;
}
// Gets or sets state
public Usuario State
{
get { return UsuarioMemento; }
}
}
And we need a class that will generate and contains our memento object:
/// <summary>
/// The 'Caretaker' class
/// </summary>
class Caretaker
{
private Memento _memento;
// Gets or sets memento
public Memento Memento
{
set { _memento = value; }
get { return _memento; }
}
}
Then for implement this pattern we have to create an instance of Caretaker class
Caretaker creadorMemento = new Caretaker();
And create our memento object when a new user was selected for edit, for example in selectedIndexChange after the SelectedUser has been initializing, I use the method for event RaisPropertyChanged like this:
internal void RaisePropertyChanged(string prop)
{
if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(prop)); }
if (prop == "RowIndexSelected") // This is my property assigned to SelectedIndex property of my DataGrid
{
if ((this.UserSelected != null) && (creadorMemento .Memento != null))
{
this.UserSelected.SetMemento(creadorMemento .Memento);
}
}
if (prop == "UserSelected") // Property UserSelected changed and if not is null we create the Memento Object
{
if (this.UserSelected != null)
creadorMemento .Memento = new Memento(this.UserSelected);
}
}
An explication for this, when selectedIndexChanged change value we check if UserSelected and our memento object are not null means that our actual item in edit mode has changed then we have to Restore our object with the method SetMemento.
And if our UserSelected property change and is not null we "Create our Memento Object" that we will use when the edit was cancel.
For finish, we have use the SetMemento method in every method that we need to cancel the edition, and when the edit has commited like in the SaveCommand we can set null our memento object like this this.creadorMemento = null.
You could also, in your ViewModel copy the model's state to internal fields, and then expose these and then only set them on the model, when the user actually commits the change.
Problem could be, that on-the-fly validation will be more troublesome if validation relies on the entity being updated - if this is a requirement you could create a clone of the model to work on and then merging the clone with the actual entity when it is saved.