One of the 3rd party CMSs that I frequently work with (Ektron) has a lot of legacy API code that uses Microsoft.VisualBasic.Collections (from hereon referred to as Collection) to pass data into the database. These API methods are slowly being replaced with strongly typed entity style methods, but the most stable and reliable methods use Collections.
There are many reasons to dislike Collections, but my top ones are:
- You can’t see the keys of the values inside – this often leads to Exception led logic
- Weakly Typed
What’s in the Box?
Not being able to see what keys are in a collection is a killer fault, but it’s not too hard to workaround. Since the Collection
class is simply a wrapper for a generic Dictionary
(and a couple of ArrayLists
– which I’m not particularly bothered about), we can simply use Reflection to crack open the shell and access the sweet Dictionary
goodness inside.
My preferred method for this is an Extension Method, but a static
method will work just as well:
private static FieldInfo KeyNodeHash = typeof(Microsoft.VisualBasic.Collection).GetField
("m_KeyedNodesHash", BindingFlags.NonPublic | BindingFlags.Instance |
BindingFlags.IgnoreCase);
public static IDictionary<string,> ToDictionary
(this Microsoft.VisualBasic.Collection collection)
{
if (collection == null) throw new ArgumentNullException("collection");
if (KeyNodeHash == null) throw new NotSupportedException
("Expected to find private field m_KeyedNodesHash within Collection implementation");
IDictionary internalDictionary = KeyNodeHash.GetValue(collection) as IDictionary;
if (internalDictionary == null) throw new NotSupportedException
("Expected private field m_KeyedNodesHash to implement IDictionary");
Dictionary<string,> mapped = new Dictionary<string,>();
foreach (string key in internalDictionary.Keys)
{
object value = internalDictionary[key];
if (value == null)
{
mapped[key] = null;
}
else
{
mapped[key] = value.GetType()
.GetField("m_Value", BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.IgnoreCase)
.GetValue(value);
}
}
return mapped;
}
Word of Warning: This technique relies on the internal implementation of the Collection
class remaining the same as in .NET 3.5. Future (or past) versions of .NET may need amending – Reflector is your friend.
To use the code, simply make sure the Extension’s hosting class (Non-Generic and static
) is included in the available namespaces for your code, and call:
Microsoft.VisualBasic.Collection coll = GetCollectionFromSomewhere();
IDictionary<string,> dict = coll.ToDictionary();
foreach (string key in dict.Keys)
{
System.Diagnostics.Debug.WriteLine(string.Format
(CultureInfo.InvariantCulture, "@{0} = '{1}'", key,dict[key]));
}
Casting Call
Getting the keys in the dictionary is good, but what about making the Collection
strongly typed? Not much I can do about that, I’m afraid. But I can create a simple(ish) mechanism to map my strongly typed entity (in this case, an entity is a simple class made up of methods… also called a DTO) to a collection (and vice-versa) without having to code masses of conversions.
The secret is to create a Custom Attribute which you can use to decorate your entities properties with the collection key value. We can then use a bit more reflection to automatically populate Collection
s correctly or create new entity objects from a Collection
.
The required Custom Attribute is pretty simple:
using System;
namespace MartinOnDotNet.Helpers.Ektron
{
[AttributeUsage(AttributeTargets.Property)]
public sealed class CollectionItemAttribute : System.Attribute
{
public CollectionItemAttribute(string collectionKey):
this(collectionKey,null,false)
{}
public CollectionItemAttribute(string collectionKey, object defaultValue)
: this(collectionKey, defaultValue, false)
{ }
public CollectionItemAttribute(string collectionKey,
object defaultValue, bool suppressIfNull)
{
CollectionKey = collectionKey;
DefaultValue = defaultValue;
SuppressItemIfNull = suppressIfNull;
}
public object DefaultValue { get; set; }
public string CollectionKey { get; set; }
public bool SuppressItemIfNull { get; set; }
}
}
Note the AttributeUsage attribute on the class which limits its scope to properties.
This can be added to your entities simply as:
[CollectionItem("MetaTypeName")]
public string Name { get; set; }
If you need to do some fancy type conversions on the item, then an internal
/private
property can be used:
public MetadataTagType TagType { get; set; }
[CollectionItem("MetaTagType")]
private long EkTagType
{
get
{
return (long)TagType;
}
set
{
TagType = (MetadataTagType)value;
}
}
The mapping magic is done using reflection (again with the Extension Methods!):
public static Microsoft.VisualBasic.Collection CreateCollection(this object entity)
{
if (entity == null) throw new ArgumentNullException("entity");
Microsoft.VisualBasic.Collection collection = new Microsoft.VisualBasic.Collection();
foreach (PropertyInfo pi in entity.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic |
BindingFlags.Public))
{
foreach (CollectionItemAttribute ia in pi.GetCustomAttributes
(typeof(CollectionItemAttribute), true)
.OfType<collectionitemattribute>().Take(1))
{
object value = pi.GetValue(entity, null);
if (value != null || !ia.SuppressItemIfNull)
{
value = value ?? ia.DefaultValue;
collection.Add(value, ia.CollectionKey, null, null);
}
}
}
return collection;
}
public static T CreateFromCollection
<t>(this Microsoft.VisualBasic.Collection collection) where T : class, new()
{
if (collection == null) throw new ArgumentNullException("collection");
T newT = new T();
foreach (PropertyInfo pi in typeof(T)
.GetProperties(BindingFlags.Instance |
BindingFlags.NonPublic | BindingFlags.Public))
{
foreach (CollectionItemAttribute ia in
pi.GetCustomAttributes(typeof(CollectionItemAttribute), true)
.OfType<collectionitemattribute>().Take(1))
{
if (collection.Contains(ia.CollectionKey))
{
pi.SetValue(newT, collection[ia.CollectionKey], null);
}
else
{
System.Diagnostics.Trace.TraceWarning
("Expected Collection to Contain key '{0}'", ia.CollectionKey);
}
}
}
return newT;
}
These thinly veiled factory methods can be called inline whenever the 3rd party API exposes (or requires) a Collection
object and converts it into a strongly typed entity with a minimal amount of mapping code:
public EktronMetadata GetMetadataType(long id, int cultureLcid)
{
global::Ektron.Cms.ContentAPI api = new global::Ektron.Cms.ContentAPI();
using (new ElevatedPermissionScope(api))
{
api.ContentLanguage = cultureLcid;
return api.EkContentRef.GetMetadataTypeByID(id)
.CreateFromCollection<ektronmetadata>();
}
}
Microsoft.VisualBasic.Collection menuCollection =
updatedMenu.CreateCollection();
api.EkContentRef.UpdateMenu(menuCollection);
Much neater.
I'm a lead developer for Freestyle Interactive Ltd where we create many wonderful websites built on Microsofts ASP.Net and Ektron CMS.
I've been developing .Net applications (both Windows and Web) since 2002.