MVC JavaScript Injection






4.54/5 (4 votes)
A way to inject JavaScript code in Razor.
Introduction
Suppose we have an MVC application which should display content from several tables in jqGrid controls. For each table we have to write JavaScript code for displaying data and more code for the controller allocated to each table. For each table, we have:
Model - generated by Visual Studio.
View:
$(function () {
jQuery(listName).jqGrid({
..........
}
Controller:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LoadDataInGrid ()
{
..........
}
If the database contains multiple tables that can mean a large amount of code/lost time.
Goal
Another approach would be that the application displays data without using the model. The advantage is that (at least) for displaying data we write less code. In addition, if you generalize the code, it may be reused.
Using the code
To try this method I created a project MVC4 - Web application / Razor (VS2010) and then I installed with nuget: jQuery, jQuery UI, and jqGrid (to get the latest version). To include these components in the application, we change:
App_Start/Bundle_Config:
public static void RegisterBundles(BundleCollection bundles)
{
//generated code
..........
// jqGrid
// js
bundles.Add(new ScriptBundle("~/bundles/jqGrid").Include(
"~/Scripts/i18n/grid.locale-en.js",
"~/Scripts/jquery.jqGrid.src.js"));
// css
bundles.Add(new StyleBundle("~/Content/jqGrid/css").Include(
"~/Content/jquery.jqGrid/ui.jqgrid.css"));
}
_Layout.cshtml:
<!-- generated code -->
..........
<head>
<meta charset="utf-8" />
<title>@ViewBag.Title - My ASP.NET MVC Application </title>
<link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<meta name="viewport" content="width=device-width" />
@Styles.Render("~/Content/css")
@Styles.Render("~/Content/themes/base/css")
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/jqueryui")
@Scripts.Render("~/bundles/modernizr")
</head>
..........
<body>
..........
<nav>
<ul id="menu">
<li>@Html.ActionLink("Home", "Index", "Home")</li/>
<li>@Html.ActionLink("About", "About", "Home")</li/>
<li>@Html.ActionLink("Contact", "Contact", "Home")</li/>
<li>@Html.ActionLink("Test", "Index", "Test")</li/>
</ul>
</nav>
..........
</body>
The automatically generated line @Scripts.Render("~/bundles/jquery")
is now in
the <head>
section.
Then I created a TestController (empty MVC controller) and the corresponding menu option Index. TestController.cs now looks like:
public ActionResult Index()
{
return View();
}
Now create the corresponding Index View (right-click on the text and AddView Index ...). Here we show the grid. First we include the js and css files for:
Views/Test/Index.cshtml:
@Styles.Render("~/Content/jqGrid/css")
@{
ViewBag.Title = "Index";
}
<div class="ui-corner-all floatLeft" style="margin-left:2%; width:96%;">
<select id="tableSelect">
<option value="" disabled selected>- select table-</option>
</select>
<div id="grid_wrapper">
<table id="list" class="scroll"></table>
<div id="pager" class="scroll" style="text-align: center;"></div>
</div>
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqGrid")
}
I created a <select>
control. It will display the tables contained in the database.
JavaScript code:
<script type="text/javascript">
$(function () {
var action = './Test/LoadTableNames';
$.get(action, 'html', function (retval) {
var arr = retval.split('*');
for (i = 0; i < arr.length; i++) {
$('#tableSelect').append(new Option(arr[i], arr[i]));
}
});
..........
In the controller, we define the LoadTableNames
function as follows:
public string LoadTableNames()
{
string sep = "*";
string[] tbls = GetTables();
return string.Join(sep, tbls);
}
The function returns the table names as "name1 * name2 * ..." and then uses JavaScript split function. To investigate the database:
public string[] GetTables()
{
string[] retval = null;
DataTable tblSchema = new DataTable();
SqlConnection cn = new SqlConnection(connectionString);
using (cn)
{
//GetSchema example
//http://msdn.microsoft.com/en-us/library/ms254934%28v=vs.80%29.aspx
cn.Open();
tblSchema = cn.GetSchema("Tables");
cn.Close();
}
retval = new string[tblSchema.Rows.Count];
for (int i = 0; i < tblSchema.Rows.Count; i++)
retval[i] = tblSchema.Rows[i][2] as string;
//string type = tblSchema.Rows[i][3] as string; type is "BASE TABLE" or "VIEW"
Array.Sort(retval);
return retval;
}
The connection string is defined as:
string connectionString =
"data source=.\\SQLEXPRESS;attachdbfilename=|DataDirectory|\\" +
"NORTHWND.MDF;integrated security=True;user instance=True;multipleactiveresultsets=True;";
At this time the database is opened and table names are displayed in the control
<select>
.
Next intercept the select change event:
..........
$("#tableSelect").change(function () {
$("#list").jqGrid("GridUnload");
var tableName = $("#tableSelect option:selected").val();
var action = './Test/InjectScript?tableName=' + tableName;
$.get(action, 'html', function (retval) {
try {
eval(retval);
jQuery("#list").setGridParam({ url: './Test/LoadGridData?tableName=' +
tableName, page: 1 }). trigger("reloadGrid");
} catch (e) {
alert(e);
}
});
});
});
</script>
The steps:
- If a grid has already been created, then destroy it:
$("#list").jqGrid("GridUnload");
- Generate JavaScript code to create a grid for the selected table:
var action = './Test/InjectScript?tableName=' + tableName;
eval(retval);
jQuery("#list").setGridParam({ url: './Test/LoadGridData?tableName=' + tableName, page: 1 }). trigger("reloadGrid");
So when the user selects a table, we try to generate JavaScript code:
public string InjectScript(string tableName)
{
string colNames = "colNames: [";
string colModel = "colModel: [";
string pk = string.Empty;
SqlConnection cn = new SqlConnection(connectionString);
using (cn)
{
DataTable t_struct = new DataTable();
string strSQL = string.Format("SELECT * FROM [{0}]", tableName);
SqlDataAdapter a_struct = new SqlDataAdapter(strSQL, cn);
a_struct.FillSchema(t_struct, SchemaType.Source);
foreach (DataColumn c in t_struct.Columns)
{
colNames += "'" + c.ColumnName + "', ";
colModel += "{ name: '" + c.ColumnName + "', index: '" + c.ColumnName + "' }, ";
}
colNames += "]";
colModel += "]";
colNames = colNames.Replace(", ]", " ]");
colModel = colModel.Replace(", ]", " ]");
pk = GetTableFirstPrimaryKeyName(t_struct);
}
if (string.IsNullOrEmpty(pk))
{
return
string.Format("alert('Table/View [{0}] does not have primary key')", tableName);
}
string r1 =
@"function createJQGrid(tblListName, divPagerName, tableName) {
var listName = '#' + tblListName;
var pagerName = '#' + divPagerName;
try {
var obg = $(listName);
if (obg != null) {
jQuery(listName).jqGrid({
url: '',
datatype: 'json',
mtype: 'POST',
autowidth: true,
height: '100%',
loadtext: 'Loading...',";
string r2 =
@"pgtext: '{0} / {1}',
recordtext: '{0} - {1} / {2}',
rowList: [10, 20, 30],
rowNum: 10,
pager: jQuery(pagerName),
sortname: '" + pk + "'," +
@"viewrecords: true,
gridview: true,
sortorder: 'asc',
caption: tableName,
emptyrecords: 'Nu exista inregistrari',
autosearch: false,
jsonReader: {
root: 'rows',
page: 'page',
total: 'total',
records: 'records',
repeatitems: true,
cell: 'cell'," +
"id: '" + pk + "' " +
@"}
}).navGrid(pagerName, { edit: false, add: false, del: false, search: true, searchtext: '' });
}
}
catch (err) {
alert(err.description);
}
};";
string r3 = string.Format("createJQGrid('list', 'pager', '{0}');", tableName);
string retval =
r1 + colNames + ", " + colModel + ", " + r2 + r3;
return
retval;
}
The GetTableFirstPrimaryKeyName
function tries to return the first column contained in the primary key definition of the table. If no primary key
is defined then we will
use the first field in the table (this field used should not be empty). The constructed string is then used in
the function eval ()
in JavaScript.
At this time we have built a grid that contains the columns for the selected table.
Loading data:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LoadGridData(string sidx, string sord, int page, int rows,
bool _search, string searchField, string searchOper, string searchString, string tableName)
{
DataTable t_data = new DataTable();
DataTable t_struct = new DataTable();
string strSQL = string.Format("SELECT * FROM [{0}]", tableName);
if (!string.IsNullOrEmpty(sidx))
strSQL = string.Format("SELECT * FROM [{0}] ORDER BY [{1}] {2}", tableName, sidx, sord);
SqlConnection cn = new SqlConnection(connectionString);
using (cn)
{
//struct
SqlDataAdapter a_struct = new SqlDataAdapter(strSQL, cn);
a_struct.FillSchema(t_struct, SchemaType.Source);
//date
SqlDataAdapter a_data = new SqlDataAdapter(strSQL, cn);
a_data.Fill(t_data);
}
string pk = GetTableFirstPrimaryKeyName(t_struct);
string[] properties = new string[t_struct.Columns.Count];
Type[] types = new Type[t_struct.Columns.Count];
for (int i = 0; i < t_struct.Columns.Count; i++)
{
DataColumn dc = t_struct.Columns[i];
properties[i] = dc.ColumnName;
types[i] = dc.DataType;
}
string jsonstring = JsonConvert.SerializeObject(t_data, Formatting.Indented);
GridResult jsonResults = null;
JContainer obarr = (JContainer)JsonConvert.DeserializeObject(jsonstring);
jsonResults = obarr.ToJQGridResult(page, rows, sidx, sord, pk, properties, types);
return Content(jsonResults.ToJson());
}
The database is open to retrieve the field names and their types. Initially the data is contained in a
DataTable
. Data conversion in jqGrid accepted format (JSON) occurs in several stages:
string jsonstring = JsonConvert.SerializeObject(t_data, Formatting.Indented);
(Formatting.Indented
only for debugging.) jsonstring
is converted into an array of type
Jcontainer
(Newtonsoft.Json.Linq
) and finally the array is converted to the format supported
by jqGrid with static class JQGridExtensions
function ToJQGridResult
. It should be noted that
JQGridExtension
contains several useful classes for data transformation.
More about transforming data into JSON data is available here.
About source code
MVC generated project is too big so I have included only the needed files.
Steps to generate project
- Create MVC4 project (internet) with name MvcApp
- Package manager console\nuget
- Install-Package jQuery
- Install-Package jQuery.UI.Combined
- Install-Package jQuery.jqGrid
- Add reference System.Runtime.Serialization
- Add empty controller TestController
- Add view for Index
- Create Helpers folder
- Copy files
- _Layout.cshtml -> Views\Shared\_Layout.cshtml
- Index.cshtml -> Views\Test\Index.xshtml
- JQGridExtensions.cs -> Helpers\JQGridExtensions.cs (and include it in project)
- TestController.cs -> Controllers\TestController.cs
- BundleConfig.cs -> App_Start\BundleConfig.cs
- NORTHWND files - > in App_Data (don't need to include in project)
Conclusion
We can write less code using JavaScript injection in MVC applications.