Create a Simple PACS Server - WinForms C#

This tutorial shows how to create a simple PACS Server that handles connection, association, storage, 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 2017, 2019
Development License Download LEADTOOLS

Required Knowledge

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,

Create the Project and Add LEADTOOLS References

In Visual Studio, create a new C# Windows Winforms project, and add the below necessary LEADTOOLS references.

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:

If using local DLL references, the following DLLs are needed.

The DLLs are located at <INSTALL_DIR>\LEADTOOLS21\Bin\Dotnet4\x64:

For a complete list of which DLL files are required for your application, refer to Files to be Included With Your Application.

Set the License File

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.

Create the Server Class

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 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 will implement server methods to listen for and handle Accept and Close requests from a listening connection.

Add the Server Class' Constructor and Properties

The Server class will include the following properties:

Add the using statements to the top of the Server class:

C#
// Using block at the top 
using System; 
using System.Collections.Generic; 
using System.Collections.Specialized; 
using System.Runtime.InteropServices; 
using Leadtools.Dicom; 

Add the code below to define the public and private properties of the Server class along with its constructor.

C#
namespace Create_a_Simple_PACS_Server 
{ 
   public class Server : DicomNet 
   { 
      public Form1 MainForm; 
      public bool ServerFinished = false; 
 
      // Collection of DICOM UID's supported by the server 
      private StringCollection _UidInclusionList = new StringCollection(); 
      public StringCollection UidInclusionList => _UidInclusionList; 
 
      // Collection of connected Clients 
      private Dictionary<string, ClientConnection> _Clients = new Dictionary<string, ClientConnection>(); 
      public Dictionary<string, ClientConnection> Clients { get { return _Clients; } } 
 
      // Server Properties 
      private string _CalledAE; 
      private string _IPAddress = ""; 
      private int _Port = 104; 
      private string _storeDirectory = string.Empty; 
      public string CalledAE { get { return _CalledAE; } set { _CalledAE = value; } } 
      public string IPAddress { get { return _IPAddress; } set { _IPAddress = value; } } 
      public int Port { get { return _Port; } set { _Port = value; } } 
      public string StoreDirectory { get { return _storeDirectory; } set { _storeDirectory = value; } } 
 
      public Server(string path, DicomNetSecurityMode mode) : base(path, mode) 
      { 
         BuildInclusionList(); 
         StoreDirectory = path; 
      } 
   } 
} 

The UID Inclusion List

Use the code below to define the IODs that will be supported by this PACS server. This will function as a whitelist to only allow connections that use the included UIDs.

C#
private void BuildInclusionList() 
{ 
   UidInclusionList.Add(DicomUidType.ImplicitVRLittleEndian); 
   UidInclusionList.Add(DicomUidType.ExplicitVRLittleEndian); 
   UidInclusionList.Add(DicomUidType.ExplicitVRBigEndian); 
   UidInclusionList.Add(DicomUidType.VerificationClass); 
   UidInclusionList.Add(DicomUidType.SCImageStorage); 
   UidInclusionList.Add(DicomUidType.USImageStorage); 
   UidInclusionList.Add(DicomUidType.USImageStorageRetired); 
   UidInclusionList.Add(DicomUidType.USMultiframeImageStorage); 
   UidInclusionList.Add(DicomUidType.USMultiframeImageStorageRetired); 
   UidInclusionList.Add(DicomUidType.CTImageStorage); 
   UidInclusionList.Add(DicomUidType.JPEGBaseline1); 
   UidInclusionList.Add(DicomUidType.JPEGExtended2_4); 
   UidInclusionList.Add(DicomUidType.JPEGLosslessNonhier14B); 
   UidInclusionList.Add(DicomUidType.EnhancedMRImageStorage); 
   UidInclusionList.Add(DicomUidType.DXImageStoragePresentation); 
} 

The OnAccept() Code

The code below will handle an Accept request from a connected client. Provided that there are no errors in the connection request, this code will accept the connection and add the client connection's information to the Clients collection.

C#
protected override void OnAccept(DicomExceptionCode error) 
{ 
   ClientConnection client; 
 
   if (error != DicomExceptionCode.Success) 
   { 
      Log(string.Format("Error in OnAccept with DicomExceptionCode: {0}", error.ToString())); 
      SendAbort(DicomAbortSourceType.Provider, DicomAbortReasonType.Unknown); 
   } 
 
   client = new ClientConnection(this); 
 
   try 
   { 
      Accept(client); 
   } 
   catch (Exception ex) 
   { 
      Log(string.Format("Connection rejected : {0}", ex.Message)); 
      client.Close(); 
      return; 
   } 
 
   if (!Clients.ContainsKey(client.PeerAddress + "_" + client.PeerPort)) 
   { 
      Clients.Add(client.PeerAddress + "_" + client.PeerPort, client); 
   } 
   else 
   { 
      Log("Connection rejected. IP already connected: " + client.PeerAddress); 
      client.Close(); 
      return; 
   } 
} 

The OnClose() Code

The following code will handle a Close request from a connected client. This will remove the connection's information from the Clients collection and dispose the relevant objects.

C#
protected override void OnClose(DicomExceptionCode error, DicomNet net) 
{ 
   if (net.Association != null) 
      Log(string.Format("Closing connection with client: {0}", net.Association.Calling)); 
   else 
      Log("Closing connection with client"); 
 
   if (Clients.ContainsKey(net.PeerAddress + "_" + net.PeerPort)) 
   { 
      ClientConnection client = Clients[net.PeerAddress + "_" + net.PeerPort]; 
      Clients.Remove(net.PeerAddress + "_" + net.PeerPort); 
      client.Dispose(); 
   } 
   else 
   { 
      net.Dispose(); 
   } 
} 

The Wait() and Log() Code

The Wait() method allows for Windows messaging to continue while the server is listening and doing connection operations.

The Log() method is used to write logging messages on the Main form.

C#
      public bool Wait() 
      { 
         do 
         { 
            Breathe(); 
         } while (!ServerFinished); 
 
         return true; 
      } 
 
      private void Log(string message) 
      { 
         MainForm.Log(message); 
      } 
   } 
} 

Create the ClientConnection Class

In the Solution Explorer, Right-click on <Project>.csproj and select Add -> New Item. Select the Class option and name the class ClientConnection.cs, then click Add.

The ClientConnection class is an extension of the DicomNet class as is the Server class.

ClientConnection implements the handling of Associate, Echo, Store, and Release requests sent by a connected client.

Add the ClientConnection Class' Constructor and Properties

Add the using statements below to the top.

C#
// Using block at the top 
using System; 
using System.Timers; 
using Leadtools.Dicom; 

Use the code below to set the properties and constructor of the ClientConnection class.

The properties include an instance of the Server class that has been created with the server information, along with a DicomTimer object defined in the code block below. DicomTimer is an extension of a System.Timers.Timer object.

C#
namespace Create_a_Simple_PACS_Server 
{ 
   public class ClientConnection : DicomNet 
   { 
      private Server _server; 
 
      private DicomTimer _connectionTimer; 
      public DicomTimer connectionTimer { get { return _connectionTimer; } } 
 
      public ClientConnection(Server server) : base(null, Leadtools.Dicom.DicomNetSecurityMode.None) 
      { 
         this._server = server; 
 
         _connectionTimer = new DicomTimer(this, 30); 
         _connectionTimer.Elapsed += ConnectionTimer_Elapsed; 
      } 
   } 
} 

OnReceive() Code

The code for this method handles any error that occurs and aborts the connection.

C#
protected override void OnReceive(DicomExceptionCode error, DicomPduType pduType, IntPtr buffer, int bytes) 
{ 
   if (error != DicomExceptionCode.Success) 
   { 
      Log(string.Format("Error in OnReceive with DicomExceptionCode: {0}", error.ToString())); 
      SendAbort(DicomAbortSourceType.Provider, DicomAbortReasonType.Unexpected); 
   } 
 
   connectionTimer.Stop(); 
   base.OnReceive(error, pduType, buffer, bytes); 
   connectionTimer.Start(); 
} 

OnReceiveAssociateRequest() Code

The following code handles association requests sent by a connected client to a server. The method derives its own association request to make sure that it conforms to the specifications of the server. This includes only allowing Transfer Syntax UIDs that are allowed in the UidInclusionList of the server.

C#
protected override void OnReceiveAssociateRequest(DicomAssociate association) 
{ 
   Log(string.Format("Associate request received from: {0}", association.Calling)); 
 
 
   bool minOnePresentationContextSupported = false; 
 
   using (DicomAssociate retAssociate = new DicomAssociate(false)) 
   { 
      // Build the Association Request 
      retAssociate.Called = _server.CalledAE; 
      retAssociate.Calling = association.Calling; 
      retAssociate.ImplementClass = "1.2.840.114257.1123456"; 
      retAssociate.ImplementationVersionName = "1"; 
      retAssociate.MaxLength = 46726; 
      retAssociate.Version = 1; 
      retAssociate.ApplicationContextName = (string)DicomUidType.ApplicationContextName; 
 
      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)) 
               { 
                  minOnePresentationContextSupported = true; 
                  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; 
      } 
 
      if (minOnePresentationContextSupported) 
      { 
         SendAssociateAccept(retAssociate); 
         Log("Sending Associate Accept"); 
      } 
      else 
      { 
         SendAssociateReject(DicomAssociateRejectResultType.Permanent, 
                        DicomAssociateRejectSourceType.Provider2, 
                        DicomAssociateRejectReasonType.Application); 
         Log("Sending Associate Reject - Abstract or transfer syntax not supported"); 
      } 
   } 
} 

OnReceiveCEchoRequest() Code

The following code handles any Echo requests sent by a connected client.

C#
protected override void OnReceiveCEchoRequest(byte presentationID, int messageID, string affectedClass) 
{ 
   Log(string.Format("C-ECHO Request received from: {0}", this.Association.Calling)); 
 
   byte id; 
   if (DicomUidTable.Instance.Find(affectedClass) == null) 
   { 
      SendCEchoResponse(presentationID, messageID, affectedClass, DicomCommandStatusType.ClassNotSupported); 
      Log(string.Format("C-ECHO response sent to \"{0}\" (Class Not Supported)", this.Association.Calling)); 
   } 
 
   id = this.Association.FindAbstract(affectedClass); 
   if (id == 0 || this.Association.GetResult(id) != DicomAssociateAcceptResultType.Success) 
   { 
      SendCEchoResponse(presentationID, messageID, affectedClass, DicomCommandStatusType.ClassNotSupported); 
      Log(string.Format("C-ECHO response sent to \"{0}\" (Class Not Supported)", this.Association.Calling)); 
   } 
 
   SendCEchoResponse(presentationID, messageID, affectedClass, DicomCommandStatusType.Success); 
   Log(string.Format("C-ECHO response sent to \"{0}\" (Success)", this.Association.Calling)); 
} 

OnReceiveCStoreRequest() Code

The code below handles Store requests sent by a client. Provided that the association request was successfully accepted and a supported Abstract Syntax is found, the method stores the passed DICOM file into the server's Store Directory.

C#
protected override void OnReceiveCStoreRequest(byte presentationID, int messageID, string affectedClass, string instance, DicomCommandPriorityType priority, string moveAE, int moveMessageID, DicomDataSet dataset) 
{ 
   DicomCommandStatusType status = DicomCommandStatusType.Failure; 
   if (this.Association == null) 
   { 
      Log("Association is invalid"); 
      this.SendAbort(DicomAbortSourceType.Provider, DicomAbortReasonType.Unexpected); 
      return; 
   } 
 
   Log(string.Format("C-STORE request received:\nCalled: {0}\nCaller: {1}", Association.Called, Association.Calling)); 
 
   DicomUid dicomUID = DicomUidTable.Instance.Find(affectedClass); 
   if (dicomUID == null) 
      status = DicomCommandStatusType.ClassNotSupported; 
   else 
   { 
      byte id = Association.FindAbstract(affectedClass); 
      while (((id != 0) && (this.Association.GetResult(id) != DicomAssociateAcceptResultType.Success))) 
         id = this.FindNextAbstract(Association, id, affectedClass); 
 
      if (id == 0) 
      { 
         Log(string.Format("Abstract syntax not supported: {0}\n\t{1}", dicomUID.Code, dicomUID.Name)); 
         status = DicomCommandStatusType.ClassNotSupported; 
      } 
      else 
         status = DicomCommandStatusType.Success; 
 
   } 
 
   // All checks passed 
   // Save the file 
   if (status == DicomCommandStatusType.Success) 
   { 
      status = DicomCommandStatusType.Failure; 
      DicomElement dicomElement = dataset.FindFirstElement(null, DicomTag.SOPInstanceUID, true); 
      if (dicomElement != null) 
      { 
         string SOPInstanceUID = dataset.GetStringValue(dicomElement, 0); 
         if (!string.IsNullOrEmpty(SOPInstanceUID)) 
         { 
            string fileName = string.Format("{0}//{1}.dcm", _server.StoreDirectory, SOPInstanceUID); 
 
            try 
            { 
               dataset.Save(fileName, DicomDataSetSaveFlags.None); 
               status = DicomCommandStatusType.Success; 
            } 
            catch (Exception ex) 
            { 
               Log(string.Format("C-STORE request exception during save:\n{0}\n{1}", ex.Message, ex.StackTrace)); 
            } 
         } 
 
         dataset.FreeElementValue(dicomElement); 
      } 
   } 
 
   this.SendCStoreResponse(presentationID, messageID, affectedClass, instance, status); 
   Log(string.Format("C-STORE response sent ({0})", status)); 
} 

OnReceiveReleaseRequest() Code

The following code handles any Release requests sent by a connected client.

C#
protected override void OnReceiveReleaseRequest() 
{ 
   if (this.Association != null) 
   { 
      Log(string.Format("Received Release Request: {0}", this.Association.Calling == null ? this.Association.Calling : @"N/A")); 
      Log(string.Format("Sending release response: {0}", this.Association.Calling == null ? this.Association.Calling : @"N/A")); 
   } 
   else 
   { 
      Log("Received Release Request"); 
      Log("Sending release response"); 
   } 
   SendReleaseResponse(); 
   connectionTimer.Stop(); 
} 

IsSupported(), FindNextAbstract(), ConnectionTimer_Elapsed(), and Log() Code

The IsSupported() method is used to check a UID to see if it is supported by the default DICOM table and the server's inclusion list.

C#
private bool IsSupported(string uid) 
{ 
   bool supported = false; 
 
   if (DicomUidTable.Instance.Find(uid) == null) 
   { 
      Log(string.Format("UID not supported: {0}", uid)); 
      return false; 
   } 
 
   if (_server.UidInclusionList.Contains(uid)) 
   { 
      Log(string.Format("UID supported: {0}", uid)); 
      supported = true; 
   } 
 
   return supported; 
} 

The FindNextAbstract() method is used to find a supported Abstract Syntax in the association.

C#
private byte FindNextAbstract(DicomAssociate dicomPDU, int id, string uid) 
{ 
   if (uid == null) 
      return 0; 
 
   int presentationCount = dicomPDU.PresentationContextCount; 
   int lastPresentation = 2 * presentationCount - 1; 
   while (id <= lastPresentation) 
   { 
      id = id + 1; 
      string szAbstract = dicomPDU.GetAbstract(byte.Parse(id.ToString())); 
      if (string.Equals(uid, szAbstract)) 
      { 
         return byte.Parse(id.ToString()); 
      } 
   } 
 
   return 0; 
} 

The ConnectionTimer_Elapsed() method is used with the DicomTimer class and aborts and closes a connection after the specified interval of inactivity is reached.

C#
private void ConnectionTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) 
{ 
   if (IsConnected()) 
   { 
      Log("Connection timed out. Aborting connection."); 
      SendAbort(DicomAbortSourceType.Provider, DicomAbortReasonType.Unknown); 
      this.Close(); 
      connectionTimer.Stop(); 
      connectionTimer.Dispose(); 
   } 
} 

The Log() method allows logging messages to be displayed in the Main form.

C#
private void Log(string message) 
{ 
    _server.MainForm.Log(message); 
} 

DicomTimer Class

The DicomTimer class is an extension of the System.Timers.Timer object and it is used to check the connection and trigger the ConnectionTimer_Elapsed() handler to close and abort a connection after inactivity.

C#
public class DicomTimer : Timer 
{ 
   private ClientConnection _Client; 
   public ClientConnection Client { get { return _Client; } } 
 
   public DicomTimer(ClientConnection client, int time) 
   { 
      _Client = client; 
      Interval = (time * 1000); 
   } 
} 

Form UI and Initialization

Initialize the Server

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 initialize it with the valid server information.

C#
// Add to global variables for the form 
private Server pacsServer; 
       
// Server Properties 
private string serverAE = "DCM_SERVER"; 
private string serverIP = "0.0.0.0"; // Use valid IP 
private string serverPort = "0"; // Use valid port number 
private string storeDirectory = @"X:\xx"; //Use valid directory 
C#
public Form1() 
{ 
   InitializeComponent(); 
   SetLicense(); 
   InitServer(); 
} 
 
private void InitServer() 
{ 
   try 
   { 
      DicomEngine.Startup(); 
 
      pacsServer = new Server(storeDirectory, DicomNetSecurityMode.None); 
      pacsServer.MainForm = this; 
      pacsServer.CalledAE = serverAE; 
      pacsServer.IPAddress = serverIP; 
      pacsServer.Port = Int32.Parse(serverPort); 
   } 
   catch(Exception ex) 
   { 
      MessageBox.Show(ex.Message); 
   } 
} 

Start and Stop Server Menu Items

Open Form1.cs in the Solution Explorer. In the Designer, add a Server dropdown menu item, using the MenuStrip tool. Add two new items to the Server dropdown, with their text property set to &Start and St&op. Leave the new items' names as startToolStripMenuItem and stopToolStripMenuItem.

Adding a MenuStrip to the form

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.

C#
private void startToolStripMenuItem_Click(object sender, EventArgs e) 
{ 
   try 
   { 
      DicomNet.Startup(); 
      pacsServer.ServerFinished = false; 
 
      pacsServer.Listen(serverIP, Int32.Parse(serverPort), 1); 
      Log(string.Format($"{serverAE} has started listening on {serverIP}:{serverPort}")); 
      pacsServer.Wait(); 
      Log(string.Format($"{serverAE} has stopped listening")); 
   } 
   catch(Exception ex) 
   { 
      MessageBox.Show(ex.Message); 
   } 
} 

Add the following code to the stopToolStripMenuItem_Click event handler.

C#
private void stopToolStripMenuItem_Click(object sender, EventArgs e) 
{ 
   try 
   { 
      pacsServer.ServerFinished = true; 
      pacsServer.Close(); 
 
      DicomNet.Shutdown(); 
   } 
   catch (Exception ex) 
   { 
      MessageBox.Show(ex.Message); 
   } 
} 

Logging TextBox

Navigate back to the Designer and add a loggingTextBox TextBox control.

Adding a TextBox to the form

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.

C#
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

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, echo, and storage requests from connected clients.

The LEADTOOLS High-level Store demo can be used to test the functions of this server. This demo can be found here: <INSTALL_DIR>\LEADTOOLS21\Shortcuts\PACS\.NET Framework Class Libraries\PACS Framework (High Level)\DICOM High-level Store\

The configuration of the default demo PACS servers can be skipped.

This server's connection information can be added to the connection list through the Options... button.

Adding the Server to Store demo

Wrap-up

This tutorial showed how to use the DicomNet class to create a PACS server that can handle connection and storage requests from PACS clients. In addition, it showed how to use the DicomScp class.

See Also

Help Version 21.0.2023.3.1
Products | Support | Contact Us | Intellectual Property Notices
© 1991-2021 LEAD Technologies, Inc. All Rights Reserved.


Products | Support | Contact Us | Intellectual Property Notices
© 1991-2021 LEAD Technologies, Inc. All Rights Reserved.