Images in this post missing? We recently lost them in a site migration. We're working to restore these as you read this. Should you need an image in an emergency, please contact us at imagehelp@codebetter.com
Unobtrusive Growl Messaging Using Monorail Filter

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.

growl

 

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

[Advertisement]

Comments

skwr wrote re: Unobtrusive Growl Messaging Using Monorail Filter
on 05-01-2008 5:32 AM

The devlicious feed strips most of the newlines and formatting in your post.

Mike wrote re: Unobtrusive Growl Messaging Using Monorail Filter
on 05-02-2008 1:51 AM

I corrected the formatting...thanks.

Pages tagged "unobtrusive" wrote Pages tagged "unobtrusive"
on 05-05-2008 12:00 AM

Pingback from  Pages tagged "unobtrusive"

yorch wrote re: Unobtrusive Growl Messaging Using Monorail Filter
on 09-16-2008 6:18 PM

Hi,

Looks very nice. I'm trying to use it, so I have succesfully migrated to trunk.. the only thing left is that I can't figure where does JSUtils.WrapWithTryCatch come from? Is JSUtils an 3rd party C# extension?

Thanks!

yorch.

Mike wrote re: Unobtrusive Growl Messaging Using Monorail Filter
on 09-16-2008 7:33 PM

@yorch,

I have made a numer of fixes to this component since this posting...let me know if you need it.

One thing you should note too is that jQuery is lame at interpolating incoming request types so I have a patch for the httpdata method there as well..I'll post on that soon.

JSUtils is something I have in my commons library :

/// <summary>

/// Wraps the <c>contents</c> with a try...catch block for javascript

/// </summary>

/// <param name="contents">The contents.</param>

/// <returns></returns>

public static string WrapWithTryCatch(string contents)

{

return

string.Format(

"try {0}{{{0} {1} {0}}}{0}catch(e){0}{{{0}alert('JS error ' + e.toString());{0}}}",

Environment.NewLine, contents);

}

yorch wrote re: Unobtrusive Growl Messaging Using Monorail Filter
on 01-20-2009 4:25 PM

Thanks Mike!

It would be great if you could post the updated component!

Julio wrote re: Unobtrusive Growl Messaging Using Monorail Filter
on 01-31-2009 8:43 PM

It looks great!!

Can you supply the entire code in zip format please?

Thanks in advance

tz wrote re: Unobtrusive Growl Messaging Using Monorail Filter
on 09-18-2009 1:17 PM

Hello Mike,

Thank you for sharing this code. It works very well.

Do you have any elegant way to tell if an Ajax request is used to execute code or load a form into a div? As of now I'm just sticking an underscore before the views I want to load by Ajax and then using StartsWith.

Michael Nichols wrote re: Unobtrusive Growl Messaging Using Monorail Filter
on 09-18-2009 1:27 PM
tz wrote re: Unobtrusive Growl Messaging Using Monorail Filter
on 09-18-2009 2:51 PM

Thanks for replying!

Sorry, let me clarify myself. I meant I have both your AjaxFilter and GrowlFilter on a controller. How do I some actions return the default view on an Ajax request and the rest just the Growl script?

Michael Nichols wrote re: Unobtrusive Growl Messaging Using Monorail Filter
on 09-18-2009 9:42 PM

@tz

I believe the Growl Filter only responds to messages in the Flash/propertybag containers you configure. So why not just avoid those specific keys when you want to do a view without growling?

About The CodeBetter.Com Blog Network
CodeBetter.Com FAQ

Our Mission

Advertisers should contact Brendan

Subscribe
Google Reader or Homepage

del.icio.us CodeBetter.com Latest Items
Add to My Yahoo!
Subscribe with Bloglines
Subscribe in NewsGator Online
Subscribe with myFeedster
Add to My AOL
Furl CodeBetter.com Latest Items
Subscribe in Rojo

Member Projects
DimeCasts.Net - Derik Whittaker

Friends of Devlicio.us
Red-Gate Tools For SQL and .NET

NDepend

SlickEdit
 
SmartInspect .NET Logging
NGEDIT: ViEmu and Codekana
LiteAccounting.Com
DevExpress
Fixx
NHibernate Profiler
Unfuddle
Balsamiq Mockups
Scrumy
JetBrains - ReSharper
Umbraco
NServiceBus
RavenDb
Web Sequence Diagrams
Ducksboard<-- NEW Friend!

 



Site Copyright © 2007 CodeBetter.Com
Content Copyright Individual Bloggers

 

Community Server (Commercial Edition)