La diferencia entre programar sitios web y desarrollar aplicaciones profesionales para Internet.
lunes, julio 31, 2006
Export SQL data in Insert statements
Muchas veces, estas tablas, sólo tienen un par de registros. Crear un archivo CSV o crear un DTS resulta ser bastante engorroso.
Para estos casos es que hice esta simple página en ASP.NET para exportar los datos de una base MS Sql Server a sentencias del tipo insert.
Solución
Primero agregamos un campo para ingresar el Connection String correspondiente a la base de nuestro desarrollo. De esta forma traemos información de las tablas existentes (no de sistema).
Para no tener que ejecutar una sentencia COUNT(*) por cada tabla, consultamos a la tabla de sistema dbo.sysindexes para obtener los últimos datos estadísticos de cada tabla. Es importante entender que de esta forma obtendremos información sólo de las tablas que contengan algún indice o Primary Key y que esta información puede estar desactualizada. Aún así, tendremos una idea de cuantos registros tiene una tabla.
Debido a que el objetivo de esta página es crear un archivo de texto con las "sentencias insert" para ser ejecutado en un Query Analyzer, debemos poner un límite de cantidad de registros. Para este fin es que existe la variable MaxRows2Export. Arbitrariamente puse esta variable en 100.
Una vez seleccionadas las tablas que queremos exportar, se genera un archivo de texto con las sentencias insert adecuadas. Para que este script funcione debemos asegurarnos de que podemos insertar Identities. Por esta razón, cada bloque se inicia con un:
SET IDENTITY_INSERT table ON
y finaliza con un
SET IDENTITY_INSERT table OFF
El siguiente es un ejemplo de archivo de texto que genera esta aplicación considerando que seleccionamos la tabla "Status" y "Genders":
-- Status
SET IDENTITY_INSERT Status ON
GO
INSERT INTO Status(ID,Name) VALUES (1,'Pending')
INSERT INTO Status(ID,Name) VALUES (2,'Approved')
INSERT INTO Status(ID,Name) VALUES (3,'Disapproved')
GO
SET IDENTITY_INSERT Status OFF
GO
-- Genders
SET IDENTITY_INSERT Genders ON
GO
INSERT INTO Genders(GenderId,Gender) VALUES (1,'Male')
INSERT INTO Genders(GenderId,Gender) VALUES (2,'Female')
GO
SET IDENTITY_INSERT Genders OFF
GO
Espero que les sea útil. En el siguiente link encontrarán un ZIP que contiene sólo una página ASPX con esta aplicación.
sql2insert.zip
lunes, mayo 29, 2006
Exportar Microsoft Project día por día
Este artículo explica cómo exportar las horas asignadas a cada recurso en Microsoft Project día por día a Microsoft Excel en un formato que permita crear una tabla pívot.
Realicé las siguientes búsquedas en Internet sin resultado:
- Exportar MS Project día por día
- Export MS Project in daily basis
- Microsoft Project export timescaled
Finalmente me enteré que MS Project no incluye esta función y, en caso de necesitarla, había que programarla en VBA (Visual Basic for Applications).
Problema Cuando exportamos los datos a otro formato, Microsoft Project crea un registro por cada tarea asignada a cada recurso y esta tarea incluye los datos de fecha de inicio de la tarea, fecha de finalización y cantidad de horas que la tarea requiere. Esto significa que, si una tarea comienza el día 1 y requiere 60 horas, sólo obtendremos un registro con esos datos. Cuando hacemos la tabla pívot, crearemos un reporte en el que dice que el recurso trabajará 60 horas el día 1 y ninguna los días siguientes.
Lo que necesitamos exportar son los datos que representan la vista de “Resource Usage”
En mi caso particular debía exportar los datos desde un archivo Project que funciona como “Resource Pool”. La particularidad de este archivo es que no contiene tareas, sólo recursos y asignaciones.
Solución
Primero debemos abrir el entorno de VBA para crear la nueva macro con la combinación de teclas ALT + F11.
Armamos la interfaz de usuario con fecha inicial, fecha final, tipo de datos a exportar, dos barras de progreso (una para recursos y otro para asignaciones) y un botón para exportar.
Para poder agregar los controles “Date Time Picker” primero debemos ir al menú Tools -> References y agregar “Microsoft Windows Common Controls 2 (MSCOMCT2.OCX)”. Si no tenemos esta librería, la pueden obtener desde http://activex.microsoft.com/controls/vb6/mscomct2.cab
Luego vamos al menú Tools -> Additional Controls y seleccionar “Microsoft Date and Time Picker Control 6.0” para agregarlo al Toolbox.
- Start Date: sDate
- End Date: eDate
- Type of data: ddAssignType
- Label resources: lblResources
- Resources progress bar: pBarResources
- Label assignments: lblAssign
- Assignment progress bar: pBarAssignments
- Export button: btnExport
Para poder poblar la lista de tipos de datos y para crear un botón con nuestra macro en una barra de herramientas de Project, creamos un nuevo módulo (yo le puse por nombre Export_Timescaled). Este módulo, además de poblar los tipos de datos, invoca al formulario.
El siguiente es el código del módulo Export_Timescaled: Sub Export_Timescaled() frmExportTimescaled.ddAssignType.Clear frmExportTimescaled.ddAssignType.AddItem ("Work") frmExportTimescaled.ddAssignType.AddItem ("Actual Work") frmExportTimescaled.ddAssignType.AddItem ("Cumularive Work") frmExportTimescaled.ddAssignType.AddItem ("Overallocation") frmExportTimescaled.ddAssignType.AddItem ("Cost") frmExportTimescaled.Show End SubEl siguiente es el código del formulario frmExportTimescaled: Option Explicit Dim xlApp As Excel.Application
Private Sub btnExport_Click() Dim t As Task Dim Asgn As Assignment Dim a As Integer Dim b As Integer Dim currRow As Integer Dim i As Double Dim c As Range Dim TSV As TimeScaleValues Dim AssignType As PjAssignmentTimescaledDataCall ControlsEnable(False) 'Disable all form controls Call CreateExcelFile Application.ActiveWindow.Refresh 'This makes form get focus
Set c = xlApp.ActiveCell currRow = 0 'Set column titles c.Offset(currRow, 0) = "Task Name" c.Offset(currRow, 1) = "Employee" c.Offset(currRow, 2) = "Date" c.Offset(currRow, 3) = "Hours" c.Offset(currRow, 4) = "Week" Call InitProgressBarResources(ActiveProject.Resources.Count)
'Determine Assignment Type to export Select Case ddAssignType.Value Case "Actual Work": AssignType = PjAssignmentTimescaledData.pjAssignmentTimescaledActualWork Case "Cumularive Work": AssignType = PjAssignmentTimescaledData.pjAssignmentTimescaledCumulativeWork Case "Overallocation": AssignType = PjAssignmentTimescaledData.pjAssignmentTimescaledOverallocation Case "Cost": AssignType = PjAssignmentTimescaledData.pjAssignmentTimescaledCost Case Else AssignType = PjAssignmentTimescaledData.pjAssignmentTimescaledWork End Select For a = 1 To ActiveProject.Resources.Count pBarResources.Value = a 'Update progress bar lblResources.Caption = a & "/" & pBarResources.Max 'Update label If Not ActiveProject.Resources.Item(a) Is Nothing And ActiveProject.Resources.Item(a).Assignments.Count > 0 Then Call InitProgressBarAssing(ActiveProject.Resources.Item(a).Assignments.Count) 'Initialize second progress bar For b = 1 To ActiveProject.Resources.Item(a).Assignments.Count If Not ActiveProject.Resources.Item(a).Assignments.Item(b) Is Nothing Then Set TSV = ActiveProject.Resources.Item(a).Assignments(b).TimeScaleData(sDate.Value, eDate.Value + 1, AssignType, pjTimescaleDays) pBarAssignments.Value = b 'Update second progress bar lblAssign.Caption = b & "/" & pBarAssignments.Max 'Update second label For i = 1 To TSV.Count DoEvents If IsNumeric(TSV.Item(i).Value) Then 'Check for data currRow = currRow + 1 c.Offset(currRow, 0) = ActiveProject.Resources.Item(a).Assignments.Item(b).TaskName c.Offset(currRow, 2) = ActiveProject.Resources.Item(a).Name c.Offset(currRow, 3) = TSV.Item(i).StartDate c.Offset(currRow, 4) = Round(TSV.Item(i).Value / 60, 2) c.Offset(currRow, 5) = DateAdd("d", 5 - (Weekday(TSV.Item(i).StartDate, vbSunday)), TSV.Item(i).StartDate) End If Next End If Next End If Next
MsgBox ("Exportación completa") ControlsEnable True End Sub
Sub ControlsEnable(status As Boolean) btnExport.Enabled = status sDate.Enabled = status eDate.Enabled = status ddAssignType.Enabled = status End SubSub InitProgressBarResources(cant As Integer) pBarResources.Min = 0 pBarResources.Max = cant pBarResources.Value = 0 End Sub
Sub InitProgressBarAssing(cant As Integer) pBarAssignments.Min = 0 pBarAssignments.Max = cant pBarAssignments.Value = 0 End SubSub CreateExcelFile() Dim xlBook As Excel.Workbook Dim xlSheet As Excel.Worksheet Dim a As Integer
'Create Excel Object Set xlApp = New Excel.Application xlApp.Visible = True AppActivate "Microsoft Excel"'Create Workbooks and Output Sheet Set xlBook = xlApp.Workbooks.Add Set xlSheet = xlBook.Worksheets.Add 'Create Sheet name. Must be less than 31 characters xlSheet.Name = Mid(Mid(ActiveProject.Name, 1, Len(ActiveProject.Name) - 4) & " (" & ddAssignType.Value & ")", 1, 31) 'Delete extra sheets For a = 1 To 3 xlBook.Worksheets(2).Delete Next End Sub
Finalmente, para poder compartir la macro con otras personas, la copié a un archivo de Project vacío utilizando el menú Tools -> Organizer, seleccionando “Modules”. De esta forma, abrimos el archivo en otra computadora y copiamos la macro hacia el Global.MPT y ya queda disponible en nuestro MS Project.Descargar archivo MS Project con la macro
IMPORTANTE Si al correr la macro aparece el error "User-defined type not defined"
significa que falta la Referencia al objeto Excel dentro de la macro. Para solucionar esto vamos a la pantalla de VBA (ALT+F11), seleccionamos Tools --> References
Y seleccionamos "Microsoft Excel 11.0 Object Library" o similar. Les muestro mis referencias:
Guardamos y ejecutamos la macro nuevamente.
martes, mayo 31, 2005
Obteniendo valores por QueryString
El problema que intentamos resolver es el siguiente. Nuestra aplicación presenta un listado en varias páginas y obtiene el número de página que debe mostrar a través del parámetro “pagina” pasado por QueryString. En un punto del código, necesito obtener un valor entero y en caso de no conseguirlo, el valor debe ser “1”, es decir, la primera página.
using System;
using System.Web;
namespace DWP.Web
{
/// Definimos la clase como sellada
/// para impedir la herencia
public sealed class Requester
{
/// Constructor privado para impedir
/// la creación de instancias.
private Requester(){}
/// Método estático para obtener
/// parámetros por QueryString
public static object FromQueryString(string key, object defaulValue)
{
/// Obtenemos el objeto Request actual
HttpRequest Request = HttpContext.Current.Request;
object o;
string val = "";
/// Verificamos si el valor existe
/// en el QueryString. Si no, devolvemos
/// el valor por default
if (Request.QueryString[key]!=null && Request.QueryString[key].ToString()!="") {
val = Request.QueryString[key].ToString();
} else {
return defaulValue;
}
/// Intentamos convertir el valor obtenido
/// al tipo de dato del valor default.
/// Si esto da error, devolvemos el
/// valor default.
try {
o = Convert.ChangeType(val, defaulValue.GetType());
} catch {
o = defaulValue;
}
return o;
}
}
}
Como podemos ver, el método “FromQueryString” tiene como tipo de retorno un object. Necesitamos que sea así porque vamos a devolver un objeto de tipo similar al valor por default. De esta forma, si estamos esperando un entero y por QueryString nos llegó un texto, el método devuelve el valor predeterminado, es decir, garantiza el tipo de dato devuelto.
Las siguientes son formas válidas de utilizar nuestra función:
///Esperamos un entero, si no, devuelve el valor 1.
int currPage = (int)Requester.FromQueryString("pagina",1);
///Esperamos una fecha, si no, 1 de enero de 2005.
DateTime currDate = (DateTime)Requester.FromQueryString("fecha", new DateTime(2005,01,01));
Les dejo a ustedes la tarea de crear los métodos que obtienen valores por Form y Cookies.
lunes, marzo 21, 2005
Zonas de riesgo
Si analizamos una aplicación de Internet basada en formularios que almacena la información ingresada por los usuarios en una base de datos, entonces, rápidamente identificamos dos zonas de riesgo:
La captura de datos
Recordemos que la información que proviene de los usuarios (esto incluye formularios, querystrings y cookies) no es confiable, puede venir con un formato erróneo o puede no llegar en absoluto. Debemos verificar los datos que recibimos y no confiar en que siempre llegarán correctamente.
Un caso muy común es cuando enviamos, por querystring, el número de página que debemos mostrar. Entonces redirigimos a los usuarios a URLs parecidas a esta:
listado.aspx?pagina=3
Luego en nuestro código escribimos algo así:
int pagina = int.Parse(Request.QueryString["pagina"].ToString());
Los desafío a que busquen páginas de este tipo en Internet y vean lo que sucede si modifican el valor de "pagina" a "abc".
Para que no sucedan estas cosas (las páginas de error no son muy agradables) deberíamos preguntar: "si vino el dato que estoy esperando, y si el dato no es vacío, y si puedo crear un entero, entonces tomar el dato, si no, debo asignar un valor predeterminado". En nuestro ejemplo, ante cualquier problema, pagina valdría "1".
Es cierto que, si por cada dato que queremos obtener debemos escribir tanto código, nuestro programa sería muy extenso, pero también es cierto que, si creamos un objeto que se encargue de obtener estos valores, habremos resuelto el problema para los próximos desarrollos que hagamos.
El almacenamiento de datos
Ya hemos obtenido la información y ahora necesitamos guardarla en una base de datos. Aquí también podemos encontrarnos con errores inesperados ya que existen factores que no podemos controlar. Pueden aparecer errores por clave principal repetida, valores incorrectos, conexiones agotadas, comunicación con la base interrumpida, etc. Otra vez, las páginas de error no son muy amigables. Tenemos que estar preparados y saber que estas cosas, y otras tantas, pueden suceder.
Existen otras zonas de riesgo dependiendo del tipo de aplicación que estemos desarrollando. Debemos identificarlas y tomar los recaudos necesarios para que nuestros desarrollos sean cada vez más robustos.
martes, marzo 15, 2005
Caching
ASP.NET nos brinda dos opciones para aplicar cache en nuestros sitios:
Output cache: con este mecanismo se mantiene una copia de la respuesta que produce una página, de modo tal que, mientras dura el cache, el código de la página se habrá ejecutado una sola vez.
Objeto cache: a través del cual podemos almacenar datos para no tener que ir a buscarlos cada vez al repositorio original
Todo muy útil, pero no debemos olvidar que el cache es una solución para cuando nuestro sitio tiene un nivel de uso que se hace necesario "proteger" al servidor web y al repositorio de datos del uso intensivo que generan los usuarios que ingresan al sitio. No tiene sentido utilizar output cache si la página es solicitada una vez cada 5 minutos y la información que muestra la página es válida por más o menos por el mismo tiempo.
Pero lo más importante es que no debemos diseñar una aplicación en función de que vamos a usar cache. En otras palabras, no debemos hacer aplicaciones que no funcionen sin cache siendo que tienen un uso moderado.
Por último, todos nuestros desarrollos que usan cache deberían poder activarlo y desactivarlo fácilmente. Salvo alguna excepción en particular, nuestros desarrollos no deben ser dependientes del caching, deben poder usarlo cuando lo necesitan.