views:

148

answers:

1

Cannot call action method 'System.Web.Mvc.PartialViewResult FooT' on controller 'Controller' because the action method is a generic method

<% Html.RenderAction("Foo", model = Model}); %>

Is there a workaround for this limitation on ASP MVC 2? I would really prefer to use a generic. The workaround that I have come up with is to change the model type to be an object. It works, but is not preferred:

    public PartialViewResult Foo<T>(T model) where T : class
    {
      // do stuff
    }
+3  A: 

The code that throws that is inside the default ActionDescriptor:

    internal static string VerifyActionMethodIsCallable(MethodInfo methodInfo) {
        // we can't call instance methods where the 'this' parameter is a type other than ControllerBase
        if (!methodInfo.IsStatic && !typeof(ControllerBase).IsAssignableFrom(methodInfo.ReflectedType)) {
            return String.Format(CultureInfo.CurrentUICulture, MvcResources.ReflectedActionDescriptor_CannotCallInstanceMethodOnNonControllerType,
                methodInfo, methodInfo.ReflectedType.FullName);
        }

        // we can't call methods with open generic type parameters
        if (methodInfo.ContainsGenericParameters) {
            return String.Format(CultureInfo.CurrentUICulture, MvcResources.ReflectedActionDescriptor_CannotCallOpenGenericMethods,
                methodInfo, methodInfo.ReflectedType.FullName);
        }

        // we can't call methods with ref/out parameters
        ParameterInfo[] parameterInfos = methodInfo.GetParameters();
        foreach (ParameterInfo parameterInfo in parameterInfos) {
            if (parameterInfo.IsOut || parameterInfo.ParameterType.IsByRef) {
                return String.Format(CultureInfo.CurrentUICulture, MvcResources.ReflectedActionDescriptor_CannotCallMethodsWithOutOrRefParameters,
                    methodInfo, methodInfo.ReflectedType.FullName, parameterInfo);
            }
        }

        // we can call this method
        return null;
    }

Because the code is calling "methodInfo.ContainsGenericParameters" I don't think there is a way to override this behavior without creating your own ActionDescriptor. From glancing at the source code this seems non-trivial.

Another option is to make your controller class generic and create a custom generic controller factory. I have some experimental code that creates a generic controller. Its hacky but its just a personal experiment.

public class GenericControllerFactory : DefaultControllerFactory
{
    protected override Type GetControllerType(System.Web.Routing.RequestContext requestContext, string controllerName)
    {
        //the generic type parameter doesn't matter here
        if (controllerName.EndsWith("Co"))//assuming we don't have any other generic controllers here
            return typeof(GenericController<>);

        return base.GetControllerType(requestContext, controllerName);

        throw new InvalidOperationException("Generic Factory wasn't able to resolve the controller type");
    }

    protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
    {
        //are we asking for the generic controller?
        if (requestContext.RouteData.Values.ContainsKey("modelType"))
        {
            string typeName = requestContext.RouteData.Values["modelType"].ToString();
            //magic time
            return GetGenericControllerInstance(typeName, requestContext);
        }

        if (!typeof(IController).IsAssignableFrom(controllerType))
            throw new ArgumentException(string.Format("Type requested is not a controller: {0}",controllerType.Name),"controllerType");

        return base.GetControllerInstance(requestContext, controllerType);
    } 

    /// <summary>
    /// Returns the a generic IController tied to the typeName requested.  
    /// Since we only have a single generic controller the type is hardcoded for now
    /// </summary>
    /// <param name="typeName"></param>
    /// <returns></returns>
    private IController GetGenericControllerInstance(string typeName, RequestContext requestContext)
    {
        var actionName = requestContext.RouteData.Values["action"];

        //try and resolve a custom view model

        Type actionModelType = Type.GetType("Brainnom.Web.Models." + typeName + actionName + "ViewModel, Brainnom.Web", false, true) ?? 
            Type.GetType("Brainnom.Web.Models." + typeName + ",Brainnom.Web", false, true);

        Type controllerType = typeof(GenericController<>).MakeGenericType(actionModelType);

        var controllerBase = Activator.CreateInstance(controllerType, new object[0] {}) as IController;

        return controllerBase;
    }
}
jfar