#Simplifying MVC for Page-focused Scenarios
We are trying to:
We are not trying to:
Big differences here are the structure of the project and file names, essentially making the controller a code-behind of the view, and using a different controller base class, ViewController
, that allows us to default some different behavior:
ViewController
are automatically routed based on the controller name, as if [Route("[controller]")]
were used. Can be overridden by adding the attribute specifically.Project Structure
Data/
AppDbContext.cs
Customer.cs
_Layout.cshtml
Customers.cshtml
Customers.cshtml.cs
Program.cs
project.json
Startup.cs
Customers.cshtml
@model Customers.ViewModel
@{
Layout = "_Layout.cshtml";
// We need to find a nicer way to pass data to the layout page
ViewData["Title"] = Model.Title;
}
<form method="post" role="form">
@if (Model.ShowAlertMessage)
{
<div class="alert alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
@Model.AlertMessage
</div>
}
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Customer.FirstName"></label>
<input asp-for="Customer.FirstName" class="form-control" />
<span asp-validation-for="Customer.FirstName" class="help-block text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Customer.LastName"></label>
<input asp-for="Customer.LastName" class="form-control" />
<span asp-validation-for="Customer.LastName" class="help-block text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Customer.BirthDate"></label>
<input asp-for="Customer.BirthDate" class="form-control" />
<span asp-validation-for="Customer.BirthDate" class="help-block text-danger"></span>
</div>
@if (Model.ShowAdultStuff)
{
<div class="form-group">
<label asp-for="Customer.FavoriteDrink"></label>
<input asp-for="Customer.FavoriteDrink" class="form-control" />
<span asp-validation-for="Customer.FavoriteDrink" class="help-block text-danger"></span>
</div>
}
<button type="submit" class="btn btn-primary">Save</button>
</form>
@section "Scripts" {
<partial name="_ValidationScripts" />
}
Customers.cshtml.cs
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MyApp.Data;
namespace MyApp
{
public class Customers : ViewController
{
public Customers(AppDbContext db)
{
Db = db;
}
public AppDbContext Db { get; }
[HttpGet]
public IActionResult Get(int? id)
{
var customer = id.HasValue ? await Db.Customers.SingleOrDefaultAsync(c => c.Id == id) : (Customer)null;
return id.HasValue && customer == null ?
NotFound() :
View(new ViewModel {
Customer = customer,
Title = customer != null ? $"Edit Customer {customer?.Id}" : "New Customer"
});
}
[HttpPost]
public IActionResult Post(ViewModel model)
{
if (!ModelState.IsValid())
{
// Model errors, just return the view to show them the errors
model.Title = "Edit Customer";
return View(model);
}
if (!model.Customer.Id.HasValue)
{
// Create
Db.Customers.Add(model.Customer);
ViewModel.AlertMessage = $"New customer {model.Customer.Id} created successfully!";
}
else
{
// Update
Db.Attach(model.Customer, EntityState.Changed);
ViewModel.AlertMessage = $"Customer {model.Customer.Id} updated successfully!";
}
await Db.SaveChangesAsync();
// TODO: Pfft, errors!? There's no errors :/
// The following line is really verbose, would love to find a way to codify reloading the page in the classic
// Post -> Redirect -> Get pattern, with type safety based on known action methods on this controller for bonus
// points, e.g. Reload(model.Customer.Id) or Redirect(() => Get(model.Customer.Id));
return RedirectToAction(nameof(Get), new { id = model.Customer.Id });
}
public class ViewModel
{
public string Title { get; set; }
// Would love to have a way to auto-back some properties with TempData via attribute so you don't
// have to do the code here I have.
// I think this is the type of the thing the Roslyn team is envisaging code-generators would be
// be used for but not sure how we'd do it, e.g.
// [TempData]
public string AlertMessage {
get { return TempData[nameof(AlertMessage)]; }
set { TempData[nameof(AlertMessage)] = value; }
}
public bool ShowAlertMessage => !string.IsNullOrEmpty(AlertMessage);
public Customer Customer { get; set; }
public bool ShowAdultOnlyThings => (DateTimeOffset.UtcNow - Customer.BirthDate).TotalYears >= 21;
}
}
}
This differs from the above example by having the view and controller in a single CSHTML file. This has the advantage of reducing the duplication of some of the C# boilerplate usually needed in the standalone CS file, e.g. the namespace
is generated, the using
statements can be gathered into the _ViewImports.cshtml
, etc. We could potentially explore the generated designer/partial file idea too to help make some of the code a little more terse/focused, e.g. the Post -> Redirect -> Get.
This is achieved by:
ViewController
implement IView
and IController
ViewController
@functions
block in the view to add the action methods and view modelProject Structure
Data/
AppDbContext.cs
Customer.cs
_Layout.cshtml
Customers.cshtml
Program.cs
project.json
Startup.cs
Customers.cshtml
@using MyApp.Data
@inherits ViewController
@model ViewModel
@inject AppDbContext Db
@functions {
[HttpGet]
public IActionResult Get(int? id)
{
var customer = id.HasValue ? await Db.Customers.SingleOrDefaultAsync(c => c.Id == id) : (Customer)null;
return id.HasValue && customer == null ?
NotFound() :
View(new ViewModel {
Customer = customer,
Title = customer != null ? $"Edit Customer {customer?.Id}" : "New Customer"
});
}
[HttpPost]
public IActionResult Post(ViewModel model)
{
if (!ModelState.IsValid())
{
// Model errors, just return the view to show them the errors
model.Title = "Edit Customer";
return View(model);
}
if (!model.Customer.Id.HasValue)
{
// Create
Db.Customers.Add(model.Customer);
ViewModel.AlertMessage = $"New customer {model.Customer.Id} created successfully!";
}
else
{
// Update
Db.Attach(model.Customer, EntityState.Changed);
ViewModel.AlertMessage = $"Customer {model.Customer.Id} updated successfully!";
}
await Db.SaveChangesAsync();
// TODO: Pfft, errors!? There's no errors :/
// The following line is really verbose, would love to find a way to codify reloading the page in the classic
// Post -> Redirect -> Get pattern, with type safety based on known action methods on this controller for bonus
// points, e.g. Reload(model.Customer.Id) or Redirect(() => Get(model.Customer.Id));
return RedirectToAction(nameof(Get), new { id = model.Customer.Id });
}
public class ViewModel
{
public string Title { get; set; }
// Would love to have a way to auto-back some properties with TempData via attribute so you don't
// have to do the code here I have.
// I think this is the type of the thing the Roslyn team is envisaging code-generators would be
// be used for but not sure how we'd do it, e.g.
// [TempData]
public string AlertMessage {
get { return TempData[nameof(AlertMessage)]; }
set { TempData[nameof(AlertMessage)] = value; }
}
public bool ShowAlertMessage => !string.IsNullOrEmpty(AlertMessage);
public Customer Customer { get; set; }
public bool ShowAdultOnlyThings => (DateTimeOffset.UtcNow - Customer.BirthDate).TotalYears >= 21;
}
}
@{
Layout = "_Layout.cshtml";
// We need to find a nicer way to pass data to the layout page
ViewData["Title"] = Model.Title;
}
<form method="post" role="form">
@if (Model.ShowAlertMessage)
{
<div class="alert alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
@Model.AlertMessage
</div>
}
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Customer.FirstName"></label>
<input asp-for="Customer.FirstName" class="form-control" />
<span asp-validation-for="Customer.FirstName" class="help-block text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Customer.LastName"></label>
<input asp-for="Customer.LastName" class="form-control" />
<span asp-validation-for="Customer.LastName" class="help-block text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Customer.BirthDate"></label>
<input asp-for="Customer.BirthDate" class="form-control" />
<span asp-validation-for="Customer.BirthDate" class="help-block text-danger"></span>
</div>
@if (Model.ShowAdultStuff)
{
<div class="form-group">
<label asp-for="Customer.FavoriteDrink"></label>
<input asp-for="Customer.FavoriteDrink" class="form-control" />
<span asp-validation-for="Customer.FavoriteDrink" class="help-block text-danger"></span>
</div>
}
<button type="submit" class="btn btn-primary">Save</button>
</form>
@section "Scripts" {
<partial name="_ValidationScripts" />
}