Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC4 Razor confused about braces

I've got a fairly simple question for all the Razor experts out there. I'm trying to make a jQuery $.ajax() call to a URL, using Url.Content() to translate the home-relative path into an root-relative path. In so doing, Razor is getting a bit confused about where the end of my @section is located. I'd prefer to be able to specify the URL inline, but when I do that, Razor thinks that the end of the $.ajax() parameter list is the end of my @section. I'm using @section because I want to use layouts to place my javascript at the bottom of each file. Why is Razor getting so confused? I've even tried using @(Url.Content(...)), but that doesn't work either.

Also, is this the best way to approach the problem? I'm using the ASP.NET MVC 4 Preview.

This works:

@section Scripts {
    <script type="text/javascript">        
        var getMessagesUrl = '@Url.Content("~/Logging/GetMessages")';

        $(document).ready(function () {
            $.ajax({
                url: getMessagesUrl,
                dataType: 'html',
                success: function (result) {
                    $('tbody').html(result);
                }
            });
        });
    </script>
}

This doesn't:

@section Scripts {
    <script type="text/javascript">        
        $(document).ready(function () {
            $.ajax({
                url: '@Url.Content("~/Logging/GetMessages")',
                dataType: 'html',
                success: function (result) {
                    $('tbody').html(result);
                }
            }); //Razor thinks this is the curly-brace that ends the section!
        });
    </script>
}
like image 693
Dave Markle Avatar asked Feb 20 '12 18:02

Dave Markle


4 Answers

This is likely down to the behaviour of the parser. When it encounters the @ symbol, the parser switches to code mode and will read the implicit expression Url.Content("~/Logging.GetMessages") (well, actually it will read until the ' symbol, determine it is not a valid character in an expression and trackback to return until the end of the ). It's after this stage that the parser is getting a little confused with your view because it's likely in code mode when it encounters the final } character, and thinks it is the end of of a code span.

The reality is, you have to be quite careful when using javascript within a razor view with C#. The transitions to code are explicit, e.g. @ and after a {, but the transitions to markup are a little harder to determine.

Razor aside, my recommendation would be to stick your javascript application code in an external file, and take advantage of data-* attributes to convey meta information to your application code, e.g:

<script id="messageScript" type="text/javascript"
    src="/Scripts/messages.js" 
    data-messages="@Url.Content("~/Logging/GetMessages")"></script>

Which you can access as:

(function($) {

    $(function() {
        var $this = $("#messageScript");
        $.ajax({
            url: $this.attr("data-messages"),
            type: "html",
            success: function(result) {
                $("tbody").html(result);
            }
        });
    });

})(window.jQuery);
like image 168
Matthew Abbott Avatar answered Nov 05 '22 13:11

Matthew Abbott


Update: Dave's less than symbol was not causing the problem, he only added it in his question for illustrative purposes.

On MVC4 I was able to isolate the issue. This would not compile:

<script type="text/javascript">
  $(document).ready(function () {
    $.ajax({
      url: '@Url.Content("~/Logging/GetMessages")',
      dataType: 'html',
      success: function (result) {
        $('tbody').html(result);
      }
    }); //<-- test
  });
</script>

But this would:

<script type="text/javascript">
  $(document).ready(function () {
    $.ajax({
      url: '@Url.Content("~/Logging/GetMessages")',
      dataType: 'html',
      success: function (result) {
        $('tbody').html(result);
      }
    }); //-- test
  });
</script>

Seems like it was just the < in the comment that was throwing it.

like image 24
Paul Tyng Avatar answered Nov 05 '22 12:11

Paul Tyng


Matthew's answer pretty much explains the behaviour (although, to be honest, I can't reproduce your problem - nor see why it wouldn't work - both your examples run just fine here). For a different approach, you could dedicate an action/view to generated javascript variables (urls, settings, localized texts, whatever), i.e.:

// Could/should be OutputCached depending on the scenario
public ActionResult Globals()
{
   var model = new ClientGlobalsModel();

   // ClientGlobalsModel has a single (could be more) Dictionary<string, string>
   // (Urls) and a ToJSON() method which uses JavaScriptSerializer to serialize 
   // the object:
   model.Urls.Add("GetMessages", Url.Content("~/Logging/GetMessages"));

   // I mostly use this method for e.g. actions:
   model.Urls.Add("UploadImage", Url.Action("Upload", "Image"));

   Response.ContentType = "text/javascript";

   return View(model);
}

Globals.cshtml:

@model ClientGlobalsModel
@{
    Layout = null; // If you have a layout supplied in e.g. _ViewStart
}
var GLOBALS = @Model.ToJSON()

Yeah, this could have been a simple Content() result rather than a view - but when you have more globals (e.g. settings + urls + texts), you may want easier control over the script output and maybe serialize each dictionary individually. May also want to namespace that "GLOBALS" variable in some shared application namespace to avoid polluting the global scope.

(e.g.) Index.cshtml:

<script src="@Url.Action("Globals", "Client")"></script>
<script src="@Url.Content("~/Scripts/main.js")"></script>

... which simply includes the output from /Client/Globals. And "main.js", into which we have now moved the rest of your script:

main.js (static):

$(document).ready(function () {
   $.ajax({
      url: GLOBALS.Urls.GetMessages,
      dataType: 'html',
      success: function (result) {
         $('tbody').html(result);
      }
   });
});

You can, of course, use the same kind of approach to output a few user/context/view-specific settings directly into the view. For a few URL's or data, the data-* attribute approach may be better depending on your tastes. I'm not a fan of stuffing tons of what's basically settings into attributes on every HTML page, though.

like image 27
JimmiTh Avatar answered Nov 05 '22 13:11

JimmiTh


Seems to still be a problem with final MVC4 / Visual Studio 2010, but here is my fix:

@section jQueryDocumentReady
{
  @{
  <text>
     // All the javascript and bracers you want here
  </text>
  }
}
like image 1
Scott R. Frost Avatar answered Nov 05 '22 11:11

Scott R. Frost