Validation on object level

Validation on object level

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


alef posted on Friday, March 26, 2010

I've the following Business Rule (the code doesn't really matter). But this buniness is not bound to a particular property, but must be executed at object level. How can I do this because ValidationRules.AddRule needs always a propertyInfo? So the purpose is that the business rule must be verified when the object is in a dirty state and the object is inserted or updated in the database.

 

    private static bool Maximum2LoopnummersOrKampnummers<T>(
       T target, Csla.Validation.RuleArgs e) where T : PisteWedstrijdRegistratie
    {
      int aantalLoopnummers = 0;
      int aantalKampnummers = 0;
      if (_discipline1IdProperty != null)
      {
        DisciplineInfo discipline1 = DisciplineLijst.GetDisciplineLijst().Where
            (d => d.Id == target.ReadProperty(_discipline1IdProperty)).Single();

        if (discipline1.IsLoopnummer)
        {
          aantalLoopnummers += 1;
        }
        else
        {
          aantalKampnummers += 1;
        }
      }

      if (_discipline2IdProperty != null)
      {
        DisciplineInfo discipline2 = DisciplineLijst.GetDisciplineLijst().Where
            (d => d.Id == target.ReadProperty(_discipline2IdProperty)).Single();

        if (discipline2.IsLoopnummer)
        {
          aantalLoopnummers += 1;
        }
        else
        {
          aantalKampnummers += 1;
        }
      }

      if (_discipline3IdProperty != null)
      {
        DisciplineInfo discipline3 = DisciplineLijst.GetDisciplineLijst().Where
            (d => d.Id == target.ReadProperty(_discipline3IdProperty)).Single();

        if (discipline3.IsLoopnummer)
        {
          aantalLoopnummers += 1;
        }
        else
        {
          aantalKampnummers += 1;
        }
      }

      if (aantalLoopnummers > 2)
      {
        e.Description = "Maximum 2 loopnummers zijn toegelaten.";
        return false;
      }
      if (aantalKampnummers > 2)
      {
        e.Description = "Maximum 2 kampnummers zijn toegelaten.";
        return false;
      }

      return true;

    }

RockfordLhotka replied on Friday, March 26, 2010

The typical solution is to attach the rule to a fake property. AddRule() doesn't require a IPropertyInfo, it can also take a string property name - and in that case you can create your own fake property name.

This is a valuable technique you can use to create "rule sets" that are executed when you want to run them by name.

CheckRules() also has an overload that takes a string, which would be the same name as the fake property.

alef replied on Friday, March 26, 2010

 

I've implemented the following code with a fake property Max2 and did also an override of the IsValid method. The only problem I still have is that I don't see the error appear at the row indicator in the grid.

    /// <summary>
    /// All custom rules need to be placed in this method.
    /// </summary>
    /// <returns>Return true to override the generated rules; If false generated rules will be run.</returns>
    protected bool AddBusinessValidationRules()
    {
      ValidationRules.AddRule<PisteWedstrijdRegistratie>(Maximum2LoopnummersOrKampnummers,"Max2");
      return false;
    }

 

    public override bool IsValid
    {
      get
      {
        ValidationRules.CheckRules("Max2"); 
        return base.IsValid;
      }
    }

RockfordLhotka replied on Friday, March 26, 2010

Windows Forms data binding relies on the PropertyChanged event to know that it should update its validation display. You'll need to call OnUnknownPropertyChanged() to raise the event so data binding knows to display the results.

You should absolutely NOT override IsValid to check rules. IsValid gets called a lot, and you could suffer nasty performance impact. Besides, this won't allow for any raising of events. If you raise a PropertyChanged event in IsValid I think you'll create an infinite loop and get a stack overflow exception.

You need to decide what should trigger your rule. Is it a property changing? Or does this rule run when the object is created or saved? Or when the user leaves the row in the grid?

Then you need to override the correct method in response - such as OnChildChanged(), OnPropertyChanged(), Save(), or others.

If you need to detect when the user has finished editing the object in a datagrid, you should be able to override AcceptChangesComplete() - though that's an advanced scenario and you MUST make sure to call base.AcceptChangesComplete() or you'll break basic CSLA functionality.

alef replied on Friday, March 26, 2010

I need to detect when the user has finished editing the object in the grid by leaving the row.

I've implemented the following code:

A) business layer

    protected override void AcceptChangesComplete()
    {     
      ValidationRules.CheckRules("Max2");
      base.AcceptChangesComplete();
    }

B) user interface

    private void gridViewRegistraties_InvalidRowException(object sender, DevExpress.XtraGrid.Views.Base.InvalidRowExceptionEventArgs e)
    {
      //Suppress displaying the error message box
      e.ExceptionMode = ExceptionMode.NoAction;
      MessageBox.Show(e.ErrorText);
    }

==> this gives me the error description in the row indicator and I get the following messagebox : "Object is not valid and can not be saved." I suppose this error is raised by calling base.AcceptChangesComplete() and is an error produced by the CSLA framework?

 

A second possibility would be the following which gives really the description of what is going wrong as implemented in the business layer by setting the e.Description property. And also the error is displayed in the row indicator of the grid. So here the user sees two times what is going wrong.

    private void gridViewRegistraties_InvalidRowException(object sender, DevExpress.XtraGrid.Views.Base.InvalidRowExceptionEventArgs e)
    {
      //Suppress displaying the error message box
      e.ExceptionMode = ExceptionMode.NoAction;
      KAVVVLibrary.PisteWedstrijdRegistratie pwr = e.Row as KAVVVLibrary.PisteWedstrijdRegistratie;
      if (pwr != null)
      {
        MessageBox.Show(pwr.BrokenRulesCollection.ToString());
      }
    }

 

Can you confirm that this is the way to go because I doubt a little bit because you are mentioning it is an advanced scenario and I only have two lines of code? Must base.AcceptChangesComplete(); be called as last step in AcceptChangesComplete?

    protected override void AcceptChangesComplete()
    {     
      ValidationRules.CheckRules("Max2");
      base.AcceptChangesComplete();
    }

RockfordLhotka replied on Friday, March 26, 2010

alef

Can you confirm that this is the way to go because I doubt a little bit because you are mentioning it is an advanced scenario and I only have two lines of code? Must base.AcceptChangesComplete(); be called as last step in AcceptChangesComplete?

    protected override void AcceptChangesComplete()
    {     
      ValidationRules.CheckRules("Max2");
      base.AcceptChangesComplete();
    }

That is correct, though you should probably call the base method first, before checking rules, otherwise the accept operation won't be complete before your rules are run. I don't remember exactly what all happens during the base class operations, but it is important Smile

From what you describe, are you using an ERLB as a list? In that case things may be more complex. The reason is that the save operation is triggered by the base AcceptChangesComplete() - that's how ERLB knows to save the object. So in that case you absolutely need to call the base method after your rules have run.

So I guess if your current code is working then you are good, and should leave it as-is.

alef replied on Saturday, March 27, 2010

I'm using an ERLB as list because I want to update/insert the data to the database when the user clicks off the row in the grid.

I can confirm what you are telling that in this case (EditableRootListBase) the call to the base method must be after ValidationRules.CheckRules("Max2");

I've tried to switch this but indeed then the rule is not checked and an invalid object is saved to the database.

 

I don't understand why this should be different in cases other then ERLB. I don't have such a case for the moment so I can't experiment with this.

RockfordLhotka replied on Saturday, March 27, 2010

This is why I said it was an advanced scenario, you need to understand the order of events and what is going on as the objects interact Big Smile

How does ERLB know that the user has "left the row"? The whole concept of "leaving the row" is a UI concept. But data binding calls CancelEdit or ApplyEdit as the user leaves the row. Of course CancelEdit means do nothing, but ApplyEdit then must mean commit the change.

So ERLB uses the fact that ApplyEdit was called to know that it should insert/update/delete the object. That is triggered when the base method is called. So if you call the base method before checking your rules, then the object will be deleted by the time your rules run, which means you must run your rules before calling the base method.

rsbaker0 replied on Sunday, March 28, 2010

Incidentally, we work extensively with ERLB in our app and the automatic saving when leaving a row was one of the hardest things to get worked out properly. (And also eventually one of the reasons why we ended up turning off the IEditableObject interface and doing the BeginEdit/ApplyEdit calls in a central "EditManager" that tracks all root objects currently being edited).

The main problem is what to do when the current row is broken and the user is trying to navigate away (even switch to another MDI form in the application since we tried to keep everything modeless).  We had problems where the user couldn't even close the screen because of the internal workings of the grid. Perhaps I could solve these differently now having had a couple of years experience with the way the grid we use works, but our current solution seems to work OK.

What we do now if the user leaves the row and the object is invalid is to just leave the invalid object in the grid and not save the row. If the user wants to fix it, they can move back to the row to correct the error (after all, there is a red X sitting where the problem is).  If they try to close the screen, we can warn them there are unsaved changes, but if they want to ignore the error, that is their choice. In any case, we didn't write the broken object to the database.

Copyright (c) Marimer LLC