Installation steps

  • use Visual Studio 2015 or higher
  • install the .NET Core tools preview for Visual Studio https://www.microsoft.com/net/core#windowsvs2015
  • install Postsharp extension for Visual Studio
  • install Chutzpah Test extensions for Visual Studio
  • restore Persons.bak DB backup
  • set connection string in API\project.json
  • check API url in Client\Shared\settings.js

Client Guide

Let's create a prototype using a component-based design with only web client technologies. I aim to do a simple Create Read Update Delete page.

First I split the page in the following web components and give them responsibilities :
  • header
  • editForm :
    • displays Model information
    • validates user input
    • adds/edits
    • calls Collection Model for CRUD operations
    • broadcasts events
  • filter
    • builds the Query Object
    • validates user input
    • broadcasts events
  • list
    • displays the list of Models
    • paging
    • sorting, calls Collection Model using the Query Object as parameter
    • broadcasts events

Below the code for the "list" web component :

<table class="table table-hover card card-block">
    <thead>
        <tr>
            <th>ID</th>
            <th>FirstName</th>
            <th>LastName</th>
            <th>&nbsp;</th>
        </tr>
    </thead>
    <tbody>
        <tr v-for="person in persons" :key="person.id">
            <td>{{person.id}}</td>
            <td>{{person.firstName}}</td>
            <td>{{person.lastName}}</td>
            <td>
                <button v-on:click="triggerViewClick(person)" class="btn btn-default">Edit</button>
                <button v-on:click="triggerDeleteClick(person.id)" class="btn btn-default">Delete</button>
            </td>
        </tr>
    </tbody>
</table>


(function () {
    "use strict";
    var that;
    var personCollection;

    var vm = Vue.component('crud-list', {
        template: templates["list"],
        data : function(){
            return {
                persons : []
            }
        },
        methods: {
            init(collection) {
                personCollection = collection;
            },
            reload : function(query){
                that.persons = personCollection.get();                
            },
            fetch(query) {
                return personCollection.fetch(query).done(function (response) {
                    that.reload();
                });
            },
            triggerViewClick: function (person) {
                that.$emit("editClick", person);
            },            
            triggerDeleteClick: function (id) {
                personCollection.remove(id).done(function () {                    
                    that.$emit("deleteClick", id);
                    that.reload();
                });                
            }
        },
        mounted: function () {
            that = this;            
        }
    });
})();


The “Root Page Controller” manages the communications between these components. It’s a mediator. The web components are the publishers of events and the mediator subscribes to these events and starts actions on other components.

<div class="container" id="root">
        <crud-header></crud-header>
        <crud-filter ref="filter"></crud-filter>
        <crud-list ref="list"></crud-list>
        <button class="card btn btn-default" v-on:click="add">Add</button>        
        <crud-editform ref="editForm"></crud-editform>
</div>


(function(){
    "use strict";
    var that;
    var filter, list, editForm;
    
    var vm = new Vue({
        el: '#root',
        created: function(){
        },
        methods: {
            add: function(){                
                editForm.showNew();
            }
        },
        mounted: function(){
            that = this;
            var collection = new PersonCollection();

            filter = that.$refs.filter;
            list = that.$refs.list;
            editForm = that.$refs.editForm;

            list.init(collection);
            editForm.init(collection);

            //events
            filter.$on("queryClick", function(query){
                list.fetch(query);
                editForm.hide();
            });

            list.$on("editClick", function(person){
                editForm.bind(person);
            });            

            editForm.$on("saveClick", function(person){
                list.reload();
            });

            list.fetch();
        }
    });
})();

The Query Object

  • is plain object containing
    • all filter criteria
    • order by
    • top

{
    name : "Cristi"
}


The Model-View-Controller pattern applies when developing these components. The web component is split in 2 files:
  • The .html file representing the View :
  • The .js file representing the Controller

The View

  • contains the HTML template
  • renders the data from a Model or a List of Models

The Controller

  • initializes the View
  • relates only to DOM elements in the View
  • updates the View
  • handles View events
  • invokes changes on the Model

The Model

  • in this case contains the data to be displayed in the View
{
    id : 1,
    firstName : "Cristi",
    lastName : "S"
}

Collection

  • it’s the List of Models
  • encapsulates the business logic for the list manipulation
  • loads and saves data from the server using the Data Services
  • emits events when data changes

Data Services

  • are objects responsible for communication with the REST API.
personDataService = (function () {
    "use strict";

    functionget(query) {
        return $.ajax({
            type: "GET",
            url: settings.personsApiUrl,
            data: query            
        });
    }    

    functionadd(person) {
        return $.ajax({
            type: "POST",
            url: settings.personsApiUrl,
            data: JSON.stringify(person)            
        });
    }

    functionedit(person) {
        ...
    }

    functionremove(id) {
        ...
    }

    return {
        get: get,
        add: add,
        edit: edit,
        remove: remove
    }


In the end I created a separate project “Client.Tests” for testing the logic with JasmineJS. The JS tests in Visual Studio are run using Chutzpah.

It’s important to note that the “reference” in the js test should point to the project to test.
///<referencepath="../Client/bundels/libs.js" />
///<referencepath="../Client/Shared/Collection.js" />


There were still some problems to solve in the web client prototype with a Task Runner.
  • Bundle/Concatenate .js, .css files
  • Concatenate “template files”
  • Minify .js, .css
  • Modify .js and .css references inside html to point to the .min versions
  • Versioning .js, .css bundle files
I started by concatenating the .js and .css files with "grunt-concat".

In order to include all the template files in the main .html file I used "grunt-html-convert" to concatenate all of them and create an array of strings in a new .js files.

Example “templates.js“

templates["filter"] = "<form class=\"form-inline card card-block….

templates["header"] = "<div class=\"card card-block\">\n...

For the minification of .js and .css file "grunt-uglify" and "grunt-cssmin" were used.

In development references should point to the original file, but in production they should point to the .min version.

Development
<script src="bundels/scripts.js"></script>

Production
<script src="bundels/scripts.min.js"></script>

I used “grunt-text-replace” with regular expressions to add the “.min” at the end of the filename.

The browser should cache the .js and .css files so that it doesn’t request them every time; but the browser should get the new file if something has changed inside that file. "grunt-cache-bust" can be used to append a “hash” to the file name. So when the contents of the file change, the hash will also change.

In Development
<head>
<link href="bundels/styles.css" rel="stylesheet" />       
<script src="bundels/libs.js"></script>   
<script src="bundels/templates.js"></script>       
<script src="bundels/scripts.js"></script>
</head>

In Production

<head>  
<link href="bundels/styles.min.0873f354d9ca5115.css" rel="stylesheet" />
<script src="bundels/libs.min.c005d2e077d05177.js"></script>
<script src="bundels/templates.min.fde820fea3e04897.js"></script>
<script src="bundels/scripts.min.b115c45b591b9990.js"></script>
</head>


In the end I created 2 tasks:

“development”
  • Clean “bundles” folder
  • Reset resource references to original file names
  • Concatenate
“production”
  • Clean “bundles” folder
  • Reset resource references to original file names
  • Concatenate
  • Minify
  • Add version hashcode
The “watch” task can be used for monitoring file changes and execute other tasks.

Last edited Dec 22, 2016 at 11:46 AM by cristi_salcescu, version 21