domingo, 3 de mayo de 2009

Implementación de ASP.NET MVC para .NET 2.0

En uno de los trabajos freelance web que me ha tocado realizar uno de los requierimientos fue que corriera sobre ASP.NET 2.0, y por error yo realice todo el trabajo sobre ASP.NET MVC y sobre .NET Framework 3.5. Al darme cuenta del error trate de buscar una forma de hacer correr el codigo igualmente pero sin suerte.

Mas alla de los cambios obios que hay que realizar al codigo por la version del CLR y el interpretador de aspx, no encontre ninguna implementacion que soportara el modelo de vistas y controladores tal como la hace ASP.NET MVC.

Por esta razon he realizado la siguiente implementacion que por el momento soporta:

Controllers
Views
Actions

La base de todo esta en la configuracion del archivo web.config, ahi es donde todo empieza a funcionar, donde tenemos que imitar el funcionamiento de ASP.NET MVC.

Web.Config:
<httpHandlers> <add verb="*" path="/Encuestas/*" type="SistemaDeEncuestas.MainController"/>
<add verb="*" path="/Encuestas/*/*" type="SistemaDeEncuestas.MainController"/>
<remove verb="*" path="/Encuestas/Views/*"/> <remove verb="*" path="/Encuestas/Views/*/*"/>
<remove verb="*" path="/Encuestas/Content/*"/> <remove verb="*" path="/Encuestas/Content/*/*"/>
</httpHandlers>


Como bien saben ASP.NET MVC utiliza la url para hacer el mapeo contra los metodos que se encuentran dentro de nuestros controladores que tambien se mapean desde la url:

/MiAplicacion/"Controller"/""

Dentro de ASP.NET MVC a todo metodo que se accede de esta manera se lo llama Action.

Esta implementacion realiza todo este mapeo a traves de la clase MainController que uds pueden ver configurada como httpHanddler en el archivo Web.config.

MainController:
namespace MiAplicacion
{
public class MainController : IHttpHandler, IRequiresSessionState
{
public void ProcessRequest(HttpContext context)
{
// Parseo de la URL
string[] parts = context.Request.FilePath.Split('/');
if( parts.Length <>
return;
}
string applicationName = parts[1];
string actionName = parts[2];
string methodName = "Index";
if (parts.Length > 3)
{
methodName = parts[3];
}

if (actionName == null)
{
return;
}

ViewPage.setContext( context, applicationName, actionName );

// Instanciacion del Controller
string controllerName = this.GetType().Namespace+"."+actionName + "Controller";
Type t = Type.GetType(controllerName);
object controller = t.InvokeMember(controllerName, BindingFlags.CreateInstance, null, null, null);

t.InvokeMember("setApplicationName", BindingFlags.InvokeMethod, null, controller, new object[] { applicationName });

t.InvokeMember("setActionName", BindingFlags.InvokeMethod, null, controller, new object[] { actionName });

t.InvokeMember("setMethodName", BindingFlags.InvokeMethod, null, controller, new object[] { methodName });

t.InvokeMember("setHttpContext", BindingFlags.InvokeMethod, null, controller, new object[] { context });

// Ejecucion del Action (Metodo dentro del controller)
ActionResult actionResult = ((Controller)controller).ExecuteAction( methodName, context);

actionResult.ExecuteResult( new ControllerContext(context) );
}
public bool IsReusable
{
get
{
return false;
}
}
}
}


Como pueden ver para poder lograr todo esto se hace uso exaustivo de reflexion y instrocpection que para quienes no estan familiarizados con los terminos son formas de manipular las clases.

Para dar soporte a los elementos que se usan en ASP.NET MVC tuve que realizar la implementacion completa de las siguientes clases:
  • ActionResult
  • Controller
  • ControllerContext
  • Html
  • Url
  • ViewActionResult
  • ViewPage
La clase ViewPage posiblemente es la mas oculta pero definitivamente la mas util en la presentación ya que es la provee a la pagina aspx de los elementos:
  • ViewData
  • Html
  • Url
ViewPage:
public class ViewPage : System.Web.UI.Page
{
public Hashtable ViewData;
public static Url Url;
public static Html Html;
private static HttpContext context;

protected void Page_Load(object sender, EventArgs e)
{
if (Context.Items["ViewData"] != null)
{
ViewData = (Hashtable)Context.Items["ViewData"];
}
}

public ViewPage()
{
ViewData = new Hashtable();
}

public static void setContext( HttpContext httpContext, string applicationName, string actionName )
{
context = httpContext;
Url = new Url( applicationName, actionName);
Html = new Html(context);
}
}


Como pueden ver en el metodo Page_Load se asigan el objeto ViewData al item del contexto tambien llamado ViewData. Este item del contexto se asigna en la clase abstracta Controller, la cual es heredada por todos los controladores de nuestra aplicacion, cuando se ejecuta la llamada al metodo setHttpContext por reflexion en la clase MainContext en la linea 37.

Controller:

public abstract class Controller
{
public string applicationName;
public string actionName;
public string methodName;
public HttpContext httpContext;
public Hashtable ViewData;
public HttpSessionState Session { get { return httpContext.Session; } }
public HttpRequest Request { get { return httpContext.Request; } }

public Controller()
{
ViewData = new Hashtable();
}

public void setHttpContext(HttpContext httpContext)
{
this.httpContext = httpContext;
this.httpContext.Items["ViewData"] = ViewData;
}

public void setMethodName(string methodName)
{
this.methodName = methodName;
}

public void setApplicationName(string s)
{
this.applicationName = s;
}

public void setActionName(string s)
{
this.actionName = s;
}

public ActionResult View( string view )
{
if (ViewData["error"] != null)
{
httpContext.Items["error"] = ViewData["error"];
}
ViewActionResult action = new ViewActionResult();
action.setUrl("/" + applicationName + "/Views/" + actionName + "/" + view + ".aspx");
return action;
}

public ActionResult View()
{
return View( methodName );
}

public ActionResult RedirectToAction(string method )
{
methodName = method;
return RedirectToAction(method, null);
}

public ActionResult RedirectToAction( string method, object[] args )
{
methodName = method;
return ExecuteAction( method, args);
}

public ActionResult ExecuteAction(string method, object[] args)
{
methodName = method;
Type t = this.GetType();

ParameterInfo[] methodParams = t.GetMethod(method).GetParameters();
int methodParamsCount = methodParams.Length;

string[] keys = null;
object[] values = null;

if (methodParamsCount > 0)
{
keys = new string[methodParamsCount];
values = new string[methodParamsCount];

int i = 0;
foreach (ParameterInfo key in methodParams)
{
keys[i] = key.Name;
if (i < methodname =" method;" t =" this.GetType();" values =" null;" keys =" null;" methodparams =" t.GetMethod(method).GetParameters();" methodparamscount =" methodParams.Length;"> 0)
{
keys = new string[methodParamsCount];
values = new string[methodParamsCount];
int i = 0;
foreach (ParameterInfo key in methodParams)
{
keys[i] = key.Name;

if (context.Request.Form[key.Name] != null)
{
values[i] = context.Request.Form[key.Name];
}
else if (context.Request.QueryString[key.Name] != null)
{
values[i] = context.Request.QueryString[key.Name];
}
else
{
values[i] = null;
}
i++;
}
}
return (ActionResult)t.InvokeMember(method, BindingFlags.InvokeMethod, null, this, values, null, null, keys);
}
}


Como pueden ver en la clase Controller es donde se implementan los metodos:
  • View y View(viewName)
  • RedirectToAction(method) y RedirectToAction(method, args)
y se definen los objetos:
  • ViewData
  • Session
  • Request
Estos elementos son usados dentro de nuestro controlador:

SistemaController:

namespace SistemaDeEncuestas
{
public class SistemaController : Controller
{
public ActionResult Index()
{
if (Session["LoggedIn"] == null || (bool)Session["LoggedIn"] != true)
{
ViewData["error"] = "Primero de ingresar el sistema con sus credenciales";
return View("Login");
}
else
{
return View("Panel");
}
}
}
}

Bueno, por ultimo les dejo algunos tips para downgrade:
  • En los aspx se debe cambiar la definicion CodeBehind por CodeFile
  • No se pueden usar los elementos ASP.NET MVC como ViewData en los master page files.
  • Todo lugar donde alla usado new { key = value} debe reemplazarce por new object{key = value}
  • Para evitar cualquier inconveniente con los tipos de los datos yo use todos los parametros de los actions como strings y luego dentro los convierto al tipo correspondiente.
  • Tienen que reorganizar todos los usings porque ya no seran todos validos
Proximamente voy a subir todo el codigo a algun sitio para que lo puedan descargar y puedan seguir completando lo que falta para tener un soporte completo de todas las clases.

El codigo completo se puede bajar desde aqui:

http://aspnetmvc20.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=26921