Всем, кто программирует в среде ASP.NET MVC, хорошо известно, насколько широко используются метаданные в .NET вообще и в MVC в частности. В MVC, атрибуты применяются как при генерации разметки, так и при валидации данных, полученных с клиента.
При использовании классической модели программирования сайтов это прекрасно работает. Но что, если Вы работаете с использование ajax и формируете html разметку динамически на клиенте? Вы хотите иметь метаданные модели (далее МДМ) на клиенте? Я — да!
Прямой путь — это сформировать json, включив туда и данные, и МДМ. Мы всегда можем написать в контроллере что-то вроде:
Это просто, но не удобно при работе с моделью в браузере, да и выглядит как-то неприглядно.
Мне пришло в голову, что передавать МДМ следует так же, как это делает сервер или браузер, а именно в заголовках http. Давайте попробуем сделать следующее:
Далее я написал простой код, который реализует эту идею.
Для начала определим модель и поставим атрибуты на свойства.
Напишем простой метод действия, который будет возвращать клиенту данные и МДМ.
Наберем в адресной строке браузера localhost:67578/Hab/GetData и посмотрим на заголовок http.
Убедимся, что все работает как надо, и перейдем к клиентской части кода.
В результате мы получаем два поля ввода с учетом метаданных модели на сервере.
Особенно элегантно данный подход выглядит при использовании на клиенте backbone. Мы можем получить МДМ, переопределив метод parse:
получив МДМ в model.meta в виде объекта js, можем использовать МДМ во view для рендеринга модели
используя для tempalte что то вроде:
В заключение добавлю, что описанный подход можно легко применить для валидации модели на клиенте.
При использовании классической модели программирования сайтов это прекрасно работает. Но что, если Вы работаете с использование ajax и формируете html разметку динамически на клиенте? Вы хотите иметь метаданные модели (далее МДМ) на клиенте? Я — да!
Прямой путь — это сформировать json, включив туда и данные, и МДМ. Мы всегда можем написать в контроллере что-то вроде:
public ActionResult GetData() { return Json(new { data = new { данные }, meta = new { мета } }); }
Это просто, но не удобно при работе с моделью в браузере, да и выглядит как-то неприглядно.
Мне пришло в голову, что передавать МДМ следует так же, как это делает сервер или браузер, а именно в заголовках http. Давайте попробуем сделать следующее:
- Сформируем объект мета.
- Cериализуем его в строку json.
- Сделаем енкодинг или конвертацию строки в base64 (это необходимо, так как заголовок http передается в ASCII).
- Cоздадим заголовок http и именем “meta-data”.
Далее я написал простой код, который реализует эту идею.
Для начала определим модель и поставим атрибуты на свойства.
public class Data { [ReadOnly(true)] [DisplayName("Номер")] public int Id { get; set; } [DisplayName("Название")] public string Name { get; set; } }
Напишем простой метод действия, который будет возвращать клиенту данные и МДМ.
public ActionResult GetData() { var data = new Data { Id = 1, Name = "Test" }; var meta = ModelMetadataProviders.Current.GetMetadataForType(() => data, typeof(Data)); var metaForJS = meta.Properties.ToDictionary( p => p.PropertyName, p => new { displayName = p.GetDisplayName(), readOnly = p.IsReadOnly }); var jsonMeta = new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(metaForJS); // Uri.EscapeDataString - использовано для простоты кода. В реальном приложении используйте base64. Response.Headers.Add("data-meta", Uri.EscapeDataString(jsonMeta)); return Json(data, JsonRequestBehavior.AllowGet); }
Наберем в адресной строке браузера localhost:67578/Hab/GetData и посмотрим на заголовок http.
HTTP/1.1 200 OK Cache-Control: private Content-Type: application/json; charset=utf-8 Server: Microsoft-IIS/8.0 X-AspNetMvc-Version: 4.0 data-meta:5B%7B%22displayName%22%3A%22%D0%9D%D0%B0%D0%B7%D0%B2%D0%B0%D0%BD %D0%B8%D0%B5%22%2C%22readOnly%22%3Afalse%7D%2C%7B%22displayName%22%3A%22%D0%9D %D0%B0%D0%B7%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%22%2C%22readOnly%22%3Atrue%7D%5D X-AspNet-Version: 4.0.30319 ...
Убедимся, что все работает как надо, и перейдем к клиентской части кода.
function createField(name, value, meta) { // Создаем поле с учетом метаданных return $('<div/>').append( $("<label/>").attr('for', name).text(meta[name].displayName), $("<input type='text'/>").attr("name", name).attr("readonly", meta[name].readOnly).val(value) ); } $(function () { $.getJSON("/Hab/GetData") .done(function (data, s, xhr) { // получаем, декодируем и создаем js объект meta var meta = $.parseJSON(decodeURIComponent(xhr.getResponseHeader("data-meta"))); // создаем и выводим поля на экран for (var p in data) $('body').append(createField(p, data[p], meta)); }) .error(function(d,s){alert(s);}); });
В результате мы получаем два поля ввода с учетом метаданных модели на сервере.
Особенно элегантно данный подход выглядит при использовании на клиенте backbone. Мы можем получить МДМ, переопределив метод parse:
var model= Backbone.Model.extend({ .... parse: function(data, xhr){ this.meta = $.parseJSON(decodeURIComponent(xhr.getResponseHeader("data-meta"))); return data; }, ...... });
получив МДМ в model.meta в виде объекта js, можем использовать МДМ во view для рендеринга модели
var view = Backbone.View.extend({ render: functoin(){ var m = this.model.toJSON(); this.$el.html(this.template(_.extend(m, { meta: this.model}))); } });
используя для tempalte что то вроде:
<script > ... <label><%-meta.name.displayName%></label> <input type='text' value="<%name%>" <%-meta.readOnly ?"readonly":"" %> />" ... </script>
В заключение добавлю, что описанный подход можно легко применить для валидации модели на клиенте.
