Getting Started
Add a new ASP.NET Core Web Application
- Name it "HelloMVC"
- Choose the "Empty" Template with No Authentication.
Add a xUnit Test Project (.NET Core) to the solution
- Name it "HelloMVC.Tests"
- Add a reference from HelloMVC.Tests to HelloMVC.
- Rename UnitTest1.cs in HelloMVC.Tests to "HomeControllerTests.cs"
Setup a project repository on GitHub
- Initialize git to track your project
- Add a .gitignore file
- Get your first commit locked in
- Finish connecting your project to the GitHub repository by finishing the setup of your remote origin and pushing your changes
Add the First Test
Add a test to verify that the return value of the HomeController's Index() action is a ViewResult.
namespace HelloMVC.Tests
{
public class HomeControllerTests
{
[Fact]
public void Index_Returns_ViewResult()
{
var controller = new HomeController();
var result = controller.Index();
Assert.IsType<ViewResult>(result);
}
}
}
Create the Controller
There is an error when trying to instantiate a new HomeController():
The type or namespace 'HomeController' could not be found
Add a "Controllers" folder to your "HelloMVC" project, then a "HomeController" class inside your Controllers folder.
Check your Test
Import the HelloMVC.Controllers namespace to fix the first compile error.
using HelloMVC.Controllers; //<--- add this line to your code
namespace HelloMVC.Tests
{
public class HomeControllerTests
{
...
Add the Index() Action
The HomeController's Index() action needs to have a ViewResult defined as its return type.
For now, let's return null from the method.
Add a using statement for the Microsoft.AspNetCore.Mvc
namespace to eliminate the error on the ViewResult type.
using Microsoft.AspNetCore.Mvc;
namespace HelloMVC.Controllers
{
public class HomeController
{
public ViewResult Index()
{
return null;
}
}
}
How is the test now?
Hover on the ViewResult error and leave your cursor there for a second.
The type or namespace 'ViewResult' could not be found..
Hit Ctrl + . to pull up the quick fix menu.
Select "using Microsoft.AspNetCore.Mvc;"
It should build now!
using Xunit;
using Microsoft.AspNetCore.Mvc;
High Fives == time to git commit
Getting this test to compile is a pretty big milestone for us.
Let's lock this in a git commit before we start our next task.
If things go sideways this will be a great spot to come back to.
Run the test
Now that we can compile, it's time to actually run our test!
Hit ctrl + r, a to run the test
The Results are in...
What's a ViewResult, anyway?
ViewResult is a class that's built into our MVC framework.
We will use ViewResult to write methods that return HTML.
We need a little help...
We need some help creating an instance of ViewResult.
Luckily, the Controller class has one built in.
Let's make our HomeController inherit from Controller so we can use it.
public class HomeController : Controller
{
public ViewResult Index()
{
return null;
}
}
Using View()
In the Index() action, delete "null" and start typing "View()" in its place.
Intellisense should show you the View method we inherited.
Run those tests...
Time to commit
Tests are green... but does it work?
Hit F5 and find out!
Not what we expected
Where is 'Hello World' coming from?
Change the Startup.cs File
Our Startup.cs file has been configured with some default code, including a WriteAsync() method which is automatically writing "Hello World!" in response to an HTTP request.
Let's write over it.
Add a Startup() constructor and Configuration property to look like this:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration {get; }
}
You will need to add a using statement for Microsoft.Extensions.Configuration
in order to use the IConfiguration interface class.
Change the ConfigureServices() method of the Startup.cs class to look like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
And, change the Configure() method of the Startup.cs class to look like this:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
}
Try again... does it work?
Hit F5 and find out!
That's a no.
That's a no.
Where's the View?
MVC will look for our View in a bunch of places by default.
Sometimes you’ll hear this called “Convention over configuration”.
We didn’t need to configure where our View file is, it just uses some intuitive defaults.
By default, our HomeController’s Index view should be in the Views/Home/Index.cshtml file.
All controllers follow this convention by default.
Creating the View
Add a "Views" folder to your project and then the "Home" folder inside "Views".
Right click on your Home folder and select Add > New Item > View.
Deselect the “Use a layout page” checkbox.
Name your view “Index”.
Click Add.
Our View
Does it work now?
Hit F5 and find out!
That's better, but why is it empty?
Take another look at the view.
There's a lot of code in there...
But it's really just an empty page.
Add some text to the view.
Hit F5 again.
Hello world!
Time to commit
That's the C and the V
Our app receives a HTTP Get request when someone visits our site.
Our app finds the HomeController’s Index Action (aka Method).
Our Index action returns a default View.
Our default view lives in the Views/{ControllerName}/{ActionName}.cshtml file
The view’s HTML is rendered in the web browser.
The Big Picture
Let's Personalize It
Add a "Name:" Textbox and a "Greet" button to our page, to look like this:
Put them inside a form element that will send a Get request to "/Greet/Index"
Try it out
Enter "test" into the text box and click "Greet".
What happened?
The Resource Cannot be Found...
We're trying to visit the URL: https:// <yourlocalserver>
/Greet/Index?name="test"
What Controller name do we need?
- GreetController
What action does the Controller need?
- Index
Does the action take any arguments?
- Yes, a string called "name"
What's our first step?
Writing a failing unit test, of course!
The Test
Add a new GreetControllerTests.cs class in your HelloMVC.Tests project.
Add a using Xunit
statement and make the class public.
Continue by adding the following test method to verify that the return value of the GreetController's Index() action is a ViewResult:
public class GreetControllerTests
{
[Fact]
public void Index_Returns_ViewResult()
{
var controller = new GreetController();
var result = controller.Index("ThisIsAString");
Assert.IsType<ViewResult>(result);
}
}
Make it compile (first step to getting red)
There is an error on the instantiation of the GreetController() object.
The type or namespace name 'GreetController' cannot be found
We've seen this error message before.
Add a new "GreetController" class inside your Controllers folder.
Make the class public and add the following Index() action:
namespace HelloMVC.Controllers
{
public class GreetController
{
public ViewResult Index(string name)
{
return null;
}
}
}
Don't forget the using Microsoft.AspNetCore.Mvc;
statement.
More errors on GreetControllerTests
The type or namespace name 'GreetController()' cannot be found
Intellisense suggests we add a using HelloMVC.Controllers;
statement.
Also,
The type or namespace 'ViewResult' cannot be found
We are also missing the using Microsoft.AspNetCore.Mvc;
statement.
Now let's run our test...
It fails for the right reason
Add just enough code to make it pass...
Make sure GreetController is inheriting from the Controller class, and add:
public class GreetController : Controller
{
public ViewResult Index(string name)
{
return View();
}
}
The Missing Link
Our controller has the name value. But we want to pass the name to the view, so we can say "Hello {username}!" on our web page.
How do we get that piece of data from the Controller, and into the View?
That's what the M in MVC is for...
Models let us move data from a Controller into a View.
Creating a Model
Add a "Models" folder to your "HelloMVC" project, then a "GreetModel" class inside your Models folder.
Add a Name property to it.
namespace HelloMVC.Models
{
public class GreetModel
{
public string Name { get; set; }
}
}
The Controller passes the Model to the View
Let's write some unit tests that confirm...
-
The GreetController passes a GreetModel to the View.
-
The GreetModel passed to the View has the correct name property.
GreetController passes Model to the View
Add the following test to your GreetControllerTests().
Don't forget to add the using HelloMVC.Models;
statement.
[Fact]
public void Index_Passes_GreetModel_To_View()
{
var controller = new GreetController();
var result = controller.Index("ThisIsAString");
Assert.IsType<GreetModel>(result.Model);
}
Run the test and it should fail. The model that we return from our GreetController Index() action does not match the GreetModel type.
GreetController passes Model to the View
Let's add some code to our Index() action to make the test pass.
Don't forget to add a using HelloMVC.Models;
using statement.
public class GreetController : Controller
{
public ViewResult Index(string name)
{
var model = new GreetModel();
return View(model);
}
}
Now run the test again and it should pass!
GreetController Sets Name on the Model
Add the following test method to your GreetControllerTests().
[Fact]
public void Index_Sets_Name_On_Model()
{
var expectedName = "ExampleString";
var controller = new GreetController();
var result = controller.Index(expectedName);
var model = (GreetModel)result.Model;
Assert.Equal(expectedName, model.Name);
}
Run the test and it should fail. The Name property of the model we return from our GreetController Index() action does not equal "ExampleString".
GreetController Sets Name on the Model
Let's add some more code to our Index() action to make the test pass. Let's set the Name property of our Model to the name value that was passed into the Index() action.
public class GreetController : Controller
{
public ViewResult Index(string name)
{
var model = new GreetModel();
model.Name = name;
return View(model);
}
}
Now run the test again and it should pass!
Does it work?
Hit F5 to find out!
Enter a name into the field and click "Greet".
Uh oh...It looks like we don't have a /Greet/Index view. Stop the code from running and let's fix it.
Creating the View
Remember that views are just .cshtml files that live inside the Views folder.
By convention, each Controller has a folder in there.
File names match the method name, with the .cshtml file extension.
In the Index() action of your GreetController, right click on your call to the View method and select Add View.
Add View Dialog
Using our Model in the View
We already know the Controller passes the name to the View.
But, how do we use the Model inside the View?
Tell the View the Type of our Model
Add this line of razor code to the top of your Greet Index view. It is a special command that let's our view know to use the GreetModel found in our Models folder.
Now let's use the model
Add a <div>
element that prints the Name property of our model to the page.
<body>
<div>Hello, @Model.Name</div>
</body>
What's with the @s?
That's called Razor Syntax, and we'll learn more about it later.
For now, you should know that we use them to write C# code inside of HTML.
Did it Run???
Hit F5 to find out!
Enter a name into the field and click "Greet".
Yes!...Our greeting has appeared on the page!
Commit your Success!
Time to commit.
git add .
git commit -m "Added GreetController and its Index view"
Recap
- We built an MVC application from scratch.
- We test drove the creation of the controllers.
- We used a Form to pass a piece of data from one page to another.
- We created a controller that passes a model into the View.