Handle Move Requests in a PACS Server - WinForms C# .NET 6

This tutorial shows how to configure a PACS Server with an XML database to send a dataset that was requested by a C-MOVE action to a client in a C# WinForms application using the LEADTOOLS SDK.

Overview  
Summary This tutorial covers how to handle C-MOVE requests for a PACS server in a WinForms C# Application.
Completion Time 60 minutes
Visual Studio Project Download tutorial project (19 KB)
Platform .NET 6 WinForms C# Application
IDE Visual Studio 2022
Development License Download LEADTOOLS

Required Knowledge

Get familiar with the basic steps of creating a project and a PACS Server with an XML database by reviewing the Add References and Set a License, Create a Simple PACS Server, and Handle Store Requests in a PACS Server tutorials, before working on the Handle Move Requests in a PACS Server - WinForms C# tutorial.

Additionally, review the Handle Find Requests in a PACS Server tutorial as a number of methods are shared between these tutorials. For this tutorial, the server will need to have datasets available before selecting any to retrieve using a move action.

Create the Project and Add LEADTOOLS References

Start with a copy of the project created in the Handle Find Requests in a PACS Server 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 packages:

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

The DLLs are located at <INSTALL_DIR>\LEADTOOLS23\Bin\net:

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.

Modify Client to Accept Move Requests

With the project created, the references added, and the license set, coding can begin.

In the Solution Explorer, open the Utilities\Client.cs file and add the code below for the OnReceiveCMoveRequest() handler method which is called when a C-MOVE request is sent:

C#
protected override void OnReceiveCMoveRequest(byte presentationID, int messageID, string affectedClass, DicomCommandPriorityType priority, string moveAE, DicomDataSet dataSet) 
{ 
   action = _server.InitAction("C-MOVE-REQUEST", ProcessType.MoveRequest, this, dataSet); 
 
   action.PresentationID = presentationID; 
   action.MessageID = messageID; 
   action.Class = affectedClass; 
   action.Priority = priority; 
   action.MoveAETitle = moveAE; 
   action.DoAction(); 
   dataSet.Dispose(); 
} 

Add Utility Methods to Parse DICOM dataset

Add the Move Action to the DicomAction Class

Open the Utilities\DicomAction.cs file and add the following to the using block.

C#
using Leadtools.Dicom.Scu; 
using Leadtools.Dicom.Scu.Common; 
using System.Net; 

Add the MoveRequest to the enumeration directly after the namespace declaration.

C#
public enum ProcessType 
{ 
   EchoRequest, 
   StoreRequest, 
   FindRequest, 
   MoveRequest 
} 

Modify the DoAction method to parse the MoveRequest process.

C#
public void DoAction() 
{ 
   if (client.Association != null) 
   { 
      switch (process) 
      { 
         // C-ECHO 
         case ProcessType.EchoRequest: 
            DoEchoRequest(); 
            break; 
         // C-STORE 
         case ProcessType.StoreRequest: 
            DoStoreRequest(); 
            break; 
         // C-FIND 
         case ProcessType.FindRequest: 
            DoFindRequest(); 
            break; 
         case ProcessType.MoveRequest: 
            DoMoveRequest(); 
            break; 
      } 
   } 
} 

Use the following code for the DoMoveRequest() method which validates the MOVE request and calls the relevant move method according to the query level of the request.

C#
private void DoMoveRequest() 
{ 
   DicomCommandStatusType status; 
   UserInfo userInfo; 
   string level, msgTag = ""; 
 
   // Check Abstract Syntax 
   if (!IsActionSupported()) 
   { 
      string name = GetUIDName(); 
 
      server.MainForm.Log("C-MOVE-REQUEST: Abstract syntax (" + name + ") not supported by association"); 
      client.SendCMoveResponse(_PresentationID, _MessageID, _Class, DicomCommandStatusType.ClassNotSupported, 0, 0, 0, 0, null); 
      return; 
   } 
 
   // Check if Client AE is Supported 
   userInfo = server.Users.FindAll(user => user.AETitle == _MoveAETitle).Find(user => user.IPAddress == _ipAddress); 
   if (userInfo == null) 
   { 
      server.MainForm.Log("C-MOVE-REQUEST: Move destination " + _MoveAETitle + " not found."); 
      client.SendCMoveResponse(_PresentationID, _MessageID, _Class, DicomCommandStatusType.RefusedMoveDestinationUnknown, 0, 0, 0, 0, null); 
      return; 
   } 
 
   // Check for missing tags 
   level = Utils.GetStringValue(ds, DicomTag.QueryRetrieveLevel); 
   status = AttributeStatus(level, ref msgTag); 
   if (status != DicomCommandStatusType.Success) 
   { 
      server.MainForm.Log("C-MOVE-REQUEST: " + msgTag); 
      client.SendCMoveResponse(_PresentationID, _MessageID, _Class, status, 0, 0, 0, 0, null); 
      return; 
   } 
 
   // Move dataset according to the query level 
   switch (level) 
   { 
      case "PATIENT": 
         MoveImages(userInfo, QueryLevel.Patient); 
         break; 
      case "STUDY": 
         MoveImages(userInfo, QueryLevel.Study); 
         break; 
      case "SERIES": 
         MoveImages(userInfo, QueryLevel.Series); 
         break; 
      case "IMAGE": 
         MoveImages(userInfo, QueryLevel.Image); 
         break; 
      default: 
         server.MainForm.Log("C-MOVE-REQUEST: Invalid query retrieve level: " + level); 
         client.SendCMoveResponse(_PresentationID, _MessageID, _Class, DicomCommandStatusType.InvalidAttributeValue, 0, 0, 0, 0, null); 
         break; 
   } 
} 

Add Move Images Code

Use the code below to implement the move images method which retrieves the specified dataset file from the database and performs a move action to send the file to the client.

C#
public void MoveImages(UserInfo userInfo, QueryLevel level) 
{ 
   string filter = ""; 
   StringCollection instanceIds; 
   DataView dv; 
 
   // Build filter according to query level in specified dataset 
   if (_Class == DicomUidType.PatientRootQueryFind) 
   { 
      filter = "PatientID = '" + Utils.GetStringValue(ds, DicomTag.PatientID) + "'"; 
   } 
 
   if (level == QueryLevel.Study || level == QueryLevel.Series || level == QueryLevel.Image) 
   { 
      if (filter.Length > 0) 
         filter += " AND "; 
 
      if (level != QueryLevel.Study) 
         filter += "StudyInstanceUID = '" + Utils.GetStringValue(ds, DicomTag.StudyInstanceUID) + "'"; 
      else 
      { 
         instanceIds = Utils.GetStringValues(ds, DicomTag.StudyInstanceUID); 
         for (int i = 0; i < instanceIds.Count; i++) 
         { 
            filter += "StudyInstanceUID ='" + instanceIds[i] + "'"; 
            if (i < instanceIds.Count - 1) 
               filter += " AND "; 
         } 
      } 
   } 
 
   if (level == QueryLevel.Series || level == QueryLevel.Image) 
   { 
      if (filter.Length > 0) 
         filter += " AND "; 
 
      if (level != QueryLevel.Series) 
         filter += "SeriesInstanceUID = '" + Utils.GetStringValue(ds, DicomTag.SeriesInstanceUID) + "'"; 
      else 
      { 
         instanceIds = Utils.GetStringValues(ds, DicomTag.SeriesInstanceUID); 
         for (int i = 0; i < instanceIds.Count; i++) 
         { 
            filter += "SeriesInstanceUID ='" + instanceIds[i] + "'"; 
            if (i < instanceIds.Count - 1) 
               filter += " AND "; 
         } 
      } 
   } 
 
   if (level == QueryLevel.Image) 
   { 
      if (filter.Length > 0) 
         filter += " AND "; 
 
      instanceIds = Utils.GetStringValues(ds, DicomTag.SOPInstanceUID); 
      for (int i = 0; i < instanceIds.Count; i++) 
      { 
         filter += "SOPInstanceUID ='" + instanceIds[i] + "'"; 
         if (i < instanceIds.Count - 1) 
            filter += " AND "; 
      } 
   } 
 
   // Retrieve record for specified dataset 
   server.MainForm.Log("DB QUERY: " + filter); 
   dv = server.MainForm.DicomData.FindRecords("Images", filter); 
   if (dv.Count == 0) 
   { 
      server.MainForm.Log("C-MOVE-REQUEST: No matching images found " + level); 
      client.SendCMoveResponse(_PresentationID, _MessageID, _Class, DicomCommandStatusType.Success, 0, 0, 0, 0, null); 
      return; 
   } 
 
   // Send the dataset to the client 
   try 
   { 
      // Configure the destination SCP using client connection information 
      DicomScp scp = new DicomScp(); 
      scp.AETitle = _MoveAETitle; 
      scp.Port = userInfo.Port; 
      scp.PeerAddress = IPAddress.Parse(userInfo.IPAddress); 
 
      // Configure a StoreSCU to send the dataset to the client 
      StoreScu store = new StoreScu(); 
      store.ImplementationClass = "1.2.840.114257.1123456"; 
      store.ImplementationVersionName = "1"; 
      store.ProtocolVersion = "1"; 
 
      store.AfterConnect += new AfterConnectDelegate(storeAfterConnect); 
      store.BeforeAssociateRequest += new BeforeAssociationRequestDelegate(storeBeforeAssociateRequest); 
      store.AfterAssociateRequest += new AfterAssociateRequestDelegate(storeAfterAssociateRequest); 
      store.BeforeCStore += new BeforeCStoreDelegate(storeBeforeCStoreRequest); 
      store.AfterCStore += new AfterCStoreDelegate(storeAfterCStoreRequest); 
      store.BeforeReleaseRequest += storeBeforeReleaseRequest; 
      store.AfterReleaseRequest += storeAfterReleaseRequest; 
 
      foreach (DataRowView drv in dv) 
      { 
         DataRow row = drv.Row; 
         store.Store(scp, row["ReferencedFile"].ToString()); 
      } 
      client.SendCMoveResponse(_PresentationID, _MessageID, _Class, DicomCommandStatusType.Success, 0, 0, 0, 0, null); 
   } 
   catch (Exception ex) 
   { 
      server.MainForm.Log("Store Error " + ex.Message); 
      client.SendCMoveResponse(_PresentationID, _MessageID, _Class, DicomCommandStatusType.Failure, 0, 0, 0, 0, null); 
      return; 
   } 
} 

Add the methods below to log the different phases of the move action.

C#
private void storeAfterConnect(object sender, AfterConnectEventArgs e) 
{ 
 
   if (e.Error == DicomExceptionCode.Success) 
      server.MainForm.Log("Connected successfully. Peer Address: " + e.Scp.PeerAddress.ToString() + " Peer Port: " + e.Scp.Port.ToString()); 
   else 
   { 
      server.MainForm.Log("Connect operation failed. Error code is: " + e.Error.ToString()); 
      client.SendCMoveResponse(_PresentationID, _MessageID, _Class, DicomCommandStatusType.Failure, 0, 0, 0, 0, null); 
   } 
} 
 
private void storeBeforeAssociateRequest(object sender, BeforeAssociateRequestEventArgs e) 
{ 
   server.MainForm.Log("Sending association request..."); 
} 
 
private void storeAfterAssociateRequest(object sender, AfterAssociateRequestEventArgs e) 
{ 
   StoreScu scu = sender as StoreScu; 
 
   if (!e.Rejected) 
   { 
      server.MainForm.Log("Received Associate Accept. Calling AE: " + e.Associate.Calling + " Called AE: " + e.Associate.Called); 
   } 
   else 
   { 
      server.MainForm.Log("Received Associate Reject!"); 
      client.SendCMoveResponse(_PresentationID, _MessageID, _Class, DicomCommandStatusType.Failure, 0, 0, 0, 0, null); 
   } 
} 
 
private void storeBeforeCStoreRequest(object sender, BeforeCStoreEventArgs e) 
{ 
   if (e.Error != null) 
   { 
      e.Skip = SkipMethod.AllFiles; 
      client.SendCMoveResponse(_PresentationID, _MessageID, _Class, DicomCommandStatusType.Failure, 0, 0, 0, 0, null); 
   } 
   else 
   { 
      server.MainForm.Log("Sending C-STORE Request..."); 
      server.MainForm.Log("Processing file: " + e.FileInfo.FullName); 
   } 
} 
 
private void storeAfterCStoreRequest(object sender, AfterCStoreEventArgs e) 
{ 
   if (e.Status != DicomCommandStatusType.Failure) 
   { 
      server.MainForm.Log("**** Storage completed successfully ****"); 
   } 
   else 
   { 
      server.MainForm.Log("**** Status code is: " + e.Status.ToString()); 
      client.SendCMoveResponse(_PresentationID, _MessageID, _Class, DicomCommandStatusType.Failure, 0, 0, 0, 0, null); 
   } 
} 
 
private void storeBeforeReleaseRequest(object sender, EventArgs e) 
{ 
   server.MainForm.Log("Sending release request..."); 
} 
 
private void storeAfterReleaseRequest(object sender, EventArgs e) 
{ 
   server.MainForm.Log("Receiving release response"); 
} 

Add the Root Query Abstract Syntaxes to the Inclusion List

Before running the project, open the Utilities/Server.cs file and add the syntaxes to support find actions.

C#
private void BuildInclusionList() 
{ 
   _UidInclusionList.Add(DicomUidType.VerificationClass); 
 
   // Store Transfer Syntax 
   _UidInclusionList.Add(DicomUidType.JPEG2000LosslessOnly); // Image1.dcm 
   _UidInclusionList.Add(DicomUidType.JPEGLosslessNonhier14B); // Image2.dcm 
   _UidInclusionList.Add(DicomUidType.ImplicitVRLittleEndian); // Image3.dcm 
 
   // Store Abstract Syntax 
   _UidInclusionList.Add(DicomUidType.EnhancedMRImageStorage); // Image1.dcm 
   _UidInclusionList.Add(DicomUidType.DXImageStoragePresentation); // Image2.dcm 
   _UidInclusionList.Add(DicomUidType.MRImageStorage); // Image3.dcm 
 
   // Find Abstract Syntax 
   _UidInclusionList.Add(DicomUidType.PatientRootQueryFind); 
   _UidInclusionList.Add(DicomUidType.StudyRootQueryFind); 
 
   // Move Abstract Syntax 
   _UidInclusionList.Add(DicomUidType.PatientRootQueryMove); 
   _UidInclusionList.Add(DicomUidType.StudyRootQueryMove); 
} 

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 a PACS client to send a C-MOVE request to the server. Upon successful validation, the request is processed allowing the specific datasets to be retrieved from the database and sent as a response to the client.

Run the LEADTOOLS Dicom Client Demo - C# demo to test the server, this demo is found here: <INSTALL_DIR>\LEADTOOLS23\Bin\DotNet4\x64\DicomClientDemo_Original.exe

Use the Options button to configure the connection then click the Search button to find all DICOM dataset stored in the PACS Server. Double-click on a study, then double-click on a series to perform a move action for the dataset file which will then be displayed in the client's viewer when the move finishes.

Note: See the Handle Store Requests in a PACS Server tutorial for how to add DICOM dataset files to the PACS server database before attempting a C-MOVE request.

Dicom Client Move
PACS Server C-MOVE

Wrap-up

This tutorial showed how to implement handling of C-MOVE requests for a PACS Server by setting a DicomScp class for the client and using a StoreScu class to perform a store operation towards the client using the retrieved dataset file.

See Also

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

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