Creating an InfoPath Custom Control using C# and .NET

In Office 2003 Service Pack 1 (SP1), new features and the InfoPathControl and InfoPathControlSite objects were added to InfoPath to support the development of custom controls implemented using Microsoft ActiveX technology. ActiveX controls are developed using unmanaged COM code, typically written in C++ or Visual Basic 6.0. With the increasing popularity of the Microsoft .NET Framework, many developers are switching to working with managed code, such as C# and Visual Basic .NET. As an alternative to using unmanaged code to create a custom ActiveX control, you can create a user control (a control derived from the .NET Windows Forms UserControl class) that will function as an InfoPath custom control by using COM Interop. COM Interop provides interoperability between the .NET assembly compiled for your user control and the unmanaged code of InfoPath. Although Windows Forms user controls are not natively supported by InfoPath SP1, once you handle the details required for COM Interop and security, writing your .NET code is really easy. In this blog entry, we'll give you an overview of how to get a .NET control to work in InfoPath. This entry won’t go over the basics of writing .NET user controls, so if you are not familiar with user controls, you will need to find that information before the discussion in this blog entry will be useful to you. The basic steps for creating a user control are described in Walkthrough: Authoring a User Control with Visual C#. For additional details on creating custom ActiveX controls for InfoPath, you can view the Creating Custom Controls for InfoPath SP1 webcast, and work with the ActiveX Controls in InfoPath 2003 hands-on training.
 
Adding the Right Attributes
To get a .NET user control to work with unmanaged code, certain attributes will need to be added to its source code. In the ActiveX world, all controls have GUIDs (globally unique identifiers). To do this in .NET, you will need to use the GuidAttribute attribute to specify a GUID. This attribute is part of the System.Runtime.InteropServices namespace.

COM interop will expose methods and properties based on the setting of the ClassInterface attribute. This attribute must be set to ClassInterfaceType.AutoDual in order for the control to work correctly in InfoPath.

[ClassInterface(ClassInterfaceType.AutoDual)]

But you will still need to expose the Value and Enabled properties of your control to InfoPath. To do this, you declare an interface for these properties that you will implement within the user control class. The InterfaceType attribute on this interface should be set to InterfaceIsDual as shown in the following line of code:

[InterfaceType(ComInterfaceType.InterfaceIsDual)]

This attribute setting will expose all of the properties on this interface.

Additionally, for the property notifications to fire you will need to specify COM dispatch identifiers (DISPIDs) for the Enabled and Value properties of your control. To assign DISPIDs using COM interop, you use the DispId attribute, which is also part of the System.Runtime.InteropServices namespace.
 
Putting all of this together, the skeletal code for your control should look something like the following example:

[InterfaceType(ComInterfaceType.InterfaceIsDual)]public interface ICOMControl {  [DispId(UserControl1.DISPID_VALUE)]  string Value { get; set; }   [DispId(UserControl1.DISPID_ENABLED)]  bool Enabled { get; set; }}

[Guid("6E6F8C69-2643-4f45-B111-3ABE034940D9")]
[ClassInterface(ClassInterfaceType.None)]
public class UserControl1 : System.Windows.Forms.UserControl, ICOMControl
{
   ...
}

Note that ICOMControl is the name we’ve given to the interface we defined which must be implemented within the control class to expose the Value and Enabled properties. The user control class derives from this interface, provides the actual implementation of the get and set methods of the properties, and specifies the values for the DISPID_VALUE and DISPID_ENABLED constants. See the full listing later in this blog entry for more details. 
 
The IPropertyNotifySink Interface
The COM IPropertyNotifySink interface is required for InfoPath to know when to update the XML field which is bound to the ActiveX control. Property notifications should be fired by the control for this to happen. .NET user controls do not have an equivalent interface that will work in COM Interop, but you can work around this by importing the unmanaged IPropertyNotifySink interface and then writing your own implementation of it in managed code. This is accomplished by using the ComImport and InterfaceType attributes as shown in the following example.

[ComImport][Guid("9BFBBC02-EFF1-101A-84ED-00AA00341D07")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]public interface IPropertyNotifySink{   int OnChanged(int dispId);   [PreserveSig]   int OnRequestEdit(int dispId);}

In addition to this code, you need to a create a delegate for the two events of your control class. The delegate should look like this:

public delegate int PropertyNotifySinkHandler(int dispId);

And your events should look like this:

public event PropertyNotifySinkHandler OnChanged;
public event PropertyNotifySinkHandler OnRequestEdit;

You also need to specify that the imported IPropertyNotifySink interface is exposed as a source of COM events. You do this by adding the ComSourceInterfaces attribute to your control's class. The attribute should look like this:

[ComSourceInterfaces(typeof(IPropertyNotifySink))]

And finally, when implementing the Value and Enabled properties of the control, don't forget to fire the OnChanged event when the Value property is changed.
 
Satisfying Security

Custom controls written with a .NET language still have the same security restrictions as unmanaged ActiveX controls used in InfoPath: the .CAB file for the control must be signed with a digital signature, and the IObjectSafety interface must be implemented on the control. The IObjectSafety interface is an unmanaged interface but can still be implemented if you import and rewrite the interface in .NET. This is similar to what we did for the IPropertyNotifySink interface above:

[ComImport][Guid("CB5BDC81-93C1-11CF-8F20-00805F2CD064")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]interface IObjectSafety{   [PreserveSig]   int GetInterfaceSafetyOptions(ref Guid riid, out int pdwSupportedOptions, out int pdwEnabledOptions);                           [PreserveSig]   int SetInterfaceSafetyOptions(ref Guid riid, int dwOptionSetMask, int dwEnabledOptions);}

The user control class must derive from the IObjectSafety interface and implement the GetInterfaceSafetyOptions and SetInterfaceSafetyOptions methods. See the complete listing in the following section for details on how to do this.

Coding Checklist
We've gone over quite a few things that need to be done to create a .NET user control that works with InfoPath. Below is a checklist of the things you should have already done:

  • Add the Guid attribute to your control class
  • Add the ComSourceInterfaces attribute to your control class
  • Set the control’s ClassInterface attribute to ClassInterfaceType.None
  • Declare an interface for the Value and Enabled properties of your control, setting the InterfaceType attribute to ComInterfaceType.InterfaceIsDual
  • Import and implement the COM IPropertyNotifySink interface
  • Import and implement the COM IObjectSafety interface

The following listing provides the code behind a simple .NET user control that contains a read-only TextBox control that can be bound to a field in an InfoPath form.

using System;using System.Collections;using System.ComponentModel;using System.Drawing;using System.Data;using System.Windows.Forms;using System.Runtime.InteropServices;

namespace WindowsControlLibrary1
{
   /// <summary>
   /// Summary description for UserControl1.
   /// </summary>

   [InterfaceType(ComInterfaceType.InterfaceIsDual)]
   public interface ICOMControl
   {
      [DispId(UserControl1.DISPID_VALUE)]
      string Value { get; set; }

      [DispId(UserControl1.DISPID_ENABLED)]
      bool Enabled { get; set; }
   }

   [ComImport]
   [Guid("CB5BDC81-93C1-11CF-8F20-00805F2CD064")]
   [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
   interface IObjectSafety
   {
      [PreserveSig]
      int GetInterfaceSafetyOptions(ref Guid riid, out int pdwSupportedOptions, out int pdwEnabledOptions);
                       
      [PreserveSig]
      int SetInterfaceSafetyOptions(ref Guid riid, int dwOptionSetMask, int dwEnabledOptions);
   }

   [ComImport]
   [Guid("9BFBBC02-EFF1-101A-84ED-00AA00341D07")]
   [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
   public interface IPropertyNotifySink
   {
      [PreserveSig]
      int OnChanged(int dispId);
 
      [PreserveSig]
      int OnRequestEdit(int dispId);
   }

   public delegate int PropertyNotifySinkHandler(int dispId);

   [Guid("1FEE489F-A555-4408-8FBF-3F69F8C57A43")]
   [ClassInterface(ClassInterfaceType.None)]
   [ComSourceInterfaces(typeof(IPropertyNotifySink))]
   public class UserControl1 : System.Windows.Forms.UserControl, ICOMControl, IObjectSafety
   {
      public event PropertyNotifySinkHandler OnChanged;
      public event PropertyNotifySinkHandler OnRequestEdit;

      private System.Windows.Forms.TextBox textBox1;
      /// <summary>
      /// Required designer variable.
      /// </summary>
      private System.ComponentModel.Container components = null;

      // Constants for implementation of the IObjectSafety interface.
      private const int INTERFACESAFE_FOR_UNTRUSTED_CALLER = 0x00000001;
      private const int INTERFACESAFE_FOR_UNTRUSTED_DATA = 0x00000002;
      private const int S_OK = 0;

      // Constants for DISPIDs of the Value and Enabled properties.
      internal const int DISPID_VALUE = 0;
      internal const int DISPID_ENABLED = 1;

      public UserControl1()
      {
         // This call is required by the Windows.Forms Form Designer.
         InitializeComponent();
         // TODO: Add any initialization after the InitComponent call
      }

      // Implementation of the IObjectSafety methods.
      int IObjectSafety.GetInterfaceSafetyOptions(ref Guid riid, out int pdwSupportedOptions, out int pdwEnabledOptions)
      {
         pdwSupportedOptions = INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA;
         pdwEnabledOptions = INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA;
         return S_OK;   // return S_OK
      }
 
      int IObjectSafety.SetInterfaceSafetyOptions(ref Guid riid, int dwOptionSetMask, int dwEnabledOptions)
      {
         return S_OK;   // return S_OK
      }

      protected int Fire_OnRequestEdit(int dispId)
      {
         if (this.OnRequestEdit != null)
            return this.OnRequestEdit(dispId);
         else return 0;
      }

      protected int Fire_OnChanged(int dispId)
      {
         if (this.OnChanged != null)
            return this.OnChanged(dispId);
         else return 0;
      }

      // Implementation of the Value property get and set methods.
      public string Value
      {
         get { return textBox1.Text; }
         set { textBox1.Text = value; }
      }

      /// <summary>
      /// Clean up any resources being used.
      /// </summary>
      protected override void Dispose( bool disposing )
      {
         if( disposing )
         {
            if( components != null )
               components.Dispose();
         }
         base.Dispose( disposing );
      }

      // Component Designer generated code goes here.
      ...

      private void textBox1_TextChanged(object sender, System.EventArgs e)
      {
         Fire_OnChanged( UserControl1.DISPID_VALUE );
      }
   }
}

Compiling a .NET User Control for COM Interop
To compile a .NET user control for COM Interop, follow these steps:

  1. In Visual Studio .NET 2003, open the Solution Explorer and right-click on the project item.
  2. Click Properties to display the properties pane for the project.
  3. Under Configuration Properties, click Build.
  4. Under Outputs, change Register for COM Interop to True.

The next time you compile, your user control will be available to unmanaged code.
 
Adding a .NET User Control to the InfoPath Controls Task Pane
When you add a new custom control using the InfoPath Add Custom Control Wizard, InfoPath will look only for controls that are in the "Controls" category. However, when .NET Controls are compiled, they are categorized as ".NET Controls", which InfoPath does not look for. To manually add a .NET control, you must create an .ICT file and store it in the C:Documents and SettingsusernameLocal SettingsApplication DataMicrosoftInfoPathControls folder. If you have not added any custom controls to InfoPath's Controls task pane, you will need to create this Controls folder yourself. The easiest way to create an .ICT file is to add an ActiveX control to the Controls task pane in InfoPath, and then copy the .ICT file which is created automatically by InfoPath.
 
Getting a .NET User Control into a Self-Registering CAB file
InfoPath requires custom controls to be packaged in CAB files for deployment. The normal way for .NET Controls to be deployed is to add a Setup Project to the solution in Visual Studio, which will produce an MSI file when the solution is compiled. An MSI file is required for a .NET control to be registered for COM Interop. The MSI file that is generated by the Setup Project can then be packaged in a CAB file, but CAB files do not automatically run and register MSI files. You can work around this by creating an .INF file similar to the following example which has hooks to execute the .MSI file after the CAB file is extracted:

[Setup Hooks]hook1=hook1

[hook1]
run=msiexec.exe /i %EXTRACT_DIR%MSI.msi /qn

[Version]
; This section is required for compatibility on both Windows 95 and Windows NT.
Signature="$CHICAGO$"
AdvancedInf=2.0

Note   There is a bug in the .NET Framework that will cause any Label controls used in a .NET user control to throw GDI+ exceptions. You can workaround this by using GDI+ to draw your own text, or you can use a TextBox control instead and set its ReadOnly property to True.