Etiqueta: .net

DBDesigner: Creando un diseñador de bases de datos con ayuda de IA

Introducción

¡Hola nuevamente! Esta vez quiero compartirles mi experiencia utilizando inteligencia artificial para resolver una necesidad muy específica que tenía en mi trabajo. Por lo general, cuando comienzo un proyecto, prefiero diseñar la base de datos de forma visual. Esto me permite tener una mejor perspectiva de su estructura y detectar inconsistencias con mayor facilidad que revisando directamente un script SQL.

Para esta tarea normalmente utilizo Visual Paradigm en su versión Community. Sin embargo, esta edición no permite exportar scripts SQL, por lo que es necesario adquirir una licencia de pago para habilitar esta funcionalidad. La verdad es que no estaba dispuesto a pagar los 349 USD que costaba la versión más económica que permitía hacerlo, especialmente porque sería una herramienta que utilizaría exclusivamente para mi trabajo.

Hace algún tiempo descubrí que Visual Paradigm permitía exportar los diagramas en formato XML, así que desarrollé una pequeña aplicación de consola en C# que leía ese archivo y generaba el script SQL. Sin embargo, el resultado no fue tan bueno como esperaba: tenía problemas para detectar las dependencias entre tablas, especialmente cuando existían referencias circulares. Aunque solo me ocurrió una vez, era suficiente para que no pudiera confiar completamente en la herramienta.

Hace unos días decidí que podía desarrollar mi propia aplicación. Primero comencé a buscar, con ayuda de ChatGPT, controles gratuitos para Windows Forms que pudieran servirme, pero ninguno terminó de convencerme. Como tampoco tenía experiencia desarrollando controles gráficos personalizados para WinForms, pensé: “Tal vez podría utilizar Codex para crear esos controles y encargarme yo del resto”. Ya había intentado desarrollar una aplicacion, así que tenía bastante claro qué funcionalidades necesitaba y cómo quería que funcionaran.

Funciones Principales

  • Diseñar las base de datos a partir de un diagrama
  • Controles graficos “arrastrables”
  • Soporte para multiples motores de bases de datos (SQL Server, MySQL, SQLite y Firebird)
  • Guardar y abrir los proyectos generados por la misma aplicacion
  • Generar el script SQL

Funciones Secundarias

  • Deshacer/Rehacer
  • Zoom
  • Mover los puntos de conexion de la relacion
  • Crear puntos en las relaciones para modificar la trayectoria de la flecha

Primera versión: C# y Windows Forms

Abrí Codex, le compartí algunas imágenes de referencia de Visual Paradigm y traté de ser lo más claro posible al describir el tipo de controles que necesitaba. El resultado me sorprendió bastante: en muy poco tiempo había conseguido lo que para mí era la parte más difícil del proyecto, los controles gráficos. Al ver que podía avanzar tan rápido, decidí continuar y terminé construyendo una aplicación prácticamente completa.

Debo confesar que la interfaz generada por Codex no era exactamente lo que tenía en mente. Por eso, tuve que intervenir directamente en el código utilizando Visual Studio, realizar algunos ajustes visuales y agregar iconos para conseguir un resultado más cercano a lo que buscaba.

DBDesigner.NET: diseñador de diagramas de bases de datos desarrollado en C#

Se me terminaron los tokens mientras intentaba solucionar algunos problemas y me quedé a medio camino. Aunque ya había conseguido algo que me gustaba, tuve que salir de casa un rato. Antes de hacerlo, volteé a ver una MacBook Air M1 que tengo y pensé: “Si fuera multiplataforma podria continuar desde la Macbook” y todavia no terminaba de pronunciar esas palabras cuando una vocecita me dijo “¿y si la hacemos multiplataforma?”, entonces pronto descargue Codex en la Macbook y cree otra cuenta jajaj.

Kotlin y JavaFX

El primer reto fue elegir la tecnología que utilizaría. Ya había probado Codex con Blazor Server y MudBlazor, pero el resultado no terminó de convencerme, así que descarté la posibilidad de desarrollar una versión web. Entonces recordé que anteriormente había trabajado un poco con Kotlin y pensé que JavaFX podría ser una buena alternativa para crear una aplicación de escritorio multiplataforma.

Una vez más, compartí con Codex capturas de Visual Paradigm y de la aplicación que ya había desarrollado en .NET. Le expliqué cómo quería que funcionara y, cuando menos me di cuenta, ya tenía una primera versión funcional. Aunque todavía presentaba varios detalles por corregir, al igual que la versión de Windows, ya tenía una base sobre la cual continuar trabajando.

DBDesigner multiplataforma ejecutándose en macOS con Kotlin y JavaFX

Llegado a este punto, decidí crear un repositorio en GitHub. Por un lado, quería mantener un respaldo del trabajo realizado y, por otro, necesitaba poder trabajar sobre el mismo proyecto tanto en mi MacBook como en mi computadora de escritorio.

Como ya tenía la experiencia de haber desarrollado la versión para .NET, pude avanzar mucho más rápido en esta segunda implementación. Esto me permitió concentrarme en corregir los problemas que iban surgiendo e incorporar algunas funcionalidades:

Funcionalidades de DBDesigner

  • Un explorador para localizar fácilmente tablas en diagramas grandes.
  • Una sección de configuración.
  • Atajos de teclado para las principales funciones.
  • Compatibilidad con tema oscuro.
  • Un logotipo y un icono creados a partir de la combinación de otros iconos con ayuda de ChatGPT.

Mejorando la interfaz de usuario

Y sí, aunque la interfaz que generó Codex era bastante mejor que la de la primera versión en .NET, todavía no terminaba de convencerme. Así que, una vez resueltos los principales problemas de funcionamiento, decidí dedicar tiempo a mejorar su apariencia hasta conseguir un diseño con el que me sintiera más cómodo.

Conclusión

Y así fue como terminé desarrollando DBDesigner, una herramienta que espero que me resulte útil en mi trabajo diario. A partir de ahora comenzaré a utilizarla en proyectos reales para comprobar su funcionamiento, detectar posibles problemas y descubrir qué otras funcionalidades podría necesitar.

Este es apenas el comienzo del proyecto, así que seguramente habrá nuevas mejoras y características conforme vaya utilizándolo.

Unit of Work con Dapper

Como mencioné en el post anterior, recientemente comencé a utilizar Dapper para algunos proyectos en mi trabajo y a causa de ellos tuve la necesidad de migrar algunas operaciones al otro sistema y para ello se deben sincronizar datos entre los dos sistemas.

Contexto

Estos datos que necesitaba sincronizar son listas pero existen algunas que dependen de otra lista, por ejemplo, si se tratara de un punto de venta es probable tengamos entidades como Caja y ésta dependería de Sucursal.

Bajo éste escenario el procedimiento a seguir es el siguiente:

  1. Se tienen dos sistemas a sincronizar (S1 y S2)
  2. Se creará una aplicación de consola que recabará las listas de S1 y las enviará a S2
  3. S2 contará con un WebApi para recibir las listas enviadas por S1

Bien ahora, desde el punto de vista de S2 tenemos los siguientes escenarios:

  1. El elemento recibido es nuevo y hay que registrarlo
  2. El elemento ya existe y se debe actualizar en S2

Desglosando el Problema

Como mencioné anteriormente, hay listas que dependen de otra y esto nos genera un caso “especial” ya que si se agregan elementos nuevos a una lista que tiene dependencias y estas mismas aun no se han registrado esto genera un conflicto en S2 al tratar de registrar el elemento recibido.

Ejemplo: Supongamos que en S1 existen las entidades Sucursal y Caja, se entiende que la caja pertenece a alguna sucursal, dicho en otras palabras, la caja depende de la sucursal, esto implica que para poder registrar una caja ya debe estar registrada la sucursal a la que pertenece, entonces: ¿Qué pasaría si recibimos una caja de una sucursal nueva y aun no se registra la sucursal?, obviamente esto es un error que se soluciona de dos formas:

  1. Asegurarnos de que la sincronización dé prioridad a las listas que tengan dependencias.
  2. Si el elemento recibido tiene una dependencia que no existe, la dependencia se registra “en blanco” y al recibir la lista de la que depende se actualizaría la dependencia “en blanco”.

Para éste caso donde la sincronización será automática no creo conveniente seguir el camino de la opción número 1 ya que si el sistema crece podríamos toparnos con referencias circulares (cosa que ya me ha pasado anteriormente) y esto provocaría otro problema, así que opté por seguir la opción número 2.

Patrón Unit of Work

Ahora que he decidido registrar las dependencias “en blanco” necesito hacer las inserciones de las dependencias en una transacción y para ello utilizaré el patrón UnitOfWork y para ello usare la siguiente clase:

C#
public class UnitOfWork : IDisposable
{
    public IDbConnection Connection { get; }
    public IDbTransaction Transaction { get; }

    public UnitOfWork()
    {
        Connection = Repository.GetConnection();
        Connection.Open();
        Transaction = Connection.BeginTransaction();
    }

    public void Commit() => Transaction.Commit();
    public void Rollback() => Transaction.Rollback();

    public void Dispose()
    {
        Transaction?.Dispose();
        Connection?.Dispose();
    }
}

Esta clase será la encargada crear la conexión a la base de datos y de hacer commit o rollback a la transacción.

A continuación les muestro como usar la clase UnitOfWork:

C#
public static Result<long?> InsertCaja(CajaRequest request)
{
    using (var unit = new UnitOfWork())
    {
        try
        {
            Result<long?> result = InsertCajaInterna(request, unit.Connection, unit.Transaction);
            unit.Commit();
            return result;
        }
        catch (Exception ex)
        {
            unit.Rollback();
            log.Error($"Message: {ex.Message} | InnerException: {ex.InnerException} | StackTrace: {ex.StackTrace}");
            return Result<long?>.Fail("Ocurrio un error al registrar la caja");
        }
    }
}

Como se puede ver creé una función “Interna” pasando como parámetros tanto la conexión como la transacción, de esta manera todas las operaciones de base de datos se harán en conjunto.

NOTA: Estoy utilizando la clase Result para manejar las respuestas de las operaciones, su código es el siguiente:

C#
public class Result
{
    protected Result() { }

    public bool Success { get; protected set; }
    public string Message { get; protected set; }

    public static Result Ok(string message = "") =>
        new Result { Success = true, Message = message };

    public static Result Fail(string message) =>
        new Result { Success = false, Message = message };
}

public class Result<T> : Result
{
    public T Data { get; set; } = default;

    public static Result<T> Ok(T data, string message = "")
        => new Result<T> { Success = true, Message = message, Data = data };

    public new static Result<T> Fail(string message)
        => new Result<T> { Success = false, Message = message, Data = default };
}

A continuación te muestro el código de la función InsertCajaInterna:

C#
private static Result<long?> InsertCajaInterna(CajaRequest request, IDbConnection con, IDbTransaction trx)
{
    long? sucursalId = null;

    // Se obtiene la sucursal
    sucursalId = con.ExecuteScalar<long?>(
        "SELECT SucursalId FROM Sucursal WHERE IdExterno = @IdExternoSucursal"
        , new { IdExternoSucursal = request.sucursalId }, trx);

    // Si no existe la sucursal se registra en blanco
    if (!sucursalId.HasValue)
    {
        Result<long?> resultSucursal = InsertSucursalInterna(new SucursalRequest
            { idExterno = request.sucursalId, IsEmptiInsert = true }, con, trx);

        sucursalId = resultSucursal.Data;

        if (!sucursalId.HasValue)
            return Result<long?>.Fail("No se pudo identificar la sucursal");
    }

    // Se valida si existe una caja con el mismo IdExterno
    Result<long?> cajaId = con.ExecuteScalar<long?>("SELECT CajaId FROM Caja WHERE IdExterno = @IdExterno",
        new { IdExterno = request.idExterno }, trx);

    if (!cajaId.HasValue)
    {
        // Se inserta la caja nueva
        cajaId = con.ExecuteScalar<long?>(
            "INSERT INTO Caja (IdExterno, NombreCaja, SucursalId) " +
            " VALUES (@IdExterno, UPPER(@NombreCaja), @SucursalId); " +
            "SELECT SCOPE_IDENTITY();"
            , new
            {
                IdExterno = request.idExterno,
                NombreCaja = request.nombreCaja,
                SucursalId = sucursalId.Value
            }, trx);
    }
    else
    {
        // Se actualiza la caja existente
        con.Execute(
            "UPDATE Caja SET NombreCaja = UPPER(@NombreCaja), SucursalId = @SucursalId " +
            "WHERE IdExterno = @IdExterno "
            , new
            {
                IdExterno = request.idExterno,
                NombreCaja = request.nombreCaja,
                SucursalId = sucursalId.Value
            }, trx);
    }

    log.Info($"CajaId: {cajaId} | Nombre: '{request.nombreCaja}' | SucursalId: {sucursalId} | IdExterno: {request.idExterno}");

    return Result<long?>.Ok(cajaId, "Caja registrada correctamente");
}

De esta manera he conseguido insertar la caja y su dependencia en una transacción para mantener la integridad de los datos.

¿Cuándo usar Unit of Work?

  1. Cuando se requieren múltiples operaciones dependientes entre sí.
  2. Procesos de negocio complejos.

Conclusión

Implementar el patrón Unit of Work con Dapper no es obligatorio, pero existen escenarios donde se requiere que múltiples operaciones se ejecuten en conjunto, y es aquí donde se vuelve una pieza clave para mantener la integridad de los datos.

Filtro tipo Excel para MudDataGrid (ExcelLikeFilterColumn)

De igual forma como vimos en el artículo pasado tuve la necesidad de crear un filtro booleano para el MudDataGrid, pues de la misma forma necesité un filtroque se comportara igual que los filtros de Excel: con una lista de valores únicos con checkboxes, para que el usuario pudiera filtrar por uno o varios elementos.

MudBlazor no trae esto de forma nativa, así que desarrollé mi propio componente Razor llamado ExcelLikeFilterColumn.

En este artículo te explico cómo funciona y cómo integrarlo fácilmente en tu proyecto.

¿Por qué hacer un filtro tipo Excel?

Cuando presentas datos tabulares ―por ejemplo, catálogos, direcciones, marcas, categorías, usuarios― los filtros tipo Excel son una bendición:

  • Muestran los valores distintos de la columna.
  • Permiten seleccionar varios valores a la vez.
  • Son intuitivos para cualquier usuario.
  • Se despliegan dentro de un popover con scroll.
  • Se integran perfectamente con MudDataGrid.

Así que decidí replicar ese comportamiento con MudBlazor.

¿Cómo funciona internamente?

Mi componente:

  • Recibe los items del grid mediante Items="context.Items".
  • Usa generics para el tipo de item y el tipo de valor (TItem, TValue).
  • Obtiene los valores únicos de la columna usando ValueSelector.
  • Construye un popover con un checkbox de “Todos” y un checkbox para cada valor distinto.
  • Permite personalizar la etiqueta a mostrar mediante LabelSelector.
  • Aplica el filtro usando FilterDefinition<TItem> tal como lo hace MudDataGrid internamente.

La experiencia final es prácticamente idéntica a un filtro de Excel.

[C#] ExcelFilterColumn.razor
@typeparam TItem
@typeparam TValue

<MudIconButton OnClick="@(() => _open = true)" Icon="@_icon" Size="Size.Small" />
<MudOverlay Visible="@_open" OnClick="@(() => _open = false)" />
<MudPopover Open="@_open" AnchorOrigin="Origin.BottomCenter" TransformOrigin="Origin.TopCenter" Class="px-4 py-2">
    <MudStack Spacing="0">
        <MudCheckBox T="bool" Label="Todos" Size="Size.Small"
                     Value="@_selectAll"
                     ValueChanged="@SelectAllChanged" />
        <MudStack Spacing="0" Style="overflow-y:auto;max-height:250px">
            @foreach (var val in _distinctValues)
            {
                <MudCheckBox T="bool" Label="@GetLabel(val)"
                             Value="@_selectedValues.Contains(val)"
                             ValueChanged="@(v => ValueChanged(v, val))"
                             Size="Size.Small" />
            }
        </MudStack>
        <MudStack Row="true">
            <MudButton OnClick="Clear">Limpiar</MudButton>
            <MudSpacer/>
            <MudButton Color="Color.Primary" OnClick="Apply">Aplicar</MudButton>
        </MudStack>
    </MudStack>
</MudPopover>

@code {
    [Parameter] public IEnumerable<TItem> Items { get; set; } = Enumerable.Empty<TItem>();
    [Parameter] public Func<TItem, TValue?> ValueSelector { get; set; } = default!;
    [Parameter] public Func<TItem, string> LabelSelector { get; set; } = default!;
    [Parameter] public FilterContext<TItem> Context { get; set; } = default!;

    private bool _open;
    private bool _selectAll = true;
    private string _icon = Icons.Material.Outlined.FilterAlt;

    private HashSet<TValue> _selectedValues = new();
    private List<TValue> _distinctValues = new();
    private FilterDefinition<TItem>? _filterDefinition;

    protected override void OnParametersSet()
    {
        _distinctValues = Items
            .Select(ValueSelector)
            .Where(v => v is not null)
            .Distinct()!
            .ToList();

        if (_selectAll)
            _selectedValues = _distinctValues.ToHashSet();

        // Crear el FilterDefinition una sola vez (o regenerarlo si cambian Items)
        _filterDefinition ??= new FilterDefinition<TItem>
        {
            FilterFunction = x =>
            {
                var val = ValueSelector(x);
                return val is not null && _selectedValues.Contains(val);
            }
        };
    }

    private string GetLabel(TValue val)
    {
        var item = Items.FirstOrDefault(x => EqualityComparer<TValue>.Default.Equals(ValueSelector(x)!, val));
        return item is null ? val?.ToString() ?? "" : LabelSelector(item);
    }

    private void SelectAllChanged(bool value)
    {
        _selectAll = value;
        if (value)
            _selectedValues = _distinctValues.ToHashSet();
        else
            _selectedValues.Clear();
    }

    private void ValueChanged(bool value, TValue val)
    {
        if (value)
            _selectedValues.Add(val);
        else
            _selectedValues.Remove(val);

        _selectAll = _selectedValues.Count == _distinctValues.Count;
    }

    private async Task Clear()
    {
        _selectedValues.Clear();
        _selectAll = false;
        _icon = Icons.Material.Outlined.FilterAlt;
        if (_filterDefinition is not null)
            await Context.Actions.ClearFilterAsync(_filterDefinition);
        _open = false;
    }

    private async Task Apply()
    {
        _icon = _selectedValues.Count == _distinctValues.Count
            ? Icons.Material.Outlined.FilterAlt
            : Icons.Material.Filled.FilterAlt;

        if (_filterDefinition is not null)
            await Context.Actions.ApplyFilterAsync(_filterDefinition);

        _open = false;
    }
}

Ejemplo de uso: ExcelLikeFilterColumn

Si tienes una propiedad como Marca, que tiene un ID y un nombre, puedes filtrar así:

[C#] Ejemplo de ExcelLikeFilterColumn
<PropertyColumn Property="x => x.MarcaNombre" Title="Marca">
    <FilterTemplate>
        <ExcelFilterColumn TItem="DireccionesViewModel" TValue="short"
                           Items="context.Items"
                           ValueSelector="x => x.MarcaId!.Value"
                           LabelSelector="x => x.MarcaNombre.ToUpperInvariant()"
                           Context="context" />
    </FilterTemplate>
</PropertyColumn>

Con esto:

  • ValueSelector define cuál es el valor real a filtrar.
  • LabelSelector define el texto que verá el usuario.
  • Los valores duplicados se agrupan automáticamente.
  • El filtro soporta múltiples selecciones.

Conclusión

Este filtro tipo Excel me ha sido ayuda para crear filtros con los que los usuarios están más familiarizados los usuarios, esto les brinda una manera familiar y potente de filtrar valores sin tener que escribir texto ni conocer criterios avanzados.

Creando un filtro booleano personalizado para MudDataGrid (BooleanFilterColumn)

En uno de mis proyectos recientes desarrollados con .NET 9 y MudBlazor, me encontré con la necesidad de mejorar los filtros que ofrece MudDataGrid. Si bien el componente ya trae una base sólida, para columnas booleanas quería algo más práctico y visual, algo similar a lo que vemos en sistemas administrativos profesionales.

Por eso construí un componente personalizado llamado BooleanFilterColumn, que me permite seleccionar rápidamente entre Todos, Activo o Inactivo, usando un pequeño popover con checkboxes.

En este artículo te cuento por qué lo hice, cómo funciona y cómo puedes utilizarlo tú también.

¿Por qué crear un filtro booleano personalizado?

MudBlazor permite aplicar filtros personalizados, pero para las columnas booleanas normalmente solo tenemos opciones binarias o expresiones. Yo quería algo más intuitivo:

  • Un botón de filtro con el icono que cambia según si hay filtros aplicados.
  • Un popover con checkboxes para seleccionar:
    • Todos
    • Activos
    • Inactivos
  • Aplicar o limpiar el filtro con un solo clic.
  • Soporte para personalizar textos (por ejemplo: “Activo”, “Inactivo”, “Todos”, etc.)

El resultado fue este componente Razor:

[C#] BooleanFilterColumn.razor
@typeparam T

<MudIconButton OnClick="@(() => _open = true)" Icon="@_icon" Size="Size.Small" />
<MudOverlay Visible="@_open" OnClick="@(() => _open = false)" />
<MudPopover Open="@_open" AnchorOrigin="Origin.BottomCenter" TransformOrigin="Origin.TopCenter" Class="px-4 py-2">
    <MudStack Spacing="0">
        <MudCheckBox T="bool" Label="@AllText" Size="Size.Small"
                     Value="@_selectAll"
                     ValueChanged="@SelectAllChanged" />
        <MudStack Spacing="0">
            <MudCheckBox T="bool" Label="@TrueText"
                         Value="@_selectedValues.Contains(true)"
                         ValueChanged="@(v => ValueChanged(v, true))"
                         Size="Size.Small" />
            <MudCheckBox T="bool" Label="@FalseText"
                         Value="@_selectedValues.Contains(false)"
                         ValueChanged="@(v => ValueChanged(v, false))"
                         Size="Size.Small" />
        </MudStack>
        <MudStack Row="true">
            <MudButton OnClick="Clear">@ClearButtonText</MudButton>
            <MudSpacer/>
            <MudButton Color="Color.Primary" OnClick="Apply">@ApplyButtonText</MudButton>
        </MudStack>
    </MudStack>
</MudPopover>

@code {
    [Parameter] public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
    [Parameter] public Func<T, bool?> ValueSelector { get; set; } = default!;
    [Parameter] public FilterContext<T> Context { get; set; } = default!;
    [Parameter] public string ApplyButtonText { get; set; } = "Apply";
    [Parameter] public string AllText { get; set; } = "All";
    [Parameter] public string ClearButtonText { get; set; } = "Clear";
    [Parameter] public string TrueText { get; set; } = "True";
    [Parameter] public string FalseText { get; set; } = "False";

    private bool _open;
    private bool _selectAll = true;
    private string _icon = Icons.Material.Outlined.FilterAlt;

    private HashSet<bool> _selectedValues = new();
    private FilterDefinition<T>? _filterDefinition;

    protected override void OnParametersSet()
    {
        if (_selectAll)
            _selectedValues = new HashSet<bool> { true, false };

        _filterDefinition ??= new FilterDefinition<T>
        {
            FilterFunction = x =>
            {
                var val = ValueSelector(x);
                return val is not null && _selectedValues.Contains(val.Value);
            }
        };
    }

    private void SelectAllChanged(bool value)
    {
        _selectAll = value;
        if (value)
            _selectedValues = new HashSet<bool> { true, false };
        else
            _selectedValues.Clear();
    }

    private void ValueChanged(bool value, bool val)
    {
        if (value)
            _selectedValues.Add(val);
        else
            _selectedValues.Remove(val);

        _selectAll = _selectedValues.Count == 2;
    }

    private async Task Clear()
    {
        _selectedValues.Clear();
        _selectAll = false;
        _icon = Icons.Material.Outlined.FilterAlt;
        if (_filterDefinition is not null)
            await Context.Actions.ClearFilterAsync(_filterDefinition);
        _open = false;
    }

    private async Task Apply()
    {
        _icon = _selectedValues.Count == 2
            ? Icons.Material.Outlined.FilterAlt
            : Icons.Material.Filled.FilterAlt;

        if (_filterDefinition is not null)
            await Context.Actions.ApplyFilterAsync(_filterDefinition);

        _open = false;
    }
}

¿Cómo funciona internamente?

  • Recibe una lista de items (Items) provenientes del contexto de filtrado de MudDataGrid.
  • Extrae los valores booleanos distintos (true/false) utilizando ValueSelector.
  • Muestra tres opciones:
    • Todos
    • TrueText
    • FalseText
  • Actualiza el ícono dependiendo del estado del filtro.
  • Construye un FilterDefinition<T> personalizado y lo aplica usando:
await Context.Actions.ApplyFilterAsync(_filterDefinition);

Además, todo vive dentro de un MudPopover, usando MudCheckBox para representar las opciones.

Ejemplo de uso: BooleanFilterColumn

Así es como lo estoy usando actualmente en una columna booleana (por ejemplo, “Activo”)

<PropertyColumn Property="x => x.Activo" Title="Activo">
    <CellTemplate>
        <MudCheckBox Value="@context.Item.Activo" ReadOnly="true"/>
    </CellTemplate>
    <FilterTemplate>
        <BooleanFilterColumn T="Rubro"
                             Items="context.Items"
                             ValueSelector="x => x.Activo"
                             Context="context"
                             AllText="Todos"
                             ApplyButtonText="Aplicar"
                             ClearButtonText="Limpiar"
                             TrueText="ACTIVO"
                             FalseText="INACTIVO" />
    </FilterTemplate>
</PropertyColumn>

Con eso, el usuario tiene un filtro booleano más claro y fácil de usar.

Conclusión

Este filtro ha sido un gran agregado a mi MudDataGrid, haciéndolo más amigable y funcional para columnas booleanas. Si también estás creando aplicaciones administrativas con MudBlazor, probablemente te sea muy útil tener un filtro tan visual y práctico.

Componentes para Blazor

Recientemente, en mi trabajo tomamos la decisión de rehacer un sistema desarrollado con ASP.NET MVC 5 debido a su alta acumulación de deuda técnica. Aunque gran parte de esa deuda se debía a que el desarrollo inicial estuvo orientado a resolver necesidades operativas inmediatas (lo cual fue una buena decisión en su momento), con el tiempo quedó claro que MVC no era la mejor opción para un sistema de ese tamaño.

Como parte del proyecto, se me encomendó buscar alternativas para la nueva versión del sistema. Inicialmente, pensé en usar WebForms para organizar mejor las vistas, pero descubrí que ya no es soportado por las versiones más recientes de .NET. Sin embargo, en mi investigación me topé con Blazor, una tecnología que no solo es moderna, sino que también permite construir componentes reutilizables y organizados, algo similar a lo que buscaba.

El reto con los Grids y Componentes

El sistema original utilizaba componentes de DevExpress y SyncFusion. Aunque DevExpress nos facilitaba un desarrollo ágil, no cumplía con un requerimiento clave: los grids no eran responsivos, algo crítico dado que el sistema debía ser accesible desde dispositivos móviles. Por ello, complementamos con los grids de SyncFusion, que sí ofrecían este soporte.

Con Blazor seleccionado como base para el desarrollo, la siguiente tarea fue buscar una librería de componentes que permitiera construir una aplicación web moderna y responsiva, sin sacrificar funcionalidad ni estética. Este artículo detalla las opciones que evalué y la elección final.

Librerías de componentes para Blazor

Durante mi análisis, encontré varias opciones interesantes. Aquí presento una lista de las principales librerías disponibles, junto con algunas notas relevantes:

• DevExpress (De pago): Conocido por su robustez y herramientas avanzadas, aunque con un costo elevado.

• Telerik (De pago): Amplia colección de componentes con soporte profesional.

• SyncFusion (De pago / Community): Excelente soporte para grids responsivos, además de una licencia gratuita para pequeñas empresas y desarrolladores individuales.

• SemanticUI (Gratuita): Diseño limpio y moderno, aunque requiere integración adicional para ciertos casos.

• FomaticUI (Gratuita): Un fork de SemanticUI con mejoras, como un componente DatePicker.

• jQWidgets (De pago): Basado en jQuery, ideal para quienes ya estén familiarizados con esa tecnología.

• Radzen (De pago / Community): Compatible con Blazor Server y WASM, con licencia gratuita limitada.

• MudBlazor (Gratuita): Ofrece componentes modernos y responsivos, con una curva de aprendizaje accesible.

• Blazorise (De pago / Community): Soporte para múltiples frameworks CSS, ideal para proyectos personalizados.

• FluentUI (Gratuita): Basado en los principios de diseño de Microsoft, ideal para aplicaciones con estilo Office.

MudBlazor – La Elección Final

Después de evaluar varias opciones, nos decidimos por MudBlazor. Esta librería destacó por su facilidad para replicar el layout del sistema anterior y por las características responsivas de su grid. Por defecto, el grid de MudBlazor ajusta su comportamiento en pantallas pequeñas, permitiendo un desplazamiento vertical en lugar de horizontal, justo lo que necesitábamos para mejorar la experiencia en dispositivos móviles.

StrongTypingException: The value for column ‘IsPrimaryKey’ in table ‘TableDetails’ is DBNull

En ocasiones me ha pasado que al querer actualizar o generar algún ADO.NET Entity Data Model en Visual Studio me ha aparecido este error:

Así que hoy voy a compartirles la forma en que soluciono este problema.

set global optimizer_switch='derived_merge=OFF';
  1. Cerrar Visual Studio
  2. Ejecutar la siguiente consulta de arriba en nuestra base de datos
  3. Abrir nuestro proyecto en Visual Studio
  4. Generar o actualizar nuestro modelo

Buscando en internet he visto que se pide que se reinicie el servicio de MySQL, en mi caso no ha sido necesario, antes al contrario, si lo hacia tenia que volver a ejecutar la consulta.

Cabe resaltar que lo he probado con MySQL 5.7.X en conjunto con EF6.