Custom Devices (Plugins)
The Custom Code Control is an excellent tool for including custom functionality within Connexion - whether that be one-off devices, event hooks, or enterprise integration. It is not the appropriate tool for creating devices which will be used in multiple channels, or, when you do not want code exposed within the management application.
Fully compiled devices (like the built-in Connexion devices) should be used in most cases. A fully compiled device (or custom device / plugin) has a number of advantages:
Use a fully featured IDE like Visual Studio or Rider for development
Use source control
Proper versioning which enables many advanced deployment scenarios
No exposed code
A full configuration user interface (with optional validation of user input)
A custom icon with optional overlays (for status)
Access to pre-built functionality such as scheduling, client-server communications, common user controls, etc.
Working with Connexion (or Remote Integrator) usually involves creating custom devices related to your business needs. This page (and child pages) will introduce you to custom devices and provide a tutorial on creating your own.
Visual Studio Project (including communications and status tutorial):
Create Visual Studio Solution
For this tutorial, we’re going to work directly in Visual Studio to create our device. Connexion has a menu option to create a Visual Studio solution for you. Right click in the title bar area and choose the Create Custom Device in Visual Studio option.
You may be prompted to run the management application elevated. This is required for Visual Studio automation to work.
You will be prompted to enter some information about your device:
Clicking the Create Visual Studio Project button will create the project files. When prompted, choose to open the solution. You should have a solution which appears similar to this:
Let’s take a quick look at each of the classes in this project:
Device: Contains the API for processing messages. This is where the bulk of your logic will go.DeviceConfiguration: Defines any properties (and classes) which will store your configuration.DeviceFactory: Contains helper methods related to device creation, including the user interface, icon, and category.DeviceUI: The Xaml and code-behind for your user interface.DeviceUIViewModel: The model which supports data-binding your user interface. This class contains any logic related to your UI.
Before we can build our device, let’s create a target database to insert data into. We’re using a Sql Server database named HL7FilerTotorial. We’ve given the the Connexion service account (by default __cxne_user__) read/write access to this database.
Create a Patient table using the following script:
USE [HL7FilerTutorial]
GO
CREATE TABLE [dbo].[Patient](
[ID] [bigint] IDENTITY(1,1) NOT NULL,
[FirstName] [nvarchar](200) NULL,
[LastName] [nvarchar](200) NULL,
[Address] [nvarchar](max) NULL,
[City] [nvarchar](100) NULL,
[State] [nvarchar](100) NULL,
[Zip] [nvarchar](10) NULL
)
GOLet’s start by filling out our message processing logic. This is going to take HL7v2 messages, extract a few fields, and store them into a database. We’re going to ignore the Start and Stop methods for now and just put our logic into the ProcessMessageAsync method. We’ve added a reference to the Microsoft.Data.SqlClient Nuget package (which supersedes the System.Data.SqlClient namespace).
Copy and paste the following code into your device class:
using Connexion.Core;
using Connexion.Core.HL7;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace HL7FilerTutorial
{
[DevicePlugin("HL7FilerTutorial", "Writes some HL7v2 fields to a database", DeviceDefinitionFlags.None, typeof(HL7Message), typeof(object), typeof(HL7FilerTutorialFactory))]
public class HL7FilerTutorial : BaseDevice<HL7FilerTutorialConfiguration>
{
public HL7FilerTutorial(Guid deviceKey, IMessageChannelDevice messageChannelDevice)
: base(deviceKey, messageChannelDevice)
{
}
public override void Start()
{
}
public override void Stop()
{
}
public override bool DeviceChangesMessage => false;
public override async Task ProcessMessageAsync(IMessageContext context, CancellationToken token)
{
var hl7 = context.GetAsHL7Message(); // get an HL7 message from the queue
// get the PID segment, or throw if there isn't one
var pidSegment = hl7.Segments.OfType<PID>().FirstOrDefault() ?? throw new Exception("PID segment not found in the HL7 message.");
// pull some fields from the PID segment
var firstName = pidSegment.PatientName_05.First.GivenName_02.Value;
var lastName = pidSegment.PatientName_05.First.FamilyName_01.Surname_01.Value;
var address = pidSegment.PatientAddress_11.First.StreetAddress_01.ToString();
var city = pidSegment.PatientAddress_11.First.City_03.Value;
var state = pidSegment.PatientAddress_11.First.StateOrProvince_04.Value;
var zip = pidSegment.PatientAddress_11.First.ZipOrPostalCode_05.Value;
using (var connection = new Microsoft.Data.SqlClient.SqlConnection("Data Source=localhost;Initial Catalog=HL7FilerTutorial;Integrated Security=True;TrustServerCertificate=True"))
{
await connection.OpenAsync(token);
using (var command = connection.CreateCommand())
{
command.CommandText = "INSERT INTO Patient (FirstName, LastName, Address, City, State, Zip) VALUES (@FirstName, @LastName, @Address, @City, @State, @Zip)";
command.Parameters.AddWithValue("@FirstName", firstName);
command.Parameters.AddWithValue("@LastName", lastName);
command.Parameters.AddWithValue("@Address", address);
command.Parameters.AddWithValue("@City", city);
command.Parameters.AddWithValue("@State", state);
command.Parameters.AddWithValue("@Zip", zip);
await command.ExecuteNonQueryAsync(token);
}
}
}
public override void OnError(IMessageContext context, ErrorEventArgs args)
{
if (args.Exception is Microsoft.Data.SqlClient.SqlException sqlException)
{
args.ShouldRetry = true;
if (args.TotalRetries % 5 == 0)
Logger.Write(EventSeverity.Error, $"Filing failed: {sqlException.Message}");
args.SleepTime = TimeSpan.FromSeconds(args.TotalRetries > 2 ? 30 : 5);
}
}
}
}
Now compile your project so we can import it into Connexion.
To import a compiled device, you’ll use the Plugin Manager dialog. Click the plugin icon in the top-right toolbar, and then click the Import from file(s) button in the lower left corner.
You will be prompted to choose one or more assembly files. Navigate to the compiler output path and select the HL7FilerTotorial.dll file.
You should select the device assembly file, plus any dependant assemblies (references) when importing. Connexion version 17 already includes the Microsoft.Data.SqlClient assemblies, but if you’re using version 16, you should select all the assemblies in this directory.
After selecting the assembly (or all assemblies for version 16), click the Open button. The import summary dialog will be displayed.
Click the Import & Close button, and notice this device is now listed in the Plugin Manager dialog.
In newer versions of Visual Studio, deterministic compilation is disabled. In this case, the Build Date displayed is incorrect until deterministic compilation is enabled and the assembly rebuilt. This has no effect on the device behavior, and can be safely ignored.
Close the Plugin Manager dialog and create a new channel. This channel should start with an HL7 Inbound Device and end with an HL7FilerTutorial Device. Note the newly created device is located in the Other category.
After saving the channel it should look as follows:
Go ahead and start the channel and transfer a message via port 11000 (via a tool like HL7Spy). If everything is working as expected, you should see the Q count increment and then the P count increment.
If errors exist, you should either see the E count increment, or, the error count overlayed on top of your device icon increment.
Look at the Patient table and verify you have data.
Congratulations! You’ve created a simple compiled device that extracts data from an HL7v2 message and inserts it into a database.
Logging
Let’s include some simple logging with our device. We’re going to return the ID of each inserted record and add a Processing History event. Processing History is metadata which is associated with a specific message. You can also log to the MessageChannel, which surfaces log information associated with your device (and not related to a specific message). Learn more about logging.
Update the data access section of your device code to the following:
using (var connection = new Microsoft.Data.SqlClient.SqlConnection("Data Source=localhost;Initial Catalog=HL7FilerTutorial;Integrated Security=True;TrustServerCertificate=True"))
{
await connection.OpenAsync(token);
using (var command = connection.CreateCommand())
{
command.CommandText = "INSERT INTO Patient (FirstName, LastName, Address, City, State, Zip) OUTPUT Inserted.ID VALUES (@FirstName, @LastName, @Address, @City, @State, @Zip)";
command.Parameters.AddWithValue("@FirstName", firstName);
command.Parameters.AddWithValue("@LastName", lastName);
command.Parameters.AddWithValue("@Address", address);
command.Parameters.AddWithValue("@City", city);
command.Parameters.AddWithValue("@State", state);
command.Parameters.AddWithValue("@Zip", zip);
var patientId = (long)await command.ExecuteScalarAsync(token);
// write to the Processing History
context.WriteEvent(EventSeverity.Info, $"Patient {firstName} {lastName} with ID {patientId} has been successfully filed to the database.");
}
}Notice we’ve made 3 changes:
Update query to output the inserted ID (line 6).
Change to the
ExecuteScalerAsyncmethod (line 13)Log to the Processing History (line 16)
In order to deploy this change, we need to do three things.
Change the version number of your assembly. Typically this means navigating to the
AssemblyInfoclass and changing a digit.Recompile your project.
Right-click your device and choose the
Re-import Device and Update Alloption
Push another message through your channel, and then find that message in the queue device. You should now have Processing History associated with that message.
Device Icon & Category
Let’s update the look of our device icon and place it into a more appropriate category. These are both done within the DeviceFactory class. Copy and paste the following code into your class:
using Connexion.Core;
using System.Reflection;
using System.Windows;
using System.Windows.Media.Imaging;
namespace HL7FilerTutorial
{
public class HL7FilerTutorialFactory : BaseDeviceFactory<HL7FilerTutorialConfiguration>
{
public override FrameworkElement GetUserInterface(IDeviceUIParams deviceUiParams)
{
return new HL7FilerTutorialUI(Configuration, deviceUiParams);
}
public override BitmapImage DeviceImage
{
get
{
var image = new BitmapImage();
image.BeginInit();
image.StreamSource = Assembly.GetExecutingAssembly().GetManifestResourceStream("HL7FilerTutorial.TutorialIcon.png");
image.EndInit();
return image;
}
}
public override string[] Categories
{
get { return new[] { "Tutorial" }; }
}
}
}
We’ve made two changes here:
We’ve changed the
Categoriesproperty to return the categoryTutorial. Our device should now be shown here when adding it to a channel.We’ve changed the name of the file returned by the
DeviceImageproperty. For this to work, we must add this file to our project and set it as anEmbedded Resource. Here is the file if you wish follow along:
This is what the class looks like in Visual Studio:
Change the version number of your assembly (in the AssemblyInfo class) and then recompile and re-import your device. You should now have a new icon in your channel
We recommend using vector graphics in version 17 and above. See the page on using vector graphics instead of bitmaps for more information.
Device UI
The next step in our tutorial will be to give our device a proper user interface. In this case, the only item which a user can modify will be the database connection string. To start, we’ll make a simple textbox where the user can enter the connection string. Then, we’ll change the textbox for a control which is much better.
The first step is to define our configuration class. This class contains all the configuration properties which a user can set. Let’s define a simple string called ConnectionString. Copy and paste this into your DeviceConfiguration class. Note the use of RaisePropertyChanged. All properties must raise this event for the Channel’s save button to become active.
using Connexion.Core;
using System.Runtime.Serialization;
namespace HL7FilerTutorial
{
[DataContract(IsReference = true)]
public class HL7FilerTutorialConfiguration : NotifyBase
{
private string m_ConnectionString;
[DataMember]
public string ConnectionString
{
get { return m_ConnectionString; }
set
{
if (m_ConnectionString != value)
{
m_ConnectionString = value;
RaisePropertyChanged(); // you must call RaisePropertyChanged for the UI to reflect property updates
}
}
}
}
}Next, let’s update our Xaml file to include a textbox and bind it to our configuration property. Copy and paste the following into your DeviceUI.xaml file:
<UserControl x:Class="HL7FilerTutorial.HL7FilerTutorialUI"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:core="clr-namespace:Connexion.Core;assembly=Connexion.Core"
xmlns:hl7filertutorial="clr-namespace:HL7FilerTutorial"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
d:DataContext="{d:DesignInstance hl7filertutorial:HL7FilerTutorialUiViewModel}"
Padding="6">
<Grid Margin="0,6,0,0"
VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="Connection String:"
VerticalAlignment="Center" />
<TextBox Text="{Binding Configuration.ConnectionString, UpdateSourceTrigger=PropertyChanged}"
VerticalContentAlignment="Center"
Height="30"
Margin="10,0,0,0"
Grid.Column="1"/>
</Grid>
</UserControl>Before we recompile our device, we must reference the ConnectionString property within our Device class.
Update the data access portion of your device class to the following. Note the change to remove the hard-coded connection string from line 1 and replace it with a reference to our new configuration property.
using (var connection = new Microsoft.Data.SqlClient.SqlConnection(Configuration.ConnectionString))
{
await connection.OpenAsync(token);
using (var command = connection.CreateCommand())
{
command.CommandText = "INSERT INTO Patient (FirstName, LastName, Address, City, State, Zip) OUTPUT Inserted.ID VALUES (@FirstName, @LastName, @Address, @City, @State, @Zip)";
command.Parameters.AddWithValue("@FirstName", firstName);
command.Parameters.AddWithValue("@LastName", lastName);
command.Parameters.AddWithValue("@Address", address);
command.Parameters.AddWithValue("@City", city);
command.Parameters.AddWithValue("@State", state);
command.Parameters.AddWithValue("@Zip", zip);
var patientId = (long)await command.ExecuteScalarAsync(token);
// write to the Processing History
context.WriteEvent(EventSeverity.Info, $"Patient {firstName} {lastName} with ID {patientId} has been successfully filed to the database.");
}
}Recompile and re-import your device. You should now have a new user interface which looks similar to this. Type in a valid connection string, save the channel, and try processing another message.
Since the connection string may not be valid (as it’s user inputted), it would be a good idea to validate it within the Start method. We’ll leave that to you, but throwing an exception from the Start method will stop the channel from starting and log an error event.
Let’s replace the textbox with a control that is more appropriate (as you may have passwords in your connection string). The assembly Connexion.Core contains a DatabaseConnectionControl. You will need to add a reference to this assembly (you can find it in the Connexion install directory). We also publish these packages on our public Nuget server (see the downloads section).
First, we need to change our configuration class so it uses a DatabaseConnectionParameters type instead of a string.
using Connexion.Core;
using System.Runtime.Serialization;
namespace HL7FilerTutorial
{
[DataContract(IsReference = true)]
public class HL7FilerTutorialConfiguration : NotifyBase
{
private DatabaseConnectionParameters m_DatabaseConnection;
[DataMember]
public DatabaseConnectionParameters DatabaseConnection
{
get { return m_DatabaseConnection; }
set
{
if (m_DatabaseConnection != value)
{
m_DatabaseConnection = value;
m_DatabaseConnection.PropertyChanged += (o, e) => RaisePropertyChanged();
}
}
}
}
}
Next, we need to update our Xaml UI to use the new control. Copy and paste the following into your DeviceUI xaml file.
<UserControl x:Class="HL7FilerTutorial.HL7FilerTutorialUI"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:core="clr-namespace:Connexion.Core;assembly=Connexion.Core"
xmlns:hl7filertutorial="clr-namespace:HL7FilerTutorial"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
d:DataContext="{d:DesignInstance hl7filertutorial:HL7FilerTutorialUiViewModel}"
Padding="6">
<Grid Margin="0,6,0,0"
VerticalAlignment="Top">
<core:DatabaseConnectionControl ConnectionParameters="{Binding Configuration.DatabaseConnection}"
DeviceUIParameters="{Binding DeviceUiParams}"
Margin="10"/>
</Grid>
</UserControl>And finally, we need to update our device class to reference this new connection string property. Copy and paste this over the data access portion of your Device class:
var builder = Configuration.DatabaseConnection.GetProviderConnectionStringBuilder();
using (var connection = new Microsoft.Data.SqlClient.SqlConnection(builder.ConnectionString))
{
await connection.OpenAsync(token);
using (var command = connection.CreateCommand())
{
command.CommandText = "INSERT INTO Patient (FirstName, LastName, Address, City, State, Zip) OUTPUT Inserted.ID VALUES (@FirstName, @LastName, @Address, @City, @State, @Zip)";
command.Parameters.AddWithValue("@FirstName", firstName);
command.Parameters.AddWithValue("@LastName", lastName);
command.Parameters.AddWithValue("@Address", address);
command.Parameters.AddWithValue("@City", city);
command.Parameters.AddWithValue("@State", state);
command.Parameters.AddWithValue("@Zip", zip);
var patientId = (long)await command.ExecuteScalarAsync(token);
// write to the Processing History
context.WriteEvent(EventSeverity.Info, $"Patient {firstName} {lastName} with ID {patientId} has been successfully filed to the database.");
}
}Lines 1 and 2 use the new connection control properties.
Update your assembly version and recompile your project. Re-import the device, input the connection parameters, save the channel, and try processing some messages. Your channel should look similar to this:
Great! We now have a custom device with an appropriate icon as well as a user interface. This device can be versioned and deployed within both Connexion and Gateway platforms.
There is much more functionality available to device developers. Here are some more topics: