Posts Tagged ‘MVC Toolkit’

August 13th, 2008

Using a Parameter Attribute to set a Default Value in MVC

A couple days ago I came across a breaking change in ASP.NET MVC PR4 that wasn’t reported.  The breaking change is that defaults from routes are no longer used as defaults for parameters in the action method, if no appropriate parameter is found in the request.  Basically what this means is the following:

I have the following route:

URL: /home
Controller: Home
Action: Index
Defaults = page: 1

I set the page so that it always defaults to “1″ if no value is found in the query string for “page”.

So when a request is executed, the Route passes back the RouteData.Values = controller: “Home”, action: “Index”, page: 1. Then it goes through it’s normally processing and the value of the page’s query string is passed in to my action method for the page parameter. So if query string page = 1 then 1, query string page = 2 then 2, and so on. This is how it worked in PR3 and how I understood it was suppose to work as a concept.

However, in PR4, this doesn’t work anymore because of Line 166 in ControllerActionInvoker. It specifically checks that the value is in the route values. However they are always going to be in the route values if they have been defined as a default.

I reported it as a bug, because it was an obvious break from the last 3 preview releases and nothing was reported about this breaking change, and went on my merry way.  However today I received a message back from auriel confriming that this was the correct process flow, and that I should set the parameter either in my action method or the action filter.

So I decided to take this obvious disapointment and turn it in to something I have been thinking about for a long time, but never really had the motivation to impliment.  In .NET you are allowed to add attributes to anything that can be defined via reflection, including classes, interfaces, structures, and even return types and parameters of methods.  So I created an attribute called DefaultAttribute that is a parameter attribute that can be added to your action methods like this:

public ActionResult Index([Default(1)]int? page)

This method tells us that page has a default value of 1 if the value of page is null or undefined.  The DefaultAttribute is rather simplistic, because it is only suppose to hold the default value:

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class DefaultAttribute : Attribute
{
	public DefaultAttribute(object @default)
	{
		if (@default == null)
			throw new ArgumentNullException("default");

		Default = @default;
	}

	public object Default
	{
		get;
		private set;
	}
}

As you can see from the above code I have told the attribute to only allow it to be attached to a parameter by specifying this in the AttributeUsageAttribute.  The next and last thing that we need inorder to complete our goal of being able to set the defaults in a parameter attribute is an action filter that can read the DefaultAttribute from the parameter and set the default if the parameter value is null or undefined.  We are going to process the parameter defaults in an action filter called UseActionParameterDefaultAttribute that can be attached to the controller or direction to the action method.  The code to process the defaults is:

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
	var defaults = GetDefaults(filterContext);
	var actionParameters = filterContext.ActionParameters;
	foreach (var value in defaults)
		if (actionParameters[value.Key] == null)
			actionParameters[value.Key] = value.Value;
}

internal static IDictionary>string, object< GetDefaults(ActionExecutingContext filterContext)
{
	string key = filterContext.ActionMethod.ToString() + "_ParameterDefaults";

	// get from application storage
	IDictionary>string, object< defaults = filterContext.HttpContext.Application[key] as IDictionary>string, object<;

	if (defaults == null)
	{
		defaults = new Dictionary>string, object<(filterContext.ActionParameters.Count);
		foreach (var parameter in filterContext.ActionMethod.GetParameters())
		{
			if (parameter.IsDefined(typeof(DefaultAttribute), false))
			{
				DefaultAttribute attr = parameter.GetCustomAttributes(typeof(DefaultAttribute), false)[0] as DefaultAttribute;
				string parameterName = parameter.Name;
				string actionName = filterContext.ActionMethod.Name;

				try
				{
					defaults.Add(parameterName, ConvertParameterType(attr.Default, parameter.ParameterType, parameterName, actionName));
				}
				catch (Exception exc)
				{
					throw new InvalidOperationException(String.Format(
						CultureInfo.CurrentUICulture,
						"The value of the DefaultAttribute could not be converted to the parameter '{0}' in action '{1}'.",
						parameterName, actionName), exc);
				}
			}
		}

		// add to application storage
		filterContext.HttpContext.Application[key] = defaults;
	}

	return defaults;
}

Each parameter of the action method is scanned for a DefaultAttribute and if found is added to the defaults dictionary to be used later when checking if each parameter is null or undefined in the OnActionExecuting method. Creating this code actually led to another request for the MVC team. The request was for a static storage collection for each action method so that compile-time code like attributes on the parameters don’t have to be processed with each request, since they can only possibly change when the code is recompiled. So I have my fingers crossed that they will actually implement this feature. In the mean time I implemented a application level storage for the parameter defaults so I don’t have to reprocess the parameters with each request.

I have added the two source files to my Google Code Project for Coder Journal, you can find them at:

Tags: , ,

Posted in ASP.NET | kick it on DotNetKicks.com | Bookmark | View blog reactions | 2 Comments »

June 24th, 2008

MVC CAPTCHA for Preview Release 3

Since my last release of the MVC toolkit some major changes have taken place in the MVC Framework. I am going to do a quick run through of how they changed the MVC CAPTCHA for the better.

Originally in MVC Preview Release 1 for the MVC CAPTCHA many of you remember that the indicator for a valid CAPTCHA was passed through the parameters of the action method like so:

[ControllerAction]
[CaptchaValidation("captcha")]
public void Register(bool captchaValid, string otherParameters)
{
    // do stuff
}

However when Preview Release 2 came out the ability to pass the parameter through the action method was broken. So I had to create a hack around this:

[CaptchaValidation("captcha")]
public void Register(string otherParameters)
{
    bool captchaValid = (bool)RouteData.Values["captchaValid"];
    // do stuff
}

Apparently without realizing it in Preview Release 1, I had discovered a major feature that everybody that I explained it to saw great potential in. So I submitted a feature request and waited for the ASP.NET Team to get back to me. With the release of Preview Release 3, they finally answered my prayers and added the parameter injection feature back in to the framework.

So now this code works again in the Preview Release 3 version of the MVC CAPTCHA control.

[CaptchaValidation("captcha")]
public void Register(bool captchaValid, string otherParameters)
{
    // do stuff
}

This works because of a new property called ActionParameterson the ActionExecutingContext. It can be used to test and change any of the action methods parameters before the action method has been executed. For the purpose of the MVC CAPTCHA control it allows me to inject a true or false value in to a parameter called captchaValid, so that the action method knows that the CAPTCHA validation passed or failed.

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
	// make sure no values are getting sent in from the outside
	if (filterContext.ActionParameters.ContainsKey("captchaValid"))
		filterContext.ActionParameters["captchaValid"] = null;

	// get the guid from the post back
	string guid = filterContext.HttpContext.Request.Form["captcha-guid"];

	// check for the guid because it is required from the rest of the opperation
	if (String.IsNullOrEmpty(guid))
	{
		filterContext.RouteData.Values.Add("captchaValid", false);
		return;
	}

	// get values
	CaptchaImage image = CaptchaImage.GetCachedCaptcha(guid);
	string actualValue = filterContext.HttpContext.Request.Form[Field];
	string expectedValue = image == null ? String.Empty : image.Text;

	// removes the captch from cache so it cannot be used again
	filterContext.HttpContext.Cache.Remove(guid);

	// validate the captch
	filterContext.ActionParameters["captchaValid"] =
		!String.IsNullOrEmpty(actualValue)
		&& !String.IsNullOrEmpty(expectedValue)
		&& String.Equals(actualValue, expectedValue, StringComparison.OrdinalIgnoreCase);
}

Notice in the code above all the filterContext.ActionParameters references, that is the CAPTCHA validation checking for the parameter in the method parameters list and adding the value of the CATPCHA validation to the list. Note that in order for this to work you must already have the captchaValid attribute present in your method, these actions merely fill in the value of the captchaValid placeholder.

For anybody who hasn’t used my MVC CAPTCHA control before, you only need to do two things:

1. You need to register the CAPTCHA image handler.

<httpHandlers>
    <add verb="GET" path="captcha.ashx" validate="false" type="ManagedFusion.Web.Handlers.CaptchaImageHandler, ManagedFusion" />
</httpHandlers>

2. Add the following to the View that you want the CAPTCHA to show in. Note the extension, CaptchaTextBox in HtmlHelper, generates a text box with autocomplete=”off” so that the CAPTCHA box will not have an auto-complete show up.

<label for="captcha">Enter <%= Html.CaptchaImage(50, 180) %> Below</label><br />
<%= Html.CaptchaTextBox("captcha") %>

Which generates the following.

Example of CAPTCHA

If you would like to download the latest copy of the MVC CAPTCHA it is available in my MVC Toolkit.

Download: Coder Journal MVC Toolkit
Source: Coder Journal MVC Toolkit

Tags: , ,

Posted in ASP.NET, C#, How To | kick it on DotNetKicks.com | Bookmark | View blog reactions | 14 Comments »