views:

292

answers:

3

I have the following class declaration:

public class EntityTag : BaseEntity, ITaggable

I have an Html helper method:

public static string TagCloud(this HtmlHelper html, IQueryable<ITaggable> taggables, 
  int numberOfStyleVariations, string divId)

This is my ASP.NET MVC ascx:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IQueryable<EDN.MVC.Models.EntityTag>>" %>
<%@Import Namespace="EDN.MVC.Helpers" %>
<%= Html.TagCloud(Model, 6, "entity-tags") %>

When I pass in an IQueryable collection to the ascx, I get this error:

Compiler Error Message: CS1928: 'System.Web.Mvc.HtmlHelper>' does not contain a definition for 'TagCloud' and the best extension method overload 'EDN.MVC.Helpers.EdnHelpers.TagCloud(System.Web.Mvc.HtmlHelper, System.Linq.IQueryable, int, string)' has some invalid arguments

If I try to explicitly convert the object collection with this:

    public static string TagCloud(this HtmlHelper html, IQueryable<Object> taggables, int numberOfStyleVariations, string divId)
    {
        var tags = new List<ITaggable>();
        foreach (var obj in taggables)
        {
            tags.Add(obj as ITaggable);
        }
        return TagCloud(html, tags.AsQueryable(), numberOfStyleVariations, divId);
    }

I get the same error - the values I'm passing in are not liked by the compiler.

Shouldn't my EntityTag class automatically be supported as IQueryable? What am I missing? It's got to be something obvious. (I hope.)

+1  A: 

Your EntityTag class is IQueryable, however the compiler doesn't know that your list of tags is actually a list of EntityTag objects, it only knows that it's a list of objects implementing ITaggable, which probably isn't IQueryable.

Pent Ploompuu
+2  A: 

C# 4.0 will support it. Search for "Covariance and Contravariance in C# 4"

deerchao
+5  A: 

Essentially, you're trying to pass an object of the non-generic type IQueryable to a method that accepts the generic IQueryable<ITaggable>, which the compiler cannot "match", resulting in the CS1928 (since the two types are, in fact, different).

In your overload that accepts an IQueryable<object> (which is already doing the necessary conversion to a generic list), you simply need to call the generic version of AsQueryable instead of the non-generic one, as such:

public static string TagCloud(this HtmlHelper html, IQueryable taggables, int numberOfStyleVariations, string divId)  
{  
    var tags = new List<ITaggable>();  
    foreach (var obj in taggables)  
    {  
        tags.Add(obj as ITaggable);  
    }  
    return TagCloud(html, tags.AsQueryable<ITaggable>(), numberOfStyleVariations, divId);  
}  

Allow me to add, as well, that IQueryable<T> derives from IQueryable, meaning that not all IQueryable objects are IQueryable<T>, thus making the conversion necessary. If the situation were reversed, i.e. your "real" helper method was defined to handle IQueryable objects, then you certainly would have no problem passing an IQueryable<T> to that method (since all IQueryable<T> objects are, in fact, IQueryable).

Per Craig Stuntz, a much more elegant solution using LINQ features: <%= Html.TagCloud(Model.Select(t => (ITaggable)t), 6, "entity-tags") %>. You can also use <%= Html.TagCloud(Model.Cast<ITaggable>(), 6, "entity-tags") %> if your queryable provider supports it.

SteveH
This is right regarding the cause (+1), but I'd use a simpler solution: `<%= Html.TagCloud(Model.Select(t => (ITaggable)t), 6, "entity-tags") %>`. You can also use `IEnumerable<T>.Cast()` if your queryable provider supports it.
Craig Stuntz
Thanks for the reply. Unfortunately, the second version is never selected by the compiler when the interface-based version is also declared, even if I cast in the ascx. It still strikes me as peculiar that the compiler can't determine that EntityTag has the ITaggable interface, though. To sum up, I'm still looking for a reasonable solution ...
John Kaster
Thanks, Craig -- that's the kind of elegant casting solution I was looking for since the compiler doesn't automatically cast for me. That worked. If you convert your comment to an answer I'll accept it as the solution.
John Kaster
I verified your original methodology works in .NET 4.0, due to support for covariance and contravariance in generics (deerchao was right). See http://msdn.microsoft.com/en-us/library/dd799517(VS.100).aspx for additional info.My "old-school" .NET 2.0-style suggestion does in fact compile and function properly (i.e. the manual method), but I much prefer Craig's LINQ-assisted Select/Cast "auto-convert" method. So essentially what we have here are 3 valid solutions to your problem: one each for .NET 2, 3, and 4!
SteveH
Steve, why don't you just add my code to your solution, then John can accept it. The rest of the solution is correct as to why you have to do this.
Craig Stuntz
Steve, thanks again for the follow-up, and the clear explanation (and testing). Please do add Craig's code to your solution, and I can accept that as the best answer. I can't believe I'm the ONLY person to encounter this, particularly since C# 4 will resolve it ;)
John Kaster