The rest of the article will introduce how to implement a
call assistant (or a call center client) like the one described above in C#.
The SIP-based application that will be presented below is able to make and
receive audio phone calls through VoIP. It has some additional features such as
hold and transfer and it is capable to check for the caller party in a
database, and if it detects the caller in the database, the caller information (username,
full name, country, note) will be displayed.
To be able to implement this call assistant application,
first of all, make your system ready for the project.
If you miss any of the software listed above, you can obtain
them from the following places for free:
II.2. Implementation of the Windows Forms application
For the implementation of this call assistant software, you
will need the following 5 classes: Program.cs
; Form1.cs
; UserInfo.cs
;
DatabaseManager.cs
; Form1.Designer.cs
. Let’s see the classes one-by-one.
Program.cs
The Program.cs
class is the main entry point for the application. It is used to run the project (Code 1).
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new FormCallAssistant());
}
}
Code 1: Program.cs
Form1.cs
The Form1.cs
class contains the implementation of the softphone that is the base of the call assistant application. The Form1.cs
present how to build a softphone and how to register it to the PBX by using a SIP account. This class also contains methods to be able to manage media handlers, to make and receive calls, and some simple call control methods (transfer, hold, hang up) are also described here.
As a first step, add the necessary using lines:
using System;
using System.Windows.Forms;
using Ozeki.Media.MediaHandlers;
using Ozeki.VoIP;
using Ozeki.VoIP.SDK;
Code 2: The necessary using lines
Thereafter, you need to create some objects by using the corresponding interfaces as follows. The softphone
and phoneline
objects are needed to declare, define and initialize the softphone. The microphone will be connected to the mediaSender
, and the speaker will be connected to the mediaReceiver
by using the connector object. To be able to send media data (such as voice), the mediaSender
and the mediaReceiver
media handlers should be attached to the call object.
ISoftPhone _softPhone;
IPhoneLine _phoneLine;
RegState _phoneLineState;
IPhoneCall _call;
Microphone _microphone;
Speaker _speaker;
MediaConnector _connector;
PhoneCallAudioSender _mediaSender;
PhoneCallAudioReceiver _mediaReceiver;
DatabaseManager _databaseManager;
UserInfo _otherParty;
Code 3: The necessary objects
The following variable is needed to indicate if there is an incoming call:
bool _incomingCall;
Code 4: Variable to indicate the incoming calls
Now there is a need to initialize the previous new objects and variables in a constructor:
public FormCallAssistant()
{
InitializeComponent();
}
void form_CallAssistant_Load(object sender, EventArgs e)
{
_microphone = Microphone.GetDefaultDevice();
_speaker = Speaker.GetDefaultDevice();
_connector = new MediaConnector();
_mediaSender = new PhoneCallAudioSender();
_mediaReceiver = new PhoneCallAudioReceiver();
_databaseManager = new DatabaseManager();
InitializeSoftphone();
}
Code 5: Initialize the new objects
Now take a closer look at the InitializeSoftphone
method that is used to initialize the softphone with default parameters. (The 5700 and 5750 parameters determine the port interval.) Here you need to create a new SIP account to be able to register the application to your PBX. For this purpose you need to set the followings:
- registration required (true)
- display name (1000)
- user name (1000)
- authentication ID (1000)
- register password (1000)
- domain host (192.168.115.103 - IP address of the PBX)
- domain port (5060 - port number of the PBX)
There is also a need to create the phone line to communicate with the PBX. The RegisterPhoneLine
can be used to register the phone line:
void InitializeSoftphone()
{
try
{
_softPhone = SoftPhoneFactory.CreateSoftPhone(SoftPhoneFactory.GetLocalIP(), 5700, 5750);
SIPAccount sa = new SIPAccount(true, "1000", "1000", "1000", "1000", "192.168.115.103", 5060);
_phoneLine = _softPhone.CreatePhoneLine(sa);
_phoneLine.RegistrationStateChanged += _phoneLine_RegistrationStateChanged;
_softPhone.IncomingCall += _softPhone_IncomingCall;
_softPhone.RegisterPhoneLine(_phoneLine);
_incomingCall = false;
ConnectMedia();
}
catch (Exception ex)
{
InvokeGUIThread(() => { tb_Display.Text = ex.Message; });
}
}
Code 6: Softphone initialization
The following code is used when the registration state of the phone line changes:
void _phoneLine_RegistrationStateChanged(object sender, RegistrationStateChangedArgs e)
{
_phoneLineState = e.State;
if (_phoneLineState == RegState.RegistrationSucceeded)
{
InvokeGUIThread(() => { lbl_UserName.Text = _phoneLine.SIPAccount.UserName; });
InvokeGUIThread(() => { lbl_DomainHost.Text = _phoneLine.SIPAccount.DomainServerHost; });
}
}
Code 7: Code to handle the changes of the registration state
This is used when there is an incoming call:
void _softPhone_IncomingCall(object sender, VoIPEventArgs e)
{
var userName = e.Item.DialInfo.Dialed;
InvokeGUIThread(() => { tb_Display.Text = "Ringing (" + userName + ")"; });
_call = e.Item;
WireUpCallEvents();
_incomingCall = true;
_otherParty = _databaseManager.GetOtherPartyInfos(userName);
ShowUserInfos(_otherParty);
}
Code 8: Code to manage an incoming call
The following snippet shows how to manage the calls in different states:
void call_CallStateChanged(object sender, CallStateChangedArgs e)
{
InvokeGUIThread(() => { lbl_CallState.Text = e.State.ToString(); });
if (e.State == CallState.Answered)
{
StartDevices();
_mediaSender.AttachToCall(_call);
_mediaReceiver.AttachToCall(_call);
InvokeGUIThread(() => { tb_Display.Text = "In call with: " + ((IPhoneCall)sender).DialInfo.Dialed; });
}
else if (e.State == CallState.InCall)
{
StartDevices();
}
if (e.State == CallState.LocalHeld || e.State == CallState.InactiveHeld)
{
StopDevices();
InvokeGUIThread(() => { btn_Hold.Text = "Unhold"; });
}
else
{
InvokeGUIThread(() => { btn_Hold.Text = "Hold"; });
}
if (e.State.IsCallEnded())
{
StopDevices();
_mediaSender.Detach();
_mediaReceiver.Detach();
WireDownCallEvents();
_call = null;
InvokeGUIThread(() => { tb_Display.Text = String.Empty; });
ClearUserInfos();
}
}
Code 9: Code to manage the calls in different states
Below you can see how to display the user data of the other party during a call:
void ShowUserInfos(UserInfo otherParty)
{
InvokeGUIThread(() =>
{
tb_OtherPartyUserName.Text = otherParty.UserName;
tb_OtherPartyRealName.Text = otherParty.RealName;
tb_OtherPartyCountry.Text = otherParty.Country;
tb_OtherPartyNote.Text = otherParty.Note;
});
}
void ClearUserInfos()
{
InvokeGUIThread(() =>
{
tb_OtherPartyUserName.Text = String.Empty;
tb_OtherPartyRealName.Text = String.Empty;
tb_OtherPartyCountry.Text = String.Empty;
tb_OtherPartyNote.Text = String.Empty;
});
}
Code 10: Displaying the user data of the other party during a call
This code example shows what happens when the user presses the buttons on the keypad of the call assistant:
void buttonKeyPadButton_Click(object sender, EventArgs e)
{
var btn = sender as Button;
if (_call != null)
return;
if (btn == null)
return;
tb_Display.Text += btn.Text.Trim();
}
Code 11: Code for pressing the buttons on the keypad
The following code snippet presents how to manage when the user of the call assistant picks up the phone if there is an incoming call:
void btn_PickUp_Click(object sender, EventArgs e)
{
if (_incomingCall)
{
_incomingCall = false;
_call.Answer();
return;
}
if (_call != null)
return;
if (_phoneLineState != RegState.RegistrationSucceeded)
{
InvokeGUIThread(() => { tb_Display.Text = "OFFLINE! Please register."; });
return;
}
if (!String.IsNullOrEmpty(tb_Display.Text))
{
var userName = tb_Display.Text;
_call = _softPhone.CreateCallObject(_phoneLine, userName);
WireUpCallEvents();
_call.Start();
_otherParty = _databaseManager.GetOtherPartyInfos(userName);
ShowUserInfos(_otherParty);
}
}
Code 12: Accepting the call
The following code snippet presents the media handler methods that can be used to make the usage of the microphone and the spreakers possible: StartDevice
, StopDevice
, ConnectMedia
.
void StartDevices()
{
if (_microphone != null)
_microphone.Start();
if (_speaker != null)
_speaker.Start();
}
void StopDevices()
{
if (_microphone != null)
_microphone.Stop();
if (_speaker != null)
_speaker.Stop();
}
void ConnectMedia()
{
if (_microphone != null)
_connector.Connect(_microphone, _mediaSender);
if (_speaker != null)
_connector.Connect(_mediaReceiver, _speaker);
}
Code 13: The media handler methods
To make a successful call, you need to subscribe to specific events by using the WireUpCallEvents
and the WireDownCallEvents
methods.
void WireUpCallEvents()
{
_call.CallStateChanged += (call_CallStateChanged);
}
void WireDownCallEvents()
{
_call.CallStateChanged -= (call_CallStateChanged);
}
Code 14: Subscribing to specific events by using WireUpCallEvents and WireDownCallEvents
To handle the GUI, you need to create a background thread for this purpose. This is a simple method that invokes the action, given as parameter:
void InvokeGUIThread(Action action)
{
Invoke(action);
}
Code 15: Background thread to handle the GUI
Below you can see the background processes when the user hangs up the call. If there is an incoming call that has not been accepted yet, and the user presses the Hang up button, the call will be rejected. If the user presses this button during a call, the call will be finished.
void btn_HangUp_Click(object sender, EventArgs e)
{
if (_call != null)
{
if (_incomingCall && _call.CallState == CallState.Ringing)
{
_call.Reject();
}
else
{
_call.HangUp();
}
_incomingCall = false;
_call = null;
}
tb_Display.Text = string.Empty;
}
Code 16: Hanging up the call
The following code snippet illustrates how to transfer the call to an other destination (to an other client) by using the BlindTransfer
method. Using this method, during a call, the phone of the third party (this is the destination) starts to ring, like it would be dialled firstly. When the third party answers the call, the original call assistant user steps out from the conversation.
void btn_Transfer_Click(object sender, EventArgs e)
{
string transferTo = "1001";
if (_call == null)
return;
if (string.IsNullOrEmpty(transferTo))
return;
if (_call.CallState != CallState.InCall)
return;
_call.BlindTransfer(transferTo);
InvokeGUIThread(() => { tb_Display.Text = "Transfering to:" + transferTo; });
}
Code 17: Transferring the call to an other destination
And finally, the code snippet below can be used to implement the hold functionality by using the ToggleHold
method. First, the method checks whether there is an active phone call or not (is the call object "null" or not), and if there is an active call, it puts the call on hold.
void btn_Hold_Click(object sender, EventArgs e)
{
if (_call != null)
_call.ToggleHold();
}
Code 18: The hold feature
Userinfo.cs
The UserInfo.cs
class is used to manage the UserInfo
object that handles the user data.
class UserInfo
{
public UserInfo(string userName, string realName, string country, string note)
{
UserName = userName;
RealName = realName;
Country = country;
Note = note;
}
public string UserName { get; private set; }
public string RealName { get; private set; }
public string Country { get; private set; }
public string Note { get; private set; }
}
Code 19: Transferring the call to an other destination
DatabaseManager.cs
The DatabaseManager.cs
class is used to access the local database. For this purpose, there is a need for a connectionString
(the name that can be found in the name property of the App.config
files). The constructor of the DatabaseManager
class creates this connectionString
variable and the SqlConnection
object as well. Thereafter, it calls the OpenConnection
method to connect to the local database.
string _connectionString;
SqlConnection _connection;
public DatabaseManager()
{
_connectionString = ConfigurationManager.ConnectionStrings["MyConnectionString"].ToString();
_connection = new SqlConnection(_connectionString);
OpenConnection();
TestAdder();
}
void OpenConnection()
{
try
{
_connection.Open();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Code 20: ConnectionString to access the local database
It also calls the TestAdder
method. It is used for test purposes. By using this method, you can create some users will be added to the database with the help of a simple SQL INSERT
command.
void TestAdder()
{
UserInfo userInfo;
userInfo = new UserInfo("1001", "First User", "Hungary", "Only a user with no creative note.");
AddUserInfo(userInfo);
userInfo = new UserInfo("1002", "Second User", "England", "User from far-far away, still with no creative note.");
AddUserInfo(userInfo);
userInfo = new UserInfo("1003", "Third User", "Chile", "User who can bring us cherries.");
AddUserInfo(userInfo);
}
Code 21: The TestAdder method
The relevant AddUserInfo
method can be seen below:
public void AddUserInfo(UserInfo userInfo)
{
using (var command = _connection.CreateCommand())
{
command.CommandText = "INSERT INTO UserInfos (UserName, RealName, Country, Note) values ('" + userInfo.UserName + "', '" + userInfo.RealName + "', '" + userInfo.Country + "', '" + userInfo.Note + "')";
command.ExecuteNonQuery();
}
}
Code 22: The AddUserInfo method
Below the GetOtherPartyInfos
method can be seen that is responsible for reading the user data in the local database. This happens by using a simple SQL SELECT
query. (The the username is equal to the caller’s username. If the database does not contain the caller username, the application displays N/A.)
public UserInfo GetOtherPartyInfos(string userName)
{
UserInfo userInfo;
using (var command = _connection.CreateCommand())
{
command.CommandText = "SELECT * FROM UserInfos WHERE Username = '" + userName + "'";
using (var reader = command.ExecuteReader())
{
if (reader.HasRows)
{
reader.Read();
var realName = reader["RealName"].ToString();
var country = reader["Country"].ToString();
var note = reader["Note"].ToString();
userInfo = new UserInfo(userName, realName, country, note);
return userInfo;
}
}
}
userInfo = new UserInfo("userName", "N/A", "N/A", "N/A");
return userInfo;
}
Code 23: The GetOtherPartyInfos method
Form1.Designer.cs
The Form1.Designer.cs
class contains the code belonging to the Graphical User Interface (GUI) of the call assistant application. Let’s take a look at some important code snippets that is used to build the following appearance:

Figure 5: The GUI of the call assistant application (Source: Self-made)
The Form1.Designer.cs class starts with the following required designer variable. The diposing parameter of the Dispose method is true if managed resources should be disposed; otherwise, false.
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
Code 24: The the beginning of the Form1.Designer.cs
This is followed by the Windows Form Designer generated code. It would be several pages long to present the full code, so below only some important code snippets have been highlighted.
The InitializeComponent
method is required method for Designer support. At the beginning of this method the following component can be seen:
this.tb_Display = new System.Windows.Forms.TextBox();
this.panel1 = new System.Windows.Forms.Panel();
this.btn_Transfer = new System.Windows.Forms.Button();
this.btn_Hold = new System.Windows.Forms.Button();
this.groupBox2 = new System.Windows.Forms.GroupBox();
this.lbl_CallState = new System.Windows.Forms.Label();
this.lbl_DomainHost = new System.Windows.Forms.Label();
this.label6 = new System.Windows.Forms.Label();
this.button14 = new System.Windows.Forms.Button();
this.button13 = new System.Windows.Forms.Button();
this.button12 = new System.Windows.Forms.Button();
this.button11 = new System.Windows.Forms.Button();
this.button10 = new System.Windows.Forms.Button();
this.button9 = new System.Windows.Forms.Button();
this.button8 = new System.Windows.Forms.Button();
this.button7 = new System.Windows.Forms.Button();
this.button6 = new System.Windows.Forms.Button();
this.button5 = new System.Windows.Forms.Button();
this.button4 = new System.Windows.Forms.Button();
this.button3 = new System.Windows.Forms.Button();
this.btn_HangUp = new System.Windows.Forms.Button();
this.btn_PickUp = new System.Windows.Forms.Button();
this.lbl_UserName = new System.Windows.Forms.Label();
this.label1 = new System.Windows.Forms.Label();
this.groupBox1 = new System.Windows.Forms.GroupBox();
this.tb_OtherPartyNote = new System.Windows.Forms.TextBox();
this.tb_OtherPartyCountry = new System.Windows.Forms.TextBox();
this.tb_OtherPartyRealName = new System.Windows.Forms.TextBox();
this.tb_OtherPartyUserName = new System.Windows.Forms.TextBox();
this.label5 = new System.Windows.Forms.Label();
this.label4 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.panel1.SuspendLayout();
this.groupBox2.SuspendLayout();
this.groupBox1.SuspendLayout();
this.SuspendLayout();
Code 25: The beginning of the InitializeComponent method
The following code belongs to the Pick Up button. The Hold, Transfer and Hang Up buttons can be implemented in the same way.
this.btn_PickUp.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(192)))), ((int)(((byte)(255)))), ((int)(((byte)(192)))));
this.btn_PickUp.Location = new System.Drawing.Point(24, 115);
this.btn_PickUp.Name = "btn_PickUp";
this.btn_PickUp.Size = new System.Drawing.Size(64, 42);
this.btn_PickUp.TabIndex = 3;
this.btn_PickUp.Text = "Pick Up";
this.btn_PickUp.UseVisualStyleBackColor = false;
this.btn_PickUp.Click += new System.EventHandler(this.btn_PickUp_Click);
Code 26: The code belonging to the Pick Up button
The following code belongs to the numbered 1 button. All the rest buttons of the keypad (including # and *) can be implemented in the same way.
this.button3.Location = new System.Drawing.Point(76, 192);
this.button3.Name = "button3";
this.button3.Size = new System.Drawing.Size(64, 34);
this.button3.TabIndex = 5;
this.button3.Text = "1";
this.button3.UseVisualStyleBackColor = true;
this.button3.Click += new System.EventHandler(this.buttonKeyPadButton_Click);
Code 27: The code belonging to the button 1
The following code is a short snippet of the code that is needed to implement the „other party” section.
this.groupBox1.Controls.Add(this.tb_OtherPartyNote);
this.groupBox1.Controls.Add(this.tb_OtherPartyCountry);
this.groupBox1.Controls.Add(this.tb_OtherPartyRealName);
this.groupBox1.Controls.Add(this.tb_OtherPartyUserName);
this.groupBox1.Controls.Add(this.label5);
this.groupBox1.Controls.Add(this.label4);
this.groupBox1.Controls.Add(this.label3);
this.groupBox1.Controls.Add(this.label2);
this.groupBox1.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.groupBox1.Location = new System.Drawing.Point(12, 25);
this.groupBox1.Name = "groupBox1";
this.groupBox1.Size = new System.Drawing.Size(393, 412);
this.groupBox1.TabIndex = 2;
this.groupBox1.TabStop = false;
this.groupBox1.Text = "Information about the other party";
//
// tb_OtherPartyNote
//
this.tb_OtherPartyNote.Enabled = false;
this.tb_OtherPartyNote.Location = new System.Drawing.Point(88, 224);
this.tb_OtherPartyNote.Multiline = true;
this.tb_OtherPartyNote.Name = "tb_OtherPartyNote";
this.tb_OtherPartyNote.ReadOnly = true;
this.tb_OtherPartyNote.Size = new System.Drawing.Size(278, 174);
this.tb_OtherPartyNote.TabIndex = 7;
//
// tb_OtherPartyCountry
//
this.tb_OtherPartyCountry.Enabled = false;
this.tb_OtherPartyCountry.Location = new System.Drawing.Point(88, 154);
this.tb_OtherPartyCountry.Name = "tb_OtherPartyCountry";
this.tb_OtherPartyCountry.ReadOnly = true;
this.tb_OtherPartyCountry.Size = new System.Drawing.Size(278, 20);
this.tb_OtherPartyCountry.TabIndex = 6;
//
// tb_OtherPartyRealName
//
this.tb_OtherPartyRealName.Enabled = false;
this.tb_OtherPartyRealName.Location = new System.Drawing.Point(88, 113);
this.tb_OtherPartyRealName.Name = "tb_OtherPartyRealName";
this.tb_OtherPartyRealName.ReadOnly = true;
this.tb_OtherPartyRealName.Size = new System.Drawing.Size(278, 20);
this.tb_OtherPartyRealName.TabIndex = 5;
//
// tb_OtherPartyUserName
//
this.tb_OtherPartyUserName.Enabled = false;
this.tb_OtherPartyUserName.Location = new System.Drawing.Point(88, 52);
this.tb_OtherPartyUserName.Name = "tb_OtherPartyUserName";
this.tb_OtherPartyUserName.ReadOnly = true;
this.tb_OtherPartyUserName.Size = new System.Drawing.Size(278, 20);
this.tb_OtherPartyUserName.TabIndex = 4;
//
// label5
//
this.label5.AutoSize = true;
this.label5.Location = new System.Drawing.Point(49, 227);
this.label5.Name = "label5";
this.label5.Size = new System.Drawing.Size(33, 13);
this.label5.TabIndex = 3;
this.label5.Text = "Note:";
//
// label4
//
this.label4.AutoSize = true;
this.label4.Location = new System.Drawing.Point(24, 55);
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(58, 13);
this.label4.TabIndex = 2;
this.label4.Text = "Username:";
//
// label3
//
this.label3.AutoSize = true;
this.label3.Location = new System.Drawing.Point(33, 157);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(49, 13);
this.label3.TabIndex = 1;
this.label3.Text = "Country: ";
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(21, 116);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(61, 13);
this.label2.TabIndex = 0;
this.label2.Text = "Real name:";
Code 28: Code for displaying information about the other party
And finally take a look at the „status section”:
this.groupBox2.Controls.Add(this.lbl_CallState);
this.groupBox2.Location = new System.Drawing.Point(219, 3);
this.groupBox2.Name = "groupBox2";
this.groupBox2.Size = new System.Drawing.Size(116, 66);
this.groupBox2.TabIndex = 19;
this.groupBox2.TabStop = false;
this.groupBox2.Text = "CallState";
//
// lbl_CallState
//
this.lbl_CallState.AutoSize = true;
this.lbl_CallState.Location = new System.Drawing.Point(31, 35);
this.lbl_CallState.Name = "lbl_CallState";
this.lbl_CallState.Size = new System.Drawing.Size(0, 13);
this.lbl_CallState.TabIndex = 20;
this.lbl_CallState.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
// lbl_DomainHost
//
this.lbl_DomainHost.AutoSize = true;
this.lbl_DomainHost.Location = new System.Drawing.Point(80, 38);
this.lbl_DomainHost.Name = "lbl_DomainHost";
this.lbl_DomainHost.Size = new System.Drawing.Size(51, 13);
this.lbl_DomainHost.TabIndex = 18;
this.lbl_DomainHost.Text = "OFFLINE";
Code 29: Code for displaying the status