Skip to content

Instantly share code, notes, and snippets.

@morbidcamel101
Last active August 29, 2015 14:10
Show Gist options
  • Save morbidcamel101/216288413735621d6dc4 to your computer and use it in GitHub Desktop.
Save morbidcamel101/216288413735621d6dc4 to your computer and use it in GitHub Desktop.
JQuery MVC Standard Response Framework
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
namespace ResponseFramework
{
public interface IResponse<T>
{
bool IsSuccess { get; set; }
string ErrorMessage { get; set; }
T Response { get; set; }
}
// The response that will change everything!
public class StandardResponse : IResponse<StandardDetail>
{
#region Properties
public bool IsSuccess
{
get;
set;
}
public string ErrorMessage
{
get;
set;
}
public StandardDetail Response
{
get;
set;
}
public IEnumerable<T> GetInstructions<T>() where T: Instruction
{
if (this.Response == null
|| Response.Instructions == null
|| Response.Instructions.Length == 0)
yield break;
for (int i = 0; i < Response.Instructions.Length; i++)
{
var instruct = Response.Instructions[i] as T;
if (instruct != null)
{
yield return instruct;
}
}
}
public T GetInstruction<T>() where T : Instruction
{
return GetInstructions<T>().FirstOrDefault();
}
#endregion
#region Static Members
public static StandardResponse Redirect(string url)
{
return new StandardResponse()
{
IsSuccess = true,
Response = new StandardDetail(new RedirectInstruction() { Url = url })
};
}
public static StandardResponse Status(HttpStatusCode statusCode)
{
return new StandardResponse()
{
IsSuccess = false,
Response = new StandardDetail(new StatusCodeInstruction(statusCode))
};
}
public static StandardResponse View(string viewName, object viewModel)
{
return new StandardResponse()
{
IsSuccess = true,
Response = new StandardDetail(new ViewInstruction(viewName, viewModel))
};
}
public static StandardResponse Error(string errorMessage)
{
return new StandardResponse()
{
IsSuccess = false,
ErrorMessage = errorMessage
};
}
internal static StandardResponse Create(bool isSuccess, params Instruction[] instructions)
{
return new StandardResponse()
{
IsSuccess = isSuccess,
Response = new StandardDetail()
{
Instructions = instructions
}
};
}
public static StandardResponse Empty = new StandardResponse() { IsSuccess = true };
public static StandardResponse OK = new StandardResponse() { IsSuccess = true };
#endregion
}
// The gold standard in detail!
public class StandardDetail
{
public StandardDetail()
{
}
public StandardDetail(params Instruction[] instructions)
{
Instructions = instructions;
}
// Instructions that needs to be executed sequentially
public Instruction[] Instructions
{
get;
set;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
namespace ResponseFramework
{
// Base instruction: Got to have a way to identify it
public abstract class Instruction
{
public abstract string InstructionKind { get; }
}
// Signal the server to redirect to the specified url
public class RedirectInstruction: Instruction
{
public string Url
{
get;
set;
}
public override string InstructionKind
{
get { return "Redirect"; }
}
}
// Signal the controller to return the specified view with the specified model
public class ViewInstruction: Instruction
{
public ViewInstruction()
{
}
public ViewInstruction(string viewName, object viewModel)
{
this.ViewName = viewName;
this.ViewModel = viewModel;
}
public string ViewName { get; set; }
public object ViewModel { get; set; }
public override string InstructionKind
{
get { return "View"; }
}
}
// Signal the controller to return a Http Status code.
public class StatusCodeInstruction: Instruction
{
public StatusCodeInstruction()
{
}
public StatusCodeInstruction(HttpStatusCode code)
{
StatusCode = code;
}
public HttpStatusCode StatusCode
{
get;
set;
}
public override string InstructionKind
{
get { return "StatusCode"; }
}
}
// Signal the client to do a JQuery .toggleClass('className') on the specified selector
public class ToggleClassInstruction: Instruction
{
public ToggleClassInstruction(string selector, string className)
{
Selector = selector;
ClassName = className;
}
public ToggleClassInstruction()
{
}
public string Selector
{
get;
set;
}
public string ClassName
{
get;
set;
}
public override string InstructionKind
{
get { return "ToggleClass"; }
}
}
// Signal the client to replace the inner html / value for the specified field
public sealed class ValueChangedInstruction: Instruction
{
public override string InstructionKind
{
get { return "ChangeValue"; }
}
public string PropertyIdentifier { get; set; }
// Value to use to find the control
public string Seek
{
get { return "#" + PropertyIdentifier.Replace(".", "\\."); }
}
public string Value { get; set; }
}
// Add more here...
// See standardResponse.js
}
namespace ResponseFramework
{
// An attribute used to expose a method on the view model to be called directly from JavaScript if the controller
// exposes the "InvokeModel" action.
[AttributeUsage(AttributeTargets.Method)]
public class ModelActionAttribute: Attribute
{
public ModelActionAttribute()
{
Instructions = new Instruction[] { };
}
public ModelActionAttribute(Instruction[] instructions)
{
Instructions = instructions;
}
/// <summary>
/// If the method doesn't return a <see cref="StandardResponse"/> you can override it with these instructions.
/// </summary>
public Instruction[] Instructions { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using ResponseFramework;
namespace MyNamespace.Model
{
public class ViewModelException : Exception
{
public ViewModelException(string message)
: base(message)
{
}
public ViewModelException(string message, Exception innerException)
: base(message, innerException)
{
}
}
public abstract class ViewModelBase
{
/// <summary>
/// One size fits all solution to invoke a view model method and return a complex response. The method has to be decorated with the
/// model action attribute. Called from the controller.
/// </summary>
/// <param name="methodName"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public StandardResponse Invoke(string methodName, Dictionary<string, string> parameters)
{
try
{
System.Type type = GetType();
var methodInfo = type.GetMethod(methodName);
if (methodInfo == null)
throw new ViewModelException(string.Format("That method was not found on the '{0}' view model.", methodName));
var actionDef = methodInfo.GetAttribute<ModelActionAttribute>();
if (actionDef == null)
return StandardResponse.Status(HttpStatusCode.Forbidden); // Security feature
var paramDefs = methodInfo.GetParameters();
object[] paramValues = new object[paramDefs.Length];
for (int i = 0; i < paramDefs.Length; i++)
{
var p = paramDefs[i];
if (!parameters.ContainsKey(p.Name))
throw new ViewModelException(string.Format("No parameter was specified for {0}", p.Name));
string textVal = parameters[p.Name];
paramValues[i] = ConvertUtil.Convert(textVal, p.ParameterType);
}
var res = methodInfo.Invoke(this, paramValues);
if (methodInfo.ReturnType == typeof(StandardResponse))
{
return (StandardResponse)res;
}
else if (actionDef.Instructions != null && actionDef.Instructions.Length > 0)
{
return new StandardResponse() { IsSuccess = true, Response = new StandardDetail(actionDef.Instructions) };
}
else
return new StandardResponse() { IsSuccess = true };
}
catch (TargetInvocationException ix)
{
return new StandardResponse()
{
IsSuccess = false,
ErrorMessage = ix.InnerException.GetFullMessage()
};
}
catch (Exception ex)
{
return new StandardResponse()
{
IsSuccess = false,
ErrorMessage = ex.GetFullMessage()
};
}
}
}
}
public class MyViewModel: ViewModelBase
{
[ModelAction]
public StandardResponse PostDocumentComment(int empId, int documentId, string comments)
{
MyContext ctx = new MyContext();
var me = ctx.Employees.FirstOrDefault(e => e.Id == empId);
if (me == null)
return StandardResponse.Error("Could not locate employee.");
var doc = ctx.Documents.FirstOrDefault(d => d.Id == documentId);
if (doc == null)
return StandardResponse.Error("Could not locate Document.");
doc.Comments.Add(new Comment()
{
CreatedDate = DateTime.Now,
DocumentId = doc.Id,
Document = doc,
EmployeeId = me.Id,
Employee = me,
Comments = comments
});
ctx.SaveChanges();
return StandardResponse.View("_DocumentComments", doc);
}
[ModelAction]
public StandardResponse TogglePriority(int docId)
{
MyContext ctx = new MyContext();
var inst = ctx.Documents.FirstOrDefault(d => d.Id == docId);
if (inst != null)
{
inst.UrgentFlag = !def.UrgentFlag;
ctx.SaveChanges();
return StandardResponse.Create(true,
new ToggleClassInstruction("#doc-details .priority,.bnToolbar.bnPriority", "on"),
new ToggleClassInstruction(string.Format("#doc-list .items-list ul li[data-id={0}] span.icon-star", def.Id), "on"));
}
else
{
return StandardResponse.Error("Could not locate document!");
}
}
}
}
using MyNamespace.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Security;
using System.Web.Mvc;
using System.Diagnostics;
using System.Collections;
using System.Collections.Specialized;
namespace MyNamespace.Controllers
{
public abstract class Controller: System.Web.Mvc.Controller
{
// Method to call to bubble up the "invoke" to the view model and invoke a "ModelAction" with a "StandardResponse" result.
protected ActionResult InvokeModel(MyViewModelBase viewModel, string id, string key, string methodName)
{
try
{
Dictionary<string, string> values = new Dictionary<string, string>();
AddValues(values, Request.QueryString);
AddValues(values, Request.Form);
var response = viewModel.Invoke(methodName, values);
return ActionFromResponse(response);
}
catch (Exception ex)
{
return new HttpNotFoundResult(ex.Message);
}
}
protected ActionResult ActionFromResponse(StandardResponse response)
{
if (response.IsSuccess)
{
var statusCode = response.GetInstruction<StatusCodeInstruction>();
if (statusCode != null)
return new HttpStatusCodeResult(statusCode.StatusCode);
var view = response.GetInstruction<ViewInstruction>();
if (view != null)
return View(view.ViewName, view.ViewModel);
}
return new JsonResult()
{
Data = response,
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
private void AddValues(Dictionary<string, string> values, NameValueCollection collection)
{
foreach (string k in collection)
{
if (!values.ContainsKey(k))
{
values.Add(k, collection[k]);
}
}
}
}
}
function processStandardResponse(response) {
if (typeof(response) == "string") // If we can't read it, it was handled on the server
return false;
if (!response || !response.IsSuccess)
{
if (debugMode)
alert(response.ErrorMessage); // Show a nice message box here
return true;
}
// No instructions
if (response.Response == null || response.Response.Instructions == null)
return false;
// FIre instructions
for (var i = 0; i < response.Response.Instructions.length; i++) {
var instruction = response.Response.Instructions[i];
switch(instruction.InstructionKind)
{
case "ChangeValue":
processChangeValueInstruction(instruction);
break;
case "ToggleClass":
processToggleClassInstruction(instruction);
break;
// TODO - More client side instructions here see Instructions.cs
}
}
return response.Response.Instructions.length > 0;
}
function processChangeValueInstruction(instruction) {
if (instruction.PropertyIdentifier == null)
return;
var $e = $(instruction.Seek);
if ($e != null)
$e.html(instruction.Value);
}
function processToggleClassInstruction(instruction) {
var $e = $(instruction.Selector);
if ($e.length > 0) {
$e.toggleClass(instruction.ClassName);
}
}
function invokeModel(methodName, data, success) {
var url = window.location.pathname + "/invokemodel?key=" + $("input#key").val() + '&methodName='+methodName;
$.ajax({
type: 'POST',
url: url,
data: data,
cache: false,
success: function (response) {
if (processStandardResponse(response))
return;
success(response);
},
error: function (jqXHR, textStatus, errorThrown) {
alert(jqXHR + "> " + textStatus + "> " + errorThrown);
}
});
}
function examples()
{
invokeModel("TogglePriority", { detailId: $('#key').val() });
invokeModel('PostDocumentComment',
{
empId: empId,
documentId: docId,
comments: $('input.new-doc-comment').val()
},
function (response) {
$('div#def-comment-section').html(response);
$('input.new-def-comment').val('');
hookDocumentCommentEvents(empId, docId);
});
}
@morbidcamel101
Copy link
Author

I developed this little framework and it reduced the amount of coding I did for MVC and client side JavaScript significantly. Essentially I can just add what action I need on the model and return instructions as to how to adjust the view. Simple, but efficient.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment