Tuesday, February 18, 2020

The Secret Code: ASP.NET MVC Conventions

ASP.NET MVC is filled with hidden conventions: put a controller or view in a certain place and give it a special name, and it magically works. If you know the conventions, you can quickly get an application up and running. But if you don't know the secret code, all is lost.

Today, we'll look at some of the conventions used in the ASP.NET Core MVC standard template:
  • File Naming and Locations
  • Basic Routing
  • Views and Layout
This will be just enough to get you started. Once you have the basic idea, you can find lots of articles that dig into each topic. Stephen Haunts (@stephenhaunts / https://stephenhaunts.com/) and Shawn Wildermuth (@ShawnWildermuth / http://wildermuth.com/) are both good resources for deeper dives.

The code for this project is on GitHub: jeremybytes/mvc-conventions-aspnet. However, the code is from the standard ASP.NET Core MVC project template, so you can create a new project locally and see the same thing (at least with the "current" .NET Core 3.1 templates). I will use Visual Studio Code to explore the files and run the project, but you can use the editor/IDE of your choice.

New ASP.NET Core MVC Project
We'll start by creating a new ASP.NET Core MVC project. I'll use the command-line for this in PowerShell (but these commands are the same from cmd.exe or bash).

From the command line, type dotnet new mvc -n "mvc-conventions'

"dotnet new" indicates we want to create a new project, "mvc" is the template for an ASP.NET Core MVC project. The "-n "mvc-conventions"" says we want to use the name "mvc-conventions" for the project.

This creates a folder called "mvc-conventions" with our new project.

Running the Application
The template has enough code for us to run. From the command line, type "dotnet run" to start the web application. Note: be sure that you are in the "mvc-conventions" folder.

Notice that the application says it is listening at "https://localhost:5001" and "http://localhost:5000". By default, the website forwards http calls to https, so if we navigate to either of these locations, we'll end up at the https location.

Note: If you do not have a localhost developer certificate set up, you may get browser warnings when going to the https site. Take a look at this article from Scott Hanselman for how to fix this: Developing locally with ASP.NET Core under HTTPS, SSL, and Self-Signed Certs.

Here's the site in the browser:

The template site only has a couple of features. The above is the "Home" screen. We can also navigate to the "Privacy" screen by clicking one of the "Privacy" links.

The Project Files
Let's take a closer look at the generated code. I have Visual Studio Code installed. To open Visual Studio Code at the current folder, we can use "code ." on the command line.

When the project opens, you will get a popup asking to add required assets. Go ahead and click "Yes" to this. This creates a ".vscode" folder that has some settings to make things easier to run and debug from within Visual Studio Code.

Here is the structure that was created (also available from the GitHub project):

The items we'll concentrate on today are the "Controllers" folder, the "Views" folder, and the "Startup.cs" file.

A Quick Overview of MVC
Before going further, we should get a quick overview of what MVC is. MVC stands for Model-View-Controller, and it is a common application UI pattern.

The Model is the data/logic of the application. This application doesn't really have that since we're dealing with just a couple of hard-coded pages.

The Controller is responsible for interacting with the Model and then displaying a View.

The View is the UI. This is what the user sees in the browser.

This is a highly-simplified description of the elements. In the case of ASP.NET MVC, we will call an action on a controller. That action will perform some type of work (often interacting with the model) and determine what view to send back to the user.

We'll walk through this by looking at a route.

The Default Route
Routing is how we turn URLs into actions. This is a very large topic. For today, we'll just look at the default route that we get with the template.

The default route is set up in the Startup.cs file at the root level of our project (from the Startup.cs file on GitHub):

We'll break this down a little bit.

First, the pattern "{controller}/{action}/{id}" says that we're trying to parse a controller, an action, and an id from the URL.

Convention: The default route pattern is [Controller Name] / [Action Name] / [ID (optional parameter)].

So if we have a sample URL of "https://localhost:5001/Home/Index/2", we would use the "Home" controller by calling the "Index" action on it, and pass in an "id" parameter of "2".

The pattern in the code is a little more complex than what I just showed. That is because there are some default values set up. If a "controller" is not provided, then by default we use the "Home" controller. In addition, if an "action" is not provided, by default we use the "Index" action.

The "?" on the "id" specifies that this is optional. So if it is not provided, then there's no harm done.

With the defaults, that means that if we navigate to "https://localhost:5001", then it will try to call use the "Index" action on the "Home" controller (since those are the defaults).

The Controller and Action
Now that we know what controller and action to use, how does the system find it? That's where some of the conventions come in.

Convention: Controllers are located in the "Controllers" folder.

Controllers are located in the "Controllers" folder. Here's that folder in our project (you can also look at the Controllers folder on GitHub):

This has one file: HomeController.cs. Here is the code for the "HomeController" class with some of the methods collapsed (from the HomeController.cs file):

Convention: Controllers are named with the suffix "Controller".

Due to the naming conventions, if our routing is looking for the "Home" controller, it will look for a class named "HomeController". The naming of our controllers is important if we want ASP.NET Core MVC to be able to find them automatically.

So we've found the controller, what about the action "Index"?

When looking for an action, we're really looking for a function of the same name on the controller.

Convention: An Action is a function with the same name in the Controller class.

For the "Index" action, we would look for an "Index" function on the controller. In addition, the method should return an "IActionResult". We won't get into what all the valid values are for "IActionResult". Today we'll just concentrate on Views.

Here is the "Index" method on the controller (from the HomeController.cs file):

Generally, an action will perform some type of work, such as get data from the model or process form data. In this case, we only return a View without passing anything programmatically.

The next question is "How do we find the View?"

The View
As you might imagine, there are conventions around how views are named and located (just as with controllers).

Here are the views in the project (from the Views folder on GitHub -- you'll have to drill down to see all the files):

Convention: Views are in the "Views" folder with sub-folders based on the Controller name.

Under the "Views" folder in our project, we have a "Home" folder. These are the views that are associated with the HomeController. In that folder, we have 2 files, "Index.cshtml" and "Privacy.cshtml".

Convention: Views are named after the corresponding Action in the Controller.

The Views correspond to 2 of the actions on the HomeController: Index and Privacy.

So when the "Index" action returns "View()", it uses the "Index.cshtml" file in the "Home" folder under "Views".

Technical note: these conventions are based on how the ASP.NET MVC infrastructure searches for views. First, it looks in the "Views" subfolder that matches the Controller name. If it cannot find a matching view there, then it looks in the "Shared" subfolder. As you dive deeper into ASP.NET MVC, you can read articles that go into this in more detail.

Here's the code for the "Index" view (from the Index.cshtml file):

This has the code that is unique to this page, but it is incomplete. It's just a "div", not a complete web page. (We'll get back to what the "@" section means in just a bit.)

The rest of the page is part of a shared layout.

The Layout
If we look at the "Views" folder again, we see that there is also a "Shared" sub-folder.

This contains views that are not particular to any controller (such as the "Error" view). But it also has the "_Layout.cshtml" file. This is what fills in the rest of the page that we see in the browser.

Here is part of the content from the layout (from the _Layout.cshtml file):

This file has the markup for the headers and footers of the page (including the menus). It also brings in style sheets and Bootstrap.

Injecting the Body of the View
The important bit for us is the code "@RenderBody()".

In .cshtml files, the "@" means "there's some C# code coming!" 

The extension of the file (cshtml) indicates that it is a mixture of C# (cs) and HTML (html).

So when we have "@RenderBody()", this is an indication that the "RenderBody" method should be run. "RenderBody" is an included helper method, and this is what puts the custom view markup into the page.

So in our case, the "div" that is in the Index.cshtml file ends up inside the "main" tag from the layout file.

Other Injected Values
The layout is not limited to "@RenderBody()". The custom views can also provide data to the layout through ViewData.

Let's go back to the Index View for a moment (from the Index.cshtml file):

Notice the "@" section at the top of the page. As we saw earlier, this indicates a section of C# code. In this case, it is blocked by curly braces; everything inside the braces is considered the C# code. "ViewData" is a dictionary that lets us set up name/value pairs. In this case, we have the "Title" for the page.

In the header section of the layout page, we can see how this is used (from the _Layout.cshtml file):

Notice that inside the "title" tag of the page head, there is "@ViewData["Title"]". Again, the "@" indicates that this is C# code. This takes the value from the Index.cshtml ViewData and puts it in here.

So when we view the page, we can see the page title set in the browser tab:

The value is "Home Page - mvc_conventions".

Note: Our pages do not need to use _Layout.cshtml. We can have some pages use the layout and some pages that don't. Or we can eliminate the layout completely. If you're interested, you can find articles that talk about this in more detail.

Routing to Other Actions
If we click on one of the "Privacy" links, we can see the route to that page:

The URL is "https://localhost:5001/Home/Privacy". Based on what we've seen, we should be able to figure out where this goes.

If we go back to the default route: Controller / Action / Optional ID, we can translate this URL to using the "Privacy" action on the "Home" controller.

The "Privacy" action translates into the "Privacy()" function on the controller (from the HomeController.cs file):

This returns a view just like the Index action.

But instead of using the "Index.cshtml" file, it uses the "Privacy.cshtml" file. Here is the code for that (from the Privacy.cshtml file):

This has ViewData specified with a title as well. The layout puts this into the "title" tag of the page. But the Privacy View also uses it in an "h1" tag on the page itself.

Using ViewData is one way to get data from the Controller to a View, but that's outside the scope of what we're looking at today.

Adding Your Own Pages
With an understanding of the conventions that are part of ASP.NET MVC, we can add our own pages fairly easily. If we wanted to add a page with a catalog of products, we would create a class called "CatalogController" and put it in the "Controllers" folder.

Then in the "CatalogController" class, we could add an "Index" method that calls into the Model to get a list of products from a database, and return that data along with the View.

To create the view, we can add an "Index.cshtml" file to a newly created "Catalog" folder under "Views".

If we follow the naming conventions, then we do not have to do anything special to wire the page up. The conventions take care of all of that for us. We would just access it with "https://locahost:5001/Catalog" (remembering that "Index" is the default action).

We could even take this a step further and create a "Product" action that takes an "id" parameter. This could get a product from the Model based on the id and then display it in a custom View. This could be accessed with "https://localhost:5001/Catalog/Product/24". And if the default routes don't work for us, we can create custom routes as well.

These are the conventions I used to make the web front end for my maze generation project. You can read more about that and dive into some basics about parameters here: Building a Web Front-End to Show Mazes in ASP.NET Core MVC.

Wrap Up
ASP.NET Core MVC (as well as ASP.NET MVC on .NET Framework) have a lot of conventions. This saves us from writing a lot of configuration code to wire things together. But we do need to be aware of the conventions in order for our pages to work properly.

Summary of Conventions:
  • The default route pattern is [Controller Name] / [Action Name] / [ID (optional parameter)].
  • Controllers are located in the "Controllers" folder.
  • Controllers are named with the suffix "Controller".
  • An Action is a function with the same name in the Controller class.
  • Views are in the "Views" folder with sub-folders based on the Controller name.
  • Views are named after the corresponding Action in the Controller.
Other Stuff:
  • In .cshtml files, the "@" means "there's some C# code coming!"
With these conventions in hand, it is easier to understand what is happening in existing ASP.NET MVC web projects that you need to modify. And it is also a good starting point to take a deeper dive into all things MVC.

Happy Coding!

No comments:

Post a Comment