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+.
Selector.cshmtlrather than theHtmlFormHelper.cs. If you look at my "Update 4" I was able to put a string in the right place. Just a matter of figuring out how to pass the data-bind. I'm not a programmer, so the more nuanced stuff escapes me.htmlAttributesthat I noted in my "Update 4" astringinstead of anobjectlike you had suggested earlier on. If I doobjectinstead when I do the@foreach . . . { @BuildInput(fieldName, item, "radio", htmlAttributes) }I get a "does not exist in the current context" for thehtmlAttributespart. If I leave it out it throws an error because it's expecting 4 parameters.