Цель урока. Научиться делать вывод данных в html, использование Razor. Helperы. PageableData. Динамические формы. RedirectToLogin, RedirectToNotFoundPage. Страница ошибки. RssActionResult.
Основа
Итак, рассмотрим как устроена часть View.
В контроллере все action-методы возвращают тип ActionResult. И для вывода результата мы используем:
return View(modelData);
Основными параметрами View может быть:
- Имя, обычно оно совпадает с именем action-метода. В случае если надо вызвать иной по имени View, то используется конструкция
return View(“ViewName”, modelData).
- Данные для отображения во View. Необязательный параметр. При передаче во View этот объект данных будет обозначаться Model. Для связывания типа данных во View указывается ожидаемый тип данных:
@model LessonProject.Model.User
- Layout. Необязательный параметр. При указании этого параметра по данной строке найдется страница-контейнер и вызовется. View-часть будет обработана методом RenderBody()
Выбор, какой же View использовать происходит следующим образом:
- Ищется в папке /Areas/[Area]/Views/[ControllerName]/
- Ищется в папке /Areas/[Area]/Views/Shared/
- Ищется в папке /Views/[ControllerName]/
- Ищется в папке /Views/Shared/
приступим к изучению.
Razor
При создании View есть выбор между двумя движками: ASPX и Razor. Первый мы не будем использовать в дальнейшем, поэтому поговорим о Razor.
ASPX был громозким движком с тегами <% %>
для выполнения кода и <%: %>
для вывода данных.
Razor использует конструкцию @Model.Name
. Т.е. всё, что начинается с @
переводит в режим или исполнения кода, или вывода данных @foreach() {…}
, или @if() { … } else { … }
:
@if (Model.Any())
{
<p>Список</p>
}
@foreach (var role in Model)
{
<div class="item">
<span class="id">
@role.ID
</span>
<span class="name">
@role.Name
</span>
<span class="Code">
@role.Code
</span>
</div>
}
Внутри { } находятся теги – это маркер того, что это шаблон. Для простого выполнения кода внутри шаблона используем структуру @{ code }, для корректного вывода данных внутри атрибутов или текстом конструкция — @(string result):
@{
int i = 0;
}
@foreach (var role in Model)
{
<div class="item @(i % 2 == 0 ? "odd" : "")">
<span class="id">
@role.ID
</span>
<span class="name">
@role.Name
</span>
<span class="Code">
@role.Code
</span>
</div>
i++;
}
Чтобы вывести не теговый текст, нужно использовать псевдотеги <text></text>
:
@foreach (var role in Model)
{
@role.Name<text>, </text>
}
Для вывода html-текста – или должна возвращаться MvcHtmlString, или использовать конструкцию Html.Raw(html-string-value), иначе текст будет выведен с экранированием тегов.
PageableData
Рассмотрим постраничный вывод таблицы из БД. Проанализируем:
- Контроллер должен получить в параметрах значение страницы, которую мы будем выводить
- По умолчанию это будет первая страница
- При выводе, мы должны знать:
- Список элементов БД, которые выводим
- Количество страниц
- Текущую страницу
Создадим Generic-класс PageableData (/Models/Info/PageableData.cs):
public class PageableData<T> where T : class
{
protected static int ItemPerPageDefault = 20;
public IEnumerable<T> List { get; set; }
public int PageNo { get; set; }
public int CountPage { get; set; }
public int ItemPerPage { get; set; }
public PageableData(IQueryable<T> queryableSet, int page, int itemPerPage = 0)
{
if (itemPerPage == 0)
{
itemPerPage = ItemPerPageDefault;
}
ItemPerPage = itemPerPage;
PageNo = page;
var count = queryableSet.Count();
CountPage = (int)decimal.Remainder(count, itemPerPage) == 0 ? count / itemPerPage : count / itemPerPage + 1;
List = queryableSet.Skip((PageNo - 1) * itemPerPage).Take(itemPerPage);
}
}
По умолчанию количество выводимых значений на странице – 20, но мы можем изменить этот параметр в конструкторе. Передаем IQueryable и вычисляем кол-во страниц CountPage. Используя PageNo, выбираем страницу:
List = queryableSet.Skip((PageNo - 1) * itemPerPage).Take(itemPerPage);
В контроллере используем:
public class UserController : DefaultController
{
public ActionResult Index(int page = 1)
{
var data = new PageableData<User>(Repository.Users, page, 30);
return View(data);
}
…
Во View используем данный класс:
@model LessonProject.Models.Info.PageableData<LessonProject.Model.User>
@{
ViewBag.Title = "Users";
Layout = "~/Areas/Default/Views/Shared/_Layout.cshtml";
}
<h2>Users</h2>
<p>
@foreach (var user in Model.List)
{
<div class="item">
<span class="id">
@user.ID
</span>
<span class="email">
@user.Email
</span>
<span class="activateDate">
@user.AddedDate
</span>
</div>
}
</p>
Запускаем, проверяем (http://localhost/User)
Для продолжения, сгенерируем больше данных (просто ctrl-c, ctrl-v в таблице в Server Explorer)
Перейдем к созданию Helper’а пагинатора, который даст нам возможность пролистывать этот список.
Helper (PagerHelper)
Так как мы используем bootstrap, то и на базе него будем делать пагинатор. В коде он выглядит так:
<div class="pagination">
<ul>
<li><a href="#">Prev</a></li>
<li><a href="#">1</a></li>
<li><a href="#">2</a></li>
<li><a href="#">3</a></li>
<li><a href="#">4</a></li>
<li><a href="#">5</a></li>
<li><a href="#">Next</a></li>
</ul>
</div>
Нас интересует только внутренняя часть <ul></ul>
.
Helper создается как Extension для класса System.Web.Mvc.HtmlHelper. План таков:
- Вывести Prev (сделать активным если надо)
- Вывести ссылки на первые три страницы 1, 2, 3
- Вывести троеточие, если необходимо
- Вывести активной ссылку текущей страницы
- Вывести троеточие, если необходимо
- Вывести последние три страницы
- Вывести Next (сделать активной если надо)
- Заключить всё в ul и вывести как MvcHtmlString
Код будет выглядеть так:
public static MvcHtmlString PageLinks(this HtmlHelper html, int currentPage, int totalPages, Func<int, string> pageUrl)
{
StringBuilder builder = new StringBuilder();
//Prev
var prevBuilder = new TagBuilder("a");
prevBuilder.InnerHtml = "«";
if (currentPage == 1)
{
prevBuilder.MergeAttribute("href", "#");
builder.AppendLine("<li class="active">" + prevBuilder.ToString() + "</li>");
}
else
{
prevBuilder.MergeAttribute("href", pageUrl.Invoke(currentPage - 1));
builder.AppendLine("<li>" + prevBuilder.ToString() + "</li>");
}
//По порядку
for (int i = 1; i <= totalPages; i++)
{
//Условие что выводим только необходимые номера
if (((i <= 3) || (i > (totalPages - 3))) || ((i > (currentPage - 2)) && (i < (currentPage + 2))))
{
var subBuilder = new TagBuilder("a");
subBuilder.InnerHtml = i.ToString(CultureInfo.InvariantCulture);
if (i == currentPage)
{
subBuilder.MergeAttribute("href", "#");
builder.AppendLine("<li class="active">" + subBuilder.ToString() + "</li>");
}
else
{
subBuilder.MergeAttribute("href", pageUrl.Invoke(i));
builder.AppendLine("<li>" + subBuilder.ToString() + "</li>");
}
}
else if ((i == 4) && (currentPage > 5))
{
//Троеточие первое
builder.AppendLine("<li class="disabled"> <a href="#">...</a> </li>");
}
else if ((i == (totalPages - 3)) && (currentPage < (totalPages - 4)))
{
//Троеточие второе
builder.AppendLine("<li class="disabled"> <a href="#">...</a> </li>");
}
}
//Next
var nextBuilder = new TagBuilder("a");
nextBuilder.InnerHtml = "»";
if (currentPage == totalPages)
{
nextBuilder.MergeAttribute("href", "#");
builder.AppendLine("<li class="active">" + nextBuilder.ToString() + "</li>");
}
else
{
nextBuilder.MergeAttribute("href", pageUrl.Invoke(currentPage + 1));
builder.AppendLine("<li>" + nextBuilder.ToString() + "</li>");
}
return new MvcHtmlString("<ul>" + builder.ToString() + "</ul>");
}
Добавим namespace LessonProject.Helper в объявления во View. Это можно сделать двумя способами:
- В самом View
@using LessonProject.Helper;
- В Web.config (рекомендуется)
<configSections> … <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"> <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" /> <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" /> </sectionGroup> </configSections> + <system.web.webPages.razor> <pages pageBaseType="System.Web.Mvc.WebViewPage"> <namespaces> <add namespace="LessonProject.Helper" /> </namespaces> </pages> </system.web.webPages.razor>
Добавляем пагинатор во View:
<div class="pagination">
@Html.PageLinks(Model.PageNo, Model.CountPage, x => Url.Action("Index", new {page = x}))
</div>
Обратите внимание на конструкцию
x => Url.Action("Index", new {page = x})
Это делегат, который возвращает ссылку на страницу. А Url.Action() – формирует ссылку на страницу /User/Index с параметром page = x.
Вот что получилось (уменьшил количество вывода на странице до 5, чтобы образовалось больше страниц):
SearchEngine
Следующим шагом к просмотру данных будет создание поиска. Поиск будет простой, по совпадению подстроки в одном из полей данных. Входной параметр – searchString.
public ActionResult Index(int page = 1, string searchString = null)
{
if (!string.IsNullOrWhiteSpace(searchString))
{
//тут Поиск
return View(data);
}
else
{
var data = new PageableData<User>(Repository.Users, page, 5);
return View(data);
}
}
Создадим класс SearchEngine
, который принимает значения IQueryable<User>
, и строку поиска, а возвращает данные по поиску (/Global/SearchEngine.cs):
Первым делом, создадим классы по очистке строки запроса, никаких тегов и убираем разделители типа [,], {,}, (,):
/// <summary>
/// The regex strip html.
/// </summary>
private static readonly Regex RegexStripHtml = new Regex("<[^>]*>", RegexOptions.Compiled);
private static string StripHtml(string html)
{
return string.IsNullOrWhiteSpace(html) ? string.Empty :
RegexStripHtml.Replace(html, string.Empty).Trim();
}
private static string CleanContent(string content, bool removeHtml)
{
if (removeHtml)
{
content = StripHtml(content);
}
content =
content.Replace("\", string.Empty).
Replace("|", string.Empty).
Replace("(", string.Empty).
Replace(")", string.Empty).
Replace("[", string.Empty).
Replace("]", string.Empty).
Replace("*", string.Empty).
Replace("?", string.Empty).
Replace("}", string.Empty).
Replace("{", string.Empty).
Replace("^", string.Empty).
Replace("+", string.Empty);
var words = content.Split(new[] { ' ', 'n', 'r' }, StringSplitOptions.RemoveEmptyEntries);
var sb = new StringBuilder();
foreach (var word in
words.Select(t => t.ToLowerInvariant().Trim()).Where(word => word.Length > 1))
{
sb.AppendFormat("{0} ", word);
}
return sb.ToString();
}
Создаем поиск:
public static IEnumerable<User> Search(string searchString, IQueryable<User> source)
{
var term = CleanContent(searchString.ToLowerInvariant().Trim(), false);
var terms = term.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
var regex = string.Format(CultureInfo.InvariantCulture, "({0})", string.Join("|", terms));
foreach (var entry in source)
{
var rank = 0;
if (!string.IsNullOrWhiteSpace(entry.Email))
{
rank += Regex.Matches(entry.Email.ToLowerInvariant(), regex).Count;
}
if (rank > 0)
{
yield return entry;
}
}
}
В первой строке очищаем строку запроса. Создаем regex для поиска. В данном случае, мы ищем только в поле Email у пользователей.
Как это работает:
- При вводе слова в поиске, например, «cher [2]», вначале убираем разделители, получаем «cher 2».
- Создаем regex = (cher|2).
- Просматриваем весь список, переданный через
IQueryable<User>
- Если есть совпадение, то выносим его в IEnumerable — yield return entry
Изменяем Action (/Areas/Default/Controller/UserController.cs):
public ActionResult Index(int page = 1, string searchString = null)
{
ViewBag.Search = searchString;
if (!string.IsNullOrWhiteSpace(searchString))
{
var list = SearchEngine.Search(searchString, Repository.Users).AsQueryable();
var data = new PageableData<User>(list, page, 5);
return View(data);
}
else
{
var data = new PageableData<User>(Repository.Users, page, 5);
return View(data);
}
}
Добавляем форму поиска во View:
@{
ViewBag.Title = "Users";
Layout = "~/Areas/Default/Views/Shared/_Layout.cshtml";
var searchString = (string)ViewBag.Search;
}
<h2>Users</h2>
@using (Html.BeginForm("Index", "User", FormMethod.Post, new { @class = "form-search" }))
{
@Html.TextBox("searchString", searchString ?? "", new { @class = "input-medium search-query" })
<button type="submit" class="btn">Поиск</button>
}
Обратите внимание на ViewBag
, это dynamic контейнер, им можно пользоваться для передачи второстепенных данных.
Добавим в пагинатор строку поиска:
@Html.PageLinks(Model.PageNo, Model.CountPage, x => Url.Action("Index", new {page = x, searchString}))
Extension
Расширения для строк или целых чисел очень удобно использовать в проекте. Мы сделаем несколько реализаций, но вынесем это в отдельный проект. Добавим проект LessonProject.Tools class WebExtensions:
public static class WebExtensions
{
}
Пропишем в reference LessonProject.
Многострочные данные хранятся с разделителем rn, и при выводе в тексте эти разделители не учитываются. Необходимо создать функцию NlToBr(). Создаем:
public static MvcHtmlString NlToBr(this string source)
{
if (string.IsNullOrWhiteSpace(source))
{
return new MvcHtmlString(string.Empty);
}
return new MvcHtmlString(source.Replace(Environment.NewLine, "<br />"));
}
Добавляем объявление в Web.config:
<add namespace="LessonProject.Tools" />
Можно использовать:
@Model.Description.NlToBr()
Так же создадим расширение Teaser, которое урезает строку до максимального допустимого количества символов, и ставит «…» после, если строка оказалась длиннее.
public static string Teaser(this string content, int length, string more = "...")
{
if (string.IsNullOrWhiteSpace(content))
{
return string.Empty;
}
if (content.Length < length)
{
return content;
}
return content.Substring(0, length) + more;
}
Использовать можно:
@Model.Description.Teaser(120, “>>>”)
Следующее расширение относится к целым числам и подставляет одно из слов в определении для 1, 2 или 5. Например, 1 год, 2 года, 5 лет:
public static string CountWord(this int count, string first, string second, string five)
{
if (count % 10 == 1 && (int)(count / 10) != 1)
{
return first;
}
if (count % 10 > 1 && count % 10 < 5 && ((int)(count / 10) % 10) != 1)
{
return second;
}
return five;
}
Использовать можно так:
@year @year.CountWord(“год”, “года”, “лет”)
Динамические формы
Я уже писал статью на эту тему. Но то был asp.net mvc первый, а сейчас можно сделать все намного проще.
Значит, проблема заключается в следующем. Мы на post-action в контроллере принимаем объект, у которого, заранее не известно количество полей. Например, это будет следующая структура:
public class Ownership
{
public string Name { get; set; }
public int Price { get; set; }
}
public class Customer
{
public int ID { get; set; }
public string Name { get; set; }
public Dictionary<string,Ownership> Ownerships { get; set; }
}
Можно использовать и List вместо Dictionary, но, в будущем, будут проблемы с валидацией для определенного элемента.
Создадим контроллер:
public class CustomerController : DefaultController
{
[HttpGet]
public ActionResult Edit(int id)
{
return View(new Customer()
{
Ownerships = new Dictionary<string, Ownership>()
});
}
[HttpPost]
public ActionResult Edit(Customer customer)
{
if (ModelState.IsValid)
{
}
return View(customer);
}
}
В БД мы не будем ничего записывать, поэтому здесь просто заглушка. Добавляем View:
@model LessonProject.Models.Info.Customer
@{
ViewBag.Title = "Edit";
Layout = "~/Areas/Default/Views/Shared/_Layout.cshtml";
}
@section scripts {
@Scripts.Render("/Scripts/default/customer-edit.js")
}
<h2>Edit</h2>
@using (Html.BeginForm("Edit", "Customer", FormMethod.Post, new { @class = "form-horizontal" }))
{
<fieldset>
@Html.Hidden("ID", Model.ID)
<div class="control-group">
<label class="control-label" for="Email">
Name</label>
<div class="controls">
@Html.TextBox("Name", Model.Name, new { @class = "input-xlarge" })
@Html.ValidationMessage("Name")
</div>
</div>
<div id="OwnershipListWrapper">
<div class="btn" id="AddOwnership">Добавить</div>
@foreach (var keyValuePair in Model.Ownerships)
{
@Html.Partial("OwnershipItem", keyValuePair)
}
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
Ок</button>
</div>
</fieldset>
}
KeyValuePair
переносим в PartialView
(/Areas/Default/Views/Customer/OwnershipItem.cshtml):
@model KeyValuePair<string, LessonProject.Models.Info.Ownership>
<div class="OwnershipWrapper">
<div class="btn remove-line">Удалить</div>
<div class="control-group">
<label class="control-label">
Имя
</label>
<div class="controls">
@Html.TextBox("Ownerships[" + Model.Key + "].Name", Model.Value.Name, new { @class = "input-xlarge" })
@Html.ValidationMessage("Ownerships[" + Model.Key + "].Name")
</div>
</div>
<div class="control-group">
<label class="control-label">
Цена
</label>
<div class="controls">
@Html.TextBox("Ownerships[" + Model.Key + "].Price", Model.Value.Price, new { @class = "input-xlarge" })
@Html.ValidationMessage("Ownerships[" + Model.Key + "].Price")
</div>
</div>
</div>
Js-обработчик состоит из обработки кнопок добавления и удаления (/Scripts/default/customer-edit.js):
function CustomerEdit() {
_this = this;
this.ajaxAddOwnership = "/Customer/AddOwnership";
this.init = function () {
$("#AddOwnership").click(function () {
$.ajax({
type: "GET",
url: _this.ajaxAddOwnership,
success: function (data) {
$("#OwnershipListWrapper").append(data);
}
})
});
$(document).on("click", ".remove-line", function () {
$(this).closest(".OwnershipWrapper").remove();
});
}
}
var customerEdit = null;
$().ready(function () {
customerEdit = new CustomerEdit();
customerEdit.init();
});
При нажатии на кнопку «добавить», мы получаем по ajax-запросу часть уже сформированный и добавим к списку. При удалении по клику, просто удалим ряд значений, найдя ближайший OwnershipWrapper. Обратите внимание на создание глобального обработчика для remove-line. Это необходимо для того, чтобы динамически созданные кнопки тоже обрабатывали этот клик.
Добавим обработчик в CustomerController, используя уже созданный нами View OwnershipItem.cshtml:
public ActionResult AddOwnership()
{
return View("OwnershipItem", new KeyValuePair<string, Ownership>(
Guid.NewGuid().ToString("N"),
new Ownership()));
}
Правила перенаправления
Есть две ситуации, которые похожи, но по-разному обрабатываются. Речь идет о перенаправлении страницы на страницу входа и об ошибке 404 (страница не найдена).
Итак, когда мы пытаемся получить по прямой ссылке доступ к админке, но в данный момент мы не залогинены, или залогинены, но не имеем прав на это, нас переправит на страницу, указанную в Web.config в секции authentication:
<authentication mode="Forms">
<forms loginUrl="~/Login" timeout="2880" />
</authentication>
Если мы пытаемся открыть несуществующую страницу, т.е. по данным роутинга не найден контроллер и action-метод, то нас перенаправляют на страницу, которая указывается в секции customErrors в Web.config:
<customErrors mode="On" redirectMode="ResponseRedirect" defaultRedirect="~/Error">
<error statusCode="403" redirect="~/Error" />
<error statusCode="404" redirect="~/NotFoundPage" />
</customErrors>
То же происходит и при обработке ошибки. На боевом сервере не очень хорошо выдавать «желтый экран смерти», где указана причина возникновения ошибки.
Но иногда, перенаправление на страницу 404 или на страницу входа, мы должны сделать самостоятельно.
Эта ситуация может возникнуть, когда проверка роли выполнена, но пользователь берется за управление чужого ресурса, при обращении к которому, естественно, в доступе ему должно быть отказано.
Или второй, и более частый случай, когда по битой ссылке не находится ресурс, в данном случае, надо переправить на NotFoundPage.
Добавим свойства в BaseController (/Controllers/BaseController.cs):
protected static string ErrorPage = "~/Error";
protected static string NotFoundPage = "~/NotFoundPage";
protected static string LoginPage = "~/Login";
public RedirectResult RedirectToNotFoundPage
{
get
{
return Redirect(NotFoundPage);
}
}
public RedirectResult RedirectToLoginPage
{
get
{
return Redirect(LoginPage);
}
}
protected override void OnException(ExceptionContext filterContext)
{
base.OnException(filterContext);
filterContext.Result = Redirect(ErrorPage);
}
Теперь можно использовать свойства RedirectToNotFoundPage и RedirectToLoginPage UserController (/Areas/Default/Controllers/UserController.cs) так:
[Authorize]
public ActionResult Edit(int id)
{
var user = Repository.Users.FirstOrDefault(p => p.ID == id);
if (user != null)
{
if (CurrentUser.InRoles("admin") || CurrentUser.ID == id)
{
//Разрешено редактирование
return View(user);
}
return RedirectToLoginPage;
}
return RedirectToNotFoundPage;
}
Обратите внимание, изначально необходимо быть авторизованным в системе, но редактирование разрешено только самому пользователю, котого эти данные или пользователю с правами админа.
Добавим обработку ошибки и 404-страницу. Создадим контроллер, но не будем его наследовать от BaseController
, чтобы ненароком не зацепить еще какую-нибудь ошибку. Добавим пути маршрутов в обработку (/Areas/Default/DefaultAreaRegistration.cs):
context.MapRoute(
null,
url: "Error",
defaults: new { controller = "Error", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "LessonProject.Areas.Default.Controllers" }
);
context.MapRoute(
null,
url: "NotFoundPage",
defaults: new { controller = "Error", action = "NotFoundPage", id = UrlParameter.Optional },
namespaces: new[] { "LessonProject.Areas.Default.Controllers" }
);
Контроллер (/Areas/Default/Controllers/ErrorController.cs):
public class ErrorController : Controller
{
public ActionResult Index()
{
Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return View();
}
public ActionResult NotFoundPage()
{
Response.StatusCode = (int)HttpStatusCode.NotFound;
return View();
}
}
Добавляем простейшую страницу ошибки View (/Areas/Default/Views/Error/Index.cshtml):
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
<div>
Ошибка <br />
<a href="/">Вернуться на главную</a>
</div>
</body>
</html>
Аналогичная страница 404 (/Areas/Default/Views/Error/NotFoundPage.cshtml).
Другие ActionResult
ActionResult не обязательно может возвращать View. Рассмотрим другие примеры:
- Content(result) – возвращает простой текст в http-ответе
- JsonResult(object) – возвращает объект в формате json (далее рассмотрим)
- FileContentResult() – возвращает ресурс для скачивания Отправляет в ответ содержимое файла. (источник — мсдн)
- FilePathResult() – возвращает файл, расположенный по указанному пути
- FileStreamResult() – возвращает в файле записанный поток
- RedirectResult() – переправляет на указанную страницу
- RedirectToRouteResult() – переправляет на страницу по новому указанному пути
RssActionResult
RSS – это XML формат, предназначенный для описания лент новостей, статей, блогов. У нас пока нет постов, так что мы просто создадим RssActionResult. В System.ServiceModel.Syndication
– это набор инструментов для работы с RSS. Нам необходимы SyndicationFeed и SyndicationItem. Подключим System.ServiceModel в reference, создадим новый контроллер (/Areas/Default/Controllers/FeedController.cs):
public class FeedController : DefaultController
{
public ActionResult Index()
{
var host = Request.Url;
var feed =
new SyndicationFeed("Site RSS",
"",
new Uri(host.AbsoluteUri + "/Feed"));
var items = new List<SyndicationItem>();
var item = new SyndicationItem(
"Title",
"content",
new Uri("http://" + host + "/some-link-url"),
"Title",
DateTime.Now
);
items.Add(item);
feed.Items = items;
return View();
}
}
Абсолютно тестовые данные, одно значение на нерабочую ссылку. Создадим RssActionResult (/Global/RssActionResult.cs):
public class RssActionResult : ActionResult
{
public SyndicationFeed Feed { get; set; }
public override void ExecuteResult(ControllerContext context)
{
context.HttpContext.Response.ContentType = "application/rss+xml";
var rssFormatter = new Rss20FeedFormatter(Feed);
using (var writer = XmlWriter.Create(context.HttpContext.Response.Output))
{
rssFormatter.WriteTo(writer);
}
}
}
Основным параметром является Feed, куда передается уже сформированный Feed. Сам ActionResult вызывает ExecuteResult(). Мы используем Rss20FeedFormatted для записи xml в Response.Output. Response.Output – это stream, куда записывая данные обрабатываются браузером как ответ. Для того чтобы браузер понимал, какой именно тип данных мы передаем, мы присваиваем в Response.ContentType = “application/rss+xml”.
Возвращаем наш RssActionResult (/Areas/Default/Controllers/FeedController.cs):
…
return new RssActionResult {Feed = feed };
Запускаем:
Если установить RSS Subscription Extension (by Google), то наш RSS-канал отобразится в браузере, и мы сможем на него подписаться.:
Можете попробовать реализовать Sitemap вывод. Вот протокол http://www.sitemaps.org/protocol.html, описывающий формат.
Все исходники находятся по адресу https://bitbucket.org/chernikov/lessons
Автор: chernikov