git mergetool
git checkout –b “admin_home”
Html helpers won’t work with dynamics
Make some Helpers
- less abstraction
- more control
@helper TextBox(string name, object value=null) {
//if value is null, set to empty string
var val = value ?? "";
<p>
<label>@name</label>
<input type="text" name="@name" id="@name" value="@val" />
</p>
}
then
<legend>Production</legend>
@Form.TextBox("Title", @Model.Title)
However we want the Title property to be required:
In MVC3 this would be:" Html.TextBoxFor(x=>x.Title);
and put a Required DataAnnotation attribute on the Production class.
could also tell it to do in unobtrusively with jquery, and jquery validations.
Getting rid of js files:

So now I just have:

Scripts loader for js
@helper Scripts(){
@Assets.Script("jquery.validate.min.js")
@Assets.Script("jquery.validate.unobtrusive.min.js")
}
in the Form.cshtml helpers
Validations
@helper ValidationMessage(string name) {
<span class="field-validation-valid" data-valmsg-for="@name" data-valmsg-replace="true"></span>
}
@helper TextBoxRequired(string name, object value = null, string requiredMessage = "Required") {
var val = value ?? "";
//using HTML5 and jQuery validations
<p>
<label>@name</label>
<input type="text" name="@name" id="@name" value="@val" data-val="true" data-val-required="@requiredMessage" />
@ValidationMessage(name)
</p>
}
then on the front end:
<legend>Production</legend>
@Form.TextBoxRequired("Title", @Model.Title)
nice.
FInally:
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
<fieldset>
<legend>Production</legend>
@Form.TextBoxRequired("Title", @Model.Title)
@Form.TextArea("Description", @Model.Description)
@Form.TextBoxNumber("Price", @Model.Price)
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
and helpers:
@helper Scripts(){
@Assets.Script("jquery.validate.min.js")
@Assets.Script("jquery.validate.unobtrusive.min.js")
}
@helper ValidationMessage(string name) {
<span class="field-validation-valid" data-valmsg-for="@name" data-valmsg-replace="true"></span>
}
@helper TextBox(string name, object value = null) {
//if value is null, set to empty string
var val = value ?? "";
<p>
<label>@name</label>
<input type="text" name="@name" id="@name" value="@val" />
</p>
}
@helper TextBoxRequired(string name, object value = null, string requiredMessage = "Required") {
var val = value ?? "";
//using HTML5 and jQuery validations
<p>
<label>@name *</label>
<input type="text" name="@name" id="@name" value="@val" data-val="true" data-val-required="@requiredMessage" />
@ValidationMessage(name)
</p>
}
@helper TextBoxNumber(string name, object value = null, string numberMessage = "Should be a number") {
var val = value ?? "";
//using HTML5 and jQuery validations
<p>
<label>@name *</label>
<input type="text" name="@name" id="@name" value="@val" data-val="true" data-val-number="@numberMessage" />
@ValidationMessage(name)
</p>
}
@helper TextArea(string name, string attributes = "", object value = null) {
var val = value ?? "";
<p>
<label>@name</label>
<textarea name="@name" id="@name" @attributes >@val</textarea>
</p>
}
More Security
Not using ASP Net Membership, so how can control security on controllers?
We could just add [Authorize(Role)] attribute if we were using ASP Net Membership on each controller class or method.
[Authorize(Roles = "asdf")]
no notion of roles in this site
We are going to go simple – create an Action Filter.
Can’t pass a dynamic argument to an extension method.
Roll own Simple Authorization
make an attribute / ActionFilter
namespace VidPub.Web.Infrastructure {
public class RequiredAdminAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext filterContext) {
//pull the controller off the context passed in
//and cast it as own ApplicationController
var controller = (ApplicationController)filterContext.Controller;
//user logged in?
if (!controller.IsLoggedIn) {
controller.TempData["Error"] = "You need to be logged in to do that";
filterContext.HttpContext.Response.Redirect("/account/logon");
return;
}
//is the user an admin
var adminEmails = new string[] { "robconery@gmail.com", "rob@tekpub.com", "davemateer@gmail.com" };
//CurrentUser is a field in our ApplicationController
//can't use var... as it would be ynamic object..need to tell the compiler
string userEmail = controller.CurrentUser.Email;
if (!adminEmails.Contains(userEmail)) {
controller.TempData["Error"] = "You are not authorized";
filterContext.HttpContext.Response.Redirect("/account/logon");
}
}
}
}
this wired up to the field on ApplicationController, which tells us if a user is logged in, and then which user.
then wire up on CruddyController the actions that need to be authorised.
[RequiredAdmin]
public virtual ActionResult Create() {
return View(_table.Prototype);
}
[HttpPost]
[ValidateAntiForgeryToken]
[RequiredAdmin]
public virtual ActionResult Create(FormCollection collection) {
dynamic item = _table.CreateFrom(collection);
try {
_table.Insert(item);
return RedirectToAction("Index");
}
catch {
TempData["alert"] = "There was an error adding this item";
return View();
}
}
[RequiredAdmin]
public virtual ActionResult Edit(int id) {
var model = _table.Get(ID: id);
model._Table = _table;
return View(model);
}
[HttpPost]
[RequiredAdmin]
public virtual ActionResult Edit(int id, FormCollection collection) {
var model = _table.CreateFrom(collection);
try {
_table.Update(model, id);
return RedirectToAction("Index");
}
catch (Exception ex) {
TempData["Error"] = "There was a problem editing this record";
return View(model);
}
}
A Little jQuery
Firebug
Why:
- Write less server code
- More response app
- More compelling app
Add in json serialization, so we can talk javascript!
Usually we’d go with something like:
return Json(_table.All(), JsonRequestBehavior.AllowGet);
json.net a lot of people use as a serialiser.
However lets see if can solve it on own without taking another dependency
Getting Json dates to displayed correctly is a long running problem. Write own, then use strings everywhere to make life simpler. Dave Ward http://encosia.com/ write this.
Couldn’t get json to output correctly, problem was glimpse…

Fiddler was very useful here.
Our new way of calling json serializer:
[HttpGet]
public virtual ActionResult Index(string query) {
//sidestep viewgeneration and return json
//
IEnumerable<dynamic> results = null;
results = _table.All();
return VidpubJSON(results);
}
public ActionResult VidpubJSON(dynamic content) {
var serializer = new JavaScriptSerializer();
serializer.RegisterConverters(new JavaScriptConverter[] { new ExpandoObjectConverter() });
var json = serializer.Serialize(content);
//Response.ContentType = "application/json";
return Content(json);
}
and Dave Wards converter which cracks open the IDictionary backing the Expando, explicitly assigns key value bindings that are required…
so then we can parse down to a shortDateString (the dates)
namespace VidPub.Web.Infrastructure {
public class ExpandoObjectConverter : JavaScriptConverter {
public override IEnumerable<Type> SupportedTypes {
get { return new ReadOnlyCollection<Type>(new List<Type>(new Type[] { typeof(ExpandoObject) })); }
}
public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer) {
ExpandoObject expando = (ExpandoObject)obj;
if (expando != null) {
// Create the representation.
Dictionary<string, object> result = new Dictionary<string, object>();
foreach (KeyValuePair<string, object> item in expando) {
var value = item.Value ?? "";
if (value.GetType() == typeof(DateTime))
result.Add(item.Key, ((DateTime)value).ToShortDateString());
else
result.Add(item.Key, value.ToString());
}
return result;
}
return new Dictionary<string, object>();
}
public override dynamic Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer) {
dynamic result = new ExpandoObject();
if (dictionary != null) {
// Create the representation.
var dc = (IDictionary<string, object>)result;
foreach (KeyValuePair<string, object> item in dictionary) {
var value = item.Value ?? "";
dc.Add(item.Key, value);
}
}
return result;
}
}
}
Put verbs HttpPut on Edit post, and HttpDelete on a delete post.
jQuery
Making a searchbox.
<script type="text/javascript">
$(document).ready(function () {
$("#searchForm").submit(function () {
var val = $("#search").val();
alert("You're looking for " + val);
return false;
});
});
</script>
<h2>Vidpub Admin</h2>
<form id="searchForm">
<label>Find:</label>
<input type="text" id="search"/>
<input type="submit" value="go" />
</form>

want to search over:
- Productions
- Episodes
- Customers
namespace VidPub.Web.Models {
public class Customers : DynamicModel {
public Customers()
: base("VidPub", "Customers", "ID") {
}
public dynamic FuzzySearch(string query) {
return this.Query(@"select ID, email from customers
where email LIKE('%'+@0+'%')", query);
}
}
}
Lucene or full text indexing would be better if we get lots of data.. but for now it will be fine.

<script type="text/javascript">
$(document).ready(function () {
$("#searchForm").submit(function () {
var val = $("#search").val();
//alert("You're looking for " + val);
//third is a callback
$.getJSON("/productions", { query: val }, function (data) {
alert(data.length);
});
return false;
});
});
</script>
[HttpGet]
public virtual ActionResult Index(string query) {
//sidestep viewgeneration and return json
IEnumerable<dynamic> results = null;
if (!string.IsNullOrEmpty(query)) {
results = _table.FuzzySearch(query);
}
else {
results = _table.All();
}
if (Request.IsAjaxRequest()) {
return VidpubJSON(results);
}
return View(results);
}
it works – we can pass back search results back to the browser using json.
jQuery Templates
using jQuery template:
<script type = "text/javascript" src="http://ajax.aspnetcdn.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$("#searchForm").submit(function () {
var val = $("#search").val();
//alert("You're looking for " + val);
//third is a callback
$.getJSON("/productions", { query: val }, function (data) {
//alert(data.length);
$("#searchTemplate").tmpl(data).appendTo("#searchResults");
});
return false;
});
});
</script>
the template:
<script id="searchTemplate" type="text/html">
<li>
${Title}
</li>
</script>
where to display results:
<div id="searchResults">
</div>
want a link, so need the controller passed in so we can give it that name:
however this will break the templating as there is an array inside…blah something:
this will fix using looping:
<script type="text/javascript">
$(document).ready(function () {
$("#searchForm").submit(function () {
var val = $("#search").val();
$("#searchResults").empty();
$.getJSON("/productions", { query: val }, function (data) {
var results = { controller: "productions", items: data };
$("#searchTemplate").tmpl(results).appendTo("#searchResults");
});
return false;
});
});
</script>
<script id="searchTemplate" type="text/html">
<table>
{{each items}}
<tr>
<td><a href="${controller}/edit/${ID}">${Title}</a></td>
</tr>
{{/each}}
</table>
</script>

Putting it All Together
Console in Google Chrome is Ctrl+Shift+I
abstracting code out to vidpub-search.js

debugging in console. undefined means jQuery can’t fire a method on the selector I’ve defined.
Instead of selecting by ID, going to select by class:
var vidpubSearch = {
resetResults: function (controller) {
$(".search-results").empty();
},
getResults: function(query, controller) {
$.getJSON("/"+controller, { query: query }, function (data) {
var results = { controller: controller, items: data };
$("#searchTemplate").tmpl(results).appendTo("#"+controller+"-search-results");
});
}
};
jQuery(function () {
$("#searchForm").submit(function () {
var val = $("#search").val();
vidpubSearch.resetResults();
vidpubSearch.getResults(val, "productions");
return false;
});
});
front end:
<script type = "text/javascript" src="http://ajax.aspnetcdn.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js"></script>
@Assets.Script("vidpub-search.js")
<script id="searchTemplate" type="text/html">
<table>
{{each items}}
<tr>
<td><a href="${controller}/edit/${ID}">${Title}</a></td>
</tr>
{{/each}}
</table>
</script>
<h2>Vidpub Admin</h2>
<form id="searchForm">
<label>Find:</label>
<input type="text" id="search"/>
<input type="submit" value="go" />
</form>
<div id="productions-search-results" class="search-results">
</div>
testing

Review
dynamics a possible problem..
we don’t have much code.. nice!
admin ui is simple.