A clever plugin for jquery recently was released which is an impression of Mac's Growl. It simply displays messages for a period of time or makes them 'sticky'. It also manages multiple messages at a time very nicely and all the styling is external so a real snap to customize. The best part is how simply it is implemented. You simply call
jQuery.jGrowl(\"My Message Should Appear\", options );
and you get a popup that gradually fades. Fire off two in quick succession and jgrowl is smart enough to fade them appropriately and provide a global 'close' bar to clear all messages.
On all projects it seems like the most messages I need to communicate to my users lump into similar categories; 'errors','warnings','messages','success','failure', and so on. Typically these are thrown in either a Flash or PropertyBag collection for consumption by the view.
I wanted to simplify how I am passing messages to my users regardless of the request type (Ajax/Non) and regardless of whether I use JS Generation to render some view , so I narrowed down to requiring some filter which would handle all calls and inspect specific collections that I can configure that may or may not be populated with messages during the action. This will either append the messages to our http response directly, or using an TransformFilter implementation (an HttpFilter) - more on that later. Using a filter this way let's me avoid having to drop anything on my layout or require any kind of view code. I simply attach this filter to my controller base and it just works.
First, let's determine what our notifications are:
public class GrowlNotification
{
private readonly string key = string.Empty;
private IDictionary options;
public GrowlNotification(string key)
: this(key, new HybridDictionary())
{
}
public GrowlNotification(string key, IDictionary options)
{
this.key = key;
this.options = options;
}
public string Key
{
get { return key; }
}
public IDictionary Options
{
get
{
options = options ?? new HybridDictionary();
HybridDictionary defaultOptions = new HybridDictionary();
defaultOptions.Add("sticky", "false");
defaultOptions.Add("theme", "\""+GetTheme()+"\"");
defaultOptions.Add("speed", "\"slow\"");
CommonUtils.MergeOptions(options, defaultOptions);
return options;
}
}
private string GetTheme()
{
if(string.IsNullOrEmpty(key))
{
return "default";
}
return key;
}
}
'Key' is simply a corresponding value with the key of the Flash/PropertyBag being inspected by the Filter after the action. jGrowl lets us pass a 'theme' to attach an additional class name to the message container on the client side so it is real easy to modify how each message appears using simple css. This will correspond with the 'Key' name.
Options are obviously the js options jgrowl supports. Each message type can have its own options. So you can, for example,make 'errors' sticky and all other message types fade out after a bit.
Now, let's encapsulate the configuration and make it accessible on the container but provide an sensible default configuration. This configuration will create the GrowlNotifications for the IGrowlGenerator at runtime:
public interface IGrowlConfiguration
{
/// <summary>
/// Gets the notifications to inject.
/// </summary>
/// <returns></returns>
IEnumerable<GrowlNotification> GetNotifications();
/// <summary>
/// Determines whether this the current request is an xhr.
/// </summary>
/// <returns>
/// <c>true</c> if this instance is ajax; otherwise, <c>false</c>.
/// </returns>
bool IsAjax();
}
'IsAjax()' simply informs the filter of the request type so that it can handle the response message(s) appropriately. This can be simply sniffing the headers, or using a more elaborate extension I wrote but is outside the scope here.
Now it's default implementation:
/// <summary>
/// Default configuration for keys <see cref="GrowlFilter"/> will sniff to inject. It assumes 'errors' and 'success'
/// are intended message keys.
/// </summary>
public class DefaultGrowlConfiguration : IGrowlConfiguration
{
public virtual IEnumerable<GrowlNotification> GetNotifications()
{
HybridDictionary errorOptions = new HybridDictionary();
errorOptions.Add("sticky", "true");
return new GrowlNotification[]
{
new GrowlNotification("errors", errorOptions),
new GrowlNotification("success"),
new GrowlNotification("failure",errorOptions),
new GrowlNotification("message"),
};
}
/// <summary>
/// Determines whether this the current request is an xhr. This assumes the <see cref="AjaxRequestExtension"/> is being
/// used for request evaluation at runtime.
/// </summary>
/// <returns>
/// <c>true</c> if this instance is ajax; otherwise, <c>false</c>.
/// </returns>
public virtual bool IsAjax()
{
object isAjax = MonoRailHttpHandlerFactory.CurrentEngineContext.Flash[AjaxRequestExtension.IsAjaxKey];
if (isAjax != null)
{
return (bool) isAjax;
}
return false;
}
}
Next, we'll separate out how the jgrowl is generated and provide a default implementation. This will be delegated to within the Filter to write the actual javascript using the configuration:
public interface IGrowlGenerator
{
string GenerateFrom(IEngineContext context, IController controller, IControllerContext controllerContext);
}
public class DefaultGrowlGenerator : IGrowlGenerator
{
private readonly IGrowlConfiguration growlConfiguration;
public DefaultGrowlGenerator(IGrowlConfiguration growlConfiguration)
{
this.growlConfiguration = growlConfiguration;
}
public string GenerateFrom(IEngineContext context, IController controller, IControllerContext controllerContext)
{
if( growlConfiguration == null )
{
throw new ArgumentNullException("growlConfiguration");
}
StringBuilder scriptContents = new StringBuilder();
bool hasContent = false;
IEnumerable<GrowlNotification> notifications = growlConfiguration.GetNotifications();
foreach (GrowlNotification item in notifications ?? new GrowlNotification[0])
{
//look in property bag first for message
object propBagMsg = controllerContext.PropertyBag[item.Key];
object flashMsg = context.Flash[item.Key];
string msg = string.Empty;
if (propBagMsg != null)
{
msg = msg + propBagMsg ?? string.Empty;
}
if (flashMsg != null)
{
msg = msg + flashMsg ?? string.Empty;
}
if (!string.IsNullOrEmpty(msg))
{
hasContent = true;
string options = AbstractHelper.JavascriptOptions(item.Options);
scriptContents.AppendFormat("jQuery.jGrowl(\"{0}\", {1} );", msg, options);
}
}
if (hasContent)
{
string inner = JSUtils.WrapWithTryCatch(scriptContents.ToString());
return "jQuery(document).ready(function() { " + inner + "});";
}
return string.Empty;
}
}
It's worth noting here that this assumes you are expecting the dataType of 'script' in your jquery ajax calls. I set a global using
$.ajaxSetup({ dataType:"script" });//all calls are returned as js to evaluate
We can finally implement an Monorail filter that will consume all these services and write the appropriate javascript to our response for jgrowl to handle. Here's the filter:
public class GrowlFilter : Filter
{
private bool prependToBodyCloseTag = false;
private string growl = string.Empty;
private IGrowlConfiguration growlConfiguration = new DefaultGrowlConfiguration();
private IGrowlGenerator growlGenerator;
private ModifyResponse responseModifier;
/// <summary>
/// Gets or sets the growl configuration.
/// </summary>
/// <value>The growl configuration.</value>
public virtual IGrowlConfiguration GrowlConfiguration
{
get { return growlConfiguration; }
set { growlConfiguration = value; }
}
/// <summary>
/// Gets or sets the response modifier.
/// </summary>
/// <value>The response modifier.</value>
public virtual ModifyResponse ResponseModifier
{
get
{
if(responseModifier==null)
{
responseModifier = DoPrependBodyCloseTag;
}
return responseModifier;
}
set { responseModifier = value; }
}
/// <summary>
/// Gets or sets the growl generator.
/// </summary>
/// <value>The growl generator.</value>
public IGrowlGenerator GrowlGenerator
{
get
{
if(growlGenerator==null)
{
if(growlConfiguration==null)
{
throw new ArgumentNullException("growlConfiguration","GrowlConfiguration must assigned before creating the DefaultGrowlGenerator. ");
}
growlGenerator = new DefaultGrowlGenerator(growlConfiguration);
}
return growlGenerator;
}
set { growlGenerator = value; }
}
/// <summary>
/// Gets a value indicating whether to prepend to body close tag, as in the case of a standard request.
/// </summary>
/// <value>
/// <c>true</c> if [prepend to body close tag]; otherwise, <c>false</c>.
/// </value>
public virtual bool PrependToBodyCloseTag
{
get { return prependToBodyCloseTag; }
}
/// <summary>
/// Override this method if the filter was set to
/// handle <see cref="F:Castle.MonoRail.Framework.ExecuteWhen.AfterAction"/>
/// </summary>
/// <param name="context">The MonoRail request context</param>
/// <param name="controller">The controller instance</param>
/// <param name="controllerContext">The controller context.</param>
protected override void OnAfterAction(IEngineContext context, IController controller,
IControllerContext controllerContext)
{
if (growlConfiguration == null)
{
throw new ArgumentNullException("growlConfiguration",
"Growl requires either an implementation of IGrowlConfiguration in the container, or using the DefaultGrowlConfiguration without any registration.");
}
if( GrowlGenerator == null)
{
throw new ArgumentNullException("growlGenerator");
}
growl = GrowlGenerator.GenerateFrom(context, controller, controllerContext);
if (growlConfiguration.IsAjax())
{
//this is an xhr so assume jquery.ajax is expecting dataType:'script' and write javascript to stream
if (controllerContext.SelectedViewName !=null && !controllerContext.SelectedViewName.EndsWith("js"))
{
controllerContext.SelectedViewName = null;
}
context.Response.Output.Write(growl);
}
else
{
prependToBodyCloseTag = true;
}
}
/// <summary>
/// Does the prepending to the body close tag.
/// </summary>
/// <param name="responseText">The response text.</param>
/// <returns></returns>
public virtual string DoPrependBodyCloseTag(string responseText)
{
return responseText.Replace("</body>", AbstractHelper.ScriptBlock(growl) + "</body>");
}
/// <summary>
/// Override this method if the filter was set to
/// handle <see cref="F:Castle.MonoRail.Framework.ExecuteWhen.AfterRendering"/>
/// </summary>
/// <param name="context">The MonoRail request context</param>
/// <param name="controller">The controller instance</param>
/// <param name="controllerContext">The controller context.</param>
protected override void OnAfterRendering(IEngineContext context, IController controller,
IControllerContext controllerContext)
{
if (prependToBodyCloseTag)
{
context.UnderlyingContext.Response.Filter =
new ModifyResponseFilter( context.UnderlyingContext.Response.Filter,ResponseModifier);
}
}
}
After the action, this filter determines whether to write the javascript directy to the response in the event of an ajax call, or whether to use an HttpFilter to prepend the javascript to the </body>. Here's the httpfilter being used to modify response output.It accepts an delegate that I can override in my filter if I want the javascript to appear somewhere else on my page:
/// <summary>
/// Delegate for modifying the contents of the response. Passes in the current response text for manipulation and
/// requires the final response to be write back into the stream.
/// </summary>
public delegate string ModifyResponse(string responseText);
/// <summary>
/// For intercepting the response and manipulating it with the <see cref="ModifyResponse"/> passed in.
/// </summary>
public class ModifyResponseFilter : TransformFilter
{
private readonly ModifyResponse responseModifier;
/// <summary>
/// Initializes a new instance of the <see cref="ModifyResponseFilter"/> class.
/// </summary>
/// <param name="baseStream">The base stream.</param>
/// <param name="responseModifier">The response modifier.</param>
public ModifyResponseFilter( Stream baseStream, ModifyResponse responseModifier):base(baseStream)
{
this.responseModifier = responseModifier;
}
public override void Write(byte[] buffer, int offset, int count)
{
if (Closed) throw new ObjectDisposedException("ModifyResponseFilter");
if(responseModifier== null)
{
throw new ArgumentNullException("responseModifier");
}
//Get a string version of the buffer
string content = responseModifier(Encoding.Default.GetString(buffer, offset, count));
byte[] newOutput = Encoding.Default.GetBytes(content);
BaseStream.Write(newOutput, 0, newOutput.Length);
}
}
Finally here's a screenshot of it in action after an ajax call. During the call I had placed a message in Flash["success"] and now it is displaying (in the gray box) that message to my user without me having to handle anything on the client side or write any view code. Simple.
Hopefully this provides some ideas on how to communicate with users about actions they perform.
Posted
05-01-2008 12:51 AM
by
Michael Nichols