View and Related Lists (Or View Lists)

Abstract

In this tutorial we'll learn to load form's related lists that are usually required to store reference data (foreign key values) alongside primary input fields. To achieve tutorial objectives we'll be writing a server side CRUDController and a front-end CRUDView.

Keywords: WindnTrees,CRUD,CRUD2CRUD,CRUDS,ViewLists
  • Setup Project

    We'll be extending tutorial Introduction to CRUDSList and you can download reference project here.

    Setup Database

    Add following table to existing database of Introduction to CRUDSList tutorial.

    Topic
    1. UID (uniqueidentifier) (PK)
    2. CategoryID (uniqueidentifier) (FK)
    3. SkillLevelID (uniqueidentifier) (FK)
    4. RecordTime (datetime)
    5. UpdateTime (datetime)
    6. Menu (nvarchar(100))
    7. Subject (nvarchar(200))
    8. Description1 (nvarchar(200))
    9. Description2 (nvarchar(200))
    10. Active (bit)
    11. RowVersion (timestamp)

    In ASP .NET Core you may use following command to synchronize application data model.

    Scaffold-DbContext "Server=.\sqlexpress;Database=Tutorials;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -DataAnnotations -OutputDir TutorialModels -Tables "Category","Rating","SkillLevel", "Topic" -Context TutorialsContext -Force

    or in case of ASP .NET use ADO .NET Entity Data Model (EDMX) to update model.

  • Javascript Object Models

    Create javascript object models (including knockout observable fields) against each database model object.

    Strategy

    WindnTrees CRUDView alongside with related lists will allow programmers to load related data with simple technique. This technique will eliminate the need of writing complex listing logics and view updates.

    In this tutorial we will write:

    1. WindnTrees CRUDView referencing respective service URI and javascript type.
    2. Script extending CRUDView to load related lists.
    3. Knockout and HTML view.

  • Writing WindnTrees View (CRUDView)

    CRUDView refering main entity (javascript type) will look like following:

    
    var view = new CRUDView({
                'uri': '/topic', //service uri address
                'observer': new CRUDObserver({ 'contentType': new Topic({}), 'messages': intialize(new MessageRepository()) }), // observer
                'fields': [ // related list fields
                    {
                        'uri': '/category', // server side CRUDController uri address
                        'target': 'List', // CRUDLController function that is invoked to get list results
                        'field': 'CategoriesList', // CRUDObserver will be extended with Categories observable (ko.observableArray)
                        'key': 'Title', // key (display) field bound with dropdown list
                        'value': 'Uid' // value field bound with dropdown list
                    },
                    {
                        'uri': '/skilllevel', // server side CRUDController uri address
                        'target': 'List', // CRUDLController function that is invoked to get list results
                        'field': 'SkillLevelsList', // CRUDObserver will be extended with SkillLevelsList observable (ko.observableArray)
                        'key': 'Title', // key (display) field bound with dropdown list
                        'value': 'Uid' // value field bound with dropdown list
                    }]
            });
    
            //Required to extend related observable fields like 'CategoriesList', 'SkillLevelsList'
            view.ExtendFields();
    
  • Knockout View Templates

    Write Knockout view templates as follows:

    <script type="text/html" id="results_processing">
        <span class="d-flex justify-content-end">
            <i class="fa fa-cog fa-spin" data-bind="visible: Processing()"></i>
        </span>
    </script>
    <script type="text/html" id="results_messages">
        <span class="alert d-flex justify-content-start" data-bind="if: ResultMessage().length > 0">
            <span data-bind="text: ResultMessage"></span>
        </span>
    </script>
    <script type="text/html" id="list_error_messages">
        <li class="alert alert-danger"><span data-bind="text: errField"></span> <span data-bind="text: errMessage"></span></li>
    </script>
    
    <script type="text/html" id="list_error_messages">
        <li class="alert alert-danger"><span data-bind="text: errField"></span> <span data-bind="text: errMessage"></span></li>
    </script>
    
    <script type='text/html' id='actions'>
        <div class='col-sm-12 col-md-12 col-lg-12'>
            <div class='input-group'>
                <span class='input-group-prepend'><button data-bind='click: function() { resetForm(); }' 
    class='btn btn-default' type='button' title='New' data-toggle='modal' 
    data-target='#__form'><span>New</span></button></span>
                <input class='form-control' data-bind='value: Keyword' type='text' placeholder='Keyword' />
                <span class='input-group-append'>
                    <button data-bind='click: function() { list(1); }' class='btn btn-default' type='button' 
    title='Search'><span>Search</span></button>
                </span>
            </div>
        </div>
    </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(CurrentList()); }"><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="listitem">
        <li>
            <div class="row align-items-center">
                <div class="col-sm-12 col-md-6 col-lg-6 order-0">
                    <!--Topic Subject-->
                    <div class="d-flex justify-content-start"><a href="#topic-detail" data-toggle="collapse" 
    data-bind="attr: { id: Uid, href: '#topic-detail' + Uid }"><h3 class="mt-3"><span 
    data-bind="text: Subject()"></span></h3></a></div>
                </div>
            </div>
            <div class="row">
                <div class="col-sm-12 col-md-12 col-lg-12">
                    <!--Menu-->
                    <div class="pb-2"><span class="" data-bind="text: Menu()"></span></div>
                    <div id="topic-detail" class="collapse" data-bind="attr: { id: 'topic-detail' + Uid }">
    
  •                     <!--Description1-->
                        <div data-bind="html: Description1()"></div>
                        <!--Description2-->
                        <div data-bind="html: Description2()"></div>
                        <div class="row">
                            <div class="col-sm-12 col-md-6 col-lg-6">
                                <div class="d-flex justify-content-start">
                                    <!--UpdateTime-->
                                    <span data-bind="text: UpdateTime()"></span>
                                </div>
                            </div>
                            <div class="col-sm-12 col-md-6 col-lg-6">
                                <!--list action fields-->
                                <div class="d-flex justify-content-end">
                                    <a class="green p-2" 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 p-2" href="#" 
    data-bind="click: function(data, event) { $parent.delete($data); }" title="Delete"><i 
    class="fa fa-times text-danger"></i>Delete</a>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </li>
    </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 justify-content-start" for="CategoryId">
                    Category
                </label>
                <select 
    data-bind="options: $root.CategoriesList, optionsText: 'key', optionsValue: 'val', value: CategoryId, 
    optionsCaption: 'select category'" title="Select Category"
                        id="CategoryId" class="form-control col-12"></select><span class="error" 
    data-bind="validationMessage: CategoryId"></span>
            </div>
            <div class="col-sm-12 col-md-4 col-lg-4 order-1">
                <label class="d-flex justify-content-start" for="SkillLevelId">
                    Skill
                </label>
                <select 
    data-bind="options: $root.SkillLevelsList, optionsText: 'key', optionsValue: 'val', value: SkillLevelId, 
    optionsCaption: 'select skill'" title="Select Skill"
                        id="SkillLevelId" class="form-control col-12"></select><span class="error" 
    data-bind="validationMessage: SkillLevelId"></span>
            </div>
            <div class="col-sm-12 col-md-4 col-lg-4 order-2"></div>
        </div>
    
  • <div class="row">
            <div class="col-sm-12 col-md-12 col-lg-12 order-0">
                <label class="control-label d-flex align-content-start" for="Subject">
                    Subject
                </label>
                <input id="Subject" name="Subject" class="form-control col-12" data-bind="value: Subject" type="text" title="Subject"
                       maxlength="200" placeholder="" /><span class="error"
                                                              data-bind="validationMessage: Subject"></span>
            </div>
        </div>
        <div class="row">
            <div class="col-sm-12 col-md-12 col-lg-12 order-0">
                <label class="control-label d-flex align-content-start" for="Menu">
                    Menu
                </label>
                <input id="Menu" name="Menu" class="form-control col-12" data-bind="value: Menu" type="text" title="Menu"
                       maxlength="100" placeholder="" /><span class="error"
                                                              data-bind="validationMessage: Menu"></span>
            </div>
        </div>
        <div class="row">
            <div class="col-sm-12 col-md-12 col-lg-12 order-0">
                <label class="control-label d-flex align-content-start" for="Description1">
                    Description 1
                </label>
                <input id="Description1" name="Description1" class="form-control col-12" 
    data-bind="value: Description1" type="text" title="Description 1"
                       maxlength="200" placeholder="" /><span class="error"
                                                              data-bind="validationMessage: Description1"></span>
            </div>
    
    </div>
        <div class="row">
            <div class="col-sm-12 col-md-12 col-lg-12 order-0">
                <label class="control-label d-flex align-content-start" for="Description2">
                    Description 2
                </label>
                <input id="Description2" name="Description2" class="form-control col-12" 
    data-bind="value: Description2" type="text" title="Description 2"
                       maxlength="200" placeholder="" /><span class="error"
                                                              data-bind="validationMessage: Description2"></span>
            </div>
        </div>
        <div class="row">
            <div class="col-sm-6 col-md-6 col-lg-6 order-0">
                <label class="control-label d-flex justify-content-start" for="Active">
                    Active
                </label>
                <input id="Active" name="Active" data-bind="checked: Active" title="Active"
                       type="checkbox" class="form-check" /><span class="error" data-bind="validationMessage: Active"></span>
            </div>
            <div class="col-sm-6 col-md-6 col-lg-6 order-1"></div>
        </div>
    </script>
    
  • HTML View

    <div id="ko-node-element" 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>
                    <ul class="p-0" data-bind="template: {name: 'listitem', foreach: Records}"></ul>
                </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 CRUDView provide efficient means of loading related lists data with minimal programming script. Download the complete project here.