0% encontró este documento útil (0 votos)
13 vistas202 páginas

Curso de CSharp

Cargado por

juandbp
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
13 vistas202 páginas

Curso de CSharp

Cargado por

juandbp
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 202

Centro Integrado de Formación Profesional

AVILÉS
Principado de Asturias

CURSO DE C#
INTRODUCCIÓN

C# (pronunciado en inglés “C Sharp” y en español “C Almohadilla”) es el lenguaje


diseñado por Microsoft para su plataforma .NET. En concreto, ha sido diseñado por
Scott Wiltamuth y Anders Hejlsberg, éste último también conocido por haber sido el
diseñador del lenguaje Turbo Pascal y la herramienta RAD Delphi.

Aunque en realidad es posible escribir código para la plataforma .NET en muchos


otros lenguajes, como Visual Basic.NET o JScript.Net, C# es el único que ha sido
diseñado específicamente para ser utilizado en esta plataforma, por lo que
programarla usando C# es mucho más sencillo e intuitivo que hacerlo con cualquiera
de los otros lenguajes. Por esta razón, Microsoft suele referirse a C# como el lenguaje
nativo de .NET, y, de hecho, gran parte de la librería de clases base de .NET ha sido
escrito en este lenguaje.

C# es un lenguaje orientado a objetos sencillo, moderno, amigable, intuitivo y


fácilmente legible que ha sido diseñado por Microsoft con el ambicioso objetivo de
recoger las mejores características de muchos otros lenguajes, fundamentalmente
Visual Basic, Java y C++, y combinarlas en uno sólo en el que se unan la alta
productividad y facilidad de aprendizaje de Visual Basic con la potencia de C++.

Quizás el más directo competidor de C# es Java, lenguaje con el que guarda un enorme
parecido en su sintaxis y características. En este aspecto, es importante señalar que C#
incorpora muchos elementos de los que Java carece (sistema de tipos homogéneo,
propiedades, indexadores, tablas multidimensionales, operadores redefinibles, etc.) y
que según los benchmarks realizados la velocidad de ejecución del código escrito en
C# es ligeramente superior a su respectiva versión en Java

A continuación, se recogen de manera resumida las principales características de C#:


• Dispone de todas las características propias de cualquier lenguaje orientado a
objetos: encapsulación, herencia y polimorfismo.
• Ofrece un modelo de programación orientada a objetos homogéneo, en el que
todo el código se escribe dentro de clases y todos los tipos de datos, incluso los
básicos, son clases que heredan de System.Object (por lo que los métodos
definidos en ésta son comunes a todos los tipos del lenguaje)
• Permite definir estructuras, que son clases un tanto especiales: sus objetos se
almacenan en pila, por lo que se trabaja con ellos directamente y no referencias
al montículo (heap), lo que permite accederlos más rápido. Sin embargo, esta
mayor eficiencia en sus accesos tiene también sus inconvenientes,
fundamentalmente que el tiempo necesario para pasarlas como parámetros a
métodos es mayor (hay que copiar su valor completo y no sólo una referencia)
y no admiten herencia (aunque sí implementación de interfaces)
• Es un lenguaje fuertemente tipado, lo que significa se controla que todas las
conversiones entre tipos se realicen de forma compatible, lo que asegura que
nunca se acceda fuera del espacio de memoria ocupado por un objeto. Así se

2
evitan frecuentes errores de programación y se consigue que los programas no
puedan poner en peligro la integridad de otras aplicaciones.
• Tiene a su disposición un recolector de basura que libera al programador de la
tarea de tener que eliminar las referencias a objetos que dejen de ser útiles,
encargándose de ello éste y evitándose así que se agote la memoria porque al
programador olvide liberar objetos inútiles o que se produzcan errores porque
el programador libere áreas de memoria ya liberadas y reasignadas.
• Incluye soporte nativo para eventos y delegados. Los delegados son similares
a los punteros a funciones de otros lenguajes como C++ aunque más cercanos a
la orientación a objetos, y los eventos son mecanismos mediante los cuales los
objetos pueden notificar de la ocurrencia de sucesos. Los eventos suelen usarse
en combinación con los delegados para el diseño de interfaces gráficas de
usuario, con lo que se proporciona al programador un mecanismo cómodo para
escribir códigos de respuesta a los diferentes eventos que puedan surgir a lo
largo de la ejecución de la aplicación. (pulsación de un botón, modificación de
un texto, etc.)
• Incorpora propiedades, que son un mecanismo que permite el acceso
controlado a miembros de una clase tal y como si de campos públicos se
tratasen. Gracias a ellas se evita la pérdida de legibilidad que en otros lenguajes
causa la utilización de métodos Set() y Get() pero se mantienen todas las ventajas
de un acceso controlado por estos proporcionada.
• Permite la definición del significado de los operadores básicos del lenguaje
(+, -, *, &, ==, etc.) para nuestros propios tipos de datos, lo que facilita
enormemente tanto la legibilidad de las aplicaciones como el esfuerzo necesario
para escribirlas. Es más, se puede incluso definir el significado del operador []
en cualquier clase, lo que permite acceder a sus objetos tal y como si fuesen
tablas. A la definición de éste último operador se le denomina indexador, y es
especialmente útil a la hora de escribir o trabajar con colecciones de objetos.
• Admite unos elementos llamados atributos que no son miembros de las clases
sino información sobre éstas que podemos incluir en su declaración. Por
ejemplo, indican si un miembro de una clase ha de aparecer en la ventana de
propiedades de Visual Studio.NET, cuáles son los valores admitidos para cada
miembro en ésta, etc.

COMPILACIÓN DE UN PROGRAMA EN C#

Para compilar en C# hay que ejecutar csc.exe con una serie de parámetros y el nombre
del archivo del código fuente:

/doc
Indica que procese los comentarios del código fuente en un archivo XML

/lib
Permite especificar la localización de los ensamblados referenciados por la opción
/reference

3
/out:
Permite especificar un nombre para el archivo de salida. Cuando se omite el nombre
del fichero de salida, coincide con el del archivo fuente

/r[eference]:
Opción para que una aplicación haga uso de los tipos definidos en un ensamblado
(librería) separado. Inmediatamente a continuación de los dos puntos, sin dejar espacio
en blanco, debe especificarse el ensamblado que se desea usar, por ejemplo:
/reference: System.Windows.Forms.dll
mscorblib.dll es un ensamblado automáticamente referenciado en el proceso de
compilación
/t[arget]:exe
Opción para construir una aplicación .EXE de consola como las aplicaciones DOS. Es
la opción por omisión.

/t[arget]:library
Permite construir un ensamblado DLL con un manifiesto relacionado

/t[arget]:module
Permite construir un módulo, sin manifiesto relacionado. Cuando un archivo no tiene
manifiesto relacionado, es decir, no es un ensamblado, no puede ser cargado por el
CLR. Estos archivos pueden incorporarse a un ensamblado mediante la opción
/addmodule

/t[arget]:winexe
Permite construir aplicaciones Windows

/addmodule
Incorpora módulos, cuya extensión por defecto es net-module, a un ensamblado. Los
módulos deben encontrarse en el mismo directorio que el archivo de salida. Por
ejemplo:
csc /addmodule:m1.netmodule;m2.netmodule /out:s.exe e.cs

/unsafe
Permite compilar código no seguro

ESTRUCTURA DE UN PROGRAMA EN C#

using System;
class Hola
{
public static int Main( )
{
Console.WriteLine("Hola, Mundo");
return 0;
}
}

4
Si se almacena este código en un fichero de texto plano de nombre HolaMundo.cs es
posible compilarlo abriendo una ventana de consola (MS-DOS) y, tras colocarse en el
directorio donde se haya almacenado el fichero, ejecutando:

csc HolaMundo1.cs

csc es el nombre del compilador de C#, y la orden anterior simplemente indica que
deseamos compilar el fichero de código fuente HolaMundo.cs, y tras ejecutarla el
compilador generará un fichero de nombre HolaMundo.exe que contendrá el
ejecutable de nuestra sencilla aplicación de ejemplo.

Diferencia con Java: El nombre de la clase no tiene por qué ser el mismo que el del
archivo

Diferencia con C++: C# no distingue entre la definición y la implementación de la clase


de la misma forma que C++. No hay archivos de cabecera, todo va en el mismo.

Cuando se escribe Main:


• Tiene que ser Main, C# discierne entre minúsculas y mayúsculas
• Designar un solo Main como punto de entrada del programa
• Declarar Main como public static int Main()
• Varias clases pueden tener un Main, pero solo uno de ellos ha de ser ejecutado,
debemos especificar cual de ellos cuando la aplicación es compilada.
• Cuando el Main finaliza, lo hace el programa

static indica que es un método estático; es decir, asociado a la clase dentro de la que se
define y no a los objetos que se creen a partir de la misma, por lo que para acceder a él
se usará la sintaxis nombreClase.NombreMétodo(parámetros) –en nuestro caso
Hola.Main()- y no objeto.NombreMétodo(parámetros) como corresponde a los métodos
no estáticos.
public static int Main(String[] args)

El parámetro que puede tomar el método Main() almacena la lista de argumentos con
los que se llamó a la aplicación; y como se ve, en caso de que no vayamos a utilizarla
no es necesario especificarla en la declaración de Main(). El tipo de este parámetro es
String[], que significa que es una tabla de cadenas de texto (objetos String); y su
nombre, que es el que habrá de usarse dentro del código del método Main() para
hacerle referencia, puede ser cualquiera (en el ejemplo es args)

Por defecto, el compilador sólo busca definiciones de clases predefinidas en el fichero


mscorlib.dll, y si vamos a usar clases definidas en otro fichero hemos de indicárselo
mediante la opción /r del compilador. Por ejemplo, un error frecuente entre
principiantes es no saber cómo compilar una aplicación de ventanas, pues hay que
indicar en qué ficheros se encuentran las clases predefinidas necesarias así:

csc /t:winexe /r:System.Winforms.dll;System.dll;Microsoft.Win32.Interop.dll


Fuente.cs

5
Nótese que también es necesario usar la opción /t: para indicar que deseamos crear un
ejecutable de ventanas, pues por defecto se crean ejecutables de consola.

LA DIRECTIVA USING Y EL ESPACIO DE NOMBRES SYSTEM

Como parte de .NET FRAMEWORK, C# viene acompañado con varias clases de


mucha utilidad organizadas en espacios de nombres (namespaces). Un namespace
puede contener, además de clases, otros namespaces.

El namespace más importante es system, y equivaldría a stdio.h de C o iostream.h de


C++. El namespace system contiene la clase Console, que a su vez contiene el método
WriteLine que sirve para mostrar por pantalla una cadena (ver ejemplo Hola Mundo).

La directiva using es la que indica con que namespace vamos a trabajar. Siempre debe
ponerse al comienzo del fichero, y podemos usar más de una.

Console.Write y Console.WriteLine son similares, solo que la segunda manda un


retorno de carro cuando termina el texto. Ambos métodos están sobrecargados, es
decir, podemos escribir Console.WriteLine(99) para imprimir un entero.

Dando formato al texto

Tanto Write como WriteLine son similares a printf, ya que podemos dar una cadena
de texto y una serie de parámetros adicionales:
Console.WriteLine("La suma de {0} y {1} is {2}", 100, 130,100+130);

Se puede especificar también dentro del parámetro cadena el ancho de los campos y si
estarán justificados a la izquierda o a la derecha:
Console.WriteLine("Justificado a la izquierda en un campo de ancho 10:{0,-
10}", 99);
Console.WriteLine("Justificado a la derecha en un campo de ancho 10:{0,10}",
99);

Resultado por pantalla:

“Justificado a la izquierda en un campo de ancho 10:99 “


“Justificado a la derecha en un campo de ancho 10: 99”

Es posible usar el literal \ para introducir caracteres como { o el mismo \ (ejemplos \\


o \{). Si se quiere poner una cadena que incluya varios de estos caracteres, usaremos
el literal @: @\\jdb\misdoc

Dando formato numérico

Sintaxis:

6
{N,M:CadenaFormato}
N: el número a dar formato
M: ancho de campo y justificación
CadenaFormato: Cuantos datos numéricos van a ser mostrados. Ver tabla

Items Significado
C Muestra el número como currency (decimal) usando los símbolos y
convenciones actuales
D Muestra el número como un entero decimal
E Muestra el número usando la notación exponencial (científica).
F Muestra el número como un valor de coma fija
G Muestra el número como coma fija o entero, dependiendo que formato se ajuste
más
N Muestra el número con comas incluídas
X Muestra el número en notación Hexadecimal.

Console.WriteLine("Formato Currency - {0:C} {1:C4}", 88.8,-888.8);


Console.WriteLine("Formato entero - {0:D5}", 88);
Console.WriteLine("Exponencial - {0:E}", 888.8);
Console.WriteLine("Coma fija - {0:F3}",888.8888);
Console.WriteLine("General - {0:G}", 888.8888);
Console.WriteLine("Numérico - {0:N}", 8888888.8);
Console.WriteLine("Hexadecimal - {0:X4}", 88);

Muestra lo siguiente:
Currency - $88.80 ($888.8000)
Entero - 00088
Exponencial - 8.888000E+002
Coma fija - 888.889
General - 888.8888
Numérico - 8,888,888.80
Hexadecimal – 0058

Se pueden concatenar cadenas con el +:


Console.WriteLine(“43”+”1”);

MÉTODOS READ Y READLINE

Read

Lee el siguiente carácter del teclado. Devuelve el entero –1 si no hay entrada. Si la hay,
devuelve un int con el ascii del carácter

ReadLine

Lee todos los caracteres hasta el retorno de carro.

7
string input = Console.ReadLine( );
Console.WriteLine("{0}", input);

APLICACIONES CON ARGUMENTOS

Antes se comentó que es posible declarar el método Main() de modo que tome un
parámetro de tipo String[] que contenga los argumentos con los que se llamó a la
aplicación. Es decir, de una de estas dos formas:

public static void Main(String[] args)


public static int Main(String[] args)

String[] indica que el parámetro Main() es un array de cadenas. Es importante resaltar


el hecho de que aunque un array siempre tiene un tamaño fijo, éste tamaño no forma
parte de la declaración de la misma. Esto permite que una misma variable de tipo array
pueda almacenar arrays de diferentes tamaños, aunque el tamaño del array
almacenado en cada instante no pueda modificarse.
Los elementos del array de cadenas que puede tomar como parámetro el método
Main() son cada uno de los argumentos con los que se llamó al programa. En C# los
arrays se indexan desde 0, lo que significa que su primer argumento se almacena en la
posición 0, el segundo en la posición 1, etc.

Esto es importante tenerlo presente a la hora de acceder a cada elemento de un array,


para lo que se usa la notación array[posiciónElemento] como muestra la siguiente
variante de la clase HolaMundo:
using System;
class HolaMundo
{
public static void Main(String[] args)
{
Console.WriteLine(“¡Hola {0}!”, args[0]);
}
}

Ahora, cuando se ejecute el programa hay que pasarle un argumento y en función del
valor que éste tome se mostrará un mensaje de bienvenida personalizado. Por ejemplo,
si se ejecuta el programa así:

HolaMundo José

Se mostrará el siguiente mensaje de saludo:

¡Hola José!

8
TIPOS BÁSICOS

Estos se resumen en la siguiente tabla:

Tipo Descripción Bi ts Rango Alias


sbyte Bytes con signo 8 -128 - 127 SByte
byte Bytes sin signo 8 0 - 255 Byte
short Enteros cortos con signo 16 -32.768 - 32.767 Int16
ushort Enteros cortos sin signo 16 0 - 65.535 UInt16
-2.147.483.648 - Int32
int Enteros normales 32
2.147.483.647
uint Enteros normales sin signo 32 0 - 4.294.967.295 UInt32
- Int64
9.223.372.036.854.775.
808 –
long Enteros largos 64
9.223.372.036.854.775.
807
0 - Uint64
ulong Enteros largos sin signo 64 18.446.744.073.709.551
.615
Reales con 7 dígitos de 1,5×10-45 - 3,4×1038 Float
float 32
precisión
Reales con 15-16 dígitos de 5,0×10-324 - 1,7×10308 Double
double 64
precisión
Reales con 28-29 dígitos de 1,0×10-28 - 7,9×1028 Decimal
decimal 128
precisión
boolean Valores lógicos 32 True, false Boolean
Unicode 0 – Unicode Char
char Caracteres Unicode 16
65535
Permitido por String
string Cadenas de caracteres var
memoria
object Cualquier objeto var Depende del objeto Object

En C# los tipos básicos son tipos del mismo nivel que cualquier otro tipo del lenguaje.
Es decir, heredan de System.Object y pueden ser tratados como objetos de la misma
por cualquier rutina que espere un System.Object, lo que cual es muy útil para el
diseño de rutinas genéricas, que admitan parámetros de cualquier tipo. En realidad
todos los tipos básicos de C# son simples alias de tipos del espacio de nombres System,
como se recoge en la última columna de la tabla. Por ejemplo, sbyte es alias de
System.Sbyte y da igual usar una forma del mismo u otra.

Los operadores aritméticos son como en C, con la salvedad de que al operador resto
(%) lo podemos utilizar con valores de tipo float.

9
Si se quisiera convertir el valor de la propiedad text en una caja de texto, podemos usar
el metodo int.Parse de la siguiente forma:
int enterocaja=int.Parse(caja.text);

El método inverso por así decirlo es el ToString() que convierte a cadena cualquier tipo
numérico.

OPERADORES IS Y AS

En ocasiones, necesitamos comprobar que un objeto es de un tipo determinado. Un


caso muy común es cuando recorremos los controles de un formulario y queremos
cambiar las propiedades solo a los textbox. Para tales menesteres, el lenguaje C# nos
proporciona dos operadores: “is” y “as”.

El operador “is” nos permite verificar si un objeto es compatible con un tipo específico.
En el caso que comentábamos en el párrafo anterior:

foreach (Control c in this.Controls)


{
if (c is Button){…}

El término compatible se refiere a si el objeto es de este tipo o de un tipo derivado de


este. Ahora podríamos hacer un casting dentro de ese if de c a Button, y poder cambiar
sus propiedades sin ningún problema.

“as” se utiliza para realizar conversiones entre tipos de referencia compatibles. El


operador as funciona como una conversión de tipos, pero si existe un error en la
conversión, proporciona el valor null en vez de producir una excepción.

foreach (Control c in this.Controls)


{
Button boton = c as Button;
if (boton!=null)
{
boton.Text="Nuevo texto";
}
}

Aunque parezca que esta segunda opción va a provocar un menor rendimiento en la


aplicación, ya que convierte todos los controles de nuestro form, mientras que la
primera solo comprueba si es de un tipo y luego hace la conversión, no es así. Al
comprobar el código intermedio de cada una de estas dos opciones, descubrimos que
en el caso de la primera, el framework hace dos comprobaciones de tipo: una, la dada
por “is” y otra al hacer la conversión.

Con “as”, simplemente chequea una sola vez, al hacer la conversión, con lo cual, su
rendimiento es mayor.

10
VIDA DE UNA VARIABLE

El tiempo de vida de una variable está limitado al espacio de tiempo durante el cual
se ejecuta el método que la contiene. Si la variable está definida como miembro de una
clase, cada vez que se inicializa un objeto de esa clase, se inicia la vida de esa variable,
permaneciendo accesible para todos los métodos de esa clase hasta que el objeto sea
destruido.

Con el modificador static podemos variar el tiempo de vida de una clase. Supongamos
que tenemos una variable Entero local a una clase, si la definimos como static, la
variable existe desde que se inicia hasta que se termina el programa, mientras dicha
variable es compartida por todos los objetos de esa clase. Vamos a ver un ejemplo de
esto. En el siguiente programa, además del método Main(), contaremos con otro
llamado MuestraVariables(). La clase cuenta con dos variables del mismo tipo. Estas
dos variables tienen un tiempo de vida muy diferente. La primera la hemos declarado
sin modificador alguno, es decir, como automática (véase C++), mientras que la
segunda por el contrario, es estática, de forma que existe durante toda la ejecución del
programa.

using System;

namespace VarEstaticas
{
class Class1
{
// una variable normal
byte Valor1;
// y otra estática
static byte Valor2;
// Iniciamos la ejecución
[STAThread]
static void Main(string[] args)
{
byte Contador;
// Invocamos 10 veces el método
for(Contador=1;Contador<=10;Contador++)
MuestraVariables();
}
// Método con dos variables
static void MuestraVariables()
{
// Creamos un objeto Class1
Class1 MiObjeto=new Class1();
// Efectuamos la misma operacion en ambas variables
MiObjeto.Valor1++;
Class1.Valor2++;
// y mostramos su contenido

Console.WriteLine("Valor={0},Valor2={1}",MiObjeto.Valor1,Class1.Valor
2);
}
}
}

11
Al ejecutar el programa obtendremos un resultado como este:

Valor=1,Valor2=1
Valor=1,Valor2=2
Valor=1,Valor2=3
Valor=1,Valor2=4
Valor=1,Valor2=5
Valor=1,Valor2=6
Valor=1,Valor2=7
Valor=1,Valor2=8
Valor=1,Valor2=9
Valor=1,Valor2=10

La variable Valor1 tiene de partida el valor 0, mientras que Valor2 conserva el


contenido de la invocación anterior y, por ello, su valor va incrementando en cada
llamada.

Las variables declaradas en el interior de una clase pueden tener un tiempo de vida
asociado a los objetos creados a partir de ella, según se ha descrito anteriormente, o
bien una vida global asociada a la propia clase. Aquí vemos que para acceder a Valor1
es necesario contar con un objeto de la clase ya que dicha variable no existe de forma
aislada. Valor2, por el contrario, existe desde el principio, sin necesidad de crear objeto
alguno, y por ello se puede hacer referencia a ella usando el nombre de la propia clase.

ENUMERACIONES

Ejemplo:

Enum Dedo {Pulgar, Indice, Corazon, Anular, Meñique};

Pulgar tiene asociado el 0, Indice el 1, etc.

DedoAfectado=Dedo.Corazon
ConsoleWriteLine((int) DedoAfectado) // Muestra el valor 2

Si queremos obtener el nombre de la constante, utilizaremos el método ToString() con


el que cuentan la mayoría de tipos de datos.
DedoAfectado.ToString() // Devuelve Corazon

COMPROBACIÓN ESTRICTA DE TIPOS

Si, partiendo de la enumeración del punto anterior declaramos una variable de tipo
Dedo y le asignamos un valor numérico entre 0 y 4, el compilador generará un error.
No se efectúa una conversión implícita, automática para convertir el entero en la
constante que le corresponde. C# es un lenguaje con comprobación estricta de tipos.

12
Para poder asignar un valor entero a una variable de tipo Dedo, por tanto es necesario
emplear una conversión explícita:

DedoAfectado=(Dedo) 3;

En este caso el valor 3 se convierte a la constante equivalente en la numeración Dedo.

ESTRUCTURAS

Como en C++ pueden contener datos y métodos. Las estructuras en C# están


derivadas de System.ValueType. Veamos un ejemplo:

struct Libro
{
public string Titulo;
public string ISBN;
public string Autor;
public float Precio;
}

Libro UnLibro;
UnLibro.Titulo="Guia práctica Visual Basic .NET";
UnLibro.Autor="Francisco Charte";
UnLibro.ISBN="84-415-1290-6";
UnLibro.Precio=31.75F;

Podemos asignar una variable Libro a otra del mismo tipo, es decir, hacer una copia:

Libro UnLibro,OtroLibro;
UnLibro.Titulo="Guia práctica Visual Basic .NET";
UnLibro.Autor="Francisco Charte";
UnLibro.ISBN="84-415-1290-6";
UnLibro.Precio=31.75F;
OtroLibro=UnLibro;

No podemos en cambio comprobar la igualdad de estructuras con el operador ==, es


decir, en este caso no podríamos hacer if (UnLibro==OtroLibro).

Todas las estructuras cuentan con un método llamado Equals() que facilita la
comprobación de igualdad entre estructuras del mismo tipo. Esta sentencia mostrará
por la consola true o false dependiendo de que UnLibro y OtroLibro sean iguales o no.

MIEMBROS PÚBLICOS Y PRIVADOS

Una de las diferencias de las estructuras de C# con respecto a C,C++ es que los
miembros no solo cuentan con un identificador y un tipo sino que además pueden
precederse de un modificador que indique su visibilidad. En Libro, todos los
miembros son public. Podemos omitir public, ya que es el modificador por defecto en
las struct.

Las otras alternativas son private e internal. Con internal tenemos una combinación
entre public y private, ya que los miembros de la estructura serían públicos para el
13
código del mismo ensamblado en el que se ha definido la estructura, pero privados
para otros ensamblados.

MÉTODOS, CONSTRUCTORES Y PROPIEDADES

Vamos a añadir un constructor y un método a la estructura Libro:

struct Libro
{
// Constructor para inicializar contenido
public Libro(string T, string I, string A, float P)
{
Titulo=T;
ISBN=I;
Autor=A;
Precio=P;
}
// Método para mostrar el contenido
public void Muestra()
{
Console.WriteLine("{0}, {1}, {2} , {3}", Titulo, ISBN, Autor,
Precio);
}
private string Titulo;
private string ISBN;
private string Autor;
private float Precio;
}

Podemos crear una estructura usando new pero no es imprescindible. La diferencia


está en que si usamos new, se llama al constructor por defecto, y si no lo hacemos, la
estructura no se inicializa y hay que hacerlo a mano mediante sentencias del tipo
libro.Precio=valor; por lo que si intentamos acceder a un campo sin haberlo
inicializado obtendremos un error.

En una estructura también podemos sobrecargar los métodos. Al heredar de la clase


Object, cualquier estructura tiene el método ToString() que podríamos sobrecargar:

public override string ToString()


{
//código
}

Las estructuras se pueden usar al trabajar con bases de datos, ya que almacenan los
registros completes de una tabla en una estructura con la misma definición que el
registro.

ARGUMENTOS IMPLÍCITOS

C# no permite, a diferencia de C++, definir argumentos con valor por defecto en una
función. Se debe resolver con sobrecarga.

14
INSTRUCCIONES ITERATIVAS

En instrucciones como if o while, se evalúa una expresión booleana, no vale considerar


que es true si es mayor que 0 como en C/C++. También en el switch, debemos salir
siempre con la sentencia break. En C/C++ si no salías, ejecutaba las sentencias del
siguiente case, pero en este caso, si no se sale con break, el compilador dará un error.

Instrucción Foreach → Se utiliza así:


foreach (tipoElemento elemento in colección)
instrucciones

Esta instrucción se utiliza para recorrer colecciones de elementos (por ejemplo,


matrices), y su significado es muy sencillo: se ejecutan las intrucciones indicadas
(estarán encerradas entre paréntesis en caso de ser varias) para cada uno de los
elementos de la colección que se especifica.

El siguiente ejemplo muestra cómo se utiliza esta instrucción:


using System;
class HolaMundo2
{
public static void Main(String[] args)
{
if (args.Length > 0)
foreach(String arg in args)
Console.WriteLine(“¡Hola {0}!”, arg);
else
Console.WriteLine(“¡Hola mundo!”); }

Es importante resaltar dos cosas sobre el foreach:


1. El elemento indicado va variando en cada ejecución de las instrucciones, de modo que
en cada ejecución se corresponde con uno de los elementos de la colección indicada. Sin
embargo, hay que tener en cuenta que este elemento (arg en el ejemplo) es de sólo
lectura, por lo que no puede usarse para modificar los valores de la colección
escribiendo en él.
2. Para los programadores más avanzados, puede ser interesante comentar que la
instrucción foreach puede usarse para recorrer cualquier objeto que sea una colección,
entendiéndose como tal cualquier objeto de una clase que implementa la interfaz
IEnumerable Esta interfaz consta de un único método getEnumerator(), que ha de
devolver un objeto que implemente la interfaz IEnumerator. Esta interfaz define las
operaciones necesarias para recorrer los elementos de la colección, y consta de los
siguientes métodos:
void Reset() → Reinicia el enumerador devolviéndolo a su estado inicial.
bool MoveNext() → Avanza el enumerador pasándose a considerar el elemento
siguiente al actual como el nuevo elemento actual. El valor devuelto indica si se ha
alcanzado el final de la colección.
Current → Propiedad de sólo lectura que devuelve el elemento actual de la colección.

15
CLASES DE ENTRADA/SALIDA

BinaryReader Lee tipos de datos primitivos como valores binarios en una


codificación específica
BinaryWriter Escribe tipos primitivos en binario en una secuencia y admite
escribir cadenas en una codificación específica
Directory Expone métodos estáticos para manipular directorios: crear,
eliminar, mover, así como para enumerar los archivos en
directorios y subdirectorios
File Proporciona métodos estáticos para crear, copiar, eliminar,
mover y abrir archivos y contribuye a la creación de objetos
FileStream
FileInfo Como File pero con métodos sobre instancia
FileStream Deriva de Stream y nos permite leer, escribir, abrir y cerrar
archivos y otras operaciones de forma síncrona y asíncrona
FileSystemInfo Proporciona clases base para FileInfo y DirectoryInfo.
Contiene métodos comunes para la manipulación de archivos
y directorios
FileSystemWatcher Permite inspeccionar cambios en el sistema de archivos y
provoca eventos cuando cambia un directorio o un archivo de
un directorio
MemoryStream Crea una secuencia cuyo almacén de respaldo es la memoria
Path Ejecuta operaciones en instancias de string que contienen
información de rutas de archivos o directorios.
Stream Proporciona una vista genérica de una secuencia de bytes
StreamReader Implementa un textreader que lee los caracteres de una
secuencia de bytes de una codificación determinada
StreamWriter Implementa TextWriter para escribir los caracteres de una
secuencia en una codificación determinada
StringReader Implementa TextReader que lee en una cadena
StringWriter Implementa TextWriter para escribir información en una
cadena. La información se almacena en el StringBuilder
subyacente
TextReader Representa un lector que puede leer una serie secuencial de
caracteres
TextWriter Representa un sistema de escritura que puede escribir una
serie secuencial de caracteres. Esta clase es abstracta

16
Las clases Directory y DirectoryInfo

Con los métodos de la clase directory podemos hacer operaciones sobre directorios de
nuestros discos duros: crear, eliminar, mover, obtener ficheros y subdirectorios, saber
fechas de creación, modificación y último acceso, incluso para averiguar el directorio
raíz y el directorio actual.

Veamos un listado de ejemplo:

static void Main(string[] args)


{
string sdir = Directory.GetCurrentDirectory();
Console.WriteLine("El directorio actual es {0}", sdir);
string[] fs;
fs = Directory.GetFiles(sdir);
foreach (string s in fs)
{
Console.WriteLine("Fichero {0}", s);
}
// Creamos un nuevo directorio
Directory.CreateDirectory(sdir + @"\prueba");
string [] ds = Directory.GetDirectories(sdir);
foreach (string s in ds)
{
Console.WriteLine("Directorio {0}", s);
DateTime d = Directory.GetCreationTime(s);
Console.WriteLine("Fecha creación {0}", d);
}
// Comprobamos si existe
if (Directory.Exists(sdir + @"\prueba"))
{
Directory.Delete(sdir + @"\prueba");
Console.WriteLine("Borrado el directorio de prueba");
}
Console.ReadLine();
}

Como podemos ver, no creamos ningún objeto de la clase Directory, ya que ésta es
estática. Por esta razón, para utilizar sus métodos, tenemos que indicar el directorio
sobre el que actuamos.

17
Si queremos usar un directorio en particular, podemos usar la clase DirectoryInfo la
cual contiene prácticamente los mismos métodos que Directory, pero antes de
utilizarla debemos crear un objeto en memoria. Éste se crea indicando el directorio que
queremos manipular. Veamos un ejemplo parecido al anterior:

static void Main(string[] args)


{
// Directorio actual
string sdir = Directory.GetCurrentDirectory();
DirectoryInfo di = new DirectoryInfo(sdir);
Console.WriteLine("El directorio actual es {0}", di.Name);
// También podíamos usar FullName

// Ficheros del directorio actual


FileInfo[] fis = di.GetFiles();
foreach (FileInfo fi in fis)
{
Console.WriteLine("Fichero {0}", fi.Name);
Console.WriteLine("Fecha creación {0}", fi.CreationTime);
}
Console.WriteLine();
// Creamos un nuevo directorio
di.CreateSubdirectory("prueba");
DirectoryInfo[] dis = di.GetDirectories();
foreach (DirectoryInfo d in dis)
{
Console.WriteLine("Directorio {0}", d.Name);
Console.WriteLine("Fecha de creación {0}", d.CreationTime);
}
// Comprobamos si existe
DirectoryInfo dirinfo = new DirectoryInfo(sdir + @"\prueba");

if (dirinfo.Exists)
{
dirinfo.Delete();
Console.WriteLine("Borrado el directorio de prueba");
}
dirinfo = new DirectoryInfo(sdir + @"\prueba2");
if (dirinfo.Exists)
dirinfo.Delete();
else
Console.WriteLine("No existe el directorio {0}",
dirinfo.Name);
Console.ReadLine();
}

18
USANDO FLUJOS DE ENTRADA Y SALIDA BINARIOS

Las clases BinaryReader y BinaryWriter se usan para entrada/salida binaria. En esta


parte aprenderemos como usar dichas clases con ficheros.

El siguiente programa muestra como escribir datos a un archivo binario y luego


leerlos:

using System;
/* Necesitamos importar System.IO para poder usar ficheros
* y clases de lectura y escritura */
using System.IO;

void Main()
{
try {
// Generamos un FileStream para crear el fichero
// Lo abrimos para acceso de lectura y escritura
FileStream ds = new FileStream("..\test.dat",
FileMode.Create, FileAccess.ReadWrite);
// Lo encapsulamos en un BinaryWriter
BinaryWriter bw = new BinaryWriter(ds);
// Escribimos algunos datos
bw.Write("Una cadena");
bw.Write(142);
bw.Write(97,4);
bw.Write(true);
// Lo abrimos para lectura
BinaryReader br = new BinaryReader(ds);
// Volvemos al principio
br.BaseStream.Seek(0, SeekOrigin.Begin);
// Leemos los datos
Console.WriteLine(br.ReadString());
Console.WriteLine(br.ReadInt32());
Console.WriteLine(br.ReadDouble());
Console.WriteLine(br.ReadBoolean());
} catch (Exception e) {
Console.WriteLine("Excepcion:" + e.ToString());
}
}

Comenzamos importando el ámbito System.IO el cual contiene todas las


funcionalidades de entrada/salida. Luego creamos un FileStream para operar sobre el
fichero. El segundo parámetro determina como será abierto, en este caso,
FileMode.Create el cual creará uno nuevo o lo sobrescribirá con el mismo nombre que
ya tiene. El tercero controla el acceso al fichero. En este caso, como vamos a leer y
escribir usamos FileAccess.ReadWrite.

FileStream lee y escribe bytes, lo cual es raramente conveniente, por lo que FileStream
se encapsula normalmente en otra clase que gestiona la conversión de bytes a bytes.
En este caso, usamos un BinaryWriter que toma datos primitivos de .Net y los
convierte en bytes. Estos bytes pueden ser luego pasados al FileStream.

19
BinaryWriter tiene una gran cantidad de métodos Write() sobrecargados, uno para
cada uno de los tipos primitivos. En este ejemplo, podemos ver cuatro de ellos,
escribiendo la salida como un string, un entero, un valor de punto flotante y un
booleano. Si se desea terminar el programa en este punto, debemos insertar un código
similar al siguiente después de la llamada a Write().

bw.Flush();
bw.Close();

Estas dos llamadas causan la escritura de cualquier dato que no haya sido escrito y la
llamada a Close() cerraría el flujo y el fichero. Ahora podemos usar un BinaryReader
para leer estos datos. Antes de poder usarlo hay que volver al principio del fichero.
Cuando usamos un flujo, el puntero de búsqueda marca el punto en el que tendrá
lugar la siguiente operación de lectura y escritura. Se ha estado escribiendo en el flujo,
por lo que el puntero está ahora al final del fichero preparado para hacer la siguiente
operación de escritura. Si deseamos leer algo que está en una posición anterior al
fichero, debemos reposicionar el puntero. La propiedad BaseStream obtiene una
referencia al FileStream dentro del BinaryReader y después llama al método Seek()
para cambiar la posición del FileStream. Seek() toma dos parámetros, un
desplazamiento en bytes y una posición a partir de la cual aplicar el offset. Aquí hemos
usado SeekOrigin.Begin, la cual denota el comienzo del fichero. Otros valores posibles
son SeekOrigin.End (fin de fichero) y SeekOrigin.Current (posición actual). Podemos
usar desplazamientos negativos así como positivos, por lo cual podemos
posicionarnos de manera relativa al final del fichero.

VARIABLES IMPLÍCITAS (C# 3.0)

La declaración de variables implícitas nos permite omitir el tipo de la variable local en


su declaración de forma que ésta adopte un tipo u otro dependiendo de su
inicialización.

var s = "soy un string";


var n = 13;
var a = 'a';

El compilador en este caso decide qué tipo usar en cada caso, de forma que el código
anterior es exactamente igual a:

string s = "soy un string";


int n = 13;
char a = 'a';

Ésto supone una gran ayuda a la hora de usar la sintaxis integrada para consultas
(LINQ).

20
ARRAY IMPLÍCITAMENTE TIPADOS (C# 3.0)

Esta caracerística nos permite declarar arrays sin tener que especificar su tipo, por lo
que expresiones como:

var a2 = new[] { 1, 10, 100 };

Aquí acabamos de crear un array de enteros.

PROGRAMACIÓN ORIENTADA A OBJETOS

Propiedades
Las propiedades proporcionan un modo de leer, o asignar valores a campos
privados a través de los métodos especiales de acceso de la propiedad, denominados
get y set; get utiliza la palabra reservada return para devolver el contenido del campo,
mientras que set utiliza la palabra reservada value que representa el valor a asignar al
campo. Veamos un ejemplo:

class Empleado
{
private string telefono; // campo privado telefono
//...
public string Telefono // propiedad Telefono
{
get
{
return telefono;
}
set
{
telefono=value; // value es el valor que se va a asignar
}
}
}
class usaEmpleado
{
/* Aqui va alojado el metodo Main */
public static void Main()
{
/* Llamada al constructor por defecto para crear un objeto
o instancia de la clase Empleado */
Empleado unEmpleado = new Empleado();
/* referencia a un miembro de la clase empleado */
unEmpleado.Telefono="91448723";
System.Console.WriteLine(unEmpleado.Telefono);
}
}

21
PARÁMETROS REF Y OUT

Puede que no sea siempre conveniente tener que llamar a dos funciones miembros
para obtener los valores, ya que sería mejor obtener ambos valores en una única
llamada. Sin embargo, en un método, sólo hay un valor de retorno.

Una solución es usar los parámetros de referencia (ref), de forma que los valores de los
parámetros pasados a la función miembro se puedan modificar:

using System;

class Punto
{
public Punto(int x, int y)
{
this.x=x;
this.y=y;
}
// obtiene ambos valores en una sola llamada
public void ObtenerPunto (ref int x,ref int y)
{
x=this.x;
y=this.y;
}
int x;
int y;
}
class Prueba
{
public static void Main()
{
Punto miPunto = new Punto(10,15);
int x;
int y;
// ilegal
miPunto.ObtenerPunto(ref x, ref y);
Console.WriteLine("miPunto({0},{1}",x,y);
}

}
En este código, los parámetros se han declarado usando la palabra clave ref, tal como
se observa en la llamada a la función. Este código debería funcionar, pero cuando se
compila, genera un mensaje de error que indica que se han usado valores sin inicializar
para los parámetros ref x e y. Esto significa que las variables han sido pasadas a la
función antes de la asignación de sus valores y el compilador no permitirá exponer
valores sin inicializar.

22
Una alternativa a esto es inicializar las variables cuando se declaran:

using System;

class Punto
{
public Punto(int x, int y)
{
this.x=x;
this.y=y;
}
// obtiene ambos valores en una sola llamada
public void ObtenerPunto (ref int x,ref int y)
{
x=this.x;
y=this.y;
}
int x;
int y;
}
class Prueba
{
public static void Main()
{
Punto miPunto = new Punto(10,15);
int x=0;
int y=0;
miPunto.ObtenerPunto(ref x, ref y);
Console.WriteLine("miPunto({0},{1}",x,y);
Console.ReadLine();
}

23
Ahora el programa compila correctamente, pero a costa de inicializar las variables a
cero previamente. Otra opción sería usar el parámetro out en vez de ref:

class Punto
{
public Punto(int x, int y)
{
this.x=x;
this.y=y;
}
// obtiene ambos valores en una sola llamada
public void ObtenerPunto (out int x,out int y)
{
x=this.x;
y=this.y;
}
int x;
int y;
}
class Prueba
{
public static void Main()
{
int x,y;
Punto miPunto = new Punto(10,15);
miPunto.ObtenerPunto(out x, out y);
Console.WriteLine("miPunto({0},{1})",x,y);
Console.ReadLine();
}

Los parámetros out son exactamente igual que los ref solo que se pueden pasar
variables sin inicializar y la llamada se realiza con out en lugar de ref.

Indexadores

Un indexador permite indexar un objeto de una clase definida por el programador


como si se tratara de un array, es decir, permite que se acceda a los elementos de la
clase mediante un índice que especifica su posición dentro de la colección. La
declaración de un indexador es parecida a la de una propiedad, aunque existen
algunas diferencias como que el nombre usado en la declaración siempre es this[ ] e
incluye entre los corchetes un parámetro para capturar la posición del elemento, es
decir:

public object this[int index]


{
get
{
//...
}
set
{
//...
}
}

24
Tipos anidados

Las declaraciones de tipos se pueden efectuar de forma anidada, unas dentro de otras,
para controlar su accesibilidad.

Eventos

Los eventos o sucesos se utilizan para mostrar los cambios asincrónicos de un objeto
observado. Los clientes permanecen atentos a los eventos y cuando se produce uno, se
invoca a una llamada a un método. La programación con Interfaces Gráficos de
Usuario (GUI) es una programación dirigida por eventos, ya que los componentes de
la GUI (botones, campos de texto, etc.) esperan a que el usuario ejecute alguna acción
sobre ellos, es decir, un evento. Así el espacio de nombres System.Windows.Forms
ofrece objetos visuales sobre los cuales pueden actuar los usuarios y de esta forma
comunicarse con un programa y, para ello define clases que cuentan con eventos como:

Métodos

Las funciones en C# no admiten argumentos por defecto, pero si sobrecarga, lo cual


nos permite simularlo. C#.Net tiene un asistente para métodos. Se accede a el yendo a
la Vista de Clases y con el menú contextual.

SOBRECARGA

A veces puede resultar útil tener dos funciones que hagan la misma cosa, pero con
diferentes parámetros. Esto es especialmente común en los constructores de clase,
donde pueden existir varias formas de crear un nuevo ejemplar.

class Punto
{
// Crea un nuevo punto con los valores x e y
public Punto(int x, int y)
{
this.x=x;
this.y=y;
}
// Crea un nuevo punto desde un punto existente
public Punto(Punto p)
{
this.x=p.x;
this.y=p.y;
}
int x;
int y;
}
class Prueba
{
public static void Main()
{
Punto miPunto = new Punto(10,15);
Punto miSegundoPunto=new Punto(miPunto);
}
}

25
Esta clase tiene dos constructores; uno puede ser llamado con los valores x e y, y el
otro se puede llamar con el valor de un punto. La función Main() usa ambos; uno para
crear un ejemplar a partir de un valor de x e y, y el otro para crear un ejemplar desde
otro existente (clase punto).

Cuando se llama a una función sobrecargada, el compilador elige la función adecuada


por comparación de los parámetros utilizados en la llamada con respecto a los
declarados en las distintas funciones.

SOBRECARGA DE OPERADORES

La idea es poder definir el comportamiento de un operador para una clase creada por
nosotros. Esto implica que un mismo operador puede comportarse de distinta forma
según que objeto esté implicado. Es importante reseñar que con la sobrecarga no se
sobrescribe el significado del operador, es decir, podemos seguir sumando números
con “+” aunque hayamos sobrecargado el operador. Utilizaremos la palabra clave
operador para crear el método que redefinirá al operador y este método puede recibir
un parámetro o dos, dependiendo de si se trata de un operador unario o binario. La
sintaxis es:

public static tipo_devuelto operator operador(tipo1 parametro1, [, tipo2


parametro2])
{
//código
}

El tipo devuelto es un tipo cuyo objeto debe haber sido creado por nosotros y actuará
para esa clase. Si hablamos de un operador unario, el parámetro debe ser también un
objeto de esa clase (tipo de datos), y si se trata de binario, al menos uno de los
parámetros debe serlo. En operador pondremos el que estamos sobrecargando y en el
cuerpo del método las operaciones que realizará dicho operador.

Veamos un ejemplo:

class Punto
{
int x, y;
public Punto()
{
x = y = 0;
}
public Punto(int a, int b)
{
x = a;
y = b;
}
}

A esta clase le añadimos dos métodos. El primero nos mostrará la representación de


cualquiera de nuestros puntos (sobrescribiendo ToString() heredado de la clase object

26
mediante override). El segundo método es el método operador de “+”. Primero
creamos un objeto de tipo Punto y después sumamos las coordenadas de nuestro
punto para obtener el resultado de la suma.

class Punto
{
int x, y;
public Punto()
{
x = y = 0;
}
public Punto(int a, int b)
{
x = a;
y = b;
}
public override string ToString()
{
return "(" + x + "," + y + ")";
}
public static Punto operator +(Punto pto1, Punto pto2)
{
Punto pto = new Punto();
pto.x = pto1.x + pto2.x;
pto.y = pto1.y + pto2.y;
return pto;
}
public static Punto operator +(Punto pto1, int valor)
{
Punto pto = new Punto();
pto.x = pto1.x + valor;
pto.y = pto1.y + valor;
return pto;
}
}
class Sobrecarga
{
static void Main()
{
Punto p1 = new Punto();
Punto p2 = new Punto(2, 5);
Punto p3 = new Punto(1, 3);

Console.WriteLine("Punto 1 = " + p1.ToString());


Console.WriteLine("Punto 2 = " + p2.ToString());
Console.WriteLine("Punto 3 = " + p3.ToString());
p1 = p2 + p3;
Console.WriteLine("Punto 2 + 3 = " + p1.ToString());
Console.ReadLine();
}
}

Puntualizar que con esto ocurre como con la suma ordinaria, no modificamos ninguno
de los dos operandos ya que devolvemos un nuevo objeto con las coordenadas
resultantes. Sin embargo, cuando sobrecargamos un operador podemos hacer que se
modifique un operando y devolvamos los resultados en él. En este caso no vamos a
necesitar usar ni ref ni out porque los objetos se pasan por referencia.

27
Para sobrecargar un operador unario procederemos de la misma forma, pero
proporcionando un solo parámetro al método de operador. La sintaxis es la misma,
pero hay alguna matización a nivel de funcionamiento. Debido a que sólo pasamos un
parámetro, no existe el problema del orden de los mismos, cuestión que si era
importante al sobrecargar un operador binario (supongamos una sobrecarga de una
resta). Además, en el caso de los operadores unarios del tipo ++ o – se da el caso de
que debemos modificar el valor del parámetro ya que el sentido de un incremento o
decremento unario es modificar el valor original.

public static Punto operador ++ (Punto parámetro)


{
parametro.x++;
parámetro.y++;
return parámetro;
}

En el listado anterior también podemos ver como la clase Punto tiene sobrecargada la
suma de forma que uno de los operandos no es del tipo de la clase Punto. Aquí vuelve
a ser importante el orden de los operandos. Si queremos que se acepte, al contrario,
debemos volver a sobrecargar el operador para que tenga otra firma en la cual el
primer parámetro sea un int y el segundo un punto. Una vez codificado vamos a poder
hacer p1 + 10, o 10 + p1.

SOBRECARGA DE OPERADORES RELACIONALES, TRUE/FALSE Y LÓGICOS

También podemos sobrecargar operadores relacionales de forma similar a como


hemos visto hasta ahora. Lo que debemos tener en cuenta es que hemos de sobrecargar
los operadores por parejas, es decir, si sobrecargamos “<” también debemos hacer lo
mismo con “>” (al igual que <=, ==, etc…). Por supuesto, también debemos devolver
true o false si queremos usarlos como expresiones condicionales.

También podemos sobrecargar los operadores unarios true y false, pero


obligatoriamente a la vez:

public static bool operator true(Punto parametro)


{
if ((parametro.x == 0) && (parametro.y == 0))
return false;
else
return true;
}

De esta forma, si las dos coordenadas del punto son cero, consideramos que no es true,
y en caso contrario consideramos true al punto. Análogamente definiríamos la
sobrecarga de false.

De esta forma podemos usar objetos Punto en expresiones condicionales del tipo if
(objeto) o incluso en bucles do…while (objeto).

28
También podemos sobrecargar operadores lógicos. Reseñar que solo podemos hacerlo
con AND (&), OR(|) y NOT (!). Cláramente hemos tenido que haber sobrecargado true
y false antes de esta sobrecarga, ya que es necesario saber si un objeto es true o false
para poder usar los operadores lógicos con él.

public static bool operator <(Punto parametro1, Punto parametro2)


{
if ((parametro1.x < parametro2.x) && (parametro1.y <
parametro2.y))
return true;
else return false;
}

public static bool operator &(Punto parametro1, Punto parametro2)


{
if (((parametro1.x!=0) && (parametro1.y!=0)) &
((parametro2.x!=0) && (parametro2.y!=0)))
return true;
else return false;
}

Para finalizar, reseñar que también podemos sobrecargar los operadores && y || pero
previamente hemos de haber sobrecargado & y |, pero con la condición de que ambos
devuelvan como tipo objetos de la clase y no tipos booleanos (true/false). Esta
particularidad permitirá que la sobrecarga de || y && esté automáticamente creada.
Para eso, tenemos que tener claro cómo codificar la sobrecarga de & y | de forma
correcta. La solución es sencilla: ¿qué pasaría si en el método de operador de &
devolvemos en lugar de true un nuevo objeto Punto?:

return new Punto(1, 1);

Realmente podríamos devolver cualquier objeto que fuera true, es decir, con alguna
coordenada distinta de cero. Para el caso de false, devolveríamos un objeto Punto(0,0),
un objeto false. Por supuesto, debemos también cambiar el tipo devuelto en la
definición del método de operador. De alguna manera, estamos devolviendo un objeto
true en vez de devolver true y un objeto false en vez de un false. De esta forma, al estar
sobrecargados también los operadores true y false, la expresión p1 & p2 se evalúa
correctamente. Así, hemos implementado implícitamente la sobrecarga de && y ||,
ya que una expresión del tipo p1 && p2 se evaluará mediante el operador false para
cada uno de los operandos. Si se puede determinar el resultado de la expresión (p1 es
false) se devuelve el resultado pero si no se puede, se usa el operador & que tenemos
ya sobrecargado. En el caso de ||se usa el operador true para determinar si
acudiremos al operador | sobrecargado o ya podemos determinar el resultado de la
expresión (si p1 es true, ya no hace falta seguir).

EXTENSIONES DE MÉTODOS (C# 3.0)

Nos permiten añadir métodos a los tipos sobre los que no tenemos control, como por
ejemplo, tipos ya existentes en la biblioteca de clases. Esto se consigue declarando un
método estático dentro de una clase estática que haga referencia mediante this al tipo
que se va a extender. En el siguiente ejemplo, vamos a declarar una clase que contiene

29
el método TieneEspacios que devolverá true si la palabra contiene al menos un espacio
y falso en caso contrario:

public static class Extension


{
public static bool TieneEspacios(this string cadena)
{
return cadena.Contains(" ");
}
static void Main(string[] args)
{
string s = "tengo espacios";
Console.WriteLine(s.TieneEspacios());
}
}

El primer parámetro del método va precedido de la palabra this. Esto significa que
hará referencia al tipo que va a extender. En todas las extensiones de métodos el primer
parámetro hace referencia al tipo que se extiende, posteriormente vendrían los
parámetros propios del método.

NUEVOS INICIALIZADORES EN C# 3.0

Supongamos la siguiente clase:

public class Cliente


{
public int Edad;
public string Nombre;
}

En C# 2.0 tenemos dos opciones, o construimos un inicializador que tome como


argumentos edad y apellidos para luego asignárselos a los campos de la clase, o bien
creamos un objeto y asignamos manualmente los campos:

Cliente c = new Cliente();


c.Edad = 32;
c.Nombre = "Pepito";

En C# 3.0 tenemos una forma más fácil de escribir esto mismo:

Cliente c = new Cliente { Edad = 32, Nombre = "Pepito" };

Esta sintaxis funciona tanto para campos públicos como para propiedades.

De la misma forma podemos inicializar una colección. Supongamos una lista genérica:

List<string> names = new List<string> {"Dylan", "Angelina", "Isabella"};


Esta inicialización tiene el mismo efecto que usar el método Add sobre cada elemento
de la colección.

30
Consideremos el siguiente ejemplo en C# 2.0:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace Colecciones
{
public class Autores
{
string autor;
List<string> libros = new List<string>();
public string Autor {
get { return autor; }
set { autor = value; }
}
public List<string> Libros { get { return libros; } }
}
class MyClass
{
static void Main(string[] args)
{
List<Autores> listaAutores = new List<Autores>();
Autores c1 = new Autores();
c1.Autor = "Dylan Combel";
c1.Libros.Add("ISBN: 555-62280-58");
c1.Libros.Add("ISBN: 555-71180-59");
listaAutores.Add(c1);
Autores c2 = new Autores();
c2.Autor = "Isabella Abolrous";
c2.Libros.Add("ISBN: 555-72390-88");
c2.Libros.Add("ISBN: 555-65412-77");
listaAutores.Add(c2);
Console.WriteLine(c1.Autor + ":\n" + c1.Libros[0] + "\n"
+ c1.Libros[1]);
Console.WriteLine(c2.Autor + ":\n" + c2.Libros[0] + "\n"
+ c2.Libros[1]);
Console.ReadKey(true);
}
}
}

Mostrará como salida lo siguiente:

Dylan Combel:
ISBN: 555-62280-58
ISBN: 555-71180-59
Isabella Abolrous:
ISBN: 555-72390-88
ISBN: 555-65412-77

En C# 3.0 podemos escribir menos código para expresar el mismo concepto:

var listaAutores = new List<Autores>{


new Autores{Autor="Dylan Combel",Libros={"ISBN:
555-62280-58", "ISBN: 555-71180-59" }},
new Autores{Autor="Isabella
Abolrous",Libros={"ISBN: 555-72390-88", "ISBN: 555-65412-77"}}
};

31
TIPOS ANÓNIMOS (C# 3.0)

Permiten crear tipos a partir de otros ya existentes. Se trata de usar new junto a un
inicializador para crear un tipo anónimo:

var p1 = new { Nombre = "Puerta", Precio = 200 };

Esto crea una variable p1 de tipo anónimo con dos campos, uno de tipo string y otro
int. Si quisiéramos crear esto en versiones anteriores de C# tendríamos que recurrir a
una clase con dos campos, crear su constructor y asignarle los valores que
correspondan. De esta forma, hacemos algo similar con una sola línea. Reseñar que los
miembros de este tipo anónimo son de sólo lectura.

CLASES BASE Y HERENCIA

Veamos la siguiente clase, la cual implementa un objeto Ingeniero y los métodos para
gestionar la facturación de ese ingeniero.

using System;
class Ingeniero
{
public Ingeniero(string nombre, float tarifa)
{
this.nombre=nombre;
this.tarifa=tarifa;
}
// calcula el cargo basado en la tarifa del ingeniero
public float CalculaCargo(float horas)
{
return (horas*tarifa);
}
public string NombreTipo()
{
return ("Ingeniero");
}
private string nombre;
protected float tarifa;
}
class Prueba
{
public static void Main()
{
Ingeniero ingeniero=new Ingeniero("Hank",21.20F);
Console.WriteLine("El nombre es: {0}",ingeniero.NombreTipo());
Console.ReadLine();
}
}

La clase Ingeniero servirá como clase base para este caso. Contiene el campo privado
nombre y el protegido tarifa. El modificador protected garantiza el acceso como
privado, salvo las clases que están derivadas de esta misma, que también tienen acceso
al campo. Por lo tanto, protected se utiliza para dar acceso a un campo a las clases

32
derivadas de una determinada clase, mientras que, para el resto de las clases, ese
campo está protegido

HERENCIA SIMPLE

Un ingeniero civil (IngenieroCivil) también es un tipo de ingeniero, y por lo tanto,


puede derivarse a partir de la clase Ingeniero:

class IngenieroCivil:Ingeniero
{
public IngenieroCivil(string nombre, float
tarifa):base(nombre,tarifa){}
//nueva función, porque difiere de la clase base
public new float CalculaCargo(float horas)
{
if (horas< 1.0F) horas=1.0F;
return (horas*tarifa);
}
public new string NombreTipo()
{
return ("Ingeniero Civil");
}
}
class Prueba
{
public static void Main()
{
Ingeniero i=new Ingeniero("Jorge",15.50F);
IngenieroCivil c = new IngenieroCivil("Juan",40F);
Console.WriteLine("El cargo de {0} es
{1}",i.NombreTipo(),i.CalculaCargo(2F));
Console.WriteLine("El cargo de {0} es
{1}",c.NombreTipo(),c.CalculaCargo(0.75F));
Console.ReadLine();
}
}

Como la clase IngenieroCivil se deriva de Ingeniero, hereda todos sus miembros de


datos (aunque el miembro nombre no puede ser accedido porque es privado) y
también hereda la función miembro CalculaCargo.

Los constructores no se heredan, por lo tanto debemos escribir uno específico para la
clase IngenieroCivil. El constructor no tiene nada en especial para hacer, por lo tanto,
simplemente llama al constructor de la clase Ingeniero usando la sintaxis base. Si la
llamada al constructor de la clase base fuera omitida, el compilador haría una llamada
al constructor de la clase base sin parámetros.

La clase IngenieroCivil tiene una forma distinta de calcular los cargos; el cargo mínimo
es de una hora, por lo cual existe una nueva versión CalcularCargo()

33
Métodos virtuales

Es posible cambiar la definición de métodos en la clase hija, para lo que debemos


previamente en la clase base de preceder el método con la palabra reservada virtual.

A este tipo de métodos se les llama métodos virtuales, y la sintaxis que se usa para
definirlos es la siguiente:

virtual <tipoDevuelto> <nombreMétodo>(<parámetros>)


{
<código>
}

Si en alguna clase hija quisiésemos dar una nueva definición del <código> del método,
simplemente lo volveríamos a definir en la misma pero sustituyendo en su definición
la palabra reservada virtual por override. Es decir, usaríamos esta sintaxis:

override <tipoDevuelto> <nombreMétodo>(<parámetros>)


{
<nuevoCódigo>
}

Nótese que esta posibilidad de cambiar el código de un método en su clase hija sólo se
da si en la clase padre el método fue definido como virtual. En caso contrario, el
compilador considerará un error intentar redefinirlo.

El lenguaje C# impone la restricción de que toda redefinición de método que queramos


realizar incorpore la partícula override para forzar a que el programador esté seguro
de que verdaderamente lo que quiere hacer es cambiar el significado de un método
heredado. Así se evita que por accidente defina un método del que ya exista una
definición en una clase padre. Además, C# no permite definir un método como
override y virtual a la vez, ya que ello tendría un significado absurdo: estaríamos
dando una redefinición de un método que vamos a definir.

Por otro lado, cuando definamos un método como override ha de cumplirse que en
alguna clase antecesora (su clase padre, su clase abuela, etc.) de la clase en la que se ha
realizado la definición del mismo exista un método virtual con el mismo nombre que
el redefinido. Si no, el compilador informará de error por intento de redefinición de
método no existente o no virtual. Así se evita que por accidente un programador crea
que está redefiniendo un método del que no exista definición previa o que redefina un
método que el creador de la clase base no desee que se pueda redefinir.

34
Para aclarar mejor el concepto de método virtual, veamos un ejemplo:

using System;
public class Persona
{
public string Nombre; // Campo de cada objeto Persona que almacena
su nombre
public int Edad; // Campo de cada objeto Persona que
almacena su edad
public string NIF; // Campo de cada objeto Persona que
almacena su NIF
public virtual void Cumpleaños() // Incrementa en uno de la edad del
objeto Persona
{
Edad++;
Console.WriteLine(“Incrementada edad de persona”);
}

public Persona (string nombre, int edad, string nif) // Constructor


{
Nombre = nombre;
Edad = edad;
NIF = nif;
}
}
public class Trabajador: Persona
{
public int Sueldo; // Campo de cada objeto Trabajador que almacena
cuánto gana
Trabajador(string nombre, int edad, string nif, int sueldo):
base(nombre, edad, nif)
{
// Inicializamos cada Trabajador en base al constructor de Persona
Sueldo = sueldo;
}
public override void Cumpleanyos()
{
Edad++;
Console.WriteLine(“Incrementada edad de trabajador”);
}
public static void Main()
{
Persona p = new Persona("Carlos", 22, "77588261-Z");
Trabajador t = new Trabajador("Josan", 22, "77588260-Z", 100000);
t.Cumpleanyos();
p.Cumpleanyos();
}
}

35
Nótese cómo se ha añadido el modificador virtual en la definición de Cumpleanyos()
en la clase Persona para habilitar la posibilidad de que dicho método puede ser
redefinido en clase hijas de Persona y cómo se ha añado override en la redefinición del
mismo dentro de la clase Trabajador para indicar que la nueva definición del método
es una redefinición del heredado de la clase. La salida de este programa confirma que
la implementación de Cumpleaños() es distinta en cada clase, pues es de la forma:

Incrementada edad de trabajador


Incrementada edad de persona

También es importante señalar que para que la redefinición sea válida ha sido
necesario añadir la partícula public a la definición del método original, pues si no se
incluyese se consideraría que el método sólo es accesible desde dentro de la clase
donde se ha definido, lo que no tiene sentido en métodos virtuales ya que entonces
nunca podría ser redefinido. De hecho, si se excluyese el modificador public el
compilador informaría de un error ante este absurdo. Además, este modificador
también se ha mantenido en la redefinición de Cumpleaños() porque toda redefinición
de un método virtual ha de mantener los mismos modificadores de acceso que el
método original para ser válida.

INTERFACES

Una interfaz es una construcción lógica que define los métodos que implementa una
clase, pero sin desarrollar dicha implementación. Con esto podemos separar la
definición de una clase de su implementación. Una vez escrito la interfaz, puede ser
implementado por varias clases al mismo tiempo y de distinta forma. Igualmente, una
clase puede implementar varios interfaces. Si varias clases implementan una interfaz,
cada una puede hacerlo de distinta forma, pero contendrán el mismo grupo de
métodos a pesar de que cada una puede añadir los suyos propios.

La sintaxis de una interfaz es la siguiente:

modif_acceso interface nombre_interface {


tipo nombre_metodo(parámetros);
tipo nombre_metodo(parámetros);

}

Toda clase que incluya una interfaz debe implementar todos sus métodos y estos son
public por defecto. No se puede incluir el modificador de acceso a ellos. Además, en
una interfaz se pueden definir las firmas de propiedades, etc… y no se pueden definir
ni constructores ni destructores.

Una vez definido la interfaz, en la clase se implementa igual que con la herencia, es
decir, detrás del nombre de la clase, dos puntos, y el nombre de la interfaz.

36
class nombre_clase:nombre_interface{ }

Si en una clase deseamos implementar más de una interfaz, estas irán separados por
comas. Si además la clase hereda de otra, la clase base tendrá que ser la primera de la
lista. El hecho de que una clase implemente métodos de un interfaz no implica que no
pueda tener los suyos propios. En el siguiente método hemos definido el interfaz con
un solo método para luego escribir el código de nuestra clase implementando dicho
método más otro adicional. En este caso, hemos escrito el código en el mismo fichero,
pero lo normal es tener los interfaces en un fichero independiente, con lo cual hemos
de compilar los ficheros juntos para obtener el ejecutable:

csc interfaces.cs Irevista.cs

using System;
public interface Irevista {
double coste_pagina();
}
public class revista:Irevista {
public string nombre;
public int paginas;
public double precio;
public double coste_pagina(){
return (this.precio/this.paginas);
}
public double pagina_por_euro(){
return (this.paginas/this.precio);
}
}
public class interfaces{

public static void Main()


{
revista arroba=new revista();
arroba.nombre="@rroba";
arroba.paginas=114;
arroba.precio=4.95;
Console.WriteLine("La revista se llama {0}",arroba.nombre);
Console.WriteLine("tiene {0} páginas y vale {1}
euros.",arroba.paginas,arroba.precio);
Console.WriteLine("La página sale a {0}
euros.",arroba.coste_pagina());
Console.WriteLine("Cada euro sale a {0}
página/s.",arroba.pagina_por_euro());
}
}

También podríamos haber declarado las variables nombre, páginas y precio como
propiedades dentro de la interfaz y lo hubiéramos hecho así:

string nombre {
get;
set;
}

37
Otra cuestión importante es que una interfaz puede heredar de otro y, como podemos
suponer, la sintaxis es la misma que con las clases. Lo importante es que si una clase
utiliza un interfaz, el cual hereda de otro, la clase debe implementar todos los métodos
de las interfaces. Tanto los de la interfaz que utiliza, como los de la interfaz de la cual
hereda del otro. Si no se implementan todos los métodos, obtendremos un error de
compilación.

Debido a la herencia y a que una clase puede implementar varios interfaces, puede
darse el caso de que una interfaz tenga definido un método que tenga exactamente la
misma firma que otro que se encuentre en otra interfaz, y los dos vayan a ser
implementados por la misma clase. Para estos casos se usa la implementación explícita,
la cual hace uso del nombre del interfaz junto con el del método para identificar
unívocamente dicha implementación. Es decir, en la implementación de un método M
de una interfaz Iprueba en la clase ejemplo, deberíamos poner:

class ejemplo : Iprueba{


int Iprueba.M(){
// Implementación de M
}
}

Como norma general, usaremos las interfaces cuando podamos definir el “qué hacer”
sin necesidad de definir “el cómo lo hace”, en otro caso, usaremos clases abstractas.
Un ejemplo del uso de interfaces podría aplicarse a lo que vamos a ver a continuación:
cifrado de datos. Podríamos definir una interfaz con el método encripta() y con el
método desencripta() y desarrollarlos luego en la clase correspondiente, dependiendo
de qué algoritmo de cifrado estuviésemos hablando. Es lo que hemos dicho antes:
Definimos “lo que hace” y “el como” ya lo implementaremos según el tipo de cifrado.
Además, las diferencias fundamentales que nos permiten decidir entre una clase
abstracta y una interface son:

- La clase abstracta no obliga a que una clase derivada implemente todos sus
métodos abstractos. En una interfaz es obligatorio.
- Una clase solo puede derivar de una clase base (abstracta o no), y una interfaz
puede hacerlo de más de una interface. Una clase puede heredar de otra clase y
de varios interfaces al mismo tiempo.

Por último, comentar que las interfaces proporcionan una propiedad que se puede
consultar con el operador is. Si el resultado de la expresión:

if (instancia_clase is ImiInterface)

es verdadero, podemos asegurar que el programa implementa las propiedades y


métodos de la interfaz ImiInterface. Como podemos imaginar, esto puede llegar a ser
muy útil en tiempo de ejecución

38
DELEGADOS Y EVENTOS

En .NET para definir el tipo de un evento, se define un delegado que no es más que
una declaración en la que se asigna un nombre a ese tipo y se le facilita la lista de
parámetros. Suponiendo que fuesemos a crear un componente que generase
periódicamente un evento, como si fuese un tic de reloj, podríamos definir el delegado
siguiente:

public delegate void OnTickReloj(int Ticks);

De no ser por la palabra clave delegate, esta declaración es idéntica a la de cabecera de


inicio de cualquier método. Cualquier clase que quisiera generar un evento de este
tipo, definirá un campo usando la palabra clave event seguida del tipo de delegado y
el nombre que tendrá el evento en dicha clase. En el fragmento siguiente, por ejemplo,
la clase Reloj contaría con un evento llamado Tick del tipo OnTickReloj. Esto significa
que dicho evento irá acompañado de un parámetro entero que se facilitará al gestor
que desee recibirlo.

public class Reloj{


public event OnTickReloj Tick;
}

Esta clase Reloj, cuando quisiera generar el evento lo primero que tendría que hacer es
comprobar si Tick es nulo, o por el contrario, se le ha asignado algún valor. Es posible
que el usuario de la clase haya optado por no usar el evento, en cuyo caso no
deberíamos usar entonces Tick para realizar la llamada.

La parte complementaria a la clase que genera el evento es la clase que lo recibe,


disponiendo para ello un gestor de evento apropiado con la lista de parámetros que
corresponda. Para asignar el evento se usa el operador += como podemos ver en el
siguiente programa. En él la clase Reloj genera el evento Tick descrito, que es recibido
por la clase Aplicación. En principio se llama al método Inicio() sin haber asignado un
gesto red eventos, por lo que el evento Tick en realidad no llega a producirse.

39
A continuación, se efectúa dicha asignación y se llama de nuevo a Inicio().
using System;
namespace Curso.Csharp.Eventos
{
// Definimos el tipo de evento
public delegate void OnTickReloj(int Ticks);
// Esta clase cuenta con un evento
// del tipo OnTickReloj
public class Reloj
{
public event OnTickReloj Tick;

// inicialmente el evento no está asignado


public Reloj() { Tick = null; }

// Este método simplemente


public void Inicio()
{
// simula un reloj que genera una señal
// cada 100 tics
for(int N=0; N<1000; N++)
if((N % 100 == 0) && Tick != null)
Tick(N); // generando el evento
}
}
// Esta es otra clase que hace uso de la anterior
class Aplicacion
{
Aplicacion()
{ // creamos un reloj
Reloj MiReloj = new Reloj();

// y llamamos al método Inicio() antes de


// asignar el gestor
Console.WriteLine(
"Antes de asignar el gestor de evento");
MiReloj.Inicio();

// asignamos el gestor
MiReloj.Tick += new OnTickReloj(TickReloj);
Console.WriteLine(
"Después de asignar el gestor de evento");
Console.ReadLine(); // esperamos Intro
MiReloj.Inicio(); // y repetimos
}

// Al recibir el evento
void TickReloj(int Ticks)
{
// simplemente mostramos el valor
Console.WriteLine(Ticks);
}

static void Main()


{
Aplicacion MiAplicacion = new Aplicacion();
}
}
}

40
MÉTODOS ANÓNIMOS

Se basan en la indicación “in situ” del código gestor del evento de forma que no sea
necesario definir un método explícitamente. El compilador es el que se encarga
mediante inferencia de tipos de deducir la firma del método y de crearlo por nosotros.
Por ejemplo, la programación click de un botón podría quedar así:

// Con método anónimo


btn.Click += delegate
{
MessageBox.Show("Saludos desde C#");
};

La especificación de un método anónimo comienza con la palabra reservada delegate


a la que sigue el bloque de código entre llaves, llevando la de cierre un punto y coma.

Opcionalmente podemos especificar argumentos de llamada a continuación de


delegate. En el código a continuación partimos de la clase empleado:

public class Empleado : Persona


{
// campos
private string empresa = null;
private decimal salario;

// constructores
public Empleado(string nombre, SexoPersona sexo, string empresa,
decimal salario)
: base(nombre, sexo)
{
this.Empresa = empresa;
this.salario = salario;
}

// propiedades
public string Empresa
{
get { return empresa; }
set
{
if (value == null)
throw new Exception("Empresa obligatoria");
else
empresa = value;
}
}
public decimal Salario
{
get { return salario; }
set
{
decimal anterior = salario;
salario = value;
if (salario > anterior &&
ExclamacionAlSubirSalario != null)
{
// disparar evento

41
ExclamacionAlSubirSalario(this, new
CambioSalarioEventArgs(anterior, salario));
}
}
}

// métodos
public override string ToString()
{
return base.ToString() + Environment.NewLine +
"Empresa: " + empresa + " Salario: " +
salario.ToString("#,##0.00");
}

// eventos
public event Exclamacion_CambioSalario
ExclamacionAlSubirSalario;
}

public class CambioSalarioEventArgs : EventArgs


{
public decimal SalarioAntes;
public decimal SalarioDespues;
//
public CambioSalarioEventArgs(decimal antes,
decimal despues)
{
SalarioAntes = antes;
SalarioDespues = despues;
}
}

pepe.ExclamacionAlSubirSalario +=
delegate(object s2, CambioSalarioEventArgs e2)
{
MessageBox.Show(((Empleado)s2).Nombre + " está contento");
MessageBox.Show("Antes ganaba: " + e2.SalarioAntes.ToString("#,##0.00"));
MessageBox.Show("Ahora gana: " + e2.SalarioDespues.ToString("#,##0.00"));
};

PASO DE DELEGADOS ANÓNIMOS COMO PARÁMETROS

Los delegados anónimos pueden usarse no sólo en contextos relacionados con la


asignación de delegados a eventos, sino también en cualquier otro contexto en el que
haga falta un objeto de tipo delegado. Por ejemplo, un hilo de ejecución. Los
constructores de System.Threading.Thread solicitan un parámetros de uno de los tipos
ThreadStart o ParametrizedThreadStart definido de la siguiente forma:

public delegate void ThreadStart();


public delegate void ParametrizedThreadStart(object obj);

El siguiente código despliega un hilo de ejecución paralelo que incrementa un


contador y lo muestra en una etiqueta de la ventana principal de la aplicación. El
código a ejecutar se suministra en un método anónimo. Las actualizaciones de
controles desde hilos secundarios en aplicaciones Windows Forms deben realizarse
mediante llamadas al método Invoke() del control. Por tanto, hemos realizado un
método anónimo para indicar el código a ejecutar de forma segura.

42
private void Hilo(object sender, EventArgs e)
{
new Thread(
delegate(){
int i = 1;
while (true)
{
lbl.Invoke(
(MethodInvoker)delegate{
lbl.Text = i.ToString();
});
Thread.Sleep(1000);
i++;
}

}
).Start();
}

Observemos la necesidad de aplicar una conversión explícita al segundo delegado


anónimo para convertirlo al tipo MethodInvoker definido en Windows.Forms como:

public delegate void MethodInvoker();

El argumento del método Control.Invoke() es un delegado y en este caso (aun cuando


este tipo es el padre de todos los delegados) el compilador no puede inferir cuáles
deben ser los tipos de los parámetros o el valor de retorno del método a generar.
Mediante la conversión explícita le suministramos esa información.

43
MÉTODOS ANÓNIMOS: ACCESO A VARIABLES DE ÁMBITOS EXTERNOS

Una de las posibilidades más interesantes de este tipo de métodos es la de acceder a


las variables de contexto que rodean al sitio en el que están definidas. Esto incluye
(peroi no está limitado a) las variables locales del método dentro del que están
definidos, los parámetros del mismo, siempre que sean de entrada (no ref o out) y los
campos de la clase a la que pertenece el método que a su vez contiene el método
anónimo.

Supongamos que el código cliente quiere sumar los salarios de los empleados que
reciben aumentos. En este caso podríamos declarar una variable local al método que
contiene al anónimo y usarla para acumular los salarios:

// Acceso al ámbito externo


private void Exclamacion2(object sender, EventArgs e)
{
Empleado[] arr = {
new Empleado("Octavio", SexoPersona.Varón, string.Empty,
1440M),
new Empleado("Sergio", SexoPersona.Varón, string.Empty,
1210M),
new Empleado("Denis", SexoPersona.Varón, string.Empty,
950.55M)
};
decimal suma = 0;
foreach (Empleado emp in arr)
{
emp.ExclamacionAlSubirSalario +=
delegate(object s2, CambioSalarioEventArgs e2)
{
MessageBox.Show(((Persona)s2).Nombre + " está contento");
MessageBox.Show("Antes ganaba: " +
e2.SalarioAntes.ToString("#,##0.00"));
MessageBox.Show("Ahora gana: " +
e2.SalarioDespues.ToString("#,##0.00"));
// acceso a variable de ámbito externo
suma += e2.SalarioDespues;
};
emp.Salario = decimal.Round(1.03M * emp.Salario, 2);
}
MessageBox.Show("El salario total es: " + suma.ToString("###,##0.00"));
}

En general, a las funciones que son capaces de acceder a las variables del entorno que
las rodea se les conoce como clausuras (closures). Se trata de un recurso tradicional en
la programación funcional que aparecen por primera vez en un lenguaje OO con C#.

44
PATRÓN DE DISEÑO SINGLETON PARA MULTIPROCESO

Volatile se puede utilizar para implementar versiones de subroceso seguro de una


clase singleton. La implementación tradicional usaría lock:

using System;
static object sync = new object();
static Singleton singleton=null;
private Singleton(){}

public static Singleton GetSingleton()


{
lock(sync)
{
if (singleton==null)
singleton=new Singleton();
return singleton;
}
}

Este código funciona bien, pero resulta un derroche; la sincronización es sólo


realmente necesaria la primera vez que se llama al método. En este caso, el bloqueo
necesita estar en una variable estática porque el método también lo es.

Con volatile tendríamos una versión más correcta:

using System;
static object sync = new object();
static volatile Singleton singleton=null;
private Singleton(){}

public static Singleton GetSingleton()


{
lock(sync)
{
if (singleton==null)
singleton=new Singleton();
return singleton;
}
}

Esta versión tiene un mejor rendimiento ya que la sincronización sólo se requiere si el


objeto no ha sido creado.

PATRONES DE DISEÑO. PATRÓN DE CREACIÓN “ABSTRACT FACTORY”

Este tipo de patrón proporciona un contrato para la creación de familias de objetos


relacionados o dependientes sin tener que especificar su clase concreta. También se le
conoce como Kit, Toolkit (grupo de herramientas)

Vamos a suponer que queremos crear un sistema para gestionar direcciones y


teléfonos como parte de una aplicación. Inicialmente podemos crear clases para
representar la dirección y el teléfono y escribirlas de forma que almacenen la

45
información relevante y fuercen las reglas de negocio sobre su formato. Por ejemplo,
en USA los números de teléfono están limitados a 10 dígitos y el código postal tiene
un formato particular.

Una vez escritas estas clases, puede ocurrir que necesitemos gestionar direcciones y
números de teléfono de otros paises, por ejemplo, Holanda, con sus diferentes reglas
de negocio.

Abstract Factory resuelve este problema. Usando este patrón definimos una clase
FabricaDirecciones (un framework genérico para generar objetos que siguen un patrón
general para Dirección y NumTelefono. En tiempo de ejecución esta fábrica se asocia
con un número concreto de fábricas para distintos paises y cada uno de ellos tiene su
propia versión de las clases Dirección y NumTelefono.

En vez de tener que pasar por el trance de añadir lógica funcional a las clases,
extendemos Direccion a DireccionEsp y NumTelefono a NumTelefonoEsp. Las
instancias de estas clases concretas se crean a partir de FabricaDireccionEsp. Esto nos
da mayor libertad a la hora de extender el código sin tener que realizar grandes
modificaciones a la estructura del resto del sistema.

Vamos a ver esto con un ejemplo. El código a continuación muestra como integrar las
direcciones y los números de teléfono internacionales en nuestro Gestor de
Información personal con el patrón Abstract Factory. La interfaz FabricaDirecciones
representa la fábrica en si misma.

public interface FabricaDirecciones


{
Direccion crearDireccion();
NumTelefono crearNumTelefono();
}

46
FabricaDirecciones define dos métodos de creación que generan los productos
abstractos Direccion y NumTelefono, que definen los métodos que soportan esos
productos.

public abstract class Direccion


{
private string calle;
private string ciudad;
private string region;
private string codigoPostal;

public const string EOL_STRING = "\n";


public const string SPACE = " ";

public string obtenerCalle() { return calle; }


public string obtenerCiudad() { return ciudad; }
public string obtenerCodigoPostal() { return codigoPostal; }
public string obtenerRegion() { return region; }
public abstract string obtenerPais();
public virtual string obtenerDireccionCompleta()
{
return calle + SPACE + codigoPostal + EOL_STRING;
}

public void asignarCalle(string nuevaCalle) { calle = nuevaCalle; }


public void asignarCiudad(string nuevaCiudad) { ciudad =
nuevaCiudad; }
public void asignarRegion(string nuevaRegion) { region =
nuevaRegion; }
public void asignarCodigoPostal(string nuevoCodigoPostal) {
codigoPostal = nuevoCodigoPostal; }
}

public abstract class NumTelefono


{
private string numTelefono;
public abstract string obtenerCodigoPais();
public string obtenerNumTelefono() { return numTelefono; }
public virtual void asignarNumTelefono(string nuevoNumero)
{
try
{
long.Parse(nuevoNumero);
numTelefono = nuevoNumero;
}
catch (System.FormatException exc) { }
}
}

47
En este ejemplo, definimos Direccion y NumTelefono como clases abstractas, pero
podrían haberse definido como interfaces si no fuera necesario definir el código
utilizado por todos los productos concretos.

public class FabricaDireccionesUS : FabricaDirecciones


{
public Direccion crearDireccion()
{
return new DireccionUS();
}
public NumTelefono crearNumTelefono()
{
return new NumTelefonoUS();
}
}

public class DireccionUS : Direccion


{
private const string PAIS = "ESTADOS UNIDOS";
private const string COMA = ",";
public override string obtenerPais() { return PAIS; }
public override string obtenerDireccionCompleta()
{
return (obtenerCalle() + EOL_STRING + obtenerCiudad() + COMA +
SPACE + obtenerRegion() + SPACE + obtenerCodigoPostal() + EOL_STRING + PAIS
+ EOL_STRING);
}
}

public class NumTelefonoUS : NumTelefono


{
private const string CODIGO_PAIS = "01";
private const int LONGITUD_NUMERO = 10;
public override string obtenerCodigoPais() { return CODIGO_PAIS; }
public override void asignarNumTelefono(string nuevoNumero)
{
if (nuevoNumero.Length == LONGITUD_NUMERO)
{
base.asignarNumTelefono(nuevoNumero);
}
}
}

48
El framework genérico para FabricaDirecciones, Direccion y NumTelefono facilita la
extensión del sistema para dar cabida a paises adicionales. Para cada país adicional
habría que definir una clase fábrica concreta adicional y la clase producto concreto
correspondiente. Este sería el código:
public class FabricaDireccionesEs : FabricaDirecciones
{
public Direccion crearDireccion()
{
return new DireccionES();
}
public NumTelefono crearNumTelefono()
{
return new NumTelefonoES();
}
}

public class DireccionES : Direccion


{
private const string PAIS = "ESPAÑA";
private const string COMA = ",";
private const string PARENTESISI = "(";
private const string PARENTESISD = ")";
public override string obtenerPais() { return PAIS; }
public override string obtenerDireccionCompleta()
{
return obtenerCalle() + EOL_STRING + PARENTESISI +
obtenerCodigoPostal() + PARENTESISD + SPACE + obtenerCiudad() + COMA +
SPACE + obtenerRegion() + EOL_STRING + PAIS + EOL_STRING;
}
}

public class NumTelefonoES : NumTelefono


{
private const string CODIGO_PAIS = "34";
private const int LONGITUD_NUMERO = 9;
public override string obtenerCodigoPais() { return CODIGO_PAIS; }
public override void asignarNumTelefono(string nuevoNumero)
{
if (nuevoNumero.Length == LONGITUD_NUMERO)
{
base.asignarNumTelefono(nuevoNumero);
}
}
}

PATRONES DE DISEÑO. PATRÓN DE CREACIÓN BUILDER (CONSTRUCTOR)

El propósito de este patrón es simplificar la creación de objetos definiendo una clase


que se encargue de construir instancias de otra clase. Vamos a suponer que en nuestra
aplicación los usuarios tal vez quieran gestionar un calendario de citas. Para este
objetivo, podemos definir una clase llamada Cita para la información de cada evento
de forma que pueda recoger información como fechas de inicio y finalización,
descripción de la cita, lugar de encuentro, etc.

49
Normalmente, esta información se introduce por un usuario cuando se crea la cita por
lo que se define un constructor que permite establecer el estado del nuevo objeto Cita.

La creación de este objeto se vuelve compleja cuando pensamos en lo que se necesita


para crear una cita, ya que depende muy mucho del tipo concreto de cita. Unas pueden
necesitar una lista de asistentes, otras pueden tener fecha de inicio y de fin y otras una
única feha, etc…

Para realizar esta tarea tenemos dos opciones, si bien ninguna de ellas es especialmente
atractiva. Podemos crear constructores para cada tipo de cita que deseemos tener, o
podemos escribir un constructor enorme con mucha lógica funcional. Cualquiera de
ellas tiene inconvenientes: con muchos constructores, la lógica de llamadas puede ser
compleja; con un solo constructor, pero mucha lógica funcional, el código se hace más
complejo y difícil de depurar. Ambas soluciones nos pueden dar problemas si hay que
crear subclases de Cita.

La alternativa a esto consiste en delegar la responsabilidad de la creación de citas a


una clase especial, ConstructorCita, lo que simplificará enormemente el código de la
propia clase Cita. Esta clase constructora contiene métodos para crear las partes de
Cita, y puede llamar a los métodos de ConstructorCita que son relevantes para cada
tipo de cita. Además, esta clase puede asegurar que la información que se recibe
cuando se crea la cita es válida ayudando así a que se cumplan las reglas de negocio.
Si necesitamos crear subclases de Cita, podemos crear otra clase constructor o heredar
de la que ya existe.

Una de las cosas que puede hacer este patrón es forzar la construcción por etapas de
un objeto complejo. Supongamos que estamos construyendo un pedido de compra.
Necesitaremos asegurarnos de que está en un estado concreto antes de construir el
método de compra, porque ese estado impactaría en los impuestos de venta aplicados
al producto.

Para implementar este patrón necesitaremos:

- Director (director): tiene una referencia a una instancia de AbstractBuilder. El


director llama a los métodos de creación de una instancia concreta de
AbstractBuilder para tener todas las partes necesarias y así poder construir el
objeto
- AbstractBuilder (constructor abstracto): es la interfaz que define los métodos
disponibles para crear las distintas partes del producto.
- ConcreteBuilder (constructor concreto): la clase que implementa la interfaz
AbstractBuilder. Esta clase implementa todos los métodos necesarios para crear
un objeto Product real. La implementación de los métodos sabe cómo procesar
la información del Director y cómo construir las respectivas partes de un
producto. La clase ConcreteBuilder también tiene un método getProduct o un
método de creación que devuelve una instancia de Product

50
- Product (producto): El objeto resultante. Podemos definir el producto como una
interfaz (es la mejor opción) o como una clase.

Vamos a ver como podemos utilizar este patrón para construir una cita para nuestra
aplicación. Veamos el propósito de cada clase:
- ConstructorCita, ConstructorReunión: clases Builder
- Calendario: clase Director
- Cita: clase Producto
- Dirección, Contacto: clases auxiliares utilizadas para guardar la información
relevante para Cita
- InformacionRequeridaExcepcion: clase Excepción producida cuando se
necesitan más datos

Para el patrón base, ConstructorCita gestiona la creación de un producto complejo,


que aquí es un producto Cita. El ConstructorCita utiliza una serie de métodos de
construcción (construirCita, construirLocalizacion, construirFechas y
construirAsistentes) para crear un objeto Cita y completarlo con datos. El objeto Cita
entre otros, va a constar de un objeto Contacto:

public class Contacto


{
public const string ESP = " ";
private string nombre, apellidos, titulo, organizacion;
public Contacto(string nuevoNombre, string nuevoApellidos, string
nuevoTitulo, string nuevaOrganizacion)
{
nombre = nuevoNombre;
apellidos = nuevoApellidos;
titulo = nuevoTitulo;
organizacion = nuevaOrganizacion;
}
public string Nombre
{
get { return nombre; }
set { nombre = value; }
}
public string Apellidos
{
get { return apellidos; }
set { apellidos = value; }
}
public string Titulo
{
get { return titulo; }
set { titulo = value; }
}
public string Organizacion
{
get { return organizacion; }
set { organizacion = value; }
}
public override string ToString() {
return nombre + ESP + apellidos;
}
}

51
El código a continuación define la clase Cita:
public class Cita
{
public const string CR = "\n";
private DateTime fechaInicio, fechaFin;
private string descripcion, localizacion;
private ArrayList asistentes = new ArrayList();
public DateTime FechaInicio
{
get { return fechaInicio; }
set { fechaInicio = value; }
}
public DateTime FechaFin
{
get { return fechaFin; }
set { fechaFin = value; }
}
public string Descripcion
{
get { return descripcion; }
set { descripcion = value; }
}
public ArrayList Asistentes
{
get { return asistentes; }
set
{
if (asistentes != null)
asistentes = value;
}
}
public string Localizacion
{
get { return localizacion; }
set { localizacion = value; }
}
public void AgregarAsistente(Contacto asistente)
{
if (!asistentes.Contains(asistente))
asistentes.Add(asistente);
}
public void EliminaAsistente(Contacto asistente)
{
asistentes.Remove(asistente);
}
public override string ToString()
{
string strasistentes ="" ;
foreach (Contacto a in asistentes)
{
if (strasistentes == "")
strasistentes = a.ToString();
else
strasistentes += ", " + a.ToString();
}
return " Descripcion: " + descripcion + "\nFecha de comienzo: "
+ fechaInicio + "\nFecha fin: " + fechaFin + "\nLocalización: " +
localizacion + "\nAsistentes: " + strasistentes;
}

52
Cuando nos falte información, saltará la siguiente excepción:

public class InformacionRequeridaExcepcion : Exception


{
private const string DESCRIPCION = "La cita no puede ser creada
porque se necesita más información";
public const int FECHA_INICIO_REQUERIDA = 1;
public const int FECHA_FIN_REQUERIDA = 2;
public const int DESCRIPCION_REQUERIDA = 4;
public const int ASISTENTES_REQUERIDOS = 8;
public const int LOCALIZACION_REQUERIDA = 16;
private int informacionRequerida;
public InformacionRequeridaExcepcion(int itemsRequeridos)
{
informacionRequerida = itemsRequeridos;
}
public int InformacionRequerida
{
get { return informacionRequerida; }
}
public override string Message
{
get
{
return DESCRIPCION;
}
}
}

53
Veamos la clase constructora de citas:
public class ConstructorCita
{
public const int FECHA_INICIO_REQUERIDA = 1;
public const int FECHA_FIN_REQUERIDA = 2;
public const int DESCRIPCION_REQUERIDA = 4;
public const int ASISTENTES_REQUERIDOS = 8;
public const int LOCALIZACION_REQUERIDA = 16;
protected Cita cita;
protected int elementosRequeridos;
public void construirCita(){
cita = new Cita();
}
public void construirFechas(DateTime fechaInicio, DateTime
fechaFin)
{
Boolean posterior;
DateTime fechaActual = new DateTime();
posterior = fechaInicio.CompareTo(fechaActual) > 0 ? true :
false;
if ((fechaInicio != null) && posterior)
cita.FechaInicio = fechaInicio;
posterior = fechaFin.CompareTo(fechaActual) > 0 ? true : false;
if ((fechaFin != null) && posterior)
cita.FechaFin = fechaFin;
}
public void construirDescripcion(string nuevaDescripcion)
{
cita.Descripcion = nuevaDescripcion;
}
public void construirAsistentes(ArrayList asistentes)
{
if (( asistentes !=null) && (asistentes.Count!=0))
{
cita.Asistentes=asistentes;
}
}
public void construirLocalizacion(string nuevaLocalizacion)
{
if (nuevaLocalizacion != "")
cita.Localizacion = nuevaLocalizacion;
}
public Cita obtenerCita()
{
elementosRequeridos = 0;
if (cita.FechaInicio.ToString()=="")
elementosRequeridos += FECHA_INICIO_REQUERIDA;
if (cita.Localizacion == null)
elementosRequeridos += LOCALIZACION_REQUERIDA;
if (cita.Asistentes.Count == 0)
elementosRequeridos += ASISTENTES_REQUERIDOS;
if (elementosRequeridos > 0)
throw new
InformacionRequeridaExcepcion(elementosRequeridos);
return cita;
}
public int ElementosRequeridos
{
get { return elementosRequeridos; }
}
}

54
La clase Calendario llama a ConstructorCita, gestionando el proceso de creación a
través del método crearCita
public class Calendario
{
public Cita crearCita(ConstructorCita constructor, DateTime
fechaInicio, DateTime fechaFin, string descripcion, string localizacion,
ArrayList asistentes)
{
if (constructor == null)
{
constructor = new ConstructorCita();
}
constructor.construirCita();
constructor.construirFechas(fechaInicio, fechaFin);
constructor.construirDescripcion(descripcion);
constructor.construirAsistentes(asistentes);
constructor.construirLocalizacion(localizacion);
return constructor.obtenerCita();
}
}
Veamos cuales son las responsabilidades de cada clase:
- Calendario: Llama al método apropiado de ConstructorCita, devuelve un
objeto Cita completo a su llamador
- ConstructorCita: Contiene métodos de construcción que fuerzan las reglas de
negocio; crea el objeto Cita real
- Cita: Contiene la información sobre una cita

La clase siguiente: ConstructorReunion muestra una de las ventajas del patrón Builder.
Para incluir reglas adicionales en Cita, amplía el constructor existente. En este caso,
ConstructorReunion fuerza una restricción adicional: si una cita es una reunión de
varios días, se deben indicar las fechas de inicio y finalización.
public class ConstructorReunion : ConstructorCita
{
public Cita obtenerCita()
{
try
{
base.obtenerCita();
}
finally
{
if (cita.FechaFin.ToString() == "")
{
elementosRequeridos += FECHA_FIN_REQUERIDA;
}
if (elementosRequeridos > 0)
{
throw new
InformacionRequeridaExcepcion(elementosRequeridos);
}
}
return cita;
}
}

55
Veamos un ejemplo de utilización de este patrón. El ejemplo es muy simple, un
formulario con un botón en el que mediante Cuadros de Mensaje nos da la
información:
public static ArrayList crearAsistentes(int numero){
ArrayList grupo = new ArrayList();
for (int i = 0; i<numero; i++){
grupo.Add(new
Contacto("John",obtenerApellidos(i),"Empleado","Voyodine Cpt."));
}
return grupo;
}
public static string obtenerApellidos(int indice){
string nombre="";
switch (indice % 6){
case 0: nombre="Worfin";
break;
case 1: nombre="SmallBerries";
break;
case 2: nombre="BigBootee";
break;
case 3: nombre="Haughland";
break;
case 4: nombre="Maassen";
break;
case 5: nombre="Sterling";
break;
}
return nombre;
}
private void button1_Click(object sender, EventArgs e){
Cita ct = null;
Calendario pimCalendario = new Calendario();
ConstructorCita cCita = new ConstructorCita();
try{
ct = pimCalendario.crearCita(cCita, new DateTime(2006, 10,
3), new DateTime(), "Convención de Trekkies", "Fargo", crearAsistentes(4));
MessageBox.Show("Cita creada correctamente");
MessageBox.Show("Información de cita:" + ct.ToString());
}
catch (InformacionRequeridaExcepcion exc){
MessageBox.Show(exc.ToString());
}
ConstructorReunion ctReunion = new ConstructorReunion();
try{
ct = pimCalendario.crearCita(ctReunion, new DateTime(2006,
9, 22), new DateTime(), "Convención de Trekkies", "Fargo",
crearAsistentes(4));
MessageBox.Show("Reunión creada correctamente:" +
ct.ToString()); }
catch (InformacionRequeridaExcepcion exc){
MessageBox.Show(exc.ToString());
} try{
ct = pimCalendario.crearCita(ctReunion, new DateTime(2002,
4, 1), new DateTime(2002, 4, 2), "000 Reunión", "Butte",
crearAsistentes(2));
MessageBox.Show("Cita creada correctamente:" +
ct.ToString());}
catch (InformacionRequeridaExcepcion exc){
MessageBox.Show(exc.ToString());}
}

56
PATRONES DE DISEÑO: PATRÓN DE CREACIÓN FACTORY METHOD

También conocido como Virtual Buider, permite definir un método estándar para
crear un objeto, además del constructor propio de la clase, si bien la decisión del objeto
a crear es delegada en las subclases.

Supongamos que estamos trabajando en el PIM, y de la información que guarda,


necesitamos tener la posibilidad de cambiar, por ejemplo, una dirección si un contacto
cambia de domicilio.

El PIM es el responsable de cambiar todos los campos, por lo que ha de preocuparse


de editar (y, por consiguiente, de la interfaz de usuario) y de validar todos los campos.
La desventaja es que el PIM tiene que conocer todos los tipos de citas y las tareas que
podemos realizar sobre ellas. Cada item tiene diferentes campos, y el usuario necesita
ver una ventana de entrada de datos apropiada para ellos. Será muy difícil presentar
nuevos tipos de tareas porque cada vez tendremos que añadir al PIM una nueva
capacidad apropiada para actualizar el nuevo tipo. Más aún, todos los cambios en un
tipo específico de tarea, como introducir un nuevo campo o cita, implica que también
tendremos que actualizar el PIM para que tenga conocimientos de ese nuevo campo.
Todo ello da lugar aun PIM enorme que dificulta el mantenimiento.

La solución es dejar que sean los propios elementos, como por ejemplo, las citas, los
responsables de proporcionar sus propios editores para gestionar cambios y adiciones.
El PIM solo neceista saber como hacer la petición al editor usando un método getEditor
o una propiedad con su getter. El método devolverá un objeto que implemente la
interfaz ItemEditor, y el PIM utiliza ese objeto para solicitar un Control como editor
gráfico. Los usuarios pueden modificar la información del elemento concreto que
desean utilizar, y el editor asegura que los cambios han sido aplicados
apropiadamente.

Toda la información sobre cómo editar un elemento específico se guarda en el editor,


que es proporcionado por el propio elemento. La representación gráfica del elemento
también es creada por el mismo. Ahora podemos usar nuevos tipos de elementos sin
tener que modificar el PIM.

Aplicabilidad

Usaremos Factory Method cuando:

− Deseemos crear un framework extensible. De esta forma podemos dejar algunas


decisiones, como el tipo específico del objeto a crear, para un momento
posterior.
− Deseemos que sea una subclase, en vez de una superclase, la que decida qué
tipo de objeto hay que crear
− Sabemos cuándo crear un objeto, pero no conocemos su tipo

57
− Necesitamos algunos constructores sobrecargados con la misma lista de
parámetros, lo que no está permitido. En lugar de esto, usaremos varios
métodos de fabricación con el mismo nombre

Descripción

Se denomina así porque crea (fabrica) objetos cuando se necesita.

Cuando comenzamos a escribir una aplicación, a menudo está claro qué tipo de
componentes se van a usar. Normalmente se tiene una idea general de las operaciones
a realizar por ciertos componentes, pero la implementación se realizará en otro
momento, por lo que pueden surgir aspectos que no se tuvieron en cuenta.

Esta flexibilidad puede ser alcanzada usando interfaces para estos componentes. El
problema es que no se puede crear un objeto a partir de una interfaz. Se necesita una
clase que las implemente para obtener un objeto. En vez de escribir una clase en la
aplicación, podemos extraer la funcionalidad del constructor e introducirla en un
método. Ese método es el método de fabricación. Esto produce un ConcreteCreator
cuya responsabilidad es crear los objetos adecuados. Este objeto creará instancias de
una implementación (ConcreteProduct) de una interfaz (Iproduct).

Implementación

El diagrama de clases de este patrón se muestra en la figura:

Para implementarlo necesitamos:

• IProducto (producto): La interfaz de los objetos creados por la fábrica


• ProductoConcreto: La clase que implementa Product. Los objetos de esta clase
son creados por el CreadorConcreto
• ICreador: La interfaz en la que se definen los métodos de fabricación

58
• CreadorConcreto: La clase que hereda de ICreador y proporciona una
implementación para factoryMethod. Puede devolver cualquier objeto que
implemente la interfaz Iproducto.

PATRONES DE DISEÑO: PATRÓN DE CREACIÓN PROTOTYPE

Facilita la creación dinámica al definir clases cuyos objetos pueden crear copias de sí
mismos.

En un PIM quizá necesitemos poder copiar una dirección de forma que el usuario no
tenga que introducir manualmente toda la información cuando se crea un nuevo
contacto. Una forma de resolver este problema es seguir los siguientes pasos:

1. Crear un nuevo objeto Direccion


2. Copiar los valores apropiados del objeto Direccion existente

Aunque esta aproximación nos resuelve el problema, tiene un serio inconveniente:


viola el principio de encapsulación de la OO. Para alcanzar la solución mencionada,
debemos hacer llamadas a métodos para copiar la información de Direccion fuera de
la clase Direccion. Esto significa que es más difícil y duro mantener el código de la
clase Address porque se extiende a lo largo de todo el proyecto. Además, también
dificulta la reutilización de la clase Direccion en proyectos futuros.

Podemos entonces definir un método copy que produzca un duplicado del objeto
Direccion con los mismos datos que el objeto original, el prototipo.

Puesto que el patrón proporciona un comportamiento de creación basada en un estado


existente, es posible que los programas lleven a cabo operaciones como la copia
dirigida por el usuario, al tiempo que permite inicializar los objetos a un estado que
ha sido establecido durante el uso del sistema.

En la figura se muestra el diagrama de clases del patrón:

59
• Prototype: Proporciona un modelo de copia. Este método devuelve un objeto
de la misma clase con los mismos valores que el objeto original. El nuevo objeto
puede ser una copia profunda o superficial del original.

En la siguiente figura se ejemplifica un uso de este patrón:

Una consideración a tener en cuenta en este patrón es la profundidad de la copia:

• Una copia superficial sólo duplica los elementos de alto nivel de una clase, lo
que proporciona una copia más rápida pero no siempre apropiada. Como las
referencias se copian del original a la copia, aún se refieren a los mismos objetos.
Los de bajo nivel son compartidos entre las copias del objeto, por lo que el
cambio en uno de ellos afecta a todas las copias.
• Las de copia profunda no sólo duplican los atributos de alto nivel, sino también
los objetos de bajo nivel. Esto suele llevar más tiempo y ser muy costoso para
objetos con una estructura arbitrariamente compleja. Esto asegura que los
cambios en una copia se aislan de las otras copias.

Variaciones

Incluyen:

• Constructor de copia: Este tipo de constructores toma un objeto de la misma


clase como parámetro y devuelve una nueva copia con los mismos valores que
el parámetro.

60
public class Prototype
{
private int dato;
// más datos
public Prototype()
{}
public Prototype(Prototype original) : this()
{
this.dato = original.dato;
// copiar el resto de datos
}
// resto del código
}

Es posible tener un constructor que utilice ambos tipos de copia, superficial o


profunda. El inconveniente es que el constructor de copia debe comprobar si la
referencia que recibe es nula. Con la implementación normal se asegura que la llamada
al método se realiza sobre un objeto válido.

Método clone: En C# existe un interfaz ICloneable que deben implementar los objetos
que deseemos clonar. Esta interfaz contiene el método Clone() pero no proporciona
ninguna garantía de que el objeto pueda ser clonado, ya que la interfaz no lo define.
Otro problema es que el método Clone() devuelve un object con lo que hay que realizar
una conversión de tipos al apropiado antes de utilizarlo.

Ejemplo:

La clase Direccion de este ejemplo usa el patrón prototype para crear una dirección
basada en una entrada existente. La funcionalidad principal se define en el interfaz
Icopyable:

public interface ICopyable


{
object Copy();
}

En él se define un método de copia y se garantiza que todas las clases que la


implementan definen una operación de copia. Este ejemplo produce una copia
superficial, es decir, de las referencias del objeto original en el duplicado.

Otra característica en este ejemplo de la operación de copia es que no es necesario


duplicar todos los campos. En este caso el tipo de dirección no se copia al nuevo objeto.
El usuario especifica manualmente el nuevo tipo de dirección.

61
public class Direccion : ICopyable {
private string tip;
public string Tipo
{
get { return tip; }
set { tip = value; }
}
private string cal;
public string Calle
{
get { return cal; }
set { cal = value; }
}
private string cidad;
public string Ciudad
{
get { return cidad; }
set { cidad = value; }
}
private string estdo;
public string Estado
{
get { return estdo; }
set { estdo = value; }
}
private string codPostal;
public string CodigoPostal
{
get { return codPostal; }
set { codPostal = value; }
}
private const string eol = "\n";
private const string coma = ",";
private const string casa = "casa";
private const string trab = "trabajo";
public Direccion(string tipoInic, string calleInic, string ciudadInic, string estadoInic, string
codigoPostalInic)
{
tip = tipoInic;
cal = calleInic;
cidad = ciudadInic;
estdo = estadoInic;
codPostal = codigoPostalInic;
}
public Direccion(string calleInic, string ciudadInic, string estadoInic, string codigoPostalInic)
: this(trab, calleInic, ciudadInic, estadoInic, codigoPostalInic) { }
public Direccion(string tipoInic) {
tip = tipoInic;
}
public Direccion() { }
public object Copy()
{
return new Direccion(cal, cidad, estdo, codPostal);
}
public override string ToString()
{
return "\t" + cal + coma + " " + eol + "\t" + cidad + coma + " " + estdo + " " + codPostal;
}
}

62
PATRONES DE DISEÑO: PATRÓN MEMENTO

Se usa para capturar el estado interno de un objeto y guardarlo externamente de forma


que pueda ser restaurado más tarde.

Ilustración

Muchos juegos de ordenador se continúan durante mucho tiempo. Ello implica que
necesitamos guardar el estado de un jego de forma que pueda ser resumido
posteriormente. Puede ser útil guardarlo como “checkpoints” de forma que se pueda
volver a un checkpoint previo después de un movimiento fatídico.

Diseño

La grabación del estado se puede realizar de forma que sea independiente del objeto
en sí mismo, y este es un punto clave en el patrón memento. A continuación podemos
ver el UML para este patrón:

Originator (originador) es la clase que soporta objetos cuyo estado será grabado; puede
decidir cuantos estados necesitan ser grabados cada vez. Memento ejecuta el grabado
y Caretaker (similar a portero), guarda el rastro de los diferentes estados almacenados.

Este patrón es interesante ya que tiene dos interfaces:

• Un amplio interfaz para el Originator que permite acceder a todo aquello que
es necesario que sea guardado o restaurado.
• Un reducido interfaz para el Caretaker de forma que puede guardar y pasar
referencias a memento, pero nada más.

La clase Memento guarda el estado del Originator pero no permite que las otras clases
puedan acceder a él. Por tanto preserva las fronteras de la encapsulación liberando de
esa responsabilidad al Originator.

En suma, los roles en el diseño son:

Originator: Clase que contiene el estado a ser grabado


Memento: Clase para objetos que representan el estado grabado en Originator
Caretaker: Clase que sirve de manejador de los mementos
Cliente: Almacena una copia del Originator

Implementación

63
Veamos el código a continuación. Este programa muestra como unas cadenas pueden
ser grabadas y luego restauradas si el primer carácter de la línea es *. De esta forma,
los errores en los datos del Originator pueden ser corregidos.

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Collections.Generic;
using System.Collections;

// Patrón Memento
// Ilustración simple con "deshacer" (*UNDO)

class MementoPattern
{

// Cliente
static void Main()
{
// Referencias a los mementos
Caretaker[] c = new Caretaker[10];
Originator originator = new Originator();

int move = 0;
// Iterador para los movimientos
Simulator simulator = new Simulator();

foreach (string command in simulator)


{
// Chequeamos el deshacer
if (command[0] == '*' && move > 0)
originator.GetMemento(c[move - 1].Memento); //Fue
previamente restaurado en vez de GetMemento
else
originator.Operation(command);
move++;
c[move] = new Caretaker();
c[move].Memento = originator.SetMemento(); //Fue previamente
grabado en vez de SetMemento
}
Console.ReadKey();
}

// Originator
[Serializable()]
class Originator
{
List<string> state = new List<string>();

public void Operation(string s)


{
state.Add(s);
foreach (string line in state)
Console.WriteLine(line);
Console.WriteLine("=======================");
}

// La referencia a memento es devuelta al cliente

64
public Memento SetMemento()
{
Memento memento = new Memento();
return memento.Save(state);
}

public void GetMemento(Memento memento)


{
state = (List<string>)memento.Restore();
}
}

[Serializable()]
// Serializa por copia profunda a memoria y vuelve
class Memento
{
MemoryStream stream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();

public Memento Save(object o)


{
formatter.Serialize(stream, o);
return this;
}

public object Restore()


{
stream.Seek(0, SeekOrigin.Begin);
object o = formatter.Deserialize(stream);
stream.Close();
return o;
}
}

class Caretaker
{
public Memento Memento { get; set; }
}

class Simulator : IEnumerable


{

string[] lines = {
"The curfew tolls the knell of parting day",
"The lowing herd winds slowly o'er the lea",
"Uh hum uh hum",
"*UNDO",
"The plowman homeward plods his weary way",
"And leaves the world to darkness and to me."};

public IEnumerator GetEnumerator()


{
foreach (string element in lines)
yield return element;
}
}
}

65
/* Salida
The curfew tolls the knell of parting day
=======================
The curfew tolls the knell of parting day
The lowing herd winds slowly o'er the lea
=======================
The curfew tolls the knell of parting day
The lowing herd winds slowly o'er the lea
Uh hum uh hum
=======================
The curfew tolls the knell of parting day
The lowing herd winds slowly o'er the lea
The plowman homeward plods his weary way
=======================
The curfew tolls the knell of parting day
The lowing herd winds slowly o'er the lea
The plowman homeward plods his weary way
And leaves the world to darkness and to me.
=======================
*/

En el siguiente ejemplo, vamos a crear el juego del tres en raya (o TicTacToe) que nos
puede servir como aplicación de este patrón. En este ejemplo, consideraremos un tutor
de Tres en Raya que enfrenta a un jugador humano contra el ordenador. El jugador
aprende las mejores estrategias por ensayo-error y por tanto debería ser capaz de
volver atrás y probar diferentes movimientos si no está ganando.

Esta es una situación ideal para este tipo de patrón. El Originator será el tablero, y su
estado es grabado en cada turno. El caretaker mantiene un array de mementos de
forma que podamos volver atrás más de un turno. En el código del ejemplo, si vemos
una posible salida, se muestra que después de un movimiento fatal, el jugador vuelve
atrás dos mementos y continua con una mejor estrategia.

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Collections;
// Memento Pattern
// Simula un tres en raya donde en el juego se puede volver atrás
// cualquier número específico de movimientos
class MementoPattern
{
// Cliente
static void Main()
{
Console.WriteLine("Practiquemos el Tres en Raya");
Console.WriteLine("Los comandos son:\n1-9 para una posición\n" +
"U-n donde n es el número de movimientos a
deshacer" +
"\nQ para finalizar");
Game game = new Game();

//Referencias a los mementos


Caretaker[] c = new Caretaker[10];

game.DisplayBoard();

66
int move = 1;
// Iterador para los movimientos
Simulator simulator = new Simulator();

foreach (string command in simulator)


{
Console.Write("Movimiento " + move + " para " + game.Player +
": " + command);
if (command[0] == 'Q') break;

//Graba al comienzo de cada movimiento


c[move] = new Caretaker();
c[move].Memento = game.Save();

// Chequea un deshacer
if (command[0] == 'U')
{
int back = Int32.Parse(command.Substring(2, 1));
if (move - back > 0)
game.Restore(c[move - back].Memento);
else
Console.WriteLine("Demasiados movimientos a deshacer");
move = move - back - 1;
}
// sino, juega
else
game.Play(Int32.Parse(command.Substring(0, 1)));

// Actualiza teclado y nombre de movimiento


game.DisplayBoard();
move++;
}
Console.WriteLine("Gracias por jugar");
Console.ReadKey();
}

// Originator
[Serializable()]
class Game
{
// nueve espacios
char[] board = { 'X', '1', '2', '3', '4', '5', '6', '7', '8', '9'
};
public char Player { get; set; }

public Game()
{
Player = 'X';
}

public void Play(int pos)


{
board[pos] = Player;
if (Player == 'X') Player = 'O'; else Player = 'X';
// conserva al jugador
board[0] = Player;
}

// La referencia al memento es devuelta al cliente


public Memento Save()
{

67
Memento memento = new Memento();
return memento.Save(board);
}

public void Restore(Memento memento)


{
board = (char[])memento.Restore();
Player = board[0];
}

public void DisplayBoard()


{
Console.WriteLine();
for (int i = 1; i <= 9; i += 3)
{
Console.WriteLine(board[i] + " | " + board[i + 1] + " | " +
board[i + 2]);
if (i < 6) Console.WriteLine("---------");
}
}
}

[Serializable()]
// Serializa por copia profunda en memoria y vuelve
class Memento
{
MemoryStream stream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();

public Memento Save(object o)


{
formatter.Serialize(stream, o);
return this;
}

public object Restore()


{
stream.Seek(0, SeekOrigin.Begin);
object o = formatter.Deserialize(stream);
stream.Close();
return o;
}
}

class Caretaker
{
public Memento Memento { get; set; }
}
class Simulator : IEnumerable
{
string[] moves = { "5", "3", "1", "6", "9", "U-2", "9", "6", "4",
"2", "7", "8", "Q" };
public IEnumerator GetEnumerator()
{
foreach (string element in moves)
yield return element;
}
}

68
/* Salida
Practiquemos tres en raya
Los comandos son:
1-9 para una posición
U-n donde n es el número de movimientos a deshacer
Q para finalizar

1 | 2 | 3
---------
4 | 5 | 6
---------
7 | 8 | 9
Movimiento 1 para X: 5
1 | 2 | 3
---------
4 | X | 6
---------
7 | 8 | 9
Movimiento 2 para O: 3
1 | 2 | O
---------
4 | X | 6
---------
7 | 8 | 9
Movimiento 3 para X: 1
X | 2 | O
---------
4 | X | 6
---------
7 | 8 | 9
Movimiento 4 para O: 6
X | 2 | O
---------
4 | X | O
---------
7 | 8 | 9
Movimiento 5 para X: 9
X | 2 | O
---------
4 | X | O
---------
7 | 8 | X
Movimiento 6 para O: U-2
X | 2 | O
---------
4 | X | 6
---------
7 | 8 | 9
Movimiento 4 para O: 9
X | 2 | O
---------
4 | X | 6
---------
7 | 8 | O
Movimiento 5 para X: 6
X | 2 | O
---------
4 | X | X
---------
7 | 8 | O
Movimiento 6 para O: 4

69
X | 2 | O
---------
O | X | X
---------
7 | 8 | O
Movimiento 7 para X: 2
X | X | O
---------
O | X | X
---------
7 | 8 | O
Movimiento 8 para O: 7
X | X | O
---------
O | X | X
---------
O | 8 | O
Movimiento 9 para X: 8
X | X | O
---------
O | X | X
---------
O | X | O
Movimiento 10 para O: QGracias por jugar
*/

PATRONES DE DISEÑO: PATRON BRIDGE

Este patrón hace posible dividir un componente complejo en dos jerarquías


relacionadas: la abstracción funcional y la implementación interna. Esto hace que sea
más fácil cambiar cualquier aspecto del componente.

Supongamos que tenemos una lista de tareas pendientes en un gestor de información


personal. En un momento dado, quizás necesitemos tener un cierto grado de
flexibilidad en lo relacionado con cómo presentarla al usuario (elementos listados con
boliches, número o gráficos). Además, es posible que queramos tener alguna forma de
modificar la funcionalidad básica de la lista, dando a los usuarios la capacidad para
elegir entre una lista desordenada, secuencial o priorizada.

Generalmente se desarrolla un grupo de clases para las listas, cada una de las cuales
puede proporcionar una forma específica de mostrar las listas. Sin embargo, esta
solución suele descartarse rápidamente, ya que hay muchas combinaciones de las
formas de mostrar una lista y de almacenar su información.

Es mejor entonces, separar la representación de la lista de tareas de la implementación


subyacente. El patrón Bridge (puente) lo consigue definiendo dos clases o interfaces
que trabajan conjuntamente. Para el Gestor de Información son List y ListImpl. List
representa la funcionalidad de mostrar los elementos, pero delega el almacenamiento
real en la implementación subyacente, la clase ListImpl.

La ventaja de esta aproximación se hace obvia cuando añadimos capacidades nuevas


al comportamiento básico. Para introducir caracteres o numeraciones, creamos una
subclase de List. Para soportar características como la agrupación secuencial de

70
elementos, heredamos de ListImpl. La belleza de esta solución radica en que puede
“mezclar y emparejar” las clases, logrando un nivel de compatibilidad mucho mayor.

Se usa el patrón Bridge cuando:

− Se desea flexibilidad entre la abstracción y la implementación del componente,


evitando una relación estática entre ambos.
− Los cambios en la implementación no deban ser visibles a los clientes.
− Identifiquemos múltiples abstracciones e implementaciones de componentes.
− Es apropiado crear subclases, pero queremos gestionar independientemente los
dos aspectos del sistema.

Los elementos complejos de un sistema pueden variar en cuanto a su funcionalidad


externa y a su implementación. En esos casos, la herencia no es una solución deseable,
ya que el número de clases a crear se incrementa en función de estos dos aspectos. Dos
implementaciones y dos representaciones generan cuatro clases, mientras que tres
implementaciones y tres representaciones generan nueve clases.

Además, la herencia asocia un componente con un modelo estático, haciendo difícil


cambiarlo en el futuro. Cambiar un componente es un desafío particularmente difícil,
ya que tienden a variar conforme se va modificando y usando el sistema. Sería
preferible crear una forma dinámica de modificar aspectos de los componentes según
sea necesario.

Este patrón resuelve el problema desacoplando los dos aspectos del componente. Con
dos cadenas de herencia separadas (una para implementación y otra para
funcionalidad) es mucho más fácil mezclar y asociar los elementos de cada lado. Esto
proporciona una flexibilidad general con un menor coste de codificación.

Es útil para cualquier sistema que deba mostrar flexibilidad en tiempo de ejecución.
Por ejemplo, los sistemas de interfaz gráfica que deben ser portables entre plataformas.
Dichos sistemas necesitan que la implementación subyacente sea aplicada cuando la
aplicación se inicia en un sistema operativo distinto. Los sistemas que cambian la
representación de sus datos dependiendo de la localización (por ejemplo, modificando
la representación de las fechas, idioma o unidad monetaria) a menudo son buenos
candidatos para usar el patrón bridge. De igual forma, este patrón suele ser efectivo
para entidades de negocio que pueden ser asignadas a diferentes fuentes de bases de
datos.

Un ejemplo conceptual del patrón es una centralita de soporte técnico. Hay varias
líneas preestablecidas que conectan a los usuarios con el personal de soporte técnico.
Naturalmente, la respuesta será diferente dependiendo de la experiencia del técnico.
La respuesta también variará en función de la pregunta.

71
Implementación

En la siguiente figura podemos ver el diagrama de clases:

La implementación necesita las siguientes clases:

• Abstraction (Abstracción): Define la abstracción funcional de Bridge,


proporcionando un comportamiento y una estructura estándar. Contiene una
referencia a un objeto de la clase Implementation, el cual normalmente se fija
mediante una propiedad o un constructor.
• RefineAbstraction (Abstracción refinada): Hereda de Abstration y proporciona
un comportamiento adicional o modificado.
• Implementation (Implementación): Interfaz que representa la funcionalidad
usada por los objetos basados en Abstraction
• ConcreteImplementation (Implementación concreta): Implementa
Implementation. Proporciona el comportamiento y estructura para las clases
Implementation.

Cuando se diseña una aplicación que usa este patrón, es importante definir
apropiadamente qué responsabilidades pertenecen a la abstracción funcional y cuales
a la clase de implementación interna. Asimismo, se debe considerar con cuidad lo que
representa el verdadero modelo base de la implementación del modelo Bridge. Un
problema común proviene del desarrollo de la implementación alrededor de una o dos
posibles variaciones. El peligro es que el desarrollo futuro del patrón revelará que
algunos de los supuestos elementos del comportamiento principal realmente
representan variaciones específicas basadas en la abstracción y/o la implementación.

Ejemplo

En el ejemplo, vamos a utilizar una sencilla lista de tareas pendientes, con capacidad
para insertar y eliminar cadenas. Para Bridge, un elemento se definen en dos partes:
abstracción e implementación. La implementación es la clase que hace el trabajo real,

72
en este caso, almacenar y recuperar entradas en la lista. El comportamiento general se
define en la interfaz IlistImpl:

public interface IListImpl


{
void AddItem(string Item);
void AddItem(string Item, int Position);
void RemoveItem(string Item);
int NumberOfItems { get; }
string GetItem(int Index);
bool SupportsOrdering();
}

La clase OrderedListImpl implementa esta interfaz y almacena las entradas de la lista


en un genérico List interno:

public class OrderedListImpl : IListImpl


{
private List<string> items = new List<string>();

public void AddItem(string Item)


{
if (!items.Contains(Item))
items.Add(Item);
}

public void AddItem(string Item, int Position)


{
if (!items.Contains(Item))
items.Insert(Position, Item);
}

public void RemoveItem(string Item)


{
if (items.Contains(Item))
items.RemoveAt(items.IndexOf(Item));
}

public bool SupportsOrdering()


{
return true;
}

public int NumberOfItems


{
get { return items.Count; }
}

public string GetItem(int Index)


{
if (Index < items.Count)
return items[Index];
return null;
}
}

73
La abstracción representa las operaciones de la lista que están disponibles para el
mundo exterior. La clase BaseList proporciona las capacidades generales de las listas:

public class BaseList


{
protected IListImpl implemtor;

public IListImpl Implementor


{
set { implemtor = value; }
}

public void Add(string Item)


{
implemtor.AddItem(Item);
}

public void Add(string Item, int Position)


{
if (implemtor.SupportsOrdering())
implemtor.AddItem(Item, Position);
}

public void Remove(string Item)


{
implemtor.RemoveItem(Item);
}

public virtual string GetByIndex(int Index)


{
return implemtor.GetItem(Index);
}

public int Count


{
get { return implemtor.NumberOfItems; }
}

Como podemos ver, todas las operaciones son delegadas a la variable que rerefencia
la implementación de la lista. Cuando se realizan peticiones a List, las operaciones son
delegadas “por el puente” al objeto ListImpl asociado.

Es fácil ampliar las características proporcionadas por BaseList creando una nueva
subclase de BaseList e introduciendo una nueva funcionalidad. La clase NumberedList
muestra la potencia del patrón redefiniendo el método GetByIndex, siendo capaz de
proporcionar una numeración para los elementos de la lista.

public class NumberedList : BaseList


{
public override string GetByIndex(int Index)
{
return (Index + 1).ToString() + ". " + base.GetByIndex(Index);
}
}

74
Podemos ver a continuación una extensión del ejemplo:
public class OrnamentedList : BaseList
{
private char itmType;
public char ItemType
{
get { return itmType; }
set {
if (value > ' ')
itmType = value; }
}
public override string GetByIndex(int Index)
{
return itmType + " " + base.GetByIndex(Index);
}
}

Aplicación de consola:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Ejemplo de patrón Bridge");
Console.WriteLine("\nEste ejemplo divide un comportamiento complejo en dos");
Console.WriteLine("clases - la abstracción y la implementación.");
Console.WriteLine("\nEn este caso, hay dos clases que pueden proporcionar la");
Console.WriteLine("abstracción - BaseList y OrnamentedList. La BaseList");
Console.WriteLine("proporciona funcionalidad básica, mientras que la Ornamented");
Console.WriteLine("se expande en el modelo añadiendo un carácter de lista.");
Console.WriteLine("\nLa clase OrderedListImpl proporciona la capacidad de almacenamiento");
Console.WriteLine("subyacente para la lista, y puede ser emparejada de forma flexible");
Console.WriteLine("cualquiera de las clases que proporcionen la abstracción.");
Console.WriteLine("\nCreando el objeto OrderedListImpl.");
IListImpl implementation = new OrderedListImpl();
Console.WriteLine("\nCreando una lista base.");
BaseList listaUno = new BaseList();
listaUno.Implementor = implementation;
Console.WriteLine("\nAñadiendo elementos a la lista.");
listaUno.Add("Uno");
listaUno.Add("Dos");
listaUno.Add("Tres");
listaUno.Add("Cuatro");
Console.WriteLine("\nCreando una Lista Ornamentada");
OrnamentedList listaDos = new OrnamentedList();
listaDos.Implementor = implementation;
listaDos.ItemType = '+';
Console.WriteLine("\nCreando una lista Numérica");
NumberedList listaTres = new NumberedList();
listaTres.Implementor = implementation;
Console.WriteLine("\nMostrando la primera lista (Lista Base)");
for (int i = 0; i < listaUno.Count; i++)
Console.WriteLine("\t" + listaUno.GetByIndex(i));
Console.WriteLine("\nMostrando la segunda lista (Lista Ornamentada)");
for (int i = 0; i < listaDos.Count; i++)
Console.WriteLine("\t" + listaDos.GetByIndex(i));
Console.WriteLine("\nMostrando la tercera lista (Lista Numerada)");
for (int i = 0; i < listaTres.Count; i++)
Console.WriteLine("\t" + listaTres.GetByIndex(i));
Console.ReadLine();
}

75
SOLID

Cohesión

Para comprender el término nivel de cohesión usaremos como ejemplo un teléfono


móvil. Estos dispositivos están compuestos de una serie de componentes (pantalla,
teclado, radio, Bluetooth, etc.) los cuales tienen una responsabilidad específica y por
tanto existe una cohesión entre ellos, ya que no hay ningún componente que haga
funciones que se solapen con las de otros componentes. Tampoco hay ninguna función
básica que no quede cubierta por un determinado componente.

En un sistema informático también tenemos componentes (además de clases, módulos,


etc) y todos ellos tienen su responsabilidad dentro del sistema. Por tanto, debemos
pensar en el nivel de cohesión de una aplicación, no como en la suma de sus
componentes sino como el conjunto de los mismos.

Acoplamiento

En un teléfono móvil, como ejemplo de acoplamiento tenemos su propio cargador.


Cada vez más se opta en estos casos por estándares que sirvan para más de un modelo
tomando esta opción con el fin de reaprovecharse para otros dispositivos, incluso de
diferentes fabricantes.

En ingeniería de software el acoplamiento entre módulos, clases o cualquier otra


entidad lógica es el grado de dependencia entre ellos. Cuanto más estándar sea la
relación de una entidad lógica con otras, mayor reaprovechamiento podemos hacer de
ella.

Partiendo de estos dos conceptos y la encapsulación entran en escena los cinco


principios descritos por el acrónimo mnemotécnico SOLID presentados a principio del
2000 por Robert C. Martin:

- SRP: Single Responsability Principle


- OCP: Open Closed Principle
- LSP: Liskov Substitution Principle
- ISP: Interface Segregation Principle
- DIP: Dependency Inversion Principle

PRINCIPIO DE RESPONSABILIDAD ÚNICA (SRP)

Fue acuñado por Robert C. Martin en un artículo del mismo título y popularizado a
través de su libro “Agile Software Development: Principles, Patterns and Practices”.
SRP tiene que ver con el nivel de acoplamiento entre módulos dentro de la ingeniería
del software. En términos prácticos establece que:

Una clase debe tener una y solo una única causa por la cual puede ser modificada.

76
Si una clase tiene dos responsabilidades, entonces asume dos motivos por los cuales
puede ser modificada. Por ejemplo, supongamos una clase Factura la cual dentro de
un contexto determinado ofrece un método para calcular el importe total.

La piedra angular de este principio es identificar la responsabilidad real de la clase.


Según SRP una responsabilidad es “un motivo de cambio”; algo en ocasiones difícil de
ver acostumbrados a pensar un conjunto de operaciones como una única
responsabilidad.

public class Factura


{
public string codigo;
public DateTime fechaEmision;
public decimal importeFactura;
public decimal importeIva;
public decimal importeDeduccion;
public decimal importeTotal;
public ushort porcentajeDeduccion;

// Método que calcula el total de la factura


public void CalcularTotal()
{
// Calculamos deducción
importeDeduccion = (importeFactura * porcentajeDeduccion) /
100;
// IVA
importeIva = importeFactura * 0.16m;
// Total
importeTotal = importeFactura - importeDeduccion + importeIva;
}
}

A la vista de este código, podemos decir que la responsabilidad de la clase es calcular


el total de la factura y así es. Sin embargo, esta clase no contiene una sola
responsabilidad. Si nos fijamos en el método CalcularTotal vemos que además de
calcular el importe base de la factura se aplica un descuento o deducción y un 16% de
IVA. El problema está en que si en el futuro tuviéramos que modificar la tasa de IVA
o tuviéramos que aplicar una deducción en base a una tarifa por cliente, tendríamos
que modificar la clase Factura por cada una de esas razones. En este caso quedaría
violado el SRP

Separando responsabilidades

Para separarlas, debemos primero identificarlas. Los pasos que realiza CalcularTotal
son:

- Aplicar una deducción. Sobre la base imponible se calcula un descuento


porcentual
- Aplicar el IVA del 16% sobre la base imponible.
- Calcular el total teniendo en cuenta los puntos anteriores.

77
Identificamos, por tanto, tres responsabilidades. Ya que una responsabilidad no es una
acción, sino un motivo de cambio, debemos extraer ambas responsabilidades en otras
clases: IVA y Deduccion:

public class Iva


{
public readonly decimal iva = 0.16m;
public decimal CalcularIva(decimal importe)
{
return importe * iva;
}
}

public class Deduccion


{
private decimal deduccion;

public Deduccion(ushort porcentaje)


{
deduccion = porcentaje;
}

public decimal CalcularDeduccion(decimal importe)


{
return (importe * deduccion) / 100;
}
}

public class Factura


{
public string codigo;
public DateTime fechaEmision;
public decimal importeFactura;
public decimal importeIva;
public decimal importeDeduccion;
public decimal importeTotal;
public ushort porcentajeDeduccion;

// Método que calcula el total de la factura


public void CalcularTotal()
{
// Calculamos deducción
Deduccion deduc = new Deduccion(porcentajeDeduccion);
importeDeduccion = deduc.CalcularDeduccion(importeFactura);
// IVA
Iva iv = new Iva();
importeIva = iv.CalcularIva(importeFactura);
// Total
importeTotal = importeFactura - importeDeduccion + importeIva;
}
}

Con esta solución conseguimos mayor cohesión y menor acoplamiento, al aumentar la


granularidad de la solución.

No es fácil detectar las responsabilidades ya que tendemos a agruparlas. Sin embargo,


existen escenarios en los que se permite una cierta flexibilidad:

78
interface Modem
{
void marcar(int numero);
void colgar();
void mandar(char[] datos);
char[] recibir();
}

En este ejemplo se detectan dos responsabilidades relacionadas con la gestión de la


comunicación (marcar y colgar) y con la comunicación de datos (enviar y recibir). Cada
una de ellas puede cambiar por diferentes motivos; sin embargo, ambas funciones se
llaman desde diferentes puntos de la aplicación y no existe una dependencia entre
ellas, con lo que no perderíamos cohesión del sistema.

EXPRESIONES LAMBDA (3.0)

Las expresiones lambda proporcionan una sintaxis concisa para escribir métodos
anónimos. La especificación C# 3.0 describe estas expresiones como un superconjunto
de métodos anónimos.

En C# 2.0, podemos escribir un delegado para usarlo como método anónimo como
podemos ver en este ejemplo:

public delegate int MiDelegado(int n);


class MyClass
{
static void Main()
{
// Método anónimo que devuelve el argumento multiplicado
por 5
MiDelegado objetoDeleg1 = new MiDelegado(delegate(int n)
{return n*5;});
// Muestra el resultado
Console.WriteLine("El valor es: {0}",objetoDeleg1(5));
Console.ReadKey(true);
}

Este programa muestra 25 como resultado. En C# 3.0 esta funcionalidad se extiende a


las expresiones lambda, las cuales son usadas en muchos lenguajes funcionales como
Lisp. Veamos como podemos usar una sintaxis sencilla para llegar al mismo objetivo:
// Expresión Lambda que devuelve el argumento multiplicado por cinco
MiDelegado objetoDeleg2 = (int n) => n * 5;

El token “=>” utilizado en esta sintaxis es lo que llamamos el operador lambda.


También podemos utilizar una forma abreviada como esta:
MiDelegado objetoDeleg2 = n => n * 5;

Una expresión lambda puede usar dos argumentos, especialmente cuando estamos
usando los “Standard Query Operators”, es decir, operadores estándar de consulta.
Primero declararemos el siguiente delegado que usa dos argumentos:
public delegate int MiDelegado(int m,int n);

79
Ahora podemos instanciar el delegado utilizando una expresión lambda tal como la
siguiente:
MiDelegado miDelegado = (x,y) => (x*y);

De esta forma, podemos invocar el delegado y mostrar el resultado que sigue:


Console.WriteLine("El producto es: {0}",miDelegado(5,4));

Lo que muestra como producto 20, como ya esperábamos.

ALGORITMO DES. EJEMPLO PRÁCTICO DE ENCRIPTACIÓN

DES significa Data Encription Standard y es un algoritmo inventado por personal de


IBM que llegó a ser un estándar comercial. En enero de 1997 se consiguió romper
“DES” por técnicas de fuerza bruta pero con una capacidad de computación muy
elevada. Aún así, costó 22 horas en obtenerse la clave y desde entonces se considera
inseguro. A pesar de ello, sigue utilizándose, y sin ir más lejos, la agencia tributaria lo
usa para construir el número de referencia completo (NRC) de la constitución de
depósito.

En el listado a continuación, vamos a hacer uso de dicho algoritmo. Vamos a utilizar


en él dos cláusulas using: System.Security y System.Security.Criptography.

Inicialmente definimos las variables que contendrán los nombres de los ficheros de
entrada y salida, así como la clave. El programa va a comprobar que le estemos
pasando 4 parámetros: la acción a realizar, el fichero de entrada, la clave y el fichero
de salida. Para ello usamos if(args.Length==4). Si han sido pasados los cuatro
parámetros, tomamos decisiones. Si hemos mandado una “e”, llamamos a la función
EncriptaDES() , y en caso contrario, llamamos a DesencriptaDES().

La función EncriptaDES() recibe como parámetros el fichero de entrada, el de salida


(que contiene el texto encriptado) y la clave, la cual ha de ser de 64 bits (8 bytes) ya que
es la longitud de la clave que maneja el algoritmo DES. Lo primero que hacemos es
abrir un FileStream para leer del fichero de entrada en modo lectura. Después creamos
el FileStream de salida, en modo de escritura, donde escribiremos los datos ya
encriptados. Lo siguiente es esencial: creamos un proveedor de servicios de cifrado, en
nuestro caso DES, le indicamos la clave, y creamos un encriptador a partir de él
(CryptoStream). Este stream recibe como parámetro el fichero de salida donde se va a
escribir, el encriptador y el modo, en este caso, de escritura. Obtenemos el número total
de bytes que tiene el fichero de entrada (el que contiene el texto a encriptar) mediante
fsEntrada.Length y creamos un array de bytes con esa longitud. Leemos todos los
bytes del FileStream de entrada que hemos creado y lo escribimos en el CryptoStream.
Para terminar, cerramos todos los streams.

Una vez pasados los argumentos, como resultado de la encriptación obtendremos un


fichero encriptado (fichero_enc.txt) el cual podríamos desencriptar también si le
damos como argumento la letra “d”.

80
using System;
using System.IO;
using System.Security;
using System.Security.Cryptography;
using System.Text;
class encripta{
static void Main(string[] args){
string entrada;
string salida;
string clave;
if (args.Length==4){
entrada=args[1];
salida=args[3];
clave=args[2];
if (args[0]=="e")
EncriptaDES(entrada,salida,clave);
else if (args[0]=="d")
DesencriptaDES(entrada,salida,clave);
else{
Console.WriteLine("encripta v0.5");
Console.WriteLine("ERROR: opción no válida. Debe
ser e/d");}}
else{
Console.WriteLine("encripta v0.5");
Console.WriteLine("Parámetros no válidos");}}
static void EncriptaDES(string fichero_entrada,string
fichero_encriptado,string clave){
FileStream fsEntrada = new
FileStream(fichero_entrada,FileMode.Open,FileAccess.Read);
FileStream fsEncriptado=new
FileStream(fichero_encriptado,FileMode.Create,FileAccess.Write);
DESCryptoServiceProvider DES = new DESCryptoServiceProvider();
DES.Key=ASCIIEncoding.ASCII.GetBytes(clave);
DES.IV=ASCIIEncoding.ASCII.GetBytes(clave);
ICryptoTransform encriptadorDES=DES.CreateEncryptor();
CryptoStream cryptostream = new
CryptoStream(fsEncriptado,encriptadorDES,CryptoStreamMode.Write);
byte[] Bytes_entrada= new byte[fsEntrada.Length];
fsEntrada.Read(Bytes_entrada,0,Bytes_entrada.Length);
cryptostream.Write(Bytes_entrada,0,Bytes_entrada.Length);
cryptostream.Close();
fsEntrada.Close();
fsEncriptado.Close(); }
static void DesencriptaDES(string fichero_entrada,string
fichero_encriptado,string clave){
DESCryptoServiceProvider DES=new DESCryptoServiceProvider();
DES.Key=ASCIIEncoding.ASCII.GetBytes(clave);
DES.IV=ASCIIEncoding.ASCII.GetBytes(clave);
FileStream fs_encriptado=new
FileStream(fichero_entrada,FileMode.Open,FileAccess.Read);
ICryptoTransform descifrador= DES.CreateDecryptor();
CryptoStream cryptostreamdesc= new
CryptoStream(fs_encriptado,descifrador,CryptoStreamMode.Read);
StreamWriter fsDesencriptado=new
StreamWriter(fichero_encriptado);
fsDesencriptado.Write(new
StreamReader(cryptostreamdesc).ReadToEnd());
fsDesencriptado.Flush();
fsDesencriptado.Close();
}
}

81
MANEJO DE EXCEPCIONES

using System;
public class Hola
{
public static int Main(string[] args)
{
try
{
Console.WriteLine(args[0]);
}
catch (Exception e)
{
Console.WriteLine("Excepcion {0}",e.StackTrace);
}
return 0;
}
}

Una aplicación C# robusta debe ser capaz de manejar situaciones inesperadas. No


importa cuantos errores hayamos chequeado al crear nuestro código, siempre hay algo
inevitable que puede ir mal. Casos como que el usuario introduzca una respuesta
inesperada a un prompt, o que intente escribir en un fichero que ha sido borrado. Las
posibilidades son infinitas.

Cuando un error en tiempo de ejecución ocurre en una aplicación C#, el S.O. lanza una
excepción. Si usamos una construcción del tipo try-catch como vemos en el ejemplo
introductorio, podemos capturar dicha excepción. Si cualquiera de las sentencias en la
parte del try causa que el S.O. lance una excepción, la ejecución será transferida al
bloque match.

En el ejemplo anterior, además usando la propiedad StackTrace de la excepción,


mostramos por pantalla donde ha fallado la ejecución del programa. Si imprimimos
una excepción usando Console.WriteLine por ejemplo, la excepción dará formato
automáticamente a sus propiedades StackTrace, Message y Source y las mostrará.

Si no utilizamos manejo de excepciones, ocurrirá una excepción de tipo run-time . Si


deseamos depurar nuestro programa utilizando la depuración Just-in-time, debemos
habilitarla primero. Si ya la tenemos habilitada, dependiendo en que entorno y que
utilidades estén instaladas, la depuración Just-in-time nos pedirá que depurador se va
a usar.

Para habilitar la depuración Just-in-time, seguiremos los siguientes pasos:

1. En el menú Herramientas, clic en Opciones.


2. En el cuadro de diálogo Opciones, clic a la carpeta Depuración.
3. En la carpeta Depuración, clic General
4. Clic en Just-In-Time
5. En el cuadro de diálogo Just-In-Time, habilitar o desactivar las casillas de
verificación según el tipo de programas y aceptar los cambios.

82
Otro ejemplo de excepciones:
using System;
public class Hola
{
public static int Main(string[] args)
{
try
{
int i, j;
string temp;
Console.WriteLine ("Introduzca el primer entero, por
favor");
temp = Console.ReadLine( );
i = Int32.Parse(temp);
Console.WriteLine ("Introduzca el segundo entero, por
favor");
temp = Console.ReadLine( );
j = Int32.Parse(temp);
int k = i / j;
Console.WriteLine("El resultado de dividir {0} por {1} es
{2}", i, j, k);
}
catch(Exception e)
{
Console.WriteLine("Se ha producido un error: {0}", e);
}
return 0;
}
}

DEPURACIÓN Y DIAGNOSIS: VISUALIZACIÓN DE DATOS DE DEPURACIÓN


DE NUESTRAS CLASES

Si necesitamos que nuestro depurador muestre algún dato que consideremos


necesario de nuestra clase, podemos añadir un objeto DebuggerDisplayAttribute.
Supongamos una clase Ciudadno que contuviera el tratamiento honorífico y el nombre
de los ciudadanos, podríamos añadir un objeto de ese tipo como el siguiente:

De esta forma, cuando instanciemos objetos de esa clase, el depurador mostrará


también la información del modo en que DebuggerDisplayAttribute le indica.

using System.Diagnostics;
[DebuggerDisplay("Nombre completo de ciudadano = {honorifico + ' ' +
primero + ' ' + segundo + ' ' + apellido}")]
public class Ciudadano{
private string honorifico;
private string primero;
private string segundo;
private string apellido;
public Ciudadano(string honorif, string primer, string segun, string
apell)
{
honorifico = honorif;
primero = primer;
segundo = segun;
apellido = apell;
}
}

83
Cuando se ejecute el código en el depurador, se utilizará una versión personalizada
que incluirá el nombre completo.

La funcionalidad más potente de esta característica es que brinda la posibilidad de que


los miembros de nuestro equipo puedan determinar qué contienen las clases de un
vistazo. El puntero this es accesible desde la declaración de
DebuggerDisplayAttribute, pero ninguno de los atributos de las propiedades
accesibles mediante this lo será antes del procesamiento. Es decir, si accediéramos a
una propiedad del objeto al construir la cadena del visualizador y dicha propiedad
tuviera atributos, estos no serían procesados, y portanto podríamos tener un valor
distinto al esperado. Si especificamos un método ToString() sobrescrito, el depurador
lo usará con DebuggerDisplayAttribute sin necesidad de especificarlo, suponiendo
que la opción adecuada está activa en Herramientas>Opciones>Depuración (Llamar a
la función de conversión de cadenas en objetos de ventanas de variables).

DESARROLLO DE APLICACIONES WINDOWS

Introducción a Visual Studio .NET

Aunque en principio sería posible desarrollar una aplicación de ventanas de forma


parecida a como hemos estado trabajando hasta ahora; es decir, con un simple editor
de texto y utilizando las clases de los espacios de nombres adecuados (principalmente
System.WinForms), esto no es lo que en la práctica se suele hacer, ya que ello
implicaría invertir mucho tiempo en la escritura del código encargado de generar la
interfaz de nuestra aplicación, tiempo que podríamos estar aprovechando para
centrarnos en resolver los problemas relativos a su lógica y no a su aspecto. Por esta
razón, vamos a utilizar la herramienta Visual Studio .NET de Microsoft.

Visual Studio.Net permite diseñar la interfaz de la aplicación de manera visual, sin


más que arrastrar con el ratón los elementos que necesitemos (botones, lista de
selección, etc.) sobre las posiciones adecuadas en la ventana de nuestra aplicación.
También incluye otras facilidades para el desarrollo, como una ventana de
propiedades desde la que se puede modificar los valores de las propiedades de cada
objeto sin tener que escribir código, un depurador de código gráfico, un editor de
códigos inteligente que puede detectar nuestros errores de sintaxis instantáneamente,
etc.

Al arrancar Visual Studio.Net (por defecto en Inicio -> Programas -> Microsoft Visual
Studio .NET 7.0 -> Microsoft Visual Studio .NET) obtendremos una pantalla como la
siguiente:

84
[Imagen 1. Pantalla inicial de Visual Studio.NET]

Lo primero que hemos de hacer es crear un nuevo proyecto donde almacenar los
archivos de la aplicación. Para ello pulsamos Archivo -> Nuevo -> Proyecto y
obtendremos una ventana desde la que podremos seleccionar tanto el lenguaje con el
que vamos a trabajar como el tipo de proyecto que vamos a desarrollar, el nombre del
mismo y el directorio base en el que se almacenarán los archivos del proyecto. Así, con
las opciones que nosotros seleccionaremos el aspecto final de esta ventana será el
siguiente:

85
[Imagen 2. Ventana de selección de tipo de proyecto]

Como se ve, nosotros hemos seleccionado en la ventana Tipos de Proyecto que vamos
a realizar un proyecto escrito en C# (opción Proyectos de Visual C#), en la ventana
Plantillas que vamos a desarrollar una aplicación de venantas (opción Aplicación para
Windows), en el cuadro de texto Nombre que vamos a darle el nombre EditorEjemplo
a nuestro proyecto, y en el cuadro de texto Ubicación que el directorio base donde
instalaremos el proyecto será c:\C# Nótese que debajo de Ubicación aparece un
mensaje que nos informa cual será el directorio donde finalmente se almacenarán los
archivos de nuestro proyecto, y como se ve, este directorio es el resultado de
concatenar el nombre de nuestro proyecto con la ruta del directorio base que hayamos
especificado.

Una vez configuradas todas estas opciones, sólo queda pulsar el botón Aceptar para
que se cree toda la infraestructura adecuada para que podamos empezar a trabajar en
nuestro proyecto. La ventana que tras ello se obtiene tiene un aspecto similar al
siguiente:

86
[Imagen 3: Ventana principal de diseño de Visual Studio.NET]

Si alguna de las ventanas indicadas no nos apareciese al iniciar el proyecto, siempre


podemos obtenerlas nosotros mismo pulsando sobre los botones situados en la zona
superior derecha de la ventana principal de Visual Studio, junto al botón >>

Como se puede ver en el dibujo, se distinguen tres partes muy importantes en nuestra
área de trabajo:
• La ventana de diseño (central), donde iremos colocando los elementos que
formarán parte de la interfaz de nuestra aplicación y desde la que podemos
modificar el código de estos con sólo hacer doble click con el ratón sobre ellos.
• La ventana de herramientas (toolbox) donde se recogen los objetos más
comunes que se utilizan a la hora de crear aplicaciones de ventanas. Estos
objetos, a los que llamaremos controles, se pueden ir colocando sobre la ventana
de diseño sin más que ir seleccionándolo en la barra de herramientas y
arrastrándolos sobre la posición deseada de la ventana de diseño.
• La ventana de propiedades (properties), desde donde aparecerán las
propiedades del objeto que en cada momento haya seleccionado haciendo click
sobre él con el ratón. A través de esta ventana podemos obtener una ayuda
rápida sobre sus propiedades y modificarlas con simples pulsaciones de ratón.
Por defecto las propiedades aparecen ordenadas en ella por categorías,
apareciendo juntas las relacionadas entre sí; sin embargo, podemos ordenarlas
alfabéticamente pulsando el segundo botón situado en la zona superior de esta
ventana (empezado a contar por la izquierda)
Diseño de la interfaz

87
Ahora que ya estamos familiarizados con el entorno con el que vamos a trabajar,
podemos empezar a diseñar la interfaz de nuestra aplicación. Para ello, de la barra de
herramientas iremos seleccionando los controles que necesitemos y los iremos
arrastrando sobre la ventana de diseño (ver Imagen 4). A medida que los vayamos
usando se explicarán a fondo estos controles, y por el momento sólo diremos que son:
+ Un RichTextBox, que será el área sobre el que mostrará el texto el editor.
+ Un MainMenu, que contendrá el menú principal de nuestra aplicación
+ Un OpenFileDialog, un SaveFileDialog, un FontDialog y un PrintDialog,
que se corresponden respectivamente con las ventanas estándar utilizadas en
Windows para la apertura y cierre de ficheros, selección de fuente e impresión.
+ Un PrintDocument, que será el objeto que utilicemos para imprimir el texto
de nuestro editor y que tomará como información los datos que recoja el
PrintDialog previamente introducido.

[Imagen 4: Ventana de diseño con los controles necesarios para el editor]

Como podemos ver, no todos los controles que arrastremos sobre la ventana de diseño
se mostrarán sobre ésta, sino que algunos lo harán en un área en blanco situado debajo
de ella. Esto se debe a que son controles que, o no tienen una componente visual, o no
merece la pena mostrarla en la zona de diseño.

Si tras arrastrar un control éste no tiene el tamaño que a nos interesa, podemos
modificar este tamaño “tirando” de él mediante los cuadrados que aparecen en sus
extremos (igual que en cualquier programa de tratamiento de imágenes) Nótese como
a medida que vaya modificando el tamaño o posición de un control se irán
actualizando automáticamente los valores de las propiedades correspondientes a su
alto (propiedad Height), ancho (Width), posición en el eje OX de su esquina superior
izquierda (Left) y posición en OY de su esquina superior izquierda (Top)

Todos los controles que hemos creado cuentan con una serie de valores por defecto
para sus propiedades, como se puede observar echándoles un vistazo en la ventana de
88
propiedades. Muchos de estos valores son los más utilizados generalmente y nos
vienen bien; sin embargo, hay otros que no nos interesan tal y como están por lo que
tendremos que cambiarlos (por ejemplo, el texto por defecto de nuestro editor de texto
no debería ser richTextBox1) Para ello, iremos seleccionando cada uno de los objetos de
nuestra aplicación y modificando los valores de sus propiedades en la ventana de
propiedades como se indica:

Al RichTextBox le cambiaremos el texto por defecto dejándoselo en blanco, para lo que


seleccionaremos su propiedad Text y borraremos su valor. A continuación, le vamos a
modificar el nombre con el que lo identificaremos desde nuestro código por uno más
español, para lo que cambiaremos el contenido de su propiedad1 (Name) por rtbTexto.

Tras esto, le cambiamos el valor de su propiedad Anchor de manera que todos los
rectángulos que aparecen en la ventana asociada a la misma, que aparecerá al pulsar
sobre la fecha junto a ella incluida, queden marcados. Esta propiedad indica respecto
a qué lados de su ventana contenedora se “anclará” el control, y seleccionar todos los
rectángulos como se ha indicado indica que deseamos que sean todos, con lo que
siempre que se redimensione la ventana nuestro control se ajustará automáticamente
al nuevo tamaño que se le dé.

Finalmente, vamos a cambiar el valor de la propiedad Font, que indica el tipo de fuente
que se usará el control para mostrar su texto, por otra fuente que puede ser más bonita:
la Arial de tamaño 10. Para ello, podemos teclear manualmente Arial 10 pt como valor
de esa propiedad o, lo que es más cómodo, pulsar el botón con puntos suspensivos
que aparece a su lado para seleccionar gráficamente de la forma en que acostumbra a
hacer en Windows.

Al OpenFileDialog le cambiaremos el título que se mostrará en la ventana de selección


de archivo a abrir por un más intuitivo Seleccione el archivo a abrir, para lo que
cambiaremos el valor de su propiedad Title por dicho texto. Al igual que en el caso
del RichTextBox, también es conveniente que le cambiemos su identificador por uno
en castellano: dlAbrirArchivo

Por último, vamos modificar el valor de su propiedad Filter Esta propiedad indica
cuáles son los archivos que la ventana nos dejará seleccionar. El valor de esta
propiedad ha de ser una cadena de texto de la forma
descripciónArchivo1|filtroArchivo1|descripciónArchivo2|filtro2... Nosotros le daremos el
valor Archivos de texto|*.txt|Todos los archivos |*.* De este modo, nuestra ventana en
primer lugar sólo nos dejará seleccionar archivos de extensión .txt, pues el tipo de
archivo indicado en primer lugar; y en caso de que el usuario quiera seleccionar otro
siempre podrá hacerlo modificando la lista desplegable incluida en la ventana de
selección de archivo gracias a que como segundo tipo de archivo admitido permitimos
cualquier archivo (*.*)

1 Nótese que (Name) aparece entre paréntesis ya que en realidad no es una propiedad del objeto, sino el
nombre del objeto. Sin embargo, por comodidad se ha optado por incluirlo en la ventana de propiedades.

89
Al SaveFileDialog le hacemos cambios muy similares caso anterior. Ahora cambiamos
el Title por Seleccione el archivo donde guardar, el (Name) por dlGuardarArchivo y el Filter
por Archivos de texto |*.txt|Todos los archivos|*.*

Al FontDialog le cambiaremos el nombre por dlFuente, le daremos el valor true a su


propiedad ShowColor para que nos permita seleccionar también nuevos colores para
la fuente de nuestro texto y le cambiaremos el valor de la fuente mostrada por defecto
por Arial 10 pt, para que se corresponda con el valor de nuestro texto por defecto.

Al PrintDocument le cambiaremos su nombre por pdImpresor y al PrintDialog se lo


cambiaremos por dlImprimir Además, en éste último modificaremos su propiedad
Document por pdImpresor, para así indicarle que el objeto PrintDocument cuyas
propiedades ha de modificar a partir de la información que recoja del usuario es
pdImpresor

Por último, a la ventana principal del editor le cambiaremos el texto de su barra de


título modificando su propiedad Texto por EditorEjemplo

Diseño de los menús

Para terminar con el diseño la interfaz de nuestra aplicación nos queda por diseñar
únicamente el menú principal. De las propiedades del objeto MainMenu sólo hemos
de cambiar su nombre por menu, siendo lo verdaderamente interesante el diseño de
los submenús que en el se contendrán, lo cual se hace de forma muy sencilla y vistosa
sin más que ir rellenando los recuadros Escriba Aquí adecuados con los nombres de los
submenús que deseemos ir creando hasta obtener la siguiente estructura:

Archivo Fuente Créditos


Nuevo
Abrir...
Reabrir
Guardar
Guardar como...
--------------------
Imprimir
--------------------
Salir

90
La siguiente imagen muestra gráficamente el proceso de relleno de recuadros “Escriba
aquí” antes comentado:

[Imagen 5: Diseño visual de menús en Visual Studio.NET]

A cada submenú le daremos un nombre igual al texto que mostrará, aunque precedido
de la cadena menu (por ejemplo, menuArchivo, menuGuardarComo...) La única excepción
a este caso serán las barras separadoras, que se incluyen escribiendo – en el Escriba
Aquí correspondiente al lugar donde se desee que aparezcan y que no usaremos para
nada aparte de para organizar un poco mejor el contenido de nuestros menús, por lo
que no tiene mucho sentido que le demos un nombre fácil de recordar.

Cabe destacar algunos aspectos:

+ A cada menú le podemos asociar una combinación de teclas para acceder


rápidamente al mismo. Para ello, hemos de seleccionar dicha combinación como valor
de la casilla correspondiente a la propiedad ShortCut en su ventana de propiedades.

+ En el texto que muestran los menús (y otros controles como los botones) le podemos
incluir un carácter & antes de cualquier letra que será interpretado como carácter a
usar para acceder al mismo mediante la combinación Alt+tecla Además, el carácter que
haya a la derecha de este & aparecerá subrayado para indicar que es el usado para
acceder rápidamente al control con dicha combinación2.

2 Si se incluyen más de un & los siguientes al primero son ignorados. Si se desea que & forme parte del
texto del control habrá que incluir dos & seguidos (&&)

91
+ Podemos indicar que un submenú es el que se activará por defecto cuando hagamos
doble click sobre el nombre del menú padre que lo contiene. Para ello, hemos de dar
el valor true a la propiedad DefaultItem del mismo. Este tipo de submenús se
caracterizan porque su texto aparece en negrita3.

+ Por defecto todos los submenús aparecen activados; es decir, de modo que puedan
ser seleccionados. Sin embargo, en el caso del submenú Reabrir no nos interesa que
esto sea posible hasta que se haya abierto algún fichero, por lo que hemos de modificar
su propiedad Enabled y darle el valor false inicialmente.

Asociación de código a eventos

Windows es un sistema guiado por eventos, y cualquier aplicación gráfica escrita para
Windows consiste básicamente en escribir código de respuesta a eventos. Por ejemplo,
cuando el usuario pulsa un botón se lanza un evento con el que se indica que dicho
botón ha sido pulsado, y es el programador el que ha de determinar qué acción se ha
de realizar en respuesta a dicha pulsación.

En C# para escribir este código de respuesta, en la clase de cada control se define una
serie de miembros denominados eventos a los que les podemos asociar una o más
funciones a ejecutar en caso de que se produzca el evento apropiado. Por ejemplo, la
clase de los submenús (MenuItem) tiene un evento Click que se declararía así en la
definición de la clase:
class MenuItem: Menu
{
// ... (Otras definiciones)
public event EventHandler Click;
// ... (Otras definiciones)
}

Nótese que tras el nombre de la clase MenuItem se ha colocado el nombre de otra clase
(Menu) separado por dos puntos. Esto significa que la clase MenuItem que estamos
declarando deriva de la clase Menu. Es decir, básicamente esto significa que incluye
todas las definiciones de métodos, propiedades y demás miembros de la clase Menu
más los que definamos en ella; y que si definimos miembros de igual signatura que
miembros definidos en Menu las nuevas definiciones de estos sustituirán a las
heredadas de Menu. Nótese que también la clase principal de nuestra aplicación deriva
de clase Form, que incluye definiciones para el comportamiento básico de una ventana.

La definición de evento del ejemplo anterior significa que Click es un evento al que le
podemos asociar como código respuesta funciones cuya signatura (definición de valor
de retorno, nombre y parámetros de la función) sea compatible con la indicada por el
delegado EventHandler Un delegado podemos verlos como una declaración de valor

3 Si se pone a true dos o más submenús de un mismo menú padre sólo se activará con el doble click el
primero. Sin embargo, ambos aparecerán en negrita.

92
de retorno y parámetros de función a la que se le asocia un nombre. Por ejemplo, en el
caso concreto de EventHandler, éste delegado está definido de la siguiente forma:

public delegate void EventHandler(object emisor, EventArgs e)

Por consiguiente, al evento Click le podremos asociar cualquier número de funciones


que tomen un primer parámetro de entrada de tipo object, un segundo de tipo
EventArgs y que no devuelva ningún valor de retorno (void) En concreto, el primer
parámetro representa al objeto que ha provocado el evento (en nuestro caso el
submenú pulsado); mientras que el segundo está destinado a contener información
variada sobre el evento que se ha producido que a nosotros no nos va a hacer falta.

Nosotros podemos escribir el código de estas funciones de respuesta sin preocuparnos


de cómo se generan estos objetos que son pasados a nuestra función, ya que de ello se
encarga el sistema en tiempo de ejecución de .NET4 y podemos estar totalmente
seguros de que siempre estarán construidos correctamente.

El siguiente ejemplo muestra cómo podríamos asociar código de respuesta a la


pulsación de un botón de nombre miMenu:

// ...
miMenu.Click += new EventHandler(miCódigoRespuesta);
// ...

Este código de respuesta estaría escrito en una función como la siguiente:


public void miCódigoRespuesta(object o, EventArgs e)
{
MessageBox.Show(“Pulsado miMenu”);
}

La función Show() de la clase MessageBox es muy utilizada en aplicaciones de


ventanas, y lo que hace es mostrar por pantalla una pequeña ventana informativa con
el texto indicado y un botón con el texto Aceptar que permite cerrarla. Por consiguiente,
lo que hemos hecho con nuestro código es que cada vez que se pulse el menú
identificado por miMenu se mostrará una ventana informando que se ha pulsado dicho
botón.

Nótese que para asociar un código de respuesta a un evento se utiliza el operador +=.
Este operador puede usarse tantas veces como se desee sobre un mismo evento, con lo
se asociarían diversos códigos de respuesta al mismo, que se ejecutarían uno detrás
tras otro en caso de producirse el evento.

4 También conocido como Common Language Runtime ó CLR

93
También podemos eliminar de un evento códigos de respuesta ya asociados. Para ello
se usa el operador -=, y un ejemplo de cómo quitar el código asociado al evento en el
ejemplo anterior es:
miMenu.Click -= new EventHandler(miCódigoRespuesta);

A partir de esta instrucción, cada vez que se pulsase miMenu no se realizará ninguna
acción (a no ser que hayan asociado otros códigos de respuesta al evento Click)

Asociación de código a eventos en Visual Studio.NET

Aunque la forma anteriormente comentada para asociar códigos de respuesta a los


eventos es la que realmente se usa, Visual Studio nos permite asociar los códigos de
respuesta más comunes a los eventos de una forma mucho más sencilla. Con tan sólo
hacer doble click sobre el control ya estaremos en condiciones de empezar a escribir el
código de respuesta a su evento más común (por ejemplo, en el caso de un menú o
botón sería el código de respuesta a su evento Click), pues ya se encarga Visual Studio
de declarar automáticamente la función de respuesta al evento y asociarla (con +=) al
evento apropiado.

Este será el método que usaremos en nuestro editor de texto para asociar código de
respuesta, y a continuación se describe cómo escribir este código para cada uno de los
controles de nuestra aplicación para conseguir la funcionalidad deseada en la misma:

menuNuevo:

Cuando se pulse el botón nuevo se ha de eliminar cualquier texto que hubiese en la


caja de texto (rtbTexto) del editor. Esto es tan sencillo como hacer que el valor de su
propiedad Text pase a ser una cadena vacía (“”), y para conseguirlo simplemente
hacemos doble click sobre la opción del menú apropiada y rellenamos el código de
respuesta al evento con el siguiente contenido:
protected void menuNuevo_Click (object sender, System.EventArgs e)
{
rtbTexto.Text="";
this.Text ="EditorEjemplo";
menuReabrir.Enabled = false;
}

Nótese que también realizamos otras acciones en el código del método. Éstas consisten
en cambiar el título de la ventana principal por EditorEjemplo, ya que en este título
almacenaremos información sobre el nombre del fichero abierto en cada momento,
pero al crear uno nuevo no tiene nombre, y desactivar el submenú Reabrir, ya que no
tiene sentido reabrir ningún archivo porque estamos creando uno nuevo.

Es significativo el uso de la palabra reservada this para hacer referencia al objeto al que
pertenece el método que estamos escribiendo. Recordemos que los códigos de
respuesta los estamos escribiendo dentro de la de la clase que representa la ventana de

94
la aplicación (que deriva de Form), por lo que this hace referencia a la ventana nuestro
editor y Text es la propiedad de la ventana que almacena su título.

También puede destacarse el hecho de que Visual Studio añade el modificador de


visibilidad protected a la declaración de cualquier código de respuesta. Este
modificador indica que dicho código sólo puede ser llamado desde la clase dentro de
la que el método se ha definido o desde subclases de la misma.

menuAbrir

En este caso hay que mostrar al usuario el fdAbrirArchivo para recoger información
sobre el nombre del archivo a abrir para, tras ello, abrirlo y guardar como contenido
de la caja de texto del editor la información en este almacenada. Esto lo hacemos con
un el siguiente código de respuesta (recordemos que para escribirlo basta hacer doble
click con el ratón sobre el submenú de título Abrir... y Visual Studio se encargará de
generar automáticamente el esqueleto del mismo):

protected void menuAbrir_Click (object sender, System.EventArgs e)


{
if (dlAbrirArchivo.ShowDialog() == DialogResult.OK)
{
rtbTexto.LoadFile(dlAbrirArchivo.FileName,
RichTextBoxStreamType.PlainText);
this.Text = "["+dlAbrirArchivo.FileName + "] EditorEjemplo";
menuReabrir.Enabled = true;
}
}

En este código lo que se hace es llamar al método ShowDialog() de nuestro


dlAbrirArchivo para que se muestre la típica ventana con la que en Windows pide al
usuario de las aplicaciones que seleccione un fichero a abrir. Este método puede
devolvernos los valores OK ó Cancel del tipo enumerado DialogResult según si el
usuario ha pulsado el botón Aceptar ó el botón Cancelar de dicha ventana.

Un tipo enumerado o enumeración es un tipo para el que indicamos explícitamente


cuáles son los valores que puede tomar. Por ejemplo, un tipo enumerado de nombre
Colores que sólo pueda tomar los valores Rojo, Verde o Azul se definiría así:

enum Colores {Rojo, Verde, Azul};

Sólo en caso de que se pulse Ok será cuando cargaremos el fichero de texto


seleccionado, por lo que hemos de comprobar en la condición del if que estemos en
ese caso. Una vez que el usuario ha seleccionado un archivo almacenamos su
contenido en la caja de texto llamando al método LoadFile() de ésta. Este método toma
dos parámetros: el primero contiene el nombre del fichero a abrir, que recuperamos a
través de la propiedad FileName del dlAbrirArchivo; mientras que el segundo indica
cuál es el formato en que se encuentra almacenada la información de dicho tipo, donde

95
nosotros indicamos que vamos a cargar un fichero en formato de texto plano5 usando
el valor PlainText de la enumeración RichTextBoxStreamType. Téngase en cuenta que
un objeto de clase RichTextBox también es capaz de almacenar texto en formato RTF6
(valor RichText de la enumeración RichTextBoxStreamType)

Tras cargar el contenido del fichero de texto en nuestra caja de texto, sólo queda
modificar el título de la ventana de propiedades para que contenga el nombre del
fichero abierto entre corchetes (nótese el uso de + como operador de concatenación de
cadenas) y, dado que ahora sí tenemos un fichero abierto, activar la posibilidad de
reabrirlo.
MenuReabrir

El funcionamiento de este menú es muy similar al anterior, sólo que ahora no hemos
de preguntar al usuario cuál es el fichero a abrir sino que podemos obtenerlo a partir
del nombre del último fichero abierto. Por ello, su código de respuesta es:

protected void menuReabrir_Click (object sender, System.EventArgs e)


{
String nombreFichero = this.Text.Substring(1, this.Text.LastIndexOf("]") -
1);
rtbTexto.LoadFile(nombreFichero, RichTextBoxStreamType.PlainText);
}

El código de este método es mucho más sencillo que en el caso anterior, ya que ahora
no hemos de actualizar la barra de título con el nombre del fichero abierto porque era
el que ya había; y no hemos de activar el menú Reabrir porque ya estaba activado (de
hecho, es el botón pulsado)

Para obtener el nombre del fichero hemos usado el método Substring() definido para
cualquier cadena (clase String) que devuelve la subcadena comprendida entre el
elemento cuyo índice coincide con el valor de su primer parámetro y cuya longitud es
la indicada en el segundo. Como el primer elemento de la barra de título siempre será
un corchete ya que lo que se indica en ella es el nombre del fichero abierto, nos
saltaremos el primer elemento del texto del título y obtendremos la subcadena que
comienza en el índice 1 (las cadenas se indexan desde 0 en C#) Para obtener la longitud
de la subcadena a obtener usamos el método LastIndexOf() de la clase String que
devuelve el valor del índice de la última aparición de la subcadena que se le pasa como
parámetro.

menuGuardar

Al pulsar este submenú hemos de guardar el contenido del texto que se esté editando
dentro del fichero que se está editando, de modo que todos los cambios hechos en la

5 Este formato consiste en almacenar en el fichero sólo los caracteres que componen el texto a mostrar.
6 Este formato consiste en almacenar en el fichero también información que indiquen características de
formato del texto, márgenes, etc.

96
caja de texto se hagan permanentes en el fichero. Para ello, hemos de comprobar si el
fichero que estamos escribiendo es un fichero nuevo o no, ya que en el primer caso
habría que solicitar al usuario un nombre de fichero donde guardar esta información
y en el segundo caso no. Esto lo hacemos así:
protected void menuGuardar_Click (object sender, System.EventArgs e)
{
String nombreFichero;

if (this.Text == "EditorEjemplo")
menuGuardarComo.PerformClick();
else
{
nombreFichero = this.Text.Substring(1, this.Text.LastIndexOf("]") - 1);
rtbTexto.SaveFile(nombreFichero, RichTextBoxStreamType.PlainText);
}
}

Véase pues, que lo que se hace para detectar si el fichero es nuevo consiste en
comprobar si el texto de la barra de título de la ventana del editor coincide con el valor
de ésta por defecto, caso en que ello querría decir que sí es nuevo y, por consiguiente,
habría que solicitar el nombre del fichero donde guardar; es decir, estaríamos en un
caso equivalente al de haber pulsado el submenú Guardar como, por lo que lo que
haremos será simular que este se ha pulsado llamando al método PerformClick() del
mismo. En caso de que el fichero no sea nuevo, obtendremos su nombre de forma
similar a como se hizo en el código de respuesta a la pulsación de menuReabrir y
guardaremos el contenido de la caja de texto en dicho fichero llamando al método
SaveFile() de la caja de texto. Este método funciona de forma recíproca al LoadFile()
ya visto, sólo que en este caso en vez de cargar el texto del fichero en la caja de texto lo
que se hace es cargar el contenido de la caja de texto en el fichero.

menuGuardarComo

Cuando se selecciona este botón hay que mostrar al usuario una de las típicas ventanas
de Windows en las que se solicita un nombre de fichero donde guardar información.
Sólo en caso de que se haya seleccionado un nombre de fichero en esta ventana y se
haya pulsado su botón OK se guardará el contenido de la caja de texto de nuestro
editor en el archivo indicado. Para hacer esto se ejecuta el siguiente código:

protected void menuGuardarComo_Click (object sender, System.EventArgs e)


{
if (dlGuardarArchivo.ShowDialog() == DialogResult.OK){
rtbTexto.SaveFile(dlGuardarArchivo.FileName,RichTextBoxStreamType.Plai
nText);
this.Text = "["+dlGuardarArchivo.FileName + "] EditorEjemplo";
menuReabrir.Enabled = true;
}
}

97
Del código puede deducirse fácilmente que lo único que se hace es llamar al método
ShowDialog() del dlGuardarArchivo para que muestre la ventana antes comentada,
comprobar que se pulse el botón OK de la misma viendo si el valor de retorno de este
método es el valor OK de la enumeración DialogResult y, en ese caso, guardar el texto
contenido en rtbTexto en el fichero indicado llamando al método SaveFile() de dicho
objeto RichTextBox. Finalmente, se cambia el título de la barra de la ventana del editor
para actualizar la información de la misma sobre el nombre del fichero que se está
editando y se habilita el submenú Reabrir dando el valor true a su propiedad Enabled,
ya que ahora sí se tiene un nombre de fichero para reabrir.

menuImprimir

El código de respuesta a la pulsación de este submenú es muy sencillo. Sólo hemos


de mostrar la ventana típica que en Windows se utiliza para recoger información sobre
los detalles de la impresión y, en caso de que el usuario pulse el botón OK de la misma,
llamar al método Print() del objeto pdImpresor para imprimir la página según la
configuración recogida. Recuérdese que el objeto dlImprimir está conectado al objeto
pdImpridor a través de su propiedad Document, y ésta será la conexión que se usará
para comunicar al pdImpresor cuál es la configuración de impresión recogida.

El código de respuesta a este evento es, por tanto, tan simple como:

protected void menuImprimir_Click (object sender, System.EventArgs e)


{
if (dlImprimir.ShowDialog() == DialogResult.OK)
pdImpresor.Print();
}

La llamada a Print() inicia la ejecución del proceso de impresión, aunque nosotros


hemos de indicar cómo se realizará la impresión definiendo el cuerpo del código de
respuesta al evento PrintPage del pdImpridor que es lanzado al llamar a Print() Como
definición de este código se usará:

protected void pdImpresor_PrintPage (object sender,


System.Drawing.Printing.PrintPageEventArgs e)
{
String texto = rtbTexto.Text;
Font fuente = rtbTexto.Font;
SolidBrush color = new SolidBrush(rtbTexto.ForeColor);
RectangleF área = new RectangleF(e.MarginBounds.Left, e.MarginBounds.Top,
e.MarginBounds.Width, e.MarginBounds.Height);

e.Graphics.DrawString(texto, fuente, color, área);


}

Para ir dibujando el contenido a imprimir sobre el papel se utiliza el método


DrawString() del objeto obtenido a través de la propiedad Graphics del argumento de

98
tipo PrintPageEventArgs con el que se llamó a éste método. DrawString() imprime la
cadena que se le pasa como parámetro usando la fuente y el color indicados dentro del
área del papel que se le especifica en su último parámetro. Para especificar el texto y
el color se usan directamente los valores almacenados en las propiedades Text y Font
de nuestro rtbTexto, mientras que para especificar el color a usar es necesario generar
un SolidBrush a partir del color almacenado en la propiedad ForeColor de dicha caja
de texto, ya que esto es lo que DrawString() espera. Finalmente, para especificar el área
de impresión se ha optado por utilizar un objeto RectangleF que almacena
información sobre el rectángulo correspondiente a los márgenes a usar (posición en
OX de su esquina superior izquierda, posición en OY, ancho y alto) Los valores
pasados a éste objeto en su constructor son los que se obtienen por defecto a través de
la propiedad MarginBounds del objeto PrintPageEventArgs pasado como argumento
al método.

menuSalir

Cuando se pulse el botón Salir lo que hay que hacer es, como su propio nombre indica,
abortar la ejecución del editor. Esto se hace con una única instrucción: llamando al
método Close() de la ventana principal del editor para cerrarla. Es decir, así:

protected void menuSalir_Click (object sender, System.EventArgs e)


{
this.Close();
}

menuFuente

Al activarse este submenú se mostrará al usuario una ventana estándar de Windows


donde podrá seleccionar las características de la fuente que desea utilizar para mostrar
el texto en el editor. Por tanto, lo único que se ha de hacer es mostrar al usuario esta
ventana cuando pulse el submenú y, en caso de que seleccione OK en la misma,
modificar las características de la fuente utilizada por las nuevas que haya
seleccionado. Esto se hace con el siguiente código

protected void menuFuente_Click (object sender, System.EventArgs e)


{
if (dlFuente.ShowDialog() == DialogResult.OK)
{
rtbTexto.Font = dlFuente.Font; // Cambiamos fuente
rtbTexto.ForeColor = dlFuente.Color; // Cambiamos color
}
}

Como es deduce, la ventana de selección de fuente almacena en su propiedad Font la


fuente elegida y en Color el color seleccionado para la misma.

99
menuCréditos

Al seleccionarse este submenú simplemente mostraremos un mensaje con información


sobre la aplicación. Para ello vamos a usar el método Show() de la clase MessageBox
que, como ya vimos anteriormente, lo que hace es mostrar una pequeña ventana con
el texto que se le indique como parámetro. De este modo nos queda:

protected void menuCréditos_Click (object sender, System.EventArgs e)


{
MessageBox.Show("Autor: José Antonio González Seco",
"Créditos de EditorEjemplo v1.0");
}

En este caso hemos usado una variante del método Show() diferente a la vista
anteriormente en la que como segundo parámetro pasamos la cadena de texto que será
utilizada como título de la ventana a mostrar.

Una vez escrito todo los códigos de respuesta indicados, nuestra aplicación ya estará
lista para funcionar. Podemos probarla seleccionando en el menú principal de Visual
Studio Depurar -> Inicio, siendo el aspecto de la misma durante su ejecución:

[Imagen 6: La aplicación EditorEjemplo en funcionamiento]

100
GENERAR AUTOMÁTICAMENTE LAS PROPIEDADES EN VISUAL STUDIO

Visual Studio ofrece la posibilidad de generar automáticamente las propiedades de


una clase a partir de una variable miembro. El proceso es muy simple, tan solo hemos
de dejar el cursor sobre la variable miembro y con el botón derecho del ratón:
Refactorizar → Encapsular campo. De esta forma, nuestro entorno nos creará
automáticamente el código para get y set referente a dicha variable miembro.

PROPIEDAD ENABLED. COMO CONSEGUIR QUE UN CUADRO DE TEXTO


NO TENGA FONDO GRIS CUANDO ENABLED=FALSE

En ocasiones, deseamos impedir la edición de nuestros cuadros de texto, pero sin


tornarse de color de fondo gris. Para ello, podemos a posteriori, en tiempo de
ejecución, cambiar el backcolor de dicho cuadro, con lo cual conseguimos el efecto
deseado.

El inconveniente de esta opción es que el color del texto se vuelve gris, y aunque
cambiemos la propiedad ForeColor, sigue manteniendo el mismo. La forma de
conseguir el efecto deseado es recurrir a la propiedad ReadOnly poniéndola a true. De
esta forma, podemos cambiar ambos colores en tiempo de ejecución.

CONTROL MONTHCALENDAR

Este control muestra un calendario agrupado por meses completamente


personalizable, tanto en aspecto como en formatos de fecha. Su utilidad principal
consiste en poder seleccionar una fecha para su posterior utilización.

Uno de los eventos de más utilidad en este componente es DateSelected, el cual salta
al seleccionar como mínimo una fecha. La selección de fechas viene dada por
SelectionStart y SelectionEnd. En el caso de que solo utilicemos una, con la primera
propiedad tendremos suficiente.

CONTROL DATETIMEPICKER

Más completo que el control MonthCalendar, es DateTimePicker, el cual nos muestra


un desplegable con el formato de fecha/hora deseado. Si actuamos sobre dicho
desplegable, obtenemos la vista de un calendario exactamente igual que el control
MonthCalendar que nos permite seleccionar la fecha visualmente.

Este control consta además de un checkbox con el cual vamos a decidir si queremos o
no seleccionar una fecha.

CONTROL FLOWLAYOUTPANEL

Este control sirve para organizar contenidos en su interior en un flujo horizontal o


vertical. Este contenido se puede ajustar de una fila a la siguiente, o de una columna a

101
la siguiente. También podemos recortar el control con el fin de ajustar sus
componentes en el interior.

La dirección de flujo viene dada por la propiedad FlowDirection. En función de su


contenido, el control FlowLayoutPanel se dimensiona automáticamente si tenemos la
propiedad Autosize a true.

La principal utilidad de este control radica en la posibilidad de realizar diseños


dinámicos (por ejemplo, un diseño que se reorganiza al cambiar el tamaño de un
formulario).

Vamos a ver esto con un ejemplo. Colocamos un FlowLayoutPanel y en él, vamos a


introducir botones hasta que uno de ellos pase a la siguiente fila. Si ahora cambiamos
la propiedad WrapContents a false, vemos como el botón que había pasado a una
segunda fila por falta de sitio, aparece ahora en la primera y recortado.

Otra propiedad interesante es FlowBreak, la cual pertenece a los controles situados


dentro del FlowLayoutPanel, y sirve para romper el flujo habitual de controles. Vamos
a utilizar el panel anterior y colocar la dirección FlowDirection en TopDown. Ahora
aparecen los botones en una sola columna. Si seleccionamos uno de los botones
centrales de la columna y le cambiamos su propiedad FlowBreak a true, vemos como
saltan los botones posteriores a otra columna, es decir, hemos dado un salto al flujo
del panel a partir de ese botón.

Por supuesto en este panel podemos aprovecharnos de las ventajas de anclado.


Supongamos el FlowLayoutPanel anterior en el cual tenemos los botones en columnas.
Si hacemos el primer botón el doble que los inferiores, podemos ver como al cambiar
el Anchor del botón inmediatamente inferior, se ajusta en base al primer botón. Otra
prueba interesante es cambiar su Dock a fill.

Otra posibilidad que tenemos es la de ajustar márgenes y espacio entre controles y


panel con las propiedades margin y padding.

INSTANCIAR UN FORM A PARTIR DE SU NOMBRE EN UNA CADENA

Supongamos que nuestra aplicación consta de varios formularios, los cuales se van a
instanciar en función de una serie de condicionantes. Es decir, se abrirá uno u otro
formulario según se necesite y vamos a imaginar que todos esos nombres de form los
tenemos almacenados en un tipo de datos cadena dentro de una tabla en una BD.

El procedimiento para realizar esto es el siguiente:

Assembly tempAssembly = Assembly.GetExecutingAssembly();


Form frm1 = (Form)tempAssembly.CreateInstance("MiNS.form1");
frm1.Show();

Para usar la clase Assembly, hay que importar el Namespace System.Reflection

102
FORMULARIOS CON FORMA

En .NET podemos definir un formulario con una forma dada por una imagen, es decir,
no tiene porque tener la forma rectangular habitual. Esto se consigue colocando una
imagen de fondo y definiendo uno de sus colores como transparente mediante la
propiedad Transparency Key. El código para realizar esto sería el siguiente, el cual
colocaríamos en el evento Load del formulario:

private void MainFormLoad(object sender, System.EventArgs e)


{
System.Drawing.Bitmap img = new System.Drawing.Bitmap("rutaim");
img.MakeTransparent(img.GetPixel(0, 0));
this.BackgroundImage = img;
this.TransparencyKey = img.GetPixel(0, 0);
}

La imagen ha de ser un mapa de bits. Además, debemos ajustar la propiedad


FormBorderStyle del formulario a None. Si hacemos esto, veremos el formulario como
transparente, pero no podremos moverlo ni cerrarlo excepto por la combinación de
teclas Alt. + F4. Si añadimos el siguiente código para manejar los eventos MouseMove
y MouseDown podremos mover el formulario como es habitual:

private Point mouse_offset;

private void Form1_MouseDown(object sender,


System.Windows.Forms.MouseEventArgs e)
{
mouse_offset = new Point(-e.X, -e.Y);
}

private void Form1_MouseMove(object sender,


System.Windows.Forms.MouseEventArgs e)
{
if (e.Button == MouseButtons.Left) {
Point mousePos = Control.MousePosition;
mousePos.Offset(mouse_offset.X, mouse_offset.Y);
Location = mousePos;
}
}

Si además queremos poder cerrar el formulario, tan solo hemos de agregar un botón o
cualquier otro control que nos lo permita y programar en él la salida del formulario.

TABCONTROL

Si queremos ocultar una pestaña (TabPage) en un TabControl, lo que debemos es hacer


que su propiedad TabParent sea null. Para volver a visualizarla, simplemente, volver
a darle el valor del TabControl:
TabPage.Parent = null;

Con esto lo elimina del TabControl

103
TabPage.Parent = TabControl;

Con esto lo vuelve a añadir

AJUSTAR LA POSICIÓN Y TAMAÑO DE LOS CONTROLES


AUTOMÁTICAMENTE (USANDO LA PROPIEDAD ANCHOR)

Una característica que tiene mucha utilidad es la posibilidad de ajustar


automáticamente la posición de los controles de un formulario al redimensionarlo.
Esto vamos a hacerlo gracias a la propiedad Anchor de los controles. Mediante esta
propiedad podemos indicar en que posición queremos "anclar" cada uno de los
controles.

Por ejemplo, si queremos que un control se ajuste al ancho del formulario, pero
permanezca en la misma posición de izquierda y arriba, sólo tenemos que darle el
valor Top, Left y Right a la propiedad Anchor del control en cuestión.
Por otro lado, si queremos que un control se ajuste al tamaño completo del formulario,
pero que la separación con respecto a los controles que estén a su izquierda, derecha,
arriba y abajo permanezcan inalterables, asignaremos los cuatro valores de la
propiedad Anchor: Top, Left, Right y Botton.
Es decir, podemos hacer que un control se ajuste de forma automática al tamaño del
formulario y a la posición (o separación) con respecto a otros controles. Todo esto lo
veremos a continuación.

Una característica que tienen los formularios es poder indicarle el tamaño mínimo y
máximo que queremos que tenga. Esto se consigue mediante las propiedades:
MinimumSize y MaximumSize. No es necesario indicarle el tamaño a las dos
propiedades, por ejemplo si lo que queremos es que nuestro formulario no pueda ser
menor de 300x200, asignaremos el valor 300 a MinimumSize.Width y 200 a
MinimumSize.Height.

Aquí tenemos dos capturas del formulario de prueba, una con el tamaño asignado en
tiempo de diseño y el otro al cambiar el tamaño:

104
Tamaño original.

Después de cambiar el tamaño.

Estos son los valores asignados a Anchor de cada uno de los controles del formulario:
(por defecto, están asignados los valores Top y Left, por tanto no es necesario asignar
estos valores a los controles que queremos que estén anclados sólo en esas dos
posiciones, tal es el caso de la etiqueta que muestra el texto Fichero:)

Nota: Estos valores se pueden asignar en tiempo de ejecución y también se pueden


asignar en tiempo de diseño.

Cuando se hace en tiempo de diseño, usando la propiedad Anchor de la ventana de


propiedades, la cual muestra algo parecido a esto:

105
La propiedad Anchor de la ventana de propiedades.

La ventaja de asignar estos valores en tiempo de diseño es que si cambiamos el tamaño


del formulario, los controles también se ajustan al nuevo tamaño que le demos tanto
en tiempo de diseño como en ejecución.

AÑADIR UN ICONO A LA BARRA DE TAREAS DE WINDOWS

En este ejemplo, vamos a ver cómo añadir un icono a la barra de tareas.


También le vamos a añadir un menú contextual, el cual se mostrará al pulsar con el
botón derecho del ratón.
Ese menú tendrá una opción para restaurar, la cual se llamará al hacer doble-click en
dicho icono de la barra de tareas.

Los iconos en la barra de tarea de Windows (al lado del reloj), se suelen usar para
aplicaciones que normalmente no están siempre visibles (o mostradas), por tanto los
formularios de los ejemplos aquí mostrados se "ocultarán" al minimizarlos, de forma
que sólo se queden en la barra de tareas (junto al reloj) cuando estén minimizados.

/*

Se usa el control NotifyIcon y un ContextMenu creado en ejecución


También se asignan los eventos de los menús en runtime (ejecución)

En C# se pueden asignar los procedimientos de eventos


directamente a los controles, así como al formulario.
*/

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

106
namespace NotifyIcon2CS
{
///
/// Summary description for Form1.
///
public class Form1 : System.Windows.Forms.Form
{
// Se declaran como variables los objetos NotifyIcon y el ContextMenu
NotifyIcon NotifyIcon1 = new NotifyIcon();
ContextMenu ContextMenu1 = new ContextMenu();

private void Salir_Click(object sender, System.EventArgs e)


{
// Este procedimiento se usa para cerrar el formulario,
// se usará como procedimiento de eventos, en principio usado por el botón
Salir
this.Close();
}

private void Restaurar_Click(object sender, System.EventArgs e)


{
// Restaurar por si se minimizó
// Este evento manejará tanto los menús Restaurar como el
NotifyIcon.DoubleClick
Show();
WindowState = FormWindowState.Normal;
Activate();
}

private void AcercaDe_Click(object sender, System.EventArgs e)


{
// Mostrar la información del autor, versión, etc.
MessageBox.Show(Application.ProductName + " v" +
Application.ProductVersion, "Prueba 2 de NotifyIcon en C#");
}

private void Form1_Load(object sender, System.EventArgs e)


{
// Asignar los submenús del ContextMenu
// Añadimos la opción Restaurar, que será el elemento predeterminado
// MenuItem tMenu = new MenuItem("&Restaurar", new
EventHandler(this.Restaurar_Click));
// tMenu.DefaultItem = true;
// ContextMenu1.MenuItems.Add(tMenu);
// Esto también se puede hacer así:
ContextMenu1.MenuItems.Add("&Restaurar", new
EventHandler(this.Restaurar_Click));
ContextMenu1.MenuItems[0].DefaultItem = true;
// Añadimos un separador
ContextMenu1.MenuItems.Add("-");
// Añadimos el elemento Acerca de...
ContextMenu1.MenuItems.Add("&Acerca de...", new
EventHandler(this.AcercaDe_Click));
// Añadimos otro separador
ContextMenu1.MenuItems.Add("-");
// Añadimos la opción de salir
ContextMenu1.MenuItems.Add("&Salir", new EventHandler(this.Salir_Click));

107
// Asignar los valores para el NotifyIcon
NotifyIcon1.Icon = this.Icon;
NotifyIcon1.ContextMenu = this.ContextMenu1;
NotifyIcon1.Text = Application.ProductName;
NotifyIcon1.Visible = true;
// Asignamos los otros eventos al formulario
this.Resize += new EventHandler(this.Form1_Resize);
this.Activated += new EventHandler(this.Form1_Activated);
this.Closing += new
System.ComponentModel.CancelEventHandler(this.Form1_Closing);
// Asignamos el evento DoubleClick del NotifyIcon
this.NotifyIcon1.DoubleClick += new EventHandler(this.Restaurar_Click);
}
private void Form1_Resize(object sender, System.EventArgs e)
{
// Cuando se minimice, ocultarla, se quedará disponible en la barra de
tareas
if( this.WindowState == FormWindowState.Minimized )
this.Visible = false;
}
// la declaramos fuera de la función, para que mantenga su valor
bool PrimeraVez = true;
//
private void Form1_Activated(object sender, System.EventArgs e)
{
if( PrimeraVez )
{
PrimeraVez = false;
Visible = false;
}
}

private void Form1_Closing(object sender,


System.ComponentModel.CancelEventArgs e)
{
// Cuando se va a cerrar el formulario...
// eliminar el objeto de la barra de tareas
this.NotifyIcon1.Visible = false;

// de paso eliminamos el menú contextual


this.ContextMenu1 = null;
}
}
}

LEER LOS PARÁMETROS DE LA LÍNEA DE COMANDOS

Los parámetros de la línea de comandos, son los que se especifican como argumentos
de un ejecutable, por ejemplo:

prueba.exe parámetro1 /parámetro2

Cada parámetro se separa mediante un espacio, (la barra o el signo menos es opcional)

En .NET Framework se pueden obtener usando la clase Environment, de dos formas:

108
- La propiedad CommandLine devuelve TODA la línea de comando, incluyendo el
nombre del ejecutable.

- El método GetCommandLineArgs devuelve un array de tipo String con cada uno


de los parámetros, siendo el que está en el índice cero el nombre del ejecutable.

Hay que tener en cuenta que se entiende que cada parámetro está separado del anterior
mediante un espacio, aunque esto último no es aplicable al nombre del ejecutable, el
cual puede contener espacios tanto en el nombre del ejecutable como en el path del
mismo.

En el código que se muestra a continuación, se usa el método GetCommandLineArgs


para acceder a los parámetros (o argumentos) que se han indicado con el ejecutable.

Para asignar todos los parámetros a un TextBox (o a un array del tipo String):

// Comprobar si hay más de un parámetro,


// el parámetro CERO es el nombre del ejecutable
if( Environment.GetCommandLineArgs().Length > 1 )
textBox1.Lines = Environment.GetCommandLineArgs();
else
textBox1.Text = "No se han indicado parámetros en la línea de
comandos\r\n" +
"El nombre (y path) del ejecutable es:\r\n" +
Environment.GetCommandLineArgs()[0];

Para asignar sólo el primer parámetro (realmente el segundo, ya que el primero es


el nombre y path del ejecutable):

if( Environment.GetCommandLineArgs().Length > 1 ){


String s = Environment.GetCommandLineArgs()[1];
textBox1.Text = s;
}
else
textBox1.Text = "No se han indicado parámetros en la línea de
comandos\r\n" +
"El nombre (y path) del ejecutable es:\r\n" +
Environment.GetCommandLineArgs()[0];

LIMPIADO DE PANTALLA EN C#

El borrado de pantalla en C# no es tan sencillo como en otros lenguajes. Para ello,


debemos crear una clase que lo haga. Su código podría ser el siguiente:

using System;
using System.Runtime.InteropServices;

namespace nsLimpiarPantalla
{
public class LimpiarConsola
{
private const int STD_OUTPUT_HANDLE = -11;
private const byte EMPTY = 32;

109
[StructLayout(LayoutKind.Sequential)]
struct COORD
{
public short x;
public short y;
}

[StructLayout(LayoutKind.Sequential)]
struct SMALL_RECT
{
public short Left;
public short Top;
public short Right;
public short Bottom;
}

[StructLayout(LayoutKind.Sequential)]
struct CONSOLE_SCREEN_BUFFER_INFO
{
public COORD dwSize;
public COORD dwCursorPosition;
public int wAttributes;
public SMALL_RECT srWindow;
public COORD dwMaximumWindowSize;
}

[DllImport("kernel32.dll", EntryPoint="GetStdHandle",
SetLastError=true, CharSet=CharSet.Auto,
CallingConvention=CallingConvention.StdCall)]
private static extern int GetStdHandle(int nStdHandle);

[DllImport("kernel32.dll",
EntryPoint="FillConsoleOutputCharacter", SetLastError=true,
CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)]
private static extern int FillConsoleOutputCharacter(int
hConsoleOutput, byte cCharacter, int nLength, COORD dwWriteCoord, ref int
lpNumberOfCharsWritten);

[DllImport("kernel32.dll",
EntryPoint="GetConsoleScreenBufferInfo", SetLastError=true,
CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)]
private static extern int GetConsoleScreenBufferInfo(int
hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo);

[DllImport("kernel32.dll", EntryPoint="SetConsoleCursorPosition",
SetLastError=true, CharSet=CharSet.Auto,
CallingConvention=CallingConvention.StdCall)]
private static extern int SetConsoleCursorPosition(int
hConsoleOutput, COORD dwCursorPosition);

private int hConsoleHandle;

public LimpiarConsola()
{
hConsoleHandle = GetStdHandle(STD_OUTPUT_HANDLE);
}

public void Limpiar()


{
int hWrittenChars = 0;

110
CONSOLE_SCREEN_BUFFER_INFO strConsoleInfo = new
CONSOLE_SCREEN_BUFFER_INFO();
COORD Home;
Home.x = Home.y = 0;
GetConsoleScreenBufferInfo(hConsoleHandle, ref strConsoleInfo);
FillConsoleOutputCharacter(hConsoleHandle, EMPTY,
strConsoleInfo.dwSize.x * strConsoleInfo.dwSize.y, Home, ref
hWrittenChars);
SetConsoleCursorPosition(hConsoleHandle, Home);
}
}
}

Para poder usar esta clase, debemos añadirla a nuestro proyecto mediante el
namespace nsLimpiarPantalla:

using nsLimpiarPantalla;

Nuestro código main sería el siguiente:

static void Main(string[] args)


{
LimpiarConsola LimpiarMiPantalla = new LimpiarConsola();
Console.WriteLine("Primera Linea");
Console.WriteLine("Segunda Linea");
Console.WriteLine("Tercera Linea");
Console.WriteLine("Cuarta Linea");
Console.WriteLine("Quinta Linea");
Console.WriteLine("Pulsa Intro para limpiar");
Console.ReadLine(); //
LimpiarMiPantalla.Limpiar();
Console.WriteLine("La consola ha sido limpiada");
Console.WriteLine("Pulsa Intro para terminar");
Console.ReadLine();
}

111
COMO VALIDAR SI UNA URL ES CORRECTA

Podemos validar si una URL existe usando la clase HttpWebRequest y su clase


respuesta HttpWebResponse de forma que si se produce una excepción al obtener la
respuesta, nos devolverá false. Conviene aclarar que la URL debe estar bien construida
(es decir, no www.google.es, sino http://www.google.es.

using System.Net;
private bool ValidarURL(string url)
{
try
{
HttpWebRequest HttpWReq =
(HttpWebRequest)WebRequest.Create(url);
HttpWebResponse HttpWResp =(HttpWebResponse)
HttpWReq.GetResponse();
HttpWResp.Close();
return true;
}
catch (Exception ex)
{
string duh = ex.Message;
return false;
}
}

FUNCIONES GENÉRICAS

Una función genérica define un conjunto general de operaciones que se aplicarán a


diferentes tipos de datos. Tiene como parámetro el tipo de datos que le son pasados
para que opere sobre ellos. Esto nos permite que pueda aplicarse el mismo
procedimiento general sobre un amplio rango de datos.

Veamos como funciona con un ejemplo:

public static void intercambia<X>(ref X a,ref X b)


{
X temporal;
temporal = a;
a=b;
b= temporal;
}
public static void Main(string[] args)
{
int i = 10, j=20;
float x=10.1F, y=23.3F;
Console.WriteLine("Original i, j: {0} , {1} \n",i,j);
Console.WriteLine("Original x, y: {0} , {1} \n",x,y);
intercambia(ref i,ref j);
intercambia(ref x,ref y);
Console.WriteLine("Nuevo i, j: {0} , {1} \n",i,j);
Console.WriteLine("Nuevo x, y: {0} , {1} \n",x,y);
string fin = Console.ReadLine();

112
La línea void intercambia<X>(ref X a,ref X b) le indica al compilador que se está
comenzando una definición genérica. X es el tipo de datos de los valores que serán
intercambiados. En main se llama a la función intercambia() utilizando dos tipos
diferentes de datos: enteros y flotantes. Puesto que es una función genérica, el
compilador crea automáticamente dos versiones de ella – una que intercambia valores
enteros y otra con valores flotantes. Podemos definir más de un tipo genérico siempre
que los separemos por comas. Veamos otro ejemplo:

public static void mifunc<tipo1,tipo2>(tipo1 x, tipo2 y)


{
Console.WriteLine(" {0} , {1}",x,y);
}
public static void Main(string[] args)
{
mifunc(10,"hola");
mifunc(0.23,10L);
string fin = Console.ReadLine();
}

Las funciones genéricas son equivalentes a las sobrecargadas excepto que son más
restrictivas. Cuando las funciones se sobrecargan, dentro de ellas pueden ocurrir
acciones diferentes, pero una genérica debe realizar la misma función general para
todas las versiones. Aunque una función plantilla puede sobrecargarse a sí misma, si
es necesario también es posible sobrecargarla de forma explícita. En este caso, la
función sobrecargada redefine (“u oculta”) la función genérica relativa a esa versión
específica. Veamos una versión diferente del primer ejemplo:

public static void intercambia<X>(ref X a,ref X b)


{
X temporal;
temporal = a;
a=b;
b= temporal;
}
// Redefinición de la versión genérica de intercambia()
public static void intercambia(int a, int b)
{
Console.WriteLine("Esto está dentro de intercambia(int,
int)\n");
}
public static void Main(string[] args)
{
int i = 10, j=20;
float x=10.1F, y=23.3F;
Console.WriteLine("Original i, j: {0} , {1} \n",i,j);
Console.WriteLine("Original x, y: {0} , {1} \n",x,y);
intercambia(i,j); // llamada a la versión de
intercambia() sobrecargada explícitamente
intercambia(ref x,ref y); // intercambio de floats
Console.WriteLine("Nuevo i, j: {0} , {1} \n",i,j);
Console.WriteLine("Nuevo x, y: {0} , {1} \n",x,y);
string fin = Console.ReadLine();

113
Cuando llamamos a intercambia(i,j) se invoca a la versión sobrecargada explícitamente
en el programa. Esta sobrecarga permite confeccionar una versión de una función
genérica para que se adapte a una situación especial. Sin embargo, en general, si
necesitamos tener diferentes versiones para una función para diferentes tipos de datos,
deberíamos usar funciones sobrecargadas y no plantillas.

CLASES GENÉRICAS

Además de las funciones genéricas podemos definir una clase genérica, esto es, una
clase que define todos los algoritmos usados por ella, pero el tipo de datos que,
realmente, va a ser manipulado se especificará como un parámetro cuando se creen
los objetos de esa clase.

Son útiles cuando una clase contiene una lógica generalizable. Por ejemplo, el mismo
algoritmo que almacena una cola de enteros funciona también para una cola de
caracteres. Lo mismo podríamos decir con una lista enlazada. El compilador generará
automáticamente el tipo adecuado de objeto en función del tipo que se especifique
cuando se crea el objeto.

La forma general de la declaración de una clase genérica se muestra aquí:

public class Generic<T>

<T> es el nombre del tipo de resguardo o parámetro de tipo que se especificará cuando
se instancie la clase. Si es necesario, puede definirse más de un tipo de datos genéricos
usando una lista separada por comas.

Una vez que se ha creado una clase genérica se puede crear una instancia específica de
esa clase usando la siguiente forma general:

Nombredeclase <Tipo> ob;

Tipo es el nombre del tipo de datos sobre el que trabajará la clase. Los miembros de
una clase genérica son, por sí mismos, genéricos.

La declaración de una clase genérica es similar a la de una función genérica. El tipo


real de datos almacenado por la lista, en la declaración de la clase es genérico.

Una clase plantilla puede tener más de un tipo genérico de datos (parámetro de tipo).
Simplemente hay que declarar todos los tipos de datos requeridos por la clase en una
lista separada por comas.

114
Veamos el siguiente ejemplo:

class miclase<Tipo1,Tipo2>
{
Tipo1 i;
Tipo2 j;
public miclase(Tipo1 a, Tipo2 b)
{
i=a;
j=b;
}
public void Mostrar()
{
Console.WriteLine("{0} {1}\n",i,j);

}
}
class MainClass
{
public static void Main(string[] args)
{
miclase<int, double> ob1= new miclase<int,
double>(10,0.23);
miclase<char, string> ob2= new miclase<char,
string>('X',"Esto es una prueba");
ob1.Mostrar();
ob2.Mostrar();
string fin= Console.ReadLine();
return;
}
}

}
}

PARÁMETROS DE TIPO

Cuando una clase genérica es instanciada, lo hace con un parámetro de tipo, enlazando
la implementación de clase incompleta y sin forma con un tipo real, creando una
instancia de clase genérica concreta, como se muestra en el siguiente ejemplo:

Cliente<ItemPedido> cliente = new Cliente<ItemPedido>();

Los parámetros de tipo se especifican siempre usando el nuevo operador de genéricos


<>.

Aunque podemos nombrar nuestros parámetros de tipo como deseemos, por convenio
se suele definir la letra “T” como el nombre estándar para el primer parámetro de tipo.
Si nuestra clase requiere parámetros adicionales, el convenio dicta que continuemos
con las letras del alfabeto siguientes: “U”, “V”… Si llegamos a la “Z” mediante
parámetros de tipo, tendríamos un problema de diseño tan grande que posiblemente
los genéricos por sí solos no lo podrían solucionar.

115
Para situaciones en las cuales el propósito del parámetro de tipo no es suficientemente
explicativo por si mismo, Microsoft recomienda usar un nombre descriptivo para el
parámetro de tipo predecido de la T mayúscula, como se muestra en el siguiente
código:

public class MiGenerico<TClaseAcesoDatos>

Para especificar múltiples parámetros de tipo en una definición de clase, simplemente


debemos separarlas con comas:

public class MiGenerico<T, U, V>

Y para instanciar una clase que requiere múltiples parámetros de tipo, separaremos
los tipos con comas como sigue:

MiGenerico<int, string, object> x = new MyGenerics<int, string, object>();

APLICANDO RESTRICCIONES A LOS PARÁMETROS DE TIPO

Cuando tenemos un parámetro de tipo especificado en nuestra definición de clase, en


realidad hay muy poco que podamos hacer con ello por defecto. Por ejemplo, ¿cómo
podemos crear una nueva instancia de ese objeto?. Podríamos pensar que el siguiente
código debería estar libre de errores de compilación:

public class Cliente<T>


{
public void AgregarItemDetalle(string nombreItem)
{
T nuevoItem = new T();
items.Add(nuevoItem);
}
}

Desgraciadamente, C# no tiene forma de conocer si el tipo de datos especificado por


el parámetro T tiene un constructor por defecto, así que el compilador no permitirá
que usemos ese tipo en una nueva sentencia sin parámetros.

Para evitar esto, debemos usar restricciones en los parámetros de tipo. Estas
restricciones especifican ciertos requerimientos que el parámetro de tipo ha de
cumplir. Por ejemplo, podemos especificar que cualquier tipo pasado a nuestra clase
genérica deba implementar un constructor (sin parámetros) por defecto.

Todas las restricciones en las clases genéricas se indican con la palabra clave “where”
y podemos usar múltiples restricciones en el mismo parámetro separándolas con una
coma. Una muestra de cómo podemos especificar una restricción se puede ver en el
siguiente código:

public class Cliente<TItemDetalle> where TItemDetalle : new()

116
La restricción precedente indica que el parámetro de tipo especificado por
TItemDetalle debe implementar un constructor por defecto. Las siguientes líneas
muestran otras formas de especificar restricciones:

public class ListaClientes<T> where T: class, IListaItems


public class Cliente<T,U> where T: new() where U: IDatoCliente

117
La siguiente tabla muestra todas las restricciones que podemos aplicar a los
parámetros de tipo genéricos.

Restricciones de Parámetros de tipo Genéricos

Restricción

Descripción

where T:struct

Indica que T debe ser un tipo valor (excepto Nullable)

where T:class

Indica que T debe ser un tipo referencia, incluyendo cualquier clase, delegado
o interface.

where T:new()

Indica que T debe implementar un constructor por defecto. La restricción


new() debe ser especificada la última si se usa con varias restricciones en el
mismo tipo.

where T: (clase base)

Indica que T debe ser o derivar de la clase base indicada.

where T: (interface)

Indica que T debe ser o implementar el interfaz indicado.

118
Un patrón de diseño muy utilizado en la POO es el patrón factory o fábrica. En este
patrón, el código solicita nuevas instancias de un tipo dado desde esa fábrica de tipos
más que instanciarlos directamente. Esto da a la fábrica control completo sobre el
proceso de instanciación, además de la posibilidad de hacer cosas como cachés de tipos
preinstanciaos, ejecutar llamadas a Remoting o servicios Web para obtener datos de
soporte, y mucho más.

Para prevenir que la fábrica no llegue a tener demasiadas conversiones de tipos, el


patrón ofrece llamadas para el desarrollo de una fábrica única para cada clase. De esta
forma, si tenemos una clase Cliente, siempre tendremos una clase FábricaClientes. Este
modelo se usa en sistemas de ORM (Object-Relational Mapping) o en Container-
Managed Persistente (CMP).

Para ver como podemos hacer un uso rápido de genéricos y restricciones de parámetro
de tipo, echemos un vistazo al siguiente código, el cual crea una clase fábrica que
puede servir tanto al tipo Cliente como al tipo ClienteEspecial:

public class FabricaClientes<TCliente, U> where TCliente : Cliente, new()


{
public TCliente ObtenerCliente()
{
return new TCliente();
}
}

El código precedente restringe al tipo TCliente requiriendo que sea o derive del tipo
Cliente además de implementar un constructor por defecto. Ya que el argumento
TCliente ha sido restringido con new(), el código puede explícitamente crear una
nueva instancia de ese tipo. A través de la potencia de los genéricos, este nuevo
ejemplar no es otra cosa que el tipo exacto especificado. No se requiere entonces
ninguna conversión de tipos en la clase fábrica, lo cual es un gran beneficio para los
desarrolladores OOP.

DELEGADOS GENÉRICOS

Otras de las entidades que pueden ser definidas de forma genérica son los delegados.
Supongamos que se desea definir un delegado que permita llamar a cualquier método
que reciba un único parámetro de un tipo T y no devuelva nada. La siguiente sentencia
cumple con ese objetivo:

public delegate void Procedimiento<T>(T t);

119
A partir de la declaración anterior se pueden crear y utilizar objetos de tipo delegado:

static void Main(string[] args)


{
// procedimientos
Action<int> p_int2 = imprimirEntero;
Action<string> p_str2 = imprimirCadena;
// llamadas
p_int2(25);
p_str2("Hola");
Console.ReadLine();
}

static void imprimirEntero(int n)


{
Console.WriteLine(n);
}

static void imprimirCadena(string s)


{
Console.WriteLine(s != null ? s.ToUpper() : "NULO");
}

Un tipo equivalente a nuestro Procedimiento <T> ya está disponible en nuestro FW


como Action<T>.

public delegate Action<T>(T t);

Para facilitar la aplicación de una misma acción a todos los elementos de un array, la
clase System.Array ofrece un método ForEach<T>() que recibe, además del array sobre
el que actuar, una instancia de delegado de tipo Action<T>. Eso nos permite escribir
código como el siguiente:

int[] arr = new int[4] { 34, 44, 12, 23 };


Action<int> imprimir = imprimirEntero;
Array.ForEach<int>(arr, imprimir);

Si se estudia mediante reflector, en realidad Action es un simple MulticastDelegate


que además incorpora adicionalmente métodos varios, y específicamente dos que dan
soporte a las llamadas asíncronas (BeginInvoke() y EndInvoke()). Por ejemplo, si se
modifica ligeramente el método imprimirEntero() para que produzca una demora
aleatoria antes de imprimir:

private static Random semilla = new Random();

static void imprimirEntero(int n)


{
System.Threading.Thread.Sleep(semilla.Next(5000));
Console.WriteLine(n);
}

120
Se podrá comprobar que al ejecutar el bucle a continuación, los elementos del array se
imprimen en un orden diferente cada vez:

for (int i = 0; i < arr.Length; i++)


imprimir.BeginInvoke(arr[i], null, null);

Otro delegado genérico interesante es Predicate<T> definido de la siguiente forma:

public delegate bool Predicate<T>(T t);

Representa a un delegado que opera sobre un objeto del tipo T y devuelve un booleano
dependiendo de si cumple o no una condición. System.Array ofrece varios métodos
que permiten recorrer los elementos de un array buscando cuáles de ellos cumplen con
una condición llegada “desde fuera”:

if (Array.Exists(arr, delegate(int e) { return e == 23; }))


Console.WriteLine("El 23 está presente");

INTERFACES GENÉRICOS

Un interface genérico funciona de una forma muy similar a como lo hace una clase
genérica. El interface en sí mismo acepta uno o más parámetros de tipo de a misma
forma que lo hace una clase genérica. En vez de utilizar el parámetro de tipo en la
definición de la clase, el parámetro de tipo es entonces usado en las sentencias de
declaración de miembros de las que consta el interface.

Un importante beneficio de permitir parámetros de tipo genéricos en los interfaces es


que las clases que implementan dichos Interfaces no necesitan ejecutar ningún
boxing/unboxing innecesarios. Lo mismo podríamos decir de las conversiones de
tipos. Boxing/unboxing son las operaciones que permiten cambiar entre tipos valor y
referencia, y son operaciones costosas. Veamos un ejemplo de interface genérico:

using System;
using System.Collections.Generic;
using System.Text;

namespace Generics1
{
public interface IManejadorDatos<TTipoFila> where TTipoFila : new()
{
void AgregarRegistro (TTipoFila registro);
void BorrarRegistro(TTipoFila registro);
void ActualizarRegistro(TTipoFila registro);
void SeleccionarRegistro(TTipoFila registro);
List<TTipoFila> SeleccionaRegistros();
}
}

Lo que hace el anterior interface es indicar que cualquier clase que implemente
IManejadorDatos<TTipoFila> debe proporcionar los métodos estándar Agregar,
Borrar, Actualizar y Seleccionar típicamente asociados con objetos de acceso a datos

121
independientemente del tipo de datos subyacente que sea almacenado. Este es un
interfaz extremadamente útil que nos va a permitir una gran flexibilidad.

COLECCIONES GENÉRICAS

Una de las utilizaciones más comunes de genéricos se basa en la implementación y


manipulación de colecciones fuertemente tipadas. En este apartado veremos muestras
de cómo trabajar con los tipos de colecciones genéricas disponibles en la versión 2.0 de
NET Framework: la clase Dictionary<>, la clase List<>, la clase Queue<> y la clase
Stack<>.

Clase Dictionary

Esta clase está diseñada para almacenar valores asociados a un valor de búsqueda.
Tradicionalmentese ha usado esta clase para almacenar valores con cadenas, tales
como nombres, palabras clave o GUIDs, pero además ahora podemos usar un objeto
de cualquier tipo como clave y otro objeto de cualquier tipo como valor. Ahora
podemos especificar la clave y el tipo del valor al mismo tiempo de la creación del
objeto usando parámetros de tipo, como se muestra en el siguiente fragmento de
código:

// Diccionario de Prueba
Dictionary<string, int> diasMes = new Dictionary<string, int>();
diasMes["Enero"] = 31;
diasMes["Febrero"] = 28;
diasMes["Marzo"] = 31;
Console.WriteLine("Marzo tiene " + diasMes["Marzo"].ToString() + " días.");

Este ejemplo crea y manipula una clase Dictionary<> en la que todas las claves son
cadenas, y todos los valores son números, almacenando el número de dias en cada
mes.

Clase List

La clase List<> se explica por si misma fácilmente. Su único propósito es almacenar


una lista de ítems. Sin genéricos, las listas eran responsabilidad de un conjunto de tipos
System.Object y cuando el desarrollador necesitaba un tipo de datos fuera de ella,
debía incluir operaciones de conversión de tipos y código potencialmente enrevesado.
En el siguiente ejemplo vemos como crear y usar arbitrariamente listas de datos
fuertemente tipados nunca había sido tan fácil:
// Clase List
List<Cliente> listaClientes = new List<Cliente>();
Cliente nuevoCli = new Cliente();
nuevoCli.Apellido = "Hoffman";
nuevoCli.Nombre = "Kevin";
nuevoCli.IDCliente = 1;
listaClientes.Add(nuevoCli);
Console.WriteLine(string.Format("Hay {0} clientes, el primero es {1} {2}",
listaClientes.Count, listaClientes[0].Nombre, listaClientes[0].Apellido));

122
El código precedente produce la siguiente salida:

Hay un clientes, el primero es Kevin Hoffman

Conviene fijarse en el hecho de que ahora podemos escribir código como el siguiente
sin tener que escribir ninguna de nuestras propias clases para ello:

listaClientes[0].Nombre

Antes, para obtener una lista como la mostrada en este ejemplo, los desarrolladores
solían pasar muchas horas creando sus propias clases de colección fuertemente
tipadas. Ahora podemos crearlas en tiempo de ejecución sin pérdida de rendimiento
y sin necesidad de hacer conversión de tipos de forma que el código es más sencillo y
reutilizable.

Clase Queue

La clase Queue no es más que una cola FIFO (First In First Out). Esta clase nos permite
especificar el tipo de datos de los elementos en una cola al momento de la
instanciación, como se muestra en el siguiente ejemplo:

Queue<Cliente> cClientes = new Queue<Cliente>();


cClientes.Enqueue(nuevoCli);
Cliente dqCli = cClientes.Dequeue();
Console.WriteLine(string.Format("Extraido de la cola el siguiente cliente:
{0} {1}",dqCli.Nombre, dqCli.Apellido));

Clase Stack

Al contrario que la clase Queue<>, la clase Stack<> es una colección LIFO (Last-in
First-out), es decir, una pila. El siguiente código muestra como colocar elementos en
una pila genérica y como obtenerlos. Para asegurarnos de saber cómo trabaja la pila,
podemos jugar con el cambio de orden en el que colocamos los ítems para ver los
resultados:

Stack<Cliente> pilaClientes = new Stack<Cliente>();


Cliente cli1 = new Cliente();
cli1.Nombre = "John";
cli1.Apellido = "Doe";
cli1.IDCliente = 99;
pilaClientes.Push(nuevoCli);
pilaClientes.Push(cli1);
Cliente popCli = pilaClientes.Pop();
Console.WriteLine(string.Format("El Cliente extraido de la pila es {0}
{1}",popCli.Nombre, popCli.Apellido));

INFERENCIA DE TIPOS

Permite llamar a un método genérico sin especificar el tipo (de lo que se encarga el
compilador). A continuación, creamos una función genérica a la que llamamos
primero indicando el tipo, y luego usando inferencia de tipos.

123
static void FuncionGenerica<T>(T t)
{
Console.WriteLine(t.GetType().ToString());
}

FuncionGenerica<int>(5);
FuncionGenerica(5);

En ambos casos se muestra por pantalla System.Int32

124
ESTRUCTURAS DE DATOS

ÁRBOLES DE BÚSQUEDA BINARIA

Un árbol de búsqueda binaria es un árbol binario con la propiedad adicional de que,


para cada nodo, el hijo izquierdo tiene un valor inferior al padre, que a su vez tiene un
valor inferior al padre, que a su vez tiene un valor igual o inferior al del hijo derecho.

Una interfaz para dicho tipo de árbol, podría venir definida así:

// Define la interfaz para un árbol de búsqueda binaria


interface IBinarySearchTree<T> : IBinaryTree<T>
{
void AddElement(T Element);
T RemoveElement(T TargetElement);
void RemoveAllOcurrences(T TargetElement);
T RemoveMin();
T RemoveMax();
T FindMin();
T FindMax();
}

IBinarySearchTree hereda de la interfaz para árboles de búsqueda binaria IbinaryTree.

public interface IBinaryTree<T>


{
// Eliminar el subárbol izquierdo de la raíz del árbol binario
void RemoveLeftSubtree();
// Eliminar el subárbol derecho de la raíz del árbol binario
void RemoveRightSubtree();
// Eliminar todos los elementos del árbol binario
void RemoveAllElements();
// Devuelve true si el árbol está vacío y false en caso contrario
bool IsEmpty();
// Devuelve el número de elementos del árbol binario
int Size{get;}
// Devuelve true si el árbol contiene un elemento que se corresponda
// con el especificado y false en caso contrario
bool Contains(T TargetElement);
// Devuelve una referencia al elemento especificado, si se encuentra dentro
// del árbol. Genera una excepción si el elemento especificado no se encuentra
// en el árbol
T Find(T TargetElement);
// Devuelve la representación del árbol en forma de cadena
string ToString();
// Realiza un recorrido en árbol del binario, invocando un método recursivo
// sobrecargado de recorrido en InOrden que comienza por la raíz
IEnumerator<T> IteratorInOrder();
// Realiza un recorrido en PreOrden del árbol binario comenzando por la raíz
IEnumerator<T> IteratorPreOrder();
// Realiza un recorrido en PostOrden del árbol binario comenzando por la raíz
IEnumerator<T> IteratorPostOrder();
// Realiza un recorrido por niveles del árbol binario usando una cola
IEnumerator<T> IteratorLevelOrder();
}

125
IMPLEMENTACIÓN DE ÁRBOLES DE BÚSQUEDA BINARIA MEDIANTE
ENLACES

Vamos a utilizar una clase BinaryTreeNode para representar un nodo del árbol. Cada
objeto de esta clase, mantiene una referencia al elemento almacenado en dicho nodo,
así como referencias a cada uno de los nodos hijos.
/// <summary>
/// Representa el nodo de un árbol binario
/// </summary>
/// <typeparam name="T">Tipo del dato a almacenar en el nodo</typeparam>
public class BinaryTreeNode<T>{
/// <summary>
/// Elemento a almacenar en el nodo
/// </summary>
private T element;
/// <summary>
/// Propiedad que recoge el elemento a almacenar en el nodo
/// </summary>
public T ElementT {
get { return element; }
set { element = value; }
}
// Hijos izquierdo y derecho del nodo
protected BinaryTreeNode<T> left, right;
// Crea un nuevo nodo del árbol con los datos especificados
public BinaryTreeNode(T Obj) {
element = Obj;
left = null;
right = null;
}
/// <summary>
/// Número de hijos del nodo
/// </summary>
public int NumChildren {
get
{
int children = 0;
if (left != null)
children = 1 + left.NumChildren;
if (right != null)
children = 1 + right.NumChildren;
return children;
}
}
/// <summary>
/// Hijo izquierdo del nodo
/// </summary>
public BinaryTreeNode<T> LeftNode{
get { return left; }
set { left = value; }
}
/// <summary>
/// Hijo derecho del nodo
/// </summary>
public BinaryTreeNode<T> RightNode {
get { return right; }
set { right = value; }
}
}

126
Nos basamos en una clase LinkedBinaryTree que implementa el interfaz IBinaryTree:

/// <summary>
/// Implementación de Arbol Binario enlazado
/// </summary>
/// <typeparam name="T">Tipo de datos de los elementos a almacenar en el árbol</typeparam>
public class LinkedBinaryTree<T> : IBinaryTree<T>
{
/// <summary>
/// Variable privada que recoge el número de nodos del árbol
/// </summary>
protected int count;
/// <summary>
/// Nodo raíz del árbol
/// </summary>
protected BinaryTreeNode<T> root;

/// <summary>
/// Contructor sin argumentos
/// </summary>
public LinkedBinaryTree()
{
count = 0;
root = null;
}

/// <summary>
/// Constructor que recoge un elemento como argumento
/// </summary>
/// <param name="Element">Elemento de tipo <b>T</b> que es raíz del árbol</param>
public LinkedBinaryTree(T Element)
{
count = 1;
root = new BinaryTreeNode<T>(Element);
}
/// <summary>
/// Constructor que recoge un elemento raíz y dos árboles hijos (izquierdo y derecho)
/// </summary>
/// <param name="Element">Elemento de tipo <b>T</b> que es raíz del árbol</param>
/// <param name="LeftSubtree">Árbol hijo izquierdo</param>
/// <param name="RightSubtree">Árbol hijo derecho</param>
public LinkedBinaryTree(T Element, LinkedBinaryTree<T> LeftSubtree, LinkedBinaryTree<T>
RightSubtree)
{
root = new BinaryTreeNode<T>(Element);
count = 1;
if (LeftSubtree != null)
{
count += LeftSubtree.Size;
root.LeftNode = LeftSubtree.root;
}
else
root.LeftNode = null;
if (RightSubtree != null)
{
count += RightSubtree.Size;
root.RightNode = RightSubtree.root;
}
else
root.RightNode = null;

127
}

/// <summary>
/// Número de nodos del árbol
/// </summary>
public int Size
{
get { return count; }
}

/// <summary>
/// Elimina el árbol hijo izquierdo
/// </summary>
public void RemoveLeftSubtree()
{
if (root.LeftNode != null)
count -= root.LeftNode.NumChildren - 1;
root.LeftNode = null;
}

/// <summary>
/// Elimina el árbol hijo derecho
/// </summary>
public void RemoveRightSubtree()
{
if (root.RightNode != null)
count -= root.LeftNode.NumChildren - 1;
root.RightNode = null;
}

/// <summary>
/// Elimina todos los elementos del árbol
/// </summary>
public void RemoveAllElements()
{
count = 0;
root = null;
}

/// <summary>
/// Busca un elemento de tipo <b>T</b> den el árbol
/// </summary>
/// <param name="TargetElement">Elemento a buscar en el árbol</param>
/// <returns>Devuelve un elemento de tipo <B>T</B> encontrado en el árbol. Sino, devuelve el valor por
defecto del tipo <B>T</B></returns>
public T Find(T TargetElement)
{
BinaryTreeNode<T> current = findAgain(TargetElement, root);
if (current == null)
return default(T);
return current.ElementT;
}

/// <summary>
/// Iterador para recorrer el árbol en InOrden
/// </summary>
/// <returns>Devuelve un iterador (IEnumerator) basado en el recorrido InOrden</returns>
public IEnumerator<T> IteratorInOrder()
{
List<T> templist = new List<T>();

128
inorder(root, templist);
return templist.GetEnumerator();
}
public bool Contains(T TargetElement)
{
throw new NotImplementedException("Método no implementado");
}
/// <summary>
/// Indica si el árbol está vacío o no
/// </summary>
/// <returns>Devuelve <b>true</b> si está vacío, <b>false</b> en caso contrario</returns>
public bool IsEmpty()
{
return (root == null);
}
public IEnumerator<T> IteratorPreOrder()
{
throw new NotImplementedException("Método no implementado");
}
// Realiza un recorrido en PostOrden del árbol binario comenzando por la raíz
public IEnumerator<T> IteratorPostOrder()
{
throw new NotImplementedException("Método no implementado");
}
// Realiza un recorrido por niveles del árbol binario usando una cola
public IEnumerator<T> IteratorLevelOrder() {
throw new NotImplementedException("Método no implementado");
}
/// <summary>
/// Método privado para recorrido InOrden del árbol
/// </summary>
/// <param name="Node">Nodo de partida</param>
/// <param name="TempList">Lista temporal de nodos</param>
protected void inorder(BinaryTreeNode<T> Node, List<T> TempList) {
if (Node != null)
{
inorder(Node.LeftNode, TempList);
TempList.Add(Node.ElementT);
inorder(Node.RightNode, TempList);
}
}
/// <summary>
/// Método privado recursivo para búsqueda de elemento en nodo
/// </summary>
/// <param name="TargetElement">Elemento a buscar</param>
/// <param name="Next">Próximo nodo a buscar</param>
/// <returns>Devuelve el nodo</returns>
private BinaryTreeNode<T> findAgain(T TargetElement, BinaryTreeNode<T> Next) {
if (Next == null)
return null;
if (Next.ElementT.Equals(TargetElement))
return Next;
BinaryTreeNode<T> temp = findAgain(TargetElement, Next.LeftNode);
if (temp == null)
temp = findAgain(TargetElement, Next.RightNode);
return temp;
}
}

129
Nuestra clase LinkedBinarySearchTree ofrece dos constructores: uno para crear un
árbol LinkedBinarySearchTree vacío y otro para crear un árbol con un elemento
concreto como raíz. Ambos constructores únicamente hacen referencia a los
constructores equivalentes de la superclase:
/// <summary>
/// Crea un árbol de búsqueda binaria vacío
/// </summary>
public LinkedBinarySearchTree() : base(){}
/// <summary>
/// Crea un árbol de búsqueda binaria utilizando un elemento especificado como raíz
/// </summary>
/// <param name="Element"></param>
public LinkedBinarySearchTree(T Element) : base(Element){ }

El método addElement añade un elemento especificado en una ubicación concreta del


árbol, teniendo en cuenta su valor. Si el elemento no es de tipo Icomparable, se genera
una excepción. Si el árbol está vacío, el nuevo elemento pasará a ser la raíz. Si es menor
que el almacenado en la raíz y el hijo izquierdo de la raíz es nulo, entonces el nuevo
elemento se convertirá en el hijo izquierdo del raíz. Si el nuevo elemento es menor que
el raíz y el hijo izquierdo no es nulo, entonces nos desplazamos hasta el hijo izquierdo
del raíz y volvemos a efectura la comparación. Si el nuevo elemento es mayor o igual
que el almacenado en la raíz y el hijo derecho es nulo, entonces el nuevo elemento
pasará a ser el hijo derecho del raíz. Si el nuevo elemento es mayor o igual que el
almacenado en la raíz y el hijo derecho no es nulo, entonces nos desplazamos hasta el
hijo derecho del raíz y volvemos a efectuar la comparación.
/// <summary>
/// Añade el elemento especificado al árbol de búsqueda binaria seleccionando
/// la posición apropiada de acuerdo con su valor clave. Los elementos iguales
/// se añaden a la derecha
/// </summary>
/// <param name="Element">Elemento a añadir al árbol</param>
public void AddElement(T Element) {
BinaryTreeNode<T> temp = new BinaryTreeNode<T>(Element);
IComparable<T> comparableElement = (IComparable<T>) Element;
if (IsEmpty())
root = temp;
else {
BinaryTreeNode<T> current = root;
bool added = false;
while (!added) {
if (comparableElement.CompareTo(current.ElementT) < 0)
if (current.LeftNode == null) {
current.LeftNode = temp;
added = true; }
else
current = current.LeftNode;
else
if (current.RightNode == null) {
current.RightNode = temp;
added = true;}
else
current = current.RightNode;
}
}
count++;
}

130
Por contra, RemoveElement elimina un elemento especificado de tipo IComparable de
un árbol de búsqueda binaria, o genera una NullReferenceException en caso de que
no exista. A diferencia de un árbol binario normal, no podemos simplemente eliminar
un nodo haciendo que la referencia puentee al nodo que hay que eliminar. En lugar de
ello, debemos “promover” a otro nodo para que sustituya al que está siendo eliminado.
El método protegido denominado replacement devuelve una referencia a un nodo que
sustituirá al que se ha especificado para la eliminación. Existen tres casos distintos para
la selección del nodo de sustitución:

− Si el nodo no tiene ningún hijo, la función de sustitución devolverá un nulo


− Si el nodo sólo tiene un hijo, la función devolverá dicho hijo.
− Si el nodo que hay que eliminar tiene dos hijos, la función de sustitución
devolverá el sucesor del nodo que hay que eliminar, según el algoritmo de
recorrido del árbol (ya que los elementos de igual valor se colocan a la derecha).

/// <summary>
/// Elimina del árbol de búsqueda binaria el primer elemento que se corresponda
/// con el elemento objetivo especificado y devuelve una referencia al mismo.
/// Genera NullReferenceException si no se encuentra el elemento objetivo
/// especificado dentro del árbol de búsqueda binario
/// </summary>
/// <param name="TargetElement">Elemento a eliminar</param>
/// <returns>Devuelve una referencia al elemento eliminado</returns>
public T RemoveElement(T TargetElement)
{
T result = default(T);
if (IsEmpty())
if (((IComparable)TargetElement).Equals(root.ElementT))
{
result = root.ElementT;
root = replacement(root);
count--;
}
else
{
BinaryTreeNode<T> current, parent = root;
bool found = false;
if (((IComparable)TargetElement).CompareTo(root.ElementT) < 0)
current = root.LeftNode;
else
current = root.RightNode;
while (current != null && !found)
{
if (TargetElement.Equals(current.ElementT))
{
found = true;
count--;
result = current.ElementT;
if (current == parent.LeftNode)
{
parent.LeftNode = replacement(current);
}
else
{
parent.RightNode = replacement(current);

131
}
}
else
{
parent = current;
if (((IComparable)TargetElement).CompareTo(current.ElementT) < 0)
current = current.LeftNode;
else
current = current.RightNode;

}
}
if (!found)
throw new NullReferenceException("No se ha encontrado el elemento indicado en el árbol
binario");
}
return result;
}

/// <summary>
/// Reemplaza un nodo con una referencia a otro
/// </summary>
/// <param name="node">Nodo a reemplazar</param>
/// <returns>Devuelve una referencia a un nodo que se ha especificado en node</returns>
protected BinaryTreeNode<T> replacement(BinaryTreeNode<T> node)
{
BinaryTreeNode<T> result = null;
if ((node.LeftNode == null) && (node.RightNode == null))
result = null;
else if ((node.LeftNode != null) && (node.RightNode == null))
result = node.LeftNode;
else if ((node.LeftNode==null) && (node.RightNode !=null))
result = node.RightNode;
else
{
BinaryTreeNode<T> current = node.RightNode;
BinaryTreeNode<T> parent = node;
while (current.LeftNode != null)
{
parent = current;
current = current.LeftNode;
}
if (node.RightNode == current)
current.LeftNode = node.LeftNode;
else
{
parent.LeftNode = current.RightNode;
current.RightNode = node.RightNode;
current.LeftNode = node.LeftNode;
}
result = current;
}
return result;

El método removeAllOcurrences elimina del árbol todas las apariciones de un


elemento especificado.

132
/// <summary>
/// Elimina del árbol de búsqueda binaria los elementos que se
/// correspondan con el elemento objetivo especificado
/// </summary>
/// <param name="TargetElement"></param>
public void RemoveAllOcurrences(T TargetElement)
{
RemoveElement(TargetElement);
while (Contains((T)TargetElement))
RemoveElement(TargetElement);

El código completo:

/// <summary>
/// Árbol de búsqueda binaria
/// </summary>
/// <typeparam name="T">Tipo de cada uno de los elementos que va a recoger el árbol</typeparam>
public class LinkedBinarySearchTree<T> : LinkedBinaryTree<T>, IBinarySearchTree<T> where T :
IComparable
{
/// <summary>
/// Crea un árbol de búsqueda binaria vacío
/// </summary>
public LinkedBinarySearchTree() : base()
{

/// <summary>
/// Crea un árbol de búsqueda binaria utilizando un elemento especificado como raíz
/// </summary>
/// <param name="Element"></param>
public LinkedBinarySearchTree(T Element) : base(Element)
{

/// <summary>
/// Añade el elemento especificado al árbol de búsqueda binaria seleccionando
/// la posición apropiada de acuerdo con su valor clave. Los elementos iguales
/// se añaden a la derecha
/// </summary>
/// <param name="Element">Elemento a</param>
public void AddElement(T Element)
{
BinaryTreeNode<T> temp = new BinaryTreeNode<T>(Element);
IComparable<T> comparableElement = (IComparable<T>)Element;
if (IsEmpty())
root = temp;
else
{
BinaryTreeNode<T> current = root;
bool added = false;
while (!added)
{
if (comparableElement.CompareTo(current.ElementT) < 0)
if (current.LeftNode == null)
{

133
current.LeftNode = temp;
added = true;
}
else
current = current.LeftNode;
else
if (current.RightNode == null)
{
current.RightNode = temp;
added = true;
}
else
current = current.RightNode;
}
}
count++;
}

/// <summary>
/// Elimina del árbol de búsqueda binaria el primer elemento que se corresponda
/// con el elemento objetivo especificado y devuelve una referencia al mismo.
/// Genera NullReferenceException si no se encuentra el elemento objetivo
/// especificado dentro del árbol de búsqueda binario
/// </summary>
/// <param name="TargetElement">Elemento a eliminar</param>
/// <returns>Devuelve una referencia al elemento eliminado</returns>
public T RemoveElement(T TargetElement)
{
T result = default(T);
if (IsEmpty())
if (((IComparable)TargetElement).Equals(root.ElementT))
{
result = root.ElementT;
root = replacement(root);
count--;
}
else
{
BinaryTreeNode<T> current, parent = root;
bool found = false;
if (((IComparable)TargetElement).CompareTo(root.ElementT) < 0)
current = root.LeftNode;
else
current = root.RightNode;
while (current != null && !found)
{
if (TargetElement.Equals(current.ElementT))
{
found = true;
count--;
result = current.ElementT;
if (current == parent.LeftNode)
{
parent.LeftNode = replacement(current);
}
else
{
parent.RightNode = replacement(current);
}
}

134
else
{
parent = current;
if (((IComparable)TargetElement).CompareTo(current.ElementT) < 0)
current = current.LeftNode;
else
current = current.RightNode;

}
}
if (!found)
throw new NullReferenceException("No se ha encontrado el elemento indicado en el árbol
binario");
}
return result;
}

/// <summary>
/// Reemplaza un nodo con una referencia a otro
/// </summary>
/// <param name="node">Nodo a reemplazar</param>
/// <returns>Devuelve una referencia a un nodo que se ha especificado en node</returns>
protected BinaryTreeNode<T> replacement(BinaryTreeNode<T> node)
{
BinaryTreeNode<T> result = null;
if ((node.LeftNode == null) && (node.RightNode == null))
result = null;
else if ((node.LeftNode != null) && (node.RightNode == null))
result = node.LeftNode;
else if ((node.LeftNode==null) && (node.RightNode !=null))
result = node.RightNode;
else
{
BinaryTreeNode<T> current = node.RightNode;
BinaryTreeNode<T> parent = node;
while (current.LeftNode != null)
{
parent = current;
current = current.LeftNode;
}
if (node.RightNode == current)
current.LeftNode = node.LeftNode;
else
{
parent.LeftNode = current.RightNode;
current.RightNode = node.RightNode;
current.LeftNode = node.LeftNode;
}
result = current;
}
return result;

#region Miembros de IBinarySearchTree<T>

/// <summary>
/// Elimina del árbol de búsqueda binaria los elementos que se
/// correspondan con el elemento objetivo especificado
/// </summary>

135
/// <param name="TargetElement"></param>
public void RemoveAllOcurrences(T TargetElement)
{
RemoveElement(TargetElement);
while (Contains((T)TargetElement))
RemoveElement(TargetElement);

public T RemoveMin()
{
throw new NotImplementedException();
}

public T RemoveMax()
{
throw new NotImplementedException();
}

public T FindMin()
{
throw new NotImplementedException();
}

public T FindMax()
{
throw new NotImplementedException();
}

#endregion
}

MULTIPROCESO

Un programa o proceso contiene al menos un subproceso y puede contener más. En


ese caso puede ejecutar varias tareas al mismo tiempo. Estos subprocesos podemos
crearlos, ejecutarlos, bloquearlos, suspenderlos, reanudarlos y terminarlos. Si
suspendemos un subproceso podremos reanudarlo. Tenemos dos tipos de
subprocesos: en primer plano (por defecto) o en segundo plano. Estos últimos
terminan automáticamente cuando acaban los procesos en primer plano.

Para esto, .NET incluye el espacio de nombres System.Threading donde se encuentra


la clase Thread que contiene lo necesario para poder crear y administrar nuestros
propios hilos de ejecución.

Si queremos crear un subproceso debemos usar el constructor proporcionado por la


clase Thread(ThreadStart comienzo). ThreadStart es un delegado de C# que contiene
el nombre del método que hay que ejecutar para comenzar el subproceso. El tipo
devuelto debe ser void y no debe tener ningún argumento. Una vez creado el
subproceso lo ejecutamos con el método Start y terminará cuando finalice el método
especificado en “comienzo”.

Con la propiedad IsAlive podemos saber si un proceso aún está en ejecución. Con el
método Join() de la clase Thread unimos los subprocesos para que esperen a su

136
finalización y así se finalice el hilo principal (nuestro programa). Veamos el siguiente
listado:

using System;
using System.Threading;

class Hilos
{
public int contador;
public Thread hilo;

public Hilos(string nombre)


{
contador = 0;
hilo = new Thread(new ThreadStart(this.ejecucion));
hilo.Name = nombre;
hilo.Start();
}
void ejecucion()
{
Console.WriteLine("Iniciando subproceso {0}", hilo.Name);
do
{
Thread.Sleep(300);
Console.WriteLine("Subproceso: {0}. Contador = {1}.",
hilo.Name, contador);
contador++;
} while (contador <= 3);
Console.WriteLine("Terminando subproceso {0}", hilo.Name);
}
}
class Multiproceso1
{
public static void Main()
{
Console.WriteLine("INICIANDO PROGRAMA PRINCIPAL");
Hilos h1 = new Hilos("Primero");
Hilos h2 = new Hilos("Segundo");
Hilos h3 = new Hilos("Tercero");
do
{
Console.WriteLine("*");
Thread.Sleep(1000);
} while ((h1.hilo.IsAlive) || (h2.hilo.IsAlive) ||
(h3.hilo.IsAlive));
Console.WriteLine("FINALIZANDO PROGRAMA PRINCIPAL");
Console.ReadLine();
}
}

La clase Hilos contiene un contador y un objeto Thread, además del constructor y el


método ejecución(). En el constructor inicializamos el contador que se va a usar para
finalizar los subprocesos al llegar a un valor y creamos un hilo usando para ello la
sintaxis antes explicada.

Como se puede observar, usamos el constructor del thread y el delegado con el nombre
del método a ejecutar por el subproceso, en este caso, el método ejecución.
Thread.Sleep() duerme la ejecución durante el tiempo expresado entre paréntesis. En

137
el programa principal, creamos los tres subprocesos y esperamos a que finalicen
mediante la comprobación realizada mediante IsAlive. De esta forma es sencillo
comprobar y sincronizar los subprocesos para esperar a que todos terminen. En este
caso usamos un simple OR cortocircuitado de forma que mientras haya alguno de los
subprocesos activos no finalice el programa.

Antes hemos comentado la posibilidad de usar Join(). Podemos eliminar el bucle en


nuestro programa e incluir estas sentencias:

h1.hilo.Join();
h2.hilo.Join();
h3.hilo.Join();

El método Join() puede aceptar como parámetro el tiempo máximo que debe esperar
hasta que termine el subproceso “unido”.

Otra propiedad importante es IsBackground. Si queremos especificar o convertir un


proceso a segundo plano, no tenemos más que asignar un valor true a la propiedad
IsBackground. El hecho de que un proceso esté en primer o segundo plano no tiene
por qué afectar a su prioridad salvo que se la asignemos nosotros desde el sistema
operativo. Sin embargo, disponemos de una propiedad para controlar las prioridades
de los subprocesos (priority). Si tenemos un proceso con una prioridad muy alta pero
que se detiene porque espera algún recurso que no está disponible (entrada de datos
desde teclado, impresora no preparada, etc…), éste recibirá poco tiempo de cpu. Por
eso es importante asignar correctamente las prioridades a los procesos sabiendo qué
van a necesitar y que disponibilidad tenemos de los recursos necesarios. Esta
propiedad se establece mediante la enumeración ThreadPriority, la cual puede tomar
como valores Highest, AboveNormal, Normal, BelowNormal, Lowest.

Cuando creamos un subproceso su prioridad asignada es Normal y la podemos


cambiar modificando el valor de la propiedad.

FINALIZAR UN SUBPROCESO

Para terminar un subproceso se debe utilizar el método Abort. Se puede llamar en la


instancia de subproceso por el bloque de código que inicialmente lo creó o bien por el
subproceso en sí.

Cuando se anula un subproceso se produce una excepción ThreadAbortException. A


esta excepción, a diferencia de otras, se le permite viajar por la cadena de excepciones
para que los subprocesos cancelados dejen de trabajar.

138
Se puede suprimir esta excepción directamente con el método Thread.ResetAbort()
como puede verse en el siguiente ejemplo:

using System.Threading;
namespace Thread1
{
class Program
{
static void Main(string[] args)
{
Thread.CurrentThread.Name = "PRINCIPAL";
ImprimirMensaje("Aplicación iniciada");
Thread worker = new Thread(new ThreadStart(DoWork));
worker.Name = "TRABAJADOR";
worker.Start();
Console.WriteLine("Presione Enter para anular el subproceso");
Console.ReadLine();
worker.Abort();
Console.WriteLine("Señal de anulación de subproceso enviada");
Console.ReadLine();

static void ImprimirMensaje(string msg)


{
Console.WriteLine("[{0}] {1}", Thread.CurrentThread.Name, msg);
}

static void DoWork()


{
try
{
while (true)
{
Console.Write("...");
Thread.Sleep(100); // tiempo de retraso para simular un trabajo de verdad
}

}
catch (Exception e)
{
ImprimirMensaje("Atrapada: " + e.ToString());
}
}
}
}

En cuanto el usuario pulse Intro, la señal de suspensión del subproceso se envía y el


subproceso de trabajo atrapa (y suprime) la excepción ThreadAbortException. Si el
método de trabajo no la eliminara, la excepción se “desbordaría” y al final haría que la
aplicación principal finalizara, casi siempre con un resultado no deseado.

SUSPENDER UN SUBPROCESO

Cuando se suspende un subproceso, se inudica al programador que éste ya no tiene


que cambiarse en el primer plano por la ejecución. Es decir, en cuanto el subproceso

139
termine de ejecutarse para dar vez a otro subproceso, éste no continuará hasta que el
primero se haya reanudado.

Un subproceso se suspende mediante el método Suspend, el cual no toma argumentos


y funciona de forma muy simple. Para reanudar el subproceso cuando se desee, sólo
hay que llamar al método Resume en ese mismo subproceso.

BLOQUEO EN ESPERA DE UN SUBPROCESO

El método Sleep hace que el subproceso repose. Con la indicación del intervalo en
milésimas de segundo, el subproceso dejará de ejecutar en esa línea durante la
duración especificada. También se puede pasar un valor 0 como el argumento que hará
que el subproceso se suspenda. Si se especifica System.Threading.Timeout.Infinite
como valor, el subproceso se bloqueará de forma indefinida.

BLOQUEAR UN SUBPROCESO

El método Join sirve como modo de bloquear el subproceso actual hasta que el
subproceso especificado se haya completado. Esto permite que el subproceso espere a
la finalización de otro método. De ahí proviene el término Join, cuando el subproceso
actual espera a otro para “alcanzarlo”. El listado siguiente ilustra el uso del método
Join:

using System.Threading;

namespace Thread1
{
class Program
{
static void Main(string[] args)
{
Thread worker = new Thread(new ThreadStart(DoWork));
worker.Start();
// realiza el método Join para bloquear
// este subproceso hasta que el trabajador se haya
// completado
worker.Join();
Console.WriteLine("Esta línea no se ejecutará hasta que 'worker' haya terminado");
Console.ReadLine();

static void DoWork()


{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(100);
Console.WriteLine(".");
}
}
}
}

140
El uso del método Join reemplazaría a la utilización de un bucle while que esperaba
hasta que la propiedad IsAlive del subproceso de ejecución se evaluaba como false.
Esta solución con join es más eficaz y segura.

ADO.NET

ASOCIACIÓN DE CAMPOS A CONTROLES (DATABINDING)

Cada formulario Windows.Form tiene al menos un objeto BindingContext. Para cada


datasource asociado con un formulario Windows (tal que una tabla o colección) hay
un objeto CurrencyManager asociado. El objeto BindinContext maneja el
CurrencyManager o los objetos del formulario. Ya que podemos asociar varios
datasources al formulario, podemos usar el BindingContext para obtener el
CurrencyManager asociado con cada datasource.

Para asociar a un control un datasource concreto, usamos DataBindings de la forma


siguiente:

TextBox1.DataBindings.Add("Text",datasource,"nombredelcampo");

En este caso, asociamos al cuadro de texto TextBox1 el contenido del campo dado por
datasource (puede ser entre otras cosas un datatable o un dataset). Text es la propiedad
a la cual asociamos el contenido del campo.

Para movernos por los registros, usamos la propiedad Position del objeto
BindingContext, y del elemento al que hagamos referencia (tabla o dataset). Por
ejemplo, si queremos ponernos en el primer campo de nuestra tabla:

this.BindingContext[tabla].Position=1;

Si usamos un dataset, el array sería así:

this.BindingContext[dataset,”tabla”].Position=1:

Si queremos obtener el número de registros:

this.BindingContext[tabla].Count;

Es importante siempre que usemos databinding en un formulario, llamar al método


EndEdit() del BindingSource justo antes de hacer el update sobre el dataadapter para
enviar las modificaciones al SGBD.

OBTENER PRIMARY KEYS DE UN DATABLE

La propiedad PrimaryKey dentro de un datatable devuelve o toma una matriz de


objetos DataColumn de forma que podamos definir claves de varias columnas para
especificar un registro único.

141
Si necesitáramos saber que clave o claves ejercen como primarias en una BD, sería tan
simple como:
DataColumn[] ClavesPrimarias;
ClavesPrimarias = MiDataset.Tables[0].PrimaryKey;
cadena=ClavesPrimarias[0].ToString();

Esto nos sirve para obtener el índice 0, pero podríamos obtener el que quisiéramos.

RELACIÓN DE FILAS ENTRE DATAVIEW Y DATATABLE

Supongamos que tenemos creada una vista sobre una tabla con unos criterios de
ordenación y filtrado. En un momento dado podemos desear conocer el número de
fila en la tabla original partiendo de una fila de nuestra vista.

El proceso para ello es simple: consiste en declarar una datarow que haga referencia a
una de las filas de la vista. Utilizando el método IndexOf de la colección de Rows del
datatable obtenemos el índice de dicha fila en la tabla original:

DataRow dr;
int IndiceTabla;
dr = dv[indicedelavista].Row;
IndiceTabla=dt.Rows.IndexOf(dr);

COMO AÑADIR UN CAMPO CALCULADO A UNA DATATABLE

Supongamos que en un momento dado necesitamos tener un campo calculado en base


a otros de una datatable con la que estamos trabajando. El proceso a seguir es muy
simple, tan solo hemos de crear una nueva DataColumn y asignarle a su propiedad
Expression la sintaxis del campo calculado:

DataColumn nuevaColumna = new DataColumn("NombredelCampo",


typeof(decimal));
nuevaColumna.Expression = "Campo1*Campo2";
Tabla.Columns.Add(nuevaColumna);

ESTILOS EN DATAGRIDVIEW

Gracias a la propiedad WrapMode del estilo, se consigue que el texto se divida en


varias líneas de forma que encaje adecuadamente en la celda. Esto lo podemos
comprobar aumentando manualmente la altura de algunas filas de la cuadrícula. Para
ello, se le asigna el valor de la enumeración DataGridViewTriState a
DataGridViewTriState.True.

Si asignamos estilos a las cabeceras de columnas mediante


ColumnHeadersDefaultCellStyle, no vamos a poder ver dichos estilos salvo que
cambiemos a false el valor de EnableHeadersVisualStyles.

142
Mediante la propiedad DataGridViewColumn.ValueType, podemos conocer el tipo de
datos que internamente manipula una columna determinada del control, lo cual nos
puede ser útilo si queremos utilizar un estilo distinto para las columnas del
DataGridView en función del tipo de datos con el que trabaja.

En el siguiente ejemplo, damos un color diferente al texto de las columnas que


contienen valores de tipo decimal:

foreach (DataGridViewColumn dc in midatagrid.Columns)


{
if (dc.ValueType == typeof(decimal))
{
dc.DefaultCellStyle.ForeColor = Color.FromArgb(83, 38, 134);
}
}

DATAGRIDVIEW: COMO CAMBIAR EL COLOR DE UNA FILA SEGÚN UNA


CONDICIÓN

Supongamos que se desea cambiar el color de fondo de una fila en función de un valor
tomado en cualquiera de los campos que muestra. Para conseguir este efecto
recurrimos al evento CellFormatting del grid, dentro de cuyo código comprobamos el
valor de la celda que necesitamos. Si se cumple esa condición, se recorre la colección
de celdas de la fila actual y se cambian sus propiedades de estilo correspondientes.
Veamos como sería:

private void miDataGrid_CellFormatting(object sender,


DataGridViewCellFormattingEventArgs e)
{
if
(miDataGrid.Rows[e.RowIndex].Cells["clmSituacion"].Value.ToString() == "4")
{
foreach (DataGridViewCell celda in
miDataGrid.Rows[e.RowIndex].Cells)
{
celda.Style.BackColor = Color.FromArgb(149, 180, 217);

}
}
}

Este código cambia el color de las filas donde se cumpla la condición de que el campo
situación (alojado en la columna clmSituacion) sea igual a 4.

COMBOBOX ENLAZADO A DATOS

Mientras un combo está dibujándose y tomando los primeros valores de una base de
datos, su SelectedValue es “System.Data.DataRowView”. Es importante tener esto en
cuenta al programar los eventos de cambio de índice o item, sobre todo si se está
teniendo en cuenta para una consulta.

143
CONCURRENCIA

Supongamos que se desea bloquear un registro en la base de datos para solucionar


problemas de concurrencia. Si lo queremos hacer en el servidor, la forma de
conseguirlo es iniciar una transacción con el suficiente nivel de aislamiento, y
modificar el registro. De esta forma, quedará bloqueado hasta que se termine la
transacción. Para asignar a la transacción el nivel de aislamiento requerido desde el
código cliente, se usa el argumento opcional que tiene para este fin el método
BeginTransaction del objeto SqlConnection en el caso de SQL Server.

using (SqlConnection conexion = new SqlConnection(CadenaConexion))


{
conexion.Open();
SqlTransaction tran = conexion.BeginTransaction(IsolationLevel.RepeatableRead);
string sentencia = "UPDATE TABLA1 SET CAMPO2=CAMPO2+1 WHERE CAMPO1=1";
SqlCommand cmd = new SqlCommand(sentencia, conexion);
cmd.Transaction = tran;
cmd.ExecuteNonQuery();
// Aquí se trabaja con el registro bloqueado
tran.Rollback(); // o bien .Commit()
}

El nivel mínimo que necesitamos para que se respete el bloqueo es


IsolationLevel.ReadCommited que resulta ser precisamente el valor por defecto.

Sin embargo, bloquear un registro de esta forma y esperar una interacción del usuario
antes de bloquearlo tiene el inconveniente de que destroza la capacidad de
concurrencia en el servidor de base de datos. Si otro proceso tratase de acceder al
registro bloqueado, se detendría por un tiempo indefinido esperando el desbloqueo
del registro. Se puede poner un límite de tiempo de espera, pero con ésto sólo se logra
ocasionar un error en el programa que espera que se libere el registro.

Es más, SQL Server realiza, cuando lo considera oportuno, una operación llamada
“escalado de bloqueos” (lock scalation). Se produce cuando hay muchos registros
bloqueados en una tabla extendiéndose a una página de datos completa e incluso a
toda la tabla. Si esto sucede, no solo se bloquearán los procesos que traten de acceder
a nuestro registro, sino los que accedan a otros de la misma tabla aún cuando no
estuvieran en uso.

Para evitar estos problemas, se recurre a realizar el control de acceso al registro desde
la parte cliente. Existen dos enfoques alternativos: concurrencia optimista y
concurrencia pesimista.

Con la optimista, primero se leen los datos, se trabaja con ellos y al ir a grabarlos se
comprueba primero si lo que hay en la base de datos coincide con lo leido inicialmente.
De no ser así, se muestra un error al usuario o se corrige el valor de forma automática
si es posible.

144
Con la pesimista, se usa algún mecanismo para bloquear el acceso a los registros de
forma que nadie más pueda acceder a un registro ocupado por un usuario. Por
ejemplo, se añade al registro un campo booleano y se pone a true cuando se lee para
trabajar con él. Si mientras tanto alguien quiere modificarlo, se le muestra un mensaje
diciendo que no se puede. El mecanismo optimista tiene soporte directo en las
distintas tecnologías mediante DataSet y DataAdapter, LinQ to SQL o Entity
Framework. Todas ellas permiten traer datos a memoria, trabajar con ellos y lanzar
una excepción al grabar si los datos se han modificado mientras tanto en el servidor.
En cuanto al pesimista, volviendo a la idea del campo BIT, cuando leamos dicho
registro, consultamos su valor. Si es true, asumimos que el registro está en uso e
informamos debidamente al usuario. En caso contrario lo ponemos a true y
presentamos el registro en pantalla. Estas dos operaciones las haremos dentro de una
transacción para garantizar (mediante el correspondiente bloqueo en el servidor) que
dos procesos no leen y cambian simultáneamente el valor, cosa que frustraría el
objetivo de nuestro mecanismo. Lógicamente, al terminar de trabajar con dicho
registro, devolveremos a false el citado campo.

private void btnEditarRegistro_Click(object sender, EventArgs e) {


using (SqlConnection conexion = new SqlConnection(CadenaConexion)) {
conexion.Open();
SqlTransaction tran = conexion.BeginTransaction();
string sentencia = "SELECT ENUSO FROM TABLA1 WHERE IDPRODUCTO=2";
SqlCommand cmd = new SqlCommand(sentencia, conexion);
cmd.Transaction = tran;
bool enUso = (bool)cmd.ExecuteScalar();
if (enUso)
{
MessageBox.Show("El registro está en uso");
tran.Rollback();
return;
}
sentencia = "UPDATE TABLA1 SET ENUSO=1";
cmd.CommandText = sentencia;
cmd.ExecuteNonQuery();
tran.Commit();
// Aquí leeríamos todos los datos del registro y los mostraríamos por pantalla
}
}
private void btnGrabarRegistro_Click(object sender, EventArgs e)
{
// Aquí grabaríamos en la BD los nuevos datos introducidos por el usuario
quitarBloqueo();
}
private void btnCancelarEdicion_Click(object sender, EventArgs e)
{
quitarBloqueo();
}
private void quitarBloqueo(){
using (SqlConnection conexion = new SqlConnection(CadenaConexion)) {
conexion.Open();
string sentencia = "UPDATE TABLA1 SET ENUSO=0";
SqlCommand cmd = new SqlCommand(sentencia, conexion);
cmd.ExecuteNonQuery();
}
}

145
Sin embargo, este mecanismo tiene un inconveniente: si un usuario abandona el
trabajo de forma anómala sin haber liberado el registro, éste permanecerá bloqueado
y nadie podrá acceder a él. Para aliviar este problema, podemos tomar alguna de las
medidas a continuación.

En primer lugar, sustituir el campo BIT por un DATETIME. Cuando contiene NULL
significa que el registro no está bloqueado. Para bloquear el registro, se introduce en
ese campo la fecha y hora actual. Cuando alguien quiere acceder al registro y el
programa lo ve bloqueado, se compara la fecha y hora actual con la del bloqueo. Si la
diferencia es mayor que un umbral preestablecido (por ejemplo, 15 minutos) se
presupone que el usuario que tenía bloqueado el registro lo ha abandonado y se libera
automáticamente.

Otra opción es sustituir el campo BIT por el usuario que realizó el bloqueo. Cuando
un segundo usuario intenta acceder al registro, se le informa que está siendo editado
por … y se le da la opción de desbloquear el registro si el usuario al que se muestra
tiene los permisos para ello. Es útil en caso que el programa lo maneje un número
reducido de usuarios que pueden comunicarse fácilmente unos con otros.

Por último, si no se quiere llenar el registro original con campos adicionales, se pueden
ubicar éstos en una tabla auxiliar enlazada con la principal. En este caso, al liberar el
bloqueo basta con borrar el registro completo de la tabla de bloqueos en lugar de poner
NULL o false en campos auxiliares.

EXPRESIONES DE CONSULTA (LINQ)

LINQ hace posible escribir una consulta sobre cualquier colección que implemente la
interfaz IEnumerable e IQueryable. Esto es, en definitiva casi cualquier colección
de .NET incluyendo string[], int[] y colecciones genéricas (List<T>).

Veamos un ejemplo básico de esto:

using System;
using System.Linq;
public class Consulta1
{
public static void Main()
{
int[] numeros = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var pNums = from n in numeros
where n < 5
select n;
Console.WriteLine("Numeros < 5:");
foreach (var x in pNums)
{
Console.WriteLine(x);
}
Console.ReadLine();
}
}

146
En este ejemplo, usamos las expresiones de consulta sobre un array de enteros para
seleccionar los que son menores que 5. Lo más interesante es que con esta sintaxis
básica podemos realizar consultas sobre objetos de naturaleza bien distinta, como
pueden ser archivos XML o bases de datos.

En este caso es donde vamos a agradecer fundamentalmente tener objetos anónimos y


variables implícitas de forma que vamos a poder escribir algo como esto:

var q = from c in clientes


where c.Ciudad == "Madrid"
select new { c.Nombre, c.Telefono };

y así terminar con una variable q que contiene un nombre y un teléfono sin tener que
diseñar una clase específica.

Las expresiones de LINQ se pueden usar para realizar consultas sobre bases de datos
relacionales sin tener que dejar la sintaxis de C#. Esta facilidad, llamada Dlinq nos
permite realizar consultas sobre una base de datos con la sintaxis genérica vista
anteriormente, con todas las ventajas que esto conlleva, de tipado fuerte o poder usar
las verificaciones que realiza el compilador. Esto se consigue usando tipos que
representan a la base de datos y que transforman las consultas de C# a SQL. En el
siguiente listado vemos un ejemplo de esto. En este caso, el tipo DataContext se
encarga de traducir las consultas dentro de C# a consultas SQL sobre la base de datos:
using System;
using System.Linq;
using System.Data.Linq.Mapping;
using System.Data.Linq;
// Esta clase almacena una tabla SQL
[Table(Name="SQL_CLIENTES")]
public class Cliente
{
[Column(DbType = "smallint not null", IsPrimaryKey = true)]
public short COD_CLIENTE;
[Column(DbType = "varchar(100) not null")]
public string NOMBRE_CLIENTE;
[Column(DbType="char(9) not null")]
public string CIF;
}
public class DlinqEjemplo
{
public static void Main()
{
DataContext db = new DataContext(CadenaConexion");
Table<Cliente> Clientes = db.GetTable<Cliente>();
var consulta =
from c in Clientes
where c.CIF == "XXXXXXXXX"
select c;
foreach (var c in consulta)
Console.WriteLine("Nombre = {0}, CIF={1}",
c.NOMBRE_CLIENTE, c.CIF);
Console.ReadLine();
}
}

147
Supongamos que tenemos un archivo XML llamado agenda.xml con el siguiente
contenido:

<?xml version="1.0" encoding="utf-8" ?>


<agenda>
<contacto categoria="amigo">
<nombre>Pepito</nombre>
</contacto>
<contacto categoria="trabajo">
<nombre>Adolfo</nombre>
</contacto>
</agenda>

La extensión que nos permite hacer consultas con XML es XLinq e introduce varios
tipos para representar archivos XML. En nuestro ejemplo, probaremos con
XDocument y haremos una consulta sobre este archivo para buscar los contactos cuya
categoría sea igual a “trabajo”:

using System.Xml.Linq;
XDocument doc = new XDocument("Agenda.xml");
var contactos = from c in doc.Descendants("contacto")
where c.Attribute("categoria").ToString() ==
"trabajo"
select c;

Las consultas, al igual que el bucle foreach se pueden realizar sobre cualquier objeto
que implemente IEnumerable. Por ejemplo, podríamos importar un ensamblado y
hacer una consulta sobre sus métodos para seleccionar aquellos que devuelvan un
entero.

A continuación, podemos ver los miembros LinQ para usar en colecciones:

Tipo de operador Nombre del método


Agregar Agregate, Average, Count, LongCount, Max, Min, Sum
on Cast, OfType, ToArray, ToDictionary, ToList, ToLookup
Elemento DefaultEmpty, ElementAt, ElementAtOrDefault, First,
FirstOrDefault, Last, LastOrDefault, Single,
SingleOrDefault
Comparar Equals
Generar Empty, Range, Repeat
Agrupar GroupBy
Cruzar GroupJoin, Join
información
Ordenar OrderBy, ThenBy, OrderByDescending,
ThenByDescending, Reverse
Restringir Skip, SkipWhile, Take, TakeWhite, Where
Cuantificar All, Any, Contains
Selección Select, SelectMany
Configurar Concat, Distinct, Except, Intersect, Union

148
SELECCIONAR Y CRUZAR INFORMACIÓN CON DATOS (LINQ)

Imaginemos dos colecciones anónimas:

var clientes = new[] {

new{IDCliente=1,nombre="pepe",apellido="gonzalez",Pais="España",edad=
21},

new{IDCliente=2,nombre="juan",apellido="rodriguez",Pais="España",edad
=33},

new{IDCliente=3,nombre="andrea",apellido="piera",Pais="Uruguay",edad=
35}
};
var ordenes = new[] {
new{IDCliente=1,ordenNumero=23,monto=400},
new{IDCliente=2,ordenNumero=24,monto=1},
new{IDCliente=2,ordenNumero=25,monto=132},
new{IDCliente=3,ordenNumero=80,monto=850},
new{IDCliente=1,ordenNumero=81,monto=14}
};

Obtener los clientes con sus respectivas órdenes se haría así:

var resultado = from c in clientes


join o in ordenes on c.IDCliente
equals o.IDCliente
select new { c.IDCliente, o.monto };

La expresión es similar a la que usaríamos con SQL pero utilizando la palabra new
para indicar más de un campo de retorno. Si deseamos ahora manipular los elementos
de la colección bastará con iterar sobre el contenido con un foreach:

foreach (var fila in resultado)


{
Console.WriteLine(fila.IDCliente + " " + fila.nombre + " "
+ fila.monto);
}

En este ejemplo vamos a calcular el total de dinero a pagar por cliente y mostrar la
última orden emitida. Dos cosas a destacar:

1. La palabra clave “into” guarda el resultado del cruce en una variable, a la que
es posible hacer referencia más adelante
2. La palabra clave “let” realiza algo parecido pero con las expresiones que están
después del signo igual

var resultado = from c in clientes


join o in ordenes on c.IDCliente equals o.IDCliente
into clientesordenes
let total=clientesordenes.Sum(o => o.monto)
let ultimaOrden=clientesordenes.Max( o => o.ordenNumero)
select new { c.IDCliente, c.nombre, total,ultimaOrden };

149
AGRUPAR ELEMENTOS CON LINQ

Esto se lleva a cabo medinte la cláusula Group:

var consulta = from c in clientes


group c by c.Pais into paisesClientes
select new { pais = paisesClientes.Key, totalEdad =
paisesClientes.Average(c => c.edad) };

Primero definimos el nombre de la matriz y su alias y el campo por el cual realizaremos


la agrupación (País en este caso). A continuación se guarda el resultado en
“paisesClientes” para luego hacer referencia a la clave de la cláusula mediante la
propiedad Key. Esta retorna el camo principal empleado en la agrupación (en este caso
Pais). Finalmente indicamos los elementos de la selección usando un método de suma
y su consiguiente expresión Lambda con el valor edad. El resultado mostrará la media
de las edades agrupadas por pais.

La misma consulta puede realizarse por medio de los métodos de extensión. En este
caso la sintaxis es un poco más compleja debido a las expresiones Lambda.

var consulta1 = clientes.GroupBy(cli => cli.Pais)


.Select(paisesClientes => new
{
pais = paisesClientes.Key,
mediaEdad = paisesClientes.Average(c => c.edad)
});

EXTENDER EL CRITERIO DE ORDEN EN LINQ

Un escenario común es cuando tenemos que ordenar un conjunto de elementos por su


campo de fecha. Esto funciona correctamente si el tipo de datos es DateTime, pero no
cuando éste se representa como texto. Nuestro objetivo ahora es convertir
internamente el texto a tipo fecha para después especificar el orden:

var resultado = ClientesString.OrderBy(c => c.fechaNacimiento, new


comparaFechas());

El método comparaFechas recoge el parámetro fechadeNacimiento. El segundo paso


consiste en escribir una clases que extienda la interfaz IComparer, la cual básicamente
compara dos objetos sin importar su tipo:

class comparaFechas : IComparer<string>


{
public int Compare(string fecha1, string fecha2)
{
DateTime dFecha1 = DateTime.Parse(fecha1);
DateTime dFecha2 = DateTime.Parse(fecha2);
return (Comparer<DateTime>.Default.Compare(dFecha1, dFecha2));
}
}

150
Cuando se quieren emplear varios campos en un criterio de ordenación se debe usar
OrderBy y ThenBy:

var resultadoUsandoThenBy = resultado.OrderBy(x => x.nombre).ThenBy(x =>


x.apellido);

MAPEO ENTRE OBJETOS Y UNA BASE DE DATOS RELACIONAL EN LINQ TO


SQL

En este apartado veremos como LINQ to SQL captura las relaciones “uno a uno” y
“uno a muchos” cuando la fuente de datos son las tablas de una BD SQL.

La figura representa el conjunto de una base de datos de ejemplo para representar


información de cine:

FILMACTOR
IDACTOR ACTOR
IDFILM ID
OSCARWINNER NAME
SEX

FILM DIRECTOR
ID ID

TITLE NAME

IDGENRE
OSCAR
IDDIRECTOR
YEAR GENRE
ID
RATING
NAME

Cuando se añade un elemento LINQ to SQL Classes a un proyecto VS2008 se crea un


conjunto de datos asociado a una conexión SQL Server; al arrastrar las tablas al área
de diseño, éste genera y añade el código fuente necesario para mapear en notación de
objetos dichas tablas.

151
Vamos a mostrar cómo las relaciones entre tablas quedan expresadas como relaciones
entre objetos, lo que se denomina clases de entidad.

CineLinQDataContext cineDB = new CineLinQDataContext();

var filmsDeSpielberg1 =
from f in cineDB.FILMs
where f.DIRECTOR.NAME == "Steven Spielberg"
select new { f.TITLE, Genero = f.GENRE.NAME };

var filmsDeSpielberg2 =
from d in cineDB.DIRECTORs
from f in d.FILMs
where d.NAME == "Steven Spielberg"
select new { f.TITLE, f.GENRE.NAME };

Gracias a este mapeo podemos construir consultas declarativas como las del listado
anterior en el siguiente:

var filmsdeSpielberg =
from f in cineDB.FILMs
where f.DIRECTOR.NAME == "Steven Spielberg"
select new { f.TITLE, Genre = f.GENRE.NAME };
Console.WriteLine("Titulo y genero de los films de Spielberg\n");
foreach (var f in filmsdeSpielberg)
{
Console.WriteLine("{0} {1}", f.TITLE, f.Genre);
}

Observar que en la tabla original FILM no hay una columna de nombre Director que
tenga como valor un objeto de tipo Director, ni una columna Genre que tenga como
valor un objeto del tipo Genre. Sin embargo, en el listado hemos podido escribir
f.Director.Name y f.Genre.Name; esto es posible gracias a las relaciones de tipo
EntityRef que se usan en el código de mapeo. Si echamos un vistazo al código del
diseñador del dbml, veremos algo similar a esto:

private EntitySet<FILMACTOR> _FILMACTORs;

private EntityRef<DIRECTOR> _DIRECTOR;

private EntityRef<GENRE> _GENRE;

La clase Film mapea la tabla con el mismo nombre; nótese que las propiedades Director
y Genre usan respectivamente las variables _Director y _Genre. Al aplicarle la
propiedad Entity a un valor de estos tipos EntityRef se obtienen objetos de tipo
Director y Genre.

Como estas propiedades no corresponden directamente con columnas de la BD, es


LINQ to SQL el que se encarga de transformar estas consultas a sentencias SQL que
pasa al motor de BD. La simple notación f.Director.Name y f.Genre se traduce, como
era de esperar, en operaciones de join entre tablas.

152
Un aspecto relevante a destacar es que EntityRef se encarga de mantener la identidad
de los objetos. A pesar de que en el listado siguiente se hacen dos consultas diferentes,
el objeto Director obtenido en ambos casos será el mismo (su comparación devolverá
true). El mecanismo de EntityRef garantiza que ambas consultas producen el mismo
objeto de tipo Director que corresponde a una misma fila en la tabla correspondiente.
Al cambiar a mayúscula el nombre del director y hacer un SubmitChanges() se está
cambiando la fila de la tabla Director correspondiente a Steven Spielberg:

CineLinQDataContext cineDB = new CineLinQDataContext();

var spielberg =
(from d in cineDB.DIRECTORs
where d.NAME == "Steven Spielberg"
select d).First();

var jurassicPark =
(from f in cineDB.FILMs
where f.TITLE == "Jurassic Park"
select f).Single();

Console.WriteLine("Spielberg y el director de JurassicPark son iguales:


{0}", spielberg == jurassicPark.DIRECTOR);

jurassicPark.DIRECTOR.NAME = jurassicPark.DIRECTOR.NAME.ToUpper();
cineDB.SubmitChanges();

ENTITYSET Y RELACIONES UNO A MUCHOS

El código de mapeo establece también relaciones uno a muchos entre Director y Film
y entre Genre y Film mediante el tipo EntitySet:

[Table(Name="dbo.DIRECTOR")]
public partial class DIRECTOR : INotifyPropertyChanging,
INotifyPropertyChanged
{
private static PropertyChangingEventArgs emptyChangingEventArgs = new
PropertyChangingEventArgs(String.Empty);

private int _ID;

private string _NAME;

private EntitySet<FILM> _FILMs;

[Table(Name="dbo.GENRE")]
public partial class GENRE : INotifyPropertyChanging,
INotifyPropertyChanged
{

private static PropertyChangingEventArgs emptyChangingEventArgs = new


PropertyChangingEventArgs(String.Empty);

private int _ID;

private string _NAME;

private EntitySet<FILM> _FILMs;

153
Como puede comprobarse, las variables _FILMS de los tipos Director y Genre son de
tipo EntitySet<FILM>. Esto permite que se pueda hacer una consulta como la
siguiente:

CineLinQDataContext cineDB = new CineLinQDataContext();


var mafia = from d in cineDB.DIRECTORs
from f in d.FILMs
where f.GENRE.NAME == "Mafia"
select new { Director = d.NAME, f.TITLE };
Console.WriteLine("Films de Mafia y sus directores\n");
foreach (var f in mafia)
Console.WriteLine("{0,-30} {1}", f.TITLE, f.Director);
Console.ReadLine();

Usando la propiedad Films de Director se averigua qué directores han dirigido algún
film del género “Mafia”

LINQ to SQL se encarga de que se preserve en la BD la consistencia de estas relaciones


si se hacen operaciones con los objetos en memoria. El siguiente código usa la
propiedad Films del objeto coppola que representa a la fila en la tabla Director
correspondiente a Francis Ford Coppola. Al añadirle a esta propiedad el objeto
godfellas de tipo Film, se provoca a su vez que actualice la propiedad Director de tipo
EntityRef<Director> del objeto godfellas.

CineLinQDataContext cineDB = new CineLinQDataContext();


// Pone godfellas como film de Coppola y actualiza la BD
DIRECTOR coppola = (from d in cineDB.DIRECTORs
where d.NAME == "Francis Ford Coppola"
select d).Single();
FILM godfellas = (from f in cineDB.FILMs
where f.TITLE == "Godfellas"
select f).Single();
coppola.FILMs.Add(godfellas);
cineDB.SubmitChanges();

// Accede a los films de Coppola


var coppolaFilms = from f in cineDB.FILMs
where f.DIRECTOR.NAME == "Francis Ford Coppola"
select f;
Console.WriteLine("Coppola y sus films\n");
foreach (var f in coppolaFilms)
Console.WriteLine("{0,-30} {1}", f.TITLE, f.DIRECTOR.NAME);

Si se accede a la BD para listar los films dirigidos por Coppola se podrá observar que
Godfellas está entre ellos.

154
RELACIONES MUCHOS A MUCHOS

En este ejemplo, dicha relación se expresa con la tabla FilmActors. LINQ expresa este
mapeo incluyendo una propiedad FilmActors en Films y otra en Actors. A través de
ellas, a partir de un actor podemos obtener todos los films en los cuales éste trabaja y
dado un film, aquellos actores que trabajan en dicho film. El siguiente listado nos da
los films dirigidos por Clint Eastwood y sus actores, y los films en los que actúa Dustin
Hoffman:

CineLinQDataContext cineDB = new CineLinQDataContext();


var clintFilms = from f in cineDB.FILMs
where f.DIRECTOR.NAME == "Clint Eastwood"
select new { f.TITLE, f.FILMACTORs };

Console.WriteLine("Films de Clint Eastwood y sus actores");


foreach (var f in clintFilms)
{
Console.WriteLine("\n {0}", f.TITLE);
foreach (var fa in f.FILMACTORs)
Console.WriteLine(" {0}", fa.ACTOR.NAME);
}

var dustinFilms = (from a in cineDB.ACTORs


where a.NAME == "Dustin Hoffman"
select new { a.FILMACTORs }).Single();

Console.WriteLine("\nFilms en que trabaja Dustin Hoffman");


foreach (var fa in dustinFilms.FILMACTORs)
Console.WriteLine(" {0}", fa.FILM.TITLE);

Hubiera sido deseable que incluyese una relación muchos a muchos de forma directa
entre film y actor, es decir, tener en el tipo film una propiedad Actors de tipo
EntitySet<ACTOR> y a su vez en el tipo Actor una de tipo EntitySet<FILM>.

155
En el código a continuación podemos ver como ampliar dichas definiciones para este
propósito:
public partial class FILM : INotifyPropertyChanging, INotifyPropertyChanged
{
private EntitySet<ACTOR> _Actors = null;
private void onAdd(ACTOR a)
{
foreach (FILMACTOR fa in _FILMACTORs)
if (fa.IDACTOR == a.ID && fa.IDFILM == _ID) return;
// Añadimos una nueva relación FilmActor al EntitySet
FILMACTOR fActor = new FILMACTOR();
fActor.IDFILM = ID;
fActor.IDACTOR = a.ID;
fActor.ACTOR = a;
fActor.FILM = this;
_FILMACTORs.Add(fActor);
return;
}

private void onRemove(ACTOR a)


{
foreach (FILMACTOR fa in _FILMACTORs)
{
// Quitar la entrada correspondiente en la relación
filmactors
if (fa.IDACTOR == a.ID && fa.IDFILM == _ID)
{
_FILMACTORs.Remove(a);
return;
}
}
return;
}
// Nueva propiedad personalizada Actors
public EntitySet<ACTOR> Actors
{
get
{
if (_Actors == null)
{
_Actors = new EntitySet<ACTOR>(new
Action<ACTOR>(onAdd),
new Action<ACTOR>(onRemove));
foreach (FILMACTOR fa in this._FILMACTORs)
this._Actors.Add(fa.ACTOR);
}
return _Actors;
}
set
{this._Actors.Assign(value);}
}
}
// Similar implementación a la anterior para la clase Actor
public partial class ACTOR : INotifyPropertyChanging,
INotifyPropertyChanged
{
//...
}

156
De esta forma, podríamos expresar la consulta anterior de esta forma:

CineLinQDataContext cineDB = new CineLinQDataContext();


var clintFilms = from f in cineDB.FILMs
where f.DIRECTOR.NAME == "Clint Eastwood"
select new { f.TITLE, f.FILMACTORs };

Console.WriteLine("Films de Clint Eastwood y sus actores");


foreach (var f in clintFilms)
{
Console.WriteLine("\n {0}", f.TITLE);
foreach (var a in f.ACTORS)
Console.WriteLine(" {0}", a.NAME);
}

var dustinFilms = (from a in cineDB.ACTORs


where a.NAME == "Dustin Hoffman"
select new { a.FILMs }).Single();

Console.WriteLine("\nFilms en que trabaja Dustin Hoffman");


foreach (var f in dustinFilms.FILMs)
Console.WriteLine(" {0}", f.TITLE);

Se ha implementado esto para que al construir inicialmente un EntitySet se indiquen


los métodos onAdd y onRemove de forma que éstos se ejecuten cuando se apliquen
las operaciones Add y Remove al EntitySet. Con estos métodos logramos mantener la
consistencia con el tipo FilmActor que es el que realmente hace el mapeo con la tabla
correspondiente con la BD. De esta forma, al hacer un SubmitChanges, los cambios se
reflejarán en la BD.

El código siguiente daría excepción en ejecución ya que LinQ no está preparado para
transformar esta consulta en sentencias SQL. El problema es que la nueva propiedad
f.Actors se está usando dentro de la cláusula from:

var authorFilms = from f in cineDB.FILMs


from a in f.Actors
where a.NAME == f.DIRECTOR.NAME
select new { f.TITLE, f.DIRECTOR };

Console.WriteLine("Films en los que actúa el director\n");


foreach (var f in authorFilms)
Console.WriteLine(" {0} actuado y dirigido por {1}", f.TITLE,
f.DIRECTOR.NAME);

Podríamos resolverlo con el siguiente código:


Console.WriteLine("Films en los que actúa el director\n");
foreach (var f in cineDB.FILMs)
{
foreach (var a in f.Actors)
if (a.NAME == f.DIRECTOR.NAME)
{
Console.WriteLine(" {0} actuado y dirigido por {1}", f.TITLE,
a.NAME);
break;
}
}

157
PRIORIZAR RENDIMIENTO, MANTENIBILIDAD Y PRODUCTIVIDAD
CUANDO ELEGIMOS PASAR DATOS ENTRE CAPAS

Debemos tener en cuenta varios factores cuando elegimos pasar datos entre capas:

- Mantenibilidad: Consideramos la dificultad de construir y mantener los


cambios.
- Programabilidad: Consideramos la dificultad del código.
- Rendimiento: Consideramos la eficiencia de colecciones, mostrar y serializar.

Veamos como se comportan las diferentes clases de .NET:


• DataSets: El DataSet genérico ofrece gran flexibilidad debido a sus
funcionalidades extensivas. Incluye serialización, soporte XML, capacidad de
manejar relaciones complejas, soporte para concurrencia optimista, y otras. Sin
embargo, los DataSet son caros de crear debido a sus jerarquía interna de
objetos y los clientes tienen que acceder a ellos mediante colecciones. El dataset
contiene colecciones de muchos subobjetos como son el DataTable, DataRow,
DataColumn, DataRelation y Constraint. La mayoría de esos objetos son
pasados con el dataset entre capas. Hay, por tanto, un montón de objetos y de
datos pasados entre las capas. También lleva tiempo llenar un dataset, ya que
hay muchos objetos que necesitan ser instanciados y publicados. Todo esto
afecta al rendimiento. Generalmente, el dataset es más útil para cachear cuando
queremos crear una representación en memoria de nuestra base de datos,
cuando queremos trabajar con relaciones entre tablas y cuando queremos
ejecutar operaciones de ordenado y filtrado.
• DataSet Tipados: En rendimiento en cuanto a instanciación y organización de
los datasets tipados es aproximadamente equivalente a los datasets. La
principal ventaja respecto al rendimiento es que los clientes pueden acceder a
métodos y propiedades directamente, sin tener que usar colecciones.
• DataReaders: Éstos ofrecen el rendimiento óptimo cuando queremos presentar
datos lo más rápidamente posible. Se deben cerrar los objetos DataReader tan
pronto como sea posible y asegurar que las aplicaciones cliente no pueden
afectar la cantidad de tiempo que el datareader mantiene la conexión abierta.
Es muy rápido comparado con un dataset pero se debe evitar pasar objetos
DataReader entre capas, ya que requieren una conexión abierta.
• XML: Se aprovecha en este caso que nativamente se soporta serialización y
colecciones de datos en este caso. Por ejemplo, un documento XML puede
contener datos de múltiples entidades de negocio. También soporta un rango
amplio de tipos de clientes. A nivel de rendimiento, hay que considerar el hecho
de que las cadenas XML requieren un esfuerzo sustancial de análisis sintáctico,
y largas y prolijas cadenas que consumen grandes cantidades de memoria.
• Clases a medida: Aquí podemos usar miembros de datos privados para
mantener el estado del objeto y proporcionar métodos de acceso público. Para
tipos simples, podemos usar estructuras en vez de clases, lo cual implica que
evitaremos tener que implementar nuestra propia serialización. El principal
beneficio es que nos permiten crear nuestra propia serialización optimizada.

158
Deberíamos evitar jerarquías complejas de objetos y optimizar nuestro diseño
de clases con el fin de minimizar el consumo de memoria y reducir la cantidad
de datos que necesitamos que sean serializados cuando el objeto se pasa entre
clases.

IMPLEMENTANDO UNA CAPA DE ACCESO A DATOS (DATA ACCESS


LAYER, DAL) GENÉRICA EN ADO.NET

Primeramente, necesitamos abrir una conexión usando un proveedor de base de datos.


También necesitaremos un objeto command que pueda ejecutarse contra cualquier
SGBD. Seguidamente deberemos usar un DataReader, DataSet o DataTable para
recoger datos.

Teniendo en cuenta esto, vamos a diseñar un proveedor DAL independiente. Las


clases principales que constituyen la biblioteca ADO.NET son:

• Connection
• Command
• Data Reader
• Data Adapter

Y los interfaces que implementan estas clases son:

• IDBConnection
• IDataReader
• IDBCommand
• IDBDataAdapter

Los proveedores de datos que contiene dicha biblioteca son específicos de una base de
datos particular a la que deberían poder conectarse. Los proveedores de datos
disponibles en ADO.NET son:

• SQL Server Data Provider


• Oracle Data Provider
• ODBC Data Provider
• OleDB Data Provider

Vamos entonces a implementar nuestro DAL. Los componentes principales que


constituyen nuestro bloque DAL son:

• ProviderType (Enum)
• DatabaseConnectionState (Enum)
• StoredProcedureParameterDirection (Enum)
• DBManager (Class)
• DBHelper (Class)

159
Vamos a comenzar con el tipo de datos enum que contendrá los tipos de proveedor de
datos:

public enum ProviderType


{
SqlServer, OleDb, Oracle, ODBC, ConfigDefined

Ahora tendremos varias situaciones donde necesitaremos mantener el estado de la


conexión abierto o cerrado después de que una operación sobre la base de datos ha
terminado. Por ejemplo, después de leer datos con un objeto DataReader,
necesitaremos mantener abierta la conexión para subsiguientes operaciones. Teniendo
en cuenta esto, utilizaremos un tipo de datos enum que aloje dos valores que
correspondan a los estados de la conexión con la base de datos.

public enum DatabaseConnectionState


{
KeepOpen, CloseOnExit
}

Cuando ejecutamos procedimientos almacenados, vamos a querer mandar datos al


SGBD o recogerlos. De esta forma, crearemos otra enumeración llamada
StoredProcedureParameterDirection que contenga valores que correspondan a las
direcciones de los parámetros para los procedimientos almacenados que se ejecutarían
con ayuda de nuestro DAL:

public enum StoredProcedureParameterDirection


{
Input, InputOutput, Output, ReturnValue
}

Ahora necesitamos una clase fábrica (que implementa el patrón de diseño factory) que
devuelva un objeto de tipo DbProviderFactory o DbDataAdapter dependiendo del
proveedor de datos que estemos usando. Esta clase contiene métodos factory que son
típicamente estáticos. Estos métodos en la clase DBFactory aceptan una referencia a la
enumeración ProviderType que denota que se está usando ese tipo de proveedor de
datos.

160
El código siguiente es el de nuestra clase DBFactory. Contiene dos métodos estáticos
llamados GetProvider y GetAdapter los cuales aceptan un tipo de proveedor de base
de datos dado por la enum ProviderType:

using System.Data.Common;
using System.Data.SqlClient;
using System.Data.OleDb;
using System.Data.Odbc;
using System.Data.OracleClient;
using System.Collections.Generic;
using System.Text;

internal class DBFactory


{
private static DbProviderFactory objFactory = null;
public static DbProviderFactory GetProvider(ProviderType provider)
{
switch (provider)
{
case ProviderType.SqlServer:
objFactory = SqlClientFactory.Instance;
break;
case ProviderType.OleDb:
objFactory = OleDbFactory.Instance;
break;
case ProviderType.Oracle:
objFactory = OracleClientFactory.Instance;
break;
case ProviderType.ODBC:
objFactory = OdbcFactory.Instance;
break;
}
return objFactory;
}
public static DbDataAdapter GetDataAdapter(ProviderType
providerType)
{
switch (providerType)
{
case ProviderType.SqlServer:
return new SqlDataAdapter();
case ProviderType.OleDb:
return new OleDbDataAdapter();
case ProviderType.ODBC:
return new OdbcDataAdapter();
case ProviderType.Oracle:
return new OracleDataAdapter();
default:
return null;
}
}

Este código chequea el tipo de proveedor proporcionado y devuelve un


DbDataProvider o DbProviderFactory en función cual se haya elegido.

161
Ahora veremos como implementar una clase llamada DatabaseHelper, la cual será
responsable de ejecutar las operaciones contra la base de datos. La clase DataHelper
encapsula las diferentes llamadas a la base de datos para ejecutas las operaciones
CRUD (Create, update, read, delete). La clase DBManager que veremos después actúa
como envoltorio de esta clase. Tenemos varios métodos en la clase DataHelper para
añadir parámetros, ejecutar consultas, procedimientos almacenados, etc. Este código
ilustra como se establece la conexión a la base de datos en función del proveedor
elegido y del objeto comando creado:

public DatabaseHelper(string connectionstring, ProviderType provider)


{
this.strConnectionString = connectionstring;
objFactory = DBFactory.GetProvider(provider);
objConnection = objFactory.CreateConnection();
objCommand = objFactory.CreateCommand();
objConnection.ConnectionString = this.strConnectionString;
objCommand.Connection = objConnection;
}

Dependiendo del proveedor de datos utilizado, necesitamos usar el objeto command


específico de dicho proveedor. El datareader también deberá ser específico del
proveedor utilizado. Diponemos de versiones sobrecargadas del método
AddParameter para añadir parámetros a los objetos command de forma que podemos
pasar parámetros a los procedimientos almacenados o a nuestras consultas SQL.
Veamos una versión muy sencilla de este método:
internal int AddParameter(string name, object value)
{
DbParameter dbParameter = objFactory.CreateParameter();
dbParameter.ParameterName = name;
dbParameter.Value = value;
return objCommand.Parameters.Add(dbParameter);
}

Mientras el ParameterName identifica el nombre único del parámetro a ser pasado, el


Value se refiere al valor del parámetro pasado. Por ejemplo, si el ParameterName es
@EmpName, su valor podría ser “Jordi Caneira”. Para asegurar que nuestra capa de
acceso a datos soporta transacciones, tenemos tres métodos que nos permiten dicho
soporte:
internal void BeginTransaction(){
if (objConnection.State == System.Data.ConnectionState.Closed)
{
objConnection.Open();
}
objCommand.Transaction = objConnection.BeginTransaction();
}
internal void CommitTransaction(){
objCommand.Transaction.Commit();
objConnection.Close();
}
internal void RollbackTransaction()
{
objCommand.Transaction.Rollback();
objConnection.Close();
}

162
También dispondremos de cuatro métodos para ejecutar operaciones CRUD. Estos
métodos son:

ExecuteScalar()
ExecuteReader()
ExecuteNonQuery()
ExecuteDataSet()

El código completo sera:

public class DatabaseHelper : IDisposable


{
private string strConnectionString;
private DbConnection objConnection;
private DbCommand objCommand;
private DbProviderFactory objFactory = null;

public DatabaseHelper(string connectionstring, ProviderType


provider)
{
this.strConnectionString = connectionstring;
objFactory = DBFactory.GetProvider(provider);
objConnection = objFactory.CreateConnection();
objCommand = objFactory.CreateCommand();
objConnection.ConnectionString = this.strConnectionString;
objCommand.Connection = objConnection;
}

internal int AddParameter(string name, object value)


{
DbParameter dbParameter = objFactory.CreateParameter();
dbParameter.ParameterName = name;
dbParameter.Value = value;
return objCommand.Parameters.Add(dbParameter);
}

internal int AddParameter(DbParameter parameter)


{
return objCommand.Parameters.Add(parameter);
}

internal int AddParameter(string name,


StoredProcedureParameterDirection parameterDirection)
{
DbParameter parameter = objFactory.CreateParameter();

parameter.ParameterName = name;
parameter.Value = String.Empty;
parameter.DbType = DbType.String;
parameter.Size = 50;

switch (parameterDirection)
{
case StoredProcedureParameterDirection.Input:
parameter.Direction =
System.Data.ParameterDirection.Input;
break;
case StoredProcedureParameterDirection.Output:

163
parameter.Direction =
System.Data.ParameterDirection.Output;
break;
case StoredProcedureParameterDirection.InputOutput:
parameter.Direction =
System.Data.ParameterDirection.InputOutput;
break;
case StoredProcedureParameterDirection.ReturnValue:
parameter.Direction =
System.Data.ParameterDirection.ReturnValue;
break;
}

return objCommand.Parameters.Add(parameter);
}

internal int AddParameter(string name, object value,


StoredProcedureParameterDirection parameterDirection)
{
DbParameter parameter = objFactory.CreateParameter();

parameter.ParameterName = name;
parameter.Value = value;
parameter.DbType = DbType.String;
parameter.Size = 50;

switch (parameterDirection)
{
case StoredProcedureParameterDirection.Input:
parameter.Direction =
System.Data.ParameterDirection.Input;
break;
case StoredProcedureParameterDirection.Output:
parameter.Direction =
System.Data.ParameterDirection.Output;
break;
case StoredProcedureParameterDirection.InputOutput:
parameter.Direction =
System.Data.ParameterDirection.InputOutput;
break;
case StoredProcedureParameterDirection.ReturnValue:
parameter.Direction =
System.Data.ParameterDirection.ReturnValue;
break;
}

return objCommand.Parameters.Add(parameter);
}

internal int AddParameter(string name,


StoredProcedureParameterDirection parameterDirection, int size, DbType
dbType)
{
DbParameter parameter = objFactory.CreateParameter();

parameter.ParameterName = name;
parameter.DbType = dbType;
parameter.Size = size;

switch (parameterDirection)
{

164
case StoredProcedureParameterDirection.Input:
parameter.Direction =
System.Data.ParameterDirection.Input;
break;
case StoredProcedureParameterDirection.Output:
parameter.Direction =
System.Data.ParameterDirection.Output;
break;
case StoredProcedureParameterDirection.InputOutput:
parameter.Direction =
System.Data.ParameterDirection.InputOutput;
break;
case StoredProcedureParameterDirection.ReturnValue:
parameter.Direction =
System.Data.ParameterDirection.ReturnValue;
break;
}

return objCommand.Parameters.Add(parameter);
}

internal int AddParameter(string name, object value,


StoredProcedureParameterDirection parameterDirection, int size, DbType
dbType)
{
DbParameter parameter = objFactory.CreateParameter();

parameter.ParameterName = name;
parameter.Value = value;
parameter.DbType = dbType;
parameter.Size = size;

switch (parameterDirection)
{
case StoredProcedureParameterDirection.Input:
parameter.Direction =
System.Data.ParameterDirection.Input;
break;
case StoredProcedureParameterDirection.Output:
parameter.Direction =
System.Data.ParameterDirection.Output;
break;
case StoredProcedureParameterDirection.InputOutput:
parameter.Direction =
System.Data.ParameterDirection.InputOutput;
break;
case StoredProcedureParameterDirection.ReturnValue:
parameter.Direction =
System.Data.ParameterDirection.ReturnValue;
break;
}

return objCommand.Parameters.Add(parameter);
}

internal DbCommand Command


{
get
{
return objCommand;
}

165
}

internal DbConnection Connection


{
get
{
return objConnection;
}
}

internal void BeginTransaction()


{
if (objConnection.State ==
System.Data.ConnectionState.Closed)
{
objConnection.Open();
}

objCommand.Transaction = objConnection.BeginTransaction();
}

internal void CommitTransaction()


{
objCommand.Transaction.Commit();
objConnection.Close();
}

internal void RollbackTransaction()


{
objCommand.Transaction.Rollback();
objConnection.Close();
}

internal int ExecuteNonQuery(string query)


{
return ExecuteNonQuery(query, CommandType.Text,
DatabaseConnectionState.CloseOnExit);
}

internal int ExecuteNonQuery(string query, CommandType


commandtype)
{
return ExecuteNonQuery(query, commandtype,
DatabaseConnectionState.CloseOnExit);
}

internal int ExecuteNonQuery(string query,


DatabaseConnectionState connectionstate)
{
return ExecuteNonQuery(query, CommandType.Text,
connectionstate);
}

internal int ExecuteNonQuery(string query, CommandType


commandtype, DatabaseConnectionState connectionstate)
{
objCommand.CommandText = query;
objCommand.CommandType = commandtype;

int i = -1;

166
try
{
if (objConnection.State ==
System.Data.ConnectionState.Closed)
{
objConnection.Open();
}

i = objCommand.ExecuteNonQuery();
}
catch
{
throw;
}
finally
{
if (connectionstate ==
DatabaseConnectionState.CloseOnExit)
{
objConnection.Close();
}
}

return i;
}

internal object ExecuteScalar(string query)


{
return ExecuteScalar(query, CommandType.Text,
DatabaseConnectionState.CloseOnExit);
}

internal object ExecuteScalar(string query, CommandType


commandtype)
{
return ExecuteScalar(query, commandtype,
DatabaseConnectionState.CloseOnExit);
}

internal object ExecuteScalar(string query,


DatabaseConnectionState connectionstate)
{
return ExecuteScalar(query, CommandType.Text,
connectionstate);
}

internal object ExecuteScalar(string query, CommandType


commandtype, DatabaseConnectionState connectionstate)
{
objCommand.CommandText = query;
objCommand.CommandType = commandtype;
object o = null;
try
{
if (objConnection.State ==
System.Data.ConnectionState.Closed)
{
objConnection.Open();
}

o = objCommand.ExecuteScalar();

167
}
catch
{
throw;
}
finally
{
objCommand.Parameters.Clear();
if (connectionstate ==
DatabaseConnectionState.CloseOnExit)
{
objConnection.Close();
}
}

return o;
}

internal DbDataReader ExecuteReader(string query)


{
return ExecuteReader(query, CommandType.Text,
DatabaseConnectionState.CloseOnExit);
}

internal DbDataReader ExecuteReader(string query, CommandType


commandtype)
{
return ExecuteReader(query, commandtype,
DatabaseConnectionState.CloseOnExit);
}

internal DbDataReader ExecuteReader(string query,


DatabaseConnectionState connectionstate)
{
return ExecuteReader(query, CommandType.Text,
connectionstate);
}

internal DbDataReader ExecuteReader(string query, CommandType


commandtype, DatabaseConnectionState connectionstate)
{
objCommand.CommandText = query;
objCommand.CommandType = commandtype;
DbDataReader reader = null;
try
{
if (objConnection.State ==
System.Data.ConnectionState.Closed)
{
objConnection.Open();
}
if (connectionstate ==
DatabaseConnectionState.CloseOnExit)
{
reader =
objCommand.ExecuteReader(CommandBehavior.CloseConnection);
}
else
{
reader = objCommand.ExecuteReader();
}

168
}
catch
{

}
finally
{
objCommand.Parameters.Clear();
}

return reader;
}

internal DataSet ExecuteDataSet(string query)


{
return ExecuteDataSet(query, CommandType.Text,
DatabaseConnectionState.CloseOnExit);
}

internal DataSet ExecuteDataSet(string query, CommandType


commandtype)
{
return ExecuteDataSet(query, commandtype,
DatabaseConnectionState.CloseOnExit);
}

internal DataSet ExecuteDataSet(string query,


DatabaseConnectionState connectionstate)
{
return ExecuteDataSet(query, CommandType.Text,
connectionstate);
}

internal DataSet ExecuteDataSet(string query, CommandType


commandtype, DatabaseConnectionState connectionstate)
{
DbDataAdapter adapter = objFactory.CreateDataAdapter();
objCommand.CommandText = query;
objCommand.CommandType = commandtype;
adapter.SelectCommand = objCommand;
DataSet ds = new DataSet();
try
{
adapter.Fill(ds);
}
catch
{
throw;
}
finally
{
objCommand.Parameters.Clear();
if (connectionstate ==
DatabaseConnectionState.CloseOnExit)
{
if (objConnection.State ==
System.Data.ConnectionState.Open)
{
objConnection.Close();
}

169
}
}
return ds;
}

public void Dispose()


{
if (objConnection.State == ConnectionState.Open)
{
objConnection.Close();
objConnection.Dispose();
}

objCommand.Dispose();
}

internal IDataReader ExecuteReader(string storedProcedureName,


params object[] parameters)
{
objCommand.CommandText = storedProcedureName;
objCommand.CommandType = CommandType.StoredProcedure;
DbDataReader reader = null;
try
{
RetrieveParameters(objCommand);
SetParameterValues(objCommand, parameters);

if (objConnection.State ==
System.Data.ConnectionState.Closed)
{
objConnection.Open();
}

reader = objCommand.ExecuteReader();
}
catch
{
throw;
}
finally
{
objCommand.Parameters.Clear();
}
return reader;
}

internal void SetParameterValues(DbCommand objCommand, object[]


parameters)
{
int index = 0;
for (int i = 0; i < parameters.Length; i++)
{
DbParameter parameter = objCommand.Parameters[i +
index];
SetParameterValue(objCommand, parameter.ParameterName,
parameters[i]);
}
}

internal virtual void SetParameterValue(DbCommand dbCommand,


string parameterName, object value)

170
{
dbCommand.Parameters[parameterName].Value = (value ==
null) ? DBNull.Value : value;
}

internal void RetrieveParameters(DbCommand dbCommand)


{
string connectionString = Connection.ConnectionString;
dbCommand.Connection = Connection;
Connection.Open();
SqlCommandBuilder.DeriveParameters(dbCommand as
SqlCommand);
}

internal object GetParameter(string name)


{
return objCommand.Parameters[name].Value;
}
}

Nótese que la mayor parte de los métodos de la clase DatabaseHelper han sido
marcados como “internal” para prevenir que sean llamados fuera de su espacio de
nombres.

Bien, ahora vamos a crear la clase envoltorio DBManager, la cual encapsula las
llamadas a la clase DBHelper. La clase DBManager extiende la clase abstracta
DBManagerBase, la cual contiene la definición de los métodos Open() y Close() y otras
propiedades públicas que son genéricas y pueden ser usadas por cualquier clase que
actúe como envoltorio. Veamos la clase DBManagerBase:
public abstract class DBManagerBase
{
protected DatabaseHelper databaseHelper = null;
protected DbDataReader dbDataReader = null;
protected DataSet dataSet = null;
protected ProviderType providerType;
protected String connectionString = String.Empty;
protected bool isOpen = false;
public bool IsOpen
{
get
{
return isOpen;
}
set
{
isOpen = value;
}
}
public string ConnectionString
{
get
{
return connectionString;
}
set
{
connectionString = value;
}

171
}
public DbConnection Connection
{
get
{
return databaseHelper.Connection;
}
}
public DbCommand Command
{
get
{
return databaseHelper.Command;
}
}
public ProviderType DBProvider
{
set
{
providerType = value;
}
get
{
return providerType;
}
}
public DataSet DBSet
{
get
{
return dataSet;
}
}
public DbDataReader DBReader
{
get
{
return dbDataReader;
}
}
protected void Open(string connectionString)
{
databaseHelper = new DatabaseHelper(connectionString,
DBProvider);
}
protected void Close(){
if (dbDataReader != null)
if (!dbDataReader.IsClosed)
dbDataReader.Close();
databaseHelper.Dispose();
}
public void BeginTransaction(){
databaseHelper.BeginTransaction();
}
public void CommitTransaction(){
databaseHelper.CommitTransaction();
}
public void RollbackTransaction(){
databaseHelper.RollbackTransaction();
}
}

172
La clase DBManagerBase contiene los métodos más comunes requeridos. Podemos
abrir o cerrar una conexión, comenzar, validar o echar atrás transacciones, etc.

La clase DBManager que extiende la clase abstracta DBManagerBas contiene una lista
de métodos que pueden ser usados para ejecutar procedimientos, consultas y devolver
datasets o datareaders. Podemos optar por mantener nuestra conexión abierta después
de que se ha llamado al método ExecuteReader de forma que podemos usar la
conexión viva en las subsiguientes operaciones que necesitemos ejecutar en nuestra
base de datos. Los nombres de métodos en la clase DBManager están relacionados con
las operaciones que deben realizar.

Disponemos del método AddParameter que puede ser usado para añadir parámetros
a nuestro procedimiento almacenado, tal que cuando invoquemos al procedimiento le
podamos pasar los parámetros. La cadena de conexión que necesitaremos usar para
conectar a nuestra base de datos puede ser ajustada usando la propiedad
ConnectionString.

El tipo de proveedor puede ser ajustado en la enumeración ProviderType. A


continuación, podemos ver el código de la clase DBManager:

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Configuration;
using System.Data.Common;
using System.Data.SqlClient;
using System.Data.OleDb;
using System.Data.Odbc;
using System.Data.OracleClient;

using System.IO;

namespace ApplicationFramework.DataAccessLayer
{
public sealed class DBManager : DBManagerBase
{
public void OpenConnection()
{

connectionString =
ConfigurationManager.AppSettings["ConnectionString"].ToString();
base.Open(connectionString);
}
public void OpenConnection(String connectionString)
{
base.Open(connectionString);
base.IsOpen = true;
}
public void CloseConnection()
{
if (base.isOpen)
base.Close();

173
base.IsOpen = false;
}
public int AddParameter(string name, object value)
{
return databaseHelper.AddParameter(name, value);
}
public int AddParameter(string name,
StoredProcedureParameterDirection parameterDirection)
{
return databaseHelper.AddParameter(name, parameterDirection);
}
public int AddParameter(string name, object value,
StoredProcedureParameterDirection parameterDirection)
{
return databaseHelper.AddParameter(name, value,
parameterDirection);
}
public int AddParameter(string name,
StoredProcedureParameterDirection parameterDirection, int size, DbType
dbType)
{
return databaseHelper.AddParameter(name, parameterDirection,
size, dbType);
}
public int AddParameter(string name, object value,
StoredProcedureParameterDirection parameterDirection, int size, DbType
dbType)
{
return databaseHelper.AddParameter(name, value,
parameterDirection, size, dbType);
}
public object GetParameter(string name)
{
return databaseHelper.GetParameter(name);
}
public DbDataReader ExecuteReader(string query)
{
this.dbDataReader = databaseHelper.ExecuteReader(query);
return this.dbDataReader;
}
public DbDataReader ExecuteReader(string query, CommandType
commandtype)
{
this.dbDataReader = databaseHelper.ExecuteReader(query,
commandtype, DatabaseConnectionState.CloseOnExit);
return this.dbDataReader;
}
public IDataReader ExecuteReader(string storedProcedureName, params
object[] parameters)
{
this.dbDataReader =
(DbDataReader)databaseHelper.ExecuteReader(storedProcedureName,
parameters);
return this.dbDataReader;
}
public DbDataReader ExecuteReader(string query, CommandType
commandtype, DatabaseConnectionState connectionstate)
{
this.dbDataReader = databaseHelper.ExecuteReader(query,
commandtype, connectionstate);
return this.dbDataReader;

174
}
public DbDataReader ExecuteReader(string query,
DatabaseConnectionState connectionstate)
{
this.dbDataReader = databaseHelper.ExecuteReader(query,
connectionstate);
return this.dbDataReader;
}
public object ExecuteScalar(string query)
{
return databaseHelper.ExecuteScalar(query);
}
public object ExecuteScalar(string query, CommandType commandtype)
{
return databaseHelper.ExecuteScalar(query, commandtype);
}
public object ExecuteScalar(string query, DatabaseConnectionState
connectionstate)
{
return databaseHelper.ExecuteScalar(query, connectionstate);
}
public object ExecuteScalar(string query, CommandType commandtype,
DatabaseConnectionState connectionstate)
{
return databaseHelper.ExecuteScalar(query, commandtype,
connectionstate);
}
public DataSet ExecuteDataSet(string query)
{
this.dataSet = databaseHelper.ExecuteDataSet(query);
return this.dataSet;
}
public DataSet ExecuteDataSet(string query, CommandType
commandtype)
{
this.dataSet = databaseHelper.ExecuteDataSet(query,
commandtype);
return this.dataSet;
}
public int ExecuteNonQuery(string query, CommandType commandtype)
{
return databaseHelper.ExecuteNonQuery(query, commandtype);
}
public int ExecuteNonQuery(string query, CommandType commandtype,
DatabaseConnectionState databaseConnectionState)
{
return databaseHelper.ExecuteNonQuery(query, commandtype,
databaseConnectionState);
}

175
Podemos usar la clase DBManager como se muestra a continuación:

DBManager dbManager = new DBManager();


dbManager.OpenConnection();
dbManager.ExecuteReader("Select * from employee");
while (dbManager.DBReader.Read())
Response.Write(dbManager.DBReader[“EmpName”].ToString());
dbManager.CloseConnection();

DBManager dbManager = new DBManager();


String sql = "insert into employee (EmpCode, EmpName) values
('E001''Joydip')";
try
{
dbManager.OpenConnection();
dbManager.ExecuteNonQuery(sql,CommandType.Text);
}
catch(Exception e)
{
HttpContext.Current.Response.Write(e);
}
finally
{
dbManager.CloseConnection();
HttpContext.Current.Response.Write("<BR>"+"1 record added...");
}

BUSCAR EN DOCUMENTOS CON MICROSOFT INDEX SERVER

Si deseamos realizar una búsqueda de texto en documentos, .NET nos permite


aprovechar la potencia del Servicio de Microsoft Index Server para búsqueda e
indexación de documentos. Si además necesitamos filtrar por un tipo de documento
concreto lo podemos usar en combinación con Ifilter.

Primero hemos de asegurarnos de que el Servicio de Microsoft Index Server está


iniciado. De no ser así, accederemos al listado de servicios de nuestra máquina y lo
pondremos en marcha. Si deseamos que permanezca más allá de una sesión,
habilitaremos el inicio automático del servicio.

Microsoft .NET permite el acceso a Index Server mediante OleDb, de forma que
podemos crear consultas SQL sobre los índices de los documentos que nos permitan
crear datasets sobre esos resultados como cualquier otro origen de datos. En el IDE,
desde el explorador de servidores se puede crear una conexión con dicho servicio.

176
Un ejemplo de consulta SQL sobre Index Server podría ser el siguiente:

SELECT Rank, VPath, DocTitle, Filename, Characterization, Write


FROM SCOPE()
WHERE FREETEXT('diseño')
ORDER BY RANK, SIZE DESC

Rank es el campo que establece el rango de coincidencia del patrón dado por
FREETEXT(). Si añadimos más palabras a esta función, la consulta nos devolverá
aquellos documentos que tengan al menos una de las palabras definidas en el patrón.

Otra función de búsqueda interesante es CONTAINS

SELECT Rank, VPath, DocTitle, Filename, Characterization, Write


FROM SCOPE()
WHERE NOT CONTAINS(VPath, '"_vti_" OR ".config"')
AND CONTAINS(Contents, '"keyword1" AND "keyword2"')
AND CONTAINS(DocTitle, '"keyword1" AND "keyword2"')

La función SCOPE nos permite limitar nuestra consulta a uno varios directorios
particulares, y definir si queremos o no que incluya subdirectorios.

ACCESO A FICHEROS EN C#. LA CLASE SYSTEM.IO.STREAM

Para ver como se accede a ficheros, en este caso de texto, desde C# vamos a verlo con
un ejemplo. Primero vamos a crear un archivo con el nombre “abc.txt” y dentro de él
vamos a escribir la cadena “Hola fichero”:

using System.IO; // IO en el namespace System se encarga de E/S de ficheros


System.IO.StreamWriter sw = new StreamWriter("abc.txt"); //Nueva instancia de
la clase StreamWriter
sw.Write("Hola fichero.”);
sw.Close();

En el caso de que quisiéramos añadir una línea a dicho fichero sin borrar lo anterior,
haríamos lo siguiente:

StreamWriter sw = new StreamWriter("abc.txt", true);


sw.Write("Estoy aquí");
sw.Close();

177
Ahora queremos leer el contenido del fichero y ponerlo en una cadena (en el fichero
anterior, pondríamos “Hola fichero. Estoy aquí”):

StreamReader sr = new StreamReader("abc.txt");


string str = sr.ReadLine();
sr.Close();

Para borrar el archivo:

System.IO.File.Delete("abc.txt");

La clase System.IO.Stream es la secuencia principal de .NET donde se definen las


operaciones estándar. Sus métodos principales son:

- Close(): No devuelve valor, cierra la secuencia


- ReadByte(): Devuelve un entero del byte de entrada actualmente en el stream.
Si se ha llegado al final de los datos devuelve -1
- WriteByte(byte b): Escribe un byte en un stream de escritura. No devuelve valor
- Seek (long desplazamiento, SeekOrigin origen): Se posiciona en un stream
origen según el desplazamiento. Devuelve un valor long.

Propiedades:

- Length: Longitud de la secuencia en long. Sólo lectura


- Position: Posición actual del stream (long). Lectura/escritura
- CanRead: De sólo lectura booleano. True si se puede leer el stream
- CanWrite: Sólo lectura booleano. True si se puede escribir
- CanSeek: Sólo lectura booleano. True si podemos posicionarnos en diferentes
desplazamientos del stream.

Si se produce un error de entrada/salida, se lanza una IOException. Si el error es por


una operación no permitida (como escribir en un stream de lectura), se lanza una
NotSupportedException. Veamos un ejemplo en el que volvemos a escribir sobre un
fichero y también sobre la pantalla usando el método Write para ilustrar que podemos
usarlo para diferente salida. Además de System.IO usamos System.Text el cual
contiene la clase UTF8Encoding para convertir caracteres en bytes. En System.Console
tenemos tres propiedades (In, Out y Error) que exponen tres streams predefinidos, los
cuales hacen referencia a la entrada y salida estándar (salida normal y de errores).

Mediante el método OpenStandardOutput creamos el stream pantalla donde


escribiremos directamente. Después creamos un stream de la clase FileStream para
escribir en un fichero, el cual especificamos mediante el método Create de la clase File
de System.IO. Como lo que pasamos es una cadena, debemos convertirla a bytes que
es lo que entiende el stream:

byte[] linea = new UTF8Encoding(true).GetBytes(cadena);

178
Usando la clase UTF8Encoding y su método GetBytes obtenemos los bytes a escribir
en los streams correspondientes, tanto de pantalla como de fichero. Finalmente
escribimos en los streams mediante el método Write de los mismos (public override
void Write(byte [] src, int src_offset, int count)). El primer parámetro es,
evidentemente, la cadena en bytes, el segundo es el desplazamiento (cero en nuestro
caso) y el tercero es la longitud de los bytes a escribir (linea.Length).

La clase FileStream realiza operaciones con bytes directamente a archivos utilizadndo


objetos TextReader y TextWriter. Esta clase deriva de Stream y tiene varios
constructores, pero el más usado posiblemente sea:

FileStream (string archivo, FileMode modo, FileAccess acceso);

FileMode es una enumeración que puede tener uno de los siguientes valores: Open
especifica la apertura de un fichero. Create crea un archivo nuevo y si ya existía lo
sobrescribe. OpenOrCreate abre el archivo si existe o lo crea si no existe. CreateNew
especifica que se crea un archivo nuevo. Append abre el archivo si existe y añade la
salida al final de él. Sólo se puede usar con FileAccess.Write. Si se intenta leer se lanza
la excepción ArgumentException. Finalmente, Truncate abre un archivo que existe y
lo trunca a 0 bytes. El parámetro acceso de la enumeración FileAccess especifica si se
va a abrir el archivo en modo lectura, escritura, o ambos (Read, Write, ReadWrite).

De los muchos constructores de los que dispone la clase FileStream hay tres que poseen
un parámetro de tipo enumerado FileShare. Este parámetro se debe indicar si
queremos compartir el archivo. Puede ser None (ningún otro proceso podrá abrir el
archivo mientras ya esté abierto), Read, Write y ReadWrite (con los que permitiremos
a otros procesos o al propio programa que lo tiene abierto, leer y/o escribir
repetidamente sobre el archivo en cuestión).

Para leer con FileStream tenemos ReadByte() y Read(). El primero lee únicamente un
byte y el segundo un número de bytes de un origen o bufer y con un desplazamiento
(en su caso). Con ReadByte() obtenemos un único byte como valor entero. Si estamos
al final del archivo, devolverá un -1. Con Read() leemos un bloque de bytes de la forma
comentada:

int Read(byte [] buffer, int desplazamiento, int numero_de_bytes)

179
De esta forma leemos la cantidad de número_de_bytes de buffer a partir de
buff[desplazamiento]. Nos devolverá un entero que significa el número de bytes que
se han leído correctamente.
using System;
using System.IO;
class leebytes {
public static void Main() {
int leido;
FileStream fichero;
try {
fichero = new FileStream("texto.txt", FileMode.Open);
} catch (FileNotFoundException e){
Console.WriteLine("Error: Fichero no encontrado." + e.Message);
return;
}
do{
try{
leido = fichero.ReadByte();
}
catch (Exception e)
{
Console.WriteLine("Error: " + e.Message);
return;
}
if (leido != -1)
Console.Write((char)leido);
} while (leido != -1);
fichero.Close();
Console.ReadLine();
}

Las clases TextReader, TextWriter son implementadas por otras clases de secuencias
basadas en caracteres (que usamos en el primer ejemplo de este apartado):
- StreamReader: Lee caracteres de un stream de bytes
- StreamWriter: Escribe caracteres en un stream de bytes
- StringReader: Lee caracteres de un stream
- StringWriter: Escribe caracteres en un stream

StreamWriter deriva de TextWriter y StreamReader de TextReader. La ventaja de estas


secuencias o streams de caracteres es que operan directamente con caracteres Unicote,
así que es la opción adecuada si deseamos eso. En el constructor de estas clases se
puede especificar también el tipo de codificación de caracteres el cual no tiene por qué
ser la misma en reader que en writer. Si no se especifica, se usa UTF-8. Finalmente,
comentar que la clase StreamReader tiene el método ReadToEnd() para leer el
contenido de un archivo a un string con una sola instrucción. Por último, nombrar dos
clases definidas como stream o secuencias binarias: BinaryReader y BinaryWriter las
cuales son usadas para leer/escribir cualquier tipo de dato representado en su formato
binario interno. BinaryReader empaqueta una secuencia de bytes controlando la
lectura de datos binarios. Normalmente se usa el constructor BinaryReader(Stream
flujodedatos) y ese stream suele ser un objeto creado por FileStream. Con
BinaryReader disponemos de métodos para leer cualquier tipo de datos simple de C#,
como puede ser ReadBoolean(), ReadDouble(), etc…

180
BinaryWriter empaqueta los datos y controla su escritura. Su constructor principal es
BinaryWriter(Stream flujodedatos). Veamos un ejemplo de esto:
using System;
using System.IO;
class binario
{
public static void Main()
{
BinaryReader brStream;
BinaryWriter bwStream;
bool logico = true;
int x = 10;
double euro = 166.386;
Console.WriteLine();
Console.WriteLine("Datos a escribir en el fichero:");
Console.WriteLine("logico = {0}, x = {1}, euro ={2}", logico, x,
euro);
try
{
bwStream = new BinaryWriter(new
FileStream("datos_binarios.txt", FileMode.Create));
}
catch (IOException e){
Console.WriteLine("Error: No se puede crear el fichero: " +
e.Message);
return;
}
try{
Console.WriteLine("Escribiendo en el fichero...");
bwStream.Write(logico);
bwStream.Write(x);
bwStream.Write(euro);
}
catch (IOException e){
Console.WriteLine("Error de escritura: " + e.Message);
}
bwStream.Close();
try{
brStream = new BinaryReader(new
FileStream("datos_binarios.txt", FileMode.Open));
}
catch(FileNotFoundException e){
Console.WriteLine("Error: Fichero no encontrado. " +
e.Message);
return;
}
try{
logico = brStream.ReadBoolean();
x = brStream.ReadInt32();
euro = brStream.ReadDouble();
}
catch (IOException e){
Console.WriteLine("Error de lectura: " + e.Message);

}
brStream.Close();
Console.WriteLine();
Console.WriteLine("Datos leídos del fichero:");
Console.WriteLine("logico ={0}, x={1}, euro={2}", logico, x, euro);
}
}

181
Para redireccionar la salida de nuestro programa usaremos los métodos SetIn(),
SetOut() y SetErr() de Console. El primero admite como parámetros un StreamReader
y los otros dos un StreamWriter. Si queremos redireccionar la salida, es tan fácil como
crear un StreamWriter y luego llamar a SetOut():

Fichero_log= new StreamWriter(“log.txt);


Console.SetOut(fichero_log);

A partir de entonces, todos los mensajes Console.Write y Console.WriteLine (las


salidas del programa) irán a parar al fichero log.txt y no aparecerán por pantalla que
es la salida estándar.

COMO SABER CUANTO TIEMPO HA PASADO DESDE UNA FECHA


DETERMINADA

Supongamos que queremos saber cuantos segundos han pasado desde el 31 de


Diciembre de 1969 hasta ahora. Crearíamos la siguiente función:

public double CTiempo(DateTime fecha) {


return (fecha - new DateTime(1969,12,31)).TotalSeconds;
}

Restar estas dos fechas produce una instancia de TimeSpan, la cual tiene una
propiedad TotalSeconds que nos da los segundos (ver TimeSpan en la ayuda). Es
evidente que tenemos propiedades como TotalMinutes, TotalHours, etc... para calcular
diferentes fracciones de tiempo.

CREACIÓN Y LECTURA DE XML

En este apartado vamos a crear un archivo XML desde C#. Primeramente vamos a
obtener un ejemplo de fichero XML desde MySQL. Dentro de la BD Test, vamos a crear
una tabla llamada prueba con dos campos: Código (int) y Descripción (varchar(50)).
En ella insertaremos cuatro registros con este formato: Código: x, Descripción: Código
x (en letra).

Para exportar estos datos a XML tan sólo hemos de teclear lo siguiente desde una
consola:

mysqldump –-xml test

lo que nos mostrará un archivo XML con datos de la BD test. Si redireccionamos esta
salida a un fichero, ya tendremos un archivo para poder trabajar con él.

182
Nos quedaría algo similar a esto:

<?xml version="1.0"?>
<mysqldump xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<database name="test">
<table_structure name="prueba">
<field Field="Codigo" Type="int(11)" Null="YES" Key="" Extra="" />
<field Field="Descripcion" Type="varchar(50)" Null="YES" Key="" Extra="" />
<options Name="prueba" Engine="InnoDB" Version="10" Row_format="Compact"
Rows="4" Avg_row_length="4096" Data_length="16384" Max_data_length="0" Index_length="0"
Data_free="0" Create_time="2007-04-22 08:56:46" Collation="latin1_swedish_ci" Create_options=""
Comment="InnoDB free: 4096 kB" />
</table_structure>
<table_data name="prueba">
<row>
<field name="Codigo">1</field>
<field name="Descripcion">CÓDIGO UNO</field>
</row>
<row>
<field name="Codigo">2</field>
<field name="Descripcion">CÓDIGO DOS</field>
</row>
<row>
<field name="Codigo">3</field>
<field name="Descripcion">CÓDIGO TRES</field>
</row>
<row>
<field name="Codigo">4</field>
<field name="Descripcion">CÓDIGO CUATRO</field>
</row>
</table_data>
</database>
</mysqldump>

183
Veamos ahora el siguiente listado:

using System;
using System.Xml;

namespace XML
{
class crearXML
{
XmlDocument docxml;
XmlNode nodoxml;
XmlElement elemxml;
XmlElement elemxml2;
XmlText textoxml;

public static void Main(string[] args)


{
crearXML app = new crearXML();
}
public crearXML()
{
docxml=new XmlDocument();

nodoxml=
docxml.CreateNode(XmlNodeType.XmlDeclaration,"","");
docxml.AppendChild(nodoxml);

elemxml=docxml.CreateElement("","RAIZ","");
textoxml=docxml.CreateTextNode("Texto del elemento
raíz");
elemxml.AppendChild(textoxml);
docxml.AppendChild(elemxml);

elemxml2=docxml.CreateElement("","Elemento1","");
textoxml=docxml.CreateTextNode("Texto del elemento 1");
elemxml2.AppendChild(textoxml);
docxml.ChildNodes.Item(1).AppendChild(elemxml2);

try{docxml.Save(@"C:\ejemplo.xml");}
catch(Exception e){Console.WriteLine(e.Message);}
}
}
}

Este miniprograma nos genera el fichero ejemplo.xml. En él, además de importar el


espacio de nombres System.XML declaramos las siguientes variables: un documento
xml, un nodo, dos elementos y un texto. Lo primero que hacemos es crear un objeto
XmlDocument que es el que vamos a rellenar y grabar en un fichero físico. Añadimos
un nodo con el método CreateNode del propio objeto y lo añadimos en un nivel
inferior al documento (AppendChild). Con CreateElement incorporamos los
elementos que hemos incluido y sus textos descriptivos. Al igual que antes, los
jerarquizamos con AppendChild. Como podemos observar, incluimos un elemento
RAIZ y dentro de él un “Elemento1” dentro del elemento raíz. Para terminar,
grabamos el resultado.

184
El resultado lo vemos a continuación abriendo el archivo con un navegador:

<RAIZ>
Texto del elemento raíz
<Elemento1>Texto del elemento 1</Elemento1>
</RAIZ>

Bien, ahora vamos a manipular este fichero XML. Con la clase XmlTextReader
creamos un stream o flujo de datos asociado a un xml, al cual podemos tratar
mediante las propiedades y métodos de dicha clase. Veamos el siguiente listado:
using System;
using System.Xml;
namespace XML
{
class leeXML
{
public static int Main(string[] args)
{
if (args.Length==0)
{
Console.WriteLine("Sintaxis: LeeXML <ficheroXML>");
return 1;
}
else if (args.Length>1)
{
Console.WriteLine("Demasiados argumentos");
Console.WriteLine("Sintaxis: LeeXML <ficheroXML>");
return 1;
}
XmlTextReader fichero = new XmlTextReader(args[0]);
while (fichero.Read())
{
if (fichero.MoveToContent()==XmlNodeType.Element)
{
Console.WriteLine("Elemento = " +
fichero.Name);
if (fichero.IsEmptyElement &&
fichero.HasAttributes)
{
for (int i=0; i<fichero.AttributeCount;
i++)
{
fichero.MoveToAttribute(i);
Console.WriteLine(" Atributo = " +
fichero.Name);
}
fichero.MoveToElement();
}
}
}
return 0;
}

}
}

185
GENERANDO DOCUMENTACIÓN XML

/// <summary> La clase Hola muestra un saludo en pantalla


/// </summary>
class Hola
{
/// <remarks> Usamos E/S basada en Consola.
/// Para más información sobre Console.WriteLine
/// <seealso cref="System.Console.WriteLine"/>
/// </remarks>
public static void Main( )
{
Console.WriteLine("Hello, World");
}
}

Podemos usar un tipo especial de comentarios para generar código XML. Lo hacemos
precediendo al código ///. Hay una serie de etiquetas XML muy útiles:

Etiqueta Propósito
<summary> … </summary> Proporciona una breve descripción.
<remarks> … </remarks> Proporciona una descripción detallada. Puede
contener párrafos anidados, listas y otro tipo de etiquetas.
<para> … </para> Para añadir estructura a la definición en <remarks>.
Permite que los párrafos sean desalineados.
<list type="…"> … </list> Añade una lista estructurada a una descripción
detallada. Los tipos soportados son “bullet”,”number” y “table”. Etiquetas adicionales
(<term>...</term> y <description>...</description> son usadas dentro de la lista para
definir más profundamente la estructura.
<example> … </example> Proporciona un ejemplo de cómo un método,
propiedad u otra librería debe ser usada. En ocasiones incluye el uso de una etiqueta
anidada <code>.
<code> … </code> Indica que el texto encerrado entre ellas es código de
aplicación.

<c> … </c> Indica que el texto entre ellas es código de aplicación.


<code> se usa para líneas de código que han de ser separadas por cualquier expresión
que pueda encerrarlas; <c> se usa para código embebido dentro de una descripción
encerrada
<see cref=" member"/> Indica una referencia a otro miembro o campo. El
compilador chequea que el miembro exista actualmente.
<seealso cref="member"/> Indica una referencia a otro miembro o campo. El
compilador chequea que el miembro actualmente existe. La diferencia con <see>
depende del procesador que manipule el XML una vez generado. El procesador debe
ser capaz de generar secciones See y SeeAlso para que estas dos etiquetas puedan estar
bien diferenciadas de una forma significativa.

186
<exception> … </exception> Proporciona una descripción para una clase de
excepción.
<permission> … </permission> Documenta la accesibilidad de un miembro.
<param name="name"> …</param> Proporciona una descripción para un método
parámetro
<returns> … </returns> Documenta el valor de retorno y el tipo de un
método.
<value> … </value> Describe una propiedad.

Se puede compilar la documentación XML dentro de un archivo XML usando la


opción /doc en el compilador:

csc miprograma.cs /doc:miscomentarios.xml

187
INTERACCIÓN CON MICROSOFT EXCEL

Para poder realizar una aplicación en la cual podamos interactuar con Excel,
primeramente, hemos de agregar la referencia a la Microsoft Excel Object Library
correspondiente (por ejemplo, para la Office 2003, la versión es la 11).

Una vez agregada, ya podemos introducir la línea: using Excel; de forma que podamos
usar las clases de dicho espacio de nombres. Los pasos que generalmente vamos a
seguir son:

1. Declaramos la aplicación, libro de trabajo y hoja/s con los que vamos a trabajar:

Excel.Application oXL=new Excel.Application();


Excel.Workbook Librodetrabajo;
Excel.Worksheet Hoja;

A continuación, añadimos a nuestra aplicación un libro en blanco con una hoja en


blanco:

Librodetrabajo=oXL.Workbooks.Add(Type.Missing);
Hoja=(Excel.Worksheet)Librodetrabajo.Sheets[1];

El siguiente paso consiste en obtener un rango de esa hoja para trabajar con el, eso se
hace mediante:

Excel.Range rg = Hoja.get_Range("A1","E1");
rg.Select();

Una vez seleccionado dicho rango (en este caso, A1:E1), es cuando procedemos a
ajustar sus propiedades. Comentar, antes de seguir, que la función get_range es propia
de C#. Para hacer lo mismo en .NET tendríamos que recurrir a Range. Volviendo a
get_Range, otra forma de seleccionar ese rango es la siguiente:
get_Range(“A1:E1”,Type.Missing).

rg.Font.Name="Arial";
rg.Font.Bold=true;
rg.Font.Size=10;
rg.WrapText=true;
rg.HorizontalAlignment=Excel.Constants.xlCenter;
rg.Interior.ColorIndex=40;
rg.Borders.Weight=3;
rg.Borders.LineStyle=Excel.Constants.xlSolid;
rg.Cells.RowHeight=38;
rg=Hoja.get_Range("A1",Type.Missing);
rg.Cells.ColumnWidth = 7;
rg.Value2 = "Valor de la celda";

La propiedad WrapText sirve para ajustar el texto a la celda, siempre que esté a true.
El resto de propiedades se ven de una forma más o menos clara, con lo cual no vamos
a profundizar en ellas. De esta forma, como podemos ver, configuramos a nuestro

188
gusto la hoja en cuestión. El siguiente paso ahora consiste en grabar esos cambios en
el archivo deseado. Veamos el siguiente código:
oXL.ActiveWorkbook.SaveAs(@"c:\prueba.xls",Excel.XlFileFormat.xlWorkbookNor
mal,Type.Missing,Type.Missing,Type.Missing,Type.Missing,Excel.XlSaveAsAcces
sMode.xlNoChange,Type.Missing, Type.Missing, Type.Missing,
Type.Missing,Type.Missing);

Huelga decir cual es el motivo de la inserción de esta línea. Indicar que el número de
argumentos puede variar según la versión de la Object Library, y que no está
sobrecargado, con lo cual debemos utilizar tantos como nos pida la propia función.
Supongamos que se desea exportar un dataset a una hoja de Excel. El ejemplo a
continuación nos muestra la manera de hacerlo:
using System.Data.SqlClient;
using Microsoft.Office.Interop.Excel;
private void button1_Click(object sender, System.EventArgs e) {
SqlConnection oConexion = new SqlConnection();
oConexion.ConnectionString = "Integrated Security=SSPI;Persist Security
Info=False;" + "Data Source=localhost;Initial Catalog=Northwind"; SqlCommand
oComando = new SqlCommand();
oComando.Connection = oConexion;
oComando.CommandType = CommandType.Text;
oComando.CommandText = "SELECT EmployeeID, (FirstName + ' ' + LastName)
AS Nombre FROM Employees";
SqlDataAdapter oAdaptador = new SqlDataAdapter();
oAdaptador.SelectCommand = oComando;
DataSet oDataSet = new DataSet();
oConexion.Open();
oAdaptador.Fill(oDataSet, "Employees");
oConexion.Close();
Microsoft.Office.Interop.Excel.Application oExcel = new
Microsoft.Office.Interop.Excel.Application();
oExcel.Visible = true;
oExcel.WindowState = XlWindowState.xlNormal;
Workbooks oWBooks = (Workbooks) Workbooks;
_Workbook oWBook =
(_Workbook)(oWBooks.Add(XlWBATemplate.xlWBATWorksheet)); Sheets oSheets
= (Sheets)oWBook.Worksheets;
_Worksheet oWSheet = (_Worksheet)(oSheets.get_Item(1));
int nContador = 0;
foreach (DataRow oRow in oDataSet.Tables["Employees"].Rows) {
nContador++;
Range oRange = oWSheet.get_Range("A" + nContador, "A" + nContador);
oRange.Value2 = oRow["Nombre"]; } }

COMO AGREGAR UN ICONO DE DESINSTALACIÓN DE UNA APLICACIÓN


EN UN PROYECTO DE IMPLEMENTACIÓN .NET

En las aplicaciones generadas por proyectos de implementación en Visual


Studio .NET, echamos de menos el poder introducir un desinstalador para nuestra
aplicación, ya que únicamente nos genera una aplicación de Microsoft Installer.

189
Una de tantas formas de hacer algo así es utilizar ORCA. Orca es un programa de
Microsoft que permite editar nuestro archivo msi de forma que podamos cambiar
muchas de las propiedades de dicho archivo, y en nuestro caso, añadir un acceso
directo a un script de desinstalación. Para poder descargarse Orca, debemos recurrir a
un kit completo de desarrollo de Microsoft llamado Microsoft Platform SDK.

Un MSI es básicamente una base de datos, la cual es relativamente fácil de interpretar.


Vamos a abrir orca y navegar sobre nuestro MSI generado por VS.NET. Dentro de los
campos, si hacemos clic en Feature podemos ver que archivos, dlls, etc, componen
nuestro msi

Un campo importante a tener en cuenta es el campo File. En la columna File tenemos


el identificador del fichero que nos va a venir muy bien en el futuro. En ShortCut
tenemos los accesos directos que crea la aplicación (al escritorio y al grupo de
programas principalmente). Aquí vamos a crear una nueva fila. Lo hacemos yendo a
Tables > Add Row.

190
Nos aparece un cuadro de diálogo donde debemos rellenar una serie de campos, algunos de
ellos obligatorios:

Campo Valor
Shortcut _51AB71ACD3B646EAA292EB4A3766037E
Directory _D072DA12046B46AA89C9C22AFA43519A
Name UNINST~1|Uninstall Application
Compone C__2972A49A7A6C4FB7BA9ECA73384DA0D
nt A
Target [SystemFolder]/msiexec.exe
Argument /i{7BFFC693-5BB6-4BB3-A311-
s 95FF0E6B8405}
Descriptio
n
Hotkey
Icon
IconIndex
ShowCm
1
d
WkDir TARGETDIR

Shortcut es el acceso directo en si. Para él podemos usar un GUID cualquiera, o usar
GUIDGEN. El que damos como ejemplo, puede servir. En Directory va el GUID del
directorio donde queremos colocar el acceso. Aquí simplemente, podemos copiar
cualquiera de las ubicaciones de los diferentes accesos que ya ha creado Visual
Studio .NET. Por ejemplo, si queremos colocarlo en el menú de programas,
simplemente copiamos el GUID del anterior Shortcut ya existente. En Component
damos el GUID del fichero principal que sirve de sustento a los accesos directos. Por
último, Arguments es /i y a continuación el GUID del archivo de instalación que
podemos obtener desde el propio proyecto de instalación en la propiedad
ProductCode.

Con esto, al instalar nuestra aplicación veremos un acceso directo que nos permitirá
su desinstalación o reparación según el caso.

CONOCER EL USUARIO AUTENTICADO ACTUALMENTE

En .NET podemos conocer que usuario está logueado en el momento en que se ejecuta
la aplicación e incluso en que grupo de usuarios está ubicado. Este proceso lo vamos a
ejecutar con la ayuda de las clases WindowsIdentity y WindowsPrincipal que se
encuentran en el namespace System.Security.Principal.

WindowsIdentity wi = WindowsIdentity.GetCurrent();
WindowsPrincipal wp = new WindowsPrincipal(wi);

191
WindowsIdentity representa a un usuario de Windows. Con el método GetCurrent()
obtenemos el usuario que está actualmente autenticado en nuestro equipo. Si
llamamos a la propiedad name de wi, obtendremos el nombre del usuario.

Mediante WindowsPrincipal vamos a comprobar la condición de pertenencia de un


usuario de Windows a un grupo. Para ello utilizamos el método IsInRole:

if (wp.IsInRole("Administradores"))

SABER SI ESTAMOS EJECUTANDO DESDE EL IDE

Una forma de averiguarse si estamos ejecutando nuestra aplicación desde el IDE es


comprobar lo siguiente:

if (System.Diagnostics.Debugger.IsAttached) ...

Esto devuelve true cuando está conectado el debugger, lo cual ocurre cuando se lanza
la ejecución con Debug desde dentro de Visual Studio. Tiene la ventaja de que, sin salir
del IDE, se puede comprobar como se va a comportar el programa cuando lancemos
el ejecutable. Para ello, solo tenemos que lanzarlo con “Iniciar sin depurar”

192
NANT

Con Nant se pueden escribir uno o más archivos de comandos que se encargan de
resolver situaciones como las siguientes:

- Recrear la BD usando la última versión guardada en Visual SourceSafe


- Configurar la forma de inicio de los servicios de indexación
- Crear directorios para los nuevos archivos de configuración
- Compilar el código para saber si todo es correcto, pudiendo incluso utilizar
pruebas unitarias
- Instalar componentes en la caché de ensamblados global

Estas tareas las podemos realizar de forma automática, por ejemplo, al comienzo del
día, el primer lunes de cada semana, etc… Veamos un pequeño ejemplo de un archivo
NAnt:

<project name="prueba" default="run">


<target name="remueve_BD" if="${db.remover='si'}">
<echo message="Eliminando la base de datos..."/>
<exec program="osql" failonerror="true" commandline="-i
${proyecto.drive}${vss.archivos.ruta}\MiProyecto\Database\remueveBD.sql -d
{db.name} -n -r -b -E"/>
<call target="creaNuevaBasedeDatos" failonerror="true" />
</target>

<!-- +++++++++++++++++++++++++++++++++ -->


<!-- Nant ejecutará primero esta tarea -->
<!-- +++++++++++++++++++++++++++++++++ -->

<target name="run" dependes="remueve_BD" description="Elimina la base de


datos" />
</project>

Básicamente el procedimiento llamado “run” será ejecutado en forma predeterminada,


lo que iniciará “remueve_BD” si es que la variable “bd.remover” se ha especificado
como “sí”. Suponiendo que sea de esta forma, entonces se procederá a ejecutar el
archivo externo “osql.exe” que pertenece a SQL Server. El mismo indica una serie de
parámetros y un archivo SQL a ejecutar, el cual removerá finalmente la base de datos.
En el caso de que se produjera un error se abortará el proceso no ejecutándose la
llamada a “CreaNuevaBasedeDatos”. El atributo “FailOnError” está a true, por lo que,
de producirse un error al ejecutarse una de estas tareas se abortará la ejecución
informando al usuario inmediatamente de ello.

En la versión de VS 2005 y posteriores se incluye una herramienta similar llamada


Microsoft Build. La sintaxis en ella es muy similar a la de NAnt. Se dispone además de
una herramienta llamada Nant-Gui que sirve de interfaz gráfica para NAnt.

193
Veamos un ejemplo de uso de NAnt. Primero vamos a crear el siguiente código en C#:

namespace PruebaAnt {
static class Program {
static void Main() {
System.Console.Write("Hola, vengo de marte...");
}
}
}

Lo guardamos en un directorio con el nombre PruebaAnt.cs y en esa misma ubicación


generamos un archivo default.build con el siguiente texto:
<?xml version="1.0"?>
<project name="Mi proyecto" default="run">
<target name="Compila" description="Saludo a los terricolas"
failonerror="true">
<mkdir dir="bin" />
<csc target="exe" output="bin/PruebaAnt.exe">
<sources>
<include name="*.cs" />
</sources>
</csc>
</target>

<target name="run" depends="Compila" description="Compila y ejecuta


mi archivo">
<exec program="bin/PruebaAnt.exe" />
</target>
</project>

Si ahora ejecutamos nant en ese directorio, nos mostrará información de compilación


para ese script. Ello se debe a que NAnt buscará siempre un archivo llamado
“default.build” en el directorio en el que estemos situados al ejecutarlo. Si se desea
especificar un nombre diferente entonces deberemos hacer uso del parámetro “-
buildfile”:

Nant – buildfile:miarhivo.default

El ejemplo anterior ejecuta primero el “target” (tarea a realizar) llamado “run”, ya que
esto se especifica en la segunda línea mediante el elemento “default”. Esta tarea
depende (depends) de “compila”, por lo que se ejecutará lo que está allí antes de hacer
nada con ese bloque. Una vez finalizado con la dependencia (compilación) se volverá
y ejecutará el resto del bloque. Para el caso que la ejecución falle y el atributo
failonerror se configure a verdadero, entonces se abortará completamente
mostrándose finalmente el error al usuario.

Un documento con comandos NAnt no es más que un archivo XML. Básicamente en


el raíz debemos poner un elemento llamado project que contendrá las diferentes
194
operaciones a realizar. Como pueden haber varios grupos de tareas, se introduce el
término “target” (objetivo) que permite agrupar el conjunto de operaciones que
deseamos llevar adelante. Si vemos el listado veremos que hay dos “target”, uno que
se encarga de realizar la compilación y otro que efectúa la ejecución. Un target es
similar a una función en un lenguaje tradicional; podemos llamarlos desde cualquier
parte. Cada target contiene una lista de instrucciones a ejecutar y el mismo puede ser
llamado de dos maneras:

- Usando el atributo “depends” desde otro target


- Empleando el comando “call”

La primera opción nos brinda la certeza de que no se va a ejecutar el contenido del


target llamador hasta que no se haya terminado la ejecución dependiente. En este caso
se ejecutará primero “Compila” y si todo va bien, va a al contenido de “run”. Podemos
incluso indicar más de un target como dependencia usando una coma como separador.

<target name=”run” depends=”Compila, enviamail, publica”/>

Si empleamos la etiqueta “call” bifurcamos temporalmente la ejecución a un target


diferente, muy similar a un lenguaje tradicional.

Utilizando propiedades

El concepto de propiedad en NAnt es similar a la definición de una constante en


lenguajes tradicionales. Básicamente este valor se deberá establecer en el momento de
crear la variable y permanecerá inmutable durante el resto de la ejecución:

<property name=”proyecto.ruta” value=”miproyecto” />

Aunque la propiedad puede llamarse con un nombre único, la utilización del punto
como separación nos permite agrupar varios nombres bajo un grupo lógico. La
definición de una propiedad puede darse dentro de un objetivo (target) lo que
alcanzará solamente dicho bloque. Sin embargo, también podemos definir una o más
a nivel general (fuera de todo “target”), lo que hará que los valores sean visibles desde
todo el proyecto.

Para hacer referencia a la propiedad, hay que indicar la misma empleando la sintaxis
${variable}. El siguiente ejemplo escribe el proyecto y la unidad a la consola:

<echo message=”La unidad es ${proyecto.unidad} y el directorio es


${proyecto.unidad}”/>

Existe una excepción a la inmutabilidad que radica en que podemos cambiar el valor
de una propiedad cuando ejecutamos NAnt usando la siguiente sintaxis:

nant – D:proyecto.unidad=”d:” –D:proyecto.ruta=”MiProyecto”

195
Si lo deseamos podemos partir un archivo en varios más pequeños; por ejemplo
podríamos agrupar todos los objetivos relacionados con la BD en uno llamado
basededatos.build y en otro poner lo relacionado con compilar:

<include buildfile=”${proyecto.unidad}\${proyecto.ruta}\basededatos.build” />


<include buildfile=”${proyecto.unidad}\${proyecto.ruta}\compilacion.build” />

Estas líneas deben ser incluidas debajo de la etiqueta raíz (proyecto) y es recomendable
no escribir un archivo gigante sino partirlo en varios.

Tareas

A continuación, se explican alguna de las tareas que podemos realizar con NAnt:

attrib: Cambia atributos de un archivo o directorio:


<attrib file=”myfile.txt” readonly=”true” />

A todos los archivo exe de “bin”:


<attrib normal=”true”>
<fileset>
<include name=”**/*.exe” />
<include name=”bin” />
</fileset>
</attrib>

call: Realiza una llamada a un “target”:


<call target=”compila” />

delete: Elimina un archivo o directorio:


<delete dir=${build.dir}” failonerror=”false” />

Elimina todos los archivos ejecutables o de depuración:

<delete>
<fileset>
<include name=”*.exe” />
<include name=”*.pdb” />
</fileset>
</delete>

gunzip: Desomprime un archivo:


<gunzip src=”text.tar.gz” dest=”test.tar” />

if: Realiza una comparación condicional:


<if archivo=”myfile.dll” nuevoarchivo=”myfile.cs”>
<echo message=”mi archivo.dll es más nuevo o de la misma fecha que
nuevoarchvo.cs” />
196
mkdir: Crea un directorio:

<mkdir dir=”midir” />

regsvcs: Instala o remueve un servicio .NET

<regsvcs action=”Create” assembly=”mytest.dll” />

setenv: Establece una variable de entorno


<setenv>
<variable name=”var1” value=”value2” />
<variable name=”var2” value=”value2” />
<variable name=”var3” value=”value3:%var2%” />
</setenv>

solution: Compila una solución Visual Studio sin necesidad de cargar el IDE. En este
caso, compila test.sln omitiendo el proyecto a.csproj:

<solution solutionfile=”test.sln” configuration=”release”>


<excludeprojects>
<include name=”A\A.csproj” />
</excludeprojects>
</solution>
En esta ocasión, lo hace con una solución web:

<solution solutionfile=”test.sln” configuration=”release”>


<webmap>
<map url=”http://localhost/A/A.csproj” path=”c:\inetpub\wwwroot\A\A.csproj”
/>
<map url=”http://localhost/B” path=”c:/other/B” />
</webmap>
</solution>

Vssget: Obtiene la última versión de archivos de Visual Source Safe:

<vssget localpath=”c:\vssfolder” recursive=”true” replace=”true” writable=”false”


dbpath=${vss.database}\srcsafe.ini” path=”$/miproyecto/ “ />

197
Además de las funciones predefinidas, podemos crear las nuestras propias con C#.
Veamos un ejemplo:

<?xml version="1.0"?>
<project name="Mi proyecto" default="run">
<target>
<setenv>
<variable name="proyecto.ruta"
value="${proyecto.ruta}" />
<variable name="archivo"
value="${proyecto.ruta}${vss.ruta}\version.txt" />
</setenv>
<echo message="La version ${micodigo::escribeVersion()} fue
creada!!" />
</target>

<script language="C#" prefix="micodigo" >


<code>
<![CDATA[
[Function("escribeVersion")]
public static string escribeVersion()
{
string version =
System.Environment.GetEnvironmentVariable("proyecto.ruta");
string ruta =
System.Environment.GetEnvironmentVariable("archivo");
//crea un escritor y abre el archivo
TextWriter tw = new System.IO.StreamWriter(ruta);
//escribe una línea de texto en el fichero
tw.WriteLine(version);
//cierra el stream
tw.Close();
return version;
}
]]>
</code>
</script>
</project>

Los valores a la función C# son pasados dentro de variables de entorno y la función se


llama dentro del propio “echo”. Por su parte el bloque de “script” deberá tener el
prefijo a utilizar mientras que la etiqueta “function” indica cual será el nombre por el
que se conocerá dicho código dentro de NAnt.

198
Funciones predefinidas

En la tabla siguiente podemos ver alguna de las funciones predefinidas de NAnt:

Función Descripción
assembly::get-full-name Obtiene el nombre completo de un
ensamblado
assembly::get-version Obtiene la versión de un ensamblado
bool::parse Convierte un texto a una expresión
booleana true/false
datetime::get-day Obtiene el día del mes representado por
la fecha dada
directory::exists Verifica si el directorio existe
directory::get-creation- Obtiene la fecha de creación de un
time directorio
framework::exists Verifica si .NET está instalado
framework::get-version Obtiene la versión instalada del
framework
string::contains Indica si una cadena de texto está
contenida en otra

Un ejemplo de uso:

<attrib file=”miarchivo.dll” readonly=”false” if=${file::exists(‘miarchivo.dll’}}”

Cambia el atributo de la biblioteca “miarchivo.dll solamente si existe previamente.

El siguiente ejemplo evalúa la variable compilar convirtiendola a un valor booleano.


Como puede verse, no es necesario indicar la variable “compilar” dentro de la función
con la notación ${variable} ya que ha sido indicado al comienzo de la llamada a la
función:

<if test=”${bool::parse(“compilar)}”>
<solution configuration=”Release”
solutionfile=”${proyecto.directorio\miproyecto2.sln”
outputdir=”${compilar.ruta.salida}”>
<webmap>
<map url=http://localhost/miproyecto/miproyecto2.csproj
path=”${wwwroot.ruta\miproyecto2.csproj />
</webmap>
</solution>

Además podemos usar NAnt con intellisense en VS. Basta con poner el archivo
nant.xsd en el directorio ..\CommonX\Packages\schemas\xml. Una vez hecho esto
se puede abrir el archivo en Visual Studio pero tendremos que adicionar dentro del
elemento Project la etiqueta xlmns con el siguiente valor para que sea reconocido:
199
<project xlmns=”http://nant.sf.net/release/0.85/nant.xsd>

</project>

Por ultimo, indicar que NAnt es sensible a mayúsculas y minúsculas.

¿CLASES DE DOMINIO O INTERFACES?

Toda la premisa del diseño basado en dominios (Domain Driven Design) se basa en la
evolución de un modelo de dominio como piedra angular de la actividad de diseño.
Un modelo de dominio consiste en varias abstracciones de nivel de dominio, las cuales
se basan en interfaces que revelan sus objetivos. Cuando se habla de abstracciones, van
incluidos tanto datos como el comportamiento asociado. Todo el propósito tras DDD
consiste en el manejo de la complejidad a la hora de modelar estas abstracciones de
forma que se pueda usar de forma fácil por otros clientes. En el proceso de extensión,
el diseñador necesita asegurar que los supuestos básicos o restricciones de
comportamiento no se vulneran y que las abstracciones en los interfaces respetan el
marco de trabajo contractual básico (precondiciones, postcondiciones e invariantes).

¿Son los interfaces suficientes para revelar intenciones posteriores?

El único ámbito en el que el diseñador tiene que revelar la intención es en el nombrado


del interface y en sus propios métodos. Desafortunadamente, los interfaces no son
muy adecuados para modelar restricciones o aspectos que puedan ser asociados con
las APIs publicadas. Sin recurrir a técnicas no nativas, no es posible expresar las
restricciones básicas que se deben respetar a la hora de implementar una interfaz.

Un ejemplo:

interface IValueDateCalculator
{
DateTime calculateValueDate(DateTime tradeDate);
}

Este interface cumple con todos los criterios de no revelación de intenciones. Además,
la pregunta que surge es si proporciona todas las restricciones necesarias que demanda
el implementador. Por ejemplo, cabe preguntarse cómo especificar que el valor
calculado debe ser una fecha de negocio posterior a la comercial (tradeDate) y que al
menos debe ser al menos tres días hábiles anteriores a la fecha comercial de entrada.
Los interfaces puros no permiten especificar tales criterios. Los atributos (o
anotaciones en Java) tampoco son de ayuda partiendo de que no son heredados por
las implementaciones.

Una alternativa puede ser hacer una clase abstracta con todas las restricciones sobre la
que basar la implementación:

200
abstract class ValueDateCalculator
{
public DateTime calculateValueDate(DateTime tradeDate)
{
DateTime valueDate = doCalculateValueDate(tradeDate);
if (valueDate < tradeDate) {
throw new ArgumentException (“…”);
}
if ((valueDate – tradeDate).Days < 3) {
throw new ArgumentException(“…”);
}
// chequeo de otras postcondiciones
return valueDate;
}

// para ser implementado por subclases


protected abstract DateTime doCalculateValueDate(DateTime tradeDate);
}

Este modelo chequea todas las restricciones necesarias a satisfacer una vez que la
implementación calcula la fecha sobreescribiendo el método plantilla. Por otra parte,
con interfaces puros (el primer modelo), para hacer cumplir todas las restricciones, se
dispone de las siguientes alternativas:

- Tener una clase abstracta implementando el interface la cual forzará el


cumplimiento de las restricciones. Esto da como resultado una indirección
necesaria sin valor añadido al modelo. Los implementadores van a tener que
extender la clases abstracta, lo que hace al interfaz redundante. Y además, es
posible que precisamente por esto, el implementador decida hacerlo
directamente con el interfaz y tirar por la borda todas las restricciones.
- Permitir que convivan varias implementaciones, cada una de ellas con su
propia versión de restricciones, lo que viola el principio de DRY (Don’t Repeat
Yourself).
- Dejar toda la responsabilidad a los implementadores, documentando todas las
restricciones en la documentación y simplemente esperar que se cumplan.

Evolucionando el modelo de dominio.

Las clases abstractas proporcionan una evolución fácil del modelo de dominio. El
proceso de modelado de dominio es iterativo y evolutivo. Por lo tanto, una vez
publicadas las APIs, se necesita asegurar cierta inmutabilidad desde el momento en el
que todas ellas serán potencialmente usadas por distintos clientes. Algunas escuelas
de pensamiento adoptan diferentes técnicas con el fin de alcanzar su inmutabilidad.
El equipo de desarrollo de Eclipse usa extensión de interfaces (The Extension Object
Design Pattern) y evoluciona sus diseños nombrando interfaces extendidas con un
sufijo numérico.

Una vez más, para dar soporte a la evolución “suave” de las APIs de dominio, las
interfaces deben estar respaldadas con una implementación de clases abstracta y los
implementadores programar dicha clase y no el interfaz. La pregunta que surge es
para qué es bueno entonces un interfaz.

201
¿Son inservibles los interfaces en DDD?

Ciertamente no. Se usarán interfaces puros para manejar las siguientes situaciones:

- Herencia múltiple, particularmente cuando se mezclan implementaciones.


- SPIs (Software Process Improvement). En el momento en el que aquellas tienen
siempre múltiples implementaciones y además fuertemente inconexas. La capa
de servicios es precisamente la principal candidata a usar interfaces. Esta
necesita “mocking” sencillo para ser probada y los interfaces se ajustan
perfectamente a este contexto.

202

También podría gustarte