Udostępnij za pośrednictwem


Building Data Adapters [Jason He]

Introduction to Adapters

 

If two types (A and B) have to version independently, they cannot have static references between them. Instead an adapter is needed to sits between A and B. It enables them to communicate without a direct reference and thus providing a versioning boundary.

 

An adapter normally takes one type in the constructor and exposes the methods as another type. Here is an example

    public interface A { void methodA();}

    public interface B { void methodB();}

    public class Adapter : A

    {

        B interfaceB;

        public Adapter(B ib) { interfaceB = ib; }

        public void methodA() { interfaceB.methodB(); }

}

This adapter looks like an A. It actually implements A by calling functions in B. Since A, B and Adapters are normally in different assembly, A and B don’t have a versioning dependency. For example, if we got a newer version B2, we don’t need to update A assembly to reflect the changes. We only need to update adapters to reroute the function calls. Updating Adapters is easier to do than updating Host and AddIn.

 

When to use data adapters

When you define a rich object model for your host and add-ins to communicate over you need to decide how you want to communicate over the isolation boundary. In our model, for each data type that goes across the isolation boundary we need one Contract, two views and two Adapters. Typically those defining the object model and building the pipeline will have one assembly for each pipeline component (Contract, Add-In/Host View, Add-In Side/Host Side Adapter) and group the pieces for all types in their system into that single assembly.

 

Calculator scenario

Let us consider the calculator sample that Jesse and Jack wrote on the MSDN magazine. In that sample, all the types passing between Host and AddIn are primitive types like “string” “double”. What happens if Host has a complex data type that it expects an AddIn to access? Let us assume we want to treat operator and operands as one object and pass it to the calculator AddIn. In this case, our contract becomes a little more complex.

 

We’ll walk through the important code in the sample below, but for the full example in VS Solution form see the attached file.

 

Here is the contract

    [AddInContract]

    public interface ICalculatorContract: IContract

    {

         string GetAvailableOperations();

        double Operate(IOperateContract operate);

    }

    public interface IOperateContract : IContract

    {

        string GetOperation();

        double GetA();

        double GetB();

    }

 

We create an operate contract which exposes all the data that calculator AddIn needs to access. This contract will be passed from Host side to the AddIn side.

 

Here is our AddInBase and AddIn

 

    [AddInBaseAttribute]

    public abstract class Calculator {

       

        public abstract string Operations {

            get;

        }

       

        public abstract double Operate(Operate operate);

    }  

   

    public abstract class Operate {

       

        public abstract string Operation {

            get;

        }

       

        public abstract double A {

            get;

        }

       

        public abstract double B {

            get;

        }

    }

    [AddIn("CalculatorV3AddIn")]

    public class CalulatorV3AddIn : Calculator

    {

        public override string Operations

        {

            get { return "+ - * / **"; }

        }

     public override double Operate(Operate operation)

        {

            switch (operation.Operation)

            {

                case "+":

                    return operation.A + operation.B;

                case "-":

                    return operation.A - operation.B;

                case "*":

                    return operation.A * operation.B;

                case "/":

                    return operation.A / operation.B;

                case "**":

                    return Math.Pow(operation.A, operation.B);

                default:

                    throw new InvalidOperationException("This add-in does not support: " + operation.Operation);

            }

        }

    }

The AddIn code looks quite similar to the old calculator sample. The biggest difference is AddIn now takes an Operate as a parameter. From AddIn side, this is how the Host side operate object looks like. This Operate is defined in AddInBase so that both AddIn and AddInAdapter can use it.

 

Here is the AddInAdapter

 

    [AddInAdapterAttribute]

    public class CalculatorViewToContractAddInAdapter :

ContractBase, ICalculatorContract {

       

        private Calculators.Extensibility.Calculator _view;

       

        public CalculatorViewToContractAddInAdapter(Calculator view) {

            _view = view;

        }

       

        public virtual string GetAvailableOperations() {

            return _view.Operations;

        }

       

        public virtual double Operate(IOperateContract operate) {

            return _view.Operate(new OperateContractToViewAddInAdapter(operate));

        }

       

    }

    public class OperateContractToViewAddInAdapter : Operate {

       

        private IOperateContract _contract;

       

        private System.AddIn.Pipeline.LifetimeTokenHandle _handle;

       

        public OperateContractToViewAddInAdapter(IOperateContract contract) {

            _contract = contract;

            _handle = new System.AddIn.Pipeline.LifetimeTokenHandle(contract);

        }

       

        public override string Operation {

            get {

                return _contract.GetOperation();

            }

        }

       

        public override double A {

            get {

                return _contract.GetA();

            }

        }

       

        public override double B {

            get {

                return _contract.GetB();

            }

        }

       

    }

 

We intentionally created a long type name here OperateContractToViewAddInAdapter. The name explains its role quite well. It is an Adapter. It is an Adapter at AddIn side. It is an Adapter to adapt a contract to a view. When we look at the AddIn code, we may ask the question who created Operate. Now we know that AddInAdapter created an instance of OperateContractToViewAddInAdapter which derives from Operate and passed it to AddIn. The contract we are adapting here is IOperateContract. The next question will be who created IOperateContract at the host side.

 

Let’s take a look at HostAdapter code.

    [HostAdapterAttribute]

    public class CalculatorContractToViewHostAdapter :Calculator {

       

        private ICalculatorContract _contract;

       

        private LifetimeTokenHandle _handle;

       

        public CalculatorContractToViewHostAdapter(ICalculatorContract contract) {

            _contract = contract;

            _handle = new LifetimeTokenHandle(contract);

        }

       

     public override string Operations {

            get {

                return _contract.GetAvailableOperations();

            }

        }

       

        public override double Operate(Operate operate) {

            return _contract.Operate(new OperateViewToContractHostAdapter(operate));

        }

       

    }

    public class OperateViewToContractHostAdapter : ContractBase, IOperateContract {

       

        private Calculators.Extensibility.Operate _view;

       

        public OperateViewToContractHostAdapter(Operate view) {

            _view = view;

        }

       

        public virtual string GetOperation() {

            return _view.Operation;

        }

       

        public virtual double GetA() {

            return _view.A;

        }

       

        public virtual double GetB() {

            return _view.B;

        }

       

    }

 

We got another adapter at host side called OperateViewToContractHostAdapter. It adapts a host side view to a contract. The host side view looks like

 

    public abstract class Calculator {

       

        public abstract string Operations {

            get;

        }

       

        public abstract double Operate(Operate operate);

    }

    public abstract class Operate {

       

        public abstract string Operation {

           get;

        }

       

        public abstract double A {

            get;

        }

       

        public abstract double B {

            get;

        }

    }

 

 

 To expose the operate object, we created 1 contract, 2 views and 2 adapters. It stacks up like below

 

Host

Operate

OperateViewToContractHostAdapter

IOperateContract

OperateContractToViewAddInAdapter

Operate

AddIn

 

As you can see our system for adapting data is same one we use for adapting the add-ins themselves: the only difference is that the system activates the add-ins and their adapters directly, rather than relying on other pieces in the pipeline to do so. We may notice from above AddIn and Host adapters that both adapters can adapt IContract to View and View to IContract. It is symmetric. It is a common misconception that adapters adapt one way not the other inside of a given pipeline component. Sometime, both ViewToContract and ContractToView adapters are required in one assembly if the data type is used as both in and out parameters of a contract.

 

We may also notice that both views and adapters have similar code. It is actually not a crazy idea to merge two views into one assembly and two adapters into one assembly. As long as we put the assemblies at the right location, our AddIn API will be able to discovery and activate the pipeline as expected.

 

In summary, we have illustrated adapters that can link views and IContract at both Host and AddIn side. For each data type that we want to expose from Host to AddIn or from AddIn to Host, we need to define one Contract, two views and two adapters. Adapter is the key to break the versioning dependency.

ExtensibleCalculator.zip

Comments

  • Anonymous
    March 16, 2007
    I have one blog posted on our team site http://blogs.msdn.com/clraddins/archive/2007/03/15/adapters.aspx
  • Anonymous
    March 21, 2007
    We've had a few questions internally and externally from people who were having problems running some
  • Anonymous
    April 11, 2007
    The comment has been removed
  • Anonymous
    June 28, 2007
    I posted a blog about AddIn Adapters here . There is a question. Can we have AddInAdapters inside HostAdapter?
  • Anonymous
    July 04, 2007
    The comment has been removed
  • Anonymous
    July 06, 2007
    The requirments that we have are as follows:The pipeline segments must have the following structure:[pipelineRoot]AddInViews[pipelineRoot]AddInSideAdapters[pipelineRoot]HostSideAdapters[pipelineRoot]Contracts[addinRoot][AddIn A][addinRoot][AddIn B][addinRoot][AddIn C][pipelineRoot] and [addinRoot] can be anywhere on the machine or on a network path and can be the same location (as is the case, for simplicity) in most of our samples.FindAddIns will automatically look for an "AddIns" folder under the pipeline root and use that if it finds it. You can also pass as many additional [addinRoot] locations as you want to your call to FindAddIns and it will look for add-ins there as well.--Jesse
  • Anonymous
    August 01, 2007
    The solution contains a reference to System.AddIn.Pipeline.LifetimeTokenHandle and it is unable to resolve... I am running VS2008 Beta 2...Has the name changed?Thanks!Pavan
  • Anonymous
    August 01, 2007
    Ok Fixed the issue with LifetimeTokenHandle...looks like it has been renamed to ContractHandle !!
  • Anonymous
    August 29, 2007
    System.AddIn in Framework V3.5 is about building hosts that load plug-in Add-Ins with functionality around...
  • Anonymous
    October 25, 2007
    What happens if IOperateContract has complex types? Do you then need to write more adapters and more views? This seems overly complex. I think there is way too much emphasis here on versioning control and less emphasis on ease of use.
  • Anonymous
    January 01, 2008
    I tend to agree with Joe S.: While the overall System.AddIn approach of isolating host and addins as far as possible is fabulous, the amount of view and adapter code which needs to be generated for any non-trivial host object model (or other protocol between host and addin) seems almost prohibitive to me, at least in scenarios we've looked at.VSTA, which builds on System.AddIn, makes it easier to wrap host object models because it provides tools to generate the pipeline code almost automatically. That addresses the complexity problem for some scenarios, but at a price: You lose some of the versioning independence (VSTA allows to add methods and objects to the host object model, but you cannot change existing methods or delete them), and the automatically generated pipeline is very slow. We've measured factors between 200 and 1400 relative to non-generic contracts.In our case, the performance problem is a showstopper, so we're back to System.AddIn's non-generic contracts. However, when I start counting how many view and adapter classes would be required for the object model we're considering, I'm pretty sure I don't want to be the one who writes or maintains all that code...Claus