Click here to Skip to main content
13,199,669 members (68,102 online)
Click here to Skip to main content
Add your own
alternative version

Stats

7.9K views
95 downloads
15 bookmarked
Posted 1 Oct 2017

From MVC to Razor Pages

Rate this:
Please Sign up or sign in to vote.
Building a tiny e-Commerce on ASP.NET Core, Razor pages, React JS.NET and EF Core

Download RazorPageShop.zip

Introduction

With ASP.NET Core 2, Microsoft has provided us with a brand new alternative to the MVC (Model-View-Controller) approach for creating web applications. Microsoft named it Razor Pages, and despite being a different approach, it's still familiar in some ways.

This article will showcase a scenario where Razor Pages is used to produce a small e-commerce website, also with a help from Facebook's React / ReactJS.NET to provide it with view rendering / binding engine based on JavaScript code.
 

Quote:

Disclaimer: This article focuses mainly on Razor Pages and complements the previous React Shop - A Tiny e-Commerce article I published some months ago. Please refer to that other article if you want to know more about working with React and React JS.NET in ASP.NET Applications.

Installation

In order to run the app within the attached source code, please install the following:

Background

If you follow ASP.NET development since the last decade, you may have already noticed how Microsoft once and while comes up with an innovation in the shape of a new development tool or framework - or even a new design pattern. Some of them have proven to be quite robust and reliable, such as ASP.NET MVC and Razor view engine, while others have not passed the test of time and were abandoned, such as AJAX Control Toolkit.

But many things have changed since the the release of ASP.NET Core 1.0 in 2016. Microsoft rewrote ASP.NET to be a open-sourced, GitHub hosted project, and introduced the multi-platform to its web development framework for the first time.

Since the first version, ASP.NET Core provided us with the standard web development framework that already had an enormous adoption since ASP.NET 3.0  - ASP.NET MVC  - based on the Model-View-Controller design pattern.
The MVC design pattern was developed first in 1979 in the mythical Xerox Palo Alto Research Center (XPARC), but only became famous when several web frameworks started adopting it: Spring for Java in 2002, Django and Ruby on Rails in 2005, and ASP.NET in 2009.

And ASP.NET MVC was a big hit in our community. Before it, we had to deal with problems that emerged from ASP.NET Web Forms, such as Update Panels, growing ViewState, Postbacks and a quite complex page event management. With ASP.NET MVC, We were taught to speak the language of separation of concerns while developing our web applications: data belongs to Model, presentation logic is only for Views, and every request must be handled by Controller actions, which in turn decide which View is to be rendered, along with its appropriate Model. There was no more need for hiding the stateless nature of web  from programmers.  

But while ASP.NET MVC brings us a lot of benefits, sometimes it is still met with some criticism. While users - and certainly web developers - think of web applications basically as a set of “web pages”, ASP.NET MVC does not have a clear concept of Web Page. Instead, in an ASP.NET MVC Project every component usually has its own file, and belongs to a different folder, depending on its role inside the MVC framework.

Now, stop for a moment and imagine how you organize your computer folders. Imagine you had some different jobs to do, which involved different files, and you had your computer folders organized not by subjects or work or topic, but by file types. And imagine you are doing multiple different jobs and, instead of grouping files by each job, you had the files for each job scattered in different folders like these: 

Would it be a good idea?

Similarly, we can see by the image below how a typical MVC project keeps the components of a single page in a set of files scattered in many files and folders:

So, in MVC there’s not a single “web page” file. And it’s a little awkward to explain it to someone who’s new to the technology.

Then someone at Microsoft thought the same thing could be done in a different way.
 

Introducing Razor Pages

What if you took an MVC application, then you called your View as a Page (e.g. in Index.cshtml file), and you centralized not only the Model data but also the server-side code related to that page (that used to reside on your Controller) inside a class dedicated to that page (inside an Index.cshtml.cs file) - that you now called a Page Model?

If you have already worked in native mobile apps, then you have probably seen something similar to this in the Model-View-ViewModel (MVVM) pattern.

The Page and Page Model in Razor Pages

This is how the Razor Page was born! ASP.NET Core 2.0 - more precisely Visual Studio 2017 -  introduces a new Web Application template using Razor Page as the default content.

Then you might think “But wait a minute... this looks a bit too familiar to me”. Because now you have a page, and a class performing server-side functions. Isn't Page Model the same as the old code behind file? Isn’t it Web Forms all over again?

No. And let me explain why Razor Pages is not a web forms revival. First, we must realize that a Razor Page is not very different from the MVC design pattern. In the past, with Web Forms you usually conflated business rules, User Interface and data layer. While in ASP.NET Web Forms you had that artificial plumbing that enabled event handling at the cost of simplicity, performance and bandwidth, on the other hand, all MVC components are more or less visible in Razor Pages. They are just positioned in different classes/files/folders in order to facilitate development of pages.

Another misconception some people had about Razor Pages was about it being mainly for junior developers or less sophisticated applications. While it may be true that it's easier for newbie developers to understand Razor Pages than MVC web apps, you'll still be able to build complex applications, as we'll see in the source code that accompanies this article.

You also may think you somehow "lost" the ability to work with MVC projects once you've created a new Razor Pages web app. But fortunately, this is not true. Remember, both templates (MVC and Razor Pages) rely on the same ASP.NET Core MVC framework. So, you can, for instance, create a new Razor Page project and then create the MVC folders (Controllers, Views, etc.) and the required files in order to work with MVC Controllers and Views alongside Razor Pages in the same project!

In the Razor Pages project, create a new Controllers folder under the project root, and then create a TestController class. Now we implement the Index action to return a plain text:

 

public class TestController : Controller
{
    public IActionResult Index()
    {
        //Accessible through /Test
        return Content("You can work with MVC Controllers and Views " +
            "alongside Razor Pages in the same project!");
    }
}

 

Running the application and typing http://localhost:XXXXX/test in the browser's address bar, we get:

Isn't it awesome?

Configuration 

When you create a new ASP.NET Core Razor Pages Web App, this is what you get in your Program.cs file:

public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .Build();
}

The Program.cs file

Now, pay attention to the .UseStartup<Startup>() line. It tells ASP.NET Core to specify the Startup class to be used by the web host.

The Startup class is also automatically created when you choose the Razor Pages template. It comes with the ConfigureServices method, which adds services to the web app, and the Configure method, which configures the HTTP request pipeline.

public class Startup
{
public Startup(IConfiguration configuration)
{
	Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
	services.AddMvc();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
		app.UseBrowserLink();
	}
	else
	{
		app.UseExceptionHandler("/Error");
	}

	app.UseStaticFiles();

	app.UseMvc(routes =>
	{
		routes.MapRoute(
			name: "default",
			template: "{controller}/{action=Index}/{id?}");
	});
}
}

The Startup.cs file

The above code shows the ConfigureServices method calling the MVC service services.AddMvc(); (See how Razor Pages depend on the MVC framework?) Also, the Configure method is called by the runtime to set up the request pipelineFrom the code above, we see the Configure method:

  • specifying the error page configuration
  • enabling static file serving for the current request path
  • adding MVC to the Microsoft.AspNetCore.Builder.IApplicationBuilder request execution pipeline

So there you have it. Just like ASP.NET Core MVC application, the Razor Pages web app also uses the Startup class to configure everything about the web application.

The New Razor Page Web Application

Remember when we said Razor Pages is like ASP.NET Core MVC done differently? Despite being different from MVC, Razor Pages still relies on ASP.NET Core MVC Framework. Once you create a new project with Razor Pages template, Visual Studio configures the application via Startup.cs file to enable the ASP.NET Core MVC Framework, as we have just seen.

The template not only configures the new web application for MVC use, but also creates the Page folder and a set of Razor pages and page models for the example application:

For the Razor Page Shop web app, I needed 3 different views (Pages):

  • Product Catalog: where user will choose from a catalog which products to put in the shopping cart
  • Shopping Cart: the products selected for the purchase order
  • Order Details: shipping data, customer info and product listing for the order that has just been placed

So I kept Index.cshtml as the Product Catalog page and included 2 more Razor Pages pages: Cart.cshtml and CheckoutSuccess.cshtml, for Shopping Cart and Order Details, accordingly:

The 2 New Razor Pages: Cart.cshtml and CheckoutSuccess.cshtml.

The Pages Folder

The previous image shows how every view (ahem, Razor Page) is now contained inside the Pages folder. The difference between the new Pages folder and the tradicional MVC web app "Views" folder goes beyond the folder name. In fact, since we don't have the concept of controllers and actions in a Razor Page web app, the very position of the cshtml file inside the Pages folder defines through which url route it should be accessed. For example:

  • /Pages/Index.cshtml -> "/" or "/Index"
  • /Pages/Cart.cshtml -> "/Cart"
  • /Pages/CheckoutSuccess.cshtml -> "/CheckoutSuccess"  

Likewise, you might want to create subfolders inside the Pages folder in order to create more complex url route schemes:

  • /Pages/Products/WhatsNew.cshtml -> "/Products/WhatsNew"
  • /Pages/Categories/Listing.cshtml -> "/Categories/Listing"
  • /Pages/Admin/Dashboard.cshtml -> "/Admin/Dashboard"  

Anatomy of a Razor Page

At first sight, a Razor Page looks pretty much like an ordinary ASP.NET MVC View file. But a Razor Page requires a new directive. Every Razor Page must start with the @page directive, which tells ASP.NET Core to treat it as Razor page. The folowing image shows a little more detail about a typical razor page.  

@page - Identify the file as a Razor Page. Without it, the page is simply unreacheable by ASP.NET Core.
@model - much like in an MVC application, defines the class from which originates the binding data, as well as the Get/Post methods requested by the page.
@using - the regular directive for defining namespaces.
@inject - configures which interface(s) instance(s) should be injected into the page model class.
@{ } - a piece of C# code inside Razor brackets, which in this case is used to define the page title.
<div…> - the regular HTML code that comes along with the Razor-enabled C# Code.

Web Development Made Simple

What about creating static pages in Razor Pages app? Let's say you had to create a Terms Of Use page.

Before Razor Pages, with a regular MVC web app, you had to follow some steps in order to include a simple static page in your web application:

 

  • Add a Controller (Controllers/TermsOfUseController.cs)
  • Add an Action method (Index())
  • Add a View Folder (/Views/TermsOfUse)
  • Add a View (/Views/TermsOfUse/Index.cshtml)

 

Now, with Razor pages you job became much simpler:

  • Add a Page at (Pages/TermsOfUse.cshtml)

Now you may be wondering: what about the Page Model for the Terms Of Use page mentioned above. Since the page is just a static page, it doesn't need any Page Model, which is another advantage over an MVC web application.

The Page Model Class

Unlike a MVC View, which usually is bound to a custom Model or ViewModel, a typical Razor Page will specify its Model as a class inheriting from the PageModel class. Take a look at the following IndexModel class:

public class IndexModel : PageModel
{
public IList<ProductDTO> Products { get; set; }
readonly ICheckoutManager _checkoutManager;

public IndexModel(ICheckoutManager checkoutManager)
{
    this._checkoutManager = checkoutManager;
}

public void OnGet()
{
    Products = this._checkoutManager.GetProducts();
}

public async Task<IActionResult> OnPostAddAsync(string SKU)
{
    this._checkoutManager.SaveCart(new CartItemDTO
    {
        SKU = SKU,
        Quantity = 1
    });

    return RedirectToPage("Cart");
}
}

The Index.cshtml.cs file

The above Page Model is usually put inside a .cshtml.cs file, which is sometimes called a "code behind" file. But please don't mistake it for the infamous "code behind" file of Web Forms from the past. They have very little in common.

Now take a look at the IndexPageModel constructor:

readonly ICheckoutManager _checkoutManager;

public IndexModel(ICheckoutManager checkoutManager)
{
this._checkoutManager = checkoutManager;
}

You might be wondering who calls this constructor with the ICheckoutManager checkoutManager parameter. Fortunately for us, since ASP.NET Core 1, the framework has a built-in dependency injection service.

The concept of dependency injection is that some class instance must be automatically created by the framework, but such class has a constructor that depends on an instance of another interface. So, before the first class (in this case, IndexModel) is instantiated, the framework should create any dependencies (in this case, the ICheckoutManager interface) and then pass them to the constructor.

But in order to create instances from interfaces, the framework should be provided with information about which classes implement such interfaces. You should configure this dependency injection information in the Startup.cs file, more precisely in the ConfigureServices method:

 

public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddScoped<ICheckoutManager, CheckoutManager>();
.
.
.

 

Configuring Dependency Injection in Startup.cs file.

Notice how the services.AddScoped<ICheckoutManager, CheckoutManager>(); code defines the CheckoutManager class as the type which should be created whenever a new instance of the ICheckoutManager is needed. And this is done by the services.AddScoped method.

Speaking of configuration, notice that we used the AddScoped method. ASP.NET Core gives us some options of how often these instances should be created and how many times they should be used for dependency injections. This is called service lifetime, and these lifetimes can be configured as follows:

Transient: a new instance is created each time it is requested.

Scoped: a new instance is created once per request.

Singleton: a new instance is created the first time it is requested, and the same instance is used in every subsequent request.

Besides the Startup.cs configuration, you should also include a @injection directive inside the Page file, indicatin which interfaces should be generated by the dependency injection service upon requests:

 

@inject ICheckoutManager ICheckoutManager;

 

Configuring the @inject directive

Introducing Page Handlers

If you are used to work with MVC pattern, you might be wondering how Razor Pages replaced MVC Action Methods. In a MVC web app, the Controller is the entry point for every request in your application. In MVC pattern, the Controller is a collection of many actions that may respond to different views in your application, that are grouped together conveniently to share resources such as filters and route templates.

Razor Pages now feature Page Handlers, which are replacements for MVC controller actions. These handlers respond to specific HTTP Verbs, such as GET and POST. The handlers exist in Razor Pages and have the name convention of "on{HTTP Verb}".

This means every HTTP Get request made against a Razor Page lands on an OnGet of the Page Model inside the "code behind" file. In Index Razor Page, for example, we have the OnGet handler method that is used to instantiate the products list displayed in the Bootstrap's product carousel:

 

public void OnGet()
{
Products = this._checkoutManager.GetProducts();
}

 

But what if the Page Model has no such OnGet method, or the Page Model does not even exist? Fortunately, in this case, the Razor Page is called anyway, without errors. This is a convenient solution that avoids ceremony and boilerplate for simple Razor Pages.

Now, if you want to deal with HTTP Post requests, such as those submitted by HTML <form> elements, such as:

<form method="post">
.
.
.
<button type="submit" class="btn btn-link" name="SKU" value="@product.SKU">
<i class="fa fa-shopping-cart" aria-hidden="false"></i>
Add to Cart
</button>
.
.
.
</form>

then you should follow the name convention of "on{HTTP Verb}" and create a new razor page handler (method) called OnPostAsync:

 

public async Task<IActionResult> OnPostAsync(string SKU)
{
this._checkoutManager.SaveCart(new CartItemDTO
{
    SKU = SKU,
    Quantity = 1
});

return RedirectToPage("Cart");
}

 

There is still another scenario where you might want to submit different HTTP POST requests for different purposes inside a Razor Page. For instance, you might have 3 buttons: one for add, another for update and another one for delete operation. How should you accomodate different POST requests in a single OnPostAsync handler?

Fortunately, in this case Razor Pages provides a handy alternative that involves the use of Tag Helpers. Tag Helpers, as first seen in ASP.NET Core 1.0, enable server-side code to take part in creating and rendering HTML elements in Razor files. With Tag Helpers, you can specify multiple POST handler methods to be used in the same page. For example, let's say you want to change your method name from OnPostAsync to OnPostAddAsync:

 

public async Task<IActionResult> OnPostAddAsync(string SKU)
{
this._checkoutManager.SaveCart(new CartItemDTO
{
    SKU = SKU,
    Quantity = 1
});

return RedirectToPage("Cart");
}

 

Obviously, the previous <form> HTML will not be able to submit a request to the newly renamed handler method. But you can change the <form> HTML element into a Tag Helper and use the asp-page-handler in order to specify the new handler variation for the POST request:

<form asp-page-handler="Add"

.

.

.

<button type="submit" class="btn btn-link" name="SKU" value="@product.SKU">
<i class="fa fa-shopping-cart" aria-hidden="false"></i>
Add to Cart
</button>
.
.
.
</form>

Integrating ReactJS.NET Into ASP.NET Core 2.0

The last time I worked with React and ASP.NET 4.x, I followed React's guide to Server-Side Rendering, configuring the ReactConfig static class with the .jsx files containing each of the view contents:

public static class ReactConfig
{
    public static void Configure()
    {
        ReactSiteConfiguration.Configuration
            .AddScript("~/Scripts/showdown.js")
            .AddScript("~/Scripts/react-bootstrap.js")
            .AddScript("~/Scripts/Components.jsx")
            .AddScript("~/Scripts/Cart.jsx")
            .AddScript("~/Scripts/CheckoutSuccess.jsx");
    }
}

The Old ReactConfig.cs configuration file

But that tutorial was made for ASP.NET 4.x and doesn't work in ASP.NET Core web apps. So I had to port the server-side rendering portion to the ASP.NET Core Startup.cs file, following the new React Tutorial. So I had to include those scripts in the Configure of the Startup class so that ReactJS.NET could render server-side HTML content from the .jsx files.

app.UseReact(config =>
{
    config
        .AddScript("~/lib/react-bootstrap/react-bootstrap.min.js")
        .AddScript("~/js/Components.jsx")
        .AddScript("~/js/Cart.jsx")
        .AddScript("~/js/CheckoutSuccess.jsx")
        .SetUseDebugReact(true);
});

The New Configure Method from Startup configuration file

But the React.AspNet extension also requires you to call services.AddReact(); method in order to register all services required for ReactJS.NET:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddScoped<ICheckoutManager, CheckoutManager>();
    services.AddReact();
    .
    .
    .
}

The Application Pages

Since the pages of this web app were ported from the views of another MVC app to this Razor Pages web app, and they have been thorougly discussed in my other article, React Shop - A Tiny e-Commerce, and since they remain mostly unchanged, I chose not to explain everything over again. Please visit my other article if you wish to dive into those details regarding React-Bootstrap components, React JS views and .jsx files with ASP.NET applications.

Nevertheless, here is a simple overview of the application pages.

Product Catalog

The Product Catalog page.

The Product Catalog page shows a simple Product Carousel control. This Bootstrap control is useful for displaying endless animation and the can show many products using only a fraction of the page space.

The Product Carousel is rendered on the server side via Razor view engine. The carousel displays four products at once, so the code in the Index.cshtml view defines a foreach loop that iterates over "pages" of 4 products each.

Shopping Cart

The Shopping Cart page.

Compared to the Product Catalog, the Cart Page is rendered in a very different way. First, the Razor engine is not used to directly render the view. Instead, Razor calls the React method of the React.Web.Mvc.HtmlHelperExtensions class and passes the model to it. The Razor in turn renders the CartView that has been declared as a React component.

Checkout details

The Checkout Success page.

Although the CheckoutSuccess page does not contain any interaction (besides the "back to the product catalog" button), it was implemented entirely as a single React component. The reason is that we could take advantage of the simple syntax provided by the components of the React-Bootstrap library that we already explained in the other article. All the binding values are passed via props and there is no need to use React's state object.

Conclusion

I hope this article helped you as a quick start to this new world of Razor Pages. I'm sure you will find many scenarios where Razor Pages may be a tempting alternative to traditional ASP.NET MVC applications.

Thank you so much for you time and patience! If you have any suggestions, please let me know. And don't forget leaving your opinion in the comments section below.

History

 

  • 2017/10/01 - Initial version.

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author


You may also be interested in...

Pro

Comments and Discussions

 
BugCannot download file Pin
Quoc Thai Nguyen4-Oct-17 5:01
memberQuoc Thai Nguyen4-Oct-17 5:01 
GeneralRe: Cannot download file Pin
Marcelo Ricardo de Oliveira4-Oct-17 6:52
memberMarcelo Ricardo de Oliveira4-Oct-17 6:52 
GeneralRe: Cannot download file Pin
Marcelo Ricardo de Oliveira4-Oct-17 15:44
memberMarcelo Ricardo de Oliveira4-Oct-17 15:44 
QuestionCannot download source files Pin
kiolo4-Oct-17 4:56
memberkiolo4-Oct-17 4:56 
AnswerRe: Cannot download source files Pin
Marcelo Ricardo de Oliveira4-Oct-17 6:52
memberMarcelo Ricardo de Oliveira4-Oct-17 6:52 
AnswerRe: Cannot download source files Pin
Marcelo Ricardo de Oliveira4-Oct-17 15:45
memberMarcelo Ricardo de Oliveira4-Oct-17 15:45 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.171020.1 | Last Updated 1 Oct 2017
Article Copyright 2017 by Marcelo Ricardo de Oliveira
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid