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;
}
}
}