Generic model view generator

Dec 2, 2011 at 11:13 PM
Edited Dec 2, 2011 at 11:15 PM

 

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/281661
    public 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:

 

    public abstract class BaseController : Controller
    {
        /// <summary>
        /// Returns the GenericObject view for a model
        /// </summary>
        /// <param name="Model"></param>
        /// <returns></returns>
        protected ViewResult RestView(object model)
        {
            return base.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 <COPYRIGHT HOLDER> 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;
}
<!doctype html>
<html lang="en-us">
<head>
<title>@Model.GetType().Name Snapshot of @DateTime.Now.ToString("yyyy-MM-dd h:mm:ss tt")</title>
<script id="dto" type="text/json">@Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model))</script>
<style type="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>
<div id="mask"></div>
<h1>Snapshot of <i>@Model.GetType().Name</i> generated by <a href="http://rom.codeplex.com">Resources over MVC</a> on <b>@DateTime.Now.ToString("yyyy-MM-dd h:mm:ss tt")</b></h1>
<div id="lnks">
  <a href="javascript:showJson()">view json datasource</a>
  <b>from original url:</b>
  <a href="@Request.Url.ToString()">@Request.Url.ToString()</a>
  <b>in other formats:</b>
  <a href="@Request.Url.AbsolutePath?format=json">json</a>
  <a href="@Request.Url.AbsolutePath?format=xml">xml</a>
  <!-- <a href="@Request.Url.AbsolutePath?format=csv">csv</a>
  <a href="@Request.Url.AbsolutePath?format=jsv">jsv</a> -->
</div>
<div id="body">
  
  <div id="show-json">
    <h3>This reports json data source</h3>
    <textarea></textarea>
    <button onclick="doc.body.className=null;">Close Window</button>
  </div>
  
  <div id="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>
<script type="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) { return typeof 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) { return new 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) { return typeof 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();
        else if (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, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
    }



// ]]>
</script>
</body>
</html>
Coordinator
Dec 3, 2011 at 11:04 PM

Thanks for providing the example. That should be easy for somebody to incorporate into their own web service. If you want to write this up in a blog post, I'll link to it from the Documentation page.