Нокаут.js: не удается выполнить привязку к пользовательскому @Html.В
Я использую селектор для построения пользовательского @Html.EditorFor (называется @Html.FullFieldEditor). Он определяет тип входных данных для генерации (текстовое поле, выпадающее меню, переключатели, флажки и т. д.). Я пытался подключить его к одному для списка переключателей, таким образом:
@Html.FullFieldEditor(m => m.MyModel.MyRadioButton, new Dictionary<string, object> { { "data_bind", "myRadioButton" } })
Или вот так:
@Html.FullFieldEditor(m => m.MyModel.MyRadioButton, new { data_bind = "checked: myRadioButton" })
Но безрезультатно.
Я пытался исправить свой код selector.cshtml
, но только в итоге сделал ужасный беспорядок. Вот код, который работал на меня до того, как я пытался реализовать 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)
}
}
Любая помощь с благодарностью.
EDIT
Я пытаюсь минимизировать существующий код JS
(более 1500 строк), чтобы показать/скрыть с помощью KO (который, похоже, способен значительно сократить код). Я знаю, что у меня есть выбор.(visible
, if
, и т.д.) с Ко, но если предположить, что то, что я хотел сделать с Ко, было выполнимо, я мог бы пойти с visible
. В любом случае, преодоление связующего барьера мешает мне зайти так далеко.
Вот пример: пример кода, который я использую, чтобы показать / скрыть с помощью 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();
});
Я полагал, что KO может свести все вышесказанное к минимуму. Опять же, я смотрю на 20-30 входов, большинство из которых имеют более 3 вариантов (некоторые имеют 10 вариантов в выпадающем списке). Это становится трудно поддерживать на 1500 линиях и растет.
А потом в моем view
у меня происходит вот что:
<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>
Код view
выше немного изменится с KO, но снова его JS
я пытаюсь сократить.
Править 2
Это часть кода для FullFieldEditor
. Некоторые части опущены для краткости (например, код для RequiredFor
, ToolTipFor
и 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);
}
Обновление 3
Варианты не работают. Вариант 1 даже не будет показывать data-bind
В <input>
. Вариант 2 не будет работать, так как он просто проверяет, является ли поле обязательным (код просто показывает "обязательное" изображение, если это так).
Когда я попробовал ваше первое предложение перед вашим "UPDATE2" (input.MergeAttributes(new RouteValueDictionary(htmlAttributes));
), это был тот самый вывод:
<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>
Так как я слил атрибут с input
частью TagBuilder
, которая выводит field-input
<div>
, вот где он находится (что логично). Обратите внимание, что он должен быть data-bind
, но отображается как data_bind
в классе field-input
. Вот как у меня получается FullFieldEditor
:
@Html.FullFieldEditor(m => m.MyModel.MyRadioButton, new Dictionary<string, object> { { "data_bind", "myRadioButton" } })
То, что он должен показать, как это, я думаю:
<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>
Что я подозреваю, так это то, что я должен поместить это htmlAttributes
в Selector.cshtml
выше, а не в файл HtmlFormHelper.cs
. The Selector.cshtml
- это то, что делает выбор между отображением, например, выпадающего списка, списка переключателей или списка флажков (среди других). Selector.cshtml
- это шаблон в папке SharedEditorTemplates
.
Для фона: у меня есть десятки форм, представляющих сотни входных данных на десятках страниц (или мастеров). Я использую @Html.FullFieldEditor
, потому что это было проще поддерживать, чем иметь спагетти-код для каждого типа ввода (выпадающий список, флажок, переключатели и т. д.).
Обновление 4 Все еще нет работающий.
Я попробовал это в коде Selector.cshtml
(его функция BuildInput
) и смог получить "привязку данных" в тег <input>
для каждой радиокнопки в списке:
input.MergeAttribute("data-bind", htmlAttributes);
И затем я сделал это ниже в том же файле:
@foreach (var item in selectorModel.Items)
{
@BuildInput(fieldName, item, "radio", "test")
}
И мой вывод HTML таков:
<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>
Именно это заставляет меня думать, что это файл Selector.cshtml
, а не файл HtmlFormHelper.cs
.
Я собираюсь открыть щедрость для всех 50+.
2 ответа:
UPDATE3
Первый хороший звонок на бит подчеркивания, я совсем забыл об этом. Ваш бит кода выглядит почти правильно, но на самом деле должен быть таким:
@Html.FullFieldEditor(m => m.MyModel.MyRadioButton, new Dictionary<string, object> { { "data_bind", "checked:myRadioButton" } })
Так как вы динамически выбираете между флажком и текстом, вам придется сделать несколько вещей. Если это флажок, вам придется использовать приведенный выше код. Если это текстовое поле, вам придется использовать:
@Html.FullFieldEditor(m => m.MyModel.MyTextBox, new Dictionary<string, object> { { "data_bind", "value:MyTextBox" } })
UPDATE2
Поэтому я обновил ваш код, где я думай тот самый data-bind принадлежит вашему html (помечено option1 и option2). Что было бы полезно, если бы вы дали мне фрагмент генерируемого html, а также где вам нужно привязать данные.
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); }
Обновить
Хотя я все еще считаю, что ниже приведен "правильный" ответ, вот чего не хватает вашему tagbuilder, чтобы вы могли передавать пользовательские атрибуты:
input.MergeAttributes(new RouteValueDictionary(htmlAttributes));
Оригинальный Ответ
У меня есть ощущение, что то, что произойдет, пытаясь смешать razor и нокаут, это материал бритвы будет визуализироваться, а затем, когда нокаут присоединен, значения в knockout viewmodel будут переопределять все, что было в представлении бритвы.
Вот мое предложение, если вы пытаетесь рефакторинговать нокаут:
- создайте представление только html (без razor).
- добавьте к нему свои нокаутирующие привязки.
- передайте данные модели в knockout в виде объекта json с помощью Json.NET . это можно сделать различными способами, но проще всего просто набить данные о представлении внутри тега
<script>
, который может найти ваш javascript.- используйте нокаут-отображение для загрузки объекта json в viewmodel.
Не уверен, что понял вашу трудность. Но если ваша трудность заключается в том, где и как разместить привязку данных в зависимости от полей ввода, которые вы использовали для отображения вашего свойства рассмотрим возможностиClientBlocks инструментарияMVC Controls Toolkit . Он вычисляет большую часть привязки автоматически, путем предварительной компиляции шаблонов (то есть помощников razor или PartialViews, отображающих фрагменты страницы) или полного представления. То есть вы можете просто написать: @Html.TextBoxFor(m => m. myProperty) и будет создана адекватная привязка данных между текстовым полем и myProperty Вашей клиентской модели ViewModel, и так далее с помощью selects, Radio и т. д. Вы также можете избежать использования helper и просто дать вашим файлам "адекватные имена", и привязка автоматически вычисляется. Кроме того, вы можете решить, какую часть серверной части ViewModel передать клиенту...вашей стороне клиента модель представления будет передан обратно в модель представления сервером автоматически на пост.