CslaModelBinder and OnChildChanged problem

CslaModelBinder and OnChildChanged problem

Old forum URL: forums.lhotka.net/forums/t/12147.aspx


j0552 posted on Monday, September 09, 2013

Hi

 

My code below works as expected in a unit test but fails when using the MVC CslaModelBinder. In this case OnChildChanged is not called. I think also it would be better if I could invoke the sibling rules only when the child object's rule gets broken. Can you help?

 

Thanks

Andrew

 

 

    [TestClass]

    public class EditableRootTest

    {

        [TestMethod]

        public void AddDuplicateEmails_BothShouldBeInvalid()

        {

            var account = EditableRoot.New();

 

            account.Email1.EmailAddress = "dup@emailagain.com";

            account.Email2.EmailAddress = "dup@emailagain.com";

 

            Assert.AreEqual(false, account.Email1.IsValid);

            Assert.AreEqual(false, account.Email2.IsValid);

        }

    }

 

    [Serializable]

    public class EditableRoot : BusinessBase<EditableRoot>

    {

        #region Business Methods

 

        public static readonly PropertyInfo<EditableChild> Email1Property =

            RegisterProperty<EditableChild>(c => c.Email1);

 

        public static readonly PropertyInfo<EditableChild> Email2Property =

            RegisterProperty<EditableChild>(c => c.Email2);

 

        public EditableChild Email1

        {

            get { return GetProperty(Email1Property); }

            private set { LoadProperty(Email1Property, value); }

        }

 

        public EditableChild Email2

        {

            get { return GetProperty(Email2Property); }

            private set { LoadProperty(Email2Property, value); }

        }

 

        #endregion

 

        #region Business Rules

 

        protected override void OnChildChanged(ChildChangedEventArgs e)

        {

            if (e.ChildObject is EditableChild && e.PropertyChangedArgs.PropertyName == "EmailAddress")

            {

                Email1.CheckEmailRules();

                Email2.CheckEmailRules();

            }

 

            base.OnChildChanged(e);

        }

 

        #endregion

 

        #region Factory Methods

 

        private EditableRoot()

        {

            /* Require use of factory methods */

        }

 

        public static EditableRoot New()

        {

            return DataPortal.Create<EditableRoot>();

        }

 

        #endregion

 

        #region Data Access

 

        [RunLocal]

        protected override void DataPortal_Create()

        {

            using (BypassPropertyChecks)

            {

                Email1 = EditableChild.New();

                Email2 = EditableChild.New();

            }

            base.DataPortal_Create();

        }

       

        #endregion

    }

 

    [Serializable]

    public class EditableChild : BusinessBase<EditableChild>

    {

        #region Business Methods

 

        public static readonly PropertyInfo<string> EmailAddressProperty = RegisterProperty<string>(c => c.EmailAddress);

 

        public string EmailAddress

        {

            get { return GetProperty(EmailAddressProperty); }

            set { SetProperty(EmailAddressProperty, value); }

        }

 

        #endregion

 

        #region Business Rules

 

        protected override void AddBusinessRules()

        {

            BusinessRules.AddRule(new DuplicatesRule(EmailAddressProperty));

        }

 

        private class DuplicatesRule : BusinessRule

        {

            public DuplicatesRule(IPropertyInfo primaryProperty)

                : base(primaryProperty)

            {

                InputProperties = new List<IPropertyInfo> { primaryProperty };

            }

 

            protected override void Execute(RuleContext context)

            {

                var address = (string)context.InputPropertyValues[PrimaryProperty];

                if (string.IsNullOrEmpty(address)) return;

 

                var target = (EditableChild) context.Target;

                var parent = (EditableRoot) target.Parent;

 

                var email1 = (EditableChild)ReadProperty(parent, EditableRoot.Email1Property);

                var email2 = (EditableChild)ReadProperty(parent, EditableRoot.Email2Property);

 

                var addresses = new List<string>();

                if (!string.IsNullOrEmpty(email1.EmailAddress)) addresses.Add(email1.EmailAddress);

                if (!string.IsNullOrEmpty(email2.EmailAddress)) addresses.Add(email2.EmailAddress);

 

                if (addresses.Count() > addresses.Distinct(StringComparer.OrdinalIgnoreCase).Count())

                {

                    context.AddErrorResult("Duplicate email address not allowed.");

                }

            }

        }

 

        internal void CheckEmailRules()

        {

            BusinessRules.CheckRules(EmailAddressProperty);

        }

 

        #endregion

 

        #region Factory Methods

 

        private EditableChild()

        {

            /* Require use of factory methods */

        }

 

        internal static EditableChild New()

        {

            return DataPortal.CreateChild<EditableChild>();

        }

 

        #endregion

    }

 

 

 

j0552 replied on Wednesday, September 11, 2013

Hi

I'm sure this is a bug in CSLA. The ChildChanged event must and should fire when a property changes regardless of the UI technology. 

This post highlights the same issues I'm having without identifying a solution.

http://forums.lhotka.net/forums/p/11282/52449.aspx

I really need a fix for this.

Thank you

Andrew

JonnyBee replied on Sunday, September 15, 2013

No, this is not a bug in CSLA. 

Here is the semantic flow of execution in the CslaModelBinder when EMal1 and EMail2 child objects is not initialized:

    [TestMethod()]
    public void CslaModelBinderTest()
    {
      var root = new EditableRoot();
      (root as ICheckRules).SuppressRuleChecking();
      // create child1
      var child1 = new EditableChild();
      (child1 as ICheckRules).SuppressRuleChecking();
      child1.EmailAddress = "my@email.com";
      (child1 as ICheckRules).ResumeRuleChecking();
      (child1 as ICheckRules).CheckRules();
      // set EMail1 property
      root.Email1 = child1;
 
      // create Child2
      var child2 = new EditableChild();
      (child2 as ICheckRules).SuppressRuleChecking();
      child2.EmailAddress = "my@email.com";
      (child2 as ICheckRules).ResumeRuleChecking();
      (child2 as ICheckRules).CheckRules();
      // set EMail2 property
      root.Email2 = child2;
 
      (root as ICheckRules).ResumeRuleChecking();
      (root as ICheckRules).CheckRules();
      Assert.IsTrue(root.Email1.GetBrokenRules().Count > 0); 
      Assert.IsTrue(root.Email2.GetBrokenRules().Count > 0);
    }

As you can see the
My preferred solution would be to add an object level rule on the root object to call checkrules on the child objects.

When I update the code to this the rules (and test above) run as expected:

  [Serializable]   public class EditableRoot : BusinessBase<EditableRoot>   {     public static readonly PropertyInfo<string> NameProperty = RegisterProperty<string>(c => c.Name);     public string Name     {       get { return GetProperty(NameProperty); }       set { SetProperty(NameProperty, value); }     }     public static readonly PropertyInfo<string> AdressProperty = RegisterProperty<string>(c => c.Adress);     public string Adress     {       get { return GetProperty(AdressProperty); }       set { SetProperty(AdressProperty, value); }     }          public static readonly PropertyInfo<EditableChild> Email1Property = RegisterProperty<EditableChild>(c => c.Email1);     public EditableChild Email1     {       get { return GetProperty(Email1Property);  }       set { SetProperty(Email1Property, value);}     }     public static readonly PropertyInfo<EditableChild> Email2Property = RegisterProperty<EditableChild>(c => c.Email2);     public EditableChild Email2     {       get { return GetProperty(Email2Property); }       set { SetProperty(Email2Property, value); }     }     public static EditableRoot New()     {       return DataPortal.Create<EditableRoot>();     }     public static EditableRoot Get(int id)     {       return DataPortal.Fetch<EditableRoot>(id);     }     protected void DataPortal_Fetch(int id)     {       LoadProperty(NameProperty, string.Format("Cust {0}", id));     }     protected override void OnChildChanged(ChildChangedEventArgs e)     {       if (e.ChildObject is EditableChild && e.PropertyChangedArgs.PropertyName == "EmailAddress")       {         BusinessRules.CheckObjectRules();       }       base.OnChildChanged(e);     }     protected override void AddBusinessRules()     {       base.AddBusinessRules();       // add object level rule to check rules in child when <this>.CheckRules is called        BusinessRules.AddRule(new CheckEMailRules());     }   }   public class CheckEMailRules : BusinessRule   {     protected override void Execute(RuleContext context)     {       var root = (EditableRoot) context.Target;       root.Email1.CheckEmailRules();       root.Email2.CheckEmailRules();     }   } }

  [Serializable]   public class EditableChild : BusinessBase<EditableChild>   {     public static readonly PropertyInfo<string> EmailAddressProperty = RegisterProperty<string>(c => c.EmailAddress);     public string EmailAddress     {       get { return GetProperty(EmailAddressProperty); }       set { SetProperty(EmailAddressProperty, value); }     }     protected override void AddBusinessRules()     {       BusinessRules.AddRule(new DuplicatesRule(EmailAddressProperty));     }     private class DuplicatesRule : BusinessRule     {       public DuplicatesRule(IPropertyInfo primaryProperty)         : base(primaryProperty)       {         InputProperties = new List<IPropertyInfo> { primaryProperty };       }       protected override void Execute(RuleContext context)       {         var address = (string)context.InputPropertyValues[PrimaryProperty];         if (string.IsNullOrEmpty(address)) return;         var target = (EditableChild)context.Target;         var parent = (EditableRoot)target.Parent;         if (parent == nullreturn;         var email1 = (EditableChild)ReadProperty(parent, EditableRoot.Email1Property);         var email2 = (EditableChild)ReadProperty(parent, EditableRoot.Email2Property);         if ((email1 == null) || (email2 == null)) return;         var addresses = new List<string>();         if (!string.IsNullOrEmpty(email1.EmailAddress)) addresses.Add(email1.EmailAddress);         if (!string.IsNullOrEmpty(email2.EmailAddress)) addresses.Add(email2.EmailAddress);         if (addresses.Count() > addresses.Distinct(StringComparer.OrdinalIgnoreCase).Count())         {           context.AddErrorResult("Duplicate email address not allowed.");         }       }     }     internal void CheckEmailRules()     {       BusinessRules.CheckRules(EmailAddressProperty);     }     internal static EditableChild New()     {       return DataPortal.CreateChild<EditableChild>();     }   }


j0552 replied on Monday, September 16, 2013

Hi

Thank you for the detailed response. I have applied your suggestions to my code and can see that the test passes correctly with both email objects having a broken rule.

However, when I run things in an MVC controller, the CslaModelBinder does not update the ModelState errors correctly. In this case only Email2 error gets added.

Also I notice that the test calls OnChildChanged but the CslaModelBinder does not.

I can send you the test project if that helps?  How would I do that?

Thank you
Andrew 

JonnyBee replied on Monday, September 16, 2013

You can send the test project to jonny.bekkum(a)gmail.com. 

j0552 replied on Tuesday, September 17, 2013

Hi Jonny

Just to let you know I've emailed you my project, subject 'CslaModelBinder and OnChildChanged problem'.

Thanks
Andrew

j0552 replied on Friday, September 20, 2013

Hi there

Any progress with this one?

Thanks

Andrew

JonnyBee replied on Friday, September 20, 2013

Hi,

No, haven't had time to dive into this one yet.

Copyright (c) Marimer LLC