One thing that is really nice in ServiceStack is the HTML output when you use a browser to request a REST object. For lists, it automatically creates an HTML table with sorting, and for complex objects it shows a nice layout with key/values listed (including nesting). It's also quite elegant, as the server-side view simply contains JSON output of the object, and then javascript handles the rest.
So, I appropriated it for use with rom. Now, literally the only thing I have to do to make a new service method is add the controller code:
public UserController : BaseController { [GET("users")] // using AttributeRouting, see http://rom.codeplex.com/discussions/281661public ActionResult Index() { var users = svc.LoadAllUsers(); return RestView(users); } }
And that's it. I have a nice HTML output, and I don't have to create any controller-specific views.
To make this work, what I have is a base controller:
publicabstractclass BaseController : Controller { ///<summary>/// Returns the GenericObject view for a model///</summary>///<param name="Model"></param>///<returns></returns>protected ViewResult RestView(object model) { returnbase.View("GenericObject", model); } }
And then a razor (C#) view placed in Views/Shared/GenericObject.cshtml (the original code for this is at https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack/WebHost.EndPoints/Formats/HtmlFormat.cs):
@{/* Copyright (c) 2007-2011, Demis Bellot, ServiceStack. http://www.servicestack.net All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the ServiceStack nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHTHOLDER> BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ } @model object @{ Layout = null; } <!doctypehtml><htmllang="en-us"><head><title>@Model.GetType().Name Snapshot of @DateTime.Now.ToString("yyyy-MM-dd h:mm:ss tt")</title><scriptid="dto"type="text/json">@Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model))</script><styletype="text/css"> BODY, H1, H2, H3, H4, H5, H6, DL, DT, DD { margin: 0; padding: 0; color: #444; font: 13px/15px Arial, Verdana, Helvetica; } H1 { text-align: center; font: 24px Helvetica, Verdana, Arial; padding: 20px 0 10px 0; background: #FBFBFB; border-bottom: solid 1px #fff; } #lnks { border-top: solid 1px #dfdfdf; border-bottom: solid 1px #dfdfdf; margin: 0 0 10px 0; padding: 5px; background: #f1f1f1; line-height: 20px; text-align: center; } #lnks B { padding: 0 3px; } #body { padding: 20px; } H1 B { font-weight: normal; color: #069; } H1 A { color: #0E8F13; text-decoration: underline; } H1 I { font-style: normal; color: #0E8F13; } A { color: #00C; text-decoration: none; } A:hover { text-decoration: underline; } .ib { position: relative; display: -moz-inline-box; display: inline-block; } * html .ib { display: inline; } *:first-child + html .ib { display: inline; } TABLE { border-collapse:collapse; border: solid 1px #ccc; clear: left; } TH { text-align: left; padding: 4px 8px; : #fff 1px 1px -1px; background: #f1f1f1; white-space:nowrap; cursor:pointer; font-weight: bold; } TH, TD, TD DT, TD DD { font-size: 13px; font-family: Arial; } TD { padding: 8px 8px 0 8px; vertical-align: top; } DL { clear: left; } DT { margin: 10px 0 5px 0; font: bold 18px Helvetica, Verdana, Arial; width: 200px; overflow: hidden; clear: left; float: left; display:block; white-space:nowrap; } DD { margin: 5px 10px; font: 18px Arial; padding: 2px; display: block; float: left; } DL DL DT { font: bold 16px Arial; } DL DL DD { font: 16px Arial; } HR { display:none; } TD DL HR { display:block; padding: 0; clear: left; border: none; } TD DL { padding: 4px; margin: 0; height:100%; max-width: 700px; } DL TD DL DT { padding: 2px; margin: 0 10px 0 0; font-weight: bold; font-size: 13px; width: 120px; overflow: hidden; clear: left; float: left; display:block; } DL TD DL DD { margin: 0; padding: 2px; font-size: 13px; display: block; float: left; } TBODY>TR:last-child>TD { padding: 8px; } THEAD { -webkit-user-select:none; -moz-user-select:none; } .desc, .asc { background-color: #FAFAD2; } .desc { background-color: #D4EDC9; } TH B { display:block; float:right; margin: 0 0 0 5px; width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid #ccc; border-bottom: none; } .asc B { border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid #333; border-bottom: none; } .desc B { border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 5px solid #333; border-top: none; } #show-json { display:none; } #mask { display: none; position:absolute; top:0; left:0; height:100%; width:100%; background: rgba(0,0,0,0.7); z-index: 1; } .show-json #show-json, .show-json #mask { display:block; } #show-json { position: absolute; left: 50%; margin: 0 0 0 -350px; border: solid 4px #ccc; padding: 10px 20px; background: #fff; text-align: center; float: left; z-index: 2; } H3 { font-size: 18px; margin: 0 0 10px 0; } #show-json TEXTAREA { width: 750px; height: 400px; overflow:visible; display: block; } #show-json BUTTON { margin: 10px 0 0 0; padding: 5px 10px; clear: left; } </style></head><body><divid="mask"></div><h1>Snapshot of <i>@Model.GetType().Name</i> generated by <ahref="http://rom.codeplex.com">Resources over MVC</a> on <b>@DateTime.Now.ToString("yyyy-MM-dd h:mm:ss tt")</b></h1><divid="lnks"><ahref="javascript:showJson()">view json datasource</a><b>from original url:</b><ahref="@Request.Url.ToString()">@Request.Url.ToString()</a><b>in other formats:</b><ahref="@Request.Url.AbsolutePath?format=json">json</a><ahref="@Request.Url.AbsolutePath?format=xml">xml</a><!-- <ahref="@Request.Url.AbsolutePath?format=csv">csv</a><ahref="@Request.Url.AbsolutePath?format=jsv">jsv</a> --></div><divid="body"><divid="show-json"><h3>This reports json data source</h3><textarea></textarea><buttononclick="doc.body.className=null;">Close Window</button></div><divid="content"></div></div><script> !window.JSON && document.write(unescape('%3Cscript src="http://ajax.cdnjs.com/ajax/libs/json2/20110223/json2.js"%3E%3C/script%3E'))</script><scripttype="text/javascript">// <![CDATA[var doc = document, win = window, $ = function (id) { return doc.getElementById(id); }, $$ = function (sel) { return doc.getElementsByTagName(sel); }, $each = function (fn) { for (var i = 0, len = this.length; i < len; i++) fn(i, this[i], this); }; $.each = function (arr, fn) { $each.call(arr, fn); }; var splitCase = function (t) { returntypeof t != 'string' ? t : t.replace(/([A-Z]|[0-9]+)/g, ' $1'); }, uniqueKeys = function (m) { var h = {}; for (var i = 0, len = m.length; i < len; i++) for (var k in m[i]) if (show(k)) h[k] = k; return h; }, keys = function (o) { var a = []; for (var k in o) if (show(k)) a.push(k); return a; } var tbls = []; function val(m) { if (m == null) return''; if (typeof m == 'number') return num(m); if (typeof m == 'string') return str(m); if (typeof m == 'boolean') return m ? 'true' : 'false'; return m.length ? arr(m) : obj(m); } function num(m) { return m; } function str(m) { return m.substr(0, 6) == '/Date(' ? dmft(date(m)) : m; } function date(s) { returnnew Date(parseFloat(/Date\(([^)]+)\)/.exec(s)[1])); } function pad(d) { return d < 10 ? '0' + d : d; } function dmft(d) { return d.getFullYear() + '/' + pad(d.getMonth() + 1) + '/' + pad(d.getDate()); } function show(k) { returntypeof k != 'string' || k.substr(0, 2) != '__'; } function obj(m) { var sb = '<dl>'; for (var k in m) if (show(k)) sb += '<dt class="ib">' + splitCase(k) + '</dt><dd>' + val(m[k]) + '</dd>'; sb += '</dl>'; return sb; } function arr(m) { if (typeof m[0] == 'string' || typeof m[0] == 'number') return m.join(', '); var id = tbls.length, h = uniqueKeys(m); var sb = '<table id="tbl-' + id + '"><caption></caption><thead><tr>'; tbls.push(m); var i = 0; for (var k in h) sb += '<th id="h-' + id + '-' + (i++) + '"><b></b>' + splitCase(k) + '</th>'; sb += '</tr></thead><tbody>' + makeRows(h, m) + '</tbody></table>'; return sb; } function makeRows(h, m) { var sb = ''; for (var r = 0, len = m.length; r < len; r++) { sb += '<tr>'; var row = m[r]; for (var k in h) if (show(k)) sb += '<td>' + val(row[k]) + '</td>'; sb += '</tr>'; } return sb; } var json = $('dto').innerHTML, model = JSON.parse(json), txt = $$('TEXTAREA')[0], isIE = /msie/i.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent); $("content").innerHTML = val(model); txt.innerHTML = enc(json); function showJson() { doc.body.className = 'show-json'; txt.select(); txt.focus(); } doc.onclick = function (e) { e = e || window.event, el = e.target || e.srcElement, cls = el.className; if (el.tagName == 'B') el = el.parentNode; if (el.tagName != 'TH') return; el.className = cls == 'asc' ? 'desc' : (cls == 'desc' ? null : 'asc'); $.each($$('TH'), function (i, th) { if (th == el) return; th.className = null; }); clearSel(); var ids = el.id.split('-'), tId = ids[1], cId = ids[2]; var tbl = tbls[tId].slice(0), h = uniqueKeys(tbl), col = keys(h)[cId], tbody = el.parentNode.parentNode.nextSibling; if (!el.className) { setTableBody(tbody, makeRows(h, tbls[tId])); return; } var d = el.className == 'asc' ? 1 : -1; tbl.sort(function (a, b) { return cmp(a[col], b[col]) * d; }); setTableBody(tbody, makeRows(h, tbl)); } function setTableBody(tbody, html) { if (!isIE) { tbody.innerHTML = html; return; } var temp = tbody.ownerDocument.createElement('div'); temp.innerHTML = '<table>' + html + '</table>'; tbody.parentNode.replaceChild(temp.firstChild.firstChild, tbody); } function clearSel() { if (doc.selection && doc.selection.empty) doc.selection.empty(); elseif (win.getSelection) { var sel = win.getSelection(); if (sel && sel.removeAllRanges) sel.removeAllRanges(); } } function cmp(v1, v2) { var f1, f2, f1 = parseFloat(v1), f2 = parseFloat(v2); if (!isNaN(f1) && !isNaN(f2)) v1 = f1, v2 = f2; if (typeof v1 == 'string'&& v1.substr(0, 6) == '/Date(') v1 = date(v1), v2 = date(v2); if (v1 == v2) return 0; return v1 > v2 ? 1 : -1; } function enc(html) { if (typeof html != 'string') return html; return html.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); } // ]]></script></body></html>