Skip to content

Response Attributes

ResponseAttributes allow business rules to present custom dialog boxes to the user — with configurable titles, message text, buttons, and input fields — and then process the user's response in a separate callback rule. This is the mechanism for building interactive on-demand tools inside P21 without modifying P21 source code.


What Are Response Attributes?

When a rule sets Data.ResponseAttributes before returning RuleResult.Success, P21 intercepts the result and displays the configured dialog to the user instead of continuing normally. After the user responds (clicks a button, fills in fields), P21 fires a second, callback rule with the user's input available in Data.Set.Tables["response_data"].

This two-rule pattern cleanly separates dialog display from response handling.

What a Dialog Can Contain

  • Custom title and message text
  • One or more buttons with configurable labels
  • Zero or more input fields of various types (text, decimal, integer, date, dropdown)
  • A reference to the callback rule that will handle the response

The Two-Rule Pattern

Rule Role Trigger
Display rule Builds and assigns ResponseAttributes; returns RuleResult.Success Normal P21 trigger (Validator, On Demand, etc.)
Handling rule Reads response_data for button clicked and field values; executes business logic Called by P21 automatically after user responds

The two rules are linked by the CallbackRuleName property on ResponseAttributes. Both rules must be registered in P21 administration, but only the display rule needs a trigger assignment — P21 calls the handling rule directly.


Example 1: Yes / No Confirmation

This is the simplest dialog pattern — ask the user to confirm an action before proceeding.

Display Rule

using P21.Extensions.BusinessRule;
using P21.Extensions.BusinessRule.ResponseAttributes;

namespace YourCompany.P21.Rules;

/// <summary>
/// Display rule: asks whether to apply future cost to all location/supplier combos.
/// Configure as: On Demand button on Inventory Pricing window.
/// </summary>
public class ConfirmFutureCostDisplay : Rule
{
    public override string GetName() => "ConfirmFutureCostDisplay";

    public override string GetDescription() =>
        "Shows a Yes/No dialog before applying future cost changes.";

    public override RuleResult Execute()
    {
        var futureCost = Data.Fields["future_cost"]?.Value<decimal>() ?? 0m;

        var response = new ResponseAttributes
        {
            Title            = "Apply Future Cost",
            Text             = $"Apply future cost of {futureCost:C} to all " +
                               "location and supplier combinations?",
            CallbackRuleName = "ConfirmFutureCostHandling"
        };

        response.Buttons.Add(new ResponseButton("Yes", "Yes"));
        response.Buttons.Add(new ResponseButton("No",  "No"));

        Data.ResponseAttributes = response;
        return RuleResult.Success;
    }
}

Handling Rule

using System.Data;
using P21.Extensions.BusinessRule;

namespace YourCompany.P21.Rules;

/// <summary>
/// Callback rule: processes the user's Yes/No response.
/// Registered in P21 administration but needs no trigger assignment —
/// P21 calls it automatically as the callback for ConfirmFutureCostDisplay.
/// </summary>
public class ConfirmFutureCostHandling : Rule
{
    public override string GetName() => "ConfirmFutureCostHandling";

    public override string GetDescription() =>
        "Applies future cost change if user confirmed Yes.";

    public override RuleResult Execute()
    {
        // Always guard with IsCallbackRule — prevents accidental direct execution
        if (!RuleState.IsCallbackRule)
            return RuleResult.Success;

        // The "button_name" column contains the Value of the button the user clicked
        var responseData   = Data.Set.Tables["response_data"];
        var selectedButton = responseData?.Rows[0]?.Field<string>("button_name");

        if (selectedButton != "Yes")
        {
            // User clicked No — do nothing
            Data.SetFocus("future_cost");
            return RuleResult.Success;
        }

        // User confirmed — apply future cost to all item-supplier rows
        var futureCost = Data.Fields["future_cost"]?.Value<decimal>() ?? 0m;

        Data.SetCascade(false);
        try
        {
            foreach (DataRow row in Data.Set.Tables["item_supplier"].Rows)
            {
                if (row.RowState == DataRowState.Deleted) continue;
                row.SetField("future_cost", futureCost);
            }
        }
        finally
        {
            Data.SetCascade(true);
        }

        Log.AddAndPersist(
            $"[ConfirmFutureCostHandling] Applied future cost {futureCost:C} " +
            "to all item_supplier rows.");

        Data.SetFocus("effective_date");
        return RuleResult.Success;
    }
}

Example 2: Dialog with Input Fields

This example shows a dialog that collects a product group and a discount percentage from the user before applying a line discount.

Display Rule

using P21.Extensions.BusinessRule;
using P21.Extensions.BusinessRule.ResponseAttributes;

namespace YourCompany.P21.Rules;

/// <summary>
/// Display rule: asks for a product group and discount % to apply.
/// Configure as: On Demand button on Order Entry.
/// </summary>
public class ApplyGroupDiscountDisplay : Rule
{
    public override string GetName() => "ApplyGroupDiscountDisplay";

    public override string GetDescription() =>
        "Prompts for a product group and discount percentage to apply to matching lines.";

    public override RuleResult Execute()
    {
        var response = new ResponseAttributes
        {
            Title            = "Apply Group Discount",
            Text             = "Enter the product group and discount percentage to apply " +
                               "to matching order lines.",
            CallbackRuleName = "ApplyGroupDiscountHandling"
        };

        // Dropdown: product group selection
        var groupField = new ResponseField
        {
            Name        = "product_group_id",
            Label       = "Product Group",
            FieldType   = ResponseFieldType.Dropdown,
            IsRequired  = true
        };

        // Populate dropdown from the product_group table
        using var conn = new System.Data.SqlClient.SqlConnection(Data.ConnectionString);
        conn.Open();
        var cmd = new System.Data.SqlClient.SqlCommand(
            "SELECT product_group_id, description FROM product_group ORDER BY product_group_id",
            conn);
        using var reader = cmd.ExecuteReader();
        while (reader.Read())
        {
            groupField.ValidValues.Add(new ResponseFieldValue(
                reader.GetString(0),
                $"{reader.GetString(0)} — {reader.GetString(1)}"));
        }

        response.Fields.Add(groupField);

        // Decimal: discount percentage
        response.Fields.Add(new ResponseField
        {
            Name       = "discount_pct",
            Label      = "Discount % (e.g., 10 for 10%)",
            FieldType  = ResponseFieldType.Decimal,
            IsRequired = true
        });

        response.Buttons.Add(new ResponseButton("Apply",  "Apply"));
        response.Buttons.Add(new ResponseButton("Cancel", "Cancel"));

        Data.ResponseAttributes = response;
        return RuleResult.Success;
    }
}

Handling Rule

using System.Data;
using System.Linq;
using P21.Extensions.BusinessRule;

namespace YourCompany.P21.Rules;

/// <summary>
/// Callback rule: applies the discount to order lines in the selected product group.
/// </summary>
public class ApplyGroupDiscountHandling : Rule
{
    public override string GetName() => "ApplyGroupDiscountHandling";

    public override string GetDescription() =>
        "Applies a percentage discount to order lines matching the selected product group.";

    public override RuleResult Execute()
    {
        if (!RuleState.IsCallbackRule)
            return RuleResult.Success;

        var responseData   = Data.Set.Tables["response_data"];
        var selectedButton = responseData?.Rows[0]?.Field<string>("button_name");

        if (selectedButton != "Apply")
            return RuleResult.Success;

        // Read the field values the user entered
        // In response_data, each ResponseField has its own row identified by field_name
        string productGroupId = GetResponseFieldValue(responseData, "product_group_id");
        string discountPctStr = GetResponseFieldValue(responseData, "discount_pct");

        if (!decimal.TryParse(discountPctStr, out decimal discountPct) || discountPct <= 0)
            return RuleResult.Error("Discount percentage must be a positive number.");

        decimal multiplier = 1m - (discountPct / 100m);

        // Apply discount to lines in the selected product group
        var lines = Data.Set.Tables["oe_line"];
        if (lines == null) return RuleResult.Success;

        Data.SetCascade(false);
        try
        {
            foreach (DataRow row in lines.Rows)
            {
                if (row.RowState == DataRowState.Deleted) continue;
                if (row.Field<string>("prod_group_id") != productGroupId) continue;

                decimal originalPrice = row.Field<decimal>("unit_price");
                decimal newPrice      = Math.Round(originalPrice * multiplier, 2);
                row.SetField("unit_price",     newPrice);
                row.SetField("extended_price", newPrice * row.Field<decimal>("unit_quantity"));
            }
        }
        finally
        {
            Data.SetCascade(true);
        }

        Log.AddAndPersist(
            $"[ApplyGroupDiscountHandling] Applied {discountPct}% discount to " +
            $"product group {productGroupId}.");

        return RuleResult.Success;
    }

    private static string GetResponseFieldValue(DataTable responseData, string fieldName)
    {
        var row = responseData?.AsEnumerable()
            .FirstOrDefault(r => r.Field<string>("field_name") == fieldName);
        return row?.Field<string>("field_value") ?? string.Empty;
    }
}

ResponseField Types

ResponseFieldType Description Notes
Alphanumeric Free-text string input Max length configurable via MaxLength
Decimal Numeric decimal input Returns string in response_data; parse with decimal.TryParse
Integer Integer numeric input Returns string; parse with int.TryParse
Date Date picker Returns string in MM/dd/yyyy format
Dropdown Select from a predefined list Populate ValidValues with ResponseFieldValue items

ResponseButton

ResponseButton takes two arguments:

new ResponseButton(displayLabel, buttonValue)
Argument Description
displayLabel Text shown on the button in the dialog
buttonValue The value written to button_name in response_data when clicked. Use this in your handling rule to determine which button was pressed.

Keep buttonValue short and code-friendly — it is the string you compare in your handling rule.


Reading Response Data

In the handling rule, user input is in Data.Set.Tables["response_data"]:

var responseData = Data.Set.Tables["response_data"];

// Which button did the user click?
string buttonClicked = responseData.Rows[0].Field<string>("button_name");

// What did the user enter in a field?
// Option A: if response_data has one row per field (layout depends on P21 version)
string fieldValue = responseData.AsEnumerable()
    .FirstOrDefault(r => r.Field<string>("field_name") == "my_field")
    ?.Field<string>("field_value") ?? string.Empty;

// Option B: check by column name if all fields are columns in row 0
string fieldValue = responseData.Rows[0].Field<string>("my_field");

response_data Layout

The exact layout of response_data may vary slightly between P21 versions. Test your handling rule against your specific P21 version to confirm whether field values appear as columns in row 0 or as separate rows with a field_name / field_value structure.


Important Notes

Always Check IsCallbackRule

if (!RuleState.IsCallbackRule)
    return RuleResult.Success;

This guard prevents the handling rule from executing if it is somehow triggered directly (e.g., if an administrator accidentally assigns it as a trigger rule). Without this check, the rule may attempt to read response_data when it doesn't exist and throw a null reference exception.

Use SetCascade When Modifying Multiple Fields

When your handling rule updates several fields or rows based on the user's input, wrap the updates in Data.SetCascade(false) / true to prevent each individual update from triggering further rule cascades:

Data.SetCascade(false);
try
{
    // batch updates here
}
finally
{
    Data.SetCascade(true);
}

Both Rules Must Be Registered

Both the display rule and the handling rule must be registered in P21 under Setup → Business Rules → Rule Registration. Only the display rule needs a trigger assignment. The handling rule is invoked automatically by P21 as the callback — P21 looks it up by the CallbackRuleName string you set on the ResponseAttributes object.

Naming Convention

A consistent naming convention avoids confusion in the P21 rule list:

Role Convention Example
Display rule {Feature}Display ConfirmFutureCostDisplay
Handling rule {Feature}Handling ConfirmFutureCostHandling