Le modal dialog sono uno strumento molto usato nelle applicazioni per interagire con l’utente. Purtroppo, mentre in una applicazione Windows esse sono tutto sommato semplici da gestire dato che il framework ci mette a disposizione più o meno tutto ciò di cui abbiamo bisogno, così non è nelle applicazione web in cui abbiamo solo alert e confirm che sono quanto di meno user friendly si possa immaginare. Inoltre, inserire delle proprie dialog è più o meno complicato in quando richiede non solo l’inserimento del codice Javascript che le gestisce, ma anche sel markup HTML. Un doppio lavoro quindi che complica non poco la questione. Personalmente ho trovato molto proficuo usare un servizio di AngularJS, scritto in Typescript, per cercare di risolvere questo problema alla radice. Per farlo mi sono appoggiato alle Modal popop di bootstrap che sono semplici ed efficaci e gestiscono molto efficacemente la responsività. Vediamo il servizio come è realizzato:

   1: module sys.services
   2: {
   3:     export class Constants
   4:     {
   5:         static containerId: string = '#modalContainer';
   6:         static defaultModalTitle: string = 'Message';
   7:         static modalDialogId: string = '#modalDialog';
   8:     
   9:         static Negative: string = '0';
  10:         static Positive: string = '1';
  11:         static Cancel: string = '2';
  12:     
  13:         static modalYesNo: sys.services.IDialogCommand[] = [
  14:             { id: Constants.Negative, text: 'No' },
  15:             { id: Constants.Positive, text: 'Yes', style: 'btn-primary' }];
  16:         static modalYesNoCancel: sys.services.IDialogCommand[] = [
  17:             { id: Constants.Cancel, text: 'Cancel' },
  18:             { id: Constants.Negative, text: 'No' },
  19:             { id: Constants.Positive, text: 'Yes', style: 'btn-primary' }];
  20:         static modalOkCancel: sys.services.IDialogCommand[] = [
  21:             { id: Constants.Cancel, text: 'Cancel' },
  22:             { id: Constants.Positive, text: 'Ok', style: 'btn-primary' }];
  23:     
  24:         static containerMarkup: string =
  25:         '<span></span>';
  26:         static modalMarkup: string =
  27:         '<div id="modalDialog" class="modal fade"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h4 class="modal-title">Modal title</h4></div><div class="modal-body"><p>One fine body&hellip;</p></div><div class="modal-footer"></div></div></div></div>';
  28:         static modalButtonMarkup: string =
  29:         '<button type="button" class="btn" data-dismiss="modal">Close</button>';
  30:     }
  31:     
  32:     export interface IDialogCommand
  33:     {
  34:         id: string;
  35:         text: string;
  36:         style?: string;
  37:     }
  38:  
  39:     export class ModalManager
  40:     {
  41:         private container: JQuery;
  42:         private modalDialog: JQuery;
  43:  
  44:         constructor()
  45:         {
  46:             this.appendModals();
  47:         }
  48:  
  49:         private exists(name: string): boolean
  50:         {
  51:             if (this.container == undefined)
  52:                 this.container = $(Constants.containerId);
  53:  
  54:             return this.container.find(name).length != 0;
  55:         }
  56:  
  57:         private appendModals(): void
  58:         {
  59:             if (!this.exists(Constants.containerId))
  60:             {
  61:                 this.container = $(Constants.containerMarkup);
  62:                 this.container.appendTo($(document.body));
  63:             }
  64:  
  65:             if (!this.exists(Constants.modalDialogId))
  66:             {
  67:                 this.modalDialog = $(Constants.modalMarkup);
  68:                 this.modalDialog.appendTo(this.container);
  69:             }
  70:         }
  71:  
  72:         public showDialog(message: string, title: string = Constants.defaultModalTitle, commands?: IDialogCommand[], callback?: (id: string) => void)
  73:         {
  74:             this.modalDialog.find('.modal-body').text(message);
  75:             this.modalDialog.find('.modal-title').text(title);
  76:  
  77:             var footer = this.modalDialog.find('.modal-footer');
  78:             footer.empty();
  79:  
  80:             for (var i in commands)
  81:             {
  82:                 var cmd = commands[i];
  83:  
  84:                 var button = $(Constants.modalButtonMarkup);
  85:  
  86:                 button.text(cmd.text);
  87:                 button.addClass(cmd.style == undefined ? 'btn-default' : cmd.style);
  88:                 button.data('command-id', cmd.id);
  89:  
  90:                 button.click((ev) =>
  91:                 {
  92:                     var id = $(event.srcElement).data('command-id');
  93:  
  94:                     this.modalDialog.modal('hide');
  95:  
  96:                     if (callback != undefined)
  97:                         callback.apply(this, [id]);
  98:                 });
  99:  
 100:                 button.appendTo(footer);
 101:             }
 102:  
 103:             this.modalDialog.modal('show');
 104:         }
 105:     }
 106: }

Il servizio così impostato è completo di tutto ciò che serve al funzionamento. Da solo per scontato che siano reparibili le librerie suddette (AngularJS e Bootstrap). Il metodo appendModals() in particolare si occupa di creare il markup necessario utilizzando alcune stringhe costanti. Esso verifica che il markup non sia già presente ed eventualmente lo inserisce aggiungendolo al body. Questo favorisce l’utilizzo della libreria che non richiede alcuna gestione del markup HTML. L’utilizzo è molto semplice. E’ sufficiente infatti registrare il servizio assieme al controller con la seguente riga:

   1: .service('$modalManager',
   2:     () => new sys.services.ModalManager());

Avremo così a disposizione il servizio $modalManager che possiamo iniettare in un controller. A questo punto, potremo richiamare la gdialog molto semplicemente usando uno dei metodi del servizio:

   1: this.$modalManager.showDialog(
   2:     'Are you sure you want to delete the resource',
   3:     'Confirm operation',
   4:     sys.Constants.modalYesNo,
   5:     (id: string) =>
   6:     {
   7:         if (id == sys.Constants.Positive)
   8:         {
   9:             this.clocks.push(timezone);
  10:         }
  11:  
  12:         this.selectedTimezone = undefined;
  13:         this.scope().$apply();
  14:     });

La dialog è sufficientemente intelligente da gestire numerose combinazioni di pulsanti. Le costanti presenti nella dichiarazione sono di aiuto in questo ma se ne possono creare di proprie dato che si tratta a tutti gli effetti di un array di IDialogCommand.

   1: static modalOkCancel: sys.services.IDialogCommand[] = [
   2:     { id: Constants.Cancel, text: 'Cancel' },
   3:     { id: Constants.Positive, text: 'Ok', style: 'btn-primary' }];

Senza dimenticare l’uso appropriato degli stili di bootstrap.


E' di questi giorni l'annuncio che Angular 2.0 è ora completamente scritto in Typescript. Questo è forse uno dei più sorprendenti risultati della politica di Satya Nadella che sta aprendo giorno dopo giorno alle librerie open source di terze parti. E ancora più sorprendente se pensiamo che Angular è un prodotto dei team di Google che recentemente, supportato da Microsoft ha deciso di adottare questo linguaggio. La cosa che più mi entusiasma, come riportato in questo post, è che la collaborazione sarà fonte di nuovi miglioramenti per Typescript che già dalla versione 1.5 vedrà l'introduzione delle annotations, espressamente progettate per Angular. Le annotation apriranno la strada a una sorta di Reflection che tipicamente è fonte di numerose soluzioni che risparmiano lavoro. Attendiamo con ansia...


L'asincronia sta ormai diventando una questione di tutti i giorni nelle applicazioni moderne così i linguaggi come C# si sono adeguati con costrutti come async/await. Javascript, pur essendo un linguaggio ormai antico secondo i tempi cui l'informatica ci ha abituato, gestisce da sempre molte attività in modo asincrono, utilizzando il meccanismo dei callback. Un esempio è il setInterval in cui uno degli argomenti passati è la funzione da chiamare ad intervalli regolari.

Librerie come jQuery e Angular stesso espongono numerosi casi di utilizzo di metodi asincroni. L'http service di Angular ad esempio è uno di questi casi. Esso però non utilizza normali callback ma un sistema molto simile al Task<T> di C#. Le promise. In effetti da ciascuna chiamata asincrona è ritornato un oggetto Promise che dispone dei metodi then, catch e finally. Questi servono per associare un callback a ciascuno delle condizioni che i nomi dei metodi evocano.

Ma la domanda cui vogli rispondere in questo post è la seguente: come faccio ad esporre una promise da un mio metodo asicrono se esso stesso no usa un servizio che la utilizzi? E' il caso ad esempio di un metodo che visualizzi una dialog modale. Essono mo potrà mai essere sincrono perchè oltre a risultare bloccante per il browser, non sarà comunque possibile gestire una eventuale "attesa" e ritornare dal metodo alla chiusure della dialog. Quello che ci viene in soccorso è il Q service. Esso è usato dallo stesso http service è ha come unico scopo la gestione asincrona. Vediamo un esempio:

   1: public showDialog(message: string, title: string = Constants.defaultModalTitle, commands?: IDialogCommand[]): ng.IPromise<string>
   2: {
   3:     var defer: ng.IDeferred<any> = this.$q.defer<string>();
   4:  
   5:     try
   6:     {
   7:         this.modalDialog.find('.modal-body').text(message);
   8:         this.modalDialog.find('.modal-title').text(title);
   9:  
  10:         var footer = this.modalDialog.find('.modal-footer');
  11:         footer.empty();
  12:  
  13:         for (var i in commands)
  14:         {
  15:             var cmd = commands[i];
  16:  
  17:             var button = $(Constants.modalButtonMarkup);
  18:  
  19:             button.text(cmd.text);
  20:             button.addClass(cmd.style == undefined ? 'btn-default' : cmd.style);
  21:             button.data('command-id', cmd.id);
  22:  
  23:  
  24:             button.click((ev) =>
  25:             {
  26:                 var id = $(event.srcElement).data('command-id');
  27:                 this.modalDialog.modal('hide');
  28:                 defer.resolve(id);
  29:             });
  30:  
  31:             button.appendTo(footer);
  32:         }
  33:  
  34:         this.modalDialog.modal('show');
  35:     }
  36:     catch (error)
  37:     {
  38:         defer.reject(error);
  39:     }
  40:  
  41:     return defer.promise;
  42: }

Diamo per assodato che questo metodo faccia capo ad una classe che ha ricevuto una istanza del Q service come argomento del costruttore. Come segue:

   1: constructor(private $q: ng.IQService)
   2: {
   3: }

Il metodo showDialog al proprio inizio crea una istanza di una classe di tipo IDeferred<any>, dove any è il tipo del valore ritornato dal metodo in questione. L'oggetto deferred è quello che ci serve per gestire l'asincronia. Al termine del metodo infatti viene ritornata la promise che esso contiene con "defer.promise".

A questo punto è necessario invocare i callback di successo (then) e quello di fallimento (catch). Per farlo sono utilizzati i metodi "resolve" e "reject" dell'oggetto deferred.

Così facendo è possibile usare il metodo come segue:

   1: this.showDialog(
   2:     'Are you sure you want to delete the item',
   3:     'Confirm operation',
   4:     sys.Constants.modalYesNo).then(
   5:         (id: string) =>
   6:         {
   7:             if (id == sys.Constants.Positive)
   8:             {
   9:                 // delete the item here
  10:             }
  11:         });

Il sistema è l'unico consigliato. Infatti l'utilizzo dei callback normali crea problemi al sistema di databinding di AngularJS. Invece il q service è perfettamente in grado di supportarlo.


Spesso può risultare molto comodo caricare una porzione di html dal server, magari generata da una partial view di ASP.NET MVC (o magari proprio da una View, perchè no?). con angular questo è del tutto possibile grazie alla direttiva ng-include che consente di specificare l'url da cui  caricare una risorsa all'interno della pagina. Vediamo un esempio con typescript:

Per prima cosa si crea una partialview  che dimostri un po' di dinamismo. Ecco un semplice esempio:

   1: <h2>@DateTime.Now.ToLongDateString()</h2>
   2: <h3>@DateTime.Now.ToLongTimeString()</h3>

In seguito aggiungiamo al controller il codice per poterla chiamare dal browser:

   1: public ActionResult _PartialContent()
   2: {
   3:     return PartialView();
   4: }

A questo punto diciamo che l'url per raggiungere la partial view sia il seguente:

/Include/_PartialContent

Creaiamo quindi un controller come segue:

   1: class IncludeController
   2: {
   3:     count: number = 0;
   4:     includeUri: string = undefined;
   5:  
   6:     click(): void
   7:     {
   8:         this.includeUri = '../Include/_PartialContent?ts=' + (++this.count).toString();
   9:     }
  10: }
  11:  
  12: angular.module('include', ['ngRoute'])
  13:     .controller('includeIndex', IncludeController);

Nel controller usiamo una variabile "count" per tenere conto delle volte in cui l'utente clicca il pulsante di test. Questo valore viene accodato all'uri della partial-view per forzarne il caricamento a ciascun click. L'uri così calcolato viene riposto nella proprietà includeUri. Vediamo ora la view:

   1: @{
   2:     ViewBag.Title = "Test Include";
   3: }
   4: <div>
   5:     &nbsp;
   6: </div>
   7: <div ng-app="include" ng-controller="includeIndex as ct" class="row">
   8:     <form>
   9:         <button type="button" class="btn btn-primary" ng-click="ct.click();">Click Me!</button>
  10:         <p ng-show="ct.count>0">clicked {{ct.count}} times</p>
  11:         <div ng-include="ct.includeUri"></div>
  12:     </form>
  13: </div>
  14: @section Scripts {
  15:     <script src="~/Scripts/Views/Include/index.js"></script>
  16: }

Niente più di un normale binding, con un pulsante per invocare la funzione click(), la visualizzazione del count e l'assegnazione della direttiva ng-include sulla variabile includeUri. Il risultato è che la partial view viene inclusa all'interno delle pagina ogni volta che clicchiamo il pulsante.

Interessante notare che mediante questa tecnica possiamo agevolmente caricare nella pagina qualunque tipo di file il server sia in grado di servire. Se ad esempio all'url della partial sostituiamo quello del file TS vedremo apparire il codice.


Capita talvolta (spesso in effetti), che quando entrate in una pagina in cui sia utilizzato AngularJS per una frazione di secondo appaiano le notazioni del binding che spariscono poco dopo quando il databinding viene applicato. Questo accade perchè in quella frazione di secondo in cui il browser scarica il codice e avvia angular, tutte quelle parentesi graffe sono niente più che semplice testo e in gran parte esse sono visualizzate dal browser perchè occupano posizioni "visibili".

Per evitare questo comportamento viene in aiuto la direttiva ng-Cloak. Essa, come testimonia la documentazione ha proprio lo scopo di applicare un "display:none" agli elementi, e questo funzione nella maggioranza dei casi. Tuttavia anche se applichiamo la direttiva ng-Cloak (ad esempio al body per generalizzare, ma anche ad un singolo elemento come invece è consigliabile) talvolta le graffe appaiono comunque. Questo è imputqabile al medesimo ritardo. Per evitarlo è opportuno aggiungere al proprio css il seguente:

   1: [ng\:cloak], [ng-cloak], .ng-cloak {
   2:   display: none !important;
   3: }

In questo modo il CSS cercherà tutti gli elementi con l'attributo ng-Cloak e li nasconderà automaticamente.

Il trick è spiegato più estensivamente qui: http://stackoverflow.com/questions/11249768/angularjs-ng-cloak-ng-show-elements-blink