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.
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 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.
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.
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.
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();
}
}
}