Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using DataTables with Web API - Part 4: Re-use

0.00/5 (No votes)
9 Dec 2016 1  
Part 4 of a series on using DataTables with WebAPI

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:

DataTables multiple tables display window

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">/* <![CDATA[ */!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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here