This topic shows how to create a view component in ASP.NET MVC 6 and how to inject a service into a view.
Add a
TODO
@foreach (var todo in Model)
{
@todo.Title
@Html.ActionLink("Details", "Details", "Todo", new { id = todo.Id }) |
@Html.ActionLink("Edit", "Edit", "Todo", new { id = todo.Id }) |
@Html.ActionLink("Delete", "Delete", "Todo", new { id = todo.Id })
}
}
@Html.ActionLink("Create New Todo", "Create", "Todo")
The following image shows the priority items:
TODO
@foreach (var todo in Model)
{
@todo.Title
@Html.ActionLink("Details", "Details", "Todo", new { id = todo.Id }) |
@Html.ActionLink("Edit", "Edit", "Todo", new { id = todo.Id }) |
@Html.ActionLink("Delete", "Delete", "Todo", new { id = todo.Id })
}
}
@Html.ActionLink("Create New Todo", "Create", "Todo")
- Create a new ASP.NET 5 starter project
- Add the Todo controller
- Install the K Version Manager (KVM)
- Run EF migrations
- Introducing view components (VCs)
- Adding a view component
- Injecting a service into a view
- Publish to Azure
Introducing view components
New to ASP.NET MVC 6, view components (VCs) are similar to partial views, but they are much more powerful. VCs include the same separation-of-concerns and testability benefits found between a controller and view. You can think of a VC as a mini-controller—it’s responsible for rendering a chunk rather than a whole response. You can use VCs to solve any problem that you feel is too complex with a partial, such as:
- Dynamic navigation menus
- Tag cloud (where it queries the database)
- Login panel
- Shopping cart
- Recently published articles
- Any other sidebar content on a typical blog
One use of a VC could be to create a login panel that would be displayed on every page with the following functionality:
- If the user is not logged in, a login panel is rendered.
- If the user is logged in, links to log out and manage his or her account are rendered.
- If the user is in the admin role, an admin panel is rendered.
You can also create a VC that gets and renders data depending on the user's claims. You can add this VC view to the layout page and have it get and render user-specific data throughout the whole application.
A VC consists of two parts, the class (typically derived from
ViewComponent
) and the Razor view which calls methods in the VC class. Like the new ASP.NET controllers, a VC can be a POCO, but most users will want to take advantage of the methods and properties available by deriving from ViewComponent
.
A view component class can be created by:
- Deriving from
ViewComponent
. - Decorating the class with the
[ViewComponent]
attribute, or deriving from a class with the[ViewComponent]
attribute. - Creating a class where the name ends with the suffix ViewComponent.
Like controllers, VCs must be public, non-nested, non-abstract classes.
Adding a view component class
- Create a new folder called ViewComponents. View component classes can be contained in any folder in the project.
- Create a new class file called PriorityListViewComponent.cs in the ViewComponents folder.
- Replace the contents of the PriorityListViewComponent.cs file with the following:
using System.Linq; using Microsoft.AspNet.Mvc; using TodoList.Models; namespace TodoList.ViewComponents { public class PriorityListViewComponent : ViewComponent { private readonly ApplicationDbContext db; public PriorityListViewComponent(ApplicationDbContext context) { db = context; } public IViewComponentResult Invoke(int maxPriority) { var items = db.TodoItems.Where(x => x.IsDone == false && x.Priority <= maxPriority); return View(items); } } }
- Because the class name
PriorityListViewComponent
ends with the suffix ViewComponent, the runtime will use the string"PriorityList"
when referencing the class component from a view. I'll explain that in more detail later. - The
[ViewComponent]
attribute can used to change the name used to reference a VC. For example, we could have named the classXYZ
, and applied theViewComponent
attribute:
The[ViewComponent(Name = "PriorityList")] public class XYZ : ViewComponent
[ViewComponent]
attribute above tells the view component selector to use the namePriorityList
when looking for the views associated with the component, and to use the string"PriorityList"
when referencing the class component from a view. I'll explain that in more detail later. - The component uses constructor injection to make the data context available, just as we did with the
Todo
controller. Invoke
exposes a method to be called from a view and itInvokeAsync
, is available. We'll seeInvokeAsync
and multiple arguments later in the tutorial. In the previous code, theInvoke
method returns the set ofToDoItems
that are not completed and have priority greater than or equal tomaxPriority
.
Adding a view component view
- Create a new folder called Components in under the Views\Todo folder. This folder must be namedComponents.
- Create a new folder called PriorityList in under the Views\Todo\Components folder. This folder name mustmatch the name of the view component class, or the name of the class minus the suffix (if we followed convention and used the
ViewComponent
suffix in the class name). If you used the theViewComponent
attribute, the class name would need to match the attribute designation. - Create a Default.cshtml Razor view file in the Views\Todo\Components\PriorityList folder, and add the following markup:
@model IEnumerable
TodoItems
and displays them. If the VC invoke
method doesn't pass the name of the view (as in our sample), Default is used for the view name by convention. Later in the tutorial, I'll show you how to pass the name of the view.
div
containing a call to the priority list component to the bottom of the views\todo\index.cshtml file:@{ ViewBag.Title = "ToDo Page"; }class="jumbotron">ASP.NET vNext
class="row">
}
else
{
class="col-md-4">
@if (Model.Count == 0)
{
No Todo Items
class="col-md-4">
@Component.Invoke("PriorityList", 1)
The markup @await Component.InvokeAsync()
shows the syntax for calling view components. The first argument is the name of the component we want to invoke or call. Subsequent parameters are passed to the component. In this case, we are passing "1" as the priority we want to filter on. The InvokeAsync
method can take an arbitrary number of arguments.The following image shows the priority items:
Note: View Component views are more typically added to the Views\Shared folder, because VCs are typically not controller specific.
Add InvokeAsync to the priority component
Update the priority view component class with the following code:
using System.Linq; using Microsoft.AspNet.Mvc; using TodoList.Models; using System.Threading.Tasks; namespace TodoList.ViewComponents { public class PriorityListViewComponent : ViewComponent { private readonly ApplicationDbContext db; public PriorityListViewComponent(ApplicationDbContext context) { db = context; } // Synchronous Invoke removed. public async Task<IViewComponentResult> InvokeAsync(int maxPriority, bool isDone) { string MyView = "Default"; // If asking for all completed tasks, render with the "PVC" view. if (maxPriority > 3 && isDone == true) { MyView = "PVC"; } var items = await GetItemsAsync(maxPriority, isDone); return View(MyView, items); } private Task<IQueryable<TodoItem>> GetItemsAsync(int maxPriority, bool isDone) { return Task.FromResult(GetItems(maxPriority, isDone)); } private IQueryable<TodoItem> GetItems(int maxPriority, bool isDone) { var items = db.TodoItems.Where(x => x.IsDone == isDone && x.Priority <= maxPriority); string msg = "Priority <= " + maxPriority.ToString() + " && isDone == " + isDone.ToString(); ViewBag.PriorityMessage = msg; return items; } } }
Note: The synchronous
Invoke
method has been removed. A best practice is to use asynchronous methods when calling a database.
Update the VC Razor view to show the priority message :
@model IEnumerable@ViewBag.PriorityMessage
Finally, update the views\todo\index.cshtml view:
@* Markup removed for brevity. *@class="col-md-4"> @await Component.InvokeAsync("PriorityList", 2, true)
The following image reflects the changes we made to the priority VC and
Index
view:Specifying a view name
A complex VC might need to specify a non-default view under some conditions. The following shows how to specify the "PVC" view from the
InvokeAsync
method:public async Task<IViewComponentResult> InvokeAsync(int maxPriority, bool isDone) { string MyView = "Default"; // If asking for all completed tasks, render with the "PVC" view. if (maxPriority > 3 && isDone == true) { MyView = "PVC"; } var items = await GetItemsAsync(maxPriority, isDone); return View(MyView, items); }
Copy Views\Todo\Components\PriorityList\Default.cshtml to a Views\Todo\Components\PriorityList\PVC.cshtml view. I changed the PVC view to verify it's being used:
@model IEnumerablePVC Named Priority Component View
@ViewBag.PriorityMessage
Finally, update the Views\Todo\Index.cshtml:
@await Component.InvokeAsync("PriorityList", 4, true)
Refresh the page to see the PVC view.
When you're running the app with ^F5 (not in debug mode), you don't need to compile or run the app when you make changes to your controller (or any code files) as you previously had to with ASP.NET MVC. You only need to save the file and refresh the page.
Injecting a service into a view
ASP.NET MVC 6 now supports injection into a view from a class. Unlike a VC class, there are no restrictions other than the class must be must be public, non-nested and non-abstract. For this example, we'll create a simple class that exposes the total todo count, completed count and average priority.
- Create a folder called Services and add a class file called StatisticsService.cs (or you can copy existing item from the sample download).
- Update the
Index
view to inject the todo statistical data. Add the inject statement to the top of the file:@inject TodoList.Services.StatisticsService Statistics
Add markup calling theStatisticsService
to the end of the file:
The
StatisticsService
class:using System.Linq; using System.Threading.Tasks; using TodoList.Models; namespace TodoList.Services { public class StatisticsService { private readonly ApplicationDbContext db; public StatisticsService(ApplicationDbContext context) { db = context; } public async TaskGetCount() { return await Task.FromResult(db.TodoItems.Count()); } public async Task GetCompletedCount() { return await Task.FromResult( db.TodoItems.Count(x => x.IsDone == true)); } public async Task GetAveragePriority() { return await Task.FromResult( db.TodoItems.Average(x => (double?)x.Priority) ?? 0.0); } } }
class="col-md-4">
@await Component.InvokeAsync("PriorityList", 4, true)
Stats
The completed file is shown below:
@inject TodoList.Services.StatisticsService Statistics @{ ViewBag.Title = "Home Page"; }class="jumbotron">ASP.NET vNext
class="row">
}
else
{
class="col-md-4">
@if (Model.Count == 0)
{
No Todo Items
class="col-md-4">
@await Component.InvokeAsync("PriorityList", 4, true)
Stats
Register the
StatisticsService
class in the Startup.cs file:// This method gets called by the runtime. public void ConfigureServices(IServiceCollection services) { // Add EF services to the services container. services.AddEntityFramework(Configuration) .AddSqlServer() .AddDbContext<ApplicationDbContext>(); // Add Identity services to the services container. services.AddDefaultIdentity<ApplicationDbContext, ApplicationUser, IdentityRole>(Configuration); // Add MVC services to the services container. services.AddMvc(); services.AddTransient<TodoList.Services.StatisticsService>(); }
The statistics are displayed: