Sunteți pe pagina 1din 23

Acceso a Base de Datos con ADO.NET 1.- Introduccin 2.- Espacio de nombres 3.

- Creacin de una base de datos 4.- Clases fundamentales 5.- Pasos fundamentales para acceder una base de datos en modo conectado 6.- Modo conectado. Dataset. 7.- Actualizacin de los DataSets 8.- Dataset con tipo 9.- Ejemplo completo 1. Introduccin ADO.NET es la evolucin de lo que conocamos por ADO (Microsoft Actives Data Objects). ADO.NET es la ltima tecnologa de acceso a datos de Microsoft y forma parte integral de .NET Framework. ADO.NET nos proporciona un conjunto extenso de clases .NET que facilitan el acceso eficaz a los datos desde casi cualquier tipo de fuente. Clases que nos ayudarn a la manipulacin de los datos. ADO.NET es una tecnologa mucho ms sencilla de utilizar, basada en XML, lo que permite una mejor interaccin con la informacin de la base de datos. ADO.NET divide sus servicios principalmente en dos ramas, un conjunto de clases preparados para trabajar con controladores OLE DB y otra rama especfica para trabajar con SQL SERVER. 2. Espacio de nombres Para realizar el tratamiento de datos desde nuestras aplicaciones C#, podemos elegir entre dos espacios de nombre (namespace). System.Data.OleDb : Lo debemos utilizar para acceder a base de datos que utilizan OLE DB o Microsoft SQL Server. System.Data.SqlClient : Para acceder a base de datos Microsoft SQL Server 7.0 en adelante. Desde el punto de vista prctico de un programador no hay diferencias, ambas proporcionan las mismas clases y mtodos. La diferencia se estriba en su funcionamiento interno, totalmente transparente para nosotros. Aunque el espacio de nombres System.Data.OleDb puede ser utilizado para acceder a base de datos SQL Server, su funcionamiento con estas es ms lento y menos eficiente. 3. Creacin de una base de datos Existen muchas maneras de crear una base de datos. Si por ejemplo usted tiene instalado una versin de Microsoft SQL Server, este proporcionar una aplicacin manager capaz de ayudarle en este cometido. Pero tambin podra utilizar Microsoft Access, que permite crear, consultar y modificar base de datos de una manera bastante fcil. Debido a su facilidad, explicaremos como crear una base de datos utilizando Access. Una vez creada, veremos cmo Visual Studio puede realizar una conexin a esta base de datos y comenzar a utilizarla fcilmente. Lo primero ser abrir Microsoft Access. Cuando el programa le responda lo primero que le preguntar ser que desea hacer. La mejor opcin ser crear una Base de datos de Access en blanco, lo cual nos permitir crear nuestra propia base de datos:

Fig. 1:Creacin de una Base de Datos en Access Cuando pulsemos en Aceptar, tendremos que elegir la carpeta y el nombre donde queremos guardar nuestra base de datos. Una vez que hagamos esta operacin, veremos una pantalla como la siguiente donde tendremos que indicar cual es la operacin que queremos llevar a cabo. Para crear una tabla, o varias deberemos hacer clic sobre Crear una tabla en vista Diseo.

Fig. 2:Creacin de Tablas En este momento veremos una nueva ventana, en la cual podemos crear campos en nuestra tabla e indicar el tipo que tendr cada uno de ellos. Podemos elegir entre diversos tipos de elementos, pero en definitiva estos son bastante intuitivos:

Valor Texto

Tipo de datos

Tamao Hasta 255 caracteres o la (Predeterminado) Texto o combinaciones longitud que indique la de texto y nmeros, as como nmeros propiedad Tamao del que no requieran clculos, como los campo (el menor de los nmeros de telfono dos valores). Microsoft

Access no reserva espacio para la parte que no se utiliza de un campo de texto. Hasta 65.535 caracteres. (Si el campo se manipula a travs de DAO y slo se Texto extenso, o combinacin extensa deva a almacenar en l Memo texto y nmeros. texto y nmeros [no datos binarios], el tamao est limitado por el de la base de datos). Datos numricos utilizados en clculos 1, 2, 4 u 8 bytes (16 matemticos. Para obtener ms bytes si el valor de la Numrico informacin sobre la forma de establecer propiedad Tamao del un tipo numrico especfico, vea el tema campo es Id. de rplica). de la propiedad Tamao del campo. Valores de fecha y hora para los aos del Fecha/Hora 8 bytes. 100 al 9999 Valores de moneda y datos numricos utilizados en clculos matemticos en los que estn implicados datos que Moneda contengan entre uno y cuatro decimales.8 bytes. La precisin es de hasta 15 dgitos a la izquierda del separador decimal y hasta 4 dgitos a la derecha del mismo. Nmero secuencial (incrementado de uno a uno) nico, o nmero aleatorio que 4 bytes (16 bytes si el Microsoft Access asigna cada vez que se valor de la propiedad Autonumrico agrega un nuevo registro a una tabla. Los Tamao del campo es Id. campos Autonumrico no se pueden la rplica). actualizar. Si desea ms informacin, vea el tema de la propiedad Nuevos valores. Valores S y No, y campos que contengan S/No uno de entre dos valores (S/No,1 bit. Verdadero/Falso o Activado/desactivado). Ntese que la tabla muestra los nombres de los diferentes tipos en espaol. Evidentemente en la versin en ingls de Access, estos nombres cambiarn, pero la correspondencia ser exactamente la misma, conservando las mismas caractersticas: TextoString, NumricoNumeric, etc. Podemos, por ejemplo crear una tabla como se muestra en la siguiente figura. Se trata de la tpica tabla empleado, donde tendremos como campos el nombre, apellidos, DNI y campo indicando si est de baja o no. No debemos olvidar indicar que campo de todos los que hemos definido es la clave principal. Para ello debemos posicionar el ratn encima del campo que queramos que sea la clave principal, pulsar el botn derecho y hacer clic sobre la opcin Clave Principal. Este campo es el utilizado para diferenciar los distintos elementos que introduzcamos en cada una de las filas de nuestra tabla, as como si queremos establecer relaciones entra distintas tablas, etc.

Fig. 3:Creacin de una Tabla de Datos en Access Cuando intentemos cerrar esta ventana, Access nos preguntar que nombre queremos darle a la tabla. Un buen nombre podra ser Empleados. Cuando demos un nombre y aceptemos, podremos ver que nuestra tabla ha sido creada. En este momento podemos crear algunos elementos, simplemente haciendo clic en la tabla y rellenando los valores de cada uno de los campos. Microsoft Access, permite muchas ms funcionalidades, pero el objeto de este articulo no es aprender como utilizar Access u otra herramienta sino permitirnos crear una pequea base de datos para poner en prctica nuestros conocimientos en ADO.NET. Ahora desde Visual Studio podemos abrir la base de datos que acabamos de crear. Simplemente debemos activar el visor de servidores, hacemos clic en View y a continuacin en Server Explorer. En este momento a nuestra derecha se abrir un explorador en forma de rbol. Una de estas ramas ser Data Connections, pulsando en esta con el botn derecho de nuestro ratn y seleccionando Add connection se abrir una ventana cmo la indicada en la siguiente figura:

Fig. 4:Aadir conexin a una Base de Datos desde Visual Studio.NET En principio debemos pulsar en la pestaa Provider, debido a que por defecto Visual Studio intenta conectarse a una base de datos SQL Server. Una vez en

Provider veremos una lista de posibles proveedores de base de datos. En este caso, que hemos utilizado Access, este proveedor es del tipo: Microsoft Jet 4.0 OLE DB Provider Ahora simplemente hacemos clic sobre el botn Next y aparecer una ventana donde podremos navegar, para encontrar nuestra base de datos. Adems existe la posibilidad de conectarse con un nombre de usuario y una clave, para los casos en que los hubiramos definido al crear la base de datos. Simplemente aceptando el Server Explorer mostrar la nueva conexin que hemos creado en forma de rbol. Ahora desde aqu podremos crear nuevas tablas, modificar datos, crear datos, etc. Sin tener que acudir al Access. El hecho ms importante es que cuando pulsemos en el explorador de servidores sobre la conexin que acabamos de abrir, si habilitamos la vista de propiedades, Properties Window, veremos la cadena de conexin, Connect String la cual nos ser muy til en puntos posteriores. 4. Clases fundamentales El siguiente diagrama muestra las clases ms importantes que podemos utilizar para trabajar con una base de datos utilizando ADO.NET:

Fig. 5:Clases fundamentales de ADO.NET Connection : Esta clase nos proporcionar objetos que nos permitirn la conexin a una fuente de datos. Todos los objetos que permiten actuar sobre la fuente de datos, necesitarn un objeto connection creado. Command: Los objetos de esta clase nos permitirn interactuar con la base de datos para transmitir comandos SQL hacia esta. Cuando trabajemos de manera conectada, estos nos devolvern objetos de tipo DataReader, que nos permitirn observar el efecto del comando sobre la base de datos. Tambin los utilizaremos junto los objetos de DataAdapter para poder crearnos una base local en nuestra memoria que nos permita trabajar con partes de la fuente de datos. DataAdapter : Esta clase nos proporciona objetos que hacen de puente entre nuestro objeto DataSet y la base de datos. Gracias a estos objetos podemos rellenar nuestra base de datos virtual con las tablas, relaciones, etc. que queramos trabajar. Una vez que nuestra base de datos local est completada, podemos trabajar con ella gracias a la jerarqua de objetos DataSet. DataSet : Esta clase nos proporciona objetos que me permitirn crear una base de datos virtual. Esto significa que podremos copiar parte o la totalidad de la base de datos real, y realizar una copia de ella en la memoria local de nuestra mquina. En ese momento, trabajaremos con nuestra base de datos

virtual. Todos los cambios y operaciones no se vern reflejadas en la base de datos real hasta que realicemos una confirmacin. Este objeto nos permite trabajar de manera desconectada, proporcionando velocidad y flexibilidad. La jerarqua de objetos DataSet se compone de Tables, que representa una coleccin de tablas. DataTable, objeto que representa una tabla. DataColumn que representa una columna de una tabla. DataRow, que representa una fila de una tabla. DataReader : Esta clase proporciona objetos cuya funcionalidad es trabajar con la base de datos de manera conectada. Por ejemplo, podremos consultar que elementos fueron afectados al lanzar un objeto Command sobre la base de datos. 5. Pasos fundamentales para acceder una base de datos en modo conectado El primer paso ser incluir el namespace apropiado a la base de datos que queramos acceder. Para ilustrar la redaccin, imaginemos que queremos utilizar una base de datos OLE DB. Por lo tanto, en nuestro ejemplo utilizaremos: using System.Data.OleDb; A continuacin deberemos formar una cadena de conexin a la base de datos, que pasaremos a un objeto de la clase OleDbConnection. Normalmente en la cadena de conexin deberemos incluir diversos campos. Data source: fuente de datos Provider: suministrador de la base de datos user id:que es la identificacin de usuario password:la clave del usuario Dependiendo de la seguridad que se le haya dado a la base de datos, podremos omitir algunos campos. Excepto Data Source que es el campo que indica la direccin de la base de datos. Por ejemplo: String cConexin = Provider=Microsoft.Jet.OLEDB.4.0;DataSource=miBase.mdb;user id=;password=; Si has seguido la todas las secciones de este artculo, en el epgrafe en el cual explique como se puede crear una base de datos, vimos como Visual Studio era capaz de abrir una conexin con una base de datos existentes. Una vez la conexin abierta, en le ventana de propiedades de la nuestra conexin podemos ver cul es la cadena de conexin que Visual Studio ha empleado, que es exactamente la misma para nosotros. Simplemente debemos copiar y pegar. OleDbConnection conexin = new OleDbConnection(cConexin); Ahora tan slo deberemos abrir la conexin con la base de datos, empleando un mtodo de la clase OleDbConnection denominada Open(): conexin.Open(); No debemos olvidar que cuando finalicemos la utilizacin de la base de datos, debemos liberar el recurso. conexin.Close(); En este punto sera aconsejable capturar excepciones, ya que al intentar abrir la conexin con la base de datos podemos tener problemas, como acceso denegado, la base de datos no se encuentra, etc. El tratamiento de excepciones es bsico en programas que acceden a base de datos.

A partir de aqu y si todo ha ido bien, slo debemos formar comandos SQL y transmitirlos. Este nuevo punto es francamente fcil, gracias a la clase de objetos OleDbCommand. Slo debemos crear un objeto de esta clase donde la pasamos la conexin establecida y una cadena con el comando SQL que queremos ejecutar. string cSQL = SELECT * FROM EMPLEADOS; OleDbCommand comando = newOleDbCommand(conexin,cSQL); int afectadas = comando.ExecuteNonQuery(); En este caso hemos utilizado el mtodo ExecuteNonQuery(), que realiza la peticin sobre la base de datos devolviendo el nmero de filas afectadas. Pero existen muchas otras formas de lanzar un comando, como son ExecuteReader() o ExecuteScalar(). La primera sirve para leer datos de la base de datos utilizando un objeto DataReader y la siguiente para leer la primera columna, de la primera fila resultado de ejecutar nuestro comando. El objeto DataReader, establece un canal de comunicaciones con la fuente de datos a travs del cual podemos interrogar sobre el resultado de nuestra consulta. En el ejemplo que se ha desarrollado ms adelante, utilizaremos el DataReader para preguntar sobre informacin estructural. Veamos con ms detalles cmo podemos consultar y modificar la base de datos en modo conectado. Utilizando nuestro objeto comando, utilizamos el mtodo ExecuteReader(). Este nos devolver un objeto de tipo DataReader, que utilizaremos para ver el resultado de nuestra consulta: DataReader lector; lector = comando.ExecuteReader(); En este momento tenemos el resultado de nuestra consulta en el DataReader. Esta respuesta, est organizada por filas. En principio, el lector no apunta a ninguna fila de la respuesta, con lo que deberemos hacer que apunte a la primera fila utilizando el mtodo Read(). Tambin es muy aconsejable utilizar un conjunto de mtodos que nos dirn si al avanzar con Read() estamos sobre una nueva fila o esta es nula, con lo cual ya habremos llegado al final de nuestra respuesta. Tambin necesitaremos informacin sobre el nmero de columnas que debemos consultar, para esto utilizamos la propiedad del lector FieldCount: int columnas = lector.FieldCount; bool hayFila = lector.Read(); Como podemos observar, Read() nos devuelve un valor booleano que nos indicar si realmente hay una nueva fila o no. Ahora para leer esta fila completa y presentar, por ejemplo, los datos por pantalla tenemos que crear una tabla de objetos. Donde el nmero de objetos ser igual al nmero de columnas de las filas que contiene el DataReader: object[] fila = new object[columnas]; Finalmente recogeremos el valor de esta fila siempre que, realmente nuestro lector apunte a una nueva fila y esta no sea nula: if(HayFila)

lector.GetValues(fila);

for(int i =0;i < columnas;i++) Console.Write( +fila[i]);

Evidentemente para obtener otra fila resultado, debemos ir avanzando nuestro lector y para ello utilizamos Read(). El siguiente ejemplo, que veremos, ser como modificar nuestra base de datos. En concreto, vamos a introducir un nuevo elemento en nuestra tabla Personas. Esta vez, utilizaremos el mtodo ExecuteNonQuery(), que simplemente nos devuelve un entero con el nmero de filas afectadas. Sin embargo, este mtodo utiliza la conexin con la base de datos para actuar sobre ella. En este caso, insertando un elemento nuevo. Lo nico que debemos definir es el comando SQL que queremos ejecutar, crear un comando y lanzar el mtodo indicado: OleDbCommand comandoInsertar; string CadInsertar = INSERT INTO Personas VALUES(Pedro,26723834); comandoInsertar = new OleDbCommand(CadInsertar,conexin); comandoInsertar.ExecuteNonQuery(); No debemos olvidar cerrar el anterior DataReader, ya que no se pueden abrir dos Readers que utilicen la misma conexin al mismo tiempo. Con esta secuencia de comandos, simplemente, hemos insertado este nuevo valor en la tabla Personas de nuestra base de datos. Aproveche la conexin que abrimos en Visual Studio para ver que efectivamente el cambio se ha producido.

Fig. 6:Visionado de Tablas con el Visual Studio.NET 6. Modo desconectado. DataSet. La manera ms habitual de trabajar con la base de datos es utilizando objetos de la clase DataSet. Esta nos proporciona la posibilidad de trabajar con una base de datos que es copia de las partes con las que queremos trabajar de la base de datos real, liberando la conexin. Al final si queremos reflejar los cambios en la base de datos real, deberemos confirmar nuestro objeto DataSet.

Los primeros pasos son similares a los que hicimos en el punto anterior, debemos crear una conexin a la base de datos, pero no la debemos abrir. El punto ms importante es que debemos crear una base de datos virtual, mediante un objeto DataSet y rellenarla con las tablas que estamos interesados trabajar. Para rellenar utilizamos un objeto de OleDbDataAdapter y su mtodo Fill() (rellenar). Veamos como hacerlo. string comando = "SELECT * FROM EMPLEADOS; DataSet BaseVirtual = new DataSet(); // El objeto DataSet OleDbDataAdapter AdaptadorDatos = new OleDbDataAdapter(comando,conexin); AdaptadorDatos.Fill(BaseVirtual,"TABLAEMPLEADOS"); El objeto Adaptador de datos se encarga de manejar la conexin por nosotros, de ah que no debamos abrir la conexin. El mtodo Fill() necesita el objeto DataSet y el nombre de la tabla que se crear en nuestra base de datos local con los datos extrados de la base de datos real. Existen diversas variantes del mtodo Fill(), pero esta es la ms sencilla de utilizar. Ahora en BaseVirtual tenemos nuestra base de datos local, que est formada por una tabla copiada de la fuente de datos. En general, tendremos un objeto DataSet relleno con el conjunto de tablas en las que nosotros estamos interesados. Ahora podemos explorar algunas operaciones tpicas tales como leer registros, modificar contenido, etc. Obtener una tabla: DataTable mitabla = BaseVirtual.Tables[0]; o DataTable miTabla = BaseVirtual.Tables[TABLAEMPLEADOS]; Ahora acceder a los elementos filas de esta tabla, foreach( DataRow fila in miTabla.Rows) { Console.WriteLine("El dni del empleado {0}", fila[0]); Console.WriteLine("Nombre {0}", fila[1].Item); } Tambin podemos utilizar directamente expresiones de tipo miTabla.Rows[0][1], con lo que nos referimos al elemento de la primera fila columna segunda. Podemos obtener informacin de las columnas, su nombre, tipo, etc. { foreach( DataColumn columna in miTabla.Columns) Console.WriteLine("Nombre : {0}", columna.ColumnName); Console.WriteLine("Tipo : {0}", columna.DataType); Console.WriteLine("Valor por defecto: {0}", columna.DefaultValue); : : }

Podemos crear una nueva fila y aadirla a nuestra base de datos: DataRow nuevaFila = miTabla.NewRow (); nuevaFila[0] = Joaquin; nuevaFila[1].Item = Castro; nuevaFila[2apellido] = Romero;

miTabla.Rows.Add(nuevaFila); Una vez que queramos validar los cambios que hemos realizado localmente en la base de datos real, debemos aceptar los cambios en los elementos que hemos creado. Este proceso es un poco ms complicado de lo que podra parecer, pero para nada insalvable. El objeto adaptador, que utilizamos para rellenar la tabla, tambin es utilizado para actualizar los datos de la base de datos real. El objeto adaptador posee un mtodo que utilizaremos para este cometido Update. Cuando llamamos a este mtodo el DataAdapter analizar los cambios que se han hecho en el objeto DataSet y ejecutar los comandos apropiados para insertar, actualizar o borrar en la base de datos original. Cuando el adaptador encuentra un cambio en una fila (DataRow), el utilizar los comandos definidos en las propiedades InsertCommand, UpdateCommand o DeleteCommand para procesar estos cambios. Estas propiedades deben contener un comando SQL apropiado en el momento de lanzar el mtodo Update. Si realizamos un Update y el adaptador utiliza una de estas propiedades con un valor nulo, una excepcin ser lanzada. Estas propiedades, de las que hablamos, son de tipo Command. A priori, puede resultar complicado crear un comando SQL que actualice la base de datos original. Pero no seremos nosotros los que lo hagamos, sino que lo har .NET por nosotros: OleDbCommandBuilder constructorComandos = new OleDbCommandBuilder(AdaptadorDatos); AdaptadorDatos.InsertCommand = ConstructorComandos.GetInsertCommand(); Utilizamos un objeto de la clase CommandBuilder que es capaz de construir comandos. Cuando le pasamos como argumento el Adaptador el sabr construir los comandos SQL que nos hagan falta para actuar sobre la base de datos. En este caso le pedimos uno para que se inserten los valores nuevos que hemos creado en nuestra base de datos local, en la base de datos fuente (real). Despus ya podemos actualizar la base de datos. AdaptadorDatos.Update(BaseVirtual,TABLAEMPLEADOS); El segundo argumento es la tabla virtual de donde queremos actualizar la tabla real, en este caso insertando. 7. Actualizacin de los DataSets Como hemos explicado los objetos DataSet nos proporcionan una manera de interactuar con partes de la base de datos, fuera de conexin. En este caso el recurso, que es la base de datos, quedar accesible a otros clientes mientras nosotros trabajamos con una copia local. La ventaja de utilizar DataSet es que la conexin es manejada por el objeto Adaptador. Debe quedar claro, que los cambios que se realizan en el DataSet son locales, y no afectan en absoluto a la fuente de datos real. Es por tanto el ltimo paso que debemos realizar. Para la actualizacin de los cambios realizados se utiliza, tambin, el objeto Adaptador. Los cambios, en general, que se pueden llevar a cabo en una base de datos son tres : Insertar, Borrar, Modificar. En el apartado anterior, donde se presentaban los DataSet, se ha explicado uno de los tres posibles casos, la insercin gracias a objetos que construan comandos. En este apartado explicaremos como borrar y modificar, y para extender nuestros conocimientos, sin utilizar estos constructores de comandos.

Cmo Borrar Si al utilizar nuestro objeto DataSet hemos borrado filas, al llamar a la funcin Update() del adaptador, su propiedad DeleteCommand tiene que estar definida. Veamos un ejemplo: DataSet miDataSet = new DataSet(); OleDbAdapter adaptador = new OleDbAdapter(comandoSELECT, conexin); adaptador.Fill(miDataSet, EMPLEADO); En este punto, hemos creado nuestra copia local de partes de la fuente real. Supongamos que estamos interesados en borrar, una fila de la tabla Empleado, Aquella cuyo DNI. sea el 28758456. Ntese que en nuestro ejemplo DNI es la supuesta clave primaria de la tabla. Aunque, DNI sea la clave primaria de nuestra tabla Empleado en la base de datos real, nuestro objeto miDataSet contiene un conjunto de tablas, en nuestro ejemplo ficticio suponemos una, donde este tipo de informacin no est reflejada Resulta totalmente necesario para poder utilizar algunos mtodos, que definamos el conjunto de campos que forman la clave primaria de una tabla. En nuestro ejemplo, estamos interesados de utilizar el mtodo Find() sobre la tabla Empleado, capaz de buscar en una DataTable una fila. La definicin de las claves de una tabla se realiza sobre la propiedad PrimaryKey de una tabla. DataTable tabla = miDataSet.Tables["Empleado"]; tabla.Columns["dni"].Unique = true; DataColumn[] keys = new DataColumn[1]; keys[0] = tabla.Columns["dni"]; tabla.PrimaryKey = keys; Primero hemos definido que la columna DNI representa un campo nico, condicin para ser una clave primaria. Despus construimos una coleccin de columnas keys, en este caso con un elemento, que se asignan a la propiedad PrimaryKey de la tabla. Una vez definido, el mtodo Find() ser capaz buscar filas en esta tabla pasndole la clave de la fila que buscamos. Buscamos la fila en la que estamos interesado y la marcamos para ser borrada con el mtodo Delete(). DataRow miFila = tabla.Rows.Find(dni_buscado); if (miFila !=null) { miFila.Delete(); ... } Llegado a este punto, estamos interesados en actualizar la base de datos real. Para ello debemos definir la propiedad DeleteCommand del objeto adaptador. Esta vez lo haremos directamente. Describiremos un comando SQL en donde todo los datos referentes a las filas que hemos borrado, quedarn reflejados en variables que el adaptador se encargar de interpretar en la actualizacin, observando que filas fueron marcadas para ser borradas: string cadSql = DELETE FROM Empleado WHERE dni = @p; OleDbCommand comando = new OleDbCommand(cadSql,conexin); Ahora tenemos que definir este parmetro, a que tipo pertenece, su tamao (0 para tomar el tamao por defecto) y a que columna de la tabla se refiere. comando.Parameters.Add("@p",OleDbType.Char,0,"dni"); Finalmente asignamos al adaptador nuestro comando y actualizamos:

adaptador.DeleteCommand = comando; adaptador.Update(miDatSet,Empleado); Cmo Modificar La nica diferencia sera que despus de buscar la fila, cambiaramos el valor de los objetos que guardan sus columnas y evidentemente el comando SQL sera un UPDATE: string cadSQL = UPDATE Empleado SET nombre=@a WHERE dni = @b; OleDbCommand comando = (cadSQL,conexin); comando.Parameters.Add("@a",OleDbType.Char,0,"nombre"); comando.Parameters.Add("@b",OleDbType.Char,0,"dni"); ... Problemas de Concurrencia Para entender que son los problemas de concurrencia supongamos la siguiente situacin. Tenemos una base de datos que puede ser accedida por mltiples usuarios. En aquella situacin donde se emplean objetos DataSet, debemos tener en cuenta que existe un periodo de tiempo donde se trabaja con una imagen de la base de datos real, base de datos real que puede ser modificada, pudiendo invalidar los datos que lemos, cuando todava estemos trabajando con ellos. Este tiempo est comprendido entre el relleno del objeto DataSet, entendido como la lectura, hasta la actualizacin de la fuente de datos real, con los cambios que hemos realizado localmente. Por ejemplo, puede darse la situacin en la que modifiquemos una fila leda, y que al realizar la modificacin en la base de datos real, esta fila ya hubiera sido modificada. Este problema que hemos explicado, se define como un problema de concurrencia y existen diferentes mtodos para solucionarlo: Concurrencia pesimista : Cuando una fila es leda, est queda bloqueada para su lectura para cualquier otro que la demande, hasta que aquel que la posee la libere. Concurrencia positiva : La filas estn disponibles para su lectura en todo momento, estas pueden ser ledas por distintos usuarios al mismo tiempo. Cuando alguno intenta modificar una fila que ya fue modificada se produce un error y no se produce la modificacin. Last win, esta tcnica implica que no existe control simplemente el ltimo en escribir es el que pertenece. ADO.NET contempla el problema de la concurrencia que puede producirse cuando utilizamos objetos DataSet, y para su tratamiento utiliza la concurrencia positiva. Aunque tambin se puede optar por Last Win. Si queremos utilizar concurrencia positiva, nos vemos obligados a introducir lgica de control en nuestras aplicaciones para comprobar si ha ocurrido un problema de concurrencia. Existen varios mtodos, y a continuacin se explica uno de ellos. El objeto DataSet mantiene dos versiones de las filas que lemos. Una versin original, que es idntica a la que lemos de la base de datos, y la versin actualizada, que representa los cambios del usuario. Cuando se actualiza la fila, los valores originales son comparados con la fila real de la base de datos y si estos son los mismos, significa que la fila no ha cambiado desde que fue leda. Y en este caso, la actualizacin es efectuada. En caso contrario la actualizacin falla y podemos capturar una excepcin para tratar el problema. Cada DataSet posee un adaptador, DataAdapter, con cuatro comandos SQL: Delete, Insert, Select, Update. Cada uno de ellos poseen, como hemos visto, una coleccin de parmetros. Cuando definimos estos parmetros cada uno de ello se refiere al valor almacenado en una fila y columna concreta de una tabla del

DataSet. Estos parmetros tienen una propiedad que nos permiten indicar si el valor del parmetro se refiere a la versin original o la versin actualizada que hablbamos. Esto nos permite crear un comando SQL dnde en la clusula WHERE compruebe si ha existido un problema de concurrencia, comparando los valores originales con aquellos en la fuente de datos. UPDATE Empleado SET nombre = @nom_modificado WHERE dni = @dni_original AND nombre = @nom_original Si el UPDATE falla para una fila, la actualizacin no se llevar a cabo. Existe un evento que es lanzado cuando se produce la actualizacin de una fila : RowUpdated. Evento que podemos capturar y que nos proporciona informacin sobre el estado de la actualizacin. Luego, podremos examinar si hubo errores de concurrencia y actuar en consecuencia. Bien, recargando los nuevos valores y intentando de nuevo la modificacin, bien rechazando esta modificacin, etc. Todo depender de nuestras intenciones. Veamos un ejemplo donde, cuando ocurre un error de concurrencia en la fila afectada escribimos en su propiedad RowError una cadena describiendo el error encontrado. ... string sql_com = Update Empleado Set nombre = @nom_actual Where nombre = nom_original AND dni = @dni_original; adaptador.UpdateCommand = new OleDbCommand(sql_com,conexion); adaptador.UpdateCommand.parameters.Add(nom_actual, OleDbType.Char,0,nombre); OleDbParameter param; param = adaptador.UpdateCommand.parameters.Add(nom_original, OleDbType.Char,0,nombre); param.SourceVersion = DataRowVersion.Original; param = adaptador.UpdateCommand.parameters.Add(dni_original, OleDbType.Char,0,dni); param.SourceVersion = DataRowVersion.Original; adaptador.RowUpdated += new OleDbRowUpdatedEventHandler(miTratamiento); adaptador.Update(miDataSet,Empleado); Como se puede observar hemos suscrito al evento RowUpdated la funcin miTratamiento, ahora debemos definir el cdigo de la misma. En este caso, comprobar si se ha producido un error de concurrencia, consultando la propiedad RecordsAffected, que ser cero en el caso de error de concurrencia. Y colocar, para la fila que provoco el error, su atributo RowError con una cadena de error: public void miTratamiento(object e , OleDbRowUpdatedEventArgs e) { if(e.RecordsAffected == 0) { e.Row.RowError = Ocurri un error de concurrencia; e.Status = UpdateStatus.SkipCurrentRow; }

En este caso tambin hemos definido un valor para la propiedad e.Status. Esta propiedad le indica al adaptador que debe hacer cuando ocurre algn tipo de error.

En este caso le indicamos que borre la fila que provoc el problema y contine con la actualizacin. Podemos indicarle que acte de ms maneras diferentes, como por ejemplo que siga de todas formas, etc. Es importante tener en cuenta que debemos definir algn tipo de comportamiento, ya que de lo contrario una excepcin ser elevada. Ahora, podramos poner algn cdigo detrs de la sentencia adaptador.Update(...). Como por ejemplo, revisar todas las filas de nuestra tabla, en busca de alguna que contenga RowError distinta de nulo. Y actuar como consideremos conveniente. foreach (DataRow row in miDataSet.Tables[Empleado].Rows) { if (row.RowError ==Ocurri un error de concurrencia) { // Acciones ... } } 8. DataSets con tipo La clase de objetos DataSet proporciona tambin una manera ms intuitiva de acceder a los datos que esta contiene, para ello debemos crear y utilizar Typed DataSets. En esta seccin veremos una breve introduccin. Un DataSet con tipo es una clase que deriva de DataSet. Esta clase hereda todos los mtodos y propiedades que hemos definido anteriormente, con lo cual podemos acceder a los datos de la misma manera que hemos definido en la seccin anterior. Adicionalmente, con los Dataset con tipo tenemos mtodos y propiedades que permiten acceder a las tablas, filas y columnas utilizando los nombres que estas poseen en realidad, permitiendo un cdigo mucho ms sencillo y legible. Nosotros somos los encargados de generar un DataSet con tipo, de alguna manera debemos generar un espacio de nombres que podamos incluir en nuestra aplicacin, como si de otro espacio de nombres se tratar y dentro de este espacio de nombres est definido nuestro nuevo DataSet. De esta manera permitiremos, tanto a Visual Studio como al compilador, comprobar si los accesos que estamos realizando son correctos. Para realizar esto, tenemos que utilizar una herramienta que se suministra con .NET Framework denominada XSD.exe. Esta herramienta bsicamente recoge un archivo con la descripcin XML de nuestra base de datos y genera un fichero .cs, que podremos convertir en un .dll y as aadir a nuestra aplicacin. Si imaginamos que tenemos construida esta descripcin bajo un archivo miDescripcion.xsd, llamaremos a la herramienta de la siguiente manera: xsd.exe /d /l:C# miDescripcion.xsd /n:XSDSchema.MiEspacioNombres Esta herramienta generar un fichero .cs contenido en el espacio de nombres que le hemos indicado con la opcin /n. La opcin /d le indica que queremos generar un DataSet y /l el lenguaje de programacin. Una vez, ejecutado nuestra herramienta, debemos llamar al compilador de .NET y generar un fichero .dll que podamos incluir en nuestra aplicacin. csc.exe /t:library miDescripcion.cs /r:System.dll /r:System.Data.dll Una vez terminado, tendremos un archivo miDescripcion.dll bajo el espacio de nombres XSDShema.MiEspacioNombres, en el cul habremos definido nuestra

clase DataSet propia. Ahora slo debemos abrir Visual Studio e incluir junto a los habituales using ..., nuestra nueva librera: using System; ... using XSDShema.MiEspacioNombres; Y aadir la referencia al archivo .dll, porque de lo contrario no funcionar ninguna de las cosas que intentaremos. A partir de aqu todo es igual : crearemos nuestro objeto DataSet de tipo la clase que acabamos de crear, lo rellenaremos y a la hora de trabajar con el, utilizaremos directamente la notacin punto seguida de el nombre de la tabla. Por ejemplo, que queremos consultar, el nmero de fila y el nombre del campo al cual queremos acceder. MiDataSet.EMPLEADOS[0].nombre = Pedro; Si todo el proceso ha tenido xito, Visual Studio nos ir indicando cuales son las tablas y que campos contienen. Veamos como hacer todo esto siguiendo el ejemplo que hasta ahora hemos utilizado: string comando = "SELECT * FROM EMPLEADOS; CustomerDataSet BaseVirtual = new CustomerDataSet(); OleDbDataAdapter AdaptadorDatos = new OleDbDataAdapter(comando,conexin); AdaptadorDatos.Fill(BaseVirtual,"EMPLEADOS"); foreach(CustomerDataSet.CustomersRow fila in BaseVirtual.EMPLEADOS) Console.WriteLine(fila.nombre); Como se puede apreciar el nico cambio que hemos tenido que aadir es utilizar la clase derivada CustomerDataSet, que es el nombre que le hemos dado al DataSet que hemos creado en nuestro archivo .dll procedente de la descripcin XML. En este punto, todo le parecer magnfico excepto un detalle: Cmo crear el archivo XML que describe la base de datos?. A continuacin se muestra un ejemplo de la descripcin de una base de datos: En este ejemplo se muestra como indicamos, que tenemos una tabla llamada Empleados y en su interior varios campos nombre y DNI. Adems se indica cual es la clave primaria de esta tabla mediante la etiqueta: < < < < xs:unique name="Constraint1"> xs:selector xpath=".//Empleados" /> xs:field xpath="dni" /> /xs:unique>

En general, podemos definir varias tablas, e incluso las relaciones que entre ellas existen, simplemente replicando la estructura que arriba se ha descrito. Por ejemplo podramos definir otra tabla Departamentos, donde indicaremos para cada DNI de empleado, el departamento de la empresa al cual pertenece: < xs:element name="Departamentos" codegen:typedName="Departamento" codegen:typedPlural="Departamentos"> < xs:complexType> < xs:sequence> < xs:element name="IdDepartamento" codegen:typedName="IdDepartamento" type="xs:int" minOccurs="0"/> < xs:element name="dni" codegen:typedName="dni" codegen:nullValue="" type="xs:string" minOccurs="0"/> < /xs:sequence> < /xs:complexType> < /xs:element>

Este trozo de XML, debera ser insertado justo detrs de la etiqueta que utilizamos al definir Empleado. Una vez, definida nuestra nueva tabla, podemos definir la relacin que existe entre ambas: < xs:keyref name="Empleadodepartamento" refer="Constraint1" codegen:typedParent="Empleado" codegen:typedChildren="ObtieneDepar"> < xs:selector xpath=".//Departamento" /> < xs:field xpath="dni" /> < /xs:keyref> En esta definicin XML, existen muchos elementos que quizs el lector no comprenda. Con este ejemplo no he pretendido ni mucho menos explicar como se debe formar la descripcin XML de una base de datos. Sin embargo, en ella se muestran algunos de los elementos ms importantes y si lee con atencin cada una de las lneas podr comprender que describe cada una de ellas y extender esta descripcin a sus propios ejemplos. 9. Ejemplo completo Para este ejemplo debemos realizar algunos procedimientos previos. Lo primero ser crear una base de datos .mdb. Abrimos el Microsoft Access y creamos nicamente la base de datos, pero no aadimos ninguna tabla. Sino se acuerda de cmo llevar a cabo la creacin, lase de nuevo el punto creacin de una base de datos. A continuacin ya podemos crear un proyecto en Visual Studio. Abra el Visual Studio, este le preguntar qu quiere hacer. Pulse en New Project y complete la ventana que a continuacin Visual Studio le muestra

Fig. 7:Creacin de un proyecto en Visual Studio.NET No olvide indicar un nombre ms intuitivo que el que Visual Studio le muestra y seale que se trata de una aplicacin de Consola. Una vez que acepte podr observar que Visual Studio ha creado una clase por usted, normalmente se llamar Class1. Esta clase de nada nos sirve, por lo cual debemos borrarla. Para ello, coloque el ratn justo encima del nombre de la clase en el explorador de soluciones, pulse el botn derecho de su ratn y seleccione Delete. A continuacin

pulse con el botn derecho en el nombre del proyecto y elija la opcin Add?Add Class. En este momento ver la siguiente ventana, que deber rellenar tal y como se muestra:

Fig. 8:Aadir un elemento al proyecto. Automticamente Visual Studio unir esta clase a nuestro proyecto y en la ventana de edicin ya podremos escribir el cdigo de nuestra aplicacin de ejemplo. La clase BaseDatos proporciona un conjunto de funciones cuyo objetivo es mostrar algunas de las posibilidades de ADO.NET, para interactuar con los datos y estructura de la base. Lo primero ser aadir todos los espacios de nombre de los que haremos uso: using using using using System; System.Data.OleDb; System.Data; System.Diagnostics;

Dentro de la seccin class BaseDatos, debemos aadir dos campos que sern atributos de esta clase: string fuenteDatos, proveedor,cadenaConexion; Crearemos dos mtodos constructores dentro de nuestra aplicacin. Estos se diferenciarn por los parmetros que reciben. El primero de ellos recibe la fuente de datos, que ser el archivo que implementa la base de datos, y una cadena con el nombre del proveedor. En el segundo caso se presupone que el proveedor es uno dado por defecto. Estos constructores lo nico que hacen es establecer los valores de los atributos: cadenaConexion, fuenteDatos y proveedor. public BaseDatos(string fuenteDatos,string proveedor) { Trace.Assert(fuenteDatos != null); this.fuenteDatos = fuenteDatos; this.proveedor = proveedor; cadenaConexion = "Provider="+proveedor + ";DataSource=" + fuenteDatos +";user id=;password=;"; } public BaseDatos(string fuenteDatos) {

Trace.Assert(fuenteDatos != null); this.fuenteDatos = fuenteDatos; this.proveedor = "Microsoft.Jet.OLEDB.4.0"; cadenaConexion ="Provider=" + proveedor + ";Data Source=" + fuenteDatos +";user id=;password=;";

A continuacin escribiremos el cdigo de los siguientes mtodos: El mtodo insertarTabla, recibe como parmetros el nombre de la tabla que se quiere crear y dos matrices. Una matriz que contiene el nombre de las columnas de la tabla y otra con los tipos, SQL, de las columnas. Este mtodo opera de manera conectada, abriendo una conexin y lanzando un comando de creacin de tabla en la base de datos. public void insertarTabla(string nombreTabla,string[] campos,string[] tipos) { Trace.Assert(nombreTabla!=null && campos!=null && tipos!=null); Trace.Assert(campos.Length == tipos.Length); OleDbConnection conexion = null; try { conexion = new OleDbConnection(this.cadenaConexion); conexion.Open(); OleDbCommand command; string comando = "CREATE TABLE "+nombreTabla; comando = comando + "("; for(int i=0;i< campos.Length;i++) { comando = comando + campos[i] + " " + tipos[i]; if (i != campos.Length - 1) comando = comando + ","; } comando = comando + ")"; command = new OleDbCommand(comando,conexion); Console.WriteLine("Comando SQL enviado" + comando); command.ExecuteNonQuery(); conexion.Close(); } catch (System.Exception e) { Console.WriteLine(e.ToString()); if(conexion!=null) conexion.Close(); Console.Read(); throw e; } } El mtodo NumeroColumnasTabla, recibe un nico parmetro que es el nombre de la tabla, el devolver un entero con el nmero de columnas que est tabla tiene. En este caso la meta-informacin ha sido obtenida de manera conectada. Una vez formado el objeto OleDbCommand, ejecutamos el mtodo ExecuteReader(), que devuelve un objeto OleDbDataReader. Este objeto sirve de canal de comunicaciones con la base de datos real y nos permite acceder al resultado de nuestra consulta, as como a informacin sobre la estructura de la misma. public int NumeroColumnasTabla(string nombreTabla) { Trace.Assert(nombreTabla!=null); OleDbConnection conexion = null;

conexion = new OleDbConnection(this.cadenaConexion); conexion.Open(); OleDbCommand comando; comando = new OleDbCommand("SELECT * FROM " + nombreTabla,conexion); int filas = comando.ExecuteReader().FieldCount; conexion.Close(); return filas; } El mtodo NombreTablas, devuelve el nombre de todas las tablas que existen el la base de datos. Cuando abrimos una conexin con una base de datos, el objeto conexin nos permite directamente mediante una serie de mtodos interrogar a la fuente sobre su estructura. Una de estos mtodos es GetOleDbSchemaTable(), que devuelve un objeto System.Data.DataTable. Este objeto es una matriz, donde cada fila contiene informacin sobre una tabla. Una de sus columnas es TABLE_NAME que es el nombre de la tabla. public string[] NombreTablas() { OleDbConnection conexion = null; conexion = new OleDbConnection(this.cadenaConexion); try { conexion.Open(); DataTable schemaTable = onexion.GetOleDbSchemaTable (OleDbSchemaGuid.Tables , new object[] {null, null, null, TABLE"}); string[] resultado= new string[schemaTable.Rows.Count] ; for(int i=0;i< schemaTable.Rows.Count;i++) resultado[i]= (string) schemaTable.Rows[i]["TABLE_NAME"]; conexion.Close(); return resultado;

} catch (Exception e) { Console.WriteLine(e.ToString()); Console.Read(); throw e; } }

NombreColumnasTabla, dada una tabla utilizando un objeto OleDbDataReader utilizamos otra de sus propiedades para obtener informacin sobre el nombre de sus columnas. La consulta SQL, debe afectar a toda la tabla: SELECT * FROM tabla public string[] NombreColumnasTabla(string nombreTabla) { Trace.Assert(nombreTabla!=null); OleDbConnection conexion = null; conexion = new OleDbConnection(this.cadenaConexion); conexion.Open(); OleDbCommand comando; comando=new OleDbCommand("SELECT * FROM" + nombreTabla,conexion);

OleDbDataReader lector = comando.ExecuteReader(); string[] resultado = new string[lector.FieldCount]; for(int i=0;i< lector.FieldCount;i++) resultado[i] = lector.GetName(i); return resultado; } ObtenerTabla, devuelve una tabla completa de la base de datos. Trabaja con un objeto DataSet, tal como hemos mostrado en secciones anteriores public DataTable ObtenerTabla(string nombreTabla) { Trace.Assert(nombreTabla!=null); OleDbConnection conexion = null; conexion = new OleDbConnection(this.cadenaConexion); try { string SelectComando = "SELECT * FROM " + nombreTabla; OleDbDataAdapter Adaptador = new OleDbDataAdapter(SelectComando,conexion); DataSet Memoria = new DataSet(nombreTabla); Adaptador.Fill(Memoria,nombreTabla); return Memoria.Tables[0]; } catch(System.IO.IOException e) { Console.WriteLine(e.ToString()); if(conexion!=null) conexion.Close(); Console.Read(); throw e;

} }

InsertarElemento, inserta una nueva fila en la tabla seleccionada. Utiliza tambin un objeto DataSet que despus debe de ser transmitido a la base de datos, para que la insercin se lleve a cabo realmente. Aqu mostramos un ejemplo de cmo utilizar un constructor de comandos para generar un comando SQL capaz de insertar en la base de datos public void InsertarElemento(string nombreTabla,object[] valores) { Trace.Assert(nombreTabla!=null); OleDbConnection conexion = null; conexion = new OleDbConnection(this.cadenaConexion); try {

string comando = "SELECT * FROM " + nombreTabla; DataSet TablasMemoria = new DataSet(); OleDbDataAdapter AdaptadorDatos = new OleDbDataAdapter(comando,conexion); AdaptadorDatos.Fill(TablasMemoria, nombreTabla); DataTable unaTabla=TablasMemoria.Tables[nombreTabla]; DataRow NuevaColumna = unaTabla.NewRow(); for(int i=0;i< unaTabla.Columns.Count;i++) { NuevaColumna[i] = valores[i]; } unaTabla.Rows.Add(NuevaColumna); TablasMemoria.Tables[0].Columns[0].Unique = true; DataColumn[] keys = new DataColumn[1]; keys[0] = TablasMemoria.Tables[0].Columns[0]; TablasMemoria.Tables[0].PrimaryKey = keys; */ OleDbCommandBuilder custCB = new OleDbCommandBuilder(AdaptadorDatos); AdaptadorDatos.InsertCommand = custCB.GetInsertCommand(); AdaptadorDatos.Update(TablasMemoria, nombreTabla); } catch (System.Exception e) { Console.WriteLine(e.ToString()); f(conexion!=null) conexion.Close(); Console.Read(); throw e; } } ImprimirTabla, mtodo que recibe un objeto de tipo DataTable y recorre sus elementos para imprimirlos por pantalla. public void ImprimirTabla(DataTable tabla) { int columnas = tabla.Columns.Count; int filas = tabla.Rows.Count; foreach(DataRow fila in tabla.Rows) { for(int j=0;j< columnas;j++) Console.Write(fila[j]); Console.WriteLine(); } }

En esta primera clase hemos mezclado el mtodo de acceso con DataSet y el mtodo con conexin basado en DataReader. La manera de indicar dnde est la fuente de datos entiende que el directorio actual es donde se ejecuta la aplicacin. Con lo cual si colocamos el archivo .mdb en otra carpeta debemos colocar la ruta de acceso general, o con respecto a la ubicacin del ejecutable. A continuacin el archivo de test.cs. Simplemente va llamando uno a uno los mtodos que hemos construido, para mostrar su funcionamiento. Tendremos que aadir una nueva clase a nuestro proyecto, como ya hicimos anteriormente. Esta clase se llamar test.cs. Una vez que escriba el cdigo, indique en el explorador de soluciones que test es la clase que contiene el mtodo Main(), es decir, el punto de arranque del ejemplo. El mtodo Main() incluye en diversos puntos instrucciones del tipo: Console.Read(); Estas tienen la misin de parar la ejecucin del programa, y esperar a que usted pulse una tecla, de lo contrario la ventana con la consola ejecutara nuestra aplicacin y se cerrara, y todo esto tan rpidamente que no lograra ver nada. using System; using System.Data; using Base; namespace Base{ public class Test{ public static void Main() { Console.WriteLine("Abriendo Base de Datos \"MiBaseDatos.mdb\""); BaseDatos bd = new BaseDatos("..\\..\\Mibase.mdb"); bool tablaYaCreada = false; string[] tablas = bd.NombreTablas(); Console.WriteLine("Las tablas que contiene son:"); foreach (string tabla in tablas){ Console.Write(tabla + " "); if (tabla == "PERSONAS") tablaYaCreada = true; } if (tablas.Length ==0) Console.Write(" No hay ninguna"); if(!tablaYaCreada){ Console.WriteLine("Insertamos la tabla PERSONAS"); string[] campos = new String[2]; string[] tipos = new String[2]; campos[0] = "nombre"; campos[1] = "dni"; tipos[0] = "char(20)"; tipos[1] = "integer"; bd.insertarTabla("PERSONAS",campos,tipos); } Console.WriteLine("\nInformacin sobre la tabla PERSONAS: "); string nombreTabla ="PERSONAS"; Console.WriteLine("**************************************"); Console.WriteLine("Numero de columnas de " + nombreTabla + " : "+bd.NumeroColumnasTabla(nombreTabla)); Console.Write("Nombre de las columnas: "); string[] camposaux = bd.NombreColumnasTabla(nombreTabla); foreach (string campo in camposaux) Console.Write(campo+" ");

Console.WriteLine("\n*******************************"); object[] valores = new Object[camposaux.Length]; Console.WriteLine("Insertamos en la tabla"+nombreTabla); for(int i=0;i< camposaux.Length;i++){ Console.Write("Insertar elemento para \""+camposaux[i]+" \": "); valores[i] = Console.ReadLine(); } Console.Read(); bd.InsertarElemento(nombreTabla,valores); Console.WriteLine("\n\nElementos de "+nombreTabla); System.Data.DataTable t = bd.ObtenerTabla("PERSONAS"); bd.ImprimirTabla(t); Console.WriteLine("\nPulsa una tecla para terminar"); Console.Read(); } } }

S-ar putea să vă placă și