This is part 4 of a 4-part series on using DataTables with WebAPI. Check out the other parts here:
Data comes in all shapes and sizes. We use it to describe attributes of all sorts of things. There are loads of web based systems out there for managing that data. Within them, we often need to look at different sets of data at the same time. Manipulate and analyse them in isolation from each other. DataTables provide a powerful way to do that. It has a lot of options though. There's a fair bit of repetition. Copying and pasting that plug in code everywhere. Can we create it once, pass a few options in, and generate it on the fly? Of course we can. We can do a similar thing with the boilerplate code surrounding the table data as well.
New Requirement: Show Some User Data As Well
We're building atop the previous article on paging, sorting and searching. If you want to follow along, head over there first and grab the Visual Studio solution. I'll wait for you to get back...
Got it? Onwards and upwards. While you were away, we got a new requirement in for the project. We now need to display user data alongside our customer information. This means we need a second table. Before we do that, we need to add some user data to our project. Head on over to the API code and add a UserData
class to Controllers/Api:
public class UserData
{
public const string DataSource = @"
{
""Data"": [
{
""CompanyName"": ""Microsoft"",
""FirstName"": ""Dave"",
""LastName"": ""Smith"",
""Email"": ""dave.smith@microsoft.com"",
""JobTitle"": ""Project Manager""
},
{
""CompanyName"": ""Nokia"",
""FirstName"": ""John"",
""LastName"": ""Smith"",
""Email"": ""john.smith@nokia.com"",
""JobTitle"": ""Project Manager""
},
{
""CompanyName"": ""Apple"",
""FirstName"": ""Paul"",
""LastName"": ""Jones"",
""Email"": ""paul.jones@apple.com"",
""JobTitle"": ""Product Manager""
},
{
""CompanyName"": ""Google"",
""FirstName"": ""Leslie"",
""LastName"": ""Richards"",
""Email"": ""leslie.richards@google.com"",
""JobTitle"": ""Product Director""
},
{
""CompanyName"": ""Samsung"",
""FirstName"": ""Michelle"",
""LastName"": ""Davis"",
""Email"": ""michelle.davis@samsung.com"",
""JobTitle"": ""Programme Manager""
}
]
}";
public IList<UserSearchDetail> Data { get; set; }
}
We also need a UserSearchDetail
and a UserSearchResponse
. Let's add those:
public class UserSearchDetail : SearchDetail
{
public string CompanyName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string JobTitle { get; set; }
}
public class UserSearchResponse : SearchResponse<UserSearchDetail>
{
}
We'd better add a filtering method to our ApiHelper
:
public static IEnumerable<UserSearchDetail> FilterUsers(IEnumerable<UserSearchDetail> details,
string searchText)
{
if (searchText.IsEmpty())
{
return details;
}
var results = details.Where(x => x.CompanyName.ContainsIgnoringCase(searchText)
|| x.FirstName.ContainsIgnoringCase(searchText)
|| x.LastName.ContainsIgnoringCase(searchText)
|| x.Email.ContainsIgnoringCase(searchText)
|| x.JobTitle.ContainsIgnoringCase(searchText)
);
return results;
}
Let's wrap it all up in a new UserSearchController
:
public class UserSearchController : SearchController
{
public IHttpActionResult Post(SearchRequest request)
{
var allUsers = JsonConvert.DeserializeObject<UserData> (UserData.DataSource);
var response = WrapSearch(allUsers.Data, request);
return Ok(response);
}
private static UserSearchResponse WrapSearch(ICollection<UserSearchDetail> details,
SearchRequest request)
{
var results = ApiHelper.FilterUsers(details, request.Search.Value).ToList();
var response = new UserSearchResponse
{
Data = PageResults(results, request),
Draw = request.Draw,
RecordsFiltered = results.Count,
RecordsTotal = details.Count
};
return response;
}
}
It's like our Customer search stuff, so I won't go into detail about it again. You can read up about that in part 3.
View the original article.
Enter Display Templates...
Now we have a couple of search functions for retrieving customers and users. Wonderful. Now, how to best integrate them into our application? Remember, we're trying to avoid repeating the plug in code or table markup. Our aim is to display 2 tables on our page. Here's a sneak peek of what we're trying to achieve:
The first thing we'll do is add a HomeViewModel
to our Index view. Within it, we'll have a TableWidgetViewModel
for each of our tables. If you haven't got one, create a Models folder inside your project. Add these 2 classes to it:
public class TableWidgetViewModel
{
public IList<string> ClientColumns { get; set; }
public IList<string> Columns { get; set; }
public string TableId { get; set; }
public string WidgetHeading { get; set; }
public TableWidgetViewModel Initialise(string[] clientColumns, string heading,
string tableId, params string[] columnHeadings)
{
ClientColumns = clientColumns;
TableId = tableId;
WidgetHeading = heading;
Columns = columnHeadings.ToList();
return this;
}
public MvcHtmlString RenderColumns()
{
var builder = new StringBuilder();
foreach (var column in Columns)
{
builder.AppendFormat("<th>{0}</th>", column);
builder.AppendLine();
}
var content = WrapColumns(builder.ToString());
return content.ToHtmlString();
}
public MvcHtmlString RenderClientColumnScript()
{
if (ClientColumns == null || ClientColumns.Count == 0)
{
return MvcHtmlString.Empty;
}
var scriptBuilder = new StringBuilder();
scriptBuilder.AppendLine("\"columns\": [");
var scriptItems = ClientColumns.Select(WrapClientColumn).ToArray();
var columnScript = string.Join(", ", scriptItems);
scriptBuilder.AppendLine(columnScript);
scriptBuilder.AppendLine("],");
return scriptBuilder.ToHtmlString();
}
private static string WrapClientColumn(string columnName)
{
return string.Concat("{ \"data\": \"", columnName, "\" }");
}
private static string WrapColumns(string columnContent)
{
return string.Concat("<tr>", columnContent, "</tr>");
}
}
public class HomeViewModel
{
public TableWidgetViewModel CustomerWidget { get; set; }
public TableWidgetViewModel UserWidget { get; set; }
}
The interesting class here is the TableWidgetViewModel
. It's got 3 public
methods that we'll call from our Controller code in a moment. 2 of these methods will generate the markup and client script we need. The 3rd, Initialise
, will allow us to create table widgets using the same process each time. Let's add the model to our Index view. Open up Views/Home/Index.cshtml. Pop this line at the top to tell the view which type of model class it uses:
@model Levelnis.Learning.UsingDataTablesWithWebApi.Models.HomeViewModel
We now have what we call a Strongly Typed View. Our view expects to get a HomeViewModel
when it renders. We'd better give it one now. Open up the HomeController
and add some code to create our ViewModel
:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = Create();
return View(model);
}
private static HomeViewModel Create()
{
var customerClientColumns = new[]
{ "companyName", "address", "postcode", "telephone" };
var customerColumnHeadings = new[]
{ "Company Name", "Address", "Postcode", "Telephone" };
var userClientColumns = new[]
{ "companyName", "firstName", "lastName", "email", "jobTitle" };
var userColumnHeadings = new[]
{ "Company Name", "First Name", "Last Name", "Email", "JobTitle" };
var model = new HomeViewModel
{
CustomerWidget = new TableWidgetViewModel().Initialise
(customerClientColumns, "Customers", "CustomerTable", customerColumnHeadings),
UserWidget = new TableWidgetViewModel().Initialise
(userClientColumns, "Users", "UserTable", userColumnHeadings)
};
return model;
}
}
This is an ideal scenario for using a factory to create the ViewModel. I'll leave that as an exercise for the reader though. We're going to keep things simple.
You can see we're calling the Initialise
method for each widget. The client columns array contains the names of the properties on our data models. Remember when we created CustomerSearchDetail
and UserSearchDetail
? Well, the names here match up with the property names there. The column headings match up with the headings on our 2 tables in the screenshot above.
That's it for the Controller. Now let's turn our attention to the View. The first thing we need is a Display Template. This is a template file that's bound to a Model
class, much like the HomeViewModel
and Index View. We're going to take the markup from our Index view and create a Display Template with it. We'll then use that Display Template to display the table widgets on our HomeViewModel
. If you want to read up on Display Templates, check out my article on keeping your ViewModels clean with Display Templates.
First up, let's add a partial view to a new folder - Views/Shared/DisplayTemplates. The convention for Display Templates is to use the same name as the Model
class. In our case, let's call it TableWidgetViewModel.cshtml. Add the markup from the Index view:
@model Levelnis.Learning.UsingDataTablesWithWebApi.Models.TableWidgetViewModel
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title")<a class="__cf_email__"
href="/cdn-cgi/l/email-protection"
data-cfemail="b8c4f8f5d7dcddd496efd1dcdfddccf0ddd9dcd1d6df">[email protected]
</a><script data-cfhash='f9e31'
type="text/javascript">!function(t,e,r,n,c,a,p)
{try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),
e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&
(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute
('data-cfemail')){for(e='',r='0x'+
a.substr(0,2>0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);
p.replaceChild(document.createTextNode
(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()</script></h3>
</div>
<div class="panel-body">
<table id="@Model.TableId"
class="table table-striped table-bordered table-hover responsive"
width="100%">
<thead class="thin-border-bottom">
@Model.RenderColumns()
</thead>
</table>
</div>
</div>
We can now use this template in our View instead of repeating the markup for our second table. Here's the new Index markup. I've wrapped both tables in their own rows so they're not too squashed up on the page.
@model Levelnis.Learning.UsingDataTablesWithWebApi.Models.HomeViewModel
<div class="row">
<div class="col-xs-12">
@Html.DisplayFor(m => m.CustomerWidget)
</div>
</div>
<div class="row">
<div class="col-xs-12">
@Html.DisplayFor(m => m.UserWidget)
</div>
</div>
We use the DisplayFor
extension method to render our markup for each table. The only thing left now is to create the plug in script code. We'll do this using a HelperResult
. Add an App_Code folder to your project. Create a partial View inside called Helpers.cshtml. Add the following code to it:
@helper CreateActiveTableScriptlet(string id, string apiUrl, MvcHtmlString clientColumnScript,
string emptyTableText, string zeroRecordsText, string isEnhanced = "true")
{
var tableVar = "generate" + id;
var scriptBuilder = new StringBuilder();
scriptBuilder.AppendLine();
scriptBuilder.AppendFormat("var {0} = $(\"#{1}\")", tableVar, id);
scriptBuilder.AppendLine(".DataTable({");
scriptBuilder.AppendLine(" \"processing\": true,");
scriptBuilder.AppendLine(" \"serverSide\": true,");
scriptBuilder.AppendLine(" \"ajax\": {");
scriptBuilder.AppendFormat(" \"url\": \"{0}\",", apiUrl);
scriptBuilder.AppendLine();
scriptBuilder.AppendLine(" \"type\": \"POST\"");
scriptBuilder.AppendLine(" },");
scriptBuilder.Append(clientColumnScript);
scriptBuilder.AppendLine(" \"language\": {");
scriptBuilder.AppendFormat(" \"emptyTable\": \"{0}\",", emptyTableText);
scriptBuilder.AppendLine();
scriptBuilder.AppendFormat(" \"zeroRecords\": \"{0}\"", zeroRecordsText);
scriptBuilder.AppendLine();
scriptBuilder.AppendLine(" },");
scriptBuilder.AppendFormat(" \"searching\": {0},", isEnhanced);
scriptBuilder.AppendLine();
scriptBuilder.AppendFormat(" \"ordering\": {0},", isEnhanced);
scriptBuilder.AppendLine();
scriptBuilder.AppendFormat(" \"paging\": {0}", isEnhanced);
scriptBuilder.AppendLine();
scriptBuilder.AppendLine("});");
@scriptBuilder.ToHtmlString()
}
We're using a StringBuilder
to build up the script that we'll output to the View. The very last line that starts with an @
symbol is where the magic happens. This writes the StringBuilder
content to the stream of HTML that gets sent back to the browser. Now we need to use this helper. Let's replace our plug in script code with 2 calls to this Helper, one for each table. Replace the scripts
section in the Index View with the following:
@section scripts {
<script>
(function($) {
@Helpers.CreateTableScript("CustomerTable", "/api/customerSearch",
Model.CustomerWidget.RenderClientColumnScript(), "There are no customers at present.",
"There were no matching customers found.")
@Helpers.CreateTableScript("UserTable", "/api/userSearch",
Model.UserWidget.RenderClientColumnScript(), "There are no users at present.",
"There were no matching users found.")
})(jQuery);
</script>
We're passing in the id of the table (which we set when we initialize the ViewModel
), the API URL and column script. That sets up our tables with the correct data. That's it! To add a 3rd table to our page, we'd go through the following steps:
- Create some data for it
- Add a
SearchDetail
and SearchResponse
- Create a filter method in the
ApiHelper
- Add a
SearchController
to bring back the data
- Add a
TableWidgetViewModel
property to the HomeViewModel
- Initialise the widget in the
HomeController.Create
method
- Add a
DisplayFor
call to the view so we can display it
- Add a
CreateTableScript
call to the scripts section, which connects the widget to its data
View the original article.