PagingTagHelper v1.0.0: Paginación lista para empresas para el núcleo moderno de ASP.NET (Español (Spanish))

PagingTagHelper v1.0.0: Paginación lista para empresas para el núcleo moderno de ASP.NET

Friday, 07 November 2025

//

23 minute read

NOTA: Llegando pronto, solo dándole los toques finales. ¡Seguid a GitHub! .

¡Esto es sólo para mostrarles todo lo que YO SOY haciendo progreso con este control! Valdrá la pena esperar.

Introducción

Después de meses de evolución y valiosos comentarios de la comunidad (5.7k+ descargas!), estoy emocionado de anunciar que la biblioteca PagingTagHelper ha llegado a la versión 1.0.0. Esto no es sólo un aumento de número de versión – representa una maduración completa de la biblioteca con características que lo hacen adecuado para aplicaciones de producción del mundo real.

Si has estado siguiendo esta serie, recordarás que empezamos con Búsqueda de huesos desnudos, añadido Cabeceras clasificables, y extraído controles de tamaño de página. La versión 1.0.0 toma todo lo que hemos aprendido y añade características empresariales críticas:

  • Continuación de la paginación token para bases de datos NoSQL (Cosmos DB, DynamoDB, Azure Table Storage)
  • Localización en varios idiomas soporta 8 idiomas fuera de la caja
  • Modos JavaScript flexibles de HTMX a cero-JavaScript
  • Vistas puras del viento de cola sin dependencias DaisyUI
  • Preservación del parámetro URL inteligente a lo largo de toda la navegación
  • HTMX 2.0.4 actualizar con compatibilidad hacia atrás

Vamos a sumergirnos en cada una de estas características y ver cómo trabajan juntos para crear una solución de paginación verdaderamente flexible.

NuGet NuGet

Continuación de la Paginación de Tokens â continuación de la Paginación de Tokens â continuación de la Paginación de Tokens â continuación de la Paginación de Tokens â continuación de la Paginación de Tokens â continuación de la Paginación de Tokens â continuación de la Paginación de Tokens â continuación de la Paginación de Tokens â continuación de la Paginación de Tokens â continuación de la Paginación de Tokens â continuación de la Paginación de Tokens â continuación de la Paginación de Tokens â continuación de la Paginación de Tokens â continuación de la Paginación de Tokens .

La paginación tradicional funciona muy bien con bases de datos SQL donde se puede fácilmente SKIP y TAKE ¿Pero qué sucede cuando trabajas con bases de datos NoSQL como Cosmos DB, DynamoDB o Azure Table Storage? Estas bases de datos no soportan la paginación basada en offset, sino que usan tokens de continuación.

Entender la Paginación Basada en Tokens, comprensión de la Paginación Basada en Tokens, comprensión de la Paginación Basada en Tokens, comprensión de la Paginación Basada en Tokens, comprensión de la Paginación Basada en Tokens, comprensión de la Paginación Basada en Tokens.

Así es como la paginación de continuación muestra difiere de la paginación tradicional:

graph TD
    A[Traditional Paging] --> B[Page 1: OFFSET 0 LIMIT 10]
    A --> C[Page 2: OFFSET 10 LIMIT 10]
    A --> D[Page 3: OFFSET 20 LIMIT 10]

    E[Token-Based Paging] --> F[Page 1: No token]
    F --> G[Returns: Data + Token_A]
    G --> H[Page 2: Token_A]
    H --> I[Returns: Data + Token_B]
    I --> J[Page 3: Token_B]

    style A stroke:#0ea5e9,stroke-width:3px
    style E stroke:#ec4899,stroke-width:3px

Paging tradicional:

  • Especifica exactamente qué registros recuperar (OFFSET/LIMIT)
  • Usted puede saltar a cualquier página directamente
  • La base de datos debe escanear todos los registros anteriores

Llamado basado en tokens:

  • La base de datos devuelve un token opaco que representa "dónde continuar"
  • El formato Token es específico de la base de datos y opaco para el cliente
  • La navegación hacia adelante es natural, la navegación hacia atrás requiere historia simbólica

Continuación de la aplicación del Pager

El nuevo <continuation-pager> tag helper hace que la implementación de la paginación basada en tokens sea sencilla. Primero, cree un modelo que implemente IContinuationPagingModel:

public class ProductPagingViewModel : IContinuationPagingModel
{
    public string? NextPageToken { get; set; }
    public bool HasMoreResults { get; set; }
    public int PageSize { get; set; } = 25;
    public int CurrentPage { get; set; } = 1;
    public Dictionary<int, string>? PageTokenHistory { get; set; }
    public ViewType ViewType { get; set; } = ViewType.TailwindAndDaisy;

    // Your actual data
    public List<Product> Products { get; set; } = new();
}

La interfaz es mínima pero potente. Echemos un vistazo a lo que hace cada propiedad:

  • NextPageToken: El token para recuperar la página siguiente (proporcionado por su base de datos)
  • HasMoreResults: Booleano indicando si hay más páginas
  • PageSize: Artículos por página
  • CurrentPage: Número de página de sólo visualización para la interfaz de usuario
  • PageTokenHistory: Dictionary mapping page numbers to tokens for back navigation
  • ViewType: Qué marco CSS usar para renderizar

Ahora vamos a implementar una acción controladora que simula la paginación al estilo Cosmos DB:

[Route("Products")]
public async Task<IActionResult> Products(
    int currentPage = 1,
    int pageSize = 25,
    string? pageToken = null,
    string? tokenHistory = null)
{
    // Simulate fetching from Cosmos DB
    var cosmosResults = await _cosmosService.GetProductsAsync(
        pageSize: pageSize,
        continuationToken: pageToken
    );

    // Deserialize token history for backward navigation
    var history = string.IsNullOrEmpty(tokenHistory)
        ? new Dictionary<int, string>()
        : JsonSerializer.Deserialize<Dictionary<int, string>>(tokenHistory)
          ?? new Dictionary<int, string>();

    // Store current token in history
    if (!string.IsNullOrEmpty(pageToken))
    {
        history[currentPage] = pageToken;
    }

    var viewModel = new ProductPagingViewModel
    {
        CurrentPage = currentPage,
        PageSize = pageSize,
        NextPageToken = cosmosResults.ContinuationToken,
        HasMoreResults = cosmosResults.HasMoreResults,
        PageTokenHistory = history,
        Products = cosmosResults.Items
    };

    if (Request.IsHtmx())
    {
        return PartialView("_ProductList", viewModel);
    }

    return View(viewModel);
}

Esta implementación muestra cómo el historial token permite la navegación hacia atrás. Sin él, la paginación token de continuación sólo soportaría botones "Next". Al mantener un diccionario de asignaciones de página a torre, podemos soportar la navegación "Anterior" y "Next".

Aquí está el flujo de acumulación simbólica visualizado:

sequenceDiagram
    participant User
    participant Controller
    participant Database
    participant TokenHistory

    User->>Controller: Request Page 1 (no token)
    Controller->>Database: Query with no token
    Database-->>Controller: Data + Token_A
    Controller->>TokenHistory: Store Token_A for page 1
    Controller-->>User: Display Page 1

    User->>Controller: Request Page 2 (Token_A)
    Controller->>Database: Query with Token_A
    Database-->>Controller: Data + Token_B
    Controller->>TokenHistory: Add Token_B for page 2
    Controller-->>User: Display Page 2

    User->>Controller: Request Page 1 (retrieve from history)
    Controller->>TokenHistory: Get Token for Page 1
    Controller->>Database: Query with Token_A
    Database-->>Controller: Data + Token_A
    Controller-->>User: Display Page 1

En su vista Razor, usar el buscador de continuación es sencillo:

@model ProductPagingViewModel

<div id="product-container">
    <table class="table">
        <thead>
            <tr>
                <th>Product</th>
                <th>Company</th>
                <th>Price</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var product in Model.Products)
            {
                <tr>
                    <td>@product.Name</td>
                    <td>@product.CompanyName</td>
                    <td>[email protected]("N2")</td>
                </tr>
            }
        </tbody>
    </table>

    <continuation-pager
        model="Model"
        htmx-target="#product-container"
        show-page-number="true"
        show-pagesize="true" />
</div>

El ayudante de etiqueta automáticamente:

  • Serializa el historial de tokens en parámetros de consulta
  • Construye URLs de navegación con tokens adecuados
  • Desactiva "Anterior" cuando está en la página 1
  • Desactiva "Siguiente" cuando HasMoreResults es falso
  • Preserva todos los demás parámetros de consulta (búsqueda, filtros, etc.)

Historial de tokens para navegación hacia atrás

El genio del enfoque de historia simbólica es que es totalmente opcional. Si sólo necesitas navegación "Next" (desplazamiento infinito, por ejemplo), puedes ignorar la historia simbólica por completo:

<continuation-pager
    model="Model"
    enable-token-accumulation="false"
    show-page-number="false" />

Esto representa sólo un botón "Next" sin indicadores de página o gestión del historial.

Para la navegación completa, el historial de tokens se seria automáticamente como JSON en la cadena de consulta. Así es como se ve una URL con el historial de tokens:

/Products?currentPage=3&pageSize=25&pageToken=abc123&tokenHistory=%7B%221%22%3A%22xyz789%22%2C%222%22%3A%22abc123%22%7D

Los tokenHistory parámetro contiene el diccionario codificado, haciendo navegación hacia atrás sin problemas.

Una de las mejoras de UX más importantes en el buscador de continuación es botones de página numerados. A medida que navega hacia adelante, el buscapersonas muestra los números de página de todas las páginas visitadas:

Initial page 1:    [Next →]
After next click:  [← Prev] [1] [2 active] [3 disabled] [Next →]
After next click:  [← Prev] [1] [2] [3 active] [4 disabled] [Next →]
Click page 2:      [← Prev] [1] [2 active] [3] [4 disabled] (no next - not visited yet)

Esto proporciona UX de paginación tradicional mientras mantiene la arquitectura de backend basada en tokens. La implementación almacena tokens para cada página visitada, permitiendo la navegación directa a cualquier página previamente visitada.

Limitando el crecimiento histórico:

Para evitar el uso de memoria sin límites, establecer max-history-pages (por defecto: 20):

<continuation-pager
    model="Model"
    max-history-pages="50"
    show-page-number="true" />

Cuando se alcanza el límite, los tokens de página más antiguos se recortan automáticamente.

Critical: Parámetro de consulta Preservación

Esta es la característica más importante de la implementación del buscador de continuación.

Los tokens de continuación solo son válidos con el mismo contexto de consulta (filtros, tipos, búsquedas) que los generó. Usar un token con diferentes parámetros de consulta devolverá datos incorrectos o fallará por completo.

El paginador de continuación conserva automáticamente TODOS los parámetros de consulta excepto los suyos:

<!-- URL with filters -->
/Products?category=electronics&brand=acme&minPrice=100

<!-- After clicking Next -->
/Products?category=electronics&brand=acme&minPrice=100&currentPage=2&pageToken=xyz123&tokenHistory={...}

<!-- All filters preserved! Token is valid because query context matches. -->

Puede deshabilitar este comportamiento si es necesario:

<continuation-pager
    model="Model"
    preserve-query-parameters="false" />

Pero esto es fuertemente desalentado a menos que estés absolutamente seguro de que tus fichas no dependen del contexto de consulta.

Por qué esto importa:

Ejemplo de Cosmos DB:

// Page 1 with filter
var query = container.GetItemQueryIterator<Product>(
    "SELECT * FROM c WHERE c.category = 'electronics'",
    continuationToken: null
);
var response = await query.ReadNextAsync();
// Returns: Products + Token_A

// Page 2 with SAME filter - Token_A is valid
var query2 = container.GetItemQueryIterator<Product>(
    "SELECT * FROM c WHERE c.category = 'electronics'",
    continuationToken: Token_A  // ✅ Works!
);

// Page 2 with DIFFERENT filter - Token_A is invalid
var query3 = container.GetItemQueryIterator<Product>(
    "SELECT * FROM c WHERE c.category = 'computers'",
    continuationToken: Token_A  // ❌ Wrong results or error!
);

La preservación automática de parámetros del buscador de continuación garantiza que los tokens se utilicen siempre con su contexto de consulta original.


Apoyo a la localización

Las aplicaciones modernas sirven a audiencias globales, y los controles de paginación necesitan hablar el idioma de sus usuarios.La versión 1.0.0 incluye un completo soporte de localización integrado en la biblioteca.

Idiomas incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes incorporados, lenguajes, lenguajes incorporados, lenguajes, lenguajes, lenguajes incorporados, lenguajes, lenguajes incorporados, lenguajes incorporados, lenguajes, lenguajes incorporados, lenguajes incorporados, lenguajes, lenguajes incorporados, lenguajes incorporados, lenguajes, lenguajes, lenguajes incorporados, lenguajes, lenguajes, lenguajes incorporados, lenguajes, lenguajes incorporados, lenguajes incorporados, lenguajes, lenguajes, lenguajes, lenguajes, lenguajes, lenguajes, lenguajes, lenguajes, lenguajes, lenguajes, lenguajes

La biblioteca envía con traducciones para 8 idiomas:

Código # # Idioma

|------|----------| | en Inglés (predeterminado) | de Alemán (Deutsch) | es Español Español | fr Francés (francés) Francés (francés) Francés (francés) Francés (francés) Francés (francés) Francés (francés) Francés (francés) Francés (francés) Francés (francés) | it Italiano (italiano) Italiano (italiano) Italiano (italiano) Italiano (italiano) Italiano (italiano) Italiano (italiano) Italiano (italiano) Italiano (italiano) Italiano (italiano) Italiano (italiano) Italiano (italiano) Italiano (italiano) Italiano (italiano) Italiano (italiano) Italiano (italiano) Italiano (italiano) | pt Portugués (Português) | ja japonés (日本語) | zh-Hans Chino simplificado ()

Todo el texto está localizado, incluyendo:

  • Previous/Next/Primer/Último botón
  • Texto resumido de la página ("Muestrando los elementos X a Y de Z")
  • Etiquetas ARIA para accesibilidad
  • Etiqueta de tamaño de página ("Temas por página")

El sistema de localización está impulsado por .resx archivos de recursos, por lo que es fácil añadir sus propios idiomas. Todos los archivos de recursos están en mostlylucid.pagingtaghelper/Resources/.

Uso de la localización

El uso de la localización es sencillo. language atributo:

<paging
    model="Model"
    language="de"
    show-summary="true"
    first-last-navigation="true" />

Esto representa todo el texto en alemán:

<!-- Previous button -->
<button>‹ Vorherige</button>

<!-- Summary -->
<div class="text-sm text-gray-600">
    Zeige 1 bis 10 von 256 Einträgen
</div>

<!-- Next button -->
<button>Nächste ›</button>

Para el cambio dinámico de idioma basado en las preferencias del usuario, establezca el idioma en su controlador:

public async Task<IActionResult> Products(
    int page = 1,
    int pageSize = 10,
    string language = "en")
{
    var pagingModel = await GenerateModel(page, pageSize);
    ViewBag.SelectedLanguage = language;
    return View(pagingModel);
}

Luego, en su vista, cree un selector de idioma:

@{
    var selectedLanguage = ViewBag.SelectedLanguage as string ?? "en";
    var languages = new Dictionary<string, string>
    {
        { "en", "English" },
        { "de", "German" },
        { "es", "Spanish" },
        { "fr", "French" },
        { "it", "Italian" },
        { "pt", "Portuguese" },
        { "ja", "Japanese" },
        { "zh-Hans", "Chinese" }
    };
}

<select onchange="window.location.href='/Products?language=' + this.value">
    @foreach (var lang in languages)
    {
        <option value="@lang.Key" selected="@(lang.Key == selectedLanguage)">
            @lang.Value
        </option>
    }
</select>

<paging
    model="Model"
    language="@selectedLanguage"
    link-url="/Products" />

También puede anular cadenas de texto individuales mientras se beneficia de la localización para otros elementos:

<paging
    model="Model"
    language="ja"
    previous-page-text="戻る"
    next-page-text="次へ"
    summary-template="全{TotalItems}件中 {StartItem}~{EndItem}件を表示" />

Los PagingLocalizer service maneja automáticamente el formato específico de cultura. Si pasas un código de idioma no válido, con gracia vuelve al inglés.

Para la integración HTMX, usted querrá preservar el lenguaje a través de las solicitudes:

<script>
    htmx.on('htmx:configRequest', function(event) {
        if (event.detail.path.includes('/Products')) {
            event.detail.parameters.language = '@selectedLanguage';
        }
    });
</script>

Esto garantiza que las actualizaciones de la vista parcial de HTMX mantengan el idioma seleccionado.


Modos JavaScript â € ¢ javascript-modes}

Una de las mejoras más significativas en v1.0.0 es la introducción de modos JavaScript flexibles. Anteriormente, tenía una opción booleana: use-htmx="true" o use-htmx="false"Ahora tienes cinco modos distintos, cada uno optimizado para diferentes escenarios.

Modos disponibles â € ¢modes disponibles}

Aquí está el desglose completo de los modos JavaScript:

graph TD
    A[JavaScript Modes] --> B[HTMX]
    A --> C[HTMXWithAlpine]
    A --> D[Alpine]
    A --> E[PlainJS]
    A --> F[NoJS]

    B --> B1[Uses HTMX for partial updates]
    B --> B2[hx-get, hx-target, hx-swap]

    C --> C1[HTMX + Alpine.js directives]
    C --> C2[Enhanced interactivity]

    D --> D1[Pure Alpine.js]
    D --> D2[x-data, @click handlers]

    E --> E1[Vanilla JavaScript]
    E --> E2[onclick handlers]

    F --> F1[Zero JavaScript]
    F --> F2[Standard anchor links & forms]

Veamos cada modo en acción:

1. Modo HTMX (Defecto)

<paging
    model="Model"
    js-mode="HTMX"
    htmx-target="#results-container" />

Renders:

<button hx-get="/Products?page=2" hx-target="#results-container" hx-swap="outerHTML">
    Next ›
</button>

Perfecto para actualizaciones dinámicas de página sin recargas de página completa. Este es el modo recomendado para las aplicaciones modernas ASP.NET Core.

2. HTMXWithAlpine Mode

<paging
    model="Model"
    js-mode="HTMXWithAlpine"
    htmx-target="#results-container" />

Renders:

<button
    x-data
    hx-get="/Products?page=2"
    hx-target="#results-container"
    hx-swap="outerHTML">
    Next ›
</button>

Combina HTMX para navegación con Alpine.js para interactividad adicional del lado del cliente. Úselo cuando necesite elementos de interfaz de usuario reactivos junto con la paginación (indicadores de carga, animaciones, validación del lado del cliente).

3. Modo alpino

<paging
    model="Model"
    js-mode="Alpine" />

Renders:

<button
    x-data
    @click="window.location.href = '/Products?page=2'">
    Next ›
</button>

Pura Alpine.js sin HTMX. Útil cuando ya estás usando Alpine.js pero no quieres dependencias de HTMX.

4. Modo PlainJS

<paging
    model="Model"
    js-mode="PlainJS" />

Renders:

<button onclick="window.location.href = '/Products?page=2'">
    Next ›
</button>

Sin dependencias de framework, solo JavaScript de vainilla. Este modo también incluye un ayudante para cambios en el tamaño de página:

@Html.PageSizeOnchangeSnippet()

Esto inyecta el JavaScript necesario para manejar los cambios desplegables del tamaño de la página sin HTMX.

5. Modo NoJS

<paging
    model="Model"
    js-mode="NoJS" />

Renders:

<!-- Navigation uses standard anchor links -->
<a href="/Products?page=2">Next ›</a>

<!-- Page size uses a form with submit button -->
<form method="get" action="/Products">
    <input type="hidden" name="page" value="1" />
    <select name="pageSize" onchange="this.form.submit()">
        <option value="10">10</option>
        <option value="25" selected>25</option>
        <option value="50">50</option>
    </select>
    <noscript>
        <button type="submit">Update</button>
    </noscript>
</form>

Cero JavaScript necesario. Perfecto para:

  • Requisitos de accesibilidad
  • Hipótesis de mejora progresiva
  • Entornos donde JavaScript está desactivado
  • SEO-páginas críticas donde quieres navegación fácil de rastrear

La belleza de este sistema es que todos los modos conservan los parámetros de consulta existentesSi estás filtrando por categoría, buscando o ordenando, la paginación mantiene tu estado automáticamente.

Migración de use-htmx

Para la compatibilidad hacia atrás, el viejo use-htmx atributo sigue funcionando:

<!-- Old syntax (still works) -->
<paging model="Model" use-htmx="true" />
<!-- Equivalent to js-mode="HTMX" -->

<paging model="Model" use-htmx="false" />
<!-- Equivalent to js-mode="PlainJS" -->

Sin embargo, recomiendo migrar a la nueva js-mode Atributo para la claridad:

<!-- New syntax (recommended) -->
<paging model="Model" js-mode="HTMX" />
<paging model="Model" js-mode="PlainJS" />

Mejoras del tipo de vista • Mejoras del tipo de vista}

La versión 1.0.0 introduce dos adiciones importantes de ViewType que abordan escenarios comunes del mundo real.

Viento puro de cola, viento puro de cola.

Anteriormente, si querías el estilo TailwindCSS, tienes el TailwindAndDaisy vista que utiliza componentes DaisyUI. Esto es genial si ya estás usando DaisyUI, pero ¿qué pasa si quieres puro Tailwind sin la dependencia DaisyUI?

Entrar ViewType.Tailwind:

<paging
    model="Model"
    view-type="Tailwind" />

Esto renderiza usando solo clases de utilidad estándar de Coilwind:

<div class="flex gap-2 items-center">
    <button class="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700">
        ‹ Previous
    </button>

    <div class="px-3 py-1 text-sm font-medium bg-gray-100 dark:bg-gray-700 dark:text-white rounded-md">
        Page 1
    </div>

    <button class="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700">
        Next ›
    </button>
</div>

No btn, badge, o join clases – sólo puro Tailwind. Esto le da un control completo sobre el estilo sin dependencias de la biblioteca de componentes.

Comparación:

VerType CSS Framework Biblioteca de componentes Caso de uso |----------|---------------|-------------------|----------| | TailwindAndDaisy TailwindCSS DaisyUI Proyectos que ya utilizan DaisyUI | Tailwind TailwindCSS Ninguno Proyectos Pure Tailwind | Bootstrap Bootstrap Bootstrap componentes Bootstrap proyectos | Plain Ninguno | NoJS CSS incrustado Ninguno Cero requisitos de JavaScript

NoJS Mode â € ¢nojs-mode}

Los NoJS ViewType combina cero JavaScript con el estilo CSS:

<paging
    model="Model"
    view-type="NoJS"
    show-pagesize="true" />

Diferencias clave de otros tipos de vista:

  1. Navegación utiliza enlaces de anclaje, no botones:
<a href="/Products?page=2" class="pager-button">Next ›</a>
  1. Selector de tamaño de página es un formulario:
<form method="get" action="/Products" class="page-size-form">
    <!-- Preserves all current query parameters as hidden inputs -->
    <input type="hidden" name="search" value="laptop" />
    <input type="hidden" name="category" value="electronics" />

    <!-- Reset to page 1 when changing page size -->
    <input type="hidden" name="page" value="1" />

    <label for="pageSize">Items per page:</label>
    <select name="pageSize" onchange="this.form.submit()">
        <option value="10">10</option>
        <option value="25" selected>25</option>
        <option value="50">50</option>
    </select>

    <!-- Button visible when JavaScript is disabled -->
    <noscript>
        <button type="submit" class="page-size-button">Update</button>
    </noscript>
</form>

Los onchange="this.form.submit()" proporciona comodidad cuando JavaScript está disponible, pero el <noscript> el botón asegura la funcionalidad completa cuando no lo es.


URL Parámetro Preservación «url-parameter-preservation»

Uno de los aspectos más frustrantes de las implementaciones de paginación es perder sus filtros, términos de búsqueda u orden de ordenación al navegar entre páginas. conservando automáticamente todos los parámetros de consulta excepto los propios parámetros del control de paginación.

Esta característica funciona idénticamente a través de tanto los buscapersonas regulares como los buscapersonas de continuación, y a través todos los modos JavaScript y ViewTypes.

Así es como funciona internamente:

string BuildQueryString(string? token, int page)
{
    var query = new Dictionary<string, string>();

    // Define continuation pager's own parameters that should be excluded from preservation
    var pagerParams = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
    {
        "pageSize", "currentPage", "pageToken", "tokenHistory"
    };

    // Add parameter prefix variants if using prefixed parameters
    if (!string.IsNullOrEmpty(Model.ParameterPrefix))
    {
        pagerParams.Add($"{Model.ParameterPrefix}_pageSize");
        pagerParams.Add($"{Model.ParameterPrefix}_currentPage");
        pagerParams.Add($"{Model.ParameterPrefix}_pageToken");
        pagerParams.Add($"{Model.ParameterPrefix}_tokenHistory");
    }

    // Preserve all existing query parameters (except pager's own) if enabled
    if (Model.PreserveQueryParameters)
    {
        foreach (var param in ViewContext.HttpContext.Request.Query)
        {
            if (!pagerParams.Contains(param.Key))
            {
                query[param.Key] = param.Value.ToString();
            }
        }
    }

    // Add continuation pager parameters (with prefix if specified)
    var pageSizeParam = Model.GetParameterName("pageSize");
    var currentPageParam = Model.GetParameterName("currentPage");
    var pageTokenParam = Model.GetParameterName("pageToken");
    var tokenHistoryParam = Model.GetParameterName("tokenHistory");

    query[pageSizeParam] = pageSize.ToString();
    query[currentPageParam] = page.ToString();

    if (!string.IsNullOrEmpty(token))
        query[pageTokenParam] = token;

    if (Model.EnableTokenAccumulation)
        query[tokenHistoryParam] = tokenHistoryJson;

    return string.Join("&", query.Select(kvp =>
        $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value)}"));
}

Este enfoque significa:

Escenario 1: Búsqueda + Paginación

Initial URL: /Products?search=laptop&category=electronics&page=1
Click Next: /Products?search=laptop&category=electronics&page=2
Change Page Size: /Products?search=laptop&category=electronics&page=1&pageSize=50

Escenario 2: Clasificación + Paginación

Initial URL: /Products?orderBy=price&descending=true&page=1
Click Page 3: /Products?orderBy=price&descending=true&page=3

Escenario 3: Página de continuación con filtros

Initial URL: /Products?category=electronics&brand=acme
Click Next: /Products?category=electronics&brand=acme&currentPage=2&pageToken=abc123&tokenHistory={...}

La misma preservación funciona en formularios (modo NoJS). Al renderizar el formulario de tamaño de página, la vista incluye automáticamente entradas ocultas para todos los parámetros que no son de paginación:

<form method="get" action="@linkUrl" class="page-size-form">
    @* Preserve all existing query parameters except pageSize and page-related ones *@
    @foreach (var param in ViewContext.HttpContext.Request.Query)
    {
        if (!new[] { "pageSize", "currentPage", "pageToken", "tokenHistory" }
            .Contains(param.Key, StringComparer.OrdinalIgnoreCase))
        {
            <input type="hidden" name="@param.Key" value="@param.Value" />
        }
    }

    @* Reset to page 1 when changing page size *@
    <input type="hidden" name="currentPage" value="1" />

    <select name="pageSize" onchange="this.form.submit()">
        <!-- options -->
    </select>
</form>

Esto funciona perfectamente a través de todos los modos JavaScript y todos los tipos de vista. Nunca tienes que administrar manualmente la propagación de la cadena de consulta.


Guía de migración

Actualizar desde versiones pre-1.0 es sencillo, pero hay algunos cambios de ruptura de los que ser conscientes.

Rompiendo cambios

1. use-htmx está obsoleta (pero todavía funciona)

Viejo:

<paging model="Model" use-htmx="true" />
<paging model="Model" use-htmx="false" />

Nuevo (recomendado):

<paging model="Model" js-mode="HTMX" />
<paging model="Model" js-mode="PlainJS" />

2. ViewType.TailwindAndDaisy ahora utiliza componentes DaisyUI completos

Si estaba usando ViewType.TailwindAndDaisy y quieren puro viento de cola sin DaisyUI:

Comportamiento antiguo (puro viento de cola):

<paging model="Model" view-type="TailwindAndDaisy" />

Nuevo (para conseguir un comportamiento antiguo):

<paging model="Model" view-type="Tailwind" />

Siga utilizando TailwindAndDaisy si está usando componentes DaisyUI:

<paging model="Model" view-type="TailwindAndDaisy" />
<!-- Uses btn, join, badge, select, etc. -->

3. HTMX actualizado a 2.0.4

Si está usando HTMX en otra parte de su aplicación, asegúrese de que sea compatible con HTMX 2.0.4. La mayor parte del código HTMX 1.x funciona sin cambios, pero revise el Guía de migración HTMX 2.0 para los estuches de borde.

Migración paso a paso

Paso 1: Actualizar el paquete NuGet

dotnet add package mostlylucid.pagingtaghelper --version 1.0.0

Paso 2: Revise su código existente

Busque su base de código para use-htmx atributos:

# PowerShell
Get-ChildItem -Recurse -Include *.cshtml | Select-String "use-htmx"

# Bash/Git Bash
grep -r "use-htmx" --include="*.cshtml" .

Paso 3: Actualizar a js-mode (recomendado)

Reemplazar use-htmx con js-mode:

- <paging model="Model" use-htmx="true" htmx-target="#results" />
+ <paging model="Model" js-mode="HTMX" htmx-target="#results" />

- <paging model="Model" use-htmx="false" />
+ <paging model="Model" js-mode="PlainJS" />

Paso 4: Revisar el uso de TailwindAndDaisy

Si usted no tiene DaisyUI instalado pero estaban utilizando TailwindAndDaisy:

- <paging model="Model" view-type="TailwindAndDaisy" />
+ <paging model="Model" view-type="Tailwind" />

Paso 5: Prueba a fondo

Ejecute su aplicación y prueba:

  • Navegación de páginas
  • Cambios en el tamaño de la página
  • Actualizaciones parciales de HTMX (si se utiliza HTMX)
  • Filtro/conservación de búsqueda
  • Capacidad de respuesta móvil

Nuevas características a adoptar

Una vez migrado, considere la adopción de estas nuevas características:

Localización:

<paging
    model="Model"
    language="@CultureInfo.CurrentUICulture.TwoLetterISOLanguageName" />

Página de continuación (si se utiliza NoSQL):

<continuation-pager
    model="Model"
    htmx-target="#results-container"
    show-page-number="true" />

Modo no JS (para accesibilidad):

<paging model="Model" js-mode="NoJS" />

Aplicación de demostración «demo-aplication»

La biblioteca incluye una completa aplicación de demostración que muestra todas las características. Puede ejecutarlo localmente o verlo en el sitio de demostración (en breve).

Ejecutando la Demo Localmente:

git clone https://github.com/scottgal/mostlylucid.pagingtaghelper.git
cd mostlylucid.pagingtaghelper/mostlylucid.pagingtaghelper.sample
dotnet run

Navega hasta https://localhost:5001 para explorar:

  1. Paginación básica con modelo - Búsqueda tradicional con paginación al estilo SQL
  2. Integración HTMX - Actualizaciones dinámicas de página sin recargas de página completa
  3. Buscar con HTMX - Búsqueda y paginación combinadas
  4. CSS simple - Ausencia de dependencias marco
  5. Viento de cola puro - TailwindCSS sin DaisyUI
  6. Sin JavaScript - Paginación cero-JS totalmente funcional
  7. Modos JavaScript - Los cinco modos JS demostraron lado a lado
  8. Ordenar página - Cabeceras clasificables con HTMX
  9. No ordenar página HTMX - Cabeceras clasificables con carga de página completa
  10. Tamaño de página con HTMX - Cambios dinámicos en el tamaño de la página
  11. Tamaño de página no HTMX - Tamaño de página con presentación de formulario
  12. Página de continuación - Paginación basada en tokens al estilo NoSQL
  13. Localización - Selector de idiomas con 8 idiomas

Cada demo incluye:

  • Código fuente de trabajo
  • Explicación de la técnica
  • Enlace a la aplicación de GitHub
  • Controles interactivos para experimentar

Conclusión

La versión 1.0.0 representa un hito importante para la biblioteca PagingTagHelper. Lo que comenzó como un simple requisito de trabajo se ha convertido en una solución de paginación completa y lista para la producción que maneja:

  • Paginación SQL tradicional con offset/límite
  • NoSQL continuación token pagination para Cosmos DB, DynamoDB, etc.
  • Localización en varios idiomas para el público mundial
  • Modos JavaScript flexibles de HTMX a cero-JavaScript
  • Múltiples marcos CSS de DaisyUI a puro viento de cola a ninguno
  • Preservación inteligente de parámetros a lo largo de toda la navegación
  • Soporte de accesibilidad completa con etiquetas ARIA y navegación por teclado

La biblioteca ha sido probada con 1.7k+ descargas y está lista para el uso de producción. Todas las pruebas de 106 unidades pasan, y la completa aplicación demo muestra patrones de uso en el mundo real.

¿Qué sigue?

Mejoras futuras que estoy considerando:

  • Apoyo adicional al marco CSS (Iniciativa de usuario del material, Bulma)
  • Más idiomas de localización (community contributions welcome!)
  • Componentes Blazor del lado del servidor
  • Mejora de la accesibilidad para grandes conjuntos de datos

Comenzando

Instalar a través de NuGet:

dotnet add package mostlylucid.pagingtaghelper --version 1.0.0

Echa un vistazo a la documentación:

¿Preguntas, comentarios o contribuciones? Abra un número en GitHub o contacte en Twitter @scottgal.

¡Feliz paginación!

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.