Archivo de marzo 2010


Enlace de elementos en controles de lista en ASP.NET

Miércoles, 24 mar, 2010 @ 23:43 | Por Dario Krapp | ASP.NET

Estoy seguro que será difícil encontrar en la actualidad algún programador que no se haya visto en la necesidad enviar información a la interfaz de usuario, y eventualmente manejar las modificaciones que un usuario haya realizado en la misma. La intención de este artículo es dar un primer paso a los conceptos de enlace en ASP.NET (una tecnología que nos permitirá resolver este tipo de problemas) para un escenario muy especifico pero frecuente que es cuando se requiere mostrar en un control ASP.NET un grupo de elementos como una lista. ASP.NET ofrecerá para este tipo de casos un grupo de controles (que llamaremos controles de lista para identificarlos de una forma sencilla) que nos permitirán realizar esta tarea en forma simple y clara.

En la década de los noventa, con ASP, lo que se acostumbraba hacer era identificar un control HTML adecuado para el grupo de datos que se deseaba mostrar y escribir los tags HTML intercalados junto con los datos para crear un fragmento de código HTML con la funcionalidad deseada, esta forma de trabajo, si bien funcionaba, daba como resultado un código poco claro y difícil de mantener.
ASP.NET y el enlace a datos permitirá obtener un resultado similar con un código de rápido desarrollo y de bajo costo de mantenimiento.

Antes que nada creo que lo más importante es definir quienes serán los actores que intervendrán en un escenario de enlace de datos y sin importar cuánto avancemos o donde comencemos, nos encontraremos con dos actores principales, el primero se estará compuesto por el grupo de datos con los que deseamos trabajar (al cual, para utilizar la nomenclatura habitual, llamaremos “Origen de datos” o “Data Source”) y el segundo por el elemento que permitirá que los mismos sean mostrados en la interfaz de usuario, y eventualmente sean modificados desde la misma, aunque esta opción quedará fuera del alcance de este artículo.
En el caso de ASP.NET ese elemento será un control y los controles que se ajustarán a las necesidades propuestas en este articulo serán los siguientes:

• BulletedList
• CheckBoxList
• DropDownList
• ListBox
• RadioButtonList

En WebControls y el control HtmlSelect en HtmlControls.

Aunque cada uno de ellos renderizará la lista de elementos a mostrar de una forma diferente, el enlace se efectuará de la misma forma para todos, por lo que en nuestros ejemplos optaremos por utilizar un control DropDownList.

Para comenzar, seremos sumamente triviales y supondremos que nuestro origen de datos es un grupo de elementos simples, por ejemplo, un grupo de cadenas de caracteres compuesta por los valores “I1”, “I2” e “I3” y nuestra intención será mostrar dichos valores en un control DropDownList.

El enlace

En nuestra primera suposición ya hemos conocido a ambos actores (el origen de datos compuesto por la cadena de caracteres “I1”, “I2” e “I3” y el control ASP.NET DropDownList), Deberemos finalmente realizar el enlace entre ambos.
Esta tarea será muy sencilla ya que los controles de lista contarán con la siguiente propiedad:

• DataSource

La propiedad DataSource representará al grupo de elementos (u origen de datos) que se enlazará al control, claro está que el grupo de elementos a enlazar no podrá ser tan arbitrario como deseemos ya que de alguna forma ASP.NET deberá saber de qué manera acceder a los elementos que componen dicho grupo, por lo que éste deberá implementar alguna de las siguientes interfaces: IEnumerable, ICollection o IListSource.
Sabemos ahora que para nuestro ejemplo, lo que llamábamos “grupo” deberá implementar alguna de las interfaces mencionadas.

Creo que de todas las comentadas, la interfaz ICollection nos será la más familiar y, para quien no la recuerde, sin usar mucho la imaginación, seguramente le será un sinónimo de Colecciones (o Collections). En base a esto podríamos reformular el problema para intentar enlazar una colección compuesta por las cadenas “I1”, “I2” e “I3” con un control DropDownList.

Los pasos para esta tarea constarán de crear una colección, de crear el control ASP.NET y establecer su propiedad DataSource, veamos un ejemplo concreto:

<asp:DropDownList runat="server" ID="DDL1"/>
List<string> Lst = new List<string>();
Lst.Add("I1");
Lst.Add("I2");
Lst.Add("I3");
DDL1.DataSource = Lst;

Si ejecutamos este código veremos que el enlace no funciona, esto se debe a que aún nos falta un detalle más, si modificamos el código de la siguiente forma:

List<string> Lst = new List<string>();
Lst.Add("I1");
Lst.Add("I2");
Lst.Add("I3");

DDL1.DataSource = Lst;
DDL1.DataBind();

Veremos que todo funciona según lo esperado, el método DataBind es el que ejecutará el pasaje de datos desde el origen de datos (Lst) hacia el control (DDL1). Es más si realizamos la siguiente prueba:

List<string> Lst = new List<string>();
Lst.Add("I1");
Lst.Add("I2");
Lst.Add("I3");

DDL1.DataSource = Lst;
DDL1.DataBind();

Lst.Add("I5");
Lst.Add("I6");

Notaremos que los valores “I5” e “I6” no serán transferidos al control.
En este primer ejemplo hemos realizado el enlace que nos habíamos propuesto y con muy poco esfuerzo.

Los controles de lista contarán también con la propiedad DataTextFormatString que permitirá establecer el formato de los datos a mostrar, para continuar con el ejemplo que ya habíamos comenzado, reemplazaremos la lista de cadenas de caracteres por una lista de fechas, el ejemplo es tan simple como antes, una opción es utilizar las siguientes líneas de código:

List<DateTime> Lst = new List<DateTime>();
Lst.Add(DateTime.Now);
Lst.Add(DateTime.Now.AddHours(1));
Lst.Add(DateTime.Now.AddHours(2));
DDL1.DataSource = Lst;
DDL1.DataBind();

donde al ejecutarlo veremos que en el DropDownList DDL1 las fechas se desplegarán como esperábamos, aunque para darle un poco de emoción al ejemplo y poner en practica lo que comentamos hace apenas unos parrafos, podríamos desear mostrar solamente la fecha pero no la hora en el formato dia/año/mes. De más está decir que una opción es modificar el código para realizar este ajuste programáticamente, pero no es la idea ya que la propiedad DataTextFormatString nos permitirá ahorrarnos las líneas de código.
A continuación se muestra el ejemplo:

<asp:DropDownList runat="server" ID="DDL1" DataTextFormatString="{0:dd/yyyy/MM}" />

Me parece que una pregunta que podríamos formularnos en este punto es ¿por qué el control está mostrando los valores adecuadamente?, si prestamos atención veremos que en ambos casos, tanto como cuando utilizamos una lista de cadenas de caracteres o una lista de fechas el control DropDownList ha podido mostrar el valor esperado sin que nosotros hayamos tenido que hacer ningún tipo de especificación, podríamos preguntarnos ¿qué pasará si utilizamos una lista de otro tipo de elementos?, por ejemplo un tipo de datos creado por nosotros mismos, como el siguiente:

public class MiElem
{
   public string Valor1 { set; get; }
   public int Valor2 { set; get; }
}

Si modificamos nuevamente el código previo de la siguiente forma

List<MiElem> Lst = new List<MiElem>();
Lst.Add(new MiElem() { Valor1 = "V1", Valor2 = 1 });
Lst.Add(new MiElem() { Valor1 = "V2", Valor2 = 2 });
Lst.Add(new MiElem() { Valor1 = "V3", Valor2 = 3 });

DDL1.DataSource = Lst;
DDL1.DataBind();

Notaremos la magia ya no funciona y que el DropDownList en este caso está mostrando por cada elemento un valor que no se encuentra en ninguna de sus propiedades. Lo que está haciendo el control es mostrar el valor de la representación de cadena de caracteres del elemento, o sea, simplemente está mostrando el resultado del método ToString() de cada elemento que carga. Si sobreescribimos el método ToString() de la clase MiElem nos convenceremos rápidamente de este hecho.
Ahora queda claro porque con cadenas de caracteres y fechas todo funcionaba sin problemas pero también está claro que con otros tipos de datos nos podríamos encontrar con complicaciones, ya que no siempre podremos ajustar el método ToString() a nuestra conveniencia. En este punto toma importancia la propiedad DataTextField de los controles de lista. DataTextField indicará el nombre de la propiedad o el campo (veremos más adelante a que nos referimos con campo) que el control mostrará. Si modificamos el código de la siguiente forma:

List<MiElem> Lst = new List<MiElem>();
Lst.Add(new MiElem() { Valor1 = "V1", Valor2 = 1 });
Lst.Add(new MiElem() { Valor1 = "V2", Valor2 = 2 });
Lst.Add(new MiElem() { Valor1 = "V3", Valor2 = 3 });

DDL1.DataSource = Lst;
DDL1.DataTextField = "Valor1";
DDL1.DataBind();

Notaremos que el control DDL1 mostrará el valor de la propiedad Valor1 de cada instantancia de MiElem.

De forma similar, la propiedad DataValueField indicará el nombre de la propiedad o el campo que el control utilizará como valor para cada ítem mostrado, ya que es frecuente el hecho de tener la necesidad de mostrar un dato, pero utilizar otro dato del elemento para realizar alguna tarea.
Si eventualmente se desea utilizar la misma propiedad para mostrarla en control tanto como para utilizar su valor, puede omitirse el uso de DataValueField, que es un detalle que permite mantener el código más simple.

Aunque en nuestro ejemplo hemos utilizado un control DropDownList enlazado a un objeto List podríamos haber utilizado cualquier otro control de lista (como mencionamos previamente), tanto como cualquier otra clase que implementase la interfaz ICollection.

Si volvemos a la propiedad DataSource, recordaremos que también pueden enlazarse objetos que implementan la interfaz IListSource, y podríamos preguntarnos ¿por qué?, la interfaz IListSource quizás no nos sea tan familiar como ICollection, pero es todo un indicio comentar que tanto la clase DataTable, como DataSet la implementan. Sabiendo esto quizás podríamos preguntarnos:
¿será entonces posible enlazar un DataTable o un DataSet a uno de estos controles?

La respuesta, como no podría ser de otra manera, es que si es posible, en tal caso en las propiedades DataTextField y DataValueField se deberá establecer el nombre de los campos a enlazar del DataTable, de esta forma el esquema de funcionamiento se mantendrá inalterable y el enlace podrá utilizarse para clases que implementen la interfaz ICollection tanto como IListSource. Por otra parte la interfaz IEnumerable (que era la tercera interfaz soportada por la propiedad DataSource) permitirá el enlace a objetos como por ejemplo un SqlDataReader donde los datos se van obteniendo de a uno a la vez. A continuación se muestra un ejemplo de enlace contra un DataSet, un DataTable y un SqlDataReader:

<asp:DropDownList
runat="server"
ID="DDL1"
DataTextField="Descripcion">
</asp:DropDownList>
<asp:DropDownList
runat="server"
ID="DDL2"
DataTextField="Descripcion">
</asp:DropDownList>
<asp:DropDownList
runat="server"
ID="DDL3"
DataTextField="Descripcion">
</asp:DropDownList>
private void DataReaderBinding()
{
        string cnstr = ...;
        SqlConnection SCon = new SqlConnection(cnstr);
        SqlCommand SCom = new SqlCommand("SELECT top 11 * FROM Datos1", SCon);
        SCom.Connection.Open();
        SqlDataReader SRead = SCom.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
        DDL1.DataSource = SRead;
        DDL1.DataBind();
}

private void DataSetAndDataTableBinding()
{
        string cnstr = ...;
        SqlConnection SCon = new SqlConnection(cnstr);
        SqlCommand SCom = new SqlCommand("SELECT top 11 * FROM Datos1", SCon);

        SqlDataAdapter SDa = new SqlDataAdapter(SCom);
        DataSet DSet = new DataSet();
        SDa.Fill(DSet, "Datos1");

        DDL2.DataSource = DSet.Tables[0] ;

        DDL3.DataSource = DSet;
        DDL3.DataMember = "Datos1";  

        DataBind();
}

En el ejemplo puede observarse el uso de la propiedad DataMember para el caso del enlace contra un objeto DataSet, la propiedad DataMember deberá utilizarse cuando sea necesario indicar el nombre del DataTable que se enlazará al control. También en el ejemplo puede observarse el uso de un método existente en la página denominado DataBind() que ejecutará todos los bindeos necesarios automáticamente ahorrandonos un poco de código.

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

Utilizar Autenticación de ASP.NET desde Silverlight

Martes, 23 mar, 2010 @ 21:38 | Por Gustavo Cantero (The Wolf) | ASP.NET, Seguridad, Silverlight, WCF

Hace unos meses Darío escribió un artículo donde explica cómo utilizar los servicios de autenticación y roles que posee ASP.NET en nuestras páginas (http://www.programandoamedianoche.com/2009/10/autenticacion-por-formularios-en-asp-net), tanto desde el servidor como utilizando AJAX (http://www.programandoamedianoche.com/2009/10/autenticacion-por-formularios-en-asp-net/3). Para complementarlo se me ocurrió escribir un artículo que explique cómo utilizar este servicio de autenticación desde Silverlight.
Para comenzar vamos a crear un proyecto Silverlight en nuestro Visual Studio, junto con su proyecto web de prueba (el cual va a exponer los servicios de autenticación).
Para utilizar estos servicios desde Silverlight (o desde cualquier aplicación externa) necesitamos publicarlos a través de WCF (Windows Communication Foundation). Para esto primero necesitamos configurar el servicio en el web.config, en la sección “Services”, apuntando a la clase “System.Web.ApplicationServices.AuthenticationService”. Cabe mencionar que necesitamos establecer la compatibilidad del servicio con ASP.NET, ya que sólo es soportado con http.

<system.serviceModel>
  <services>
    <service name="System.Web.ApplicationServices.AuthenticationService"
             behaviorConfiguration="AuthenticationServiceTypeBehaviors">
      <endpoint contract="System.Web.ApplicationServices.AuthenticationService"
                binding="basicHttpBinding" bindingConfiguration="http"
                bindingNamespace="http://asp.net/ApplicationServices/v200"/>
    </service>
  </services>
  <bindings>
    <basicHttpBinding>
      <!--En producción es aconsejable utilizar https-->
      <binding name="http">
        <security mode="None"/>
      </binding>
    </basicHttpBinding>
  </bindings>
  <behaviors>
    <serviceBehaviors>
      <behavior name="AuthenticationServiceTypeBehaviors">
        <serviceMetadata httpGetEnabled="true"/>
      </behavior>
    </serviceBehaviors>
  </behaviors>
  <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
</system.serviceModel>

Luego necesitamos habilitar los servicios web de autenticación a través de las siguientes líneas:

<system.web.extensions>
  <scripting>
    <webServices>
      <authenticationService enabled="true" requireSSL="false"/>
    </webServices>
  </scripting>
</system.web.extensions>

Y el último cambio que necesitamos hacer al web.config es para establecer la autenticación en “Forms”, utilizando:

<authentication mode="Forms" />

Bien, ahora que ya tenemos configurada nuestra aplicación debemos crear el archivo que exponga el servicio de autenticación. Para esto simplemente tenemos que agregar un archivo de tipo texto (“Text File”), para que el Visual Studio no le cree el code behind, y llamarlo, por ejempo, Authentication.svc. En este archivo vamos a agregar solamente una línea que apunte a la clase de ASP.NET que implementa el servicio de autenticación:

<%@ ServiceHost Service="System.Web.ApplicationServices.AuthenticationService" %>

Ahora ya tenemos una aplicación web que utiliza los servicios de autenticación de ASP.NET y los expone a través de servicios web, por lo tanto ahora, en el proyecto Silverlight, vamos a agregar la referencia al servicio creado en el paso anterior. Para esto pulsamos el botón derecho del mouse sobre el proyecto y elegimos la opción “Add Service Reference”, luego en el diálogo que se muestra pulsamos en “Discover” para que busque los servicios publicados en nuestra solución, elegimos “Authentication.svc” de la lista, cambiamos el namespace donde queremos que cree las clases (en nuestro ejemplo lo dejamos como está: “ServiceReference1”), y pulsamos “OK”. Esto nos creará las clases proxy para que nuestra aplicación Silverlight se conecte y utilice los servicios de autenticación expuestos por nuestra aplicación web.

Agregar servicio web de Autenticación

La clase cliente que creamos, llamada en nuestro ejemplo “ServiceReference1.AuthenticationServiceClient”, tiene tres métodos importantes: LoginAsync, LogoutAsync e IsLoggedInAsync. Nótese que todos los métodos de los servicios web de Silverlight utilizan “Async” como sufijo debido a que siempre son asincrónicos, o sea, cuando los llamamos no se detiene la ejecución de la aplicación hasta obtener el valor solicitado, sino que se ejecutan de forma paralela y luego se llama a un evento determinado al finalizar la consulta, en nuestro caso, LoginCompleted, LogoutCompleted y IsLoggedInCompleted respectivamente.
Para nuestro ejemplo vamos a crear una grilla en la página principal de nuestra aplicación donde agregaremos tres botones para realizar el login, logout y consultar si el usuario está logeado:

<Grid x:Name="LayoutRoot">
    <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>

    <StackPanel Orientation="Horizontal" Grid.ColumnSpan="2">
        <TextBlock Text="Usuario:" />
        <TextBox x:Name="txtUser" />
        <TextBlock Text="Contraseña:" />
        <PasswordBox x:Name="txtPassword" />
    </StackPanel>

    <Button x:Name="btnLogin" Content="Login" Grid.Row="1" Click="btnLogin_Click" />
    <Button x:Name="btnLogout" Content="Logout" Grid.Row="2" Click="btnLogout_Click" />
    <Button x:Name="btnIsLogedIn" Content="IsLogedIn" Grid.Row="3" Click="btnIsLogedIn_Click" />

    <TextBlock x:Name="lblLogin" Grid.Column="1" Grid.Row="1" />
    <TextBlock x:Name="lblLogout" Grid.Column="1" Grid.Row="2" />
    <TextBlock x:Name="lblIsLogedIn" Grid.Column="1" Grid.Row="3" />

    <TextBlock Text="Hora último resultado:" Grid.Row="4" />
    <TextBlock x:Name="lblTime" Grid.Column="1" Grid.Row="4" />
</Grid>

Y por último vamos a agregar el código que maneja los eventos de los botones del ejemplo:

private void btnLogin_Click(object sender, RoutedEventArgs e)
{
    lblLogin.Text = string.Empty;
    ServiceReference1.AuthenticationServiceClient objWSAuth = new ServiceReference1.AuthenticationServiceClient();
    objWSAuth.LoginCompleted += new EventHandler<ServiceReference1.LoginCompletedEventArgs>(objWSAuth_LoginCompleted);
    objWSAuth.LoginAsync(txtUser.Text, txtPassword.Password, string.Empty, true);
}

void objWSAuth_LoginCompleted(object sender, ServiceReference1.LoginCompletedEventArgs e)
{
    lblLogin.Text = e.Result.ToString();
    lblTime.Text = DateTime.Now.ToLongTimeString();
}

private void btnLogout_Click(object sender, RoutedEventArgs e)
{
    lblLogout.Text = string.Empty;
    ServiceReference1.AuthenticationServiceClient objWSAuth = new ServiceReference1.AuthenticationServiceClient();
    objWSAuth.LogoutCompleted += new EventHandler<AsyncCompletedEventArgs>(objWSAuth_LogoutCompleted);
    objWSAuth.LogoutAsync();
}

void objWSAuth_LogoutCompleted(object sender, AsyncCompletedEventArgs e)
{
    lblLogout.Text = "Listo";
    lblTime.Text = DateTime.Now.ToLongTimeString();
}

private void btnIsLogedIn_Click(object sender, RoutedEventArgs e)
{
    lblIsLogedIn.Text = string.Empty;
    ServiceReference1.AuthenticationServiceClient objWSAuth = new ServiceReference1.AuthenticationServiceClient();
    objWSAuth.IsLoggedInCompleted += new EventHandler<ServiceReference1.IsLoggedInCompletedEventArgs>(objWSAuth_IsLoggedInCompleted);
    objWSAuth.IsLoggedInAsync();
}

void objWSAuth_IsLoggedInCompleted(object sender, ServiceReference1.IsLoggedInCompletedEventArgs e)
{
    lblIsLogedIn.Text = e.Result.ToString();
    lblTime.Text = DateTime.Now.ToLongTimeString();
}

Para probar este ejemplo tenemos que tener configurada nuestra aplicación para que utilice un proveedor de membership, por ejemplo, SqlMembershipProvider, el cual se explica en el artículo de Dario (http://www.programandoamedianoche.com/2009/10/autenticacion-por-formularios-en-asp-net/2). Luego, a través de la herramienta de configuración de ASP.NET (ASP.NET Configuration) a la que se puede acceder a través del menú “Project” del Visual Studio, podemos crear usuarios de prueba ingresando en “Security” y luego “Create User”.

Crear usuario

Descargar proyecto de ejemplo para utilizar Autenticación de ASP.NET desde SilverlightComo siempre les dejo el proyecto para que lo prueben.
¡Suerte!

VN:F [1.7.3_972]
Rating: 9.2/10 (5 votos cast)

Betas de exámenes de certificación de .NET 4.0

Miércoles, 17 mar, 2010 @ 18:17 | Por Gustavo Cantero (The Wolf) | .NET, Certificaciones


El día de hoy Microsoft ha lanzado una invitación a los desarrolladores a dar GRATUITAMENTE los exámenes de certificación para .NET Framework 4.0 en su modalidad beta. Quienes aprueben alguno obtendrán la certificación de la misma manera que si fueran los exámenes finales. Los exámenes disponibles son:

  • Exam 71-511, TS: Windows Applications Development with Microsoft .NET Framework 4
  • Exam 71-515, TS: Web Applications Development with Microsoft .NET Framework 4
  • Exam 71-513: TS: Windows Communication Foundation Development with Microsoft .NET Framework 4
  • Exam 71-516: TS: Accessing Data with Microsoft .NET Framework 4
  • Exam 71-518: Pro: Designing and Developing Windows Applications Using Microsoft .NET Framework 4
  • Exam 71-519: Pro: Designing and Developing Web Applications Using Microsoft .NET Framework 4

Cabe mencionar que hay un cupo limitado utilizar los código de promoción para rendir gratuitamente estas certificaciones, por lo cual los insto a inscribirse lo antes posible para no quedar afuera.

Los códigos de promoción de cada examen y demás información lo pueden encontrar en el blog de Microsoft Learning: http://borntolearn.mslearn.net/btl/b/weblog/archive/2010/03/17/register-for-visual-studio-2010-beta-exams.aspx.

Es una oportunidad que no se puede perder.

Suerte a todos!

Agregado el 19/03/2010
En el día de hoy agregaron 150 lugares más para cada examen utilizando los mismos códigos, los cuales copio acá:

Examen Beta Code
71-511 511BC
71-515 515AA
71-513 513CD
71-516 516B1
71-518 518PE
71-519 519ZS

Suerte a todos!

VN:F [1.7.3_972]
Rating: 8.7/10 (3 votos cast)

Introducción a la programación con XNA 3.1 y C#

Jueves, 04 mar, 2010 @ 14:16 | Por Gustavo Cantero (The Wolf) | XNA

XNAXNA es un conjunto de herramientas y librerías de Microsoft que facilita el desarrollo de videojuegos para PC, XBOX y Zune, y cada vez está siendo utilizado más por estudiantes y principiantes para aprender a programar utilizando C# como lenguaje.

A Simple Introduction to Game Programming With C# and XNA 3.1 En el libro “A Simple Introduction to Game Programming With C# and XNA 3.1″ se explica como desarrollar videojuegos a aquellos que no tienen ningún conocimiento de programación, enfocado en los conceptos y fundamentos de XNA.

A quien le interese puede descargar gratuitamente de http://www.lulu.com/product/download/a-simple-introduction-to-game-programming-with-c%23-and-xna-31/5438606 o, si lo prefieren, pueden consultarlo y leerlo on-line en http://xnagamemaking.com. Si prefieren tener el libro “físico”, pueden comprarlo en http://www.lulu.com/content/7658212.

El centro de desarrollo de XNA pueden encontrarlo en http://msdn.microsoft.com/es-ar/aa937791.aspx y pueden descargar el XNA Game Studio 3.1, su documentación y otras herramientas de http://creators.xna.com/es-ar/downloads

VN:F [1.7.3_972]
Rating: 8.8/10 (5 votos cast)