Sunday, December 14, 2008

ASP.NET Confirmation Button using JQuery

Quite often in business applications the use of confirmation modal popups is required in order to get confirmation from the user of a specific action he’s going to carry out. In web environment the “gorgeous” built-in javascript confirm function is a valid option, but we are living in the XXI century and we can do much better! Particularly with JQuery and the SimpleModal plug-in we can achieve something like this with very little effort:

Confirmation

So, let's get started! First of all we’ll download jQuery js file, SimpleModal js file and css file and add references to them in our master page:

<script src="/Sample/Scripts/jquery-1.2.6.js" type="text/javascript"></script>
<script src="/Sample/Scripts/SimpleModal/jquery.simplemodal.js" type="text/javascript"></script>
<link type='text/css' href='/Sample/Scripts/SimpleModal/css/confirm.css' rel='stylesheet' />

Then we can create the actual html that will be used as modal popup. The html will be reused in any call to the jQuery plugin so we’ll put it in the master page anywhere within the body tag.
 
<div id='confirm' style='display: none'>
    <div class='header'>
        <asp:Literal ID="ConfirmLiteral" runat="server" 
            Text="<%$ Resources:Global, Confirm %>"/>
    </div>
    <p class='message'></p>
    <div class='buttons'>
        <div class='no modalClose'>
            <asp:Literal ID="NoLiteral" runat="server" 
                Text="<%$ Resources:Global, No %>"/>
            </div>
            <div class='yes'>
                <asp:Literal ID="YesLiteral" runat="server" 
                    Text="<%$ Resources:Global, Yes %>"/>
            </div>
    </div>
</div>

If you don’t need to localize the popup, of course you can get rid of the resources and the Literal controls. Now we can create the javascript function that uses the plugin to show the modal popup.

function confirm(message, callbackYes) {
   $('#confirm').modal({
        close:false, 
        overlayId:'confirmModalOverlay',
        containerId:'confirmModalContainer', 
        onShow: function (dialog) {
            dialog.data.find('.message').append(message);
            // if the user clicks "yes"
            dialog.data.find('.yes').click(function () {
                // call the callback
                if ($.isFunction(callbackYes)) {
                    callbackYes.apply();
                }  
                // close the dialog
                $.modal.close();
            });

We already have the means to show the popup but we still need to trigger it on button clicked.

var Page_IsValid = true;
function SetConfirmation(buttonId, message)
{
    $("#" + buttonId).click(function(e) {
        //We keep the original click event so
        //it can be executed as a callback
        var target = $(e.target);
        //We prevent the form from posting back
        e.preventDefault();
        if(Page_IsValid)
        {
            confirm(message,function() {target.click();});
        }
    });
}

The SimpleModal plugin provides asynchronous and non-blocking functionally (not like the builtin javascript confirm) and that means that we need to prevent the original button’s click event from happening (e.preventDefault();) but at the same time we need to keep it as a callback function to be used in case the user confirms the action to be executed (e.target.click();).

If we you are using ASP.NET validators you might want to show the confirmation pop up only the page is valid (Page_IsValid), there is no point of asking for confirmation if the action cannot be carried out anyways! The variable initialization outside the function guarantees that our function won’t break if there is no ASP.NET validator in the page.

Well, client-side wise we are done, here it goes a usage sample.

SetConfirmation('<%= myButton.ClientID %>','Are you sure of this?');

Let’s focus on server-side now, is there anything we can do? Well, we can create a asp.net control that hides the javascript event subscription to the consumer, so dropping this line in the Page/UserControl markup would be enough to have the confirmation button functionality all in a single place.

<sample:ConfirmButton ID="actionButton" runat="server" Text="<%$ Resources:Global, Action %>"
 OnClick="actionButton_Click" Message="<%$ Resources:Global, ConfirmationMessage %>" />

Here goes the implementation of the ConfirmButton control

public class ConfirmButton : Button
{
    public string Message
    {
        get;
        set;
    }
 
    protected override void OnPreRender(EventArgs e)
    {
        base.OnPreRender(e);
        if (!String.IsNullOrEmpty(this.Message))
        {
            string script = String.Format("SetConfirmation('{0}','{1}');", this.ClientID, this.Message);
            if (Page.IsAsyncPostBack())
            {
                ScriptManager.RegisterStartupScript(Page, typeof(Page), this.ClientID, script, true);
            }
            else
            {
                Page.ClientScript.RegisterStartupScript(type, this.ClientID, script , true);
            }
        }
    }
}

A few things to point out in the implementation: the javascript call is only rendered if a message text has been assigned to the control, which means that we could potentially use the ConfirmButton as our regular button and provide the confirmation message only when needed. The second thing is that we could extract a helper method from the logic in the OnPreRender method, so we could use this helper method to set confirmations programmatically upon any sort of button control (Button, ImageButton, LinkButton). Finally, note that the ScriptManager has to be used to register the javascript in the page when the control is rendered as a part of an UpdatePanel partial postback. The following page extension method is defined to identify those scenarios.

public static class PageExtensions
{
    public static bool IsAsyncPostBack(this Page page)
    {
        var result = false;
        var scriptManager = ScriptManager.GetCurrent(page);
        if(scriptManager != null)
        {
            result = scriptManager.IsInAsyncPostBack;
        }
        return result;
    }
}

And that's it, I hope this helps!

UPDATE: You can find the code of this post here

Besides, I've added some code to enable confirmation popup on link click and I've fixed a bug regarding validation.

UPDATE #2:I've updated the javascript code in order to fix an issue that I ran into when using the confirmation buttons with Update Panels (The javascript event handler binder was getting executed on every partial post back which was causing some issues with the popup callback. Check out the implementation for more details, all the changes I did are documented in the js file)

kick it on DotNetKicks.com

Monday, December 8, 2008

Team Build notification on test failed

A nice feature in a Continuous Integration environment is the ability to notify developers (or even managers yuk!) when a build breaks. The command Biisubscribe in TFS 2008 allows to hook up email alerts on build completion, for example this is how we can configure email notifications on build failure due to compilation errors:

bissubscribe /eventType BuildCompletionEvent /address myemail@domain.com
/deliveryType EmailPlaintext /server tfsserver1
/filter "TeamProject = 'MyTeamProject' AND CompletionStatus='Failed'

If you are running tests as a part of the build process though, you might want to take it to the next level and send notifications when any of your tests has failed. Tests are one of the most important pieces of a Continuous integration environment and as such they deserve to be taken in consideration in the notification process :)

In TFS 2008 “Failed” builds are builds that have failed during compilation time while “Partial Succeeded” builds are builds that have gone through the compilation process correctly but have failed in any of the post build actions… such as the test runner! So we have all the info needed to modify the filter from the previous command and get notifications when our tests fail :

bissubscribe /eventType BuildCompletionEvent /address myemail@domain.com
/deliveryType EmailPlaintext /server tfsserver1
/filter "TeamProject = 'MyTeamProject' AND
(CompletionStatus='Failed' OR CompletionStatus='Partially Succeeded')“

Hope this helps!

kick it on DotNetKicks.com