-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstrongly-typed-html-helper.html
129 lines (109 loc) · 13.3 KB
/
strongly-typed-html-helper.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<!DOCTYPE html>
<html lang="ru">
<head>
<title>tkirill's blog</title>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="./theme/css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="./theme/css/blog.css" />
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-49219502-1', 'tkirill.org');
ga('send', 'pageview');
</script>
<link rel="stylesheet" type="text/css" href="./theme/css/pygments-friendly.css" />
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1><a href=".">tkirill's blog</a></h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<hr>
</div>
</div>
<div class="row">
<div class="col-md-8">
<p class="article-date">
Чт. 20 Март 2014
</p>
<h2>
<a href="./strongly-typed-html-helper.html" rel="bookmark" title="Permalink to Строго-типизированные extensions для HtmlHelper">Строго-типизированные extensions для HtmlHelper</a>
</h2>
<div class="article-content">
<p>Уже не раз мне требовалось написать свой extension для HtmlHelper, который в качестве аргумента принимал бы поле модели. В очередной раз, когда это случилось, мне нужно было генерировать <span class="caps">JSON</span> для передачи данных с сервера в JavaScript код при рендере страницы. Получившийся метод должен был максимально походить на встроенные в <span class="caps">ASP</span>.<span class="caps">NET</span> <span class="caps">MVC</span> хелперы, то есть:</p>
<ul>
<li>Использовать compile-time проверки, никаких строк с названиями полей, как, например, в <a href="http://msdn.microsoft.com/en-us/library/dd492494%28v=vs.118%29.aspx">Html.TextBox</a>. Это позволяет избегать проблем с типизацией и переименованиями полей.</li>
<li>Работать с моделью, а не ViewBag.</li>
<li>Использовать лямбда-выражения для выбора поля. Это даёт бонус в виде использования вещей из ReSharper, например, автодополнения.</li>
</ul>
<p>Сейчас у меня есть такой extension и ниже я разберу, как он устроен:</p>
<div class="highlight"><pre><span></span>@model IssuesViewModel
<script>
var issues = new IssuesCollection(@Html.JsonFor(m => m.Issues));
</script>
</pre></div>
<p>Весь код хелпера занимает три строки, для сериализации используется ServiceStack.Text:</p>
<div class="highlight"><pre><span></span><span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">HtmlJsonHelper</span>
<span class="p">{</span>
<span class="k">public</span> <span class="k">static</span> <span class="n">IHtmlString</span> <span class="n">JsonFor</span><span class="p"><</span><span class="n">TModel</span><span class="p">,</span> <span class="n">TProperty</span><span class="p">>(</span><span class="k">this</span> <span class="n">HtmlHelper</span><span class="p"><</span><span class="n">TModel</span><span class="p">></span> <span class="n">htmlHelper</span><span class="p">,</span>
<span class="n">Expression</span><span class="p"><</span><span class="n">Func</span><span class="p"><</span><span class="n">TModel</span><span class="p">,</span> <span class="n">TProperty</span><span class="p">>></span> <span class="n">expression</span><span class="p">)</span>
<span class="p">{</span>
<span class="kt">var</span> <span class="n">meta</span> <span class="p">=</span> <span class="n">ModelMetadata</span><span class="p">.</span><span class="n">FromLambdaExpression</span><span class="p">(</span><span class="n">expression</span><span class="p">,</span> <span class="n">htmlHelper</span><span class="p">.</span><span class="n">ViewData</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">json</span> <span class="p">=</span> <span class="n">JsonSerializer</span><span class="p">.</span><span class="n">SerializeToString</span><span class="p">(</span><span class="n">meta</span><span class="p">.</span><span class="n">Model</span><span class="p">);</span>
<span class="k">return</span> <span class="n">htmlHelper</span><span class="p">.</span><span class="n">Raw</span><span class="p">(</span><span class="n">json</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>Здесь используется хелпер из <span class="caps">ASP</span>.<span class="caps">NET</span> <span class="caps">MVC</span>, который позволяет получить ModelMetadata для поля по expression. Примечательно, что точно таким же способом реализованы встроенные в <span class="caps">ASP</span>.<span class="caps">NET</span> <span class="caps">MVC</span> хелперы. Посмотрим, например, на реализацию Html.LabelFor:</p>
<div class="highlight"><pre><span></span><span class="k">public</span> <span class="k">static</span> <span class="n">MvcHtmlString</span> <span class="n">LabelFor</span><span class="p"><</span><span class="n">TModel</span><span class="p">,</span> <span class="n">TValue</span><span class="p">>(</span><span class="k">this</span> <span class="n">HtmlHelper</span><span class="p"><</span><span class="n">TModel</span><span class="p">></span> <span class="n">html</span><span class="p">,</span>
<span class="n">Expression</span><span class="p"><</span><span class="n">Func</span><span class="p"><</span><span class="n">TModel</span><span class="p">,</span> <span class="n">TValue</span><span class="p">>></span> <span class="n">expression</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">return</span> <span class="n">LabelFor</span><span class="p"><</span><span class="n">TModel</span><span class="p">,</span> <span class="n">TValue</span><span class="p">>(</span><span class="n">html</span><span class="p">,</span> <span class="n">expression</span><span class="p">,</span> <span class="n">labelText</span><span class="p">:</span> <span class="k">null</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// ещё несколько перегрузок с разным количеством параметров, которые в конечном счёте вызывают такой internal метод</span>
<span class="k">internal</span> <span class="k">static</span> <span class="n">MvcHtmlString</span> <span class="n">LabelFor</span><span class="p"><</span><span class="n">TModel</span><span class="p">,</span> <span class="n">TValue</span><span class="p">>(</span><span class="k">this</span> <span class="n">HtmlHelper</span><span class="p"><</span><span class="n">TModel</span><span class="p">></span> <span class="n">html</span><span class="p">,</span>
<span class="n">Expression</span><span class="p"><</span><span class="n">Func</span><span class="p"><</span><span class="n">TModel</span><span class="p">,</span> <span class="n">TValue</span><span class="p">>></span> <span class="n">expression</span><span class="p">,</span>
<span class="kt">string</span> <span class="n">labelText</span><span class="p">,</span>
<span class="n">IDictionary</span><span class="p"><</span><span class="kt">string</span><span class="p">,</span> <span class="kt">object</span><span class="p">></span> <span class="n">htmlAttributes</span><span class="p">,</span>
<span class="n">ModelMetadataProvider</span> <span class="n">metadataProvider</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">return</span> <span class="nf">LabelHelper</span><span class="p">(</span><span class="n">html</span><span class="p">,</span>
<span class="n">ModelMetadata</span><span class="p">.</span><span class="n">FromLambdaExpression</span><span class="p">(</span><span class="n">expression</span><span class="p">,</span> <span class="n">html</span><span class="p">.</span><span class="n">ViewData</span><span class="p">,</span> <span class="n">metadataProvider</span><span class="p">),</span>
<span class="n">ExpressionHelper</span><span class="p">.</span><span class="n">GetExpressionText</span><span class="p">(</span><span class="n">expression</span><span class="p">),</span>
<span class="n">labelText</span><span class="p">,</span>
<span class="n">htmlAttributes</span><span class="p">);</span>
<span class="p">}</span>
</pre></div>
<p>В общем, <code>ModelMetadata.FromLambdaExpression</code> берёт на себя всю грязную работу по работе с expression, типами и всеми специфичными для <span class="caps">MVC</span> атрибутами, вроде <code>[DisplayName]</code>. Для того, чтобы наш <span class="caps">JSON</span> не испортило стандартное <span class="caps">HTML</span>-экранирование в Razor, мы используем HtmlHelper.Raw, который возвращает специальную реализацию <code>IHtmlString</code>, не использующую экранирование.</p>
<p>Однако <span class="caps">HTML</span>-экранирование необходимо использовать в случаях, когда <span class="caps">JSON</span> вставляется, например, в значение атрибута для какого-нибудь тега. Для этого нужно изменить используемую реализацию <code>IHtmlString</code>:</p>
<div class="highlight"><pre><span></span><span class="k">public</span> <span class="k">static</span> <span class="n">IHtmlString</span> <span class="n">EncodedJsonFor</span><span class="p"><</span><span class="n">TModel</span><span class="p">,</span> <span class="n">TProperty</span><span class="p">>(</span><span class="k">this</span> <span class="n">HtmlHelper</span><span class="p"><</span><span class="n">TModel</span><span class="p">></span> <span class="n">htmlHelper</span><span class="p">,</span>
<span class="n">Expression</span><span class="p"><</span><span class="n">Func</span><span class="p"><</span><span class="n">TModel</span><span class="p">,</span> <span class="n">TProperty</span><span class="p">>></span> <span class="n">expression</span><span class="p">)</span>
<span class="p">{</span>
<span class="kt">var</span> <span class="n">meta</span> <span class="p">=</span> <span class="n">ModelMetadata</span><span class="p">.</span><span class="n">FromLambdaExpression</span><span class="p">(</span><span class="n">expression</span><span class="p">,</span> <span class="n">htmlHelper</span><span class="p">.</span><span class="n">ViewData</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">json</span> <span class="p">=</span> <span class="n">JsonSerializer</span><span class="p">.</span><span class="n">SerializeToString</span><span class="p">(</span><span class="n">meta</span><span class="p">.</span><span class="n">Model</span><span class="p">);</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">MvcHtmlString</span><span class="p">(</span><span class="n">json</span><span class="p">);</span>
<span class="p">}</span>
</pre></div>
<p>На этом всё. Для интереса можно посмотреть на полную реализацию Html.LabelFor на GitHub: <a href="https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/Html/LabelExtensions.cs">LabelExtensions.cs</a>.</p>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<hr>
</div>
</div>
<div class="row">
<div class="col-md-12">
<p class="pelican-credits">Powered by <a href="http://docs.getpelican.com/">Pelican</a></p>
</div>
</div>
</div>
</body>
</html>