Here I have an example I'm building (with help).
Currently, the XML is stored in a data island. But what if I wanted to make a request to an external server? Would I use an XMLHttpRequest?
How would I code that in this example, and avoid the Cross Origin XMLHttpRequest problem?
In this example, I've tried playing with function loadXMLDoc(statelabel) but without success.
Am I on the right track with this function?
function loadXMLDoc( statelabel ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
myFunction(this);
}
};
xhttp.open("GET", "state_data.xml", true);
xhttp.send();
}
Fully functional code here:
<!DOCTYPE html>
<!--
SO
datalist / xml handling
Q 51200490 (https://stackoverflow.com/questions/51200490/how-to-find-the-node-position-of-a-value-in-an-xml-tree/51201494)
A
-->
<html>
<head>
<title>SO sample</title>
<script>
// Setup of keypress event handler, default selection of xml data.
function setupEH () {
var n = document.getElementById("myInputId");
n.addEventListener("keyup", function(event) {
event.preventDefault();
if (event.keyCode === 13) {
document.getElementById("myButton").click();
}
});
loadXMLDoc('Alabama'); // comment out this line if you want a vanilla UI after loading the html page.
}
// Load the xml document
function loadXMLDoc( statelabel ) {
// The xml document is retrieved with the following steps:
// 1. Obtain the (in-document) source as a DOM node.
// 2. Extract textual content.
// 3. Instantiate the xml parser (a browser built-in)
// 4. Parse textual content into an xml document
//
// When retrieving the xml document by means of ajax, these steps will be handled by the library for you - a parsed xml document will be available as a property or through calling a method.
//
let x_xmlisland = document.getElementById("template_xml");
let s_xmlsource = x_xmlisland.textContent;
let parser = new DOMParser();
let xmlDoc = parser.parseFromString(s_xmlsource, "application/xml");
myFunction(xmlDoc, statelabel); // Actual work ...
}
// Processing the xml document
function myFunction(xmlDoc, statelabel) {
// debugger; // uncomment to trace
//
// Every bit of information is processed as follows:
// - Get the relevant xml subtree ( `UNIT` element of the selected state incl.descendants )
// - Extract the textual value.
// - Feed the textual value to the Html elements prsenting the result.
//
var xpr_current_unit = xmlDoc.evaluate("/STATE_DATA/UNIT[./STATE[./text() = '"+statelabel+"']]",xmlDoc,null,XPathResult.ORDERED_NODE_ITERATOR_TYPE,null);
var node_current_unit = xpr_current_unit.iterateNext();
//
// The subsequent calls to xmlDoc.evaluate set the current UNIT element as their context node ('starting point'/'temporary root' for the xpath expression).
// The context node is referenced by '.' (dot)
//
var xpr_s = xmlDoc.evaluate("./STATE/text()",node_current_unit,null,XPathResult.ORDERED_ANY_TYPE,null);
var node_s = xpr_s.iterateNext();
var s = node_s.textContent
document.getElementById("state").innerHTML = s;
var xpr_g = xmlDoc.evaluate("./GDP/text()",node_current_unit,null,XPathResult.ORDERED_ANY_TYPE,null);
var node_g = xpr_g.iterateNext();
var g = "Unknown";
if ( node_g !== null ) {
g = node_g.textContent;
}
document.getElementById("gdp").innerHTML = g;
var xpr_p = xmlDoc.evaluate("./POPULATION/text()",node_current_unit,null,XPathResult.ORDERED_ANY_TYPE,null);
var node_p = xpr_p.iterateNext();
var p = "Unknown";
if ( node_p !== null ) {
p = node_p.textContent;
}
document.getElementById("population").innerHTML = p;
// cf. https://stackoverflow.com/a/3437009
var xpr_u = xmlDoc.evaluate("count(./preceding::UNIT)+1.",node_current_unit,null,XPathResult.ORDERED_ANY_TYPE,null);
var n_ucount = xpr_u.numberValue;
document.getElementById("inputValue").innerHTML = s;
document.getElementById("nodePosition").innerHTML = n_ucount;
}
// Setup the submit click handler
function ehClick ( ) {
let node_choice = document.getElementById('myInputId');
loadXMLDoc(node_choice.value);
}
</script>
<style>
</style>
</head>
<body onload="setupEH()">
<script id="template_xml" type="text/xml"><?xml version="1.0" encoding="UTF-8"?>
<STATE_DATA>
<UNIT>
<STATE>Wisconsin</STATE>
<GDP>232,300,000,000</GDP>
<POPULATION>5,800,000</POPULATION>
</UNIT>
<UNIT>
<STATE>Alabama</STATE>
<GDP>165,800,000,000</GDP>
<POPULATION>4,900,000</POPULATION>
</UNIT>
<UNIT>
<STATE>California</STATE>
<!-- Note: the GDP node for this unit is missing -->
<POPULATION>39,600,000</POPULATION>
</UNIT>
<UNIT>
<STATE>Texas</STATE>
<GDP>1,600,000,000,000</GDP>
<POPULATION>28,300,000</POPULATION>
</UNIT>
<UNIT>
<STATE>Michigan</STATE>
<GDP>382,000,000</GDP>
<POPULATION>10,000,000</POPULATION>
</UNIT>
</STATE_DATA>
</script>
<input list="myInput" id="myInputId" value="">
<button id="myButton" onClick="ehClick()">submit</button>
<p>input value: <span id="inputValue"></span></p>
<p>XML tree node position of input value: <span id="nodePosition"></span></p>
<p>State: <span id="state"></span></p>
<p>GDP: <span id="gdp"></span></p>
<p>Population: <span id="population"></span></p>
<datalist id="myInput">
<option id="AL">Alabama</option>
<option id="CA">California</option>
<option id="MI">Michigan</option>
<option id="TX">Texas</option>
<option id="WI">Wisconsin</option>
</datalist>
</body>
</html>
This gets a little bit involved using vanilla XMLHttpRequest to load XML. Here is a quick sample.
function loadXMLDoc() {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
myFunction(this);
}
};
xmlhttp.open("GET", "sample.xml" , true);
xmlhttp.send();
}
function myFunction(xml) {
var x, i, xmlDoc, table;
xmlDoc = xml.responseXML;
table = "<tr><th>Artist</th><th>Title</th></tr>";
x = xmlDoc.getElementsByTagName("CD")
for (i = 0; i < x.length; i++) {
table += "<tr><td>" +
x[i].getElementsByTagName("ARTIST")[0].childNodes[0].nodeValue +
"</td><td>" +
x[i].getElementsByTagName("TITLE")[0].childNodes[0].nodeValue +
"</td></tr>";
}
document.getElementById("demo").innerHTML = table;
}
However, it gets much more complicated esp. if you want to do a cross-domain request.
That's probably the right moment to pick up jQuery to make things easier:
$.ajax({
url: 'https://www.w3schools.com/xml/note.xml',
dataType: 'XML',
type: 'GET',
async: false,
crossDomain: true,
success: function () { },$.ajax({
url: 'https://www.w3schools.com/xml/note.xml',
dataType: 'XML',
type: 'GET',
async: false,
crossDomain: true,
success: function () { },
failure: function () { },
complete: function (xml) {
// Parse the xml file and get data
var xmlDoc = $.parseXML(xml);
$xml = $(xmlDoc);
$xml.find('body').each(function () {
console.log($(this).text());
});
}
});
Related
basic question. Am using Fetch XML builder in XRM. How do I tell Fetch to return the optionset label instead of the numeric value?
I have tried everything. I can see in the result set the name of the OptionSet item is "there":
<resultset morerecords="0" >
<result>
<est formattedvalue="4,337.50" >4337.5</est>
<sector name="Sustainable Industrial" formattedvalue="100000000" >100000000</sector>
<sector_new_nsectorname>Sustainable Industrial</sector_new_nsectorname>
</result>
<result>
<est formattedvalue="3,216.00" >3216</est>
<sector name="Renewable" formattedvalue="100000002" >100000002</sector>
<sector_new_nsectorname>Renewable</sector_new_nsectorname>
</result>
<result>
<est formattedvalue="2,329.25" >2329.25</est>
<sector name="Environmental" formattedvalue="100000001" >100000001</sector>
<sector_new_nsectorname>Environmental</sector_new_nsectorname>
</result>
</resultset>
But the actual results I get in column view are:
In the bottom left corner of the Results view, click Appearance - Friendly Names.
I am not sure if you wanted to know how to display the OptionSet Label in the FetchXrmBuilder or actually get the value of the label using fetchXML in your code.
Unfortunately you cannot get the label value through manipulating the fetch. I am adding a sample JS code to get the label value for an OptionSet by making another fetch request to stringmap entity. I have used product(Entity) and producttypecode(OptionSet) as to display how you can get the corresponding label values in a single result set by manipulating the result set of 2 different fetch.
var finalProductList = [];
var optionSetValueDict = {};
var productResultSet = [];
function OnLoad(executionContext)
{
RetrieveOptionSetValue();
RetrieveProduct();
}
function CreateOptionSetDict(optionSetValueResultSet)
{
optionSetValueResultSet.forEach(function (obj)
{
optionSetValueDict[obj.attributevalue] = obj.value;
});
}
function CreateProductList(productResultSet)
{
productResultSet.forEach(function (obj)
{
var finalProduct = {};
finalProduct['producttypecode'] = obj.producttypecode;
finalProduct['producttypecodevalue'] = optionSetValueDict[obj.producttypecode];
finalProduct['productnumber'] = obj.productnumber;
finalProductList.push(finalProduct);
});
}
function RetrieveProduct()
{
var results;
var fetchXmlQuery = '<fetch top="50" ><entity name="product" ><attribute name="productnumber" /><attribute name="producttypecode" /></entity></fetch>';
var req = new XMLHttpRequest();
req.open(
"GET",
Xrm.Page.context.getClientUrl() +
"/api/data/v9.0/products?fetchXml=" + encodeURIComponent(fetchXmlQuery),
false);
req.setRequestHeader("Prefer", 'odata.include-annotations="*"');
req.onreadystatechange = function ()
{
if (this.readyState === 4)
{
req.onreadystatechange = null;
if (this.status === 200)
{
var results = JSON.parse(this.response);
CreateProductList(results.value);
}
else
{
alert(this.statusText);
}
}
};
req.send();
}
function RetrieveOptionSetValue()
{
var fetchXmlQuery = '<fetch><entity name="stringmap" ><attribute name="attributevalue" /><attribute name="value" /><filter type="and" ><condition attribute="objecttypecodename" operator="eq" value="product" /><condition attribute="attributename" operator="eq" value="producttypecode" /></filter></entity></fetch>';
var req = new XMLHttpRequest();
req.open(
"GET",
Xrm.Page.context.getClientUrl() +
"/api/data/v9.0/stringmaps?fetchXml=" + encodeURIComponent(fetchXmlQuery),
false);
req.setRequestHeader("Prefer", 'odata.include-annotations="*"');
req.onreadystatechange = function ()
{
if (this.readyState === 4)
{
req.onreadystatechange = null;
if (this.status === 200)
{
var results = JSON.parse(this.response);
CreateOptionSetDict(results.value);
}
else
{
alert(this.statusText);
}
}
};
req.send();
}
Hope it helps. If you are looking for a way to this in C#, please comment so.
This is a follow up to a question I posted earlier on parsing xAPI statements. I got the parsing to work and now I'm using the code below to get statements from the ADL LRS and it pulls the first 50 records. Is there a way to specify more records? Thank you.
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>Get my Statements</title>
<script type="text/javascript" src="tincan.js"></script>
</head>
<body>
<h1>Get statements</h1>
<div id='response'></div>
<script>
var tincan = new TinCan (
{
recordStores: [
{
endpoint: "https://lrs.adlnet.gov/xapi/",
username: "xapi-tools",
password: "xapi-tools",
allowFail: false
}
]
}
);
var container = document.getElementById('response');
tincan.getStatements({
'callback': function (err, result) {
container.innerHTML = (err !== null ? 'ERROR' : parseMyData(result));
}
});
parseMyData = function(result) {
var statements = result.statements;
var output = '';
var name,verb,activity;
for(var i=0;i<statements.length;i++){
// check the statement for a usable name value
// (priority = actor.name, actor.mbox, actor.account.name)
if(statements[i].actor.name != null && statements[i].actor.name != "") {
name = statements[i].actor.name
}else if(statements[i].actor.mbox != null && statements[i].actor.mbox != "") {
name = statements[i].actor.mbox
}else{
name = statements[i].actor.account.name
}
// check the statement for a usable verb value
// (priority = verb.display['en-US'], verb.id)
try{
verb = statements[i].verb.display['en-US'];
}catch(e){
verb = statements[i].verb.id;
}
// check the activity for a usable value
// (priority = definition.name['en-US'], id)
try{
activity = statements[i].target.definition.name['en-US'];
}catch(e){
activity = statements[i].target.id;
}
output += name + ' - ' +
verb + ' - ' +
activity +
'<br>'
}
return output;
}
</script>
</body>
</html>
More records would be in the request itself. Look at the API documentation, most times there is a parameter you can pass for records to be retrieved.
Use the code below to request statements with a set limit per page. If you make a request without a limit parameter or a limit of 0, then the maxmimum number of statements allowed by the server will be returned in the first page (this is what you are already doing above).
tincan.getStatements({
params: {
limit: 100
},
'callback': function (err, result) {
container.innerHTML = (err !== null ? 'ERROR' : parseMyData(result));
}
See https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#stmtapiget
To get additional pages of statements use the TinCanJS LRS moreStatements method: https://github.com/RusticiSoftware/TinCanJS/blob/master/src/LRS.js#L766
See http://rusticisoftware.github.io/TinCanJS/ for how to create the LRS object.
I have a View in which i have criteria a Supplier TextBox a LastMonth dropdown and a Months Textbox.
#using JouleBrokerDB.ViewModels;
#model AssignPayReportToDepositViewModel
#{
ViewBag.Title = "View";
}
<link href="#Url.Content("~/Content/kendo/kendo.common-bootstrap.min.css")" rel="stylesheet" />
<link href="#Url.Content("~/Content/kendo/kendo.bootstrap.min.css")" rel="stylesheet" />
<link href="#Url.Content("~/Content/kendo/kendo.dataviz.min.css")" rel="stylesheet" />
<link href="#Url.Content("~/Content/kendo/kendo.dataviz.bootstrap.min.css")" rel="stylesheet" />
<style>
.treediv {
display: inline-block;
vertical-align: top;
width: 440px;
/*height:400px;*/
min-height: 400px;
text-align: left;
margin: 0 2em;
border-radius: 25px;
border: 2px solid #8AC007;
padding: 15px;
overflow: auto;
}
</style>
<div class="row">
<div class="col-md-9 col-md-offset-1">
#using (Html.BeginForm("Show", "AssignPayReportToDeposit", FormMethod.Post, new { id = "AssignToPayReportForm", #class = "form-horizontal" }))
{
<fieldset>
<!-- Form Name -->
<legend>Assign Pay Report to Deposit</legend>
<div class="form-group">
<!-- Supplier -->
<div class="col-sm-4">
#Html.Label("", "Supplier:", new { #class = "control-label", #for = "textinput" })
<div id="suppliers">
#Html.DropDownListFor(x => x.SuppliersList, new SelectList(Model.SuppliersList, "SupplierID", "Name"), new { id = "ddSupplier", #class = "form-control" })
</div>
</div>
<!-- Last Month -->
<div class="col-sm-4">
#Html.Label("", "Last Month:", new { #class = "control-label", #for = "textinput" })
#Html.DropDownListFor(x => x.LastMonthsList, new SelectList(Model.LastMonthsList), new { #id = "ddLastMonth", #class = "form-control" })
</div>
<!-- Months-->
<div class="col-sm-4">
#Html.Label("", "Months:", new { #class = "control-label", #for = "textinput" })
#Html.TextBox("txtMonths", null, new { type = "number", step = 1, min = 1, max = 12, #class = "form-control", required = "required" })
</div>
</div>
</fieldset>
<div class="treediv">
#Html.Label("", "UnAssigned PayReport:", new { #class = "control-label", #for = "textinput" })
<div id="TreeView_UPR" style="padding:5px"></div>
</div>
<div class="treediv">
#Html.Label("", "Deposits:", new { #class = "control-label", #for = "textinput" })
<h4></h4>
<div id="TreeView_AD" style="padding:5px"></div>
</div>
}
</div>
</div>
<script src="#Url.Content("~/Scripts/kendo/kendo.all.min.js")"></script>
<script src="#Url.Content("~/Scripts/Views/AssignPayReportToDeposit/Show.js")"></script>
Here on this text box i have attached changed event though jQuery. The requirement is that whenever the criteria changes the treeview div will be filled with data will be refreshed.
AssignPayReportsToDeposit.AttachEvents = function () {
$("#ddSupplier").change(AssignPayReportsToDeposit.OnSupplierChange);
$("#ddLastMonth").change(AssignPayReportsToDeposit.OnLastMonthChange);
$("#txtMonths").change(AssignPayReportsToDeposit.OnMonthsChange);
}
these changed event handler will handle the refreshing the treeview. The whole thing is handled through ajax calls.
Now i know that using Ajax.ActionLink and UpdateTargetId parameter with Replace option i can return the treeview in partial view so the manual handling can be removed. but that will require me put the anchor button which user have to click. Requirement is that the refresh of treeview should be done on any criteria change.
Is there any way i am able to achieve this using Ajax.ActionLink (or any another razor syntax that will take load off from the manual handling ) ? On change event of the controls i would like to call a controller using ajax.actionlink which will return a partialview and update the div.
Edit: I am handling this through jQuery right now. so i will post the complete code for more understanding.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using JouleBrokerDB;
using JouleBrokerDB.ViewModels;
using JouleBroker.Filters;
namespace JouleBroker.Controllers
{
[RoutePrefix("AssignPayReportToDeposit")]
[Route("{action=Show}")]
public class AssignPayReportToDepositController : Controller
{
// GET: AssignPayReportToDeposit
//[Route("Show",Name = "APTDShow")]
//[ValidateLogin]
public ActionResult Show()
{
List<SupplierViewModel> suppliers = DBCommon.GetAllSuppliers(false);
SuppliersList_LastMonthsList_ViewModel model = new SuppliersList_LastMonthsList_ViewModel()
{
SuppliersList = suppliers,
LastMonthsList = new List<string>()
};
return View(model);
}
[HttpPost]
[Route("GetUnAssignedPayReports")]
public JsonResult GetUnAssignedPayReports(int SupplierID,
string MonthPaid,
int Months)
{
var payreports = AssignPayReportsToDepositData.GetUnAssignedPayReports(SupplierID,
MonthPaid,
Months);
return Json(payreports);
}
[HttpPost]
[Route("GetAssignedPayReports")]
public JsonResult GetAssignedPayReports(int SupplierID,
string MonthPaid,
int Months)
{
var payreports = AssignPayReportsToDepositData.GetAssignedPayReports(SupplierID,
MonthPaid,
Months);
return Json(payreports);
}
[HttpPost]
[Route("AssignDepositIdToPayReport")]
public bool AssignDepositIdToPayReport(int PayReportID, int DepositID)
{
return AssignPayReportsToDepositData.AssignDepositIdToPayReport(PayReportID, DepositID);
}
}
}
JavaScript File (the code is a bit lengthy so you don't need to look at all of them you can see the methods which are calling the action methods. GetUnAssignedPayReports and GetAssignedPayReports which returns the data which is used to fill the tree view.) I just want this portion to moved to partial view and passing model to partial view generate treeview there and replace the div each time on change event with rendering partial view again. Hope i am clear enough. so change the above methods to return partial instead of json result that what i am trying to achive
function AssignPayReportsToDeposit() { }
AssignPayReportsToDeposit.SelectedSupplierID = 0;
AssignPayReportsToDeposit.SelectedLastMonth = null;
AssignPayReportsToDeposit.SelectedMonths = 0;
AssignPayReportsToDeposit.LastMonthsList = null;
AssignPayReportsToDeposit.UnAssignedPayReportsList = null;
AssignPayReportsToDeposit.AssignedPayReportsList = null;
AssignPayReportsToDeposit.LastTextChangedNode = null;
//--------- Document Ready Function -------- //
$(document).ready(function () {
//AttachEvents
AssignPayReportsToDeposit.AttachEvents();
});
AssignPayReportsToDeposit.AttachEvents = function () {
$("#ddSupplier").change(AssignPayReportsToDeposit.OnSupplierChange);
$("#ddLastMonth").change(AssignPayReportsToDeposit.OnLastMonthChange);
$("#txtMonths").change(AssignPayReportsToDeposit.OnMonthsChange);
}
//Handles Supplier ChangeEvents
AssignPayReportsToDeposit.OnSupplierChange = function () {
//Get Changed Supplier ID
AssignPayReportsToDeposit.SelectedSupplierID = $('#ddSupplier').val();
//Get Last Month List
AssignPayReportsToDeposit.LastMonthsList = CommonAction.GetLastPayReportMonthsBySupplierID(AssignPayReportsToDeposit.SelectedSupplierID);
//Fill Last Month List
AssignPayReportsToDeposit.FillLastMonths();
//Refresh TreeView_UPR
AssignPayReportsToDeposit.RefreshTreeViewUPR();
//Refresh TreeView_AD
AssignPayReportsToDeposit.RefreshTreeViewAD();
}
//Handles Last Month Change Event
AssignPayReportsToDeposit.OnLastMonthChange = function () {
AssignPayReportsToDeposit.SelectedLastMonth = $('#ddLastMonth').val();
//Refresh TreeView_UPR
AssignPayReportsToDeposit.RefreshTreeViewUPR();
//Refresh TreeView_AD
AssignPayReportsToDeposit.RefreshTreeViewAD();
}
//Handles Month Change Event
AssignPayReportsToDeposit.OnMonthsChange = function () {
AssignPayReportsToDeposit.SelectedMonths = $('#txtMonths').val();
//Refresh TreeView_UPR
AssignPayReportsToDeposit.RefreshTreeViewUPR();
//Refresh TreeView_AD
AssignPayReportsToDeposit.RefreshTreeViewAD();
}
//Fills Last Month Dropdown with options
AssignPayReportsToDeposit.FillLastMonths = function () {
var ddLastMonth = $("#ddLastMonth");
if (ddLastMonth != undefined) {
ddLastMonth.empty();
if (AssignPayReportsToDeposit.LastMonthsList != undefined) {
$.each(AssignPayReportsToDeposit.LastMonthsList, function () {
Common.AddOptionToSelect(ddLastMonth, this.Text, this.Text);
});
ddLastMonth.val(AssignPayReportsToDeposit.LastMonthsList[0].Text);
AssignPayReportsToDeposit.SelectedLastMonth = ddLastMonth.val();
}
}
}
AssignPayReportsToDeposit.ValidateControls = function () {
var success = true;
if (AssignPayReportsToDeposit.SelectedSupplierID == undefined ||
AssignPayReportsToDeposit.SelectedSupplierID == 0) {
// bootbox.alert('Please select a Supplier');
success = false;
}
else if (AssignPayReportsToDeposit.SelectedLastMonth == undefined ||
AssignPayReportsToDeposit.SelectedLastMonth == '') {
// bootbox.alert('Please select Last Month');
success = false;
}
else if (AssignPayReportsToDeposit.SelectedMonths == undefined ||
AssignPayReportsToDeposit.SelectedMonths == 0) {
// bootbox.alert('Please Enter Months');
success = false;
}
return success;
}
//Assigns DepositIdToPayReport
AssignPayReportsToDeposit.AssignDepositIdToPayReport = function (PayReportID, DepositID) {
var success = false;
if (PayReportID != undefined && DepositID != undefined) {
var jsonData = JSON.stringify({ PayReportID: PayReportID, DepositID: DepositID });
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: 'AssignPayReportToDeposit/AssignDepositIdToPayReport',
data: jsonData,
async: false,
success: function (result) {
success = result;
},
error: Common.AjaxErrorHandler
});
}
return success;
}
//--------- Tree View UPR Functions -------- //
//Gets UnAssigned Pay Reports
AssignPayReportsToDeposit.GetUnAssignedPayReports = function () {
var payReports;
if (AssignPayReportsToDeposit.ValidateControls()) {
var jsonData = JSON.stringify(
{
SupplierID: AssignPayReportsToDeposit.SelectedSupplierID,
MonthPaid: AssignPayReportsToDeposit.SelectedLastMonth,
Months: AssignPayReportsToDeposit.SelectedMonths
});
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: "AssignPayReportToDeposit/GetUnAssignedPayReports",
data: jsonData,
async: false,
success: function (data) {
if (data != undefined && data != "")
payReports = data;
},
error: Common.AjaxErrorHandler
});
}
return payReports;
}
AssignPayReportsToDeposit.BindTreeViewUPR = function () {
var treeview = $("#TreeView_UPR");
var inline = new kendo.data.HierarchicalDataSource({
data: AssignPayReportsToDeposit.UnAssignedPayReportsList,
schema: {
model: {
id: "PayReportID"
}
}
});
treeview.kendoTreeView({
dragAndDrop: true,
dataSource: inline,
dataBound: function (e) {
if (!this.dataSource.data().length) {
this.element.append("<p class='no-items'>No items yet.</p>");
} else {
this.element.find(".no-items").remove();
}
},
dataTextField: ["DisplayValue"],
drop: AssignPayReportsToDeposit.OnTreeViewUPRDrop
});
}
AssignPayReportsToDeposit.OnTreeViewUPRDrop = function (e) {
var isTargetTreeViewAD = false;
var sourceDataItem = this.dataItem(e.sourceNode);
var targetDataItem = this.dataItem(e.destinationNode);
if (targetDataItem == undefined) {
targetDataItem = $("#TreeView_AD").data("kendoTreeView").dataItem(e.destinationNode);
isTargetTreeViewAD = true;
}
if (sourceDataItem == undefined ||
targetDataItem == undefined) {
//Source and target both must exists
e.preventDefault();
return;
}
if (sourceDataItem.IsDeposit == true) {
//Deposits cannot be drag and Drop
e.preventDefault();
return;
}
if (isTargetTreeViewAD) {
if (e.dropPosition == "over" &&
sourceDataItem.IsPayReport == true &&
sourceDataItem.IsAssignedPayReport == false &&
targetDataItem.IsDeposit == true) {
//Source must UnAssigned Payreport Target Must be Deposit and Drop position must over
//Implement logic to assign deposit id to the Pay Report
var PayReportID = sourceDataItem.PayReportID;
var DepositID = targetDataItem.DepositID;
if (AssignPayReportsToDeposit.AssignDepositIdToPayReport(PayReportID, DepositID)) {
sourceDataItem.set("DepositID", DepositID);
sourceDataItem.set("IsAssignedPayReport", true);
}
else {
//Didnt update the record don't do the drop
e.preventDefault();
return;
}
}
else {
e.preventDefault();
return;
}
}
else {
if ((e.dropPosition == "before" || e.dropPosition == "after") &&
sourceDataItem.IsPayReport == true &&
targetDataItem.IsPayReport == true &&
targetDataItem.IsAssignedPayReport == false) {
//Only allow sorting in this condition otherwise cancel drop event
//Means only allow sorting of unassigned payreports within the tree
}
else {
e.preventDefault();
return;
}
}
}
AssignPayReportsToDeposit.RefreshTreeViewUPR = function () {
//Destroy and empty tree
var treeview = $("#TreeView_UPR").data("kendoTreeView");
if (treeview != undefined) { treeview.destroy(); }
treeview = $("#TreeView_UPR");
treeview.empty();
AssignPayReportsToDeposit.UnAssignedPayReportsList = AssignPayReportsToDeposit.GetUnAssignedPayReports();
AssignPayReportsToDeposit.BindTreeViewUPR();
}
//--------- TreeView_AD Functions -------- //
//Gets Assigned Pay Reports
AssignPayReportsToDeposit.GetAssignedPayReports = function () {
var payReports;
if (AssignPayReportsToDeposit.ValidateControls()) {
var jsonData = JSON.stringify(
{
SupplierID: AssignPayReportsToDeposit.SelectedSupplierID,
MonthPaid: AssignPayReportsToDeposit.SelectedLastMonth,
Months: AssignPayReportsToDeposit.SelectedMonths
});
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: "AssignPayReportToDeposit/GetAssignedPayReports",
data: jsonData,
async: false,
success: function (data) {
if (data != undefined && data != "")
payReports = data;
},
error: Common.AjaxErrorHandler
});
}
return payReports;
}
AssignPayReportsToDeposit.BindTreeViewAD = function () {
var treeview = $("#TreeView_AD");
var inline = new kendo.data.HierarchicalDataSource({
data: AssignPayReportsToDeposit.AssignedPayReportsList,
schema: {
model: {
id: "DepositID",
hasChildren: "HasAnyAssignedPayReports",
children: "AssignedPayReports"
}
}
});
treeview.kendoTreeView({
dragAndDrop: true,
dataSource: inline,
dataBound: function (e) {
if (!this.dataSource.data().length) {
this.element.append("<p class='no-items'>No items yet.</p>");
} else {
this.element.find(".no-items").remove();
}
},
dataTextField: ["DisplayValue", "DisplayValue"],
drop: AssignPayReportsToDeposit.OnTreeViewADDrop,
select: AssignPayReportsToDeposit.OnTreeViewADSelect
});
}
AssignPayReportsToDeposit.OnTreeViewADSelect = function (e) {
var dataItem = this.dataItem(e.node);
var treeview = this;
if (AssignPayReportsToDeposit.LastTextChangedNode != undefined) {
//Restore last node's Text
var previousDataItem = this.dataItem(AssignPayReportsToDeposit.LastTextChangedNode);
if (previousDataItem != undefined) {
var date = AssignPayReportsToDeposit.FormatDepositMonthToDisplay(previousDataItem.DepositDate);
var displaytext = "[" + date + "]" + "-[" + previousDataItem.BankName + "]-" + "[" + previousDataItem.Amount + "]";
this.text(AssignPayReportsToDeposit.LastTextChangedNode, displaytext);
}
AssignPayReportsToDeposit.LastTextChangedNode = undefined;
}
if (dataItem.IsDeposit) {
if (dataItem.hasChildren > 0) {
dataItem.set("expanded", true);
//Append sum to selected node's diplay value
var childs = dataItem.children.data();
var sum = 0;
$.each(childs, function () { sum += this.Amount });
var date = AssignPayReportsToDeposit.FormatDepositMonthToDisplay(dataItem.DepositDate);
var displaytext = "[" + date + "]" + "-[" + dataItem.BankName + "]-" + "[" + dataItem.Amount + "(" + sum + ")" + "]";
this.text(e.node, displaytext)
AssignPayReportsToDeposit.LastTextChangedNode = e.node;
}
}
}
AssignPayReportsToDeposit.FormatDepositMonthToDisplay = function (jsondate) {
var depositedate = "";
if (jsondate != undefined && jsondate != "") {
var date = Common.ParseDate(jsondate);
var month = ("0" + (date.getMonth() + 1)).slice(-2);
depositedate = date.getFullYear() + "-" + (month);
}
return depositedate;
}
AssignPayReportsToDeposit.OnTreeViewADDrop = function (e) {
var isTargetTreeViewURP = false;
var DroptoNoItemZone = false;
var sourceDataItem = this.dataItem(e.sourceNode);
var targetDataItem = this.dataItem(e.destinationNode);
var treeview_UPR = $("#TreeView_UPR").data("kendoTreeView");
if (targetDataItem == undefined) {
targetDataItem = treeview_UPR.dataItem(e.destinationNode);
if (treeview_UPR.element.find(".no-items").length > 0) DroptoNoItemZone = true;
isTargetTreeViewURP = true;
}
if ((sourceDataItem == undefined ||
targetDataItem == undefined) && DroptoNoItemZone == false) {
e.preventDefault();
return;
}
if (sourceDataItem.IsDeposit == true) {
//Deposits can not be moved within the tree view
e.preventDefault();
return;
}
if (isTargetTreeViewURP) {
if (((e.dropPosition == "before" || e.dropPosition == "after") &&
sourceDataItem.IsPayReport == true &&
sourceDataItem.IsAssignedPayReport == true &&
targetDataItem.IsPayReport == true) || (e.dropPosition == "over" && DroptoNoItemZone)) {
//Implement logic to unassing deposit id to PayReport
var PayReportID = sourceDataItem.PayReportID;
var DepositID = 0;
if (AssignPayReportsToDeposit.AssignDepositIdToPayReport(PayReportID, DepositID)) {
sourceDataItem.set("DepositID", DepositID);
sourceDataItem.set("IsAssignedPayReport", false);
}
else {
//Didnt update the record don't do the drop
e.preventDefault();
return;
}
}
else {
e.preventDefault();
return;
}
}
else {
if (e.dropPosition == "over" &&
sourceDataItem.IsPayReport == true &&
targetDataItem.IsDeposit == true) {
//Implement Logic to change deposit ID for assigned payreport
var PayReportID = sourceDataItem.PayReportID;
var DepositID = targetDataItem.DepositID;
if (AssignPayReportsToDeposit.AssignDepositIdToPayReport(PayReportID, DepositID)) {
sourceDataItem.set("DepositID", DepositID);
sourceDataItem.set("IsAssignedPayReport", true);
}
else {
//Didnt update the record don't do the drop
e.preventDefault();
return;
}
}
else {
e.preventDefault();
return;
}
}
}
AssignPayReportsToDeposit.RefreshTreeViewAD = function () {
//Destroy and empty tree
var treeview = $("#TreeView_AD").data("kendoTreeView");
if (treeview != undefined) { treeview.destroy(); }
treeview = $("#TreeView_AD");
treeview.empty();
AssignPayReportsToDeposit.LastTextChangedNode = undefined;
AssignPayReportsToDeposit.AssignedPayReportsList = AssignPayReportsToDeposit.GetAssignedPayReports();
AssignPayReportsToDeposit.BindTreeViewAD();
}
Unfortunately not out the box.
The Ajax extension methods are really just HTML helpers that work with other jQuery libraries. The helpers create the relavant HTML markup (such as adding custom addtributes data-*="") and the client scripts use this to determine their behaviour.
You could create your own MVC HTML helper and script library to handle change events for you however I would recommend looking at a front end framework such as Angular instead. This library would handle all the events declaratively so you don't need to waste time writing event handlers.
This is a simple test case for PhantomJS to demonstrate that an event handler that, when invoked, executes an AJAX call, does not work.
I've created a simple test here to try and access some content loaded via AJAX. It's very possible I've done something wrong, in which case I'd appreciate someone pointing out what that is. However, if not, I think there is a problem with PhantomJS.
Here's a simple page with a single that has a change event bound to it. When the value of the changes, it loads some content from the server and replaces the content of a specific <p>
The text of the <p id="bar">foo</p> should change to 'bar' after the ajax call is completed and processed.
Can anyone help me out?
<html>
<head>
<title>AJAX test</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script>
$(function(){
$('#getBar').change(function() {
$('#bar').load("/test/bar");
});
});
</script>
</head>
<body>
<h1>Foo</h1>
<div>
<select id="getBar">
<option value=""></option>
<option value="go" id="go">Get Bar Text</option>
</select>
</div>
<p id="bar">foo</p>
</body>
</html>
Here's the script I use to navigate to this simple page and ATTEMPT to use jQuery to change the value of the and trigger the change event.
The steps of the script are broken out into an array of 'step' functions:
var wp = require("webpage");
var system = require('system');
var util = require('./util-module.js'); // my logging API
var baseUrl = 'http://127.0.0.1:8080';
/* Global error handler for phantom */
phantom.onError = function(msg, trace) {
var msgStack = ['PHANTOM ERROR: ' + msg];
if (trace) {
msgStack.push('TRACE:');
trace.forEach(function(t) {
msgStack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line);
});
}
util.log.error(msgStack.join('\n'));
// exit phantom on error
phantom.exit();
};
/* Inject jQuery into the phantom context */
var injected = phantom.injectJs('./jquery.min.js');
util.log.debug('phantom injected jQuery: ' + injected);
/* Create and initialize the page */
var page = wp.create();
var loadInProgress = false;
page.onLoadStarted = function() {
loadInProgress = true;
util.log.debug("page load started: " + page.url);
};
page.onLoadFinished = function() {
loadInProgress = false;
util.log.debug("page load finished: " + page.url);
// inject jquery onto the page
var injected = page.injectJs('./jquery.min.js');
util.log.debug('page injected jQuery: ' + injected);
page.evaluate(function() {
jQuery.noConflict();
});
};
page.onResourceRequested = function(request) {
console.log('Request (#' + request.id + '): ' + JSON.stringify(request));
};
page.onResourceReceived = function(response) {
console.log('Response (#' + response.id + ', stage "' + response.stage + '"): ' + JSON.stringify(response));
};
/* Redirect all console messages logged on page to debug */
page.onConsoleMessage = function(msg) {
util.log.debug(msg);
};
var steps = [
function() {
util.log.debug('LOAD THE TEST PAGE');
page.open(baseUrl + "/test/foo");
},
function() {
util.log.debug('CHANGE THE SELECT');
// see what the first result is. change the sort. Wait for the ajax update to complete
// start iterating over results.
var oldTitle = page.evaluate(function() {
return jQuery('#bar').text();
});
util.log.debug('OLD TEXT: ' + oldTitle);
page.evaluate(function(){
jQuery('select').val('go');
jQuery('select').trigger('change');
jQuery('select').change();
console.log('SELECT VALUE AFTER UDPATE: ' + jQuery('select').val());
});
loadInProgress = true;
count = 0;
var fint = setInterval(function() {
var newTitle = page.evaluate(function() {
return jQuery('#bar').text();
});
util.log.debug('NEW TEXT: ' + newTitle);
count++;
if (oldTitle != newTitle) {
clearInterval(fint);
loadInProgress = false;
}
if (count > 5) {
clearInterval(fint);
loadInProgress = false;
}
}, 500);
},
function() {
util.log.debug('PRINT PAGE TITLE');
page.evaluate(function(){
console.log(document.title);
});
},
];
// harness that executes each step of the scraper
var testIndex = 0;
interval = setInterval(function() {
if (!loadInProgress && typeof steps[testIndex] == "function") {
util.log.debug("step " + (testIndex + 1));
steps[testIndex]();
testIndex++;
}
if (typeof steps[testIndex] != "function") {
util.log.debug("test complete!");
clearInterval(interval);
phantom.exit();
}
}, 500);
And here is the output. I'm expecting the text to change from 'foo' to 'bar' but it never happens
DEBUG: CHANGE THE SELECT
DEBUG: OLD TEXT: foo
DEBUG: SELECT VALUE AFTER UDPATE: go
DEBUG: NEW TEXT: foo
DEBUG: NEW TEXT: foo
DEBUG: NEW TEXT: foo
DEBUG: NEW TEXT: foo
DEBUG: NEW TEXT: foo
DEBUG: NEW TEXT: foo
DEBUG: step 5
DEBUG: PRINT PAGE TITLE
DEBUG: AJAX test
DEBUG: test complete!
BTW, PhantomJS 1.7. This is a great project.
The problem with the example listed above is that I simply injected jQuery into a page that already had jQuery. When I stopped doing that, it worked.
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.