ASP.NET MVC MPA (multiple page application) with AngularJS
If you create your modern, brand-new page in a well-known ASP.NET MVC, sooner or later you will need an asynchronous code. The obvious choice is (or was?) a jQuery. The problem is that despite being a powerful library, jQuery requires quite a lot of coding to create even basic stuff. Our team needed a 2-way-binding, easy-to-use, well-documented substitution to jQuery. Finally – we have chosen AngularJS. However, in the .NET world the MVC is an MPA (multi-page application) but AngularJS is more into the SPA (single page application) approach. But first, let’s explain why we had to implement such an exotic solution. We were given a legacy code done in the MVC/spaghetti/jQuery/more-spaghetti/JS approach. The client’s requirement was to preserve the existing functionality, but we were given a free hand regarding refactoring. Rewriting stuff from scratch was quite risky so we decided to approach the problem piece by piece.
At the very beginning, we noticed the MVC limitations (one model per page, synchronous requests, same model for the response and request operations).
The solution, of course, was AJAX.
If AJAX then jQuery, right? Right, but not these days because you will probably end up with huge JS files, in which you will be manually accessing form fields (constantly keeping an eye on the selectors, IDS or CSS classes just for field identification, etc.). I grew up on AngularJS/knockout.js 2-way-binding, so I knew that this would solve a lot of problems. Additionally, we got a great mechanism (AngularJS directives) for creating reusable widgets/components.
Note: in our real application we use more technologies, like SCSS, gulp, etc. but provided examples will be simplified for the purpose of this article.
So… how to combine MVC and AngularJS? The answer is:
- Give up (at least at the beginning) on the AngularJS routing
- Consider each ASP.NET MVC page as a small and super-simple AngularJS SPA
- (optional) Convert an MVC model into JSON so that AngularJS can understand it
I assume you can install AngularJS yourself within your project. We used npm and gulp for that but you can use whatever technique you like.
Here goes the example of a basic project structure:
- AngularJS files go here.
- For the demo purpose, models are located here but for a really big application it is better to create separate projects with Entities, DTOs, etc.
- Here go other JS scripts, which are NOT AngularJS ones.
- A standard MVC Views folder but for a large scale application you can consider using Areas.
Combining AngularJS scripts with razor file
So now let’s see the cshtml Razor file, for Cars:
- Standard ASP.NET MVC Model type declaration
- Here we render the AngularJS files required ONLY for this single view. The ngApp section placeholder is defined in _Layout.cshtml
- Here comes THE MAGIC. This line of code “converts” an MVC backend model into a JSON object understood by an AngularJS frontend (it will be consumed in the browser, not on the server side).
I have presented above the header part of a file and now it’s time for the “real” AngularJS markups (the continuation of the same file):
- Defining what the AngularJS controller is responsible for while handling this part of a frontend application (carsController is explained in detail in the following part of this article).
- “Angularish” forEach loop. Here we iterate items in our model. We know that our model is an array but you can easily imagine your models with a much more sophisticated structure.
- Simple access to the property of an item. Double mustache brackets tell AngularJS to render it in DOM.
- Accessing a nested object within the item.
- Accessing the JS length property on a nested array Wheels (Note: we assume that Wheels is never null in that case).
- Building URLs for MVC actions.
Basic HTML is covered but it won’t work without a controller.
- This is not crucial but I like using the vm (short for ViewModel) naming instead of a $scope.
- Example of a function that can be accessed from HTML.
- Calling initialization. I like such an approach to put all the initialization logic in a function (the function that is not exposed in the vm). Note how MvcModel is accessed. This is the MvcModel that we converted our model to.
- We assign our model to the model so it can be used in an HTML (cshtml) file. In this case, the body of the _init function is extremely simple but it may grow while the project is being developed.
Backend code that handles a razor page – Index (listing) action
Let’s see a little bit of the C# backend code.
As you can see – nothing here is different from the standard MVC approach.
- The action. Standard MVC stuff.
- In your real application, you will get required data from repository, service, or any other DAL.
- Super standard return of a razor view with an attached model.
- Mocked data.
The conclusion is that using AngularJS does NOT change the C# controller approach.
The idea of using RootUrl variable
In the above examples, you may notice the RootUrl. It is used because you can’t always use relative URLs starting with “/”, e.g. in the case your URL is like this http://localhost/APP_NAME/. If you use relative URL, the “APP_NAME” part will be missed and your app will stop working. Here is the solution (in _Layout.cshtml and app.js):
- Server-side rendering of a root URL and saving a global JS variable under a given name.
- Saving RootUrl in $rootScope so it can be used in every AngularJS controller.
So far so good. Next issue to be solved is error handling. Let’s start with ModelState errors. First, we will modify our cshtml model-to-json serialization so that it serializes both model and errors. The above code was based on the listing of all cars whereas the following example covers adding and editing car.
Razor file for “Add and Edit” actions
First, let’s create an AddEdit.cshtml file:
The AddEdit.cshtml file starts like this:
- We are using here another model which is only for adding and editing a certain entity. It will be explained in the following part of this article.
- This is the most important part of the errors (and operation) serialization model. It uses an extension method that serializes model and errors to a convenient representation.
Extending ModelState and ViewModel
First, let’s create a new Extensions folder in our solution:
Now, let’s analyse the extension methods:
- We return our custom class object which combines a model and possible errors. It is convenient to keep it this way.
- We simply copy the Model to the Model property of our custom class.
- Here comes a bit more magic – the actual ModelState error mapping.
- Convert errors to a dictionary.
- Take the errors of the keys with the number of errors different than zero. In other words – do not take empty errors.
There is one more trick to make it work. Since razor is not aware of these extensions methods, go to the Web.config file in the Views folder and add such a line:
- Adding extension namespace
Add and Edit controller actions
Having the above code prepared, let’s see how the CarsController.cs gained few new methods.
- For the Add action, return to the AddEdit view with an empty model (because we don’t need any predefined existing model).
- For the Edit action, return to the AddEdit view but the model should be predefined with the data of an already existing car.
- In a real application, you should get existing model data from DB and map it to a model type that is expected by the cshtml file.
- Add the POST action. It gets the model of an AddEditCarViewModel type and checks if it is valid.
- If not, it is returned (together with model errors) so we can correct the errors.
- In another case, if the model is valid, we perform some add/save operations on it, and we redirect it to the Index action, which is a list of all cars.
Let’s proceed to discuss an AddEditCarViewModel file as I have promised earlier.
- Id is nullable (Guid? notation) because we don’t need it for an adding operation (Id is set/determined on a server side). Because we aren’t able to avoid invalid model errors (due to missing Id), we set it as nullable.
- You can denote some properties as required, so validation will be performed automatically. Validation is a big issue and since it is not the scope of this article, it won’t be discussed in detail.
Getting back to Add and Edit razor file
Now we can see the AddEdit.cshtml:
- Client-side rendering of the action’s URL – either Add or Edit, depending on the model. If the model contains Id, we assume we are editing. If Id is empty, we assume we are adding.
- Validation summary directive. It is a custom directive and will be discussed later.
- Keep a current model Id in a hidden field.
- name properties of inputs have to match properties of AddEditCarViewModel.
- Custom submit button directive, which will be discussed later.
To make it work, we also need an AngularJS controller.
- Determine the mode for the angular controller. It is either Add or Edit so the Boolean value is sufficient.
- Keep object (car) model and errors in separate properties of vm. Please note how vm.validationErrors are passed to a validation-summary directive.
One of the final things to be presented here is directives. They are reusable items and you can consider using them in the case you have repeated code in html. In our case, there are validation-summary and submit-buttons.
Since they are used in many places, let’s add it to the _Layout.cshtml.
Submit button – HTML:
Submit button – js:
HTML Validation summary:
JS Validation summary:
And finally, validation summary in action:
In this article, we have covered the basics of combining ASP.NET MVC with AngularJS. Sure thing, the discussed examples are simplified but the point of this article is to show just the code needed – no more, no less.