Sempre più spesso, nelle moderne applicazioni web, il codice Javascript di una pagina ha la necessità di accedere ad una webapi. Questo è certamente il più semplice metodo per attivare la pagina caricando informazioni dal server senza necessariamente effettuare il refresh dell'intera pagina. La chiamata può essere necessaria per riempire una semplice dropdowlist il cui contenuto dipende da qualche altro valore impostato in una form, piuttosto che una lista di risultati di una ricerca. Qualunque sia il contenuto, l'operazione è sempre quella di richiemare un metodo di una webapi e in seguito alla risposta deserializzare il json per popolare l'interfaccia utente.
In casi come questi ho trovato molto utile usare una classe proxy, mimando quello che avviene in C# quando si interroga un servizio WCF. La cosa interessante è che le WebApi mettono a disposizione di un ApiExplorer che è in grado di fare l'inspect della api stessa e restituire tutti i dettagli quali i metodi, i loro parametri, tipi di ritorno etc.. Grazie ad esso è possibile scrivere il seguente metodo:
1:publicabstractclass ApScriptableController : ApiController
2: {
3:/// <summary>
4:/// Il metodo restituisce lo script Javascript che consente di interrogare il controller
5:/// </summary>
6:/// <returns>Ritorna il </returns>
7: [HttpGet]
8:public HttpResponseMessage GetScript()
9: {
10: ApiExplorer explorer = new ApiExplorer(this.Configuration);
11:
12: StringBuilder builder = new StringBuilder();
13:
14: builder.Append("var __extends = this.__extends || function (d, b) {");
15: builder.Append(" for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];");
16: builder.Append(" function __() { this.constructor = d; }");
17: builder.Append(" __.prototype = b.prototype;");
18: builder.Append(" d.prototype = new __();");
19: builder.Append("};");
20:
21: var actions = (from api in explorer.ApiDescriptions
22:where api.ActionDescriptor.ControllerDescriptor.ControllerType.IsAssignableFrom(this.GetType())
23: orderby api.ActionDescriptor.ActionName ascending
24: select api).ToArray();
25:
26:if (actions.Count() > 0)
27: {
28:string controllerName = actions.First().ActionDescriptor.ControllerDescriptor.ControllerName;
29: controllerName = controllerName.Substring(0, 1).ToUpper() + controllerName.Remove(0, 1);
30:
31: builder.AppendFormat("var {0}Proxy = (function (_super) {{", controllerName);
32: builder.AppendFormat(" __extends({0}Proxy, _super);", controllerName);
33: builder.AppendFormat(" function {0}Proxy(baseUri, accessToken, context) {{", controllerName);
34: builder.Append(" _super.call(this, baseUri, accessToken);");
35: builder.Append(" this.context = context;");
36: builder.Append(" }");
37:
38:foreach (var item in actions)
39: {
40: builder.AppendFormat(" {1}Proxy.prototype.{0} = function (value) {{", item.ActionDescriptor.ActionName, controllerName);
41: builder.AppendFormat(" return _super.prototype.callApi.call(this, '{0}', value, this.context);", item.RelativePath);
42: builder.Append(" };");
43: }
44:
45: builder.AppendFormat(" return {0}Proxy;", controllerName);
46: builder.Append("})(sys.net.Proxy);");
47: }
48:
49: var response = new HttpResponseMessage(HttpStatusCode.OK);
50: response.Content = new StringContent(builder.ToString(), Encoding.UTF8, "text/javascript");
51:return response;
52: }
53: }
Il codice rappresenta una classe base da utilizzare per i controller delle WebApi. Essa implementa un metodo GetScript che non fa altro che ispezionare i metodi della api stessa e genera di conseguenza un codice Javascript che rappresenterà il proxy della WebApi. In coda al metodo, le ultime righe cambiano il content-type della risposta impostandolo e text/javascript. In questo modo sarà possibile fornire l'url della api ad un tag script e di conseguenza far leggere il codice e caricarlo nel browser.
1:<script src="~/api/Home/getscript"></script>
L'ultimo tassello di questo piccolo puzzle è una classe Typescript che rappresenta la base per il proxy Javascript. Il codice generato infatti fa uso delle funzioni e dei tipi presenti in questo breve snippet.
1: module sys.net
2: {
3:// represents a base for api responses
4: export interface IApiResponseBase
5: {
6: HasErrors: boolean
7: Message: string;
8: }
9:
10:// represents a successful response from an api
11: export interface IApiResponse<T> extends IApiResponseBase
12: {
13: Result: T;
14: }
15:
16: export class Proxy
17: {
18:// creates and initializes the proxy with the given base uri
19: constructor(private baseUri: string, private accessToken: string)
20: { }
21:
22:// calls a generic API with the specified route and argument
23:public callApi<T>(route: string, argument: any, context?: string): JQueryPromise<T>
24: {
25:var uri = this.baseUri + route;
26:
27:return $.Deferred(
28: (deferred: JQueryDeferred<T>) =>
29: {
30: $.ajax({
31: url: uri,
32: type: 'POST',
33: data: JSON.stringify(argument),
34: contentType: 'application/json; charset=utf-8',
35: beforeSend: (xhr) =>
36: {
37: xhr.setRequestHeader('Authorization', 'Bearer ' + this.accessToken);
38:
39:if (context != undefined)
40: xhr.setRequestHeader('X-Tsf-Context', context);
41: }
42: })
43: .done(
44: (retVal: IApiResponse<T>) =>
45: {
46:if (retVal.HasErrors)
47: deferred.reject(retVal);
48:else
49: deferred.resolve(retVal.Result);
50: })
51: .fail((err: any) => deferred.reject(this.mapToError(uri, err)));
52: }).promise();
53: }
54:
55:// maps any error information to a common object
56:private mapToError(uri: string, err: any): IApiResponseBase
57: {
58:return<IApiResponseBase>
59: {
60: Message: 'Api "' + uri + '" reported an error: ' + err.statusText,
61: HasErrors: true
62: };
63: }
64: }
65: }
Una volta che il codice sia caricato nell'ordine corretto, avremo a disposizione una classe che riporta il nome del controller seguito dalla parola Proxy. Perciò se il controller ha il nome "Home" il proxy sarà "HomeProxy".
1: var proxy = new HomeProxy(sys.Application.ServiceUri, sys.Application.AccessToken, sys.Application.Context);
2: proxy.GetValuesForList()
3: .done((result) =>
4: {
5:// TODO: process here
6: });
Il proxy fa uso delle promise di jQuery perciò il suo utilizzo è molto semplice e gestisce perfettamente la asincronicità.