Extending View With Observables

Abstract

Tutorial extends Getting Started With WindnTrees and present concept of integrating additional observables with view that is usually required to count records and/or calculate totals. Tutorial is implemented in ASP .NET Core and reference project code is available here.

Keywords: WindnTrees,CRUD,CRUDS,CRUD2CRUD
  • Review

    In Getting Started tutorial we implemented server side EmployeeController (CRUDLController) and client side CRUDView to achieve client/server CRUD2CRUD behavior with minimal set of code and we learnt that EmployeeController utilizes EntityFramework driven EmployeeRepository based on ADO .NET Entity Data model and database context.

    You may port your existing application in ASP .NET Core that would require you to write new controller and repository on server side or extend your existing application with following changes that applies equally to both frameworks.

    Changes in database

    Following changes were made in "Employee" table to meet tutorial objectives:

    Id (nvarchar(450)) field is replaced with UID (uniqueidentifier)
    Allowance (decimal) field is added.

    Extended "Employee" table would yield in following fields in your database:

    UID (uniqueidentifier) (PK)
    Name (nvarchar(100))
    Designation (nvarchar(100))
    Email (nvarchar(450))
    Salary (decimal)
    Allowance (decimal)
  • In ASP .NET Core you may use following command to synchronize application data model with database.

    Scaffold-DbContext "Server=.\sqlexpress;Database=Articles;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -DataAnnotations -OutputDir ArticleModels -Tables "Employee" -Context ArticleContext -Force

    or

    In case of ASP .NET use ADO .NET Entity Data Model (EDMX) to update model. Make sure you also update Employee.js file.

    Strategy

    WindnTrees allow programmers to add new observables and process records in view scope by registering respective events.

    In this tutorial we will extend:

    1. WindnTrees view observer with new (knockout) observables to calculate and display total of salary and allowances.
    2. WindnTrees view with events to process result(s).
    3. HTML view with WindnTrees view logic.

    Extending View With Observables

    Original WindnTrees JS view look like following:

    var view = new CRUDView({
                'uri': '/employee',
                'observer': new CRUDObserver({ 'contentType': new Employee({}), 'messages': intialize(new MessageRepository()) })
            });
    

    Extend above view's observer with following observables:

    view.getObserverObject().TotalSalary = ko.observable(0);
    view.getObserverObject().TotalAllowance = ko.observable(0);
    

    Updated view would result in following:

    var view = new CRUDView({
                'uri': '/employee',
                'observer': new CRUDObserver({ 'contentType': new Employee({}), 'messages': intialize(new MessageRepository()) })
            });
    
    view.getObserverObject().TotalSalary = ko.observable(0);
    view.getObserverObject().TotalAllowance = ko.observable(0);
    
  • Event Registrations and Results Processing

    WindnTrees notify data results at different levels and scopes to meet processing requirements. Usually, results (record(s)) are pre-processed, type ready and are directly available to view for presentation. However, in certain scenarios WindnTrees view is required to be extended with custom functionalities and UI logic like calculating total and we may also be required to register events for processing result(s).

    Register event for single record updates (create,read,update,delete)

    view.subscribeEvent('record.after.rendering.view.CRUD.WindnTrees', view.getObserverObject().processRecord);
    

    Register event for multi record updates (list)

    view.subscribeEvent('records.after.rendering.view.CRUD.WindnTrees', view.getObserverObject().processRecords);
    

    Register above events with respective processing functions in WindnTrees view

    view.getObserverObject().processRecord = function (event, data) {
    		//data processing code here
    }
    
    view.getObserverObject().processRecords = function (event, data) {
    		//data processing code here
    }
    

    Updated view would result in following:

    
    var view = new CRUDView({
                'uri': '/employee',
                'observer': new CRUDObserver({ 'contentType': new Employee({}), 'messages': intialize(new MessageRepository()) })
            });
    
    view.getObserverObject().TotalSalary = ko.observable(0);
    view.getObserverObject().TotalAllowance = ko.observable(0);
    
    view.subscribeEvent('record.after.rendering.view.CRUD.WindnTrees', view.getObserverObject().processRecord);
    view.subscribeEvent('records.after.rendering.view.CRUD.WindnTrees', view.getObserverObject().processRecords);
    
    view.getObserverObject().processRecord = function (event, data) {
    
                //Gets first hand record from Observer as recieved from CRUDProcessor
                var result = view.getObserverObject().Record();
    
                //if data is available within scope of event reporting
                if (data !== null && data !== undefined) {
    
                    //then check if data contains updated list of results
                    if (data.result !== null && data.result !== undefined) {
    
                        //update existing results with data.result
                        result = data.result
                    }
                }
    
                try {
    
                    //check whether its create response
                    if (data.request === 'create') {
    
                        //add newly added salary and allowance
                        view.getObserverObject().TotalSalary(result.Salary() + view.getObserverObject().TotalSalary());
                        view.getObserverObject().TotalAllowance(result.Allowance() + view.getObserverObject().TotalAllowance());
                    }
                    else if (data.request === 'update') {
    
                        //extract records from CRUDProcessor
                        var result = view.getObserverObject().Records();
    
                        //reset TotalSalary observable
                        view.getObserverObject().TotalSalary(0);
                        view.getObserverObject().TotalAllowance(0);
                        //calculate sum of all result salaries
                        for (var i = 0; i < result.length; i++) {
    
                            view.getObserverObject().TotalSalary(result[i].Salary() + view.getObserverObject().TotalSalary());
                            view.getObserverObject().TotalAllowance(result[i].Allowance() + view.getObserverObject().TotalAllowance());
                        }
                    }
                }
                catch (e) {
                    console.log(e.message);
                }
            };
    
  • view.getObserverObject().processRecords = function (event, data) {
    
                //Gets first hand records from Observer as recieved from CRUDProcessor
                var result = view.getObserverObject().Records();
    
                //if data is available within scope of event reporting
                if (data !== null && data !== undefined) {
    
                    //then check if data contains updated list of results
                    if (data.result !== null && data.result !== undefined) {
    
                        //update existing results with data.result
                        result = data.result
                    }
                }
    
                try {
    
                    //reset TotalSalary observable
                    view.getObserverObject().TotalSalary(0);
                    view.getObserverObject().TotalAllowance(0);
                    //calculate sum of all result salaries
                    for (var i = 0; i < result.length; i++) {
    
                        view.getObserverObject().TotalSalary(result[i].Salary() + view.getObserverObject().TotalSalary());
                        view.getObserverObject().TotalAllowance(result[i].Allowance() + view.getObserverObject().TotalAllowance());
                    }
                    //that will update view accordingly.
                }
                catch (e) {
                    console.log(e.message);
                }
            };
    
  • Knockout View Templates

    Write Knockout templates to integrate with HTML and WindnTrees view and observer object.

    Write Knockout view templates within HTML document as follows:

    <script type="text/html" id="headings">
            <tr>
                <th class="col-sm-12 col-md-2 col-lg-2 order-0" scope="col">
                    <span class="d-flex align-content-start" title="Name">Name</span>
                </th>
                <th class="col-sm-12 col-md-2 col-lg-2 order-1" scope="col">
                    <span class="d-flex align-content-start" title="Designation">Designation</span>
                </th>
                <th class="col-sm-12 col-md-4 col-lg-4 order-1" scope="col">
                    <span class="d-flex align-content-start" title="Email">Email</span>
                </th>
                <th class="col-sm-12 col-md-1 col-lg-1 order-1" scope="col">
                    <span class="d-flex align-content-start" title="Salary">Salary</span>
                </th>
                <th class="col-sm-12 col-md-1 col-lg-1 order-1" scope="col">
                    <span class="d-flex align-content-start" title="Allowance">Allowance</span>
                </th>
                <th class="col-sm-12 col-md-2 col-lg-2 order-2" scope="col"> </th>
            </tr>
        </script>
    
    <script type='text/html' id='standard_listings'>
            <div class='col-sm-12 col-md-6 col-lg-6 order-first'>
                <div class='input-group justify-content-start'>
                    <span class='input-group-prepend'><span class='input-group-text'>Size</span></span><select 
          class='form-control col-2' data-bind="value: ListSize, event: { change: function() { list(CurrentList()); } }" 
          id='form-field-select-1' style='width:auto;'>
                        <option value='10'>10</option>
                        <option value='20'>20</option>
                        <option value='50'>50</option>
                        <option value='100'>100</option>
                    </select>
                </div>
            </div>
            <div class='col-sm-12 col-md-6 col-lg-6 order-last'>
                <nav class="d-flex justify-content-end" aria-label="Listings">
                    <ul class="pagination">
                        <li class="page-item" data-bind="css: {disabled: CurrentList() === 1 }"><a class="page-link" href="#" 
                data-bind="click: function() { list(CurrentList() - 1); }">Prev</a></li>
                        <!-- ko foreach: ListNavigator().getLists() -->
                        <li class="page-item"><a class="page-link" href="#" data-bind="click: function() { list(Number); }">
                                           <span data-bind="text: Number"></span></a></li>
                        <!-- /ko -->
                        <li class="page-item" data-bind="css: {disabled: CurrentList() === ListNavigator().calculateTotalPages()}">
    <a class="page-link" href="#" data-bind="click: function() { list(CurrentList() + 1); }">Next</a></li>
                    </ul>
                </nav>
            </div>
        </script>
    
  • <script type="text/html" id="headings">
            <tr>
                <th class="col-sm-12 col-md-2 col-lg-2 order-0" scope="col">
                    <span class="d-flex align-content-start" title="Name">Name</span>
                </th>
                <th class="col-sm-12 col-md-2 col-lg-2 order-1" scope="col">
                    <span class="d-flex align-content-start" title="Designation">Designation</span>
                </th>
                <th class="col-sm-12 col-md-4 col-lg-4 order-1" scope="col">
                    <span class="d-flex align-content-start" title="Email">Email</span>
                </th>
                <th class="col-sm-12 col-md-1 col-lg-1 order-1" scope="col">
                    <span class="d-flex align-content-start" title="Salary">Salary</span>
                </th>
                <th class="col-sm-12 col-md-1 col-lg-1 order-1" scope="col">
                    <span class="d-flex align-content-start" title="Allowance">Allowance</span>
                </th>
                <th class="col-sm-12 col-md-2 col-lg-2 order-2" scope="col"> </th>
            </tr>
        </script>
    
    <script type="text/html" id="rows">
            <tr>
                <td><span class="d-flex align-content-start" data-bind="text: Name()"></span></td>
                <td><span class="d-flex align-content-start" data-bind="text: Designation()"></span></td>
                <td><span class="d-flex align-content-start" data-bind="text: Email()"></span></td>
                <td><span class="d-flex align-content-start" data-bind="text: Salary()"></span></td>
                <td><span class="d-flex align-content-start" data-bind="text: Allowance()"></span></td>
                <td>
                    <div class="d-flex justify-content-end">
                        <a class="green" href="#" data-bind="click: function(data, event) { $parent.resetFormForEditing($index); }" 
    data-toggle="modal" data-target="#__form" title="Edit"><i class="fa fa-edit text-success"></i>Edit</a>
                        <a class="red" href="#" data-bind="click: function(data, event) { $parent.delete($data); }" title="Delete">
    <i class="fa fa-times text-danger"></i>Delete</a>
                    </div>
                </td>
            </tr>
        </script>
    
  • <script type="text/html" id="summary">
            <tr>
                <td></td>
                <td></td>
                <td></td>
                <td><span class="d-flex align-content-start" data-bind="text: TotalSalary()"></span></td>
                <td><span class="d-flex align-content-start" data-bind="text: TotalAllowance()"></span></td>
                <td></td>
            </tr>
        </script>
    
    <script type="text/html" id="formcontent">
            <div class="row">
                <div class="col-sm-12 col-md-4 col-lg-4 order-0">
                    <label class="d-flex align-content-start" for="Name">
                        Name
                    </label>
                    <input class="form-control col-12" data-bind="value: Name" id="Name" type="text" title="Name"
                           maxlength="100" placeholder="" /><span class="error"
                                                                  data-bind="validationMessage: Name"></span>
                </div>
                <div class="col-sm-12 col-md-4 col-lg-4 order-1">
                    <label class="d-flex align-content-start" for="Designation">
                        Designation
                    </label>
                    <input class="form-control col-12" data-bind="value: Designation" id="Designation" type="text" title="Designation"
                           maxlength="100" placeholder="" /><span class="error"
                                                                  data-bind="validationMessage: Designation"></span>
                </div>
                <div class="col-sm-12 col-md-4 col-lg-4 order-1">
                    <label class="d-flex align-content-start" for="Email">
                        Email
                    </label>
                    <input class="form-control col-12" data-bind="value: Email" id="Email" type="text" title="Email"
                           maxlength="450" placeholder="" /><span class="error"
                                                                  data-bind="validationMessage: Email"></span>
                </div>
            </div>
    
  •         <div class="row">
                <div class="col-sm-12 col-md-4 col-lg-4 order-0">
                    <label class="d-flex align-content-start" for="Salary">
                        Salary
                    </label>
                    <input class="form-control col-12" data-bind="value: Salary" id="Salary" type="text" title="Salary"
                           maxlength="10" placeholder="" /><span class="error"
                                                                 data-bind="validationMessage: Salary"></span>
                </div>
                <div class="col-sm-12 col-md-4 col-lg-4 order-1">
                    <label class="d-flex align-content-start" for="Allowance">
                        Allowance
                    </label>
                    <input class="form-control col-12" data-bind="value: Allowance" id="Allowance" type="text" title="Allowance"
                           maxlength="10" placeholder="" /><span class="error"
                                                                 data-bind="validationMessage: Allowance"></span>
                </div>
            </div>
    </script>
    
  • HTML View

    Now complete HTML view as follows:

    <div class="container-fluid">
        <div class="container pb-2">
            <div class="card">
                <div class="card-header">
                    <div class="row">
                      <!-- Display action response result message. -->
                      <span class="col-sm-12 col-md-10 col-lg-10 order-first result" data-bind="template: {name: 'results_messages'}"></span>
                      <!-- Display processing status. -->
                      <span class="col-sm-12 col-md-2 col-lg-2 order-last status" data-bind="template: {name: 'results_processing'}"></span>
                    </div>
                    <div class="row" data-bind="if: Errors().length > 0">
                        <div class="col-sm-12 col-md-12 col-lg-12">
                            <!-- Display server-side validation errors list -->
                            <ul class='errorlist' data-bind="template: {name: 'list_error_messages' , foreach: Errors}"></ul>
                        </div>
                    </div>
                </div>
                <div class="card-body">
                    <!-- Display action elements (new, search etc) -->
                    <div class="row" data-bind="template: {name: 'actions'}"></div>
                    <div class="table-responsive">
                        <table class="table table-hover grid-style-0">
                            <!-- Display data table heading columns -->
                            <thead data-bind="template: {name: 'headings'}"></thead>
                            <!-- Display data table rows -->
                            <tbody data-bind="template: {name: 'rows', foreach: Records}"></tbody>
                            <tbody data-bind="template: {name: 'summary'}"></tbody>
                        </table>
                    </div>
                </div>
    
    <div class="card-footer">
                    <!-- Display table listing elements -->
                    <div class="row" data-bind="template: {name: 'standard_listings' }"></div>
                </div>
            </div>
    <!-- Modal form for saving input data -->
            <div id="__form" class="modal fade" style="display: none;">
                <div class="modal-dialog modal-dialog-centered" style="min-width: 800px;">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h4 class="col order-0">
                                <span class="d-flex justify-content-start" 
    data-bind="text: getEditMode() ? getEditModeCaption() : getNewModeCaption()"></span>
                            </h4>
                            <div class="col order-1">
                                <span class="d-flex justify-content-end">
                                    <button type="button" data-dismiss="modal"><span>×</span></button>
                                </span>
                            </div>
                        </div>
    
  •               <div class="modal-body">
                            <div class="row">
                                <span class="col-sm-12 col-md-10 col-lg-10 order-first result" 
    data-bind="template: {name: 'results_messages'}"></span>
                                <span class="col-sm-12 col-md-2 col-lg-2 order-last status" 
    data-bind="template: {name: 'results_processing'}"></span>
                            </div>
                            <div class="row" data-bind="if: Errors().length > 0">
                                <div class="col-sm-12 col-md-12 col-lg-12">
                                    <ul class='errorlist' data-bind="template: {name: 'list_error_messages' , foreach: Errors}"></ul>
                                </div>
                            </div>
                            <div data-bind="with: getFormObject()">
                                <div data-bind="template: {name: 'formcontent' }"></div>
                            </div>
                        </div>
                        <div class="modal-footer">
                            <span class="d-flex justify-content-end">
                                <button type="button" id="btnCloseAddForm" class="btn btn-default" data-dismiss="modal">
    <span>Close</span></button>
                                <!-- invokes create, update method to save new and existing record. -->
                                <!-- create/update are CRUD methods that are integrated in View (CRUDView) and linked with
     Observer (CRUDObserver composed of observables). -->
                                <button type="button" data-bind="click: function() { getEditMode() ? update() : create(); }" id="btnAddEdit" 
    class="btn btn-primary"><span>Save</span></button>
                            </span>
                        </div>
                    </div>
                </div>
    
            </div>
        </div>
    </div>
    
  • Summary

    WindnTrees view is extensible and in above example we learnt to add new observables, write appropriate HTML and knockout templates and extend program logic with events.