DevOps & Development 4 min de lectura 29 de agosto de 2025

Cómo reemplazar ImageButton por LinkButton en ASP.NET

Paso a paso para migrar controles ImageButton a LinkButton con iconos Bootstrap en ASP.NET Web Forms, resolviendo excepciones de casting y descalibres en ModalPopupExtender.

Solución en "Dos Clics" (TL;DR)

Guía para migrar controles ImageButton a LinkButton en ASP.NET con VB.NET, resolviendo errores de casting y actualizando el TargetControlID del ModalPopupExtender.

En la actualización de la interfaz de un control de usuario en ASP.NET Web Forms con VB.NET (wucEstudiantes2.ascx), surgió la necesidad de modernizar los viejos controles asp:ImageButton que dependían de imágenes GIF fijas, reemplazándolos por controles asp:LinkButton integrados con Bootstrap Icons. Aunque la migración parecía ser un cambio menor de marcado, desencadenó errores de conversión de eventos, excepciones de formato en backend y un fallo en la apertura de modales AJAX.

1. Sustitución de marcado inicial

El control de usuario utilizaba un ImageButton para activar la búsqueda desplegable y seleccionar filas dentro de un GridView. El marcado original para seleccionar a un estudiante lucía así:

Archivo wucEstudiantes2.ascx (Original)
<asp:ImageButton runat="server" ID="imbSelect" ImageUrl="~/imagenes/seleccionar.gif" CommandArgument='<%# Eval("idnumber") %>' OnClick="imbSelect_Click" CausesValidation="False" CommandName='<%# Eval("student_name") %>' ValidationGroup='<%# Eval("career_idnumber") %>'></asp:ImageButton>

Para aprovechar los estilos del marco de trabajo CSS y sustituir la imagen por un icono, reestructuré el control a un asp:LinkButton:

Archivo wucEstudiantes2.ascx (Nuevo)
<asp:LinkButton runat="server" ID="lbtnSelect" CssClass="btn btn-outline-primary btn-sm" CommandArgument='<%# Eval("idnumber") %>' CommandName='<%# Eval("student_name") %>' ValidationGroup='<%# Eval("career_idnumber") %>' OnClick="lbtnSelect_Click" CausesValidation="False">
    <i class="bi bi-check-circle"></i>
</asp:LinkButton>

2. El primer escollo: InvalidCastException en el evento Click

Al hacer clic en el nuevo botón, la aplicación se detuvo inmediatamente lanzando un error en tiempo de ejecución durante la fase de PostBack.

Error detectado: System.InvalidCastException: No se puede convertir un objeto de tipo 'System.EventArgs' al tipo 'System.Web.UI.ImageClickEventArgs'.

La causa técnica reside en las diferentes firmas de evento que maneja la infraestructura de ASP.NET Web Forms:

  • Un ImageButton transmite argumentos del tipo ImageClickEventArgs (el cual contiene coordenadas de clic X/Y).
  • Un LinkButton transmite argumentos genéricos del tipo EventArgs.

El controlador de eventos original en el código subyacente (VB.NET) mantenía la firma antigua:

wucEstudiantes2.ascx.vb (Firma antigua)
Protected Sub imbSelect_Click(sender As Object, e As System.Web.UI.ImageClickEventArgs)
    hdfIdEstudiante.Value = DirectCast(sender, ImageButton).CommandArgument
    hdfIdCarrera.Value = DirectCast(sender, ImageButton).ValidationGroup
    txtNombre.Text = DirectCast(sender, ImageButton).CommandName
    RaiseEvent EstudianteSeleccionado(IdEstudiante, IdCarrera)
End Sub

Para solucionar esta incompatibilidad, refactoricé el evento ajustando la firma a EventArgs genérico y utilizando TryCast para realizar una conversión segura del emisor (sender):

wucEstudiantes2.ascx.vb (Corregido)
Protected Sub lbtnSelect_Click(sender As Object, e As EventArgs)
    Dim btn As LinkButton = TryCast(sender, LinkButton)
    If btn IsNot Nothing Then
        hdfIdEstudiante.Value = btn.CommandArgument
        hdfIdCarrera.Value = btn.ValidationGroup
        txtNombre.Text = btn.CommandName
        RaiseEvent EstudianteSeleccionado(IdEstudiante, IdCarrera)
    End If
End Sub

3. Segundo problema: FormatException al parsear propiedades

Resuelto el inconveniente del casting de evento, emergió un segundo error durante la lectura de las propiedades del control de usuario.

Error detectado: System.FormatException: La cadena de entrada no tiene el formato correcto en Long.Parse(hdfIdEstudiante.Value).

La propiedad IdEstudiante realizaba una conversión directa asumiendo que el campo oculto siempre contendría una cadena numéricamente válida:

wucEstudiantes2.ascx.vb (Propiedad vulnerable)
Public Property IdEstudiante() As Long
    Get
        Return Long.Parse(hdfIdEstudiante.Value)
    End Get
    Set(ByVal value As Long)
        hdfIdEstudiante.Value = value.ToString()
    End Set
End Property

Si durante la reconstrucción del árbol de controles o antes de la asignación del clic la propiedad Get era invocada estando el HiddenField vacío (""), Long.Parse() interrumpía la ejecución. Se fortalecieron ambas propiedades sustituyendo Parse por TryParse:

wucEstudiantes2.ascx.vb (Propiedad blindada)
Public Property IdEstudiante() As Long
    Get
        Dim valor As Long = -1
        Long.TryParse(hdfIdEstudiante.Value, valor)
        Return valor
    End Get
    Set(ByVal value As Long)
        hdfIdEstudiante.Value = value.ToString()
        If value = -1 Then
            IdCarrera = -1
            txtNombre.Text = ""
        Else
            LoadName()
        End If
    End Set
End Property

4. El punto de quiebre: El TargetControlID del ModalPopupExtender

Tras solucionar los errores de backend, persistía un comportamiento anómalo: al presionar el nuevo botón de búsqueda principal (lbtnSearch), el modal emergente no se abría en pantalla.

Al inspeccionar la configuración del kit de herramientas AJAX (AjaxControlToolkit), se identificó la raíz del problema. El extensor del modal dependía explícitamente del identificador del control de imagen original:

wucEstudiantes2.ascx (Incongruencia en TargetControlID)
<asp:ModalPopupExtender ID="mpePrincipal" runat="server" BackgroundCssClass="modalBackground"
    DropShadow="False" PopupControlID="pnlPrincipal" PopupDragHandleControlID="pnlDragMsg"
    Enabled="True" DynamicServicePath="" CancelControlID="btnCancelar" TargetControlID="imbBuscar">
</asp:ModalPopupExtender>

Dado que el botón imbBuscar había sido sustituido en la vista por lbtnSearch, el JavaScript del cliente no podía enlazar el evento lanzador del modal. La solución definitiva consistió en sincronizar el atributo TargetControlID hacia el nuevo LinkButton:

wucEstudiantes2.ascx (Sincronizado)
<asp:ModalPopupExtender ID="mpePrincipal" runat="server" BackgroundCssClass="modalBackground"
    DropShadow="False" PopupControlID="pnlPrincipal" PopupDragHandleControlID="pnlDragMsg"
    Enabled="True" DynamicServicePath="" CancelControlID="btnCancelar" TargetControlID="lbtnSearch">
</asp:ModalPopupExtender>
Consejo Práctico: Al reemplazar controles de disparo en ASP.NET Web Forms, revisa siempre los componentes declarativos que dependen de sus IDs (como extenderes de AJAX Toolkit, disparadores de UpdatePanel o scripts JavaScript cliente).
La historia detrás de la nota

En ocasiones, los fallos de ejecuciones complejas o excepciones desconcertantes en backend son desencadenados simplemente por un ID desalineado en un extender de interfaz. Verificar las referencias cruzadas entre marcado y extensiones AJAX ahorra horas de depuración.