ASP.NET ed i Patterns: ASP.NET MVC (Model, View, Controller) Framework (Parte 2)

Nella prima parte del post ho introdotto il nuovo ASP.NET MVC  Framework fino all'implementazione del Controller. Procediamo ora con l'approfondire alcuni elementi relativi all'interazione del Controller con la View che sono tra i principali valori dell'implementazione del MVC.

Nell'ASP.NET MVC Framework il Controller procede a determinare la view da implementare attraverso una Factory che implementa l'interfaccia IViewFactory così definita:

public interface IViewFactory
    {
        IView CreateView(ControllerContext controllerContext, string viewName, string masterName, object viewData);
    }

Questa Factory consente al controller di determinare la view e di poter passare ad essa le informazioni di contesto della richiesta (ControllerContext)  e l'oggetto che rappresenta il Model (viewData) .

La View è costruita con una classe che implementa l'interfaccia IView che ha la seguente struttura:

public interface IView
    {
        void RenderView(ViewContext viewContext);
    }

dove il viewContext è la seguente classe che deriva dal ControllerContext:

public class ViewContext : ControllerContext
    {
        public ViewContext(ControllerContext controllerContext, object viewData, TempDataDictionary tempData);
        public ViewContext(RequestContext requestContext, IController controller, object viewData, TempDataDictionary tempData);
        public ViewContext(IHttpContext httpContext, RouteData routeData, IController controller, object viewData, TempDataDictionary tempData);

        public TempDataDictionary TempData { get; }
        public object ViewData { get; }
    }

Avendo derivato il nostro controller dalla classe controller del framework MVC abbiamo a disposizione una serie di metodi e proprietà Helper per innescare la nostra View e fornirgli il Model che indicheremo come viewData come possiamo vedere guardando alla struttura della classe System.Web.MVC.Controller :

public class Controller : IController
    {
        public Controller();

        public ControllerContext ControllerContext { get; set; }
        public IHttpContext HttpContext { get; }
        public IHttpRequest Request { get; }
        public IHttpResponse Response { get; }
        public RouteData RouteData { get; }
        public IHttpServerUtility Server { get; }
        public TempDataDictionary TempData { get; internal set; }
        public IPrincipal User { get; }
        public IDictionary<string, object> ViewData { get; } public IViewFactory ViewFactory { get; set; }

        protected internal virtual void Execute(ControllerContext controllerContext);
        protected internal virtual void HandleUnknownAction(string actionName);
        protected internal virtual bool InvokeAction(string actionName);
        protected internal virtual void InvokeActionMethod(MethodInfo methodInfo);
        protected virtual bool OnError(string actionName, MethodInfo methodInfo, Exception exception);
        protected virtual void OnPostAction(string actionName, MethodInfo methodInfo);
        protected virtual bool OnPreAction(string actionName, MethodInfo methodInfo);
        protected virtual void RedirectToAction(object values);
        protected void RedirectToAction(string actionName);
        protected void RedirectToAction(string actionName, string controllerName);
protected void RenderView(string viewName);
        protected void RenderView(string viewName, object viewData);
protected void RenderView(string viewName, string masterName);
protected virtual void RenderView(string viewName, string masterName, object viewData);

    }

Ho evidenziato gli elementi della classe direttamente collegati alla gestione della view ed in particolare abbiamo:

- la proprietà ViewFactory sulla quale viene impostata la factory per la costruzione delle view ;

- i metodi Render che permettono di innescare la view fornendo come parametro sia i dati che le informazioni relative al nome della view;

- la proprietà ViewData che è un Dictionary utilizzato per fornire i dati che devono essere presentati al client dalla View.

Nell'implementazione di default del framework del nostro CustomerController, la proprietà ViewFactory viene impostata a runtime nel momento in cui viene assegnata una richiesta e creato il nostro controller e viene valorizzata con la ViewFactory di base del framework System.Web.Mvc.WebFormViewFactory . Possiamo verificarlo mandando in debug la nostra applicazione e analizzando la proprieta sulla classe con il debbugger come di seguito:

image 

 

Le View generate da questa factory sono essenzialmente della pagine che derivano dalla classe System.Web.Mvc.PageView  che come si vede dalla struttura della classe:

public abstract class ViewPage : Page, IView, IViewDataContainer
    {
        protected ViewPage();

        public AjaxHelper Ajax { get; internal set; }
        public HtmlHelper Html { get; internal set; }
        public string MasterLocation { get; set; }
        public UrlHelper Url { get; internal set; }
        public ViewContext ViewContext { get; }
        public ViewData ViewData { get; }

        protected override void OnPreInit(EventArgs e);
        protected virtual void RenderView(ViewContext viewContext);
        protected internal virtual void SetViewData(object viewData);
    }

Questa non è altro che una particolare pagina aspx (deriva da System.Web.Page come tutte le pagine asp.net) che implementa l'interfaccia IView e l'interfaccia IViewDataContainer che permette la gestione dei dati del modello alla vista:

public interface IViewDataContainer
    {
        object ViewData { get; }
    }

Esiste anche una implementazione della ViewPage che permette di passare alla view le informazioni relative ai dati del model in modo tipizzato:

public abstract class ViewPage<TViewData> : ViewPage
   {
       protected ViewPage();

       public TViewData ViewData { get; }

       protected internal override void SetViewData(object viewData);
   }

E' evidente che implementando le nostre View e fornendo al controller la nostra ViewFactory possiamo in qualunque momento decidere di implementare delle view alternative come ad esempio una XmlView  che fornisca una rappresentazione Xml dei dati nella risposta,oppure utilizzare delle view specifiche per eseguire gli unit test sul controller.

La WebViewFormViewFactory implementata di default sul controller dal framework, prevede che le viewpage siano inserite nella cartella view del sito, all'interno una sottocartella con il nome corrispondente al controller. Se nell'utilizzo del metodo RenderView non vogliamo indicare il nome della viewpage, possiamo anche chiamare la pagina .aspx con il nome della action che è il nome  di default che viene utilizzato, altrimenti indicheremo il nome che richiameremo utilizzando lo specifico parametro del metodo RenderView. 

Per poter chiudere il nostro piccolo esempio ed avere a disposizione nella view la lista dei customer,  procediamo con l'implementare una pagina esattamente come descritto utilizzando il template predefinito. La ViewPage può anche essere una content page e quindi avere a disposizione una page master attraverso le specifiche classi messe a disposizione dal framework.

Come primo test implementiamo una ViewPage tipizzata rispetto alla classe che rappresenta i dati del nostro model ed avremo una nuova pagina che potrà essere invocata attraverso il metodo helper RenderView e le informazione del model potranno essere inserite in nel Dictonary ViewData presente come proprietà nella nostra classe Controller ed utilizzato per passare i dati del model alla view.

Il codice del controller sarà il seguente:

image

e la ViewPage sarà come di seguito:

image

image

E' possibile anche usare un approccio non tipizzato rispetto alla classe che rappresenta i dati del nostro model, ed avremo una nuova pagina che potrà essere invocata attraverso il metodo helper RenderView e le informazione del model potranno essere inserite in nel Dictonary ViewData presente come proprietà nella nostra classe Controller ed utilizzato per passare i dati del model alla view.

Il codice del controller sarà:

image

Nella ViewPage si potrà accedere alla lista dei customers riferendosi alla ViewData nel seguente modo:

ViewData["List"] as List<Customers>

Nella ViewPage per facilitare la costruzione dei link relativi ad una specifica action è a disposizione una classe di Helper HtmlHelper esposta già alla pagina con la proprietà Html. In particolare attraverso il metodo ActionLink è possibile costruire un link html alla action di uno specifico controller sfruttando la configurazione dell'URL routing. Ad esempio immaginiamo di avere una ulteriore action di nome ViewCustomer che permette di visualizzare i dati di un singolo customers e come parametro accetta un customerid, volendo aggiungere un link a questa action ad ogni elemento della lista della nostra pagina di esempio potremo scrivere :

Html.ActionLink(cn.CompanyName,"CustomerView","Customers")

ottenendo in risposta una stringa contenente il link URL costruito in base all'attuale configurazione del routing ovvero:

https://localhost:64701/Customers/CustomerView

Attraverso uno specifico overload del metodo è possibile passare uteriori parametri sfruttando gli "anonymus type" del linguaggio come dictionary ottenendo la possibilità di aggiungere anche il parametro per estrarre il customer:

<%= Html.ActionLink(cn.CompanyName, new {controller="Customers", action="CustomerView" , id=cn.CustomerID     })%>

Ulteriore overload del metodo ci permette di lavorare in modo tipizzato rispetto all'oggetto controller:

<%= Html.ActionLink<MvcApplication1.Controllers.CustomersController>  (s=>s.CustomerView (cn.CustomerID ) ,cn.CompanyName)%> </li>

In questo caso viene sfruttato l'expression tree delle lambda expression passate al metodo in modo tipizzato per poter calcolare l'url richiesto per lo specifico controller e la action con i rispettivi parametri  in base alla configurazione dell'infrastruttura di url routing.

Scaricando l'MVC Toolkit (download separato dalle ASP.NET extension 3.5)  e referenziandolo nel nostro progetto, si hanno a disposizione ulteriori estensioni alla classe HtmlHelper implementate come "Extension Method" . Ad esempio il metodo Select che consente di generare una select Html con il contenuto della sorgente allegata in databinding :

<%= Html.Select("test", ViewData,  "CompanyName", "CustomerId") 
      %>

clip_image012

o il metodo ListBox con cui si ottiene una specifica listbox html :

<%= Html.ListBox ("test", ViewData,  "CompanyName", "CustomerId") 
     %>

clip_image014

e così di seguito per gli altri metodi helper.

Effettuare uno Unit Test del nostro esempio

Avendo a disposizione una infrastruttura che ci ha permesso di implementare in modo completamente separato il controller ed il model dalla view , è possibile implementare degli unit test sul controller senza coinvolgere direttamente tool per il test della UI. E' infatti possibile sviluppare degli unit test come il seguente:

image

In questo caso con delle specifiche classi abbiamo sostituito il comportamento della classe controller e creato una view di test e possiamo in questo modo effettuare uno unit test del controller senza coinvolgere il resto dell'applicazione e dell'infrastruttura di ASP.NET. 

Piccolo approfondimento url routing

Abbiamo visto come nel MVC framework sia presente un'infrastruttura configurabile per il routing delle richieste che arrivano all'applicazione. Questa parte dell'framework è una implementazione del pattern "Front Controller" per determinare in modo configurabile la mappature URL -> Action\Controller.

Tutte le richieste in arrivo vengono agganciate da un HttpModule inserito nella pipeline del HttpRuntime di asp.net che si occupa di inviarle mapparle in base alla configurazione.

La configurazione di quelle che vengono definite le "Route" possibili per il calcolo del controller\action da applicare viene effettuata allo start della Web Application attraverso l'oggetto RouteTable.Routes  che con il metoto Add permette di aggiungere ulteriori Route di configurazione.

Le Route rappresentano essenzialmente il mapping tra URL e Type del controller e action che devono essere eseguite per la specifica richiesta.

Aprendo il Global.asax del progetto troviamo le impostazioni di default del framework:

image

 

Per poter inserire delle nuove configurazioni occorre quindi costruire una nuova istanza della classe Route , fornirgli i parametri desiderati e appenderla alla collezione che contiene le configurazioni.

Ad esempio per ottenere un mapping di questo tipo:

Customers/ricercaclienti/[query]/[page]

per un controller CustomersController con la seguente action:

[ControllerAction]

public void RisultatiRicerca(string query,int page)

occorrerà aggiungere la seguente Route:

RouteTable.Routes.Add(new Route
{
     Url = "Customers/ricercaclienti/[query]/[page]",
     Defaults = new { controller = "Customers", action = "RisultatiRicerca", page = 0 },
     RouteHandler = typeof(MvcRouteHandler)
});

La classe Route permette anche di utilizzare le RegEx per validare il formato dei parametri utilizzati nella richiesta attraverso l'utilizzo della proprietà UrlRegEx .

L'HttpModule di routing inserito dall'MVC framework utilizza la tabella che contiene le Route per determinare  l'Handler a cui passare il compito di creare il controller e gestire il ciclo di vita della richiesta.

image

 

La classe impostata sulla proprietà RouteHandler dell'oggetto Route rappresenta un ulteriore punto di intercezione del framework. E' l'elemento che si occupa di fornire l'HttpHandler che utilizzerà la Factory di costruzione del Controller e ad impostare su questo gli oggetti di contesto gestendo il ciclo di vita della richiesta. La classe di default che viene utilizzata dal framewor per questa funzione è la System.Web.Mvc.MvcRouterHandler che  ha la seguente struttura:

public class MvcRouteHandler : IRouteHandler
    {
        public MvcRouteHandler();

        protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext);
    }

L'interfaccia base che viene utilizzata per interagire con il RouteHandler è la seguente:

public interface IRouteHandler
    {
        IHttpHandler GetHttpHandler(RequestContext requestContext);
    }

dove viene fornito il contesto della richiesta attraverso:

public class RequestContext
    {
        public RequestContext(IHttpContext httpContext, RouteData routeData);

        public IHttpContext HttpContext { get; internal set; }
        public RouteData RouteData { get; internal set; }
    }

L'Handler che viene restituito di default dalL'MvcRouteHandler è implementato dalla classe:

public class MvcHandler : IHttpHandler, IRequiresSessionState
    {
        public MvcHandler();

        protected virtual bool IsReusable { get; }
        public RequestContext RequestContext { get; set; }

        protected internal virtual IController GetControllerInstance(Type controllerType);
        protected internal virtual Type GetControllerType(string controller);
        protected virtual void ProcessRequest(HttpContext httpContext);
        protected internal virtual void ProcessRequest(IHttpContext httpContext);
    }

Implementando una nostra classe di RouteHandler attraverso l'implementazione di IRouteHandler , possiamo inserirci all'interno del processo di caricamento dell'Handler che poi gestisce il ciclo di vita della richiesta controllandolo e cambiandone a piacimento il comportamento.

E' possibile anche sostituire la factory che viene utilizzata per la costruzione del Controller inserendo un ulteriore punto di intercezione.

La factory del controller implementa l'interfaccia System.Web.Mvc.IControllerFactory  ed ha questo tipo di definizione:

public interface IControllerFactory

{

        IController CreateController(IHttpContext context, RouteData routeData, Type controllerType);

}

per implementarne una nostra e sostituire la classe di default , occorre definire una classe che implementa l'interfaccia e registrarla con l'apposito metodo della classe System.Web.Mvc.ControllerBuilder  all'application start nel global.asax:

ControllerBuilder.Current.SetDefaultControllerFactory(typeof(MyControllerFactory));

Conclusioni

Il nuovo MVC Framework offre una nuova infrastruttura basata sul pattern MVC per sviluppare soluzioni Web con ASP.NET completamente integrata con il resto delle funzionalità della piattaforma . Il nuovo framework verrà rilaciato nella ua versione definitiva con le ASP.NET 3.5 Extensions la cui data di rilascio è al momento prevista entro l'estate.

Per poterlo sperimentare un ottimo punto di partenza è rappresentato dal Quickstart on line delle ASP.NET Extension presente online. Interessanti anche i post di Scott sull'MVC Framework e ottimo il Post di Phil Haack post : TDD and Dependency Injection with ASP.NET MVC sull'utilizzo del pattern Dependency Injection e sull'approccio TDD (Test Driven Development) con l'MVC Toolkit .