Client side URL generation with client binding...

Developer
Dec 7, 2012 at 2:46 AM
Edited Dec 7, 2012 at 2:47 AM

If I have a controller action like this:

 

public ActionResult MyAction(Guid myParam)

In javasript, I can do this:

var someUrl=@Url.Action(MVC.MyController.MyAction())

That will generate "/MyController/MyAction'....and that is perfect.

Now, if you decide to do some sort of client side binding where the parameter is bound from a datasource you can use

someUrl+"?myParam="+theparam;

All good....now for the issue.  As you can see, the Guid parameter is required for this method.  So, if I change the route for this action to contain a Guid constraint on the myParam parameter, T4MVC will no longer find the route!  This make sense because it passes the area, controller and action to the Routing engine and there is not a match.   How would you propose we could fix that?

One thing I thought of was our own Url extension method - something like Url.ActionRoot(MVC.MyController.MyAction()) where it is NOT sent to MVC routing engine for outbound url generation, but rather just creates /{area}/{controller/{action} url and returns it?  Basically we are in a situation where we do want to restrict what is required from a routing point of view - but we also need to be able to build that route string dynamically without any magic strings.

Without something like the above, we would have to resort to something like this in javascript:

var someUrl="/"+MVC.MyController.Name+"/"MVC.MyController.MyAction.Name

Davide - any thoughts?   (BTW, really liked the change to having the common classes for T4MVC in their own NuGet dll - made it where I could extend it 'normally' rather than putting code in the hooks class.)

Coordinator
Dec 7, 2012 at 8:48 PM

I think the danger here is to build invalid assumptions on the shape of the route. e.g writing "someUrl+"?myParam="+theparam" assumes that myParam is passed on the query string, while it very well could be part of the path.

For that reason, I don't think there can ever be a Url.ActionRoot method. If your route looks like /{Controller}/{Id}/{Action} (unusual but perfectly valid), then the concept of a 'root' is not really meaningful.

I think your best bet to avoid all these issues is to pass a dummy recognizable value in the T4MVC call, and then replace it by what you want on the client using JS.

Note that this is not really T4MVC specific. The same would be true when using straight MVC.

Developer
Dec 7, 2012 at 10:19 PM

Note, in the route shaping - I am never passing a parameters...but yes, it would be more defined as 'convention based' default...

 

However, your best bet does not work.  If I do this:

MVC.MyController.MyAction(3), the url generated on client side would be (perhaps)

/MyController/MyAction?someparam=3

That is very difficult to take that and replace what I want on the client side!

This is T4MVC specific, because T4MVC is helping me eliminate magic strings!

Note, I could easily make the overload I am suggesting as it would be easy...but as you say..it does assume a certain convention....

What would be nice is "/{controller}/{Id}/{action}" is the route....what would be nice would be to get something like this back in javascript:

"/mycontroller/{id}/myaction"   and that could be reliably replaced...humm..I need to think about this....it could work really....

?

Coordinator
Dec 7, 2012 at 10:32 PM

Well, try MVC.MyController.MyAction(3141592654) and you might have more luck. But sure, it's a hack! It's easier to make it work well when you're dealing with a string, where we can pass any token you want. Actually, can't you write MVC.MyController.MyAction().AddRouteValue("myParam", "[TOKEN]")) and then replace that?

But I still think it's interesting to first answer the question: if you were not using T4MVC, and you wanted to do this without making invalid route shape assumptions, what would you write? My only thought would be to use the exact same token replacement approach.

Developer
Dec 7, 2012 at 11:44 PM
Edited Dec 7, 2012 at 11:45 PM

So, as you know we have bounced stuff off each other for years on this T4MVC and resulted in some nice enhancements.  I think I have another one...

Given whatever route shape you want and whatever tokens you have, we need a javascript url.  So here goes:

You define a route like this:

"/{controller}/{action}/{myParam}/{someId}"  - and that could be in an area and/or could have constraints on it, etc.

Then you call Url.JavaScriptReplacableUrl(MVC.MyController.MyAction()) you will get the following back:

"/MyController/MyAction/{myParam}/{someId}" and then you can just use standard javascript to find and replace the tokens!

I am not sold on the extension method name ;-) but I guess it works.  The code that makes this work actually does a manual query of the routing tables to locate the defined route for this.  Note, normally the Url.Action() works, but in cases where we have parameters that are constrained and such - we of course WILL have a route defined for that specific controller/action, this will find it.  (Note, I have begun to use http://attributerouting.net and really like it - this works well with that since you are able to more easily constrain and make routes).  

I think this could work really well - and if it cannot find a route match could default to exactly what Url.Action() does now.

What do you think?

 

--BTW,  MVC.MyController.MyAction().AddRouteValue("myParam", "[TOKEN]"))  will not work because the param has an int constraint on it  - so will not match!!  ----

Coordinator
Dec 8, 2012 at 12:23 AM

Yes, if you can make it work, that sounds interesting. But I'm worried that this can be hard to do. I would go as far as arguing that in the general case, it's impossible to do! :) The reason is that you cannot reliably locate the correct route unless you know the full set of parameters. So with MVC.MyController.MyAction(), you can't always find the right route, because passing a foo param to that vs a bar param might trigger a different route selection.

That being said, I suspect it's possible to make it work so that it is reliable enough in common scenarios, and maybe we can call that good enough, since it's new optional functionality that won't break existing apps. So have a go at it and see how things go!

Developer
Dec 8, 2012 at 12:48 AM

I would agree you cannot locate the correct route reliably without all the parameters....however in this case the problem space says we will not have the parameters and will construct the correct URL based on controller/action/area 'base' url.   There are some cases where even this would not work, depending on how complex your routing was (for the same area/controller/action - but then again, it will just pick the first match - so depending again on route precedence....

I have built the extension and it does work in my initial testing of my scenarios.  Here is example of its use.

//get the base url we will be using...

var url="'@Url.JavaScriptReplacableUrl(MVC.MyArea.MyController.MyAction())"';
//on your post/get/calculation - do a simple replace...
var swappedParamsUrl = url.replace('{" + MVC.MyArea.MyController.MyActionParams.someId+ "}', 112);

Note, no magic strings - even captured the parameter name in there in case anything changes.  I am liking this solution - especially considering it is about 25 lines of code.  :-)

Coordinator
Feb 27 at 4:52 PM
Wayne, see this issue where someone is having problems with it when using areas.
Feb 27 at 6:59 PM
Wayne and David – I'm looking into this now. Thanks for the help so far David.
Feb 27 at 7:16 PM
Wayne and David – see my latest comment on the issue. I found a workaround and it might be possible to incorporate it into the JavaScriptReplacableUrl method.
Coordinator
Feb 27 at 7:25 PM
Thanks Kenny. I'll let Wayne comment as tbh I haven't used this method myself.
Developer
Feb 27 at 7:32 PM
davidebbo wrote:
Thanks Kenny. I'll let Wayne comment as tbh I haven't used this method myself.
Kenny,
I fail to see why adding the "root folder of website" to what JavaScriptReplacableUrl returns would change the output. My only concern is if you were in say...area 'testsite' and used a javascript replacable url from 'mysite'...would it still work correctly.

I have no problem with the fix, I just wish I knew why what you did seemed to work?
Feb 27 at 9:00 PM
Wayne – in the original description in the issue, the URL for one action method has the 'root folder' and the other doesn't.

I just stepped thru the code and confirmed that, for whatever reason, the first action method hits the following code in the JavaScriptReplacableUrl method:
return urlHelper.RouteUrl(null, result.GetRouteValueDictionary());
The second action method does not and I'm guessing that because, in this case, the final line of the JavaScriptReplacableUrl method is executed:
return "/" + specificActionUrl;
The code that generates the value stored in specificActionUrl doesn't account for any possible root folder and URLs starting with a leading / are interpreted as being relative to the server itself, not the relevant site root folder. Wrapping that final line with Url.Content("~" + ...) just adds the site root folder if there is one.
Developer
Feb 27 at 9:58 PM
Ok, sounds good. If it works for you I have no problem changing it as it does not look like it breaks anything.
Feb 28 at 1:26 PM
Wayne, I submitted a pull request just now with the change.
Coordinator
Mar 3 at 12:38 AM
Darn, we have it spelled Replacable instead of Replaceable! :(

I'm going to rename it and live with the fact that 3 people in the world might hate me ;)

Kenny's fix (+ the rename) is in 3.8.0.
Mar 3 at 1:40 PM
Thanks David and Wayne!