jueves, 13 de diciembre de 2012

Reducir el tamaño de la base de datos del VMware Vcenter Server

Sintomas:

Vcenter va lento e incluso en un  momento el servicio se para con el siguiente error en el visor de eventos
No se encuentra la descripción del id. de evento 1000 en el origen .........
Se incluyó la siguiente información con el evento:

An unrecoverable problem has occurred, stopping the VMware VirtualCenter service. Error: Error[VdbODBCError] (-1) "ODBC error: (42000) - [Microsoft][SQL Native Client][SQL Server]Could not allocate space for object 'dbo.VPX_TEXT_ARRAY' in database 'VIM_VCDB' because the 'PRIMARY' filegroup is full. Create disk space by deleting unneeded files, dropping objects in the filegroup, adding additional files to the filegroup, or setting autogrowth on for existing files in the filegroup." is returned when executing SQL statement "INSERT INTO VPX_TEXT_ARRAY WITH (ROWLOCK) (ARRAY_ID, TYPE_ID, VALUE, MO_TYPE, MO_ID) VALUES (?, ?, ?, ?, ?)"

El recurso de mensaje está presente, pero el mensaje no se encuentra en la tabla de cadenas o mensajes
O algo similar. La calve es que la base de datos se ha llenado.

Solución

Deten el servicio de vCenter Server
En la base de datos de Vcenter ( VIM_VCDB )
Haz una copia de seguridad completa.

Abre la tabla dbo.VPX_PARAMETER para editar las primeras 200 filas.
Edita las filas:
event.maxAge  nuevo valor: 30
event.maxAgeEnabled value valor true
task.maxAge nuevo valor: 30

task.maxAgeEnabled value valor true

Luego ejecuta el procediminento almacenado dbo.cleanup_events_tasks_proc Le llevará bastante tiempo pero cuando acabe el vCenter irá como una seda.   Fuente: http://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=1025914

viernes, 9 de noviembre de 2012

Problemas con almacenamiento ISCSI en vmWare

Síntomas:

Tienes varios ESX y añades uno nuevo. Los ESX existentes tiene almacenes (datastores) iSCSI
Configuras la red y el iniciador iSCSI.
Después de volver a barrer la red (rescaneo) ves los dispositivos y  que están montados.
Sin embargo los almacenes (datastores) que estan en otros host ESX no son visible en el almacenamiento (storage) de tu nuevo host.

Cuando intentas añadirlos manualmente te ocurren dos cosas:
a) En algún caso lo único que te propone es formatear, cosa que desde luego no haces.
b) En otros casos te da las tres opciones, pero cuando eliges utilizar la signatura existente (keep existing signature) obtienes el error:
Error: Cannot change the host configuration.
Error Stack
Call "HostStorageSystem.ResolveMultipleUnresolvedVmfsVolumes" for object "storageVolume" on vCenter Server "MyVC" failed

Solución:

a) Cuando solo propone el formateo es porque el datastore esta utilizado por una VM en marcha o por algún mecanismos de vmWare View. No muevas los clones con el vCenter, si es preciso para los servicios del composer e incluso el View Server
b) Cuando ya puedas añadirlos manualmente utilizando la signatura existente, deberás conectar el cliente vCenter directamente al host ESX. (esto es no al vCenter Server). De esta forma evitaras el error mas arriba.

Referencias:

Las formas en que un iSCSI te puede causar problemas en vmWare son infinitas, pero yo encontré mi solución desde los logs y gracias a estas dos entradas que se complementan.
Forcemounting a VMFS datastore residing on a snapshot LUN results in the error:Cannot change the host configuration




lunes, 5 de noviembre de 2012

Error en la instalación del cliente SCCM debido a la versión BITS

Enunciado

No se produce la instalación del cliente SCCM mediante WSUS em maquinas Windows 2003 o XP.
El registro en %windir%\WINDOWSUPDATE.LOG contiene el error:
2012-10-30    15:10:59:184     892    165c    Agent    *********
2012-10-30    15:10:59:184     892    165c    Agent      * Updates to install = 1
2012-10-30    15:10:59:200     892    165c    Agent      *   Title = Configuration Manager Client Installation
.....................
2012-10-30    15:11:02:965     892    59c    AU      # WARNING: Install failed, error = 0x80070643 / 0x00000001
2012-10-30    15:11:02:965    5416    1cc    Handler      : WARNING: Exit code = 0x8024200B


(Que en resumidas cuentas nos dice que la instalación ha fallado)

La clave como otras veces la tenemos en el registro en %windir%\CCMSETUP\CCMSETUP.LOG que en este caso contiene:
<![LOG[This operating system does not contain the correct version of BITS. BITS 2.5 or later is required.]LOG]!>

Que nos está diciendo claramente lo que hay que hacer.

Solución

Instala BITS 2.5 para tu versión de sistema operativo, por ejemplo para un windows 2003 x86
http://www.microsoft.com/es-es/download/details.aspx?id=4933

jueves, 25 de octubre de 2012

Limpiar WSUS de las descargas (útil si usas SCCM)

Enunciado

Instalas WSUS y configuras la descarga de actualizaciones aprobadas.
Las apruebas.
Instalas luego SCCM y descubres que realmente no quieres que no necesitas descargar las aplicaciones.
Estas duplicando el espacio en disco.
(Para SCCM se debe instalar WSUS pero no configurarlo)

Solución

Tan simple como marcar todas las descargas como rechazadas o no aceptada y ejecutar el asistente de limpieza en opciones.

Paquetes de definiciones EndPoint Protection 2012 vacios

Síntomas:

English Version at MSDN Forums.

En System Center Configuration Manager 2012 con un servidor SQL Server español las reglas para la distribución automática de paquetes con definiciones de End Point Protection nunca se rellenan con las actualizaciones del WSUS.

Cuando analizas el rulengine.log encuentras estas entradas:
Query to run is: select CI_ID from dbo.fn_ListUpdateCIs(3082) ci~where IsExpired=0~ and (DateRevised>=N'2012-10-23 22:22:08')~ and (CI_ID in (select CI_ID from fn_ListCICategoriesAll(3082) where CategoryTypeName='Product' and (CategoryInstanceName in (N'Forefront Endpoint Protection 2010')))) SMS_RULE_ENGINE 25/10/2012 0:22:08 3136 (0x0C40)
Rule resulted in a total of 0 updates SMS_RULE_ENGINE 25/10/2012 0:22:09 3136 (0x0C40)

Solución:

En mi caso bastó con cambiar el idioma prederminado del SQL Server a Inglés ( en propiedades del servidor motor de BD, Avanzadas, Idioma por defecto).
Para ver si es tu caso lee los siguientes comentarios.

Comentarios:

Si te fijas en la primera línea del registro, a continuación del Query to run is:  tenemos una clausula SQL que podemos copiar y pegar en la en la consola (SSMS) del SQL Server. Una vez eliminado los símbolos ~   y con unos saltos de línea queda bastante legible.
select CI_ID from dbo.fn_ListUpdateCIs(3082) ci 
where IsExpired=0  
 and (DateRevised>=N'2012-10-23 22:22:08')  
 and (CI_ID in (select CI_ID from fn_ListCICategoriesAll(3082) 
                where CategoryTypeName='Product' 
                and (CategoryInstanceName in (N'Forefront Endpoint Protection 2010')
))) 
Pero si la ejecutamos tenemos un  un error:

CI_ID
-----------
Mens. 242, Nivel 16, Estado 3, Línea 1
La conversión del tipo de datos nvarchar en datetime produjo un valor fuera de intervalo.

Este error debería desaparecer si cambias en la consulta el formato de fecha (En lugar de 2012-10-23 escribes 23-10-2020).

select CI_ID from dbo.fn_ListUpdateCIs(3082) ci 
where IsExpired=0  
 and (DateRevised>=N'23-10-2012 22:22:08')  
 and (CI_ID in (select CI_ID from fn_ListCICategoriesAll(3082) 
                where CategoryTypeName='Product' 
                and (CategoryInstanceName in (N'Forefront Endpoint Protection 2010')
))) 
Obtendrás la lista de ids de definiciones, en mi caso fué:
CI_ID
-----------
16811415
16811428
16811468

(3 filas afectadas)
Por eso si cambias el idioma predeterminado a inglés del SQL Server te funcionara la ADR de SCCM.

Nota:

En mi caso opté por cambiar el idioma en el servidor  pero es posible que cambiándolo en los usuarios de SCCM o el origen de datos ODBC del servidor SCCM se resuelva también el problema.

viernes, 19 de octubre de 2012

Algunos consejos instalando SCCM con End Point Security Manager

Enunciado:

Instalar MS End Point Protection Manager con System Control Configuration Manager es una tortura (http://angrytechnician.wordpress.com/2010/08/09/the-torture-of-installing-system-center-essentials-2010/)
Espero que las siguientes notas sean de ayuda:

Problema 1:

La collation del SQL Server debe ser SQL:Latin1_General_CP1_CI_AS. 
Lo debes especificar al instalar el SQL SERVER

Si como yo procedes a instalar con los valores por defecto tendras que:

  • Desinstalar WSUS completamente si ya lo has instalado como hice yo.
  • Realizar un rebuild de la base MASTER

Para ello usas este mandato:
Setup /QUIET /ACTION=REBUILDDATABASE /INSTANCENAME=InstanceName /SQLSYSADMINACCOUNTS=accounts [ /SAPWD= StrongPassword ] [ /SQLCOLLATION=CollationName]


  • Volver a configurar las bases de datos del Report Server
  • Reinstalar WSUS

Problema 2

La documentación para configurar las actualizaciones de Windows Update no esta muy clara, puedes seguir este tutorial:
http://www.windows-noob.com/forums/index.php?/topic/5452-using-system-center-2012-configuration-manager-part-1-installation-cas/
y mejor que no te dejes nada de las primeras páginas.

Problema 3

Cuando configuras la primera máquina para que descargue el cliente SCCM, se produce un error. En mi caso es que no había configurado un Boundary Group. Al configurarlo le dices con que servidores tiene que conectar.

Realmente es una tortura.

miércoles, 10 de octubre de 2012

DisplayMember en windows forms checkedlistbox

Problema:

 El control checkedlistbox de de Windows Forms no me ofrece los atributos "Displaymember" o "Valuemember" en las ventana de propiedades o en la lista inteligente de código.

Solución:

Establecerlo en código aunque no aparezca en la lista. Existe pero está marcado como no browseable.
!Atención¡ Escribe bien las mayúsculas y minúsculas.

Ejemplo;

miCheckedListBox.DisplayMember="Descripcion"

Referencia:

lunes, 1 de octubre de 2012

Chekbox en windows forms solo lectura

Enunciado

A diferencia de otros controles de Windows forms las checkbox no pueden marcarse como de solo lectura (readonly). Si utilizas la propiedad Enabled, pasan a un color gris claro que se ve mal, y no es modificable.

Solución:

Coloca las casillas dentro de un panel (por ejemplo panel1)
Importa la función WindowEnabled de user32.dll para poder deshabilitar eventos de ratón y teclado en ese panel.
Imports System.Runtime.InteropServices
Public Class Form1
    <DllImport("user32.dll")> _
    Private Shared Function EnableWindow(hWnd As IntPtr, bEnable As Boolean) As Boolean
    End Function

    Private Sub Form1_Load(sender As Object, e As System.EventArgs) Handles Me.Load
        EnableWindow(Panel1.Handle, False)
    End Sub

martes, 18 de septiembre de 2012

Formula para repartir los mismos puntos en una lista de longitud variable

Enunciado:

Realizas una encuesta con preguntas donde te responde con una lista de longitud variable por orden de preferencia.
Quieres repartir una cantidad fija de puntos entre las respuestas dando mas siempre a las situadas en primer lugar.

Por ejemplo "elija su ciudad favorita". En una respuesta te dicen solo una "CASTELLÓN", otra persona te dice "MADRID", "BARCELONA", "VALENCIA" y una tercera te dice "MADRID", "VALENCIA". "SEVILLA", "BURRIANA"
Hay 100 puntos a repartir, veamos los resultados:
  • La primera respuesta dará a CASTELLÓN 100 puntos
  • La segunda entregará  50 puntos a MADRID, 33 a BARCELONA y 17 a VALENCIA.
  • La tercera entrega a 40 a MADRID,  30 a VALENCIA, 20 a SEVILLA y 10 a BURRIANA
Al final tenemos: CASTELLON (100), MADRID (73), VALENCIA (47),BARCELONA (33) y BURRIANA (10).
Gana CASTELLON a MADRID, pese que CASTELLON solo haya sido votada una vez, y MADRID dos veces y en primer lugar en ambos casos.
Se valora mas cuando solo se da una respuesta que si se responde con varias. La persona que ha votado a CASTELLON está muy convencida, mientras que las demás tienen gustos dispersos.

Se me ocurre aplicarlo en las carreras de los autos locos, los participantes puntúan mas cuantos menos coches acaban. Seria peligroso aplicarlo en la F2 (Fernando Alonso dixit).

Solución

puntuación(r,p)=2*( (r-p+1)/(r*(r+1)))

Donde r es el número de respuestas y p la posición de la respuesta que estamos puntuando.

Comentario 

Esta formula puede implantarse en SQL  o en MDX. Es fácil construir una tabla Excel para ver las puntuaciones.


Cómo puede verse me apoyo en la suma de la serie geometríca.

Archivos duplicados y hash MD5

Enunciado

Quieres detectar y mover a una carpeta todos los archivos duplicados en una carpeta y subcarpetas. 
Cuando hay duplicidados mueves el archivo con nombre mas largo o que está en una subcarpeta.
En la nueva carpeta creas las subcarpetas originales.

Solución:

Para detectar los archivos duplicados usamos el hash MD5 calculado con la clase correspondiente en el framework .NET, recorremos recursivamente las carpetas, comparando archivos  y moviendo el que tiene path completo mas largo.
Const pathdup="c:\duplicados" 'carpeta donde se moveran los duplicados
Sub procesaCarpeta(carpeta As String)
'carpeta: carpeta a recorrer
   Dim existencias As Dictionary(Of String, String)
    Dim Md5 As New MD5CryptoServiceProvider()
        Try
            Dim archivos = My.Computer.FileSystem.GetFiles(carpeta)

            For Each archivo In archivos
                Dim acont = My.Computer.FileSystem.ReadAllBytes(archivo)
                Dim clave = Convert.ToBase64String(Md5.ComputeHash(acont))
                If existencias.ContainsKey(clave) Then
                    If existencias(clave).Length <= archivo.Length Then
                        'mover nuevo
                        My.Computer.FileSystem.MoveFile(archivo, archivo.Replace("C:\", pathdup))
                    Else
                        My.Computer.FileSystem.MoveFile(existencias(clave), existencias(clave).Replace("C:\", pathdup))
                        existencias(clave) = archivo
                    End If
                Else
                    existencias.Add(clave, archivo)
                End If
            Next
        Catch ex As Exception
            MsgBox(ex.Message, MsgBoxStyle.OkOnly, carpeta)
        End Try

        For Each hija In My.Computer.FileSystem.GetDirectories(carpeta)
            procesaCarpeta(hija)
        Next
    End Sub

Comentario

Puede parecer tonto pero limpié 7 Gb del disco duro de mi casa. La explicación es simple, mi hija antes de editar copiaba todas las imágenes de una carpeta y las pegaba en la misma carpeta o en una  subcarpeta (a veces ambas cosas).

jueves, 23 de agosto de 2012

Interfaz de usuario de Windows 8

Síntomas

Instalas Windows 8, al arrancar te encuentras con una pantalla que parece un Windows Phone.
¿Qué haces a continuación? ¿Donde está botón de inicio? ¿el panel de control? ¿Cómo lo apagas?

Solución:

Para ver el escritorio normal busca un cuadro con margaritas entre todos esos que cambian de color y hablan de fútbol.
Cuando hagas clic en las margaritas llegaras al escritorio.
Pero no encontraras el botón de inicio, porque no lo hay, ni sin duda lo habrá.
Una alternativa al botón de inicio es desplazar el ratón a las esquina superior o inferior  derecha
Se despliega una barra en el lateral derecho, allí tienes la lupa, le das clic y tienes una lista de programas e incluso el panel de control.
Otra opción interesante en la barra que has desplegado es la de configuración (la rueda tipo tiempos modernos abajo). Allí tienes un botón iniciar/apagar que sirve para apagar o REiniciar.
Para cerrar sesión tienes que volver a la ventana de los cuadrados cambiantes (se llama Metro) y pulsar en tu nombre arriba a la derecha.
Pero desde el escritorio ¿cómo vuelves al dichoso Metro?
Hay algo maravilloso en Windows 8, la tecla de Windows  te lleva a Metro sacándote de cualquier embrollo donde te hayas metido sin encontrar la flecha de salida.
¡Es como la tecla escape pero con dibujos!

También sigue funcionando esta tecla con la R para ejecutar un programa. Es una lástima que no sirva también para desinstalar Windows 8 y volver al 7.

Comentario:

Microsoft ha vuelto a cometer los errores del Vista y del Office 2007: confiar en diseñadores y creativos del marketing que piensan que un ordenador es un producto para consumir (metro) no para crear (escritorio), y que a un teclado le sobran 103 teclas porque les molesta que otros escribamos con todos los dedos.

Las personas normales somos reacias al cambio, nos gusta que la llave o interruptor para arrancar el coche esté en el lado derecho del volante. Aceptamos los cambios cuando nos aportan algún beneficio.
En mi Nissan, no me molesta presionar el pedal de freno al mismo tiempo porque la llave inteligente me evita rebuscar en los bolsillos (ante guardaba la llave nada mas abrir la puerta del coche y luego no la encontraba).

Pero ¿qué aporta Metro? ¿El mismo interfaz en todos mis dispositivos?
¡Pero si yo cambié los teléfonos móviles de mi empresa desde Windows Phone a Android cuando salió Windows Phone 7.0 precisamente con ese interfaz Metro y sus contenidos cambiantes!
Si fuera solo una pantalla de bienvenida pasaría, pero al quitar el botón de inicio pierdes mucho tiempo cada vez que quieres hacer algo normal.
Lo peor es que cuando salio la preview algunos programadores sacaron utilidades para recuperar ese botón de inicio, pero desde Microsoft alguien ordenó que bloquearan esta posibilidad. Sostenella  y no enmendalla

Desde el punto de vista de empresa no anticipo nada bueno a Windows 8:
  1. Costes de formación usuario final. ¿os acordaís del botón de office 2007 y los usuarios queriendo imprimir?.
  2. La pantalla inicial que ofrece juegos, musica, videos, noticias de futbol, cualquier cosa menos productividad. (y eso que estoy probando la versión enterprise).
El caso es que por lo que he leído y experimentado en Windows 8 hay mucho trabajo de core que agiliza el sistema en el arranque y en el uso. Es una lastima que todo ese trabajo se desaproveche y espero que Microsoft corrija antes de que sea tarde como ya hizo con el Windows 7 y otros muchos productos antes.

Yo he usado Windows en todas las versiones tanto de escritorio ( 1, 2, 2.1, 386,3,0, 3,1,  3.11, 95, 98, me, XP,  Vista, Windows 7) y como de servidor (OS/2 con IBM, NT 3.1, NT 3.5, NT 3.51, NT4, 2000, 2003, 2003R2, 2008, 2008R2,...y se de que hablo.
Porque al fin y al cabo a mí me gusta Microsoft,..., aunque en Visual Studio 2012 haya puesto los menús en mayúsculas, ¡otro creativo suelto!.






Se interrumpe la instalación del Rollup 4 para Exchange 2010 SP2

Síntomas:

Cuando aparece la barra de progreso de instalación del RU4 para XCHG2012 SP2 indica que está intentado detener los servicios, inmediatamente después informa que está deshaciendo las acciones y te informa que no se ha instalado.

Solución:

Guarda el archivo msp del RU en una carpeta.
Abre un ventana de símbolo de sistema en modo administrador 
(ya sabes botón derecho en el menú de inicio, ejecutar como administrador)
Ves a la carpeta y ejecuta.

domingo, 19 de agosto de 2012

Script PowerShell para apagar infraestructura vmWare

Enunciado

Quieres que en caso de corte de suministro eléctrico prolongado las maquinas virtuales de tu infraestructura vmWare se apaguen ordenadamente por grupos y dejando para el final las maquinas mas críticas (los servidores de dominio).

Entorno

En mi caso dispongo de un SAI EATON que puede notifica a un software Intelligent Power Protector que puede lanzar un batch.

Solución

Me baso en el post http://www.virtu-al.net/2010/01/06/powercli-shutdown-your-virtual-infrastructure/ y algunos de los comentarios que allí hacen.
Coloco las maquinas que deben apagarse en varias carpetas o grupos según su importancia.
La idea es lanzar el siguiente script PowerShell desde un batch (powershell script.ps1)
if (!(get-pssnapin -name VMware.VimAutomation.Core -erroraction silentlycontinue)) {
     add-pssnapin VMware.VimAutomation.Core
 }
Set-PowerCLIConfiguration -DisplayDeprecationWarnings $false -Confirm:$false 

function ApagaMaquinasCarpeta 
#Ordena el apagado de las $maquinas en un grupo (host, carpeta, cluster,...etc) y espera un $tiempo dado 
{
 write $Carpeta
 $Maquinas=Get-Folder -Name $Carpeta 
 Foreach ($VM in ($Maquinas |Get-VM  )){
      Shutdown del invitado
     $VM | Shutdown-VMGuest -Confirm:$false
 }
 #Bucle esperando que apague
 $Inicio= (Get-Date).TimeofDay
 do {
   sleep 1.0
  $Lapso=(Get-Date).TimeofDay - $Inicio
     $Quedan = $Tiempo - ($Lapso.TotalSeconds)
  write  $quedan
  $numvms = ($Maquinas |  Get-VM | Where { $_.PowerState -eq "poweredOn"} ).count
  write "Esperando que se apaguen $numvms VMs o que transcurran $Quedan segundos"
  } until ((@($Maquinas |  Get-VM | Where { $_.PowerState -eq "poweredOn"} ).count -eq 0) -or ($Lapso).TotalSeconds -ge $Tiempo)
 write "Maquinas apagadas o plazo consumido ($Lapso)"
}
 clear 
$VCenter= "vcenter.empresa.org"
 
Connect-VIServer $VCenter
$Hosts= Get-VMHost
$Tiempo=1
$Carpeta= "Apagado Directo"
ApagaMaquinasCarpeta 
$Tiempo=20
$Carpeta= "Escritorios"
ApagaMaquinasCarpeta 
$Tiempo=300
$Carpeta= "Servidores"
ApagaMaquinasCarpeta 
$Tiempo=300
$Carpeta= "Ultimo apagado"
ApagaMaquinasCarpeta 
$ESXSRV = Get-VMHost
$ESXSRV | Foreach {Get-View $_.ID} | Foreach {$_.ShutdownHost_Task($TRUE)}
Disconnect-VIServer -server $VCenter -Confirm:$false
write "Fin"
#Apago la estación IPP
Stop-Computer

Comentarios

Eaton suministra un modulo para instalar en la infraestructura, pero me gusta mas esta solución porque puedo verla, probarla y guardar un archivo de seguimiento (redireccionando powershell).

Alguien se preguntará el por qué no uso parámetros de función en lugar de variables globales: no funcionaban, tenia prisa y trabajar con mandatos del tipo shutdown me pone muy nervioso.
En primer lugar porque un error te tira todas las maquinas, pero también porque la actualización ESXi 5.0 me había dejado sin módulos de shutdown, en el agosto mas caluroso desde hace años y podían producirse cortes de suministro por el elevado consumo de aire acondicionado.

viernes, 13 de julio de 2012

Descargar un informe desde Report Server en PDF

Enunciado 

Usando Visual Basic.NET, descarga un informe desde Report Server que puede ser utilizado para mandarlo por correo o generar una descarga por web.

Solución

Es tan simple como esto:
Private Function solicitaInformeSSRS(idRespuesta As Integer) As Byte()
        Dim informe As New ServerReport
        With informe
            .ReportServerUrl = New Uri(My.Settings.urlServerReport)
            .ReportPath = My.Settings.SSRSEncuestaPromocion
            .ReportServerCredentials = New MyReportServerCredentials
            .SetParameters(New ReportParameter("idRespuesta", idRespuesta))
            .Refresh()
            Return .Render("PDF")
        End With
End Function
Hay que utilizar la clase  MyReportServerCredentials que puedes encontare en mi post Autentificación Reporting Services

Usos:

Una vez descargado el array de byte, puedes utilizarlo para adjuntarlo en un email como un adjunto:

 msj.Attachments.Add(New Attachment(New System.IO.MemoryStream(informe),"informe.pdf")) 
O entregarlo en el stream de salida de una página (descarga del PDF):
Response.Clear()
Response.ContentType = "Application/PDF"
Response.AddHeader("content-disposition", String.Format("attachment; filename={0}.PDF", "archivo.PDF"))
Response.BinaryWrite(bytes)

Comentario:

Usar informes del servidor resulta mas simple, y no digamos ya si existen subinformes, ver Subinformes RDLC en Report cargado dinámicamente.

miércoles, 11 de julio de 2012

Lanzar eventos desde un control de usuario ASP.NET

Enunciado

Una forma muy cómoda de comunicar un control de usuario (.ascx) con la página u otro control de usuario que le contiene es definir un evento, lanzarlo y recogerlo.

Por ejemplo:
Un control de usuario realiza un búsqueda y la visualiza en un dataGrid.
Cuando el usuario elige un comando en una fila del datagrid el control de usuario lanza un evento informando con un texto y un identificador.

Solución:

Un pequeño truco, en el data grid usamos un comando con nombre y argumento.

   
        
           
         
         
   

En el código del control de usuario definiremos el evento y lo lanzaremos en dos casos:
  1. Con el evento RowCommand del datagrid
  2. Al hacer en click en cancelar (un botón)

Public Event eleccion As System.EventHandler

Private Sub gvActiv_RowCommand(
                        sender As Object, 
                        e As GridViewCommandEventArgs
                        ) Handles gvActiv.RowCommand
        RaiseEvent eleccion(Me, New ArgumentoObjetoElegido(e))
End Sub

Protected Sub LBCancelar_Click(
                        sender As Object,
                        e As EventArgs) Handles LBCancelar.Click
        RaiseEvent eleccion(Me, New ArgumentoObjetoElegido)
End Sub
Observar que como segundo argumento al lanzar el evento (RaiseEvent) hemos usado la clase ArgumentoObjetoElegido que hereda de EventArgs.
Esta clase puede crearse con y sin los argumentos. En caso de usar argumentos estos son del tipo GridViewCommandEventArgs.
Public Class ArgumentoObjetoElegido
    Inherits EventArgs
    Public Property Identificador As Integer = 0
    Public Property Descripcion As String = "(Nada seleccionado)"

    Public Sub New(Argumentosfila As GridViewCommandEventArgs)
        Identificador = CInt(Argumentosfila.CommandArgument)
        Descripcion = Argumentosfila.CommandName
    End Sub
    Public Sub New()
        'sin argumentos, valores iniciales arriba
    End Sub
End Class
Finalmente hay que recoger el evento en la página principal:
Private Sub EligeActiv_eleccion(sender As Object, e As ArgumentoObjetoElegido) Handles EligeActiv.eleccion
        If e.Identificador = 0 Then
            'Codigo para procesar cancelación
            If controlador.ActividadEnCurso.identificador = 0 Then
                controlador.ControlMenos()
            Else
                EstableceVista(VistaActividad)
            End If
        Else
            'codigo para procesar una elección el grid
            controlador.ActividadEnCurso.identificador = e.Identificador
            controlador.ActividadEnCurso.Descripcion = e.Descripcion
            EstableceVista(VistaActividad)
        End If
    End Sub

lunes, 9 de julio de 2012

WSUS: problema al conectarse a la base de datos el servicio de red

Sintomas

El servidor de actualizaciones (WSUS) no arranca. aparecen varios errores en el registros de eventos, el más destacado e indicativo es:

Tipo de suceso: Errores
Origen del suceso: MSSQL$MICROSOFT##SSEE
Categoría del suceso: (4)
Id. suceso: 18456
Fecha: 09/07/2012
Hora: 14:05:32
Usuario: NT AUTHORITY\Servicio de red
Equipo: W2K3R2
Descripción:
Login failed for user 'NT AUTHORITY\Servicio de red'. [CLIENT: ]

Solución:

Necesitarás las herramientas de administración de SQL Server.
Arranca el SQL Server Configuration Manager
Habillita las canalizaciones con nombre en configuración de red de SQL Server.../Protocolos de Microsoft##SSEE

De paso copiate al portapapeles el nombre de la canalización
\\.\pipe\MSSQL$MICROSOFT##SSEE\sql\query
Que encontraras en las propiedades de las canalizaciones con nombre

Ahora con SQL Server Managemente Studio
Conectate al motor de la base de datos
\\.\pipe\MSSQL$MICROSOFT##SSEE\sql\query
Si me has hecho caso, te bastarácon pegarlo.

Abre las propiedades de la base de datos SUSDB y activale la opción de multiusuario (estará en usuario único)

Comentario:

Encontré la pista en http://steili.com/2012/06/19/wsus-3-0-sp2-will-not-run-after-installing-update-2720211/ aunque en su caso no había funcionado. El habla de que el problema estaba en una instalación fallida de la KB2720211, pero en mi caso tengo que reconocer que se llenó el disco. Y eso que es una VM y tengo suficiente espacio. Pero es un crimen.



viernes, 6 de julio de 2012

Autocompletar un cuadro de texto en jQuery Mobile

Enunciado:

Tenemos la clásica búsqueda con un cuadro de texto al que normalmente desplegaríamos un lista de sugerencias a medida que escriben. En una aplicación de escritorio ASP.NET usariamos el componente autocomplete de AJAX.NET o de jQuery UI. Pero ¿En una aplicación móvil?

Solución:

Usar jQuery y el componente jQM Autocomplete de Andy Matthews.
Si haces clic en el vínculo llegaras a una demo y te enamoraras como lo hice yo.

Desarrollo de la solución

Necesitaras un servicio JSON que te entregue las sugerencias de palabras. Si desarrollas en ASP.NET puedes consultar mi post Desarrollo de un servicio json sobre ASP.NET para consumo desde Jquery
En mi caso además estaba buscando nombres de empresa en una base de datos por lo que dispongo del identificador único de registro.
Por eso devuelvo un array de objetos con dos propiedades de texto "label" y "value" .
Esta es la respuesta JSON:

[{"label":"INCOAZUL","value":"353"},{"label":"AZULIBER","value":"18"},{"label":"AZULEJERA ALCORENSE","value":"62"},{"label":"MAYOLICA AZULEJOS, S.L.","value":"60"},{"label":"AZULEJOS ALCOR","value":"61",{"label":"AZULINDUS & MARTI, S.A.","value":"13"},{"label":"NOMAZUL, S.A.","value":"85"}}] Este un ejemplo código de página:

    Que completaríamos con esté código JavaScript
           $(document).ready( function (e) {
             $("#busqueda").autocomplete({
                 method: 'GET',
                 target: $('#sugerencias'),
                 source: "/MAC.svc/NomIdEmpresa",
                 link: 'ficha?id=',
                 minLength: 3
             });
         });
     
    
    Por supuesto en los encabezados debes incluir las referencias a las librerias y estilos jQuery, jQuery Mobile y jQM autocomplete.
    Observarás que en la parte java especifico un enlace ficha?id= que se completará al final con el campo value que me devuelve el JSON. Asi cuando clicken en INFOAZUL navegaremos hacia ficha?id=353.
    Sin embargo, esta no es la solución optima, lo mejor sería cambiar a una página móvil ya cargada y actualizarla con los datos leidos con un llamada a un servicio JSON datosEmpresa(idEmpresa). 
    Es mi próximo objetivo, así que....continuará en próximas entregas

    Desarrollo de un servicio json sobre ASP.NET para consumo desde Jquery

    Enunciado:

    Necesitamos un servicio web que responda en formato JSON para poderlo leer desde Jquery.

    En este ejemplo partiremos del resultado otro servicio web WCF "directorio", pero podríamos estar devolviendo los resultados de una consulta linq o cualquier otra fuente serializable

    Solución

    Añadir un Servicio WCF con AJAX habilitado.
    IMPORTANTE: En el Web.config sección system.servicemodel ELIMINAR o comentar las referencias a este servicio  . En mi caso que el servicio se llamaba MAC eliminé las líneas: el behavior  enablewebScript
    
    
    
          
            
                 
                
          
          
        
        
        
          
            
          
        
    

    Luego editamos el archivo de etiquetado (.svc) añadiendo el atributo Factory apundando a la clase System.ServiceModel.Activation.WebServiceHostFactory.
    <%@ ServiceHost Language="VB" Debug="true" Service="myAplication.MAC" CodeBehind="MAC.svc.vb" Factory="System.ServiceModel.Activation.WebServiceHostFactory" %>
    
    En el archivo de código MAC.svc.vb definiremos los metodos con el atributo WebInvoke especificando el método (GET o POST) y el formato de la respuesta (JSON).
    Imports System.ServiceModel
    Imports System.ServiceModel.Activation
    Imports System.ServiceModel.Web
    
    
    
    Public Class MAC
    
        
        
        Public Function NomIdEmpresa(term As String) As directorio.EmpresaIdentificada()
            'ahora llamo a la clase proxy de mi servicio WCF.
            Dim servicio = New directorio.IdirectorioClient
            Dim resultado As directorio.EmpresaIdentificada() = servicio.SugerenciasNombreIdEmpresa(term, 20) 
            servicio.Close()
            Return resultado
        End Function
    End Class
    

    jueves, 5 de julio de 2012

    Donde esta wsdl.exe

    Pregunta:

    Para generar clases proxy de servicios web necesitas usar wsdl.exe ¿Donde está?

    Solución:

    En  C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin 
    (x86 para sistemas X64)

    Comentario:

    Cada vez que tengo que usarlo no lo encuentro y es mas efectivo que el GUI.

    martes, 3 de julio de 2012

    Emulador para depurar web apps para iphone

    Situación

    Desarrollas una aplicación para móviles, la pruebas en varios teléfonos Android con varios navegadores (Dolphin, Navigator y Firefox). También en una tableta Android, y en el emulador del SDK Android.
    Por supuesto en todos los exploradores de escritorio habituales.
    Bueno pues cuando está en el aire, alguien te dice que falla en iPhone.
    ¿Te compras un iPhone o un Mac? :-(

    Solución

    Usas el emulador on-line http://www.testiphone.com/  

    Comentario

    ¡Gracias Joe!,  me sentía como Newton cuando descubrió la ley de la gravedad.

    sábado, 23 de junio de 2012

    Descargar una archivo ZIP de la web y procesarlo en memoria


    Enunciado

    Usando Visual Basic .NET, descargar un archivo comprimido desde un sitio web, descomprimirlo e importar el contenido, importando sus líneas en una base de datos. Todo ello sin guardar en disco una copia, esto es trabajando en memoria.

    Solución

    Se utiliza la librería dotnetzip   y la clase webclient de .net Framework

    Imports System.Web, System.IO, System.Net, Ionic.Zip, Microsoft.AnalysisServices
    
    Module Module1
        Sub ejemplo()
            Dim cliente As New WebClient
            Dim datos As Byte()
            datos = cliente.DownloadData(My.Settings.CaminoZIP)
            Dim cola = New MemoryStream(datos)
            Dim comprimido = ZipFile.Read(cola)
            Dim Entrada = comprimido.Entries(0)
            Dim colaMemoria As New MemoryStream
            Entrada.Extract(colaMemoria)
            colaMemoria.Position = 0
            Dim lector As New StreamReader(colaMemoria)
            lector.ReadLine()
            Do While Not lector.EndOfStream
                Dim lineaDatos = LeeLineaGas(lector.ReadLine, Today)
                If lineaDatos IsNot Nothing Then 
                                        bd.ImportacionGas.InsertOnSubmit(lineaDatos)
            Loop
        End Sub
    end module
    

    Aclaración

    Si a alguien le intriga por qué etiqueto con SSAS,... porque los archivos descargados alimentan maravillosamente bases de datos SQL Server Analysis Services. Por ejemplo gracias a este código controlo la evolución precios y orígenes de las importaciones de gas españolas. Otra cosa es encontrar las fuentes que tiene su miga.

    domingo, 17 de junio de 2012

    Una escala de colores "redonda" para los mapas en Reporting Services

    Enunciado

    Las formas automáticas para definir la escala de colores en los mapas de reporting services tienen un defecto: los máximos y mínimos de cada rango pueden ser algo así como:

    • De 567 a 1002
    • De 1002 a 3141    
    • De 3141 a 9999
    • De 9999 a 20010
    En este post copio un código que genera una escala:

    • De 500 a 1000
    • De 1000 a 3000    
    • De 3000 a 10000    
    • De 10000 a 30000
    Cada color contiene "mas o menos" la misma cantidad de elementos.

    Solución

    Hay que hacer varias cosas:
    1. Evaluar la sentencia del informe (en mi código una sentencia MDX, pero no es necesario podría ser SQL)
    2. Para que haya la misma cantidad de países en cada color usamos percentiles. En el código que muestro hay diez colores, por tanto uso deciles (uso un código similar al de mi post Cuartiles en SSAS, pero es aplicable con otras formas de obtener los datos, i.e. SQL)
    3. A medida que los obtenemos redondeamos el dígito mas alto esto es de 9.999.999 "redondeamos a 10.000.000 y lo guardamos.
    4. Cuando terminamos, comprobamos si hay dos valores iguales y lo resolvemos(lo puede haber provocado el redondeo). Por ejemplo 10000 a 20000,  20000 a 20000, 20000 a 30000 pasaría a ser 10000 a 20000 20000 a 25000 25000 a 30000.
    5. Modificamos la definición del informe (ver mi post sobre cómo hacerlo)
    Bien, este es el código:
    Public Function RangosMapa(s As String) As List(Of Double)
            Dim r As New List(Of Double)
            Dim isov As String()
            If Ambito = "M" Then
                isov = IsoEsriMundo 'lista de ISO2 paises del mundo
            Else
                isov = IsoEsriEuropa 'Lista de ISO2 paises de Europa
            End If
            Dim lista As New List(Of Double)
    
            'Calculamos la sentencia en el informe
            Dim cn As New AdomdConnection(My.Settings.SSASConexion)
            cn.Open()
            Dim cmd As AdomdCommand = cn.CreateCommand()
            cmd.CommandText = evaluaSentencia(s)
    
            Dim lector = cmd.ExecuteReader()
            Do While lector.Read
                If isov.Contains(lector.GetString(0)) Then lista.Add(lector.GetDecimal(4))
            Loop
            lector.Close()
            cn.Close()
            cn.Dispose()
    
            'recorremos lo deciles
            lista.Sort()
            r.Add(Piso(lista(0)))
            Dim paso = lista.Count / 10
            For i = paso To lista.Count - 2 Step paso
                Dim valor = lista(Math.Round(i) - 1)
                Dim redondo = Redondeo(valor)
                If redondo > r(r.Count - 1) Then
                    r.Add(Redondeo(lista(Math.Round(i) - 1)))
                Else
                    r.Add(Techo(valor))
                End If
            Next
            r.Add(Techo(lista(lista.Count - 1)))
            'Si hay valores duplicados partimos por la mitad.
            Dim cambio As Boolean
            Do
                cambio = False
                For i = 1 To r.Count - 1
                    If r(i) = r(i - 1) Then
                        r(i) = (r(i - 1) + r(i + 1)) / 2
                        cambio = True
                    End If
                Next
            Loop Until Not cambio
            Return r
        End Function
    'Funciones de redondeo
        Public Function CalculoFactor10(n As Double) As Integer
            Return Math.Pow(10, Math.Round(Math.Log10(n)))
        End Function
        Public Function Piso(n As Double) As Double
            If RedondeoSuperior Then
                Dim factor10 = CalculoFactor10(n)
                Return Math.Floor(n / factor10) * factor10
            Else
                Return Math.Floor(n)
            End If
    
        End Function
    
        Public Function Techo(n As Double) As Double
            If RedondeoSuperior Then
                Dim factor10 = CalculoFactor10(n)
                Return Math.Ceiling(n / factor10) * factor10
            Else
                Return Math.Ceiling(n)
            End If
        End Function
    
        Public Function Redondeo(n As Double) As Double
            If RedondeoSuperior Then
                Dim factor10 = CalculoFactor10(n)
                Return Math.Round(n / factor10) * factor10
            Else
                Return Math.Round(n)
            End If
        End Function
    'parametrización del mapa
     Public Function ParametrizaMapa(informe As XDocument, rangosMapa As List(Of Double)) As XDocument
            Dim df = informe.Root.Name.Namespace
            Dim mapa = informe.Root.Element(
                df + "ReportSections").Element(
                df + "ReportSection").Element(
                df + "Body").Element(
                df + "ReportItems").Element(
                df + "Map")
            Dim capa = mapa.Element(df + "MapLayers").Element(df + "MapPolygonLayer")
            capa.Element(df + "MapPolygons").ReplaceNodes(Poligonos.Nodes)
            Dim reglasColor = capa.Element(df + "MapPolygonRules").Element(df + "MapColorRangeRule")
            Dim culturaUSA = New System.Globalization.CultureInfo("en-US")
            reglasColor.Element(df + "DistributionType").Value = "Custom"
            Dim i = 0
            For Each elemento In reglasColor.Element(df + "MapBuckets").Elements
                elemento.Element(df + "StartValue").Value = rangosMapa(i).ToString(culturaUSA.NumberFormat)
                elemento.Element(df + "EndValue").Value = rangosMapa(i + 1).ToString(culturaUSA.NumberFormat)
                    i += 1
            Next
            Return informe
        End Function
    
    Si alguien necesita mas detalles, estoy a su disposición

    Autentificación Reporting Services en VB.NET

    Enunciado

    Construir una clase para autentificarse ante un servidor de informes de Microsoft (SSRS) puede ser útil para:
    1. Utilizar el visor de informes con Informes de servidor (processingMode=Remote)
    2. Utilizar servicios web para Cargar y modificar la definición de informes

    Solución

    Crearemos la siguiente clase:>

    Imports System.Net
    Imports System.Security.Principal
    Imports Microsoft.Reporting.WebForms
     _
    Public NotInheritable Class MyReportServerCredentials
        Implements IReportServerCredentials
    
        Public ReadOnly Property ImpersonationUser() As WindowsIdentity _
                Implements IReportServerCredentials.ImpersonationUser
            Get
    
                'Use the default windows user.  Credentials will be
                'provided by the NetworkCredentials property.
                Return Nothing
    
            End Get
        End Property
    
        Public ReadOnly Property NetworkCredentials() As ICredentials _
                Implements IReportServerCredentials.NetworkCredentials
            Get
    
                'Read the user information from the web.config file.  
                'By reading the information on demand instead of storing 
                'it, the credentials will not be stored in session, 
                'reducing the vulnerable surface area to the web.config 
                'file, which can be secured with an ACL.
    
                'User name
                Dim userName As String = My.Settings.usuario
    
                If (String.IsNullOrEmpty(userName)) Then
                    Throw New Exception("Missing user name from web.config file")
                End If
    
                'Password
                Dim password As String = My.Settings.password
    
                If (String.IsNullOrEmpty(password)) Then
                    Throw New Exception("Missing password from web.config file")
                End If
    
                'Domain
                Dim domain As String = My.Settings.dominio
    
                If (String.IsNullOrEmpty(domain)) Then
                    Throw New Exception("Missing domain from web.config file")
                End If
    
                Return New NetworkCredential(userName, password, domain)
    
            End Get
        End Property
    
        Public Function GetFormsCredentials(ByRef authCookie As Cookie, _
                                            ByRef userName As String, _
                                            ByRef password As String, _
                                            ByRef authority As String) _
                                            As Boolean _
                Implements IReportServerCredentials.GetFormsCredentials
    
            authCookie = Nothing
            userName = Nothing
            password = Nothing
            authority = Nothing
    
            'Not using form credentials
            Return False
    
        End Function
    
    End Class
    

    Para usarla en el visor de informes:

    Protected Sub Page_Init(ByVal sender As Object, _
                                  ByVal e As System.EventArgs) _
                                  Handles Me.Init
            Visor.ServerReport.ReportServerUrl = New Uri(My.Settings.SSRS)
            Visor.ServerReport.ReportServerCredentials = _
                New MyReportServerCredentials()
        End Sub
    

    Para usarla en los web Services

            Dim SSRS As New ReportingService2010
            SSRS.Credentials = (New MyReportServerCredentials).NetworkCredentials
            Dim informe = SSRS.GetItemDefinition(My.Settings.mapas + nombre)
            SSRS = Nothing
    

    Cargar y modificar un informe de Report Server

    Enunciado

    En ocasiones puede resultar interesante leer un informe definido en el servidor de  Microsoft Reporting Services, modificar la definición y volver subirlo al servidor para visualizar en un webform Report viewer.
    Redactaré mas adelante algunos ejemplos de aplicación, por el momento.

    Solución

    El problema se divide en tres partes:

    1. Leer la definición del informe.
    2. Modificar el XML de esta definición mediante Linq to XML
    3. Establecer la definición modificada en el report Viewer.

    Leer la definición del informe desde el servidor


    Hay que acceder a los servicios web de Reporting Services, para ello se crea una un clase proxy mediante WSDL.EXE y una clase con las credenciales necesarias para autentificarse.
    Una vez hecho esto es relativamente fácil.
     Private Function CargaInforme(nombre As String) As XDocument
            Dim SSRS As New ReportingService2010
            SSRS.Credentials = (New MyReportServerCredentials).NetworkCredentials
            Dim informe = SSRS.GetItemDefinition(My.Settings.mapas + nombre)
            SSRS = Nothing
            Dim stream = New System.IO.MemoryStream(informe)
            Dim lector = System.Xml.XmlReader.Create(stream)
            Dim doc As XDocument = XDocument.Load(lector, LoadOptions.None)
            Return doc
        End Function
    

    En breve, el servicio veb me devuelve un array de bytes, que traslado a un memory stream que a su vez me permite crear un xdocument.

    Modificar la definición de informe

    Esto es lo mas fácil, aunque a veces engorroso. Por ejemplo vamos a cambiar la definición del Dataset "datos" en el informe.
    Dim df = doc.Root.Name.Namespace
                Dim xdatos = (From f In doc.Root.Element(df + "DataSets").Elements(df + "DataSet")).First(
                Function(x As XElement) x.Attribute("Name") = "Datos").Element(df + "Query").Element(df + "CommandText")
                xdatos.SetValue(s)
    
    S es un string con la sentencia SQL, MDX o lo que quiera que utilices, por supuesto tiene que se coherente con el resto del informe en lo que atañe a nombres de campo, parametros,...etc

    Visualizar el informe modificado en Report Viewer

    Para hacerlo necesitaremos convertir nuestro Xdocument que contiene la definición en un stream:
     Private Function XMLBytes(doc As XDocument) As System.IO.Stream
            Dim stream As New System.IO.MemoryStream
            Dim escritor = System.Xml.XmlWriter.Create(stream)
            doc.Save(escritor)
            escritor.Close()
            stream.Position = 0
            Return stream
        End Function
    
    Lo que sigue es asignar este stream al visor de informes, establecer los parámetros que hagan falta y refrescar
    Visor.ServerReport.LoadReportDefinition(e.Informe)
    Visor.ServerReport.SetParameters(New ReportParameter("miParametro", suValor))
    Visor.ServerReport.Refresh()
    

    viernes, 15 de junio de 2012

    Determinar el path a un archivo en global.asax

    Enunciado:
    Durante el arranque de la aplicación en global.asax quieres leer una archivo de configuración controles.xml almacenado en el raíz.
    ¿cuál es el path?
    Solución

    'Separo en dos líneas para facilitar la lectura
    Dim nombreArchivo=Server.MapPath("Global.asax").Replace("Global.asax", "controles.xml")
    Dim cfg = XElement.Load(nombreArchivo)
    

    jueves, 14 de junio de 2012

    Publicar código de programación en blogger usando plantilla de vistas dinámicas

    Problema: 
    Los informáticos solemos publicar código HTML, XML, Java, SQL, Visual Basic en nuestros blogs.
    La forma habitual es utilizar SyntaxHighlighter que es el estandar de facto.
    Para incorporarlo se edita el HTML de la plantilla de blogger.
    El problema surge cuando queremos usar las plantillas de vistas dinámicas porque en estas no se puede editar el HTML.
    Solución:
    No voy a entrar en detalles solo daré la idea base y dos enlaces:
    Idea base:
    1. Incluir los estilos CSS en personalización de la plantilla/Avanzado/CSS
    2. Incluir en cada post la carga de scripts de autocarga, por ejemplo:
    Yo utilizo el script de autocarga de Kobe Wuyts porque carga todos los lenguajes, hay otras alternativas con menos lenguajes, por ejemplo la de los creadores de la idea original crux-framework.tools

    Borrar cookies desde ASP.NET (por ejemplo de autenticación por formularios)

    Problema:
    Sigo las instrucciones en http://msdn.microsoft.com/es-es/library/ms178195(v=vs.80).aspx pero la cookie no desaparece.
    Solución:
    Establece también el path y el dominio
    El código completo sería:
                        Dim myCookie As HttpCookie
                        myCookie = New HttpCookie("miGalleta")
                        myCookie.Path = "/"
                        myCookie.Domain = ".midominio.es"
                        myCookie.Expires = DateTime.Now.AddDays(-1D)
                        Response.Cookies.Add(myCookie)
    
    
    Esto es particularmente interesante para omitir las cookies de la autenticación por formulario, como las del pobre Forefront TMG.

    lunes, 23 de abril de 2012

    Cuartiles en SSAS

    Problema:
     Obtener los cuartiles en un cubo Microsoft Analysys Services.
    Solución: La función para calcular cuartiles no existe en MDX. Existen varias formas de calcularlos como puede leerse en este interesante articulo de ElectroVoid, pero la optima es usar una procedimiento almacenado SSAS.
    Hay que desarrollarlo en un lenguaje .NET, en el articulo mencionado utilizan C#, yo aportó aquí el código VB.NET.

    Imports Microsoft.AnalysisServices
    Namespace Percentiles
        Public Class Percentil
             _
            Public Shared Function Cuartil(ByVal conjunto As AdomdServer.Set, _
                                                ByVal expr As AdomdServer.Expression, _
                                                ByVal Qtile As Integer) As AdomdServer.MDXValue
    
                If Qtile < 0 Or Qtile > 4 Then Throw New ArgumentOutOfRangeException(Qtile)
                Dim valores = New List(Of Double)
                For Each tupla In conjunto.Tuples
                    Dim mdxVal = expr.Calculate(tupla)
    
                    If mdxVal.ToChar <> ChrW(0) Then valores.Add(mdxVal.ToDouble)
                Next
    
                If valores.Count > 0 Then
                    valores.Sort()
                    Dim cuenta = valores.Count
                    If (Qtile = 4) OrElse (cuenta = 1) Then Return (AdomdServer.MDXValue.FromDouble(valores.Item(cuenta - 1)))
                    Dim porcentaje As Double = CDbl(Qtile) / 4.0
                    Dim qidx As Double = porcentaje * CDbl(cuenta - 1)
                    Dim qfloor = Math.Floor(qidx)
                    Dim qfrac = qidx - qfloor
                    Return AdomdServer.MDXValue.FromDouble( _
                                                            valores.Item(CInt(qfloor)) _
                                                            + (qfrac * (valores.Item(CInt(qfloor + 1)) _
                                                                        - valores.Item(CInt(qfloor)) _
                                                              )) _
                                                            )
    
                Else
                    Return Nothing
                End If
    
            End Function
             _
            Public Shared Function CuartilOrdenado(ByVal conjunto As AdomdServer.Set, _
                                                ByVal expr As AdomdServer.Expression, _
                                                ByVal Qtile As Integer) As AdomdServer.MDXValue
                'En esta versión la llamada debe realizarse con el conjunto ordenado y sin valores vacios
    
                If Qtile < 0 Or Qtile > 4 Then Throw New ArgumentOutOfRangeException(Qtile)
                Dim valores = New List(Of Double)
                For Each tupla In conjunto.Tuples
                    Dim mdxVal = expr.Calculate(tupla)
                    valores.Add(mdxVal.ToDouble)
                Next
    
                If valores.Count > 0 Then
                    Dim cuenta = valores.Count
                    If (Qtile = 4) OrElse (cuenta = 1) Then Return (AdomdServer.MDXValue.FromDouble(valores.Item(cuenta - 1)))
                    Dim porcentaje As Double = CDbl(Qtile) / 4.0
                    Dim qidx As Double = porcentaje * CDbl(cuenta - 1)
                    Dim qfloor = Math.Floor(qidx)
                    Dim qfrac = qidx - qfloor
                    Return AdomdServer.MDXValue.FromDouble( _
                                                            valores.Item(CInt(qfloor)) _
                                                            + (qfrac * (valores.Item(CInt(qfloor + 1)) _
                                                                        - valores.Item(CInt(qfloor)) _
                                                              )) _
                                                            )
    
                Else
                    Return Nothing
                End If
    
            End Function
        End Class
    End Namespace
    
    Observad que he creado dos metodos, en el primero incluyo el ordenado y la eliminación de elementos vacios, en el segundo espero recibir el conjunto con estas tareas realizadas.
    percentiles.cuartilOrdenado(
                ORDER(
                      NONEMPTY 
                         (descendants([DimEmpresas].[Facturacion].currentmember, [DimEmpresas].[Facturacion].[Empresa])
                          ,[Measures].[% A-1 VN Euros]
                          )
                      ,[Measures].[% A-1 VN Euros]
                      ,BASC
                      )
                ,[Measures].[% A-1 VN Euros]
                ,3)
    
    Para desarrollar la aplicación instalar SSAS en la máquina de desarrollo y añadi una referencia a msmgdsrv.dll en C:\Archivos de programa\Microsoft SQL Server\MSAS10_50.MSSQLSERVER\OLAP\bin
    Luego añadir el ensamblado al servidor SSAS, a la base de datos o al proyecto (ver msdn)

    Si alguien necesita la ddl que me lo diga.<(p>

    jueves, 12 de abril de 2012

    Crear una tabla desde una lista separada por comas

    La siguiente función devuelve una tabla desde una lista de números separados por comas:
    FUNCTION [dbo].[fnSplitCSVxml] 
                   (@NumberList VARCHAR(4096)) 
    RETURNS @SplitList TABLE( ListMember INT) 
    AS 
      BEGIN 
        DECLARE  @xml XML 
        SET @NumberList = LTRIM(RTRIM(@NumberList)) 
        IF LEN(@NumberList) = 0 
          RETURN 
        SET @xml = '' + REPLACE(@NumberList,',','') + '' 
        INSERT INTO @SplitList 
        SELECT x.i.value('.','VARCHAR(MAX)') AS Member 
        FROM   @xml.nodes('//n') x(i) 
        RETURN 
      END 
    

    Tiene diversas aplicaciones del tipo:
    SELECT A from B Where B.c in dbo.fnSplitList(@cadena)
    Pero es especialmente interesante para informes de Report Server que usen parametros con multiples valores.