1. Ejemplos. 1.1 Introducción. Vamos a visualizar dos tipos de programas de mantenimiento, uno con una tabla que esta enlazada a un objeto de visualización como el DataGridView, y que necesita muy pocas líneas de código. El otro ejemplo es el de un mantenimiento más clásico, y sin enlazar la tabla de datos. Un ejemplo de un listado, sin uso del Crystal Report. Y algunos procedimientos que pueden resultar útiles. Los pasos que se van a considerar, con el fin de no alargar en exceso los ejemplos, son exclusivamente los referentes a lo imprescindible, dejando de lado lo accesorio, dicho de otra forma, configuraciones previas, mensajes de ayuda, cargas iniciales, etc.. 1.2 Mantenimiento con tabla sin enlazar. Comparado con el enlazado, es mucho más costoso, pero claro, los resultados no son iguales, y no siempre un mantenimiento debe considerarse como un grid que visualice sus datos y dejar todo en manos del usuario, por eso conviene elegir el más adecuado en cada ocasión. Por lo tanto vamos a empezar con el punto de partida que es el control del campo de código de la tabla, el ejemplo es de una tabla con una clave principal compuesta por dos campos. 1.2.1 Punto de partida. Validación de los campos de código. Private Sub Campo00_Validating( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.CancelEventArgs) _ Handles Campo01.Validating, _ Campo02.Validating, _ .. / .. Campo13.Validating, _ Campo14.Validating Dim Cual As Integer Dim Mensaje As String Cual = CInt(Strings.Right(CType(sender, TextBox).Name, 2)) Validacion(Cual, CType(sender, TextBox), e.Cancel) End Sub Al abandonar los TextBox, se valida su contenido. 1.2.2 Validación. En este procedimiento la primera parte de la clave se obtiene de un ComboBox,, Tipo = CType(ComboBox01.SelectedItem, ItemLista) Después el código se obtiene Campo.Text = Format(CInt(Campo.Text), "0000") Cuando ya disponemos de los dos datos, pasamos a realizar una lectura de comprobación: LeerRegistro(Tipo.Codigo, Campo01.Text) En este procedimiento obtendremos la situación de la clave introducida, existente o no. El procedimiento de validación es el que sigue. Private Sub Validacion(ByVal Cual As Integer, _ ByRef Campo As TextBox, ByRef Cancel As Boolean) Dim Tipo As ItemLista Cancel = False Select Case Cual Case 1 If Campo.Text <> "" Then Campo.Text = Format(CInt(Campo.Text), "0000") Select Case ComboBox01.SelectedIndex > -1 Case True Tipo = CType(ComboBox01.SelectedItem, ItemLista) LeerRegistro(Tipo.Codigo, Campo01.Text) Case Else Cancel = True MsgBox("Debe indicar cual es el tipo de la publicación", _ MsgBoxStyle.Information, NomProgram) End Select Case 5 .. / .. End Select End Sub 1.2.3 Lectura de control. Veamos el procedimiento de lectura. El procedimiento realiza una lectura de un registro que exista en la tabla, si existe en la propiedad Tag del objeto button, Comando01, que hay en el formulario asignamos una “A”, actualizar, y si no “N”, nuevo, inserción. Si existe aprovechamos para visualizar los datos, se podía haber enfocado de varias formas. Los pasos son Generamos la instrucción SQL. CadenaSQL = "SELECT TipPub, Codigo, Titulo, ClasEdad, " & _ "ClasTema, Period, Editorial, ISBN, Autor, " & _ "PreCoste, PreVenta, EntAcu, SalAcu, EntDev, SalDev, " & _ "StockMax, StockMin, UltCompra, UltVenta " & _ "FROM Titulos " & _ "WHERE TipPub = '" & TipoPub & "' And " & _ "Codigo = '" & Codigo & "'" Inicializamos los objetos de acceso a la base de datos, el objeto conexión se configura una vez al inicio de la aplicación. ' Código SQL para la consulta Comando.CommandText = CadenaSQL ' Tipo de comando a ejecutar Comando.CommandType = CommandType.Text ' Conexión a utilizar, configurada previamente. Comando.Connection = Conexion Y después se ejecuta la acción. ' Uso del comando Try Lector = Comando.ExecuteReader Después de la ejecución se comprueba si se encuentra o no un registro, y se asigna “A” o “N” según el resultado, y visualizamos los datos. Select Case Lector.HasRows Case True Lector.Read() Campo01.Text = Lector.Item("Codigo").ToString . / .. Comando01.Tag = "A" ‘ Actualización Case Else Comando01.Tag = "N" ‘ Inserción End Select El código completo es el siguiente. Private Sub LeerRegistro(ByVal TipoPub As String, ByVal Codigo As String) Dim Comando As New System.Data.OleDb.OleDbCommand Dim Lector As System.Data.OleDb.OleDbDataReader Dim CadenaSQL As String Conexion.Open() CadenaSQL = "SELECT TipPub, Codigo, Titulo, ClasEdad, " & _ "ClasTema, Period, Editorial, ISBN, Autor, " & _ "PreCoste, PreVenta, EntAcu, SalAcu, EntDev, SalDev, " & _ "StockMax, StockMin, UltCompra, UltVenta " & _ "FROM Titulos " & _ "WHERE TipPub = '" & TipoPub & "' And " & _ "Codigo = '" & Codigo & "'" ' Código SQL para la consulta Comando.CommandText = CadenaSQL ' Tipo de comando a ejecutar Comando.CommandType = CommandType.Text ' Conexión a utilizar, configurada previamente. Comando.Connection = Conexion ' Uso del comando Try Lector = Comando.ExecuteReader Select Case Lector.HasRows Case True Lector.Read() Campo01.Text = Lector.Item("Codigo").ToString Campo02.Text = Lector.Item("Titulo").ToString .. / .. Campo14.Text = Lector.Item("UltVenta").ToString Comando01.Tag = "A" ‘ Actualización Case Else Comando01.Tag = "N" ‘ Inserción Campo01.Tag = Campo01.Text InicializarCampos(Me) Campo01.Text = Campo01.Tag.ToString End Select Catch ex As OleDb.OleDbException MsgBox(ex.Message, MsgBoxStyle.Information) End Try ' Liberar recursos Comando.Dispose() Comando = Nothing Conexion.Close() End Sub El siguiente paso, después de la ejecución normal del programa, sería la acción a realizar por el usuario. Borrado Grabación. 1.2.4 Elección del usuario. En el formulario se han previsto las siguientes posibilidades. Grabar Baja Cancelar Salida Que son las cuatro posibilidades que contempla el evento. Private Sub Comando01_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles Comando01.Click, _ Comando02.Click, _ Comando03.Click, _ Comando04.Click Dim Cual As Integer = CInt(Strings.Right(CType(sender, Button).Name, 2)) TrataComando(Cual) End Sub Capturada la acción del usuario el siguiente paso es actuar. Private Sub TrataComando(ByVal Cual As Integer) Select Case Cual Case 1 ' Aceptar Grabar() Case 2 ' Baja Borrar() Case 3 ' Cancelar InicializarCampos(Me) Case 4 ' Salir Salida() End Select Campo01.Focus() End Sub 1.2.5 Borrado. El borrado queda como sigue. Crear la cadena SQL CadenaSQl = "Delete From Titulos " & _ "Where (Titulos.TipPub = '" & Tipo.Codigo.ToString & "') and " & _ "(Titulos.Codigo = '" & Campo01.Text & "');" Inicializar los objetos de acceso a la base de datos. Ejecución de la instrucción SQL Cuantos = Comando.ExecuteNonQuery En este caso como no hay datos de retorno, se usa ExecuteNonQuery. Y el ejemplo completo a continuación. Private Sub Borrar() Dim Comando As New System.Data.OleDb.OleDbCommand Dim Cuantos As Integer Dim CadenaSQl As String Dim Tipo As ItemLista Tipo = CType(ComboBox01.SelectedItem, ItemLista) CadenaSQl = "Delete From Titulos " & _ "Where (Titulos.TipPub = '" & Tipo.Codigo.ToString & "') and " & _ "(Titulos.Codigo = '" & Campo01.Text & "');" Conexion.Open() ' Nombre de la consulta en la base de datos. Comando.CommandText = CadenaSQl ' Tipo de comando a ejecutar Comando.CommandType = CommandType.Text ' Conexión a utilizar, configurada previamente. Comando.Connection = Conexion Try Cuantos = Comando.ExecuteNonQuery Select Case Cuantos Case Is <> 0 MsgBox("Datos borrados", MsgBoxStyle.Information, Me.Text) Case Else MsgBox("Incidencia en borrado", MsgBoxStyle.Information, Me.Text) End Select Catch ex As OleDb.OleDbException MsgBox(ex.Message, MsgBoxStyle.Information, "Error en borrado") End Try Conexion.Close() ' Liberar recursos Comando.Dispose() Comando = Nothing End Sub 1.2.6 Grabación. La acción de grabación, oculta al usuario la acción de inserción o actualización, que ya se ha seleccionado en el momento de validar a nivel de programa. Por lo tanto el primer paso es seleccionar la acción a realizar. Private Sub Grabar() Select Case Comando01.Tag.ToString Case "A" Actualizar() Case "N" Insertar() End Select End Sub Como se puede observar se utiliza la propiedad Tag, que se ha cargado previamente en validación. Por lo tanto ahora o se inserta o se actualiza. Veamos primero la actualización. Primero como siempre creación de la instrucción SQL, CadenaSQl = "Update Titulos Set " & _ "TipPub = '" & TipoPub.Codigo.ToString & "' , " & _ "Codigo = '" & Campo01.Text & "' , " & _ .. / .. "UltVenta = '" & UltVent & "' " & _ "WHERE (Titulos.TipPub = '" & TipoPub.Codigo.ToString & "') And " & _ "(Titulos.Codigo = '" & Campo01.Text & "');" Después como siempre la inicialización de los objetos de acceso a la base de datos, y la ejecución de la instrucción SQL. Cuantos = Comando.ExecuteNonQuery Y el control del resultado de la actualización. Select Case Cuantos Case Is <> 0 MsgBox("Datos grabados", MsgBoxStyle.Information, Me.Text) Case Else ' cuando hay error salta el Catch, y sino MsgBox("Error en actualización", MsgBoxStyle.Information, Me.Text) End Select El procedimiento completo queda. Private Sub Actualizar() Dim Comando As New System.Data.OleDb.OleDbCommand Dim Cuantos As Integer Dim CadenaSQl As String CadenaSQl = "Update Titulos Set " & _ "TipPub = '" & TipoPub.Codigo.ToString & "' , " & _ "Codigo = '" & Campo01.Text & "' , " & _ "Titulo = '" & Campo02.Text & "' , " & _ .. / .. "UltVenta = '" & UltVent & "' " & _ "WHERE (Titulos.TipPub = '" & TipoPub.Codigo.ToString & "') And " & _ "(Titulos.Codigo = '" & Campo01.Text & "');" Conexion.Open() ' Código SQL Comando.CommandText = CadenaSQl ' Tipo de comando a ejecutar Comando.CommandType = CommandType.Text ' Conexión a utilizar, configurada previamente. Comando.Connection = Conexion Try Cuantos = Comando.ExecuteNonQuery Select Case Cuantos Case Is <> 0 MsgBox("Datos grabados", MsgBoxStyle.Information, Me.Text) Case Else ' cuando hay error salta el Catch, y sino MsgBox("Error en actualización", MsgBoxStyle.Information, Me.Text) End Select Catch ex As OleDb.OleDbException MsgBox(ex.Message, MsgBoxStyle.Information, "Error en actualización") End Try Conexion.Close() ' Liberar recursos Comando.Dispose() Comando = Nothing End Sub El proceso de inserción es similar al anterior pero claro cambia la instrucción SQL, por lo que hay poco que comentar. Private Dim Dim Dim Sub Insertar() Comando As New System.Data.OleDb.OleDbCommand Cuantos As Integer CadenaSQl As String CadenaSQl = "INSERT INTO Titulos ( TipPub, Codigo, Titulo, ClasEdad, " & _ "ClasTema, Period, Editorial, ISBN, Autor, " & _ "PreCoste, PreVenta, EntAcu, SalAcu, " & _ "EntDev, SalDev, StockMax, StockMin, " & _ "UltCompra, UltVenta ) " & _ "Values ('" & TipoPub.Codigo.ToString & "', " & _ "'" & Campo01.Text & "', " & _ .. / .. "'" & UltVent & "') " Conexion.Open() ' Código SQL Comando.CommandText = CadenaSQl ' Tipo de comando a ejecutar Comando.CommandType = CommandType.Text ' Conexión a utilizar, configurada previamente. Comando.Connection = Conexion Try ' hay que borrarlo cuando está probado Cuantos = Comando.ExecuteNonQuery Select Case Cuantos Case Is <> 0 MsgBox("Datos grabados", MsgBoxStyle.Information, Me.Text) Case Else ' cuando hay error salta el Catch, y sino MsgBox("Error en inserción", MsgBoxStyle.Information, Me.Text) End Select Catch ex As OleDb.OleDbException MsgBox(ex.Message, MsgBoxStyle.Information, "Error en inserción") End Try Conexion.Close() ' Liberar recursos Comando.Dispose() Comando = Nothing End Sub En clase se les propone a los alumnos, que abrevien el código para realizar un único paso. Visto el ejemplo comentar, que en caso de querer realizarlo con un DataSet, los cambios son mínimos, pero pensamos que un Mantenimiento es más lógico hacerlo enlazando con la base de datos y dejando los cambios plasmados en la misma, sin que se difieran los mismos en el tiempo. Con un DataSet por otro lado se puede utilizar el método de búsqueda Find. 1.3 Mantenimiento con tabla enlazada. La ventaja de este planteamiento, es los pocos recursos que se necesitan para dejar una tabla accesible al usuario y permitir que realice tareas de mantenimiento. El inconveniente, lo parco en recursos de validación, pero se puede alcanzar un buen nivel. Por otro lado queda el tema del control de teclado, que hemos podido resolver creando una clase que herede el objeto DataGridView y a la que le hemos mejorado el control del teclado, personalizándolo por columnas. No hemos incluido dicha clase, pues aunque funciona, aún es un embrión, y no nos gusta su aspecto. Veamos el ejemplo. 1.3.1 Punto de partida. En este caso el punto de partida tiene que ser el enlace de la tabla al DataGridView. Private Sub CargaFormulario(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles Me.Load ConfigConexion(Conexion) Carga(Conexion, Adaptador, Enlace, ObjDataGrid, “Select * from provincia") End Sub Por lo tanto el mejor sitio para hacerlo es el evento Load del formulario, en el cual también configuramos el objeto DataConnection. Los objetos a utilizar son: ' Conexión a la base de datos Dim Conexion As New System.Data.OleDb.OleDbConnection ' Acceso a la base de datos Dim Adaptador As New System.Data.OleDb.OleDbDataAdapter ' Enlace a datos, no es imprescindible, solo por mostrarlo Dim Enlace As New BindingSource ' Comando para la ejecución de SQL Dim Comando As New System.Data.OleDb.OleDbCommand Definidos a nivel de formulario. En este procedimiento se realiza la carga de los datos, y el enlace del objeto. Public Sub Carga(ByVal ByRef ByRef ByRef ByVal Conexion As System.Data.OleDb.OleDbConnection, _ Adaptador As System.Data.OleDb.OleDbDataAdapter, _ EnlaceTabla As BindingSource, _ ObjDataGrid As DataGridView, _ CadenaSql As String) Dim ComandoActualizar As OleDb.OleDbCommandBuilder Try Conexion.Open ' Crear un nuevo adaptador de datos Adaptador = New OleDb.OleDbDataAdapter(CadenaSql, Conexion) ' Crear un 'commandbuilder' que genere el SQL Update/Insert/Delete ' no debe cambiarse de sitio ComandoActualizar = New OleDb.OleDbCommandBuilder(Adaptador) ' Llenar la tabla con los datos Dim Tabla As New DataTable Adaptador.Fill(Tabla) ' Enlazarla al 'bindingsource' ObjDataGrid.DataSource = EnlaceTabla EnlaceTabla.DataSource = Tabla Conexión.Close Catch ex As OleDb.OleDbException MsgBox(ex.Message, MsgBoxStyle.Information) End Try End Sub El enlace también puede realizarse de forma directa sin usar el bindingsource. ObjDataGrid.DataSource = Tabla 1.3.2 Elección del usuario. Teniendo en cuenta que el DataGrid contiene todos los datos de la tabla, el usuario se supone que sabe localizar lo que desea modificar, borrar o cuando debe insertar una fila nueva. Salvada esta consideración, hay que tener presente que mientras no se ejecute el método Update los cambios no se reflejan en la base de datos, por lo tanto es posible en la acción de cancelar, recargar el DataGrid, y éste retomará la situación inicial, o desde la última actualización. El borrado se realiza mediante el método Remove del DataGridView, sobre la fila actual. En el formulario se han previsto las siguientes posibilidades. Actualizar Baja Cancelar Salida Que son las cuatro posibilidades que contempla el evento, y el código que se acompaña. Private Sub Comando01_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles Comando01.Click, _ Comando02.Click, _ Comando03.Click, _ Comando04.Click Dim Cual As Integer Cual = CInt(Strings.Right(CType(sender, Button).Name, 2)) Select Case Cual Case 1 ' Actualizar la base de datos con los cambios efectuados Try Adaptador.Update(CType(Enlace.DataSource, DataTable)) MsgBox("Datos actualizados", MsgBoxStyle.Information) Catch ex As System.Data.OleDb.OleDbException MsgBox("Errores " & vbCrLf & ex.Message, MsgBoxStyle.Critical) End Try Case 2 ' Borrar ObjDataGrid.Rows.Remove(ObjDataGrid.CurrentRow) Case 3 ' Cancelar, recargar sin actualizar los cambios Carga(Conexion, _ Adaptador, _ Enlace, _ ObjDataGrid, _ Adaptador.SelectCommand.CommandText) Case 4 ' salida Salida() End Select End Sub Y no es necesario nada más, para que funcione. Las columnas en el DataGridView, se generan de forma automática a partir de la estructura de la tabla que estamos utilizando. Mediante las propiedades de configuración de este objeto es posible el bloqueo de las acciones de edición de forma personalizada. Este sistema no siempre es posible plantearlo, acudir a la documentación de VB.Net. El procedimiento Carga se puede utilizar también de esta otra forma, por otro lado más lógica, pues dispone de la propiedad DataSource, y no precisa del objeto BindingSource de forma imprescindible. Private Sub Carga(ByVal Conexion As System.Data.OleDb.OleDbConnection, _ ByRef Adaptador As System.Data.OleDb.OleDbDataAdapter, _ ByRef ObjDataGrid As DataGridView, _ ByRef Tabla As DataTable, _ ByVal CadenaSql As String) Dim ComandoActualizar As OleDb.OleDbCommandBuilder Try ' Crear un nuevo adaptador de datos Adaptador = New OleDb.OleDbDataAdapter(CadenaSql, Conexion) ' Crear un 'commandbuilder' que genere el SQL Update/Insert/Delete ' no debe cambiarse de sitio ComandoActualizar = New OleDb.OleDbCommandBuilder(Adaptador) ' Llenar la tabla con los datos Tabla = New DataTable Adaptador.Fill(Tabla) ' Enlazar tabla y DataGrid ObjDataGrid.DataSource = Tabla Catch ex As OleDb.OleDbException MsgBox(ex.Message, MsgBoxStyle.Information) End Try End Sub La diferencia está en que se enlaza la tabla al DataGrid, no se usa el BindingSource, que es más adecuado. Por lo tanto desaparece del procedimiento el objeto BindingSource y aparece la tabla. Para esta versión, el procedimiento de evento del objeto Button, debe quedar así. Private Sub Comando01_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles Comando01.Click, _ Comando02.Click, _ Comando03.Click, _ Comando04.Click Dim Cual As Integer Cual = CInt(Strings.Right(CType(sender, Button).Name, 2)) Select Case Cual Case 1 ' Actualizar la base de datos con los cambios efectuados Try Adaptador.Update(Tabla) MsgBox("Datos actualizados", MsgBoxStyle.Information) Catch ex As System.Data.OleDb.OleDbException MsgBox("Errores " & vbCrLf & ex.Message, MsgBoxStyle.Critical) End Try Case 2 ' Borrar ObjDataGrid.Rows.Remove(ObjDataGrid.CurrentRow) Case 3 ' Cancelar, recargar sin actualizar los cambios Carga(Conexion, _ Adaptador, _ ObjDataGrid, _ Tabla, _ Adaptador.SelectCommand.CommandText) Case 4 ' salida Salida() End Select End Sub Y como consecuencia de los cambios, en el evento Load también ha de cambiarse la llamada a Carga Private Sub CargaFormulario(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles Me.Load ConfigConexion(Conexion) Carga(Conexion, Adaptador, ObjDataGrid, Tabla, ”Select * from provincia") End Sub Y debe aparecer el objeto tabla en la definición de objetos en el formulario. Dim Tabla as DataTable En función de cómo se quiera enriquecer el programa, son de setenta a cien líneas de código para el mismo, lo cual es ridículo, comparado con uno más exhaustivo en su validación. Conviene recordar las posibilidades de formato que incorpora éste objeto a la hora de visualizar información y que permiten mejorar el aspecto de los datos visualizados incorporando distintas opciones de fondos o colores en las celdas que se desee. 1.4 Listado. Crystal Report, he mejorado en mucho desde sus inicios, malo sería que no fuera así, pero la ventaja de controlar el uso de la impresión sin el asistente en cuestión, es la libertad que se tiene de poder abordar cualquier tipo de proceso de impresión sin el corsé que podría suponer dicha utilidad, a la que evidentemente siempre se puede recurrir. Exponemos solo los dos procedimientos base, el bucle de impresión y la línea de detalle, el resto de procedimientos no entrañan ningún problema. La línea de detalle se utiliza partiendo de la existencia de un vector en el que hemos calculado la ubicación de cada campo en el listado. El procedimiento que realiza dichos cálculos es el que sigue. Private Sub ConfigCabecera(ByRef Textocabecera() As CabecDetalle) Dim Fuente As Font Dim Lapiz As New System.Drawing.SolidBrush(System.Drawing.Color.Black) Dim AnchoString As New SizeF Dim Formato As New System.Drawing.StringFormat Dim Cx As Long Dim Grafico As Graphics = Me.CreateGraphics Dim X As Integer Fuente = Est_Lin_Det ' Campos del listado Textocabecera(0).Texto = "Código " Textocabecera(1).Texto = "Nombre " Textocabecera(2).Texto = "Apellido 1 " Textocabecera(3).Texto = "Apellido 2 " Textocabecera(4).Texto = "Domicilio " Textocabecera(5).Texto = "" ' Formato del texto Formato.FormatFlags = StringFormatFlags.MeasureTrailingSpaces ' Margen lateral Cx = CLng(Hoja.DefaultPageSettings.Margins.Left) ' Fuente a utiilizar Fuente = Est_Lin_Det ' New Font("Arial", 12, FontStyle.Italic) ' Bucle de cálculo While X < UBound(Textocabecera) Textocabecera(X).Cx = Cx ' Ancho del texto AnchoString = Grafico.MeasureString(StrDup(Len(Textocabecera(X).Texto), “n"), Fuente) Cx = CLng(Cx + AnchoString.Width) X = X + 1 End While End Sub 1.4.1 Punto de partida. Load Por lo tanto el punto de arranque debe ser la ejecución del procedimiento de configuración en el evento Private Sub ListAlum_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load ConfigCabecera(TextoCabecera) End Sub El inicio del listado arrancará, después de haberse captado por uno u otro sistema los rangos del mismo, en la pulsación del Button Aceptar o Listar, es lo mismo. Suponemos que en pantalla el destino del listado. Private Sub Comando01_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles Comando01.Click VistaPrevia = New PrintPreviewDialog VistaPrevia.Document = Hoja VistaPrevia.ShowDialog() End Sub Hay que tener presente que VistaPrevia.Document = Hoja Es donde se indica el destino del listado. El objeto de impresión, PrintDocument se llama Hoja. Este objeto es indistinto del destino del listado, podríamos decir que es un gestor que realiza la labor de direccionamiento al destino de la impresión. El destino en pantalla se obtiene con el objeto PrintPreviewDialog. 1.4.2 Los datos. El primer paso será obtener los datos a listar a partir de las circunstancias del listado. Ese paso se puede hacer desde el evento de inicio de listado del PrintDocument, BeginPrint Private Sub Hoja_BeginPrint( _ ByVal sender As Object, _ ByVal e As System.Drawing.Printing.PrintEventArgs) _ Handles Hoja.BeginPrint CrearDataReader() End Sub En el ejemplo se cargan los datos de la cabecera de forma manual, pero podría ejecutarse un procedimiento como éste para automatizar la tarea. CargaCamposCabecera(TextoCabecera, Reader) Cuyo contenido sería Private Sub CargaCamposCabeceraDos(ByVal V() As CabecDetalle, _ ByVal RecordSet As OleDb.OleDbDataReader) Dim Diseny As DataTable = RecordSet.GetSchemaTable() Dim Fila As DataRow Dim X As Integer For Each Fila In Diseny.Rows ' elemento cero tiene el nombre del campo V(X).Texto = Fila(0).ToString() X = X + 1 Next End Sub El procedimiento para la obtención de los datos del listado sería Private Sub CrearDataReader() Dim CadenaSQL As String Dim Condicion As String Dim Clave As String CreaRangos(Condicion, Clave) CadenaSQL = "Select Exped , Ape1 , Ape2, Nombre, Domic From Alumnos " & _ "Where " & Condicion & _ " Order by " & Clave & " ;" ContPag = 0 ' Contador de páginas Try Conexion = New System.Data.OleDb.OleDbConnection Comando = New System.Data.OleDb.OleDbCommand ' Abrir la base de datos. ConfigConexion(Conexion) Conexion.Open() ' Tipo de comando a ejecutar Comando.CommandType = CommandType.Text ' Contenido del comando Comando.CommandText = CadenaSQL ' Conexión a utilizar, configurada previamente. Comando.Connection = Conexion ' Ejecución de SQL Reader = Comando.ExecuteReader Catch Ex As Exception MsgBox(Ex.Message, MsgBoxStyle.Information, "Crear RecordSet") End Try End Sub Obtenidos los datos ahora pasamos a la ejecución del bucle. 1.4.3 El bucle. Ahora se trata de realizar la lectura de la estructura obtenida en el procedimiento anterior. Se ha utilizado un DataReader, pero igual se podía haber utilizado un DataTable, La lectura la realizaremos dentro del evento PrintPage del objeto PrintDocument. Solo recordar, o indicar, que algunas variables usadas en el ejemplo no se declaran a nivel de procedimiento por la idiosincrasia de funcionamiento de éste evento. Veamos los pasos principales. Dado que se entra y sale del evento por la acción de la propiedad e.HasMorePages = True las variables que siguen han de tener siempre este valor al entrar en el procedimiento. Dim Cabec As Boolean = True Dim Pie As Boolean = False Si hay algo a listar la condición será cierta If Reader.HasRows Then Y después mientras algo haya que leer While Reader.Read ' Línea de detalle LineaDet(e, Cy, Reader, Fuente, TextoCabecera) Iremos sacando líneas de detalle. Cada vez que se llene la página haremos el pié de página, si procede, y sacaremos cabeceras. ' Control de fin de página If Cy > e.MarginBounds.Height Then PiePagina(Cy, e) e.HasMorePages = True Exit Sub End If El control se realiza con el valor de Cy comparado con la altura del objeto que se usa en la impresión, hay que tener presente que se usa el valor de la zona imprimible, no de toda la altura del objeto. If Cy > e.MarginBounds.Height Then Sacar cabeceras es salir y entrar del seiscientos, que es un gran coche. e.HasMorePages = True Exit Sub Por eso es necesario los valores iniciales de Cabec y Pie Cuando se acabe el contenido del Reader, se realizará el fin de listado, y se dejará a falso la propiedad FinImpresion(Cy, e) e.HasMorePages = False Y con eso acabaría el listado. El fin de impresión también puede colocarse en el evento End_Print. Veamos ahora el contenido del evento PrintPage completo. Private Sub Hoja_PrintPage( _ ByVal sender As Object, _ ByVal e As System.Drawing.Printing.PrintPageEventArgs) _ Handles Hoja.PrintPage ' ' Ejemplo con un datareader ' Dim Cy As Long ' Coordenada vertical Dim Cabec As Boolean = True Dim Pie As Boolean = False Dim Fuente As Font Fuente = Est_Lin_Det ' estilo línea de detalle Try If Reader.HasRows Then If Cabec Then If ContPag = 0 Then LineaIden(Cy, ContPag, e) ' Línea de identificación InicioImpresion(e) e.HasMorePages = True Exit Sub End If Cabeceras(Cy, e) Cabec = False End If While Reader.Read ' Línea de detalle LineaDet(e, Cy, Reader, Fuente, TextoCabecera) ' Control de fin de página If Cy > e.MarginBounds.Height Then PiePagina(Cy, e) e.HasMorePages = True Exit Sub End If End While End If Catch ex As Exception MsgBox(ex.Message, MsgBoxStyle.Information, "Evento Print Page") End Try FinImpresion(Cy, e) e.HasMorePages = False End Sub 1.4.4 La línea de detalle. En el procedimiento de impresión se recibe la variable Cy, que nos indicará la situación en vertical de la línea de detalle, en éste procedimiento se incrementa en el valor de la altura de la fuente utilizada. De este forma el número de líneas de detalle depende de la altura, o tamaño de la fuente no del número de líneas que se lleven impresas, que evidentemente también se puede usar. En el ejemplo se usa un objeto Pincel, por lo de dibujar, al cual se le asigna el color negro, pero también se puede hacer lo siguiente en la línea de impresión y no es necesario utilizar ningún objeto. e.Graphics.DrawString(.Item(0).ToString, Fuente, Brushes.Black, V(0).Cx, Cy) En el procedimiento se utiliza una línea para cada campo de impresión. Y cada campo de impresión será uno de los campos de la Select utilizada en la carga del Reader, es decir Reader.Item(0), Reader.Item(1). Si que comentar, que mejor que Item(0), es utilizar Item(“Exped”), ya que de esa forma, aunque cambiará el orden de los campos en la Select, no se alteraría el listado. Por lo tanto la línea de impresión quedaría, e.Graphics.DrawString(.Item(“Exped”).ToString, Fuente, Pincel, V(0).Cx, Cy) e.Graphics.DrawString(.Item(“Ape1”).ToString, Fuente, Pincel, V(1).Cx, Cy) e.Graphics.DrawString(.Item(“Ape2”).ToString, Fuente, Pincel, V(2).Cx, Cy) e.Graphics.DrawString(.Item(“Nombre”).ToString, Fuente, Pincel, V(3).Cx, Cy) e.Graphics.DrawString(.Item(“Domic”).ToString, Fuente, Pincel, V(4).Cx, Cy) Y el procedimiento Private Sub LineaDet(ByVal ByRef ByVal ByVal ByVal e As System.Drawing.Printing.PrintPageEventArgs, _ Cy As Long, _ Reader As OleDb.OleDbDataReader, _ Fuente As Font, _ V() as CabecDetalle) Dim Pincel As New System.Drawing.SolidBrush(System.Drawing.Color.Black) With Reader e.Graphics.DrawString(.Item(0).ToString, Fuente, Pincel, V(0).Cx, Cy) e.Graphics.DrawString(.Item(1).ToString, Fuente, Pincel, V(1).Cx, Cy) e.Graphics.DrawString(.Item(2).ToString, Fuente, Pincel, V(2).Cx, Cy) e.Graphics.DrawString(.Item(3).ToString, Fuente, Pincel, V(3).Cx, Cy) e.Graphics.DrawString(.Item(4).ToString, Fuente, Pincel, V(4).Cx, Cy) End With Cy = Cy + Fuente.Height End Sub 1.4.5 El final del listado. Private Sub Hoja_EndPrint(ByVal sender As Object, _ ByVal e As System.Drawing.Printing.PrintEventArgs) _ Handles Hoja.EndPrint Conexion.Close() Comando.Dispose() Reader.Close() End Sub 1.4.6 Impresora como destino. Todo lo visto se usa igual, y hay que cambiar en el evento Clic del inicio del listado el objeto de destino PrintPreviewDialog por el de PrintDialog, pasando de VistaPrevia.Document = Hoja A PrinterDialogo.Document = Hoja Ya que es de esta forma como se asigna el destino del listado. 1.4.7 Con un DataTable como objeto de datos. Si usamos un DataTable como objeto para los datos, en el procedimiento de creación de los datos se ha de sustituir lo del DataReader por ' Asignación del comando al objeto adapter. Adaptador.SelectCommand = Comando Adaptador.Fill(Tabla) Por lo que ahora dispondremos de un DataTable y en el evento PrintPage quedaría como sigue. Private Sub Hoja_PrintPage( _ ByVal sender As Object, _ ByVal e As System.Drawing.Printing.PrintPageEventArgs) _ Handles Hoja.PrintPage ' ' Ejemplo con un DataTable ' Dim Cy As Long ' Coordenada vertical Dim Cabec As Boolean = True Dim Pie As Boolean = False Dim Fuente As Font Fuente = Est_Lin_Det ' Estilo línea de detalle Try If Posicion < Tabla.Rows.Count Then If Cabec Then If ContPag = 0 Then LineaIden(Cy, ContPag, e) ' Línea de identificación InicioImpresion(e) e.HasMorePages = True Exit Sub End If Cabeceras(Cy, e) Cabec = False End If While Tabla.Rows.Count > Posicion ' Línea de detalle LineaDet(e, Cy, Tabla, Fuente, Posicion) ' Incremento de posición en la Tabla Posicion = Posicion + 1 ' Control de fin de página If Cy > e.MarginBounds.Height Then PiePagina(Cy, e) e.HasMorePages = True Exit Sub End If End While End If Catch ex As Exception MsgBox(ex.Message, MsgBoxStyle.Information, "Evento Print Page") End Try FinImpresion(Cy, e) e.HasMorePages = False End Sub La filosofía del procedimiento se mantiene igual, pero el origen es un DataTable. El bucle While se usa con una variable integer y con Count, también se podría haber utilizado un bucle For each, con un objeto DataRow. 1.5 Mantenimiento con Button de navegación. La labor de navegación se obtiene con un objeto BindingSource, ya que este objeto dispone de los métodos adecuados. Los objetos a utilizar en el programa son: Dim Dim Dim Dim Dim Dim Dim Dim Conexion As New System.Data.OleDb.OleDbConnection Adaptador As New System.Data.OleDb.OleDbDataAdapter Tabla As New DataTable EnlaceTabla As New BindingSource Actualizador As New OleDb.OleDbCommandBuilder(Adaptador) CadenaSql As String = "Select * From Provincia Order By CodProv" Nuevo As Boolean = False Actualizado As Boolean = True Además utilizamos TextBox para la visualización y Button para las acciones de navegación y mantenimiento. La apariencia del formulario es la que vemos en la imagen. Los campos los hemos enlazado Private Sub Enlaces() Campo00.DataBindings.Add("Text", EnlaceTabla, "CodProv") Campo01.DataBindings.Add("Text", EnlaceTabla, "DenomCas") Campo02.DataBindings.Add("Text", EnlaceTabla, "DenomVal") End Sub La carga de datos se realiza como sigue: Private Sub CargaDatos() If Conexion.State = ConnectionState.Closed Then Conexion.Open() Adaptador = New OleDb.OleDbDataAdapter(CadenaSql, Conexion) Adaptador.Fill(Tabla) EnlaceTabla.DataSource = Tabla Actualizador = New OleDb.OleDbCommandBuilder(Adaptador) Conexion.Close() End Sub 1.5.1 Punto de partida. Arrancamos del evento Load, y en él realizamos las siguientes tareas. Private Sub Mantenimiento_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles Me.Load ConfigConexion(Conexion) CargaDatos() Enlaces() End Sub 1.5.2 Los eventos La captura de las acciones del usuario es como sigue: Private Sub Comando00_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles Comando01.Click, _ Comando02.Click, _ Comando03.Click, _ Comando04.Click, _ Comando05.Click, _ Comando06.Click, _ Comando07.Click, _ Comando08.Click, _ Comando09.Click Dim Cual As Integer Cual = CInt(Strings.Right(CType(sender, Button).Name, 2)) Select Case Cual Case 1 ' cancelar EnlaceTabla.CancelEdit() Case 2 ' borrar EnlaceTabla.RemoveCurrent() Case 3 ' salida Salida() Case 4 ' primero EnlaceTabla.MoveFirst() Case 5 ' anterior EnlaceTabla.MovePrevious() Case 6 ' siguiente EnlaceTabla.MoveNext() Case 7 ' último EnlaceTabla.MoveLast() Case 8 ' nuevo EnlaceTabla.AddNew() Case 9 ' actualizar EnlaceTabla.EndEdit() Actualizar() End Select End Sub Como se puede observar toda la labor de navegación se realiza en la captura de evento de los Button adecuados y con el uso de los métodos MoveXXXX. Hacer hincapié en el uso del EndEdit para su correcto funcionamiento a la hora de actualizar. EnlaceTabla.EndEdit() Actualizar() Y del uso de Case 2 ' borrar EnlaceTabla.RemoveCurrent() Para el borrado del registro actual. 1.5.3 La actualización. Ejecutar el método Update del DataAdapter y emitir el mensaje adecuado. Private Sub Actualizar() Conexion.Open() Try Adaptador.Update(CType(EnlaceTabla.DataSource, DataTable)) MsgBox("Datos actualizados.", MsgBoxStyle.Information, Me.Text) Catch ex As OleDb.OleDbException MsgBox("Datos existentes", MsgBoxStyle.Critical, Me.Text) End Try Conexion.Close() Actualizado = True End Sub Tal como está generado el procedimiento, si al pulsar la opción de registro nuevo se coloca un código existente, se genera la excepción que se captura en el procedimiento. Si se coloca un código existente, no hay problema el procedimiento lo realiza correctamente. El borrado se realiza con el RemoveCurrent del Button adecuado. 1.5.4 La salida. Dada la condición de no grabar hasta que se pulse el botón de actualización en la salida se puede hacer lo siguiente. Private Sub Salida() ' Liberar recursos, en este orden, por el FormClosing Select Case Actualizado Case False Select Case MsgBox("Hay datos sin guardar," & vbCrLf & _ "¿desea guardarlos ahora.? ", _ MsgBoxStyle.YesNoCancel, Me.Text) Case MsgBoxResult.Yes Actualizar() Case MsgBoxResult.No Case MsgBoxResult.Cancel Exit Sub End Select End Select Me.Close() Me.Dispose() Me.Finalize() Actualizador.Dispose() EnlaceTabla.Dispose() Adaptador.Dispose() Conexion.Dispose() End Sub De esa forma se puede advertir al usuario de la situación en ese momento y grabar o continuar. Para ello es necesario que en el evento KeyPress figure: Private Sub Campo00_KeyPress( _ ByVal sender As Object, _ ByVal e As System.Windows.Forms.KeyPressEventArgs) _ Handles Campo00.KeyPress, _ Campo01.KeyPress, _ Campo02.KeyPress Actualizado = False End Sub 1.6 Leer las tablas de una base de datos. El ejemplo que sigue muestra como leer las tablas de una base de datos de Access, y dejarlas cargada en un ListBox. En el ListBox queda cargado el nombre de la tabla, con lo que en el momento de utilizarse se puede usar para cargar los campos de la tabla seleccionada. Se utiliza el procedimiento que sigue y la llamada a la función que figura a continuación. El procedimiento recorre con el bucle For each, la tabla obtenida en la función y la visualiza en el ListBox. Public Sub CargarListaTablasBaseDatos( _ ByVal Conexion As System.Data.OleDb.OleDbConnection, _ ByVal Adaptador As System.Data.OleDb.OleDbDataAdapter, _ ByRef Lista As ListBox) ' El paso inicial es obtener la lista de tablas disponibles ' luego se visualiza de modo predeterminado la primera ' tabla de la lista y ordenada por su primera columna Dim Tablas As DataTable Tablas = ObtenerTablasBaseDatos(Conexion) Lista.Items.Clear() For Each Row As DataRow In Tablas.Rows 'la tercera columna tiene el nombre de la tabla Lista.Items.Add(Row(2).ToString) Next End Sub Esta función es la que obtiene las tablas de la base de datos, utilizando el objeto Connection. Public Function ObtenerTablasBaseDatos( _ ByVal Conexion As System.Data.OleDb.OleDbConnection) _ As DataTable Dim EsquemaTabla As DataTable EsquemaTabla = _ Conexion.GetOleDbSchemaTable(System.Data.OleDb.OleDbSchemaGuid.Tables, _ New Object() {Nothing, Nothing, Nothing, "TABLE"}) Return EsquemaTabla End Function 1.7 Leer los campos de una tabla. No tiene ningún misterio, solo se incorpora el contenido de la propiedad Text del ListBox a la cadena SQL que se envía como parámetro al procedimiento de Carga, que es muy similar, igual, al visto anteriormente en éste mismo tema, y se enlaza la tabla al DataGrid, incorporando las posibilidades de edición que se desee, y que en el ejemplo se basa en la configuración del Objeto BindingSource. Private Sub Lista_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles Lista.Click Dim CadenaSql as String = "Select * From " & Lista01.Text ' Esta línea realiza el enlace entre el DataGrid y la tabla ObjDataGrid.DataSource = Me.Enlace CargaDataGrid(Conexion, Adaptador, Enlace, ObjDataGrid,CadenaSQL) Edit.Checked = Enlace.AllowEdit Borrar.Checked = Enlace.AllowRemove Add.Checked = Enlace.AllowNew End Sub Con el contenido de estos dos apartados se puede confeccionar un programa que lea todas las tablas de la base de datos, y permita su edición. Para ello solo hace falta un DataGridView, para el contenido de la tabla seleccionada en cada momento y un ListBox para cargar las tablas de la base de datos. Después ya incorporar los Button y los objetos en función de la sofisticación que deseemos. 1.8 Las tablas provisionales. Muchas veces necesitamos por uno u otro motivo del uso de una tabla provisional de trabajo. El ejemplo que se expone sirve de apoyo a un DataGridView, para la visualización de los datos seleccionados por el usuario, y es capaz de controlar la duplicidad de un dato introducido por error dos veces. 1.8.1 La configuración. La tabla de trabajo la creamos a partir de una Select en la cual podemos o no cargar los datos que nos sean precisos para nuestro programa. CadenaSql = "Select Movimientos.TipTitulo as Tipo, " & _ "Movimientos.Titulo as Codigo, " & _ "Titulos.Titulo, " & _ "Titulos.PreVenta as Precio, " & _ "Movimientos.Cantidad " & _ "From Movimientos " & _ "Inner Join Titulos On " & _ "(Movimientos.TipTitulo = Titulos.TipPub) and “ & _ “(Movimientos.Titulo = Titulos.Codigo) " & _ "Where Movimientos.Numero = '" & Campo01.Text & "';" Esta Select, es la que se utilizará para el llenado, o no, de la tabla. ' Crear un nuevo adaptador de datos Adaptador = New OleDb.OleDbDataAdapter(CadenaSql, Conexion) ' Llenar la tabla con los datos y enlazarza con el 'bindingsource' Adaptador.Fill(Tabla) Para el correcto funcionamiento y evitar el duplicado de datos en el DataGridView es necesario crear una clave principal. ' Se define la restricción CrearRestriccion(Tabla) Cuyo procedimiento es el que sigue: Private Sub CrearRestriccion(ByVal Tabla As System.Data.DataTable) Dim ColPri(1) As DataColumn Dim Restriccion As UniqueConstraint ColPri(0) = Tabla.Columns("Tipo") ColPri(1) = Tabla.Columns("Codigo") Restriccion = New UniqueConstraint("Principal", ColPri, True) Tabla.Constraints.Add(Restriccion) End Sub Hay que enlazar la tabla y el DataGridView. ' Se enlaza el objeto ObjDataGrid.DataSource = Tabla ' EnlaceTabla ' EnlaceTabla.DataSource = Tabla De las dos formas funciona, pero es más lógico ObjDataGrid.DataSource = Tabla Queda configurar el DataGrid y asignar el ancho de columnas y alineación de las columnas.. Y el procedimiento queda como sigue: Private Sub CargaDataGrid() Dim CadenaSql As String ' Se deshace de los datos anteriores Tabla = New DataTable CadenaSql = "Select Movimientos.TipTitulo as Tipo, " & _ "Movimientos.Titulo as Codigo, " & _ "Titulos.Titulo, " & _ "Titulos.PreVenta as Precio, " & _ "Movimientos.Cantidad " & _ "From Movimientos " & _ "Inner Join Titulos On " & _ "(Movimientos.TipTitulo = Titulos.TipPub) and “ & _ “(Movimientos.Titulo = Titulos.Codigo) " & _ "Where Movimientos.Numero = '" & Campo01.Text & "';" Try ' Crear un nuevo adaptador de datos Adaptador = New OleDb.OleDbDataAdapter(CadenaSql, Conexion) ' Llenar la tabla con los datos y enlazarla con el 'bindingsource' Adaptador.Fill(Tabla) ' Se define la restricción CrearRestriccion(Tabla) ' Se enlaza el objeto ObjDataGrid.DataSource = Tabla ' EnlaceTabla ' EnlaceTabla.DataSource = Tabla ConfigDataGrid(Me, ObjDataGrid, False) Catch ex As OleDb.OleDbException MsgBox(ex.Message, MsgBoxStyle.Information) End Try End Sub 1.8.2 Llenado de la tabla. Finalizada la parte de configuración ahora queda llenar la tabla. Para ello se genera un objeto DataRow, el cual al utilizar el método NewRow, queda cargado con la estructura de la tabla provisional, que sale de la Select, y solo queda cargarlo con datos. Registro.Item("Tipo") = Titulo.Tipo.ToString Para luego añadirlo a la tabla. Tabla.Rows.Add(Registro) Y si hubiera duplicidad se captura el error en la instrucción Try Catch. Por lo que el procedimiento queda: Private Sub AnyadirTitulos() Dim Registro As DataRow = Tabla.NewRow Dim Titulo As ItemLista = CType(Lista.SelectedItem, ItemLista) Dim RegTit As System.Data.DataRow Registro.Item("Tipo") = Titulo.Tipo.ToString Registro.Item("Codigo") = Titulo.Codigo.ToString Registro.Item("Titulo") = Titulo.ToString Registro.Item("Cantidad") = "0" Registro.Item("Precio") = Titulo.Precio Try Tabla.Rows.Add(Registro) Catch ex As ConstraintException MsgBox("Título existente", MsgBoxStyle.Critical, NomProgram) End Try End Sub 1.8.3 Lectura de la tabla. Como el objeto Datagrid es un reflejo del contenido de la tabla, podemos tratar cualquiera de los dos objetos mediante un bucle por el sistema que deseemos, con un contador y usando Count, While X < ObjDataGrid.RowCount And Not Error Fila = ObjDataGrid.Rows.Item(X) While X < ObjDataSet.Tables("TablaGrid").Rows.Count And Not Error Registro = ObjDataSet.Tables("TablaGrid").Rows(X) ActualizarStock(Registro) GrabarMovim(Conexion, _ Comando, _ Fec, _ Clientes.Codigo.ToString, _ Clientes.Codig2.ToString, _ Fila.Cells.Item("Tipo").FormattedValue.ToString, _ Fila.Cells.Item("Codigo").FormattedValue.ToString, _ Fila.Cells.Item("Cantidad").FormattedValue.ToString, _ Format(CInt(Campo01.Text), "0000"), _ Error) o con un objeto del tipo DataRow, o DataGridViewRow y el bucle del tipo For Each in “Objeto”.