Ooops.. esta página no la tengo, pero tengo otra parecida para tí…

Este genial post de José M. Aguilar sobre como procesar peticiones existentes en ASP.NET MVC, me ha dado una idea que quiero compartir con vosotros… El tema consiste en que si el usuario se equivoca y entra una URL errónea como /Home/Jindex (en lugar de /Home/Index) le podemos sugerir que quizá quería ir a /Home/Index. Vamos a ver como podríamos hacerlo…

La idea es que cuando recibamos una petición errónea en el HandleUnknownAction miremos cuales son las acciones del controlador y miremos cual es la acción que más se aproxima a la acción que el usuario ha entrado.

1. Obteniendo las acciones del controlador actual

Si usamos el ActionInvoker por defecto de ASP.NET MVC, las acciones están mapeadas a métodos públicos del controlador. El nombre del método define el nombre de la acción, excepto si el método está decorado con el atributo ActionNameAttribute que especifica un nombre de acción distinto.

Así pues, una manera de obtener las acciones del controlador actual es recorrerse sus métodos públicos y obtener su nombre o bien el nombre del atributo ActionNameAttribute que tuviese asociado:

namespace System.Web.Mvc
{
public static class ControllerExtensions
{
public static IEnumerable<string> GetAllActions(this Controller self)
{
var methods = self.GetType().GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public);
return methods.Select(x =>
x.GetCustomAttributes(typeof(ActionNameAttribute), true).Length == 1 ?
((ActionNameAttribute)x.GetCustomAttributes(typeof(ActionNameAttribute), true)[0]).Name :
x.Name);
}
}
}

2. Calculando que acción es la más parecida a la que ha entrado el usuario

El siguiente paso es ver cual de todas las acciones se parece más a la acción que ha entrado el usuario. Hay varios algoritmos para calcular la distancia entre dos cadenas, uno conocido es la distancia de Levenshtein que es el que yo he usado. En el artículo de la wikipedia enlazado tenéis el pseudo-código del algoritmo… para los vagos aquí tenéis una implementación de la distancia de Levenshtein en C#.

Yo he implementado el método cómo un método de extensión de la clase string:

namespace MvcApplication1.Extension
{
public static class StringExtensions
{
public static int LevenshteinDistance(this string s, string t)
{
// Ver una implementación en http://www.merriampark.com/ldcsharp.htm
}
}
}

Finalmente en el método HandleUnknownAction sólo nos queda recorrer el enumerable de acciones devuelto por GetAllActions y para cada acción calcular la distancia de Levenshtein entre esta acción y el nombre que ha entrado el usuario… y cojer la menor:

protected override void HandleUnknownAction(string actionName)
{
var actions = this.GetAllActions();
int min = int.MaxValue;
string newAction = null;
foreach (var action in actions)
{
int ld = action.LevenshteinDistance(actionName);
if (ld < min)
{
min = ld;
newAction = action;
}
}
if (min < int.MaxValue)
{
View("RedirectView", new RedirectModel(newAction, "Home", actionName)).
ExecuteResult(this.ControllerContext);
}
else
{
base.HandleUnknownAction(actionName);
}
}

La clase RedirectModel es una clase que tiene tres propiedades: Acción a donde pensamos que el usuario quería ir, controlador de dicha acción, y acción tecleada por el usuario:

public class RedirectModel
{
public RedirectModel(string action, string controller, string originalAction)
{
this.Action = action;
this.Controller = controller;
this.OriginalAction = originalAction;
}

public string Action { get; private set; }
public string Controller { get; private set; }
public string OriginalAction { get; private set; }
}

Finalmente, sólo nos queda la vista “RedirectView”, que yo he puesto en Shared (para que pueda ser reutilizada por varios controladores):

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

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
ViewUserControl1
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

<h2>Pos por <%= Model.OriginalAction%> no me viene nada...</h2>

OOppps... esta página no existe... ¿seguro que no querías ir a
<a href="<%= Url.Action(Model.Action, Model.Controller)%>"><%= Model.Action %></a>?
</asp:Content>

Y este es el resultado, si el usuario teclea /Home/Jindex esto es lo que se le muestra:

image

Ya véis, qué fácil 🙂 Un saludo!!!

PD: Os dejo un zip con la solución completa!!!

2 comentarios sobre “Ooops.. esta página no la tengo, pero tengo otra parecida para tí…”

  1. @José M.
    Gracias a tí, por el comentario!!! 🙂
    La verdad, se me ocurrió esto, cuando poco despúes de leer tu artículo google me soltó uno de esos «quizo decir» 🙂

    @Ivan
    Muchas gracias! Encantado de que te haya gustado. Para cualquier duda, ya sabes donde estoy 🙂

    Un saludo a todos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *