Herencia Visual ASP.NET

Click here for the English version of this article

Problema:

Existe una clase base llamada BasePage.cs la cual hereda de System.Web.UI.Page. Esta clase base es responsable de definir las propiedades comunes, definir los valores por defecto, lógica de renderización básica, lógica de cross-cutting concerns, entre otras funcionalidades. También existe una master page llamada MasterPage.master la cual provee a la página heredada los elementos visuales comunes que son compartidos por todas las páginas del sitio, como está descrito en el diagrama:

inheritance

En algunos casos MyPage debe renderizar algunos controles extra dependiendo del propósito de la página. Por ejemplo, en ocasiones MyPage es usada para transacciones que no necesitan autorización y en otros casos para transacciones que necesitan autorización. Para este propósito existe un control de usuario UserControl.ascx que es renderizado para proveer la funcionalidad de autorización. Adicionalmente, MyPage debería poder mostrar este control en cualquier parte del layout de la página.

layout

Diseño:

A primera vista la solución más simple es proveer un campo protegido en la clase base BasePage.cs para indicar si la transacción necesita o no autorización, es decir, si la página heredada debe o no renderizar el control de usuario que realiza la autorización.

Luego, se necesitará asignar el valor de esta propiedad en algún lugar del code behind de la página heredada, en el archivo MyPage.aspx.cs pero esto tiene que ser antes de que la lógica de renderización sea ejecutada. De acuerdo con el Ciclo de Vida de las Páginas ASP.NET el evento para hacer esta asignación es Init porque este evento es "lanzado después de que todos los controles han sido inicializados y antes de que alguna configuración de formato haya sido aplicada. Se debe usar este evento para leer o inicializar las propiedades de los controles."

Finalmente, se necesita escribir la lógica que decidirá si se debe o no agregar el control de usuario a la colección de controles de la página final, dependiendo del valor de la propiedad definida anteriormente en la clase base. Esta lógica se debe definir en el evento Init de la clase base BasePage.cs. Para permitir que la clase heredada pueda ubicar el control en cualquier lugar del layout, el control debe ser agregado a la colección de controles de un objeto PlaceHolder específico. Para este ejemplo será llamado plhAuthControl.

Listo. La solución está definida pero existe un reto técnico. ¿Cómo encontrar el control plhAuthControl? Lo primero que hay que hacer es hallar el elemento HtmlForm porque la página final está usando una master page y esto añade un conjunto de elementos adicionales a la jerarquía de controles. En este caso, el elemento HtmlForm está en this.Controls[0].Controls[3], El elemento ContentPlaceHolder que tiene los controles para la página heredada está en this.Controls[0].Controls[3].Controls[1]. El control PlaceHolder que quiero encontrar está en este ContentPlaceHolder de tal forma que para acceder a él se lo puede hacer de las siguientes formas:

this.Controls[0].Controls[3].Controls[1].Controls[9]
this.Controls[0].Controls[3].Controls[1].FindControl("plhAuthControl")

El problema con este método es que yo no quiero que la jerarquía de controles sea fija. La master page puede cambiar, y los controles contenedores de la página heredada pueden cambiar en el tiempo o dependiendo de la página, entonces el código mostrado no sería útil y tendría que ser modificado. Una de las ideas que vino a mi mente para resolver esto fue una implementación recursiva del método FindControl de tal manera que siempre se pueda encontrar al control así:

this.FindControlRecursive(this.Master, "plhAuthControl")

... y de esta forma se obtiene el control inmediatamente sin importar la jerarquía de controles. Antes de empezar a implementar la función encontré un blog post de Rick Strahl sobre este tema, escrito hace más de dos años. Aquí hay una implementación de la función que estaba buscando:

public static Control FindControlRecursive(Control Root, string Id)
{
    if (Root.ID == Id)
        return Root;
    foreach (Control Ctl in Root.Controls)
    {
        Control FoundCtl = FindControlRecursive(Ctl, Id);
        if (FoundCtl != null)
            return FoundCtl;
    return null;
}

Simple. Problema resuelto. Entonces empecé a leer los comentarios y me di cuenta que el primer comentario fue escrito por Scott Guthrie (The Gu), primero dando una explicación sobre el porqué no existe una función FindControl recursiva en ASP.NET:

“Way back when we debated having a recursive FindControl method. One thing we worried about what the performance impact of people mis-using it -- since doing deep walks of the entire control tree lots of time can really slow things down if you don't know what you are doing.”

y luego dando una solución:

“Going back to your problem above, you should be able to just declare the control names as protected fields on your base class -- ASP.NET 2.0 wires these up just like V1.1 does (if a page has a base class and it has the controls already declared, then it doesn't re-generate them on any sub-class).”

Adicionalmente, Scott Allen (OdeToCode) contribuyó lo siguiente:

“Try using CodeFileBaseClass in your @ Page directive. This let the ASP.NET code generator find those protected fields in your base class and wire them to the controls in the aspx.”

Ok. Gracias a Rick, ScottGu y OdeToCode finalmente entendí que no hace falta la función FindControl recursiva, solamente agregar el atributo CodeFileBaseClass en la directiva @Page apuntando al nombre de la case base, y agregar los campos protegidos en la clase base con el mismo nombre que tienen en la página heredada aspx. Fácil.

Solución:

Una vez que todos los retos técnicos han sido analizados, y de acuerdo al requerimiento inicial, ésta es una implementación de referencia para esta solución:

  1. Clase Base BasePage.cs

Declarar el campo protegido que será utilizado para decidir si el control de usuario debe ser agregado o no.

        private bool hasAuthorization = false;
        protected bool HasAuthorization
        {
            get { return hasAuthorization; }
            set { hasAuthorization = value; }
        }

Declarar un campo protegido con el mismo nombre el control en MyPage.aspx de tal forma que pueda ser accesado directamente desde la clase base.

        protected System.Web.UI.WebControls.PlaceHolder plhAuthControl
        {
            get;
            set;
        }

Sobreescribir el método OnInit para agregar la lógica para agregar o no el control de usuario.

        protected override void OnInit(EventArgs e)
        {
            if (HasAuthorization)
            {
                System.Web.UI.UserControl usercontrol = LoadControl("UserControl.ascx") as System.Web.UI.UserControl;
                plhAuthControl.Controls.Add(usercontrol);
            }
            base.OnInit(e);
        }
  1. Code Behind de la página heredada (MyPage.aspx.cs)

Asegurarse que la clase hereda de la clase base

public partial class MyPage : BasePage

Sobreescribir el método OnInit para asignar el valor que indica si la página debe renderizar el control de usuario (en este caso el control será agregado y presentado):

    protected override void OnInit(EventArgs e)
    {
        HasAuthorization = true;
        base.OnInit(e);
    }
  1. Página Final (MyPage.aspx)

Agregar el atributo CodeFileBaseClass en la directiva @Page para especificar el archivo de la clase base, también asegurarse de que el atributo MasterPageFile esté asignado a la respectiva master page.

<%@ Page CodeFileBaseClass="BasePage" 
         Title="My Page" 
         Language="C#" 
         MasterPageFile="~/View/MasterPage.master" 
         AutoEventWireup="true" 
         CodeFile="MyPage.aspx.cs" 
         Inherits="MyPage" %>

Agregar un control PlaceHolder llamado plhAuthControl en la página.

<asp:PlaceHolder ID="plhAuthControl" runat="server" />

Este ejemplo muestra cómo usar el atributo CodeFileBaseClass para enlazar automáticamente controles de páginas aspx con campos protegidos de la clase base de la cual hereda. También presenta el reto técnico que supone combinar herencia visual (master pages) y herencia de clases en C#.

Published Tuesday, July 28, 2009 10:51 AM by Carlos Figueroa

Comments

# re: Herencia Visual ASP.NET

Tuesday, July 28, 2009 11:59 AM by Roberto Hernandez

Excelente post!! No obstante, dentro del mismo se ven muchos de los motivos por el que ASP.NET MVC es necesario.

FindControl Recursivo -> Yuck!

Roberto.-

Leave a Comment

(required) 
(required) 
(optional)
(required)