how to load a partial view inside an anchor tag which has been generated via Ajax - ajax

I have a form with a dropdownlist. When selecting an option, I make an ajax call to dynamically add a list of links in the view. When I click on one of the links, I want to update the existing page with a partial view returned by the PostListSubCategory() method.
Currently, clicking on one of the links does a redirect and shows the partial view in a new page. How can I update the the existing page?
<script language="javascript" type="text/javascript">
function GetSubCategory(_categoryId) {
var procemessage = "<a='0'> Please wait...</a>";
$("#SubCategoryID").html(procemessage).show();
var url = "/Posts/GetSubCategoryById/";
$.ajax({
url: url,
data: { categoryid: _categoryId },
cache: false,
type: "POST",
success: function (data) {
var markup = "";
for (var x = 0; x < data.length; x++) {
var num = data[x].Text;
markup += "<a href='/posts/postlistsubcategory?subcategoryid=" + data[x].Text + "'>" + data[x].Text + "</a><br />";
// markup += "<a href=" + Url.Action("postlistsubcategory", new { subcategoryid = num });
}
$("#SubCategoryID").html(markup).show();
},
error: function (reponse) {
alert("error : " + reponse);
}
});
$.ajax({
url: "/Posts/PostListCategory",
data: { categoryid: _categoryId },
cache: false,
type: "POST",
success: function (data) {
$("#postList").html(data).show();
},
error: function (reponse) {
alert("error : " + reponse);
}
});
}
</script>
#using (Html.BeginForm())
{
#Html.ListBoxFor(m => m.CategoryModel, new SelectList(Model.CategoryModel, "CategoryId", "Name"), new { #id = "ddlcategory", #style = "width:200px;", #onchange = "javascript:GetSubCategory(this.value);" })
<br />
<br />
<div id="SubCategoryID" name="SubCategoryID" style="width: 200px"></div>
<br /><br />
}
In the controller
public PartialViewResult PostListSubCategory(string subcategoryid)
{
if (subcategoryid == null)
{
return PartialView(db.Posts.ToList());
}
return PartialView("PostList", db.Posts.Include(i => i.SubCategory).Where(p => p.SubCategory.Name == subcategoryid));
}

You currently dyamically generating links with an href attribute so clicking on them will do a redirect. You need to handle the click event of those links using event delegation and then use ajax to update the existing DOM. There a some other bad practices in your code and I suggest you use the following
#using (Html.BeginForm())
{
// no need to override the id attribute and use Unobtrusive Javascript (don't pollute markup with behavior)
#Html.ListBoxFor(m => m.CategoryModel, new SelectList(Model.CategoryModel,"CategoryId", "Name"))
}
<div id="SubCategoryID"></div> // no point adding a name attribute
<div id="postList"></div>
var subcategories = $('#SubCategoryID');
$('#CategoryModel').change(function() {
var url = '#Url.Action("GetSubCategoryById", "Posts")'; // don't hard code url's
var category = $(this).val();
subcategories.empty(); // clear any existing links
$.post(url, { categoryid: category }, function(data) { // this could be a GET?
$.each(data, function(index, item) {
subcategories.append($('<a></a>').text(item).attr('href','#').addClass('subcategory')); // see note below
});
});
});
Note: Since your ajax only needs one property to generate the links (the value to display in the link), then your GetSubCategoryById() should be returning IEnumerable<string> not a collection of complex objects (you current code suggest your returning other data which you never use). If you do need to return a collection of objects, then change the above to use .text(item.Text). The above code will generate
.....
for each item you return. Then add an additional script to handle the .click() event of the links (since the links are dynamically added, you need event delegation using the .on() method)
var posts = $('#postList');
$('#SubCategoryID').on('click', '.subcategory', function() {
var url = '#Url.Action("postlistsubcategory", "posts")';
var subcategory = $(this).text();
posts.load(url, { subcategoryid: subcategory });
});

Related

Razor not printing Values to screen after ASP 3.1 RazorPages AJAX Post Updates Model

Hello I am updating a model with an AJAX call on the event of a dropdown selection.
The model is updated and when I step through the below razor loop the values exist.
However nothing inside the #if statement prints to the screen, not even the H2.
The div is just empty... Thoughts?
#if (Model.FieldsRequiredOnStart != null)
{
foreach (var item in Model.FieldsRequiredOnStart)
{
for (int i = 0; i < #item.Inputs.Count(); i++)
{
<h2>Fields Required on Start</h2>
var x = #item.Inputs[i];
<span>#x.Name</span>
<input placeholder="#x.Name" maxlength="#x.MaxSize" type="#x.InputType"> <input />
}
}
}
function onSelect(e) {
let id = $("#wfDropdown").data("kendoDropDownList").value()
if (e.item) {
$('#wfDefId').val(id)
} else {
$('#wfDefId').val(id)
}
$.ajax({
type: 'Post',
url: '/CreateNewWorkflow?handler=RequiredInputs',
beforeSend: function (xhr) {
xhr.setRequestHeader("XSRF-TOKEN",
$('input:hidden[name="__RequestVerificationToken"]').val());
},
data: { Id: id },
success: function () {
}
});
}
EDIT ON SUCCESS:
I ended up using the Partial View solution. My issue was I was not sending a refreshed model when I had previously tried the partial way. The Second answer is also valid. Gotcha: If you make a partial be sure to remove the #Page reference at the top or you will get Model is null errors.
Also worth noting the C# Syntax I had to use was slightly different than what is provided in the answer to return the Partial view..
public ActionResult OnPostRequiredInputs(int id)
{
//DO STUFF
//Refresh Model to pass to partial
IEnumerable<Razor.PageModels.CreateWorkflowNames> namelist = da.GetWfDefVersionNameAndIds();
var freshModel = new CreateNewWorkflowModel(_cache, _mapper, _wlog, _workflowFactory, _configuration)
{
FieldsRequiredOnStart = entityDefinitionFieldsList,
CreateWorkflowModel = namelist
};
return Partial("/Pages/_CreateNewWorkflowRequiredFieldsPartial.cshtml", freshModel);
}
Assuming your CreateNewWorkflow controller action returns a model rehydrated with the new data, you should be able to set that new data in the onSuccess callback of your ajax request.
I'd do this to accomplish the result.
Create partial view for the razor we're gonna need to refresh.
//Filename: YourView.cshtml
<div id="partialWrapper"></div>
//File name: _YourPartialView.cshtml
#if (Model.FieldsRequiredOnStart != null)
{
foreach (var item in Model.FieldsRequiredOnStart)
{
for (int i = 0; i < #item.Inputs.Count(); i++)
{
<h2>Fields Required on Start</h2>
var x = #item.Inputs[i];
<span>#x.Name</span>
<input placeholder="#x.Name" maxlength="#x.MaxSize" type="#x.InputType"> <input />
}
}
}
Make sure your controller action returns a partial view.
public IActionResult<YourModelClass> CreateNewWorkflow(YourRequestClass request) {
//your logic
//...
var rehydratedModel = new YourModelClass(); //actually fill this with data
return PartialView(rehydratedModel);
}
Set the partial view result to your wrapper div in the onSuccess call back.
function onSelect(e) {
let id = $("#wfDropdown").data("kendoDropDownList").value()
if (e.item) {
$('#wfDefId').val(id)
} else {
$('#wfDefId').val(id)
}
$.ajax({
type: 'Post',
url: '/CreateNewWorkflow?handler=RequiredInputs',
beforeSend: function (xhr) {
xhr.setRequestHeader("XSRF-TOKEN",
$('input:hidden[name="__RequestVerificationToken"]').val());
},
data: { Id: id },
success: function (data) { //data represents your partial view
$('#partialWrapper').html(data) //set partial view
}
});
That is a pretty typical flow of how you refresh razor pages with ajax.
Razor not printing Values to screen after ASP 3.1 RazorPages AJAX Post Updates Model, The div is just empty
The issue is related to the Ajax success function, according to your code, we can see that you didn't do anything to update the page with the latest data.
Generally, after getting the latest data in the success function, we could use JQuery to find the page elements and bind the latest data or populate the new page element to replace the old data. You could refer to the following sample code:
<select id="ddl1" asp-for="CategoryId" asp-items="Model.Categories">
<option value="">Select Category</option>
</select>
<h4>SubCategories</h4>
#if (Model.SubCategories != null)
{
<table >
<tr><th>SubCategoryId</th><th>CategoryId</th><th>SubCategoryName</th></tr>
<tbody id="tbody">
#foreach (var item in Model.SubCategories)
{
<tr>
<td>#item.SubCategoryId</td>
<td>#item.CategoryId</td>
<td>#item.SubCategoryName</td>
</tr>
}
</tbody>
</table>
}
Code in the cshtml.cs file:
private ICategoryService _categoryService;
public DDLpageModel(ICategoryService categoryService)
{
_categoryService = categoryService;
}
[BindProperty(SupportsGet = true)]
public int CategoryId { get; set; }
public int SubCategoryId { get; set; }
public SelectList Categories { get; set; }
public List<SubCategory> SubCategories { get; set; }
public void OnGet()
{
Categories = new SelectList(_categoryService.GetCategories(), nameof(Category.CategoryId), nameof(Category.CategoryName));
SubCategories = _categoryService.GetSubCategories(1).ToList();
}
public JsonResult OnGetSubCategories()
{
return new JsonResult(_categoryService.GetSubCategories(CategoryId));
}
Then, in the Ajax success function, find the element and set the value or dynamic add page elements with the latest data and replace the old one.
#section scripts{
<script>
$(function () {
$("#ddl1").on("change", function () {
var categoryId = $(this).val();
//method 1: using JQuery Ajax get the latest data and update the main page content
$.ajax({
url: `?handler=SubCategories&categoryId=${categoryId}`,
contentType: 'application/json; charset=utf-8',
type: 'get',
dataType: 'json',
success: function (data) {
$("#tbody").html("");
//loop through the data and append new data to the tbody
$.each(data, function (i, item) {
$("#tbody").append("<tr><td>" + item.subCategoryId + "</td><td>" + item.categoryId + "</td><td>" + item.subCategoryName + "</td></tr>");
});
}
});
});
});
</script>
}
Besides, you could also create a Partial page (for example: _SubCategories.cshtml):
#model List<SubCategory>
<table class="table table-striped">
<tr><th>SubCategoryId</th><th>CategoryId</th><th>SubCategoryName</th></tr>
<tbody id="tbody">
#foreach (var item in Model)
{
<tr>
<td>#item.SubCategoryId</td>
<td>#item.CategoryId</td>
<td>#item.SubCategoryName</td>
</tr>
}
</tbody>
</table>
In the main page .cshtml.cs file, add the following handler:
public PartialViewResult OnGetSubcategoryPartial()
{
var subcategories = _categoryService.GetSubCategories(CategoryId).ToList();
return Partial("_SubCategories", subcategories);
}
Then, using JQuery Ajax to call the above handler and load the partial page:
<h2>Using Partial Page</h2>
<select id="ddl2" asp-for="CategoryId" asp-items="Model.Categories">
<option value="">Select Category</option>
</select>
<div id="output">
</div>
#section scripts{
<script>
$(function () {
$("#ddl2").on("change", function () {
var categoryId = $(this).val();
$.ajax({
url: `?handler=SubcategoryPartial&categoryId=${categoryId}`,
contentType: 'application/html; charset=utf-8',
type: 'get',
dataType: 'html',
success: function (result) {
$("#output").html("");
$("#output").append(result);
}
});
});
});
</script>
}
The screenshot like this:

knockout computed not updating while change in view model being made from ajax

How much I know about knockout Js is that a computed gets updated anyhow depending on the viewmodel, but in my case its not happening. So basically I have a radio button which turns off and on and changes the date in the database, and the ajax calls returns and pushes the new date in the viewmodel so that the data changes.
So thats the summary. But the thing I want is that while the radio button is being updated I want a part of the html to change to active or disabled based on the radio button.
Firstly here is the HTML code.
<div class="col-sm-4">
<p>
<span data-bind="text : $data.basketStatusValue"></span>
</p>
</div>
<div class="col-sm-4">
<div class="on_off">
<input type="checkbox" data-bind="bootstrapSwitchOn: {
tocall: $root.changeActiveBasketStatus
}" />
</div>
</div>
Here is the JS code.
function MoneyInvestedViewModel(root /* root not needed */, money) {
var self = this;
self.ID = money.ID;
self.ORIG_ID = money.ORIG_ID;
self.Available = money.Available;
self.basketStatusValue = ko.computed (function () {
if (self.Available == '9999-12-01') {
return "Active";
} else {
return "Disabled";
}
});
};
And next is the code which is updating the view model moneyInvested . So the checkbox can show on or off.
self.changeActiveBasketStatus = function (bindingContext) {
console.log(bindingContext);
var Id = bindingContext.$data.ORIG_ID;
var Available = bindingContext.$data.Available;
if (Available == '9999-12-01') {
$.ajax({
type: 'POST',
url: BASEURL + 'index.php/moneyexchange/changeBasketStatus/' + auth + "/" + Id + "/" + 1,
contentType: 'application/json; charset=utf-8'
})
.done(function (newAvailableDate) {
bindingContext.$data.Available = newAvailableDate;
})
.fail(function (jqXHR, textStatus, errorThrown) {
self.errorMessage(errorThrown);
})
.always(function (data){
});
} else {
$.ajax({
type: 'POST',
url: BASEURL + 'index.php/moneyexchange/changeBasketStatus/' + auth + "/" + Id + "/" + 0,
contentType: 'application/json; charset=utf-8'
})
.done(function (newAvailableDate) {
bindingContext.$data.Available = newAvailableDate;
})
.fail(function (jqXHR, textStatus, errorThrown) {
self.errorMessage(errorThrown);
})
.always(function (data) {
});
}
};
So basically the PROBLEM is that when all this update is done, the computed self.basketStatusValue does not get updated. So when I click the checkbox on, it doesnt show Active, or off for disabled, the checkbox is working perfectly, only the html $data.basketStatusValue is not updating through the computed function.
Just incase if necessary here is the code for the checkbox.
(function ($) {
ko.bindingHandlers.bootstrapSwitchOn = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var options = ko.utils.unwrapObservable(valueAccessor());
var tocall = ko.utils.unwrapObservable(options.tocall);
$elem = $(element);
$(element).bootstrapSwitch();
$(element).bootstrapSwitch('setState', bindingContext.$data.Available === '9999-12-01' ? true : false); // Set intial state
$elem.on('switch-change', function (e, data) {
tocall(bindingContext);
// valueAccessor()(data.value);
});
},
update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
}
};
})(jQuery);
To summarize, all I want to do is have the $data.basketStatusValue have "active" or "disabled" when the checkbox is on or off.
A dirty trick you can use to pull this off is empty the whole observable and push it with the new data. But honestly its not the right way to use it. I am assuming right now thats its a array, but you can remove observables too. Just put the observable name instead of YourArray().
self.refresh = function(){
var data = YourArray().slice(0);
YourArray.removeAll();
self.YourArray(data);
};
And place this function right after the done function
.done(function(newAvailableDate) {
bindingContext.$data.Available = newAvailableDate;
// here self.refresh();
})
Your binding handler is wrong, let's start with that.
It should:
Set up Bootstrap switch on the element in init()
React to change in update()
Bind to an observabe (don't use a callback function). In our case the observable should contain the checkbox state, i.e. true or false.
Properly dispose of the Bootstrap widget when the time comes.
So, this would work better:
(function ($) {
ko.bindingHandlers.bootstrapSwitch = {
init: function (element, valueAccessor) {
var options = valueAccessor();
// set up bootstrap in init()
$(element).bootstrapSwitch().on('switch-change', function (e, data) {
options.value(data.value);
});
// see http://knockoutjs.com/documentation/custom-bindings-disposal.html
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
$(element).bootstrapSwitch("destroy");
});
},
update: function(element, valueAccessor) {
var options = valueAccessor();
// react to change in update()
$(element).bootstrapSwitch('setState', options.value());
}
};
})(jQuery);
Next we need to set up the viewmodel accordingly.
Available needs to be observable. View changes depend on it.
we need an observable that returns true or false, depending on Available
we need an observable that returns "Active" or "Disabled", depending on that
we need a function that updates the server on change (through a subscription)
like this:
function MoneyInvestedViewModel(money) {
var self = this;
self.ID = money.ID;
self.ORIG_ID = money.ORIG_ID;
self.Available = ko.observable(money.Available);
self.errorMessage = ko.observable();
self.BasketStatus = ko.computed(function () {
return self.Available() == '9999-12-01';
});
self.BasketStatusText = ko.computed(function () {
return self.basketStatus() ? "Active" : "Disabled";
});
// BEWARE: this is not actually correct (cicular dependency)
self.BasketStatus.subscribe(function () {
$.ajax({
type: 'POST',
url: BASEURL + 'index.php/moneyexchange/changeBasketStatus/' + auth + "/" + self.ID + "/" + 1,
contentType: 'application/json; charset=utf-8'
})
.done(function (newAvailableDate) {
self.Available(newAvailableDate);
})
.fail(function (jqXHR, textStatus, errorThrown) {
self.errorMessage(errorThrown);
});
};
}
Note: Subscribe to the correct observable to update the server with the proper value. It was not clear from your question what value the server update depends on.
And now it's straightforward to bind a view to that:
<div class="col-sm-4">
<p><span data-bind="text: BasketStatusText"></span></p>
</div>
<div class="col-sm-4">
<div class="on_off">
<input type="checkbox" data-bind="bootstrapSwitch: {
value: BasketStatus
}" />
</div>
</div>
if(self.Available == '9999-12-01'){ return "Active"; }else{ return "Disabled";}
The trouble with this line is that it does not look up the value of any observable or computed, and thus it does not cause the computed to ever be updated.
You need to make self.Available an observable and then do self.Available()
This is how computeds work, they are recomputed when any of their observable/computed dependencies change. Knockout will not see a simple property update like you are doing.

knockoutjs data bind hidden field value

I'm having a hidden field in a knockout template that its value gets updated with jquery. The problem is when trying to pass this value to the server with ajax, I get null value in the controller. But the html source code shows that the value of the hidden field is updated. If I replaced the hidden field with a textbox, it would work fine only when I enter text manually.
jQuery
function getFileDetail(fileID, fileName) {
$('#hdnFileName' + fileID).val(fileName);
$('#lblFileName' + fileID).text(fileName);
}
Here is the html knockout template:
<script type="text/html" id="fileTemplate">
<div data-role="fieldcontain">
<label data-bind="text: 'File Upload ' + ID, attr: { id: 'lblFileName' + ID }"></label><input type="button" value="Remove" data-bind="click: removeFile" />
</div>
<input type="hidden" name="hdnFileName" data-bind="attr: { id: 'hdnFileName' + ID, value: fileName }" />
</script>
ViewModel
function FileViewModel() {
var self = this;
self.ID = ko.observable();
self.fileName = ko.observable();
self.removeFile = function (file) { };
self.Files = ko.observableArray([{ ID: 1, fileName: "", removeFile: function (file) { self.Files.remove(file); }}]);
self.addNewFile = function () {
var newFile = new FileViewModel();
newFile.ID = self.Files().length + 1;
newFile.fileName = "";
newFile.removeFile = function (file) { self.Files.remove(file); };
self.Files.push(newFile);
//$("input[name='hdnFileName'").trigger("change");
}
}
function ViewModel() {
var self = this;
self.fileViewModel = new FileViewModel();
self.submitForm = function () {
$.ajax({
type: "POST",
url: "<%= Url.Action("MeetingPresenter")%>",
data: "{Files:" + ko.utils.stringifyJson(self.fileViewModel.Files) + "}",
contentType: "application/json",
success: function (data) {},
});
};
}
Your model property ID is an observable, so you need to 'unwrap' to get the value from it when you are concatenating, like this:
<input type="hidden" name="hdnFileName" data-bind="attr: { id: 'hdnFileName' + ID(), value: fileName }" />
and this:
<label data-bind="text: 'File Upload ' + ID(), attr: { id: 'lblFileName' + ID() }"></label>
If you are using knockout.js you don't neede to modify the DOM, you can just update the ViewModel and the DOM will be updated according
function getFileDetail(fileID, fileName) {
viewModel.fileViewModel.update(fileID, fileName);
}
Add the update function in FileViewModel
function FileViewModel() {
// rest of the code
self.update = function(fileID, fileName) {
var file = ko.utils.arrayFirst(self.Files(), function(file) {
return file.ID == fileID;
});
file.fileName(fileName); // this will change and the UI will be updated according
};
}
Note: Please notice that you have a default item in Files that will not be changed with update function because properties are not observable
self.Files = ko.observableArray([{ ID: 1, fileName: "", removeFile: function (file) { self.Files.remove(file); }}]);
You can solve this by making them observable (i.e. ID: observable(1)) or you can create a new FileViewModel().
Note: The viewModel must be accesible in the function (i.e. global instance), otherwise will be undefined.
It looks to me that setting a field's value via the DOM does not interact with knockout. If you are setting its value using .value, the observable will not be updated. You should be updating the observable.
I wrote a little Fiddle to demonstrate. Every 2 seconds, it sets the input's value via the DOM, but the bound observable only changes when you type something.
http://jsfiddle.net/qcv01h2e/
var viewModel = (function () {
return {
fv: ko.observable().extend({notify:'always'})
};
}());
ko.applyBindings(viewModel);
setInterval(function () {
console.debug("Set it");
var f = document.getElementById('field');
f.value = "Hi";
console.debug("fv is", viewModel.fv());
}, 2000);
I came across a similar issue where I need to set a value without user input.
Before doing the click update function I do the required model update. If you have mode operations better to introduce a function in the model.
<input data-bind="click: function(){ isEnabled(true); update() }" />
What I actually did was,
<input data-bind="click: function(){ isEnabled(!isEnabled()); update() }" />
Keep in mind that asynchronous nature of javascript.

how to use cascading dropdownlist in mvc

am using asp.net mvc3, i have 2 tables in that i want to get data from dropdown based on this another dropdown has to perform.for example if i select country it has to show states belonging to that country,am using the following code in the controller.
ViewBag.country= new SelectList(db.country, "ID", "Name", "--Select--");
ViewBag.state= new SelectList("", "stateID", "Name");
#Html.DropDownListFor(model => model.Country, (IEnumerable<SelectListItem>)ViewBag.country, "-Select-")
#Html.DropDownListFor(model => model.state, (IEnumerable<SelectListItem>)ViewBag.state, "-Select-")
but by using this am able to get only the countries.
There is a good jQuery plugin that can help with this...
You don't want to refresh the whole page everytime someone changes the country drop down - an ajax call to simply update the state drop down is far more user-friendly.
Jquery Ajax is the best Option for these kind of questions.
Script Code Is Given below
<script type="text/javascript">
$(function() {
$("##Html.FieldIdFor(model => model.Country)").change(function() {
var selectedItem = $(this).val();
var ddlStates = $("##Html.FieldIdFor(model => model.state)");
$.ajax({
cache:false,
type: "GET",
url: "#(Url.Action("GetStatesByCountryId", "Country"))",
data: "countryId=" ,
success: function (data) {
ddlStates.html('');
$.each(data, function(id, option) {
ddlStates.append($('<option></option>').val(option.id).html(option.name));//Append all states to state dropdown through returned result
});
statesProgress.hide();
},
error:function (xhr, ajaxOptions, thrownError){
alert('Failed to retrieve states.');
statesProgress.hide();
}
});
});
});
</script>
Controller:
public ActionResult GetStatesByCountryId(string countryId)
{
// This action method gets called via an ajax request
if (String.IsNullOrEmpty(countryId))
throw new ArgumentNullException("countryId");
var country = GetCountryById(Convert.ToInt32(countryId));
var states = country != null ? GetStatesByConutryId(country.Id).ToList() : new List<StateProvince>();//Get all states by countryid
var result = (from s in states
select new { id = s.Id, name = s.Name }).ToList();
return Json(result, JsonRequestBehavior.AllowGet);
}
Try this,
<script type="text/javascript">
$(document).ready(function () {
$("#Country").change(function () {
var Id = $("#Country").val();
$.ajax({
url: '#Url.Action("GetCustomerNameWithId", "Test")',
type: "Post",
data: { Country: Id },
success: function (listItems) {
var STSelectBox = jQuery('#state');
STSelectBox.empty();
if (listItems.length > 0) {
for (var i = 0; i < listItems.length; i++) {
if (i == 0) {
STSelectBox.append('<option value="' + i + '">--Select--</option>');
}
STSelectBox.append('<option value="' + listItems[i].Value + '">' + listItems[i].Text + '</option>');
}
}
else {
for (var i = 0; i < listItems.length; i++) {
STSelectBox.append('<option value="' + listItems[i].Value + '">' + listItems[i].Text + '</option>');
}
}
}
});
});
});
</script>
View
#Html.DropDownList("Country", (SelectList)ViewBag.country, "--Select--")
#Html.DropDownList("state", new SelectList(Enumerable.Empty<SelectListItem>(), "Value", "Text"), "-- Select --")
Controller
public JsonResult GetCustomerNameWithId(string Country)
{
int _Country = 0;
int.TryParse(Country, out _Country);
var listItems = GetCustomerNameId(_Country).Select(s => new SelectListItem { Value = s.CountryID.ToString(), Text = s.CountryName }).ToList<SelectListItem>();
return Json(listItems, JsonRequestBehavior.AllowGet);
}

MVC2 / MVC3 Html Helper or Editor Template solution required

I have a jquery based bespoke lookup control that works well, but I wish to encapsulate it into a neater solution. The below is the code included in the Razor view. Behind the scenes there is also a bespoke jquery function called 'InitLookup' that does the majority of the work at runtime.
<script type="text/javascript">
$(document).ready(function () {
InitLookup('#txtDelegatedToLookup', '#Delegated_To_ID', '#Url.Action("ContactLookup", "marMIS")');
});
</script>
#Html.HiddenFor(m => m.Delegated_To_ID)
<input id="txtDelegatedToLookup" type="text" />
Ideally, I would like to whittle this down to a neater and more re-usable solution, where the javascript InitLookup() is dynamically created and encapsulated within, possibly as below...
#Html.DynamicLookupFor(m => m.Delegated_To_ID, "ContactLookup", "marMIS")
...where "marMIS" is the controller, and "ContactLookup" is a controller method. These being the address to use to get the data during a lookup.
I tried to create an Editor Template called DynamicLookup in the /Views/Shared/EditorTemplates folder, but the wasn't recognised when using #Html.DynamicLookup(...
Any takers on this one? Cheers!
------------ App_Code suggestion below! Addendum to original question! -------------------------
OK, so I have copied my code into a new App_Code folder file called CustomHelpers.cshtml. How do I pass the lambda expression into here and then use it?
#using System.Security.Policy
#using System.Web.Mvc.Html
#helper DynamicLookup(LAMBDAEXPR, CtrlId, Controller, Method) {
#Html.HiddenFor(LAMBDAEXPR)
<input id="txtDelegatedToLookup" type="text" />
#Html.ValidationMessageFor(LAMBDAEXPR)
<script type="text/javascript">
$(document).ready(function () {
InitLookup(txtCtrlId, idCtrlId, '#Url.Action(Controller, Method)');
});
</script>
}
If you want to encapsulate the javascript portion you could create an HtmlHelper that will generate the desired content.
An example of what one would look like is as follows:
public static MvcHtmlString DynamicLookupFor(this HtmlHelper htmlHelper, string txtCtrlId, string idCtrlId, string controllerName, string actionName)
{
// create script tag
var script = new TagBuilder("script");
// generate script content
var result = new StringBuilder();
result.AppendLine("$(document).ready(function () {");
result.Append("InitLookup(");
result.Append("'" + txtCtrlId + "',");
result.Append("'" + idCtrlId + "',");
// generate action link
var url = new UrlHelper(HttpContext.Current.Request.RequestContext);
result.Append("'" + url.Action(actionName, controllerName));
result.AppendLine("'});");
script.InnerHtml = result.ToString();
return MvcHtmlString.Create(script.ToString());
}
Within your view you can call the helper as follows:
#Html.DynamicLookupFor("#txtDelegatedToLookup", "#Delegated_To_ID", "ContactLookup", "marMIS")
Don't forget to register the helper within the Views folder web.config:
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="Names Space Of Helper"/>
</namespaces>
OK, here's what I ended up doing in the end. It works very very well indeed! It will need a little polish in the future, and it leaves the door open to extend this a little too. For example, the control validates, but it doesn't color itself as per the rest of my validation, rather it just shows the textual validation message.
The functionality is called as such...
#Html.CustomLookupFor(m => m.Task_History.Delegated_To_ID, "txtDelegatedToLookup", #Url.Action("ContactLookup", "marMIS"), new { width = "250px" })
...and requires a using statement at the top of the razor view page...
#using Custom.MVC.Helpers;
BTW, two things...
jquery 1.7.2+ and jquery ui 1.8.16+ is what was used whilst I was developing this!
Also, the functionality in the first code section below includes both a minified version of the
javascript functionality within the code, and also a commented section containing the original
javascript code formatted nicely.
There are then two peices of code in the background. The first one here is the re-usable code that I was seeking to develop with regards to my initial question. The second one below it is the controller method on the server.
using System;
using System.Linq.Expressions;
using System.Text;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.Routing;
namespace Custom.MVC.Helpers
{
public static class CustomHtmlHelperExtensions
{
public static MvcHtmlString CustomLookupFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> exp, string id, string url, object options)
{
var hidCtrlId = id + "_id";
//Options
var opt = new RouteValueDictionary(options);
var textBoxWidth = (opt["width"] != null) ? opt["width"].ToString() : "";
var textBoxVisibility = (opt["visibility"] != null) ? opt["visibility"].ToString() : "";
//Construct the script fired when the document is fully loaded
var sbScript = new StringBuilder();
sbScript.Append("<script type='text/javascript'>");
sbScript.Append(" function InitDynamicLookupFor(e,f){var g='#'+e;var h='#'+e+'_id';$(g).click(function(){$(g).val('');$(h).val('');$(h).trigger('change')});$(g).autocomplete({minLength:3,delay:100,autoFocus:true,autofill:true,mustMatch:true,matchContains:true,width:220,source:function(c,d){$.ajax({url:f,type:'POST',dataType:'json',data:{searchId:0,searchTerm:c.term,searchLimit:10},success:function(b){d($.map(b,function(a){return{id:a.id,value:a.value}}))}})},create:function(b,c){if($(h).val()!=''){$.ajax({url:f,type:'POST',dataType:'json',data:{searchId:$(h).val(),searchTerm:'',searchLimit:1},success:function(a){$(g).val(a[0].value);$(g).removeClass('DynamicLookupForNotSelected');$(g).addClass('DynamicLookupForSelected')}})}},select:function(a,b){$(h).val(b.item.id);$(g).val(b.item.value);$(g).removeClass('DynamicLookupForNotSelected');$(g).addClass('DynamicLookupForSelected');$(h).trigger('change');return false},open:function(a,b){$(h).val(null);$(g).removeClass('DynamicLookupForSelected');$(g).addClass('DynamicLookupForNotSelected')}});if($(h).val()==''){$(g).val('Type here to search!');$(g).removeClass('DynamicLookupForSelected');$(g).addClass('DynamicLookupForNotSelected')}}");
sbScript.Append(" ");
sbScript.Append(" $(document).ready(function () {");
sbScript.Append(" InitDynamicLookupFor('" + id + "', '" + url + "');");
sbScript.Append(" });");
sbScript.Append("</script>");
//Construct the HTML controls for the DynamicLookup and its validation
var sbCtrls = new StringBuilder();
sbCtrls.Append(html.HiddenFor(exp, new { id=hidCtrlId }));
sbCtrls.Append("<input id='" + id + "' type='text' style='width:" + textBoxWidth + "; visibility:" + textBoxVisibility + ";' />");
sbCtrls.Append(html.ValidationMessageFor(exp));
//Return the lot back to the interface
var retString = sbScript.ToString() + sbCtrls.ToString();
return new MvcHtmlString(retString);
}
}
}
//*** This is the original javascript code before it is minified for use above! DON'T DELETE! ***
//
// function InitDynamicLookupFor(textBox, url) {
// var $textBox = '#' + textBox;
// var $hiddenId = '#' + textBox + '_id';
// $($textBox).click(function () {
// $($textBox).val('');
// $($hiddenId).val('');
// $($hiddenId).trigger('change');
// });
// $($textBox).autocomplete({
// minLength: 3,
// delay: 100,
// autoFocus: true,
// autofill: true,
// mustMatch: true,
// matchContains: true,
// width: 220,
// source: function (request, response) {
// $.ajax({
// url: url, type: 'POST', dataType: 'json',
// data: { searchId: 0, searchTerm: request.term, searchLimit: 10 },
// success: function (data) {
// response($.map(data, function (item) {
// return {
// id: item.id,
// value: item.value
// };
// }));
// }
// });
// },
// create: function (event, ui) {
// if ($($hiddenId).val() != '') {
// $.ajax({
// url: url, type: 'POST', dataType: 'json',
// data: { searchId: $($hiddenId).val(), searchTerm: '', searchLimit: 1 },
// success: function (data) {
// $($textBox).val(data[0].value);
// $($textBox).removeClass('DynamicLookupForNotSelected');
// $($textBox).addClass('DynamicLookupForSelected');
// }
// });
// }
// },
// select: function (event, ui) {
// $($hiddenId).val(ui.item.id);
// $($textBox).val(ui.item.value);
// $($textBox).removeClass('DynamicLookupForNotSelected');
// $($textBox).addClass('DynamicLookupForSelected');
// $($hiddenId).trigger('change');
// return false;
// },
// open: function (event, ui) {
// $($hiddenId).val(null);
// $($textBox).removeClass('DynamicLookupForSelected');
// $($textBox).addClass('DynamicLookupForNotSelected');
// }
// });
// //If no value selected by now, indicate to the user how to use the control
// if ($($hiddenId).val() == '') {
// $($textBox).val('Type here to search!');
// $($textBox).removeClass('DynamicLookupForSelected');
// $($textBox).addClass('DynamicLookupForNotSelected');
// }
//}
The controller method on the server...
public JsonResult ContactLookup(int searchId, string searchTerm, int searchLimit)
{
//Prepare search filter from criteria entered
var p = PredicateBuilder.True<vw_Contact_Verbose>();
if (searchId != 0) p = p.And(x => x.Contact_ID == searchId);
if (searchTerm != "") p = p.And(x => x.Fullname.Contains(searchTerm));
//Grab data
var results =
(from x in _mDb.ent.vw_Contact_Verbose.AsExpandable().Where(p).OrderBy("Fullname Desc").Take(searchLimit)
select new { id = x.Contact_ID, value = x.Fullname + " (" + x.Company + ")" }).ToArray();
return Json(results, JsonRequestBehavior.AllowGet);
}
I do hope that this re-useable code is found to be as useful to anybody else as it is to me. It's certainly made my code less verbose in razor views.

Resources