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
}
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
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 == null) return; 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>(); } }
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
You can send the test project to jonny.bekkum(a)gmail.com.
Hi Jonny
Just to let you know I've emailed you my project, subject 'CslaModelBinder and OnChildChanged problem'.
Thanks
Andrew
Hi there
Any progress with this one?
Thanks
Andrew
Hi,
No, haven't had time to dive into this one yet.
Copyright (c) Marimer LLC