Fran Otero

Programación C#, ASP.NET,SqlServer, Mobile y electrónica.
DNI Electrónico Helper

Siempre me ha parecido muy mala la implantación del DNI electrónico: hay muchas páginas que sólo lo soportan en navegadores específicos, otras necesitan lectores específicos y otras directamente no lo soportan aunque aparentemente sí que lo hagan.

En cualquier caso, incluso las que funcionan bien, te exigen escribir la clave unas cuantas veces durante el proceso: para acceder al almacén de certificados, selecionarlo, utilizarlo... Al final si tienes una clave segura resulta muy incómodo utilizarlo.

Pensando en esta comodidad y en poder hacer aplicaciones de integración con bancos automatizadas he desarrollado un pequeño programa que al ser ejecutado detecta todas las interacciones con el lector de DNIs e introduce la contraseña sin necesidad de intervención del usuario. Aunque no os interese la funcionalidad es interesante ver cómo se detecta una ventana y se envían caracteres a otra aplicación.

La presentación es mejorable :-P, of course, pero funciona! La clave se guarda en AppData encriptada. La opción de "seleccionar certificado" hace que tambien se seleccione automáticamente el certificado, útil si sólo tienes uno en tu equipo. El segundo check muestra un aviso en el área de notificación cada vez que se detecta la ventana y espera un segundo antes de actuar automáticamente.

Veamos un poco de código:

                 //Detección de la Ventana de introducción clave DNI electrónico y envío de caractéres
                dniApp = FindWindow(null, "DNI electrónico");
                if (dniApp.ToInt32() > 0)
                {
                    if (chkNotificaciones.Checked)
                    {
                        notify.ShowBalloonTip(500, "DNI Electrónico Helper", "Peticion de clave DNI", ToolTipIcon.None);
                        System.Threading.Thread.Sleep(1000);
                    }
                    SetForegroundWindow(dniApp);
                    SendKeys.SendWait(Crypto.DecryptStringAES(Properties.Settings.Default.Clave, claveEncriptacion));
                    SendKeys.SendWait("{ENTER}");
                }

        //Imports de DLLs
        [DllImport("USER32.DLL", CharSet = CharSet.Unicode)]
        public static extern IntPtr FindWindow(string lpClassName,
            string lpWindowName);
       
        // Activate an application window.
        [DllImport("USER32.DLL")]
        public static extern bool SetForegroundWindow(IntPtr hWnd);
        #endregion

Adjunto el código y el ejecutable para probarlo, espero que os sea útil!


 

Posted: 10/12/2011 13:11 por Fran Otero Otero | con 4 comment(s)
Archivado en: ,,
Asociar tabla a aspnet_users

A pesar de no tener ninguna complicación especial, son muchos los programadores que utilizan enlaces no naturales para vincular tablas con el sistema de usuarios de ASP.NET.  En la jerarquía de tablas instalada, la tabla de usuarios ASPNET_Users tiene como clave primaria la columna UserId y esa será la columna que debamos de utilizar para enlazar con el resto de tablas.

El problema es que esta columna es un GUID, característica que hace que nos pueda parecer poco idónea para utilizarla como clave foránea. Las alternativas que tenemos como claves candidatas en la tabla de usuarios es la combinación de 'LoweredUserName' y 'ApplicationId'. Aún en el caso de que ignoremos 'ApplicationId' por utilizar la base de datos para una única aplicación, el campo LoweredUserName es de tipo varchar(256), mucho mayor que un GUID y desde luego mucho menos adecuado para utilizar como clave externa.

Veamos un ejemplo básico de como se personaliza un control CreateUserWizard para añadir información personalizada en una tabla viculada con la estructura de Membership.

1.- Diseño de la tabla: la clave primaria coincide con el campo UserId de ASPNET_Users. En este sencillo ejemplo vamos a guardar el nombre, apellido y fecha de nacimiento.

2.- Añadimos un paso más al control para solicitar la información adicional:

 3.- Controlamos la navegación en el primer paso del Wizard, que sigue siendo el paso de creación del usuario:

4.- Guardamos la información adicional al finalizar el asistente. Evidentemente podemos utilizar cualquier tecnología de acceso a datos aunque para mayor claridad haya escogido la más básica.

 Fíjate que este código es independiente del proveedor del Membership Provider que utilicemos, ya que la propiedad ProviderUserKey devuelve un objeto con la clave primaria del proveedor. En nuestro caso esta clave primaria es el UserId de tipo Guid, para otros proveedores puede ser cualquier otra cosa.

Change Data Capture. Histórico de cambios en SQL Server 2008 R2

SQL Server posee un mecanismo propio de traza de cambios DML y DDL que permite consultar versiones anteriores de una misma tabla, ver las modificaciones que ha sufrido durante un determinado espacio de tiempo, o un amplio espectro de modificaciones DDL de la propia tabla.

Para ello no es  necesario crear ningún mecanismo externo, ni implementar complejas soluciones basadas en triggers: basta con habilitar CDC: Change Data Capture.

  1. Habilitar CDC en la base de datos
  2. Habilitar CDC para la tabla correspondiente, puede habilitarse para todas las columnas o para columnas en particular
  3. Siempre que queramos utilizamos la consulta cdc.fn_cdc_get_all_changes_nombreTabla para obtener los cambio realizados entre dos LSN determinados.

Los pasos a seguir son muy sencillos, y pueden verse la demostración completa en el código de ejemplo. Se trata de la creación de una tabla de ejemplo con columnas IdProducto, Nombre, Precio, PrecioRebajado, donde despues de habilitar CDC se realizan tres inserciones, dos actualizaciones y un borrado. El resultado de consultar los cambios es una tabla como esta:

Las operaciones se ordenan por orden de ejecución, siendo la primera la más antigua. Podemos observar tres primeros INSERT y un DELETE. En estos casos la información que aparece es la insertada y la borrada de la tabla respectivamente.

En el caso de UPDATE, se insertan dos filas por actualización. La primera de ellas se corresponde con lo que había en la tabla antes del UPDATE y la segunda con lo que hay después. En esta operación también es interesante consultar la columna __$update_mask, que nos indica cuáles han sido las columnas afectadas por la actulización. Por ejemplo, 0x0A=0000 1010 indica que se han modificado la segunda y la cuarta, mientras que 0x02=0000 0010 indica que únicamente se ha visto afectada la segunda columna de la tabla.

Posted: 25/4/2011 21:30 por Fran Otero Otero | con 4 comment(s)
Archivado en:
Autonumérico entre varias tablas

Tenemos dos tablas de datos en SQL Server en las cuales se van a insertar filas indiscriminadamente. Necesitamos saber en qué orden fueron insertadas estas filas, poder sacar un UNION entre ambas tablas y ordenarla por su inserción. Es necesario además detectar 'huecos' en las filas, es decir, queremos crear un autonumérico pero que implique a ambas tablas.

Cómo sabemos, si utlizamos un campo integer la numeración es independiente en cada una de las tablas implicadas, así que no nos sirve. Un timestamp tampoco es la solución, ya que varía con cada versión de la fila: cada update que hagamos sobre la fila actualizará a su vez la columna timestamp.

La otra solución sería utilizar como clave un campo fecha, lo cual nos solucionaría el problema de unir las dos tablas pero no el de la detección de huecos. No habría forma a saber si entre dos filas de la tabla A hay alguna de la tabla B a no ser que hagamos la union directamente.

Desde 2005 tenemos una solución a este problema: newsequentialId(). Esta función sólo se puede utilizar como Default Constraint y nos devuelve un GUID distinto de cada vez, pero de forma secuencial.

En la imagen superior podemos ver el resultado de realizar dos inserciones en la tabla A, una en la B, y a continuación seguir insertando en la A. El byte menos significativo del GUID está representado por los dos primeros caracteres hexadecimales. Cómo se puede ver, son secuenciales E7, E8, falta el E9 y continua con EA, EB... La serie es muy sencilla de predecir: ECD0, EDD0, EED0, EFD0, F0D0, F1D0, ..., FFD0, 00D1, 01D1 ,...

Objetivo cumplido: orden, unicidad y densidad! El código de prueba para generar las tablas de ejemplo es el siguiente:

create table #TablaA
(
  
IdNum int identity(1,1),
  
IdGuid uniqueidentifier default(NEWSEQUENTIALID()),
  
valor varchar(50)
);

create table #TablaB
(
   IdNum int identity(1,1),
  
IdGuid uniqueidentifier default(NEWSEQUENTIALID()),
  
valor varchar(50)
);

insert into #TablaA(valor)values ('a')
insert into #TablaA(valor)values ('a')
insert into #TablaB(valor)values ('a')
insert into #TablaA(valor)values ('a')
insert into #TablaA(valor)values ('a')

select * from #TablaA
select * from #TablaB
Posted: 31/3/2011 21:00 por Fran Otero Otero | con 3 comment(s)
Archivado en:
Examen de actualización a MCPD 40 Web Developer

Ya está disponible la información del examen de actualización 70-523: Updgrade transition your MCPD.NET Framework 3.5 Web Developer Skills to MCPD Framework 4 Web Developer.

Aunque la certificación se pudo obtener desde hace ya unos meses gracias a los exámenes beta, y desde el 22 de octubre está disponible en Prometric, no existía hasta hoy ninguna página (pública) donde se detallase el contenido de dicho examen.

Ahora falta saber cómo está de acertado en nivel, porque si es de un nivel parecido al de certificación MCPD en 4.0 directo sale más rentable empezar certificándose en 3.5 y migrar con este único examen a 4.0. La ruta directa para ser MCPD 4.0 consta de tres exámenes MCTS y un último para ser MCPD. En cambio la ruta alternativa consta de dos exámentes MCTS, uno para ser MCPD en 3.5 y un último de transición.

Son cuatro exámenes igual, pero de la forma alternativa tienes las certificaciones de 2008 y las de 2010, además de demostrar que eres capaz de aprobar el 70-536 y por tanto conoces los fundamentos de la plataforma.

Cada uno que saque sus conclusiones!

Posted: 3/11/2010 15:40 por Fran Otero Otero | con no comments
Archivado en: ,
Conversiones implícitas en SQL Server

Una de las novedades que Sql Server 2008 trajo en su día fue la capacidad de poder pasar listas de valores a una instrucción Insert Into sin tener que unirlos en una tabla mediante UNION. El siguiente código es totalmente correcto:

create table #pruebas (idCargo varchar(50),nombre varchar(50));

insert into #pruebas values
   
(0,'Persona_1'),
    (
'1','Persona_2'),
    (
'','Persona_3');

Fíjate que estamos insertando en una tabla con dos columnas tipo texto distintos valores, como son un número, un caracter y un texto sin contenido. El resultado es el siguiente:

Lo que nos debería de preocupar de este resultado es la fila 3. Se puede observar que se ha convertido la cadena vacía a un 0, lo cual significa que ha utilizado la conversión implícita desde varchar a integer. Este efecto puede tener consecuencias desastrosas, cómo en este otro ejemplo:

insert into #pruebas values
   
(0,'Persona_1'),
    (
'1','Persona_2'),
    (
'a','Persona_3');
 

Este "inocente" script hace que salte una excepción
"Msg 245, Level 16, State 1, Line 3 Error de conversión al convertir el valor varchar 'a' al tipo de datos int."

¡¡¡Se está quejando de que no puede convertir el texto a número cuando estamos insertando textos en un campo de texto!!!

El motivo es que en la primera fila estamos insertando un int, e internamente Sql Server va a tratar de crear una tabla para los tres valores con la estructura que define esa primera fila, en nuestro caso un entero en la primera columna y un varchar en la segunda :-(

 

 

Posted: 25/10/2010 23:46 por Fran Otero Otero | con no comments
Archivado en:
ASP.NET Web Forms y los nuevos selectores CSS3

Aunque no es en absoluto la mejor opción, tengo observado en programadores de ASP.NET el intento de incluir estilos en elementos buscándolos por su identificador. Siempre será una opción menos mala que la de meter el estilo en línea, pero no tan buena cómo utilizar clases CSS.

En ASP.NET Web Forms nos encontramos con una barrera adicional: no conocemos los identificadores de los elementos en tiempo de diseño. Un ejemplo sencillo, incluyo una <asp:label id="lblPrueba"> dentro de un <asp:content> que a su vez utilizará una MasterPage, y el resultado final será que dicho elemento se me renderiza cómo <span id="MainContent_lblPrueba">. En ese caso si queremos aplicar un estilo CSS a dicho elemento buscándolo por id, tendremos que escribir el selector correspondiente para el id final, es decir, el que se enviará al navegador. Es evidente que un cambio en los contenedores del elemento puede dar al traste con esta feliz unión entre CSS e Id del elemento.

Aprovechando la circunstancia de que el identificador original que nosotros dimos al elemento siempre aparece al final del identificador cliente podemos utilizar el nuevo selector de CSS3 para seleccionar lo que nos interesa. En este caso nos valdría algo así:

     *[id$=lblPrueba]        { "estilos para la etiqueta lblPrueba"           }

Este selector viene diciendo: selecciona cualquier elemento (*) cuyo id termine en "lblPrueba", con lo cual asunto zanjado.

Puedes ampliar información sobre este selector y otros parecidos en esta página.

Posted: 12/10/2010 22:45 por Fran Otero Otero | con no comments
Archivado en: ,
Intercambio de passwords en ASP.NET

A veces para depurar nuestra aplicación necesitamos entrar "como si fuésemos" otro usuario. Si utilizamos la seguridad de formularios de ASP.NET la información de usuarios se guardará normalmente en una base de datos de SQLServer. Por seguridad la contraseña de los usuarios no se guarda en la BBDD, sino que se guarda un hash que nos sirve para saber si el usuario ha metido la contraseña correcta.

La forma menos intrusiva y más sencilla que tenemos de autenticarnos como otro usuario, es copiar nuestro password en el suyo. Para ello debemos de copiar dos campos de la tabla aspnet_Membership: Password y PasswordSalt. Para encontrar cuál es el usuario tenemos también que acceder a la tabla aspnet_Users, en donde podemos buscarlo por UserName. Y al acabar de hacer las pruebas estaría bien dejar todo como estaba, así que lo ideal sería haber guardado la información original en algún sitio.

El siguiente script hace todo esto por nosotros: intercambia la contraseña de dos usuarios, definidos por sendos parámetros al principio. De esta forma si quiero hacer una prueba en mi aplicación con el usuario 'admin', basta con que ejecute el script, entre con mi contraseña y el usuario 'admin' y finalmente vuelva a ejecutar el script para intercambiar de nuevo las contraseñas y dejar todo como estaba.

declare @Usuario1 varchar(250),@Usuario2 varchar(250);
--Datos de prueba
select @Usuario1='miUsuario',@Usuario2='admin';

begin
tran
    --Guardo la informacin del usuario1 y 2
   
select U.UserName,M.UserId,M.ApplicationId,M.Password,M.PasswordSalt
   
into #infoUsuario
   
from aspnet_Users U
   
inner join aspnet_Membership M
   
on U.UserId=M.UserId and U.ApplicationId=M.ApplicationId
   
where U.UserName=@Usuario1 or U.UserName=@Usuario2;

    --Intercambio las informaciones
   
update aspnet_Membership
   
set [Password]=n.[Password], PasswordSalt=n.PasswordSalt
   
from
   
(--Consulta que hace el verdadero swap
       
select A.UserName, A.UserId,A.ApplicationId,B.Password,B.PasswordSalt
        from
#infoUsuario A,#infoUsuario B
       
where A.UserId<>B.UserId
   
) n
   
where
    aspnet_Membership
.ApplicationId=n.ApplicationId
   
and aspnet_Membership.UserId=N.UserId;

    --Borrado de la tabla temporal
    if(OBJECT_ID('tempdb..#infoUsuario') is not null)
   
    drop table #infoUsuario;
commit tran

 

Fíjate que la base de datos de membership puede ser compartida por varias aplicaciones de asp.net, de ahí que introduzca en la consulta el campo ApplicationId. Si tienes esta información en tu base de datos exclusiva para la aplicación puedes obviar esta condición.

¿Sencillo, no?

Doodle K

Estos días está todo el mundo conjeturando acerca de las intenciones y significados de los últimos doodels. Más allá de sus motivos (supongo que hacer ruido) el efecto es cuánto menos curioso y muy sencillo de replicar:


¿Cuál es el truco? No hay Flash, ni Silverlight ni nada complicado detrás de este efecto: simplemente tenemos dos divs con su background establecido a la misma imagen, una de ellas en color y la otra en gris. El único truco de este efecto es una función de javascript que responde al evento onkeyup(this.value.length) que envía el número de caracteres del cuadro de texto para redimensionar el div que contiene la imagen en color.
 
function SetFrontWidth(value)
{
    var div1=document.getElementById('divK');
    var longitud=Math.round(700*value/6);
    if(longitud>580)
        longitud-=20;
    div1.style.width=longitud;
}

Para más detalle se puede bajar el código completo de este ejemplo (no utiliza css ni código externo alguno).

 

Posted: 10/9/2010 17:50 por Fran Otero Otero | con no comments
Archivado en:
¿Por qué cambiar IsNull por Coalesce?

IsNull es la función de Sql Server que la mayoría de los programadores que llevan unos años escribiendo consultas utilizan. Su función es devolver un valor por defecto si el primer argumento pasado es nulo.

Por ejemplo, supongamos que tenemos la siguiente tabla:

declare @Clientes as table (Id int, Nombre varchar(100));

 Y queremos actualizar un campo determinado, por ejemplo el Id=2, para que el nombre coincida con un parámetro que nos pasarán desde programa. Entonces la consulta a ejecutar sería tan sencilla como esta:

update @Clientes set Nombre = @NuevoNombre where Id=2;

Ahora  queremos mejorar nuestra consulta, de forma que si el parámetro @NuevoNombre es nulo la fila no se actualice y mantenga su valor. Entonces podríamos substituir la segunda línea por cualquiera de estas dos opciones:

        a) set Nombre=IsNull(@NuevoNombre,Nombre);

        b) set Nombre=Coalesce(@NuevoNombre,Nombre) 

 Siempre que el parámetro esté correctamente definido ambas consultas se comportarán igual. El problema viene cuando el parámetro (por ejemplo por utilizar inferencia en el sistema de origen) viene con una definición distinta. Imaginemos por ejemplo que definimos así el parámetro y ejecutamos la consulta:

declare @NuevoNombre as varchar(2)= null;

En el update con calesce el resultado es el esperado, mientras que en el caso de IsNull, la tabla se actualizará incorrectamente. Esto es debido a que realmente IsNull no devuelve el segundo parámetro, sino el resultado de convertir el valor del segundo parámetro al tipo del primero, lo cual consituye una diferencia fundamental.

A continuación listamos el código completo para copiar y pegar y demostrar el funcionamiento. Basta con descomentar alternativamente uno u otro UPDATE para comparar los resultados.

declare @Clientes as table ( Id int, Nombre varchar(100));
insert into @Clientes values
    (1,'Francisco Otero'),
    (
2,'Otro fulano');

declare @NuevoNombre as varchar(2)=null;
--update @Clientes
--set Nombre= @NuevoNombre
--where Id=2

--update @Clientes
--set Nombre=Coalesce(@NuevoNombre,Nombre)
--where Id=1

select * from @Clientes;
Posted: 9/9/2010 21:00 por Fran Otero Otero | con 7 comment(s)
Archivado en:
Common Table Expression simple

Las CTE o Common Table Expression son expresiones que facilitan la labor de escribir consultas complicadas, permitiendo definir subconsultas previas como tablas que se utilizarán en la consulta. Suena complicado, y realmente a veces lo es: las CTE pueden incluso ser recursivas y hacer consultas que ni te imaginabas que se pudiesen conseguir en una sola consulta (queda pendiente para otro post). Pero conocer y saber utilizar CTE básicas nos puede ayudar a mejorar nuestras consultas de forma muy sencilla. Ese es el caso de este ejemplo.

En la base de datos AdventureWorks existe una tabla con históricos de precios de productos.

select  * from Production.ProductCostHistory
order  by ProductID asc,StartDate asc;

Que da como resultado una tabla como esta:

 Queremos hacer una consulta que nos devuelva el primer precio que tuvo asignado cada producto, es decir, para el producto 707 tendría que devolver la prima fila, que se corresponde con el año 2001 y un precio de 12,0278. Lo primero que se nos puede venir a la cabeza será un GROUP BY, pero os adelanto que no va a salir bien :-). Mediante una agrupación podríamos saber la primera fecha para cada productId, pero: ¿y el precio? Tendríamos que hacer un InnerJoin posterior que utilizase este resultado: se complica mucho más si metemos más campos debido a que en el JOIN vamos a tener que igualar todos los campos para asegurarnos de que estamos seleccionando la misma fila.

La solución con CTE quedaría así:

with   Ampliada(ProductId,StandardCost,Indice) as
   (select   ProductID ,StandardCost,
   
RANK() over(partition by ProductId order by StartDate) Indice
    from   Production.ProductCostHistory )
select * from Ampliada where Indice=1;

Como ves la CTE crea una tabla previa, en este caso llamada "Ampliada" que podemos utilizar en la consulta siguiente (realmente es la misma consulta). Aquí el único truco es el campo índice que está construído a partir de la función Rank().

 

Posted: 26/8/2010 23:15 por Fran Otero Otero | con 2 comment(s)
Archivado en:
Candlestick para traders con SqlServer

Creo que el título no puede ser más descriptivo: un artículo publicado esta semana en ScribD en el que comento algunas técnicas útiles para análisis bursátil utilizando consultas de Sql Server. Únicamente con consultas de Sql Server conseguimos detectar patrones de velas japonesas que indican comienzos o finales de tendencias, proporcionando un apoyo a la decisión de inversión.

 

Publico en la zona de medios la base de datos real que utilizo en el artículo.

Posted: 19/8/2010 22:44 por Fran Otero Otero | con 2 comment(s) |
Archivado en:
Fila a fila sin cursores II: SELECT simple

Para los que les haya gustado mi anterior artículo en el cual substituiamos el uso de cursores por un bucle While, aquí está la continuación DEFINITIVA. Supongo que muchos pensareis que no ganamos mucho substituyendo una iteración por la otra, y en parte tenéis razón. No obstante la no utilización de cursores tiene beneficios intrínsecos que van más allá del propio rendimiento de la consulta, como podría ser la seguridad ante fallos de que no tendremos objetos instanciados en memoria.

De todas formas viendo el código anterior uno se queda con la sensación de que es un poco rudimentario. Hay otras alternativasparecidas, como puede ser incluir en la tabla temporal una columna IDENTITY que nos permita iterar la tabla en el While a través de una variable puntero que iremos incrementando en el propio While: más de lo mismo. Ahora bien, que os parecería si os digo que se puede substituir todo el bucle, el recorrido de filas, la comparación e incluso la tabla temporal por una única instrucción SELECT "corriente". Lo de corriente tiene un poco de trampa, pero nada de complicación: si utilizamos una instrucción select de asignación, la asignación se repetirá para cada fila de la consulta. Si además introducimos una expresión de tipo "case ... then .... else ..... end" podemos en cada fila comparar el valor máximo con el que llevamos agregado y asignar al máximo el mayor de ellos.

-- Creamos la tabla del ejemplo. Para simplificar introducimos solo los

--datos que utilizaremos, pero esto no afecta al ejemplo

declare @movimientos as table (Producto nvarchar(10), Cantidad int, Fecha date)

insert into @movimientos values ('tornillos',500,'20090101')

insert into @movimientos values ('tornillos',-200,'20090104')

insert into @movimientos values ('tornillos',-50,'20090102')

insert into @movimientos values ('tornillos',-220,'20090110')

insert into @movimientos values ('tornillos',500,'20090111')

insert into @movimientos values ('tornillos',-100,'20090120')

--Variables auxiliares que utilizaremos

declare @Acc int,@Max

int set @Acc=0 --Valor acumulado hasta la fila actual

set @Max=0 --Valor máximo hasta la fila actual

-- Consulta "importante"

select

@Acc=@Acc+Cantidad, --Valor acumulado hasta la fila actual

@Max=case sign(@Acc-@Max) when 1 then @Acc else @Max end --Asigna el nuevo máximo o se queda con el que había

From @movimientos

Where Producto= 'tornillos'

orderby fecha asc

print 'Máximo alcanzado:'

print @Max

 

-- Héchale una pensada, puede ser muy útil !!!!

 

 

 

Posted: 16/12/2009 23:15 por Fran Otero Otero | con 1 comment(s)
Archivado en:
Fila a fila sin cursores: columna agregado

Tenemos una tabla en nuestra base de datos llamada “Movimientos”, que registra entradas y salidas de material de nuestro almacén. La siguiente consulta es el resultado de consultar los movimientos de “tornillos” en el mes de enero. Cómo se puede ver la tabla carece de un autonumérico, los registros no tienen porqué estar en orden cronológico y hay más productos mezclados. 

tornillos

500

01/01/2009

tornillos

-50

02/01/2009

tornillos

-200

04/01/2009

tornillos

-220

10/01/2009

tornillos

500

11/01/2009

tornillos

-100

20/01/2009

Si en este escenario nos pidiesen obtener la cantidad máxima de tornillos que llegó a haber en el almacén podrían ponernos en un pequeño apuro. La idea sería conseguir una tercera columna con la cantidad en stock y poder seleccionar el máximo. El problema de eso es que necesitamos de alguna manera recorrer la tabla fila a fila, pero claro, dicen por ahí que eso de los cursores es cosa mala….

tornillos

500

01/01/2009

500

tornillos

-50

02/01/2009

450

tornillos

-200

04/01/2009

250

tornillos

-220

10/01/2009

30

tornillos

500

11/01/2009

530

tornillos

-100

20/01/2009

430

He aquí un método alternativo a la utilización de los cursores para obtener la columna “agregado” y poder seleccionar el máximo stock, que tiene lugar el 11 de enero con un valor de 530 tornillos.

1º) Creamos una tabla temporal con los registros que nos interesan, ordenándolos por orden cronológico

2º) Recorremos un bucle que analice la primera fila y la eliminamos, de esta forma podemos recorrer la tabla completa.

Veámoslo en el ejemplo:

--Creamos la tabla del ejemplo. Para simplificar introducimos solo los

--datos que utilizaremos, pero esto no afecta al ejemplo

declare @movimientos as table

(Producto nvarchar(10), Cantidad int, Fecha date)

insert into @movimientos values ('tornillos',500,'20090101')

insert into @movimientos values ('tornillos',-200,'20090104')

insert into @movimientos values ('tornillos',-50,'20090102')

insert into @movimientos values ('tornillos',-220,'20090110')

insert into @movimientos values ('tornillos',500,'20090111')

insert into @movimientos values ('tornillos',-100,'20090120')

 

--Definimos una tabla temporal, con la misma estructura que la original

declare @tmp as table

(Producto nvarchar(10), Cantidad int, Fecha date)

 

--Introducimos aquí los datos que nos interesen, ordenados convenientemente

insert into @tmp

      select * from @movimientos

      where Producto='tornillos'

      order by fecha asc

 

--Variables auxiliares que utilizaremos

declare @Acc int,@Max int

set @Acc=0 --Valor acumulado hasta la fila actual

set @Max=0 --Valor máximo hasta la fila actual

 

--Esta instrucción elimina de la ventana de mensajes el recuento de filas afectadas

set nocount on

 

--Mientras exista alguna fila en @tmp recorro el bucle

While(Exists(select * from @tmp))

      BEGIN

            --Sumamos la 1ªfila al acumulado

            select top(1) @Acc=@Acc+Cantidad from @tmp;

            --Eliminamos la 1ª fila; ya está computada

            delete top(1) from @tmp;

            --Comprobamos si el acumulado es máximo

            if(@Acc>@Max)

                  set @Max=@Acc;

           

            print @Acc;

      END

     

print ''

print 'Máximo alcanzado:'

print @Max

 

Lo que nos devuelve por pantalla la columna de acumulados y el máximo stock que llegó a haber en el almacén:

500

450

250

30

530

430

 

Máximo alcanzado:

530

 

La pega de este método son la cantidad de deletes que utiliza, así que en el siguiente post tocará un método alternativo (sin cursores, of course) más eficaz.

 

Posted: 4/12/2009 0:39 por Fran Otero Otero | con 3 comment(s)
Archivado en:
El atributo 'for' en ASP.NET: AssociatedControlId

A vueltas con esto de hacer una aplicación accesible y estándar con ASP.NET me encontré con un detalle aparentemente trivial: cómo generar un formulario estándar. Vale, utilizando un control de datos el problema "se resuelve solo", pero si pienso en un par de etiquetas con cuadros de texto para recolectar datos de forma sencilla supongo que todo el mundo pensaría en algo como esto:

<asp:label runat="server" Text="Nombre: " > <asp:textbox runat="server"><br/>

...etc,etc

Lo cual nos genará un aparente formulario, pero cuyos textos están en un span. ¿Qué implicaciones tiene esto? Pues por ejemplo, que el span es un elemento inline por defecto, y nos costará un poquito más formatearlo con css, a parte de que los textos no estarán asociados a los cuadros de texto correspondientes con lo cual la accesibilidad se verá mermada. La primera solución que se nos ocurre supongo que será, ¿porqué un control de asp.net para la etiqueta? Es cierto, si substituyo el control asp.label por un label html normal con el atributo "for" establecido al id del cuadro de texto asunto resuelto!.

¿Pero qué ocurre si estas etiquetas han de ser accedidas desde servidor? ¿O simplemente si la página ha de ser localizable? Hay solución para todo, pero lo cómodo sería poder utilizar el código mostrado al principio indicándole que se renderizase como <label> en lugar de hacerlo como <span> y se estableciese el atributo "for" al id que se le asigne al control <input> que generará el control textbox. Pues bien, eso es exactamente lo que se consigue con el atributo AssociatedControlId:

<asp:label runat="server" Text="Nombre: " AssociatedControlId="tbxNombre" > <asp:textbox runat="server" ID="tbxNombre"><br/>

 

 Sencillo, pero hay que saberlo!!!

 

Set SelectedValue in DataList

Otra de esas pequeñas cosas que nos puede hacer perder el tiempo. Queremos cargar un Datalist con una lista de items, en el caso del ejemplo imágenes, y hacer que una de ellas aparezca seleccionada. Si recorremos los métodos y propiedades disponibles en este control, nos daremos cuenta de que la única forma de seleccionar por código un elemento es estableciendo la propiedad SelectedIndex. Existe la propiedad SelectedValue, pero es únicamente de lectura. Por desgracia, el índice no suele ser lo más recomendable para identificar a un elemento, así que en este ejemplo vamos a ver cómo hacer que este mismo control permita establecer su SelectedValue.

Se trata de un método extensor que recorrerá los elementos de la colección buscando coincidencias de clave, y una vez encontrada selecciona el elemento por su índice. Fijaos en que la dificultad reside en que la lista de claves no está indexada, con lo cual la tenemos que recorrer en un foreach y llevar la cuenta del índice "a pelo".

  public static void SetSelectedValue(this DataList dataList,String value)
    {
        if (dataList.Items.Count > 0)
            {
                int i = 0;
                foreach (Object key in dataList.DataKeys)
                {//Recorremos las claves, llevando el índice en la vble i
                    if (value ==  key.ToString())
                    {//Elemento a seleccionar, fin del método
                        dataList.SelectedIndex = i;
                        break;
                    }
                    else
                        i++;
                }
            }
    }

 

Como siempre, espero que a alguien más le resulte útil ;-)

 

FileUpload dentro de UpdatePanel

Sabido es que ciertos controles no funcionan correctamente dentro de un UpdatePanel. Es el caso de los Treeview, menú, gridview o detailsView entre otros. Algunos de estos no funcionan bajo determinadas circunstancias, o simplemente quedan totalmente invalidados. A este último grupo pertenece el control FileUpload, que nos  permite subir archivos al servidor.

Buscando por ahí, se encuentran varias soluciones, (destacaría la de Isaac Fernández en este mismo site), la mayoría enfocadas a la situación sencilla de que el control fileUpload esté situado directamente dentro del update panel. En este caso basta con agregar a la lista de triggers del UpdatePanel un nuevo PostBackTrigger con el ControlId apuntando al botón de subir archivo de nuestro FileUpload.

El problema de esta solución viene cuando la arquitectura se complica y tenemos, por ejemplo, el fileUpload dentro de un control de usuario propio, que a su vez será utilizado en páginas con varias MasterPage anidadas y el UpdateManager se encuentra en la primera de dichas MasterPages (suena rebuscado así contado, pero no estoy inventando nada :-)) Está claro que no podemos referirnos directamente desde el ASCX al UpdateManager (de forma sencilla, se entiende) ni viceversa. En este caso la solución sería tan simple como crear el PostBackTrigger referido a nuestro botón desde código, para lo cual necesitamos obtener el ScriptManager que se está utilizando en la página actual. El siguiente código muestra cómo hacerlo:

  

    protected void Page_Load(object sender, EventArgs e)

    {

        ScriptManager scripManager = ScriptManager.GetCurrent(this.Page);

        scripManager.RegisterPostBackControl(lnkNuevaImagen);

    }

 

 

Asunto resuelto, nos cargamos la funcionalidad asíncrona para este botón, pero aseguramos que funciona con sólo incluir dos líneas de código fácilmente entendibles.

Espero que a alguien más le sirva!

Posted: 7/9/2009 18:25 por Fran Otero Otero | con 5 comment(s)
Archivado en:
Capturar punto de google maps

A veces uno se encuentra con pequeños problemas que deberían de ser sencillos. Este fue el caso de cuando me propuse realizar una interfaz para una aplicación windows donde el usuario pudiese elegir cual era su localización en un mapa de google maps y calcular la distancia a uno de nuestros almacenes. La opción buena sería utilizar la api de google maps para estas tareas, pero quiero algo más sencillo, que funcione sin tener que instalar nada a mayores.

Primer inconveniente, ¿de donde saco las coordenadas del usuario? Googleando un poco encontré un sencillo script que comparten muchos sitios. Basta con introducir en la barra de direcciones el script java_script:void(prompt('',gApplication.getMap().getCenter()))  y nos aparecerá un mensaje con la latitud y longitud.

Segundo problema, ¿como recojo esto en mi código? Bueno, pues básicamente el truco está en utilizar ese resultado no para sacarlo en un prompt, si no para redirijir la página del webbrowser en donde está cargado google maps. De esta forma en el evento navigating puedo analizar la dirección a la que intento navegar, cancelarla y conseguir las coordenadas. 

El siguiente ejemplo   hace precisamente eso, en un formulario donde unicamente se ha agregado un webBrowser (webBrowser1) y un botón (button1):

        private void Form2_Load(object sender, EventArgs e)

        {

            webBrowser1.Url = new Uri("http://maps.google.es");

        }

        private void button1_Click(object sender, EventArgs e)

        {

            webBrowser1.Navigate("java_script:navigate('" + clave + "'+gApplication.getMap().getCenter())");

        }

 

        private void webBrowser1_Navigating(object sender, WebBrowserNavigatingEventArgs e)

        {

            string aux = e.Url.ToString();

 

            if (aux.StartsWith("http://maps.google.es/"+clave))

            {//Navegación iniciada por nuestro código javascript               

                PointF direccion;

                aux = aux.Substring(("http://maps.google.es/"+clave).Length);//Eliminamos la url incial, quedándonos solo con el punto

 

                aux= aux.Replace("(", "").Replace(")",""); //Eliminamos los paréntesis

                string[] punto=aux.Split(',');//Dividimos el texto en las dos coordenadas

                direccion = new PointF(                

                    float.Parse(punto[0].Replace(".", ",")),

                    float.Parse(punto[1].Replace(".", ","))); //Parseamos las dos coordenadas al punto resultado

 

                e.Cancel = true; //Cancelamos la navegación, la página resultante no es válida.

 

                //Ya tenemos el punto, podemos hacer lo que queramos con él!!! 

                MessageBox.Show(direccion.ToString());

            }

        }

 Para completar el ejemplo falta la parte de ¿que hago ahora con el punto? y ¿como encuentro la distancia a mis almacenes registrados en Sql Server?.

Queda pendiente para el siguiente post...

 Nota: substituir java_script por el equivalente sin guión bajo para que funcione el código.

Implementación sencilla del ControlState

El ControlState es un mecanismo alternativo al ViewState para guardar el estado de sesión en un control de usuario. Como es bien conocido disponemos múltiples objectos para guardar variables de sesión, todos ellos con una utilización parecida (básicamente son colecciones clave-valor de tipo string-object), que nos permiten mantener el estado entre llamadas en distintos ámbitos. El ámbito del controlState es pareceido al ViewState, con la diferencia fundamental de que éste último puede desactivarse. Éste es el motivo por el cual datos críticos para el buen funcionamiento del control deberían de guardarse en el ControlState (Ver Artículo MSDN) . Esta semana he constatado que su especial funcionamiento constituye una barrera de entrada para su utilización en programadores poco iniciados. Por este motivo me he decidido a publicar este post, con un pequeño código que, insertado en nuestro control, hará posible la utilización del ControlState de forma análoga al objeto Cache, ViewState, Session, etc.

    protected Dictionary<string,object> ControlState;

 

    //Guarda el diccionario personalizado y lo que

    //tuviese que guardar en el método base

    protected override object SaveControlState()

    {

        return new Pair(base.SaveControlState(), ControlState);

    }

 

    //Recupera por separado el diccionario personalizado

    //y el objeto para el método original

    protected override void LoadControlState(object savedState)

    {

        if (savedState != null)

        {

            Pair par=(Pair)savedState;

            base.LoadControlState(par.First);

            if (par.Second != null)

            {

                ControlState = (Dictionary<string, Object>)par.Second;

            }

        }

       if(ControlState==null)

           ControlState=new Dictionary<string,object>();

    }

 

Y para que todo esto funcione solo nos resta añadir en el evento load de la página la siguiente línea:

        Page.RegisterRequiresControlState(this);

 

Posted: 19/2/2009 18:23 por Fran Otero Otero | con no comments
Archivado en:
Puesta a cero de la base de datos en pre-producción

Tenemos nuestro nuevo proyecto preparado para el despliegue. Subimos nuestro sitio web al servidor de producción, ejecutamos el script de creación de la base de datos, configuramos la aplicación y vamos a probar y ..... no funciona grrrr#**&*!!!! ¡A cuántos nos sonará este tema! Lo cierto es que es posible ( e incluso probable) que con el cambio de entorno tengamos que hacer unos cuantos retoques en nuestra aplicación, realizar unas cuantas pruebas, etc. Despues de esto nuestra recién creada base de datos estará sucia y posiblemente queramos volver a inicializarla.

Por diversos motivos puede que no nos interese cargarnos la base de datos otra vez, con lo cual intentaremos unicamente borrar las tablas (puede que ni siquiera tengamos permisos para borrar la base y volverla a crear, de ahí el interes de este método). Podemos utilizar un procedimiento que recorra las tablas listadas en INFORMATION_SCHEMA.TABLES e vaya ejecutando un delete sobre ellas. Con este método únicamente borraremos parte de las tablas, ya que tenemos altas probabilidades de que muchas de las filas se queden bloqueadas por relaciones foreign key al ejecutar el barrido con un orden arbitrario (el de INFORMATION_SCHEMA.TABLES). Además tenemos el problema añadido de que los índices de las tablas no se reinicializan ya que debido otra vez a las foreign keys no podemos utilizar Truncate en lugar de Delete.

El código listado a continuación viene a solucinonar la problemática descrita, borrando todas las tablas de la base de datos independientemente de sus relaciones y reinicializando los índices a 0 en las tablas que dispongan de este tipo de campos. Nótese que el código hace incidencia especialmente en lo funcional, obviando consideraciones que nos distaigan del objetivo principal

            

            //Definimos la conexión a la base

            SqlConnection cn = new SqlConnection("ConnectionString");

            //Definimos un comando auxiliar

            SqlCommand aux = new SqlCommand("", cn);

            //Por último un comando auxiliar que carga las tablas de la base de datos.

            SqlCommand select = new SqlCommand(@"select * from INFORMATION_SCHEMA.TABLES where TABLE_TYPE like 'BASE TABLE'", cn);

            SqlDataAdapter selectDA = new SqlDataAdapter(select);

            DataTable tablas;

            selectDA.Fill(tablas = new DataTable());

            cn.Open();

            bool repetir;

            do

            {

                repetir=false;

                foreach (DataRow tabla in tablas.Rows)

                {//Borramos cada una de las tablas

                    aux.CommandText = "Delete " + tabla["TABLE_NAME"].ToString();

                    try

                    {

                        aux.ExecuteNonQuery();

                    }

                    catch (SqlException ex)

                    {

                                               if(ex.Number==547) //Infracción de FK: hay que repetir el borrado

                            repetir=true;

                    }

                }

            } while (repetir);

            foreach (DataRow tabla in tablas.Rows)

            {                   

                aux.CommandText = "DBCC CHECKIDENT (" + tabla["TABLE_NAME"] + ",RESEED,0)";

                try

                {

                    aux.ExecuteNonQuery();

                }

                catch

                {}//No todas las tablas tienen columnas de identidad para regenerar

            }

            cn.Close();

Posted: 7/2/2009 16:53 por Fran Otero Otero | con 3 comment(s)
Archivado en:
Más artículos Página siguiente >