5

I have developed a MVC helper for generating display and editable tables (a jquery plugin is required to allow dynamic addition and deletion of rows with full postback in the editable tables) e.g.

@Htm.TableDisplayFor(m => m.MyCollection as ICollection)

which used in conjunction with attributes will include totals in the footer, add columns for view and edit links, render hyperlinks for complex type etc. e.g.

[TableColumn(IncludeTotals = true)]

I'm about to publish it on CodeProject but before doing so, would like to solve one issue. The helper first gets the ModelMetadata from the expression, checks that it implements ICollection, then gets the type in the collection (note the following snippet is from accepted answers on SO, but as explained below, is not entirely correct)

if (collection.GetType().IsGenericType)
{
  Type type = collection.GetType().GetGenericArguments()[0]

The type is used to generate ModelMetadata for the table header (there might not be any rows in the table) and each row in the table body (in case some items are inherited types which have additional properties and would otherwise screw up the column layout)

foreach (var item in collection)
{
  ModelMetadata itemMetadata = ModelMetadataProviders.Current
    .GetMetadataForType(() => item, type);

What I would like to be able to do is use IEnumerable rather than ICollection so that .ToList() does not need to be called on linq expressions.

In most cases IEnumerable works fine e.g.

IEnumerable items = MyCollection.Where(i => i....);

is OK because .GetGenericArguments() returns an array containing only one type. The problem is that '.GetGenericArguments()' on some queries returns 2 or more types and there seems to be no logical order. For example

IEnumerable items = MyCollection.OrderBy(i => i...);

returns [0] the type in the collection, and [1] the type used for ordering.

In this case .GetGenericArguments()[0] still works, but

MyCollection.Select(i => new AnotherItem()
{
  ID = i.ID,
  Name = 1.Name
}

returns [0] the type in the original collection and [1] the type of AnotherItem

So .GetGenericArguments()[1] is what I need to render the table for AnotherItem.

My question is, is there a reliable way using conditional statements to get the type I need to render the table?

From my tests so far, using .GetGenericArguments().Last() works in all cases except when using OrderBy() because the sort key is the last type.

A few things I've tried so far include ignoring types that are value types (as will often be the case with OrderBy(), but OrderBy() queries might use a string (which could be checked) or even worse, a class which overloads ==, < and > operators (in which case I would not be able to tell which is the correct type), and I have been unable to find a way to test if the collection implements IOrderedEnumerable.

  • So, if I understand it, you feed _some_ `IEnumerable` that's the result (typically) of some _arbitrary_ LINQ query. And you want to reliably get the `T` part of the `IEnumerable`? – Chris Sinclair May 15 '14 at 01:23
  • @Chris Yes, although it could be just IEnumerable (e.g. ArrayList) –  May 15 '14 at 01:30
  • 1
    If so, try iterating on the implemented interfaces, find the `IEnumerable` interface and extract the `T` part: `var type = items.GetType().GetInterfaces().Where(t => t.IsGenericType).Where(t => t.GetGenericTypeDefinition() == typeof(IEnumerable<>)).Single().GetGenericArguments()[0];` Note my use of `Single`: if the interface isn't implement it fails, or if it implements two (or more) different `IEnumerable` interfaces, then it fails. You can change it to use `FirstOrDefault` or `First` and handle it however you wish for those extra cases. – Chris Sinclair May 15 '14 at 01:31
  • 1
    Regarding your comment, `ArrayList` isn't typed. You _could_ extract the first item and call its `GetType()` method. But if it has no items, you're out of luck: impossible to tell. Also doesn't guarantee that you have a mixed set of objects, or want to use a base class rather than the derived class returned by `GetType()`. If you wish, if it doesn't implement `IEnumerable` (just `IEnumerable`) then you _could_ treat it as type `object`? – Chris Sinclair May 15 '14 at 01:33
  • @Chris From the unit tests so far, checking the implemented interfaces looks promising. –  May 15 '14 at 01:47

1 Answers1

6

Solved (using comments posted by Chris Sinclair)

private static Type GetCollectionType(IEnumerable collection)
{
  Type type = collection.GetType();
  if (type.IsGenericType)
  {
    Type[] types = type.GetGenericArguments();
    if (types.Length == 1)
    {
      return types[0];
    }
    else
    {
      // Could be null if implements two IEnumerable
      return type.GetInterfaces().Where(t => t.IsGenericType)
        .Where(t => t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
        .SingleOrDefault().GetGenericArguments()[0];
    }
  }
  else if (collection.GetType().IsArray)
  {
    return type.GetElementType();
  }
  // TODO: Who knows, but its probably not suitable to render in a table
  return null;
}
Community
  • 1
  • 1
  • Regarding your usage of `SingleOrDefault` and your comment that "Could be null if implements two IEnumerable"; this isn't true. `SingleOrDefault` will _throw an exception_ if there is more than one. Even if it did return `null` (which it will if it does _not_ implement any `IEnumerable` interfaces), you'll get a `NullReferenceException` directly afterward when you attempt to call `GetGenericArguments`. I recommend that you break up the query and perform more accurate checks. – Chris Sinclair May 15 '14 at 10:36
  • Also, I would personally prefer to do the interface checking first rather than defaulting to the first generic type parameter. While unlikely, it's still possible to have a class definition like `class MyStuff : IEnumerable`; most likely in this case, especially if you _iterate_ on them later (by using `foreach` or calling `GetEnumerator`) you would want to favour the `int` type (since that will be what is iterated) rather than the `Foo` type. – Chris Sinclair May 15 '14 at 10:38