I'm having trouble trying to figure out the best way to implement IDataErrorInfo in my WPF application. I have a few models, each with a lot of properties, that are being used. The ViewModel has Properties of each of these models and the View uses these properties to bind to the model. This is a reduced version of the structure I have:
public class ModelA
{
public string PropertyA1 {get; set;}
public string PropertyA2 {get; set;}
}
public class ModelB
{
public string Property B1 {get; set;}
public string Property B2 {get; set;}
}
public class ViewModel
{
public ModelA modelA {get; set;}
public ModelB modelB {get; set;}
}
My questions is - Where do I implement IDataErrorInfo - In the ViewModel or in the Model?
The View binds to these properties as modelA.PropertyA1 and so on so the errors are raised in the Models and not in the ViewModel which makes it necessary to implement IDataErrorInfo in the Models. However, I've heard that it's a good practice to implement validation logic in the ViewModel.
I was wondering if there was a way to catch the errors in the ViewModel without writing a wrapper for each of the properties that would raise an error as there are lots of properties and writing a wrapper for each of them would be tedious.
Thanks for your help!
I think you should implement IDataErrorInfo in the view-model. If you have a base class for your view-models, and you probably should, you can implement it there. Now, all your implementation/validation-logic is in your base class and not spread amongst n-number of view-models.
You want a Valaidation of an Property of an Model this can only be done if you are implement in a Base class or for each Model in the model. If you want to Validate ModelA in ViewModel you need to impliment it in ViewModel but if you want to Implement a Validation for A1 you are need to Implement it in ModelA.
If you change the instance of modelA, IDataViewModel will go into the class and try to call (Instance of your ViewModel)["modelA"]. OK but this is not what you want, if you change a property of modelA IDataViewModel will go into the Instance of modelA and call modelA["B1"], and if you are now implement your validation in ViewModel, modelA will retrun an Empty string
Thank you for your help guys! Here's how I decided to solve my problem:
I created two base classes, one for normal models (Ones that don't have any validations. It only implements INotifyPropertyChanged) and another for models that have validations as shown below.
public abstract class ModelBase : INotifyPropertyChanged
{
//Implement INotifyPropertyChanged here
}
public delegate string ValidateProperty(string propertyName);
public abstract class ValidationModelBase : ModelBase, IDataErrorInfo
{
private bool _canValidate;
public bool CanValidate
{
get { return _canValidate; }
set { _canValidate = value; }
}
#region IDataErrorInfo Members
public string Error
{
get { return string.Empty; }
}
public string this[string columnName]
{
get
{
if (this.CanValidate)
{
return this.Validate(columnName);
}
return string.Empty;
}
}
#endregion
#region Validation Section
public event ValidateProperty OnValidateProperty;
public string Validate(string propertyName)
{
if (this.OnValidateProperty != null)
{
return OnValidateProperty(propertyName);
}
return string.Empty;
}
#endregion
}
Now my models looked like this:
public class ModelA : validationModelBase
{
public string PropertyA1 {get; set;}
public string PropertyA2 {get; set;}
}
public class ModelB : ValidationModelBase
{
public string Property B1 {get; set;}
public string Property B2 {get; set;}
}
Not a huge change there. The ViewModel now looks like this:
public class ViewModel
{
public ModelA modelA {get; set;}
public ModelB modelB {get; set;}
public ViewModel()
{
this.modelA.OnValidateProperty += new ValidateProperty(ValidateModelA);
this.modelB.OnValidateProperty += new ValidateProperty(ValidateModelB);
}
private string ValidateModelA(string propertyName)
{
//Implement validation logic for ModelA here
}
private string ValidateModelB(string propertyName)
{
//Implement validation logic for ModelB here
}
}
This seems to be working for me so far. This way, any new models that have validation need only derive from ValidationModelBase and have the ViewModel add an event handler for the validation event.
If anyone has a better way of solving my problem, do let me know - I'm open to suggestions and improvements.
Related
As written everywhere (e.g. here, here, here, here...), reporting an ObservableCollection item property change to a view requires the item to implement INotifyPropertyChanged.
Using CommunityToolkit.Mvvm this can be done using an attribute:
public class MyViewModel : ObservableObject
{
public ObservableCollection<MyItem> MyCollection { get; set; }
//...
}
[INotifyPropertyChanged] // yay! No boilerplate code needed
public partial class MyItem
{
public string MyProperty { get; set; }
}
If somewhere inside MyViewModel there is a change to the MyProperty of an item of MyCollection, the view will be updated.
What if an interface comes into play?
public class MyViewModel : ObservableObject
{
public ObservableCollection<IMyInterface> MyCollection { get; set; }
//...
}
[INotifyPropertyChanged]
public partial class MyItem : IMyInterface // MyProperty is in IMyInterface too
{
public string MyProperty { get; set; }
}
The view seems not to be updated anymore.
I tried:
Inheriting INotifyPropertyChanged in IMyInterface (that requires explicit implementation of the PropertyChanged event and OnPropertyMethod method in MyItem, which I don't want as otherwise I would have not used CommunityToolkit.Mvvm)
Adding [INotifyPropertyChanged] to MyViewModel (expecting nothing but there was an answer somewhere telling that)
Is there an obvious, no-boilerplate solution that I'm missing here?
The view is updated if I do something like suggested here
var item = MyCollection[0];
item.MyProperty = "new value";
MyCollection[0] = item;
but I hope there's a better way.
I'm not sure this will solve your issue but I can see you're using the attribute incorrectly.
Please see
https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/inotifypropertychanged
"These attributes are only meant to be used in cases where the target types cannot just inherit from the equivalent types (eg. from ObservableObject). If that is possible, inheriting is the recommended approach, as it will reduce the binary size by avoiding creating duplicated code into the final assembly."
You're not inheriting from IMyInterface, you're implementing it.
Your viewmodel should inherit ObservableObject
Instead of
[INotifyPropertyChanged]
public partial class MyItem : IMyInterface
You should have:
public partial class MyItem : ObservableObject, IMyInterface
Properties look like:
[ObservableProperty]
private List<FlatTransactionVM> transactions = new List<FlatTransactionVM>();
That generates a public Transactions property.
If you only have properties, you don't need that to be a partial class. That's necessary for relaycommand generation.
EDIT
This works for me:
MainWindow
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<ListBox ItemsSource="{Binding MyCollection}"
DisplayMemberPath="MyProperty"/>
</Grid>
MainWindowViewModel
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
private ObservableCollection<IMyInterface> myCollection = new ObservableCollection<IMyInterface>();
public MainWindowViewModel()
{
MyCollection = new ObservableCollection<IMyInterface>(
new List<MyItem>{
new MyItem{ MyProperty = "A" },
new MyItem { MyProperty = "B" },
new MyItem { MyProperty = "C" }
});
}
}
MyItem
public partial class MyItem : ObservableObject, IMyInterface
{
[ObservableProperty]
private string myProperty;
}
Interface
public interface IMyInterface
{
string MyProperty { get; set; }
}
Quick and dirty code in the view. This is purely to see what happens when one of the MyItems.MyProperty is set.
private async void Window_ContentRendered(object sender, EventArgs e)
{
await Task.Delay(5000);
var mw = this.DataContext as MainWindowViewModel;
mw.MyCollection[1].MyProperty = "XXXXX";
}
After a short wait, I see the second item change to XXXXX as expected.
To simplify situation. I have MainWindow with two user controls, all of them have corresponding Viewmodels. Everything works fine, properties bind and so on, beside one functionality.
I want to refresh data on second user control after event happened in the first one. Unfortunetly in this scenario, PropertyChanged event (derived from INotifyPropertyChanged, defined in ViewModelBase) is null.
However, if I raise an event from second user control, property on view gets updated as expected!
public class MainWindowViewModel : ViewModelBase
{
public FirstUserControl FirstUserControl {get; set;}
public SecondUserControl SecondUserControl {get; set;}
public MainWindowViewModel ()
{
FirstUserControl =new FirstUserControl();
FirstUserControl.RaiseClicked+=OnRaiseClicked;
SecondUserControl = new SecondUserControl();
SecondUserControl .RaiseClicked+=OnRaiseClicked;
}
private void OnRaiseClicked(object sender, EventArgs e)
{
SecondUserControl.RefreshView();
}
}
public class FirstUserControl : ViewModelBase
{
public ICommand Raise { get; private set; }
public EventHandler RaiseClicked {get;set;}
public FirstUserControl ()
{
Raise = new RelayCommand( p=> RaiseClicked(this, null));
}
}
public class SecondUserControl: ViewModelBase
{
public ICommand Raise { get; private set; }
public EventHandler RaiseClicked {get;set;}
public string Title
{
get
{
return MyLogic.GetCurrentTitle(); // debuggers enter here only while event on second user control raised
}
}
public void RefreshView()
{
OnPropertyChanged("Title"); // debugger enter here in cases
}
}
I suppose there is something with threads going on, but I'm not that familiar with WPF to work out it by my own. Can someone help how to quickly and easy make event from first UC refresh data on the second?
I need to use a wrapper for my model, because I need to create some "View Only" properties, that I think, doesn't fit in the Model. The problem is, it's a collection member
This is my code snippet to make things clearer. Even though I didn't write it down completely here (to shorten the code), but I still use INotifyPropertyChanged correctly in my real code.
This is my A,B Class Model and MainWindowVM :
public class A : INPC
{
//Some codes
}
public class B : INPC
{
public ObservableCollection<A> As {get; set;}
}
public class MainWindowVM : INPC
{
public ObservableCollection<B> Bs {get; set;}
}
This is my MainWindow code :
<ItemsControl Grid.Column="0" Grid.Row="0" ItemsSource="{Binding CollectionOfA, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<c:B_UserControl/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
As we all already know, code above "flow" is : MainWindowVM (VM) -> B (Model) -> A (Model)
But, I need to make it : MainWindowVM (VM) -> [ BVM (VM) -> B (Model) ] -> [ AVM (VM) -> A (Model) ]
Given this AVM and BVM (as "Model Wrapper"):
public class AVM : INPC
{
public A Model {get; set;}
public AVM (A model) { Model = model; }
}
public class BVM : INPC
{
public B Model {get; set;}
public BVM (B model) { Model = model; }
}
How to do it?
My guess on the solution :
Change this :
public class MainWindowVM : INPC
{
public ObservableCollection<A> As {get; set;}
}
public class AVM : INPC
{
public A Model {get; set;}
public AVM (A model) { Model = model; }
}
public class BVM : INPC
{
public B Model {get; set;}
public BVM (B model) { Model = model; }
}
Into :
public class MainWindowVM : INPC
{
public ObservableCollection<BVM> BVMs {get; set;}
}
public class AVM : INPC
{
public A Model {get; set;}
public AVM (A model) { Model = model; }
}
public class BVM : INPC
{
public B Model {get; set;}
public ObservableCollection<AVM> AVMs {get; set;} //This is the Binding Property from the UserControl's ItemsControl (code similar to the MainWindow one)
public BVM (B model) { Model = model; //Then Initialize AVMs, based on Collection of As in B Model }
}
Is my answer already correct, or is there any more appropriate answer? (Without using DependencyProperty )
When wrapping a model class in a data type view model, it is customary to wrap each property so that you can implement the INotifyPropertyChanged interface in the wrapper class. Your model classes are supposed to represent your business objects, so you shouldn't implement the NotifyPropertyChanged interface there. Therefore, your wrapper class would then look more like this:
public class AVM : INotifyPropertyChanged
{
private A model;
public AVM(A model) { this.model = model; }
public string SomeStringProperty
{
get { return model.SomeStringProperty; }
set
{
model.SomeStringProperty = value;
NotifyPropertyChanged("SomeStringProperty");
}
}
...
}
As a new one to WPF, I start my MVVM travel recently. I can understand the orginal intension about why we need MVVM, but some of the implementation detail still confuse me a lot.
Here is one of my questions:
How should I export the property in model to View via ViewModel
I can show some of my idea here, so please share your view with me.
Here is one of my implementation:
class MyModel : INotifyPropertyChanged
{
private String _name;
public String Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class MyViewModel
{
private MyModel _model;
public MyModel Model
{
get { return _model; }
set { _model = value; }
}
}
I think the problem for this is we do not decouple the view from model. And the view need to know every detail about the model implementation to accomplish the Binding process. And exporting the propert in Model directly to view from ViewModel cannot be treat as a good design in my opinion.
So I hope you can share your experience on the design about this topic.
BTW,
If we export many object, such as, a List of MyModel object to view from viewmodel, how can I implement this to decouple the view from model?
Not your Model, but your ViewModel shall implement INotifyPropertyChanged. Then you can use Binding in the View to get the Data from the ViewModel.
In your View / XAML you have statements like
... Content="{Binding myViewModelProperty}" ...
depending on what you are binding.
myViewModelProperty has to be a public property in your ViewModel
public string myViewModelProperty { get; set; }
Do not forget to call RaiseNotifyPropertyChanged (or what your handler is called) in the setter to get updates in the View.
private string myViewModelField;
public string myViewModelProperty
{
get
{
return myViewModelField;
}
set
{
myViewModelField = value;
RaiseNotifyPropertyChanged(() => myViewModelProperty);
}
}
** Update **
Lists are typically "exported" via ObservableCollection<Type>.
Ideally Type is some ViewModel here, created with data from the model to
be shown in your View. You can imagine the ViewModel as Adapter between
your model and your view.
This is not a strict rule, but in my case I prefer to implement INotifyPropertyChanged in the ViewModels and to leave the Models as simple POCOs.
It can be annoying to "repeat" the properties of the Model in the ViewModel, but :
Generally you don't need to expose all the properties to the View.
It allows you to add additional code like user input validation in your VM and keep your Model "clean".
So in your example it would be:
class MyModel
{
public String Name
{
get;
set;
}
}
class MyViewModel : ViewModelBase //let's say you have this base class in your framework
{
private MyModel model;
public MyViewModel(MyModel model)
{
this.model = model;
}
public string Name
{
get
{
return this.model.Name;
}
set
{
if(IsValidInput(value)
{
this.model.Name = value;
this.RaisePropertyChanged("Name"); // the ViewModelBase base class provide this helper.
}
}
}
}
About your second question, ObservableCollection is usually a nice way to expose a collection of Models to the view:
class ContactListViewModel
{
public ObservableCollection<MyModel> Contacts { get; private set;}
}
You can simply bind your property on view wiuth Model.Name
Ok I'll make this very simple! Here are viewmodels :
public class ObjectsModel
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private string _objectName;
public string ObjectName
{
get
{
return _objectName;
}
set
{
if (value != _objectName)
{
_objectName = value;
PropertyChanged(this, new PropertyChangedEventArgs("ObjectName"));
}
}
}
public IEnumerable<Object> Objects {get;set;}
public ICommand AddCommand { get; private set; }
public ICommand SaveChangesCommand { get; private set; }
myDomainContext context = new myDomainContext();
public ObjectsModel()
{
objects = context.Objects;
context.Load(context.GetObjectsQuery());
}
}
public class InventoryModel
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public IEnumerable<Inventory> Inventories {get;set;}
public ICommand AddCommand { get; private set; }
public ICommand SaveChangesCommand { get; private set; }
myDomainContext context = new myDomainContext();
public ObjectsModel()
{
objects = context.Objects;
context.Load(context.GetObjectsQuery());
}
}
So what I'm trying to do is in my second form where I want to add an inventory for an object, I have to select the object in a combobox. The question is, how do I fill my combobox? Create another instance of the "ObjectsModel" in the InventoryModel? or use another "context" where I would query the other table? Or is there an easier Xaml approach? If I'm not clear, tell me I'll put more examples/code.
tx a lot!
You want to bind the contents of the combobox to a list of items provided by your ViewModel and bind the selected item to another property on the same ViewModel.
Please get into the habit of naming actual view models to end in "ViewModel", rather than "Model", so they do not clash with your other "real" models. It actually looks like you are binding directly to your business models instead of ViewModels (which is not good).