HTML Render de controles ASP.NET
Hace algún tiempo, en una compañía dedicada a consultaría Web en donde trabajaba surgió un requerimiento de “integrar” ASP.NET con un CMS que permite únicamente tener contenido estático HTML, CSS y JavaScript en tiempo de diseño.
El escenario era el siguiente:
-
Un sitio en ASP.NET con varios User Controls.
-
Los editores usan el CMS para crear nuevas páginas, y en tiempo de diseño pueden insertar los controles de usuario del proyecto de ASP.NET.
-
En tiempo de diseño, el CMS no soporta ASP.NET.
-
Cuando el editor envía a publicar las páginas, estas son copiadas dentro del sitio ASP.NET, donde recién el código ASP.NET podrá ser ejecutado.
-
En tiempo de diseño del CMS, el editor debe poder ver el preview de los controles de ASP.NET.
Era una tarea bastante interesante, la cual de lo que me comentaron no se había podido realizar antes, por suerte con un poco C# se me ocurrió la siguiente idea:
Crear una página en el proyecto ASP.NET que reciba como parámetros el nombre del UserControl que se debe mostrar y los parámetros para inicializarlo, luego obtener el HTML resultante y devolverlo al cliente como una función JavaScript. De esta forma quien quiera mostrar un control de usuario en sitios no ASP.NET, solo necesita usar un poco de JavaScript (muy poco a decir verdad).
RenderControl.aspx
Esta es la página que recibe como parámetro en el QueryString el nombre del User Control y crea dinámicamente una función JavaScript con el HTML resultante. Adicionalmente el UserControl puede recibir parámetros de entrada como Strings, Booleanos, o números. Haciendo uso de Reflection, esta página se encarga de asignar dinámicamente estas propiedades antes de dibujar el control.
Creando los User Controls
Lo primero que haremos es crear unos 3 controles de usuario de ejemplo:
AllUsers.ascx: Contiene un ASp.NET GridView con una lista de usuarios obtenida de un archivo XML.
MyCalendar.ascx: Contiene un ASP.NET Calendar
UserProfile.ascx: Muestra los datos de un usuario especificado, el usuario se asigna usando la propiedad Username, la cual lee de nu archivo XML los datos del usuario.
public string Username
{
set
{
lblUsername.Text = value;
LoadProfile();
}
get
{
return lblUsername.Text;
}
}
private void LoadProfile()
{
if (!String.IsNullOrEmpty(lblUsername.Text))
{
XDocument userXml = XDocument.Load(Server.MapPath("Data/Users.xml"));
var userProfile = (from user in userXml.Descendants("user")
where user.Attribute("id").Value == lblUsername.Text
select user).Single();
lblFirstname.Text = userProfile.Attribute("firstName").Value;
lblLastname.Text = userProfile.Attribute("lastName").Value;
lblRole.Text = userProfile.Attribute("role").Value;
}
}
Estos son User Controls normales, que pueden tener elementos ASP.NET cuyo HTML resultando será mostrado en otro sitio externo.
Crear la función JavaScript
Para que nuestra página RenderContro.aspx sea capaz de dibujar el control y devolver código JavaScript necesitamos sobreescribir dos métodos de la clase Page:
-
CreateHtmlTextWriter
-
Render
Si recordamos el ciclo de vida de una página ASP.NET: Init, Load, Render y Unload, nos damos cuenta de que justamente el método Render es el encargado de “dibujar en la página” el contenido:
StringWriter sw = new StringWriter();
HtmlTextWriter pageWriter = null;
protected override HtmlTextWriter CreateHtmlTextWriter(TextWriter tw)
{
pageWriter = base.CreateHtmlTextWriter(tw);
return base.CreateHtmlTextWriter(sw);
}
protected override void Render(HtmlTextWriter writer)
{
base.Render(writer);
string html = sw.ToString();
html = html.Replace(Environment.NewLine, String.Empty);
html = html.Replace("\"", "’");
string script = "function render_" + controlName + uniqueId + "() { document.write(\"" + html + "\");}";
pageWriter.Write(script);
}
En el método CreateHtmlTextWriter estamos pasando un objeto StringWriter en lugar del predeterminado (TextWriter), es en este objeto donde el método Render insertará el código HTML de los controles. Todo el contenido de la página en forma de HTML, cualqueira que se este, será escrito dentro de nuestro StringWriter, y posteriormente podremos manipularlo según nuestras necesidades.
En el método Render modificamos el HTML antes de que s dibuje en la página, de tal forma que el resultado sea una función JavaScript con el método document.write.
La función es creada dinámicamente y se forma de la siguiente forma:
-
El prefijo “render_”
-
El nombre del control.
-
Un identificador único para el nombre de la función.
-
El cuerpo de la función consiste del método document.write y el contenido es el HTML resultando de la página.
NOTA: Es siempre importante remover saltos de línea y comillas que del contenido puedan generar errores en la función JavaScript.
Insertar el control dinámicamente
La última parte de nuestro proyecto es insertar el control dinámicamente en la página para que se cree la función JavaScript. Insertar un control dinámicamente es muy sencillo en ASp.NET:
Control ctrl = LoadControl(@"~\UserControls\" + controlName + ".ascx");
canvas.Controls.Add(ctrl);
La parte interesante está en asginar las propiedades de forma dinámica a cada control. Por ejemplo asignar el valor de la propiedad Username de control UserProfile. Esto lo logré usando Reflection, gracias a esto puedo asignar el valor de cualqueir propiedad cuyo tipo de dato soporte el método Parse:
private void SetProperties(Control ctrl)
{
Type controlType = ctrl.GetType();
List<PropertyInfo> properties = controlType.GetProperties().ToList<PropertyInfo>();
foreach (var qs in Request.QueryString)
{
if (!qs.ToString().Equals(qsControl, StringComparison.InvariantCultureIgnoreCase)
&& !qs.ToString().Equals(qsUniqueId, StringComparison.InvariantCultureIgnoreCase))
{
PropertyInfo currentProperty = properties.Find(delegate(PropertyInfo pi)
{
return pi.Name == qs.ToString();
});
if (currentProperty != null)
{
if (currentProperty.PropertyType.Name == "String")
{
currentProperty.SetValue(ctrl, Request.QueryString[qs.ToString()], null);
}
if (currentProperty.PropertyType.GetMember("Parse").Length > 0)
{
Object parsedValue = currentProperty.PropertyType.InvokeMember("Parse", BindingFlags.InvokeMethod, null, ctrl, new object[] { Request.QueryString[qs.ToString()] });
currentProperty.SetValue(ctrl, parsedValue, null);
}
}
}
}
}
Este método recibe como parámetro el Control y lee del QueryString las propiedades que se le deben asignar mediante Reflection. De especial interés es el siguiente bloque de código:
if (currentProperty.PropertyType.GetMember("Parse").Length > 0)
{
Object parsedValue = currentProperty.PropertyType.InvokeMember("Parse", BindingFlags.InvokeMethod, null, ctrl, new object[] { Request.QueryString[qs.ToString()] });
currentProperty.SetValue(ctrl, parsedValue, null);
}
Esta sección de código permite transformar el valor de la propiedad que siempre viene como String al tipo de dato que espera la propiedad, por ejemplo Bool o Int. La transformación se la hace invocando dinámicamente el método Parse del tipo de datos de la propiedad.
Finalmente ya podemos incrustar controles ASP.NET en paginas desarrolladas en otras tecnologías, o páginas simples HTML, así:
<head>
<title></title>
<script src="RenderControl.aspx?control=UserProfile&Username=cintriago&UpperCase=true"></script>
<script src="RenderControl.aspx?control=UserProfile&uniqueId=2&Username=jane"></script>
<script src="RenderControl.aspx?control=UserProfile&uniqueId=3&Username=john"></script>
<script src="RenderControl.aspx?control=AllUsers"></script>
<script src="RenderControl.aspx?control=MyCalendar"></script>
</head>
<body>
<script language="javascript" type="text/javascript"> render_AllUsers();</script>
<script language="javascript" type="text/javascript"> render_UserProfile();</script>
<script language="javascript" type="text/javascript"> render_UserProfile2();</script>
<script language="javascript" type="text/javascript"> render_MyCalendar();</script>
<script language="javascript" type="text/javascript"> render_UserProfile3();</script>
</body>
</html>
Y en el navegador tendremos el siguiente resultado:
Espero que este artículo les haya pareceido de utilidad en interés. Cualquier duda, comentario o sugerencia es bienvenida. Hasta la próxima!!.