Custom MVC HTML extension for DateTime property - asp.net-mvc

Is it possible to create a custom html helper for asp.net mvc that would take a DateTime as TProperty, create 3 textboxes for day, month and year and allow me to get the full DateTime back when posting the form ?
In other words, I want something that is written like this in Razor :
#Html.TripleTextboxFor(userModel => userModel.Birthdate)
That outputs something like :
<input type="text" name="BirthdateDay" />
<input type="text" name="BirthdateMonth" />
<input type="text" name="BirthdateYear" />
And finally, is it possible to create such an extension without doing extra model custom binding to retrieve the DateTime posted ?
Here is what I have been using so far. It is ok to display the value, but I have extra work to get the DateTime back + validation.
public static MvcHtmlString TripleBirthDateTextBoxes<TModel>(this HtmlHelper<TModel> htmlHelper,Expression<Func<TModel, DateTime>> expression)
{
StringBuilder output = new StringBuilder();
var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
DateTime dt = (DateTime)metaData.Model;
output.Append(htmlHelper.TextBoxFor(m => dt.Day));
output.Append(htmlHelper.TextBoxFor(m => dt.Month));
output.Append(htmlHelper.TextBoxFor(m => dt.Year));
return new MvcHtmlString(output.ToString());
}
Thanks !

Related

Build list of data validation attributes for a given element

When using any of the Input Extension Helper Methods, like #Html.TextboxFor, any Validation Attributes from your model are automatically generated by the Razor engine (via ClientValidationEnabled/UnobtrusiveJavaScriptEnabled).
For example, take the following case which works fine
Model:
[Required]
public string QuestionOne { get; set; }
View:
#Html.TextBoxFor(model => model.QuestionOne)
#Html.ValidationMessageFor(model => model.QuestionOne)
Generated Markup:
<input type="text" id="QuestionOne" name="QuestionOne" value=""
data-val="true" data-val-required="The QuestionOne field is required." >
<span class="field-validation-valid" data-valmsg-for="QuestionOne" data-valmsg-replace="true"></span>
In this case the attributes data-val="true" & data-val-required="The QuestionOne field is required." are picked up by Unobtrusive validation and the form element is successfully validated.
However, for extensibility reasons, I want to be able to generate the <input> element myself instead of using TextBoxFor. So my view would now look like this:
<input type="textbox"
id="#Html.IdFor(m => m.QuestionTwo)"
name="#Html.NameFor(m => m.QuestionTwo)"
value="#Model.QuestionTwo"
data-val="true" data-val-required="Selection is Required" />
#Html.ValidationMessageFor(model => model.QuestionTwo)
In this case, I'm faking the validation attribute output by just re-writing data-val="true" (etc) by hand, but this would have to be expanded to cover every single case.
Here's a running Demo in .NET Fiddle
Q: Can I build /return a list of data-val-* attributes for a given element?
You can use the GetUnobtrusiveValidationAttributes() method of HtmlHelper to get the validation attributes associated with a specific property.
For example in the view
#{ var attributes = Html.GetUnobtrusiveValidationAttributes("QuestionTwo"); }
<input
type="textbox"
#foreach(var attr in attributes)
{
#:#attr.Key="#attr.Value"
}
id="#Html.IdFor(m => m.QuestionTwo)"
....
/>
Note the #:#attr.Key="#attr.Value" line will give a warning (Missing attribute name) but will run correctly
Alternatively, you could use javaScript/jQuery to add the attributes
<script type="text/javascript">
var attributes = #Html.Raw(Json.Encode(attributes));
var input = $('#QuestionTwo');
for(var i in attributes) {
input.attr(i, attributes[i]);
}
</script>
I have forked the DotNetFiddle here to show the working code for both options.
While the above code shows how it can be done, you should not be doing that. The HtmlHelper methods execute a lot of code your ignoring to ensure correct 2-way model binding, for example, the value attribute is determined by first checking for a value in ModelState, then in the ViewDataDictionary, and only if the previous values do not exist, does it use the value of the property (the second part of TextBoxFor displaying initial value, not the value updated from code explains the behavior).
Except for the incorrect value attribute, the code you have shown for the <input> is the same as will be generated by simply using #Html.TextBoxFor(m => m.Question2). I assume your real case is different, but if you cannot make use of TextBoxFor() and using an overload that accepts htmlAttributes to generate the html you need, then the correct approach is to create your own HtmlHelper method (and making use of existing methods in the HtmlHelper class and System.Web.Mvc.Html namespace)

Fields marked HiddenFor not binding to Model in MVC4

I have implemented an image upload child action form for an application. I have a strongly typed partial view.
public class ImageViewModel{
public long ImageId{get;set;}
public long OwnerId{get;set;}
public string ImageName{get;set;}
public string ImageDescription{get;set;}
public IEnumerable<HttpPostedFileBase> Files { get; set; }
}
Razor code looks something like this:
<form action="UploadImage" method="post" enctype="multipart/form-data">
#Html.ValidationSummary()
#Html.HiddenFor(m => m.OwnerId)
#Html.HiddenFor(m => m.ImageId)
#HtmlEditorFor(m=>m.ImageName)
<input type="file" name="Files" id="file0" />
<input type="submit" value="Upload" />
</form>
Here is the problem. When form is posted back, the model has uploaded file and ImageName value in it. But values that were bound using HiddenFor are missing.
[HttpPost]
public ActionResult UploadImage(ImageViewModel model)
{ ...}
I have checked HTML source. Hidden fields are rendered corrected with Id and names matching to property named of model. On post back I checked the raw request. Both hidden fields are carried in Form collection. But model binding is not setting the values of these fields in properties.
Is there something that I am missing about these hidden fields?
Thanks

How to extend html.textboxfor to remove the name attribute?

I want to extend the helper to make it like this:
#html.TextBoxFor(x=>x.CustomerId).ReadOnly()
and output the input element without the name attribute, so that it will not be posted to the server.
This should do the trick:
public static class MyInputExtensions
{
public static MvcHtmlString NameLessTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
var textBox = htmlHelper.TextBoxFor(expression);
string pattern = #"name=""([^""]*)""";
string fixedHtml = Regex.Replace(textBox.ToHtmlString(), pattern, "");
return new MvcHtmlString(fixedHtml);
}
}
Usage:
#Html.NameLessTextBoxFor(x=> x.CustomerId)
You can't do it.
(at least without some ugly workarounds with processing string value returned from helper)
Html helpers were written to help you generate form fields for your model with intention that they will sent data to server. For strongly-typed helper (like Html.TextBoxFor(x => x.CustomerId)) the name is taken from passed expression and for not strongly-typed helpers (like Html.TextBoxFor("CustomerId", Model.CustomerId)) there is a check that throws exception when name is null or empty.
If you want to generate input without "name" attribute then simply do not use html helper methods.
For example, if you want to change you html helper usage to generate same output but without "name" attribute then:
instead of this:
#Html.TextBoxFor(x => x.BetAmount)
write this:
<input type="text" value="#Model.BetAmount" />
instead of this:
#Html.TextBoxFor(x => x.BetAmount, new { #class = "red", placeholder = "Type Stuff", data_maximum_value = Model.MaximumBetAmount })
write this:
<input type="text" value="#Model.BetAmount" class="red" placeholder="Type Stuff" data_maximum_value="#Model.MaximumBetAmount" />
instead of this (you use overload with "format" argument):
#Html.TextBoxFor(x => x.BetAmount, "{0:0}", new { #class = "blue" })
write this:
<input type="text" value="#Html.FormatValue(Model.BetAmount,"{0:0}")" class="red" />
because Html.TextBoxFor uses Html.FormatValue when you pass "format" argument.
This is not exactly the same what html helper do because html helpers first tries to get data from ModelState for validation purpose (it's a common gotcha). But for 99% of times this is probably good enough
I recommend checking actual source code of ASP.NET MVC if you want to know what helper methods are actually doing. It's not black magic.

Using an EditorTemplate with RenderPartial

Is there an accepted way to render an EditorTemplate using RenderPartial?
I put a lot of work into an EditorTemplate that would use JQuery UI autocomplete to let the user choose from a list of companies. (It uses an HTML helper to ensure the right JS libraries and CSS rules are included in the "right" places in the generated web page.)
While building another page for that same application, I found I wanted to use that template again, outside the model I built the EditorTemplate for.
This code accomplishes the task, but in a way I can only consider a hack.
#using(Html.BeginForm()) {
ViewData.TemplateInfo.HtmlFieldPrefix = "CompanyName";
Html.RenderPartial("~/Views/Shared/EditorTemplates/Company.cshtml", string.Empty);
<input type="submit" value='Filter' />
}
Because I'm not using EditorFor, there is no "name" parameter, so the rendered input field is just the HtmlFieldPrefix of "CompanyName". The autocomplete works, and I can submit the form and filter the data. But this solution feels sloppy and fragile. Anyone have a better idea?
I've moved re-usable "controls" like that to a helper in app_code, and make the editor template a small wrapper, to call the helper.
My editorTemplate for DateTime looks like this:
#model DateTime?
#using System.Globalization
#{
var name = MvcHtmlString.Create(ViewData.TemplateInfo.GetFullHtmlFieldName(""));
var id = MvcHtmlString.Create(ViewData.TemplateInfo.GetFullHtmlFieldId(""));
var value = Model.HasValue && Model.Value > DateTime.MinValue ? string.Format("{0:d}", Model) : "";
}
<span class="edit">
#Editor.DateTime(id, name, value)
</span>
And I have and Editor.cshtml in app_code that contains:
#helper DateTime(IHtmlString id, IHtmlString name, string value) {
<input type="text" class="editor-date" id="#id" name="#name" value="#value" />
if (HttpContext.Current.Items["editor-date"] == null) {
HttpContext.Current.Items["editor-date"] = new object();
<script type="text/javascript">
$(function () {
$(".editor-date").datepicker({ dateFormat: 'd/m/yy' });
});
</script>
}
}
So I can use my date-editor from EditorFor, or any other way, while including the script to start it only once, and have all code to change on one place.

MVC partial views (or editor templates): append index to input name for multiple use of form?

How can I add an index, to input names and ids in forms that are used multiple times in one view?
I have created a form for photo rotator that provides the ability to edit a context-specific caption for each photo (billboard). I need to be able to include multiple instances of the form fields for this so the admins can edit all of the captions for a rotator's set of photos in one view, and so I need a way to keep ids and field names unique.
Editor templates automatically add a prefix, but when I loop over the photos like this:
<% foreach (var billboard in Model.Billboards ) { %>
<%: Html.EditorFor(x => billboard, "BillboardForm")%>
<% } %>
It simply adds "billboard_" as the prefix, which doesn't solve my problem.
I'd like to append the rotator id billboard id to the end of each input name and id:
<form action="/Rotators/Edit/5" method="post">
<input id="billboard_21_RotatorId" name="billboard_21_RotatorId" type="hidden" value="5" />
<input id="billboard_21_ImageId" name="billboard_21_ImageId" type="hidden" value="19" />
<label for="billboard_21_Title">Title</label>
<textarea cols="20" id="billboard_21_Title" name="billboard_21_Title" rows="2">Title</textarea>
<label for="billboard_21_Caption">Caption</label>
<textarea cols="20" id="billboard_21_Caption" name="billboard_21_Caption" rows="2">This is the caption</textarea>
<select id="billboard_21_TopicId" name="billboard_21_TopicId">
<option value="1">1st option</option>
</select>
</form>
Any easy way to do this??
plz download this sampel code from steve sanderson's blog post. it does not directly relate to your question. But in demo project you will find BeginCollectionItem html helper that is changing the prefix scope for input or series of inputs. This can give u a starting point
Edit:
in ur editor template u can use following method from steve's code like
using(Html.BeginHtmlFieldPrefixScope("BillBoard" + Model.ID.ToString())){
<label>Image<label>
#Html.TextBoxFor(x=>x.Image)
<label>Caption</label>
#Html.TextBoxFor(x=>x.Caption)
}
if ID is property of your model and has value e.g 4 then u will have html like
<label>Image</label>
<input name = "BillBoard4.Image" .../>
<label>Caption</label>
<input name = "BillBoard4.Caption" .../>
Note: The features used below may not have existed 4 years ago...
Firstly, you don't have to use beestings any more, the # syntax used
in the Razor examples is far cleaner.
The method you're calling is in System.Web.Mvc.Html.EditorExtensions:
public static MvcHtmlString EditorFor<TModel, TValue>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> expression
)
...
Your approach:
#foreach (var billboard in Model.Billboards ) {
#Html.EditorFor(x => billboard, "BillboardForm")
}
The body of expression x => billboard is a ConstantExpression.
This approach results in the appropriate scope applying in the EditorTemplate:
#for (var i = 0; i < Model.BillBoards.Count(); i++)
{
#Html.EditorFor(x => Model.BillBoards[i], "BillboardForm")
}
If Model.BillBoards is an array, the expression x => Model.BillBoards[i] can be described as
SimpleBinaryExpression{NodeType:ArrayIndex}(
Left: ConstantExpression,
Right: ConstantExpression
)
If Model.BillBoards is an IList<T>, the expression x => Model.BillBoards[i] can be described as
InstanceMethodCallExpressionN(
Method:RuntimeMethodInfo(IList<T>.get_Item (Int32 index)),
Object:PropertyExpression(ConstantExpression),
Arguments:[ConstantExpression]
)
The overloads of EditorFor() that accept Expressions check the expression body's Type and NodeType and construct the scope accordingly.
This code should be equivalent if you don't have anything else inside the loop:
#Html.EditorFor(x => Model.BillBoards, "BillboardForm")
If you only have a read-only view and an editing view, you can rename your templates and remove the second parameter. Assuming BillBoard is your Model class, rename BillboardForm.cshtml to EditorTemplates/BillBoard.cshtml, then change the code to
#Html.EditorFor(x => Model.BillBoards)

Resources