Bonner Bonner - 6 months ago 17
Ajax Question

TagBuilder not generating unqiue id attribute values across ajax requests

I have an issue where I'm using the same template to render some content on a page and the same template is used to render additional content on the page using an AJAX request.

The following code renders the partial razor view:

@model SearchBoxViewModel
<div class="smart-search">
@using (Html.BeginForm("Index", "Search", FormMethod.Get, new { @class = "form-horizontal", role = "form" }))
{
<div class="form-group">
<div class="hidden-xs col-sm-1 col-md-1 col-lg-1 text-right">
@Html.LabelFor(m => m.SearchPhrase, new { @class = "control-label heading" })
</div>
<div class="col-xs-8 col-md-9 col-lg-10">
@Html.TextBoxFor(m => m.SearchPhrase, new {@class = "form-control", placeholder = "for products, companies, or therapy areas"})
</div>
<div class="col-xs-4 col-sm-3 col-md-2 col-lg-1">
<input type="submit" value="Search" class="btn btn-default"/>
</div>
<div class="what-to-search hidden-11 col-sm-11 col-sm-offset-1">
<div class="checkbox">
<label>
@Html.CheckBoxFor(m => m.WhatToSearch, null, false)
@Html.DisplayNameFor(m => m.WhatToSearch)
</label>
</div>
</div>
</div>
}
</div> <!-- /.smart-search -->


The following code makes the AJAX request:

$.ajax(anchor.attr("data-overlay-url-action"))
.done(function (data) {
$("div.overlay").addClass("old-overlay");

$("div.navbar").after(data);

$("div.overlay:not(.old-overlay)")
.attr("data-overlay-url-action", anchor.attr("data-overlay-url-action"))
.hide()
.fadeIn();

$("div.old-overlay")
.fadeOut()
.removeClass("old-overlay");

anchor.addClass("overlay-exists");
});


So what you get is the same partial razor view output on the page twice, once during the page request and once during the AJAX request.

The problem is that TextBoxFor, CheckBoxFor, etc. all make use of TagBuilder.GenerateId to generate the id attribute value but it doesn't account for generating id's across multiple requests where AJAX might be involved. This results in the same id value being output on the page, causing JavaScript to break.

The following is the HTML that is output twice (once during the request and then added in a separate part of the page during an AJAX request):

<div class="smart-search">

<form role="form" method="get" class="form-horizontal" action="/PharmaDotnet/ux/WebReport/Search"> <div class="form-group">
<div class="hidden-xs col-sm-1 col-md-1 col-lg-1 text-right">
<label for="SearchPhrase" class="control-label heading">Search</label>
</div>
<div class="col-xs-8 col-md-9 col-lg-10">
<input type="text" value="" placeholder="for products, companies, or therapy areas" name="SearchPhrase" id="SearchPhrase" class="form-control">
</div>
<div class="col-xs-4 col-sm-3 col-md-2 col-lg-1">
<input type="submit" class="btn btn-default" value="Search">
</div>
<div class="what-to-search hidden-11 col-sm-11 col-sm-offset-1">
<div class="checkbox">
<label>
<input type="checkbox" value="true" name="WhatToSearch" id="WhatToSearch">
NewsManager Search Only
</label>
</div>
</div>
</div>
</form></div>


So the SearchPhrase and WhatToSearch id's are duplicated.

Is there any way to work around this, or is there a better way to render the form elements to avoid this issue?

Answer

You could specify your own ID for the items, and then you wouldn't have this ID collision problem. Have you tried setting the id manually: Html.TextBoxFor( ... , new { id = "AnythingHere" }), where the id could be a freshly generated Guid? (Note that you'd probably need to add a prefix, because Guids can start with a number.

So you can use the following. The Guid doesn't look good, and is unnecessarily long. You might want to go with something shorter, like short guid, DateTime.Ticks, ...

@{
  var id = "chk_" + Guid.NewGuid().ToString();
}
@Html.CheckBoxFor(..., new { id = id })
@Html.LabelFor(..., new { @for = id })