Building Custom CSA Controls in C# for Enterprise Workflow Systems

To develop a custom CSA (Customer Service Application) control in C#, follow this structured implementation pattern that integrates with the underlying Pebble-based workflow engine.

Control Initialization and Lifecycle Management

Begin by defining a dedicated namespace for your control library:

namespace SimPerfect.Workflow.Controls

Implement two constructors—one parameterless and one acceptnig a Candy context object:

public class DealProcessControl : CSABaseControl
{
    public DealProcessControl() => Initialize();

    public DealProcessControl(Candy hostCandy) : base(hostCandy)
    {
        Initialize();
    }

    private void Initialize()
    {
        SetDraggable(true);
        SetDisplayName("Deal Process Panel");
        SetDefaultSize(315, 975);
    }
}

Override RefreshMe() to register asynchronous event handlers safely:

public override void RefreshMe()
{
    base.RefreshMe();

    if (HostCandy?.BindPebble is { } pebble)
    {
        pebble.Event_PebbleRunSQLInvokeIDWithReturn -= HandleDatabaseResponse;
        pebble.Event_PebbleRunSQLInvokeIDWithReturn += HandleDatabaseResponse;
    }
}

Override UpdateMe() to trigger data synchronization logic:

public override void UpdateMe()
{
    base.UpdateMe();
    SynchronizeDataList();
}

Define the asynchronous handler for SQL execution results:

private void HandleDatabaseResponse(string invocationKey, XmlElement responseXml)
{
    var parser = new XmlDataProcessor();
    var records = parser.ExtractRows(responseXml);

    switch (invocationKey)
    {
        case "fetch_deal_summary":
            RenderSummaryView(records);
            break;
        case "validate_workflow_state":
            UpdateWorkflowStatus(records);
            break;
    }
}

Provide a utility method to dispatch SQL commands to the DTE layer:

protected void DispatchSqlQuery(string query, string operationTag)
{
    HostCandy?.BindPebble?.Method_DTESQLCommand(query, operationTag, includeTransaction: false);
}

Optionally override OnResize for responsive layout adjustments:

protected override void OnResize(EventArgs e)
{
    base.OnResize(e);
    AdjustChildLayout();
}

Working with Pebble Data Sources

To retrieve the current work order identifier, use either of these approaches:

  • Direct binding via configured Pebble:
string orderId = HostCandy.BindPebble.GetFieldValue("Order.ID");
  • Explicti reference to a named Pebble instance:
string orderId = HostCandy.BindPebble.AllPebbles["P_Order"]?.GetFieldValue("Order.ID") ?? string.Empty;

Always validate Pebble existence before accessing fields:

if (HostCandy.BindPebble.AllPebbles.TryGetValue("P_Order", out var orderPebble))
{
    orderPebble.SetFieldValue("Order.HandleType.ID", handleTypeId);
}

Inserting Records and Retrieving Generated IDs

Issue a Pebble command to create a new record and capture its auto-generated ID:

string createCommand = @"<Pebble ID=""''"" Name=""P_DealProcess"" Command=""New""><Field Name=""DealProcess.ID"" /></Pebble>";
HostCandy.BindPebble.Method_DTEPebbleCommand(createCommand, "assign_new_process_id");

Parse the returned XML using a robust regex extractor:

case "assign_new_process_id":
    string rawId = Regex.Match(resultXMLInfo.InnerXml, @"<Object ID=""([^""]+)"" />").Groups[1].Value;
    if (string.IsNullOrWhiteSpace(rawId))
    {
        ShowErrorDialog("Failed to generate process ID. Please retry.");
        return;
    }
    
    ProcessNewInstanceId(rawId);
    break;

Accessing Agent Context Information

Retrieve logged-in agent details from the shared system state:

private readonly string AgentWorkstationId = SystemContext.Current.AgentWorkstationId;
private readonly string AgentUserId = SystemContext.Current.AgentUserId;

Extending Control Configuration in the Designer

Add configurable properties to your control’s base class:

// File attachment settings
public string FtpServerAddress { get; set; } = "";
public string FtpUsername { get; set; } = "";
public string FtpPassword { get; set; } = "";
public int MaxAttachmentSizeInMB { get; set; } = 25;

Override OnControlPropertiesAdded to map designer-provided values:

public override void OnControlPropertiesAdded(Dictionary<string, Pebble> boundPebbles)
{
    base.OnControlPropertiesAdded(boundPebbles);

    FtpServerAddress = GetPropertyValue("CAF_FTPServerIP");
    FtpUsername = GetPropertyValue("CAF_FTPUserID");
    FtpPassword = GetPropertyValue("CAF_FTPPassword");
    MaxAttachmentSizeInMB = int.TryParse(GetPropertyValue("CAF_FileLimit"), out int limit) ? limit : 25;
}

In the base class, declare design-time metadata for the property editor:

private void ConfigureDesignTimeProperties()
{
    if (!ParentCandy.IsEdit) return;

    ClearPropertyDefinitions();

    AddPropertyDefinition("CAF_FTPServerIP", "", ValueType.ShortText, "FTP Server Address", "CWorkSheetAttachedFile");
    AddPropertyDefinition("CAF_FTPUserID", "", ValueType.ShortText, "FTP Username", "CWorkSheetAttachedFile");
    AddPropertyDefinition("CAF_FTPPassword", "", ValueType.PasswordText, "FTP Password", "CWorkSheetAttachedFile");
    AddPropertyDefinition("CAF_FileLimit", "", ValueType.ShortText, "Max Attachment Size (MB)", "CWorkSheetAttachedFile");

    // Additional definitions for fax-related controls...
}

Finally, populate runtime properties during initialization using the standard property resolution loop:

public virtual void OnControlPropertiesAdded(Dictionary<string, Pebble> pebbles)
{
    foreach (var prop in PropertyDefinitions)
    {
        switch (prop.Name)
        {
            case "CF_FTPServerIP": FtpServerAddress = prop.Value; break;
            case "CF_FTPUserID": FtpUsername = prop.Value; break;
            case "CF_FTPPassword": FtpPassword = prop.Value; break;
            case "CF_IVRNum": IvrPhoneNumber = prop.Value; break;
        }
    }
}

Tags: csharp csa pebble workflow-engine enterprise-software

Posted on Sun, 31 May 2026 21:39:55 +0000 by l00ph0le