Nov 9, 2009

acton parameters

The asp.net mvc framework matches action parameters in the following order:

  1. Form values
  2. Route arguments
  3. Querystring parameters

The parameters that is passed in the action method is handled by ControllerActionInvoker.The ControllerActionInvoker tries to using reflection and other techniques to extract information for client's request, as the source code shows below.

//ControllerActionInvoker members
public bool InvokeAction(ControllerContext controllerContext, string actionName)
{
//..code skip
IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor);
ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);
InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result);
//..
}
protected virtual IDictionary<string, object> GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
    Dictionary<string, object> parametersDict = new Dictionary(StringComparer.OrdinalIgnoreCase);
    ParameterDescriptor[] parameterDescriptors = actionDescriptor.GetParameters();

    foreach (ParameterDescriptor parameterDescriptor in parameterDescriptors)
    {
        parametersDict[parameterDescriptor.ParameterName] = GetParameterValue(controllerContext, parameterDescriptor);
    }
    return parametersDict;
}

protected virtual object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
{
    // collect all of the necessary binding properties
    Type parameterType = parameterDescriptor.ParameterType;
    //you can register your binder, but default is DefaultModelBinder
    IModelBinder binder = GetModelBinder(parameterDescriptor);
    IDictionary<string, ValueProviderResult> valueProvider = controllerContext.Controller.ValueProvider;
    string parameterName = parameterDescriptor.BindingInfo.Prefix ?? parameterDescriptor.ParameterName;
    Predicate<string> propertyFilter = GetPropertyFilter(parameterDescriptor);

    // finally, call into the binder
    ModelBindingContext bindingContext = new ModelBindingContext()
    {
        FallbackToEmptyPrefix = (parameterDescriptor.BindingInfo.Prefix == null), // only fall back if prefix not specified
        ModelName = parameterName,
        ModelState = controllerContext.Controller.ViewData.ModelState,
        ModelType = parameterType,
        PropertyFilter = propertyFilter,
        ValueProvider = valueProvider
    };
    object result = binder.BindModel(controllerContext, bindingContext);
    return result;
}

You can also register your ModelBinder to help mvc to build your parameter in the application startup like the code below.

public class ConferenceModelBinder : DefaultModelBinder
{
 private readonly IConferenceRepository _repository;

 //We can require dependencies in binders just like normal classes
 public ConferenceModelBinder(IConferenceRepository repository)
 {
  _repository = repository;
 }

 public override object BindModel(ControllerContext controllerContext,
                                  ModelBindingContext bindingContext)
 {
  //first, we get the value from the request that matches the name
  ValueProviderResult providerResult =
   bindingContext.ValueProvider[bindingContext.ModelName];

  //next we use our repository to find the Conference by the key
  Conference conference = _repository.GetByKey(providerResult.AttemptedValue);
  return conference;
 }
}

ModelBinders.Binders.Add(typeof (Conference),
new ConferenceModelBinder(
new ConferenceRepositoryStub()));