Feed Subscribe
Exception has been thrown by the target of an invocation.


When BeginForm is not BeginForm<T>

by ondrejsv 30. August 2010 16:03

or why the ASP.NET MVC 2 client validation fails when you use Microsoft.Web.Mvc (aka MvcFutures)?

There’s an incredibly useful set of extension methods in the MvcFutures called BeginForm<T>. These methods provide an alternative type-safe way of building MVC forms instead of the old string based BeginForm. Instead of:

<% using (Html.BeginForm("Submit", "ThreadedInputController")) { %>

you just write

<% using (Html.BeginForm<ThreadedInputController>(c => c.Submit()) { %>

No strings here, everything type-safe.

But when you enable fantastic client side validations with the BeginForm<T> constructed form you end with a JavaScript error “Object Required” somewhere deep inside the MicrosoftMvcValidation.js:

image

on the line:

formElement[Sys.Mvc.FormContext._formValidationTag] = this;

The cause of this is that the new BeginForm<T> and the old BeginForm do not have a common implementation; in fact they are completely separated. The client side validation model requires all HTML forms have an ID value and also the framework expects this ID value to be set in the FormContext.FormId property of the current ViewContext. The original BeginForm does exactly this. If you don’t provide the form ID yourself, it will generate one in the form “formN” where N is the sequence number of the form.

It’s nothing difficult to fix the implementation but you must rename your extension methods to avoid conflict with the MvcFutures implementation or directly fix it in the MvcFutures and build your own version.

Here’s the fix (I renamed methods to the BeginFormA<T>):

private static readonly object _lastFormNumKey = new object(); private static int IncrementFormCount(IDictionary items) { object lastFormNum = items[_lastFormNumKey]; int newFormNum = (lastFormNum != null) ? ((int)lastFormNum) + 1 : 0; items[_lastFormNumKey] = newFormNum; return newFormNum; } [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an Extension Method which allows the user to provide a strongly-typed argument via Expression")] public static MvcForm BeginFormA<TController>(this HtmlHelper helper, Expression<Action<TController>> action, FormMethod method, IDictionary<string, object> htmlAttributes) where TController : Controller { TagBuilder tagBuilder = new TagBuilder("form"); if (helper.ViewContext.ClientValidationEnabled && htmlAttributes["id"] == null) { // forms must have an ID for client validation int formNum = IncrementFormCount(helper.ViewContext.HttpContext.Items); var formId = String.Format(CultureInfo.InvariantCulture, "form{0}", formNum); tagBuilder.GenerateId(formId); } tagBuilder.MergeAttributes(htmlAttributes); string formAction = Microsoft.Web.Mvc.LinkExtensions.BuildUrlFromExpression(helper, action); tagBuilder.MergeAttribute("action", formAction); tagBuilder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method)); helper.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag)); var theForm = new MvcForm(helper.ViewContext); if (helper.ViewContext.ClientValidationEnabled) { helper.ViewContext.FormContext.FormId = tagBuilder.Attributes["id"]; } return theForm; }

You can download the whole static class with the extension methods.

Note: This bug is still not fixed in the ASP.NET MVC 3 Preview 1.

Tags: ,

How to specify UIHint template only for specific mode in ASP.NET Dynamic Data

by ondrejsv 8. October 2009 16:58

If you want to display a data field in your custom template instead of the default one coming with ASP.NET Dynamic Data, you can by decorating your metadata with the UIHint attribute:

[UIHint("Attributes")] public EntityCollection<Attribute> Attributes

In this case, Attributes collection will be displayed using Attributes.ascx template. Dynamic Date is clever enough to use Attributes_Edit.ascx if you happen to edit the collection. If Attributes_Edit.ascx does not exist, Dynamic Date will use the default template.

However, if you have a template that you want to use only for editing (or inserting) and use the default template otherwise (for read only), you cannot. If you supply the UIHint, you are required to have also a read-only template – in our case, you must have Attributes_Edit.ascx together with Attributes.ascx. You end up copying the content of the default template and renaming it (to Attributes.ascx and so on). As we know that copy-paste is a bad programmer’s friend which promotes low code maintainability and readability, we must come up with another solution.

Dynamic Data delegates deciding which template to use for which field to an instance of the FieldTemplateFactory class. In particular, we are interested in its GetFieldTemplateVirtualPath method which eats a column to get the template for, mode (ReadOnly, Edit, Insert) and UIHint value. We can, of course, extend the default factory to process UIHints in the form realUIHint|mode (e.g. “Attributes|Edit” which means use the Attributes_Edit.ascx for editing but the default template for anything else). The actual code is simple:

public class MyFieldTemplateFactory : FieldTemplateFactory { private const char HintSeparator = '|'; public override string GetFieldTemplateVirtualPath(MetaColumn column, System.Web.UI.WebControls.DataBoundControlMode mode, string uiHint) { if (!string.IsNullOrEmpty(uiHint) && uiHint.IndexOf(HintSeparator) > 0) { string[] sArr = uiHint.Split(HintSeparator); string hint = sArr[0]; string forMode = sArr[1]; // If current mode is the one specified in the UiHint // use it, otherwise fall back to the base implemenation. if (mode.ToString() == forMode) uiHint = hint; else uiHint = ""; } var temp = base.GetFieldTemplateVirtualPath(column, mode, uiHint); return base.GetFieldTemplateVirtualPath(column, mode, uiHint); } }

Associate your custom template factory with the model in the RegisterRoutes method inside the Global.asax:

model.FieldTemplateFactory = new MyFieldTemplateFactory();
Done! :-)
kick it on DotNetKicks.com [digg]

Tags: ,