Archivo de Mayo 2009


Obtener la ciudad y el país de una dirección IP

Domingo, 24 May, 2009 @ 15:24 | Por Gustavo Cantero (The Wolf) | ASP.NET, GIS

Muchos habrán notado que al ingresar en algunos sitios web éstos modifican su contenido, incluyendo idioma, publicidad, textos, etc., dependiendo del país desde el que estamos ingresando, por ejemplo, al ingresar en www.linksys.com desde Argentina éste nos redirecciona a www-ar.linksys.com, o al ingresar a www.google.com nos reenvía a www.google.com.ar. Para poder hacer esto la aplicación web busca la IP con la que estamos ingresando en una base de datos donde previamente se cargaron rangos de IP con su respectivo país.

Hasta acá todo parece muy obvio, el problema surge cuando nos preguntamos: ¿Dónde conseguimos estas bases de datos? La respuesta es sencilla: en internet.

En internet hay varios sitios que ofrecen, gratuitamente o como servicio pago, archivos con rangos de IP con su respectivo país. Muchos de estos sitios ofrecen además web services para poder consultar contra su base de datos la ubicación correspondiente a una dirección IP sin necesidad de tener los datos alojados en nuestro sitio. En este artículo voy a mostrar cómo obtener la ubicación consultando los datos en una base de datos local.

Uno de los sitios desde donde se pueden bajar estos datos es WebHosting.Info (www.webhosting.info), un sitio dedicado a dar servicios y estadísticas sobre ISP, WHOIS, Reverse IP Lookup, etc. Los datos que estos sitios ofrecen casi siempre están en archivos CSV, donde el país está codificado en ISO 3166 y la dirección IP está como un entero sin signo de 4 bytes llamado “número IP”.

Para obtener la lista de códigos de países según la ISO 3166 pueden ver en el sitio mismo de ISO, http://www.iso.org/iso/country_codes, o en Wikipedia, http://es.wikipedia.org/wiki/ISO_3166-1.
Para convertir la dirección IP en un número IP (un entero de 4 bytes) es una simple fórmula matemática donde lo que tenemos que hacer es un “shift left” de cada número para “posicionarlo” en el octeto correspondiente o, en español, moverlo hasta el byte correspondiente. Como cada byte representa 256 números (de 0 a 255), lo que debemos hacer es multiplicar cada octeto de la dirección por 256 elevado a la posición de ese byte. Supongamos que nuestra dirección a convertir es A.B.C.D, entonces lo que debemos hacer es:

(A * 256^3) + (B * 256^2) + (C * 256^1) + (D * 256^0)

Si reemplazamos las constantes por su resultado nos quedaría lo siguiente:

(A * 16777216) + (B * 65536) + (C * 256) + D

Ahora supongamos que la IP de mi cliente es la 190.2.8.88, entonces deberíamos hacer:

(190 * 16777216) + (2 * 65536) + (8 * 256) + 88 =
   3187671040    +   131072    +    2048   + 88 = 3187804248

Cabe mencionar que para obtener la dirección IP del usuario que se está conectando a nuestro sitio se debería leer el encabezado de la petición http. Para los desarrolladores de .NET esto se puede hacer simplemente utilizando la propiedad UserHostAddress del objeto Request del Page, por ejemplo, en C# sería:

string strIP = Page.Request.UserHostAddress;

o, para los usuarios de Visual Basic.NET:

Dim strIP as string = Page.Request.UserHostAddress

Obviamente la IP está en un string, con lo cual debería dividir cada octeto de la dirección, convertirlo a un tipo de dato numérico y hacer la fórmula. A continuación les dejo un ejemplo en C#:

//Convierto la IP a un entero de 32 bits
string[] strBytes = Page.Request.UserHostAddress.Split('.');
uint intNumeroIP=
    (uint.Parse(strBytes[0]) * 16777216) +
    (uint.Parse(strBytes[1]) * 65536) +
    (uint.Parse(strBytes[2]) * 256) +
    uint.Parse(strBytes[3]);

y, para los usuarios de Visual Basic.NET:

'Convierto la IP a un entero de 32 bits
Dim strBytes As String() = Page.Request.UserHostAddress.Split('.')
Dim intNumeroIP As UInteger = _
    (UInteger.Parse(strBytes(0)) * 16777216) + _
    (UInteger.Parse(strBytes(1)) * 65536) + _
    (UInteger.Parse(strBytes(2)) * 256) + _
    UInteger.Parse(strBytes(3))

Nótese que en el ejemplo utilicé un entero sin signo, esto es porque el número IP no lleva signo. También cabe notar que, aunque cada octeto se puede representar con un byte, yo lo convierto a un entero sin signo para evitar la conversión del tipo de dato al almacenarlo en la variable final.

Ahora con este número en mente lo podemos buscar en el archivo con la información de la localización de las IPs.  Obviamente lo mejor es guardar previamente la información de este archivo en una base de datos. Como mínimo deberíamos guardar la dirección IP y el país. Muchos de estos archivos de IP nos traen una “dirección desde” y una “dirección hasta” por cada línea del archivos, pero con almacenar la “dirección desde” ya nos alcanza.
Para los usuarios de motores de bases de datos como Oracle o MySql no tendrán problema con guardar los números IP en campos enteros de 4 bytes sin signo, pero para los usuarios de SQL Server se nos complica, ya que este motor no permite utilizar este tipo de datos. Para solucionar este inconveniente podemos hacer dos cosas: guardar el número en un campo del tipo entero de 8 bytes o a cada número IP restarle 2147483648.

Ahora sólo nos quedaría buscar la IP del usuario en la base, con un script sencillo como el siguiente para SQL Server:

SELECT TOP 1 Pais
FROM ListaIPs
WHERE IP >= 3187804248
ORDER BY IP DESC

Si en SQL Server elegimos utilizar un entero de 4 bytes (int) y restarle la mitad para no utilizar un entero de 8 bytes (bigint) habría que utilizar el valor 1040320600 en lugar de 3187804248.

Si quisiéramos hacer esta misma consulta pero utilizando MySql como motor de base de datos deberíamos utilizar un script como el siguiente:

SELECT Pais
FROM ListaIPs
WHERE IP >= 3187804248
ORDER BY IP DESC
LIMIT 1

Bien, hasta acá ya sabemos cómo obtener el país correspondiente a una dirección IP pero, como el título bien dice, también podemos obtener la ciudad correspondiente a esta dirección. Para esto la solución vuelve a ser sencilla, solamente tenemos que buscar en internet donde se puede conseguir una lista de IPs correspondiente a cada ciudad.  Un lugar donde se puede obtener esta lista es en IP Location Tools (http://iplocationtools.com), donde se puede conseguir la lista de país por IP y ciudad por IP, en formato CSV y en SQL. La única diferencia con el proceso anterior es que en la base de datos, además o en lugar de guardar el país, se debería guardar la ciudad.

Espero que este artículo les haya sido de utilidad y, como siempre, los animo a escribir sus consultas, sugerencias y comentarios.

VN:F [1.7.3_972]
Rating: 9.3/10 (6 votos cast)

Nueva versión del ASP.NET AJAX Control Toolkit

Sábado, 16 May, 2009 @ 00:31 | Por Gustavo Cantero (The Wolf) | AJAX, ASP.NET

Este mes salió a la luz una nueva versión del ASP.NET AJAX Control Toolkit, y este nuevo release viene nuevos tutoriales, la corrección de más de 20 bugs y tres nuevos controles: HTMLEditor, ComboBox y ColorPicker.  Cabe mencionar que esta nueva versión sólo está disponible para .NET Framework 3.5 y Visual Studio 2008.

HTMLEditor

Gracias a la compañía Obout (www.obout.com), que hizo el desarrollo de este control, ahora es posible crear y editar contenido HTML fácilmente en el navegador, utilizando una barra de herramientas con varios botones para cambiar el estilo del contenido.

ajax-htmleditor

ComboBox

En este caso el agradecimiento va dirigido a Dan Ludwig, quien desarrolló el control ComboBox de esta librería, el cual combina una lista desplegable y un cuadro de texto, con la ayuda del conocido “autocomplete”.

ColorPicker

Por último nos queda el control creado por Alexander Turlov, el cual es un Extender que se puede adjuntar a un TextBox para agregarle la funcionalidad de poseer un popup para seleccionar un color.

Nuevos tutoriales

Los nuevos tutoriales, junto con los que ya existían, se pueden encontrar en la siguiente dirección: http://www.asp.net/learn/ajax/

VN:F [1.7.3_972]
Rating: 7.9/10 (8 votos cast)

Editores de WebParts contraibles

Miércoles, 06 May, 2009 @ 00:48 | Por Gustavo Cantero (The Wolf) | ASP.NET, WebParts

Hace un tiempo desarrollamos un tablero de control para un cliente, el cual nos pedía que éste fuera personalizable por el usuario, tanto en contenido y estética, como en la posición de los controles a visualizar, motivo por el cual nos decidimos a construirlo con WebParts. La primera versión tuvo buena aceptación y gustó mucho, por lo cual en la segunda versión nos pidieron que hubieran más componentes y que éstos tuvieran muchas más opciones de configuración. Hasta acá todo iba bien, pero nos dimos cuenta que al agregar muchos WebParts al catálogo éste se hacía incómodo de utilizar (el catálogo tiene más de 200 componentes), y al agregarle muchas opciones de edición a éstos componentes era incómoda la edición, ya que esta se hacía muy extensa y el usuario debía “scrollear” mucho la página. Para resolver estos inconvenientes hicimos dos nuevos controles que utilizamos en ese desarrollo: un catálogo de componentes que utiliza un árbol (del cual hablaré en otro artículo), y una zona de edición donde cada EditorPart es “contraíble” para ocupar menos espacio en la página. Sobre éste último hablaremos en este artículo.

La idea será hace una zona de edición que se asemeje a la imagen a continuación:

Para modificar la forma en que se van a ver los editores de los componentes hay que crear una nueva “zona de edición”, la cual debe heredar de EditorZone y llamaremos CollapsibleEditorZone. En esta clase cambiaremos el método que devuelve el “chrome” a utilizar en esta zona, el cual modificaremos para que le agregue la opción de contraer y expandir como se ve en la imagen anterior. A nuestro “chrome” lo vamos a llamar EditorPartChrome, por lo tanto, el código de la clase CollapsibleEditorZone debería quedar como se muestra en el código que se encuentra a continuación:

using System.Web.UI.WebControls.WebParts;

/// <summary>
/// Nuevo editor de webparts
/// </summary>
public class CollapsibleEditorZone : EditorZone
{
    /// <summary>
    /// Escribe el control
    /// </summary>
    /// <param name="writer">Objeto sobre el cual escribir</param>
    protected override void Render(System.Web.UI.HtmlTextWriter writer)
    {
        writer.Write("<span class="CollapsibleEditorZone">");
        base.Render(writer);
        writer.Write("</span>");
    }

    /// <summary>
    /// Obtiene una referencia a un nuevo objeto <see cref="CollapsibleEditorChrome"/>
    /// que se utiliza para representar los editores
    /// </summary>
    /// <returns>Un objeto <see cref="CollapsibleEditorChrome"/></returns>
    protected override EditorPartChrome CreateEditorPartChrome()
    {
		return new CollapsibleEditorChrome(this);
    }
}

Ahora que tenemos la nueva zona tenemos que crearle el “chrome” del que hablamos antes. Esta nueva clase debe heredar de EditorPartChrome y debe tener el constructor (para que utilice el de la clase base) y dos métodos: uno para escribir el contenido de cada editor a utilizar y otro para dibujar el título de cada uno de éstos, en donde van a estar los botones para contraer y expandir el contenido. Estos botones van a ejecutar un JavaScript que modifica el estilo “display” de un DIV que envuelve el editor, para evitar hacer postbacks innecesarios.

Pero no todo es tan sencillo como parece, supongamos que el usuario expande algunos editores, modifica algunos parámetros del componente y luego pulsa sobre “aplicar” para ver los cambios, la aplicación va a hacer un postback y aplicará los cambios sobre las propiedades de los WebParts, pero al volver a dibujar los editores éstos se verán contraídos nuevamente. Para solucionar esto agregamos un INPUT HIDDEN por cada editor donde guardamos del lado del cliente el último estado del editor, así cuando hacemos un postback simplemente tenemos que verificar el valor de este campo para saber si estaba expandido o contraído.

A continuación muestro el código de esta clase:

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;

/// <summary>
/// PartChrome de editores para hacerlos contraibles
/// </summary>
public class CollapsibleEditorChrome : EditorPartChrome
{
    /// <summary>
    /// Constructor del chrome
    /// </summary>
    /// <param name="Zone">Zona</param>
    public CollapsibleEditorChrome(CollapsibleEditorZone Zone)
        : base(Zone)
    {
    }

    /// <summary>
    /// Representa un control <see cref="EditorPart"/> completo con todas sus secciones.
    /// </summary>
    /// <param name="Writer">Objeto <see cref="HtmlTextWriter"/> que recibe el contenido de editorPart.</param>
    /// <param name="EditorPart">Control que se está procesando en la actualidad.</param>
    public override void RenderEditorPart(HtmlTextWriter Writer, EditorPart EditorPart)
    {
        if (EditorPart == null)
            throw new ArgumentNullException("EditorPart");

        PartChromeType enuPartChromeType = Zone.GetEffectiveChromeType(EditorPart);
        Style objPartChromeStyle = CreateEditorPartChromeStyle(EditorPart, enuPartChromeType);

        objPartChromeStyle.AddAttributesToRender(Writer, Zone);
        Writer.RenderBeginTag(HtmlTextWriterTag.Fieldset);

        if (enuPartChromeType == PartChromeType.TitleAndBorder || enuPartChromeType == PartChromeType.TitleOnly)
            RenderTitle(Writer, EditorPart);

        Style objPartStyle = Zone.PartStyle;
        objPartStyle.AddAttributesToRender(Writer, Zone);
        Writer.AddAttribute(HtmlTextWriterAttribute.Id, EditorPart.ClientID + "_div");
        if (EditorPart.Page.Request.Form.Get(EditorPart.ClientID + "_state") == "1")
            Writer.AddStyleAttribute(HtmlTextWriterStyle.Display, "inline");
        else
            Writer.AddStyleAttribute(HtmlTextWriterStyle.Display, "none");
        Writer.RenderBeginTag(HtmlTextWriterTag.Div);

        RenderPartContents(Writer, EditorPart);

        Writer.RenderEndTag(); // div
        Writer.RenderEndTag();// fieldset
    }

    /// <summary>
    /// Representa el título de un control <see cref="EditorPart"/>.
    /// </summary>
    /// <param name="Writer">Objeto <see cref="HtmlTextWriter"/> que recibe el contenido de editorPart.</param>
    /// <param name="EditorPart">Control que se está procesando en la actualidad.</param>
    private void RenderTitle(HtmlTextWriter Writer, EditorPart EditorPart)
    {
        string strDisplayTitle = EditorPart.DisplayTitle;

        if (!String.IsNullOrEmpty(strDisplayTitle))
        {
            Style objTitleTextStyle;

            TableItemStyle objPartTitleStyle = this.Zone.PartTitleStyle;
            Style objStyle = new Style();
            objStyle.CopyFrom(objPartTitleStyle);
            objTitleTextStyle = objStyle;

            objTitleTextStyle.AddAttributesToRender(Writer, Zone);

            string strDescription = EditorPart.Description;
            if (!String.IsNullOrEmpty(strDescription))
                Writer.AddAttribute(HtmlTextWriterAttribute.Title, strDescription);

            string strAccessKey = EditorPart.AccessKey;
            if (!String.IsNullOrEmpty(strAccessKey))
                Writer.AddAttribute(HtmlTextWriterAttribute.Accesskey, strAccessKey);

            Writer.RenderBeginTag(HtmlTextWriterTag.Legend);

            if (EditorPart.Page.Request.Form.Get(EditorPart.ClientID + "_state") == "1")
                Writer.AddAttribute(HtmlTextWriterAttribute.Src, EditorPart.ResolveClientUrl("~/Images/Minus.gif"));
            else
                Writer.AddAttribute(HtmlTextWriterAttribute.Src, EditorPart.ResolveClientUrl("~/Images /Plus.gif"));

            Writer.AddAttribute(HtmlTextWriterAttribute.Onclick, "collapseExpandEditor('" + EditorPart.ClientID + "');");
            Writer.AddAttribute(HtmlTextWriterAttribute.Width, "9px");
            Writer.AddAttribute(HtmlTextWriterAttribute.Height, "9px");
            Writer.AddAttribute(HtmlTextWriterAttribute.Align, "middle");
            Writer.AddStyleAttribute(HtmlTextWriterStyle.Cursor, "hand");
            Writer.AddAttribute(HtmlTextWriterAttribute.Id, EditorPart.ClientID + "_img");
            Writer.RenderBeginTag(HtmlTextWriterTag.Img);
            Writer.RenderEndTag(); // img
            Writer.AddAttribute(HtmlTextWriterAttribute.Name, EditorPart.ClientID + "_state");
            Writer.AddAttribute(HtmlTextWriterAttribute.Type, "hidden");
            Writer.AddAttribute(HtmlTextWriterAttribute.Value, EditorPart.Page.Request.Form.Get(EditorPart.ClientID + "_state"));
            Writer.RenderBeginTag(HtmlTextWriterTag.Input);
            Writer.RenderEndTag(); // input

            Writer.WriteFullBeginTag("B");
            Writer.Write(' ');
            Writer.Write(strDisplayTitle);
            Writer.WriteEndTag("B");
            Writer.RenderEndTag(); // legend
        }
    }
}

Cabe mencionar que para mostrar los botones de “más” y “menos” de cada editor la aplicación web debe tener las imágenes “plus.gif” y “minus.gif” dentro de la carpeta “images”.
Ahora faltaría mostrar el JavaScript que utiliza estas clases del lado del cliente para ocultar y mostrar los editores:

function collapseExpandEditor(id) {
    var editor = $get(id + '_div');
    var img = $get(id + '_img');
    var input = $get(id + '_state');
    if (editor.style.display == 'none') {
        editor.style.display = 'inline';
        img.src = 'Images/Minus.gif';
        input.value = '1';
    } else {
        editor.style.display = 'none';
        img.src = 'Images/Plus.gif';
        input.value = '';
    }
}

En este ejemplo nosotros utilizamos el ScriptManager de ASP.NET, por eso se ve que usamos el método $get(), pero si prefieren no usarlo simplemente lo tienen que cambiar por document.getElementById().
Por si no se dieron cuenta, en la clase CollapsibleEditorZone creamos un SPAN donde metemos todos los editores, el cual tiene un estilo llamado igual que la clase. Este estilo lo utilizamos para agregarle el borde a cada uno de los editores:

.CollapsibleEditorZone FIELDSET
{
border: 1px solid #777777;
padding: 2px 2px 2px 2px;
}
.CollapsibleEditorZone FIELDSET FIELDSET
{
border: 1px solid #aaaaaa;
margin-bottom: 6px;
}

Lo último que quedaría hacer es agregar esta clase en alguna página, la cual debería quedar algo como lo que se muestra a continuación:

<wp:CollapsibleEditorZone ID="cezEdit" runat="server" VerbButtonType="Button"
    HeaderCloseVerb-Visible="false" Width="100%">
    <ApplyVerb Text="Aplicar" />
    <OKVerb Text="Aceptar" />
    <CancelVerb Text="Cancelar" />
    <ZoneTemplate>
        <asp:PropertyGridEditorPart runat="server" />
    </ZoneTemplate>
</cwp:CollapsibleEditorZone>

Nótese que el prefijo “wp” debe apuntar al namespace donde se encuentra la clase CollapsibleEditorZone.

Espero que este control les sea de utilidad y como siempre los invito a dejar sus opiniones y comentarios.

VN:F [1.7.3_972]
Rating: 8.0/10 (1 voto cast)