2014-12-12 15 views
23

Esiste un metodo consigliato per usare il nuovoCome utilizzare C# nameof() con ASP.NET MVC Url.Action

nameof() 

espressione in ASP.NET MVC per i nomi di controller?

Url.Action("ActionName", "Home") <------ works 

vs

Url.Action(nameof(ActionName), nameof(HomeController)) <----- doesn't work 

ovviamente non funziona a causa di nameof (HomeController) converte in "HomeController" e ciò che MVC ha bisogno è solo "Home".

+0

Suppongo che si possa fare un extensionmethod in poi 'classe Controller' whhich potrebbe tagliare la parte "Controller" lontano da voi. Ma non ho ancora visto una raccomandazione specifica su come usarla. –

+0

Sembra che [T4MVC] (https://t4mvc.codeplex.com/) sia più adatto per questo T4 utilizzato da –

+0

. Ora con nameof come funzione di una lingua, suppongo che sia meglio rimanere in C# ogni volta che è possibile. – Mikeon

risposta

5

consideri un metodo di estensione:

public static string UrlName(this Type controller) 
{ 
    var name = controller.Name; 
    return name.EndsWith("Controller") ? name.Substring(0, name.Length - 10) : name; 
} 

quindi è possibile utilizzare:

Url.Action(nameof(ActionName), typeof(HomeController).UrlName()) 
+0

passare 'Type' è rischioso. –

+2

Per questo motivo vorrei lanciare un'eccezione se il nome non termina con "Controller". –

17

mi piace James' suggestion di utilizzare un metodo di estensione. C'è solo un problema: sebbene tu stia usando nameof() e hai eliminato le stringhe magiche, c'è ancora un piccolo problema di sicurezza dei tipi: stai ancora lavorando con le stringhe. Come tale, è molto facile dimenticare di usare il metodo di estensione, o di fornire una stringa arbitraria che non è valida (per esempio digitando il nome di un controller).

Penso che possiamo migliorare il suggerimento di James' utilizzando un generic extension method per il regolatore, dove il parametro generico è il controller di destinazione:

public static class ControllerExtensions 
{ 
    public static string Action<T>(this Controller controller, string actionName) 
     where T : Controller 
    { 
     var name = typeof(T).Name; 
     string controllerName = name.EndsWith("Controller") 
      ? name.Substring(0, name.Length - 10) : name; 
     return controller.Url.Action(actionName, controllerName); 
    } 
} 

L'utilizzo è ora molto più pulito:

this.Action<HomeController>(nameof(ActionName)); 
1

Tutto le soluzioni che ho visto finora hanno uno svantaggio: mentre cambiano il nome del controllore o dell'azione, non garantiscono la coerenza tra queste due entità. Si può specificare un'azione da un controller diverso:

public class HomeController : Controller 
{ 
    public ActionResult HomeAction() { ... } 
} 

public class AnotherController : Controller 
{ 
    public ActionResult AnotherAction() { ... } 

    private void Process() 
    { 
     Url.Action(nameof(AnotherAction), nameof(HomeController)); 
    } 
} 

per renderlo ancora peggio, questo approccio non può tenere conto dei numerosi attributi si può applicare ai controllori e/o azioni di cambiare il routing, per esempio RouteAttribute e RoutePrefixAttribute, quindi qualsiasi modifica al routing basato sugli attributi potrebbe passare inosservata.

Infine, il Url.Action() sé non garantisce la coerenza tra metodo di azione e dei suoi parametri che costituiscono l'URL:

public class HomeController : Controller 
{ 
    public ActionResult HomeAction(int id, string name) { ... } 

    private void Process() 
    { 
     Url.Action(nameof(HomeAction), new { identity = 1, title = "example" }); 
    } 
} 

La mia soluzione è basata su Expression e metadati:

public static class ActionHelper<T> where T : Controller 
{ 
    public static string GetUrl(Expression<Func<T, Func<ActionResult>>> action) 
    { 
     return GetControllerName() + '/' + GetActionName(GetActionMethod(action)); 
    } 

    public static string GetUrl<U>(
     Expression<Func<T, Func<U, ActionResult>>> action, U param) 
    { 
     var method = GetActionMethod(action); 
     var parameters = method.GetParameters(); 

     return GetControllerName() + '/' + GetActionName(method) + 
      '?' + GetParameter(parameters[0], param); 
    } 

    public static string GetUrl<U1, U2>(
     Expression<Func<T, Func<U1, U2, ActionResult>>> action, U1 param1, U2 param2) 
    { 
     var method = GetActionMethod(action); 
     var parameters = method.GetParameters(); 

     return GetControllerName() + '/' + GetActionName(method) + 
      '?' + GetParameter(parameters[0], param1) + 
      '&' + GetParameter(parameters[1], param2); 
    } 

    private static string GetControllerName() 
    { 
     const string SUFFIX = nameof(Controller); 
     string name = typeof(T).Name; 
     return name.EndsWith(SUFFIX) ? name.Substring(0, name.Length - SUFFIX.Length) : name; 
    } 

    private static MethodInfo GetActionMethod(LambdaExpression expression) 
    { 
     var unaryExpr = (UnaryExpression)expression.Body; 
     var methodCallExpr = (MethodCallExpression)unaryExpr.Operand; 
     var methodCallObject = (ConstantExpression)methodCallExpr.Object; 
     var method = (MethodInfo)methodCallObject.Value; 

     Debug.Assert(method.IsPublic); 
     return method; 
    } 

    private static string GetActionName(MethodInfo info) 
    { 
     return info.Name; 
    } 

    private static string GetParameter<U>(ParameterInfo info, U value) 
    { 
     return info.Name + '=' + Uri.EscapeDataString(value.ToString()); 
    } 
} 

Ciò impedisce dal passaggio di parametri errati per generare un URL:

ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, 1, "example"); 

Poiché è un'espressione lambda, l'azione è sempre vincolata al relativo controller. (E hai anche Intellisense!) Una volta scelta l'azione, ti costringe a specificare tutti i suoi parametri di tipo corretto.

Il codice indicato continua a non risolvere il problema di routing, tuttavia è almeno possibile fissarlo, poiché sono disponibili sia Type.Attributes sia MethodInfo.Attributes del controller.

EDIT:

Come @CarterMedlin sottolineato, parametri di azione di tipo non-primitivo non possono avere uno-a-uno vincolante per interrogare parametri. Attualmente, questo viene risolto chiamando ToString() che può essere sovrascritto nella classe parametro appositamente per questo scopo. Tuttavia, l'approccio potrebbe non essere sempre applicabile, né controlla il nome del parametro.

Per risolvere il problema, è possibile dichiarare la seguente interfaccia:

public interface IUrlSerializable 
{ 
    Dictionary<string, string> GetQueryParams(); 
} 

e implementarlo nella classe di parametri:

public class HomeController : Controller 
{ 
    public ActionResult HomeAction(Model model) { ... } 
} 

public class Model : IUrlSerializable 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 

    public Dictionary<string, string> GetQueryParams() 
    { 
     return new Dictionary<string, string> 
     { 
      [nameof(Id)] = Id, 
      [nameof(Name)] = Name 
     }; 
    } 
} 

E rispettive modifiche ActionHelper:

public static class ActionHelper<T> where T : Controller 
{ 
    ... 

    private static string GetParameter<U>(ParameterInfo info, U value) 
    { 
     var serializableValue = value as IUrlSerializable; 

     if (serializableValue == null) 
      return GetParameter(info.Name, value.ToString()); 

     return String.Join("&", 
      serializableValue.GetQueryParams().Select(param => GetParameter(param.Key, param.Value))); 
    } 

    private static string GetParameter(string name, string value) 
    { 
     return name + '=' + Uri.EscapeDataString(value); 
    } 
} 

Come potete vedere, ha ancora un fallback per ToString(), quando la classe parametro non implementa l'interfaccia.

Usage:

ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, new Model 
{ 
    Id = 1, 
    Name = "example" 
}); 
+0

Hai visto T4MVC? Risolve tutti i tuoi problemi – trailmax

+0

@trailmax, sì, ha molte funzionalità ma a volte non ti serve più di tanto. –

+0

Questa è un'ottima soluzione. Il mio unico problema è che la maggior parte delle volte i miei 'routeValues' non corrispondono al conteggio e ai tipi dei miei parametri di azione. I tipi di date sono un buon esempio, in quanto generalmente desidero formattarli come stringhe di trattini prima di inserirli in un URL. Apprezzo il lavoro in quanto ha contribuito alla mia soluzione. –

0

ho bisogno di fare in modo routeValues sono trattati correttamente, e non sempre trattati come querystring valori. Ma, voglio ancora assicurarmi che le azioni corrispondano ai controller.

La mia soluzione è di creare sovraccarichi di estensione per Url.Action.

<a href="@(Url.Action<MyController>(x=>x.MyAction))">Button Text</a> 

I sovraccarichi per azioni a parametro singolo per diversi tipi. Se ho bisogno di passare routeValues ...

<a href="@(Url.Action<MyController>(x=>x.MyAction, new { myRouteValue = myValue }))">Button Text</a> 

Per le azioni con parametri complicati che non ho creato esplicitamente per sovraccarichi, i tipi devono essere specificato con il tipo di controller in modo che corrisponda alla definizione dell'azione.

<a href="@(Url.Action<MyController,int,string>(x=>x.MyAction, new { myRouteValue1 = MyInt, MyRouteValue2 = MyString}))">Button Text</a> 

Naturalmente, la maggior parte del tempo l'azione rimane entro lo stesso controller, quindi ho ancora solo uso nameof per quelli.

<a href="@Url.Action(nameof(MyController.MyAction))">Button Text</a> 

Poiché routeValues non necessariamente corrispondere i parametri di azione, questa soluzione consente una tale flessibilità.

estensione di codice

namespace System.Web.Mvc { 
    public static class UrlExtensions { 

    // Usage : <a href="@(Url.Action<MyController>(x=>x.MyActionNoVars, new {myroutevalue = 1}))"></a> 
    public static string Action<T>(this UrlHelper helper,Expression<Func<T,Func<ActionResult>>> expression,object routeValues = null) where T : Controller 
     => helper.Action<T>((LambdaExpression)expression,routeValues); 

    // Usage : <a href="@(Url.Action<MyController,vartype1>(x=>x.MyActionWithOneVar, new {myroutevalue = 1}))"></a> 
    public static string Action<T, P1>(this UrlHelper helper,Expression<Func<T,Func<P1,ActionResult>>> expression,object routeValues = null) where T : Controller 
     => helper.Action<T>(expression,routeValues); 

    // Usage : <a href="@(Url.Action<MyController,vartype1,vartype2>(x=>x.MyActionWithTwoVars, new {myroutevalue = 1}))"></a> 
    public static string Action<T, P1, P2>(this UrlHelper helper,Expression<Func<T,Func<P1,P2,ActionResult>>> expression,object routeValues = null) where T : Controller 
     => helper.Action<T>(expression,routeValues); 

    // Usage : <a href="@(Url.Action<MyController>(x=>x.MyActionWithOneInt, new {myroutevalue = 1}))"></a> 
    public static string Action<T>(this UrlHelper helper,Expression<Func<T,Func<int,ActionResult>>> expression,object routeValues = null) where T : Controller 
     => helper.Action<T>((LambdaExpression)expression,routeValues); 

    // Usage : <a href="@(Url.Action<MyController>(x=>x.MyActionWithOneString, new {myroutevalue = 1}))"></a> 
    public static string Action<T>(this UrlHelper helper,Expression<Func<T,Func<string,ActionResult>>> expression,object routeValues = null) where T : Controller 
     => helper.Action<T>((LambdaExpression)expression,routeValues); 

    //Support function 
    private static string Action<T>(this UrlHelper helper,LambdaExpression expression,object routeValues = null) where T : Controller 
     => helper.Action(
       ((MethodInfo)((ConstantExpression)((MethodCallExpression)((UnaryExpression)expression.Body).Operand).Object).Value).Name, 
       typeof(T).Name.Replace("Controller","").Replace("controller",""), 
       routeValues); 
    } 
} 
Problemi correlati