Using ITypeDiscoveryService in a typeconverter

While working on Versatile DataSources, I wanted to provide a new design mode feature on properties where you specify the name of a class type. For example, EntityDAODataSource has EntityTypeName and DataContextTypeName.

Adding a dropdownlist to a property in the Visual Studio Properties Editor is easy: Create a TypeConverter class, return true in the GetStandardValuesSupported() method, and implement the GetStandardValues() function to return a list of strings.

The trick is to return a list of known types, and only those added by the user (as opposed to those in the GAC and third party assemblies). That’s where the System.ComponentModel.Design.ITypeDiscoveryService interface applies. It lets you retrieve a complete list of all available types currently compiled, from design mode code like your GetStandardValues() method.

Before going into details about the TypeConverter I’ve written, lets look at the heart of it, the static function GetAppClasses() because it consumes the ITypeDiscoveryService and does something pretty weird to work around a VS problem.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Windows.Forms;
using System.Globalization;

public static List<Type> GetAppClasses(IServiceProvider pServiceProvider, TypeTester pTestTypeFunc)
{
ITypeDiscoveryService vService = vService = (ITypeDiscoveryService)pServiceProvider.GetService(typeof(ITypeDiscoveryService));
if (vService != null)
{
for (int vTry = 0; vTry < 4; vTry++)
{
List<Type> vTypes = new List<Type>();
ICollection vAllTypes = vService.GetTypes(typeof(object), true); // excludes referenced assemblies in the GAC
foreach (Type vType in vAllTypes)
{
if (vType.IsEnum || vType.IsInterface || vType.IsAbstract || vType.IsNotPublic)
continue;
try
{
if ((pTestTypeFunc != null) && !pTestTypeFunc(vType))
continue;
}
catch (Exception)
{ // VS will crash if an exception is thrown while being called from GetStandardValues
// This case protects against the PeterBlum.DataSources.dll from being not found, which throws a FileNotFoundException
continue;
}
if (fIgnoredNamespaces.IsMatch(vType.FullName))
continue;

if (typeof(System.Web.HttpApplication).IsAssignableFrom(vType)) // eliminate Global.asax
continue;

vTypes.Add(vType);
} // foreach

if (vTypes.Count > 0)
return vTypes;

// the number of types was 0. This may happen when the ITypeDiscoveryService
// first attempts to load because not all assemblies have been prepared
// (moved into the Users\[login]\AppData\Local\Microsoft\VisualStudio\10.0\ProjectAssemblies\ folder)
// Delay .5 second and try again
// This delay does NOT impact the web app. Its only here in design mode.
System.Threading.Thread.Sleep(500);
} // for vI

} // if vService
return new List<Type>();
} // GetAppClasses
static Regex fIgnoredNamespaces = new Regex(@"^(System)|(Microsoft)|(PeterBlum)|(Telerik)|(AjaxControlToolkit)\.");
<summary>
Function passed to GetAppTypes to determine if a Type should be included or excluded
</summary>
<returns>When true, the type should be included.</returns>
public delegate bool TypeTester(Type pTypeToEvaluate);

 Here’s a look into the function:

  • It is passed an IServiceProvider. This is assigned the context object passed to GetStandardValues().
  • It is passed an optional function using the delegate TypeTester. This allows you to test individual types to see if they should be kept. For my DataContextTypeName property, I needed to keep those that came from a specific base class.
  • Notice the loop and the nasty Thread.Sleep(500). This is where we are hacking around VS. The comment above Sleep(500) explains the issue, that assemblies containing our types will be found in the ProjectAssemblies folder, but only after VS does some compiling. The first call the GetTypes() seems to get compiling started. I give it 8 attempts to finish compiling before giving up and returning an empty list. I find this works only some of the time. A restart of VS and rebuild usually gets it to work. (Perhaps this is a bug in VS2010 Beta 2?)
  • If you are concerned about Thread.Sleep(), remember that this code only should run in design mode.
  • The regular expression, fIgnoredNamespaces, should identify the namespaces of types to ignore, including those of third party libraries.

Here’s the rest of the class, BaseTypeNameConverter, which is an abstract class.

abstract public class BaseTypeNameTypeConverter : TypeConverter
{
public override bool CanConvertTo(ITypeDescriptorContext pContext, Type pDestinationType)
{
if (pDestinationType == typeof(string))
return true;
return base.CanConvertTo(pContext, pDestinationType);
}
public override object ConvertTo(ITypeDescriptorContext pContext,
CultureInfo pCulture, object pValue, Type pDestinationType)
{
if (pDestinationType == typeof(string))
{
return pValue;
}
return base.ConvertTo(pContext, pCulture, pValue, pDestinationType);
}

public override bool CanConvertFrom(ITypeDescriptorContext pContext, Type pSourceType)
{
if (pSourceType == typeof(string))
return true;
return base.CanConvertFrom(pContext, pSourceType);
}

public override object ConvertFrom(ITypeDescriptorContext pContext, CultureInfo pCulture, object pValue)
{
if (pValue is string)
return pValue;
return base.ConvertFrom(pContext, pCulture, pValue);
}

public override bool GetStandardValuesSupported(ITypeDescriptorContext pContext)
{
return true;
}

<summary>
Exposes all public class types, except those removed by SupportedType() returning false.
</summary>
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext pContext)
{
Cursor.Current = Cursors.WaitCursor;
List<Type> vTypes = GetAppClasses(pContext, SupportedType);
List<string> vList = new List<string>();
foreach (Type vType in vTypes)
vList.Add(vType.FullName);

vList.Sort();
return new StandardValuesCollection(vList);

}
<summary>
Used by GetStandardValues to determine if a type should be kept or excluded.
</summary>
<param name="pTypeToEvaluate">The type to evaluate.</param>
<returns>When true, the type should be kept.</returns>
abstract public bool SupportedType(Type pTypeToEvaluate);

public static List<Type> GetAppClasses(IServiceProvider pServiceProvider, TypeTester pTestTypeFunc)
{
// see above
}
}

For the DataContextTypeName property, I implemented this class:

public class DataContextTypeNameTypeConverter : BaseTypeNameTypeConverter
{

public override bool SupportedType(Type pTypeToEvaluate)
{
if (typeof(IQueryableDataContext).IsAssignableFrom(pTypeToEvaluate) ||
typeof(IEntityFactoryDataContext).IsAssignableFrom(pTypeToEvaluate) ||
typeof(System.Data.Linq.DataContext).IsAssignableFrom(pTypeToEvaluate) ||
typeof(System.Data.Objects.ObjectContext).IsAssignableFrom(pTypeToEvaluate))
{
return true; // Test for PeterBlum.DataSources is done in ancestor

// eliminate those in the EntityDAODataSource assembly
return !pTypeToEvaluate.Assembly.FullName.StartsWith("PeterBlum.DataSources,");

}
else
return false;
}

} // DataContextTypeConverter

Here is the actual DataContextTypeName property consuming DataContextTypeNameTypeConverter:

[TypeConverter("PeterBlum.DataSources.Designer.DataContextTypeNameTypeConverter, PeterBlum.DataSources.Designer, Version=0.0.1.0, Culture=neutral, PublicKeyToken=e17e5d6d5148a0a9")] 
public virtual string DataContextTypeName
{
get { return GetView().DataContextTypeName; }
set { GetView().DataContextTypeName = value; }
}

If your TypeConverter class is in the same assembly as your control, use this format:

[TypeConverter(typeof(DataContextTypeNameTypeConverter))]

No Comments