6 min readRishi

Table and Form Data Source Events in D365 F&O: A Practical X++ Reference

Event handlers are one of the safest ways to extend Dynamics 365 Finance and Operations, but they are also easy to overuse. The problem starts when table validation, form behavior, and business process customization all get dumped into random handlers with no clear boundary. This reference gives you the practical map: which event fires when, what belongs there, and when Chain of Command is the cleaner choice.

Table events are best for persistence rules that must follow the record

Table event handlers run with the table operation, not with a specific form. That makes them appropriate for rules that must apply whether the record is changed from a form, data entity, batch job, or service call. If the rule is only a form experience, keep it on the form data source instead.

The common table CRUD events are exposed through DataEventHandler attributes. You attach a static method to the table and event type, receive the sender record, and inspect or change values before or after the operation.

public final class TAGCustTableEventHandlers
{
    [DataEventHandler(tableStr(CustTable), DataEventType::Updating)]
    public static void CustTable_onUpdating(Common sender, DataEventArgs e)
    {
        CustTable custTable = sender as CustTable;

        if (custTable.Blocked == CustVendorBlocked::All && !custTable.CreditMax)
        {
            throw error("A fully blocked customer must have a reviewed credit limit.");
        }
    }

    [DataEventHandler(tableStr(CustTable), DataEventType::Inserted)]
    public static void CustTable_onInserted(Common sender, DataEventArgs e)
    {
        CustTable custTable = sender as CustTable;
        info(strFmt("Customer %1 was created.", custTable.AccountNum));
    }
}

Use pre-events for control and post-events for reaction. Inserting, Updating, and Deleting are where you block invalid persistence. Inserted, Updated, and Deleted are where you enqueue follow-up work, write audit records, or signal another process after the database operation succeeds.

Form data source events are for user experience and screen state

Form data source events fire as the user moves through records, edits fields, validates writes, or completes form-level persistence. They are excellent for enabling controls, defaulting values for that screen, and validating UI-specific behavior. They are a poor place for rules that must also apply to imports or services.

EventWhen it firesTypical use
ActivatedA record becomes current on the data sourceEnable controls based on selected record
ActiveThe active method runs for a data source recordRefresh dependent UI state
ValidatingWriteBefore the form data source writes a recordBlock invalid user edits on this form
WrittenAfter the form data source writes a recordRefresh totals or show confirmation
Modified field eventAfter a field value changes on the formRecalculate dependent fields
Leaving field eventUser leaves a field controlLight UI validation or formatting
public final class TAGSalesTableFormHandlers
{
    [FormDataSourceEventHandler(formDataSourceStr(SalesTable, SalesTable), FormDataSourceEventType::Activated)]
    public static void SalesTable_OnActivated(FormDataSource sender, FormDataSourceEventArgs e)
    {
        SalesTable salesTable = sender.cursor();
        FormRun formRun = sender.formRun();
        FormControl confirmButton = formRun.design().controlName(formControlStr(SalesTable, ConfirmSalesOrder));

        confirmButton.enabled(salesTable.SalesStatus == SalesStatus::Backorder);
    }

    [FormDataSourceEventHandler(formDataSourceStr(SalesTable, SalesTable), FormDataSourceEventType::ValidatingWrite)]
    public static void SalesTable_OnValidatingWrite(FormDataSource sender, FormDataSourceEventArgs e)
    {
        SalesTable salesTable = sender.cursor();

        if (!salesTable.CustAccount)
        {
            FormDataSourceCancelEventArgs cancelArgs = e as FormDataSourceCancelEventArgs;
            cancelArgs.cancel(true);
            warning("Customer account is required before saving the sales order.");
        }
    }
}

Keep these handlers small. If you need a service class, complex query, or cross-company calculation, call a domain method from the handler rather than burying business logic inside the form extension layer.

Field events should be narrow because they fire often

Field events are tempting because they feel close to the user action. They are also noisy. A modified field handler can run repeatedly during manual entry, personalization, copy operations, or programmatic value changes triggered by the form.

Use field events for immediate feedback:

  • Dependent defaults: update delivery mode when the customer changes.
  • Control state: enable a dimension tab when a category is selected.
  • Cheap validation: warn about a missing reference before the save attempt.

Avoid field events for persistence guarantees. A data entity import will not care that your form field handler once showed a warning. If the rule matters to the table, put it on the table.

public final class TAGSalesLineFieldHandlers
{
    [FormDataFieldEventHandler(formDataFieldStr(SalesTable, SalesLine, ItemId), FormDataFieldEventType::Modified)]
    public static void ItemId_OnModified(FormDataObject sender, FormDataFieldEventArgs e)
    {
        FormDataSource salesLineDs = sender.datasource();
        SalesLine salesLine = salesLineDs.cursor();

        if (salesLine.ItemId)
        {
            salesLine.Name = InventTable::find(salesLine.ItemId).itemName();
            salesLineDs.refresh();
        }
    }
}

Chain of Command wins when you must preserve method intent

Events are not a universal replacement for Chain of Command. If the extension depends on the exact semantics of an existing method, use CoC. That includes wrapping validateWrite, modifiedField, initValue, canSubmitToWorkflow, and service methods where the base method return value is part of the contract.

NeedPreferWhy
Add validation to all writesTable CoC or table eventKeeps rule close to persistence
Toggle controls on one formForm data source eventUI-specific and isolated
Extend a protected method flowChain of CommandYou can call next and preserve intent
React after insertInserted eventNo need to wrap base insert
Change return valueChain of CommandEvents often cannot safely replace result logic
[ExtensionOf(tableStr(SalesTable))]
final class TAGSalesTable_Extension
{
    public boolean validateWrite()
    {
        boolean ret = next validateWrite();

        if (ret && this.SalesType == SalesType::Journal)
        {
            ret = checkFailed("Journal sales orders are not allowed in this process.");
        }

        return ret;
    }
}

Ordering caveats are real, so design handlers to be independent

Multiple handlers can subscribe to the same event. You should not build a solution that depends on handler A running before handler B unless the platform gives you an explicit ordering contract for that extension point. Most event subscription code should be idempotent, local, and tolerant of other extensions.

That means avoiding hidden coupling:

  • Do not rely on another handler setting a field first.
  • Do not throw generic errors that mask another validation failure.
  • Do not update unrelated records from a UI event without a transaction strategy.
  • Do not use post-events to repair data that should have been validated before write.

The senior-engineer test is simple: if the handler would still make sense when another ISV adds its own handler tomorrow, it is probably placed correctly. Use table events for record lifecycle rules, form events for screen behavior, field events for immediate feedback, and CoC when the method contract matters. That split keeps extensions upgrade-safe without turning the event model into a guessing game.

Keep reading

Newsletter

New posts, straight to your inbox

One email per post. No spam, no tracking pixels, unsubscribe anytime.

Comments

No comments yet. Be the first.