Recently able to create some funky functionality using Managed Smart Tags (ISmartTagAction, ISmartTagAction2, ISmartTagRecognizer, ISmartTagRecognizer2) to provide communication to a Custom Task Pane that would in turn execute a search based on the context of the word.
Really basic message, simply the term to be searched and the provider to search on. Sounds simple enough to do and most likely a struct would do the trick. Not to be...
A simple line of code brought all my dreams down to nothing AppDomain.CurrentDomain.Id this indicated that the Smart Tags and the Custom Task Pane got loaded by word into different AppDomains, not nice when you want to make ‘em send messages across...
Decided to look into various options including the new IPC namespace in Framework 2.0 , but during my search I came across the idea of named pipes (http://en.wikipedia.org/wiki/Named_pipe) pretty tough to do, if only to send a message within the same process. However it gave me the opportunity to read a book on WCF and expand my knowledge slightly and it was surprisingly simple.
After deciding what I needed to communicate a set out to create a Data Contract that would communicate the data over my named pipes connection between the two AppDomains
[DataContract()]
public class SearchMessage
{
public SearchMessage(string term, string provider)
{
SearchTerm = term;
SearchProvider = provider;
}
public SearchMessage() { }
[DataMember()]
public string SearchTerm;
[DataMember()]
public string SearchProvider;
}
When using the [DataContract()]to declare the custom type fit for transport(Serializable) remember that the members needs to opt in rather than out as in standard Serialization, meaning that if you do not mark it as [DataMember()]it will not be visible to the receiver.
The other option is to use [MessageContract()] attribute that provides more granular control over the SOAP envelope and extends the attributes to include [MessageBody()]and [MessageHeader()] allowing control over the location of the data member. For the purpose of my sample [DataContract()] is sufficient.
Search Service
The following part of the puzzle is to provide a handling service for the data so that it could be put to use. For this we use simple interface driven development practice and we need the service to look like decorating it with the [ServiceContract(Namespace = "http://PeterVerster.co.uk/WCF")] attribute enabling us to declare methods (operations) using [OperationContract()]. The following code snippet depicts the interface:
[ServiceContract(Namespace = "http://PeterVerster.co.uk/WCF")]
public interface ISearchService
{
[OperationContract()]
void ExecuteSearch(SearchMessage searchMessage);
}
Next the simple part of implementting the interface:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, IncludeExceptionDetailInFaults = true)]
public class SearchService : ISearchService
{
public static event SearcRequestRxEventHandler SearcRequestRxEvent;
#region ISearchService Members
public void ExecuteSearch(SearchMessage searchMessage)
{
if (SearcRequestRxEvent != null)
SearcRequestRxEvent(this, new SearchRxEventArgs(searchMessage));
}
#endregion
}
A few notes on the attributes used, most imprtantly the InstanceContextMode.Single where we declare the service to have a single instance, in the case where this is an endpoint for multiple clients it could be useful to promote concurrency.
Notice that my implementation sparks an event used to notify listeners that we have received a message wrapped in a Custom Events Args Class SearchRxEventArgs.
Client
The client implementation does not need a whole lot of work, Basically where it is going EndpointAddress, what binding we are using , in this case NetNamedPipeBinding. Finally IChannelFactory providing it with a description of the service that can be expected when calling it from the client. In this case I provide it with the interface I created, and we derive a proxy from that to execute the operations (methods) against.
public sealed class SearchClient
{
private static readonly EndpointAddress m_address;
private static readonly NetNamedPipeBinding m_binding;
private static readonly IChannelFactory<ISearchService> m_channelFactory;
static SearchClient()
{
try
{
m_address = new EndpointAddress("net.pipe://localhost/Search/CTP");
m_binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.Transport);
m_channelFactory = new ChannelFactory<ISearchService>(m_binding);
}
catch
{
//TODO:Provide Implementation
}
}
public static void ExecuteSearch(string term, string provider)
{
ISearchService searchProxy = m_channelFactory.CreateChannel(m_address);
searchProxy.ExecuteSearch(new SearchMessage(term, provider));
}
}
Note that we treat the SearchMessage [DataContract()] like we would treat any custom type. In this case I have provided a constructor that can be used to inline the creation when receiving arguments in my ExecuteSearch method.
Host
The final steps in creating the communications application is to provide a host that would host the service we have defined. Similar to the client this is fairly easy:
public sealed class SearchHost
{
//TODO: Wrap these in static constructor to catch exceptions
private static readonly Uri baseAddress = new Uri("net.pipe://localhost/Search");
private static readonly Type serviceType = typeof(SearchService);
private static readonly NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.Transport);
private static readonly ServiceHost host = new ServiceHost(serviceType, baseAddress);
static SearchHost()
{
host.Description.Endpoints.Clear();
host.AddServiceEndpoint(typeof(ISearchService), binding, "CTP");
host.Open();
}
}
Few obvious things noted are the service type, again we define the binding as NetNamedPipeBinding. Note that the address specified does not provide the full location of the service but rather for a collection of services, when adding the end point we specify “CTP” as the service end point using AddServiceEndpoint.
Implementation
Now we have all the pieces hanging together we can go ahead and fire it all up. On the smart tag side things look like this :
public void InvokeVerb2(int VerbID, string ApplicationName, object Target, ISmartTagProperties Properties, string Text, string Xml, int LocaleID)
{
try
{
SearchClient.ExecuteSearch(new SearchMessage(Text , VerbID.ToString()));
}
catch(Exception ex)
{
//TODO:Provide implementation
}
finally
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(Target);
GC.Collect();
}
return;
}
On the Custom Task Pane side things look like this:
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
SearchService.SearcRequestRxEvent += new SearcRequestRxEventHandler(m_searchService_SearcRequestRxEvent);
}
void m_searchService_SearcRequestRxEvent(object o, SearchRxEventArgs e)
{
AddSearchPane(e.SearchRxMessage.SearchTerm);
}
The standard implementation pattern for adding a CTP is the following:
public void AddSearchPane(string searchWord)
{
lock (m_customTaskPaneSearch)
{
if (!m_searchPaneExists)
{
m_customTaskPaneSearch = this.CustomTaskPanes.Add(m_searchControl, m_controlCaption);
m_customTaskPaneSearch.DockPosition = Microsoft.Office.Core.MsoCTPDockPosition.msoCTPDockPositionRight;
m_customTaskPaneSearch.Visible = true;
m_searchPaneExists = true;
}
if (m_searchControl is ITabSearch)
(m_searchControl as ITabSearch).Search(searchWord);
if (!m_customTaskPaneSearch.Visible)
m_customTaskPaneSearch.Visible = true;
}
}
Conclusion
Including some quick reading the sample was working within a couple of hours overcoming the boundaries of AddDomains. It might not be production ready, but it is sure easy doing this kind of thing with Windows Communication Foundation. Have a good one...
NamedPipes.zip (2.61 KB)