:::: MENU ::::

Wednesday, November 26, 2014

This topic shows how to create a view component in ASP.NET MVC 6 and how to inject a service into a view.

  • 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

  1. Create a new folder called ViewComponents. View component classes can be contained in any folder in the project.
  2. Create a new class file called PriorityListViewComponent.cs in the ViewComponents folder.
  3. 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);
        }
      }
    }
Notes on the code:
  • 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 class XYZ,  and  applied the  ViewComponent attribute:
    [ViewComponent(Name = "PriorityList")]
    public class XYZ : ViewComponent
    The [ViewComponent] attribute above tells the view component selector to use the name PriorityListwhen 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 Todocontroller.
  • Invoke exposes a method to be called from a view and it can take an arbitrary number of arguments. An asynchronous version, InvokeAsync, is available. We'll see InvokeAsync and multiple arguments later in the tutorial. In the previous code, the Invoke method  returns the set of ToDoItems that are not completed and have priority greater than or equal to  maxPriority.

Adding a view component view

  1. Create a new folder called Components in under the Views\Todo folder. This folder must be namedComponents.
  2. 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 the  ViewComponentattribute, the class name would need to match the attribute designation. 
  3. Create a Default.cshtml Razor view file in the Views\Todo\Components\PriorityList  folder, and add the following markup:
    @model IEnumerable
    
    

    Priority Items
    @foreach (var todo in Model) {
  • @todo.Title } The Razor view takes a list of 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.
  • Add a 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">
    class="col-md-4"> @if (Model.Count == 0) {

    No Todo Items
    } else {
    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")
    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
      @foreach (var todo in Model) {
    • @todo.Title }
      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 IEnumerable
      
      

      PVC Named Priority Component View

      @ViewBag.PriorityMessage
        @foreach (var todo in Model) {
      • @todo.Title }
        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.
        1. Create a folder called Services and add a class file called StatisticsService.cs (or you can copy existing item from the sample download).
        2. 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 Task GetCount()
              {
                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);
              }
            }
          }
        3. 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 the StatisticsService to the end of the file:
          @Html.ActionLink("Create New Todo", "Create", "Todo")
        class="col-md-4"> @await Component.InvokeAsync("PriorityList", 4, true)

        Stats
        • Items: @await Statistics.GetCount()
        • Completed:@await Statistics.GetCompletedCount()
        • Average Priority:@await Statistics.GetAveragePriority()
          The completed file is shown below:
          @inject TodoList.Services.StatisticsService Statistics
          @{
              ViewBag.Title = "Home Page";
          }
          
          
          class="jumbotron">

          ASP.NET vNext
          class="row">
          class="col-md-4"> @if (Model.Count == 0) {

          No Todo Items
          } else {
          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")
          class="col-md-4"> @await Component.InvokeAsync("PriorityList", 4, true)

          Stats
          • Items: @await Statistics.GetCount()
          • Completed:@await Statistics.GetCompletedCount()
          • Average Priority:@await Statistics.GetAveragePriority()
          • 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: