ict.ken.be

 

Posts in Category: WebApi

Building Apps with Angular and Breeze - Notes 

Categories: Angularjs WebApi

by John Papa

Links:

SPA

  • Reach, Rich, Reduced Round Tripping
  • Navigation, History, Deep Linking, Persisting State, Most Load On Initial Response, Progressive Loading When Needed

Techniques

  • Dependency Injection
  • Routing
  • MV*
  • Data Binding
  • Publisher/Subscriber
  • DOM Templates
  • Views

Value

  • Reduces the plumbing
  • Handles the monotony
  • Spend more time on what matters
  • Focus on the user stories

Solution Structure

  • Server Models
  • Data Access (Entity Framework)
  • Web Server
  • HTML/CSS/Javascript + ASP.NET WebAPI
  • Modules
  • Startup Code
  • Twitter Bootstrap
  • Angular
  • Services

Unblock file: use alt + enter
Build: ctrl + shift + B

Hot Towel

  • AngularJS.Animate
  • AngularJS.Core
  • AngularJS.Route
  • AngularJS.Sanitize
  • FontAwesome
  • HotTowel.Angular
  • jQuery
  • Moment.js
  • Spin.js
  • toastr
  • Twitter.Bootstrap

Scripts OR vendor ?
app
app/config.js OR startup ?
app/config.exceptionhandler.js
app/config.route.js
app/layout/shell.html
app/services/datacontext.js
app/services/directives.js OR subfolders ?
index.html
ie10mobile.css
initial splash page
minify and bundle before production deploy

var app = angular.module('app', ['ngRoute']):
app.Value('config', config);
app.config(['$logProvider', function ($logProvider) ... ]) //runs before app.run
app.run(['$route', function ($route) {}]);

app.config(function ($provide) { $provide.decorator('$exceptionHandler', ['$delegate', 'config', 'logger', extendExceptionHandler]);});

app.constant('routes', getRoutes());

<div data-ng-include="'/app/layout/shell.html'"></div>
<div data-ng-view class="shuffle-animation"></div>

Angular v1.2 supports controller as: <section id="dashboard view" class="mainbar" data-ng-controller="dashboard as vm">

<section data-ng-controller="sessions as vm">
<div data-ng-repeat="s in vm.sessions" data-ng-click="vm.gotoSession(s)">
<div data-cc-widget-header title="{{vm.title}}"></div>

Directives can be A-Attribute, E-Element, C-Class, M-Comment

<img data-ng-src="..." />

GetDataPartials (as projection) -> map it into an existing entity -> extend this entity
metadataStore.registerEntityTypeCtor('Person', Person);
function Person() {
this.isSpeaker = false;
}

Breeze caches data locally in memory.
if (_areSessionsLoaded() && !forceRemote) {
sessions = _getAllLocal(entityNames.session, orderBy);
return $q.when(sessions); //transform it into a promise
}

function _areSessionsLoaded(value) {
...
}
function _areItemsLoaded(key, value) {
if (value == undefined) {
return storeMeta.isLoaded[key]; //get
}
return storeMeta.isLoaded[key] = vlaue;
}

Entity types are not necessary real resources. (extendMetadata)
Eg. Speakers, Attendees all users but different entities.

Make more responsive: class="hidden-phone"

Sharing Data: creating object graphs on the client

Routing:
$locationChangeStart
$routeChangeStart
$routeChangeSuccess
$routeChangeError
definition.resolve = angular.extend(definition.resolve || {}, { prime: prime });

Filtering:
<input data-ng-model="vm.search">
<div data-ng-repeat="s in vm.speakers | filter:vm.search">

<input ng-model="search.$">
<input ng-model="search.name">
<div data-ng-repeat="friends in friends | filter:search:strict">

vm.search = function($event) {
if($event.keyCode == config.keyCodes.esc) {
vm.speakerSearch = '';
}
applyFilter();
}

function speakerFilter(speaker) {
var isMatch = vm.speakerSearch
? common.textContains(speaker.fullName, searchText)
:true;
return isMatch;
}

Breeze & UI-Bootstrap:
<pagination boundry-links="true"
on-select-page="vm.pageChanged(page)"
total-items="vm.attendeeFilteredCount"
items-per-page="vm.paging.maxPagesToShow"
class="pagination-small"
previous-text=Prev"
next-text="Next"
first-text="First"
last-text="Last"
</pagination>

.executeLocally()

Dashboard:

  • Promises, Responsive Layout, Just the counts, Local and Remote Queries

.take(0).inlineCount()
_getAllLocal(...)

<th><a data-ng-click="vm.content.setSort('track')" ...
<tr data-ng-repeat="t in vm.content.tracks | orderBy:vm.content.predicate:vm.content.reverse">

ES5's.reduce() funciton will help create the mapped object
sessions.reduce (function (accum, session) {
var trackname = session.track.name;
var trackId = session.track.id;
if (accum[trackId-1]) accum[trackId-1].count++;
else accum[trackId-1] = { track: trackName, count: 1 }
}, []);

Animations (ngAnimate):

hide ngHide .ng-hide-add (start) .ng-hide-add-active (end)
show ngShow .ng-hide-remove .ng-hide-remove-active

Splash page in index.html
-> move to shell.html
-> turn off in activateController().then(...)

ngRepeat, ngInclude, nglf, ngView
enter .ng-enter .ng-enter-active
leave .ng-leave .ng-leave-active
move .ng-move .ng-move-active (only for ng-repeat)

http://www.yearofmoo.com (keyframe animations)
http://jpapa.me/yomanimations

DataContext will grow -> Repositories

http://johnpapa.net/protoctor

function gotoSpeaker(speaker) {
if (speaker && speaker.id) {
$location.path('/speaker/' + speaker.id);

}
}

Object.defineProperty(vm, 'canSave', {
get: canSave
});
function canSave() { return !vm.isSaving; }

function goBack() { $window.history.back(); }

function onDestroy() {
$scope.$on('$destroy', function() { datacontext.cancel(); });
}

manager.hasChangesChanged.subscribe(function(eventArgs){
... eventArgs.hasChanges ...
common.$broadcast(events.hasChangesChanged, data);
});

<select data-ng-options="t.name for t in vm.tracks"
data-ng-model="vm.session.track">
</select>

function createNullo(entityName) {
var initialValues = {
name: ' [Select a ' + entityName.toLowerCase() + ']'
}:
return manager.createEntity(
entityName, initialValues, breeze.EntityState.Unchanged);
);
}

Object.defineProperty(TimeSlot.prototype, 'name', {
get : function() {
var start = this.start;
var value = ((start - nulloDate) == 0) ?
' [Select a timeslot]' :
(start && moment.utc(start).isValid()) ?
moment.utc(start).format('ddd hh:mm a') : ' [Unknown]';
return value;
}
});

Validation on the Server for Data Integrity always
Validation on the Client for User Experience

View Level: Should you copy business rules to HTML?
vs
Model Level: Should the model drive the UI?

Separation of how you calculate the validation error from how you display it.

breeze.directives.validation.js
Install-Package Breeze.Angular.Directives

<input data-ng-model="vm.session.title" placeholder="Session title" data-z-validate />

//tell breeze not to validate when we attach a newly created entity to any manager
new breeze.ValidationOptions({ validateOnAttach: false }).setAsDefault();

app.config(['zDirectivesConfigProvider', function(cfg) {
cfg.zValidateTemplate = '<span class="invalid"><i class="icon-warning-sign"></i>%error%</span>';
}])

Saving State - How do we recover quickly?

Instal-Package angular.breeze.storagewip (ngzWip)

zStorage.init(manager);

function listenForStorageEvents() {
$rootScope.$on(config.events.storage.storeChanged, function ...
... config.events.storage.wipChanged ...
... config.events.storage.error ...
}

var exportData = entityManager.exportEntities();
$window.localStorage.setItem(key, exportData);

var importData = $window.localStorage.getItem(storageKey);
entityManager.importEntities(importedData);

eg. after loading lookups in prime method, we could call zStorage.save();
but also after query and saves that are successful

http://caniuse.com/#search=localstorage

Storing work in progress (zStorageWip)

zStorageWip.init(manager);

manager.entityChanged.subscribe(function(changeArgs) {
if (changeArgs.entityAction == breeze.EnityAction.propertyChanged) {
common.$broadcast(events.entitiesChanged, changeArgs);
}
});

function autoStoreWip(immediate) {
common.debouncedThrottle(controllerId, storeWipEntity, 1000, immediate);
}

function onEveryChange() {
$scope.$on(config.events.entitiesChanged, function(event,data) { autoStoreWip(); });
}


//Forget certain changes by removing them from the entity's originalValues
//This function becomes unnecessary if Breeze decides that unmapped properties are not recorded in originalValues
//We remove the properties we do not want to track
function interceptPropertyChange(changeArgs) {
var changedProp = changeArgs.args.propertyName;
if (changedProp === 'isPartial' || changedProp === 'isSpeaker') {
delete changeArgs.entity.entityAspect.originalValues[changedProp];
}
}

var importedEntity = this.zStorageWip.loadWipEntity(wipEntityKey);
if (importedEntity) {
importedEntity.entityAspect.validateEntity();
return $.when({ entity:importedEntity, key:wipEntityKey });
}

<li data-cc-wip wip="vm.wip" routes="vm.routes" changed-event="{{vm.wipChangedEvent}}" class="nlightblue"></li>

$scope.wipExists = function() { return !!$scope.wip.length; } //double bang will turn number into boolean

<div class="widget-content" data-ng-include="'/app/wip/widget.html'"> //double, single quote

<div class="widget-content referrer" data-ng-switch="!!vm.wip.length">
<div data-ng-switch-when="false">No work in progress is found</div>
<table data-ng-switch-when="true">
...
... {{item.date | date: 'MM/dd/yyyyy @ h:mma'}}
</table>
</div>

more: http://jpape.me/htmlstore - html5 web storage indexeddb and filesystem by craig shoemaker

 

REST 

Categories: WebApi

A rest api should be entered with no prior knowledge beyond the intial uri and set of standardized media types that are appropriate for the intended audience. From that point on, all application state transitions must be driven by client selection of server-provided choices that are present in the received representations or implied by the user's manipulation of those representations. The transition may be limited by the client's knowledge of media types and resource communication mechanisms, both of which may be improved on-the-fly.

Roy Fielding

 

REST: "Representational state transfer"

HATEOAS: "Hypermedia as the engine of application state"

Richardson's Maturity Model

  • Level 0: POX (Plain Old Xml)
  • Level 1: Resources
  • Level 2: HTTP Verbs
  • Level 3: Hypermedia (REST)

Misconception Forces: Network reliability, Latency, Bandwidth, Security, Network topology, Administration, Transport cost, Heterogeneous network, Complexity
Constraints: Client-Server, Stateless, Cache, Uniform Interface, Layered System, Code-On-Demand (optional)
Components: Origin server, User agent, Gateway, Proxy
Connectors: Client, Server, Cache, Resolver, Tunnel

Mapping Concepts to Entities

A resource should be a concept and only the mappings should change over time.
eg. /children /children/youngest /children/oldest /children/grace /children/sarah /children/abigail

Resource Types

  • document: a singular noun should be used for document names. eg. .../leagues/seattle
  • collection: a plural noun should be used for collection names. eg. .../leagues
  • store: a plural noun should be used for store names. eg. .../users/1234/favorites/alonso
  • controller: a verb or verb phrase should be used for controller names and the controllername should be as last segment in the uri path. eg. .../alerts/1234/resend 

Resource Identifier

An aesthetically pleasing rest api design is a must have feature.

uri = scheme :// authority / path ? Query # fragment

  • / in path must indicate a hierarchical relationship between resources
  • A trailing forward slash should not be included in uris (do you hear this chrome?)
  • Use hyphens - to improve readability
  • Do not use underscores
  • Lowercase letters should be preferred in uri paths.
  • Case different in path then not same resource
  • File extensions should not be included in uris
  • The full domain name of an api should add a subdomain named api eg. http://api.somesite.be
  • Client developer portal at http://developer.somesite.be
  • Subpaths should be addressable
  • Variable path segments may be substituted with identity based values
  • Crud function names should not be used in uris
  • The query component of a uri may be used to filter collections or stores, preferably using OData
  • The query component should be used to paginate collection or store result using OData (read this wordpress)
  • If needed a separated method can be assigned eg. Post /users/search + body + cacheheaders

Http Methods

GET

  • Templated
  • Get and post must not be used to tunnel other request methods.
  • Get must be used to retrieve a representation of a resource
  • Clients count on being able to repeat get requests without causing side effects

HEAD

  • Head should be used to retrieve response headers.

PUT

  • Idempotent (will return seem result over and over)
  • The uri identifies the entity enclosed with the request
  • Put must be used to both insert and update a stored resource.
  • Put must be used to update mutable resources.

POST

  • Non-Idempotent, the uri identifies the resource that will handle the enclosed entity
  • Post must be used to create a new resource in a collection
  • Post must be used to execute controllers

DELETE

  • Delete must be used to remove a resource from its parent
  • Soft deletes should use a post, delete should result in 404 afterwards.

OPTIONS

  • Options should be used to retrieve metadata that describes a resources available interactions.

Response Status Codes

see http status codes explained

Http headers & Control Data

  • Content type must be used.
  • Content length should be used.
  • Last modified should be used in responses.
  • Etag should be used in responses. eg. if-none-match: "etag-guid"
  • Stores must support conditional put requests. If unmodified since or if match headers. 
  • Location must be used to specify the uri of a newly created resource.
  • Cache control, expires and date response headers should be used to encourage caching. Max age value in seconds. eg. Cache-Control: max-age=86400
  • Expires and datetime for legacy
  • Cache control, expires and pragma should be used to discourage caching. eg. Cache-control no-cache no-store pragma: no-cache expires:0
  • Caching should be encouraged even by using a small max-age
  • Set expiration headers in responses of succesful get and head requests
  • Post is cacheble but most will not treat it that way
  • Expiration caching headers may optionally be used with 3xx and 4xx responses. Known as negative caching reducing the amount of redirecting and error triggering
  • Custom http headers must not.be used to change the behavior of http methods

Representational Design

A resource can have multiple available representations. Content negotiation is the process of selecting the best representation of a resource.

eg. Accept: application/json Accept-Encoding: gzip,deflate,sdch Accept-Language: en-US,en;q=0.8 Accept-
Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

  • Json should be supported for resource representation
  • Json must be well formated
  • Xml and other formats may optionally be used for resource representation
  • Additional envelopes must not be created

Media types aka mime types

Type / subtype *(; parameter )

  • application, audio, image, message, model, multipart, text, video
  • Registering media types: IANA allows anyone to propose a new media type
  • Vender specific media types. eg. Application/vnd.ms-excel
  • Application specific media types should be used.
  • The uri of a resource type current schema version always identifies the concept of the most recent version. A uri that ends with a number represents that version.
  • Media type negotiation should be supported when multiple representations are available.
  • Media type selection using a query parameter may be supported. eg. Get /bookmarks/mike?accept=application/xml Instead of using extentions in the uri.

Hypermedia

Coupling is reduced by reducing the number of URLs that the client needs to know about single entry point url
<a href="root/children/grace" rel="child">foo</a>
<form class="newchild" action="children" method="post"></form>

Types of Links
<img> - Embedded
<a> - Outbound

Example of a RESTful Service

Bug Tracking Workflow

What does the sticky note look like? What does the board look like?
Title, Description, Initials, State Icons
Backlog, Working, QA, Done + Icons
Allow to change service workflow without changing the client workflow.

List the requirements
(discover bugs, add new bugs, activate a bug, complete a bug)
Identify the state transitions
(client should be able to transition to all states + navigation and error states)
Identify the resources
/bugs - entry point (GET navigate and add new bug)
/bugs/backlog (GET fetch list of bugs POST add new bug to backlog)
/bugs/working (GET fetch POST activate a bug)
/bugs/done (GET fetch POST complete a bug)
Design the media type
Base format: XML, JSON, HTML, ...
State transfer: read only, predefined, ad-hoc
Domain style: specific, general (ATOM), agnostic (HTML)
Application flow: None, Intrinsic, Applied (using decorators rel and class)
We choose: HTML, Ad-Hoc, Agnostic, Applied
Need elements for: List of bugs, link template for moving a bug to backlog, ...

Mapping to Base Format
eg. id bugs DIV container for bugs state elements, class all UL list of bugs, ...
Add a sample representation (here a markup)
eg. class="move active next"

Before versioned service URIs look like: http://foo/services/v2/bla.svc
Now: versioning within representation, representation, resource

Use content negotiation to version media types
accept:text/vnd.howard.bugs.v1+html

RESTfull Clients

Runtime vs Development time

  1. Study the media type and sample documents
  2. Request entry point resource
  3. Inspect representation
  4. Write client code
  5. Follow links

navigationElements = _document.XPathSelectElements("//div[@id='{0}']/a", BugsMediaTypeConstatns.ID_LINKS)

CLOUD

  • From kapital to operational expenditureIncrease resources should increase performance
  • Operational efficiency
  • Resilient to failure (spread the risk around)
  • Realizes economy of scale
  • Cost per unit goes down as the number of units goes up
  • Cost goes down when requests by second goes up

Opportunities
Cloud overflow / failover : start on premise or go from one cloud to the other
Auto-tuning : etags, cache, representation inlining

More

Fiddler add duration column 

Categories: WebApi

Rules -> Customize Rules -> Edit CustomRules.js

public static BindUIColumn("Duration")
function CalcTimingCol(oS: Session){
var sResult = String.Empty;
if ((oS.Timers.ServerDoneResponse > oS.Timers.ClientDoneRequest))
{
var ts: System.TimeSpan = null;
var dtStart: DateTime = oS.Timers.ClientDoneRequest;
var dtEnd: DateTime = oS.Timers.ServerDoneResponse;
ts = dtEnd - dtStart;
if (ts.TotalMilliseconds > 2000)
{
oS["ui-color"]="brown";
oS["ui-italic"]="true";
}
sResult = ts.ToString();
}
return sResult;
}

Http status codes explained 

Categories: Network WebApi

Funny way to finally remember all of these, the boring text version of http status codes you can find here.

Http status codes explained with pictures

1xx Informational

  • 100 Continue
  • 101 Switching Protocols

2xx Successful

  • 200 OK : should be used to indicate a non-specific success
  • 201 Created : must be used to indicate successful resource creation
  • 202 Accepted : must be used to indicate a succesful start of an asynchronous action and should only be send by controllers
  • 203 Non-Authoritative Information
  • 204 No Content : should be used when the response body is intentionally empty
  • 205 Reset Content
  • 206 Partial Content

3xx Redirection

  • 300 Multiple Choices
  • 301 Moved Permanently : should be used to relocate resources
  • 302 Found : should only redirect automatically when it was a get or head request
  • 303 See Other : should be used to refer the client to a different uri, so sending a reference to a client without forcing it to download it's state
  • 304 Not Modified : should be used to preserve bandwidth and the response body must be empty
  • 305 Use Proxy
  • 306 (Unused)
  • 307 Temporary Redirect : should be used to tell clients to resubmit the request to another uri
  • 308 Permanent Redirect : does not allow to change the method

4xx Client Error

  • 400 Bad Request : may be used to indicate non-specific failure
  • 401 Unauthorized : must be used when there is a problem with the clients credentials
  • 402 Payment Required
  • 403 Forbidden : should be used to forbid access regardless of authorization state, so for any requests outside the clients permitted scope
  • 404 Not Found : must be used when a client uri cannnot be mapped to a resource
  • 405 Method Not Allowed : must be used when the http method is not supported, for example using a PUT on a read-only resource (must include the allow header in response)
  • 406 Not Acceptable : must be used when requested media type cannot be served (accept request header)
  • 407 Proxy Authentication Required
  • 408 Request Timeout
  • 409 Conflict : should be used to indicate a violation of resource state, for example a client tries to delete a non empty store
  • 410 Gone
  • 411 Length Required
  • 412 Precondition Failed
  • 413 Request Entity Too Large
  • 414 Request-URI Too Long
  • 415 Unsupported Media Type : must be used when the media type of a requests payload cannot be processed (content type header)
  • 416 Requested Range Not Satisfiable
  • 417 Expectation Failed
  • 418 I'm a teapot (RFC 2324)
  • 419 Authentication Timeout
  • 420 Enhance Your Calm (use 429 instead)
  • 421 Bad mapping
  • 422 Unprocessable Entity
  • 423 Locked
  • 424 Failed Dependency
  • 425 Unordered Collection
  • 426 Upgrade Required
  • 427 Soap action required (in case your api is dirty)
  • 428 Precondition Required (RFC 6585)
  • 429 Too Many Requests (RFC 6585)
  • 430 (not used ?? sometimes misused for 431)
  • 431 Request Header Fields Too Large (RFC 6585)

5xx Server Error

  • 500 Internal Server Error
  • 501 Not Implemented
  • 502 Bad Gateway
  • 503 Service Unavailable
  • 504 Gateway Timeout
  • 505 HTTP Version Not Supported

5xx Server Error

  • 500 Internal Server Error
  • 501 Not Implemented
  • 502 Bad Gateway
  • 503 Service Unavailable
  • 504 Gateway Timeout
  • 505 HTTP Version Not Supported
  • 520 Origin Error
  • 521 Web server is down
  • 522 Connection timed out
  • 523 Proxy Declined Request
  • 524 A timeout occured

7xx Developer Error

  • 70x - Inexcusable
  • 701 - Meh
  • 702 - Emacs
  • 703 - Explosion
  • 704 - Goto Fail
  • 705 - I wrote the code and missed the necessary validation by an oversight (see 795)
  • 71x - Novelty Implementations
  • 710 - PHP
  • 711 - Convenience Store
  • 712 - NoSQL
  • 719 - I am not a teapot
  • 72x - Edge Cases
  • 720 - Unpossible
  • 721 - Known Unknowns
  • 722 - Unknown Unknowns
  • 723 - Tricky
  • 724 - This line should be unreachable
  • 725 - It works on my machine
  • 726 - It's a feature, not a bug
  • 727 - 32 bits is plenty
  • 73x - F*cking
  • 731 - F*cking Rubygems
  • 732 - F*cking Unic💩de
  • 733 - F*cking Deadlocks
  • 734 - F*cking Deferreds
  • 735 - F*cking IE
  • 736 - F*cking Race Conditions
  • 737 - F*ckThreadsing
  • 738 - F*cking Bundler
  • 739 - F*cking Windows
  • 74x - Meme Driven
  • 740 - Computer says no
  • 741 - Compiling
  • 742 - A kitten dies
  • 743 - I thought I knew regular expressions
  • 744 - Y U NO write integration tests?
  • 745 - I don't always test my code, but when I do I do it in production
  • 746 - Missed Ballmer Peak
  • 747 - Motherf*cking Snakes on the Motherf*cking Plane
  • 748 - Confounded by Ponies
  • 749 - Reserved for Chuck Norris
  • 75x - Syntax Errors
  • 750 - Didn't bother to compile it
  • 753 - Syntax Error
  • 754 - Too many semi-colons
  • 755 - Not enough semi-colons
  • 756 - Insufficiently polite
  • 757 - Excessively polite
  • 759 - Unexpected T_PAAMAYIM_NEKUDOTAYIM
  • 76x - Substance-Affected Developer
  • 761 - Hungover
  • 762 - Stoned
  • 763 - Under-Caffeinated
  • 764 - Over-Caffeinated
  • 765 - Railscamp
  • 766 - Sober
  • 767 - Drunk
  • 768 - Accidentally Took Sleeping Pills Instead Of Migraine Pills During Crunch Week
  • 769 - Questionable Maturity Level
  • 77x - Predictable Problems
  • 771 - Cached for too long
  • 772 - Not cached long enough
  • 773 - Not cached at all
  • 774 - Why was this cached?
  • 776 - Error on the Exception
  • 777 - Coincidence
  • 778 - Off By One Error
  • 779 - Off By Too Many To Count Error
  • 78x - Somebody Else's Problem
  • 780 - Project owner not responding
  • 781 - Operations
  • 782 - QA
  • 783 - It was a customer request, honestly
  • 784 - Management, obviously
  • 785 - TPS Cover Sheet not attached
  • 786 - Try it now
  • 787 - Further Funding Required
  • 79x - Internet crashed
  • 791 - The Internet shut down due to copyright restrictions.
  • 792 - Climate change driven catastrophic weather event
  • 793 - Zombie Apocalypse
  • 794 - Someone let PG near a REPL
  • 795 - #heartbleed (see 705)
  • 797 - This is the last page of the Internet. Go back
  • 799 - End of the world

Still need more?

Building a SPA with html5, web api, knockout and jquery - Notes 

Categories: CSS Javascript Notes WebApi

Building single page apps with html5, asp.net web api, knockout and jquery by JohnPapa.net and Pluralsight

 
Wonderful, just wonderful. This is one of the best and most complete tutorials I have ever seen. A must for every web developer. Non-developers should at least watch the application, to see how user friendly and responsive it is. The senior developer will get plenty of pointers to useful frameworks and libraries, while the beginner will get clear explanations and the why and how things are done. The included source code is very reusable and the only things that might need to be added to be able to use this in an enterprise environment are some secure token service and caching. (the use of breeze or upshot is recommended but not shown in the tutorial, probably because these frameworks are very new?) Enjoy. 
Why SPA?
  • As rich and responsive as a desktop app
  • Persisting important state on the client
  • Mostly loaded in the intitial page load
  • Progressively downloads features as required
  • 2-way data binding (MVVM)
  • Tombstoning / dropped connections 
1.CODE CAMPER
Technologies used:
  • CSS
  • LESS
  • Media Queries and responsive design
  • HTML5
  • Modernizr
  • HTML5 Boilerplate
  • AMD - Async Module Definition
  • Revealing Module Pattern
  • Prototypes
  • MVVM
  • jQuery - DOM/AJAX (+jquery.mockjson +jquery.activity)
  • knockout.js - Data Binding/MVVM (+Commanding +ChangeTracking +Templating +Validation)
  • amplify.js - Data Push/Pull, Client Storage, Messaging
  • Sammy.js - Nav/History
  • require.js - Dependency Resolution
  • underscore.js - Javascript Helpers
  • moment.js - Date parsing and formatting
  • toastr.js - UI Alerts
  • qunit - Testing
  • POCO Models
  • JSON and AJAX
  • Web API
  • Unit of Work Pattern
  • Repository Pattern
  • Single Responsibility Principle (SRP)
  • Entity Framework (EF Code First)
  • SQL Database
  • NuGet
  • JSON.NET
  • Ninject IoC
  • Web Optimization
2. TOOLS
  • JsFiddle.com / JsBin.com
  • Sublime / Notepad++
  • Mindscape - less in visual studio
  • Resharper
  • Responsinator.com - check in different sizes
  • ElectricPlum.com - iphone emulator
  • http://jpapa.me/responsivedesignbookmarklet
  • Web Standards Visual Studio Plugin
  • Web Essentials Visual Studio Plugin
3.DATA LAYER (EntityFramework 5.0)
SessionBrief (uses IDs)
Session: SessionBrief (gets full properties)
[Key] (uses Id as default)
DbSet<T>
EntityTypeConfiguration<T>
HasRequired( ).WithMany( ).HasForeignKey( ) 
override OnModelCreating : modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
CodeCamperDbContext : DbContext
{
   public CodeCamperDbContext() : base(nameOrConnectionString: "CodeCamper") { }
   public DbSet<Session> Sessions { get; set; }
}
Database.SetInitializer( );
CreateDatabaseIfNotExists<T>
DropCreateDatabaseIfModelChanges<T>
HasKey
EnityTypeConfiguration
HasRequired (s => s.Speaker).WithMany(p => p.SpeakerSessions).HasForeignKey(s => s.SpeakerId);
SRP - Single Responsibility Principle
public interface IRepository<T> where T : class
{
   IQueryable<T> GetAll();
   T GetById(int id);
   void Add(T entity);
   void Update(T entity);
   void Delete(T entity);
   void Delete(int id);
}
public class EFRepository<T> : IRepository<T> where T : class
{
   protected DbContext DbContext { get; set; }
   protected DbSet<T> DbSet { get; set; }
   public virtual T GetById(int id)
   {
      return DbSet.Find(id);
   }
  ...
}
Unit of Work Pattern decouples webapi controllers from the repositories and dbcontext
4. JSON with WebApi
Ninject IoC Container
kernel = new StandardKernel();
Kernel.Bind<ICodeCamperUow>().To<CodeCamperUow>();
..
config.DependencyResolver = new NinjectDependencyResolver(kernel);
 
[ActionName("rooms")]
GetRooms()
[ActionName("timeslots")]
GetTimeslots()
 
get throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
put return new HttpResponseMessage(HttpStatusCode.NoContent);
post 
response = request.createresponse(HttpStatusCode.Created);
response.Headers.Location = new Uri(Url.Link(RouteConfig.ControllerAndId, new { id = ...
testing : endpoints respond with success? return expected data? crud works?
config.Formatters.Remove(config.Formatters.XmlFormatter);
 
var json = config.Formatters.JsonFormatter;
json.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
 
[Required(ErrorMessage = "Title is required")]
config.Filters.Add(new ValidationActionFilter());
//Puts the model errors in a JObject and returns them with
content.Response.CreateResponse<JObject>(HttpStatusCode.BadRequest, errors);
5. SPA Frame
 
Fallback for jQuery if CDN fails
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="script/lib/jquery-1.7.2.min.js"><\/script>')</script>
 
To activate bundles, the compilation debug has to be false.
bundles.UseCdn = true 
Modernizr goes separate since it loads first
bundles.Add(new ScriptBundle("~/bundles/modernizr").Include("~/Scripts/lib/modernizr-{version}.js"));
@Scripts.Render("~/bundles/modernizr") 
bundles.Add(new ScriptBundle("~/bundles/jsmocks").IncludeDirectory("~/Scripts/app/", "*.js", searchSubdirectories:false);
 
bundles.Add(new StyleBundle("~/Content/css").Include(...);
bundles.Add(new StyleBundle("~/Content/less", new LessTransform(), new CssMinify()).Include("~/Content/styles.less"));
@Styles.Render("~/Content/css","~/Content/less")
6. Javascript Structure
  • Who is responsible for what?
  • Global scope pollution
  • High code re-use
  • Separation of concerns
  • Revealing Module Pattern
  • Bootstrapper.js will prime the data and setup the presentation
 
jQuery's deferreds, promises and $.when() at http://api.jquery.com/jquery.when/
 
ModularDemo to show js module dependencies: http://requirejs.org
define('alerter', ['dataservice'], function (dataservice) { ... });
requirejs.config({ baseUrl:'scripts', paths: { 'jquery' : 'jquery-1.7.2' } });
7. MVVM & KNOCKOUT
<input class="filter-box" type="text" 
data-bind="value: sessionFilter.searchText, 
valueUpdate: 'afterkeydown', 
escape: clearFilter"
placeholder="filter by model and brand"/>
 
_.reduce(timeslots, function (memo, slot) {
...
day = moment(slotStart).format('ddd MM DD');
...
}
 
configureExternalTemplates = function () {
infuser.defaults.templatePrefix = "_";
infuser.defaults.templateSuffix = ".tmpl.html";
infuser.defaults.templateUrl = "/Tmpl";
}
 
ko.bindingHandlers.escape example
 
self.searchText = ko.observable().extend({ throttle: config.throttle });
 
WebEssentials Plugin
ko.bindingHandlers example for favorites icon
 
sessions = ko.observableArray().trackReevaluations()
 
Debugging Tip
<div style="border: white dotted thin; clear: both">
<h4>Sessions JSON</h4>
<pre data-bind="text:ko.utils.debugInfo(speaker())"></pre>
</div>
ko.utils.debugInfo = function (items) {
return ko.computed(function() {
return ko.toJSON(items, null, 2); // JSON.stringify(ko.toJS(timeslots), null, 2);
}
}
8. Data Services on the Client
  • DataContext, DataService and Model Mappers
  • SRP - Single Responsibility Principle
  • datacontext.sessions.getData({data: sessions});
 
It's faster to create the raw array and then stick it into an ko.observableArray, so you get only one event notification !
 
  • $.ajax() vs $.getJSON()
  • http://amplifyjs.com : Wraps Ajax calls, Always Async, Mock Data, Caching
var init = function () {
amplify.request.define('session-briefs', 'ajax', {
url: 'api/sessionbriefs',
dataType: 'json',
type: 'GET'
//cache: true
//cache: 60000 // 1 minute
//cache: 'persist'
});
}
dataservice.session.js vs mock.dataservice.session.js
$.mockJSON.data.TITLE = [
'Pick and choose from these lines',
'Bla bla bla',
  'more bla bla' 
];
 
var data = $.mockJSON.generateFrom Template({
 'sessions|100-120' : [{
'id|+1': 1,
title: '@TITLE',
code: '@LOREM', //just one word
'speakerId|1-50':1,
description: '@LOREM_IPSUM' //just some text  
  }]
});
 
//some hardcoded to do some testing against
data.sessions[0].id = 1;
data.sessions[0].title = 'Single Page Apps';
 
if (_useMocks) {
dataserviceInit = mock.dataserviceInit;
}
dataserviceInit();
 
config.useMocks(true); //can be used in your tests
Role of the Data Context
  • Caches client data (Stores data in memory)
  • Facade for Data Services (Simplified API)
  • HandlesDataRequestLogic (Cached then return else get and cache) 
datacontext.sessions.getData(dataOptions(false))
sessions = new EntitySet(dataservice.session.getSessionBriefs, modelmapper.session, model.session.Nullo); 
EnititySet has mapDtoToContext, add, removeById, getLocalById, getAllLocal, getData, updateData
 
Prototypes in Javascript
 
Upshot.js 
  • Uses DataController that inherits from ApiController
  • Can get meta data from your WCF services
  • From the asp.net team
var dataSource = upshot.RemoteDataSource({
    providerParameters: { 
        url: constants.serviceUrl, 
        operationName: "GetProducts" 
    },
    provider: constants.provider,
    entityType: "Product:#Sample.Models"
}).refresh();
Breezejs.com
manager = new entityModel.EntityManager('api/todos');
var query = new entityModel.EntityQuery()
.from("Todos")
.orderBy("CreatedAt");
query = query.where("IsArchived", op.Equals, false);
  • Uses a Metadata action method on your controller (contextProvider.Metadata());
9. Navigation between views
sammy.get("#/sessions/:id', function (context) {
vm.callback(context.params);
presenter.transitionTo( $(viewName), path );
}
  •  jQuery event delegation to the container (performance)
  • bindEventToList(config.viewIds.sessions, sessionBriefSelector, callback, eventName)
  • resharper ctrl-shift-f7 and ctrl-alt-down will find the selected word in file.
$(rootSelector).on(eName, selector, function() {
var session = ko.dataFor(this);
callback(session);
return false;
});
Adding page transitions
  • css3-transitions: http://caniuse.com or http://realworldvalidator.com
  • jQuery transitions if you want to go cross-browser
  • presenter.transitionTo();
 
Browser history
<button class="button-back" data-bind="command: goBackCmd"></button>
 
goBackCmd = ko.asyncCommand({
execute: function (complete) { router.navigateBack(); complete() },
canExecute: function (isExecuting) { return !isExecuting && !isDirty(); }
});
ko.bindingHandlers.command = { ... };
 
Cancel & Recall using amplifyjs
  • module amplify.store for client storage on old and new browsers
  • messenger module amplify.publish and amplify.subscribe
 
If changes and user clicks back button, we use toastr to display a warning message (http://nuget.org/packages/toastr).
sammy.before(/*/, function () { ... response = routeMediator.canLeave(); ... }
 
store.save(config.stateKeys.lastView, context.path);
This module wraps amplify, so it can easily be replaced with localstorage on HTML5
expires: config.storeExpirationMs in milliseconds
 
Alternatives for sammy.js
10. Saving, Change Tracking and Validation
model.person.js
self.dirtyFlag = new ko.DirtyFlag([
self.firstName,
self.lastName,
self.email,
]);
isDirty = ko.computed(function() {
return canEdit() ? speaker().dirtyFlag().isDirty() : false;
});
 
Asynchronous Commands 
(eg. disable save button once it is clicked and put status indicator)
saveCmd = ko.asyncCommand({
execute: function(complete) {
$.when(datacontext.person.updateData(speaker()))
.always(complete);
},
canExecute: function(isExecuting){
return !isExecuting && isDirty() && isValid();
}
});
<button data-bind="command: saveCmd, activity: saveCmd.isExecuting'>Save</button>
Explicit vs Implicit Save?
saveFavorite = function (selectedSession) {
if (isBusy) { return; }
isBusy = true;
var cudMethod = selectedSession.isFavorite()
? datacontext.attendance.deleteData
: datacontect.attendance.addData;
cudMethod(selectedSession,
{
success: function() { isBusy = false; },
error: function() { isBusy = false; }
}
);
}
 
Use valueHasMutated() to tell and observable its value has updated.
 
Validate Data?
 
  • Using Knockout.Validation plugin.
self.firstName = ko.observable().extend({ required: true });
self.email = ko.observable().extend({ email: true });
self.blog = ko.observable().extend({
pattern: {
message: 'Not a valid url',
params: /[@]([A-Za-z0-9_]{1,15})/i
}
});
 
validationErrors = ko.validation.group(speaker());
isValid = ko.computed(function() {
return canEdit() ? validationErrors().length == 0 : true;
});
canLeave = function() {
return canEdit() ? !isDirty() && isValid() : true;
}
11. Mobility and Responsive Design with CSS and Less
  • Balsamiq to do mocks of gui
  • Web Essentials in Extensions and Updates of Visual Studio 2012
  • http://lesscss.org
  • Less: Variables, Mixins, Nested Rules, Functions and Operations
  • Media Queries
  • Testing for multiple devices: http://jpapa.me/electricplum 
@media only screen and (max-width: 1000px)
@media only screen and (max-width: 900px)
@media only screen and (max-width: 800px)
@media only screen and (max-width: 480px)
@media only screen and (max-width: 320px)
@media only screen and (max-width: 240px)
 
@media only screen and (max-height: 768px) {
.view-list { height: 330px; }
}

 

Page 1 of 2 1 2 > >>