Durante el desarrollo de aplicaciones con ASP.NET Web Forms, es común encontrarse con la necesidad de limitar la cantidad de registros devueltos por un control LinqDataSource. A diferencia de SQL tradicional, este control no cuenta con una propiedad directa como Top. En esta bitácora, detallo cómo logré implementar un límite dinámico tipo "TOP N" utilizando el evento Selecting, cómo transformé un control de usuario rígido en un componente altamente configurable y cómo superé un clásico dolor de cabeza del diseñador de Web Forms.
1. El desafío inicial: Simular un TOP N en LinqDataSource
El control LinqDataSource es excelente para enlazar datos rápidamente, pero carece de una propiedad nativa para limitar el número de filas devueltas. Para simular un TOP 10, la clave está en interceptar la consulta antes de que se ejecute en la base de datos. Esto se logra mediante el evento Selecting.
Al capturar el evento, podemos manipular la consulta LINQ directamente y aplicar el método .Take(N) antes de asignar el resultado a e.Result:
Protected Sub LinqDataSource1_Selecting(sender As Object, e As LinqDataSourceSelectEventArgs)
Dim db As New TuDataContext()
Dim query = From p In db.Productos
Order By p.FechaCreacion Descending
Select p
e.Result = query.Take(10).ToList()
End Sub
Order By antes del Take para garantizar que los resultados devueltos sigan un criterio lógico (por ejemplo, los registros más recientes o alfabéticos). Además, asegúrate de aplicar el límite antes de llamar a .ToList() para que la paginación se resuelva de manera eficiente a nivel de base de datos (SQL) y no en memoria.
2. Evolucionando a un Control de Usuario (User Control) Configurable
El escenario real se complicó cuando necesité aplicar este límite dentro de un control de usuario personalizado (wucEstudiantes.ascx). Si definía un límite estático de 5 o 10 registros dentro del control, afectaría a todas las pantallas que lo consumían. La solución elegante fue exponer una propiedad pública desde el código del control de usuario (code-behind) para permitir que la página contenedora definiera el límite bajo demanda.
Primero, definí la propiedad en el archivo wucEstudiantes.ascx.vb:
Private _LimiteResultados As Integer = 10 ' Valor por defecto
Public Property LimiteResultados As Integer
Get
Return _LimiteResultados
End Get
Set(value As Integer)
_LimiteResultados = value
End Set
End Property
Luego, modifiqué el método Selecting del control para consumir esta propiedad:
Protected Sub ldsList_Selecting(sender As Object, e As System.Web.UI.WebControls.LinqDataSourceSelectEventArgs) Handles ldsList.Selecting
Dim db As New dtcVistasDataContext
Dim query = From tbl In db.vwEstudiantes
Where tbl.Nombre.Contains(Me.wucBusqueda1.TextoBuscar) _
Or tbl.status.Contains(Me.wucBusqueda1.TextoBuscar) _
Or tbl.motive.Contains(Me.wucBusqueda1.TextoBuscar) _
Or tbl.Identificacion.Contains(Me.wucBusqueda1.TextoBuscar) _
Or tbl.Carrera.Contains(Me.wucBusqueda1.TextoBuscar) _
Or tbl.NoCarne.Contains(Me.wucBusqueda1.TextoBuscar)
Select tbl
e.Result = query.Take(Me.LimiteResultados).ToList()
End Sub
De este modo, desde la página .aspx.vb que aloja el control, basta con establecer el valor deseado durante el Page_Load:
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
If Not IsPostBack Then
Me.wucEstudiantes1.LimiteResultados = 5
End If
End Sub
3. Filtros condicionales avanzados (Estado y Carrera)
Con la infraestructura de propiedades públicas funcionando, surgió un nuevo requerimiento: el control de usuario también debía filtrar opcionalmente por campos específicos como status = 1 o Carrera = "Medicina". Para lograr esto sin romper la flexibilidad del control, implementé una consulta dinámica utilizando AsQueryable().
Definí las nuevas propiedades públicas en el control de usuario:
Private _FiltrarPorStatus As Boolean = False
Private _StatusValor As Integer = 1
Public Property FiltrarPorStatus As Boolean
Get
Return _FiltrarPorStatus
End Get
Set(value As Boolean)
_FiltrarPorStatus = value
End Set
End Property
Public Property StatusValor As Integer
Get
Return _StatusValor
End Get
Set(value As Integer)
_StatusValor = value
End Set
End Property
Private _FiltrarPorCarrera As Boolean = False
Private _CarreraValor As String = ""
Public Property FiltrarPorCarrera As Boolean
Get
Return _FiltrarPorCarrera
End Get
Set(value As Boolean)
_FiltrarPorCarrera = value
End Set
End Property
Public Property CarreraValor As String
Get
Return _CarreraValor
End Get
Set(value As String)
_CarreraValor = value
End Set
End Property
Y reescribí el evento Selecting para concatenar los filtros de manera condicional:
Protected Sub ldsList_Selecting(sender As Object, e As System.Web.UI.WebControls.LinqDataSourceSelectEventArgs) Handles ldsList.Selecting
Dim db As New dtcVistasDataContext
Dim query = db.vwEstudiantes.AsQueryable()
If Not String.IsNullOrEmpty(Me.wucBusqueda1.TextoBuscar) Then
query = query.Where(Function(tbl) _
tbl.Nombre.Contains(Me.wucBusqueda1.TextoBuscar) _
Or tbl.status.Contains(Me.wucBusqueda1.TextoBuscar) _
Or tbl.motive.Contains(Me.wucBusqueda1.TextoBuscar) _
Or tbl.Identificacion.Contains(Me.wucBusqueda1.TextoBuscar) _
Or tbl.Carrera.Contains(Me.wucBusqueda1.TextoBuscar) _
Or tbl.NoCarne.Contains(Me.wucBusqueda1.TextoBuscar))
End If
If Me.FiltrarPorStatus Then
query = query.Where(Function(tbl) tbl.status = Me.StatusValor.ToString())
End If
If Me.FiltrarPorCarrera AndAlso Not String.IsNullOrEmpty(Me.CarreraValor) Then
query = query.Where(Function(tbl) tbl.Carrera = Me.CarreraValor)
End If
e.Result = query.OrderBy(Function(tbl) tbl.Nombre).Take(Me.LimiteResultados).ToList()
End Sub
Ahora, desde la página contenedora, el control se puede configurar con total precisión:
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
If Not IsPostBack Then
With Me.wucEstudiantes1
.LimiteResultados = 10
.FiltrarPorStatus = True
.StatusValor = 1
.FiltrarPorCarrera = True
.CarreraValor = "Medicina"
End With
End If
End Sub
4. Haciendo el límite de resultados realmente opcional
¿Qué pasa si en alguna pantalla no queremos limitar los resultados? En VB.NET, las propiedades numéricas estándar no pueden omitirse fácilmente sin asumir un valor por defecto (como 0 o 10). Para solucionar esto y permitir un comportamiento "sin límite", cambié el tipo de la propiedad a un tipo anulable (Nullable(Of Integer) o Integer?):
Private _LimiteResultados As Integer? = Nothing
Public Property LimiteResultados As Integer?
Get
Return _LimiteResultados
End Get
Set(value As Integer?)
_LimiteResultados = value
End Set
End Property
Con este cambio, ajusté la lógica del evento para aplicar el Take únicamente si la propiedad tiene un valor asignado:
If Me.LimiteResultados.HasValue AndAlso Me.LimiteResultados.Value > 0 Then
e.Result = query.OrderBy(Function(tbl) tbl.Nombre).Take(Me.LimiteResultados.Value).ToList()
Else
e.Result = query.OrderBy(Function(tbl) tbl.Nombre).ToList()
End If
5. El error del compilador: "is not a member of 'controles_wucestudiantes.ascx'"
Al intentar consumir las nuevas propiedades desde el formulario, me topé con un error frustrante durante la compilación:
'LimiteResultados' is not a member of 'controles_wucestudiantes.ascx'. El compilador reconocía la existencia del control, pero se negaba a ver las nuevas propiedades públicas.
Este comportamiento es un clásico "desajuste" del diseñador de Web Forms. Ocurre cuando el archivo de diseño (.designer.vb) o la clase autogenerada se desincronizan del archivo de código subyacente (.ascx.vb). Aunque la propiedad esté declarada como Public, el entorno no refresca las referencias.
Tras intentar limpiar la solución y reconstruir el proyecto sin éxito, apliqué la solución definitiva y más rápida para este bug del IDE: eliminar el control de usuario del formulario .aspx y volverlo a arrastrar/escribir. Al hacer esto, Visual Studio se vio obligado a regenerar la clase parcial del diseñador, reconociendo de inmediato todas las nuevas propiedades expuestas.
La historia detrás de la nota
A veces pasamos horas depurando código impecable cuando el verdadero culpable es el propio entorno de desarrollo. En Web Forms, un simple "quitar y poner" sigue siendo un recurso de soporte técnico de primer nivel.