MSDN Magazine > Inicio > Todos los números > 2007 > November >  Instintos básicos: Métodos de extensi...
Instintos básicos
Métodos de extensión
Adrian "Spotty" Bowles

En el desarrollo orientado a objetos tradicional, la extensión de las clases típicamente se consigue al derivar de una clase base y mejorar la funcionalidad en la clase derivada. Visual Basic® todavía es compatible con estos conceptos orientados a objetos, pero a veces las clases se marcan como NotInheritable para evitar la modificación de su comportamiento a través de la herencia. En consecuencia, no hay manera de personalizar estas clases. Un ejemplo de esto es la clase System.String.
Sin embargo, una nueva característica que se puso a disposición en Visual Basic 2008 permite extender cualquier funcionalidad de tipos existente, incluso cuando un tipo no sea heredable. Y estos métodos de extensión desempeñan un papel crucial en la implementación de LINQ.
Muchos tipos que ya existen no se pueden actualizar fácilmente sin necesidad de dividir código existente. Un ejemplo de esto es la interfaz IEnumerable(Of T). Para admitir LINQ, se tuvieron que agregar nuevos métodos a esta interfaz, pero el cambio de la interfaz mediante adición de nuevos métodos hubiera interrumpido la compatibilidad con los consumidores existentes. La adición de una nueva interfaz era una posibilidad, pero crear una nueva interfaz para complementar la interfaz IEnumerable(Of T) existente hubiera dado la impresión de un diseño extraño.
Lo que se necesitaba era una manera de extender los tipos existentes con la nueva funcionalidad, pero sin cambiar el contrato existente.
Los métodos de extensión ofrecen un mecanismo sencillo para extender tipos en el sistema (tipos de valor, referencia e interfaz) con nuevos métodos. Estos métodos extienden el tipo original y pueden invocarse como los métodos de instancia definidos y normales, pero conservan intactos el tipo original y sus métodos. Los métodos de extensión crean la ilusión de que están definidos en un tipo real, pero en realidad, no se realiza ningún cambio en los tipos originales.
Este no es un concepto orientado a objetos estándar; es una característica de implementación específica de Microsoft® .NET Framework. Aunque esta característica abre un conjunto nuevo de posibilidades, conviene tener en cuenta que el código de lenguaje intermedio (IL) subyacente generado por el compilador en realidad no hace nada nuevo o específico de .NET Framework 3.5. En realidad, simplemente hace una llamada al método compartido.
Esto significa que en Visual Basic 2008 se dispone de la capacidad de usar esta característica para abordar versiones anteriores de .NET Framework. No debería presentar problemas de seguridad adicionales debido a que esta característica no cambia el tipo que se extiende y en realidad no hace nada que ya no fuera posible hacer con versiones anteriores de Framework.

Creación y uso de los métodos de extensión
El código de la figura 1 muestra un método de extensión AlternateCase, que devuelve una cadena que alterna mayúsculas y minúsculas. Esto extenderá la clase System.String que, como ya dije, se marca como NotInheritable.
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim s As String = "Hello World"

'Call AlternateCase method as an extension member on 
'variable s
        Console.WriteLine(s.AlternateCase)

'Invoke if it was not an extension method or 
'resolve conflicts
Console.WriteLine(Extensions.AlternateCase(s))

    End Sub
End Module

Module Extensions
<Extension()> _ 
Function AlternateCase(ByVal x As String) As String
        Dim bCase = False
        Dim SB as new System.Text.StringBuilder
        For Each c In x
            If bCase Then
                SB.Append(c.ToUpper)
            Else
                SB.Append(c.ToLower)
            End If
            bCase = Not bCase
        Next
        Return SB.ToString
    End Function
End Module

Lo guiaré por este ejemplo. La declaración del método contiene el atributo que identifica este método como un método de extensión. Los métodos de extensión se definen mediante la aplicación del atributo de extensión que se encuentra en el espacio de nombres System.Runtime.CompilerServices contenido con System.Core.Dll. Para quienes no conocen los atributos, éstos se pueden usar para ofrecer metadatos que el compilador usa para activar funcionalidad adicional. En este caso, el atributo de extensión ofrece información que le indica al compilador que ese método se puede invocar en el tipo identificado como el primer argumento para el método de extensión, String, en mi ejemplo.
El lenguaje Visual Basic insiste que todas las declaraciones de métodos de extensión se coloquen dentro de un módulo. Y por definición, los métodos de extensión se pueden crear como subrutinas o funciones, pero está prohibido tratar de declararlos de otra manera, por ejemplo como propiedades o eventos. No existen restricciones en los tipos que se pueden extender; se puede extender cualquier clase, estructura o interfaz.
Los métodos de extensión se restringen a los módulos para garantizar que sean fáciles de consumir. Este diseño está concebido para ampliar el concepto de poner nuevas API al alcance de un programa de Visual Basic mediante la instrucción Imports para poner a disposición todos los métodos que se encuentren en un espacio de nombres.
En Visual Basic, cada método se tiene que declarar dentro de un tipo, sea una clase, estructura, módulo u otro elemento. Los métodos de extensión no son una excepción, aunque son un poco diferentes debido a que se definen en un tipo que se encuentra fuera del tipo que extienden. Esto deja abierta la posibilidad de que aparezcan conflictos de nombres durante el uso de métodos de extensión desde múltiples módulos o espacios de nombres.
Si se restringe a los módulos la declaración de métodos de extensión, el diseño se simplifica considerablemente debido a que los módulos no pueden tomar parte en las cadenas de herencia y no existe el concepto de módulos parciales. El uso de una instrucción Imports con un espacio de nombres o un módulo pone esos métodos compartidos al alcance y permite invocar a los métodos sin tener que escribir sus nombres completos.
A modo de ejemplo, supongamos que se colocan las extensiones de módulo dentro de un espacio de nombres diferente, como el siguiente:
Namespace NewFeatures
Module Extensions
<Extension()> Function AlternateCase(ByVal x As String) As String
        Dim bCase = False
        Dim SB as new System.Text.StringBuilder
        For Each c In x
            If bCase Then
                SB.Append(c.ToUpper)
            Else
                SB.Append(c.ToLower)
            End If
            bCase = Not bCase
        Next
        Return SB.ToString
End Module
End Namespace
Para poner al alcance las declaraciones del método de extensión que se encuentran dentro de este espacio de nombres, usaría la siguiente instrucción:
Imports NewFeatures  
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim s As String = "Hello World"

'Call AlternateCase method as an extension member on 'variable s
        Console.WriteLine(s.AlternateCase)

'Invoking if it was not an extension method or 
'resolve conflicts
Console.WriteLine(Extensions.AlternateCase(s))
    End Sub
End Module
Los métodos de extensión no son exclusivos de Visual Basic; C# dispone de una implementación semejante con métodos de extensión que se tienen que declarar en una clase estática. Esto ofrece una funcionalidad similar a la de un módulo. Si se trabaja tanto con C# como con Visual Basic, los métodos de extensión permiten la interoperabilidad: se pueden crear métodos de extensión en un lenguaje y consumirlos en el otro.
Tenga en cuenta que si se actualizó un proyecto a partir de una versión anterior, se necesitará actualizar el Framework de destino a la versión 3.5 y, a continuación, agregar manualmente una referencia a System.Core.Dll. Si no se desea actualizar el Framework de destino, aún se pueden aprovechar los métodos de extensión si se vuelve a crear ExtensionAttribute (se hablará de esto más adelante). También se deberá importar el espacio de nombres System.Runtime.CompilerServices (en el cuál reside ExtensionAttribute) o escribir el nombre completo del atributo cuando se use.
La parte final del ejemplo es la llamada. Las mejoras de Intellisense® permiten ver los métodos de extensión junto con todos los métodos de instancia existentes. Allí podrá ver que los métodos de extensión tienen un icono ligeramente diferente (una flecha azul hacia abajo, tal como se muestra en la figura 2) para diferenciarlos de los métodos de instancia.
Figura 2 Iconos de métodos de extensión y de instancia (Hacer clic en la imagen para ampliarla)
Las funciones del IDE de Visual Studio®, como Goto Definition, Rename, etc., funcionan con los métodos de extensión de la misma manera que con los métodos de instancia normales. Se puede observar que la información sobre herramientas también muestra el atributo de extensión cuando se visualiza la firma.
Es posible que haya observado otros métodos en Intellisense, tal como First y FirstorDefault. Estos son algunos de los métodos de extensión que ya se han proporcionado como parte de LINQ y se incluyen en las referencias predeterminadas de .NET Framework 3.5. Estos métodos extienden una interfaz genérica System.Collections.Generic.IEnumerable(Of T) desde el espacio de nombres System.Linq.
La funcionalidad de LINQ se habilita de forma predeterminada en proyectos de la versión 3.5, lo que significa que se obtienen todos los métodos de extensión definidos en System.Core.Dll. Se puede confirmar al examinar las referencias de ensamblados predeterminadas y los espacios de nombres importados, al ir a Propiedades del proyecto | Referencias | Espacios de nombres importados, se mostrará que System.Linq ya está activado. También se puede usar una instrucción Imports de nivel de archivo (Imports System.Linq) para poner estos métodos otra vez al alcance.

Ventaja de los métodos de extensión
Los métodos de extensión permiten agregar funcionalidad a un tipo que no se desea modificar, evitando así el riesgo de dividir el código presente en las aplicaciones existentes. Se pueden extender interfaces estándar con métodos adicionales sin modificar físicamente las bibliotecas de clases existentes. Se pueden extender tipos .NET y tipos de control COM o ActiveX® anteriores para el nuevo código sin el riesgo de dividir las antiguas aplicaciones que usan estos tipos.
Antes de los métodos de extensión, había pocas opciones para agregar funcionalidad a clases e interfaces:
  • Se podía agregar funcionalidad al código fuente, pero se requería acceso al código fuente que podía no estar disponible.
  • Se podía usar herencia para heredar la funcionalidad contenida dentro de un tipo en un nuevo tipo derivado, pero no todos los tipos eran heredables.
  • Se podía volver a implementar la funcionalidad desde cero.
Todos estos métodos hacían muy difícil la extensión de tipos de terceros. Cuando se podía obtener el código fuente y volver a compilar el componente, también era necesario garantizar que se mantuviera la compatibilidad con el componente original para evitar dividir las aplicaciones existentes que usaban el componente. Cuando se creaba una nueva clase derivada o se volvía a implementar la funcionalidad desde cero, era necesario corregir todas las aplicaciones existentes para usar el nuevo componente o crear una bifurcación entre la funcionalidad original y la nueva funcionalidad actualizada, y esto requería dos versiones del componente.
Ninguna de estas soluciones era ideal. Con los métodos de extensión, simplemente se puede agregar la funcionalidad a las clases existentes. Todas las aplicaciones existentes continúan funcionando con los archivos binarios originales porque en ellas no se ha modificado nada, pero ahora las aplicaciones pueden extender la funcionalidad en lo sucesivo.
El rendimiento permanece relativamente sin cambios. Aunque el código fuente de Visual Basic pueda parecer diferente, se compila en una llamada sencilla al método compartido y genera el mismo IL que si se hubiera usado la llamada al método compartido. Así que el rendimiento no se ve afectado al usarlas.
Dicho esto, los métodos de extensión no son un reemplazo de los conceptos orientados a objetos. Si se conocen los conceptos orientados a objetos y se cuenta con una aplicación bien diseñada, probablemente ya se usa herencia en alguna parte de la aplicación y se debe continuar así. Probablemente se poseen muchos de los tipos usados, lo cuál significa que se tiene acceso al código fuente y que se puede extender la funcionalidad a través de conceptos orientados a objetos normales. Los métodos de extensión ofrecen en realidad una manera de mejorar las clases que anteriormente no se podían extender. Los métodos de extensión se deben usar de una manera racionalizada, cuando otros métodos no son posibles ni adecuados.

Temas de seguridad
A menudo he escuchado la preocupación de que los métodos de extensión se pueden usar para secuestrar o subvertir el comportamiento original de métodos existentes. Visual Basic soluciona esto al asegurar que, siempre que sea posible, se prefiere un método de instancia a un método de extensión.
El lenguaje permite que los métodos de extensión se usen para crear sobrecargas para métodos de instancia existentes con firmas diferentes. Esto permite usar métodos de extensión para crear sobrecargas, a la vez que se impide que se invalide el método de instancia existente. Si existe un método de extensión con la misma firma que un método de instancia, las reglas de ocultación integradas en el compilador preferirán el método de instancia, y por lo tanto eliminan la posibilidad de que un método de extensión invalide la funcionalidad de instancia de clase base existente (consulte la figura 3).
Imports System.Runtime.CompilerServices 

Module Module1
    Sub Main()
        Dim x As New ClsFoo
        Dim s As String = x.Method01
    End Sub
End Module

Class ClsFoo
    Function Method01() As String
        Return "Instance"
    End Function
End Class

Module ExtMethods
    <Extension()> _
    Function Method01(ByVal a As clsFoo) As String
        Return "Extension"
    End Function
End Module

Dado que la implementación de los métodos de extensión no cambia el tipo original, sólo aquellas aplicaciones escritas para usar el método de extensión conocen esta funcionalidad adicional. Las aplicaciones existentes que usan el tipo original no verán ninguna diferencia.
Si echa un vistazo al IL subyacente generado por el compilador, verá que la llamada al método de extensión se traslada en una sencilla llamada al método compartido. De este modo, no hay un aumento de riesgos de seguridad al usar los métodos de extensión ya que no hace nada que ya no fuera posible hacer mediante una llamada al método compartido normal.

Sobrecarga con métodos de extensión
Los métodos de extensión permiten sobrecargar métodos de instancia. Para contribuir a garantizar que las aplicaciones funcionen correctamente, existen muchas reglas integradas en el compilador para ofrecer un comportamiento de enlace coherente. El objetivo era hacer que los métodos de extensión parecieran métodos de instancia, pero ofrecer también una experiencia coherente; cuando se escribe código heredado en un proyecto mediante métodos de extensión, el código debe seguir funcionando de la misma manera.
Un método de instancia con una firma aceptable que usa conversión de ampliación se preferirá casi siempre a un método de extensión con una firma con coincidencia exacta. Si esto tiene como resultado el enlace al método de instancia cuando en realidad se desea usar el método de extensión, se puede llamar explícitamente al método de extensión mediante la convención de llamada al método compartido. Esta es también una manera de poner fin a la ambigüedad entre dos métodos cuando ninguno es más específico.
Los campos y las propiedades siempre ocultan los métodos de extensión con el nombre. La figura 4 muestra un método de extensión y un campo público con el mismo nombre y distintas llamadas. Aunque el método de extensión contiene un segundo argumento, el campo oculta el método de extensión con el nombre y todas las llamadas que usan este nombre provocan el acceso al campo. Todas las distintas llamadas sobrecargadas se compilarán, pero sus resultados en tiempo de ejecución pueden ser inesperados ya que se enlazarán a la propiedad y usarán el comportamiento predeterminado de la propiedad para devolver un único carácter o provocarán una excepción de tiempo de ejecución. Es importante elegir los nombres de los métodos de extensión para evitar conflictos con propiedades, campos y métodos de instancia existentes.
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim x As New Foo

        'Field - (Test)
        Console.WriteLine(x.TypeField) 
        'Default Property Transformation on field resulting in T
        Console.WriteLine(x.TypeField(0)) 

        'Invalid Cast Exception Converting "fred" to integer
        'to use in default property transformation
        Console.WriteLine(x.TypeField("fred")) 
    End Sub
End Module

Class Foo
    Public TypeField As String = "Test"
End Class

Module Extension
    <Extension()> _
    Function TypeField(ByVal a As Foo, ByVal x As String) As String
        Return "Extension"
    End Function
End Module

Las reglas en el compilador referentes a cuál método se enlazará el código de invocación son complejas. Estas reglas se ponderan orientadas a la selección de un método de instancia existente siempre que sea posible. Sin embargo, existen algunas excepciones. Los puntos generales a considerar son los siguientes:
  1. Casi siempre los métodos de instancia se considerarán antes que los métodos de extensión. Los métodos de instancia de ampliación se consideran antes que los métodos de extensión. Los candidatos de métodos de instancia no aplicables, tales como los que están fuera de ámbito, no se consideran para la resolución de sobrecarga.
  2. Los métodos de instancia con la firma con coincidencia de tipos correcta se consideran antes que los métodos con conversiones de ampliación.
  3. Las propiedades o campos de la clase ocultarán los métodos de extensión con el nombre.
Debe quedar claro que el nombre y la firma del método de extensión son críticos en la determinación del método que elige el compilador, sea un método de extensión o de instancia.

Qué tipos
Otra consideración se relaciona con qué tipos se extienden. Aunque prácticamente es posible extender cualquier tipo, esto no significa que se deba hacerlo. Se debe tener en cuenta el "reenlace silencioso", que se puede describir como lo que sucede cuando un método se está enlazando a los cambios, pero este cambio no resulta inmediatamente obvio para el desarrollador. No se genera ninguna advertencia ni error y la única indicación de que esto ha ocurrido es el cambio en el comportamiento resultante, provocado por el uso de un método diferente.
Imagine que crea un método de extensión que sobrecarga un método en una biblioteca de clases de terceros. Su aplicación funciona como estaba previsto. La herramienta de terceros ofrece una actualización, que instala en su equipo. La aplicación se compila correctamente, sin errores. Pero cuando se ejecuta la aplicación, el comportamiento es incorrecto. Esto puede ocurrir si el componente actualizado incluyó sobrecargas adicionales al método que había extendido, lo que tiene como resultado la sobrecarga del método de instancia adicional que se llama y no la de su método de extensión.
Los métodos de extensión son frágiles en escenarios de control de versiones. Por este motivo se deben usar con mucho cuidado los nombres de métodos de extensión, especialmente cuando se usan con tipos que con frecuencia se actualizan con nuevas sobrecargas. El uso de Option Strict On puede ayudar a evitar el reenlace silencioso, porque le pondrá sobre aviso en ciertas condiciones en que se puedan producir las conversiones implicadas.

Extensión del tipo de objeto
Se puede extender el tipo de objeto, lo que permite que cualquier tipo que derive del tipo de objeto pueda ver el método. Pero si se crea un tipo como objeto y se intenta consumir el método de extensión en éste, no se verá el método de extensión. Esto se debe a que los métodos de extensión no admiten enlace en tiempo de ejecución.
Tenga en cuenta que en la figura 5, la primera llamada funcionará porque la variable s es de tipo Integer y deriva de Object. Como resultado, se verá el método de extensión. Sin embargo, la variable T es de tipo de objeto y se enlaza en tiempo de ejecución, por lo tanto generará un error de compilación.
Imports System.Runtime.CompilerServices 

Module Module1
    Sub Main()
        Dim s As Integer = 1234
        Console.WriteLine(s.Foo) '<- This will work

        Dim T As Object
        T = 1234
        Console.WriteLine(T.Foo)  '<- This will fail
    End Sub
End Module

Module Extensions01
    <Extension()> _
    Function Foo(ByVal b As Object) As String
        Return "Bar01"
    End Function
End Module

El código se podría corregir mediante el siguiente cambio:
Dim T = 1234
Console.WriteLine(T.Foo)  
Ahora funcionará debido a la característica de inferencia de tipos. La variable T se infiere como entero según el valor que se le asigna. Esto difiere de Visual Studio 2005, en que T habría sido un tipo de objeto que contiene un entero al que se ha aplicado la conversión boxing.

Métodos de extensión en aplicaciones de .NET Framework 2.0
Como se ha mencionado anteriormente, los métodos de extensión no hacen nada en el código IL subyacente que no se pueda realizar con Visual Basic 2005; simplemente hacen una llamada al método compartido. Lo que ha cambiado es la capacidad del compilador para identificar los métodos específicos y permitir cambios en la sintaxis del método de invocación, que da la apariencia de que el tipo usado tiene una nueva funcionalidad y hace que las extensiones funcionen igual que en los métodos de instancia existentes.
Otra característica nueva en Visual Basic 2008 es la compatibilidad con múltiples destinos (multi-targeting), que permite usar Visual Basic 2008 para escribir código destinado a una versión específica de Framework. Dado que el mismo compilador se usa para múltiples destinos, es posible usar los métodos de extensión en aplicaciones que se destinan, por ejemplo, a la versión 2.0 de Framework. Esto necesita de una pequeña solución alternativa, porque IDE evita la adición de las referencias necesarias a una aplicación destinada a .NET Framework 2.0.
Si se crea una aplicación en la versión 3.5 y se cambia el Framework de destino a la versión 2.0 (a través de Propiedades del proyecto | Compilador | Opciones avanzadas | Versión de .NET Framework de destino), se verá que algunas de las referencias se resaltan como faltantes, incluidas aquellas que contienen la mayor parte de la funcionalidad de LINQ original. Cuando se intenta agregar estas referencias a un proyecto de la versión 2.0 aparece un cuadro de diálogo que informa que éstas necesitan una versión diferente del Framework de destino (consulte la figura 6).
Figura 6 Referencias para un destino que se cambia a .NET Framework 2.0 (Hacer clic en la imagen para ampliarla)
Los métodos de extensión se pueden crear y usar en aplicaciones de 2.0 después de crear su propio atributo System.Runtime.CompilerServices.Extension. Primero se debe crear un nuevo proyecto destinado a la versión 2.0. Las referencias predeterminadas no incluirán System.Core, que contiene el atributo de extensión. En Applications Project Properties, vaya a la ficha Application, borre el espacio de nombres Root y, a continuación, cree su propio atributo de extensión en el espacio de nombres System.Runtime.CompilerServices correcto. El código de la figura 7 muestra la creación del atributo de extensión y, a continuación, lo usa para declarar y usar los métodos de extensión. Una vez que se compila la aplicación, se puede llamar a los métodos de extensión.
Imports System.Runtime.CompilerServices 

Module Module1
    Sub Main()
        Dim i_x As Integer = 1
        Dim i_s As String = "Test"

        Dim x = i_x.IntegerExtension
        MsgBox(x)

        Dim y = i_s.StringExtension
        MsgBox(y.ToString)
    End Sub
End Module

'CREATE A USER-DEFINED ATTRIBUTE CALLED EXTENSION IN THE CORRECT NS
Namespace System.Runtime.CompilerServices
    <AttributeUsage(AttributeTargets.Assembly Or AttributeTargets.Class Or AttributeTargets.Method, AllowMultiple:=False, Inherited:=False)> Class ExtensionAttribute
        Inherits Attribute
    End Class
End Namespace

'DEFINED EXTENSION METHODS WITH USER-DEFINED ATTRIBUTE ON THEM
Module ExtMethods
    <Extension()> _ 
Function StringExtension(ByVal a As String) As String
        Return "Success"
    End Function

    <Extension()> _
Function IntegerExtension(ByVal a As Integer) As Integer
        Return 100
    End Function
End Module

Esta son algunas consideraciones importantes con respecto a las aplicaciones destinadas a 2.0. En primer lugar, la inferencia de tipos está desactivada de forma predeterminada. Para volverla a activarla, use Option Infer On. Probablemente se desee hacerlo cuando se migren las aplicaciones de .NET Framework 2.0 a la versión 3.5.
En segundo lugar, para usar los métodos de extensión, necesita crear su propio atributo de extensión que imite al que se encuentra en System.Core. IDE impedirá que use la referencia de System.Core en un proyecto destinado a 2.0.
Por último, esta técnica no funcionará para las aplicaciones de ASP.NET destinadas a .NET Framework 2.0 porque tienen una dependencia de tiempo de ejecución en el compilador de línea de comandos de la versión 2.0. Cuando esas aplicaciones se implementan en servidores web que sólo tienen instalado .NET Framework 2.0, no funcionarán porque el compilador de línea de comandos VBC.EXE 2.0 no entiende métodos de extensión.
Incluso si no se desea iniciar aplicaciones de escritura para .NET Framework 3.5 ni se dispone de aplicaciones existentes en la versión 2.0 que sea necesario mantener, aún se pueden obtener beneficios de la actualización a Visual Basic 2008. La funcionalidad del método de extensión está integrada en el compilador y se puede usar para cualquier versión de Framework que el compilador de Visual Basic 2008 pueda establecer como destino.

Envíe sus comentarios y preguntas a instinct@microsoft.com.


Adrian "Spotty" Bowles ha desarrollado con todas las versiones de Visual Basic y ha logrado descubrir cómo llegar a Redmond, WA, donde trabaja en el equipo de producto de Visual Basic como ingeniero de pruebas de diseño de software centrado en el compilador de Visual Basic. Durante el lanzamiento de Visual Basic 2008, trabajó en muchas de las características del lenguaje, incluidos los métodos de extensión. Puede ponerse en contacto con Spotty en la dirección Abowles@microsoft.com.

Page view tracker