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

13 comments:

  1. Congratulations, very nice article.

    Could you put your code or control to download?

    Thanks

    ReplyDelete
  2. Hi Agnaldo,

    Thanks very much for the kind words!
    I've updated the post with a link to the code.

    Thanks

    ReplyDelete
  3. Hi Javi,

    thanks for your excellent post. I have altered the code slightly so that it also can be used with dropdownlist controls. I hope you don't mind.

    ReplyDelete
  4. Hi Ralph,

    You can do whatever you want with the code, I'm not gonna sue you for that :)
    Thanks for the kind words!

    ReplyDelete
  5. Thank Javi,
    Excellent post, I was looking for something along these lines, your post saved me a bunch of time.
    Thanks,
    -Girish

    ReplyDelete
  6. setting Message in the control gives the error.
    Am trying to use linkbutton in the gridview and am unable to set message.

    ReplyDelete
  7. Hi,
    I faced problem with adding "smple:ConfirmationButton ID= "btn1" to my page. please help me to add this custom button to my page. problem was adding Assembly to my web config.

    thnks,
    -dinu

    ReplyDelete
  8. Hey Dinu,

    I recommend that you download the sample project and take a look in there, you should be able to figure it out on your own

    Hope this helps,

    Javier

    ReplyDelete
  9. Your code is brilliant.
    But I have a problem with a ConfirmLinkButton inside a ListView control which itself is part of an update panel.
    The first click works fine and the chosen element disappears (due to business logic). When I click a second item it removes the second item before I can grant or decline it. The modal popup appears regardless and I can see in the background that the second item is already removed from the list.

    Any ideas?

    Tobey

    ReplyDelete
  10. Hi! Thanks a lot for your code. I have also problems in an UpdatePanel.
    I send you along my code for you as a sample:

    <%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
    CodeBehind="Sample.aspx.cs" Inherits="ConfirmButtonSample.Sample" %>










    <%div id="phContact" runat="server">



    <%table>
    <%tr>
    <%td class="column1">

    <%# Eval("Name") %>

    <%/td>
    <%td>



    <%/td>

    <%/tr>







    using System;
    using System.Web.UI.WebControls;
    using ConfirmButtonSample.Utils;

    namespace ConfirmButtonSample
    {
    public partial class Sample : System.Web.UI.Page
    {
    protected void ConfirmLinkButton_Click(object sender, EventArgs e)
    {
    ConfirmButtonSample.Controls.ConfirmLinkButton cl = (ConfirmButtonSample.Controls.ConfirmLinkButton)sender;
    DataKey key = lvContactsDirectory.DataKeys[((ListViewDataItem)cl.NamingContainer).DataItemIndex];
    string name = key["Name"].ToString();
    PersonManager.HidePerson(name);
    lvContactsDirectory.DataBind();
    }

    }

    }


    namespace ConfirmButtonSample.Utils
    {
    ///
    /// Page Extensions
    ///
    public class PersonManager
    {
    private static List _personList = new List()
    {
    new Person(){ Name = "P1"},
    new Person(){ Name = "P2"},
    new Person(){ Name = "P3"}
    };

    public static List Get()
    {


    return _personList;
    }

    public static void HidePerson(string name)
    {
    var x = (from p in _personList where p.Name == name select p).First();
    _personList.Remove(x);
    }
    }

    public class Person
    {
    public string Name
    {
    get;
    set;
    }
    }
    }

    Try to hide one item (which works) and then try to hide another one...

    Thanks a lot in advance.
    Tom

    ReplyDelete
  11. Is it possible to get UPDATE #2 code? i have also problem with updatepanel.

    Thx

    ReplyDelete
  12. Good! Very good! Thanks, this help me!

    ReplyDelete
  13. i've tested it and seems really cool. thanks for it

    ReplyDelete