VSTA and Generics

VSTA has been officially released now, in InfoPath in the 2007 Office System, and for use by third parties in the VS SDK. So we're getting our first batch of feature requests. Far and away the biggest request so far is generic support. Admittedly, generic support for VSTA hosts is limited. I know it's an excuse, but we had limited time and resources to get VSTA done in time for Office 2007, and we had to make some hard choices. One of the reasons generic support was considered "cut-able" was that with a small amount of work, the user (you) could add the support you needed back in. In other words, there is a workaround. In this blog post, I'm going to talk about how this is done.

So the good news is that the infrastructure supports, to a certain extent, generics. And there is always the flexibility of using custom contracts to support anything you want. I'm going to leave custom contracts to a future post; this post is all about doing the smallest amount of work necessary to minimally support generics in a couple common cases. I'm interested in hearing whether this meets your needs, or if there are more common cases that are not done so easily.

The bad news is that proxygen doesn't support generics at all. It just skips over them when it sees them. But that's one reason why proxygen emits source code. In my mind, anyway (not sure if everyone on the team would agree with this) proxygen was never intended to generate code that gets shipped as is. The code is there to be tweaked. In some cases it may be as simple as renaming some classes. I would expect that many, if not most, hosts will want to use targeted custom contracts for performance – and other – reasons; stay tuned for my next post. And if your OM contains generics, you'll have to write a little code to augment what we put out.

So I took the ShapeApp sample and started changing it. The first case I tried was events. The .NET Framework guidelines tell you to use EventHandler<T> to declare events. ShapeApp didn't use these of course, and, well, that's not too helpful to you, if you followed the guidelines. So I removed all of the custom delegates and changed all of the events to use either System.EventHandler or System.EventHandler<T> when they had custom event args. I also changed the EventArgs class to derive from System.EventArgs, also according to the guidelines. Seems like a bug to me that it wasn't this way in the first place. I did this on both sides, in the ShapeApp app itself and in the proxy assembly that the addins consume.

This worked very well. All I had to do was change the code that populates the TypeInfrastructureManager: I just changed the typeof declarations to use the qualified generic types, and events were done.

The next common case is collections. Again, the guidelines tell us to use the generic collection types when we define collections – and it is ton easier if you've done it this way. ShapeApp has two collections, the DrawingCollection and the ShapeCollection. The first thing I did was change the application's implementation of these collections. As you'll see, I removed a bunch of code from these by using the built-in Framework classes. I derived them both from System.Collections.ObjectModel.Collection<T>. In the DrawingCollection the only thing additional thing I had to do was to override the InsertItem method to set up the drawingNumber field correctly. Notice I made the DrawingCollection type itself internal, and everywhere it was exposed I changed it to System.Collections.Generic.IList<Drawing> just to make sure we were really making things generic.

I had to do a little more work in the ShapeCollection. First off, it supported indexing by string as well as integer. Built-in collections don't support this, of course. So I defined a new interface IShapeList that derives from IList<IShape>. My ShapeCollection class derives from System.Collections.ObjectModel.Collection<IShape> and also implements IShapeList. The old ShapeCollection also fired a couple internal events: ShapeAdded and ShapeRemoved. In order to fire these at the right time (the new generic version of ShapeAdded and the new System.EventHandler version of ShapeRemoved) I had to override InsertItem and RemoveItem. Again, ShapeCollection was changed to an internal type. In this case, in the places where ShapeCollection had been exposed before, I changed it to my new interface: IShapeList. But System.Collections.Generic.IList<IShape> will show up again.

I turned out that just updating the TypeInfrastructureManager population code didn't complete the story – you do need to update it, and in fact we'll take advantage of some additional functionality it supports, but it wasn't as simple as the events. The problem turned out to be that the type returned by GetEnumerator was IEnumerator<T> and not just IEnumerator.

I decided to create a custom adapter for these collections. I created a new generic class (hey, I'm all about generics, now) called CollectionAdapter<T>. I decided to derive CollectionAdapter<T> from RemoteObjectAdapter (rather than ContractAdapterBase). This means that addins would have the ability to late-bind to it in addition to the work we're doing here. I don't actually use this late-binding capability in this sample, but all of the other objects that use the built-in adapters support late-binding, so I wanted to be consistent. And I do plan on taking advantage of this in a future post.

Here is the code from CollectionAdapter<T> is at the bottom. As you can see, there are really two classes here. The EnumeratorAdapter<T> is necessary to support GetEnumerator, of course. The EnumeratorAdapter *does* derive from ContractAdapterBase, because it is only used as an enumerator and never late-bound. The CollectionAdapter wraps an IList<T> and implements IRemoteArgumentArrayListContract which is in System.AddIn.Contract.Collections. The implementation of the contract (and its base contracts) defers to the wrapped list, and where necessary unwraps the RemoteArguments to local instances of T, or wraps them up. The where T : class constraint on the Adapter classes is there so we can use the typeof(T).

OK, so we have an Adapter, how does it get created? Go back to the code in extension.cs and notice we handle the AdapterResolve event. The TypeInfrastructureManager (I'm just going to call it TIM from now on, tired of typing it out) has this hook just for this purpose; for the host to provide custom adapters. The implementation is simple enough, if the TIM asks for an adapter for an IList<Drawing> or an IList<IShape> we instantiate a CollectionAdapter<T> and return it.

But what about the other side? The generated ShapeCollection and DrawingCollection didn't work correctly, so I implemented some new ones. Unfortunately, on the proxy side, I couldn't use System.Collections.ObjectModel.Collection<T> as the base class. This is because I had to have custom enumerators (to call into the IRemoteArgumentEnumeratorContract) and System.Collections.ObjectModel.Collection<T> doesn't expose out the ability to override it. In fact, its Enumerator implementation is internal. So the proxies were a little more work but not much. I had them implement ICollection<T>. They also implement IProxy to identify them to the infrastructure as proxies. Here the implementation just defers to the contract, and when necessary uses the helpers to wrap and unwrap RemoteArguments. The DrawingCollection is implemented as ReadOnly. The ShapeCollection supports Adding and Removing. It also implements a string based indexer like the application's OM class. However, as you can see, this implementation just loops through the elements until it finds the right one. I doesn't use the IShapeList. Why? Because IShapeList is not a contract, and in order to call into it we'd need a custom contract. And I'm not covering custom contract's here. Of course, it would be *far* more efficient to do the looping locally, instead of retrieving each object, pulling across the AppDomain boundary and checking it there. An ideal place for a custom contract – guess I have more work to do ;).

On the proxy side, we handle the ProxyResolve event rather than the AdapterResolve event off of TIM. Makes sense. The code there is equally simple. I also changed the name of IShapeClass to Shape, just 'cause it makes more sense.

If you have macro or addin code written against another version of ShapeApp, it should still compile, but you *will* have to recompile since the types have changed.

Please let me know if this sample is helpful – or not, especially not.

//Collection Adapter

using System;

using System.Collections.Generic;

using System.Text;

using System.AddIn.Contract;

using Microsoft.VisualStudio.Tools.Applications;

using System.AddIn.Contract.Collections;

using System.Runtime.Remoting;

namespace Microsoft.VisualStudio.Tools.Applications.Samples.ShapeApp

{

internal class EnumeratorAdapter<T> : ContractAdapterBase, IRemoteArgumentEnumeratorContract

where T : class

{

internal EnumeratorAdapter(IEnumerator<T> wrappedEnumerator, TypeInfrastructureManager tim) :

base(tim)

{

this.wrappedEnumerator = wrappedEnumerator;

this.tim = tim;

}

private IEnumerator<T> wrappedEnumerator;

private TypeInfrastructureManager tim;

#region Base Class Overrides

protected override IContract QueryContract(string contractIdentifier)

{

if (String.Compare(contractIdentifier, typeof(IRemoteArgumentEnumeratorContract).AssemblyQualifiedName) == 0)

return (IRemoteArgumentEnumeratorContract)this;

return base.QueryContract(contractIdentifier);

}

protected override int GetRemoteHashCode()

{

return wrappedEnumerator.GetHashCode();

}

protected override bool RemoteEquals(IContract contract)

{

if (contract == null)

return false;

if (!RemotingServices.IsObjectOutOfAppDomain(contract))

{

EnumeratorAdapter<T> contractAdapter = contract as EnumeratorAdapter<T>;

if (contractAdapter == null)

return false;

return this.wrappedEnumerator.Equals(contractAdapter.wrappedEnumerator);

}

return false;

}

protected override string RemoteToString()

{

return this.wrappedEnumerator.ToString();

}

#endregion

#region IRemoteArgumentEnumeratorContract Members

RemoteArgument IRemoteArgumentEnumeratorContract.GetCurrent()

{

return new RemoteArgument( new RemoteObjectAdapter(typeof(T), wrappedEnumerator.Current, tim));

}

bool IRemoteArgumentEnumeratorContract.MoveNext()

{

return wrappedEnumerator.MoveNext();

}

void IRemoteArgumentEnumeratorContract.Reset()

{

wrappedEnumerator.Reset();

}

#endregion

}

internal class CollectionAdapter<T> : RemoteObjectAdapter, IRemoteArgumentArrayListContract

where T : class

{

internal CollectionAdapter(IList<T> wrappedList, TypeInfrastructureManager tim)

{

this.wrappedList = wrappedList;

this.tim = tim;

}

#region Base Class Overrides

protected override IContract QueryContract(string contractIdentifier)

{

if (String.Compare(contractIdentifier, typeof(IRemoteArgumentArrayListContract).AssemblyQualifiedName, StringComparison.Ordinal) == 0)

return (IRemoteArgumentArrayListContract)this;

if (String.Compare(contractIdentifier, typeof(IRemoteArgumentArrayContract).AssemblyQualifiedName, StringComparison.Ordinal) == 0)

return (IRemoteArgumentArrayContract)this;

if (String.Compare(contractIdentifier, typeof(IRemoteArgumentCollectionContract).AssemblyQualifiedName, StringComparison.Ordinal) == 0)

return (IRemoteArgumentCollectionContract)this;

if (String.Compare(contractIdentifier, typeof(IRemoteArgumentEnumerableContract).AssemblyQualifiedName, StringComparison.Ordinal) == 0)

return (IRemoteArgumentEnumerableContract)this;

return base.QueryContract(contractIdentifier);

}

#endregion

private IList<T> wrappedList;

private TypeInfrastructureManager tim;

#region IRemoteArgumentArrayListContract Members

void IRemoteArgumentArrayListContract.Add(System.AddIn.Contract.RemoteArgument newItem)

{

T localItem = TypeServices.ObjectFromRemoteArgument(newItem, typeof(T), tim) as T;

if (localItem == null)

throw new ArgumentException();

wrappedList.Add(localItem);

}

void IRemoteArgumentArrayListContract.Clear()

{

wrappedList.Clear();

}

void IRemoteArgumentArrayListContract.Insert(int index, System.AddIn.Contract.RemoteArgument item)

{

T localItem = TypeServices.ObjectFromRemoteArgument(item, typeof(T), tim) as T;

if (localItem == null)

throw new ArgumentException();

wrappedList.Insert(index, localItem);

}

void IRemoteArgumentArrayListContract.Remove(System.AddIn.Contract.RemoteArgument item)

{

T localItem = TypeServices.ObjectFromRemoteArgument(item, typeof(T), tim) as T;

if (localItem == null)

throw new ArgumentException();

wrappedList.Remove(localItem);

}

void IRemoteArgumentArrayListContract.RemoveAt(int index)

{

wrappedList.RemoveAt(index);

}

bool IRemoteArgumentArrayListContract.Contains(System.AddIn.Contract.RemoteArgument item)

{

T localItem = TypeServices.ObjectFromRemoteArgument(item, typeof(T), tim) as T;

if (localItem == null)

return false;

return wrappedList.Contains(localItem);

}

int IRemoteArgumentArrayListContract.IndexOf(System.AddIn.Contract.RemoteArgument item)

{

T localItem = TypeServices.ObjectFromRemoteArgument(item, typeof(T), tim) as T;

if (localItem == null)

throw new ArgumentException();

return wrappedList.IndexOf(localItem);

}

#endregion

#region IRemoteArgumentArrayContract Members

System.AddIn.Contract.RemoteArgument IRemoteArgumentArrayContract.GetItem(int index)

{

T localVal = wrappedList[index];

return new RemoteArgument(new RemoteObjectAdapter(typeof(T), localVal, tim));

}

void IRemoteArgumentArrayContract.SetItem(int index, System.AddIn.Contract.RemoteArgument value)

{

T localVal = TypeServices.ObjectFromRemoteArgument(value, typeof(T), tim) as T;

if (localVal == null)

throw new ArgumentException();

wrappedList[index] = localVal;

}

#endregion

#region IRemoteArgumentCollectionContract Members

int IRemoteArgumentCollectionContract.GetCount()

{

return wrappedList.Count;

}

#endregion

#region IRemoteArgumentEnumerableContract Members

IRemoteArgumentEnumeratorContract IRemoteArgumentEnumerableContract.GetEnumeratorContract()

{

return new EnumeratorAdapter<T>(this.wrappedList.GetEnumerator(), this.tim);

}

#endregion

}

}

ShapeAppAdvancedCSharpGenerics.zip