EndNote Interprocess Communication Tutorial
This tutorial will show how to do Interprocess Communication by self hosting a service. This allows a process that is external to EndNote to call functions that are exposed by your plugin.
Environment Notes:
I am using Visual Studio 2010, Service Pack 1. This tutorial has only been tested with EndNote X4.
Creating the EndNote plugin:
Create a new Visual C++ -> Class Library Project. I titled mine “en_ipc_lib”.
Add the reference to System.ServiceModel and WindowsBase:
- Right click on the project
- Click references
- Click Add New References
- Go to the .NET tab
- Select System.ServiceModel
Repeat this procedure for WindowsBase.
Implement Init and Exit
Here we have a fairly standard set of Init and Exit functions. To start our service, you just need to grab an instance of the service and call CreateService(). Stopping the service works basically the same way.
/*
* Program: EndNote IPC Tutorial
* File: EntryExit.cpp
* Namespace: None
* Description:
*
*/
#pragma unmanaged
// platform
#include "stdafx.h"
// project
#include "RSPlugin.h"
#include "EntryExit.h"
#include "Callback.h"
#include "ServiceHost.h"
bool Init(CServiceRequest* p) {
if( p )
{
//Create the callback instance
gpIUIServiceNotifyCB = RS::CreateInstance<MyIUIServiceNotifyCallbackImp>();
//Install the menu item to the Tools menu
if( gpIUIServiceNotifyCB != NULL )
{
gCServiceRequest = p; //save the pointer to the CServiceRequest
IUIServicePtr pUI = (IUIService*) p->GetService(kGUI);
if( pUI != NULL)
{
if( (pUI->AddToolsMenuItem("IPC Plugin Test",gpIUIServiceNotifyCB)) == kServiceNoErr )
{
// Do whatever else needs to be done here!
}
else return false; //failed to add to the menu
}
else return false; //failed to get the kGUI service
}
else return false; //failed to create IUIServiceNotifyCallback instance
}
else return false;//p == NULL
//Instantiate our service
ServiceCreation^ serviceInstance = ServiceCreation::Instance;
serviceInstance->CreateService();
return true;
} //Init
bool Exit() {
ServiceCreation^ serviceInstance = ServiceCreation::Instance;
serviceInstance->DestroyService();
return false;
} // Exit
Write the code to host the service:
We’re using a singleton, so that we can easily access the service in our Init and Exit functions. (You can’t just declare a global object of managed type) Also, because of a bug/omission that seems to run rampant in most introductory guides to hosting services ( http://www.danrigsby.com/blog/index.php/2008/02/26/dont-wrap-wcf-service-hosts-or-clients-in-a-using-statement/) we’re using ServiceHost in a slightly different (better, safer) way than you’ll see in most other tutorials.
/*
* Program: EndNote IPC Tutorial
* File: ServiceHost.h
* Namespace: None
* Description:
*
*/
#include "MyService.h"
#include "IPCSynchronizationContext.h"
#pragma managed
using namespace System;
using namespace System::ServiceModel;
using namespace System::ServiceModel::Description;
using namespace IPCService;
ref class ServiceCreation
{
private:
ServiceHost^ host;
ServiceCreation(){}
ServiceCreation(const ServiceCreation%)
{
throw gcnew System::InvalidOperationException("ServiceCreation cannot be copy-constructed");
}
static ServiceCreation m_instance;
public:
static property ServiceCreation^ Instance
{
ServiceCreation^ get()
{
return %m_instance;
}
}
bool CreateService()
{
Uri^ baseAddress = gcnew Uri((String^)"http://localhost:8080/myservice");
String^ address = "net.pipe://localhost/myservice";
MyService^ myService = gcnew MyService();;
try
{
// Set the synchronization context to keep it in the same thread as EndNote
IPCSynchronizationContext^ synchronizationContext = gcnew IPCSynchronizationContext();
SynchronizationContext::SetSynchronizationContext(synchronizationContext);
// Create the ServiceHost.
host = gcnew ServiceHost(myService, baseAddress);
// Enable metadata publishing.
ServiceMetadataBehavior^ smb = gcnew ServiceMetadataBehavior();
smb->HttpGetEnabled = true;
host->Description->Behaviors->Add(smb);
//Add an endpoint to the service
host->AddServiceEndpoint(IPCService::IMyService::typeid,
gcnew NetNamedPipeBinding(),
address);
// Open the ServiceHost to start listening for messages.
host->Open();
return true;
}
catch (CommunicationException ^e)
{
if (host != nullptr)
{
host->Abort();
}
}
catch (TimeoutException ^e)
{
if (host != nullptr)
{
host->Abort();
}
}
catch (Exception ^e)
{
if (host != nullptr)
{
host->Abort();
}
throw;
}
return false;
}
bool DestroyService()
{
// Close the ServiceHost.
try{
host->Close();
return true;
}
catch (Exception ^e)
{
if (host != nullptr)
{
host->Abort();
}
}
return false;
}
};
Write the service itself:
Here, we simply write a function to get the API version and return it as a string. The abridged story is that each service is defined by a ServiceContract and can do one or more actions that are each defined be an OperationContract. The actual implementation of this is defined by a ServiceBehavior.
/*
* Program: EndNote IPC Tutorial
* File: MyService.h
* Namespace: MyService
* Description:
*
*/
#pragma once
#include <iostream>
#include <sstream>
#pragma managed
using namespace System;
using namespace System::Runtime::InteropServices;
using namespace System::ServiceModel;
using namespace System::ServiceModel::Description;
namespace IPCService {
[ServiceContract()]
public interface class IMyService {
[OperationContract()]
String^ getAPIVersion();
};
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode::Single, InstanceContextMode = InstanceContextMode::Single)]
public ref class MyService : public IMyService
{
public:
virtual String^ getAPIVersion() sealed
{
IVersionPtr pVersion = (IVersion*)gCServiceRequest->GetService(kVersion);
long version = pVersion->GetServiceVersion();
String^ result = gcnew String("Hello!");
std::ostringstream o;
o <<std:: hex << version;
return gcnew String(o.str().c_str());
}
};
}
Writing the Synchronization Context:
Often, WCF services are implemented with Windows Forms or Windows Presentation Foundation. There is a built in Synchronization context that can be used to force execution to happen in the UI thread. However, if as in our case, you do not use either of these, work will be dipatched to the default synchronization context, a thread pool. In order to lock it to the thread used by EndNote, we have to implement our own SynchronizationContext. This example is odd because we only need synchronous events. If you want asynchronous events, Post and Send will be different.
/*
* Program: EndNote IPC Tutorial
* File: IPCSynchronizationContext.h
* Namespace: None
* Description:
*
*/
#pragma once
#include <WinBase.h>
#pragma managed
using namespace System;
using namespace System::Threading;
using namespace System::Windows::Threading;
public ref class IPCSynchronizationContext : SynchronizationContext, IDisposable
{
static System::Windows::Threading::Dispatcher^ dispatcher = System::Windows::Threading::Dispatcher::CurrentDispatcher;
public:
virtual void Send(SendOrPostCallback^ d, Object^ state) override sealed
{
dispatcher->Invoke(d, state);
}
virtual void Post(SendOrPostCallback^ d, Object^ state) override sealed
{
dispatcher->Invoke(d, state);
}
~IPCSynchronizationContext() {};
};
Testing the plugin:
Build the project, copy the dll into one of the EndNote plugin directories, and start EndNote.
First, make sure that EndNote can load the plugin by checking that the IUIService Callback works. Select “IPC Plugin Test” from the Tools menu. A MessageBox saying “Hello World!” should appear.
Now, we are ready to test the service:
- Open EndNote if it is not already open.
- In Visual Studio, open a Visual Studio Command Prompt from the Tools menu.
- Run wcftestclient
- Go to file->Add Service
- Enter http://localhost:8080/myservice. You should see IMyService and getAPIVersion().
- The right side of the window should have a getAPIVersion tab. If not, double click on getAPIVersion.
- Press Invoke. If a confirmation window appears, press OK.
- In the Response window, you should now see the correct RSServices API version.
Creating the Project to consume the plugin
Create a new Visual C# -> Console Application Project. I titled mine “en_ipc_consumer”.
Add a reference to the service:
- Right click on the project and select “Add Service Reference”
- Set the Address to: http://localhost:8080/myservice
- Click go
- The service should appear. Select it.
- Change the namespace to MyServiceReference.
- Hit OK.
Visual Studio will now generate an app.config file that will look something like this:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<netNamedPipeBinding>
<binding name="NetNamedPipeBinding_IMyService" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
transactionFlow="false" transferMode="Buffered" transactionProtocol="OleTransactions"
hostNameComparisonMode="StrongWildcard" maxBufferPoolSize="524288"
maxBufferSize="65536" maxConnections="10" maxReceivedMessageSize="65536">
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<security mode="Transport">
<transport protectionLevel="EncryptAndSign" />
</security>
</binding>
</netNamedPipeBinding>
</bindings>
<client>
<endpoint address="net.pipe://localhost/myservice" binding="netNamedPipeBinding"
bindingConfiguration="NetNamedPipeBinding_IMyService" contract="MyServiceReference.IMyService"
name="NetNamedPipeBinding_IMyService">
<identity>
<userPrincipalName value="someuser@someplace.com" />
</identity>
</endpoint>
</client>
</system.serviceModel>
</configuration>
We are now ready to call the service. Change the main method in Program.cs to something like this:
static void Main(string[] args)
{
MyServiceReference.MyServiceClient sClient = new MyServiceReference.MyServiceClient("NetNamedPipeBinding_IMyService");
Console.WriteLine("API Version: " + sClient.getAPIVersion());
Console.ReadLine();
}
Make sure that the argument you pass to the MyServiceClient constructor matches the bindingConfiguration for the endpoint in your app.config:
<endpoint address="net.pipe://localhost/myservice" binding="netNamedPipeBinding"
bindingConfiguration="NetNamedPipeBinding_IMyService" contract="MyServiceReference.IMyService"
name="NetNamedPipeBinding_IMyService">
We’re done!
To test our service, first make sure that EndNote is open. Now, just hit f5 and a console window should pop up and return the version number of your RSServices API. To close the window, just press Enter.
There’s a little bit of code missing, but if you’re familiar with creating EndNote plugins, it should be relatively easy to fill in the blanks. If anyone has trouble getting this example to work, I can post the rest of the code.