RehanSaeed / rehansaeed.github.io

Muhammad Rehan Saeed's Blog
https://rehansaeed.com
30 stars 6 forks source link

[Comment] Model-View-ViewModel (MVVM) - Part 4 - INotifyDataErrorInfo #93

Open RehanSaeed opened 4 years ago

RehanSaeed commented 4 years ago

https://rehansaeed.com/model-view-viewmodel-mvvm-part4-inotifydataerrorinfo/

RehanSaeed commented 4 years ago

Nicolas Nicolas commented on 2014-11-08 22:44:40

Hi,

What is RuleCollection in this private static RuleCollection rules = new RuleCollection();

/// 
/// Gets the rules which provide the errors.
/// 
/// The rules this instance must satisfy.
protected static RuleCollection Rules
{
    get { return rules; }
}

I cannot figure this out if it is a list or anything else. Can you define it some where. Everything else is awesome.

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2014-11-09 12:04:44

Hi,

What is RuleCollection in this private static RuleCollection rules = new RuleCollection();

/// 
/// Gets the rules which provide the errors.
/// 
/// The rules this instance must satisfy.
protected static RuleCollection Rules
{
    get { return rules; }
}

I cannot figure this out if it is a list or anything else. Can you define it some where. Everything else is awesome.

It holds the validation rules for your view model.

For a real example look at the Validation tab in the Elysium Extra sample application. You can find the project on CodePlex, there are posts on my blog or install the Elysium.Extra NuGet package which also contains the sample application and source code.

That example shows how the above code can be used to validate input from various controls including TextBox, ComboBox and DatePicker with cool effects too.

RehanSaeed commented 4 years ago

Nicolas Nicolas commented on 2014-11-10 14:42:36

It holds the validation rules for your view model.

For a real example look at the Validation tab in the Elysium Extra sample application. You can find the project on CodePlex, there are posts on my blog or install the Elysium.Extra NuGet package which also contains the sample application and source code.

That example shows how the above code can be used to validate input from various controls including TextBox, ComboBox and DatePicker with cool effects too.

Thanks

RehanSaeed commented 4 years ago

Suhas Suhas commented on 2015-06-12 16:07:17

Hey I have been reading your implementations for these classes and have been using them for development. I notice that you have implemented your INotifyErrorInfo & INotifyPropertyChanged as two classes rather than interfaces. This would mean that I cannot define a BaseViewModel class that can handle Error Notifications and Property changed notifications concurrently. Am I incorrect in my assessment? How do you deal with something like this? (example Login page where you need to implement INotifyPropertyChanged to show a Splash Screen when the password is entered & a key command is pressed but also throw exceptions for your credentials.) By the way,Great implementation for Elysium Extra. Makes UI much easier!. Thanks, Suhas

RehanSaeed commented 4 years ago

Suhas Suhas commented on 2015-06-12 16:25:50

Sorry. Reread your post. You already thought of that and inherited from the class you defined earlier. My bad! This makes it much easier. Thanks a TON!

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2015-06-12 17:15:08

Sorry. Reread your post. You already thought of that and inherited from the class you defined earlier. My bad! This makes it much easier. Thanks a TON!

Good stuff. You saved me from writing a really long reply.

RehanSaeed commented 4 years ago

Rata Rata commented on 2015-10-20 19:05:03

Hi. Very nice blog, it helped me a lot!

I got a questions:

  1. Don't you think, that passing propertyName as string when you add Rules can lead to typo-like mistakes? Maybe it should be done with reflection as far as we initialize Rules only 1 time on viewmodel creation, so it will not so performance-affective but more safe for human mistakes?
  2. If I need to implement some display-related logic in viewmodel, that should run when my property changed, should i execute my method from property setter or my viewmodel should subscribe on PropertyChanged event in viewmodelBase? And if i should use second way, should i make switch based on string property name in method that process PropertyChanged event, or there is some alternative?

I ask this questions because im really afraid of human factor when i use string names instead of types and so on.

Sorry for my bad english.

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2015-10-21 17:13:30

Hi. Very nice blog, it helped me a lot!

I got a questions:

  1. Don't you think, that passing propertyName as string when you add Rules can lead to typo-like mistakes? Maybe it should be done with reflection as far as we initialize Rules only 1 time on viewmodel creation, so it will not so performance-affective but more safe for human mistakes?
  2. If I need to implement some display-related logic in viewmodel, that should run when my property changed, should i execute my method from property setter or my viewmodel should subscribe on PropertyChanged event in viewmodelBase? And if i should use second way, should i make switch based on string property name in method that process PropertyChanged event, or there is some alternative?

I ask this questions because im really afraid of human factor when i use string names instead of types and so on.

Sorry for my bad english.

Hi,

  1. Yes it can. That's why you can use the [CallerMemberName] attribute or the new C# 6 nameof operator. Reflection is slow but maybe it works for you if you can't use C# 6.
  2. I tend to call the method in the property setter as the property changed event handler can be called if any other property changes too.

Hope that helps.

RehanSaeed commented 4 years ago

Ali Ali commented on 2016-03-03 20:29:13

Hi Rehan

Thanks for your article; i have learned a lot from it; i am new to WPF and wondering if following can be done. I have created following classes and want to add addition rules in owner class for re-usability of FMName class:

public class FMName : NotifyDataErrorInfo
{
    private string _FirstName;
    private string _MiddleName;
    private string _LastName;

    static FMName()
    {
        Rules.Add(new Base.Rules.DelegateRule(
            "FirstName", "Name cannot be Empty", x => !string.IsNullOrEmpty(x.FirstName)));
        Rules.Add(new Base.Rules.DelegateRule(
            "LastName", "Name cannot be Empty", x => !string.IsNullOrEmpty(x.FirstName)));
        Rules.Add(new Base.Rules.DelegateRule(
            "MiddleName", "Name cannot be Empty", x => !string.IsNullOrEmpty(x.FirstName)));
    }

    public string LastName
    {
        get { return this._LastName; }
        set { this.SetProperty(ref this._LastName, value); }
    }

    public string MiddleName
    {
        get { return _MiddleName; }
        set { this.SetProperty(ref this._MiddleName, value); }
    }

    public string FirstName
    {
        get { return _FirstName; }
        set { this.SetProperty(ref this._FirstName, value); }
    }
}

public class Owner : NotifyDataErrorInfo
{
    private int _OwnerID;
    private FMName _OwnerName = new FMName();

    static Owner()
    {
        Rules.Add(new Base.Rules.DelegateRule(
           "Owner.FirstName",
           "Owner First Name Cannot have less than 4 Characters",
           x => x.OwnerName.FirstName.Length < 4);
    }

    public int OwnerID
    {
        get { return this._OwnerID; }
        set { this.SetProperty(ref this._OwnerID, value); }
    }

    public FMName OwnerName
    {
        get { return _OwnerName; }
        set { this.SetProperty(ref this._OwnerName, value); }
    }
}

I am getting property doesn't exist in following:

static Owner()
{
    Rules.Add(new Base.Rules.DelegateRule(
        "Owner.FirstName", "Owner First Name Cannot have less than 4 Characters", x => x.OwnerName.FirstName.Lengt < 4));
}

Thanks again!

Ali

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2016-03-03 21:25:01

Hi Rehan

Thanks for your article; i have learned a lot from it; i am new to WPF and wondering if following can be done. I have created following classes and want to add addition rules in owner class for re-usability of FMName class:

public class FMName : NotifyDataErrorInfo
{
    private string _FirstName;
    private string _MiddleName;
    private string _LastName;

    static FMName()
    {
        Rules.Add(new Base.Rules.DelegateRule(
            "FirstName", "Name cannot be Empty", x => !string.IsNullOrEmpty(x.FirstName)));
        Rules.Add(new Base.Rules.DelegateRule(
            "LastName", "Name cannot be Empty", x => !string.IsNullOrEmpty(x.FirstName)));
        Rules.Add(new Base.Rules.DelegateRule(
            "MiddleName", "Name cannot be Empty", x => !string.IsNullOrEmpty(x.FirstName)));
    }

    public string LastName
    {
        get { return this._LastName; }
        set { this.SetProperty(ref this._LastName, value); }
    }

    public string MiddleName
    {
        get { return _MiddleName; }
        set { this.SetProperty(ref this._MiddleName, value); }
    }

    public string FirstName
    {
        get { return _FirstName; }
        set { this.SetProperty(ref this._FirstName, value); }
    }
}

public class Owner : NotifyDataErrorInfo
{
    private int _OwnerID;
    private FMName _OwnerName = new FMName();

    static Owner()
    {
        Rules.Add(new Base.Rules.DelegateRule(
           "Owner.FirstName",
           "Owner First Name Cannot have less than 4 Characters",
           x => x.OwnerName.FirstName.Length < 4);
    }

    public int OwnerID
    {
        get { return this._OwnerID; }
        set { this.SetProperty(ref this._OwnerID, value); }
    }

    public FMName OwnerName
    {
        get { return _OwnerName; }
        set { this.SetProperty(ref this._OwnerName, value); }
    }
}

I am getting property doesn't exist in following:

static Owner()
{
    Rules.Add(new Base.Rules.DelegateRule(
        "Owner.FirstName", "Owner First Name Cannot have less than 4 Characters", x => x.OwnerName.FirstName.Lengt < 4));
}

Thanks again!

Ali

Hmmmm, I'm not sure that would work. You are creating a validation rule for FMName.FirstName but in the Owner class, I would say this is bad from a design perspective. That rule should live in FMName. If you need to turn the rule on or off under certain conditions then this can be part of the rule itself.

RehanSaeed commented 4 years ago

Ali Ali commented on 2016-03-03 21:45:20

Hmmmm, I'm not sure that would work. You are creating a validation rule for FMName.FirstName but in the Owner class, I would say this is bad from a design perspective. That rule should live in FMName. If you need to turn the rule on or off under certain conditions then this can be part of the rule itself.

Is there any way I turn the rule on/off from owner class?

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2016-03-03 22:01:11

Is there any way I turn the rule on/off from owner class?

Your owner class could set a boolean property on the child class to turn the property on or off. When the boolean property changes, you can raise a PropertyChanged notification for FirstName which will cause the rule to be re-evaluated.

RehanSaeed commented 4 years ago

Ali Ali commented on 2016-03-03 22:06:53

Your owner class could set a boolean property on the child class to turn the property on or off. When the boolean property changes, you can raise a PropertyChanged notification for FirstName which will cause the rule to be re-evaluated.

I will try that, thanks a lot.

RehanSaeed commented 4 years ago

Alex C Alex C commented on 2016-04-06 19:19:16

Hi. This is a great article. As a noob, I'm wondering how the errors would be displayed in the XAML. Do you have a sample of that?

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2016-04-07 09:05:15

Hi. This is a great article. As a noob, I'm wondering how the errors would be displayed in the XAML. Do you have a sample of that?

There is a working example in Elysium Extra or see this.

RehanSaeed commented 4 years ago

Nuri Nuri commented on 2016-05-09 09:17:43

Hi Muhammad,

Nice page. Thanks. I'm wondering that how can I handle (forexample display dialog to user) when Command throw exception? Command called by View, it mutes when catch exception.

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2016-05-09 14:51:15

Hi Muhammad,

Nice page. Thanks. I'm wondering that how can I handle (forexample display dialog to user) when Command throw exception? Command called by View, it mutes when catch exception.

First don't use exceptions to write application logic. If something is invalid, try to use an 'if statement' instead. If you still need to catch an exception, then go for it. Not sure what you mean by 'mutes'.

RehanSaeed commented 4 years ago

Jason Jason commented on 2016-06-21 22:41:49

Probably one the best MVVM series I've read. How do you deal with binding errors from the view? Consider the situation where the user enters letters into textbox that is bound to an integer property in your view model. The view model is remains in a valid state because the change never got that far.

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2016-06-22 15:29:21

Probably one the best MVVM series I've read. How do you deal with binding errors from the view? Consider the situation where the user enters letters into textbox that is bound to an integer property in your view model. The view model is remains in a valid state because the change never got that far.

The way to deal with binding errors is to not have them. You get notified of them in the output window in Visual Studio, so it's pretty easy to find and fix them.

If you are asking about how to do the XAML side of validation, then take a look at the Elysium Extra on Codeplex which has a very good example.

RehanSaeed commented 4 years ago

Jason Jason commented on 2016-06-23 20:13:41

Thanks for the reply. Silverlight has a BindingValidationError routedevent on the FrameworkElement that needs to be handled in a robust application. If you don't, the view could be in an invalid state while the view model remains valid. I suspect WPF and others have similar issues, but using different events. I think the Prism library examples expose all view model properties as nullable even though they are required to get around some of it. Since the binding mechanism defaults to null on binding error, this method will work for required fields. It will fail if the user puts garbage into a field that isn't required. The user will think they saved garbage while the view model saved an empty field. I just thought you might be familiar with the problem and already had a slick implementation. Cheers.

RehanSaeed commented 4 years ago

Anders Anders commented on 2016-10-14 09:40:22

That's 300 lines of code to validate if a textbox is empty!!! GEESUS !!!

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2016-10-14 10:04:54

That's 300 lines of code to validate if a textbox is empty!!! GEESUS !!!

That's 300 lines you don't need to write! Look how clean and simple the ZombieViewModel looks because of those 300 lines!

RehanSaeed commented 4 years ago

Mark Mark commented on 2016-12-21 04:22:57

Awesome work btw!

RehanSaeed commented 4 years ago

Artur Artur commented on 2017-03-18 22:57:49

Is it possible to write a required if rule with this approach? I need to raise exception only if not all 3 properties are filled at the same time. When none of them is filled, I don't want to raise exception.

Could you help me? Thanks in advance

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2017-03-23 16:34:05

Is it possible to write a required if rule with this approach? I need to raise exception only if not all 3 properties are filled at the same time. When none of them is filled, I don't want to raise exception.

Could you help me? Thanks in advance

Something like this? You can refer to other properties in the rule condition.

Rules.Add(new DelegateRule<myviewmodel>(
    "Item",
    "Item cannot be empty.",
    x => string.IsNullOrEmpty(x.SomeOtherProperty) || !string.IsNullOrEmpty(x.Item)));
RehanSaeed commented 4 years ago

Kevin Burns Kevin Burns commented on 2017-04-11 16:47:29

I am trying to figure out how to implement this with a ViewModel that is part of a base class.

This doesn't work obviously, aside from copying the base class properties/functions constantly. Is there an easier way of handling this?

public sealed class AgentViewModel : EditorBaseViewModel, NotifyDataErrorInfo
{
}
RehanSaeed commented 4 years ago

Ole Ole commented on 2017-10-17 16:07:08

Nice article. Looks very clean. How do you handle issues where you have one or more 'related' properties? if you update one property, you need to trigger a validation of the related property(ies), because they could be invalid because of the update.

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2017-10-18 09:21:41

Nice article. Looks very clean. How do you handle issues where you have one or more 'related' properties? if you update one property, you need to trigger a validation of the related property(ies), because they could be invalid because of the update.

You can pass multiple property names to both the SetProperty and OnPropertyChanged methods. To avoid using strings, you can use the nameof keyword.

RehanSaeed commented 4 years ago

Kevin Burns Kevin Burns commented on 2017-11-07 21:55:21

Is there anyone to implement this properly for a Collection count? I have an observable collection, and the rule is setup to throw when the count <= 0. It is showing an error around the listbox bound to the collection, even when the itemsource has items in it. Any ideas?

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2017-11-13 09:19:51

Is there anyone to implement this properly for a Collection count? I have an observable collection, and the rule is setup to throw when the count <= 0. It is showing an error around the listbox bound to the collection, even when the itemsource has items in it. Any ideas?

You don't say it but probably the problem you are having is that you are not calling OnPropertyChanged("Count") which will cause the validation logic to get called and the listbox to update.

RehanSaeed commented 4 years ago

Max Max commented on 2018-01-23 11:31:47

Very nice approach! I have a ViewModel that contains a list of Sub-ViewModels and that has to be erroneous if any of them has an error. What would be the right approach to fire ErrorsChanged on the upper ViewModel when any of the Sub-ViewModels raises WhenErrorsChanged?

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2018-01-23 12:21:26

Very nice approach! I have a ViewModel that contains a list of Sub-ViewModels and that has to be erroneous if any of them has an error. What would be the right approach to fire ErrorsChanged on the upper ViewModel when any of the Sub-ViewModels raises WhenErrorsChanged?

I've done this in a few ways myself. Ideally each class is encapsulated and you register for the childs ErrorsChanged event and bubble it up manually.

RehanSaeed commented 4 years ago

Max Max commented on 2018-01-23 12:39:40

I've done this in a few ways myself. Ideally each class is encapsulated and you register for the childs ErrorsChanged event and bubble it up manually.

I've already subscribed to the WhenErrorsChanged of my Sub-ViewModels, but I don't see how to trigger the WhenErrorsChanged of the upper ViewModel. Raising ErrorsChanged requires DataErrorsChangedEventArgs with property name, which I don't have.

RehanSaeed commented 4 years ago

Ollie Ollie commented on 2018-02-21 08:14:57

Referring to Kevin Burns's question from April 11th of 2017: I'm also curious about how this can be solved.

RehanSaeed commented 4 years ago

Fred Fred commented on 2019-04-05 08:23:52

Hey Rehan,

thank you for your work on this!

My question is, if it is possible to use the "old school" C# events instead of the Rx events? So how to replace the IObservable WhenErrorsChanged with the defualt C# events. I'm not sure if I want to use the Rx Framework just for the Observable class.

Thanks Fred

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2019-04-05 09:15:35

Hey Rehan,

thank you for your work on this!

My question is, if it is possible to use the "old school" C# events instead of the Rx events? So how to replace the IObservable WhenErrorsChanged with the defualt C# events. I'm not sure if I want to use the Rx Framework just for the Observable class.

Thanks Fred

If you're familiar with C# events, you can easily plug them in, in place of the Rx equivalents.

public event EventHandler Foo;

public void OnFoo() => this.Foo?.Invoke(this, EventArgs.Empty);
RehanSaeed commented 4 years ago

Thavious Thavious commented on 2019-05-17 15:03:36

Why did you implement this set of rules, rather than using something existing like DataAnnotations?

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2019-06-11 12:31:52

Why did you implement this set of rules, rather than using something existing like DataAnnotations?

I prefer functions over attributes. They are more powerful, simpler to reason about and faster.

RehanSaeed commented 4 years ago

Remy Meier Remy Meier commented on 2019-06-12 21:53:06

Quite good coded and commented. My Visual Studio shows me an error for Observable. This cannot find the class. Which part of the .NET framework is to use for using?

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2019-06-15 10:38:39

Quite good coded and commented. My Visual Studio shows me an error for Observable. This cannot find the class. Which part of the .NET framework is to use for using?

You can find that in the System.Reactive package.

RehanSaeed commented 4 years ago

David Piepgrass David Piepgrass commented on 2019-07-31 22:45:44

This is all very nice, but how do you write the corresponding XAML for the view?

RehanSaeed commented 4 years ago

Pete Pete commented on 2019-09-04 16:43:41

I love this implementation to establish rules but I have an issue.

I have added rules to my tblCompany model for when creating a new Company the rules ensure the relevant information is added. Although when I use a List as an Itemssource for a Combobox the rules are still in affect. So when a Company is selected the Combobox goes red if one of the rules aren't met. I would like to be able to ignore the rules on the model in some certain circumstances if possible??

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2019-09-23 09:02:22

I love this implementation to establish rules but I have an issue.

I have added rules to my tblCompany model for when creating a new Company the rules ensure the relevant information is added. Although when I use a List as an Itemssource for a Combobox the rules are still in affect. So when a Company is selected the Combobox goes red if one of the rules aren't met. I would like to be able to ignore the rules on the model in some certain circumstances if possible??

Sure they can be ignored. Just add a property to the view model called something like IsValidationEnabled. When it is false, make all your rules valid. When it is true, then run the code in your rules as normal.

RehanSaeed commented 4 years ago

MarkusB MarkusB commented on 2019-10-08 09:11:15

Very nice solution! But i have a problem with inherited classes.

I've created a BaseClass named NotifyDataErrorInfo like you and a Class

public class ValidationExample : NotifyDataErrorInfoBase<ValidationExample>
{
}

All works great.

Now i want another inherited class

public class ValidationDerivedExample : ValidationExample
{
    private int _subVar;

    public int SubVar
    {
        get { return this._subVar; }
        set { this.SetProperty(ref this._subVar, value, "SubVar"); }
    }

    static ValidationDerivedExample()
    {
        Rules.Add(new DelegateRule<ValidationDerivedExample> (
            "SubVar",
            "SubVar cannot be < 0.",
            x => (x.SubVar < 0));
    }
}

And add Rules -> that doesn't work Can you give me please an advice.

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2019-10-10 20:11:28

Very nice solution! But i have a problem with inherited classes.

I've created a BaseClass named NotifyDataErrorInfo like you and a Class

public class ValidationExample : NotifyDataErrorInfoBase<ValidationExample>
{
}

All works great.

Now i want another inherited class

public class ValidationDerivedExample : ValidationExample
{
    private int _subVar;

    public int SubVar
    {
        get { return this._subVar; }
        set { this.SetProperty(ref this._subVar, value, "SubVar"); }
    }

    static ValidationDerivedExample()
    {
        Rules.Add(new DelegateRule<ValidationDerivedExample> (
            "SubVar",
            "SubVar cannot be < 0.",
            x => (x.SubVar < 0));
    }
}

And add Rules -> that doesn't work Can you give me please an advice.

Yes it gets a bit tricky here. I can't remember what my goto solution was but you can fix this in a couple of ways. You can cast in the rule:

Rules.Add(new DelegateRule<ValidationExample> (
    "SubVar",
    "SubVar cannot be < 0.",
    x => (((ValidationDerivedExample)x).SubVar < 0));

Or you can move the generic type up the chain:

public class ValidationExample<T> : NotifyDataErrorInfoBase<T>
    where T : ValidationSubExample
{
}

public class ValidationDerivedExample : ValidationExample<ValidationDerivedExample>
{
}
RehanSaeed commented 4 years ago

MarkusB MarkusB commented on 2019-10-11 08:21:32

Yes it gets a bit tricky here. I can't remember what my goto solution was but you can fix this in a couple of ways. You can cast in the rule:

Rules.Add(new DelegateRule<ValidationExample> (
    "SubVar",
    "SubVar cannot be < 0.",
    x => (((ValidationDerivedExample)x).SubVar < 0));

Or you can move the generic type up the chain:

public class ValidationExample<T> : NotifyDataErrorInfoBase<T>
    where T : ValidationSubExample
{
}

public class ValidationDerivedExample : ValidationExample<ValidationDerivedExample>
{
}

Hello Rehan,

thanks for the fast reply.

Casting the rule i tried befor, unfortunately it doesn't work on runtime, i get an error at Rules.Add(ValidationDerivedExample) because x is not of type ValidationExample

And move up the chain, is also no solution, because i want the error handly in my 'base classes' too.

Is there a possible solution without the generic type in the rule, delegateRule and ruleCollection?

RehanSaeed commented 4 years ago

Kiwi Kiwi commented on 2019-10-11 13:39:09

I'm working on a personal project and I'm using your example code, but I'm calling NotifyDataErrorInfo ModelBase because I want my models to implement the INotifyDataError and the INotifyPropertyChanged.

From my limited knowledge of MVVM, if I want the models to implement the INotify interfaces, and I want my ViewModels to wrap around the models (and not just be a pass-through), I need to have the ViewModel intercept the model's notify event, and re-raise those events.

I don't have a 1 to 1 Model to ViewModel ratio - I have a 1 to Many Model to ViewModel ratio. I'm not sure if this is a bad design decision, but I want the models to stay alive longer than the ViewModels they will be passed to, so for that I also new weak event handlers in my ViewModels coupling to my model's notify events. To accomplish, I have made the following ViewModelBase class:

namespace MyNamespace.ViewModels
{
    abstract class ViewModelBase : Models.ModelBase where T : ViewModelBase
    {
        private object[] models;
        private static readonly Dictionary<(Type, string), string> propertyMappings = new Dictionary<(Type, string), string>();

        protected object[] Models
        {
            get => this.models;
            private set => this.models = value;
        }

        protected static Dictionary<(Type, string), string> PropertyMappings
        {
            get => propertyMappings;
        }

        private void Model_ErrorsChanged(object sender, DataErrorsChangedEventArgs e)
        {
            string propertyName = e.PropertyName;
            if (PropertyMappings.ContainsKey((sender.GetType(), propertyName))) propertyName = PropertyMappings[(sender.GetType(), propertyName)];

            // If PropertyMappings returned a null property name, that means skip the property.
            if (string.IsNullOrWhiteSpace(propertyName)) return;

            // Re-raise property changed event. Chain of events are: Model.ErrorsChanged(Model.Property) -> ViewModel.ErrorsChanged(ViewModel.Property).
            this.OnErrorsChanged(propertyName);
        }

        private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            string propertyName = e.PropertyName;
            if (PropertyMappings.ContainsKey((sender.GetType(), propertyName))) propertyName = PropertyMappings[(sender.GetType(), propertyName)];

            // If PropertyMappings returned a null property name, that means skip the property.
            if (string.IsNullOrWhiteSpace(propertyName)) return;

            // Re-raise property changed event. Chain of events are: Model.PropertyChanged(Model.Property) -> ViewModel.PropetyChanged(ViewModel.Property).
            this.OnPropertyChanged(propertyName);
        }

        protected ViewModelBase(object model) : this(new object[] { model }) { }
        protected ViewModelBase(params object[] models)
        {
            if (models == null) throw new ArgumentNullException(nameof(models));
            if (models.Length < 1) throw new ArgumentException("At least one model is required.", nameof(models));

            this.Models = models;

            foreach (object model in this.Models)
            {
                if (model is INotifyPropertyChanged property)
                    WeakEventManager<object, PropertyChangedEventArgs>.AddHandler(model, nameof(property.PropertyChanged), this.Model_PropertyChanged);
                if (model is INotifyDataErrorInfo data)
                    WeakEventManager<object, DataErrorsChangedEventArgs>.AddHandler(model, nameof(data.ErrorsChanged), this.Model_ErrorsChanged);
            }
        }

        protected override void DisposeManaged()
        {
            foreach (object model in this.Models)
            {
                if (model is INotifyPropertyChanged property)
                    WeakEventManager<object, PropertyChangedEventArgs>.RemoveHandler(model, nameof(property.PropertyChanged), this.Model_PropertyChanged);
                if (model is INotifyDataErrorInfo data)
                    WeakEventManager<object, DataErrorsChangedEventArgs>.RemoveHandler(model, nameof(data.ErrorsChanged), this.Model_ErrorsChanged);
            }
        }
    }
}

So if my design isn't complete garbage, it should be able to have ViewModels that extend this ViewModelBase that are tied to 1 or more object models that may or may not implement the INotify interfaces. It should also be weakly bound to those events, allowing the ViewModels to GCed when the View they are attached to leaves the scope.

One part I'm a little iffy on is Dictionary<(Type, string), string> and how it will work with sub classes. I'm not overly worried about it because my small personal project shouldn't have any models that have sub classes (beyond the initial ModelBase). IE, I won't have "class Model1 : ModelBase" + "class SubModelA : Model1" - I'll only have the "Model1" class in that example, and not the "SubModelA". It would be nice if I had designed Dictionary<(Type, string), string> to work correctly whether the object raising the event was subclass or not - but I only plan on crossing that bridge when I have to (which currently seems unlikely - at least in this personal project).

Anyway, thank you for your articles - I found them very informative. I shared the above class in the hopes that if you have 5 minutes, you'll share any thoughts / wisdom you have on my approach. If not, no biggie. I'm sure I'll find the hidden gotchas once I start debugging the project.

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2019-10-12 10:20:10

Hello Rehan,

thanks for the fast reply.

Casting the rule i tried before, unfortunately it doesn't work on runtime, i get an error at Rules.Add (ValidationDerivedExample)x is not of type ValidationExample

and move up the chain, is also no solution, because i want the error handy in my 'base classes' too.

Is there a possible solution without the generic type in the rule, delegateRule and ruleCollection ?

You should be able to specify rules in both classes using the generics solution.

RehanSaeed commented 4 years ago

Muhammad Rehan Saeed Muhammad Rehan Saeed commented on 2019-10-12 10:26:31

I'm working on a personal project and I'm using your example code, but I'm calling NotifyDataErrorInfo ModelBase because I want my models to implement the INotifyDataError and the INotifyPropertyChanged.

From my limited knowledge of MVVM, if I want the models to implement the INotify interfaces, and I want my ViewModels to wrap around the models (and not just be a pass-through), I need to have the ViewModel intercept the model's notify event, and re-raise those events.

I don't have a 1 to 1 Model to ViewModel ratio - I have a 1 to Many Model to ViewModel ratio. I'm not sure if this is a bad design decision, but I want the models to stay alive longer than the ViewModels they will be passed to, so for that I also new weak event handlers in my ViewModels coupling to my model's notify events. To accomplish, I have made the following ViewModelBase class:

namespace MyNamespace.ViewModels
{
    abstract class ViewModelBase : Models.ModelBase where T : ViewModelBase
    {
        private object[] models;
        private static readonly Dictionary<(Type, string), string> propertyMappings = new Dictionary<(Type, string), string>();

        protected object[] Models
        {
            get => this.models;
            private set => this.models = value;
        }

        protected static Dictionary<(Type, string), string> PropertyMappings
        {
            get => propertyMappings;
        }

        private void Model_ErrorsChanged(object sender, DataErrorsChangedEventArgs e)
        {
            string propertyName = e.PropertyName;
            if (PropertyMappings.ContainsKey((sender.GetType(), propertyName))) propertyName = PropertyMappings[(sender.GetType(), propertyName)];

            // If PropertyMappings returned a null property name, that means skip the property.
            if (string.IsNullOrWhiteSpace(propertyName)) return;

            // Re-raise property changed event. Chain of events are: Model.ErrorsChanged(Model.Property) -> ViewModel.ErrorsChanged(ViewModel.Property).
            this.OnErrorsChanged(propertyName);
        }

        private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            string propertyName = e.PropertyName;
            if (PropertyMappings.ContainsKey((sender.GetType(), propertyName))) propertyName = PropertyMappings[(sender.GetType(), propertyName)];

            // If PropertyMappings returned a null property name, that means skip the property.
            if (string.IsNullOrWhiteSpace(propertyName)) return;

            // Re-raise property changed event. Chain of events are: Model.PropertyChanged(Model.Property) -> ViewModel.PropetyChanged(ViewModel.Property).
            this.OnPropertyChanged(propertyName);
        }

        protected ViewModelBase(object model) : this(new object[] { model }) { }
        protected ViewModelBase(params object[] models)
        {
            if (models == null) throw new ArgumentNullException(nameof(models));
            if (models.Length < 1) throw new ArgumentException("At least one model is required.", nameof(models));

            this.Models = models;

            foreach (object model in this.Models)
            {
                if (model is INotifyPropertyChanged property)
                    WeakEventManager<object, PropertyChangedEventArgs>.AddHandler(model, nameof(property.PropertyChanged), this.Model_PropertyChanged);
                if (model is INotifyDataErrorInfo data)
                    WeakEventManager<object, DataErrorsChangedEventArgs>.AddHandler(model, nameof(data.ErrorsChanged), this.Model_ErrorsChanged);
            }
        }

        protected override void DisposeManaged()
        {
            foreach (object model in this.Models)
            {
                if (model is INotifyPropertyChanged property)
                    WeakEventManager<object, PropertyChangedEventArgs>.RemoveHandler(model, nameof(property.PropertyChanged), this.Model_PropertyChanged);
                if (model is INotifyDataErrorInfo data)
                    WeakEventManager<object, DataErrorsChangedEventArgs>.RemoveHandler(model, nameof(data.ErrorsChanged), this.Model_ErrorsChanged);
            }
        }
    }
}

So if my design isn't complete garbage, it should be able to have ViewModels that extend this ViewModelBase that are tied to 1 or more object models that may or may not implement the INotify interfaces. It should also be weakly bound to those events, allowing the ViewModels to GCed when the View they are attached to leaves the scope.

One part I'm a little iffy on is Dictionary<(Type, string), string> and how it will work with sub classes. I'm not overly worried about it because my small personal project shouldn't have any models that have sub classes (beyond the initial ModelBase). IE, I won't have "class Model1 : ModelBase" + "class SubModelA : Model1" - I'll only have the "Model1" class in that example, and not the "SubModelA". It would be nice if I had designed Dictionary<(Type, string), string> to work correctly whether the object raising the event was subclass or not - but I only plan on crossing that bridge when I have to (which currently seems unlikely - at least in this personal project).

Anyway, thank you for your articles - I found them very informative. I shared the above class in the hopes that if you have 5 minutes, you'll share any thoughts / wisdom you have on my approach. If not, no biggie. I'm sure I'll find the hidden gotchas once I start debugging the project.

These are good questions. Nobody teaches this stuff and I had a great deal of trouble trying things and finding what I thought was the best way for my purposes. I quite often ended up with two scenarios:

  1. I ended up with simple POCO model classes that don't implement the INotifyPropertyChanged interface. Then the view model wraps the properties of the model and does all the property changed stuff.
  2. In other scenarios, my model implemented INotifyPropertyChanged and my view model exposed the model as a single property.

What you end up doing depends on your scenario. Whether you have control over the model etc. The second scenario is a bit quicker and less work but the first probably a more pure solution.

RehanSaeed commented 4 years ago

Shimmy Shimmy commented on 2019-12-05 08:47:10

Hi Rehan, I've been subscribed to your blog for a while and love it! I'm using ReactiveUI validation in a WPF project, and I was wondering if it's possible to keep the entities clean (I've only decorated them with data-annotations attributes or IValidatoableObject implementations), and perform all validation logic in the VM, all this while binding directly to the entities (i.e. exposing them as a whole via a property in the VM, not proxying the properties individually in the VM.