Triggers in Insert,Update and Delete operations

You can use triggers flexibly when changing data. You can set them to be invoked upon necessity during the form saving process. See this picture for save form process.

Triggers execution chain

This picture seems to be very complex, but actually it is quite easy to comprehend. Let's look at it step by step.

  • Firstly, Validate is called at all times, whether you are saving a form in the collection or removing rows from the paging grid. This trigger always executes first, and that is why you could write server validation code in it. As this trigger activates for all items whatever the operation (insert, update or delete), you might need to check this in this item. Use entity.GetState() to do that. When saving a form, in the entities list there will be one item for the form with all collections. However the entities parameter was transformed into the list, in order to unify trigger signature and expand its functionality to simultaneous saving of several similar items.
    public TriggerResult SomeTrigger(EntityModel model, List<dynamic> entities, TriggerExecutionContext context, dynamic options)
    {
    ...
    foreach (var entity in entities)
    {
        if (entity.GetState() == DynamicEntityState.Insert)
        {
            //this item will be inserted
        }
        else if (entity.GetState() == DynamicEntityState.Update)
        {
            //this item will be updated
        }
        if (entity.GetState() == DynamicEntityState.Delete)
        {
            //this item will be deleted
        }
    }
    ...
    }
  • Next the BeforeTransaction will be called. Its logic is no different from Validate, but it is always executed after Validate. Attention, this trigger will definitely be executed before the transaction which opens DWKit ORM in case of data change. But if you create an external transaction, this trigger will be executed in your transaction.
  • DWKit ORM opens transaction.
  • BeforeBatch trigger will be called at all times. Other than the execution order, it works the same way the Validate trigger does.
  • If an item (items) in the main form is inserted into the database, BeforeInsert will be called for Main Entity first, sql command INSERT executed next, and finally the AfterInsert trigger will be called. These triggers contain items selected by operation type (insert), that is why there's no need to check for entity.GetState().
  • If an item (items) in the main form is updated in the database, BeforeUpdate will be called for the Main Entity first, sql command UPDATE executed next, and finally the AfterUpdate will be called. These triggers contain items selected by operation type (update), that is why there's no need to check for entity.GetState().
  • Now, if there are collections in the saved data set, collection cycle is organized. Operations Insert, Update and Delete are executed for the collection elements.
  • If there are collection items which need to be inserted into the database, the BeforeInsert trigger will be called once for all inserted items of the collection, then sql commands INSERT will be executed, then AfterInsert will be called. Attention. You need to define these triggers for each collection in the Mapping data model interface of the Admin Panel.
  • If there are collection items which need to be updated in the database, the BeforeUpdate trigger will be called once for all inserted items of the collection, then sql commands UPDATE will be executed, then AfterUpdate will be called. Attention. You need to define these triggers for each collection in the Mapping data model interface of the Admin Panel.
  • If there are collection items which need to be deleted from the database, the BeforeDelete trigger will be called once for all inserted items of the collection, then sql commands DELETE will be executed, then AfterDelete will be called. Attention. You need to define these triggers for each collection in the Mapping data model interface of the Admin Panel.
  • After completing all operations with all collections let's get back to the main item.
  • If an item (items) of the main form is deleted from the database, BeforeDelete will be activated for Main Entity first, sql command DELETE executed next, and finally the AfterDelete trigger will be called. These triggers contain items selected by operation type (delete), that is why there's no need to check for entity.GetState().
  • AfterBatch will be called at all times. Other than the execution order, it works the same way the Validate trigger does.
  • DWKit commits the transaction
  • AfterTransaction will be called at all times. Other than the execution order, it works the same way the Validate trigger does. Attention, this trigger will definitely be executed after the transaction which opens DWKit ORM in case of data change. But if you create an external transaction, this trigger will be activated in your transaction.
  • All operations completed.

Using this information, you can organize business logic of any complexity bound to data change. Let's look at the following aspects in more detail.

  • In complex cases you can use TriggerExecutionContext to transfer data between triggers.
  • You can return the validation error result return TriggerResult.ValidationFailed(validationResult); from any trigger.
  • If you are using result return from trigger return TriggerResult.Result(resultObj);, and the object with the result is returned by several triggers in the uninterrupted chain, DWKit will merge these objects into one. The resulted object will then be applied to global state of the client application as delta.
  • You can change properties in entities, and if such change occurred before SQL command executed in the database, it will be submitted to database. Attention. Now changing the unchanged data (client performed) will not result in their submission to the database. If you need such a case, contact us at support@optimajet.com, we'll do our best to realize it ASAP.

Server validation by triggers

Server validation is such a common use case, that we'll give several examples of how it can be organized. First of all you're going to learn to work with TriggerValidationResult class.

  • Let's create its instance
    var validationResult = new TriggerValidationResult();
  • You can write validation errors in the instance. Note that whether you are writing an error for the main form or for the collection, the same method is activated. You need to submit an item which contains an error as entity parameter. Property (or attribute) name containing an error - "AttributeName". Error message - "Error message".
    validationResult.AddValidationError(entity, "AttributeName", "Error message");
  • At the end of the trigger check for validation errors. If none, validationResult.IsEmpty will return true.
    if (validationResult.IsEmpty)
    {
    ...
    }
  • If there are no errors, let's return the positive result of the current operation. Trigger chain will be continued.
    if (validationResult.IsEmpty)
    {
    return TriggerResult.Success();
    }
  • In case of errors return ValidationFailed. Trigger chain will be interrupted. If it was activated in transaction, it will be rolled back. If it was activated before transaction, it won't be even initiated. You don't have to set an additional error message, but it could be useful.
    else 
    {
    return TriggerResult.ValidationFailed(validationResult,"Some validation error message");
    }

    Now let's look at the examples.

Form validation when changing data.

This example can be used as Validate or BeforeTransaction.

public TriggerResult Validate(EntityModel model, List<dynamic> entities, TriggerExecutionContext context, dynamic options)
{
    var validationResult = new TriggerValidationResult();
    foreach (var entity in entities)
    {
        //main entity validation
        if (entity.GetState() == DynamicEntityState.Insert || entity.GetState() == DynamicEntityState.Update)
        {
            if (entity.a <= entity.b)
            {
                validationResult.AddValidationError(entity, "a", "A must be greater than B");
            }
        }
        //collection validation
        if (entity.collectionName != null)
        {
            foreach (var collectionItem in entity.collectionName)
            {
                if (collectionItem.GetState() != DynamicEntityState.Insert && collectionItem.GetState() != DynamicEntityState.Update)
                    continue;
                if (collectionItem.a > collectionItem.b)
                {
                    validationResult.AddValidationError(collectionItem, "a", "A must be less or equal B");
                }
            }
        }
    }
    if (validationResult.IsEmpty) //the form is valid
        return TriggerResult.Success();
    return TriggerResult.ValidationFailed(validationResult,"Server validation failed!"); //the form is not valid
}

Collection validation when changing an item

This example must be set as BeforeUpdate and BeforeInsert in corresponding triggers. Now we don't have to check entity.GetState(), as items have been previously filtered.

public TriggerResult CollectionValidate(EntityModel model, List<dynamic> entities, TriggerExecutionContext context, dynamic options)
{
    foreach (var collectionItem in entities)
    {
        //collection validation
        if (collectionItem.a > collectionItem.b)
        {
            validationResult.AddValidationError(collectionItem, "a", "A must be less or equal B");
        }
    }
    if (validationResult.IsEmpty) //the collection is valid
        return TriggerResult.Success();
    return TriggerResult.ValidationFailed(validationResult,"Server validation failed!"); //the collection is not valid
}

Validation when deleting rows from the paging grid.

Paging grid is always bound to the collection. Data delete validation can be performed in two ways: in Validate for Main entity or in BeforeDelete for the corresponding collection. These are codes for both cases:

  • Validate trigger

    public async Task<TriggerResult> ValidateDelete(EntityModel model, List<dynamic> entities, TriggerExecutionContext context, dynamic options)
    {
    var validationResult = new TriggerValidationResult();
    
    foreach (var entity in entities)
    {
        foreach (var collectionItem in entity.gridViewName)
        {
            if (collectionItem.GetState() == DynamicEntityState.Delete)
            {
                //collection validation
                if (collectionItem.a > collectionItem.b)
                {
                    validationResult.AddValidationError(collectionItem, "a", "A must be less or equal B");
                }
            }
        }
    }
    
    if (validationResult.IsEmpty)
        return TriggerResult.Success();
    
    return TriggerResult.ValidationFailed(validationResult, "Can't Delete records! Server validation failed.");
    }
  • BeforeDelete trigger

    public async Task<TriggerResult> ValidateDelete(EntityModel model, List<dynamic> entities, TriggerExecutionContext context, dynamic options)
    {
    var validationResult = new TriggerValidationResult();
    
    foreach (var collectionItem in entities)
    {
        //collection validation
        if (collectionItem.a > collectionItem.b)
        {
            validationResult.AddValidationError(collectionItem, "a", "A must be less or equal B");
        }
    }
    
    if (validationResult.IsEmpty)
        return TriggerResult.Success();
    
    return TriggerResult.ValidationFailed(validationResult, "Can't Delete records! Server validation failed.");
    }

Validation display in client

Validation display will depend on where it is displayed.

  • for form controls validation will be displayed in the corresponding form control.
  • for CollectionEditor - in a corresponding cell.
  • for GridView - whole row containing an error will be highlighted.