Capítulo 11: Manejo de errores en C# (try/catch)

6 min de lectura

Parte I - Sección 2: Programación desde cero - Nivel: Principiante


🎯 Objetivos de aprendizaje

Al finalizar este capítulo serás capaz de:

  • Entender qué son las excepciones y por qué ocurren
  • Usar try-catch-finally para manejar errores de forma controlada
  • Identificar tipos comunes de excepciones en C#
  • Lanzar excepciones propias con throw
  • Implementar logging para rastrear errores
  • Validar datos antes de procesarlos
  • Construir un validador robusto que nunca falla inesperadamente
  • Aplicar mejores prácticas de manejo de errores en scripts de automatización

📋 Requisitos previos

Antes de comenzar este capítulo debes:

  • ✅ Haber completado el Capítulo 10
  • ✅ Entender clases y objetos básicos
  • ✅ Saber trabajar con colecciones (List, Dictionary)
  • ✅ Conocer funciones y métodos
  • ✅ Comprender control de flujo (if/else, loops)

🏆 Proyecto del capítulo

Validador Robusto de Archivos y Datos EPLAN

Al final de este capítulo habrás construido un sistema completo que:

  • Valida archivos antes de procesarlos (existencia, extensión, permisos)
  • Procesa listas de componentes con validación de datos
  • Maneja errores de forma elegante sin interrumpir la ejecución
  • Genera logs detallados de errores y advertencias
  • Implementa validaciones personalizadas con excepciones
  • Crea reportes de validación profesionales

Tiempo estimado: 70-80 minutos


📚 Contenido

1. Introducción: El problema de los errores

1.1 ¿Qué puede salir mal?

Sin manejo de errores (código frágil):

public void ProcesarArchivo()
{
    string archivo = @"C:\datos\componentes.txt";

    // ❌ ¿Y si el archivo no existe?
    string contenido = System.IO.File.ReadAllText(archivo);

    // ❌ ¿Y si el contenido está vacío?
    string[] lineas = contenido.Split('\n');

    // ❌ ¿Y si una línea está mal formateada?
    foreach (string linea in lineas)
    {
        string[] partes = linea.Split(',');
        double corriente = double.Parse(partes[2]);  // ❌ ¿Y si no es un número?
    }

    MessageBox.Show("Procesamiento completado");
}

// Resultado: Script se cae con excepción no controlada

Con manejo de errores (código robusto):

public void ProcesarArchivo()
{
    try
    {
        string archivo = @"C:\datos\componentes.txt";

        // Validar que existe
        if (!System.IO.File.Exists(archivo))
        {
            MessageBox.Show("Error: El archivo no existe", "Error");
            return;
        }

        string contenido = System.IO.File.ReadAllText(archivo);

        // Validar contenido
        if (string.IsNullOrEmpty(contenido))
        {
            MessageBox.Show("Error: El archivo está vacío", "Error");
            return;
        }

        string[] lineas = contenido.Split('\n');

        foreach (string linea in lineas)
        {
            try
            {
                string[] partes = linea.Split(',');

                double corriente;
                if (!double.TryParse(partes[2], out corriente))
                {
                    MessageBox.Show(string.Format("Advertencia: Línea inválida: {0}", linea));
                    continue;  // Salta esta línea, pero continúa con las demás
                }

                // Procesar corriente...
            }
            catch (Exception ex)
            {
                MessageBox.Show(string.Format("Error en línea: {0}\n{1}", linea, ex.Message));
            }
        }

        MessageBox.Show("Procesamiento completado con éxito");
    }
    catch (Exception ex)
    {
        MessageBox.Show(string.Format("Error general: {0}", ex.Message), "Error crítico");
    }
}

Ventajas del manejo de errores:

  • ✅ El script no se cae inesperadamente
  • ✅ Mensajes informativos para el usuario
  • ✅ Posibilidad de recuperación o alternativas
  • ✅ Registro de errores para debugging
  • ✅ Experiencia profesional del usuario

2. Excepciones: ¿Qué son?

Una excepción es un evento anormal que ocurre durante la ejecución de un programa.

2.1 Tipos comunes de excepciones

ExcepciónCausaEjemplo
FileNotFoundExceptionArchivo no existeAbrir archivo inexistente
DirectoryNotFoundExceptionCarpeta no existeAcceder a ruta inexistente
FormatExceptionConversión de tipo fallidaint.Parse("abc")
IndexOutOfRangeExceptionÍndice fuera de límitesarray[10] en array de 5
NullReferenceExceptionObjeto es nullobjeto.Metodo() cuando objeto es null
ArgumentExceptionArgumento inválidoPasar valor incorrecto a método
InvalidOperationExceptionOperación inválidaOperación en estado incorrecto
UnauthorizedAccessExceptionSin permisosAcceder a archivo protegido

2.2 Anatomía de una excepción

Cuando ocurre una excepción, contiene información útil:

try
{
    int resultado = int.Parse("abc");  // Causa FormatException
}
catch (FormatException ex)
{
    // Propiedades útiles:
    MessageBox.Show(ex.Message);        // Descripción del error
    MessageBox.Show(ex.StackTrace);     // Pila de llamadas
    MessageBox.Show(ex.Source);         // Fuente del error
}

3. Try-Catch-Finally: La estructura fundamental

3.1 Try-Catch básico

try
{
    // Código que puede fallar
    int numero = int.Parse("123");
    MessageBox.Show("Número: " + numero);
}
catch (Exception ex)
{
    // Código que se ejecuta si hay error
    MessageBox.Show("Error: " + ex.Message);
}

Flujo de ejecución:

  1. Se ejecuta el bloque try
  2. Si no hay error, se salta el catch
  3. Si hay error, se ejecuta el catch inmediatamente

3.2 Múltiples catch (específicos primero)

try
{
    string texto = System.IO.File.ReadAllText(@"C:\datos\archivo.txt");
    int numero = int.Parse(texto);
    MessageBox.Show("Número: " + numero);
}
catch (FileNotFoundException ex)
{
    MessageBox.Show("El archivo no existe: " + ex.Message);
}
catch (FormatException ex)
{
    MessageBox.Show("El contenido no es un número válido: " + ex.Message);
}
catch (Exception ex)
{
    // Captura cualquier otra excepción
    MessageBox.Show("Error inesperado: " + ex.Message);
}

Importante: Los catch más específicos deben ir antes que los genéricos.

// ❌ MAL - Nunca se ejecutarán los catch específicos
catch (Exception ex) { }
catch (FileNotFoundException ex) { }  // Nunca se ejecuta

// ✓ BIEN - Específicos primero
catch (FileNotFoundException ex) { }
catch (FormatException ex) { }
catch (Exception ex) { }  // Captura el resto

3.3 Finally: Código que siempre se ejecuta

System.IO.StreamReader reader = null;

try
{
    reader = new System.IO.StreamReader(@"C:\datos\archivo.txt");
    string contenido = reader.ReadToEnd();
    MessageBox.Show(contenido);
}
catch (Exception ex)
{
    MessageBox.Show("Error: " + ex.Message);
}
finally
{
    // Se ejecuta SIEMPRE (haya o no error)
    if (reader != null)
    {
        reader.Close();  // Liberar recursos
        MessageBox.Show("Archivo cerrado");
    }
}

Usos típicos de finally:

  • Cerrar archivos
  • Liberar conexiones de base de datos
  • Limpiar recursos temporales
  • Restaurar estados

3.4 Try-Finally (sin catch)

System.IO.StreamWriter writer = null;

try
{
    writer = new System.IO.StreamWriter(@"C:\datos\log.txt", true);
    writer.WriteLine("Inicio del proceso");
    // Código que puede fallar...
}
finally
{
    // Siempre se cierra el archivo
    if (writer != null)
    {
        writer.Close();
    }
}

4. Lanzar excepciones: throw

4.1 Throw básico

public void ValidarCorriente(double corriente)
{
    if (corriente <= 0)
    {
        throw new ArgumentException("La corriente debe ser mayor que cero");
    }

    if (corriente > 1000)
    {
        throw new ArgumentException("La corriente excede el límite de 1000A");
    }

    MessageBox.Show("Corriente válida: " + corriente + "A");
}

// Uso
try
{
    ValidarCorriente(-5.0);  // Lanza excepción
}
catch (ArgumentException ex)
{
    MessageBox.Show("Error de validación: " + ex.Message);
}

4.2 Re-lanzar excepciones

try
{
    // Código que falla
    int resultado = int.Parse("abc");
}
catch (FormatException ex)
{
    // Registrar error
    MessageBox.Show("Registrando error en log...");

    // Re-lanzar la misma excepción
    throw;  // Sin "ex" para mantener stack trace original
}

4.3 Excepciones personalizadas

// Definir excepción personalizada
public class ComponenteInvalidoException : Exception
{
    public string Designacion { get; set; }

    public ComponenteInvalidoException(string designacion, string mensaje)
        : base(mensaje)
    {
        this.Designacion = designacion;
    }
}

// Uso
public void ValidarComponente(string designacion, double corriente)
{
    if (string.IsNullOrEmpty(designacion))
    {
        throw new ComponenteInvalidoException("", "La designación no puede estar vacía");
    }

    if (!designacion.StartsWith("K") && !designacion.StartsWith("Q"))
    {
        throw new ComponenteInvalidoException(designacion,
            "La designación debe comenzar con K o Q");
    }

    if (corriente <= 0)
    {
        throw new ComponenteInvalidoException(designacion,
            string.Format("Corriente inválida para {0}: {1}A", designacion, corriente));
    }
}

// Captura
try
{
    ValidarComponente("X1", 25.0);
}
catch (ComponenteInvalidoException ex)
{
    MessageBox.Show(string.Format("Componente inválido: {0}\nError: {1}",
        ex.Designacion, ex.Message));
}

5. Validación de datos

5.1 Validación defensiva

public double CalcularPotencia(double corriente, double voltaje)
{
    // Validar parámetros
    if (corriente < 0)
        throw new ArgumentException("La corriente no puede ser negativa", "corriente");

    if (voltaje < 0)
        throw new ArgumentException("El voltaje no puede ser negativo", "voltaje");

    if (corriente == 0 || voltaje == 0)
        return 0;

    return corriente * voltaje;
}

5.2 TryParse: Validación sin excepciones

// ❌ MAL - Usar excepciones para control de flujo
try
{
    int numero = int.Parse(texto);
}
catch (FormatException)
{
    numero = 0;  // Valor por defecto
}

// ✓ BIEN - Usar TryParse
int numero;
if (int.TryParse(texto, out numero))
{
    MessageBox.Show("Número válido: " + numero);
}
else
{
    MessageBox.Show("Texto inválido, usando 0");
    numero = 0;
}

TryParse está disponible para:

  • int.TryParse()
  • double.TryParse()
  • bool.TryParse()
  • DateTime.TryParse()

5.3 Validación con retorno booleano

public bool ValidarComponente(string designacion, double corriente, out string mensajeError)
{
    mensajeError = "";

    if (string.IsNullOrEmpty(designacion))
    {
        mensajeError = "La designación no puede estar vacía";
        return false;
    }

    if (corriente <= 0)
    {
        mensajeError = "La corriente debe ser mayor que cero";
        return false;
    }

    if (corriente > 1000)
    {
        mensajeError = "La corriente no puede exceder 1000A";
        return false;
    }

    return true;  // Validación exitosa
}

// Uso
string error;
if (ValidarComponente("K1", 25.0, out error))
{
    MessageBox.Show("Componente válido");
}
else
{
    MessageBox.Show("Error: " + error);
}

6. Logging de errores

6.1 Logger simple a archivo

public class Logger
{
    private string archivoLog;

    public Logger(string ruta)
    {
        this.archivoLog = ruta;
    }

    public void Info(string mensaje)
    {
        EscribirLog("INFO", mensaje);
    }

    public void Warning(string mensaje)
    {
        EscribirLog("WARNING", mensaje);
    }

    public void Error(string mensaje, Exception ex = null)
    {
        string mensajeCompleto = mensaje;

        if (ex != null)
        {
            mensajeCompleto += "\n  Excepción: " + ex.Message;
            mensajeCompleto += "\n  Stack Trace: " + ex.StackTrace;
        }

        EscribirLog("ERROR", mensajeCompleto);
    }

    private void EscribirLog(string nivel, string mensaje)
    {
        try
        {
            string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            string lineaLog = string.Format("[{0}] [{1}] {2}", timestamp, nivel, mensaje);

            // Agregar al archivo (append)
            System.IO.File.AppendAllText(archivoLog, lineaLog + "\n");
        }
        catch
        {
            // Si falla el log, no hacer nada (evitar loop infinito)
        }
    }
}

// Uso
Logger log = new Logger(@"C:\logs\proceso.log");

log.Info("Iniciando procesamiento de componentes");

try
{
    // Código que puede fallar
    int resultado = int.Parse("abc");
}
catch (FormatException ex)
{
    log.Error("Error al convertir texto a número", ex);
    MessageBox.Show("Error en el procesamiento. Ver log para detalles.");
}

log.Info("Procesamiento completado");

Ejemplo de archivo log generado:

[2025-01-15 10:30:45] [INFO] Iniciando procesamiento de componentes
[2025-01-15 10:30:45] [ERROR] Error al convertir texto a número
  Excepción: Input string was not in a correct format.
  Stack Trace:    at System.Number.ParseDouble(String value, NumberStyles options, NumberFormatInfo numfmt)
[2025-01-15 10:30:46] [INFO] Procesamiento completado

6.2 Logger con niveles

public enum NivelLog
{
    Debug = 0,
    Info = 1,
    Warning = 2,
    Error = 3
}

public class LoggerAvanzado
{
    private string archivoLog;
    private NivelLog nivelMinimo;

    public LoggerAvanzado(string ruta, NivelLog nivelMinimo = NivelLog.Info)
    {
        this.archivoLog = ruta;
        this.nivelMinimo = nivelMinimo;
    }

    public void Debug(string mensaje)
    {
        Log(NivelLog.Debug, mensaje);
    }

    public void Info(string mensaje)
    {
        Log(NivelLog.Info, mensaje);
    }

    public void Warning(string mensaje)
    {
        Log(NivelLog.Warning, mensaje);
    }

    public void Error(string mensaje, Exception ex = null)
    {
        string mensajeCompleto = mensaje;
        if (ex != null)
            mensajeCompleto += "\n  " + ex.ToString();

        Log(NivelLog.Error, mensajeCompleto);
    }

    private void Log(NivelLog nivel, string mensaje)
    {
        // Solo registrar si el nivel es suficiente
        if (nivel < nivelMinimo)
            return;

        try
        {
            string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            string lineaLog = string.Format("[{0}] [{1}] {2}",
                timestamp, nivel.ToString().ToUpper(), mensaje);

            System.IO.File.AppendAllText(archivoLog, lineaLog + "\n");
        }
        catch { }
    }
}

// Uso
LoggerAvanzado log = new LoggerAvanzado(@"C:\logs\app.log", NivelLog.Info);

log.Debug("Detalle interno");     // No se registra (nivel muy bajo)
log.Info("Inicio del proceso");   // Se registra
log.Warning("Archivo no óptimo"); // Se registra
log.Error("Fallo crítico", ex);   // Se registra

7. Mejores prácticas

7.1 No uses excepciones para control de flujo

// ❌ MAL
try
{
    int numero = int.Parse(input);
}
catch (FormatException)
{
    numero = 0;  // Usar excepción como if
}

// ✓ BIEN
int numero;
if (!int.TryParse(input, out numero))
{
    numero = 0;
}

Razón: Las excepciones son costosas en rendimiento. Úsalas solo para casos excepcionales.

7.2 Captura excepciones específicas

// ❌ MAL - Captura demasiado amplia
try
{
    // ...
}
catch (Exception ex)  // Captura TODO (incluso bugs del sistema)
{
    MessageBox.Show("Error");
}

// ✓ BIEN - Específico
try
{
    // ...
}
catch (FileNotFoundException ex)
{
    MessageBox.Show("Archivo no encontrado");
}
catch (UnauthorizedAccessException ex)
{
    MessageBox.Show("Sin permisos de acceso");
}
catch (IOException ex)
{
    MessageBox.Show("Error de E/S: " + ex.Message);
}

7.3 Siempre valida entrada de usuario

public void ProcesarCorriente(string inputCorriente)
{
    // Validar antes de convertir
    if (string.IsNullOrWhiteSpace(inputCorriente))
    {
        MessageBox.Show("Debe ingresar un valor de corriente");
        return;
    }

    double corriente;
    if (!double.TryParse(inputCorriente, out corriente))
    {
        MessageBox.Show("El valor ingresado no es un número válido");
        return;
    }

    if (corriente <= 0)
    {
        MessageBox.Show("La corriente debe ser mayor que cero");
        return;
    }

    // Ahora sí procesar
    MessageBox.Show("Corriente válida: " + corriente + "A");
}

7.4 Proporciona mensajes útiles

// ❌ MAL - Mensaje genérico
catch (Exception ex)
{
    MessageBox.Show("Error");
}

// ✓ BIEN - Mensaje informativo
catch (FileNotFoundException ex)
{
    MessageBox.Show(string.Format(
        "No se encontró el archivo:\n{0}\n\nVerifique que la ruta sea correcta.",
        ex.FileName));
}

// ✓ MEJOR - Mensaje + acción sugerida
catch (UnauthorizedAccessException ex)
{
    MessageBox.Show(
        "Error de permisos.\n\n" +
        "El archivo está protegido o en uso.\n\n" +
        "Soluciones:\n" +
        "1. Cierre el archivo si está abierto\n" +
        "2. Ejecute como administrador\n" +
        "3. Verifique permisos de carpeta");
}

7.5 No ocultes excepciones

// ❌ MAL - "Tragarse" el error
try
{
    // Código que falla
}
catch
{
    // No hacer nada - el usuario no sabe qué pasó
}

// ✓ BIEN - Al menos registrar
try
{
    // Código que falla
}
catch (Exception ex)
{
    Logger.Error("Error en procesamiento", ex);
    MessageBox.Show("Ocurrió un error. Revise el log para detalles.");
}

💻 Manos a la obra: Construyendo el proyecto

Vamos a crear un Validador Robusto de Archivos y Datos completo.

Proyecto completo: ValidadorRobusto.cs

//
// Proyecto: Validador Robusto de Archivos y Datos EPLAN
// Capítulo: 11
// Descripción: Sistema completo de validación con manejo de errores robusto,
//              logging detallado, y validaciones personalizadas
// Autor: C.D. López
// Fecha: Enero 2025
//

using System;
using System.Collections.Generic;
using System.IO;
using Eplan.EplApi.Base;
using Eplan.EplApi.ApplicationFramework;

// ========================================
// LOGGER PROFESIONAL
// ========================================
public enum NivelLog
{
    Debug = 0,
    Info = 1,
    Warning = 2,
    Error = 3
}

public class Logger
{
    private string archivoLog;
    private NivelLog nivelMinimo;

    public Logger(string ruta, NivelLog nivelMinimo = NivelLog.Info)
    {
        this.archivoLog = ruta;
        this.nivelMinimo = nivelMinimo;

        // Crear archivo de log si no existe
        try
        {
            string directorio = Path.GetDirectoryName(ruta);
            if (!Directory.Exists(directorio))
            {
                Directory.CreateDirectory(directorio);
            }
        }
        catch { }
    }

    public void Debug(string mensaje)
    {
        Log(NivelLog.Debug, mensaje);
    }

    public void Info(string mensaje)
    {
        Log(NivelLog.Info, mensaje);
    }

    public void Warning(string mensaje)
    {
        Log(NivelLog.Warning, mensaje);
    }

    public void Error(string mensaje, Exception ex = null)
    {
        string mensajeCompleto = mensaje;
        if (ex != null)
        {
            mensajeCompleto += string.Format("\n  Excepción: {0}\n  Tipo: {1}",
                ex.Message, ex.GetType().Name);
        }

        Log(NivelLog.Error, mensajeCompleto);
    }

    private void Log(NivelLog nivel, string mensaje)
    {
        if (nivel < nivelMinimo)
            return;

        try
        {
            string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            string lineaLog = string.Format("[{0}] [{1}] {2}",
                timestamp, nivel.ToString().ToUpper(), mensaje);

            File.AppendAllText(archivoLog, lineaLog + "\n");
        }
        catch { }
    }
}

// ========================================
// EXCEPCIÓN PERSONALIZADA
// ========================================
public class ComponenteInvalidoException : Exception
{
    public string Designacion { get; set; }
    public string Campo { get; set; }

    public ComponenteInvalidoException(string designacion, string campo, string mensaje)
        : base(mensaje)
    {
        this.Designacion = designacion;
        this.Campo = campo;
    }
}

// ========================================
// CLASE COMPONENTE
// ========================================
public class ComponenteElectrico
{
    public string Designacion { get; set; }
    public string Codigo { get; set; }
    public string Tipo { get; set; }
    public double Corriente { get; set; }
    public double Voltaje { get; set; }

    public ComponenteElectrico(string designacion, string codigo, string tipo,
                              double corriente, double voltaje)
    {
        this.Designacion = designacion;
        this.Codigo = codigo;
        this.Tipo = tipo;
        this.Corriente = corriente;
        this.Voltaje = voltaje;
    }

    public override string ToString()
    {
        return string.Format("{0} | {1} | {2} | {3}A | {4}V",
            Designacion, Codigo, Tipo, Corriente, Voltaje);
    }
}

// ========================================
// VALIDADOR DE COMPONENTES
// ========================================
public class ValidadorComponentes
{
    private Logger log;

    public ValidadorComponentes(Logger logger)
    {
        this.log = logger;
    }

    // Validar designación
    public void ValidarDesignacion(string designacion)
    {
        if (string.IsNullOrWhiteSpace(designacion))
        {
            throw new ComponenteInvalidoException("", "Designacion",
                "La designación no puede estar vacía");
        }

        if (designacion.Length < 2)
        {
            throw new ComponenteInvalidoException(designacion, "Designacion",
                "La designación debe tener al menos 2 caracteres");
        }

        char primerCaracter = designacion[0];
        if (!char.IsLetter(primerCaracter))
        {
            throw new ComponenteInvalidoException(designacion, "Designacion",
                "La designación debe comenzar con letra");
        }
    }

    // Validar código
    public void ValidarCodigo(string designacion, string codigo)
    {
        if (string.IsNullOrWhiteSpace(codigo))
        {
            throw new ComponenteInvalidoException(designacion, "Codigo",
                "El código no puede estar vacío");
        }

        if (codigo.Length < 5)
        {
            throw new ComponenteInvalidoException(designacion, "Codigo",
                "El código debe tener al menos 5 caracteres");
        }
    }

    // Validar corriente
    public void ValidarCorriente(string designacion, double corriente)
    {
        if (corriente <= 0)
        {
            throw new ComponenteInvalidoException(designacion, "Corriente",
                string.Format("La corriente debe ser mayor que 0 (actual: {0}A)", corriente));
        }

        if (corriente > 10000)
        {
            throw new ComponenteInvalidoException(designacion, "Corriente",
                string.Format("La corriente excede el límite de 10000A (actual: {0}A)", corriente));
        }
    }

    // Validar voltaje
    public void ValidarVoltaje(string designacion, double voltaje)
    {
        double[] voltajesPermitidos = {24, 48, 110, 220, 230, 380, 400, 440, 480, 690};

        bool voltajeValido = false;
        foreach (double v in voltajesPermitidos)
        {
            if (Math.Abs(voltaje - v) < 5)  // Tolerancia de ±5V
            {
                voltajeValido = true;
                break;
            }
        }

        if (!voltajeValido)
        {
            throw new ComponenteInvalidoException(designacion, "Voltaje",
                string.Format("Voltaje no estándar: {0}V", voltaje));
        }
    }

    // Validar componente completo
    public bool ValidarComponente(ComponenteElectrico componente, out string mensajeError)
    {
        mensajeError = "";

        try
        {
            ValidarDesignacion(componente.Designacion);
            ValidarCodigo(componente.Designacion, componente.Codigo);
            ValidarCorriente(componente.Designacion, componente.Corriente);
            ValidarVoltaje(componente.Designacion, componente.Voltaje);

            log.Info(string.Format("Componente válido: {0}", componente.Designacion));
            return true;
        }
        catch (ComponenteInvalidoException ex)
        {
            mensajeError = string.Format("[{0}] Campo '{1}': {2}",
                ex.Designacion, ex.Campo, ex.Message);

            log.Warning(mensajeError);
            return false;
        }
        catch (Exception ex)
        {
            mensajeError = "Error inesperado: " + ex.Message;
            log.Error(mensajeError, ex);
            return false;
        }
    }
}

// ========================================
// VALIDADOR DE ARCHIVOS
// ========================================
public class ValidadorArchivos
{
    private Logger log;

    public ValidadorArchivos(Logger logger)
    {
        this.log = logger;
    }

    // Validar que archivo existe
    public bool ValidarExistencia(string rutaArchivo, out string mensajeError)
    {
        mensajeError = "";

        if (string.IsNullOrWhiteSpace(rutaArchivo))
        {
            mensajeError = "La ruta del archivo no puede estar vacía";
            log.Error(mensajeError);
            return false;
        }

        if (!File.Exists(rutaArchivo))
        {
            mensajeError = string.Format("El archivo no existe:\n{0}", rutaArchivo);
            log.Error(mensajeError);
            return false;
        }

        log.Info(string.Format("Archivo existe: {0}", rutaArchivo));
        return true;
    }

    // Validar extensión
    public bool ValidarExtension(string rutaArchivo, string[] extensionesPermitidas,
                                 out string mensajeError)
    {
        mensajeError = "";

        string extension = Path.GetExtension(rutaArchivo).ToLower();

        bool extensionValida = false;
        foreach (string ext in extensionesPermitidas)
        {
            if (extension == ext.ToLower())
            {
                extensionValida = true;
                break;
            }
        }

        if (!extensionValida)
        {
            mensajeError = string.Format(
                "Extensión inválida: {0}\nExtensiones permitidas: {1}",
                extension, string.Join(", ", extensionesPermitidas));

            log.Error(mensajeError);
            return false;
        }

        log.Info(string.Format("Extensión válida: {0}", extension));
        return true;
    }

    // Validar permisos de lectura
    public bool ValidarPermisosLectura(string rutaArchivo, out string mensajeError)
    {
        mensajeError = "";

        try
        {
            using (FileStream fs = File.Open(rutaArchivo, FileMode.Open, FileAccess.Read))
            {
                // Si llegamos aquí, tenemos permisos
            }

            log.Info("Permisos de lectura: OK");
            return true;
        }
        catch (UnauthorizedAccessException ex)
        {
            mensajeError = "Sin permisos de lectura para el archivo";
            log.Error(mensajeError, ex);
            return false;
        }
        catch (Exception ex)
        {
            mensajeError = "Error al verificar permisos: " + ex.Message;
            log.Error(mensajeError, ex);
            return false;
        }
    }

    // Validar que archivo no está vacío
    public bool ValidarContenido(string rutaArchivo, out string mensajeError)
    {
        mensajeError = "";

        try
        {
            FileInfo info = new FileInfo(rutaArchivo);

            if (info.Length == 0)
            {
                mensajeError = "El archivo está vacío";
                log.Warning(mensajeError);
                return false;
            }

            log.Info(string.Format("Tamaño del archivo: {0} bytes", info.Length));
            return true;
        }
        catch (Exception ex)
        {
            mensajeError = "Error al leer información del archivo: " + ex.Message;
            log.Error(mensajeError, ex);
            return false;
        }
    }
}

// ========================================
// PROCESADOR DE ARCHIVOS
// ========================================
public class ProcesadorArchivos
{
    private Logger log;
    private ValidadorArchivos validadorArchivos;
    private ValidadorComponentes validadorComponentes;

    public ProcesadorArchivos(Logger logger)
    {
        this.log = logger;
        this.validadorArchivos = new ValidadorArchivos(logger);
        this.validadorComponentes = new ValidadorComponentes(logger);
    }

    // Procesar archivo de componentes
    public void ProcesarArchivoComponentes(string rutaArchivo)
    {
        log.Info("========================================");
        log.Info("Iniciando procesamiento de archivo");
        log.Info("Archivo: " + rutaArchivo);

        int lineasProcesadas = 0;
        int lineasValidas = 0;
        int lineasInvalidas = 0;
        List<string> errores = new List<string>();

        try
        {
            // Validar archivo
            string error;

            if (!validadorArchivos.ValidarExistencia(rutaArchivo, out error))
            {
                MessageBox.Show(error, "Error de archivo");
                return;
            }

            string[] extensionesPermitidas = {".txt", ".csv"};
            if (!validadorArchivos.ValidarExtension(rutaArchivo, extensionesPermitidas, out error))
            {
                MessageBox.Show(error, "Error de extensión");
                return;
            }

            if (!validadorArchivos.ValidarPermisosLectura(rutaArchivo, out error))
            {
                MessageBox.Show(error, "Error de permisos");
                return;
            }

            if (!validadorArchivos.ValidarContenido(rutaArchivo, out error))
            {
                MessageBox.Show(error, "Error de contenido");
                return;
            }

            // Leer archivo
            log.Info("Leyendo archivo...");
            string[] lineas = File.ReadAllLines(rutaArchivo);
            log.Info(string.Format("Total de líneas: {0}", lineas.Length));

            // Procesar cada línea
            foreach (string linea in lineas)
            {
                lineasProcesadas++;

                // Saltar líneas vacías
                if (string.IsNullOrWhiteSpace(linea))
                {
                    log.Debug("Línea vacía ignorada");
                    continue;
                }

                // Saltar líneas de comentario
                if (linea.Trim().StartsWith("#") || linea.Trim().StartsWith("//"))
                {
                    log.Debug("Comentario ignorado: " + linea);
                    continue;
                }

                try
                {
                    // Parsear línea (formato: Designacion,Codigo,Tipo,Corriente,Voltaje)
                    string[] partes = linea.Split(',');

                    if (partes.Length != 5)
                    {
                        string errorLinea = string.Format(
                            "Línea {0}: Formato inválido (esperaba 5 campos, encontró {1})",
                            lineasProcesadas, partes.Length);

                        errores.Add(errorLinea);
                        log.Warning(errorLinea);
                        lineasInvalidas++;
                        continue;
                    }

                    // Parsear campos
                    string designacion = partes[0].Trim();
                    string codigo = partes[1].Trim();
                    string tipo = partes[2].Trim();

                    double corriente;
                    if (!double.TryParse(partes[3].Trim(), out corriente))
                    {
                        string errorLinea = string.Format(
                            "Línea {0}: Corriente inválida '{1}'", lineasProcesadas, partes[3]);

                        errores.Add(errorLinea);
                        log.Warning(errorLinea);
                        lineasInvalidas++;
                        continue;
                    }

                    double voltaje;
                    if (!double.TryParse(partes[4].Trim(), out voltaje))
                    {
                        string errorLinea = string.Format(
                            "Línea {0}: Voltaje inválido '{1}'", lineasProcesadas, partes[4]);

                        errores.Add(errorLinea);
                        log.Warning(errorLinea);
                        lineasInvalidas++;
                        continue;
                    }

                    // Crear componente
                    ComponenteElectrico componente = new ComponenteElectrico(
                        designacion, codigo, tipo, corriente, voltaje);

                    // Validar componente
                    string errorValidacion;
                    if (validadorComponentes.ValidarComponente(componente, out errorValidacion))
                    {
                        lineasValidas++;
                        log.Info(string.Format("Línea {0}: OK - {1}",
                            lineasProcesadas, componente));
                    }
                    else
                    {
                        errores.Add(string.Format("Línea {0}: {1}",
                            lineasProcesadas, errorValidacion));
                        lineasInvalidas++;
                    }
                }
                catch (Exception ex)
                {
                    string errorLinea = string.Format(
                        "Línea {0}: Error inesperado - {1}", lineasProcesadas, ex.Message);

                    errores.Add(errorLinea);
                    log.Error(errorLinea, ex);
                    lineasInvalidas++;
                }
            }

            // Generar reporte
            GenerarReporte(rutaArchivo, lineasProcesadas, lineasValidas, lineasInvalidas, errores);
        }
        catch (IOException ex)
        {
            string mensaje = "Error de lectura/escritura: " + ex.Message;
            log.Error(mensaje, ex);
            MessageBox.Show(mensaje, "Error de E/S");
        }
        catch (UnauthorizedAccessException ex)
        {
            string mensaje = "Sin permisos de acceso al archivo";
            log.Error(mensaje, ex);
            MessageBox.Show(mensaje, "Error de permisos");
        }
        catch (Exception ex)
        {
            string mensaje = "Error inesperado: " + ex.Message;
            log.Error(mensaje, ex);
            MessageBox.Show(mensaje, "Error crítico");
        }
        finally
        {
            log.Info("Procesamiento finalizado");
            log.Info("========================================");
        }
    }

    // Generar reporte de validación
    private void GenerarReporte(string archivoOrigen, int totalLineas, int lineasValidas,
                                int lineasInvalidas, List<string> errores)
    {
        string reporte = "REPORTE DE VALIDACIÓN\n";
        reporte += "========================================\n";
        reporte += string.Format("Archivo: {0}\n", Path.GetFileName(archivoOrigen));
        reporte += string.Format("Fecha: {0}\n\n", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));

        reporte += "RESUMEN:\n";
        reporte += string.Format("  Líneas procesadas: {0}\n", totalLineas);
        reporte += string.Format("  Líneas válidas: {0}\n", lineasValidas);
        reporte += string.Format("  Líneas inválidas: {0}\n\n", lineasInvalidas);

        if (lineasInvalidas > 0)
        {
            reporte += "ERRORES ENCONTRADOS:\n";
            reporte += "----------------------------------------\n";
            foreach (string error in errores)
            {
                reporte += error + "\n";
            }
        }
        else
        {
            reporte += "✓ Todos los componentes son válidos\n";
        }

        reporte += "========================================\n";

        log.Info("Reporte generado");
        MessageBox.Show(reporte, "Reporte de Validación");
    }
}

// ========================================
// SCRIPT PRINCIPAL
// ========================================
public class ValidadorRobusto
{
    [Start]
    public void Ejecutar()
    {
        try
        {
            // Crear logger
            string rutaLog = @"C:\temp\eplan_logs\validacion.log";
            Logger log = new Logger(rutaLog, NivelLog.Info);

            log.Info("==================================================");
            log.Info("VALIDADOR ROBUSTO DE ARCHIVOS Y DATOS");
            log.Info("==================================================");

            // Crear procesador
            ProcesadorArchivos procesador = new ProcesadorArchivos(log);

            // Archivo de prueba
            string archivoComponentes = @"C:\temp\componentes.txt";

            // Procesar
            procesador.ProcesarArchivoComponentes(archivoComponentes);

            // Mostrar ubicación del log
            MessageBox.Show(string.Format("Procesamiento completado.\n\nLog disponible en:\n{0}", rutaLog),
                "Validación completada");
        }
        catch (Exception ex)
        {
            MessageBox.Show(string.Format("Error crítico en la aplicación:\n\n{0}\n\n{1}",
                ex.Message, ex.StackTrace), "Error crítico");
        }
    }
}

Archivo de prueba: componentes.txt

Crea este archivo en C:\temp\componentes.txt:

# Archivo de componentes eléctricos
# Formato: Designacion,Codigo,Tipo,Corriente,Voltaje

K1,3RT2026-1BB40,Contactor,25.0,400
K2,3RT2036-1BB40,Contactor,40.0,400
Q1,3RV2011-1JA10,Guardamotor,4.5,400
M1,WEG-7.5KW,Motor,15.2,400

# Líneas con errores para probar validación
K3,ABC,Contactor,-10.0,400
,3RT2026,Contactor,25.0,400
K5,3RT2026-1BB40,Contactor,25.0,999
K6,3RT2026-1BB40,Contactor,abc,400
K7,SHORT,Contactor,25.0,400,extra

🔍 Deep Dive: Entendiendo en profundidad

¿Cuándo usar excepciones vs validación?

Usa excepciones para:

  • Condiciones verdaderamente excepcionales
  • Errores que no esperas normalmente
  • Violaciones de contratos (pre/postcondiciones)

Usa validación para:

  • Entrada de usuario
  • Datos externos (archivos, red)
  • Condiciones esperadas del negocio

Stack unwinding

Cuando se lanza una excepción, el programa "desenrolla" la pila de llamadas hasta encontrar un catch:

public void Metodo1()
{
    try
    {
        Metodo2();
    }
    catch (Exception ex)
    {
        MessageBox.Show("Capturado en Metodo1: " + ex.Message);
    }
}

public void Metodo2()
{
    Metodo3();  // No captura excepciones
}

public void Metodo3()
{
    throw new Exception("Error en Metodo3");
}

// Flujo:
// Metodo3 lanza excepción
// → Metodo2 no la captura, sube
// → Metodo1 la captura y maneja

Using: Manejo automático de recursos

// Sin using - manual
StreamReader reader = null;
try
{
    reader = new StreamReader("archivo.txt");
    string contenido = reader.ReadToEnd();
}
finally
{
    if (reader != null)
        reader.Close();
}

// Con using - automático
using (StreamReader reader = new StreamReader("archivo.txt"))
{
    string contenido = reader.ReadToEnd();
}  // Automáticamente llama reader.Close()

Ventajas de using:

  • Garantiza liberación de recursos
  • Código más limpio
  • Menos propenso a errores

📝 Resumen

En este capítulo aprendiste:

  • Excepciones: Eventos anormales durante la ejecución
  • Try-catch-finally: Estructura fundamental de manejo de errores
  • Tipos de excepciones: FileNotFoundException, FormatException, etc.
  • Throw: Lanzar excepciones propias y personalizadas
  • Validación: Prevenir errores antes de que ocurran
  • Logging: Registrar eventos y errores para debugging
  • Mejores prácticas: Capturar específico, validar entrada, mensajes útiles
  • Proyecto construido: Validador robusto con manejo completo de errores

Conceptos clave:

  • Las excepciones son para casos excepcionales, no para control de flujo
  • Siempre valida entrada de usuario antes de procesar
  • Captura excepciones específicas, no genéricas
  • Proporciona mensajes de error útiles y accionables
  • Registra errores para poder diagnosticar problemas
  • Usa finally para liberar recursos (o mejor aún, using)

🔗 Conexiones

⬅️ Capítulo anterior

Capítulo 10: Clases y objetos (POO)

➡️ Próximo capítulo

Capítulo 12: Arquitectura de EPLAN API

En el próximo capítulo comenzarás a trabajar con EPLAN API. Aprenderás su estructura, namespaces principales, cómo leer la documentación, y crearás tus primeros scripts que interactúan con proyectos EPLAN reales.


📚 Recursos adicionales


❓ Preguntas frecuentes

P: ¿Debo capturar todas las excepciones?

R: No. Solo captura excepciones que puedas manejar de forma significativa. Si no puedes hacer nada útil con la excepción, déjala propagarse. Evita catch (Exception) genérico a menos que sea el nivel más alto de tu aplicación.

P: ¿Cuál es la diferencia entre throw y throw ex?

R:

  • throw; re-lanza la excepción original manteniendo el stack trace completo
  • throw ex; re-lanza pero pierde el stack trace original

Siempre usa throw; sin parámetro para re-lanzar.

P: ¿Las excepciones son lentas?

R: Sí, las excepciones tienen un costo de rendimiento. Por eso no debes usarlas para control de flujo normal. Usa TryParse en lugar de try-catch para conversiones, y validaciones if para condiciones esperadas.

P: ¿Cuándo debo crear excepciones personalizadas?

R: Cuando necesitas comunicar un error específico de tu dominio que no está cubierto por las excepciones estándar. Ejemplo: ComponenteInvalidoException, VoltajeIncompatibleException.

P: ¿Qué hacer si un método lanza múltiples tipos de excepciones?

R: Usa múltiples bloques catch, ordenados de más específico a más genérico:

try
{
    // código
}
catch (FileNotFoundException ex) { }
catch (UnauthorizedAccessException ex) { }
catch (IOException ex) { }
catch (Exception ex) { }  // Captura el resto

P: ¿Es malo usar catch sin especificar el tipo?

R: Sí, evita catch { } sin tipo. Siempre especifica al menos catch (Exception ex) para poder acceder a la información del error y registrarlo.


📋 Checklist de completitud

Antes de pasar al siguiente capítulo, asegúrate de:

  • Entender qué son las excepciones y cuándo ocurren
  • Poder escribir try-catch-finally correctamente
  • Conocer los tipos comunes de excepciones
  • Saber lanzar excepciones con throw
  • Poder crear excepciones personalizadas
  • Implementar validación de datos con TryParse
  • Crear un logger simple para registrar errores
  • Haber ejecutado el validador robusto completo
  • Entender las mejores prácticas de manejo de errores
  • (Opcional) Adaptar el validador para tus propios datos

¡Felicitaciones! Has completado la Parte I de la guía. 🎉

Ya tienes todos los fundamentos de programación necesarios para trabajar con EPLAN API. En la Parte II comenzarás a crear scripts reales que automatizan tareas en EPLAN Electric P8.

¿Listo para empezar con EPLAN API? ¡Vamos al Capítulo 12! 🚀


Última actualización: Enero 2025 Tiempo de lectura estimado: 70-80 minutos Código de ejemplo: code/cap-11/