在ASP.NET MVC中具有动态数量的texbox的表单

时间:2011-03-08 17:46:31

标签: asp.net-mvc

考虑以下模型和控制器:

public class SimpleModel
{
    [Required(ErrorMessage="Email Address is required.")]
    [DataType(DataType.EmailAddress)]
    [DisplayName("EmailAddress")]
    public string EmailAddress { get; set; }
}

[HandleError]
public class SimpleController : Controller
{
    public ActionResult Simple()
    {
        return View();
    }

    [HttpPost]
    public ActionResult Simple(SimpleModel model)
    {
        if (ModelState.IsValid)
        {
            // handling code here
        }

        return View(model);
    }
}

...以及匹配视图中的相关部分:

    <% using (Html.BeginForm()) { %>
    <%= Html.ValidationSummary(true, "The form submitted is not valid.") %>
    <div> 
        <fieldset>
            <div class="editor-label">
                <%= Html.LabelFor(m => m.EmailAddress)%>
            </div>
            <div class="editor-field">
                <%= Html.TextBoxFor(m => m.EmailAddress)%>
                <%= Html.ValidationMessageFor(m => m.EmailAddress)%>
            </div>

            <div class="editor-field">
                <input type="submit" value="Submit" />
            </div>
        </fieldset>
    </div>
<% } %>

修改模型,视图和控制器以支持控制器定义的动态数量的电子邮件地址的最佳方法是什么。

3 个答案:

答案 0 :(得分:7)

根据Steve Sanderson的文章,我找到了我想要的优雅解决方案:

首先,需要对模型进行如下修改:

public class SimpleModel
{
    public IEnumerable<EmailAddress> EmailAddresses { get; set; }
}

public class EmailAddress
{
    [Required(ErrorMessage = "Email Address is required.")]
    [DataType(DataType.EmailAddress)]
    [DisplayName("Email Address")]
    public string Value { get; set; }        
}

处理GET方法的控制器方法需要根据需要使用尽可能多的条目预填充模型:

[HandleError]
public class SimpleController : Controller
{
    public ActionResult Simple()
    {
        SimpleModel model = new SimpleModel
            {
                EmailAddresses =
                    new List<EmailAddress>
                        { 
                            // as many as required
                            new EmailAddress { Value = string.Empty }, 
                            new EmailAddress { Value = string.Empty },
                            new EmailAddress { Value = string.Empty }
                        }
            };
        return View(model);
    }

    [HttpPost]
    public ActionResult Simple(SimpleModel model)
    {
        if (ModelState.IsValid)
        {
            // handling code here
        }

        return View(model);
    }
}

视图还需要更改:

    <% using (Html.BeginForm()) { %>
    <%= Html.ValidationSummary(true, "The form submitted is not valid.") %>
    <div> 
        <fieldset>
            <% foreach (var item in Model.EmailAddresses)
                   Html.RenderPartial("SimpleRows", item);
             %>

            <div class="editor-field">
                <input type="submit" value="Submit" />
            </div>

        </fieldset>
    </div>
<% } %>

...需要创建一个新的局部视图。请注意,视图是强类型的,具有集合项的类型。

<% using(Html.BeginCollectionItem("EmailAddresses")) { %>
<div class="editor-label">
    <%= Html.LabelFor(x => x.Value)%>
</div>
<div class="editor-field">
    <%= Html.TextBoxFor(x => x.Value)%>
    <%= Html.ValidationMessageFor(x => x.Value)%>
</div>
<% }%>

BeginCollectionItem是由Sanderson创建的Helper方法:

    public static class HtmlPrefixScopeExtensions
{
    private const string idsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";

    public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName)
    {
        var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
        string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();

        // autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
        html.ViewContext.Writer.WriteLine(string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />", collectionName, html.Encode(itemIndex)));

        return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
    }

    public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix)
    {
        return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
    }

    private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName)
    {
        // We need to use the same sequence of IDs following a server-side validation failure,  
        // otherwise the framework won't render the validation error messages next to each item.
        string key = idsToReuseKey + collectionName;
        var queue = (Queue<string>)httpContext.Items[key];
        if (queue == null) {
            httpContext.Items[key] = queue = new Queue<string>();
            var previouslyUsedIds = httpContext.Request[collectionName + ".index"];
            if (!string.IsNullOrEmpty(previouslyUsedIds))
                foreach (string previouslyUsedId in previouslyUsedIds.Split(','))
                    queue.Enqueue(previouslyUsedId);
        }
        return queue;
    }

    private class HtmlFieldPrefixScope : IDisposable
    {
        private readonly TemplateInfo templateInfo;
        private readonly string previousHtmlFieldPrefix;

        public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
        {
            this.templateInfo = templateInfo;

            previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;
            templateInfo.HtmlFieldPrefix = htmlFieldPrefix;
        }

        public void Dispose()
        {
            templateInfo.HtmlFieldPrefix = previousHtmlFieldPrefix;
        }
    }
}

...就是这样......当您发布表单时,模型将自动填充并传递给处理POST方法的控制器操作。

请注意,使用此解决方案,包括验证在内的所有属性都按预期工作。

答案 1 :(得分:0)

public class SimpleModel
{
    [Required(ErrorMessage = "Email Address is required.")]
    [DataType(DataType.EmailAddress)]
    [DisplayName("EmailAddress")]
    public List<string> EmailAddress { get; set; }
}

[HandleError]
public class SimpleController : Controller
{
    public ActionResult SimpleTest()
    {
        SimpleModel model = new SimpleModel();
        model.EmailAddress = new List<string>();
        model.EmailAddress.Add("email1");
        model.EmailAddress.Add("email2");
        return View(model);
    }

    [HttpPost]
    public ActionResult SimpleTest(FormCollection formvalues)
    {
        if (ModelState.IsValid)
        {
            // handling code here
        }
        SimpleModel model = new SimpleModel();
        model.EmailAddress = new List<string>();
        model.EmailAddress.Add("email1");
        model.EmailAddress.Add("email2");
        return View();
    }
}

...以及匹配视图中的相关部分:

 <% using (Html.BeginForm())
   {%>
<%: Html.ValidationSummary(true)%>
<fieldset>
    <legend>Fields</legend>
    <%{
          foreach (var i in Model.EmailAddress)
          { %>
    <div class="editor-label">
        <%: Html.LabelFor(model => model.EmailAddress)%>
    </div>
    <div class="editor-field">
        <%: Html.TextBoxFor(model =>i)%>
        <br />
    </div>
    <%}
      }%>
    <p>
        <input type="submit" value="Create" />
    </p>
</fieldset>
<% } %>

我不认为验证会起作用。

答案 2 :(得分:0)

我也将@JCallico的答案标记为正确。

对于任何顽固的VB编码器,这里是史蒂夫桑德森用你最喜欢的语言的扩展方法:

 'from http://stackoverflow.com/questions/5236251/form-with-dynamic-number-of-texboxes-in-asp-net-mvc based on http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/

    Imports System.IO
    Imports System.Web.Mvc
    Imports System.Web
    Imports System.Collections.Generic

Namespace HtmlHelpers.BeginCollectionItem
    Public Module HtmlPrefixScopeExtensions
        Private Const IdsToReuseKey As String = "__htmlPrefixScopeExtensions_IdsToReuse_"

        <System.Runtime.CompilerServices.Extension> _
        Public Function BeginCollectionItem(html As HtmlHelper, collectionName As String) As IDisposable
            Return BeginCollectionItem(html, collectionName, html.ViewContext.Writer)
        End Function

        <System.Runtime.CompilerServices.Extension> _
        Public Function BeginCollectionItem(html As HtmlHelper, collectionName As String, writer As TextWriter) As IDisposable
            Dim idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName)
            Dim itemIndex = If(idsToReuse.Count > 0, idsToReuse.Dequeue(), Guid.NewGuid().ToString())

            ' autocomplete="off" is needed to work around a very annoying Chrome behaviour
            ' whereby it reuses old values after the user clicks "Back", which causes the
            ' xyz.index and xyz[...] values to get out of sync.
            writer.WriteLine("<input type=""hidden"" name=""{0}.index"" autocomplete=""off"" value=""{1}"" />", collectionName, html.Encode(itemIndex))

            Return BeginHtmlFieldPrefixScope(html, String.Format("{0}[{1}]", collectionName, itemIndex))
        End Function

        <System.Runtime.CompilerServices.Extension> _
        Public Function BeginHtmlFieldPrefixScope(html As HtmlHelper, htmlFieldPrefix As String) As IDisposable
            Return New HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix)
        End Function

        Private Function GetIdsToReuse(httpContext As HttpContextBase, collectionName As String) As Queue(Of String)
            ' We need to use the same sequence of IDs following a server-side validation failure,
            ' otherwise the framework won't render the validation error messages next to each item.
            Dim key = IdsToReuseKey & collectionName
            Dim queue = DirectCast(httpContext.Items(key), Queue(Of String))
            If queue Is Nothing Then
                httpContext.Items(key) = InlineAssignHelper(queue, New Queue(Of String)())
                Dim previouslyUsedIds = httpContext.Request(collectionName & Convert.ToString(".index"))
                If Not String.IsNullOrEmpty(previouslyUsedIds) Then
                    For Each previouslyUsedId In previouslyUsedIds.Split(","c)
                        queue.Enqueue(previouslyUsedId)
                    Next
                End If
            End If
            Return queue
        End Function

        Friend Class HtmlFieldPrefixScope
            Implements IDisposable
            Friend ReadOnly TemplateInfo As TemplateInfo
            Friend ReadOnly PreviousHtmlFieldPrefix As String

            Public Sub New(templateInfo__1 As TemplateInfo, htmlFieldPrefix As String)
                TemplateInfo = templateInfo__1

                PreviousHtmlFieldPrefix = TemplateInfo.HtmlFieldPrefix
                TemplateInfo.HtmlFieldPrefix = htmlFieldPrefix
            End Sub

            Public Sub Dispose() Implements System.IDisposable.Dispose
                TemplateInfo.HtmlFieldPrefix = PreviousHtmlFieldPrefix
            End Sub
        End Class
        Private Function InlineAssignHelper(Of T)(ByRef target As T, value As T) As T
            target = value
            Return value
        End Function
    End Module
End Namespace

我很高兴与Value Injector一起使用。