Dynamic texboxes form in ASP.NET MVC

Consider the following model and controller:

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

... and the corresponding section in the corresponding submission:

    <% 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>
<% } %>

What would be the best way to change the model, view, and controller to support the dynamic number of email addresses defined by the controller.

+3
source share
3 answers

Based on the article by Steve Sanderson, I was able to find the elegant solution I was looking for:

First of all, the model should be modified as follows:

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

The controller method that processes the GET method must preinstall the model with as many entries as required:

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

The view also needs to be changed:

    <% 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:

    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.

, , , .

+7
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>
<% } %>

, .

0

@JCallico .

VB- Steve Sanderson :

 '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.

0

All Articles