2/13/12

Some more useful MVC grub

Assigning values to Action method parameters via OnActionExecuting handler

Let us say there is an action method as shown below:


[SetParameterValueFilter]
public ActionResult GetCityDetails([Bind(Prefix="MyCity")]string cityName)
{

 return View();

}


This method takes one string input parameter called "cityName" and returns a view.As you can see, the method is also decorated with a custom filter called "SetParameterValueFilter".

The purpose of this filter is to assign a value to the input parameter "cityName" of the action method GetCityDetails based on a certain condition X. So if the condition X is satisfied we will set the parameter value to "Dallas" else we will set it to "Houston".

We achieve this by overriding the OnActionExecuting method of the MVC framework that gets called before the GetCityDetails method gets called.

The code is as follows:


public class SetParameterValueFilterAttribute : ActionFilterAttribute
{  
   public override void OnActionExecuting(ActionExecutingContext context)
   {
     ParameterDescriptor[] ActionMethodParams = context.ActionDescriptor.GetParameters();

    //get the parameter that has a prefix 
    //of "MyCity" and set the value
     foreach (var param in ActionMethodParams)
     {
      if (param.BindingInfo.Prefix.ToUpper() == "MYCITY")
       {
          //set the parameter value
        if(ConditionX)
          context.ActionParameters[param.ParameterName] = "Dallas";
        else
          context.ActionParameters[param.ParameterName] = "Houston";
        break;
       }
     }
  }   
}

Some useful MVC grub

Changing the view name via the OnActionExecuted handler

Let us say there is an action method as shown below:


[ChangeViewFilter]
public ActionResult GetTeamDetails(string teamName)
{

 return View("DallasCowboys");

}

This method takes one input parameter called "teamName" and returns a view called "DallasCowboys".As you can see, the method is also decorated with a custom filter called "ChangeViewFilter".

The purpose of this filter is to change the View Name in the ActionResult of the GetTeamDetails method,if a certain condition X is satisfied. So if the condition X is satisfied we will change the View Name to "HoustonTexans" else we do nothing(the name remains "DallasCowboys").

We achieve this by overriding the OnActionExecuted method of the MVC framework that gets called after the GetTeamDetails gets called and before the View Engine is invoked. The code is as follows:


public class ChangeViewFilterAttribute : ActionFilterAttribute
 {
  public override void OnActionExecuted(ActionExecutedContext context)
    {
        //get the current view name
        string ViewName = ((ViewResultBase)(context.Result)).ViewName;

        //if the view name is not specified
     //it means the view name is the same as the action method name
        if (string.IsNullOrEmpty(ViewName))
            ViewName = context.ActionDescriptor.ActionName;

       if(ConditionX)
        ((ViewResultBase)(context.Result)).ViewName = "HoustonTexans";
    }
 }

2/11/12

Custom Model Binder in MVC

During postbacks, the MVC framework automatically binds the incoming data to the specified model. Let us say, you have a postback handler as show below:
 

  [HttpPost]
  public ActionResult MyPostbackHandler(Person person)
  {

  }
  
Assuming that your MVC view was bound to the Person object, when you postback to this method, the framework will automatically create an instance of Person and populate it with the values in the incoming form fields.

So if the Person object has a property called 'Location', the framework will look for a form field with the same name(Remember - "name" not "Id"). If it finds it, it will get the value from that form field and map it to the property. Same is the case with all the other properties and associated form fields. This is the Default MVC Model binding functionality.

 But what if our postback handler was as follows:

[HttpPost]
public ActionResult MyPostbackHandler(Dictionary<string,string> customObject)
{

}
In this case, default model binding will not work, since the method expects a dictionary object. We will have to explicitly extract the values from the incoming form fields and populate the dictionary object.

To do this we will need to create a custom model binder class as shown below.
Let us assume that we are going to only map fields whose names are prefixed with "Custom:"


public class CustomModelBinder : IModelBinder
{
    public object BindModel(ControllerContext cContext, ModelBindingContext bContext)
    {
        const string Prefix = "Custom:";
        string[] AllKeys = cContext.HttpContext.Request.Form.AllKeys;

        //1.get all the form fields with the specified prefix
        List CustomKeys = AllKeys.Where(x => x.StartsWith(Prefix)).ToList();

        var KeyValuePairs = new Dictionary();
        foreach (var key in CustomKeys)
        {
            ValueProviderResult objResult = bContext.ValueProvider.GetValue(key);
            //get the key's value
            string Value = objResult.AttemptedValue;

            //*Refer NOTE 1
            bContext.ModelState.SetModelValue(key, objResult);

            //*Refer NOTE 2
            if (!CusomValidator(Value))
                bContext.ModelState.AddModelError(key, "ErrorMessage");

            //add key and Value to dictionary
            KeyValuePairs.Add(key, Value);
        }
        return KeyValuePairs;
    }
}

NOTE 1
Before you add an error message for a key in modelstate, we must add it's ValueProviderResult(that contains the attempted value) to ModelState by calling SetModelValue. If we do not do that, the key has an associated error message, but does not have an attempted value to display in the front end. So if you use HtmlHelpers and the framework tries to set the attempted value when validation fails(by implicitly calling ModelState["key"].Value.AttemptedValue), it will throw a null exception since the Value is missing.

NOTE 2
Since we are not using default model binding, we cannot use DataAnnotations for Validation purposes, which means if the fields require some validation we will have to explicitly call the validation code and, if there are any errors, add them to ModelState.

Now that we have a custom model binder called CustomModelBinder, we need to tell the framework to use this custom model binder instead of the default one.We do it by adding the following attribute to the postback handler:


[HttpPost]
public ActionResult MyPostbackHandler([ModelBinder(typeof(CustomModelBinder))]Dictionary<string,string> customObject)
{

}