Hello,
I am using the [Required] attribute on Properties in my Edit Classes that are "Mandatory".
public static readonly PropertyInfo<string> CompanyNameProperty = RegisterProperty<string>(c => c.CompanyName);
[Required]
[Display(Name = "Company Name")]
public string CompanyName
{
get { return GetProperty(CompanyNameProperty); }
set { SetProperty(CompanyNameProperty, value); }
In my WPF xaml View I usr the PropertyStatus Control
<csla:PropertyStatus Content="{Binding Path=BrokenRules}" Property="{Binding Path=Model.Company.CompanyName}" />
When I load this view for the first time (To Create a new Company) the red Exclamation icon is displayed for all required fields. The Customer has opened a defect stating that the Error Icon should not display when the Form is diaplyed for the first time.
The User Experience should be as follows.
1. For all requied fields display a * (Black star) during first Load.
2. If the user focuses on the field and moves out, Show the Validation icon (Red Exclamation mark) if the entered value is incorrect.
3. If the user does not focus on the Field and clicks "Save", show the Validation Icon (Red Excalation Mark).
What is teh best way to acheive this? Any help will be appreciated. Also can you point me to a sample code if it exists?
Thanks,
sran
Hi,
1. You can either hardcode this in your form or you need to create your own extension that searches each property in the BO and looks for [Required] attribute
2. You _could_ make sure to NOT call BusinessRules.CheckRules in DataPortal_Create (also called from base.DataPortal_Create). This way - your object will have no broken rules when it is new. When user edits fields the fields will automatically be revalidated - but not as a result of just tab'ing or focusing on a control. If you always want to check rules in this conditions it requires more changes to your BO - property setter.
3. Make sure to call BusinessRules.CheckRules to force revalidation of object when user clicks Save.
Thanks Jonny.
1. I do not explicitly call BusinessRules.CheckRules in DataPortal_Create. How do I make sure to _not_ call this from base.DataPortal_Create
2. As a Workaround, I implemented a GatewayRule that accepts a Boolean Value "IsFirstLoad". If this is set to False, I do not execute the InnerRule. I just add a Informational Message. This shows an Information Message next to the control on first Load. If the user changes the content of the control, It automatically changes to an error icon appropriately. When User Clicks Save, I validate the model from the view model by setting a property isFirstLoad=true. I have also created a dependency rule of all required Properties on the isFirstLoad property.
3. The above workaround is providing the User Experience, the client wants. I am just concerned that this may be a hack and there is a more elegant way of acheiving this. Is the above pattern ok?
Followup Questions
1. Is there a way to Override the Error Icons without having to change source code. It would be great to set the * instead of the standard Information Icon.
2. Is there a way to access the error message of an innerRule in a Gateway Rule. Since I could not find a way, I pass the error message of the inner rule as an input Parameter to the Gateway Rule. I am using this to add an Informational Message.
Please see my CompanyEdit and GatewayRule Snippets.
GatewayRule
public NewRequiredRule(IPropertyInfo primaryProperty, Csla.Rules.IBusinessRule innerRule,string errorMessage, bool isSavedOnce)
: base(primaryProperty)
{
InputProperties = new List<Csla.Core.IPropertyInfo> { PrimaryProperty };
InnerRule = innerRule;
IsSavedOnce = isSavedOnce;
ErrorMessage = errorMessage;
RuleUri.AddQueryParameter("rule", System.Uri.EscapeUriString(InnerRule.RuleName));
RuleUri.AddQueryParameter("isSavedOnce", isSavedOnce.ToString());
}
protected override void Execute(RuleContext context)
{
var target = (Csla.Core.ITrackStatus)context.Target;
var chainedContext = context.GetChainedContext(InnerRule);
if (IsSavedOnce|| context.IsPropertyChangedContext)
{
InnerRule.Execute(chainedContext);
}
else
{
context.AddInformationResult(ErrorMessage);
}
}
Viewmodel
___________
protected override void AddBusinessRules()
{
base.AddBusinessRules();
BusinessRules.AddRule(new Common.NewRequiredRule(CompanyNameProperty, new Csla.Rules.CommonRules.Required(CompanyNameProperty, "Company Name is Required"), "Company Name is Required", IsSavedOnce));
BusinessRules.AddRule(new Common.NewRequiredRule(TaxRegistrationNumberProperty, new Csla.Rules.CommonRules.Required(TaxRegistrationNumberProperty, "TRN is Required."),"TRN is Required.", IsSavedOnce));
BusinessRules.AddRule(new Common.NewRequiredRule(TaxRegistrationNumberProperty, new Csla.Rules.CommonRules.RegExMatch(TaxRegistrationNumberProperty, "^[1-9]{9}$", "TRN should be 9 Digits "),"TRN should be 9 Digits ", IsSavedOnce));
BusinessRules.AddRule(new Common.NewRequiredRule(NISReferenceNumberProperty, new Csla.Rules.CommonRules.Required(NISReferenceNumberProperty, "NIS is required."),"NIS is required.", IsSavedOnce));
BusinessRules.AddRule(new Common.NewRequiredRule(NISReferenceNumberProperty, new Csla.Rules.CommonRules.RegExMatch(NISReferenceNumberProperty, "^[1-9]{7}$", "NIS should be 7 Digits "),"NIS should be 7 Digits ", IsSavedOnce));
BusinessRules.AddRule(new Common.NewRequiredRule(Address1Property, new Csla.Rules.CommonRules.Required(Address1Property, "Street Address is Required"),"Street Address is Required",IsSavedOnce));
BusinessRules.AddRule(new Common.NewRequiredRule(CityProperty, new Csla.Rules.CommonRules.Required(CityProperty, "City is Required"), "City is Required",IsSavedOnce));
BusinessRules.AddRule(new Common.NewRequiredRule(CountryIDProperty, new Csla.Rules.CommonRules.Required(CountryIDProperty, "Country is required"),"Country is required", IsSavedOnce));
BusinessRules.AddRule(new Common.NewRequiredRule(CountryIDProperty, new Csla.Rules.CommonRules.RegExMatch(CountryIDProperty, "^[1-9]\\d*$", "Please select a Valid Country"),"Please select a Valid Country", IsSavedOnce));
BusinessRules.AddRule(new Common.NewRequiredRule(StateIDProperty, new Csla.Rules.CommonRules.Required(StateIDProperty, "State is Required"), "State is Required",IsSavedOnce));
BusinessRules.AddRule(new Common.NewRequiredRule(StateIDProperty, new Csla.Rules.CommonRules.RegExMatch(StateIDProperty, "^[1-9]\\d*$", "Please select a Valid State"),"Please select a Valid State", IsSavedOnce));
BusinessRules.AddRule(new Common.NewRequiredRule(IndustryIDProperty, new Csla.Rules.CommonRules.Required(IndustryIDProperty, "Industry is Required"),"Industry is Required", IsSavedOnce));
BusinessRules.AddRule(new Common.NewRequiredRule(IndustryIDProperty, new Csla.Rules.CommonRules.RegExMatch(IndustryIDProperty, "^[1-9]\\d*$", "Please select a Valid Industry"),"Please select a Valid Industry", IsSavedOnce));
BusinessRules.AddRule(new Csla.Rules.CommonRules.Dependency(IsSavedOnceProperty, CompanyNameProperty,TaxRegistrationNumberProperty,NISReferenceNumberProperty,Address1Property,CityProperty,CountryIDProperty,StateIDProperty,IndustryIDProperty));
}
Thanks,
You have missed on one important step:
Rule instances must be considered a singelton object - ie: CSLA 4.x does only supports type-rule (and not Instance rules) so one instance of the rule is registered for ALL instances of the class type and cannot store information such as IsSavedOnce which would be instance specific for your BO. AddBusinessRules method is only called ONCE per type of BO.
Assuming that IsSavedOnce is a property in your BO then you cannot send it as a parameter on the constructor - it must be read from context.Target (the actual BO to be validated) in the Execute method .
Unless you override DataPortal_Create the default DataPortal_Create in you BO's will call BusinessRules.CheckRules as this is considered best practice and the default behavior.
You can access the results of inner rules in your GatewayRule by inspecting the context.Results property - a list of RuleResult. This is only possible for Sync-rules. Async rules is responsible for calling context.Complete themselves after the async operation is completed.
And if you cast the inner rule to Csla.Rules.PropertyRule you can also access the GetMessage() method of the inner rule.
Thanks for calling out the Singleton! I did notice that IsSavedOnce was always False and now I know the reason! However, I want to keep the rule generic for all BusinessObjects and cannot access the iSavedOnce through the Actual BO. I do not want to create a parent class and define the property there and inherit all my BO's from that. That will be a lot of change. Reflection is also not an option. Extensions Methods are also not an option.
I noticed that context.IsPropertyChangedContext is false on initial load and becomes true as soon as I bind it to UI and return to server. I did not change the content in the UI. What is the trigger for changing context.IsPropertyChangedContext from false to true? Is it that the rule is triggered before the property is initialized (during DataPortal_Create) and the property will be set to true immediately after that (as soon as the rule is completed?). Or is this set when UI binding happens?
For my scenario decribed above, where I want Infomation Icons on first Load and after that I always want the Required Rule ro run, will the following work as expected?
if (context.IsPropertyChangedContext)
{
InnerRule.Execute(chainedContext);
}
else
{
context.AddInformationResult(ErrorMessage);
}
You should not test for any of the context.IsXYZ inside the rule Execute method - and NO - it would not work as you described here.
IsPropertyChangedContext means that the rule is triggered from a PropertyHasChanged event.
When you call BusinessRules.CheckRules to check all rules, no matter if it is in DataPortal_Create or just before Save, the IsPropertyChanged mode will be false.
The best solution is to create IsSavedOnce as a managed property and register the IPropertyInfo with the rule. You can then use the <rule>.ReadProperty in the Execute method to get the actual value.
So I'd rather recommend a rule that inspects a "flag" in your BO and then changes the severity of any broken rule to information like this:
public class ChangeToInformationWhenInitial : BusinessRule { public IPropertyInfo IsSavedOnceProperty { get; private set; } public IBusinessRule InnerRule { get; private set; } public ChangeToInformationWhenInitial(IPropertyInfo primaryProperty, IPropertyInfo isSavedOnceProperty, IBusinessRule innerRule) : base(primaryProperty) { IsSavedOnceProperty = isSavedOnceProperty; InputProperties = new List<IPropertyInfo> { primaryProperty }; InnerRule = innerRule; RuleUri.AddQueryParameter("rule", System.Uri.EscapeUriString(InnerRule.RuleName)); if (InnerRule.InputProperties != null) { InputProperties.AddRange(InnerRule.InputProperties); } InputProperties = new List<IPropertyInfo>(InputProperties.Distinct()); AffectedProperties.AddRange(innerRule.AffectedProperties); } protected override void Execute(RuleContext context) { context.ExecuteRule(InnerRule); var isSavedOnce = (bool) ReadProperty(context.Target, IsSavedOnceProperty); if (!isSavedOnce) return; // change priority for all broken rules to Information foreach (var result in context.Results) result.Severity = RuleSeverity.Information; } }
Copyright (c) Marimer LLC