jueves, junio 19, 2008

Tecnologías de Presentación de Microsoft

Me parece muy interesante la comparativa de Tecnologías de Presentación de MS realizada por José Murillo en su blog

martes, junio 17, 2008

XPS (o el PDF de Microsoft)

Microsoft ha inventado su propio formato de documento-papel (rivalizando con pdf?) llamado XPS (XML Paper Specification).
Los documentos en este formato deben tener extensión .xps aunque internamente es un .zip que contiene el texto en .xml + imágenes y demás (similar a docx, extensión de los documentos de Word 2007).

Ya veremos si tiene éxito...

Para más info ver www.microsoft.com/xps

jueves, abril 03, 2008

Mantenimiento con "LinQ to SQL"/"Entity Data Model (EDM)"

En Visual Studio 2008 aparece LinQ, y en el Framework 3.5, EDM (Entity Data Model).

A continuación explico brevemente cómo hacer un mantenimiento básico con esto (sólo el modelado de entidades, no la capa de presentación):

1) Abrir un proyecto de Visual Studio 2008 de WinForms (p.e.).
2) Añadir un nuevo item de tipo "LinQ to SQL Classes". Se abre un "designer" para modelar la entidad.
3) Arrastrar desde el Server Explorer las tablas que formen parte de la entidad (p.e. Orders y OrderDetails). Esto crea un "DataContext".

4) Desde el interface se utiliza el "DataContext" para obtener la lista completa o cada uno de los elementos de la entidad. Por ejemplo, con "Orders":

MiDataContext dc = new MiDataContext();

//Obtener una "order" (Acceso a la BDD)
Order order1 = dc.Orders.First();

//Modificar la "order"
order1.ShipCountry = "Spain";

//Insertar nueva "order"
Order order2 = new Order();
order2.ShipCity = "Barcelona";
order2.InsertOnSubmit();

//Eliminar una "order"
dc.Orders.DeleteOnSubmit(dc.Orders.Where(/*...*/));

//Persistir cambios (en la BDD)
dc.SubmitChanges();

miércoles, abril 02, 2008

Impresión matricial en C


// A continuación se explican 2 maneras de generar una impresión matricial.
// 1) En RawDataFileToPrinter() se utiliza la estructura AddJob/ScheduleJob.
// Utilizando GetJob() se puede obtener el nombre del fichero de impresión
// por si tengo que hacer cosas con él (p.e. en entorno ASP).
// 2) En RawDataFileToPrinter2() se utiliza la estructura
// StartDocPrinter/WritePrinter, porque con 1) no se puede acceder a
// impresoras de red desde S.O. basados en W2000.
//
// NOTA: Para impresión no matricial de un WMF se utiliza PlayMetafile(hPrinter,WMF...)


//Jaac 23/07/2002
// Copiada de MSDN!!!
// RawDataToPrinter - sends binary data directly to a printer
//
// szPrinterName: NULL-terminated string specifying printer name
// lpData: Pointer to raw data bytes
// dwCount Length of lpData in bytes
//
// Returns: TRUE for success, FALSE for failure.
//
BOOL RawDataToPrinter(LPSTR szPrinterName, LPBYTE lpData, DWORD dwCount)
{
HANDLE hPrinter;
DOC_INFO_1 DocInfo;
DWORD dwJob;
DWORD dwBytesWritten;

// Need a handle to the printer.
if( ! OpenPrinter( szPrinterName, &hPrinter, NULL ) )
return FALSE;

// Fill in the structure with info about this "document."
DocInfo.pDocName = "My Document";
DocInfo.pOutputFile = NULL;
DocInfo.pDatatype = "RAW";
// Inform the spooler the document is beginning.
if( (dwJob = StartDocPrinter( hPrinter, 1, (LPSTR)&DocInfo )) == 0 )
{
ClosePrinter( hPrinter );
return FALSE;
}
// Start a page.
if( ! StartPagePrinter( hPrinter ) )
{
EndDocPrinter( hPrinter );
ClosePrinter( hPrinter );
return FALSE;
}
// Send the data to the printer.
if( !WritePrinter( hPrinter, lpData, dwCount, &dwBytesWritten ) )
{
EndPagePrinter( hPrinter );
EndDocPrinter( hPrinter );
ClosePrinter( hPrinter );
return FALSE;
}
// End the page.
if( ! EndPagePrinter( hPrinter ) )
{
EndDocPrinter( hPrinter );
ClosePrinter( hPrinter );
return FALSE;
}
// Inform the spooler that the document is ending.
if( ! EndDocPrinter( hPrinter ) )
{
ClosePrinter( hPrinter );
return FALSE;
}
// Tidy up the printer handle.
ClosePrinter( hPrinter );
// Check to see if correct number of bytes were written.
if( dwBytesWritten != dwCount )
return FALSE;
return TRUE;
}

//Jaac 23/07/2002
// Adaptación de RawDataToPrinter de MSDN
// RawDataFileToPrinter - sends binary data directly to a printer
//
// szPrinterName: NULL-terminated string specifying printer name
// szFileName: Pointer to file containing raw data bytes
//
// Returns: TRUE for success, FALSE for failure.
//
BOOL RawDataFileToPrinter(LPSTR szPrinterName, LPSTR szDataFileName, LPSTR szDocTitle)
{
HANDLE hPrinter;
HANDLE hDataFile;
DOC_INFO_1 DocInfo;
DWORD dwJob;
DWORD dwBytesWritten;
DWORD dwBytesRead;
char lpData[0x1000];

// Need a handle to the printer.
if( !OpenPrinter( szPrinterName, &hPrinter, NULL ) )
return FALSE;

// Fill in the structure with info about this "document."
DocInfo.pDocName = szDocTitle;
DocInfo.pOutputFile = NULL;
DocInfo.pDatatype = "RAW";

// Inform the spooler the document is beginning.
if( (dwJob = StartDocPrinter( hPrinter, 1, (LPSTR)&DocInfo )) == 0 )
{
ClosePrinter( hPrinter );
return FALSE;
}

/*// Start a page.
if( !StartPagePrinter( hPrinter ) )
{
EndDocPrinter( hPrinter );
ClosePrinter( hPrinter );
return FALSE;
}*/

// Open raw data file
if( (hDataFile = CreateFile( szDataFileName,
GENERIC_READ, 0, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL )) == INVALID_HANDLE_VALUE )
{
EndDocPrinter( hPrinter );
ClosePrinter( hPrinter );
return FALSE;
}

// Read-Write data
for(;;)
{
dwBytesRead = 0;

// Read data from file
if( !ReadFile( hDataFile, lpData, sizeof(lpData), &dwBytesRead, NULL ) )
{
//EndPagePrinter( hPrinter );
EndDocPrinter( hPrinter );
ClosePrinter( hPrinter );
CloseHandle( hDataFile );
return FALSE;
}

// Comprobar si se ha alcanzado el final del DataFile.
if(dwBytesRead == 0)
break;

// Send the data to the printer.
if( !WritePrinter( hPrinter, lpData, dwBytesRead, &dwBytesWritten ) )
{
//EndPagePrinter( hPrinter );
EndDocPrinter( hPrinter );
ClosePrinter( hPrinter );
CloseHandle( hDataFile );
return FALSE;
}
}

// Close DataFile handle
if( !CloseHandle( hDataFile ) )
{
//EndPagePrinter( hPrinter );
EndDocPrinter( hPrinter );
ClosePrinter( hPrinter );
return FALSE;
}

/*// End the page.
if( !EndPagePrinter( hPrinter ) )
{
EndDocPrinter( hPrinter );
ClosePrinter( hPrinter );
return FALSE;
}*/

// Inform the spooler that the document is ending.
if( !EndDocPrinter( hPrinter ) )
{
ClosePrinter( hPrinter );
return FALSE;
}

// Tidy up the printer handle.
if( !ClosePrinter( hPrinter ) )
{
return FALSE;
}

// ok
return TRUE;
}

//Jaac 23/07/2002
// Adaptación de RawDataToPrinter de MSDN
// RawDataFileToPrinter - sends binary data directly to a printer
//
// szPrinterName: NULL-terminated string specifying printer name
// szFileName: Pointer to file containing raw data bytes
//
// Returns: TRUE for success, FALSE for failure.
//
BOOL RawDataFileToPrinter2(LPSTR szPrinterName, LPSTR szDataFileName, LPSTR szDocTitle)
{
HANDLE hPrinter;
ADDJOB_INFO_1 info;
char path[1000];
DWORD Nece;
HANDLE hFileJob;
HANDLE hFileSpool;
DWORD leidos;
DWORD grabados;
PRINTER_DEFAULTS PrinterDefs;

info.Path = (LPTSTR) &path[0];

PrinterDefs.DesiredAccess = PRINTER_ALL_ACCESS; // | PRINTER_ACCESS_ADMINISTER;
PrinterDefs.pDatatype = NULL;
PrinterDefs.pDevMode = NULL;
if (!OpenPrinter(PrinterName, &hPrinter, &PrinterDefs/*NULL*/))
return FALSE;

if(AddJob(hPrinter,(DWORD)1,(LPBYTE)&path[0],(DWORD)sizeof(path),(LPDWORD)&Nece))
{
info = *((ADDJOB_INFO_1 *)&path[0]);

{//Jaac 11/07/2002: Para ASP...
DWORD dwNeeded, dwReturned;
int i;
JOB_INFO_2 *pJobInfo;
//char sAutor[] = "Jaac";
char sMascara[_MAX_FNAME];
char sMascaraExt[_MAX_EXT];
char sPageCount[5];
//char sCopies[2];

GetJob(hPrinter, info.JobId, 2, NULL, 0, &dwNeeded);
pJobInfo = malloc( dwNeeded );
GetJob(hPrinter, info.JobId, 2, pJobInfo, dwNeeded, &dwReturned);

if (strlen(g_sTitleByProgram) > 0)
{
pJobInfo->pDocument=malloc(strlen(g_sTitleByProgram)+1);
strcpy(pJobInfo->pDocument, g_sTitleByProgram);
}
else
{
pJobInfo->pDocument = malloc(strlen(FormHdr.name)+1);
strcpy(pJobInfo->pDocument, FormHdr.name);
}

_splitpath( FrFileName, NULL, NULL, sMascara, sMascaraExt );
strcat(sMascara, sMascaraExt);
sprintf(sPageCount, "%d", PageCount);
//sprintf(sCopies, "%d", (OpcionInforme.NroCopias>1) ? OpcionInforme.NroCopias : 1);

pJobInfo->pDocument = realloc(pJobInfo->pDocument,
strlen(FormHdr.name)+1+
strlen(info.Path)+1+
strlen(sMascara)+1+
strlen(sPageCount)+1);
//strlen(sCopies)+1);

strcat(pJobInfo->pDocument, "|");
strcat(pJobInfo->pDocument, info.Path);
strcat(pJobInfo->pDocument, "|");
strcat(pJobInfo->pDocument, sMascara);
strcat(pJobInfo->pDocument, "|");
strcat(pJobInfo->pDocument, sPageCount);
//strcat(pJobInfo->pDocument, "|");
//strcat(pJobInfo->pDocument, sCopies);

//pJobInfo->pNotifyName = &sAutor[0];
SetJob(hPrinter, info.JobId, 2, pJobInfo, (DWORD)(0));

free(pJobInfo);
}

hFileSpool = CreateFile(info.Path,
GENERIC_WRITE,0,NULL,OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,NULL);
if(hFileSpool != INVALID_HANDLE_VALUE)
{

hFileJob = CreateFile(FileJob,
GENERIC_READ,0,NULL,OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,NULL);
if(hFileJob != INVALID_HANDLE_VALUE)
{
// bucle leer grabar
for(;;)
{
leidos = grabados = 0;

ReadFile(hFileJob,buffer,sizeof(buffer),&leidos,NULL);
if(leidos == 0) break;
WriteFile(hFileSpool,buffer,leidos,&grabados,NULL);
}

CloseHandle(hFileJob);
}
CloseHandle(hFileSpool);
}
ScheduleJob(hPrinter,info.JobId);
}
else
{
// el AddJob ha fallado ( debe ser un NT compartiendo impresora)
PRINTER_INFO_2 APrinter[100];
DWORD Needed;
DWORD copiados;

char remoto[512];
remoto[0] = '\0';
if(!EnumPrinters(
PRINTER_ENUM_CONNECTIONS,
PrinterName, // name of printer object
2, // specifies type of printer info structure
(LPBYTE)&APrinter, // pointer to buffer to receive printer info structures
sizeof(APrinter), // size, in bytes, of array
&Needed, // pointer to variable with no. of bytes copied (or required)
&copiados // pointer to variable with no. of printer info. structures copied
))
{
return FALSE;
}
else
{
strcpy(remoto,APrinter[0].pServerName);
strcat(remoto,"\\");
strcat(remoto,APrinter[0].pShareName);
}
hFileSpool = CreateFile(remoto,
GENERIC_WRITE,0,NULL,OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,NULL);
if(hFileSpool != INVALID_HANDLE_VALUE)
{
hFileJob = CreateFile(FileJob,
GENERIC_READ,0,NULL,OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,NULL);

if(hFileJob != INVALID_HANDLE_VALUE)
{
// bucle leer grabar
for(;;)
{
leidos = grabados = 0;

ReadFile(hFileJob,buffer,sizeof(buffer),&leidos,NULL);
if(leidos == 0) break;
WriteFile(hFileSpool,buffer,leidos,&grabados,NULL);
}

CloseHandle(hFileJob);
}
CloseHandle(hFileSpool);
}
}
ClosePrinter(hPrinter);
}

martes, febrero 05, 2008

Mantenimientos Master-Details con Datasets


Supuestos:





  • BDD: Northwind.

  • Tabla master: Orders. PK: OrderID, identidad autoincremental.

  • Tabla details: Orders. PK: OrderID, ProductID. FK: OrderID (relación con Orders.OrderID, enforced for INSERTs and UPDATEs pero sin Cascade Update ni Cascade Delete).


  • Proyecto WinForms en VS2005 (.Net Framework 2.0)

  • Creación de dataset (DatasetNorthwind) creado por diseño que incluye las tablas Orders, OrderDetails y tal vez otras, y sus relaciones.

  • Pantalla de 2 grids editables (nuevo, editar, borrar), donde el primero tiene un BindingSource = DatasetNorthwind.Orders, y el segundo el FK_xxx del anterior (seleccionable por diseño).








Si se ha hecho correctamente, al navegar por el primer grid se irá actualizando el segundo.





A tener en cuenta:





Si modificamos directamente en los grids (editando, añadiendo y borrando, tanto pedidos enteros como líneas), el dataset asociado (DatasetNorthwind) irá guardando las modificaciones.





En el momento de persistir dichas modificaciones a la base de datos (xxxTableAdapter.Update), se deben tener en cuenta las relaciones:






  1. El eliminar un pedido del grid, no elimina automáticamente sus líneas, y si quedan líneas "huérfanas" en el dataset, al hacer el Update se producirá un error.

  2. Al añadir un pedido, sus líneas toman el OrderID de la cabecera, pero este valor no es definitivo hasta que se hace el Update, pero este valor definitivo no se propaga por defecto a las líneas. (Algo equivalente ocurriría si se pudiera modificar el OrderID en las cabeceras, pero este caso no lo trataré).




Carga de pantalla (Form_Load)





this.ordersTableAdapter.Fill(this.dataSetNorthwind.Orders);
this.order_DetailsTableAdapter.Fill(this.dataSetNorthwind.Order_Details);





Implementar altas/bajas/editar (1er método)



Para solucionar el punto 1 a tener en cuenta, implementaremos el evento Orders_OrdersRowDeleting():

void Orders_OrdersRowDeleting(object sender, DataSetNorthwind.OrdersRowChangeEvent e) {
if (MessageBox.Show(
"Se eliminará el pedido " + e.Row.OrderID.ToString() + ".",
"Eliminar pedido",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Information)
== DialogResult.OK)
{
try {
DataSetNorthwind.Order_DetailsRow[] orderDetails = e.Row.GetOrder_DetailsRows();
foreach (DataSetNorthwind.Order_DetailsRow orderDetail in orderDetails)
{
orderDetail.Delete();
}
}
catch (Exception ex)
{
throw new Exception("No se han podido eliminar los detalles del pedido " + e.Row.OrderID.ToString() + ".", ex);
}
}


El manejador de este evento se debe insertar a mano en el Form_Load, tras el InitializeComponent( ):

this.dataSetNorthwind.Orders.OrdersRowDeleting += new DataSetNorthwind.OrdersRowChangeEventHandler(Orders_OrdersRowDeleting);

En el momento de salvar los cambios, se debe tener en cuenta el punto 2 (y el 1):


private void SaveChanges()
{
try
{
//Nuevos pedidos (cabeceras nuevas)? --> En los detalles hay que asignar el OrderID en el momento en que el pedido se da de alta
if (this.dataSetNorthwind.Orders.GetChanges(DataRowState.Added) != null)
{
foreach (DataSetNorthwind.OrdersRow order in this.dataSetNorthwind.Orders)
{
if (order.RowState == DataRowState.Added)
{
DataSetNorthwind.Order_DetailsRow[] orderDetails = order.GetOrder_DetailsRows();
this.ordersTableAdapter.Update(order);
foreach (DataSetNorthwind.Order_DetailsRow orderDetail in orderDetails)
{
orderDetail.OrderID = order.OrderID;
this.order_DetailsTableAdapter.Update(orderDetail);
}
}
}
}
//Resto: Hay que actualizar primero los detalles por si se han eliminado pedidos
//Para que el eliminar funcione correctamente, previamente se ha implementado el evento Orders_OrdersRowDeleting
this.order_DetailsTableAdapter.Update(this.dataSetNorthwind.Order_Details);
this.ordersTableAdapter.Update(this.dataSetNorthwind.Orders);
}
catch (Exception ex)
{
throw new Exception("Error intentando guardar los cambios", ex);
}
}


Implementar altas/bajas/editar (2º método)

En el DatasetNorthwind, editar la relación FK_xxx y forzar Delete Rule = Cascade y Update Rule = Cascade. En el salvar poner:

this.ordersTableAdapter.Update(this.dataSetNorthwind.Orders);
this.order_DetailsTableAdapter.Update(this.dataSetNorthwind.Order_Details);









Implementar ficha con DataBinding





Pasos:

  1. En el diseño de la pantalla, añadir el DatasetNorthwind.
  2. En cada TextBox indicar, en la propiedad (DataBindings)/Text el campo que toque del BindingSource.
  3. Implementar el FillByOrderID, desde el diseño del DatasetNorthwind mostrar el menú contextual sobre la tabla Orders, seleccionar AddQuery y crear el select... where OrderID = @OrderID.
  4. En el Form_Load:
    this.ordersTableAdapter.FillByOrderID(this.dataSetNorthwind.Orders, orderID);
  5. En el evento Guardar hacer:

    /*Para los controles que tengan Data Source Update Mode = Never (DataBindings/Advanced)
    foreach (System.Windows.Forms.Control control in this.Controls)
    {

    if (control.DataBindings["Text"] != null)
    control.DataBindings["Text"].WriteValue();

    }*/
    this.ordersBindingSource.EndEdit(); //Es necesario para que el RowState pase de Unchanged a Modified.
    this.ordersTableAdapter.Update(this.dataSetNorthwind.Orders);
    this.DialogResult = DialogResult.OK;
  6. Y en el Cancelar (o salir sin guardar):
    this.ordersBindingSource.CancelEdit();