MVC CheckBoxListFor not posting as expected - asp.net-mvc

I have build a CheckBoxListFor extension on HtmlHelper thanks to this wonderful answer http://bit.ly/Aevcea (code below) but am finding it doesn't post as expected.
My form is based on a model Group which has (amongst other properties) string Name and int[] PersonIDs.
The CheckBoxListFor renders something like this:
<ul>
<li><input type="checkbox" name="PersonIDs" value="1" id="PersonIDs_1" /></li>
<li><input type="checkbox" name="PersonIDs" value="2" id="PersonIDs_2" /></li>
</ul>
My controller has an Edit(Group group) method to handle submission of this form. However, upon submit I'm finding group.PersonIDs is null. There is though a Request.Form["PersonIDs"] set to the selected values (e.g. "1,2" if both items above are checked). Also, if I add another parameter to my Edit method (int[] PersonIDs) then that arrives with the expected contents (the selected IDs).
Can anyone explain what I'm doing wrong? The relevant bit of my view looks like this (extra bits stripped out):
#Html.TextBoxFor(m => m.Group.Name)
#Html.CheckBoxListFor(m => m.Group.PersonIDs, Model.MultiSelectListOfAllPeople)
Note that the group parameter in my Edit method does come back with Name set according to the form.
Just for completeness, here is the full body of my CheckBoxListFor extension:
public static MvcHtmlString CheckBoxListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, IEnumerable<TProperty>>> expression, MultiSelectList multiSelectList, object htmlAttributes = null)
{
//Derive property name for checkbox name
MemberExpression body = expression.Body as MemberExpression;
string propertyName = body.Member.Name;
//Get currently select values from the ViewData model
IEnumerable<TProperty> list = expression.Compile().Invoke(htmlHelper.ViewData.Model);
//Convert selected value list to a List<string> for easy manipulation
List<string> selectedValues = new List<string>();
if (list != null)
{
selectedValues = new List<TProperty>(list).ConvertAll<string>(delegate(TProperty i) { return i.ToString(); });
}
//Create div
TagBuilder wrapper = new TagBuilder("ul");
wrapper.AddCssClass("clearfix");
wrapper.MergeAttributes(new RouteValueDictionary(htmlAttributes), true);
//Add checkboxes
foreach (SelectListItem item in multiSelectList)
{
wrapper.InnerHtml += String.Format("<li><input type=\"checkbox\" name=\"{0}\" id=\"{0}_{1}\" " +
"value=\"{1}\" {2} /><label for=\"{0}_{1}\">{3}</label></li>",
propertyName,
item.Value,
selectedValues.Contains(item.Value) ? "checked=\"checked\"" : "",
item.Text);
}
return MvcHtmlString.Create(wrapper.ToString());
}

OK, the problem was that the CheckBoxListFor extension needed to render the control name as Group.PersonIDs and not simply PersonIDs. The form was bound to an object that itself was a sub-property of the view model. I've quickly adapted my CheckBoxListFor method as follows, but would gratefully accept a more elegant solution! I'm passing an additional boolean parameter includeDeclaringType to tell it whether to include the name of the declaring type in the ID. Not sure if this can be inferred any other way..?
public static MvcHtmlString CheckBoxListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, IEnumerable<TProperty>>> expression, MultiSelectList multiSelectList, bool includeDeclaringType, object htmlAttributes = null)
{
//Derive property name for checkbox name
MemberExpression body = expression.Body as MemberExpression;
string declaringTypeName = body.Member.DeclaringType.Name;
string propertyName = body.Member.Name;
//Get currently select values from the ViewData model
IEnumerable<TProperty> list = expression.Compile().Invoke(htmlHelper.ViewData.Model);
//Convert selected value list to a List<string> for easy manipulation
List<string> selectedValues = new List<string>();
if (list != null)
{
selectedValues = new List<TProperty>(list).ConvertAll<string>(delegate(TProperty i) { return i.ToString(); });
}
//Create div
TagBuilder wrapper = new TagBuilder("ul");
wrapper.AddCssClass("clearfix");
wrapper.MergeAttributes(new RouteValueDictionary(htmlAttributes), true);
//Add checkboxes
foreach (SelectListItem item in multiSelectList)
{
var name = string.Concat(
includeDeclaringType ? string.Format("{0}.", declaringTypeName) : "",
propertyName
);
var id = string.Concat(
includeDeclaringType ? string.Format("{0}_", declaringTypeName) : "",
propertyName,
"_",
item.Value
);
wrapper.InnerHtml += String.Format("<li><input type=\"checkbox\" name=\"{0}\" id=\"{1}\" " +
"value=\"{2}\" {3} /><label for=\"{1}\">{4}</label></li>",
name,
id,
item.Value,
selectedValues.Contains(item.Value) ? "checked=\"checked\"" : "",
item.Text);
}
return MvcHtmlString.Create(wrapper.ToString());
}

You can also use ExpressionHelper.GetExpressionText method, it will generate the correct input name for you:
var expressionText = ExpressionHelper.GetExpressionText(expression);
wrapper.InnerHtml +=
string.Format("<li><input type=\"checkbox\" name=\"{0}" id=\"{0}_{1}\" " +
"value=\"{1}\" {2} /><label for=\"{0}_{1}\">{3}</label></li>",
expressionText,
item.Value,
selectedValues.Contains(item.Value) ? "checked=\"checked\"" : "",
item.Text);

Related

Can I force the Razor TextBoxFor helper to use a specific decimal separator than the default one?

I have the following markup:
#Html.TextBoxFor(m => m.Items[i].Rate, new { #class = "form-control text-center Rate", #readonly = Model.PreTenderLockedDown, title = "Rate" })
When the Rate property is e.g. 12.45 in the db, the above TextBoxFor renders the number with a comma separator and not the period I want. Can I do anything outside of setting the language settings on IIS?
Using regex will be an option, if you are not interested in over engineering.
Sample code like below would work. I haven't tested the code, its just for illustration.
#Html.TextBoxFor(m => Regex.Replace(m.Items[i].Rate.ToString(), #",(?<=\d,)(?=\d)", "."), new { #class = "form-control text-center Rate", #readonly = Model.PreTenderLockedDown, title = "Rate" })
https://stackoverflow.com/a/51221701/575924 use regex from here if possible.
You can use "Value" attribute of TextBoxFor markup.
#Html.TextBoxFor(m => m.Items[i].Rate, new { #class = "form-control text-center Rate", #readonly = Model.PreTenderLockedDown, title = "Rate", Value=String.Format("{0:0.###}", m.Items[i].Rate) })
Here "{0:0.###}" means that it will display 3 digits under decimal points.
Try this:
web.config:
<system.web>
<globalization culture="en-US" uiCulture="en-US" />
</system.web>
You can also apply this for the view where you want to force this setting:
<%# Page="" UICulture="en-US" Culture="en-US" %>
Alternatively, you can override default behavior of TextBoxFor by using a custom helper method. I used a similar one for DropDownListFor in some of my projects as shown below:
Helper Method:
public static class MyHelpers
{
//Custom HTML Helper method used for setting "class" and disabled attributes of MyDropdownlist
public class MySelectItem : SelectListItem
{
/* Since you are passing this data using ViewData, you don't have a limitation and
you can put anything there. I advise that you use ViewBag instead of ViewData. */
public string Class { get; set; }
public string Disabled { get; set; }
}
public static MvcHtmlString MyDropdownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<MySelectItem> list, string optionLabel, object htmlAttributes)
{
return MyDropdownList(htmlHelper, ExpressionHelper.GetExpressionText(expression), list, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static MvcHtmlString MyDropdownList(this HtmlHelper htmlHelper, string name, IEnumerable<MySelectItem> list, string optionLabel, IDictionary<string, object> htmlAttributes)
{
TagBuilder dropdown = new TagBuilder("select");
dropdown.Attributes.Add("name", name);
dropdown.Attributes.Add("id", name);
StringBuilder options = new StringBuilder();
// Make optionLabel the first item that gets rendered.
if (optionLabel != null)
options = options.Append("<option value='" + String.Empty + "'>" + optionLabel + "</option>");
foreach (var item in list)
{
if(item.Disabled == "disabled")
options = options.Append("<option value='" + item.Value + "' class='" + item.Class + "' disabled='" + item.Disabled + "'>" + item.Text + "</option>");
else
options = options.Append("<option value='" + item.Value + "' class='" + item.Class + "'>" + item.Text + "</option>");
}
dropdown.InnerHtml = options.ToString();
dropdown.MergeAttributes(new RouteValueDictionary(htmlAttributes));
return MvcHtmlString.Create(dropdown.ToString(TagRenderMode.Normal));
}
}

Create an ASP.NET MVC Html helper similar to DropDownListFor

In certain cases I want to display SelectList object not using DropDownListFor helper. Instead, I want to create a helper that iterates over the SelectListItems, and draw something different.
I have created an EditorTemplate:
#model RadioButtonOptions
<div class=" switch-field noselect" style="padding-left: 0px;">
#foreach (SelectListItem op in Model.Values.Items)
{
var idLabelF = ViewData.TemplateInfo.GetFullHtmlFieldId("") + "_" + op.Value;
var esChecked = "";
if (op.Selected)
{
esChecked = "checked";
}
<input type="radio" id="#idLabelF" name="#(ViewData.TemplateInfo.GetFullHtmlFieldName(""))" value="#op.Value" #esChecked />
<label for="#idLabelF" style="width: 100px;">#op.Text</label>
}
</div>
The RadioButtonOptions class is a ViewModel:
public class RadioButtonOptions
{
public SelectList Values { get; set; }
}
The final resul looks like this:
My ViewModel is like this (simplified):
public class MainVisitVM
{
public MainVisit Visit { get; set; }
public RadioButtonOptions VisitOptions { get; set; }
}
And I use it in Razor View as:
<div class="clearfix">
#Html.LabelFor(x=> x.Visit.Tipo)
<div class="input">
#Html.EditorFor(x=> x.VisitOptions ) //HERE
</div>
</div>
The problem I have is that I want this to work more like the DropDownListFor, so the lamda expresion I pass is the property holding the selected value, and then just pass the SelectList object (or a custom list).
<div class="clearfix">
#Html.LabelFor(x=> x.Visit.Tipo)
<div class="input">
#Html.CustomDropDownListFor(x=> x.Visit.Tipo, Model.VisitOptions ) //This would be ideal!!
</div>
</div>
So, I think doing this using EditorTemplates will not be possible.
Any idea in how to accomplish this?
Thanks to #StephenMuecke suggestion, I ended up with this HtmlHelper extension method:
public static MvcHtmlString RadioButtonForSelectList<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
SelectList listOfValues)
{
string htmlFieldName = ExpressionHelper.GetExpressionText(expression);
if (listOfValues == null) return MvcHtmlString.Create(string.Empty);
var wrapperDiv = new TagBuilder("div");
wrapperDiv.AddCssClass("switch-field noselect");
wrapperDiv.Attributes.Add("style", "padding-left: 0px;");
var sb = new StringBuilder();
foreach (SelectListItem item in listOfValues)
{
var idLabelF = htmlFieldName.Replace(".","_") + "_" + item.Value;
var label = htmlHelper.Label(idLabelF, item.Text, new { style = "width: 100px;" }).ToHtmlString();
var radio = htmlHelper.RadioButtonFor(expression, item.Value, new { id = idLabelF }).ToHtmlString();
sb.AppendFormat("{0}{1}", radio, label);
}
wrapperDiv.InnerHtml = sb.ToString();
return MvcHtmlString.Create(wrapperDiv.ToString());
}
Not particulary proud of my htmlFieldName.Replace(".","_"), but works.

Asp.Net Form submit using URL parameters

That's my URL:
http://localhost:7071/TODO?page=1&codcat=56
It returns me a simple form , it is my form of research:
<div class="col-md-8">
#using (Html.BeginForm("Index", "TODO", FormMethod.Get))
{
<p>
Find in ...: #Html.TextBox("SearchString", ViewBag.CurrentFilter as string)
<input type="submit" value="Search" />
</p>
}
<br />
</div>
The Search method in TODO control is :
public async Task<ActionResult> Index(string sortOrder, string currentFilter, string searchString, int? page, int? codcat)
{
ViewBag.CurrentSort = sortOrder;
ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "title_desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";
if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}
ViewBag.CurrentFilter = searchString;
var todos = from v in db.TODOs
join cv in db.TODO_CATEGORIA on v.ID equals cv.ID_TODO
where cv.ID_CATEGORIA == codcat select v;
if (!String.IsNullOrEmpty(searchString))
{
todos = todos.Where(s => s.TODO_TITLE.Contains(searchString)
|| s.DESCRIPTION.Contains(searchString)
);
}
switch (sortOrder)
{
case "title_desc":
todos = todos.OrderByDescending(s => s.TODO_TITLE);
break;
case "Date":
todos = todos.OrderBy(s => s.DATE);
break;
case "date_desc":
todos = todos.OrderByDescending(s => s.DATE);
break;
default:
todos = todos.OrderBy(s => s.TODO_TITLE);
break;
}
int pageSize = 21;
int pageNumber = (page ?? 1);
return View(todos.ToPagedList(pageNumber, pageSize));
}
The problem is that the codcat parameter is sent by the form as null.
How could I do to the form keeping the value of the parameter codcat and send the parameter research.
The one way is to use hidden fields.
<input type="hidden" name="codcat" value="#Request.QueryString["codcat"]" />
When submitting a form the POST request is sent to the server, and all your GET request's parameters are left behind.
OR to use this:
public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, object routeValues, FormMethod method);
OR this overload of BeginForm:
public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, FormMethod method, object htmlAttributes);

Radio Button Enum Helper for model with resource file

Problem:
Need to bind a strongly typed model which has a Gender as enum property. Also i like to show a Display text from a Resource file.
My Model is
public enum GenderViewModel
{
[Display(Name = "Male", ResourceType = typeof(Resources.Global), Order = 0)]
Male,
[Display(Name = "Female", ResourceType = typeof(Resources.Global), Order = 1)]
Female
}
Initially, I tried following http://romikoderbynew.com/2012/02/23/asp-net-mvc-rendering-enum-dropdownlists-radio-buttons-and-listboxes/
But it was bit complex and i was unable to correct my HTML however i want.
Then i had a look of simple and easy implementation from stackoverflow, pass enum to html.radiobuttonfor MVC3
and used a HtmlHelper in cshtml like below
#Html.RadioButtonForEnum(m => m.Gender)
HTML Produced
<label for="_Gender_Male">
<input type="radio" value="Male" name="Gender" id="_Gender_Male"
data-val-required="Gender is required" data-val="true" checked="checked">
<span class="radiotext">Male</span>
</label>
<label for="_Gender_Female">
<input type="radio" value="Female" name="Gender" id="_Gender_Female">
<span class="radiotext">Female</span></label>
It really simple and works well for me. But i am not getting values
from Resource files. My application is multilingual and I use a Global
Resource file for different language support.
Issue:
Male displayed should be Man and Female displayed should be Kvinna should be from Resource file, as my current culture is sv-se
Could any one help/ provide a simple solution which has a good control over HTML?
All you have to do is adapt my original helper so that it takes into account the DisplayAttribute:
public static class HtmlExtensions
{
public static MvcHtmlString RadioButtonForEnum<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression
)
{
var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
if (!metaData.ModelType.IsEnum)
{
throw new ArgumentException("This helper is intended to be used with enum types");
}
var names = Enum.GetNames(metaData.ModelType);
var sb = new StringBuilder();
var fields = metaData.ModelType.GetFields(
BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public
);
foreach (var name in names)
{
var id = string.Format(
"{0}_{1}_{2}",
htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix,
metaData.PropertyName,
name
);
var radio = htmlHelper.RadioButtonFor(expression, name, new { id = id }).ToHtmlString();
var field = fields.Single(f => f.Name == name);
var label = name;
var display = field
.GetCustomAttributes(typeof(DisplayAttribute), false)
.OfType<DisplayAttribute>()
.FirstOrDefault();
if (display != null)
{
label = display.GetName();
}
sb.AppendFormat(
"<label for=\"{0}\">{1}</label> {2}",
id,
HttpUtility.HtmlEncode(label),
radio
);
}
return MvcHtmlString.Create(sb.ToString());
}
}
Now if you have decorated some of the enum values with the DisplayAttribute, the values will come from the resource file.
You should replace in the extension method were it uses name for the <label> to use the resource you would like.
You should use a code kind of this one I adapted from here:
var type = typeof(metaData.ModelType);
var memInfo = type.GetMember(name);
var attributes = memInfo[0].GetCustomAttributes(typeof(DisplayAttribute), false);
var description = ((DisplayAttribute)attributes[0]).GetDescription();
And then put description into the <label>.
I've not tested it!

Knockout.js: Cannot bind to custom #Html.EditorFor

I am using a selector to build a custom #Html.EditorFor (called #Html.FullFieldEditor). It determines the type of input to generate (textbox, drop down, radio buttons, check boxes, etc.). I have been trying to hook it into one for a radio button list, thusly:
#Html.FullFieldEditor(m => m.MyModel.MyRadioButton, new Dictionary<string, object> { { "data_bind", "myRadioButton" } })
or like this:
#Html.FullFieldEditor(m => m.MyModel.MyRadioButton, new { data_bind = "checked: myRadioButton" })
But with no luck.
I was trying to fix my selector.cshtml code but only wound up making a horrible mess. Here is the code that has worked for me BEFORE I was trying to implement knockout.js:
#{
var supportsMany = typeof (IEnumerable).IsAssignableFrom(ViewData.ModelMetadata.ModelType);
var selectorModel = (Selector)ViewData.ModelMetadata.AdditionalValues["SelectorModelMetadata"];
var fieldName = ViewData.TemplateInfo.GetFullHtmlFieldName("");
var validationClass = ViewData.ModelState.IsValidField(fieldName) ? "" : "input-validation-error";
// Loop through the items and make sure they are Selected if the value has been posted
if(Model != null)
{
foreach (var item in selectorModel.Items)
{
if (supportsMany)
{
var modelStateValue = GetModelStateValue<string[]>(Html, fieldName) ?? ((IEnumerable)Model).OfType<object>().Select(m => m.ToString());
item.Selected = modelStateValue.Contains(item.Value);
}
else
{
var modelStateValue = GetModelStateValue<string>(Html, fieldName);
if (modelStateValue != null)
{
item.Selected = modelStateValue.Equals(item.Value, StringComparison.OrdinalIgnoreCase);
}
else
{
Type modelType = Model.GetType();
if (modelType.IsEnum)
{
item.Selected = item.Value == Model.ToString();
}
}
}
}
}
}
#functions
{
public MvcHtmlString BuildInput(string fieldName,
SelectListItem item, string inputType, object htmlAttributes)
// UPDATE: Trying to do it above
{
var id = ViewData.TemplateInfo.GetFullHtmlFieldId(item.Value);
var wrapper = new TagBuilder("div");
wrapper.AddCssClass("selector-item");
var input = new TagBuilder("input");
input.MergeAttribute("type", inputType);
input.MergeAttribute("name", fieldName);
input.MergeAttribute("value", item.Value);
input.MergeAttribute("id", id);
input.MergeAttributes(new RouteValueDictionary(htmlAttributes));
// UPDATE: and trying above, but see below in the
// #foreach...#BuildInput section
input.MergeAttributes(Html.GetUnobtrusiveValidationAttributes(fieldName, ViewData.ModelMetadata));
if(item.Selected)
input.MergeAttribute("checked", "checked");
wrapper.InnerHtml += input.ToString(TagRenderMode.SelfClosing);
var label = new TagBuilder("label");
label.MergeAttribute("for", id);
label.InnerHtml = item.Text;
wrapper.InnerHtml += label;
return new MvcHtmlString(wrapper.ToString());
}
/// <summary>
/// Get the raw value from model state
/// </summary>
public static T GetModelStateValue<T>(HtmlHelper helper, string key)
{
ModelState modelState;
if (helper.ViewData.ModelState.TryGetValue(key, out modelState) && modelState.Value != null)
return (T)modelState.Value.ConvertTo(typeof(T), null);
return default(T);
}
}
#if (ViewData.ModelMetadata.IsReadOnly)
{
var readonlyText = selectorModel.Items.Where(i => i.Selected).ToDelimitedString(i => i.Text);
if (string.IsNullOrWhiteSpace(readonlyText))
{
readonlyText = selectorModel.OptionLabel ?? "Not Set";
}
#readonlyText
foreach (var item in selectorModel.Items.Where(i => i.Selected))
{
#Html.Hidden(fieldName, item.Value)
}
}
else
{
if (selectorModel.AllowMultipleSelection)
{
if (selectorModel.Items.Count() < selectorModel.BulkSelectionThreshold)
{
<div class="#validationClass">
#foreach (var item in selectorModel.Items)
{
#BuildInput(fieldName, item, "checkbox") // throwing error here if I leave this as is (needs 4 arguments)
//But if I do this:
//#BuildInput(fieldName, item, "checkbox", htmlAttributes) // I get does not exit in current context
}
</div>
}
else
{
#Html.ListBox("", selectorModel.Items)
}
}
else if (selectorModel.Items.Count() < selectorModel.BulkSelectionThreshold)
{
<div class="#validationClass">
#*#if (selectorModel.OptionLabel != null)
{
#BuildInput(fieldName, new SelectListItem { Text = selectorModel.OptionLabel, Value = "" }, "radio")
}*#
#foreach (var item in selectorModel.Items)
{
#BuildInput(fieldName, item, "radio")//same here
}
</div>
}
else
{
#Html.DropDownList("", selectorModel.Items, selectorModel.OptionLabel)
}
}
Any help is greatly appreciated.
EDIT
I am trying to minimize existing JS code (over 1500 lines) to show/hide with KO (which just seems to be able to cut down the code considerably). I know I've got choices (visible, if, etc.) with KO, but assuming what I wanted to accomplish with KO was doable I could go with visible. In any event, getting past the binding hurdle prevents me from getting that far.
Here is an example of some code I am using to show/hide with plain JS:
$(document).ready(function () {
$("input[name$='MyModel.MyRadioButton']").click(function () {
var radio_value = $(this).val();
if (radio_value == '1') {
$("#MyRadioButton_1").show();
$("#MyRadioButton_2").hide();
$("#MyRadioButton_3").hide();
}
else if (radio_value == '2') {
$("#MyRadioButton_1").show();
$("#MyRadioButton_2").hide();
$("#MyRadioButton_3").hide();
}
else if (radio_value == '3') {
$("#MyRadioButton_1").show();
$("#MyRadioButton_2").hide();
$("#MyRadioButton_3").hide();
}
});
$("#MyRadioButton_1").hide();
$("#MyRadioButton_2").hide();
$("#MyRadioButton_3").hide();
});
I figured KO could minimize the above. Again, I'm looking at 20-30 inputs, most with more than 3 choices (a few have 10 choices in a Drop Down). This is getting hard to maintain at 1500 lines and growing.
And then in my view I've got this going on:
<div id="MyRadioButton_1">
#Helpers.StartingCost(MyModel.Choice1, "1")
</div>
<div id="MyRadioButton_2">
#Helpers.StartingCost(MyModel.Choice2, "2")
</div>
<div id="MyRadioButton_3">
#Helpers.StartingCost(MyModel.Choice2, "2")
</div>
The view code above will change slightly with KO, but again its the JS I am trying to cut down on.
EDIT 2
This is part of the code for FullFieldEditor. Some parts are left out for brevity (such as code for RequiredFor, ToolTipFor and SpacerFor).
public static MvcHtmlString FullFieldEditor<T, TValue>(this HtmlHelper<T> html, Expression<Func<T, TValue>> expression)
{
return FullFieldEditor(html, expression, null);
}
public static MvcHtmlString FullFieldEditor<T, TValue>(this HtmlHelper<T> html, Expression<Func<T, TValue>> expression, object htmlAttributes)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
if (!metadata.ShowForEdit)
{
return MvcHtmlString.Empty;
}
if (metadata.HideSurroundingHtml)
{
return html.EditorFor(expression);
}
var wrapper = new TagBuilder("div");
wrapper.AddCssClass("field-wrapper");
var table = new TagBuilder("table");
table.Attributes["border"] = "0";
table.Attributes["width"] = "100%";//added this to even out table columns
var tbody = new TagBuilder("tbody");
var tr = new TagBuilder("tr");
var td1 = new TagBuilder("td");
td1.Attributes["width"] = "40%";
td1.Attributes["valign"] = "top";
var label = new TagBuilder("div");
label.AddCssClass("field-label");
label.AddCssClass("mylabelstyle");
label.InnerHtml += html.MyLabelFor(expression);
td1.InnerHtml = label.ToString();
var td2 = new TagBuilder("td");
td2.Attributes["width"] = "50%";
td2.Attributes["valign"] = "top";
var input = new TagBuilder("div");
input.AddCssClass("field-input");
input.InnerHtml += html.EditorFor(expression);
td2.InnerHtml = input.ToString();
var td3 = new TagBuilder("td");
td3.Attributes["width"] = "5%";
td3.Attributes["valign"] = "top";
if (metadata.IsRequired && !metadata.IsReadOnly)
{
td3.InnerHtml += html.RequiredFor(expression);
}
var td4 = new TagBuilder("td");
td4.Attributes["width"] = "5%";
td4.Attributes["valign"] = "middle";
if (!string.IsNullOrEmpty(metadata.Description))
{
td4.InnerHtml += html.TooltipFor(expression);
}
else td4.InnerHtml += html.SpacerFor(expression);
td4.InnerHtml += html.ValidationMessageFor(expression);
tr.InnerHtml = td1.ToString() + td2.ToString() + td3.ToString() + td4.ToString();
tbody.InnerHtml = tr.ToString();
table.InnerHtml = tbody.ToString();
wrapper.InnerHtml = table.ToString();
return new MvcHtmlString(wrapper + Environment.NewLine);
}
UPDATE 3
The options are not working. Option 1 will not even show data-bind in the <input>. Option 2 will not work since it's just checking if the field is required (the code just shows a "required" image if it is).
When I tried your first suggestion before your "UPDATE2" (input.MergeAttributes(new RouteValueDictionary(htmlAttributes));), this was the output:
<div class="field-input" data_bind="checked: MyRadioButton">
<div class="">
<div class="selector-item">
<input id="MyModel_MyRadioButton_Choice1" name="MyModel.MyRadioButton" type="radio" value="Choice1">
<label for="MyModel_MyRadioButton_Choice1">I am thinking about Choice 1.</label>
</div>
<!--Just showing one radio button for brevity-->
</div>
</div>
Since I merged the attribute with the input part of TagBuilder, which is outputting the field-input <div>, that is where it's being placed (which is logical). Notice that it should be data-bind but is showing as data_bind in the field-input class. This is how I have the FullFieldEditor:
#Html.FullFieldEditor(m => m.MyModel.MyRadioButton, new Dictionary<string, object> { { "data_bind", "myRadioButton" } })
What it should be showing up as is this, I think:
<div class="field-input">
<div class="">
<div class="selector-item">
<!-- "data-bind" should be showing up in the following INPUT, correct?-->
<input data-bind="checked: MyRadioButton" id="MyModel_MyRadioButton_Choice1" name="MyModel.MyRadioButton" type="radio" value="Choice1">
<label for="MyModel_MyRadioButton_Choice1">I am thinking about Choice 1.</label>
</div>
<!--Just showing one radio button for brevity-->
</div>
</div>
What I suspect is that I have to get that htmlAttributes into the Selector.cshtml above, and not in the HtmlFormHelper.cs file. The Selector.cshtml is what is making the determination between showing, for example, a drop down list, a radio button list or a checkbox list (among others). The Selector.cshtml is a template in the Shared\EditorTemplates folder.
For background: I have dozens of forms representing hundreds of inputs over dozens of pages (or wizards). I am using the #Html.FullFieldEditor because it was easier to maintain than having spaghetti code for each type of input (drop down, checkbox, radio buttons, etc.).
UPDATE 4
Still not working.
I tried this in the Selector.cshtml (its the BuildInput function)code and was able to get "data-bind" into the <input> tag for each radio button in the list:
input.MergeAttribute("data-bind", htmlAttributes);
and then I did this lower down in the same file:
#foreach (var item in selectorModel.Items)
{
#BuildInput(fieldName, item, "radio", "test")
}
and my HTML output is this:
<div class="selector-item">
<input data-bind="test" id="MyModel_MyRadioButton_Choice1" name="MyModel.MyRadioButton" type="radio" value="Choice1">
<label for="MyModel_MyRadioButton_Choice1">Choice 1</label>
</div>
Which is what is leading me to believe it's the Selector.cshtml file, not the HtmlFormHelper.cs file.
I am going to open up the bounty to everyone 50+.
UPDATE3
First good call on the underscore bit, I totally forgot about that. Your bit of code looks almost right, but should actually be this:
#Html.FullFieldEditor(m => m.MyModel.MyRadioButton, new Dictionary<string, object> { { "data_bind", "checked:myRadioButton" } })
So since you are dynamically selecting between checkbox and text, you'll have to do a couple of things. If its a checkbox, you'll have to use the code above. If its a textbox, you'll have to use:
#Html.FullFieldEditor(m => m.MyModel.MyTextBox, new Dictionary<string, object> { { "data_bind", "value:MyTextBox" } })
UPDATE2
So I updated your code where I think the data-bind belongs in your html (marked option1 and option2). What would be helpful is if you gave me a snippet of the html being generated, along with where you need the data bind.
public static MvcHtmlString FullFieldEditor<T, TValue>(this HtmlHelper<T> html, Expression<Func<T, TValue>> expression)
{
return FullFieldEditor(html, expression, null);
}
public static MvcHtmlString FullFieldEditor<T, TValue>(this HtmlHelper<T> html, Expression<Func<T, TValue>> expression, object htmlAttributes)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
if (!metadata.ShowForEdit)
{
return MvcHtmlString.Empty;
}
if (metadata.HideSurroundingHtml)
{
return html.EditorFor(expression);
}
var wrapper = new TagBuilder("div");
wrapper.AddCssClass("field-wrapper");
var table = new TagBuilder("table");
table.Attributes["border"] = "0";
//added this to even out table columns
table.Attributes["width"] = "100%";
var tbody = new TagBuilder("tbody");
var td1 = new TagBuilder("td");
td1.Attributes["width"] = "40%";
td1.Attributes["valign"] = "top";
var label = new TagBuilder("div");
label.AddCssClass("field-label");
label.AddCssClass("mylabelstyle");
label.InnerHtml += html.MyLabelFor(expression);
td1.InnerHtml = label.ToString();
var td2 = new TagBuilder("td");
td2.Attributes["width"] = "50%";
td2.Attributes["valign"] = "top";
var input = new TagBuilder("div");
input.AddCssClass("field-input");
// option1
input.InnerHtml += html.EditorFor(expression, htmlAttributes);
td2.InnerHtml = input.ToString();
var td3 = new TagBuilder("td");
td3.Attributes["width"] = "5%";
td3.Attributes["valign"] = "top";
if (metadata.IsRequired && !metadata.IsReadOnly)
{
// option2
td3.InnerHtml += html.RequiredFor(expression, htmlAttributes);
}
var td4 = new TagBuilder("td");
td4.Attributes["width"] = "5%";
td4.Attributes["valign"] = "middle";
if (!string.IsNullOrEmpty(metadata.Description))
{
td4.InnerHtml += html.TooltipFor(expression);
}
else
{
td4.InnerHtml += html.SpacerFor(expression);
}
td4.InnerHtml += html.ValidationMessageFor(expression);
var tr = new TagBuilder("tr");
tr.InnerHtml = td1.ToString() + td2.ToString() + td3.ToString() + td4.ToString();
tbody.InnerHtml = tr.ToString();
table.InnerHtml = tbody.ToString();
wrapper.InnerHtml = table.ToString();
return new MvcHtmlString(wrapper + Environment.NewLine);
}
UPDATE
While I still believe below is the "correct" answer, here is what your tagbuilder is missing so you can pass custom attributes along:
input.MergeAttributes(new RouteValueDictionary(htmlAttributes));
Original Answer
I have a feeling that what is going to happen by trying to mix razor and knockout, is the razor stuff will render, then when knockout is attached, the values in the knockout viewmodel are going to override whatever was in the razor view.
Here is my suggestion if you are trying to refactor to knockout:
Create an html only view (no razor).
Add your knockout bindings to it.
Pass your model data to knockout in the form of a json object using Json.NET. You can do this in a variety of ways, but the easiest being simply stuffing the data on the view inside of a <script> tag that your javascript can find.
Use Knockout Mapping to load the json object into the viewmodel.
Not sure I have understood your difficulty. But if your difficulty is where and how to place the data-bind depending of the input fields you used to render your property
give a look to the ClientBlocks features of the Mvc Controls Toolkit. It computes most of the binding automatically, by precompiling templates(that is razor helpers or PartialViews that display pieces of the page), or the full view.
That is you can just write: #Html.TextBoxFor(m => m.myProperty) and an adequate data-bind between the textbox and the myProperty of your client side ViewModel will be created, and so on with selects, radios etc. You can also to avoid using helper and just give your fileds "adequate names" and the binding is automatically computed. Moreover you can decide wichh part of the Server Side ViewModel to transfer to the client...your client side ViewmOdel will be transferred back inside the Server Side ViewModel automatically on post.

Resources