10

I'm having trouble getting C# and JavaScript/jQuery to play nice here.

I have a knockout view model, plain old javascript object... one of its property/methods fires off an .ajax() call, and the url parameter is built using some of its other property values (javascript variables).

This works fine when completely contained in JavaScript, but when deployed as an app to IIS, the relative pathing is hosed.

In MVC3 normally I would use something like @Url.Action and let the server side build the address... but again, the trick is C# is unaware of the changing javascript values.

Code:

var viewModel = {
    vendors: ko.observableArray([]),
    count: ko.observable(10),
    page: ko.observable(1),
    filterText: ko.observable(""),
    submit: function () {
        $.ajax({
            // works fine, until deploy when it is no longer a site relative URL
            url: 'vendors/' + viewModel.count() + '/' + viewModel.filterText(),

            // does not work, because C# is unaware of the javascript variables.
            //url: @Url.Action("Vendors", "Home", new { count = viewModel.count(), filter = viewModel.filterText() })

            dataType: 'json',
            success: function (data) {
                viewModel.vendors(data);
            }
        });    
    }
    // next: // load sequence starting with (page+1 * count) 
    // previous: // load sequence starting with (page-1 * count)
};
ko.applyBindings(viewModel);

Question:

My question then is, how can I build the url for the ajax call using the javascript variable values (ex. count, filterText) and still map from the relative root of the application?

one.beat.consumer
  • 9,414
  • 11
  • 55
  • 98
  • Is the javascript viewModel defined in a .js file, or on your view's page? You might consider mapping the url @Url.Action("Vendors","Home") and then adding the properties in javascript – Chris Carew Mar 16 '12 at 22:14

3 Answers3

18

The way we do this in my MVC 3 project is to include the following in the Master Layout:

<script type="text/javascript">
    var baseSiteURL = '@Url.Content("~/")';
</script>

Then you just prepend that to your URLs in the JavaScript.

Which in your sample would read:

url: baseSiteURL + 'vendors/' + viewModel.count() + '/' + viewModel.filterText()
jmoerdyk
  • 5,544
  • 7
  • 38
  • 49
16

One possibility is to send those javascript values as request parameters:

$.ajax({
    url: '@Url.Action("vendors")',
    data: { count: viewModel.count(), filter: viewModel.filterText() },
    dataType: 'json',
    success: function (data) {
        viewModel.vendors(data);
    }
});

Of course this implies that you are using default routes and the parameters will simply be sent to the server either as query string parameters (if you are using GET) or as part of the POST request body. In both cases you will fetch them on the server the same way:

public ActionResult Vendors(int count, string filter)
{
    ...
}

Another possibility, if you absolutely insist on having some custom routes for your AJAX requests, would be to use a simple string replace:

var url = '@Url.Action("vendors", new { count = "__count__", filter = "__filterText__" })';
url = url.replace('__count__', viewModel.count())
         .replace('__filter__', viewModel.filterText());
$.ajax({
    url: url,
    dataType: 'json',
    success: function (data) {
        viewModel.vendors(data);
    }
});
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • you're answer was most helpful, though I didn't want the query string params. See my answer... if you edit yours to include my example, I'll toss you the check. If not, I'll mark mine. Thanks for your help. – one.beat.consumer Mar 16 '12 at 23:00
  • 1
    @one.beat.consumer, no, will not include your answer as part of mine as I don't like it. In your answer you are hardcoding your urls instead of using url helpers and routes to generate them. In your code you assume in your javascript that the url is of the form `{controller}/{action}/{count}/{filter}`. This is fragile and it will break at the moment you touch at your routes. With my solution you are using an url helper to generate the entire url based on your route definitions. You simply replace on the client the dummy placeholders with their corresponding javascript values. – Darin Dimitrov Mar 16 '12 at 23:27
  • aside from a single `/` what is hard coded in my answer? I am using the same Url helper as you (though I'll probably change it to use the named route rather than an action pattern - to account for the fragility you mention) and the javascript variables always have values because they are initialized in ko.observables before this method can ever be called. again, aside from the single whack, id like to understand why you think it's much more different? – one.beat.consumer Mar 17 '12 at 16:19
  • 1
    @one.beat.consumer, what's different is that you rely on a specific order of your route tokens and that they are separated by `/`. What if you decide to invert the order of the `{count}` and `{filter}` route tokens in your Global.asax? Now you will also have to modify all your client code in which you hardcoded this order. On the other hand if you use an url helper: `@Url.Action("vendors", new { count = "__count__", filter = "__filterText__" })` it doesn't really matter how your routes are configured. – Darin Dimitrov Mar 17 '12 at 16:23
  • Also, the route looks like this `("Api-Vendors", "/vendors/{count}/{filter}", new { controller = "Home", action = "Vendors", UrlParameter.Optional, UrlParameter.Optional })` and the signature on the Action method looks like this: `public ActionResult Vendors(int count = 20, string filter = "")` just to ensure even if someone breaks the javascript or trys to access it with finger typed URLs the values were always have defaults. still too brittle? – one.beat.consumer Mar 17 '12 at 16:24
  • I see your point. Is it at all possible to support the clean URL structure without querystring params with a dummy value replace approach? As always, Darin, your expertise is appreciated. – one.beat.consumer Mar 17 '12 at 16:28
  • That's what I suggested: dummy values replace approach if you want clean urls. But honestly why care about clean url in AJAX requests? Query string parameters do this just fine and nobody sees them. – Darin Dimitrov Mar 17 '12 at 16:34
  • The sample code for the solution you dont suggest does not work fo me. The url gets rendered with an Html encoded & sign in the html source (&) and that leads to the parameter value "filter" not coming in in the action. I needed to use @Html.Raw(Url.Action(.... – Mathias F Dec 20 '17 at 08:31
3

Darin's answer is the most solid, but it requires sending data using query string params on the ajax call.

To avoid that, I simply wrapped the @Url.Action() method in quotes and appended the javascript values as I intended.

url: "@Url.Action("Vendors", "Home")/" + viewModel.count() + "/" + viewModel.filterText(),

Ultimately this produced the best results as it lets me keep a very clean url... Request.ApplicationPath seemed too hackish and can technically be null... @Url.Content() is intended for static "content" paths (ex. images, scripts)... etc.

one.beat.consumer
  • 9,414
  • 11
  • 55
  • 98
  • filterText for me looks like something thats entered by the user and a ' sign might end up in it. This will pose a XSS vulnerability. – Mathias F Dec 20 '17 at 08:46