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 (23 KB) |
Platform | Windows WinForms C# Application |
IDE | Visual Studio 2019 |
Development License | Download LEADTOOLS |
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.
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:
Leadtools.Dicom.Pacs.Scp
Leadtools.Dicom.Pacs.Scu
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
Leadtools.Dicom.Scu.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, 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:
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();
}
Open the Utilities\DicomAction.cs
file and add the following to the using
block.
using Leadtools.Dicom.Scu;
using Leadtools.Dicom.Scu.Common;
using System.Net;
Add the MoveRequest
to the enumeration directly after the namespace declaration.
public enum ProcessType
{
EchoRequest,
StoreRequest,
FindRequest,
MoveRequest
}
Modify the DoAction
method to parse the MoveRequest
process.
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.
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;
}
}
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.
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.
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");
}
Before running the project, open the Utilities/Server.cs
file and add the syntaxes to support find actions.
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 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>\LEADTOOLS22\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.
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.