Monday, April 29, 2013

Display message on client from anywhere in code with ASP.NET


When I was working with WinForms application in previous work I have found one of most useful feature, ability to show some message to the client. For example user cannot edit some entity in particular time (it is being edited by someone else) or should change pasword because it will exprire in x days. I wanted to do something similar in my web application.

To do that I started with creating control with html code of new message window. Sure you can use just JS alert() function, but I find this really annoying. So instead I used jQuery UI dialog.
<script language="javascript" type="text/javascript">
    function showMessage(message) {
        if (typeof (message) !== "undefined" && message) {
            jQuery('#messageContent').html(message);
            jQuery('#messageDialog').dialog({
                width: 600,
                height: 300
            });
        }
    };
</script>
<div id="messageDialog" class="hidden">
    <div class="ui-widget center height100per width100per">
        <div style="padding: 0 .7em;" class="ui-state-highlight ui-corner-all">
            <p>
                <span style="float: left; margin-right: .3em;" class="ui-icon ui-icon-info"></span>
                <strong>Information:</strong> <span id="messageContent"></span>
            </p>
        </div>
    </div>
</div>

I decided to put this control called WUC_MessageDoalog in Site.Master file. That way JS function showMessage will be available for all pages using master page (in my application that was all desired pages). If it is not enough, it is possible to inject this control and JS code into page whenever it will be needed, but it was not necessary in my case.

Now we can execute showMessage function when we need to show some message. To do that we have to usePage.RegisterStartupScript method.

Page.ClientScript.RegisterStartupScript(page.GetType(), 
  key, string.Format("<script type='text/javascript'>{0}</script>", script));


To abstract this code an made it available in whole application I made extension method of Page type.
public static void AddStartupScripts(this Page page, string key, string script)
{
    if (!page.ClientScript.IsStartupScriptRegistered(key))
    {
        page.ClientScript.RegisterStartupScript(page.GetType(), key,
            string.Format("<script type='text/javascript'>{0}</script>", script));
    }
}

After using this function it will render <script type="text/javascript"> tag with our desired code. To executeshowMessage we have to insert JS code to our AddStartupScripts method:

jQuery().ready(function () {
    {
        showMessage("I am a message!");
    }
});

I made another extension method from this.

public static void AddClientReadyScript(this Page page, string key, string script)
{
    page.AddStartupScripts(key, string.Format(
        @"jQuery().ready(function () {{
            {0}
        }});", script));
}


But still it will only work for full HTTP Postback. How to make this work with AJAX? Things are more trickier from here. To execute JS code after UpdatePanel AJAX PostBack, we have to use methodScriptManager.RegisterClientScriptBlock.

I encapsulated this in similar way like AddStartupScripts method.


public static void AddUpdateScript(this UpdatePanel panel, string key, string script)
{
    if (!panel.Page.ClientScript.IsClientScriptBlockRegistered(key))
    {
        ScriptManager.RegisterClientScriptBlock(
            panel, typeof(UpdatePanel), key, script, true);
    }
}

JS code injected into page like this does not need further encapsulating so we leave it like that. But we still have to know which UpdatePanel will be updated to inject JS into it. How to do that? Sadly there is no easy solution. After some Google research, and few hours I managed to create another extension method (yes I like them a lot!):

public static UpdatePanel FindAsyncPostBackUpdatePanel(this Page page)
{
    var scriptManager = ScriptManager.GetCurrent(page);
    var pageRequestMngr = scriptManager.GetNonPublicProperty<object>("PageRequestManager");
    var updatePanels = pageRequestMngr.GetNonPublicField<List<UpdatePanel>>("_allUpdatePanels");
    var source = page.FindControl(scriptManager.AsyncPostBackSourceElementID);
    foreach (var up in updatePanels)
    {
        foreach (var trigger in up.Triggers)
        {
            var t = trigger as AsyncPostBackTrigger;
            if (t == null)
            {
                continue;
            }
            if (t.ControlID == source.ID)
            {
                return up;
            }
        }
    }
    return null;
}


If you wondering what are GetNonPublicField and GetNonPublicProperty methods do you can find about the here: post/2012/12/09/Getting-private-field-or-property-from-object.aspx
I found this working fine for me, but still it have some obvious drawbacks:
  1. It founds update panel only by its explicitly defined triggers. So it will not work for triggers that are children of update panel. Still I recommend you to not do that. Nested panels with ChildrensAsTriggers = true will cause to update, panel which is most higher in controls tree hierarchy. So biggest update panel (for example containing whole page) will update. Why even bother with nested panels in that case or even AJAX at all then?
  2. It uses control id that is not unique. When you will use not unique ids for your controls (i.e. button1, textbox1), you will end up adding script to first update panel that will pop up. And it will not work. But if you writing your site like this I bet that you have bigger problems :)
  3. It will not work with RegisterAsyncPostBackControl method and control outside of panel. But if it is static relation it can be placed inside of update panel triggers collection.
Anyway, with this method mostly working we can write yet another extension to Page:

public static void ShowClientMessage(this Page page, string message)
{
    if (page.Master != null)
    {
        var siteMasterDialog = page.Master.FindControl("wucMessageDialog");
        const string messageScriptKey = "clientDialogMessage";
        if (siteMasterDialog != null)
        {
            var jsCode = string.Format("showMessage('{0}');", message.Replace("'", "\\'"));
            if (page.IsPostBack && ScriptManager.GetCurrent(page).IsInAsyncPostBack)
            {
                var up = page.FindAsyncPostBackUpdatePanel();
                up.AddUpdateScript(messageScriptKey, jsCode);
            }
            else
            {
                page.AddClientReadyScript(messageScriptKey, jsCode);
            }
        }
    }
}

And finally with right tools we can abstract this further to stand alone class. If we will be using only ASP.NET Page handlers, current page instance can be obtain from HTTPContext object via Handler property:


public class ClientMessage
{
    public void ShowWarning(string message)
    {
        var page = ((Page)HttpContext.Current.Handler);
        page.ShowClientMessage(message);
    }
}


That is all about magic. To show message from some code in our site just call:
new ClientMessage().ShowWarning("I am message!");



No comments:

Post a Comment