ASP.NET CORE MVC - Controllers, Actions and Views

ASP.NET CORE MVC TUTORIAL SERIES · PART 2

Controllers, Actions and Views

Create a new ProductsController, add action methods, understand IActionResult, use conventional routing, return Razor views, and pass simple data from a controller to a view using ViewData, ViewBag and a strongly typed model.

Objective

By the end of this tutorial, you will be able to create MVC controllers, define action methods, connect URLs to actions through conventional routing, and return Razor views containing data.

Lab environment used in this series

This tutorial assumes a Xubuntu virtual machine with Linux username xubuntu. The project is located under /home/xubuntu/aspnet-mvc-tutorial/ProductManagement.

In this tutorial
  1. Review the MVC request flow
  2. Create ProductsController
  3. Create the Index action
  4. Create the Details action
  5. Understand conventional routing
  6. Return Razor views
  7. Understand view discovery and diagnose a missing-view error
  8. Pass data with ViewData
  9. Pass data with ViewBag
  10. Create a simple strongly typed view
  11. Test the routes in the browser
  12. Complete a hands-on exercise

1. Review the MVC Request Flow

In Part 1, the default application used HomeController. We will now create a separate controller for product-related requests.

Browser ↓ Routing ↓ ProductsController ↓ Action Method ↓ View ↓ HTML Response

The controller acts as the coordinator between the incoming HTTP request and the response.

2. Open the Existing Project

Open the Xubuntu Terminal. The project created in Part 1 is located under ~/aspnet-mvc-tutorial/ProductManagement. Go to the project directory:

cd ~/aspnet-mvc-tutorial/ProductManagement

Confirm the current location:

pwd

For the xubuntu user, the expected path is:

/home/xubuntu/aspnet-mvc-tutorial/ProductManagement

Open the project in Visual Studio Code:

code .
Do not run /Products yet

We will first create both the controller and its matching Razor view. If ProductsController.Index() returns View() before Views/Products/Index.cshtml exists, ASP.NET Core will report that the Index view was not found.

3. Create ProductsController

In the VS Code Explorer, open the Controllers folder. Create a new file named:

ProductsController.cs

Add the following code:

using Microsoft.AspNetCore.Mvc;

namespace ProductManagement.Controllers;

public class ProductsController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

What does this code mean?

Code Purpose
using Microsoft.AspNetCore.Mvc; Imports MVC types such as Controller and IActionResult.
ProductsController The controller class for product-related requests.
: Controller Inherits MVC controller functionality.
Index() An action method.
return View(); Returns a Razor view to the browser.
Controller naming convention

MVC controller class names normally end with Controller. The URL uses the shorter name. Therefore, ProductsController is addressed as /Products.

4. What Is an Action Method?

A public method on a controller can act as an MVC action when it is selected by routing.

Our first action is:

public IActionResult Index()
{
    return View();
}

When the browser requests /Products, conventional routing can select this Index() action.

/Products ↓ ProductsController ↓ Index() ↓ View()

5. What Is IActionResult?

IActionResult is an interface representing the result of an MVC action. An action can return different kinds of results.

ViewResult Produces a Razor view.
NotFoundResult Produces an HTTP 404 response.
RedirectResult Redirects the browser.
ContentResult Returns text content directly.

For now, our actions will mainly return View().

6. Conventional Routing

Recall the default route from Program.cs:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

The URL convention is therefore:

/{controller}/{action}/{id?}

Examples:

URL Controller Action ID
/Products Products Index —
/Products/Index Products Index —
/Products/Details/1 Products Details 1

The route is called conventional routing because the URL follows a predefined controller/action/id pattern.

7. Create the Products View Folder

Before testing the controller, create the Razor view that Index() expects.

From the project directory, create the Products view folder:

mkdir -p Views/Products

Linux file and folder names are case-sensitive. Use Products with a capital P.

The structure should now contain:

Views/
├── Home/
├── Products/
├── Shared/
├── _ViewImports.cshtml
└── _ViewStart.cshtml

8. Create the Index View

Create the view file:

touch Views/Products/Index.cshtml

Open it in Visual Studio Code:

code Views/Products/Index.cshtml

Add:

@{
    ViewData["Title"] = "Products";
}

<h1>Products</h1>

<p>This is the product list page.</p>

Save the file.

Verify the file before running the application

From the project directory, run:

ls -l Views/Products

You should see:

Index.cshtml

The required MVC path is exactly:

Views/Products/Index.cshtml
Case matters on Xubuntu/Linux

These paths are not equivalent:

Views/Products/Index.cshtml
Views/products/Index.cshtml
Views/Products/index.cshtml

Run the application

Now start the ASP.NET Core application:

dotnet run

Use the exact localhost address shown after Now listening on:, then add /Products.

For example:

http://localhost:5223/Products

Your port may be different. Use the port displayed on your VM.

You should now see:

Products

This is the product list page.
First custom MVC page complete

The URL /Products now reaches ProductsController.Index(), which successfully finds and renders Views/Products/Index.cshtml.

9. How Does View() Find the Correct View?

When ProductsController.Index() executes:

return View();

MVC uses naming conventions to search for a matching view.

ProductsController ↓ Index() ↓ View() ↓ Views/Products/Index.cshtml

The controller name determines the folder, and the action name normally determines the view file.

10. Troubleshooting: “The view 'Index' was not found”

If the browser displays an error similar to:

InvalidOperationException: The view 'Index' was not found.

The following locations were searched:
/Views/Products/Index.cshtml
/Views/Shared/Index.cshtml

this means the controller and action were found, but MVC could not find the Razor view requested by return View();.

Check the file directly from the terminal:

cd ~/aspnet-mvc-tutorial/ProductManagement
ls -l Views/Products/Index.cshtml

If the file does not exist, create it:

mkdir -p Views/Products
touch Views/Products/Index.cshtml

Then add the Index view content from Section 8, save the file, and refresh /Products.

ProductsController ↓ Index() ↓ return View() ↓ MVC searches: Views/Products/Index.cshtml ↓ View found → HTML View missing → exception
What this error teaches

An MVC route can successfully find the controller and action while still failing later because the requested Razor view does not exist. Controller discovery and view discovery are separate steps.

11. Create the Details Action

Return to ProductsController.cs and add:

public IActionResult Details(int id)
{
    return View();
}

The complete controller now looks like:

using Microsoft.AspNetCore.Mvc;

namespace ProductManagement.Controllers;

public class ProductsController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    public IActionResult Details(int id)
    {
        return View();
    }
}

12. Create the Details View

Create the Details view file:

touch Views/Products/Details.cshtml

Open it in Visual Studio Code:

code Views/Products/Details.cshtml

Add:

@{
    ViewData["Title"] = "Product Details";
}

<h1>Product Details</h1>

<p>This page will display one product.</p>

Now browse to:

/Products/Details/1

Routing selects ProductsController.Details() and supplies 1 to the id parameter.

/Products/Details/1 ↓ ProductsController ↓ Details(int id) ↓ id = 1 ↓ Views/Products/Details.cshtml

13. Display the Route ID Using ViewData

Modify the action:

public IActionResult Details(int id)
{
    ViewData["ProductId"] = id;
    return View();
}

Then modify Details.cshtml:

@{
    ViewData["Title"] = "Product Details";
}

<h1>Product Details</h1>

<p>Product ID: @ViewData["ProductId"]</p>

Test:

/Products/Details/1
/Products/Details/25
/Products/Details/100

The displayed product ID should change according to the URL.

14. What Is ViewData?

ViewData is a dictionary that lets a controller pass small pieces of data to a Razor view.

Controller:

ViewData["ProductId"] = id;

View:

@ViewData["ProductId"]

It is useful for small values such as page titles or short messages.

Do not use ViewData as the main application data model.

Later in this tutorial we will use strongly typed models and ViewModels for structured application data.

15. Pass Data Using ViewBag

ASP.NET Core MVC also provides ViewBag, a dynamic way of passing small values to a view.

Modify Index():

public IActionResult Index()
{
    ViewBag.Message = "Welcome to the Product Management System";
    return View();
}

Then add this to Views/Products/Index.cshtml:

<p>@ViewBag.Message</p>

Refresh /Products.

16. ViewData versus ViewBag

ViewData ViewBag
Dictionary-based. Dynamic property syntax.
ViewData["Message"] ViewBag.Message
String key required. Property-style syntax.
Useful for small supplementary values. Useful for small supplementary values.

For important structured data, a strongly typed model is usually clearer and safer.

17. Create a Simple Product Model for the View

We will formally study models in Part 3. For now, create a very small class so that we can see how strongly typed views work.

Create:

Models/Product.cs

Add:

namespace ProductManagement.Models;

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

18. Return a Product Object from the Controller

Add the model namespace to ProductsController.cs:

using ProductManagement.Models;

Then change Details():

public IActionResult Details(int id)
{
    var product = new Product
    {
        Id = id,
        Name = "Sample Product"
    };

    return View(product);
}

Notice the difference:

return View();

becomes:

return View(product);

The product object is now supplied to the Razor view.

19. Make the Details View Strongly Typed

Replace Views/Products/Details.cshtml with:

@model ProductManagement.Models.Product

@{
    ViewData["Title"] = "Product Details";
}

<h1>Product Details</h1>

<p><strong>ID:</strong> @Model.Id</p>
<p><strong>Name:</strong> @Model.Name</p>

The first line:

@model ProductManagement.Models.Product

declares the type of object expected by the view.

The view can then access:

@Model.Id
@Model.Name
Why strongly typed views matter

Strong typing makes the expected data structure explicit and provides better compile-time checking than passing important application data through dynamic values. Microsoft also recommends strongly typed approaches for model-driven MVC views.

20. Test the Strongly Typed Details Page

Run the application if necessary:

dotnet run

Open:

/Products/Details/7

You should see:

Product Details

ID: 7
Name: Sample Product

Try:

/Products/Details/42

The ID changes because it comes from the route.

21. Compare the Three Ways of Passing Data

Method Example Best use
ViewData ViewData["Title"] Small supplementary values.
ViewBag ViewBag.Message Small supplementary values using dynamic syntax.
Strongly typed model @Model.Name Structured application data.

22. Current Project Structure

ProductManagement/
├── Controllers/
│   ├── HomeController.cs
│   └── ProductsController.cs
├── Models/
│   ├── ErrorViewModel.cs
│   └── Product.cs
├── Views/
│   ├── Home/
│   ├── Products/
│   │   ├── Index.cshtml
│   │   └── Details.cshtml
│   └── Shared/
├── wwwroot/
├── Program.cs
└── ProductManagement.csproj

23. Full ProductsController at the End of Part 2

using Microsoft.AspNetCore.Mvc;
using ProductManagement.Models;

namespace ProductManagement.Controllers;

public class ProductsController : Controller
{
    public IActionResult Index()
    {
        ViewBag.Message = "Welcome to the Product Management System";
        return View();
    }

    public IActionResult Details(int id)
    {
        var product = new Product
        {
            Id = id,
            Name = "Sample Product"
        };

        return View(product);
    }
}

24. Request Flow for /Products

GET /Products ↓ Routing ↓ ProductsController ↓ Index() ↓ View() ↓ Views/Products/Index.cshtml ↓ HTML Response

25. Request Flow for /Products/Details/7

GET /Products/Details/7 ↓ Routing ↓ ProductsController ↓ Details(7) ↓ Create Product object ↓ View(product) ↓ Views/Products/Details.cshtml ↓ HTML Response

26. Hands-On Exercise

Exercise: Add an About Action to ProductsController

Add a new action:

public IActionResult About()
{
    ViewBag.Message = "Product Management System tutorial";
    return View();
}

Create:

Views/Products/About.cshtml

Use this content:

@{
    ViewData["Title"] = "About Products";
}

<h1>About Products</h1>

<p>@ViewBag.Message</p>

Test:

/Products/About

27. Challenge Exercise

Modify Details() so that the product name includes the requested ID.

For example:

/Products/Details/5

should display:

ID: 5
Name: Product 5
Show solution
public IActionResult Details(int id)
{
    var product = new Product
    {
        Id = id,
        Name = $"Product {id}"
    };

    return View(product);
}

28. Knowledge Check

  1. What naming convention is normally used for MVC controller classes?
  2. What URL maps to ProductsController.Index() under the default route?
  3. What is an action method?
  4. What does IActionResult represent?
  5. What does return View(); do?
  6. Where does MVC normally search for the ProductsController.Index() view?
  7. What does the optional {id?} segment represent?
  8. How can a route value be received by an action?
  9. What is ViewData?
  10. What is ViewBag?
  11. What does the Razor @model directive do?
  12. Why is a strongly typed model preferable for structured application data?
Show suggested answers
  1. The class name normally ends with Controller.
  2. /Products or /Products/Index.
  3. A controller method selected to handle an HTTP request.
  4. The result produced by an MVC action.
  5. It returns a Razor view.
  6. Views/Products/Index.cshtml.
  7. An optional route value named id.
  8. By declaring a matching action parameter such as int id.
  9. A dictionary used to pass small values from controller to view.
  10. A dynamic property-based mechanism for passing small values to a view.
  11. It declares the type of model expected by the Razor view.
  12. It makes the expected structure explicit and provides stronger compile-time checking.

29. Part 2 Summary

In this tutorial, you:

  • created ProductsController;
  • created Index and Details actions;
  • used IActionResult;
  • returned views with View();
  • used conventional routing;
  • received an id route value;
  • created Razor views under Views/Products;
  • used ViewData and ViewBag;
  • created a simple Product object; and
  • created your first strongly typed Razor view.
Browser ↓ Routing ↓ ProductsController ↓ Action ↓ Model / Data ↓ View ↓ HTML
Next: Part 3 — Building Product Models and Strongly Typed Razor Views

In Part 3, we will expand the Product model with Id, Name, Description, Price and Quantity, then explore Razor syntax, @model, HTML helpers and Tag Helpers in more detail.