Ask Kathleen

Extend Your Apps with External Add-ins

Take advantage of the System.AddIn namespace to handle logic external to your application; don't get caught by Excel's one-based indexes; and make LINQ extension methods work with ArrayLists.

Technologies mentioned in this article include VB.NET and C#.

Q Our sales staff promised one of our customers that it could customize how a particular cost calculation is done in our application. These calculations are too complicated to parse, and not appropriate for a rules engine. I don't want to introduce a service interface when we're running on a single machine and most of our clients don't need this feature. I'm worried about performance, particularly if we use a tool like Excel, which our customers might not have anyway. I'm also worried about security because we'll have no way to control what their tool does. And I'm afraid we'll block future changes if we have to coordinate updates with our customers' staff. How do you think I should solve this?

A There are a few approaches you can take to handling logic external to your application. There's a new solution for .NET 3.5 that I think fits your scenario best: the System.AddIn namespace lets you extend your app with plug-ins, also called add-ins. Before I explain further, let me bypass confusion by clarifying that the System.AddIn namespace is entirely unrelated to Visual Studio extensibility. It's a way to manage plug-ins or add-ins to your application, which is exactly what you want to do.

System.AddIn works well with add-ins running on the same machine as the application, and it doesn't require configuring and managing services. The add-in runs in a separate application domain. Security settings are per app domain, so you can lock down the add-in in a sandbox to alleviate security concerns. Also, you can unload code only by unloading the containing app domain, and add-ins might need to be unloaded for updates without stopping the host process, or if the add-in misbehaves.

One of the most important benefits of System.AddIn also leads to the biggest hurdle to getting started with it. System.AddIn is designed to work only with a pipeline model that facilitates later versioning. This pipeline is important to the long term viability of your add-in architecture, but it can be overwhelming at first to see seven -- and potentially as many as nine -- assemblies to manage your add-in (see Figure 1).

Your application accesses the host view by deriving from a base class or implementing an interface. When you call a method that implements the host view, the call is transformed by the host adapter to match the contract. As your add-in contract or host evolves, the host adapter can also provide customized translations between versions of your app as your contract or the host evolves so older versions of your host can call newer contracts, and vice versa. On the other side, the add-in communicates by deriving from a base class or implementing an interface within the add-in view and also interacts through an adapter. While complex, this approach allows the host, add-in, and contract to evolve individually while the adapters coordinate any required translation.

System.AddIn would be a real pain to use if you had to create all these assemblies yourself. Instead, you can generate the majority of the code for the first version of your add-in with a tool the CLR team provides on CodePlex called Pipeline Builder. You can later take control of the generated files as your add-in design evolves, and you need to provide custom adapter behavior. After you download and install Pipeline Builder, it appears in your Visual Studio Tools menu.

To use Pipeline Builder, create and build a project containing your contract and run Pipeline Builder to generate source code for four of the required assemblies. Then write the code for your host and add-in implementing the host and add-in views. Pipeline Builder generates its assemblies in C# at this time, but your contract, add-in, and host can be Visual Basic or C#. I'll show an example in VB.

A contract is nothing more than an interface that inherits from IContract and has the AddInContract attribute:

Imports System.AddIn.Contract
Imports System.AddIn.Pipeline

<AddInContract()> _
Public Interface ICostCalculation
   Inherits System.AddIn.Contract.IContract

   Function Calculate( _
      ByVal hours As Int32, _
      ByVal rate As Single) _
      As Single
End Interface

Running Pipleine Builder generates source code and creates the four assemblies depicted with green arrows in Figure 1. It also sets up the build output to be in the specific structure demanded by System.AddIn:

Pipeline Root (any name and location)
   AddIns
      < directory for each implementation>
   AddInSideAdapters
   AddInViews
   Contracts
   HostSideAdapters

Most of this structure is created when you run Pipeline Builder. This gives you three ICostCalculation interfaces: one in the Contract, another in the AddInView, and still another in the HostView assemblies. The host view can be anywhere the host can access it, often directly under the Pipeline root. Note that this is the build structure for the assemblies (DLLs), your source code can reside anywhere you'd like.

You create an add-in by implementing the interface of the add-in view and supplying an identifying attribute:

<AddIn( _
   "StandardCostCalculation", _
   Version:="1.0.0.0", _
   Description:= _
   "StandardCalculation")> _
Public Class StandardCalculation
   Implements ICostCalculation

   Public Function Calculate( _
      ByVal hours As Integer, _
      ByVal rate As Single) _
      As Single Implements _
      KadGen.CostCalculation. _
      ICostCalculation.Calculate
      Return hours * rate
   
   End Function
End Class

You must place the add-in DLL file in a subdirectory of the pipeline structure's AddIns directory. You can do this in the Project dialog by changing the Compile/Build output path in VB or the Build/OutputPath in C#. Your add-in assembly must reference the add-in view to access the add-in view interface. The pipeline builder requires that no copy of the add-in view assembly exist in the directory that contains the add-in DLL. You can accomplish this by setting the reference property Copy Local to False. In Visual Basic, you must first select Show All Files from the header of Solution Explorer.

To use an add-in, your application must know the path to the pipeline structure root and the name of the add-in implementation. You can get the pipeline root either by requiring a specific location such as the user's directory or by supplying the path as a configurable application setting. System.AddIn hosting can query the pipeline root directory for add-ins implementing a specific interface. I've isolated this behavior into a handful of generic utility routines (see Listing 1). You can retrieve a list of available add-ins to fill a combo box, or you can retrieve the add-in name from the application settings; what you do depends on whether you want to choose to involve the user in the selection process.

There are several ways to speed up System.AddIn performance in certain scenarios such as Windows Presentation Foundation (WPF), but it commonly takes around a half a second to initialize, including the process of rebuilding the store file that manages the pipeline and creating the AppDomain. Once this step is complete, the calculation takes around a half a millisecond to complete on my relatively slow laptop. The download sample supplies the add-in name through both a combo box and an application setting; it also includes the rough benchmarking code. Check out my blog for links to techniques for improving System.AddIn performance if this isn't fast enough for you.

There are a few easy mistakes you can make with System.AddIn. If you fail to include the AddInContract attribute, the contract isn't recognized when the add-in manager builds the store. If that's the only contract, you'll get a warning that no usable parts were found. Failing to set the add-in view reference in the add-in to "Copy Local = False," causes the add-in to fail with this error message:

"The assembly <assemblyName> should not be in location <addinDirectory>"

It's also easy to incorrectly think of the root as the location of the add-ins. It's actually the root that contains the entire pipeline, which is one level up from the location of the add-ins.

Q I'm trying to access an Excel spreadsheet, and I keep getting this error:

System.Runtime.InteropServices.COMException was unhandled
   ErrorCode=-2146827284
   Message="Exception from 
      HRESULT: 0x800A03EC"

Why doesn't this code work?

Dim excelApp = New Application()

Dim workbook = _
   excelApp.Workbooks.Open( _
   fileName, ReadOnly:=True, _
   CorruptLoad:=Microsoft.Office. _
   Interop.Excel.XlCorruptLoad. _
   xlRepairFile)

For Each sheet As Worksheet _
   In workbook.Sheets
   Dim column = 0
   Dim rowCount = _
      sheet.UsedRange.Rows.Count
   For i = 0 To rowCount - 1
      Dim test = TryCast( _
      sheet.Cells( _
      i, column), Range).Value
      
      If test IsNot Nothing Then
         Console.WriteLine( _
         test.ToString)
      End If
   Next
Next

A Excel uses one-based indexes, not zero-based indexes. Excel is notorious for giving meaningless error messages like the one you received. Just add one to the column and the iteration variables, and you'll be good to go:

For i = 1 To rowCount
   Dim test = TryCast( _
      sheet.Cells( _
      i, column), Range).Value
   If test IsNot Nothing Then
      Console.WriteLine( _
      test.ToString)
   End If
Next

Q I have some legacy Visual Basic code that uses ArrayList. We're moving slowly on the transition to generic lists, and I want to include some Language Integrated Query (LINQ) extension methods so I can illustrate how useful LINQ is; however, this snippet doesn't compile:

For Each item In aList.Where(Function(x) x = "apple")
   ' DoStuff
Next

The obvious question is this: Do the LINQ extension methods work with ArrayList?

A You can make LINQ work with ArrayList using one of two approaches. First, you can create a query, and then iterate across it. This works because of the explicit conversion:

Dim q = From x In aList _
   Where CStr(x) = "apple"
For Each item In q
   ' DoStuff
Next

Second, you can use the OfType operator if you want to force ArrayList to become an IEnumerable(Of T). The LINQ extension methods then become available:

For Each item In aList.OfType( _
   Of String).Where(Function(x) _
   x = "apple")
   ' DoStuff
Next

I'm assuming you have ArrayLists that contain a homogeneous string type because your example compares to a string. If you have heterogeneous types, you can use OfType to cast to Object, but then the lambda expression has to manage the indeterminate type.

About the Author

Kathleen is a consultant, author, trainer and speaker. She’s been a Microsoft MVP for 10 years and is an active member of the INETA Speaker’s Bureau where she receives high marks for her talks. She wrote "Code Generation in Microsoft .NET" (Apress) and often speaks at industry conferences and local user groups around the U.S. Kathleen is the founder and principal of GenDotNet and continues to research code generation and metadata as well as leveraging new technologies springing forth in .NET 3.5. Her passion is helping programmers be smarter in how they develop and consume the range of new technologies, but at the end of the day, she’s a coder writing applications just like you. Reach her at [email protected].

comments powered by Disqus

Featured

Subscribe on YouTube