Dependency injection into views
By Steve Smith
ASP.NET Core supports (xref:)dependency injection into views. This can be useful for view-specific services, such as localization or data required only for populating view elements. You should try to maintain separation of concerns between your controllers and views. Most of the data your views display should be passed in from the controller.
View or download sample code ((xref:)how to download)
A Simple Example
You can inject a service into a view using the @inject
directive. You can think of @inject
as adding a property to your view, and populating the property using DI.
The syntax for @inject
: @inject <type> <name>
An example of @inject
in action:
[!code-csharpMain]
1: @using System.Threading.Tasks
2: @using ViewInjectSample.Model
3: @using ViewInjectSample.Model.Services
4: @model IEnumerable<ToDoItem>
5: @inject StatisticsService StatsService
6: <!DOCTYPE html>
7: <html>
8: <head>
9: <title>To Do Items</title>
10: </head>
11: <body>
12: <div>
13: <h1>To Do Items</h1>
14: <ul>
15: <li>Total Items: @StatsService.GetCount()</li>
16: <li>Completed: @StatsService.GetCompletedCount()</li>
17: <li>Avg. Priority: @StatsService.GetAveragePriority()</li>
18: </ul>
19: <table>
20: <tr>
21: <th>Name</th>
22: <th>Priority</th>
23: <th>Is Done?</th>
24: </tr>
25: @foreach (var item in Model)
26: {
27: <tr>
28: <td>@item.Name</td>
29: <td>@item.Priority</td>
30: <td>@item.IsDone</td>
31: </tr>
32: }
33: </table>
34: </div>
35: </body>
36: </html>
This view displays a list of ToDoItem
instances, along with a summary showing overall statistics. The summary is populated from the injected StatisticsService
. This service is registered for dependency injection in ConfigureServices
in Startup.cs:
[!code-csharpMain]
1: using System.IO;
2: using Microsoft.AspNetCore.Builder;
3: using Microsoft.AspNetCore.Hosting;
4: using Microsoft.Extensions.DependencyInjection;
5: using ViewInjectSample.Helpers;
6: using ViewInjectSample.Infrastructure;
7: using ViewInjectSample.Interfaces;
8: using ViewInjectSample.Model.Services;
9:
10: namespace ViewInjectSample
11: {
12: public class Startup
13: {
14: // This method gets called by the runtime. Use this method to add services to the container.
15: // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
16: public void ConfigureServices(IServiceCollection services)
17: {
18: services.AddMvc();
19:
20: services.AddTransient<IToDoItemRepository, ToDoItemRepository>();
21: services.AddTransient<StatisticsService>();
22: services.AddTransient<ProfileOptionsService>();
23: services.AddTransient<MyHtmlHelper>();
24: }
25:
26: // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
27: public void Configure(IApplicationBuilder app)
28: {
29: app.UseMvc();
30: }
31:
32: // Entry point for the application.
33: public static void Main(string[] args)
34: {
35: var host = new WebHostBuilder()
36: .UseKestrel()
37: .UseContentRoot(Directory.GetCurrentDirectory())
38: .UseIISIntegration()
39: .UseStartup<Startup>()
40: .Build();
41:
42: host.Run();
43: }
44: }
45: }
The StatisticsService
performs some calculations on the set of ToDoItem
instances, which it accesses via a repository:
[!code-csharpMain]
1: using System.Linq;
2: using ViewInjectSample.Interfaces;
3:
4: namespace ViewInjectSample.Model.Services
5: {
6: public class StatisticsService
7: {
8: private readonly IToDoItemRepository _toDoItemRepository;
9:
10: public StatisticsService(IToDoItemRepository toDoItemRepository)
11: {
12: _toDoItemRepository = toDoItemRepository;
13: }
14:
15: public int GetCount()
16: {
17: return _toDoItemRepository.List().Count();
18: }
19:
20: public int GetCompletedCount()
21: {
22: return _toDoItemRepository.List().Count(x => x.IsDone);
23: }
24:
25: public double GetAveragePriority()
26: {
27: if (_toDoItemRepository.List().Count() == 0)
28: {
29: return 0.0;
30: }
31:
32: return _toDoItemRepository.List().Average(x => x.Priority);
33: }
34: }
35: }
The sample repository uses an in-memory collection. The implementation shown above (which operates on all of the data in memory) is not recommended for large, remotely accessed data sets.
The sample displays data from the model bound to the view and the service injected into the view:
Populating Lookup Data
View injection can be useful to populate options in UI elements, such as dropdown lists. Consider a user profile form that includes options for specifying gender, state, and other preferences. Rendering such a form using a standard MVC approach would require the controller to request data access services for each of these sets of options, and then populate a model or ViewBag
with each set of options to be bound.
An alternative approach injects services directly into the view to obtain the options. This minimizes the amount of code required by the controller, moving this view element construction logic into the view itself. The controller action to display a profile editing form only needs to pass the form the profile instance:
[!code-csharpMain]
1: using Microsoft.AspNetCore.Mvc;
2: using ViewInjectSample.Model;
3:
4: namespace ViewInjectSample.Controllers
5: {
6: public class ProfileController : Controller
7: {
8: [Route("Profile")]
9: public IActionResult Index()
10: {
11: // TODO: look up profile based on logged-in user
12: var profile = new Profile()
13: {
14: Name = "Steve",
15: FavColor = "Blue",
16: Gender = "Male",
17: State = new State("Ohio","OH")
18: };
19: return View(profile);
20: }
21: }
22: }
The HTML form used to update these preferences includes dropdown lists for three of the properties:
These lists are populated by a service that has been injected into the view:
[!code-csharpMain]
1: @using System.Threading.Tasks
2: @using ViewInjectSample.Model.Services
3: @model ViewInjectSample.Model.Profile
4: @inject ProfileOptionsService Options
5: <!DOCTYPE html>
6: <html>
7: <head>
8: <title>Update Profile</title>
9: </head>
10: <body>
11: <div>
12: <h1>Update Profile</h1>
13: Name: @Html.TextBoxFor(m => m.Name)
14: <br/>
15: Gender: @Html.DropDownList("Gender",
16: Options.ListGenders().Select(g =>
17: new SelectListItem() { Text = g, Value = g }))
18: <br/>
19:
20: State: @Html.DropDownListFor(m => m.State.Code,
21: Options.ListStates().Select(s =>
22: new SelectListItem() { Text = s.Name, Value = s.Code}))
23: <br />
24:
25: Fav. Color: @Html.DropDownList("FavColor",
26: Options.ListColors().Select(c =>
27: new SelectListItem() { Text = c, Value = c }))
28: </div>
29: </body>
30: </html>
The ProfileOptionsService
is a UI-level service designed to provide just the data needed for this form:
[!code-csharpMain]
1: using System.Collections.Generic;
2:
3: namespace ViewInjectSample.Model.Services
4: {
5: public class ProfileOptionsService
6: {
7: public List<string> ListGenders()
8: {
9: // keeping this simple
10: return new List<string>() {"Female", "Male"};
11: }
12:
13: public List<State> ListStates()
14: {
15: // a few states from USA
16: return new List<State>()
17: {
18: new State("Alabama", "AL"),
19: new State("Alaska", "AK"),
20: new State("Ohio", "OH")
21: };
22: }
23:
24: public List<string> ListColors()
25: {
26: return new List<string>() { "Blue","Green","Red","Yellow" };
27: }
28: }
29: }
[!TIP] Don???t forget to register types you will request through dependency injection in the
ConfigureServices
method in Startup.cs.
Overriding Services
In addition to injecting new services, this technique can also be used to override previously injected services on a page. The figure below shows all of the fields available on the page used in the first example:
As you can see, the default fields include Html
, Component
, and Url
(as well as the StatsService
that we injected). If for instance you wanted to replace the default HTML Helpers with your own, you could easily do so using @inject
:
[!code-htmlMain]
1: @using System.Threading.Tasks
2: @using ViewInjectSample.Helpers
3: @inject MyHtmlHelper Html
4: <!DOCTYPE html>
5: <html>
6: <head>
7: <title>My Helper</title>
8: </head>
9: <body>
10: <div>
11: Test: @Html.Value
12: </div>
13: </body>
14: </html>
If you want to extend existing services, you can simply use this technique while inheriting from or wrapping the existing implementation with your own.
See Also
- Simon Timms Blog: Getting Lookup Data Into Your View
|