Client Side Validation con ASP.NET MVC 2

En un post anterior, IDataErrorInfo y ASP.NET MVC 1, se hablaba de la posibilidad de validar nuestros objetos desde el modelo y comprobar el resultado de las validaciones través de ModelState.IsValid. Con ASP.NET MVC 2 llegaremos más allá de la mano de JQuery y Data Annotations. 

Desde las primeras revisiones de esta segunda versión, tenemos la posibilidad de reutilizar las validaciones descritas en nuestros objetos en el lado del cliente. Para ello, necesitamos especificar las validaciones oportunas en los objetos, utilizando Data Annotations.

Siguiendo con los ejemplos basados en Twitter, en esta ocasión he creado un cliente que recupera el time line de tus amigos y además te permite responderlos o mandarles un mensaje directo. 

 

Los mensajes directos se utilizan para realizar envíos privados a una persona en concreto. Para poder envíar un mensaje de este tipo, es necesario escribir un texto de tamaño limitado y, además, que tenga un destinatario. 

using System.ComponentModel.DataAnnotations;

namespace ClientSideValidation.Models.Objects
{
    public class DirectMessage
    {
        public long ID { get; set; }
        [Required(ErrorMessage = "Tweet message is required!")]
        [StringLength(131, ErrorMessage = "Your direct message is too long!")]
        public string Message { get; set; }
        public TwittUser Addressee { get; set; }
    }
}

Los dos atributos, Required y StringLength, pertenecen a la propiedad Message donde se está indicando que el mismo es requerido y además que tiene un tamaño máximo de 131 caracteres.

La última propiedad corresponde al remitente, el cual tiene sus propias validaciones en la clase correspondiente. 

using System.ComponentModel.DataAnnotations;

namespace ClientSideValidation.Models.Objects
{
    public class TwittUser
    {
        public long ID { get; set; }
        [Required(ErrorMessage = "User name is required!")]
        public string UserName { get; set; }
        public string PhotoURL { get; set; }
        public string Status { get; set; }
    }
}

Quizás la opción de decorar las propiedades de una clase dificultan la compresión de la misma. Si queremos hacer una separación de las validaciones, podemos hacer uso del atributo MetaDataType creando una clase independiente. 

using System.ComponentModel.DataAnnotations;

namespace ClientSideValidation.Models.Objects
{
    [MetadataType(typeof(TweetReplyMetadata))]
    public class TweetReply
    {
        public long ID { get; set; }
        public string Text { get; set; }
        public TwittUser Addressee { get; set; }
        public Tweet TweetToReply { get; set; }
    }
}

Se decorda la principal indicando en el typeof la clase que contiene las validaciones. 

using System.ComponentModel.DataAnnotations;

namespace ClientSideValidation.Models.Objects
{
    public class TweetReplyMetadata
    {
        [Required(ErrorMessage = "Reply message is required!")]
        [StringLength(131, ErrorMessage = "Your reply is too long!")]
        public string Text { get; set; }
    }
}

Por otro lado, debemos generar el controlador donde seguiremos utilizando ModelState para comprobar el estado de las validaciones desde el servidor. 

using System.Web.Mvc;
using ClientSideValidation.Models;
using ClientSideValidation.Models.Objects;

namespace ClientSideValidation.Controllers
{
    public class TweetController : Controller
    {
        private readonly ITweetService _twitterService;

        public TweetController()
        {
            _twitterService = new TweetService();
        }

        public ActionResult Index()
        {
            return View(_twitterService.FetchTweets());
        }
        public ActionResult Reply(long id)
        {
            var reply = new TweetReply { TweetToReply = _twitterService.ShowTweet(id) };
            return View(reply);
        }

        [HttpPost]
        public ActionResult Reply(TweetReply reply)
        {
            if (ModelState.IsValid)
            {
                _twitterService.ReplyTweet(reply);
                return RedirectToAction("Index");
            }
            return View(reply);
        }

        public ActionResult SendDM(long ID)
        {
            var DM = new DirectMessage { Addressee = _twitterService.RetrieveUser(ID) };
            return View(DM);
        }

        [HttpPost]
        public ActionResult SendDM(DirectMessage DM)
        {
           if (ModelState.IsValid)
            {
                _twitterService.SendDM(DM);
                return RedirectToAction("Index");
            }
            return View(DM);
        }
    }
}

Una vez creadas las vistas, podríamos intentar el envío de un mensaje directo (sin introducir el texto del mismo) y obtendríamos el siguiente resultado: 

Actualmente, cuando pulsamos en el botón Send, estamos acudiendo al servidor y ModelState está comprobando si nuestros objetos son válidos o no para mantenernos en la misma vista y mostrar el error o regresar de nuevo al Time Line. Quizás, si un usuario hace una acción de este tipo, no nos interesa siquiera que acuda al servidor. Aquí es donde viene la mágia :)

  1.     Referenciamos las librerías javascript, ubicadas en la carpeta Scripts, en la Master Page en este orden:
    <%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head runat="server">
        <title>
            <asp:ContentPlaceHolder ID="TitleContent" runat="server" />
        </title>
        <link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
    
        <script src="../../Scripts/jquery-1.3.2.min.js" type="text/javascript"></script>
    
        <script src="../../Scripts/jquery.validate.min.js" type="text/javascript"></script>
    
        <script src="../../Scripts/MicrosoftAjax.js" type="text/javascript"></script>
    
        <script src="../../Scripts/MicrosoftMvcValidation.js" type="text/javascript"></script>
    
        <script src="../../Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script>
    
    </head>
    
  2. Habilitamos la línea Html.EnableClientValidation(); en las vistas donde queramos realizar la validación en el lado del cliente.
    <%@ Page Title="" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ClientSideValidation.Models.Objects.DirectMessage>" %>
    <asp:Content ContentPlaceHolderID="TitleContent" runat="server">
        Direct Message
    </asp:Content>
    <asp:Content ContentPlaceHolderID="MainContent" runat="server">
        <h2>
            Direct Message</h2>
        <%Html.EnableClientValidation(); %>
        <% using (Html.BeginForm())
           {%>
        <div>
            <img src="<%=Model.Addressee.PhotoURL%>" />
            <div>
                <%=Html.DisplayFor(m=>m.Addressee.Status) %>
            </div>
            <%=Html.HiddenFor(m=>m.Addressee.ID) %>
        </div>
        <fieldset>
            <legend>
                <%=Model.Addressee.UserName %></legend>
            <div>
                <%= Html.LabelFor(model => model.Message) %>
            </div>
            <div>
                <%= Html.TextAreaFor(model => model.Message) %>
                <%= Html.ValidationMessageFor(model => model.Message) %>
            </div>
            <p>
                <input value="Send!" />
            </p>
        </fieldset>
        <% } %>
        <div>
            <%=Html.ActionLink("Back to TimeLine", "Index") %>
        </div>
    </asp:Content>

Si lanzamos de nuevo la aplicación, y realizamos la misma operación que antes ¡El resultado es exactamente el mismo pero sin ir al servidor!
Si observamos el código fuente de la página, observamos que la forma de trabajar es mediante la creación de un objeto JSON donde almacena cada unas de las validaciones alojadas en nuestras clases.

//<![CDATA[CommunityServer
if (!window.mvcClientValidationMetadata) { window.mvcClientValidationMetadata = []; }
window.mvcClientValidationMetadata.push({"Fields":[{"FieldName":"Message","ReplaceValidationMessageContents":true,"ValidationMessageId":"form0_Message_validationMessage","ValidationRules":[{"ErrorMessage":"Your direct message is too long!","ValidationParameters":{"minimumLength":0,"maximumLength":131},"ValidationType":"stringLength"},{"ErrorMessage":"Tweet message is required!","ValidationParameters":{},"ValidationType":"required"}]}],"FormId":"form0","ReplaceValidationSummary":false});
//]]>

Como siempre, facilito el código fuente para poder probar el ejemplo completo.

¡Saludos y feliz 2010!

InstanceContextMode en WCF

Cuando creamos un servicio con Windows Communication Foundation, debemos conocer  el comportamiento que queremos que tenga en cuanto a sus instancias se refiere. Para controlar el tiempo de vida del contexto disponemos de los siguientes modos:

  • PerCall: Cada cliente creará una nueva instancia por llamada al servicio.
  • PerSession: Se genera una instancia por sesión.
  • Single: Se utiliza la misma instancia para todas las peticiones independientemente del cliente.

Como he encontrado mucha literatura al respecto y los ejemplos han sido prácticamente el mismo en todos los sitios, he creado uno personalizado donde se realiza el envío de tweets e intento recuperar los mismos utilizando los distintos modos.
Creo un proyecto de tipo WCF Service Library:

Elimino tanto la clase y la interfaz generada por la plantilla y creo una clase Tweet.cs con el rol de DataContract.


using System.Runtime.Serialization;

namespace InstanceContextModeWCF
{
    [DataContract]
    public class Tweet
    {
        [DataMember]
        public int Id { get; set; }
        [DataMember]
        public string User { get; set; }
        [DataMember]
        public string Message { get; set; }
    }
}

Una interfaz como ServiceContract:


using System.Collections.Generic;
using System.ServiceModel;

namespace InstanceContextModeWCF
{
    [ServiceContract]
    public interface ITwitter
    {
        [OperationContract]
        void SendTweet(Tweet tweet);

        [OperationContract]
        List<Tweet> GetMyTweets();
    }
}

Y por último la clase que implementa nuestro contrato.


using System.Collections.Generic;
using System.ServiceModel;

namespace InstanceContextModeWCF
{
    //[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    //[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
    //[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    public class Twitter : ITwitter
    {
        private readonly List<Tweet> _tweets = new List<Tweet>();

        public void SendTweet(Tweet tweet)
        {
            _tweets.Add(tweet);
        }

        public List<Tweet> GetMyTweets()
        {
            return _tweets;
        }
    }
}

En la clase Twitter he dejado comentadas las tres posibilidades. ¡Veamos cada una de ellas!

PER CALL

Cuando activamos el modo PerCall nuestro servicio se comportará de la siguiente manera: Creará una instancia para realizar la llamada al método solicitado y, una vez que haya finalizado la llamada, destruirá la instancia creada.

Para este primer ejemplo he activado la primera línea comentada y he seguido los pasos en el orden que señalo en la imagen. Como podemos ver, he creado una conexión con el programa que está actuando como servidor, he rellenado el apartado de mensaje y he enviado el mismo. En este caso, no ha sido posible recuperar el tweet al realizar una nueva llamada con PerCall.

PER SESSION

Con PerSession vamos a utilizar la misma instancia hasta que la sesión finalice. Es decir, un cliente crea un canal y tiene la posibilidad de realizar distintas llamadas a distintos métodos dentro del mismo contexto. Siguiendo el mismo ejemplo, habilitando la segunda línea y deshabilitando la primera, realizamos las mismas acciones desde el cliente.

Como la instancia permanece con vida durante toda la sesión, el cliente es capaz de poder enviar un tweet y después recuperarlo. Sin embargo, si nosotros pulsamos sobre Disconnect  e intentamos recuperar el tweet enviado a través del botón Get All!, la información ya habrá desaparecido al haber creado una nueva sesión.


private void btnConnect_Click(object sender, EventArgs e)
{
    _channel = new ChannelFactory<ITwitter>("TwitterService");
    _client = _channel.CreateChannel();
    lblState.Text = "Connected!";
    lblState.ForeColor = System.Drawing.Color.Blue;
}

private void btnDisconnect_Click(object sender, EventArgs e)
{
    _channel.Close();
    lblState.Text = "Disconnected!";
    lblState.ForeColor = System.Drawing.Color.Red;
}

SINGLE

El modo Single se utiliza para mantener una misma instancia desde la primera petición de un cliente cualquiera hasta que el servicio se ha parado completamente.

Si habilitamos el último modo comentado en el código, y deshabilitamos el anterior, podemos realizar los siguientes pasos:

Si cerramos y abrimos de nuevo el cliente, sin cerrar el programa servidor, y pulsamos de nuevo en Get All!  descubrimos que nuestro tweet sigue con vida ;)

Adjunto el ejemplo completo, con el servicio, cliente y servidor. Están los tres proyectos en una misma solución. En cualquier caso, podemos abrir los .exe ubicados en el bin de cada proyecto como administrador o abrir dos Visual Studio con credenciales de administrador para poder debuggear en Vista o Windows 7 :)

¡Saludos!

IDataErrorInfo y ASP.NET MVC 1

Versión actualizada para ASP.NET MVC 2 aquí

En la mayoría de las aplicaciones es probable que el usuario final necesite rellenar un formulario, modificar datos requeridos, etc. Como es normal, en muchas de estas ocasiones no se introducen los datos de forma correcta, obviamos alguno de los campos requeridos, etcétera.

La interfaz IDataErrorInfo nos ofrece la posibilidad de generar errores personalizados y poder mostrarlos en la interfaz de usuario correspondiente. Cuando creamos una vista de manera automática con MVC,  está preparada para mostrar estos errores gracias a los siguientes elementos:

  •     ValidationSummary: nos permite mostrar todos los errores producidos a modo de resumen en la vista.
  •     ValidationMessage: puede resultar útil a la hora de mostrar cada error de forma particular, enlazando el mismo con un control de nuestra interfaz.
  •     ModelState: se encargará de recopilar todos los errores producidos en nuestro objeto.

 Si generamos una vista de tipo Create o Edit, podemos ver ValidationSummary al comienzo y ValidationMessage en cada uno de los controles.


<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MovieManager.Models.Objects.Movie>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Edit
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2>
        Edit</h2>
    <%= Html.ValidationSummary("Edit was unsuccessful. Please correct the errors and try again.") %>
    <% using (Html.BeginForm())
       {%>
    <fieldset>
        <legend>Fields</legend>
        <p>
            <label for="Name">
                Name:</label>
            <%= Html.TextBox("Name", Model.Name) %>
            <%= Html.ValidationMessage("Name", "*") %>
        </p>
        <p>
            <label for="Genre">
                Genre:</label>
            <%= Html.TextBox("Genre", Model.Genre) %>
            <%= Html.ValidationMessage("Genre", "*") %>
        </p>
        <p>
            <label for="Synopsis">
                Synopsis:</label>
            <%= Html.TextBox("Synopsis", Model.Synopsis) %>
            <%= Html.ValidationMessage("Synopsis", "*") %>
        </p>
        <p>
            <label for="Year">
                Year:</label>
            <%= Html.TextBox("Date", String.Format("{0:g}", Model.Date)) %>
            <%= Html.ValidationMessage("Date", "*") %>
        </p>
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
    <% } %>
    <div>
        <%=Html.ActionLink("Back to List", "Index") %>
    </div>
</asp:Content>

Para mostrar un ejemplo, voy a implementar IDataErrorInfo en la clase Movie para controlar la creación y edición de películas.


using System;
using System.ComponentModel;

namespace MovieManager.Models.Objects
{
    public class Movie : IDataErrorInfo
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Genre { get; set; }
        public string Synopsis { get; set; }
        public DateTime? Date { get; set; }

        public string this[string columnName]
        {
            get
            {
                var result = string.Empty;
                switch (columnName)
                {
                    case "Name":
                        {
                            if (string.IsNullOrEmpty(Name))
                                result = "Movie Name is required";
                            break;
                        }
                    case "Genre":
                        {
                            if (string.IsNullOrEmpty(Genre))
                                result = "Movie Genre is required";
                            break;
                        }
                    case "Year":
                        {
                            if (Date.HasValue)
                            {
                                if (Date.Value == DateTime.MinValue)
                                {
                                    result = "Movie Date is must be real";
                                }
                            }
                            break;
                        }
                }
                return result;
            }
        }

        public string Error
        {
            get { return string.Empty; }
        }
    }
}

Debemos crear dos propiedades:

  • Item: Devuelve el mensaje de error de la propiedad solicitada entre corchetes. En este caso, se están cubriendo aquellas propiedades que se consideran requeridas o, en el caso de Date, con una fecha correcta. Un string vacío se considera como acertado.
  • Error: Devuelve el mensaje de error por el cual el objeto es incorrecto. En esta ocasión, nos interesa más el caso particular de cada propiedad. Por ello, retornamos solamente un string.empty, ya que sería su valor por defecto.

Cuando llamamos a una acción desde la vista, y bindeamos la respuesta dentro de un objeto te tipo Movie, automáticamente comprueba que cada propiedad bindeada cumpla las condiciones implementadas en la propiedad ítem de nuestra clase. Si alguna de ellas no cumple las condiciones establecidas, quedará registrada en ModelState como una propiedad inválida.

[AcceptVerbs(HttpVerbs.Post)]
 public ActionResult Create([Bind(Exclude = "Id")] Movie newMovie)
 {
     if (ModelState.IsValid)
     {
         _movieRepository.SaveMovie(newMovie);
         return RedirectToAction("Index");
     }
     return View();
 }

Si intentamos crear una nueva película y no rellenamos los campos obligatorios obtendríamos la siguiente imagen:

¡Saludos!