MVC AutoComplete EditorFor while using Html.BeginCollectionItem - javascript

I am trying to create an AutoComplete textbox, while using an EditorTemplate. The problem I am facing is that by using the Html.BeginCollectionItem() extension solution (https://www.nuget.org/packages/BeginCollectionItem/), the Id's of the EditorFor() and TextBoxFor() methods get set dynamically and this breaks my javascript. Next to that, I do not exactly know if this is even possible (and if so, how. Below you will find how far I have come).
In the main view I have a loop to generate a partial view for each item in a collection
for (int i = 0; i < Model.VoedingCollection.Count; i++)
{
#Html.EditorFor(x => x.VoedingCollection[i], "CreateVoedingTemplate")
}
The partial view CreateVoedingTemplate.cshtml uses the Html.BeginCollectionItem() method
#using (Html.BeginCollectionItem("VoedingCollection"))
{
string uniqueId = ViewData.TemplateInfo.HtmlFieldPrefix.Replace('[', '_').Replace(']', '_').ToString();
string searchId = "Search_";
string standaardVoedingId = "StandaardVoeding_";
foreach (KeyValuePair<string, object> item in ViewData)
{
if (item.Key == "Count")
{
searchId = searchId + item.Value.ToString();
standaardVoedingId = standaardVoedingId + item.Value.ToString();
}
}
<div class="form-horizontal">
<div class="form-group" id=#standaardVoedingId>
#Html.LabelFor(model => model.fk_standaardVoedingId, "Voeding naam", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.HiddenFor(model => model.fk_standaardVoedingId)
<input type="text" id='#searchId' placeholder="Search for a product"/>
</div>
</div>
</div>
<script type="text/javascript">
var id = '#uniqueId' + '_fk_standaardVoedingId'.toString();
var search = '#searchId'.toString();
var url = '#Url.RouteUrl("DefaultApi", new { httproute = "", controller = "AgendaApi" })';
$(document.getElementById(search)).autocomplete({
source: function (request, response) {
$.ajax({
url: url,
data: { query: request.term },
dataType: 'json',
type: 'GET',
success: function (data) {
response($.map(data, function (item) {
return {
label: item.standaardVoedingNaam,
value: item.standaardVoedingId
}
}));
}
})
},
select: function (event, ui) {
$(document.getElementById(search)).val(ui.item.label);
//$('#id').val(ui.item.value);
document.getElementById(id).value = ui.item.value;
return false;
},
minLength: 1
});
</script>
}
<link href="~/Content/SearchBox/jquery-ui.css" rel="stylesheet" />
<script src="~/Scripts/SearchBox/jquery-1.9.1.js"></script>
<script src="~/Scripts/SearchBox/jquery-ui.js"></script>
In the script above, I am trying to make a function where the user can type in the name of a standaardVoeding item and then get results, where, after the user selects a standaardVoeding item, the standaardVoedingId property gets set. Then, after submitting the whole form, the controller receives the standaardVoedingId (with all the other info as well)
So I guess Javascript somehow cannot handle the Razor View # code and, next to that, Html.BeginCollectionItem does something fishy because you cannot set the value of its textboxes via code during runtime. Next to that, I have tried doing alert(document.getElementById(*html.begincollectionitemId*)) and it finds the fields fine. But apparently all other methods do not work?
Is there perhaps a better solution to getting this to work?

The BeginCollectionItem() method alters the id and name attributes of the html generated by the inbuilt helpers, in your case, for the hidden input, instead of
<input ... name="fk_standaardVoedingId" .... />
it will generate
<input ... name="VoedingCollection[xxxx].fk_standaardVoedingId" .... />
where xxxx is a Guid.
While it would be possible to use javascript to extract the Guid value from the textbox (assuming that was generated correctly usind #Html.TextBoxFor()) and build the id of the associated hidden input to use as a selector, it is far easier to use class names and relative selectors.
You also need to remove your scripts and css from the partial and put that in the main view (or its layout). Apart from the inline scripts, your duplicating it for each item in your collection.
Your partial needs to be
#using (Html.BeginCollectionItem("VoedingCollection"))
{
<div class="form-horizontal">
<div class="form-group">
#Html.LabelFor(model => model.fk_standaardVoedingId, "Voeding naam", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10 item"> // add class name
#Html.HiddenFor(model => model.fk_standaardVoedingId)
<input type="text" class="search" placeholder="Search for a product"/>
</div>
</div>
</div>
}
Note the class name for the textbox and its container which also includes the hidden input. Then in the main view, the script will be
<script type="text/javascript">
var url = '#Url.RouteUrl("DefaultApi", new { httproute = "", controller = "AgendaApi" })';
// Attach the script to all textboxes
$('.search').autocomplete({
source: function (request, response) {
$.ajax({
url: url,
data: { query: request.term },
dataType: 'json',
type: 'GET',
success: function (data) {
response($.map(data, function (item) {
return {
label: item.standaardVoedingNaam,
value: item.standaardVoedingNaam, // this needs to be the name
id: item.standaardVoedingId // add property for the id
}
}));
}
})
},
select: function (event, ui) {
// Get the associated hidden input
var input = $(this).closest('.item').find('input[type="hidden"]');
// Set the value of the id property
input.val(ui.item.id);
},
minLength: 1
});
</script>
Based on your comments that your are not dynamically adding or removing items in the view, then there is no point in the extra overhead or using the BeginCollectionItem() method. Change the name of your partial to standaardvoeding.cshtml (assuming that's the name of the class) and move it to the /Views/Shared/EditorTemplates folder.
Then in the main view, replace your for loop with
#Html.EditorFor(m => m.VoedingCollection)
which will generate the correct html for each item in the collection. Finally remove the BeginCollectionItem() method from the template so that its just
<div class="form-horizontal">
<div class="form-group">
#Html.LabelFor(m => m.fk_standaardVoedingId, "Voeding naam", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10 item"> // add class name
#Html.HiddenFor(m => m.fk_standaardVoedingId)
<input type="text" class="search" placeholder="Search for a product"/>
</div>
</div>
</div>

Related

How do I check many values inside script in ASP.NET MVC 4?

I can easily show/hide a <div> based on this View code:
<div class="form-group">
#Html.LabelFor(m => m.countryID, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.DropDownListFor(m => m.countryID, ((IEnumerable<Corporate.Models.Country>) ViewBag.Possiblecountries).OrderBy(c => c.countryName).Select(option => new SelectListItem
{
Text = Html.DisplayTextFor(_ => option.countryName).ToString(),
Value = option.countryID.ToString(CultureInfo.InvariantCulture),
Selected = (Model != null) && (option.countryID == Model.countryID)
}), new { #class = "form-control" })
#Html.ValidationMessageFor(m => m.countryID, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group" id="vatNumberDiv">
#Html.LabelFor(m => m.vatNumber, new {#class = "col-md-2 control-label"})
<div class="col-md-10">
#Html.TextBoxFor(m => m.vatNumber, new {#class = "form-control"})
#Html.ValidationMessageFor(m => m.vatNumber, "", new {#class = "text-danger"})
</div>
</div>
and based on this Script:
<script type="text/javascript">
$(function () {
$('#countryID').change(function () {
var value = $(this).val();
if (value == 'FRA') {
$('#vatNumberDiv').show();
} else {
$('#vatNumberDiv').hide();
}
});
});
</script>
but what about checking all the EU members? I have a method called bool IsMemberEU() that requires MVC context to execute. Can I call it inside the script?
Maybe it's better to generate by code all the options inside the script? Something like:
if (value == 'FRA' ||
value == 'DEU' ||
value == 'ITA' ||
...
...
) {
Do I have some other option?
Thanks.
EDIT:
This is the code I need, to check if the country is EU member:
foreach(Country c in context.Countries)
{
if (IsMemberEU(c))
{
// is EU memeber
}
}
EDIT2: For M12 Bennet
<script type="text/javascript">
// $(function () {
$(document).ready(function() {
$('#countryID').change(function () {
// get selected option to submit to method IsMemberEU
var selectedOption = $(this).val();
// create URL for ajax call
var ajaxUrl = '#Url.Action("IsMemberEU", "Customers")';
$.ajax({
url: ajaxUrl,
data: { countryAbbv: selectedOption },
success: function(result) {
if (result) {
$("#vatNumberDiv").show();
} else {
$("#vatNumberDiv").hide();
}
// show result of ajax call in the `p` element on page. This is just testing to see if ajax call worked.
// this can be done with console.log(result) as well.
$("#ShowResult").text(result);
},
error: function(xhr, status, error) {
console.log(xhr);
}
});
});
});
</script>
In a controller, you could create the method IsMemberEU(string countryAbbv). It needs to accept a parameter because you're checking against what you're sending to the method. So your code could look like this:
Controller Method
public bool IsMemberEU(string countryAbbv)
{
var lstCountries = db.Countries.Where(x => x.isEU).Select(t => t.Abbr).ToList();
return lstCountries.Contains(countryAbbv);
}
Then on your Razor/HTML page:
Razor/HTML
<div>
<select id="CountrySelect" name="countryAbbv">
<option value="">-- Select Country --</option>
<option value="FRA">FRA</option>
<option value="DEU">DEU</option>
<option value="ITA">ITA</option>
<option value="USA">USA</option>
</select>
<p id="ShowResult"></p>
</div>
Then in your jQuery to include AJAX:
jQuery/AJAX
<script>
$(document).ready(function() {
// create event listener for change of select
$("#CountrySelect").change(function() {
// get selected option to submit to method IsMemberEU
var selectedOption = $(this).val();
// create URL for ajax call
var ajaxUrl = '#Url.Action("IsMemberEU", "Home")';
$.ajax({
url: ajaxUrl,
data: { countryAbbv: selectedOption },
success: function(result) {
if (result)
$("#vatNumberDiv").show();
else
$("#vatNumberDiv").hide();
},
error: function(xhr, status, error) {
console.log(xhr);
}
});
});
});
</script>
This is working as expected on my end. Now, this is just a basic example that I created myself based off of the information that was provided in the question. You shouldn't have to worry about the HTML/Razor that I provided because you're using Razor syntax.

Javascript object binding in .NET MVC 5 and server side validation

I have a class in my project which looks like this:
public class MyClass
{
[Required]
public string keyword { get; set; }
public string saleRange { get; set; }
}
This is how the action looks like:
[HttpPost]
[ActionName("Analyze")]
public ActionResult Analyze(MyClass obj)
{
if (!ModelState.IsValid)
{
return Json("error");
}
else
{
return Json("ok");
}
}
And in jQuery:
$(document).on("click",".btnAnalyze",function() {
var data =
{
keyword: $(".keywordInput").val(),
saleRange: "1"
};
console.log($(".keywordInput").val());
$.post("/Controller/Analyze",data).done(function(){
if (data == "error") {
alert("all fields are required");
}
});
});
As you can see I'm mapping a JS object to a C# class object which I pass to my action. Now what the problem here is, even if I don't supply anything for the keyword property (ie. it's null or ""), the ModelState still shows it's property IsValid as true, but it should be set on false if I don't pass anything as keyword parameter.
This is first part of the problem, the other question I have here is how would I, If I have 10 textbox field, of which all are required. And if user enters only 1/10 of those, I would like to send a message for the first next field user has to validate in order to proceed, like "texbox #2 is required, please enter something" instead of a just general message like "all fields are required" or something like that ?
Can someone help me out with this ?
Why does the modelstate shows the it's valid even though after I dont' supply keyword parameter in the object?
#T.Jung here Is a sample of input:
<input placeholder="Enter keywords" class="form-control keywordInput" type="text" style="width:80%">
It's basically just a plain input field in html..
You can use form validation, which will perform data annotation validation on the client side and you do not have to go to the server side.
$('#btnAnalyze').on('click', function (evt) {
if ($('#YourformId').valid()) {
//Perform AJAX call
}
This way if any of the validations do not work, the form.valid() will throw the data annotation errors. So you can add Required to those fields in the models and it will ensure data is not sent if validation fails.
I would do the following:
Make sure your script bundle includes the following 2 files after your jquery and in this order:
jquery.validate.js
jquery.validate.unobtrusive.js
Create your partial / view for your form:
#model MyClassObject
<form action="/action" method="post" id="form">
<div class="row">
#Html.ValidationMessageFor(m => m.Param1)
#Html.LabelFor(m => m.Param1, new { #class = "label" })
#Html.TextBoxFor(m => m.Param1, new { #class = "textbox" })
</div>
<div class="row">
#Html.ValidationMessageFor(m => m.Param2)
#Html.LabelFor(m => m.Param2, new { #class = "label" })
#Html.TextBoxFor(m => m.Param2, new { #class = "textbox" })
</div>
<div class="row">
#Html.ValidationMessageFor(m => m.Param3)
#Html.LabelFor(m => m.Param3, new { #class = "label" })
#Html.TextBoxFor(m => m.Param3, new { #class = "textbox" })
</div>
<input type="submit" value="submit">
</form>
Do you server side validation
[HttpPost]
[ActionName("action")]
public ActionResult action(MyClassObject obj)
{
if(!ModelState.IsValid)
{
return action(); // this is your original action before you do the post
}
else
{
// do your processing and return view or redirect
return View(); // or redirect to a success page
}
}
Add the error message to your class:
public class MyClassObject
{
[Required(ErrorMessage = "Please enter a keyword")]
public string keyword { get; set; }
public string saleRange { get; set; }
}
Your js validation will be plumbed in now but you will need to stop the form from posting and check the validation before doing your ajax request:
var form = document.getElementById('form'), // get a normal js form (I do this just so I can pass in the method and action dynamically - seems better than using .attr() to get them
jqForm = $(form);
jqForm.on('submit', function(e) {
e.preventDefault();
if (jqForm.valid()) {
var formData = jqForm.serializeArray(); // I use serializeArray so I can add extra values if I need to, you can use serialize()
// formData.push({ name: this.name, value: this.value }); - optional for adding extra data
$.ajax({
url: form.action, // this can be just a specific json url if you just need to get some json - it doesn't have to be the same url as the fallback (no js url)
type: form.method,
data: formData,
success: function(result) {
// do success stuff!
}
});
}
})
You should stringify the JavaScript Object in the Jquery post
JSON.stringify(data)
If you want to do the Validation on server side:
Contoller:
[HttpPost]
[ActionName("Analyze")]
public ActionResult Analyze(Model viewModel)
{
if(viewModel.PropertyName.IsNullOrEmpty()){
{
ModelState.AddModelError("PropertyName", "The Field InputFieldName neeeds to be filled!");
}
if (ModelState.IsValid)
{
//Do what ever you want with your Inofrmation
//return a redirct or anything else
}
//If you got here some fields are not filled
return View(viewModel);
}
View:
#Html.TextBoxFor(x => x.PropertyName)
#Html.ValidationMessageFor(m => m.PropertyName, "", new { #class = "text-danger" })
//#class = "text-danger" only if you're using bootstrap
I do all of my validation this way.

AJAX not updating partial view

I'm currently having difficulty using Ajax to update a partial view without having to refresh the whole page. I'm using MVC and entity framework to scaffold views.
I'll try and include as much as possible to help explain myself:
I have a div which is going to be used to hold a list view of all my comments
<div id="ContainAnnouncementComments"> </div>
This div gets populated using the following:
<script src="~/Scripts/jquery.unobtrusive-ajax.min.js"></script>
<script src="~/Custom_Scripts/BuildAnnouncement.js"></script>
#Scripts.Render("~/bundles/jqueryval")
$(document).ready(function () {
$.ajax({
url: '/Comments/BuildAnnouncementComment',
data: { AnnouncementId: #Model.AnnouncementId},
success: function (result) {
$('#ContainAnnouncementComments').html(result);
}
});
});
Which calls the BuildAnnouncementComment() method in my controller:
public ActionResult BuildAnnouncementComment(int? AnnouncementId)
{
return PartialView("_AnnouncementComment", db.Comments.ToList().Where(x => x.AnnouncementId == AnnouncementId));
}
I then have a second div container which is used to hold a text box for a user to enter some information which 'should' then update the ContainAnnouncementComments using Ajax replace call:
<div id="ContainAnnouncementCommentCreate">
#using (Ajax.BeginForm("AJAXCreate", "Comments", new { announcementId = Model.AnnouncementId }, new AjaxOptions
{
InsertionMode = InsertionMode.Replace,
HttpMethod = "POST",
UpdateTargetId = "ContainAnnouncementComments"
}))
{
<div class="form-group">
#Html.AntiForgeryToken()
<div class="col-md-10">
#Html.EditorFor(model => model.Message, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Message, "", new { #class = "text-danger" })
</div>
</div>
}
</div>
The Ajax method calls the AJAXCreate method in the controller:
public ActionResult AJAXCreate(int announcementId, [Bind(Include = "CommentId, Message")] Comment comment)
{
if (ModelState.IsValid)
{
comment.UserId = User.Identity.GetUserId();
comment.AnnouncementId = announcementId;
db.Comments.Add(comment);
db.SaveChanges();
}
return PartialView("_AnnouncementComment", db.Comments.ToList());
}
From here, when running, I try to create a new comment, but when submitted, instead of the partialView being updated, all that is being displayed is the partialview.
Not sure if I've explained this properly so if i'm missing any information please let me know and i'll update accordingly.
After 3 hours of staring at my code I realised that nothing was actually wrong with the implementation and all that was wrong was I hadn't installed AJAX through the NuGet manager.
Joys.

ASP.NET MVC jQuery Ajax - close and refresh parent table from modal dialog

Very new to both MVC and jQuery, so I'm not sure how to get this to work. I've cobbled together a (somewhat) working modal dialog form with an ajax postback. Been searching for two days and not finding great MVC+jQuery examples out there. The data gets inserted as expected, it's just the UI I'm having a hard time with.
I've got two views: Index and Create. Index lists all records inside a plain table, looping the results with Razor. Create is the insert form which I'm loading into a modal dialog:
#model IEnumerable<MyProject.Models.StockIndex>
#{
ViewBag.Title = "Admin: Stock Index Home";
Layout = "~/Views/Shared/_LayoutAdmin.cshtml";
}
<h2>View Stock Indices</h2>
<p>
Create New
<div id="createStockIndexForm"></div>
</p>
<table class="table">
<tr>
<th>
#Html.DisplayNameFor(model => model.Name)
</th>
<th>
#Html.DisplayNameFor(model => model.Description)
</th>
<th></th>
</tr>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.Description)
</td>
<td>
#Html.ActionLink("Edit", "Edit", new { id = item.Id }) |
#Html.ActionLink("Details", "Details", new { id = item.Id }) |
#Html.ActionLink("Delete", "Delete", new { id = item.Id })
</td>
</tr>
}
</table>
#section Scripts {
<script>
$('#createLink').on('click', function () {
$("#createStockIndexForm").dialog({
autoOpen: true,
position: { my: "center", at: "top+350", of: window },
width: 600,
resizable: false,
title: 'Create Stock Index',
modal: true,
open: function () {
$(this).load('#Url.Action("Create", "AdminStockIndex")');
}
});
return false;
});
</script>
}
Controller actions:
public ActionResult Create()
{
var model = new StockIndexEditViewModel()
{
StockIndices = GetIndices()
};
return View(model);
}
[HttpPost]
public ActionResult Create(StockIndexEditViewModel model)
{
if (ModelState.IsValid)
{
...
}
return PartialView(model);
}
The "Create" form that was loaded into the dialog (above):
#model MyProject.Models.StockIndexEditViewModel
#{
ViewBag.Title = "CreatePartial";
Layout = "~/Views/Shared/_LayoutAdmin.cshtml";
}
<h2>CreatePartial</h2>
#using (Html.BeginForm("Create", "AdminStockIndex", FormMethod.Post, new { id = "createForm" }))
{
#Html.AntiForgeryToken()
<div id="result"></div>
<div class="form-horizontal">
<h4>StockIndexEditViewModel</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.SelectedParentId, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(model => model.SelectedParentId, Model.StockIndices, "- Select One -", new { #class = "form-control" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Name, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Name, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Name, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Description, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Description, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Description, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" id="createButton" class="btn btn-default" />
</div>
</div>
</div>
}
#section Scripts {
<script>
$("#createForm").on("submit", function (event) {
event.preventDefault();
$.ajax({
url: this.action,
type: this.method,
async: true,
data: $(this).serialize(),
success: function (data) {
if (data) {
var createForm = $("#createStockIndexForm").dialog();
createForm.dialog("close");
}
else {
$("#result").append("Something went fail.");
}
}
});
});
</script>
}
The modal dialog always goes blank, rather than closing. In testing in Firefox using Firebug, I occasionally see this error, but not every time:
InvalidAccessError: A parameter or an operation is not supported by
the underlying object
Upon searching, I see that it's a CORS problem where FF is enforcing the rules and other browsers may not be. It's frustrating that the error doesn't always occur - appears to be random. Behaves the same in Chrome but doesn't ever throw an error in the JS console.
Firstly, how would I pull this off? Secondly, is there a way to refresh the table on the parent page via ajax, without plugins?
UPDATE:
With Eckert's help, I've made a little progress.
I'm trying to avoid the MVC Ajax helpers and sticking with a "pure" jQuery approach. I replaced the list of records on Index with a div, which contains a PartialView:
<div id="stockIndices">
#Html.Partial("_StockIndices", Model)
</div>
I've got the underlying table refresh working by reloading the div with the close property of the jQuery dialog:
$('#createLink').on('click', function () {
$("#createStockIndexForm").dialog({
autoOpen: true,
position: { my: "center", at: "top+400", of: window },
width: 600,
resizable: false,
title: 'Create Stock Index',
modal: true,
open: function () {
$(this).load('#Url.Action("Create", "AdminStockIndex")');
},
close: function () {
$("#stockIndices").load('#Url.Action("GetStockIndices", "AdminStockIndex")');
}
});
return false;
});
Upon closing the modal dialog manually, the div reloads just how I wanted. Great! Now if I could get the dialog to close when the form posts, I'd be set. This does not work:
$("#createStockIndexForm").dialog("close");
Firebug reports the error:
Error: cannot call methods on dialog prior to initialization;
attempted to call method 'close'
The modal dialog always goes blank, rather than closing.
It's probably behaving improperly because you're creating a variable based on the dialog method of an object, and not the object itself. Try this instead:
if (data) {
$("#createStockIndexForm").dialog("close");
}
Secondly, is there a way to refresh the table on the parent page via
ajax, without plugins?
There sure is, but it may require you to change some things, including the code to close your dialog. Here's how I would handle it:
1) your table of records should be a partial view within the main Index view. The reason for this is because when we submit your form, we'll use the ajax-option "InsertionMode" coupled with the target-id to replace your old records table with the updated one from the form.
2) rather than handling your dialog-close code in the on-submit method, we'll instead use the ajax-option "OnSuccess" to do that (and also "OnFailure" to handle your 'Something went fail' error).
So here's your new Index view:
#model IEnumerable<MyProject.Models.StockIndex>
#{
ViewBag.Title = "Admin: Stock Index Home";
Layout = "~/Views/Shared/_LayoutAdmin.cshtml";
}
<h2>View Stock Indices</h2>
<p>
Create New
<div id="createStockIndexForm"></div>
</p>
<div id="recordsDiv">
#Html.Partial("RecordsPartial", model)
</div>
// all your script stuff can still go at the end
Most of the Index view is unchanged, with the exception that we're now using a that contains a partial view. This partial view will include the table of records, coded here:
Create a new partial view named "RecordsPartial":
#model IEnumerable<MyProject.Models.StockIndex>
<table class="table">
<tr>
<th>
#Html.DisplayNameFor(model => model.Name)
</th>
<th>
#Html.DisplayNameFor(model => model.Description)
</th>
<th></th>
</tr>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.Description)
</td>
<td>
#Html.ActionLink("Edit", "Edit", new { id = item.Id }) |
#Html.ActionLink("Details", "Details", new { id = item.Id }) |
#Html.ActionLink("Delete", "Delete", new { id = item.Id })
</td>
</tr>
}
Now your "Create" view will be updated to use the mvc-ajax helper rather than use all that additional javascript code:
#model MyProject.Models.StockIndexEditViewModel
#{
ViewBag.Title = "CreatePartial";
Layout = "~/Views/Shared/_LayoutAdmin.cshtml";
}
<h2>CreatePartial</h2>
#using (Ajax.BeginForm("CreateRecord", "AdminStockIndex", new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.Replace, UpdateTargetId = "recordsDiv", OnSuccess = "postSuccess", OnFailure = "postFailed" })
{
#Html.AntiForgeryToken()
<div id="result"></div>
<div class="form-horizontal">
<h4>StockIndexEditViewModel</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
/* form fields here */
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" id="createButton" class="btn btn-default" />
</div>
</div>
</div>
}
We changed your form to be ajax-post, and we've added ajax-options to handle what happens after your form has posted. The data that returns after the post (our updated records partial) will replace the current contents of the target-id "recordsDiv". OnSuccess function "postSuccess" will handle closing the dialog box. OnFailure function "postFailed" will report that something bad happened. The last thing to mention is that we changed the post-action from "Create" to "CreateRecord". I prefer to have unique action names when working with ajax-data returns. It's just a cleaner approach.
Before we define the new "CreateRecord" post-action, we need to implement our success and failure functions. Just create them at the bottom of your script-section block in the main "Index" view:
#section Scripts {
<script>
// ... other stuff that was already here ...
function postSuccess() {
$("#createStockIndexForm").dialog("close");
}
function postFailed() {
alert("Failed to post"); // you can define your own error
}
</script>
}
Lastly, we create the "CreateRecord" post-action, which will process our form and return an updated "records partial" view:
[HttpPost]
public ActionResult CreateRecord(StockIndexEditViewModel model)
{
if (ModelState.IsValid)
{
... create record here ...
}
var records = db.Indexes.ToList(); // or whatever your table name is
return PartialView("RecordsPartial", records);
}
This is repeating some of your existing "Create" action. We simply process our post data, then we get a new updated list of records, and lastly we return that list of records back to our "RecordsPartial" view, which will be inserted into our "recordsDiv" as we specified in our ajax-options.
Very clean and simple solution. If you have questions, feel free to ask.
In your main Index view, rather than calling for your Create view to be inserted into your view, have it initially present at view load, and hide it within a div:
<div id="createStockIndexForm">
#Html.Action("Create", "AdminStockIndex")
</div>
In your Index script section, we're creating the dialog OUTSIDE your click event. We're also turning the "autoOpen" value to false so the div hides at view load.
Index script section:
#section Scripts {
<script>
$("#createStockIndexForm").dialog({
autoOpen: false,
position: { my: "center", at: "top+350", of: window },
width: 600,
resizable: false,
title: 'Create Stock Index',
modal: true
});
$('#createLink').on('click', function () {
$("#createStockIndexForm").show();
});
</script>
}
Also, when you use the PreventDefault() command, it appears to be interfering with your modal-close command (after some of my own testing). I suggest that you change your form's "Create" button to type="button" rather than "submit", and then use the ID of the button to handle your ajax-post method.
<input type="button" id="createButton" value="Create" class="btn btn-default" />
Add this to your main Index script section at the bottom:
$("#createButton").on("click", function () {
$.ajax({
url: this.action,
type: this.method,
async: true,
data: $(this).serialize(),
success: function (data) {
if (data) {
$("#createStockIndexForm").dialog("close");
}
else {
$("#result").append("Something went fail.");
}
}
});
});
Make sure to direct your "close" command at the dialog object itself.
Here's a fiddle I created that shows you the gist of how it should be:
http://jsfiddle.net/ch5aLezg/
So to recap on the script stuff, you should have NO SCRIPTS in your "Create" partial. All the scripts go into the main Index view, as I've detailed in this answer.
I found a way to get everything I wanted, while retaining the "pure" jQuery Ajax approach, instead of resorting to the MVC Ajax helpers, suggested by Eckert. His suggestions lead me to the solution, however. Big thanks!
I created a PartialView in the controller:
public ActionResult GetStockIndices()
{
_service = new StockIndexService();
var data = _service.GetAll();
return PartialView("_StockIndices", data);
}
...and its view:
#model IEnumerable<MyApp.Models.StockIndex>
<table class="table">
<tr>
<th>
#Html.DisplayNameFor(model => model.Name)
</th>
<th>
#Html.DisplayNameFor(model => model.Description)
</th>
<th></th>
</tr>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.Description)
</td>
<td>
#Html.ActionLink("Edit", "Edit", new { id = item.Id }) |
#Html.ActionLink("Details", "Details", new { id = item.Id }) |
#Html.ActionLink("Delete", "Delete", new { id = item.Id })
</td>
</tr>
}
</table>
Then I changed the modal dialog script to load the partial view when closed. Here's the entire Index view, for posterity:
#model IEnumerable<MyApp.Models.StockIndex>
#{
ViewBag.Title = "Admin: Stock Index Home";
Layout = "~/Views/Shared/_LayoutAdmin.cshtml";
}
<h2>View Stock Indices</h2>
<p>
Create New
<div id="createStockIndexForm"></div>
</p>
<div id="stockIndices">
#Html.Partial("_StockIndices", Model)
</div>
#section Scripts {
<script>
var _dialog;
$('#createLink').on('click', function () {
_dialog = $("#createStockIndexForm").dialog({
autoOpen: true,
position: { my: "center", at: "top+400", of: window },
width: 600,
resizable: false,
title: 'Create Stock Index',
modal: true,
open: function () {
$(this).load('#Url.Action("Create", "AdminStockIndex")');
},
close: function () {
$("#stockIndices").load('#Url.Action("GetStockIndices", "AdminStockIndex")');
}
});
return false;
});
</script>
}
Notice the global "_dialog" variable. This gave me access to the dialog from the Create form, so it could be closed:
<script>
$("#createForm").on("submit", function (event) {
event.preventDefault();
$.ajax({
url: this.action,
type: this.method,
async: true,
data: $(this).serialize(),
success: function (data) {
if (data) {
if (_dialog) {
_dialog.dialog("close");
}
}
else {
$("#result").append("Error! Record could not be added.");
}
},
error: function (error) {
console.error(error);
}
});
});
</script>

Make a textbox appear when a value is selected from a dropdown list in MVC

When "Other" is selected from the DDL all I want is for the textbox to appear. However it always displays instead of being hidden until called.
My view markup is:
<div class="form-group">
#Html.LabelFor(model => model.SelectType, "Select Type", new { #class = "control-label col-md-5" })
<div class="col-md-1">
#Html.DropDownList("SelectType", null, new { #id = "Other" })
#Html.TextBoxFor(model => model.OtherSpecify, new { #id = "OtherSpecify" })
#Html.ValidationMessageFor(model => model.SelectType)
</div>
I tried the following two javacript codes without any success
<script>
document.addEventListener("DOMContentLoaded", function () {
$("SelectType").trigger("change");
})
$("#SelectType").on("change", function () {
if ($("#SelectType option:selected").val() == 3) {
$("#OtherSpecify").hide();
} else {
$("#OtherSpecify").show();
}
});
</script>
<script>
document.addEventListener("DOMContentLoaded", function () { $("SelectType").trigger("change");
})
$(function () {
$('.OtherSpecify').show();
$("Other").change(function () {
if ($(this).is(":selected")) {
$(this).parent().next().hide();
}
else {
$(this).parent().next().show();
}
});
})
</script>
First you shoud check how jQuery selectors work.
In HTML above '$("#SelectType")' - is your select and $("#OtherSpecify") is your textbox.
If you are using jQuery you shoud use it all the time.
Use $(handler) insted of DOMContentLoaded event:
<div class="form-group">
<div class="col-md-1">
#Html.DropDownList("SelectType", new List<SelectListItem> {
new SelectListItem{Text = "test 1", Value = "1"},
new SelectListItem{Text = "test 2", Value = "2"},
new SelectListItem{Text = "Other", Value = "3"}
}, new { #id = "SelectType" })
#Html.TextBox("OtherSpecify", "")
</div>
</div>
#section Scripts {
<script>
$(function() {
$("#SelectType").on("change", function() {
if (parseInt($("#SelectType").val()) == 3) {
$("#OtherSpecify").show();
} else {
$("#OtherSpecify").hide();
}
});
$("#SelectType").trigger("change");
});
</script>
}
Remember to place script after jQuery library is loaded. In most cases #section Scripts do the work.
I have to adjust a few things to enable the Javascript to work. Firstly I seperated out my HTML helpers:
<div class="form-group">
#Html.LabelFor(model => model.SelectType, "Select Type", new { #class = "control-label col-md-5" })
<div class="col-md-1">
#Html.DropDownList("SelectType", String.Empty)
#Html.ValidationMessageFor(model => model.SelectType)
</div>
</div>
<div class="form-group" id="OtherSpecifyFormGroup">
#Html.LabelFor(model => model.OtherSpecify, new { #class = "control-label col-md-4" })
<div class="col-md-4 sbchanged">
#Html.TextBoxFor(model => model.OtherSpecify)
#Html.ValidationMessageFor(model => model.OtherSpecify)
</div>
</div>
Then wrote the following JavaScript code:
<script>
$(document).ready(function () {
//this line fires no matter what
$("#OtherSpecifyFormGroup").hide();
$("#SelectType").change(function () {
var value = document.getElementById("SelectType").value;
if (value == "4") {
$("#OtherSpecifyFormGroup").show("highlight", { color: "#7FAAFF" }, 1000);
}
else {
$("#OtherSpecifyFormGroup").hide();
}
});
})
</script>
I gave my form group for Other Specify an ID so that I could initially hid the textbox. Then declared the variable "value" as in my database the values that populate the DDL have SelectType Ids, therefore it wouldn't call "Other" as it wasn't recognised but as shown when the value "4" is called it works! The else ensures that if any other DDL value is selected then the textbox is hidden again.

Categories

Resources