Share via


ASP.Net MVC Life Cycle and State Management

Introduction

In a MVC application, no physical page exists for a specific request. All the requests are routed to a special class called the Controller. The controller is responsible for generating the response and sending the content back to the browser. Also, there is a many-to-one mapping between URL and controller.

When you request a MVC application, you are directly calling the action method of a controller. When you request http://mysite/Controller1/method1, you are actually calling Controller1's method1. We will see how our request is routing to an ActionMethod of a controller.

Steps Overview

The procedure involved is:

  1. An instance of the RouteTable class is created on application start. This happens only once when the application is requested for the first time.
  2. The UrlRoutingModule intercepts each request, finds a matching RouteData from a RouteTable and instantiates a MVCHandler (an HttpHandler).
  3. The MVCHandler creates a DefaultControllerFactory (you can create your own controller factory also). It processes the RequestContext and gets a specific controller (from the controllers you have written). Creates a ControllerContext. es the controller a ControllerContext and executes the controller.
  4. Gets the ActionMethod from the RouteData based on the URL. The Controller Class then builds a list of parameters (to to the ActionMethod) from the request.
  5. The ActionMethod returns an instance of a class inherited from the ActionResult class and the View Engine renders a view as a web page.

Details

Now, let's understand in detail

  1. Every ASP.NET MVC application has a RouteTable class. This RouteTable is responsible for mapping the MVC requests to a specific controller's ActionMethod.

    Whenever the application starts, the Application_Start event will be fired and it will call the RegisterRoutes() with the collection of all the available routes of the MVC application as a parameter that will add the routes to the Routes property of the System.Web.Routing.RouteTable class. The Routes property is of type RouteCollection.

Global.asax

public class  MvcApplication : System.Web.HttpApplication  
{  
 protected void  Application_Start()  
 {  
 WebSecurity.InitializeDatabaseConnection("DefaultConnection", "UserProfile",  "UserId", "UserName", autoCreateTables:true);  
 AreaRegistration.RegisterAllAreas();  
 WebApiConfig.Register(GlobalConfiguration.Configuration);  
 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);  
 RouteConfig.RegisterRoutes(RouteTable.Routes);  
 BundleConfig.RegisterBundles(BundleTable.Bundles);  
 }  
}

RouteConfig.cs:

public class  RouteConfig  
{  
 public static  void RegisterRoutes(RouteCollection routes)   
 {  
 routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); // Ignore route ending with axd  
 routes.MapRoute(  
 name: "Default",  // route name  
 url: "{controller}/{action}/{id}", // Url pattern  
 defaults: new  { controller = "Home", action = "Index", id = UrlParameter.Optional } // create a default route  
 );  
 }  
}

The MapRoute() actually creates a default route. Adds all routes to the RouteTable and associates the RouteHandlers with the routes.

Now, all the mapped routes are stored as a RouteCollection in the Routes property of the RouteTable class.

NOTE: The RouteTable class has a Routes property that holds a collection of objects that derive from the RouteBase class. The RouteCollection class is derived from Collection<RouteBase>. Hence RegisterRoutes() is taking an object of RouteCollection.

When an ASP.NET MVC application handles a request, the application iterates through the collection of routes in the Routes property to find the route that matches the format of the URL requested. The application uses the first route that it finds in the collection that matches the URL. So the most specific route should be added first then the general ones.

 Whenever you request an ASP.NET MVC application, the request is intercepted by the UrlRoutingModule (an HTTP Module) and:

  1. The UrlRoutingModule wraps up the current HttpContext (including the URL, form parameters, query string parameters and cookies associated with the current request) in an HttpContextWrapper object as below.

    NOTE: The HttpContext class has no base class and isn't virtual and hence is unusable for testing (it cannot be mocked). The HttpContextBase class is a (from C# 3.5) replacement to HttpContext. Since it is abstract, it is mockable. It is concretely implemented by HttpContextWrapper. To create an instance of HttpContextBase in a normal web page, we use:

New **HttpContextWrapper(HttpContext.Current). **

private void  OnApplicationPostResolveRequestCache(object sender, EventArgs e)  
 {  
 HttpApplication application = (HttpApplication) sender;  
 HttpContextBase context = new  HttpContextWrapper(application.Context);  
 this.PostResolveRequestCache(context);  
 }
  1. Now, the module sends this HTTPContextBase object to the PostResolveRequestCache() method. 
  2. Based on the HttpBaseContext object, the postResolveRequestCache() will return the correct RouteData from the RouteTable (that was created in the previous Step #1).
  3. If the UrlRoutingModule successfully retrieves a RouteData object then it creates a RequestContext object that represents the current HttpContext and RouteData.
  4. The UrlRoutingModule then instantiates a new HttpHandler based on the RouteTable and es the RequestContext (created in Step c) to the new handler's constructor.
  5. For an ASP.NET MVC application, the handler returned from the RouteTable will always be an MvcHandler. This MVCHandler implements an IHTTPHandler interface and hence the ProcessRequest() method.
  6. Finally, it will call the RemapHandler() method that will set the MVCHandler just obtained to be the Current HTTP Handler.
public virtual  void PostResolveRequestCache(HttpContextBase context)  
{  
 RouteData routeData = this.RouteCollection.GetRouteData(context);  
 if (routeData != null)  
 {  
 IRouteHandler routeHandler = routeData.RouteHandler;  
 if (routeHandler == null)  
 {  
 throw new  InvalidOperationException(string.Format(CultureInfo.CurrentCulture,  
 SR.GetString("UrlRoutingModule_NoRouteHandler"), new  object[0]));  
 }  
 if (!(routeHandler == StopRoutingHandler))  
 {  
 RequestContext requestContext = new  RequestContext(context, routeData);  
 context.Request.RequestContext = requestContext;  
 IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);  
  
 if (httpHandler == null)  
 {  
 throw new  InvalidOperationException(  
 string.Format(CultureInfo.CurrentUICulture,  
 SR.GetString("UrlRoutingModule_NoHttpHandler"),  
 new object[] { routeHandler.GetType() }));  
 }  
  
 if (httpHandler == UrlAuthFailureHandler)  
 {  
 if (!FormsAuthenticationModule.FormsAuthRequired)   
 {  
 throw new  HttpException(0x191,  
 SR.GetString("Assess_Denied_Description3"));  
 }  
 UrlAuthorizationModule.ReportUrlAuthorizationFailure(HttpContext.Current,this);  
 }  
 else 
 {  
 context.RemapHandler(httpHandler);  
 }  
 }  
 }  
}

3. MVCHandler is also inherited from the IHTTPAsyncHandler hence implements the ProcessRequest() method. When MVCHandler executes, it calls the ProcessRequest() method that in turn calls the ProcessRequestInit() method.

The ProcessRequestInit() method creates a ControllerFactory and a Controller. The Controller is created from a ControllerFactory. There is a ControllerBuilder class that will set the ControllerFactory.

NOTE: By default it will be DefaultControllerFactory. But you can create your own ControllerFactory as well.

By implementing the IControllerFactory interface and then adding the following code to the Application_Start event in the gloabal.asax.

ControllerBuilder.Current.SetControllerFactory(typeof(NewFactory))

Now, the NewFactory will be used instead of the DefaultControllerFactory .

The RequestContext and the name of the Contoller (from the URL) will be ed to CreateController() method to get the specific Contoller (that you have written).

Next, a ControllerContext object is constructed from the RequestContext and the controller using the method GetContollerInstance().

private void  ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory)  
{  
 bool? isRequestValidationEnabled = ValidationUtility.IsValidationEnabled(HttpContext.Current);  
 if (isRequestValidationEnabled == true)  
 {  
 ValidationUtility.EnableDynamicValidation(HttpContext.Current);  
 }  
 AddVersionHeader(httpContext);  
 RemoveOptionalRoutingParameters();  
 // Get the controller type  
 string controllerName = RequestContext.RouteData.GetRequiredString("controller");  
 // Instantiate the controller and call Execute  
 factory = ControllerBuilder.GetControllerFactory();  
 controller = factory.CreateController(RequestContext, controllerName);  
 if (controller == null)  
 {  
 throw new  InvalidOperationException(  
 String.Format(CultureInfo.CurrentCulture, MvcResources.ControllerBuilder_FactoryReturnedNull,factory.GetType(),controllerName));  
 }  
}  
public virtual  IController CreateController(RequestContext requestContext, string controllerName)  
{  
 if (requestContext == null)  
 {  
 throw new  ArgumentNullException("requestContext");  
 }  
 if (string.IsNullOrEmpty(controllerName))  
 {  
 throw new  ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");  
 }  
 Type controllerType = this.GetControllerType(requestContext, controllerName);  
 return this.GetControllerinstance(requestContext, controllerType);  
}

4. Our Controller inherits from the Controller class that inherits from ControllerBase that implements the Icontroller interface. The Icontroller interface has an Execute() abstract method that is implemented in the ControllerBase class.

public abstract  class ControllerBase : Icontroller  
{  
 protected virtual  void Execute(RequestContext requestContext)   
 {  
 if (requestContext == null)  
 {  
 throw new  ArgumentNullException("requestContext");  
 }  
 if (requestContext.HttpContext == null)  
 {  
 throw new  ArgumentException(  MvcResources.ControllerBase_CannotExecuteWithNullHttpContext,  
 "requestContext");  
 }  
 VerifyExecuteCalledOnce();  
 Initialize(requestContext);  
 using (ScopeStorage.CreateTransientScope())  
 {  
 ExecuteCore();  
 }  
 }  
 protected abstract  void ExecuteCore();  
 //Other stuffs here  
}
  1. The ViewBag, ViewData, TempData and so on properties of the ControllerBase class is initialized. These properties are used for ing data from the View to the Controller or vice-versa or among action methods.
  2. The Execute() method of the ControllerBase class is executed that calls the ExecuteCore() abstract method. ExecuteCore() is implemented in the Controller class.
protected override  void ExecuteCore()  
{  
 PossiblyLoadTempData();  
 try 
 {  
 string actionName = RouteData.GetRequiredString("action");  
 if (!ActionInvoker.InvokeAction(ControllerContext, actionName))  
 {  
 HandleUnknownAction(actionName);  
 }  
 }  
 finally 
 {  
 PossiblySaveTempData();  
 }  
}
  1. The ExecuteCore() method gets the Action name from the RouteData based on the URL.
  2. The ExecuteCore() method then calls the InvokeAction() method of the ActionInvoker class. This builds a list of parameters from the request. This list of parameters are ed as method parameters to the ActionMethod that is executed. Here the Descriptor objects viz.ControllerDescriptor and ActionDescriptor, that provide information on the controller (like name, type, actions) and Action (name, parameter and controller) respectively play a major role. Now you have your Controller name and Action name.

This controller class is something that you wrote. So one of the methods that you wrote for your controller class is invoked.

NOTE: controller methods that are decorated with the [NonAction] attribute will never be executed.

Finally It will call the InvokeAction method to execute the Action.

public virtual  bool InvokeAction(ControllerContext controllerContext, string  actionName)  
{  
 if (controllerContext == null)  
 {  
 throw new  ArgumentNullException("controllerContext");  
 }  
 if (string.IsNullOrEmpty(actionName))  
 {  
 throw new  ArgumentException(MvcResources.Common_NullOrEmpty, "actionName");  
 }  
 ControllerDescriptor controllerDescriptor = this. GetControllerDescriptor(controllerContext);  
 ActionDescriptor actionDescriptor = this.FindAction(controllerContext, controllerDescriptor, actionName);  
 if (actionDescriptor == null)  
 {  
 return false;  
 }  
 FilterInfo filters = this.GetFilters(controllerContext, actionDescriptor);  
  
 try 
 {  
 AuthorizationContext context = this.InvokeAuthorizationFilters(controllerContext, filters.AuthorizationFilters, actionDescriptor);  
 if (context.Result != null)  
 {  
 this.InvokeActionResult(controllerContext, context.Result);  
 }  
 else 
 {  
 if (controllerContext.Controller.ValidateRequest)   
 {  
 ValidateRequest(controllerContext);  
 }  
 IDictionary<string, object> parameterValues = this.GetParameterValues(controllerContext, actionDescriptor);  
 ActionExecutedContext context2 = this.InvokeActionMethodWithFilters(controllerContext, filters.ActionFilters, actionDescriptor, parameterValues);  
 this.InvokeActionResultWithFilters(controllerContext, filters.ResultFilters, context2.Result);  
 }  
 }  
 catch (ThreadAbortException)  
 {  
 throw;  
 }  
 catch (Exception exception)  
 {  
 ExceptionContext context3 = this.InvokeExceptionFilters(controllerContext, filters.ExceptionFilters, exception);  
 if (!context3.ExceptionHandled)  
 {  
 throw;  
 }  
 this.InvokeActionResult(controllerContext, context3.Result);  
 }  
 return true;  
}
  1. The Controller returns an instance of ActionResult. The Controller typically executes one of the helper methods (mostly View() that returns an instance of the ViewResult class, that is derived from the ActionResult class). Here's the list of classes that extend from the ActionResult class. You just need to call a specific Helper method to return the respective ActionResult.
Action Result Helper Method Description
ViewResult View Renders a view as a Web page.
PartialViewResult PartialView Renders a partial view, that defines a section of a view that can be rendered inside another view.
RedirectResult Redirect Redirects to another action method by using its URL.
RedirectToRouteResult RedirectToAction

RedirectToRoute

Redirects to another action method.
ContentResult Content Returns a user-defined content type.
JsonResult Json Returns a serialized JSON object.
JavaScriptResult JavaScript Returns a script that can be executed on the client.
FileResult File Returns binary output to write to the response.
EmptyResult (None) Represents a return value that is used if the action method must return a null result (void).

[Courtesy: Controllers and Action Methods in ASP.NET MVC Applications

public abstract  class ActionResult  
{  
 public abstract  void ExecuteResult(ControllerContext context);   
}

ExecuteResult() is implemented differently in various sub-classes of ActionResult. ViewResult is the most commonly used ActionResult. So let's discuss this.

The following happens after the ExecuteResult() method of ViewResult is called.

  1. ViewResultBase calls the FindView() method of the ViewResult class.
  2. The FindView() method of the ViewResult class returns an instance of the ViewEngineResult class.
  3. The Render() method of the ViewEngineResult class is called to Render the view using the ViewEngine.
  4. The Render() method internally calls the RenderViewPage() method that sets the master page location and ViewData.
  5. The response is rendered on client browser.
public virtual  ViewEngineResult FindView( ControllerContext controllerContext, string viewName, string masterName)  
{  
 if (controllerContext == null)  
 {  
 throw new  ArgumentNullException("ControllerContext");  
 }  
 if (string.IsNullOrEmpty(viewName))  
 {  
 throw new  ArgumentException(MvcResources.Common_NullOrEmpty, "viewName");  
 }  
 Func<IViewEngine, ViewEngineResult> cacheLocator = e => e.FindView(controllerContext,  viewName, masterName, true);  
 Func<IViewEngine, ViewEngineResult> locator = e => e.FindView(controllerContext, viewName,  masterName, false);  
 return Find(cacheLocator, locator);  
}  
  
public virtual  void Render(ViewContext viewContext, TextWriter writer)  
{  
 if (viewContext == null)  
 {  
 throw new  ArgumentNullException("viewContext");  
 }  
  
 object obj2 = this.BuildManager.CreateInstanceFromVirtualPath(this.Viewpath,typeof(object));  
 if (obj2 == null)  
 {  
 throw new  InvalidOperationException(string.Format(CultureInfo.CurrentUICulture,  MvcResources.WebFormViewEngine_ViewCouldNotBeCreated,  
 new object[] { this.ViewPath }));  
 }  
 ViewPage page = (ViewPage) obj2;  
 if (page != null)  
 {  
 this.RenderViewPage(viewContext, page);  
 }  
 else 
 {  
 ViewUserControl control = (ViewUserControl) obj2;  
 if (control == null)  
 {  
 throw new  InvalidOperationException(string.Format(CultureInfo.CurrentUICulture,  MvcResources.WebFormViewEngine_WrongViewBase,  new  object[] { this. ViewPath }));  
 }  
 this.RenderViewUserControl(viewContext, control);  
 }  
}  
  
private void  RenderViewPage(ViewContext context, ViewPage page)  
{  
 if (!string.IsNullOrEmpty(this.MasterPath))  
 {  
 page.MasterLocation = this.MasterPath;  
 }  
 page.ViewData = context.ViewData;  
 page.RenderView(context);  
}

NOTE: The ViewPage class is derived from System.Web.UI.Page class. This is the same base class from which classic ASP.NET pages are derived.

The RenderView() method finally calls the ProcessRequest() method of the Page class that renders the view in the client browser. 

Overview of the Lifecycle Steps

There are five main steps that happen when you make a request from an ASP.NET MVC website:

  1. The RouteTable is Created: This first step happens only once when an ASP.NET application first starts. The RouteTable maps URLs to handlers.
  2. The UrlRoutingModule Intercepts the Request: This second step happens whenever you make a request. The UrlRoutingModule intercepts every request and creates and executes the right handler.
  3. The MvcHandler Executes: The MvcHandler creates a controller, passes the controller a ControllerContext, and executes the controller.
  4. The Controller Executes: The controller determines which controller method to execute, builds a list of parameters, and executes the method.
  5. The RenderView Method is Called

Typically, a controller method calls RenderView() to render content back to the browser. The Controller.RenderView() method delegates its work to a particular ViewEngine.

Let's examine each of these steps in detail.

Step 1 : The RouteTable is Created

When you request a page from a normal ASP.NET application, there is a page on disk that corresponds to each page request. For example, if you request a page named SomePage.aspx then there better be a page named SomePage.aspx sitting on your web server. If not, you receive an error.

Technically, an ASP.NET page represents a class. And, not just any class. An ASP.NET page is a handler. In other words, an ASP.NET page implements the IHttpHandler interface and has a ProcessRequest() method that gets called when you request the page. The ProcessRequest() method is responsible for generating the content that gets sent back to the browser.

So, the way that a normal ASP.NET application works is simple and intuitive. You request a page, the page request corresponds to a page on disk, the page executes its ProcessRequest() method and content gets sent back to the browser.

An ASP.NET MVC application does not work like this. When you request a page from an ASP.NET MVC application, there is no page on disk that corresponds to the request. Instead, the request is routed to a special class called a controller. The controller is responsible for generating the content that gets sent back to the browser.

When you write a normal ASP.NET application, you build a bunch of pages. There is always a one-to-one mapping between URLs and pages. Corresponding to each page request, there better be a page.

When you build an ASP.NET MVC application, in contrast, you build a bunch of controllers. The advantage of using controllers is that you can have a many-to-one mapping between URLs and pages. For example, all of the following URLs can be mapped to the same controller:

  • http://MySite/Products/1
  • http://MySite/Products/2
  • http://MySite/Products/3

The single controller mapped to these URLs can display product information for the right product by extracting the product Id from the URL. The controller approach is more flexible than the classic ASP.NET approach. The controller approach also results in more readable and intuitive URLs.

So, how does a particular page request get routed to a particular controller? An ASP.NET MVC application has something called a Route Table. The Route Table maps particular URLs to particular controllers.

An application has one and only one Route Table. This Route Table is setup in the Global.asax file. Listing 1 contains the default Global.asax file that you get when you create a new ASP.NET MVC Web Application project by using Visual Studio.

Listing 1 - Global.asax

using System;
using System.Collections.Generic;
using System.Linq;
 using System.Web;
 using System.Web.Mvc;
 using System.Web.Routing;
 
 namespace TestMVCArch
 {
 public class  GlobalApplication : System.Web.HttpApplication
 {
 public static  void RegisterRoutes(RouteCollection routes)
 {
 // Note: Change the URL to "{controller}.mvc/{action}/{id}" to enable
 //  automatic support on IIS6 and IIS7 classic mode
 routes.Add(new Route("{controller}/{action}/{id}", new  MvcRouteHandler())
 {
 Defaults = new  RouteValueDictionary(new { action = "Index", id = "" }),
 });
 routes.Add(new Route("Default.aspx", new  MvcRouteHandler())
 {
 Defaults = new  RouteValueDictionary(new { controller = "Home", action = "Index", id =  "" }),
 });
 }
 protected void  Application_Start(object sender, EventArgs e)
 {
 RegisterRoutes(RouteTable.Routes);
 }
 }
 }

An application's Route Table is represented by the static RouteTable.Routes property. This property represents a collection of Route objects. In the Global.asax file in Listing 1, two Route objects are added to the Route Table when the application first starts (The Application_Start() method is called only once when the very first page is requested from a website).

A Route object is responsible for mapping URLs to handlers. In Listing 1, two Route objects are created. Both Route objects map URLs to the MvcRouteHandler. The first Route maps any URL that follows the pattern {controller}/{action}/{id} to the MvcRouteHandler. The second Route maps the particular URL Default.aspx to the MvcRouteHandler.

By the way, this new routing infrastructure can be used independently of an ASP.NET MVC application. The Global.asax file maps URLs to the MvcRouteHandler. However, you have the option of routing URLs to a different type of handler. The routing infrastructure described in this section is contained in a distinct assembly named System.Web.Routing.dll. You can use the routing without using the MVC. 

Step 2 : The UrlRoutingModule Intercepts the Request

Whenever you make a request against an ASP.NET MVC application, the request is intercepted by the UrlRoutingModule HTTP Module. An HTTP Module is a special type of class that participates in each and every page request. For example, classic ASP.NET includes a FormsAuthenticationModule HTTP Module that is used to implement page access security using Forms Authentication.

When the UrlRoutingModule intercepts a request, the first thing the module does is to wrap up the current HttpContext in an HttpContextWrapper2 object. The HttpContextWrapper2 class, unlike the normal HttpContext class, derives from the HttpContextBase class. Creating a wrapper for HttpContext makes it easier to mock the class when you are using a Mock Object Framework such as Typemock Isolator or Rhino Mocks.

Next, the module passes the wrapped HttpContext to the RouteTable that was setup in the previous step. The HttpContext includes the URL, form parameters, query string parameters, and cookies associated with the current request. If a match can be made between the current request and one of the Route objects in the Route Table, then a RouteData object is returned.

If the UrlRoutingModule successfully retrieves a RouteData object then the module next creates a RouteContext object that represents the current HttpContext and RouteData. The module then instantiates a new HttpHandler based on the RouteTable and passes the RouteContext to the new handler's constructor.

In the case of an ASP.NET MVC application, the handler returned from the RouteTable will always be an MvcHandler (The MvcRouteHandler returns an MvcHandler). Whenever the UrlRoutingModule can match the current request against a Route in the Route Table, an MvcHandler is instantiated with the current RouteContext.

The last step that the module performs is setting the MvcHandler as the current HTTP Handler. An ASP.NET application calls the ProcessRequest() method automatically on the current HTTP Handler which leads us to the next step.

Step 3 : The MvcHandler Executes

In the previous step, an MvcHandler that represents a particular RouteContext was set as the current HTTP Handler. An ASP.NET application always fires off a certain series of events including Start, BeginRequest, PostResolveRequestCache, PostMapRequestHandler, PreRequestHandlerExecute, and EndRequest events (there are a lot of application events - for a complete list, lookup the HttpApplication class in the Microsoft Visual Studio 2008 Documentation).

Everything described in the previous section happens during the PostResolveRequestCache and PostMapRequestHandler events. The ProcessRequest() method is called on the current HTTP Handler right after the PreRequestHandlerExecute event.

When ProcessRequest() is called on the MvcHandler object created in the previous section, a new controller is created. The controller is created from a ControllerFactory. This is an extensibility point since you can create your own ControllerFactory. The default ControllerFactory is named, appropriately enough, DefaultControllerFactory.

The RequestContext and the name of the controller are passed to the ControllerFactory.CreateController() method to get a particular controller. Next, a ControllerContext object is constructed from the RequestContext and the controller. Finally, the Execute() method is called on the controller class. The ControllerContext is passed to the Execute() method when the Execute() method is called.

Step 4 : The Controller Executes

The Execute() method starts by creating the TempData object (called the Flash object in the Ruby on Rails world). The TempData can be used to store temporary data that must be used with the very next request (TempData is like Session State with no long-term memory).

Next, the Execute() method builds a list of parameters from the request. These parameters, extracted from the request parameters, will act as method parameters. The parameters will be passed to whatever controller method gets executed.

The Execute() method finds a method of the controller to execute by using reflection on the controller class (.NET reflection and not navel gazing reflection). The controller class is something that you wrote. So the Execute() method finds one of the methods that you wrote for your controller class and executes it. The Execute() method will not execute any controller methods that are decorated with the NonAction attribute.

At this point in the lifecycle, we've entered your application code.

Step 5 : The RenderView Method is Called

Normally, your controller methods end with a call to either the RenderView() or RedirectToAction() method. The RenderView() method is responsible for rendering a view (a page) to the browser.

When you call a controller's RenderView() method, the call is delegated to the current ViewEngine's RenderView() method. The ViewEngine is another extensibility point. The default ViewEngine is the WebFormViewEngine. However, you can use another ViewEngine such as the NHaml ViewEngine.

The WebFormViewEngine.RenderView() method uses a class named the ViewLocator class to find the view. Next, it uses a BuildManager to create an instance of a ViewPage class from its path. Next, if the page has a master page, the location of the master page is set (again, using the ViewLocator class). If the page has ViewData, the ViewData is set. Finally, the RenderView() method is called on the ViewPage.

The ViewPage class derives from the base System.Web.UI.Page class. This is the same class that is used for pages in classic ASP.NET. The final action that RenderView() method performs is to call ProcessRequest() on the page class. Calling ProcessRequest() generates content from the view in the same way that content is generated from a normal ASP.NET page.

Extensibility Points

The ASP.NET MVC lifecycle was designed to include a number of extensibility points. These are points where you can customize the behavior of the framework by plugging in a custom class or overriding an existing class. Here's a summary of these extensibility points:

  1. Route objects - When you build the Route Table, you call the RouteCollection.Add() method to add new Route objects. The Add() method accepts a RouteBase object. You can implement your own Route objects that inherit from the base RouteBase class.
  2.  MvcRouteHandler - When building an MVC application, you map URLs to MvcRouteHandler objects. However, you can map a URL to any class that implements the IRouteHandler interface. The constructor for the Route class accepts any object that implements the IRouteHandler interface.
  3.  MvcRouteHandler.GetHttpHandler() - The GetHttpHandler() method of the MvcRouteHandler class is a virtual method. By default, an MvcRouteHandler returns an MvcHandler. If you prefer, you can return a different handler by overriding the GetHttpHandler() method.
  4. ControllerFactory - You can assign a custom class by calling the System.Web.MVC.ControllerBuilder.Current.SetControllerFactory() method to create a custom controller factory. The controller factory is responsible for returning controllers for a given controller name and RequestContext.
  5. Controller - You can implement a custom controller by implementing the IController interface. This interface has a single method: Execute(ControllerContext controllerContext).
  6. ViewEngine - You can assign a custom ViewEngine to a controller. You assign a ViewEngine to a controller by assigning a ViewEngine to the public Controller.ViewEngine property. A ViewEngine must implement the IViewEngine interface which has a single method: RenderView(ViewContext viewContext).
  7. ViewLocator - The ViewLocator maps view names to the actual view files. You can assign a custom ViewLocator to the default WebFormViewEngine.ViewLocator property.

If you can think of any other extensibility points that I overlooked, please add a comment to this blog post and I will update this entry.

I want to understand everything that happens when you type a URL in a browser and hit the enter key when requesting a page from an ASP.NET MVC website.

There are two reasons. First, one of the promises of ASP.NET MVC is that it will be a very extensible framework. For example, you'll be able to plug in different view engines to control how your website content is rendered. You also will be able to manipulate how controllers get generated and assigned to particular requests. I want to walk through the steps involved in an ASP.NET MVC page request because I want to discover any and all of these extensibility points.

Second, I'm interested in Test-Driven Development. In order to write unit tests for controllers, I need to understand all of the controller dependencies.

**State Management **

ASP.NET MVC also provides state management techniques that can help us to maintain the data when redirecting from one page to other page or in the same page after reloading. There are several ways to do this in ASP.NET MVC .

HTTP is a stateless protocol, i.e., each HTTP request does not know about the previous request. If you are redirecting from one page to another page, then you have to maintain or persist your data so that you can access it further. To do this, there were many techniques available in ASP.NET like ViewState, SessionState, ApplicationState etc.

  1. Hidden Field
  2. Cookies
  3. Query String
  4. ViewData
  5. ViewBag
  6. TempData

The above objects help us to persist our data on the same page or when moving from "Controller" to "View" or "Controller" to Controller". Let us start practical implementation of these so that we can understand in a better way.

Hidden Field

It is not new, we all know it from HTML programming. Hidden field is used to hide your data on client side. It is not directly visible to the user on UI but we can see the value in the page source. So, this is not recommended if you want to store a sensitive data. It's only used to store a small amount of data which is frequently changed.

The following code is storing the Id of employee and its value is 1001.

@Html.HiddenFor(x=>x.Id)

If you open the page source code for that page in the browser, you will find the following line of code, which is nothing but the HTML version of the above code with value. Just focus on last three attributes.

<input data-val="true" data-val-number="The field Id must be a number." data-val-required="The Id field is required." id="Id" name="Id" type="hidden" value="1001" />

Cookies

Cookies are used for storing the data but that data should be small. It is like a small text file where we can store our data. The good thing is that a cookie is created on client side memory in the browser. Most of the times, we use a cookie for storing the user information after login with some expiry time. Basically, a cookie is created by the server and sent to the browser in response. The browser saves it in client-side memory.

We can also use cookies in ASP.NET MVC for maintaining the data on request and respond. It can be like the below code.

HttpCookie cookie = new  HttpCookie("TestCookie");  
cookie.Value = "This is test cookie";  
this.ControllerContext.HttpContext.Response.Cookies.Add(cookie);

Here, we have created one cookie and named it as "TestCookie". We can create multiple cookies and add with Response. For getting the value from an existing cookie, we first need to check that the cookie is available or not; then, we can access the value of the cookie.

if (this.ControllerContext.HttpContext.Request.Cookies.AllKeys.Contains("TestCookie"))  
{  
 HttpCookie cookie = this.ControllerContext.HttpContext.Request.Cookies["TestCookie"];  
  
 ViewBag.CookieMessage = cookie.Value;  
}

Cookies are all depended on expiry, you can create a cookie on one action method in a controller and it will save on the client side and can be accessed in another action method easily.

Query String

In ASP.NET, we generally use a query string to pass the value from one page to the next page. Same we can do in ASP.NET MVC as well.

http://localhost:49985/home/editemployee?name=TestValue

I am making one request with the above URL. You can see that in this, I am passing name's value as a query string and that will be accessible on "EditEmployee" action method in "Home" controller as the following image shows.

ViewData

It helps us to maintain your data when sending the data from Controller to View. It is a dictionary object and derived from ViewDataDictionary. As it is a dictionary object, it takes the data in a key-value pair.

Once you create ViewData object, pass the value, and make redirection; the value will become null. The data of ViewData is not valid for next subsequent request. Take care of two things when using ViewData, first, check for null and second, check for typecasting for complex data types.

public ActionResult Index()  
 {  
 Employee emp = new  Employee()  
 {  
 Id = 1001,  
 Name = "Satyaprakash Samantaray",  
 Address = "Bangalore",   
 Age = 25  
 };  
  
 ViewData["Message"] = "This is ViewData";  
 ViewData["Emp"] = emp;  
  
 return View();  
 }

The above code contains two ViewData dictionary objects - ViewData["Message"] and ViewData["Emp"]. The first one is a simple string value but the next one contains complex employee data. When the View is going to render, we need to first check the ViewData for null and if it is not, then we can get the value.

@{  
 ViewBag.Title = "Home Page";  
}  
  
<div class="row">  
 <div class="col-md-4">  
 <h2>Employee Details</h2>  
 <br />  
 <p>  
 @if(ViewData["Message"] !=null)  
 {  
 <b>@ViewData["Message"].ToString();</b>  
 }  
 </p>  
 <br />  
 @if (ViewData["Emp"] != null)  
 {  
  
 var emp = (MVCStateManagement.Models.Employee)ViewData["Emp"];  
 <table>  
 <tr>  
 <td>  
 Name :  
 </td>  
 <td>  
 @emp.Name  
 </td>  
 </tr>  
 <tr>  
 <td>  
 Address :  
 </td>  
 <td>  
 @emp.Address  
 </td>  
 </tr>  
 <tr>  
 <td>  
 Age :  
 </td>  
 <td>  
 @emp.Age  
 </td>  
 </tr>  
 </table>  
 }  
 </div>  
</div>  

ViewBag

The ViewBag's task is same as that of ViewData. It is also used to transfer the data from Controller to View. However, the only difference is that ViewBag is an object of Dynamic property introduced in C# 4.a. It is a wrapper around ViewData. If you use ViewBag rather than ViewData, you will not have to do typecasting with the complex objects and do not need to check for null.

If we consider the same above code with ViewBag, the output will be same.

public ActionResult Index()  
 {  
 Employee emp = new  Employee()  
 {  
 Id = 1001,  
 Name = "Satya",   
 Address = "Bangalore",   
 Age = 25  
 };  
  
 ViewBag.Message = "This is ViewBag";  
 ViewBag.Emp = emp;  
  
 return View();  
 }

On View, you have to change ViewData with ViewBag.

@{  
 ViewBag.Title = "Home Page";  
}  
  
<div class="row">  
 <div class="col-md-4">  
 <h2>Employee Details</h2>  
 <br />  
 <p>  
  
 <b>@ViewBag.Message</b>  
  
 </p>  
 <br />  
 @{  
 var emp = ViewBag.Emp;  
 <table>  
 <tr>  
 <td>  
 Name :  
 </td>  
 <td>  
 @emp.Name  
 </td>  
 </tr>  
 <tr>  
 <td>  
 Address :  
 </td>  
 <td>  
 @emp.Address  
 </td>  
 </tr>  
 <tr>  
 <td>  
 Age :  
 </td>  
 <td>  
 @emp.Age  
 </td>  
 </tr>  
 </table>  
 }  
 </div>  
</div>  

Note

  1. If you are using ViewData that is not defined on Controller, then it will throw an error; but with ViewBag, it will not.
  2. Do not use ViewBag and ViewData with the same name, otherwise, only one message will display. See the following code in the controller is using both ViewData and ViewBag with same name "Message".
public ActionResult Index()  
 {  
 ViewData["Message"] = "This is ViewData";  
 ViewBag.Message = "This is ViewBag";  
  
 return View();  
 }   
On view defined both as  following.   
 <b>@ViewBag.Message</b>  
 @if(ViewData["Message"]!=null)  
 {  
 ViewData["Message"].ToString();  
 }

The output will show only one message and that will be the last one [in this case, message will be "This is ViewBag"].

TempData

TempData is also a dictionary object as ViewData and stores value in key/value pair. It is derived from TempDataDictionary. It is mainly used to transfer the data from one request to another request or we can say subsequent request. If the data for TempData has been read, then it will get cleaned. To persist the data, there are different ways. It all depends on how you read the data.

No Read Data

If you haven't read the data in redirection process, then your data is available with the next subsequent request. You can see that in the following code, we have set up a TempData["Emp" but neither read it in any action method nor in view.

So, once the "About" page renders and if we move to "Contact" page, the TempData["Emp"] will be available.

Note

Do not read data on View.

public ActionResult Index()  
 {  
 Employee emp = new  Employee() { Id = 1001, Name = "Satya", Address = "Bangalore", Age = 25 };  
  
 //Setting the TempData  
 TempData["Emp"] = emp;  
 return RedirectToAction("Index1");  
 }  
 public ActionResult Index1()  
 {  
 //Not reading TempData  
 return RedirectToAction("Index2");  
 }  
 public ActionResult Index2()  
 {  
 //Not reading TempData  
 return RedirectToAction("About");  
 }  
 public ActionResult About()  
 {  
//Not reading TempData  
 return View();  
 }  
  
 public ActionResult Contact()  
 {  
 //Data will available here because we have not read data yet  
 var tempEmpData = TempData["Emp"];  
 return View();  
 }

Normal Data Read

If you read the data on "About" page when it will render and try to access the value on "Contact" page, it will not be available.

@{  
 ViewBag.Title = "About";  
}  
<h2>About Page</h2>  
<br />  
@{  
 var data = (MVCStateManagement.Models.Employee)TempData["Emp"];  
}

TempData will not be available with Contact page because we have already read that data on "About" page. TempData is only available with subsequent request if you have not read yet.

public ActionResult Contact()  
 {  
 //Data will not available here because already read on About page  
 var tempEmpData = TempData["Emp"];  
 return View();  
 }

Keep TempData

If you still want to persist your data with the next request after reading it on "About" page that you can use "Keep()" method after reading data on "About" page. The Keep method will persist your data for next subsequent request.

@{  
 var data = (MVCStateManagement.Models.Employee)TempData["Emp"];  
 TempData.Keep();  
}  
public ActionResult Contact()  
 {  
 //TempData will available here because we have keep on about page  
 var tempEmpData = TempData["Emp"];  
 return View();  
 }

Peek TempData

Using Peek() method, we can directly access the TempData value and keep it for next subsequent request.

@{  
 var data = (MVCStateManagement.Models.Employee)TempData.Peek("Emp");  
}

When we move to "Contact" page, the TempData will be available.

public ActionResult Contact()  
 {  
 //TempData will available because we have already keep data using Peek() method.  
 var tempEmpData = TempData["Emp"];  
 return View();  
 }

Today, we have learned state management techniques to help maintain the data.

Summary

  1. Life cycle in MVC and its configuration.
  2. State Management techniques
  3. Maintain the data and transfer data from controller to view.