Часть 18 – Пример создания провайдера метаданных

Как и было обещано, давайте посмотрим вариант применения собственной реализации провайдера метаданных Модели. Кроме того, создадим еще один класс Модели для новых демонстраций.

Расширяем возможности DataAnnotationsModelMetadataProvider

Данный вариант предназначен для добавления новых функций к реализации по умолчанию. Ведь для чего создавать с нуля код работы с атрибутами DataAnnotations, если его можно просто унаследовать. Не будем вносить изменения в проект, а просто рассмотрим сценарий решения подобной задачи.

Предположим, что уже существует атрибут [Hint], который предоставляет текст подсказки для выбранного компонента. Его значение содержится в свойстве Text. При этом изменять реализацию нет возможности, а значит нельзя добавить поддержку интерфейса IMetadataAware. Также будем считать, что код Представления ожидает получить текст подсказки в качестве элемента коллекции AdditionalValues.

Решение задачи очень простое – создадим новый класс AdvancedMetadataProvider, который будет наследником DataAnnotationsModelMetadataProvider. Затем добавим нужную логику после вызова метода базового класса. Реализация будет выглядеть следующим образом:

public class AdvancedMetadataProvider: DataAnnotationsModelMetadataProvider
{
    protected override ModelMetadata CreateMetadata(
        System.Collections.Generic.IEnumerable<Attribute> attributes,
        Type containerType,
        Func<object> modelAccessor,
        Type modelType,
        string propertyName)
    {
        var metadata = base.CreateMetadata(
            attributes, containerType, modelAccessor, modelType, propertyName);

        var hint = attributes.SingleOrDefault(a => typeof(HintAttribute) == a.GetType());
        if (hint != null) {
            metadata.AdditionalValues.Add("hint", ((HintAttribute)hint).Text);
        }
        return metadata;
    }
}

Установим созданного провайдера в качестве используемого:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    ModelMetadataProviders.Current = new AdvancedMetadataProvider();
}

Теперь, при создании метаданных Модели, в них будет добавляться текст из атрибута [Hint].

Площадка для новых экспериментов

Рассмотрим более интересные случаи. Предположим, что нет возможности изменить исходный код Модели чтобы добавить атрибуты. Или заранее не известно какие их них необходимы. Также может потребоваться реализация сценария, когда метаданные Модели должны иметь возможность настройки с помощью конфигурационного файла.

Во всех этих случаях задача решается созданием провайдера метаданных. Добавим Модель, а так же соответствующие ей Контроллер и Представления, которые потребуется для этого и ряда других примеров.

Добавляем Модель

Начнем с класса Модели, которая представляет данные о платеже пользователя и, по условию задачи, не будет иметь никаких атрибутов. Создадим в папке Models файл с классом PaymentModel:

namespace MVCDemo.Models
{
    using System;

    public class PaymentModel
    {
        public enum PaymentSource
        {
            Unknown = 0,
            Direct = 1
        }

        public Guid Id { get; set; }

        public string UserLogin { get; set; }

        public DateTime Date { get; set; }

        public double Amount { get; set; }

        public PaymentSource Source { get; set; }

        public string Info { get; set; }
    }
}

Смысл его свойств достаточно прост:

  • Id – уникальный идентификатор каждого платежа;
  • UserLogin – имя пользователя, связывающее его профиль и данный платеж;
  • Date – дата и время платежа;
  • Amount – сумма платежа в рублях;
  • PaymentSource – способ совершения платежа;
  • Info – дополнительная информация о платеже.

Как и в случае с профилями, в качестве хранилища будет выступать класс со статическим полем:

namespace MVCDemo.Models
{
    using System;
    using System.Collections.Generic;

    public class PaymentRepository
    {
        private static List<PaymentModel> _payments = new List<PaymentModel>();

        static PaymentRepository()
        {
            var userProfiles = new UserRepository();

            _payments.Add(new PaymentModel() {
                Id = Guid.NewGuid(),
                UserLogin = userProfiles.Profiles[0].Login,
                Date = DateTime.Now,
                Amount = 1000.0f,
                Source = PaymentModel.PaymentSource.Direct,
                Info = string.Empty
            });
        }

        public List<PaymentModel> Payments
        {
            get { return _payments; }
        }

        public void Add(
            PaymentModel newPayment,
            PaymentModel.PaymentSource paymentSource = PaymentModel.PaymentSource.Unknown)
        {
            if (newPayment == null) {
                throw new ArgumentNullException();
            }

            // Validate the payment source value
            if ((paymentSource == PaymentModel.PaymentSource.Unknown) &&
                (newPayment.Source == PaymentModel.PaymentSource.Unknown)) {
                    // TODO: Replace the exception with the logging code
                    throw new ArgumentException("Unknown payment source.");
            }

            // Validate the user login
            var userProfiles = new UserRepository();
            if (!userProfiles.IsLoginExists(newPayment.UserLogin)) {
                // TODO: Replace the exception with the logging code
                throw new ArgumentException(
                    string.Format("Invalid login: {0}", newPayment.UserLogin));
            }

            // Update and store the payment information
            newPayment.Id = Guid.NewGuid();
            if (paymentSource != PaymentModel.PaymentSource.Unknown) {
                newPayment.Source = paymentSource;
            }

            _payments.Add(newPayment);
        }
    }
}

Единственное существенное отличие заключается в проверке имени пользователя и источника платежа перед сохранением данных. Также в подобной задаче при передаче некорректных значений обязательна должна вестись запись в журнал, иначе данные платежа будут утеряны. Для упрощения в данном примере будем просто выбрасывать исключение.

Новый Контроллер

Следующий шаг – создание класса Контроллера. При помощи помощника, в папке Controllers создадим класс PaymentsController. Добавим только два Действия: Index и Add.

namespace MVCDemo.Controllers
{
    using System;
    using System.Web.Mvc;
    using MVCDemo.Models;

    public class PaymentsController : Controller
    {

        // GET: /Payments/
        public ActionResult Index()
        {
            this.ViewBag.Message = this.TempData["Message"];

            var paymentsRepository = new PaymentRepository();
            return View(paymentsRepository.Payments);
        }

        // GET: /Payments/Add
        public ActionResult Add()
        {
            return View(new PaymentModel());
        }

        // POST: /Payments/Add
        [HttpPost]
        public ActionResult Add(PaymentModel newPayment)
        {
            if (!this.ModelState.IsValid) {
                return this.View(newPayment);
            }

            var paymentsRepository = new PaymentRepository();
            paymentsRepository.Add(newPayment, PaymentModel.PaymentSource.Direct);

            this.TempData["Message"] = Resources.Controllers.PaymentsRes.PaymentAddedMessage;

            return this.RedirectToAction("Index");
        }
    }
}

Особых отличий от уже созданного Контроллера нет. Кроме того, как уже можно быть понять,  потребуется файлы ресурсов с текстами для Модели и Контроллера (не забудьте установить модификатор доступа в значение public):

Файл Resources\Model\PaymentRes.resx
Amount Amount Сумма (RUR)
Date Payment date Дата и время платежа
Id Payment id Уникальный идентификатор платежа
Info Additional info Дополнительная информация
Source Payment source Источник поступления
UserLogin User login Имя входа пользователя
Файл Resources\Controllers\PaymentsRes.resx
PaymentAddedMessage The payment information was added to the database. Сообщение о добавлении платежа в базу

Добавляем Представления

И, наконец, создадим Представления. Начнем с Index:

@model IEnumerable<MVCDemo.Models.PaymentModel>
@using MVCDemo.Resources.Views.Payments;
@using MVCDemo.Resources.Shared;
@using MVCDemo.Resources.Models;
@{
    ViewBag.Title = IndexRes.PageTitle;
}
<h2>@ViewBag.Title</h2>
<p>@ViewBag.Message</p>
<p>@Html.ActionLink(IndexRes.AddPaymentLink, "Add")</p>
<table>
    <tr>
        <th>@PaymentRes.UserLogin</th>
        <th>@PaymentRes.Date</th>
        <th>@PaymentRes.Amount</th>
        <th>@PaymentRes.Source</th>
        <th>@PaymentRes.Info</th>
    </tr>
    @foreach (var item in Model) {
        <tr>
            <td>@item.UserLogin</td>
            <td>@String.Format("{0:F}", item.Date)</td>
            <td>@String.Format("{0:F} {1}", item.Amount, MessagesRes.CurrencyCodeRUR)</td>
            <td>@item.Source</td>
            <td>@item.Info</td>
        </tr>
    }
</table>

Файлы ресурсов:

Файл Resources\View\Payments\IndexRes.resx
PageTitle Payments Заголовок страницы
AddPaymentLink Add payment Ссылка на страницу добавления платежа
Файл Resources\Shared\MessagesRes.resx
CurrencyCodeRUR RUR Код валюты: рубли

Аналогично создадим Представление Add для ввода нового платежа и файл с ресурсами для него:

@model MVCDemo.Models.PaymentModel
@using MVCDemo.Resources.Views.Payments;
@{
    ViewBag.Title = AddRes.PageTitle;
}

<h2>@ViewBag.Title</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <div class="editor-label">
            @Html.LabelFor(model => model.UserLogin)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.UserLogin)
            @Html.ValidationMessageFor(model => model.UserLogin)
        </div>
        <div class="editor-label">
            @Html.LabelFor(model => model.Date)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Date)
            @Html.ValidationMessageFor(model => model.Date)
        </div>
        <div class="editor-label">
            @Html.LabelFor(model => model.Amount)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Amount)
            @MessagesRes.CurrencyCodeRUR
            @Html.ValidationMessageFor(model => model.Amount)
        </div>
        <div class="editor-label">
            @Html.LabelFor(model => model.Info)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Info)
            @Html.ValidationMessageFor(model => model.Info)
        </div>
        <p>
            <input type="submit" value="@AddRes.AddPaymentButton" />
        </p>
    </fieldset>
}

<div>@Html.ActionLink(AddRes.BackToPaymentsList, "Index")</div>
Файл Resources\View\Payments\AddRes.resx
PageTitle Add payment Заголовок страницы
AddPaymentButton Add payment Кнопка добавления платежа
BackToPaymentsList Back to List Ссылка возврата к списку платежей

Добавим ссылки в шаблон Shared\_Layout.cshtml, чтобы можно был выбрать нужный список:

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
</head>

<body>
    <div>
        @Html.ActionLink(
            MVCDemo.Resources.Views.UserProfiles.IndexRes.PageTitle, "Index", "UserProfiles")
&nbsp;|&nbsp;
        @Html.ActionLink(
            MVCDemo.Resources.Views.Payments.IndexRes.PageTitle, "Index", "Payments")
    </div>
    <hr />
    @RenderBody()
</body>
</html>

Все необходимые изменения сделаны и можно запустить веб-приложение. Давайте перейдем на страницу добавления платежа. Здесь все поля подписаны именами свойств. Практически отсутствует контроль вводимых данных, за исключением проверки имени пользователя в коде Модели.

Начнем исправлять ситуацию, реализовав свой вариант провайдера метаданных.


Исходный код проекта (C#, Visual Studio 2010): MVCDemo-Part18.zip (497 Kb)

Комментарии (4) -

Андрей 22.03.2011 20:49:36

Спасибо, отличная статья.

Приятно было бы почитать об архитектуре ASP.NET MVC приложения за пределеами UI слоя. Как граммотно организовать бизнесс слой, DAL.

Пожалуйста. Насчет организации именно бизнес слоя мысль интересная. Но в любом случае, не раньше завершения запланированных частей. Надеюсь времени хватит.

Андрей 22.03.2011 21:11:28

@ Andrey:

было бы круто, пока продолжаю с удовольствем читать серию. В первый раз вижу такой материал на русском.

essay paper 18.01.2012 15:53:43

Спасибо, отличная статья.

Pingbacks and trackbacks (3)+

Добавить комментарий