Почти в каждом веб сайте есть общая часть дизайна, которую можно поместить в master page страницу. Все складывается хорошо, если этой частью является статический контент(шапка сайта с рисунком, меню и т.д.), но иногда надо показывать и динамические данные, например, список категорий или свежие новости сайта.
В asp.net webforms все очень просто: открываем файл с codebehind-ом и начинаем писать функционал для получения данных из бд. В asp.net mvc так сделать нельзя - там нет файла с c# кодом :).
Есть несколько способов реализации данной задачи:
1). Первый и самый плохой - написать helper метод, который будет вызываться прямо из Site.master.
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<html>
<body>
<div class="page">
<div id="header">
<div id="title">
<h1>My MVC Application</h1>
</div>
<div id="categories">
<%: MyHelper.RenderCategories() %>
</div>
<div id="menucontainer">
<ul id="menu">
<li><%: Html.ActionLink("Home", "Index", "Home")%></li>
<li><%: Html.ActionLink("About", "About", "Home")%></li>
</ul>
</div>
</div>
<div id="main">
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
<div id="footer">
</div>
</div>
</div>
</body>
</html>
* This source code was highlighted with Source Code Highlighter.
<%: MyHelper.RenderCategories() %> - этот кусочек кода должен получить данные из бд и вернуть строку с разметкой. Этот вариант не подходит, поскольку он нарушает всю концепцию mvc - файлы представлений(view) не должны содержать в себе бизнес логики.
2). Второй способ - в каждом Action методе делать выборку и вставлять в коллекцию ViewData.
Для этого требуется изменить код контролера таким образом:
public class HomeController : Controller
{
public const string CATEGORIES_KEY = "categories";
public CategoriesRepository _categoryRepository;
public HomeController()
{
_categoryRepository = new CategoriesRepository();
}
public ActionResult Index()
{
ViewData[CATEGORIES_KEY] = _categoryRepository.GetMainCategories();
return View();
}
public ActionResult About()
{
ViewData[CATEGORIES_KEY] = _categoryRepository.GetMainCategories();
return View();
}
}
* This source code was highlighted with Source Code Highlighter.
Также надо сделать функционал, который будет показывать разметку в Site.master:
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
<link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="page">
<div id="header">
<div id="title">
<h1>My MVC Application</h1>
</div>
<div id="categories">
<% var categories = ViewData["categories"] as IEnumerable<Category>; %>
<ul>
<% foreach (var category in categories)
{ %>
<li><%= category.Name %></li>
<% } %>
</ul>
</div>
<div id="menucontainer">
<ul id="menu">
<li><%: Html.ActionLink("Home", "Index", "Home")%></li>
<li><%: Html.ActionLink("About", "About", "Home")%></li>
</ul>
</div>
</div>
<div id="main">
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
<div id="footer">
</div>
</div>
</div>
</body>
</html>
* This source code was highlighted with Source Code Highlighter.
Это решение лучше первого варианта, т.к. не нарушает идею MVC, но здесь отчетливо видно, что код дублируется в разных методах.
3). Создать базовый класс BaseController для нашего HomeController.
Основная идея этого подхода - это создание базового класса для HomeController и реализации в нем логики выборки данных из базы данных.
Site.master остается без изменений.
public class BaseController : Controller
{
public BaseController()
{
CategoriesRepository repository = new CategoriesRepository();
ViewData["categories"] = repository.GetMainCategories();
}
}
public class HomeController : BaseController
{
public ActionResult Index()
{
return View();
}
public ActionResult About()
{
return View();
}
}
* This source code was highlighted with Source Code Highlighter.
Теперь код HomeController выглядит красивее и соблюдает принцип DRY.
4). Создать фильтр, который будет заполнять коллекцию viewdata нужными данными
В таком случае HomeController будет выглядеть вот так:
[CategoriesFilter]
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult About()
{
return View();
}
}
* This source code was highlighted with Source Code Highlighter.
А код атрибута:
public class CategoriesFilterAttribute : ActionFilterAttribute
{
CategoriesRepository _repository;
public CategoriesFilterAttribute()
{
_repository = new CategoriesRepository();
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.Controller.ViewData["categories"] = _repository.GetMainCategories();
}
}
* This source code was highlighted with Source Code Highlighter.
5). Еще один, наверное, самый распространенный метод - использование метода RenderAction
Вкратце: метод RenderAction - вызывает метод контролера.
В HomeController добавится еще один метод:
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult About()
{
return View();
}
[ChildActionOnly]
public ActionResult GetCategories()
{
CategoriesRepository repository = new CategoriesRepository();
var categories = repository.GetMainCategories();
return View(categories);
}
}
* This source code was highlighted with Source Code Highlighter.
Вьюшка, которая будет показывать разметку для категорий выглядит след. образом:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<Category>>" %>
<ul>
<% foreach (var category in Model)
{ %>
<li><%= category.Name %></li>
<% } %>
</ul>
* This source code was highlighted with Source Code Highlighter.
Вывод:
Мне больше всего нравится метод с фильтром по нескольким причинам:
1). Метод с базовым классом не гибкий: запрос в базу данных будет происходить при вызове любого метода контролера, даже если он не будет использовать эти данные. В случае с фильтром, можно просто пометить те методы, которые будут использовать данные из бд.
2). Последний способ(RenderAction) - один из наиболее популярных. При его вызове создается новый экземпляр контролера и вызывается нужный action метод - это занимает некоторое время(тем более если вы используете иньекции в контролеры). Фильтр же позволяет избежать создание экземпляра класса контролера.