Pergunta

Eu tenho dois métodos de ação que são conflitantes. Basicamente, eu quero ser capaz de chegar ao mesmo ponto de vista utilizando duas rotas diferentes, seja por ID de um item ou pelo nome do item e do seu pai (itens podem ter o mesmo nome em diferentes pais). Um termo de pesquisa pode ser usado para filtrar a lista.

Por exemplo ...

Items/{action}/ParentName/ItemName
Items/{action}/1234-4321-1234-4321

Aqui estão os meus métodos de ação (também existem métodos de ação Remove) ...

// Method #1
public ActionResult Assign(string parentName, string itemName) { 
    // Logic to retrieve item's ID here...
    string itemId = ...;
    return RedirectToAction("Assign", "Items", new { itemId });
}

// Method #2
public ActionResult Assign(string itemId, string searchTerm, int? page) { ... }

E aqui estão as rotas ...

routes.MapRoute("AssignRemove",
                "Items/{action}/{itemId}",
                new { controller = "Items" }
                );

routes.MapRoute("AssignRemovePretty",
                "Items/{action}/{parentName}/{itemName}",
                new { controller = "Items" }
                );

Eu entendo porque o erro está ocorrendo, uma vez que o parâmetro page pode ser nulo, mas eu não consigo descobrir a melhor maneira de resolvê-lo. É a minha má concepção para começar? Eu pensei sobre como estender a assinatura de Method #1 para incluir os parâmetros de pesquisa e movendo a lógica em Method #2 para um método particular que iriam ambos chamada, mas eu não acredito que vai realmente resolver a ambigüidade.

Qualquer ajuda seria muito apreciada.


Solução real (com base na resposta de Levi)

Eu adicionei o seguinte classe ...

public class RequireRouteValuesAttribute : ActionMethodSelectorAttribute {
    public RequireRouteValuesAttribute(string[] valueNames) {
        ValueNames = valueNames;
    }

    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
        bool contains = false;
        foreach (var value in ValueNames) {
            contains = controllerContext.RequestContext.RouteData.Values.ContainsKey(value);
            if (!contains) break;
        }
        return contains;
    }

    public string[] ValueNames { get; private set; }
}

E, em seguida, decorado a métodos de ação ...

[RequireRouteValues(new[] { "parentName", "itemName" })]
public ActionResult Assign(string parentName, string itemName) { ... }

[RequireRouteValues(new[] { "itemId" })]
public ActionResult Assign(string itemId) { ... }
Foi útil?

Solução

MVC não suporta a sobrecarga de método baseado unicamente em assinatura, de modo que este irá falhar:

public ActionResult MyMethod(int someInt) { /* ... */ }
public ActionResult MyMethod(string someString) { /* ... */ }

No entanto, faz método de apoio sobrecarga com base no atributo:

[RequireRequestValue("someInt")]
public ActionResult MyMethod(int someInt) { /* ... */ }

[RequireRequestValue("someString")]
public ActionResult MyMethod(string someString) { /* ... */ }

public class RequireRequestValueAttribute : ActionMethodSelectorAttribute {
    public RequireRequestValueAttribute(string valueName) {
        ValueName = valueName;
    }
    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
        return (controllerContext.HttpContext.Request[ValueName] != null);
    }
    public string ValueName { get; private set; }
}

No exemplo acima, o atributo simplesmente diz "Este método partidas se a chave xxx estava presente na solicitação." Você também pode filtrar por informações contidas no trajecto (controllerContext.RequestContext) se isso melhor se adapte às suas finalidades.

Outras dicas

Os parâmetros em suas rotas {roleId}, {applicationName} e {roleName} não correspondem aos nomes de parâmetro em seus métodos de ação. Eu não sei se o que importa, mas torna mais difícil descobrir o que sua intenção é.

Faça o seu de ItemId obedecer a um padrão que pode ser combinado via regex? Se assim for, então você pode adicionar uma restrição para o seu percurso, de modo que é apenas de URL que correspondem ao padrão são identificados como contendo um itemId.

Se o seu itemId continha apenas dígitos, então isso iria funcionar:

routes.MapRoute("AssignRemove",
                "Items/{action}/{itemId}",
                new { controller = "Items" },
                new { itemId = "\d+" }
                );

Edit:. Você também pode adicionar uma restrição à rota AssignRemovePretty para que ambos {parentName} e {itemName} são obrigados

Edit 2:. Também, desde sua primeira ação é apenas redirecionando para o seu 2º ação, você pode remover alguma ambiguidade, renomeando o primeiro

// Method #1
public ActionResult AssignRemovePretty(string parentName, string itemName) { 
    // Logic to retrieve item's ID here...
    string itemId = ...;
    return RedirectToAction("Assign", itemId);
}

// Method #2
public ActionResult Assign(string itemId, string searchTerm, int? page) { ... }

Em seguida, especifique os nomes de ação em suas rotas para forçar o método adequado para ser chamado:

routes.MapRoute("AssignRemove",
                "Items/Assign/{itemId}",
                new { controller = "Items", action = "Assign" },
                new { itemId = "\d+" }
                );

routes.MapRoute("AssignRemovePretty",
                "Items/Assign/{parentName}/{itemName}",
                new { controller = "Items", action = "AssignRemovePretty" },
                new { parentName = "\w+", itemName = "\w+" }
                );

Outra abordagem é para renomear um dos métodos para que não haja conflito. Por exemplo

// GET: /Movies/Delete/5
public ActionResult Delete(int id = 0)

// POST: /Movies/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id = 0)

http://www.asp.net/mvc/tutorials/ getting-started-com-MVC3-part9-cs

Recentemente eu aproveitei a oportunidade para melhorar a resposta da @ Levi para suportar uma ampla gama de cenários que tive de lidar, tais como: suporte parâmetro múltipla, coincidir com qualquer um deles (em vez de todos eles) e até mesmo coincidir com nenhum deles.

Aqui está o atributo que estou usando agora:

/// <summary>
/// Flags an Action Method valid for any incoming request only if all, any or none of the given HTTP parameter(s) are set,
/// enabling the use of multiple Action Methods with the same name (and different signatures) within the same MVC Controller.
/// </summary>
public class RequireParameterAttribute : ActionMethodSelectorAttribute
{
    public RequireParameterAttribute(string parameterName) : this(new[] { parameterName })
    {
    }

    public RequireParameterAttribute(params string[] parameterNames)
    {
        IncludeGET = true;
        IncludePOST = true;
        IncludeCookies = false;
        Mode = MatchMode.All;
    }

    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
    {
        switch (Mode)
        {
            case MatchMode.All:
            default:
                return (
                    (IncludeGET && ParameterNames.All(p => controllerContext.HttpContext.Request.QueryString.AllKeys.Contains(p)))
                    || (IncludePOST && ParameterNames.All(p => controllerContext.HttpContext.Request.Form.AllKeys.Contains(p)))
                    || (IncludeCookies && ParameterNames.All(p => controllerContext.HttpContext.Request.Cookies.AllKeys.Contains(p)))
                    );
            case MatchMode.Any:
                return (
                    (IncludeGET && ParameterNames.Any(p => controllerContext.HttpContext.Request.QueryString.AllKeys.Contains(p)))
                    || (IncludePOST && ParameterNames.Any(p => controllerContext.HttpContext.Request.Form.AllKeys.Contains(p)))
                    || (IncludeCookies && ParameterNames.Any(p => controllerContext.HttpContext.Request.Cookies.AllKeys.Contains(p)))
                    );
            case MatchMode.None:
                return (
                    (!IncludeGET || !ParameterNames.Any(p => controllerContext.HttpContext.Request.QueryString.AllKeys.Contains(p)))
                    && (!IncludePOST || !ParameterNames.Any(p => controllerContext.HttpContext.Request.Form.AllKeys.Contains(p)))
                    && (!IncludeCookies || !ParameterNames.Any(p => controllerContext.HttpContext.Request.Cookies.AllKeys.Contains(p)))
                    );
        }
    }

    public string[] ParameterNames { get; private set; }

    /// <summary>
    /// Set it to TRUE to include GET (QueryStirng) parameters, FALSE to exclude them:
    /// default is TRUE.
    /// </summary>
    public bool IncludeGET { get; set; }

    /// <summary>
    /// Set it to TRUE to include POST (Form) parameters, FALSE to exclude them:
    /// default is TRUE.
    /// </summary>
    public bool IncludePOST { get; set; }

    /// <summary>
    /// Set it to TRUE to include parameters from Cookies, FALSE to exclude them:
    /// default is FALSE.
    /// </summary>
    public bool IncludeCookies { get; set; }

    /// <summary>
    /// Use MatchMode.All to invalidate the method unless all the given parameters are set (default).
    /// Use MatchMode.Any to invalidate the method unless any of the given parameters is set.
    /// Use MatchMode.None to invalidate the method unless none of the given parameters is set.
    /// </summary>
    public MatchMode Mode { get; set; }

    public enum MatchMode : int
    {
        All,
        Any,
        None
    }
}

Para mais informações e como fazer amostras de implementação verificar este post que eu escrevi sobre este tema.

routes.MapRoute("AssignRemove",
                "Items/{parentName}/{itemName}",
                new { controller = "Items", action = "Assign" }
                );

considerar o uso de MVC contribs biblioteca rotas de teste para testar suas rotas

"Items/parentName/itemName".Route().ShouldMapTo<Items>(x => x.Assign("parentName", itemName));
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top