This is really a part II discussion from a previous article entitled Serialize C# Structs To Message Queues which showed the basics of serializing objects to and from message queues. In that
article, I described the ability to use message queues for a small-scale call
center type application. If you are new to message queues, I'd suggest reading
it first to familiarize yourself with some of the syntax. Today, I'll take it
one step further and show you how to create one plus demonstrate one way to serialize
an XmlDocument to and from the message queue.
Project Scenario
A client provides you with an Excel spreadsheet of customer data. By the end of
the day, they expect to have their data accessible in read/write mode to over
100 sales agents via a web browser script. Of course, they'll want the updated
customer and call result data in the same spreadsheet and sent back to them on
an hourly basis for distribution to their local sales force. Those of you in
the call center business can relate to the technical limitations/desires of most
clients.
Project Objective
Develop a web based application that can distribute records to 100 agents simultaneously
via some sort of queuing mechanism to guarantee that two agents don't get the
same record at the same time. It should be able to repopulate the calling queues
with XmlDocument data from an Excel spreadsheet based on call results. It should
be able to retrieve call results from each agent and update the Excel spreadsheet
at regular intervals. It should serialize an XmlDocument to and from the queue
providing a dynamic data structure suitable for each individual call list. The
application definition files should be simple enough for a non-technical person
to set up a few database columns and settings to make the call list ready to
call. No database programming in the agent scripts should be required.
Why Excel?
It is critical to note that I chose Excel as the data store for no other reason than
to prove it could be done. Excel is commonly thought to work best in a single
user environment and is not well suited for a large number of users. You could
make this statement about many file types. Thus, I thought getting this to work
in Excel would really hammer home the point. Assuming your IT shop has more
time as well as the proper database skill set in house, I would highly recommend
utilizing an enterprise RDBMS like SQL Server 2000, Oracle, Informix, etc. or
even SQL Server Desktop Engine. Also, this application is a mere prototype. It
is not intended to be a fully functional production ready application but it
will definitely get you off to the right start.
In the call center industry, it is also very common to get customer data sent in
Excel spreadsheets and be expected to return the updated data in the exact same
format. Normally, developers have very little programming time and resources
available to write robust applications for this type of endeavor. Thus, a quick
and dirty process like the one outlined here is often just what the doctor ordered.
How does it work?
I based the applications basic functionality on the EIS Call Manager system from SER Solutions. The system retrieves records from each call list and queues
them up for agents. When the agents log on to the call list, they retrieve records
one at a time to contact the customer and offer a product. Upon completion of
the call, the record is dispositioned to track call results and the agent moves
onto the next record. In the Utilities.cs class, you'll see a string of dispositions
that indicate the record should be recalled at a later date.
There is an administrative page in the /admin folder (Admin.cs in CallCenter.dll
and CallCenter Namespace) that displays a list of available call lists, their
record counts, and administrative tasks that can be executed against the list. Each
list has a Start, Stop, Recycle, and Process option depending on the current
call list state. Start and Stop is fairly self explanatory. Recycle, however,
is a term that describes resetting all records to be resent to the calling queue. Whenever
a record is sent to the queue, it is flagged so that future queue loads don't
pick it up. This would result in the same customer getting called multiple times
a day. Recycle simply changes the flag for those records that were called but
not finalized so they can be called again in the future. Process allows the
administrator to manually process all records in the disposition queue ahead
of schedule should it be necessary.
You'll also want to take notice of when the system processes finalized records. Almost
every admin command will trigger this process just prior to or right after the
desired command. At the very least, the admin page refreshes itself every 60
seconds so the calling queues are always populated and the most recent records
in the disposition queue have been processed.
In the /admin folder you'll find a folder named ListConfig. It contains the master
calllists.xml file that manages which call lists are in production or not. It
also contains individual calllistdef xml files (ex. calllist_1.xml). These calllistdef
xml files contain the column and data type definitions for each call list. You
can add custom attributes to each column for data validation purposes or dynamic
HTML generation based on data type (ex. state data type might automatically display
a drop down list of states with the current answer populated). Each call list
should be given a unique number and the file naming convention maintained (ex.
calllist_1.xls, calllistdef_1.xml, and so on). You'll undoubtedly want to password
protect the admin folder so normal users can't download your call list data.
For simplicity sake, I've created separate agent scripts for each of the three test
call lists in the /home folder called calllist_1.aspx, calllist_2.aspx, and calllist_3.aspx. Typically,
one script could handle multiple call lists for the same offer. For testing
purposes, just browse to the prospective page to view a record from the designated
queue, update the record, and submit it for processing.
One last note of interest. I've seen quite a few requests on various forums asking
how to serialize an XmlDocument. I managed to find an article on MSDN describing
how to create your own custom formatting class for use with message queues. You'll
find an example of this in the CustomMessageFormatter.cs class source code below.
How do I run the test code?
If you already have the Message Queue service running, then all that is necessary prior to downloading the code is to
add a reference in your machine.config file to the assemblies node if it isn't
already there. Just copy one of the existing nodes and change the name to System.Messaging. I've
included test call lists under the /admin folder as well as the solution called
SiteDebugger in the root of the website. This will allow you to launch the solution
and step through the standard C# class libraries used for both the admin page
and agent script methods. The /admin folder will need write permissions if you
haven't already given that to your anonymous IIS user account.
Of course, you'll want to launch the admin page first so that you can start at least
one of the call lists. Then, launch one or more browser instances of a specific
call list page to watch the system in action.
Adding a new call list to the mix is simple. Put the spreadsheet in the /admin/lists
folder. Make sure it has the required columns added to it: queue, fdisp, curdate,
curtime, callbackdate, callbacktime, agentid, and recordid. Create a calllistdef
xml file in the /admin/listconfig folder and adjust it's column/field name definitions. Then,
open /admin/listconfig/calllists.xml and add a node for your calllist. That's
it!
Download the complete solution and test call lists: Call Center Solution
Summary
There you have it. The basic groundwork for a browser based outbound call center
application. Feel free to take the bits and pieces you need to create your own
production ready application.
Admin.cs Source Code
using System;
using System.Data;
using System.Data.OleDb;
using System.Xml;
using System.Messaging;
using System.Runtime.Serialization;
namespace CallCenter
{
public class Admin : CallCenter.Utilities
{
public XmlDocument XmlListConfig = new XmlDocument();
public Admin()
{
// Load up the admin systems Xml config file. We'll use
// this to determine which lists are open/closed.
try
{
XmlListConfig.Load(this.ListCfgPath + @"\listconfig\calllists.xml");
}
catch (Exception e) { this.RW(e.Message + "<br>"); }
}
public void StartList(string sID)
{
// Activate a specific list and save the updated node back to calllists.xml
// We redirect back to the same page to avoid page a refresh processing
// a list start/stop/recycle when it shouldn't.
try
{
XmlNode oNode = this.XmlListConfig.SelectSingleNode("//LIST[@id='" + sID + "']");
oNode.Attributes.GetNamedItem("openforcalling").Value = "1";
}
catch (Exception e) { this.RW(e.Message + "<br>"); }
this.XmlListConfigSave();
this.SiteRedirect("default.aspx");
}
public void StopList(string sID)
{
string sFile="";
string sList="";
string sQueue="";
// Stop a list. Process any remaining records in the disposition queue.
// Even though a list is stopped, as long as it is not "hidden", future
loads
// of the admin page will finalize any records still left over from agents still
// on the call when the list was stopped.
// Also clears all unretrieved messages from the calling queue. This does
// not make records flagged as in the queue immediately callable again.
// The list must be recycled for this.
try
{
XmlNode oNode = this.XmlListConfig.SelectSingleNode("//LIST[@id='" + sID + "']");
sFile = oNode.Attributes.GetNamedItem("filename").Value;
oNode.Attributes.GetNamedItem("openforcalling").Value = "0";
sList = this.CallList + sID;
sQueue = this.QueuePath + sList;
this.ProcessFinalizedRecords(sID,sFile);
ClearQueue(sQueue);
}
catch (Exception e) { this.RW(e.Message + "<br>"); }
this.XmlListConfigSave();
this.SiteRedirect("default.aspx");
}
public void RecycleList(string sID)
{
string sFile="";
string sList="";
string sSQL ="";
string sQueue="";
// Reset all records that had been previously dialed that were dispositioned
// to be called again in the future. Processes any finalized records before
// initiating the reset.
try
{
XmlNode oNode = this.XmlListConfig.SelectSingleNode("//LIST[@id='" + sID + "']");
sFile = oNode.Attributes.GetNamedItem("filename").Value;
sFile = this.ListCfgPath + @"\lists\" + sFile;
sList = this.CallList + sID;
sQueue = this.QueuePath + sList;
this.ProcessFinalizedRecords(sID,sFile);
ClearQueue(sQueue);
sSQL = "update [" + sList + "$] set queue='0' where fdisp in (" + this.FdispCallable + ")";
this.DBExecute(sSQL,sFile);
}
catch (Exception e) { this.RW(e.Message + "<br>"); }
this.XmlListConfigSave();
this.SiteRedirect("default.aspx");
}
public void XmlListConfigSave()
{
try
{
XmlListConfig.Save(this.ListCfgPath + @"\listconfig\calllists.xml");
}
catch (Exception e) { this.RW(e.Message + "<br>"); }
}
public bool FlagRecordInQueue(string sRecordID,string sFile,string sList)
{
bool fRet=false;
string sSQL ="";
// Set the queue column in the spreadsheet so the record won't be
// pulled up again in queue load.
try
{
sSQL = "update [" + sList + "$] set queue='1',callcnt = callcnt + 1 ";
sSQL += " where recordid in (" + sRecordID + ")";
this.DBExecute(sSQL,sFile);
fRet=true;
}
catch (Exception e) { this.RW("Flag In Queue: " + e.Message + "<br>"); }
return fRet;
}
public bool SaveRecordToExcel(string sXML,string sID,string sFile,string sList)
{
string sRecordID="";
string sSQL="";
string sField="";
bool fRet=false;
XmlDocument oXML = new XmlDocument();
XmlNodeList oList;
// Typically called from ProcessFinalizedRecords(). Loads the Xml
// document deserialized from the message queue. Builds a SQL update
// string and saves the record back to Excel.
try
{
oXML.LoadXml(sXML);
sRecordID = oXML.SelectSingleNode("//DEF[@fieldname='recordid']").InnerText.ToString();
oList = oXML.SelectNodes("//DEF");
foreach(XmlNode oNode in oList)
{
sField=oNode.Attributes.GetNamedItem("fieldname").Value.ToString();
switch(sField)
{
case "recordid":
break;
case "queue":
sSQL += "queue='2',";
break;
default:
sSQL += this.CreateSqlPair(oNode);
break;
}
}
sSQL = "update [" + sList + "$] set " + sSQL.Substring(0,sSQL.Length -1);
sSQL += " where recordid in (" + sRecordID + ")";
this.DBExecute(sSQL,sFile);
fRet=true;
}
catch (Exception e) {this.RW(e.Message + "<br>"); }
return fRet;
}
public void SetRecordCounts(string sID,string sFile)
{
int lTotal=0;
int lCallable=0;
string sList="";
string sSQL="";
DataSet oDS;
// Update the admin display to reflect the various records counts for
// the call list.
try
{
sFile = this.ListCfgPath + @"\lists\" + sFile;
sList = this.CallList + sID;
oDS = this.GetDataSetFromExcel(sFile,"select * from [" + sList + "$]");
lTotal = oDS.Tables[0].Rows.Count;
sSQL = "select * from [" + sList + "$] where fdisp in (" + this.FdispCallable + ")";
oDS = this.GetDataSetFromExcel(sFile,sSQL);
lCallable = oDS.Tables[0].Rows.Count;
XmlNode oNode = this.XmlListConfig.SelectSingleNode("//LIST[@id='" + sID + "']");
oNode.Attributes.GetNamedItem("totalrecords").Value = lTotal.ToString();
oNode.Attributes.GetNamedItem("callablerecords").Value = lCallable.ToString();
this.XmlListConfigSave();
}
catch (Exception e) { this.RW(e.Message + "<br>"); }
}
public void LoadCallingQueue(string sID,string sFile)
{
string sQueue="";
string sList="";
string sCol="";
string sRecordID="";
string sSQL="";
int lRecords=0;
int lMessageCount=0;
DataSet oDS;
XmlDocument oRecord;
XmlNodeList oList;
try
{
sList = this.CallList + sID;
sQueue = this.QueuePath + sList;
sFile = this.ListCfgPath + @"\lists\" + sFile;
// Subtract the number of messages in the queue
// from our MaxRecordsInQueue variable. This
// allows to keep the desired number of callable records
// at all times.
lMessageCount = this.GetQueueMessageCount(sQueue);
if (this.MaxRecordsInQueue >= lMessageCount)
{
lRecords = this.MaxRecordsInQueue - lMessageCount;
}
else { lRecords = 0; }
if (lRecords<1) { return; }
// Query the top x records based on lowest number of call attempts.
// Notice the ORDER BY clause. It appears that either Excel or ADO.NET
// has a small bug when you use ORDER BY with Excel. If the column values are not
// unique enough, it causes the query to bring back improper results. Very
// strange. I've added the unique id "recordid" to guarantee uniqueness.
// Since "callcnt" is really the column we want to order by, this keeps
the
// problem from occurring.
sSQL = "select TOP " + lRecords.ToString() + " * from [" + sList + "$] ";
sSQL += " where queue in (0) and fdisp in (" + this.FdispCallable + ") ";
sSQL += " order by callcnt,recordid ASC ";
oDS = this.GetDataSetFromExcel(sFile,sSQL);
if (oDS==null) { return; }
// Load the call list defintion blank shell document with column settings
oRecord = new XmlDocument();
oRecord.Load(this.ListCfgPath + @"\listconfig\" + this.CallListDef + sID + ".xml");
foreach(DataRow oRow in oDS.Tables[0].Rows)
{
oList = oRecord.SelectNodes("//DEF");
// Populate the shell XmlDocument with actual data from
// the spreadsheet using the "fieldname" attribute to determine
// which column data each node should get.
foreach(XmlNode oNode in oList )
{
sCol = oNode.Attributes.GetNamedItem("fieldname").Value.ToString();
if (sCol=="recordid") { sRecordID = oRow[sCol].ToString(); }
oNode.InnerText = oRow[sCol].ToString();
}
// Flag this record in the spreadsheet so future queue loads won't
// add this record.
if (this.FlagRecordInQueue(sRecordID,sFile,sList)==true)
{
// Serialize the XmlDocument and add it to the message queue
if (this.SendRecordToQueue(sQueue,oRecord.InnerXml)==false) { return; }
}
}
}
catch (Exception e) { this.RW(e.Message + "<br>"); }
}
public void ProcessFinalizedRecords(string sID,string sFile)
{
string sQueue="";
string sList="";
string sXML="";
int lCount=0;
int lMessageCount=0;
Message oMessage;
// Go through the disposition queue for this call list
// and save the updated record back to the Excel sheet.
try
{
sList = this.CallList + sID;
sQueue = this.QueuePath + this.CallListDisp + sID;
sFile = this.ListCfgPath + @"\lists\" + sFile;
lMessageCount = GetQueueMessageCount(sQueue);
for (lCount=0;lCount<lMessageCount;lCount++)
{
oMessage = this.GetRecordFromQueue(sQueue);
if (oMessage!=null)
{
sXML = (string) oMessage.Body;
this.SaveRecordToExcel(sXML,sID,sFile,sList);
}
}
}
catch (Exception e) { this.RW(e.Message + "<br>"); }
}
public void WriteMainMenu()
{
XmlNodeList oList;
string sID="";
string sDesc="";
string sFile="";
string sOpen="";
string sHide="";
string sTotal="";
string sCallable="";
string sQueue="";
string sList="";
int lCallableQueue=0;
try
{
oList = this.XmlListConfig.SelectNodes("//LIST");
foreach(XmlNode oNode in oList )
{
sID = oNode.Attributes.GetNamedItem("id").InnerText.ToString();
sFile = oNode.Attributes.GetNamedItem("filename").InnerText.ToString();
sDesc = oNode.Attributes.GetNamedItem("desc").InnerText.ToString();
sOpen = oNode.Attributes.GetNamedItem("openforcalling").InnerText.ToString();
sHide = oNode.Attributes.GetNamedItem("hide").InnerText.ToString();
sList = this.CallList + sID;
sQueue = this.QueuePath + sList;
if (sHide!="1")
{
this.ProcessFinalizedRecords(sID,sFile);
if (sOpen=="1") { this.LoadCallingQueue(sID,sFile); }
this.SetRecordCounts(sID,sFile);
sTotal = oNode.Attributes.GetNamedItem("totalrecords").InnerText.ToString();
sCallable = oNode.Attributes.GetNamedItem("callablerecords").InnerText.ToString();
lCallableQueue=this.GetQueueMessageCount(sQueue);
this.RW("<tr>");
this.RW("<td align="left" width=200 class=BodyText8 >" + sDesc + "</td>");
if (sOpen=="1")
{
this.RW("<td align="center" nowrap><a href=default.aspx?action=stop&id=" + sID);
this.RW(" class=BodyLink8>Stop List</a></td>");
this.RW("<td align="center"> </td>");
this.RW("<td align="center"><a href=default.aspx class=BodyLink8>Process</a></td>");
}
else
{
this.RW("<td align="center" nowrap><a href=default.aspx?action=start&id=" + sID);
this.RW(" class=BodyLink8>Start List</a></td>");
this.RW("<td align="center" nowrap><a href=default.aspx?action=recycle&id=" + sID);
this.RW(" class=BodyLink8>Recycle List</a></td>");
this.RW("<td align="center"><a href=default.aspx class=BodyLink8>Process</a></td>");
}
this.RW("<td align="right" class=BodyText8 nowrap>" + lCallableQueue.ToString() + "</td>");
this.RW("<td align="right" class=BodyText8 nowrap>" + sTotal + "</td>");
this.RW("<td align="right" class=BodyText8 nowrap>" + sCallable + "</td>");
this.RW("</tr>");
}
}
}
catch (Exception e) { this.RW("<tr><td align="left" class=BodyText10>" + e.Message + "</td></tr>"); }
}
}
}
Utilities.cs Source Code
using System;
using System.Data;
using System.Data.OleDb;
using System.Xml;
using System.Messaging;
using System.Runtime.Serialization;
namespace CallCenter
{
public class Utilities
{
public string CallList = "calllist_";
public string CallListDef = "calllistdef_";
public string CallListDisp = "calllistdisp_";
public string QueuePath = ".\\PRIVATE$\\";
public string ListCfgPath=System.Web.HttpContext.Current.Server.MapPath("/admin");
public string FdispCallable = "0,30,31,32,33,34,35,36,37,38,39,40";
public string ExcelCon = @"Provider=Microsoft.Jet.OLEDB.4.0;Extended Properties=Excel 8.0;";
public int MaxRecordsInQueue=20;
public Utilities()
{
System.Web.HttpContext.Current.Response.Buffer = true;
}
public string RequestObject(string sName)
{
string sRet="";
try { sRet = System.Web.HttpContext.Current.Request[sName].ToString().Trim(); }
catch (Exception) { sRet = "";}
return sRet;
}
public void SiteRedirect(string sURL)
{
System.Web.HttpContext.Current.Response.Redirect(sURL);
}
public string HTMLName(string sFieldName)
{
return " name='" + sFieldName + "' id='" + sFieldName + "'";
}
public void RW(string sVal)
{
try
{
if (System.Web.HttpContext.Current !=null)
{
System.Web.HttpContext.Current.Response.Write(sVal +'\n');
}
}
catch(Exception) {}
}
public int GetQueueMessageCount(string sQueue)
{
int nRet=0;
MessageQueue oQueue;
Message[] oMessages;
try
{
if (!MessageQueue.Exists(sQueue)) { MessageQueue.Create(sQueue); }
oQueue = new MessageQueue(sQueue);
oMessages = oQueue.GetAllMessages();
nRet = oMessages.GetLength(0);
oQueue.Close();
}
catch (Exception) {}
return nRet;
}
public Message GetRecordFromQueue(string sQueue)
{
MessageQueue oQueue;
Message oMessage;
try
{
if (!MessageQueue.Exists(sQueue)) { MessageQueue.Create(sQueue); }
oQueue = new MessageQueue(sQueue);
// Utilize our custom message formatter to deserialize the XmlDocument stored
// in the message queue.
oQueue.Formatter = new CallCenter.CustomMessageFormatter();
oMessage = oQueue.Receive(new TimeSpan(0,0,5));
try {oQueue.Close(); } catch (Exception) { }
oQueue.Close();
return oMessage;
}
catch (Exception) { }
return null;
}
public bool SendRecordToQueue(string sQueue,object oRecord)
{
bool fRet=false;
MessageQueue oQueue;
try
{
if (!MessageQueue.Exists(sQueue)) { MessageQueue.Create(sQueue); }
oQueue = new MessageQueue(sQueue);
// Utilize our custom message formatter to serialize oRecord which is
// itself an XmlDocument.
oQueue.Formatter = new CallCenter.CustomMessageFormatter();
oQueue.Send(oRecord);
fRet = true;
oQueue.Close();
}
catch (Exception e) { this.RW("Send Queue Error:" + e.Message + "<br>"); }
return fRet;
}
public bool ClearQueue(string sQueue)
{
bool fRet=false;
MessageQueue oQueue;
try
{
if (!MessageQueue.Exists(sQueue)) { MessageQueue.Create(sQueue); }
oQueue = new MessageQueue(sQueue);
oQueue.Purge();
fRet = true;
oQueue.Close();
}
catch (Exception e) { this.RW("Clear Queue Error:" + e.Message + "<br>"); }
return fRet;
}
public bool DBExecute(string sSQL,string sFile)
{
bool fRet=false;
OleDbConnection oConn = new OleDbConnection();
try
{
oConn.ConnectionString = this.ExcelCon + "Data Source=" + sFile + ";";
oConn.Open();
OleDbCommand oCmd = new OleDbCommand(sSQL,oConn);
oCmd.ExecuteNonQuery();
oConn.Close();
fRet=true;
}
catch (Exception e) { this.RW("DBExecute: " + e.Message + "<br>"); }
finally { if (oConn.State == ConnectionState.Open) { oConn.Close(); }}
return fRet;
}
public string CreateSqlPair(XmlNode oNode)
{
string sCol="";
string sColVal="";
sCol=oNode.Attributes.GetNamedItem("fieldname").Value.ToString();
sColVal = oNode.InnerText.ToString();
if (sColVal.Length <1) { return ""; }
sColVal = sColVal.Replace("'","");
return sCol + "='" + sColVal + "',";
}
public DataSet GetDataSetFromExcel(string sFile,string sSQL)
{
DataSet oDS = new DataSet();
OleDbConnection oConn = new OleDbConnection();
try
{
oConn.ConnectionString = this.ExcelCon + "Data Source=" + sFile + ";";
oConn.Open();
OleDbDataAdapter oCmd = new OleDbDataAdapter(sSQL,oConn);
oCmd.Fill(oDS);
}
catch (Exception) { }
finally { if (oConn.State == ConnectionState.Open) { oConn.Close(); } }
return oDS;
}
}
}
CustomMessageFormatter.cs Source Code
using System;
using System.IO;
using System.Text;
using System.Messaging;
namespace CallCenter
{
public class CustomMessageFormatter : IMessageFormatter
{
public CustomMessageFormatter() { }
public object Clone() { return new CustomMessageFormatter(); }
public bool CanRead (System.Messaging.Message oMessage){ return true;}
public void Write (System.Messaging.Message oMessage, object oMessageObject)
{
try
{
byte[] buff;
buff = Encoding.UTF8.GetBytes (oMessageObject.ToString());
Stream oStream = new MemoryStream(buff);
oMessage.BodyStream = oStream;
}
catch (Exception e){System.Web.HttpContext.Current.Response.Write(e.Message + "<br>");}
}
public object Read (System.Messaging.Message oMessage)
{
try
{
Stream oStream = oMessage.BodyStream;
StreamReader oReader = new StreamReader (oStream);
return oReader.ReadToEnd();
}
catch (Exception e){System.Web.HttpContext.Current.Response.Write(e.Message + "<br>");}
return null;
}
}
}
CallList_3.aspx Source Code (Sample Agent Script)
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Xml" %>
<%@ Import Namespace="System.Messaging" %>
<%@ Import Namespace="CallCenter" %>
<script Language="C#" runat="server">
protected CallCenter.Utilities oSite = new CallCenter.Utilities();
protected XmlDocument oRecord = new XmlDocument();
protected string sCallListID="3";
protected string sRecordID="";
protected bool ShowRecord=false;
protected void Page_Load(object sender, EventArgs e) { ProcessPage(); }
public void ProcessPage()
{
string sXML="";
string sRecordQueue="";
string sRecordDispositionQueue="";
Message oMessage;
sRecordQueue=oSite.QueuePath + "calllist_" + sCallListID;
sRecordDispositionQueue=oSite.QueuePath + "calllistdisp_" + sCallListID;
try
{
if (oSite.RequestObject("action")=="post") { SaveCustomerRecord(sRecordDispositionQueue); }
oMessage = oSite.GetRecordFromQueue(sRecordQueue);
sXML = (string) oMessage.Body;
if (sXML.Length >1)
{
oRecord.LoadXml(sXML);
sRecordID = oRecord.SelectSingleNode("//DEF[@fieldname='recordid']").InnerText.ToString();
ShowRecord=true;
}
}
catch (Exception) { }
}
public void SaveCustomerRecord(string sQueue)
{
XmlDocument oXML = new XmlDocument();
oXML.Load(oSite.ListCfgPath + @"\listconfig\" + oSite.CallListDef + sCallListID + ".xml");
oXML.SelectSingleNode("//DEF[@fieldname='fdisp']").InnerText = oSite.RequestObject("FDISP");
oXML.SelectSingleNode("//DEF[@fieldname='recordid']").InnerText = oSite.RequestObject("RECORDID");
oXML.SelectSingleNode("//DEF[@fieldname='phone']").InnerText = oSite.RequestObject("PHONE");
oXML.SelectSingleNode("//DEF[@fieldname='fname']").InnerText = oSite.RequestObject("FNAME");
oXML.SelectSingleNode("//DEF[@fieldname='lname']").InnerText = oSite.RequestObject("LNAME");
oXML.SelectSingleNode("//DEF[@fieldname='addr1']").InnerText = oSite.RequestObject("ADDR1");
oXML.SelectSingleNode("//DEF[@fieldname='addr2']").InnerText = oSite.RequestObject("ADDR2");
oXML.SelectSingleNode("//DEF[@fieldname='city']").InnerText = oSite.RequestObject("CITY");
oXML.SelectSingleNode("//DEF[@fieldname='state']").InnerText = oSite.RequestObject("STATE");
oXML.SelectSingleNode("//DEF[@fieldname='zip']").InnerText = oSite.RequestObject("ZIP");
oSite.SendRecordToQueue(sQueue,oXML.InnerXml);
}
public string GetDispositionList()
{
string sH="";
sH = "<select id=FDISP name=FDISP>";
sH += "<option value='0'>Not Dialed</option>";
sH += "<option value='30' selected>Unavailable</option>";
sH += "<option value='20'>Refused</option>";
sH += "<option value='01'>Sale</option>";
sH +="</select>";
return sH;
}
public void DisplayRecord()
{
string sFieldName="";
string sFieldValue="";
string sFieldDesc="";
string sName="";
try
{
oSite.RW("<table border="0" align="left" cellspacing="2" cellpadding="2">");
if (ShowRecord==true)
{
XmlNodeList oList = oRecord.SelectNodes("//DEF[@showinscript='true']");
foreach(XmlNode oNode in oList )
{
sFieldName=oNode.Attributes.GetNamedItem("fieldname").InnerText.ToString();
sFieldDesc=oNode.Attributes.GetNamedItem("desc").InnerText.ToString();
sFieldValue=oNode.InnerText.ToString();
sName = oSite.HTMLName(sFieldName);
oSite.RW("<tr><td align="left" class=BodyText8><b>" + sFieldDesc + "</b></td>");
oSite.RW("<td align="left" class=BodyText8>");
switch(sFieldName)
{
case "fdisp":
oSite.RW(GetDispositionList() + "---" + sFieldValue);
break;
case "phone":
oSite.RW(sFieldValue);
break;
case "recordid":
oSite.RW(sFieldValue);
break;
case "callcnt":
oSite.RW(sFieldValue);
break;
case "fname":
oSite.RW("<input " + sName + " type=text size=40 maxlength=40 value='" + sFieldValue + "'>");
break;
case "lname":
oSite.RW("<input " + sName + " type=text size=40 maxlength=40 value='" + sFieldValue + "'>");
break;
case "addr1":
oSite.RW("<input " + sName + " type=text size=40 maxlength=40 value='" + sFieldValue + "'>");
break;
case "addr2":
oSite.RW("<input " + sName + " type=text size=40 maxlength=40 value='" + sFieldValue + "'>");
break;
case "city":
oSite.RW("<input " + sName + " type=text size=40 maxlength=40 value='" + sFieldValue + "'>");
break;
case "state":
oSite.RW("<input " + sName + " type=text size=2 maxlength=2 value='" + sFieldValue + "'>");
break;
case "zip":
oSite.RW("<input " + sName + " type=text size=5 maxlength=5 value='" + sFieldValue + "'>");
break;
}
oSite.RW("</td></tr>");
}
oSite.RW("<tr><td align="left"> </td><td align="left" class=BodyText8>");
oSite.RW("<input type=button name=Submit value='Submit' onclick=SubmitForm();></td></tr>");
}
else
{
oSite.RW("<tr><td align="left" colspan="2" class=BodyText8>");
oSite.RW("There are no records in call list: " + sCallListID + ". ");
oSite.RW("<a href=javascript:ReloadPage(); class=BodyLink8 target=_self>Retry record
query</a></td></tr>");
}
oSite.RW("</table>");
}
catch (Exception e) { Response.Write("Display Record Error: " + e.Message + "<br>"); }
}
</script>
<HTML><TITLE>Call Center Agent Demo</TITLE>
<HEAD>
<LINK href=/home/global.css rel=Stylesheet>
<SCRIPT language=JavaScript>
var fSubmitted=false;
function ReloadPage()
{
document.frmSubmit.Action.value='reload';
document.frmSubmit.submit();
}
function SubmitForm()
{
if (fSubmitted==false)
{
document.frmSubmit.Action.value='post';
document.frmSubmit.submit();
fSubmitted=true;
}
}
</SCRIPT>
</HEAD>
<BODY bottomMargin=0 bgColor=#ffffff leftMargin=10 topMargin=0 rightMargin=0
marginwidth=0 marginheight=0 >
<FORM method=post action=calllist_<%=sCallListID%>.aspx id=frmSubmit
name=frmSubmit>
<table valign="top" align="left" class=ColorTable border="0"
cellpadding="2" cellspacing="2" width='90%' >
<tr><td align="left" class=BodyText8><br></td></tr>
<tr><td align="left" class=ColorText10><b>Call List <%=sCallListID%></b></td></tr>
<tr><td><hr size=1 color=#CCCCCC noshade></td></tr>
<tr><td align="left" class=BodyText9>Hello, my name is _________ and
I'm calling for the Happy Gardener Magazine!</td></tr>
<tr><td align="left" class=BodyText8><br></td></tr>
<tr><td align="left" class=BodyText9>We'd like to offer you a full one
year subscription for $1. Would you like to buy a subscription?</td></tr>
<tr><td align="left" class=BodyText8><br></td></tr>
<tr><td align="left"><% DisplayRecord(); %></td></tr>
</table>
<input type=hidden name=Action id=Action value='0'>
<input type=hidden name=RECORDID id=RECORDID value='<%=sRecordID%>'>
</form></body></html>