Introducción al Framework de WinForms para Sistemas de Permisos y Clase de Acceso a Datos Genérica

Introducción al Diseño del Framework

El desarrollo de aplicaciones Windows Forms (WinForms) a menudo requiere una estructura robusta para gestionar la seguridad y los permisso de los usuarios. Este artículo introduce los fundamentos de un framwork diseñado para este propósito, enfocándose inicialmente en la configuración de la base de datos y una capa de acceso a datos reutilizable, componentes esenciales para cualquier sistema de gestión de permisos.

Diseño y Creación de la Base de Datos

Para un sistema de permisos, la base de datos es el pilar central que almacena la información sobre usuarios, roles, menús y las relaciones entre ellos. A continuación, se detalla el esquema de la base de datos y el script SQL para su creación e inicialización.

Esquema de la Base de Datos

  • tbl_Usuarios: Almacena la información básica de los usuarios, incluyendo su ID, nombre de usuario y contraseña.
  • tbl_Roles: Define los diferentes roles dentro del sistema (ej. Administrador, Usuario Estándar), junto con una descripción y un indicador de si es un rol de administrador.
  • tbl_Menus: Contiene la estructura jerárquica de los menús de la aplicación, con detalles como el ID del menú, su nombre, el ID del menú padre, el nombre del formulario asociado y una posible tecla de acceso rápido.
  • tbl_UsuariosRoles: Establece la relación muchos a muchos entre usuarios y roles, permitiendo que un usuario tenga múltiples roles.
  • tbl_RolesMenus: Define los permisos, vinculando roles con menús específicos, lo que significa que los usuarios con un determinado rol tendrán acceso a los menús asociados.

Proceso de Creación e Inserción de Datos

El siguiente script SQL creará la base de datos SistemaPermisosDB y las tablas necesarias, además de poblar algunas tablas con datos iniciales para demostración. Ajuste las rutas de los archivos .mdf y .ldf según su entorno.


USE [master]
GO

-- Crear la base de datos
CREATE DATABASE SistemaPermisosDB
 ON  PRIMARY 
( NAME = 'SistemaPermisosDB', FILENAME = 'C:\SQL_DATA\SistemaPermisosDB.mdf' , SIZE = 5120KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB )
 LOG ON 
( NAME = 'SistemaPermisosDB_log', FILENAME = 'C:\SQL_DATA\SistemaPermisosDB_log.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
GO

ALTER DATABASE [SistemaPermisosDB] SET COMPATIBILITY_LEVEL = 110
GO

IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled'))
begin
EXEC [SistemaPermisosDB].[dbo].[sp_fulltext_database] @action = 'enable'
end
GO

ALTER DATABASE [SistemaPermisosDB] SET ANSI_NULL_DEFAULT OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET ANSI_NULLS OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET ANSI_PADDING OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET ANSI_WARNINGS OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET ARITHABORT OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET AUTO_CLOSE OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET AUTO_CREATE_STATISTICS ON 
GO
ALTER DATABASE [SistemaPermisosDB] SET AUTO_SHRINK OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET AUTO_UPDATE_STATISTICS ON 
GO
ALTER DATABASE [SistemaPermisosDB] SET CURSOR_CLOSE_ON_COMMIT OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET CURSOR_DEFAULT  GLOBAL 
GO
ALTER DATABASE [SistemaPermisosDB] SET CONCAT_NULL_YIELDS_NULL OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET NUMERIC_ROUNDABORT OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET QUOTED_IDENTIFIER OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET RECURSIVE_TRIGGERS OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET  DISABLE_BROKER 
GO
ALTER DATABASE [SistemaPermisosDB] SET AUTO_UPDATE_STATISTICS_ASYNC OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET DATE_CORRELATION_OPTIMIZATION OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET TRUSTWORTHY OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET ALLOW_SNAPSHOT_ISOLATION OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET PARAMETERIZATION SIMPLE 
GO
ALTER DATABASE [SistemaPermisosDB] SET READ_COMMITTED_SNAPSHOT OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET HONOR_BROKER_PRIORITY OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET RECOVERY FULL 
GO
ALTER DATABASE [SistemaPermisosDB] SET  MULTI_USER 
GO
ALTER DATABASE [SistemaPermisosDB] SET PAGE_VERIFY CHECKSUM  
GO
ALTER DATABASE [SistemaPermisosDB] SET DB_CHAINING OFF 
GO
ALTER DATABASE [SistemaPermisosDB] SET FILESTREAM( NON_TRANSACTED_ACCESS = OFF ) 
GO
ALTER DATABASE [SistemaPermisosDB] SET TARGET_RECOVERY_TIME = 0 SECONDS 
GO
EXEC sys.sp_db_vardecimal_storage_format N'SistemaPermisosDB', N'ON'
GO

USE [SistemaPermisosDB]
GO

-- Tabla de Menús
CREATE TABLE [dbo].[tbl_Menus](
	[IdMenu] [int] IDENTITY(1,1) NOT NULL,
	[NombreMenu] [nvarchar](50) NOT NULL,
	[IdPadre] [int] NOT NULL,
	[NombreFormulario] [varchar](500) NULL,
	[TeclaAcceso] [nchar](10) NULL,
 CONSTRAINT [PK_tbl_Menus] PRIMARY KEY CLUSTERED 
(
	[IdMenu] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

-- Tabla de Roles
CREATE TABLE [dbo].[tbl_Roles](
	[IdRol] [int] IDENTITY(1,1) NOT NULL,
	[NombreRol] [nvarchar](50) NULL,
	[Descripcion] [nvarchar](500) NULL,
	[EsAdmin] [bit] NOT NULL, -- Cambiado a bit para booleano
 CONSTRAINT [PK_tbl_Roles] PRIMARY KEY CLUSTERED 
(
	[IdRol] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

-- Tabla de Relación Roles-Menús
CREATE TABLE [dbo].[tbl_RolesMenus](
	[IdRolMenu] [int] IDENTITY(1,1) NOT NULL,
	[IdRol] [int] NOT NULL,
	[IdMenu] [int] NOT NULL,
 CONSTRAINT [PK_tbl_RolesMenus] PRIMARY KEY CLUSTERED 
(
	[IdRolMenu] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

-- Tabla de Usuarios
CREATE TABLE [dbo].[tbl_Usuarios](
	[IdUsuario] [int] IDENTITY(101,1) NOT NULL,
	[NombreUsuario] [varchar](20) NOT NULL,
	[Contrasena] [varchar](50) NOT NULL,
 CONSTRAINT [PK_tbl_Usuarios] PRIMARY KEY CLUSTERED 
(
	[IdUsuario] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

-- Tabla de Relación Usuarios-Roles
CREATE TABLE [dbo].[tbl_UsuariosRoles](
	[IdUsuarioRol] [int] IDENTITY(1,1) NOT NULL,
	[IdUsuario] [int] NOT NULL,
	[IdRol] [int] NOT NULL,
 CONSTRAINT [PK_tbl_UsuariosRoles] PRIMARY KEY CLUSTERED 
(
	[IdUsuarioRol] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

-- Insertar datos iniciales en tbl_Menus
SET IDENTITY_INSERT [dbo].[tbl_Menus] ON 
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (1, N'Administración del Sistema', 0, NULL, NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (2, N'Añadir Nuevo Menú', 1, N'sm.FrmAddMenuInfo', NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (3, N'Listado de Menús', 1, N'sm.FrmMenuList', NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (4, N'Listado de Roles', 1, N'sm.FrmRoleList', NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (5, N'Asignación de Permisos', 1, N'sm.FrmRight', NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (6, N'Listado de Usuarios', 1, N'sm.FrmUserList', NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (8, N'Añadir Usuario', 6, N'sm.FrmUserInfo', NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (10, N'Gestión de Calificaciones', 0, N'', N'Alt+S     ')
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (11, N'Listado de Calificaciones', 10, N'score.FrmScoreList', N'          ')
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (12, N'Detalle de Calificación', 10, N'score.FrmScoreInfo', N'          ')
SET IDENTITY_INSERT [dbo].[tbl_Menus] OFF

-- Insertar datos iniciales en tbl_Roles
SET IDENTITY_INSERT [dbo].[tbl_Roles] ON 
INSERT [dbo].[tbl_Roles] ([IdRol], [NombreRol], [Descripcion], [EsAdmin]) VALUES (1, N'Administrador Maestro', N'Posee todos los privilegios del sistema', 1)
INSERT [dbo].[tbl_Roles] ([IdRol], [NombreRol], [Descripcion], [EsAdmin]) VALUES (3, N'Administrador de Sistema', N'Responsable de la gestión de funciones del sistema.', 0)
INSERT [dbo].[tbl_Roles] ([IdRol], [NombreRol], [Descripcion], [EsAdmin]) VALUES (4, N'Administrador de Calificaciones', N'Responsable de la gestión de la información de calificaciones', 0)
SET IDENTITY_INSERT [dbo].[tbl_Roles] OFF

-- Insertar datos iniciales en tbl_RolesMenus
SET IDENTITY_INSERT [dbo].[tbl_RolesMenus] ON 
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (7, 3, 1)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (8, 3, 2)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (9, 3, 3)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (10, 3, 6)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (11, 3, 8)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (12, 4, 10)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (13, 4, 11)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (14, 4, 12)
SET IDENTITY_INSERT [dbo].[tbl_RolesMenus] OFF

-- Insertar datos iniciales en tbl_Usuarios
SET IDENTITY_INSERT [dbo].[tbl_Usuarios] ON 
INSERT [dbo].[tbl_Usuarios] ([IdUsuario], [NombreUsuario], [Contrasena]) VALUES (101, N'admin', N'admin')
INSERT [dbo].[tbl_Usuarios] ([IdUsuario], [NombreUsuario], [Contrasena]) VALUES (102, N'usuario001', N'123456')
SET IDENTITY_INSERT [dbo].[tbl_Usuarios] OFF

-- Insertar datos iniciales en tbl_UsuariosRoles
SET IDENTITY_INSERT [dbo].[tbl_UsuariosRoles] ON 
INSERT [dbo].[tbl_UsuariosRoles] ([IdUsuarioRol], [IdUsuario], [IdRol]) VALUES (1, 101, 1)
INSERT [dbo].[tbl_UsuariosRoles] ([IdUsuarioRol], [IdUsuario], [IdRol]) VALUES (3, 102, 3)
SET IDENTITY_INSERT [dbo].[tbl_UsuariosRoles] OFF

-- Definir restricciones y relaciones de clave externa
ALTER TABLE [dbo].[tbl_Roles] ADD  CONSTRAINT [DF_tbl_Roles_EsAdmin]  DEFAULT ((0)) FOR [EsAdmin]
GO

ALTER TABLE [dbo].[tbl_RolesMenus]  WITH CHECK ADD  CONSTRAINT [FK_tbl_RolesMenus_tbl_Roles] FOREIGN KEY([IdRol])
REFERENCES [dbo].[tbl_Roles] ([IdRol])
GO
ALTER TABLE [dbo].[tbl_RolesMenus] CHECK CONSTRAINT [FK_tbl_RolesMenus_tbl_Roles]
GO

ALTER TABLE [dbo].[tbl_UsuariosRoles]  WITH CHECK ADD  CONSTRAINT [FK_tbl_UsuariosRoles_tbl_Usuarios] FOREIGN KEY([IdUsuario])
REFERENCES [dbo].[tbl_Usuarios] ([IdUsuario])
GO
ALTER TABLE [dbo].[tbl_UsuariosRoles] CHECK CONSTRAINT [FK_tbl_UsuariosRoles_tbl_Usuarios]
GO

ALTER TABLE [dbo].[tbl_UsuariosRoles]  WITH CHECK ADD  CONSTRAINT [FK_tbl_UsuariosRoles_tbl_Roles] FOREIGN KEY([IdRol])
REFERENCES [dbo].[tbl_Roles] ([IdRol])
GO
ALTER TABLE [dbo].[tbl_UsuariosRoles] CHECK CONSTRAINT [FK_tbl_UsuariosRoles_tbl_Roles]
GO

USE [master]
GO
ALTER DATABASE [SistemaPermisosDB] SET  READ_WRITE 
GO

Capa de Acceso a Datos

Una capa de acceso a datos (DAL) es crucial para desacoplar la lógica de negocio de los detalles de la base de datos. Se presenta una clase genérica para la interacción con SQL Server, facilitando operaciones CRUD y transacciones.

Clase de Acceso a Datos Genérica

La clase AccesoDatosBD es una utilidad estática que encapsula las operaciones comunes de la base de datos, como ejecutar comandos SQL o procedimientos almacenados, y recuperar datos.


using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Proyecto.DAL.Utilidades
{
    /// <summary>
    /// Clase estática de utilidad para el acceso a datos de SQL Server.
    /// </summary>
    public static class AccesoDatosBD
    {
        /// <summary>
        /// Cadena de conexión a la base de datos.
        /// </summary>
        private static readonly string cadenaConexion = ConfigurationManager.ConnectionStrings["cadenaConexionDB"].ConnectionString;

        /// <summary>
        /// Ejecuta una consulta SQL o procedimiento almacenado que no devuelve resultados (INSERT, UPDATE, DELETE).
        /// Devuelve el número de filas afectadas.
        /// </summary>
        /// <param name="consultaSql">Sentencia SQL o nombre del procedimiento almacenado.</param>
        /// <param name="tipoComando">Tipo de comando (1: Texto, 2: Procedimiento Almacenado).</param>
        /// <param name="parametros">Array de parámetros SQL.</param>
        /// <returns>Número de filas afectadas.</returns>
        public static int EjecutarComandoNoConsulta(string consultaSql, int tipoComando, params SqlParameter[] parametros)
        {
            int filasAfectadas = 0;
            using (SqlConnection conexion = new SqlConnection(cadenaConexion))
            {
                SqlCommand comando = ConstruirComandoSql(conexion, consultaSql, tipoComando, null, parametros);
                filasAfectadas = comando.ExecuteNonQuery();
                comando.Parameters.Clear();
            }
            return filasAfectadas;
        }

        /// <summary>
        /// Ejecuta una consulta SQL y devuelve el valor de la primera columna de la primera fila.
        /// Útil para obtener valores únicos o agregados (ej. COUNT, MAX).
        /// </summary>
        /// <param name="consultaSql">Sentencia SQL o nombre del procedimiento almacenado.</param>
        /// <param name="tipoComando">Tipo de comando (1: Texto, 2: Procedimiento Almacenado).</param>
        /// <param name="parametros">Array de parámetros SQL.</param>
        /// <returns>El valor de la primera columna de la primera fila, o null si no hay resultados.</returns>
        public static object EjecutarEscalar(string consultaSql, int tipoComando, params SqlParameter[] parametros)
        {
            object resultado = null;
            using (SqlConnection conexion = new SqlConnection(cadenaConexion))
            {
                SqlCommand comando = ConstruirComandoSql(conexion, consultaSql, tipoComando, null, parametros);
                resultado = comando.ExecuteScalar();
                comando.Parameters.Clear();
                return (resultado == null || resultado == DBNull.Value) ? null : resultado;
            }
        }

        /// <summary>
        /// Ejecuta una consulta SQL y devuelve un SqlDataReader para leer los resultados.
        /// La conexión se cerrará automáticamente cuando se cierre el lector.
        /// </summary>
        /// <param name="consultaSql">Sentencia SQL o nombre del procedimiento almacenado.</param>
        /// <param name="tipoComando">Tipo de comando (1: Texto, 2: Procedimiento Almacenado).</param>
        /// <param name="parametros">Array de parámetros SQL.</param>
        /// <returns>Un objeto SqlDataReader.</returns>
        /// <exception cref="Exception">Lanzada si ocurre un error al crear el lector.</exception>
        public static SqlDataReader EjecutarLector(string consultaSql, int tipoComando, params SqlParameter[] parametros)
        {
            SqlConnection conexion = new SqlConnection(cadenaConexion);
            SqlCommand comando = ConstruirComandoSql(conexion, consultaSql, tipoComando, null, parametros);
            try
            {
                return comando.ExecuteReader(CommandBehavior.CloseConnection);
            }
            catch (Exception ex)
            {
                conexion.Close();
                throw new Exception("Error al obtener SqlDataReader.", ex);
            }
        }

        /// <summary>
        /// Ejecuta una consulta SQL y rellena un DataTable con los resultados.
        /// Ideal para recuperar datos de una única tabla.
        /// </summary>
        /// <param name="consultaSql">Sentencia SQL o nombre del procedimiento almacenado.</param>
        /// <param name="tipoComando">Tipo de comando (1: Texto, 2: Procedimiento Almacenado).</param>
        /// <param name="parametros">Array de parámetros SQL.</param>
        /// <returns>Un DataTable con los datos resultantes.</returns>
        public static DataTable ObtenerTablaDatos(string consultaSql, int tipoComando, params SqlParameter[] parametros)
        {
            DataTable tablaDatos = new DataTable();
            using (SqlConnection conexion = new SqlConnection(cadenaConexion))
            {
                SqlCommand comando = ConstruirComandoSql(conexion, consultaSql, tipoComando, null, parametros);
                using (SqlDataAdapter adaptador = new SqlDataAdapter(comando))
                {
                    adaptador.Fill(tablaDatos);
                }
            }
            return tablaDatos;
        }

        /// <summary>
        /// Ejecuta una consulta SQL y rellena un DataSet con los resultados.
        /// </summary>
        /// <param name="consultaSql">Sentencia SQL o nombre del procedimiento almacenado.</param>
        /// <param name="tipoComando">Tipo de comando (1: Texto, 2: Procedimiento Almacenado).</param>
        /// <param name="parametros">Array de parámetros SQL.</param>
        /// <returns>Un DataSet con los datos resultantes.</returns>
        public static DataSet ObtenerConjuntoDatos(string consultaSql, int tipoComando, params SqlParameter[] parametros)
        {
            DataSet conjuntoDatos = new DataSet();
            using (SqlConnection conexion = new SqlConnection(cadenaConexion))
            {
                SqlCommand comando = ConstruirComandoSql(conexion, consultaSql, tipoComando, null, parametros);
                using (SqlDataAdapter adaptador = new SqlDataAdapter(comando))
                {
                    adaptador.Fill(conjuntoDatos);
                }
            }
            return conjuntoDatos;
        }

        /// <summary>
        /// Ejecuta una lista de sentencias SQL dentro de una transacción.
        /// </summary>
        /// <param name="listaSql">Lista de cadenas SQL a ejecutar.</param>
        /// <returns>True si la transacción se completó exitosamente, false en caso contrario.</returns>
        /// <exception cref="Exception">Lanzada si la transacción falla y se revierte.</exception>
        public static bool EjecutarTransaccion(List<string> listaSql)
        {
            using (SqlConnection conexion = new SqlConnection(cadenaConexion))
            {
                conexion.Open();
                SqlTransaction transaccion = conexion.BeginTransaction();
                SqlCommand comando = ConstruirComandoSql(conexion, "", (int)CommandType.Text, transaccion);
                try
                {
                    foreach (string sqlItem in listaSql)
                    {
                        if (!string.IsNullOrWhiteSpace(sqlItem))
                        {
                            comando.CommandText = sqlItem;
                            comando.ExecuteNonQuery();
                        }
                    }
                    transaccion.Commit();
                    return true;
                }
                catch (Exception ex)
                {
                    transaccion.Rollback();
                    throw new Exception("Error al ejecutar la transacción de comandos SQL.", ex);
                }
            }
        }

        /// <summary>
        /// Ejecuta una lista de objetos CommandInfo dentro de una transacción.
        /// Permite ejecutar diferentes tipos de comandos (SQL o procedimientos) con sus respectivos parámetros.
        /// </summary>
        /// <param name="listaComandos">Lista de objetos CommandInfo.</param>
        /// <returns>True si la transacción se completó exitosamente, false en caso contrario.</returns>
        /// <exception cref="Exception">Lanzada si la transacción falla y se revierte.</exception>
        public static bool EjecutarTransaccionComandos(List<InformacionComando> listaComandos)
        {
            using (SqlConnection conexion = new SqlConnection(cadenaConexion))
            {
                conexion.Open();
                SqlTransaction transaccion = conexion.BeginTransaction();
                SqlCommand comando = ConstruirComandoSql(conexion, "", (int)CommandType.Text, transaccion); // Inicialmente como texto
                try
                {
                    foreach (InformacionComando infoComando in listaComandos)
                    {
                        comando.CommandText = infoComando.TextoComando;
                        comando.CommandType = infoComando.EsProcedimientoAlmacenado ? CommandType.StoredProcedure : CommandType.Text;

                        comando.Parameters.Clear(); // Limpiar parámetros para cada comando
                        if (infoComando.Parametros != null && infoComando.Parametros.Length > 0)
                        {
                            comando.Parameters.AddRange(infoComando.Parametros);
                        }
                        comando.ExecuteNonQuery();
                    }
                    transaccion.Commit();
                    return true;
                }
                catch (Exception ex)
                {
                    transaccion.Rollback();
                    throw new Exception("Error al ejecutar la transacción con objetos CommandInfo.", ex);
                }
            }
        }

        /// <summary>
        /// Ejecuta una operación personalizada dentro de una transacción, usando un delegado Func.
        /// Permite una mayor flexibilidad para operaciones complejas que requieren múltiples interacciones con la base de datos.
        /// </summary>
        /// <typeparam name="T">Tipo de retorno del delegado Func.</typeparam>
        /// <param name="accion">Delegado Func que encapsula la lógica de la transacción.</param>
        /// <returns>El resultado de la operación definida en el delegado.</returns>
        public static T EjecutarTransaccionPersonalizada<T>(Func<IDbCommand, T> accion)
        {
            using (IDbConnection conexion = new SqlConnection(cadenaConexion))
            {
                conexion.Open();
                IDbTransaction transaccion = conexion.BeginTransaction();
                IDbCommand comando = conexion.CreateCommand();
                comando.Transaction = transaccion;
                
                try
                {
                    T resultado = accion(comando);
                    transaccion.Commit();
                    return resultado;
                }
                catch (Exception ex)
                {
                    transaccion.Rollback();
                    throw new Exception("Error al ejecutar la transacción personalizada.", ex);
                }
            }
        }

        /// <summary>
        /// Método auxiliar para construir un objeto SqlCommand.
        /// </summary>
        /// <param name="conexion">Objeto de conexión SQL.</param>
        /// <param name="consultaSql">Sentencia SQL o nombre del procedimiento almacenado.</param>
        /// <param name="tipoComando">Tipo de comando (1: Texto, 2: Procedimiento Almacenado).</param>
        /// <param name="transaccion">Objeto de transacción SQL (opcional).</param>
        /// <param name="parametros">Array de parámetros SQL (opcional).</param>
        /// <returns>Un objeto SqlCommand configurado.</returns>
        private static SqlCommand ConstruirComandoSql(SqlConnection conexion, string consultaSql, int tipoComando, SqlTransaction transaccion, params SqlParameter[] parametros)
        {
            if (conexion == null) throw new ArgumentNullException(nameof(conexion), "El objeto de conexión no puede ser nulo.");
            SqlCommand comando = new SqlCommand(consultaSql, conexion);
            comando.CommandType = (tipoComando == 2) ? CommandType.StoredProcedure : CommandType.Text;

            if (conexion.State == ConnectionState.Closed)
            {
                conexion.Open();
            }

            if (transaccion != null)
            {
                comando.Transaction = transaccion;
            }

            if (parametros != null && parametros.Length > 0)
            {
                comando.Parameters.AddRange(parametros);
            }
            return comando;
        }
    }
}

Clase de Información de Comando para Transacciones

La clase InformacionComando se utiliza para encapsular los detalles de un comando SQL o procedimiento almacenado que forma parte de una transacción, permitiendo un manejo más estructurado de operaciones por lotes.


using System.Data.Common; // Para DbParameter

namespace Proyecto.DAL.Utilidades
{
    /// <summary>
    /// Encapsula la información de un comando SQL o procedimiento almacenado.
    /// Útil para operaciones transaccionales por lotes.
    /// </summary>
    public class InformacionComando
    {
        public string TextoComando { get; set; } // Sentencia SQL o nombre del procedimiento
        public DbParameter[] Parametros { get; set; } // Lista de parámetros para el comando
        public bool EsProcedimientoAlmacenado { get; set; } // Indica si es un procedimiento almacenado

        public InformacionComando() { }

        public InformacionComando(string textoComando, bool esProcedimiento)
        {
            TextoComando = textoComando;
            EsProcedimientoAlmacenado = esProcedimiento;
        }

        public InformacionComando(string textoComando, bool esProcedimiento, DbParameter[] parametros)
        {
            TextoComando = textoComando;
            EsProcedimientoAlmacenado = esProcedimiento;
            Parametros = parametros;
        }
    }
}

Configuración de la Cadena de Conexión

La cadena de conexión a la base de datos se almacena en el archivo de configuración de la aplicación (App.config o Web.config).


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name ="cadenaConexionDB" connectionString ="server=.;database=SistemaPermisosDB;uid=sa;pwd=123;" providerName="System.Data.SqlClient"/>
  </connectionStrings>
</configuration>

Clases de Utilidad Comunes

Un conjunto de clases de utilidad puede simplificar tareas repetitivas. A continuación, se presenta una clase estática para el manejo de cadenas y conversiones de tipo.

Utilidades de Cadena y Conversión

La clase UtilidadesCadena proporciona métodos de extensión para la conversión segura de tipos y manipulación de cadenas, mejorando la robustez de la aplicación.


using System;
using System.Collections.Generic;
using System.Text;
using System.Linq; // Necesario para algunas operaciones de LINQ si se usaran en el futuro

namespace Proyecto.Comun
{
    /// <summary>
    /// Clase estática con métodos de extensión para utilidades de cadena y conversión de tipos.
    /// </summary>
    public static class UtilidadesCadena
    {
        /// <summary>
        /// Convierte una cadena a un entero de forma segura.
        /// Devuelve 0 si la conversión falla.
        /// </summary>
        /// <param name="valorCadena">Cadena a convertir.</param>
        /// <returns>Valor entero o 0.</returns>
        public static int AEntero(this string valorCadena)
        {
            int resultado = 0;
            int.TryParse(valorCadena, out resultado);
            return resultado;
        }

        /// <summary>
        /// Convierte una cadena a un decimal de forma segura.
        /// Devuelve 0 si la conversión falla.
        /// </summary>
        /// <param name="valorCadena">Cadena a convertir.</param>
        /// <returns>Valor decimal o 0.</returns>
        public static decimal ADecimal(this string valorCadena)
        {
            decimal resultado = 0;
            decimal.TryParse(valorCadena, out resultado);
            return resultado;
        }

        /// <summary>
        /// Convierte un objeto a un entero de forma segura.
        /// Devuelve 0 si la conversión falla.
        /// </summary>
        /// <param name="valorObjeto">Objeto a convertir.</param>
        /// <returns>Valor entero o 0.</returns>
        public static int AEntero(this object valorObjeto)
        {
            int resultado = 0;
            try
            {
                resultado = Convert.ToInt32(valorObjeto);
            }
            catch
            {
                resultado = 0; // En caso de error de conversión, se devuelve 0.
            }
            return resultado;
        }

        /// <summary>
        /// Divide una cadena por un delimitador y devuelve una lista de cadenas.
        /// </summary>
        /// <param name="cadenaFuente">La cadena original.</param>
        /// <param name="delimitador">El carácter delimitador.</param>
        /// <param name="aMinusculas">Indica si las cadenas resultantes deben convertirse a minúsculas.</param>
        /// <returns>Una lista de cadenas.</returns>
        public static List<string> ObtenerListaCadenas(this string cadenaFuente, char delimitador, bool aMinusculas)
        {
            List<string> lista = new List<string&gt();
            string[] partes = cadenaFuente.Split(delimitador);
            foreach (string parte in partes)
            {
                if (!string.IsNullOrWhiteSpace(parte) && parte != delimitador.ToString())
                {
                    string valor = parte;
                    if (aMinusculas)
                    {
                        valor = parte.ToLower();
                    }
                    lista.Add(valor);
                }
            }
            return lista;
        }

        /// <summary>
        /// Divide una cadena por comas y devuelve un array de cadenas.
        /// </summary>
        /// <param name="cadenaFuente">La cadena original.</param>
        /// <returns>Un array de cadenas.</returns>
        public static string[] ObtenerArrayCadenas(this string cadenaFuente)
        {
            return cadenaFuente.Split(new Char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
        }

        /// <summary>
        /// Concatena una lista de cadenas usando un delimitador específico.
        /// </summary>
        /// <param name="listaCadenas">La lista de cadenas.</param>
        /// <param name="delimitador">El delimitador a usar.</param>
        /// <returns>Una única cadena unida.</returns>
        public static string UnirListaCadenas(this List<string> listaCadenas, string delimitador)
        {
            return string.Join(delimitador, listaCadenas);
        }

        /// <summary>
        /// Concatena una lista de enteros usando una coma como delimitador.
        /// </summary&        /// <param name="listaEnteros">La lista de enteros.</param>
        /// <returns>Una cadena de enteros separados por comas.</returns>
        public static string UnirListaEnteros(this List<int> listaEnteros)
        {
            return string.Join(",", listaEnteros);
        }

        /// <summary>
        /// Obtiene una cadena de valores de un diccionario (int, int) separados por comas.
        /// </summary>
        /// <param name="diccionario">El diccionario de enteros.</param>
        /// <returns>Una cadena con los valores del diccionario.</returns>
        public static string ObtenerValoresDiccionario(this Dictionary<int, int> diccionario)
        {
            if (diccionario == null || !diccionario.Any())
            {
                return string.Empty;
            }
            return string.Join(",", diccionario.Values);
        }

        /// <summary>
        /// Elimina la última coma de una cadena, si existe.
        /// </summary>
        /// <param name="cadena">La cadena a procesar.</param>
        /// <returns>La cadena sin la última coma.</returns>
        public static string EliminarUltimaComa(this string cadena)
        {
            if (string.IsNullOrEmpty(cadena) || !cadena.Contains(',')) return cadena;
            return cadena.TrimEnd(',',' '); // Mejor usar TrimEnd para espacios y comas.
        }

        /// <summary>
        /// Elimina el último carácter específico de una cadena, si existe.
        /// </summary>
        /// <param name="cadena">La cadena a procesar.</param>
        /// <param name="caracter">El carácter a eliminar del final.</param>
        /// <returns>La cadena sin el último carácter especificado.</returns>
        public static string EliminarUltimoCaracterEspecifico(this string cadena, string caracter)
        {
            if (string.IsNullOrEmpty(cadena) || !cadena.EndsWith(caracter)) return cadena;
            return cadena.Substring(0, cadena.LastIndexOf(caracter));
        }

        /// <summary>
        /// Divide una cadena por un delimitador y devuelve una lista de subcadenas únicas.
        /// Elimina duplicados y cadenas vacías.
        /// </summary>
        /// <param name="cadenaFuente">La cadena original.</param>
        /// <param name="delimitador">El carácter delimitador.</param>
        /// <returns>Una lista de subcadenas únicas.</returns>
        public static List<string> ObtenerSubcadenasUnicas(string cadenaFuente, char delimitador)
        {
            return cadenaFuente.Split(delimitador, StringSplitOptions.RemoveEmptyEntries)
                               .Where(s => !string.IsNullOrWhiteSpace(s))
                               .Select(s => s.Trim())
                               .Distinct()
                               .ToList();
        }

        /// <summary>
        /// Calcula la longitud de una cadena, tratando los caracteres multi-byte (ej. caracteres chinos) como dos unidades.
        /// </summary>
        /// <param name="cadena">La cadena de entrada.</param>
        /// <returns>La longitud calculada.</returns>
        public static int LongitudCadena(string cadena)
        {
            Encoding ascii = Encoding.ASCII;
            int longitudCalculada = 0;
            byte[] bytes = ascii.GetBytes(cadena);
            foreach (byte b in bytes)
            {
                if (b == 63) // 63 es el código ASCII para '?', que a menudo representa un caracter no ASCII.
                    longitudCalculada += 2;
                else
                    longitudCalculada += 1;
            }
            return longitudCalculada;
        }

        /// <summary>
        /// Recorta una cadena a una longitud máxima especificada.
        /// Añade puntos suspensivos (...) si la cadena original es más larga.
        /// </summary>
        /// <param name="cadena">La cadena a recortar.</param>
        /// <param name="longitudMaxima">La longitud máxima deseada.</param>
        /// <returns>La cadena recortada.</returns>
        public static string RecortarCadena(string cadena, int longitudMaxima)
        {
            if (string.IsNullOrEmpty(cadena) || cadena.Length <= longitudMaxima)
            {
                return cadena;
            }
            
            // Ajustar longitud máxima para caracteres multi-byte si es necesario.
            // Para simplicidad, se asume longitudCaracter = 1 para este método en el contexto de C# Substring.
            string cadenaRecortada = cadena.Substring(0, longitudMaxima);
            // Si la longitud original es mayor, añadir puntos suspensivos.
            return cadenaRecortada + (cadena.Length > longitudMaxima ? "…" : "");
        }

        /// <summary>
        /// Elimina los espacios en blanco del principio y el final de una cadena.
        /// </summary>
        /// <param name="cadena">La cadena a limpiar.</param>
        /// <returns>La cadena sin espacios en los extremos.</returns&gt>
        public static string RecortarEspacios(this string cadena)
        {
            return cadena.Trim();
        }
    }
}

Etiquetas: WinForms C# SQL Server ADO.NET Capa de Acceso a Datos

Publicado el 6-7 05:18