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:
| 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¶
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:
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 |