This tutorial shows how to create a simple PACS Server that handles basic connection, association, echo, and release requests from a PACS client in a C# WinForms application using the LEADTOOLS SDK.
Overview | |
---|---|
Summary | This tutorial covers how create a simple PACS server in a WinForms C# Application. |
Completion Time | 60 minutes |
Visual Studio Project | Download tutorial project (14 KB) |
Platform | Windows WinForms C# Application |
IDE | Visual Studio 2019 |
Development License | Download LEADTOOLS |
Get familiar with the basic steps of creating a project by reviewing the Add References and Set a License tutorial, before working on the Create a Simple PACS Server - WinForms C# tutorial.
Start with a copy of the project created in the Add References and Set a License tutorial. If you do not have that project, follow the steps in that tutorial to create it.
The references needed depend upon the purpose of the project. References can be added by one or the other of the following two methods (but not both).
If using NuGet references, this tutorial requires the following NuGet package:
Leadtools.Dicom.Pacs.Scp
If using local DLL references, the following DLLs are needed.
The DLLs are located at <INSTALL_DIR>\LEADTOOLS22\Bin\Dotnet4\x64
:
Leadtools.dll
Leadtools.Dicom.dll
For a complete list of which DLL files are required for your application, refer to Files to be Included With Your Application.
The License unlocks the features needed for the project. It must be set before any toolkit function is called. For details, including tutorials for different platforms, refer to Setting a Runtime License.
There are two types of runtime licenses:
Note
Adding LEADTOOLS NuGet and local references and setting a license are covered in more detail in the Add References and Set a License tutorial.
With the project created, the references added, and the license set, coding can begin.
In the Solution Explorer, Right-click on <Project>.csproj
and select Add -> New Folder. Create a folder called Utilities
. Right-click on this folder and select Add -> New Item... Select the Class
option and name the class Server.cs
, then click Add.
The Server
class is an extension of the DicomNet
class and implements the methods to listen for and handle the Accept and Close requests from a connected PACS client. It also builds and sends the Association response to the client after verifying the request.
Add the using
statements to the top of the Server
class:
// Using block at the top
using Leadtools.Dicom;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
Use the code below to define the public and private properties of the Server
class along with its constructor.
The constructor calls the BuildInclusionList()
which creates a collection of white-listed transfer and abstract UIDs to verify if client requests are supported by the server.
public class UserInfo // Holds user connection information
{
public string AETitle { get; set; }
public string IPAddress { get; set; }
public int Port { get; set; }
}
public class Server : DicomNet
{
// Server Properties
private string _CalledAE;
private int _Port = 104;
private int _Peers = 1;
private string _IPAddress = "";
public Form1 MainForm;
// Collection of connected Clients
private Dictionary<string, Client> _Clients = new Dictionary<string, Client>();
// Collection of allowed Users
private List<UserInfo> _Users = new List<UserInfo>();
// Get/Set Server Properties
public string CalledAE { get { return _CalledAE; } set { _CalledAE = value; } }
public int Port { get { return _Port; } set { _Port = value; } }
public int Peers { get { return _Peers; } set { _Peers = value; } }
public string IPAddress { get { return _IPAddress; } set { _IPAddress = value; } }
public Dictionary<string, Client> Clients { get { return _Clients; } }
public List<UserInfo> Users { get { return _Users; } }
// Collection of DICOM UID's supported by the server
private StringCollection _UidInclusionList = new StringCollection();
public StringCollection UidInclusionList => _UidInclusionList;
public Server() : base(null, DicomNetSecurityMode.None)
{
BuildInclusionList();
}
private void BuildInclusionList()
{
_UidInclusionList.Add(DicomUidType.ImplicitVRLittleEndian); // Default DICOM Transfer Syntax
_UidInclusionList.Add(DicomUidType.VerificationClass); // Echo Abstract Syntax
}
The Server
class uses the code below to verify and accept a Connection request from a client.
A Timer
class is used to check and close the connection in case of a timeout.
protected override void OnAccept(DicomExceptionCode error)
{
Client client;
if (error != DicomExceptionCode.Success)
{
MainForm.Log("Error in OnAccept with DicomExceptionCode: " + error.ToString());
SendAbort(DicomAbortSourceType.Provider, DicomAbortReasonType.Unknown);
}
// Accept Connection
client = new Client(this);
try
{
Accept(client);
}
catch (Exception ex)
{
MainForm.Log("Connection rejected : " + ex.Message);
client.Close();
client.Dispose();
return;
}
// Add to Collection of Connected Clients
if (!Clients.ContainsKey(client.PeerAddress + "_" + client.PeerPort))
{
Clients.Add(client.PeerAddress + "_" + client.PeerPort, client);
}
else
{
MainForm.Log("Connection rejected. IP already connected: " + client.PeerAddress);
client.Close();
client.Dispose();
return;
}
// Check against the Set Maximum Number of Peers
if (Clients.Count > Peers)
{
Clients.Remove(client.PeerAddress + "_" + client.PeerPort);
MainForm.Log("Connect: Connection rejected. Max connections reached");
client.Close();
client.Dispose();
return;
}
// Check against Allowed Users
UserInfo connectedUser = Users.FindAll(user => user.IPAddress == client.PeerAddress).Find(user => user.AETitle == client.Association.Calling);
if (connectedUser == null)
{
Clients.Remove(client.PeerAddress + "_" + client.PeerPort);
MainForm.Log("Connect: Connection rejected. Unknown User: " + client.PeerAddress);
client.Close();
client.Dispose();
return;
}
client.Timer.Tick += new EventHandler(Timer_Tick);
MainForm.Log("Connect: Accepted");
}
// Timer Checks for Connection Timeout
private void Timer_Tick(object sender, EventArgs e)
{
DicomTimer dt = sender as DicomTimer;
if (dt.Client.IsConnected())
{
CloseClient(dt.Client);
}
dt.Stop();
}
private void CloseClient(Client client)
{
Clients.Remove(client.PeerAddress + "_" + client.PeerPort);
client.SendAbort(DicomAbortSourceType.Provider, DicomAbortReasonType.Unknown);
client.CloseForced(true);
if (client.Association != null)
MainForm.Log("Timeout: Connection closed: " + client.Association.Calling);
else
MainForm.Log("Timeout: Connection closed: " + client.PeerAddress);
client.Dispose();
}
The following code handles a Close connection request from a client.
protected override void OnClose(DicomExceptionCode error, DicomNet net)
{
if (net.Association != null)
MainForm.Log("Closing connection with client: " + net.Association.Calling);
else
MainForm.Log("Closing connection with client");
if (Clients.ContainsKey(net.PeerAddress + "_" + net.PeerPort))
{
Client client = Clients[net.PeerAddress + "_" + net.PeerPort];
Clients.Remove(net.PeerAddress + "_" + net.PeerPort);
client.Dispose();
}
else
{
net.Dispose();
}
}
The DoAssociateRequest()
method builds and sends the response to an Association request from the client.
public void DoAssociateRequest(Client client, DicomAssociate association)
{
using (DicomAssociate retAssociate = new DicomAssociate(false))
{
if (retAssociate == null)
{
client.SendAssociateReject(DicomAssociateRejectResultType.Permanent, DicomAssociateRejectSourceType.Provider1, DicomAssociateRejectReasonType.Application);
return;
}
// Build the Association Request
retAssociate.MaxLength = 46726;
retAssociate.Version = 1;
retAssociate.Called = CalledAE;
retAssociate.Calling = association.Calling;
retAssociate.ImplementClass = "1.2.840.114257.1123456";
retAssociate.ImplementationVersionName = "1";
retAssociate.ApplicationContextName = (string)DicomUidType.ApplicationContextName;
// Check if Abstract and Transfer Syntaxes are Supported
for (int i = 0; i < association.PresentationContextCount; i++)
{
byte id = association.GetPresentationContextID(i);
string abstractSyntax = association.GetAbstract(id);
retAssociate.AddPresentationContext(id, DicomAssociateAcceptResultType.Success, abstractSyntax);
if (IsSupported(abstractSyntax))
{
for (int j = 0; j < association.GetTransferCount(id); j++)
{
string transferSyntax = association.GetTransfer(id, j);
if (IsSupported(transferSyntax))
{
retAssociate.AddTransfer(id, transferSyntax);
break;
}
}
if (retAssociate.GetTransferCount(id) == 0)
{
// Presentation id doesn't have any abstract syntaxes therefore we will reject it.
retAssociate.SetResult(id, DicomAssociateAcceptResultType.AbstractSyntax);
}
}
else
{
retAssociate.SetResult(id, DicomAssociateAcceptResultType.AbstractSyntax);
}
}
if (association.MaxLength != 0)
{
retAssociate.MaxLength = association.MaxLength;
}
MainForm.Log("Sending Associate Accept");
client.SendAssociateAccept(retAssociate);
}
}
// Check UID against the Inclusion List and UIDs supported by LEADTOOLS
private bool IsSupported(string uid)
{
if (DicomUidTable.Instance.Find(uid) == null || !UidInclusionList.Contains(uid))
{
string uidName = DicomUidTable.Instance.Find(uid).Name;
MainForm.Log("UID not supported: " + uid + " (" + uidName + ")");
return false;
}
else
return true;
}
The Server
class uses the following code to initialize an instance of the DicomAction
class for a supported request from a PACS client.
public DicomAction InitAction(string actionOp, ProcessType process, Client client)
{
DicomAction action = new DicomAction(process, this, client);
action.AETitle = client.Association.Calling;
action.ipAddress = client.PeerAddress;
client.Timer.Start();
MainForm.Log(actionOp + ": Received from " + action.AETitle);
return action;
}
private void action_ActionComplete(object sender, EventArgs e)
{
DicomAction action = (DicomAction)sender;
action.Client.Timer.Start();
}
In the Solution Explorer, Right-click on Utilities folder and select Add -> New Item. Select the Class
option and name the class Client.cs
, then click Add.
The Client
class is an extension of the DicomNet
class that receives and processes requests from a connected PACS client. In this tutorial, this class handles Associate, Release, and Echo requests.
Add the using
statements below to the top.
// Using block at the top
using Leadtools.Dicom;
Use the code below to define the public and private properties of the Client
class along with its constructor. An instance of the DicomTimer
is used to check for client connection time-out.
namespace PacsServer
{
public class Client : DicomNet
{
private Server _server;
private DicomAction action;
// Timer to Monitor Connection Timeout
private DicomTimer _Timer;
public DicomTimer Timer { get { return _Timer; } }
public Client(Server server) : base(null, DicomNetSecurityMode.None)
{
_server = server;
_Timer = new DicomTimer(this, 30);
}
When an Association request is received by the server, the OnReceiveAssociateRequest()
method is called. Use the following code to call the DoAssociateRequest()
method from the Server
class instance.
protected override void OnReceiveAssociateRequest(DicomAssociate association)
{
_server.MainForm.Log("ASSOCIATE-REQUEST: Received from " + association.Calling);
_server.DoAssociateRequest(this, association);
_server.MainForm.Log("ASSOCIATE-REQUEST: Association accepted from " + association.Calling + " (" + PeerAddress + ")");
}
The following code handles any Release requests sent by a connected client.
protected override void OnReceiveReleaseRequest()
{
_server.Clients.Remove(PeerAddress + "_" + PeerPort);
SendReleaseResponse();
_Timer.Stop();
}
The following code handles any Echo requests sent by a connected client. It initializes and runs a DicomAction
instance configured for sending an Echo response back to the client.
protected override void OnReceiveCEchoRequest(byte presentationID, int messageID, string affectedClass)
{
action = _server.InitAction("C-ECHO-REQUEST", ProcessType.EchoRequest, this);
action.PresentationID = presentationID;
action.MessageID = messageID;
action.Class = affectedClass;
action.DoAction();
}
In the Solution Explorer, Right-click on the Utilities
folder and select Add -> New Item. Select the Class
option and name the class DicomAction.cs
, then click Add.
The Client
class configures and uses an instance of the DicomAction
class to construct and send the appropriate response back to a connected client. In this tutorial, client Echo requests are handled by this class.
Add the using
statements to the top of the Server
class:
// Using block at the top
using Leadtools.Dicom;
Use the code below to define the public and private properties of the DicomAction
class along with its constructor.
namespace PacsServer
{
public enum ProcessType
{
EchoRequest
}
public class DicomAction
{
ProcessType process;
// Connection Properties
private Server server;
private Client client;
private string _AETitle;
private string _ipAddress;
// Request Properties
private byte _PresentationID;
private int _MessageID;
private string _Class;
private string _Instance;
// Get/Set Properties
public Client Client { get { return client; } }
public string AETitle { get { return _AETitle; } set { _AETitle = value; } }
public string ipAddress { get { return _ipAddress; } set { _ipAddress = value; } }
public byte PresentationID { get { return _PresentationID; } set { _PresentationID = value; } }
public int MessageID { get { return _MessageID; } set { _MessageID = value; } }
public string Class { get { return _Class; } set { _Class = value; } }
public string Instance { get { return _Instance; } set { _Instance = value; } }
public DicomAction(ProcessType process, Server server, Client client)
{
this.server = server;
this.client = client;
this.process = process;
}
The DoAction()
checks the request type and calls the relevant method to build and send a response to the client. In this tutorial, the client Echo request is handled.
public void DoAction()
{
if (client.Association != null)
{
// C-ECHO
if (process == ProcessType.EchoRequest)
DoEchoRequest();
}
}
The IsActionSupported()
checks if the request's class UID is supported by the server.
private bool IsActionSupported()
{
byte id;
if (DicomUidTable.Instance.Find(Class) == null)
return false;
id = client.Association.FindAbstract(Class);
if (id == 0 || client.Association.GetResult(id) != DicomAssociateAcceptResultType.Success)
return false;
return true;
}
The GetUIDName()
gets the name of the request's class.
private string GetUIDName()
{
DicomUid uid = DicomUidTable.Instance.Find(Class);
if (uid == null)
return Class;
return uid.Name;
}
The DoEchoRequest()
method verifies and sends an Echo response to a client.
private void DoEchoRequest()
{
// Check if Abstract Syntax is Supported by the LEADTOOLS Classes
if (!IsActionSupported())
{
string name = GetUIDName();
server.MainForm.Log("C-ECHO-REQUEST: Abstract syntax (" + name + ") not supported by association");
client.SendCEchoResponse(_PresentationID, _MessageID, Class, DicomCommandStatusType.ClassNotSupported);
return;
}
// Send C-Echo Response to the Client
client.SendCEchoResponse(_PresentationID, MessageID, Class, DicomCommandStatusType.Success);
server.MainForm.Log("C-ECHO-RESPONSE: Response sent to " + AETitle);
}
In the Solution Explorer, Right-click on the Utilities
folder and select Add -> New Item. Select the Class
option and name the class DicomTimer.cs
, then click Add.
The DicomTimer
class is an extension of the System.Windows.Forms.Timer
object and it is used to check the connection and to close and abort a connection after it times out.
using System.Windows.Forms;
namespace PacsServer
{
public class DicomTimer : Timer
{
private Client _Client;
public Client Client { get { return _Client; } }
public DicomTimer(Client client, int time)
{
_Client = client;
Interval = (time * 1000);
}
}
}
Add the following to the using
block at the top of Form1.cs
:
using System;
using System.Windows.Forms;
using System.IO;
using Leadtools;
using Leadtools.Dicom;
Add a call to InitServer()
in the Form1()
method after the SetLicense()
call. This calls a method to create an instance of the Server
class and initializes it with valid server information.
// Add to global variables for the form
private Server pacsServer;
// Server Properties
private string serverAE = "LEAD_SERVER";
private string serverIP = "0.0.0.0"; // Use valid IP
private string serverPort = "0"; // Use valid port number
// Client Properties
private string clientAE = "CLIENT";
private string clientIP = "0.0.0.0"; // Use valid IP
private string clientPort = "0"; // Use valid port number
public Form1()
{
InitializeComponent();
SetLicense();
InitServer();
}
private void InitServer()
{
try
{
DicomEngine.Startup();
pacsServer = new Server();
// Server Connection Information
pacsServer.CalledAE = serverAE;
pacsServer.IPAddress = serverIP;
pacsServer.Port = int.Parse(serverPort);
// Add Permitted User Information
pacsServer.Users.Add(new UserInfo { AETitle = clientAE, IPAddress = clientIP, Port = int.Parse(clientPort) });
pacsServer.MainForm = this;
pacsServer.Peers = 1; // Maximum Number of Connected Clients
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Open Form1.cs
in the Solution Explorer. In the Designer, add a Server drop-down menu item, using the MenuStrip tool. Add two new items to the Server drop-down, with their text property set to &Start and St&op. Leave the new items' names as startToolStripMenuItem
and stopToolStripMenuItem
.
Double-click the Start
and Stop
menu items to create their individual event handlers. This will bring up the code behind the form.
Add the following code to the startToolStripMenuItem_Click
event handler.
private void startToolStripMenuItem_Click(object sender, EventArgs e)
{
try
{
DicomNet.Startup();
pacsServer.Listen(pacsServer.IPAddress, pacsServer.Port, pacsServer.Peers);
Log(serverAE + " has started listening on " + serverIP + ":" + serverPort);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Add the following code to the stopToolStripMenuItem_Click
event handler.
private void stopToolStripMenuItem_Click(object sender, EventArgs e)
{
try
{
pacsServer.Close();
DicomNet.Shutdown();
Log(serverAE + " has stopped listening");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Navigate back to the Designer
and add a loggingTextBox
TextBox control.
Change the following properties for this TextBox control to:
Right-click the Designer
and select View Code, or press F7, to bring up the code behind the form. Create a new method in the Form1
class named Log(string message)
. Add the code below to display logging messages from the Server
and ClientConnection
classes in the TextBox.
public void Log(string message)
{
if (InvokeRequired)
{
this.Invoke(new MethodInvoker(delegate
{
loggingTextBox.Text += message + "\r\n";
}));
}
else
loggingTextBox.Text += message + "\r\n";
}
Run the project by pressing F5, or by selecting Debug -> Start Debugging.
If the steps were followed correctly, the application runs and allows the user to initialize and start a simple PACS server that handles connection, association, and echo requests from connected clients.
Run the CDLL Verification - SCU
demo to test the server, this demo is found here:
<INSTALL_DIR>\LEADTOOLS22\Bin\CDLL\x64\DicomVrf_Original.exe
Fill-in the Server and Client AE information then click Verify to send a C-ECHO request to the server. The server will send a response back to the client.
This tutorial showed how to use the DicomNet
class to create the basic components for a simple PACS server that handles connection, association, echo and release requests from PACS clients.