Apr 3 2012

Calling MVC Actions from Javascript

Category: MVC | Asp.netVincenzo @ 07:57

Oops…this is not one of my posts…but a friend of mine that is the coordinator of the jsAction team asked me to write a post on my Blog since he has not yes his own Blog. He promised me to enhance jsAction to make easier the use of my updateManager js class….

                                                              Francesco

Suppose we have a very simple action method: it takes no parameters and just returns a string:

public string MyTestMethod()
{
     return "Test String";
}

What if we would like to call this action method from javascript through Ajax and to display the value it returns?
We can do it with jQuery:

function AjaxDisplayString()
{
   $.ajax({
        url: @Url.Action("MyTestMethod"),
        method: 'GET',
        success: function(data) { alert(data); }
   });
}

Not so bad, after all. However, let think, about all implications of the above simple raw approach to the interaction with an action method:

  1. Repeating the above code several times in an application is annoying, and error-prone.
  2. We have no access to Razor helpers from within js files, so we can’t use @Url.Action, and other similar methods to compute the Url we need.
  3. The code depends on the action method being marked  as HttpPost or HttpGet. Each time we modify these attributes we have to go through all our js files that might contain references to the action method and change them.


Are there a better ways to interact with an action methods that doesn’t suffer of the above problems?
….There are some:

  1. We can pack all information that define the details of the call into a standard option object and we may simply write something like:
    $.ajax(options);
  2. There are extensions, like RazorJs, that enable us to write Razor sintax into Js files (http://john.katsiotis.com/blog/razorjs---write-razor-inside-your-javascript-files)
  3. ….Actually problem 3. this is not easy to solve….


However, is there a way to solve all the above problems together?
….This lead me to conceive JsAction.
Well, now that the we made the problem clear, we are ready to go thorugh JsAction and see how it can help us.

JsAction can be installed in 2 ways:

  1. By downloading the library from download page and by referencing it in Visual Studio
  2. By using the NuGet package manager and adding the JsAction package.
    Go to Tools -> Library Package Manager -> Add Library Package Reference and select OnLine. Once located the package press install.
 

You just need to add script references  to the pages that use it. This is easily accomplished by placing the reference into the layout pages (usually your layout.cshtml).
It must be called after the jQuery reference:

@using JsAction

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="../../Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
    <script src="../../Scripts/jquery.unobtrusive-ajax.min.js" type="text/javascript"></script>
    @Html.JsAction()
    <script src="../../Scripts/script.js" type="text/javascript"></script>
</head>

<body>
    @RenderBody()
</body>
</html>

NOTE: script.js is not a part of the libray but just the example javascript file that needs to call the action method.
Clearly we must place it after the jsAction reference.

Simple Usage

Once configuration is done, we can use JsAction.
Let's take the usual Home controller class and let decorate the methods we would like to call from the client with JsAction attribute:

        [JsAction()]
        public string MyTestMethod(int a, int b)
        {
            return (a+b).ToString();
        }

For sake of simplicity let's use just the default parameterless costructor, further options can be found in the documentation
JsAction  creates a JsActions object containing all methods we marked with the JsAction attribute.
Now we are ready to use them from Javascript code with the pattern JsAction.ControllerName.Method:

$(document).ready(function () {
    var ret = JsActions.Home.MyTestMethod(1,2);
});

Where ret is the object returned by the $.ajax call.

According to the jQuery documentation:
The jQuery XMLHttpRequest (jqXHR) object returned by $.ajax() as of jQuery 1.5 is a superset of the browser's native XMLHttpRequest object.
and
The jqXHR objects returned by $.ajax() as of jQuery 1.5 implement the Promise interface

This means, if we are using jQuery 1.5+, we can take advantage of the Promise interface:

$(document).ready(function () {
     JsActions.Home.MyTestMethod(1,2).then(function(data){ alert(data); });
});

What if i'm using jQuery < 1.5?
Well, …we can still set all usual jQuery ajax call options, since all methods created by JsAction takes an optional option parameter, that is merged with the jQuery ajax call options(see the jQuery ajax documentation –> http://api.jquery.com/jQuery.ajax/ ),  Thus, our previous example becomes:

$(document).ready(function () {
    var ret = JsActions.Home.MyTestMethod(1, 2, {
        success: function (data) {
            alert(data);
        }
    });
});

Summing up JsAction provide a quick and easy way to map  server side methods into client side methods in  1:1 fahion.

Visual Studio provides a smart intellisense support not only for .NET languages (C#, Vb.NET), but also for JavaScript (see the new ide features http://weblogs.asp.net/scottgu/archive/2010/04/08/javascript-intellisense-improvements-with-vs-2010.aspx ).

The good news is that jSAction not only imports in javascript the action methods, but also the the help encoded into the action methods comments:
Let's  experiment this by writing a simple class and an action method marked with the JsAction attribute…. and by adding some server side “help comments” on this action method:

    public class Student
    {
        public string firstName { get; set; }
        public string lastName { get; set; }
    }

        /// <summary>
        /// Sums two numbers
        /// </summary>
        /// <param name="st">The student</param>
        /// <param name="a">First number</param>
        /// <param name="b">Second number</param>
        /// <returns>The sum</returns>
        [JsAction()]
        public JsonResult MyTestMethod(Student st, int a, int b)
        {
            st.SumHere = a + b;
            return Json(st);
        }

1.png
Now let run the application and let verify carefully that it actually compile and runs, since the Doc generation feature requires that

  • The application compiles.
  • The application runs.
  • The Xml output file is enabled (Project Properties -> Build -> Check "Xml Documentation File").


Now we're ready to generate automatically the javascript documentation. Go to Tools->Library Package Manager-> Package Manager Console
2.png
Once opened we just have to write
PM> JsAction-GenDoc
The project will be built and run. If any error occurs, it will be displayed in the console. 
Please just wait without interacting with the consolle until the project close automatically. Then lets go to the Solution Explorer and let give a look to the script folder
3.PNG
A new file has been added: JsAction documentation file. It contains unminified and commented javascript code.

/*Generated: 19/02/2012 23:50:35*/
var JsActions = {
    MyTestMethod: function (st, a, b, options) { ///<summary>
        ///            Sums two numbers
        ///            </summary>
///<param name="st" type="Student">The student</param>
///<param name="a" type="Int32">First number</param>
///<param name="b" type="Int32">Second number</param>
///<returns>The sum</returns>
///<param name="options" type="ajaxSettings">[OPTIONAL] AjaxOptions partial object; it will be mergend with the one sent to .ajax jQuery function</param> var opts = { url: "/Home/MyTestMethod", async: true, cache: true, type: "GET", data: $.toDictionary({ st: st, a: a, b: b }) }; jQuery.extend(opts, options); return jQuery.ajax(opts); } };

Now let's create a new javascript file and use the doc file as reference.
Code completion
4.png
Code comments!!
5.png
As you can see from the second image, also the parameter types are resolved. (Student,Int32). This should help you to never use a wrong type.

The Intellisense feature will work also if no Xml Documentation file generation is enabled. However, it will miss YOUR comments, providing only method signature and type resolution.

Please Notice, the vsdoc file must never be used as a @JsScript call replacement. It misses internal functions and it's purpose it's only to documentate function during development.

JsAction has also a basic WebApi support.
Below an ApiController decorated with the JsAction attribute

[JsAction()]
 public class StudentController : ApiController
 {
     private List<Student> data;

     public StudentController()
     {
         this.data = new List<Student>()
         {
             new Student(){id=0, Name="Vincenzo", Surname="Chianese", BirthDay=DateTime.Parse("20/05/1989"), Exams=10},
             new Student(){id=1, Name="Fernando", Surname="Alonso", BirthDay=DateTime.Parse("19/07/1981"),Exams=0},
             new Student(){id=2, Name="Bill", Surname="Gates", BirthDay=DateTime.Parse("28/10/1955"), Exams=2}
         };

     }
     public IEnumerable<Student> GetStudentList()
     {
         return data;
     }

     public Student GetById(int id)
     {
         return data.Where(s => s.id == id).First();
     }

     public Student GetByName(string name)
     {
         return data.Where(s => s.Name == name).First();

     }

     public Student GetBySurname(string surname)
     {
         return data.Where(s => s.Surname == surname).First();
     }

     public HttpResponseMessage PostStudent(Student st)
     {
         //new student logic
         return new HttpResponseMessage(System.Net.HttpStatusCode.OK);
     }

     public HttpResponseMessage DeleteStudent(int id)
     {
         var elem = data.Where(q => q.id == id);
         if (elem.Count() > 0)
         {
             //Remove logic
             return new HttpResponseMessage(System.Net.HttpStatusCode.OK);
         }
         return new HttpResponseMessage(System.Net.HttpStatusCode.NotFound);
     }
 }

The above controller implements just a simple CRUD logic. As you can see the JsAction attribute is applied to the whole class instead of on the Action Methods. In fact in thie case of ApiControllers, substantially, all methods are created to be called from javascript, so it make no sense to decorate only some methods.
 
Anyway, since all methods decorated with NonActionAttribute are not intended to be exposed on the Web no javascript method is created for calling them.

All WebApi methods are placed into the JsActions.WebApi namespace. Below the unit test results of the first controller, and the an example of usage:

test('JsAction WebApi support', function () {
    stop();
    JsActions.WebApi.Student.GetStudentList().then(function (data) { ok(typeof data != 'undefined', 'WebApi data retrieving'); start(); });
    stop();
    JsActions.WebApi.Student.GetById(2).then(function (data) { ok(typeof data != 'undefined', 'WebApi data retrieving 2'); start(); });
    stop();
    JsActions.WebApi.Student.PostStudent({ id: 3, Name: "Francisco", Surname: "Franco", BirthDay: new Date(), Exams: 15 }, { statusCode: { 200: function () { ok(true, 'New element inserted'); start(); } } });
    stop();
    JsActions.WebApi.Student.DeleteStudent(500, { statusCode: { 200: function () { ok(false, 'This is not good'); start(); }, 404: function () { ok(true, 'Element not found'); start(); } } });

 

image

JsAction, when needed, performs also a Complex Type Decomposition:.

image

As you can see from the image image above, if a method  has complex types as parameters, then JsAction creates  javascript proxies for the with full intellisense support.

                      That’s all for now……more to come

                                    Vincenzo

Tags: , ,