sábado, 28 de mayo de 2022

Curso avanzado de C#. Manipulación de cadenas I

Las cadenas son diferentes de otros tipos de datos. Los programas generalmente las tratan como si fueran cualquier otro dato de tipo valor, pero detrás hay una clase cadena que es notablemente compleja. Podemos ignorar esta complejidad en la mayoría de la programación diaria, pero es importante comprender cómo funcionan las cadenas para que poder manejar situaciones especiales cuando surjan. 

Por ejemplo, si comprendemos cómo se almacenan las cadenas, sabremos cuándo sería mejor utilizar la clase StringBuilder en lugar de concatenarlas. 

Curso avanzado de C#. Manipulación de cadenas I


Nota: 

En C#, palabra clave string es un alias de System.String, por lo que cuando creamos una variable de tipo string, en realidad estamos creando un objeto String. Estilísticamente, la mayoría de los programadores de C# prefieren usar string, pero aquí utilizaremos String para enfatizar que son objetos y no los tipos de valores simples que pueden parecer. 

Detrás de las cadenas 

.NET representa los caracteres como la versión Unicode UTF-16, un formato que usa 16 bits para almacenar cada carácter. Eso permite que un carácter Unicode represente muchos más caracteres de los que se proporcionan en un teclado estándar. (La última versión de Unicode define valores para más de 110.000 caracteres en más de 100 scripts). Una cadena es un objeto que usa una serie de caracteres Unicode para representar un texto. Una de las características más inusuales de los objetos String es que son inmutables. Eso significa que el contenido de una cadena no se puede cambiar después de que se haya creado la cadena. En cambio, los métodos que parecen modificar el valor de una cadena, como Replace y ToUpper, en realidad devuelven un nuevo objeto String que contiene el valor modificado. 

Para conservar memoria, el CLR mantiene una tabla llamada grupo interno (intern pool) que contiene una única referencia a cada valor de texto único utilizado por el programa. Cualquier variable de cadena que se refiera a un texto en particular es en realidad una referencia a un grupo de internos. Varias cadenas que representan el mismo valor se refieren a la misma entrada en el grupo de internos. 

Todo esto requiere algo de sobrecarga, por lo que trabajar con cadenas no es tan rápido como trabajar con tipos de valor. Si un programa debe realizar una gran cantidad de concatenaciones, cada una crea una nueva instancia de String que debe ser internada (añadida al grupo de internos o intern pool) y eso lleva tiempo. En ese caso, el uso de la clase StringBuilder puede ofrecer un mejor rendimiento. 

Constructores de String 

Tres de las formas más comunes de inicializar una variable de cadena son: 

-Establecerla igual a un literal de cadena.

-Establecerla igual al texto introducido por el usuario en un control como un TextBox o ComboBox. 

-Establecerla igual al resultado de un cálculo de cadena. 

El último de estos incluye métodos que formatean una variable para producir un String, como usar el método ToString o el método String.Format. 

Además de estos métodos, la clase String proporciona varios constructores que a veces pueden ser útiles: 

-Un constructor inicializa String a partir de una matriz de caracteres char.

 -Un segundo constructor usa solo parte de una matriz de caracteres, tomando como parámetros la matriz, una posición de inicio y la longitud de los caracteres a usar. 

-Un tercer constructor toma como parámetro un carácter y la cantidad de veces que queremos repetir ese carácter en la nueva cadena. Esto puede resultar especialmente útil si queremos aplicar una sangría a una cadena con un cierto número de espacios o caracteres de tabulación. Por ejemplo, el siguiente código muestra los números del 1 al 10 en líneas separadas con cada línea con sangría de cuatro espacios más que la anterior:

For (int i = 1;i <= 10; i++)

{

     String indent = New String(' ', 4 * i);

    Console.WriteLine(indent + i.ToString());

La mayoría de los valores de cadena se crean mediante literales de cadena, texto ingresado por el usuario o resultados de cálculos, pero los constructores de cadenas a veces pueden ser útiles. 

Campos y propiedades de String 

La clase String proporciona solo tres campos y propiedades: Empty, Length  y un indexador de solo lectura. El campo Empty devuelve un objeto que representa una cadena vacía. Podemos utilizar este valor para establecer el valor de una cadena o para ver si una cadena tiene un valor vacío. (Alternativamente, podemos utilizar el literal de cadena vacía " ".) 

La propiedad Length devuelve el número de caracteres de la cadena. El indexador de solo lectura devuelve los caracteres chars de la cadena. Debido a que es un indexador, podemos obtener sus valores agregando

un índice para el nombre de una variable String. Por ejemplo, la instrucción username [4] devuelve el carácter número 4 en la cadena username. 

El indexador es de solo lectura, por lo que no podemos establecer uno de los caracteres de la cadena con una declaración como username [4] = 'x'. Si queremos hacer algo así, tenemos que utilizar los métodos String que se describen en el siguiente epigrafe. 


Si fuera más fácil tratar String como si fuera una matriz de caracteres de lectura / escritura, podemos utilizar el método ToCharArray para convertir String en una matriz (array) de caracteres, manipularlos y luego crear un nuevo String pasando al constructor la matriz modificada. Por ejemplo, el siguiente código utiliza una matriz para hacer que los caracteres de una cadena se alternen entre mayúsculas y minúsculas: 

Char [] caracteres = text.ToCharArray ();

For (int i = 0; i <caracteres .Length; i ++)

     If (i% 2 == 0) caracteres[i] = char.ToUpper ( caracteres[i]);

     Else caracteres[i] = Char.ToLower (caracteres[i]);

text = New string(caracteres);

También podemos utilizar el indexador como fuente de iteración en un bucle foreach:

String texto = "Podemos acercarnos más a la verdad mediante ejemplos negativos que con verificaciones de nuestras creencias.";

int[] conteos = New int[26]; texto = texto.ToUpper ();

foreach (char ch in texto)

{

     If (Char.IsLetter(ch))

     {

         int index = (Int()) ch - (int)'A';

         conteos [index] ++;

     }

} 

Este código crea un objeto String llamado texto. Crea una matriz (array) de conteos para contener los conteos de las 26 letras de la A a la Z utilizadas en la cadena. Antes de procesar la cadena, el código convierte el texto en mayúsculas. 

A continuación, se utiliza una instrucción foreach para recorrer los caracteres de la cadena. Para cada carácter, el código utiliza el método IsLetter de la clase char para decidir si el carácter es una letra y no un espacio o un signo de puntuación. Si el carácter es una letra, el código lo convierte en un número entero y le resta el valor de "A" convertido en un número entero para obtener un índice en la matriz de conteos. La letra A tiene índice 0, B tiene índice 1 y así sucesivamente. Luego, el código incrementa el recuento de ese índice. Cuando finaliza el código, la matriz de conteos contiene el número de veces que aparece cada carácter en la cadena. 

Métodos de String

 La clase String proporciona muchos métodos que nos permiten trabajar con cadenas. La Tabla mostrada a continuación, describe los métodos estáticos más útiles proporcionados por la clase String. Debido a que estos son métodos estáticos, un programa usa la clase String para invocar estos métodos. Por ejemplo, para utilizar el método Compare, el programa utiliza una declaración similar a if (String.Compare (value1, value2)> 0) .... 

 

Método

Descripción

Compare

Compara dos cadenas y devuelve –1, 0 o 1 para indicar si la primera cadena debe considerarse anterior, igual o posterior de la segunda cadena en el orden de clasificación. Versiones sobrecargadas de este método nos permiten especificar reglas de comparación de cadenas, como ignorar mayúsculas y minúsculas, o qué reglas de comparación de cultura utilizar.

Concat

Toma como parámetro una matriz de cadenas u otros objetos y devuelve una cadena que contiene la concatenación de los objetos. Una versión sobrecargada permite pasar cualquier número de argumentos como parámetros y devuelve los argumentos concatenados. (ver también join)

Copy

Devuelve una copia de la cadena (ver también clone en la siguiente tabla)

Equals

Devuelve verdadero si dos cadenas tienen el mismo valor. (Ver también la instancia Equals de la siguiente tabla)

Format

Utiliza una cadena de formato y una serie de objetos para generar una cadena de texto formateada.

IsNullOrEmpty

Devuelve verdadero si String contiene una cadena en blanco "" o la variable de cadena se refiere a nulo. El siguiente código establece dos variables iguales a una cadena vacía y una tercera variable a nula:

 

String value1 = "";

String value2 = String.Empty;

String value3 = null;

 

Existe una diferencia entre una cadena vacía y una nula El método IsNullOrEmpty hace que sea más fácil tratar ambos valores de la misma manera

 

IsNullOrWhiteSpace

Devuelve verdadero si la variable String contiene una cadena en blanco, hace referencia a nulo o solo contiene caracteres de espacio en blanco. Los caracteres de espacio en blanco son los que Char.IsWhiteSpace devuelve verdadero

Join

Une los valores en una matriz (array) de cadenas u otros objetos separados por una cadena de separación. Por ejemplo, el siguiente código establece la variable allDays para contener los días de la semana separados por comas:

 

String[] diasSemana =

{

"Lunes", "Martes", "Miércoles",

"Jueves", "Viernes", "Sabado",

"Domingo"

};

String todosLosDias = String.Join(",", diasSemana);

 


Métodos de Instancia

La siguiente tabla describe los métodos de instancia más útiles proporcionados por la clase String. Debido a que estos son métodos de instancia, un programa debe usar una instancia de la clase String para invocar estos métodos. 

Por ejemplo, para usar el método CompareTo, el programa usaría una declaración similar a if (valor1.CompareTo (valor2)> 0) .... 

Método

Descripción

Clone

Devuelve una nueva referencia a la cadena. El comportamiento es un poco diferente al

Clonar métodos proporcionados por la mayoría de las otras clases porque las cadenas son inmutables.

Para esta clase, la nueva referencia se refiere al mismo valor en el grupo de internos. Como el String original.

CompareTo

Compara la cadena con otra cadena y devuelve –1, 0 o 1 para indicar que esta Cadena debe considerarse antes, igual o después de la otra cadena en el orden de clasificación. Si queremos especificar reglas de comparación de cadenas, ya sea

para ignorar el uso de mayúsculas y minúsculas y qué reglas de comparación de la cultura utilizar, utilizaremos el método

Compare.

Contains

Devuelve verdadero si la cadena contiene una subcadena especificada.

CopyTo

Copia un número específico de caracteres desde una posición de inicio especificada en una matriz (array)  de caracteres.

EndsWith

Devuelve verdadero si la cadena termina con una subcadena especificada.

Versiones sobrecargadas nos permiten especificar el tipo de comparación de cadenas.

Equals

Devuelve verdadero si esta cadena tiene el mismo valor que otra cadena.

IndexOf

Devuelve el índice de la primera aparición de un carácter o subcadena dentro de

una cadena. Los parámetros permiten especificar la posición en la cadena donde la búsqueda debe comenzar y terminar, y las opciones de comparación de cadenas.

IndexOfAny

Devuelve el índice de la primera aparición de cualquier carácter en una matriz dentro de la cadena. Los parámetros permiten especificar la posición en la Cadena donde  debe comenzar y terminar la búsqueda.

Insert

Inserta una cadena en una posición específica dentro de esta cadena y devuelve el resultado.

LastIndexOf

Devuelve el índice de la última aparición de un carácter o subcadena dentro de una cadena. Los parámetros permiten especificar la posición en la cadena donde debe comenzar y finalizar la búsqueda, y las opciones de comparación.

LastIndexOfAny

Devuelve el índice de la última aparición de cualquier carácter en una matriz dentro de la cadena. Los parámetros permiten especificar la posición en la cadena donde debe comenzar y terminar la búsqueda.

PadLeft

Devuelve la cadena rellenada a una cierta longitud agregando espacios o un carácter especificado a la izquierda. Esto facilita la alineación del texto en columnas con un ancho de fuente fija.

PadRight

Devuelve la Cadena rellenada a una cierta longitud agregando espacios o un carácter especificado a la derecha. Esto facilita la alineación del texto en columnas con un ancho de fuente fija.

Remove

Elimina los caracteres que comienzan en una posición especificada o al final de la

cadena o para un cierto número de caracteres y devuelve el resultado.

Replace

Reemplaza todas las instancias de un carácter o cadena con otro carácter o cadena y devuelve el resultado.

Split

Devuelve una matriz que contiene trozos de cadena delimitados por caracteres. Las versiones sobrecargadas permiten indicar el número máximo de trozos a devolver y dividir opciones, como eliminar entradas vacías. por ejemplo, el siguiente código divide una serie de números separados por comas y guiones, eliminando cualquier entrada que esté vacía:

 

Char[] delimitadores = { ',', '-' };

String valores = "14-63,,27-87,565";

String[]campos = valores.Split(delimitadores,

StringSplitOptions.RemoveEmptyEntries);

StartsWith

Devuelve verdadero si la cadena comienza con una subcadena especificada. Versiones sobrecargadas  permiten especificar el tipo de comparación, si ignorar el caso o el tipo.

Substring

Devuelve una nueva cadena que contiene una subcadena de esta cadena especificada por un inicio de posición y su longitud.

ToCharArray

Devuelve una matriz de caracteres que representa algunos o todos los caracteres de la cadena.

ToLower

Devuelve una copia de la cadena convertida a minúsculas.

ToString

Devuelve la cadena. Normalmente, no necesitamos hacer esto, pero si estamos tratando String como un objeto, por ejemplo, si está en una lista o matriz de objetos, es útil saber que este objeto tiene un método ToString.

ToUpper

Devuelve una copia de la cadena convertida a mayúsculas.

Trim

Devuelve una copia de la cadena a la que se le han eliminado espacios en blanco iniciales y finales. Una versión sobrecargada permite especificar qué caracteres deben ser eliminados.

TrimEnd

Devuelve una copia de la cadena con los espacios en blanco finales eliminados

TrimStart

Devuelve una copia de la cadena con los espacios en blanco iniciales eliminados.

Los métodos de la clase String permiten que un programa realice todo tipo de manipulaciones de cadenas, como analizar la entrada del usuario para obtener las partes de una dirección, un número de teléfono u otras partes de información formateada. 

Ejemplo práctico

Modificaremos el formulario de pedidos que creamos en esta entrada  para que que nos permita manejar el porcentaje de IVA especificado. Ahora si el valor introducido por el usuario contiene un carácter %, parseamos el valor y lo dividimos entre 100. 

El método decimal.TryParse no puede analizar (parsear)  una cadena que contenga el carácter %. Para analizar el valor, el programa debemos eliminar el carácter % si está presente, utilizaremos TryParse para convertir el resultado en un valor decimal y luego dividir entre 100 si el texto original contenía el carácter %. El siguiente código muestra como podemos hacer esto:

// Recogemos el IVA como cadena

String strIVA = txtIVA.Text;

// Eliminamos el caracter % si existe.

strIVA = strIVA.Replace("%", "");

// Parseamos el IVA.

Decimal decIVA;

If (!decimal.TryParse(strIVA, out decIVA))

{

     DisplayErrorMessage(

     "Formato no válido. El IVA debe ser un valor decimal.",

     "Formato no válido", txtIVA);

     Return;

}

// Si la cadena contiene el carácter %, dividimos entre 100.

If (txtIVA .Text.Contains("%")) decIVA /= 100;

 

sábado, 21 de mayo de 2022

Curso avanzado de C#. Manejo de tipos dinámicos

Los atributos DllImport y MarshalAs descritos en el post anterior permiten decirle al programa dónde encontrar un método no administrado y qué tipos de datos utiliza para los parámetros y finalmente, un tipo de retorno. Esto permite que el programa invoque métodos no administrados a través de P/invoke.

COM Interop proporciona otra forma en que un programa administrado puede interactuar con código no administrado. Para usar COM Interop, necesitamos dar a nuestro   programa una referencia a una biblioteca apropiada. Para ello,  buscamos en el Explorador de soluciones, hacemos clic con el botón derecho en Referencias y seleccionamos Agregar referencia.

Manejo de tipos dinámicos C#

Buscamos la referencia que deseamos agregar en la sección Bibliotecas de tipos de la pestaña COM (por ejemplo, Biblioteca de Microsoft ActiveX Data Objects 2.0), marcamos la casilla junto a la entrada y aceptamos.

Agregar una referencia .NET con Visual Studio

Agregar una referencia de la biblioteca le dice a nuestro programa (y Visual Studio) mucho sobre la aplicación COM no administrada. Si abrimos el menú Ver y seleccionamos Explorador de objetos, podemos utilizarlo para buscar entre los objetos y tipos definidos por la biblioteca.

Examinador de Objetos Visual Studio


La biblioteca brinda a Visual Studio suficiente información para que proporcione IntelliSense sobre algunos de los miembros de la biblioteca, pero es posible que Visual Studio aún no comprenda todos los tipos usados por la biblioteca. 

Examinador de Objetos Visual Studio

Desde la versión 4.0 C# proporciona un tipo de datos especial llamado dinámico (dynamic) que podemos utilizar en esta situación. Este es un tipo de datos estático, pero su tipo verdadero no se evalúa hasta el momento de la ejecución. En el momento del diseño y la compilación, C# no evalúa el tipo de elemento dinámico, por lo que no marca errores de sintaxis para problemas tales como discrepancias de tipos porque aún no ha evaluado el tipo dinámico. Esto puede resultar útil si no podemos proporcionar información completa sobre el tipo de un elemento al compilador.

C# considera que los objetos definidos por el código COM Interop no administrado tienen tipo dinámico (dynamic),  como se ha indicado, espera hasta el tiempo de ejecución para ver si el código tiene sentido.

El programa de ejemplo ExcelInterop, utiliza el siguiente código para crear un libro de excel Microsoft Excel (previamente hemos tenido que añadir la referencia Microsoft Excel 14.0 Object Library)  :

 // Abre la aplicación excel

Excel._Application excelApp = New Excel.Application();

// añade un libro

Excel.Workbook nuevo_libro = excelApp.Workbooks.Add();

Excel.Worksheet sheet = nuevo_libro.Worksheets[1];

// Muestra el Excel.

excelApp.Visible = true;

// Pone cabeceras a las columnas.

sheet.Cells[1, 1].Value = "Valor";

sheet.Cells[1, 2].Value = "Cuadrado del Valor";

// Muestra los primeros 10 cuadrados.

For (int i = 1; i <= 10; i++)

{

sheet.Cells[i + 1, 1].Value = i;

sheet.Cells[i + 1, 2].Value = (i * i).ToString();

}

// Rellena las columnas.

sheet.Columns[1].AutoFit();

sheet.Columns[2].AutoFit();

En este código, el tipo de datos dinámicos se usa implícitamente en un par de lugares. Visual Studio no comprende realmente el tipo de datos de la hoja. Cells[1, 1], por lo que difiere la verificación de tipos para ese valor. Eso permite que el programa se refiera a la propiedad Value de esta entidad aunque el programa no sepa si la celda tiene tal propiedad. En realidad, podríamos intentar establecer sheet.Cells[1, 1].Whatever = i y Visual Studio no mostraría error hasta el momento de la ejecución, cuando intente acceder a la propiedad Whatever y descubra que no existe.

 De forma similar, Visual Studio trata a sheet.Columns[1] como si tuvieran un tipo dinámico, por lo que no sabe que el método AutoFit existe hasta el momento de la ejecución. En es siguiente código veremos un ejemplo más específico de C#.

// rellenamos un array de numeros.

int[]array1 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// No funciona bien por que array1.Clone es un objeto.

//int[] array2 = array1.Clone();

// Esto si funciona.

int[]array3 = (int[])array1.Clone();

array3[5] = 55;

// Esto también funciona.

dynamic array4 = array1.Clone();

array4[6] = 66;

array4[7] = "Esto no funciona";

 Este código inicializa una matriz de números enteros. El código comentado intenta usar el método Clone de la matriz para hacer una copia de la matriz. Pero el método Clone devuelve un objeto no específico, por lo que el código no puede guardarlo en una variable que se refiere a una matriz de int. Y falla. La siguiente declaración introduce correctamente el objeto en un int[] para que funcione. Después, el código almacena un nuevo valor entero en la matriz. Después, el código declara un array4 que tiene el tipo dinámico.  Se clona el array y guarda la copia en la variable array4. En tiempo de ejecución, el programa puede decir que la copia es en realidad un int[10], por lo que ese es el tipo de datos que asigna a array4.

La sentencia final intenta guardar una cadena en array4[7]. En diseño y compilación, Visual Studio no intenta validar esta declaración porque array4 se declaró dinámico. Sin embargo, en tiempo de ejecución, esto falla porque array4 es en realidad un int[] y no puede contener una cadena.

El tipo de datos dinámicos permiten evitar errores de sintaxis cuando no conocemos (o no podemos saber) el tipo de un objeto en el momento de la compilación.

Desafortunadamente, no saber el tipo de un objeto en el momento del diseño también significa que Visual Studio no puede proporcionar verificación de tipo o IntelliSense. Eso significa que debemos asegurarnos de que los métodos que invoquemos realmente existan, que asignemos valores específicos a una variable o propiedad dinámica y que no intentemos guardar un valor dinámico en una variable incompatible. El programa mostrará los errores durante la ejecución, pero no recibiremos mucha ayuda durante su diseño y compilación.

Para evitar este tipo de errores en tiempo de ejecución, debemos evitar el tipo de datos dinámicos y utilizar tipos de datos más específicos siempre que sea posible

Ejercicio de ejemplo  Formulario de pedidos

Creamos un formulario de pedidos como similar a este.

Manejo de tipos dinámicos


Este formulario de entrada de pedidos analiza (parsea) los valores numéricos y de moneda introducidos por el usuario. Cuando el usuario hace clic en el botón Aceptar, se valida el formulario, calcula y muestra los valores apropiados. (No es necesario preocuparse por formatear los campos de salida como moneda. Solo utilizaremos métodos
ToString de las variables para mostrar el texto.) Si todos los valores introducidos por el usuario son válidos, se muestra un mensaje que indica que el pedido está bien y nos pregunta si queremos continuar. Si decimos que sí

el programa realiza las siguientes operaciones:

Si alguno de los campos de una fila no está en blanco, el resto de campos de esa fila no deben estar en blanco.

La cantidad es un número entero entre 1 y 100.

El precio debe estar entre 0.01 € y $ 100,000.00 € (Hay que asegurarse  de permitir valores con formato de moneda).

La tasa de impuestos es un decimal entre 0,00 y 0,20. (No hay que preocuparse por valores porcentuales como el 7% por ciento.


No tenemos que olvidarnos de agregar una declaración using System.Globalization para poder usar NumberStyles.

Hay varias formas de estructurar el código del programa para que sea más fácil de usar y mantener. 

Debemos considerar escribir los siguientes métodos:

-DisplayErrorMessage que muestre un mensaje de error estándar y establezca el foco en un TextBox que tenga el valor no válido o faltante.

-ValidateRequiredTextBox deberá verificar que los TextBox no queden en blanco (sin rellenar).

-ValidateRow validará una fila de entrada que consta de Descripción, Cantidad y Precio de cada cuadro de texto.

Para obtener un valor de un TextBox, utilizaremos el método TryParse apropiado. Por ejemplo, el siguiente código muestra cómo leer un valor de Precio Unidad:

 // Intenta analizar priceEach.

If (!decimal.TryParse(TxtPrecio.Text, NumberStyles.Currency,

null, out precioUnidad))

{

    DisplayErrorMessage(

    "Formato no válido. El precio debe ser un     valor de tipo currency.",

    "Formáto no válido", TxtPrecio);

    Return True;

}

Este código utiliza NumberStyles.Currency para habilitar los valores de moneda.

- Debemos utilizar sentencias if para determinar si los valores se encuentran dentro de los límites esperados. El siguiente código muestra cómo el programa puede validar un valor de Price Each:

// Nos aseguramos de que el precio esté entre 0,01 y 100.000,00.

If ((priceEach < 0.01m) || (precioUnidad > 100000.00m))

{

DisplayErrorMessage(

"Formato no válido. El precio debe estar entre 0,01 y 100000,00.",

"Cantidad no válida", TxtPrecio);

Return True;

}

-Calculamos y mostramos los valores de Precio ampliado, Subtotal, Impuesto sobre las ventas y Total general. El siguiente código muestra cómo procesar la primera fila del formulario de pedido: 

subtotal = 0;

If (ValidateRow(txtDescripcion, txtCantidad, txtPrecioUnidad,

out Cantidad, out PrecioUnidad)) Return;

Precio = Cantidad * PrecioUnidad;

If (Precio == 0m) txtPrecio.Clear();

Else txtPrecio.Text = Precio.ToString();

subtotal += Precio;

Este código llama al método ValidateRow para validar y obtener los valores de Descripción, Cantidad y Precio de la primera fila. Si ese método devuelve un error al devolver true, el código retorna. Si la fila no contiene error, el código calcula Precio y muestra su valor en el TextBox apropiado. Después añade el precio extendido de la fila al valor subtotal actual y continúa procesando las otras filas.

Una forma obvia de mejorar la interfaz de usuario sería eliminar los cuadros de texto de cantidad y reemplazarlos con controles NumericUpDown. Así, el usuario puede seleccionar un valor dentro de los valores mínimos y máximos permitidos. El usuario no podía escribir basura y no podía seleccionar valores fuera del rango permitido. Incluso podemos utilizar un control NumericUpDown para la tasa impositiva estableciendo sus propiedades Mínimo = 0, Máximo = 0,2, Incremento = 0.05 y DecimalPlaces = 2.

También podemos utilizar los controles NumericUpDown para los campos PrecioUnidad, pero ese control hace que se introduzcan valores monetarios incorrectos. En general, es mejor permitir que los usuarios seleccionen un valor en lugar de introducir uno en un TextBox para que no puedan ingresar valores no válidos.

A continuación el ejemplo de código completo:

using System;

using System.Windows.Forms;

using System.Globalization;

 

namespace Ejemplo_Tipos_Dinamicos

{

    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

        }

        // Salida.

        private void cancelButton_Click(object sender, EventArgs e)

        {

            Close();

        }


        // Valida los valores en el formulario. Si hay errores,

        // indica al usuario y establece el foco en el TextBox apropiado.

        // Si no hay errores, muestra un mensaje de éxito y sale.

        private void okButton_Click(object sender, EventArgs e)

        {

            int cantidad;

            decimal precioUnitario, precio, subtotal;

 

            // Valida las filas. Si alguna falla, regresa.

            subtotal = 0;

            if (ValidaFila(txtDescripcion1, txtCantidad1, txtPrecioUnitario1,

                out cantidad, out precioUnitario)) return;

            precio = cantidad * precioUnitario;

            if (precio == 0m) txtPrecio1.Clear();

            else txtPrecio1.Text = precio.ToString("C");

            subtotal += precio;

 

            if (ValidaFila(txtDescripcion2, txtCantidad2, txtPrecioUnitario2,

                out cantidad, out precioUnitario)) return;

            precio = cantidad * precioUnitario;

            if (precio == 0m) txtPrecio2.Clear();

            else txtPrecio2.Text = precio.ToString("C");

            subtotal += precio;

 

            if (ValidaFila(txtDescripcion3, txtCantidad3, txtPrecioUnitario3,

                out cantidad, out precioUnitario)) return;

            precio = cantidad * precioUnitario;

            if (precio == 0m) txtPrecio3.Clear();

            else txtPrecio3.Text = precio.ToString("C");

            subtotal += precio;

 

            if (ValidaFila(txtDescripcion4, txtCantidad4, txtPrecioUnitario4,

                out cantidad, out precioUnitario)) return;

            precio = cantidad * precioUnitario;

            if (precio == 0m) txtPrecio4.Clear();

            else txtPrecio4.Text = precio.ToString("C");

            subtotal += precio;

 

            // Muestra el subtotal.

            txtSubtotal.Text = subtotal.ToString("C");

 

            // Obtiene la tasa de impuestos como una cadena.

            string IVAPorcString = txtIVAPorc.Text;

 

            // Elimina el carácter% si está presente.

            IVAPorcString = IVAPorcString.Replace("%", "");

 

            // Parsea la tasa de impuestos.

            decimal IVAPorc;

            if (!decimal.TryParse(IVAPorcString, out IVAPorc))

            {

          

                MuestraErrores(

                    "Formato no válido. el % de IVA debe ser un valor decimal.",

                    "Formato no válido", txtIVAPorc);

                return;

            }

 

            // Si la cadena original contiene el carácter%, divide entre 100.

            if (txtIVAPorc.Text.Contains("%")) IVAPorc /= 100;

 

            // Se Asegura de que la tasa de impuestos está entre 0,00 y 0,20.

            if ((IVAPorc < 0m) || (IVAPorc > 0.2m))

            {

            

                MuestraErrores(

                    "Formato no válido. El % de IVA debe estar entre  0,00 y 0,20.",

                    "Formato no válido", txtIVAPorc);

                return;

            }

 

            // En este punto tenemos todos los datos que necesitamos.

            // Calcula y muestra el impuesto sobre las ventas.

            decimal decIVA = subtotal * IVAPorc;

            txtIVATotal.Text = salesTax.ToString("C");

 

            // Calcula y muestra el total

            decimal Total = subtotal + decIVA;

            txtTotal.Text = grandTotal.ToString("C");

 

            // Muestra un mensaje de éxito  y pregunta si queremos continuar.

            if (MessageBox.Show("Orden válida. Desea Continuar?", "Continuar?",

                MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)

            {

                // Si el usuario pulsa si,  sale se deshabilita pues si no, no se ve el resultado

                //Close();

            }

        }

 

        // Valida una fila. Si hay algún valor presente, todos son obligatorios.

        // Si todos los valores están presentes, establece los parámetros de salida.

        // Si hay un error, pone el foco en el

        // cuadro de texto apropiado y devuelve true.

        private bool ValidaFila(TextBox descrTextBox, TextBox cantidadTextBox,

            TextBox precioUnitarioTextBox, out int cantidad, out decimal precioUnitario)

        {

            // Asume que hay 0.

            cantidad = 0;

            precioUnitario = 0;

 

            // Si no hay valores presentes, la fila está bien.

            if ((descrTextBox.Text == "") &&

                (cantidadTextBox.Text == "") &&

                (precioUnitarioTextBox.Text == ""))

                return false;

 

            // Se asegura de que estén todos los valores rellenos.

            if (ValidaTextBoxObligatoria(descrTextBox, "Descripcion")) return true;

            if (ValidaTextBoxObligatoria(cantidadTextBox, "Cantidad")) return true;

            if (ValidaTextBoxObligatoria(precioUnitarioTextBox, "Precio Unitario")) return true;

 

            // Todos los valores están presentes.

            // Intenta analizar (parsear)  la cantidad.

            if (!int.TryParse(cantidadTextBox.Text, out cantidad))

            {

        

                MuestraErrores(

                    "Cantidad no válida. La cantidad debe ser un valor entero.",

                    "Cantidad no válida", cantidadTextBox);

                return true;

            }

 

            // S easegura de que la cantidad esté entre 1 y 100.

            if ((cantidad < 1) || (cantidad > 100))

            {

           

                MuestraErrores(

                    "Cantidad no válida. La cantidad debe estar entre  1 y 100.",

                    "Cantidad no válida", cantidadTextBox);

                return true;

            }

 

            // Parsea el precio Unitario.

            if (!decimal.TryParse(precioUnitarioTextBox.Text, NumberStyles.Currency,

                null, out precioUnitario))

            {

 

                MuestraErrores(

                    "Precio Unitario no válido. El precio unitario debe ser un valor de tipo moneda.",

                    "Precio Unitario no válido", precioUnitarioTextBox);

                return true;

            }

 

            // Se asegura de que el precio Unitario esta entre  0,01 € y  100.000,00 €.

            if ((precioUnitario < 0.01m) || (precioUnitario > 100000.00m))

            {

              

                MuestraErrores(

                    "Precio Unitario no válido. El precio unitario debe estar entre 0,01 € and 100.000,00 €",

                    "Precio Unitario no válido", precioUnitarioTextBox);

                return true;

            }

 

            // Si llegamos aquí, tendremos todos nuestros datos.

            // Devuelve falso para indicar que no hay ningún error en esta línea.

            return false;

        }

 

        // Este TextBox es obligatorio. Si está en blanco,

        // se lo indica al usuario, le pone el foco y devuelve true

        private bool ValidaTextBoxObligatoria(TextBox txt, string name)

        {

            // Si el TextBox no está en blanco, devuelve falso.

            if (txt.Text != "") return false;

 

            // El TextBox está en blanco.

            MuestraErrores(name + " es requerido", "Valor faltante", txt);

            return true;

        }

 

        // Informa al usuario que hay un error, selecciona todos los

        // textos del TextBox y establece el foco en TextBox.

        private void MuestraErrores(string message, string title, TextBox txt)

        {

            MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Error);

            txt.SelectAll();

            txt.Focus();

        }

        //sale

        private void button1_Click(object sender, EventArgs e)

        {

            Close();

        }

    }

}