【问题标题】:Knockout.js: Cannot bind to custom @Html.EditorForKnockout.js:无法绑定到自定义 @Html.EditorFor
【发布时间】:2012-04-19 18:23:12
【问题描述】:

我正在使用选择器来构建自定义的@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)
}
}

非常感谢任何帮助。

编辑 我正在尝试最小化现有的JS 代码(超过 1500 行)以使用 KO 显示/隐藏(这似乎能够大大减少代码)。我知道我有 KO 的选择(visibleif 等),但假设我想用 KO 完成的事情是可行的,我可以选择visible。无论如何,越过约束障碍会阻止我走得那么远。

这是我用来显示/隐藏纯 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 代码的一部分。为简洁起见,省略了某些部分(例如 RequiredForToolTipForSpacerFor 的代码)。

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 甚至不会在 &lt;input&gt; 中显示 data-bind。选项 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>

由于我将属性与TagBuilderinput 部分合并,输出field-input &lt;div&gt;,这就是放置它的位置(这是合乎逻辑的)。请注意,它应该是 data-bind,但在 field-input 类中显示为 data_bind。这就是我拥有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 文件中。 Selector.cshtml 是在显示例如下拉列表、单选按钮列表或复选框列表(以及其他)之间做出决定的原因。 Selector.cshtmlShared\EditorTemplates 文件夹中的模板。

对于背景:我有几十个表单,代表几十个页面(或向导)上的数百个输入。我正在使用@Html.FullFieldEditor,因为它比为每种类型的输入(下拉菜单、复选框、单选按钮等)使用意大利面条代码更容易维护。

更新 4 还是不行。

我在Selector.cshtml(它的BuildInput函数)代码中尝试了这个,并且能够将“数据绑定”放入列表中每个单选按钮的&lt;input&gt;标签中:

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 岁以上的人开放赏金。

【问题讨论】:

  • 您找到解决问题的方法了吗?
  • @JasonMore 不。不确定我是否在正确的轨道上查看我的Selector.cshmtl 而不是HtmlFormHelper.cs。如果您查看我的“Update 4”,我能够将字符串放在正确的位置。只需弄清楚如何传递数据绑定。我不是程序员,所以更细微的东西让我忽略了。
  • 我想我不确定您现在遇到了什么问题,因为您说数据绑定在正确的位置。
  • @JasonMore 但我传递的只是一个简单的字符串。我将我在“更新 4”中提到的htmlAttributes 设为string,而不是您之前建议的object。如果我做object 而不是当我做@foreach . . . { @BuildInput(fieldName, item, "radio", htmlAttributes) } 时,我会得到htmlAttributes 部分的“在当前上下文中不存在”。如果我忽略它会引发错误,因为它需要 4 个参数。

标签: asp.net-mvc asp.net-mvc-3 knockout.js knockout-2.0


【解决方案1】:

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" } })

更新2

所以我更新了您的代码,我认为数据绑定属于您的 html(标记为 option1 和 option2)。如果你给我一个正在生成的 html 的 sn-p 以及你需要数据绑定的位置,那将会很有帮助。

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);
}

更新

虽然我仍然相信下面是“正确”的答案,但这是您的标签生成器缺少的内容,因此您可以传递自定义属性:

input.MergeAttributes(new RouteValueDictionary(htmlAttributes));

原答案

我有一种感觉,尝试混合 razor 和 knockout 会发生什么,是 razor 的东西会渲染,然后当附加了 knockout 时,knockout 视图模型中的值将覆盖 razor 视图中的值.

如果你想重构淘汰赛,这是我的建议:

  1. 创建仅 html 视图(无剃刀)。
  2. 将您的淘汰赛绑定添加到其中。
  3. 使用Json.NET 将模型数据以json 对象的形式传递给knockout。您可以通过多种方式执行此操作,但最简单的方法是将数据填充到您的 javascript 可以找到的 &lt;script&gt; 标记内的视图中。
  4. 使用Knockout Mapping 将json 对象加载到视图模型中。

【讨论】:

  • 虽然我明白你的意思,但现在我关心的是操纵页面上的用户输入。我没有保存它,而是在向导中将它传递到最终的“确认”页面。我真的很想最小化我用来完成显示/隐藏&lt;div&gt;元素的JS。我认为KO可以减少线条。现在我有一个单独的.js 文件,其中有 1500 行代码要显示/隐藏。参见上面的编辑示例。
  • 是的,在这种情况下淘汰赛将是完美的。问题是您需要开始将视图逻辑分成多个视图模型,然后使用 foreach 和模板重用代码。你去过learn.knockoutjs.com吗?对你有很大帮助。
  • 是的,这就是我开始的地方。我只是无法将它连接到我的自定义 EditorFor
  • 不幸的是,这有效,但只是将"data-bind" 放在field-input &lt;div&gt; 中,而不是放在需要的&lt;input&gt; 中。我试图在位于我的Selector.cshtmlTagBuilder 中使用您的代码(我在我的问题中发布的第一个大代码块),但无济于事。我将用我尝试过的代码标记该代码(给我几分钟)。
  • 您找到解决方案了吗?
【解决方案2】:

不确定我是否理解您的困难。但是,如果您的困难在于根据用于呈现属性的输入字段在何处以及如何放置数据绑定 看看Mvc Controls ToolkitClientBlocks 功能。它通过预编译模板(即显示页面片段的 razor 助手或 PartialViews)或完整视图来自动计算大部分绑定。 那就是您可以只写:@Html.TextBoxFor(m => m.myProperty) 并且将创建文本框和客户端 ViewModel 的 myProperty 之间的适当数据绑定,等等,选择、收音机等。你也可以避免使用帮助程序,只需为您的文件提供“适当的名称”,然后自动计算绑定。此外,您可以决定将服务器端 ViewModel 的哪一部分传输到客户端......您的客户端 ViewmOdel 将在发布时自动传输回服务器端 ViewModel 中。

【讨论】:

  • 这没什么用。在我自己的范式之上,我有一个范式 (KO) 需要应对,而您的解决方案是检查第三个范式?
  • 它不是 3d 范例,它是一种基于命名约定自动生成“足够”的 ko 数据绑定属性的方法。这样,在您已经构建的局部视图中,无需过多修改即可将没有 ko 的东西转换为使用 ko 的东西应该更容易。另一方面,您应该使用 ko 模板以及“with”和“if”ko 模板运算符来简化您的 js。显然,如果您的使用仅限于 EditorFor
猜你喜欢
  • 2013-11-07
  • 2016-08-25
  • 1970-01-01
  • 1970-01-01
  • 2015-05-15
  • 1970-01-01
  • 2012-10-31
  • 2011-11-14
  • 2014-07-09
相关资源
最近更新 更多