Skip to content
padzikm edited this page Dec 15, 2015 · 13 revisions

Table order

For rendering table we need two things - some identification of data that you want to render and order in which this data will be displayed in table. Most popular and simplest is identifying each row of table by some resourceId and displaying in each column properties that are binded to this resourceId. There may be cases when you want to identify each row by multiple identificators, so you can expand this approach to fits your needs, or you may check if these multiple identificators are in fact connected in some kind relation, so you can identify each row by one relationId.
For combining these two things we can notice that the same component that provides table order, also provides identificators for each row. Next we can say that table is composed of columns, so each column will be a separate widget. In this way when we want to create table, we define its columns as widgets and services will be responsible for delivering appropriate widgets as usual. For each row each column widget will be asked to provide its component, given as parameter row identificator.
There is also question what identificator of each row means. It must be data that is public to all services, because each column can be from different service boundary. From definition, data that is public can contain only id, status and datetime, so each row's identificator will be represented as object carrying these three properties.
As a result, when we services respond to published incoming request, the only data returned for table will be ordered table identificators. We can provide data for each column only when we know identificator of each row, but these are returned as TableViewModel with all other widgets for current page, so we know these data after each service finished providing view models. We can of course publish request to services once again, including table identificators as parameter, but it will be waste of server resources, especially if there is more than one table on page. Instead asking services once again, we can store identificators and ask for table in for example ajax request. To save bandwith we can store this data in a session taking advantage from integrating all services into one process, so we only send request for table, and not sending data needed for it.
Example of delivering table identificators in a given order:

public class PublicServiceData
{
    public string Id { get; set; }
    public string Status { get; set; }
    public DateTime? DateTime { get; set; }
}

public interface ITableOrderViewModel : IViewModel
{
    List<PublicServiceData> SortedIds { get; }
}

[HttpGet]
[InternalAction]
public ActionResult SomePage(some_parameters)
{
    var tableOrder = // get sorted order for given table
    var tableViewModel = new TableOrderViewModel("tableId", tableOrder);
    return ViewModel(tableViewModel);
}

Example of storing provided data:

[HttpGet]
public ActionResult SomePage()
{
    var viewModels = //generate view models
    var tableOrders = viewModels.OfType<ITableOrderViewModel>().ToList(); //extracting data for every table
    foreach(var data in tableOrders)
    {
        Session[data.Id] = data.SortedIds;
        ViewData[data.Id] = data.SortedIds; //passing data on page for later use on client side
    }
}

Example of getting stored data:

<script type="text/javascript">
    $(function () {
        $.ajax({
            method: "GET",
            url: "@Url.Action("GetTable", new { key = "tableId" })",
            success: function (data) {
                //handling logic
            },
            error: function () { //rare case when user refreshes page before loading whole data and we will have concurrency issue
                $.ajax({
                    method: "GET",
                    url: "@Url.Action("GetTable", new { key = tableId })",
                    data: "@Html.Raw(serialized_order_data_for_table)",
                    success: function (data) {
                        //handling logic
                    }
                });
            }
        });
    });
</script>
[HttpGet]
public ActionResult GetTable(string tableKey, List<ServicePublicData> orderData)
{
    if(orderData == null) //first attempt with only tableKey send to extract data from session
    {
        data = Session[tableKey];
        Session.Remove(tableKey); //clean session storage
    }
    //generating table response
}

You can see two attempts for getting table - first we make request only passing key to session data, so main application can extract data from session and pass it as parameter to services (we can let services extract data from session, but we can decouple them from knowing where data is stored and extract it in main app action). This will almost always succeed, but in some rare cases user may refresh page, before it is loaded and there may be following sequence of actions - request is handled again, widgets are gathered, order data for each table is stored in session, page is returned to user, ajax call for table from previously loaded page is handled and it removes data from session, ajax call for table from current page is handled and it finds that there is no data stored in session so it fails, second attempt is made - this time sending stored on client and serialized table order data. In most cases we will complete getting table successfully saving bandwith, but we're also prepared for rare cases, which can happen for example during server's high load time.

If we want to identify resources for table by logic expression that spans across many services - for example display data for every customer, that spent at least some amount of money and live in some specific region - we can return identificators from each service, and in main application perform logical operations on returned sets - for example intersect for and logical operation.

Server-side rendering

Example of this you can find in /ProductDrafts/Index action

Rendering table on server side can be desired for example when we want to heavily use templates for many table cells which would require lots of work on client side.
When request is made for whole table, each service delivers TableViewModel for given column widget, that can display cell content for a given row identificator. Because services may want to provide behaviour for a given table, TableViewModel derives from normal ViewModel, so it can render for example service's javascript code when asked for render itself.
Example of creating widget for column:

public ITableViewModel : IViewModel
{
    MvcHtmlString Execute(WebViewPage viewPage, ServicePublicData id);
}

[HttpGet]
[InternalAction]
public ActionResult GetTable(List<ServicePublicData> tableOrdering)
{
    var data = //create for example dictionary with ServicePublicData key and table data as value
    var tableViewModel = new TableViewModel("columnId", func or expression for given cell, expression for whole widget);
    return ViewModel(tableViewModel);
}

Example of creating table:

<table>
    <thead>
    <tr>
        <th>First column</th>
        <th>Second column</th>
    </tr>
    </thead>
    <tbody>
        @foreach (var rowId in tableOrdering)
        {
            var firstColumnWidget = //widget for first column
            var secondColumnWidget = //widget for second column
            <tr>
                <td>@firstColumnWidget.Execute(this, rowId)</td> //providing content for given rowId in first column
                <td>@secondColumnWidget.Execute(this, rowId)</td> //providing content for given rowId in second column
            </tr>
        }
    </tbody>
</table>

@foreach (var viewModel in AllTableViewModels) //each column widget can provide its for example javascript code
{
    @viewModel.Execute(this);
}

You can see that you function for returning content for a cell can be either func or expression. This is because you can want to render more complicated content, so you want to execute view for each cell and for example make use of templates, so in order to get to the view you pass expression as in normal view model. On the other hand, if you want to provide simple value for each cell, then executing view each time will be unwanted, so you can pass func, because expression tress have some limitations - for example func can be much more complex and developed than expression which must be simple.

Client-side rendering

Example of this you can find in /ProductDrafts/IndexDiff action

Rendering table on client side can be desired when table cells contain simple values, especially when table is quite big. In order to do that we must gather data for all columns and have a way to create each cell on client side.
For gathering table data we make use of integrating services into one process and make ajax call to main application api for json data. In this way each interested service will provide data for each column in one network call.
For doing this each service returns JsonViewModel which only carries data to main application and is never executed, so each service must provide one object - that will be serialized to json - for all columns that it is responsible for. Next when data is returned we must create table, so we want to have some javascript function for each column, that will be called to return content for each cell in given column. With each call appropriate row identificator will be passed to function and whole data returned from server, so each service can extract data that it needs in a way it wants. Because returned json is a collection of objects from all services, we have to find a way to differentiate these objects, because they may be the same, as services are developed independently. For this, each object returned from service is packed into wrapper object, that has two properties - Name and Object. Object is an actual object returned from service and name is unique name of this object in collection. This name can be for example service name, which is unique, so it is set by default if service won't provide name itselef.
Next question is how to define javascript function for each column. The simplest answer is they will have names exactly the same as column ids that they represent - column ids are unique per page, so there won't be name conflict. In this way for each cell, appropriate well defined function will be called with row identificator parameter and table data parameter.
Example of gathering json data:

[HttpGet]
[InternalAction]
public ActionResult GetTableData(List<ServicePublicData> tableOrdering)
{
    var data = //get data for table
    return JsonViewModel(data);
}

Example of creating table:

<script type="text/javascript">
    $(function () {
        $.ajax({
            method: "GET",
            url: "@Url.Action("GetTableData", new { key = "tableId" )",
            success: createTableFunc,
            error: function () {
                $.ajax({
                    method: "GET",
                    url: "@Url.Action("GetTableData", new { key = "tableId" })",
                    data: "@Html.Raw(serialized_table_ordering_data)",
                    success: createTableFunc
                });
            }
        });
    });

    var createTableFunc = function(data) {
        var tableOrdering= @Html.Raw(serialized_table_ordering_to_json);
        var table = "<table><thead><tr><th>First column</th><th>Second column</th></thead><tbody>";
        for (var i = 0, len = tableOrdering.length; i < len; ++i) {
            var obj = tableOrdering[i];
            var row = "<tr>";
            row += "<td>" + @("firstColumnId")(id, data) + "</td>";
            row += "<td>" + @("secondColumnId")(id, data) + "</td>";
            row += "</tr>";
            table += row;
        }
        table += "</tbody></table>";
        $("#table").append(table);
    };
</script>

Example of function for one column:

<script type="text/javascript">
    var myServiceName_cachedTableIdData = null;
    function @("firstColumnId")(id, data) {
        for (var i = 0, len = data.length; i < len && myServiceName_cachedTableIdData !== null; ++i) {
            if (data[i].Name === "MyServiceName") { //finding data for this service
                myServiceName_cachedTableIdData = data[i].Object; //cache data for my service to not look for it again
            }
         }
         for (var j = 0, llen = myServiceName_cachedTableIdData .length; j < llen; ++j) {
             var obj = myServiceName_cachedTableIdData [j];
             if (obj.id === id) //finding content for current cell with given id
                return obj.Content;
         }
    };
</script>

Nested tables

Creating nested table inside given cell will depend on what data you need for rendering this table and in what order. If each nested table depends on parent cell identificator in which it is nested, then you have to have these identificators first, so then you can ask for identificators for nested table. When this is the case you can do nested tables recursive in the same way as main table - asking for ordering and then getting table, or data for this ordering. If however nested table won't depend on parent identificator, then you may just return data also for nested table and then calling rendering cell content will result in returning nested table.

Clone this wiki locally