Contents tagged with AJAX

  • Taking Picture Snapshots with ASP.NET and HTML5

    This is another post on HTML5 and ASP.NET integration. HTML5 brought along with it a number of great JavaScript APIs; one of them is getUserMedia, which is W3C standard and you can get a good description of it in MDN. In a nutshell, it allows access to the PC’s camera and microphone. Unfortunately, but not unexpectedly, it is not supported by any version of Internet Explorer, but Chrome and Firefox have it.

    I wanted to be able to capture a snapshot and to upload it in AJAX style. Of course, if you know me you can guess I wanted this with ASP.NET Web Forms and client callbacks! Smile

    OK, so I came up with this markup:

       1: <web:PictureSnapshot runat="server" ID="picture" Width="400px" Height="400px" ClientIDMode="Static" OnPictureTaken="OnPictureTaken"/>

    The properties and events worth notice are:

    • Width & Height: should be self explanatory;
    • PictureTaken: a handler for a server-side .NET event that is raised when the picture is sent to the server asynchronously.

    The PictureSnapshot control exposes a number of JavaScript methods:

    • startCapture: when called, starts displaying the output of the camera in real time;
    • stopCapture: pauses the update of the camera;
    • takeSnapshot: takes a snapshot of the current camera output (as visible on screen) and sends it asynchronously to the server, raising the PictureTaken event.

    When the startCapture method is called, the PictureSnapshot control (or, better, its underlying VIDEO tag) starts displaying whatever the camera is pointing to (if we so authorize it).

    image

    Here is an example of one of my home walls… yes, I deliberately got out of the way! Smile

    OK, enough talk, the code for PictureSnapshot looks like this:

       1: public class PictureSnapshot : WebControl, ICallbackEventHandler
       2: {
       3:     public PictureSnapshot() : base("video")
       4:     {
       5:     }
       6:  
       7:     public event EventHandler<PictureTakenEventArgs> PictureTaken;
       8:  
       9:     protected override void OnInit(EventArgs e)
      10:     {
      11:         var sm = ScriptManager.GetCurrent(this.Page);
      12:         var reference = this.Page.ClientScript.GetCallbackEventReference(this, "picture", "function(result, context){ debugger; }", String.Empty, "function (error, context) { debugger; }", true);
      13:         var takeSnapshotScript = String.Format("\ndocument.getElementById('{0}').takeSnapshot = function(){{ var video = document.getElementById('{0}'); var canvas = document.createElement('canvas'); canvas.width = video.width; canvas.height = video.height; var context = canvas.getContext('2d'); context.drawImage(video, 0, 0, video.width, video.height); var picture = canvas.toDataURL(); {1} }};\n", this.ClientID, reference);
      14:         var startCaptureScript = String.Format("\ndocument.getElementById('{0}').startCapture = function(){{ var video = document.getElementById('{0}'); if ((video.paused == true) && (video.src == '')) {{ var getMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia); debugger; getMedia = getMedia.bind(navigator); getMedia({{ video: true, audio: false }}, function (stream) {{ video.src = window.URL.createObjectURL(stream); }}, function (error) {{ debugger; }}) }}; video.play(); }};\n", this.ClientID);
      15:         var stopCaptureScript = String.Format("\ndocument.getElementById('{0}').stopCapture = function(){{ var video = document.getElementById('{0}'); video.pause(); }};\n", this.ClientID);
      16:         var script = String.Concat(takeSnapshotScript, startCaptureScript, stopCaptureScript);
      17:  
      18:         if (sm != null)
      19:         {
      20:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat("snapshot", this.ClientID), String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ {0} }});\n", script), true);
      21:         }
      22:         else
      23:         {
      24:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat("snapshot", this.ClientID), script, true);
      25:         }
      26:  
      27:         if (this.Width != Unit.Empty)
      28:         {
      29:             this.Attributes.Add(HtmlTextWriterAttribute.Width.ToString().ToLower(), this.Width.ToString());
      30:         }
      31:  
      32:         if (this.Height != Unit.Empty)
      33:         {
      34:             this.Attributes.Add(HtmlTextWriterAttribute.Height.ToString().ToLower(), this.Height.ToString());
      35:         }
      36:  
      37:         this.Attributes.Remove("autoplay");
      38:         this.Attributes.Remove("controls");
      39:         this.Attributes.Remove("crossorigin");
      40:         this.Attributes.Remove("loop");
      41:         this.Attributes.Remove("mediagroup");
      42:         this.Attributes.Remove("muted");
      43:         this.Attributes.Remove("poster");
      44:         this.Attributes.Remove("preload");
      45:         this.Attributes.Remove("src");
      46:  
      47:         base.OnInit(e);
      48:     }
      49:  
      50:     protected virtual void OnPictureTaken(PictureTakenEventArgs e)
      51:     {
      52:         var handler = this.PictureTaken;
      53:  
      54:         if (handler != null)
      55:         {
      56:             handler(this, e);
      57:         }
      58:     }
      59:  
      60:     #region ICallbackEventHandler Members
      61:  
      62:     String ICallbackEventHandler.GetCallbackResult()
      63:     {
      64:         return (String.Empty);
      65:     }
      66:  
      67:     void ICallbackEventHandler.RaiseCallbackEvent(String eventArgument)
      68:     {
      69:         this.OnPictureTaken(new PictureTakenEventArgs(eventArgument));
      70:     }
      71:  
      72:     #endregion
      73: }

    As you can see, it inherits from WebControl. This is the simplest class in ASP.NET that allows me to output the tag I want, and also includes some nice stuff (width, height, style, class, etc). Also, it implements ICallbackEventHandler for the client callback stuff.

    You may find it strange that I am setting explicitly the WIDTH and HEIGHT attributes, that is because VIDEO requires it in the tag, not just on the STYLE. In the way, I am removing any CROSSORIGIN, SRC, MUTED, PRELOAD, LOOP, AUTOPLAY, MEDIAGROUP, POSTER and CONTROLS attributes that may be present in the markup, because we don’t really want them.

    The current picture is translated to a Data URI by using a temporary CANVAS, and this is what is sent to the server.

    The PictureTakenEventArgs class:

       1: [Serializable]
       2: public sealed class PictureTakenEventArgs : EventArgs
       3: {
       4:     public PictureTakenEventArgs(String base64Picture)
       5:     {
       6:         var index = base64Picture.IndexOf(',');
       7:         var bytes = Convert.FromBase64String(base64Picture.Substring(index + 1));
       8:  
       9:         using (var stream = new MemoryStream(bytes))
      10:         {
      11:             this.Picture = Image.FromStream(stream);
      12:         }
      13:     }
      14:  
      15:     public Image Picture
      16:     {
      17:         get;
      18:         private set;
      19:     }
      20: }

    And this is how to use this control in JavaScript:

       1: <script type="text/javascript">
       1:  
       2:     
       3:     function takeSnapshot()
       4:     {
       5:         document.getElementById('picture').takeSnapshot();
       6:     }
       7:  
       8:     function startCapture()
       9:     {
      10:         document.getElementById('picture').startCapture();
      11:     }
      12:  
      13:     function stopCapture()
      14:     {
      15:         document.getElementById('picture').stopCapture();
      16:     }
      17:  
    </script>
       2:  
       3: <input type="button" value="Start Capturing" onclick="startCapture()"/>
       4: <input type="button" value="Take Snapshot" onclick="takeSnapshot()" />
       5: <input type="button" value="Stop Capturing" onclick="stopCapture()" />

    Finally, a simple event handler for PictureTaken:

       1: protected void OnPictureTaken(Object sender, PictureTakenEventArgs e)
       2: {
       3:     //do something with e.Picture
       4: }

    Pictures arrive as instances of the Image class, and you can do whatever you want with it.

    As always, feedback is greatly appreciated!

    Read more...

  • ASP.NET Callback Panel

    Continuing with my quest for reusable, no dependencies, Web Forms AJAX controls, this time I wanted a replacement for the venerable UpdatePanel control. Specifically, I wanted to address the following issues:

    • Allow the partial update of a region on my page, including all of its controls;
    • Being able to cause an update through JavaScript (tricky with UpdatePanel);
    • Have the ability to only send data for controls living inside my control, not everything else on the page (read, __VIEWSTATE);
    • Support JavaScript events at the beginning/end of a callback or in the occurrence of an error (yes, I know about PageRequestManager, but I wanted something different).

    I ended up with a CallbackPanel control, which is what I am going to talk about. Here is its declaration:

       1: <web:CallbackPanel runat="server" ID="callback" SendAllData="false" OnBeforeCallback="onBeforeCallback" OnAfterCallback="onAfterCallback" OnCallbackError="onCallbackError" OnCallback="OnCallback">
       2:     <!-- some controls -->
       3:     <asp:Label runat="server" ID="time"/>
       4:     <asp:TextBox runat="server" ID="text"/>
       5:     <asp:Button runat="server" ID="button" Text="Button"/>
       6: </web:CallbackPanel>

    The CallbackPanel control supports some properties:

    • SendAllData: if all the data in the form should be sent, including viewstate, or just the data for the controls inside the CallbackPanel (default is false);
    • OnAfterCallback: the name of a JavaScript function to be called after the callback terminates;
      OnBeforeCallback: the optional name of a JavaScript function that gets called before a callback; if we so want, we can return false on this function to cancel the callback;
    • OnCallbackError: the name of a JavaScript function that is called in the event of an error.

    Some examples of the JavaScript functions:

       1: <script type="text/javascript">
       1:  
       2:         
       3:     function onCallbackError(error, context)
       4:     {
       5:     }
       6:  
       7:     function onBeforeCallback(arg, context)
       8:     {
       9:         //return false to cancel
      10:     }
      11:  
      12:     function onAfterCallback(result, context)
      13:     {
      14:     }
      15:  
    </script>

    For causing an update, we call its callback function, passing it a parameter and an optional context:

       1: document.getElementById('callback').callback('test', null);

    The most important property in CallbackPanel is the server-side event, OnCallback: this is raised whenever the callback function is called:

       1: protected void OnCallback(Object sender, CallbackEventArgs e)
       2: {
       3:     this.time.Text = e.Parameter + ": " + DateTime.Now.ToString();
       4: }

    This event receives a CallbackEventArgs argument, which is nothing more than:

       1: [Serializable]
       2: public sealed class CallbackEventArgs : EventArgs
       3: {
       4:     public CallbackEventArgs(String parameter)
       5:     {
       6:         this.Parameter = parameter;
       7:     }
       8:  
       9:     public String Parameter { get; private set; }
      10: }

    And finally, the code for the CallbackPanel itself:

       1: public class CallbackPanel : Panel, INamingContainer, ICallbackEventHandler
       2: {
       3:     public CallbackPanel()
       4:     {
       5:         this.OnAfterCallback = String.Empty;
       6:         this.OnBeforeCallback = String.Empty;
       7:         this.OnCallbackError = String.Empty;
       8:         this.SendAllData = true;
       9:     }
      10:  
      11:     public event EventHandler<CallbackEventArgs> Callback;
      12:  
      13:     [DefaultValue("")]
      14:     public String OnBeforeCallback { get; set; }
      15:  
      16:     [DefaultValue("")]
      17:     public String OnAfterCallback { get; set; }
      18:  
      19:     [DefaultValue("")]
      20:     public String OnCallbackError { get; set; }
      21:  
      22:     [DefaultValue(true)]
      23:     public Boolean SendAllData { get; set; }
      24:  
      25:     protected override void OnInit(EventArgs e)
      26:     {
      27:         var sm = ScriptManager.GetCurrent(this.Page);
      28:         var reference = this.Page.ClientScript.GetCallbackEventReference(this, "arg", String.Format("function(result, context){{ document.getElementById('{0}').innerHTML = result; {1} }}", this.ClientID, (String.IsNullOrWhiteSpace(this.OnAfterCallback) == false) ? String.Concat(this.OnAfterCallback, "(result, context);") : String.Empty), "context", String.Format("function(error, context){{ {0} }}", ((String.IsNullOrWhiteSpace(this.OnCallbackError) == false) ? String.Concat(this.OnCallbackError, "(error, context)") : String.Empty)), true);
      29:         var script = String.Concat("\ndocument.getElementById('", this.ClientID, "').callback = function(arg, context){", ((this.SendAllData == true) ? "__theFormPostCollection.length = 0; __theFormPostData = '';  WebForm_InitCallback(); " : "__theFormPostCollection.length = 0; __theFormPostData = '';  WebForm_InitCallback(); for (var i = 0; i < __theFormPostCollection.length; ++i) { if (__theFormPostCollection[i].name.indexOf('" + this.UniqueID + "$') == -1) { __theFormPostCollection[i].value = '' } }; "), (String.IsNullOrWhiteSpace(this.OnBeforeCallback) == true ? String.Empty : String.Concat("if (", this.OnBeforeCallback, "(arg, context) === false) return; ")), reference, ";};\n");
      30:  
      31:         if (sm != null)
      32:         {
      33:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat("callback", this.ClientID), String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ {0} }});\n", script), true);
      34:         }
      35:         else
      36:         {
      37:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat("callback", this.ClientID), script, true);
      38:         }
      39:  
      40:         base.OnInit(e);
      41:     }
      42:  
      43:     protected virtual void OnCallback(CallbackEventArgs e)
      44:     {
      45:         var handler = this.Callback;
      46:  
      47:         if (handler != null)
      48:         {
      49:             handler(this, e);
      50:         }
      51:     }
      52:  
      53:     #region ICallbackEventHandler Members
      54:     String ICallbackEventHandler.GetCallbackResult()
      55:     {
      56:         var builder = new StringBuilder();
      57:  
      58:         using (var writer = new StringWriter(builder))
      59:         using (var htmlWriter = new HtmlTextWriter(writer))
      60:         {
      61:             this.Render(new HtmlTextWriter(writer));
      62:  
      63:             return (builder.ToString());
      64:         }
      65:     }
      66:  
      67:     void ICallbackEventHandler.RaiseCallbackEvent(String eventArgument)
      68:     {
      69:         this.OnCallback(new CallbackEventArgs(eventArgument));
      70:     }
      71:     #endregion
      72: }

    Again, it is implementing ICallbackEventHandler, for client callbacks, but this time it is inheriting from Panel, which is a nice container for other controls. The rest should be self-explanatory, I guess. If you have questions, do send them to me!

    As always, hope you like it! Winking smile

    Read more...

  • Speech Synthesis with ASP.NET and HTML5

    The .NET framework includes the SpeechSynthesizer class which can be used to access the Windows speech synthesis engine. The problem with web applications is, of course, this class runs on the server. Because I wanted a mechanism to have speech synthesis (text-to-speech) fired by JavaScript, without requiring any plugin, I decided to implement one myself.

    Once again, I will be using client callbacks, my out-of-the-box ASP.NET favorite AJAX technique. I will also be using HTML5’s AUDIO tag and Data URIs. What I’m going to do is:

    • Set up a control that renders an AUDIO tag;
    • Add to it a JavaScript function that takes a string parameter and causes a client callback;
    • Generate a voice sound from the passed text parameter on the server and save it into an in-memory stream;
    • Convert the stream’s contents to a Data URI;
    • Return the generated Data URI to the client as the response to the client callback.

    Of course, all of this in cross-browser style (provided your browser knows the AUDIO tag and Data URIs, which all modern browsers do).

    So, first of all, my markup looks like this:

       1: <web:SpeechSynthesizer runat="server" ID="synthesizer" Ssml="false" VoiceName="Microsoft Anna" Age="Adult" Gender="Male" Culture="en-US" Rate="0" Volume="100" />

    As you can see, the SpeechSynthesizer control features a few optional properties:

    • Age: the age for the generated voice (default is the one of the system’s default language);
    • Gender: gender of the generated voice (same default as per Age);
    • Culture: the culture of the generated voice (system default);
    • Rate: the speaking rate, from –10 (fastest) to 10 (slowest), where the default is 0 (normal rate);
    • Ssml: if the text is to be considered SSML or not (default is false);
    • Volume: the volume %, between 0 and 100 (default);
    • VoiceName: the name of a voice that is installed on the server machine.

    The Age, Gender and Culture properties and the VoiceName are exclusive, you either specify one or the other. If you want to know the voices installed on your machine, have a look at the GetInstalledVoices method. If no property is specified, the speech will be synthesized with the operating system’s default (Microsoft Anna on Windows 7, Microsoft Dave, Hazel and Zira on Windows 8, etc). By the way, you can get additional voices, either commercially or for free, just look them up in Google.

    Without further delay, here is the code:

       1: [ConstructorNeedsTag(false)]
       2: public class SpeechSynthesizer : HtmlGenericControl, ICallbackEventHandler
       3: {
       4:     private readonly System.Speech.Synthesis.SpeechSynthesizer synth = new System.Speech.Synthesis.SpeechSynthesizer();
       5:  
       6:     public SpeechSynthesizer() : base("audio")
       7:     {
       8:         this.Age = VoiceAge.NotSet;
       9:         this.Gender = VoiceGender.NotSet;
      10:         this.Culture = CultureInfo.CurrentCulture;
      11:         this.VoiceName = String.Empty;
      12:         this.Ssml = false;
      13:     }
      14:  
      15:     [DefaultValue("")]
      16:     public String VoiceName { get; set; }
      17:  
      18:     [DefaultValue(100)]
      19:     public Int32 Volume { get; set; }
      20:  
      21:     [DefaultValue(0)]
      22:     public Int32 Rate { get; set; }
      23:  
      24:     [TypeConverter(typeof(CultureInfoConverter))]
      25:     public CultureInfo Culture { get; set; }
      26:  
      27:     [DefaultValue(VoiceGender.NotSet)]
      28:     public VoiceGender Gender { get; set; }
      29:  
      30:     [DefaultValue(VoiceAge.NotSet)]
      31:     public VoiceAge Age { get; set; }
      32:  
      33:     [DefaultValue(false)]
      34:     public Boolean Ssml { get; set; }
      35:  
      36:     protected override void OnInit(EventArgs e)
      37:     {
      38:         AsyncOperationManager.SynchronizationContext = new SynchronizationContext();
      39:  
      40:         var sm = ScriptManager.GetCurrent(this.Page);
      41:         var reference = this.Page.ClientScript.GetCallbackEventReference(this, "text", String.Format("function(result){{ document.getElementById('{0}').src = result; document.getElementById('{0}').play(); }}", this.ClientID), String.Empty, true);
      42:         var script = String.Format("\ndocument.getElementById('{0}').speak = function(text){{ {1} }};\n", this.ClientID, reference);
      43:  
      44:         if (sm != null)
      45:         {
      46:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat("speak", this.ClientID), String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ {0} }});\n", script), true);
      47:         }
      48:         else
      49:         {
      50:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat("speak", this.ClientID), script, true);
      51:         }
      52:  
      53:         base.OnInit(e);
      54:     }
      55:  
      56:     protected override void OnPreRender(EventArgs e)
      57:     {
      58:         this.Attributes.Remove("class");
      59:         this.Attributes.Remove("src");
      60:         this.Attributes.Remove("preload");
      61:         this.Attributes.Remove("loop");
      62:         this.Attributes.Remove("autoplay");
      63:         this.Attributes.Remove("controls");
      64:         
      65:         this.Style[HtmlTextWriterStyle.Display] = "none";
      66:         this.Style[HtmlTextWriterStyle.Visibility] = "hidden";
      67:  
      68:         base.OnPreRender(e);
      69:     }
      70:  
      71:     public override void Dispose()
      72:     {
      73:         this.synth.Dispose();
      74:  
      75:         base.Dispose();
      76:     }
      77:  
      78:     #region ICallbackEventHandler Members
      79:  
      80:     String ICallbackEventHandler.GetCallbackResult()
      81:     {
      82:         using (var stream = new MemoryStream())
      83:         {
      84:             this.synth.Rate = this.Rate;
      85:             this.synth.Volume = this.Volume;
      86:             this.synth.SetOutputToWaveStream(stream);
      87:  
      88:             if (String.IsNullOrWhiteSpace(this.VoiceName) == false)
      89:             {
      90:                 this.synth.SelectVoice(this.VoiceName);
      91:             }
      92:             else
      93:             {
      94:                 this.synth.SelectVoiceByHints(this.Gender, this.Age, 0, this.Culture);                    
      95:             }
      96:  
      97:             if (this.Ssml == false)
      98:             {
      99:                 this.synth.Speak(this.Context.Items["data"] as String);
     100:             }
     101:             else
     102:             {
     103:                 this.synth.SpeakSsml(this.Context.Items["data"] as String);
     104:             }
     105:  
     106:             return (String.Concat("data:audio/wav;base64,", Convert.ToBase64String(stream.ToArray())));
     107:         }
     108:     }
     109:  
     110:     void ICallbackEventHandler.RaiseCallbackEvent(String eventArgument)
     111:     {
     112:         this.Context.Items["data"] = eventArgument;
     113:     }
     114:  
     115:     #endregion
     116: }

    As you can see, the SpeechSynthesizer control inherits from HtmlGenericControl, this is the simplest out-of-the-box class that will allow me to render my tag of choice (in this case, AUDIO); by the way, this class requires that I decorate it with a ConstructorNeedsTagAttribute, but you don’t have to worry about it. It implements ICallbackEventHandler for the client callback mechanism. I make sure that all of AUDIO’s attributes are removed from the output, because I don’t want them around.

    Inside of it, I have an instance of the SpeechSynthesizer class, the one that will be used to do the actual work. Because this class is disposable, I make sure it is disposed at the end of the control’s life cycle. Based on the parameters being supplied, I either call the SelectVoiceByHints or the SelectVoice methods. One thing to note is, we need to set up a synchronization context, because the SpeechSynthesizer works asynchronously, so that we can wait for its result.

    The generated sound will be output to an in-memory buffer and then converted into a WAV Data URI, which is basically a Base64 encoded string with an associated mime-type.

    Finally, on the client-side, all it takes is to set the returned Data URI as the AUDIO SRC property, and that's it.

    A full markup example would be:

       1: <%@ Register Assembly="System.Speech, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Namespace="System.Speech" TagPrefix="web" %>
       2: <!DOCTYPE html>
       3: <html xmlns="http://www.w3.org/1999/xhtml">
       4: <head runat="server">
       5:     <script type="text/javascript">
       1:  
       2:         
       3:         function onSpeak(text)
       4:         {
       5:             document.getElementById('synthesizer').speak(text);
       6:         }
       7:  
       8:     
    </script>
       6: </head>
       7: <body>
       8:     <form runat="server">
       9:     <div>
      10:         <web:SpeechSynthesizer runat="server" ID="synthesizer" Age="Adult" Gender="Male" Culture="en-US" Rate="0" Volume="100" />
      11:         <input type="text" id="text" name="text"/>
      12:         <input type="button" value="Speak" onclick="onSpeak(this.form.text.value)"/>
      13:     </div>
      14:     </form>
      15: </body>
      16: </html>

    And that’s it! Have fun with speech on your web apps! Winking smile

    Read more...

  • ASP.NET Upload Panel

    Introduction

    I have been playing recently with HTML5, and one thing that I got to understand really well was the new upload mechanisms available. Specifically, I wanted to understand how SkyOneDrive, Google Drive, Dropbox, etc, all support dropping files from the local machine, and how to use it in an ASP.NET Web Forms (sorry!) project, and I got it!

    So, I want to have a panel (a DIV) that allows dropping files from the local machine; I want to be able to filter these files by their content type, maximum length, or by any custom condition. If the dropped files pass all conditions, they are sent asynchronously (read, AJAX) and raise a server-side event. After that, if they are multimedia files, I can preview them on the client-side.

    This example will use the XMLHttpRequest object and the new FormData and FileReader JavaScript classes and will work on all modern browsers, from IE9 to Firefox, Chrome and Safari.

    image

    Markup

    My markup looks like this:

       1: <web:UploadPanel runat="server" ID="uploadPanel" MaximumFiles="2" MaximumLength="1000000" ContentTypes="image/gif,image/png,image/jpeg" OnPreviewFile="onPreviewFile" OnBeforeUpload="onBeforeUpload" onUploadCanceled="onUploadCanceled" OnUploadComplete="onUploadComplete" OnUploadFailure="onUploadFailure" OnUploadProgress="onUploadProgress" OnUploadSuccess="onUploadSuccess" OnValidationFailure="onValidationFailure" OnUpload="OnUpload" Style="width: 300px; height: 300px; border: solid 1px;" />

    The UploadPanel control inherits from the Panel class, so it can have any of its properties. In this case, I am setting a specific width, height and border, so that it is easier to target.

    Validations

    Out of the box, it supports the following validations:

    • MaximumFiles: The maximum number of files to drop; if not set, any number is accepted;
    • MaximumLength: The maximum length of any individual file, in bytes; if not set, any file size is accepted;
    • ContentTypes: A comma-delimited list of content types; can take a precise content type, such as “image/gif” or a content type part, such as “image/”; if not specified, any content type is accepted.

    There is also an optional JavaScript event, OnBeforeUpload, that can be used to validate the files individually.

    Client-Side Events

    When a file is dropped into the UploadPanel, the following client-side events are raised:

    • OnValidationFailure: Raised whenever any of the built-in validations (maximum files, maximum length and content types) fails, for any of the dropped files;
    • OnBeforeUpload: Raised before the upload starts, and after all built-in validations (maximum files, maximum length and content types) succeed; this gives developers a chance to analyze the files to upload and to optionally cancel the upload, or to add additional custom parameters that will be posted together with the files;
    • OnUploadFailure: Raised if the upload fails for some reason;
    • OnUploadCanceled: Raised if the upload is canceled;
    • OnUploadProgress: Raised possibly several times as the file is being uploaded, providing an indication of the total upload progress;
    • OnUploadSuccess: Raised when the upload terminates successfully;
    • OnUploadComplete: Raised when the upload completes, either successfully or not;
    • OnPreviewFile: Raised for each multimedia file uploaded (images, videos, sound), to allow previewing it on the client-side; the handler function receives the file as a data URL.

    For each you can optionally specify the name of a JavaScript function that handles it. Some examples of all these events:

       1: <script>
       1:  
       2:  
       3:     function onPreviewFile(name, url)
       4:     {
       5:         document.getElementById('preview').src = url;
       6:     }
       7:  
       8:     function onBeforeUpload(event, props)
       9:     {
      10:         //set two custom properties
      11:         props.a = 1;
      12:         props.b = 2;
      13:         return (true);
      14:     }
      15:  
      16:     function onUploadCanceled(event)
      17:     {
      18:         //canceled
      19:     }
      20:  
      21:     function onUploadComplete(event)
      22:     {
      23:         //complete
      24:     }
      25:  
      26:     function onUploadFailure(event)
      27:     {
      28:         //failure
      29:     }
      30:  
      31:     function onUploadProgress(event)
      32:     {
      33:         if (event.lengthComputable)
      34:         {
      35:             var percentComplete = event.loaded / event.total;
      36:         }
      37:     }
      38:  
      39:     function onUploadSuccess(event)
      40:     {
      41:         //success
      42:     }
      43:  
      44:     function onValidationFailure(event, error)
      45:     {
      46:         //error=0: maximum files reached
      47:         //error=1: maximum file length reached
      48:         //error=2: invalid content type
      49:     }
      50:  
    </script>

    Besides these custom events, you can use any of the regular HTML5 events, such as dragdragenter, dragend, dragover, dragstart, dragleave or drop.

    Server-Side Events

    The Upload event takes a parameter of type UploadEventArgs which looks like this:

       1: [Serializable]
       2: public sealed class UploadEventArgs : EventArgs
       3: {
       4:     public UploadEventArgs(HttpFileCollection files, NameValueCollection form)
       5:     {
       6:         this.Files = files;
       7:         this.Response = String.Empty;
       8:         this.Form = form;
       9:     }
      10:  
      11:     public String Response
      12:     {
      13:         get;
      14:         set;
      15:     }
      16:  
      17:     public NameValueCollection Form
      18:     {
      19:         get;
      20:         private set;
      21:     }
      22:  
      23:     public HttpFileCollection Files
      24:     {
      25:         get;
      26:         private set;
      27:     }
      28: }

    This class receives a list of all the files uploaded and also any custom properties assigned on the OnBeforeUpload event. It also allows the return of a response string, that will be received by the OnUploadSuccess or OnUploadComplete client-side events:

       1: protected void OnUpload(Object sender, UploadEventArgs e)
       2: {
       3:     //do something with e.Files
       4:     
       5:     //output the parameters that were received
       6:     e.Response = String.Format("a={0}\nb={1}", e.Form["a"], e.Form["b"]);
       7: }

    Code

    Finally, the code:

       1: public class UploadPanel : Panel, ICallbackEventHandler
       2: {
       3:     private static readonly String [] MultimediaContentTypePrefixes = new String[]{ "image/", "audio/", "video/" };
       4:  
       5:     public UploadPanel()
       6:     {
       7:         this.ContentTypes = new String[0];
       8:         this.OnUploadFailure = String.Empty;
       9:         this.OnUploadSuccess = String.Empty;
      10:         this.OnValidationFailure = String.Empty;
      11:         this.OnBeforeUpload = String.Empty;
      12:         this.OnUploadComplete = String.Empty;
      13:         this.OnUploadProgress = String.Empty;
      14:         this.OnUploadCanceled = String.Empty;
      15:         this.OnPreviewFile = String.Empty;
      16:     }
      17:  
      18:     public event EventHandler<UploadEventArgs> Upload;
      19:  
      20:     [DefaultValue("")]
      21:     public String OnPreviewFile
      22:     {
      23:         get;
      24:         set;
      25:     }
      26:  
      27:     [DefaultValue("")]
      28:     public String OnBeforeUpload
      29:     {
      30:         get;
      31:         set;
      32:     }
      33:  
      34:     [DefaultValue("")]
      35:     public String OnUploadCanceled
      36:     {
      37:         get;
      38:         set;
      39:     }
      40:  
      41:     [DefaultValue("")]
      42:     public String OnUploadProgress
      43:     {
      44:         get;
      45:         set;
      46:     }
      47:  
      48:     [DefaultValue("")]
      49:     public String OnValidationFailure
      50:     {
      51:         get;
      52:         set;
      53:     }
      54:  
      55:     [DefaultValue("")]
      56:     public String OnUploadComplete
      57:     {
      58:         get;
      59:         set;
      60:     }
      61:  
      62:     [DefaultValue("")]
      63:     public String OnUploadSuccess
      64:     {
      65:         get;
      66:         set;
      67:     }
      68:  
      69:     [DefaultValue("")]
      70:     public String OnUploadFailure
      71:     {
      72:         get;
      73:         set;
      74:     }
      75:  
      76:     [DefaultValue(null)]
      77:     public Int32? MaximumLength
      78:     {
      79:         get;
      80:         set;
      81:     }
      82:  
      83:     [DefaultValue(null)]
      84:     public Int32? MaximumFiles
      85:     {
      86:         get;
      87:         set;
      88:     }
      89:  
      90:     [TypeConverter(typeof(StringArrayConverter))]
      91:     public String[] ContentTypes
      92:     {
      93:         get;
      94:         set;
      95:     }
      96:  
      97:     protected override void OnInit(EventArgs e)
      98:     {
      99:         var script = new StringBuilder();            
     100:         script.AppendFormat("document.getElementById('{0}').addEventListener('drop', function(event) {{\n", this.ClientID);
     101:  
     102:         script.Append("if (event.dataTransfer.files.length == 0)\n");
     103:         script.Append("{\n");
     104:         script.Append("event.returnValue = false;\n");
     105:         script.Append("event.preventDefault();\n");
     106:         script.Append("return(false);\n");
     107:         script.Append("}\n");
     108:  
     109:         if (this.MaximumFiles != null)
     110:         {
     111:             script.AppendFormat("if (event.dataTransfer.files.length > {0})\n", this.MaximumFiles.Value);
     112:             script.Append("{\n");
     113:  
     114:             if (String.IsNullOrWhiteSpace(this.OnValidationFailure) == false)
     115:             {
     116:                 script.AppendFormat("{0}(event, 0);\n", this.OnValidationFailure);
     117:             }
     118:  
     119:             script.Append("event.returnValue = false;\n");
     120:             script.Append("event.preventDefault();\n");
     121:             script.Append("return(false);\n");
     122:             script.Append("}\n");
     123:         }
     124:  
     125:         if (this.MaximumLength != null)
     126:         {
     127:             script.Append("var lengthOk = true;\n");
     128:             script.Append("for (var i = 0; i < event.dataTransfer.files.length; ++i)\n");
     129:             script.Append("{\n");
     130:             script.AppendFormat("if (event.dataTransfer.files[i].size > {0})\n", this.MaximumLength.Value);
     131:             script.Append("{\n");
     132:             script.Append("lengthOk = false;\n");
     133:             script.Append("break;\n");
     134:             script.Append("}\n");
     135:             script.Append("}\n");
     136:             script.Append("if (lengthOk == false)\n");
     137:             script.Append("{\n");
     138:  
     139:             if (String.IsNullOrWhiteSpace(this.OnValidationFailure) == false)
     140:             {
     141:                 script.AppendFormat("{0}(event, 1);\n", this.OnValidationFailure);
     142:             }
     143:  
     144:             script.Append("event.returnValue = false;\n");
     145:             script.Append("event.preventDefault();\n");
     146:             script.Append("return(false);\n");
     147:             script.Append("}\n");
     148:         }
     149:  
     150:         if (this.ContentTypes.Any() == true)
     151:         {
     152:             script.Append("for (var i = 0; i < event.dataTransfer.files.length; ++i)\n");
     153:             script.Append("{\n");
     154:             script.Append("var contentTypeOk = false;\n");
     155:  
     156:             script.AppendFormat("if ({0})", String.Join(" || ", this.ContentTypes.Select(x => String.Format("(event.dataTransfer.files[i].type.toLowerCase().indexOf('{0}') == 0)", x.ToLower()))));
     157:             script.Append("{\n");
     158:             script.Append("contentTypeOk = true;\n");
     159:             script.Append("}\n");
     160:  
     161:             script.Append("}\n");
     162:             script.Append("if (contentTypeOk == false)\n");
     163:             script.Append("{\n");
     164:  
     165:             if (String.IsNullOrWhiteSpace(this.OnValidationFailure) == false)
     166:             {
     167:                 script.AppendFormat("{0}(event, 2);\n", this.OnValidationFailure);
     168:             }
     169:  
     170:             script.Append("event.returnValue = false;\n");
     171:             script.Append("event.preventDefault();\n");
     172:             script.Append("return(false);\n");
     173:             script.Append("}\n");
     174:         }
     175:  
     176:         if (String.IsNullOrWhiteSpace(this.OnBeforeUpload) == false)
     177:         {
     178:             script.Append("var props = new Object();\n");
     179:             script.AppendFormat("if ({0}(event, props) === false)\n", this.OnBeforeUpload);
     180:             script.Append("{\n");
     181:             script.Append("event.returnValue = false;\n");
     182:             script.Append("event.preventDefault();\n");
     183:             script.Append("return(false);\n");
     184:             script.Append("}\n");
     185:         }
     186:  
     187:         script.Append("var data = new FormData();\n");
     188:         script.Append("for (var i = 0; i < event.dataTransfer.files.length; ++i)\n");
     189:         script.Append("{\n");
     190:         script.Append("var file = event.dataTransfer.files[i];\n");
     191:         script.Append("data.append('file' + i, file);\n");
     192:  
     193:         if (String.IsNullOrWhiteSpace(this.OnPreviewFile) == false)
     194:         {
     195:             script.AppendFormat("if ({0})", String.Join(" || ", MultimediaContentTypePrefixes.Select(x => String.Format("(file.type.toLowerCase().indexOf('{0}') == 0)", x.ToLower()))));
     196:             script.Append("{\n");
     197:             script.Append("var reader = new FileReader();\n");
     198:             script.Append("reader.onloadend = function(e)\n");
     199:             script.Append("{\n");
     200:             script.AppendFormat("{0}(file.name, reader.result);\n", this.OnPreviewFile);
     201:             script.Append("}\n");
     202:             script.Append("reader.readAsDataURL(file);\n");
     203:             script.Append("}\n");
     204:         }
     205:  
     206:         script.Append("}\n");
     207:         script.AppendFormat("data.append('__CALLBACKID', '{0}');\n", this.UniqueID);
     208:         script.Append("data.append('__CALLBACKPARAM', '');\n");
     209:         script.Append("data.append('__EVENTTARGET', '');\n");
     210:         script.Append("data.append('__EVENTARGUMENT', '');\n");
     211:         script.Append("for (var key in props)\n");
     212:         script.Append("{\n");
     213:         script.Append("data.append(key, props[key]);\n");
     214:         script.Append("}\n");
     215:         script.Append("var xhr = new XMLHttpRequest();\n");
     216:  
     217:         if (String.IsNullOrWhiteSpace(this.OnUploadProgress) == false)
     218:         {
     219:             script.Append("xhr.onprogress = function(e)\n");
     220:             script.Append("{\n");
     221:             script.AppendFormat("{0}(e);\n", this.OnUploadProgress);
     222:             script.Append("}\n");
     223:         }
     224:  
     225:         if (String.IsNullOrWhiteSpace(this.OnUploadCanceled) == false)
     226:         {
     227:             script.Append("xhr.oncancel = function(e)\n");
     228:             script.Append("{\n");
     229:             script.AppendFormat("{0}(e);\n", this.OnUploadCanceled);
     230:             script.Append("}\n");
     231:         }
     232:  
     233:         script.Append("xhr.onreadystatechange = function(e)\n");
     234:         script.Append("{\n");
     235:         script.Append("if ((xhr.readyState == 4) && (xhr.status == 200))\n");
     236:         script.Append("{\n");
     237:         script.AppendFormat("{0}(e);\n", this.OnUploadSuccess);
     238:         script.Append("}\n");
     239:         script.Append("else if ((xhr.readyState == 4) && (xhr.status != 200))\n");
     240:         script.Append("{\n");
     241:         script.AppendFormat("{0}(e);\n", this.OnUploadFailure);
     242:         script.Append("}\n");
     243:         script.Append("if (xhr.readyState == 4)\n");
     244:         script.Append("{\n");
     245:         script.AppendFormat("{0}(e);\n", this.OnUploadComplete);
     246:         script.Append("}\n");
     247:         script.Append("}\n");
     248:         script.AppendFormat("xhr.open('POST', '{0}', true);\n", this.Context.Request.Url.PathAndQuery);
     249:         script.Append("xhr.send(data);\n");
     250:         script.Append("event.returnValue = false;\n");
     251:         script.Append("event.preventDefault();\n");
     252:         script.Append("return (false);\n");
     253:         script.Append("});\n");
     254:  
     255:         if (ScriptManager.GetCurrent(this.Page) == null)
     256:         {
     257:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(this.UniqueID, "drop"), script.ToString(), true);
     258:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(this.UniqueID, "dragenter"), String.Format("document.getElementById('{0}').addEventListener('dragenter', function(event){{ event.returnValue = false; event.preventDefault(); return(false); }});\n", this.ClientID), true);
     259:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(this.UniqueID, "dragover"), String.Format("document.getElementById('{0}').addEventListener('dragover', function(event){{ event.returnValue = false; event.preventDefault(); return(false); }});\n", this.ClientID), true);
     260:         }
     261:         else
     262:         {
     263:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(this.UniqueID, "drop"), String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ {0} }});\n", script), true);
     264:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(this.UniqueID, "dragenter"), String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ document.getElementById('{0}').addEventListener('dragenter', function(event){{ event.returnValue = false; event.preventDefault(); return(false); }}); }});\n", this.ClientID), true);
     265:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(this.UniqueID, "dragover"), String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ document.getElementById('{0}').addEventListener('dragover', function(event){{ event.returnValue = false; event.preventDefault(); return(false); }}); }});\n", this.ClientID), true);
     266:         }
     267:         
     268:         base.OnInit(e);
     269:     }
     270:  
     271:     protected virtual void OnUpload(UploadEventArgs e)
     272:     {
     273:         var handler = this.Upload;
     274:  
     275:         if (handler != null)
     276:         {
     277:             handler(this, e);
     278:         }
     279:     }
     280:  
     281:     #region ICallbackEventHandler Members
     282:  
     283:     String ICallbackEventHandler.GetCallbackResult()
     284:     {
     285:         var args = new UploadEventArgs(this.Context.Request.Files, this.Context.Request.Form);
     286:  
     287:         this.OnUpload(args);
     288:  
     289:         return (args.Response);
     290:     }
     291:  
     292:     void ICallbackEventHandler.RaiseCallbackEvent(String eventArgument)
     293:     {
     294:     }
     295:  
     296:     #endregion
     297: }

    The UploadPanel class inherits from Panel and implements ICallbackEventHandler, for client callbacks. If you are curious, the __CALLBACKID, __CALLBACKPARAM, __EVENTTARGET and __EVENTARGUMENT are required for ASP.NET to detect a request as a callback, but only __CALLBACKID needs to be set with the unique id of the UploadPanel control.

    Conclusion

    HTML5 offers a lot of exciting new features. Stay tuned for some more examples of its integration with ASP.NET!

    Read more...

  • Client Callbacks in Action Part 2 : Self-filling Drop Down List

    On the first part of this two-post series I presented a text box that has the ability to suggest values, after a number of characters has been entered. It does this by leveraging the client callbacks functionality of ASP.NET. This time, I am going to show a drop down list that exposes a JavaScript function (fill) that receives a value, and will pass that value to a server-side event also through a client callback where we have the option to populate the drop down.

    First, the target markup:

       1: <my:AutoFillDropDownList runat="server" ID="list" OnAutoFill="list_AutoFill" />

    No custom properties whatsoever, just an handler for the AutoFill event, which will be called whenever the fill function is invoked.

    The AutoFillDropDownList inherits from DropDownList:

       1: public class AutoFillDropDownList : DropDownList, ICallbackEventHandler
       2: {
       3:     public event EventHandler<AutoFillEventArgs> AutoFill;
       4:  
       5:     protected override void OnInit(EventArgs e)
       6:     {
       7:         this.Page.ClientScript.RegisterStartupScript(this.GetType(), this.UniqueID + "onFillCallback", String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ document.getElementById('{0}').onFillCallback = function(result, context) {{ document.getElementById('{0}').options.length = 0; var r = result.split('\\n'); for (var i = 0; i < r.length; ++i) {{ var keyValue = r[i].split('='); if (keyValue.length == 1) {{ continue }}; var option = document.createElement('option'); option.value = keyValue[0]; option.text = keyValue[1]; document.getElementById('{0}').options.add(option);  }} }} }});", this.ClientID), true);
       8:         this.Page.ClientScript.RegisterStartupScript(this.GetType(), this.UniqueID + "fill", String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ document.getElementById('{0}').fill = function(value) {{ document.getElementById('{0}_HiddenField').value = value; {1}; }} }});\n", this.ClientID, String.Format(this.Page.ClientScript.GetCallbackEventReference(this, "value", "document.getElementById('{0}').onFillCallback", null, true), this.ClientID)), true);
       9:         this.Page.ClientScript.RegisterHiddenField(String.Concat(this.ID, "_HiddenField"), this.Context.Request.Form[String.Concat(this.ID, "_HiddenField")]);
      10:  
      11:         this.Page.PreLoad += this.OnPreLoad;
      12:  
      13:         base.OnInit(e);
      14:     }
      15:  
      16:     public override void Dispose()
      17:     {
      18:         this.Page.PreLoad -= this.OnPreLoad;
      19:  
      20:         base.Dispose();
      21:     }
      22:  
      23:     protected void OnPreLoad(object sender, EventArgs e)
      24:     {
      25:         if (this.Page.IsPostBack == true)
      26:         {
      27:             var fillValue = this.Context.Request.Form[String.Concat(this.UniqueID, "_HiddenField")];
      28:  
      29:             if (String.IsNullOrWhiteSpace(fillValue) == false)
      30:             {
      31:                 var args = new AutoFillEventArgs(fillValue);
      32:                 
      33:                 this.OnAutoFill(args);
      34:  
      35:                 foreach (var key in args.Results.Keys.OfType<String>())
      36:                 {
      37:                     this.Items.Add(new ListItem(args.Results[key], key));
      38:                 }
      39:  
      40:                 var selectedValue = this.Context.Request.Form[this.UniqueID];
      41:  
      42:                 this.SelectedIndex = this.Items.IndexOf(this.Items.FindByValue(selectedValue));
      43:             }
      44:         }
      45:     }
      46:     
      47:     protected virtual void OnAutoFill(AutoFillEventArgs e)
      48:     {
      49:         var handler = this.AutoFill;
      50:  
      51:         if (handler != null)
      52:         {
      53:             handler(this, e);
      54:         }
      55:     }
      56:  
      57:     #region ICallbackEventHandler Members
      58:  
      59:     String ICallbackEventHandler.GetCallbackResult()
      60:     {
      61:         var output = this.Context.Items["Results"] as StringDictionary;
      62:  
      63:         return (String.Join(Environment.NewLine, output.Keys.OfType<String>().Select(x => String.Concat(x, "=", output[x]))));
      64:     }
      65:  
      66:     void ICallbackEventHandler.RaiseCallbackEvent(String eventArgument)
      67:     {
      68:         var args = new AutoFillEventArgs(eventArgument);
      69:  
      70:         this.OnAutoFill(args);
      71:  
      72:         this.Context.Items["Results"] = args.Results;
      73:     }
      74:  
      75:     #endregion
      76: }

    Again, for client callbacks, we need to implement ICallbackEventHandler. In its RaiseCallbackEvent we raise the AutoFill event, store its output in the request itself (HttpContext.Items collection), and on GetCallbackResult, we take this value and pass it to the client.

    Another important thing is, whenever there is a postback, we need to raise the AutoFill event again, because the drop down must be in a consistent state, therefore we need to get its items. We do that in the PreLoad event of the page.

    The AutoFill event uses a special argument:

       1: [Serializable]
       2: public sealed class AutoFillEventArgs : EventArgs
       3: {
       4:     public AutoFillEventArgs(String parameter)
       5:     {
       6:         this.Parameter = parameter;
       7:         this.Results = new StringDictionary();
       8:     }
       9:  
      10:     public String Parameter { get; private set; }
      11:  
      12:     public StringDictionary Results { get; private set; }
      13: }

    Two properties:

    • Parameter: the read-only value passed to the fill JavaScript function;
    • Results: the key-value pairs that will be used to fill the drop down list items.

    A typical handler might look like this:

       1: protected void list_AutoFill(object sender, AutoFillEventArgs e)
       2: {
       3:     //just add 10 items
       4:     for (var i = 0; i < 10; ++i)
       5:     {
       6:         e.Results.Add(i.ToString(), e.Parameter + i.ToString());
       7:     }
       8: }

    In order to invoke the filling, just call fill on JavaScript:

       1: document.getElementById('list').fill('something');

    There are no dependencies on any external libraries other than Microsoft AJAX Library, which is included by the ScriptManager.

    Hope you like it!

    Read more...

  • My All Time Favorite Posts

    Since this blog started, back in 2008, I wrote a lot of posts. I’d say some are still up to date. I picked a few of them, those I’m more proud of, in no particular order.

    ASP.NET Web Forms:

    ASP.NET MVC:

    NHibernate:

    .NET:

    Let me know what you think of them! Are there others you particularly enjoyed?

    Read more...

  • Wrapping ASP.NET Client Callbacks

    Client Callbacks are probably the less known (and I dare say, less loved) of all the AJAX options in ASP.NET, which also include the UpdatePanel, Page Methods and Web Services. The reason for that, I believe, is it’s relative complexity:

    • Get a reference to a JavaScript function;
    • Dynamically register function that calls the above reference;
    • Have a JavaScript handler call the registered function.

    However, it has some the nice advantage of being self-contained, that is, doesn’t need additional files, such as web services, JavaScript libraries, etc, or static methods declared on a page, or any kind of attributes.

    So, here’s what I want to do:

    • Have a DOM element which exposes a method that is executed server side, passing it a string and returning a string;
    • Have a server-side event that handles the client-side call;
    • Have two client-side user-supplied callback functions for handling the success and error results.

    I’m going to develop a custom control without user interface that does the registration of the client JavaScript method as well as a server-side event that can be hooked by some handler on a page. My markup will look like this:

       1: <script type="text/javascript">
       1:  
       2:  
       3:     function onCallbackSuccess(result, context)
       4:     {
       5:     }
       6:  
       7:     function onCallbackError(error, context)
       8:     {
       9:     }
      10:  
    </script>
       2: <my:CallbackControl runat="server" ID="callback" SendAllData="true" OnCallback="OnCallback"/>

    The control itself looks like this:

       1: public class CallbackControl : Control, ICallbackEventHandler
       2: {
       3:     #region Public constructor
       4:     public CallbackControl()
       5:     {
       6:         this.SendAllData = false;
       7:         this.Async = true;
       8:     }
       9:     #endregion
      10:  
      11:     #region Public properties and events
      12:     public event EventHandler<CallbackEventArgs> Callback;
      13:  
      14:     [DefaultValue(true)]
      15:     public Boolean Async
      16:     {
      17:         get;
      18:         set;
      19:     }
      20:  
      21:     [DefaultValue(false)]
      22:     public Boolean SendAllData
      23:     {
      24:         get;
      25:         set;
      26:     }
      27:  
      28:     #endregion
      29:  
      30:     #region Protected override methods
      31:  
      32:     protected override void Render(HtmlTextWriter writer)
      33:     {
      34:         writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
      35:         writer.RenderBeginTag(HtmlTextWriterTag.Span);
      36:  
      37:         base.Render(writer);
      38:  
      39:         writer.RenderEndTag();
      40:     }
      41:  
      42:     protected override void OnInit(EventArgs e)
      43:     {
      44:         String reference = this.Page.ClientScript.GetCallbackEventReference(this, "arg", "onCallbackSuccess", "context", "onCallbackError", this.Async);
      45:         String script = String.Concat("\ndocument.getElementById('", this.ClientID, "').callback = function(arg, context, onCallbackSuccess, onCallbackError){", ((this.SendAllData == true) ? "__theFormPostCollection.length = 0; __theFormPostData = '';  WebForm_InitCallback(); " : String.Empty), reference, ";};\n");
      46:  
      47:         this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat("callback", this.ClientID), script, true);
      48:  
      49:         base.OnInit(e);
      50:     }
      51:  
      52:     #endregion
      53:  
      54:     #region Protected virtual methods
      55:     protected virtual void OnCallback(CallbackEventArgs args)
      56:     {
      57:         EventHandler<CallbackEventArgs> handler = this.Callback;
      58:  
      59:         if (handler != null)
      60:         {
      61:             handler(this, args);
      62:         }
      63:     }
      64:  
      65:     #endregion
      66:  
      67:     #region ICallbackEventHandler Members
      68:  
      69:     String ICallbackEventHandler.GetCallbackResult()
      70:     {
      71:         CallbackEventArgs args = new CallbackEventArgs(this.Context.Items["Data"] as String);
      72:  
      73:         this.OnCallback(args);
      74:  
      75:         return (args.Result);
      76:     }
      77:  
      78:     void ICallbackEventHandler.RaiseCallbackEvent(String eventArgument)
      79:     {
      80:         this.Context.Items["Data"] = eventArgument;
      81:     }
      82:  
      83:     #endregion
      84: }

    And the event argument class:

       1: [Serializable]
       2: public class CallbackEventArgs : EventArgs
       3: {
       4:     public CallbackEventArgs(String argument)
       5:     {
       6:         this.Argument = argument;
       7:         this.Result = String.Empty;
       8:     }
       9:  
      10:     public String Argument
      11:     {
      12:         get;
      13:         private set;
      14:     }
      15:  
      16:     public String Result
      17:     {
      18:         get;
      19:         set;
      20:     }
      21: }

    You will notice two properties on the CallbackControl:

    • Async: indicates if the call should be made asynchronously or synchronously (the default);
    • SendAllData: indicates if the callback call will include the view and control state of all of the controls on the page, so that, on the server side, they will have their properties set when the Callback event is fired.

    The CallbackEventArgs class exposes two properties:

    • Argument: the read-only argument passed to the client-side function;
    • Result: the result to return to the client-side callback function, set from the Callback event handler.

    An example of an handler for the Callback event would be:

       1: protected void OnCallback(Object sender, CallbackEventArgs e)
       2: {
       3:     e.Result = String.Join(String.Empty, e.Argument.Reverse());
       4: }

    Finally, in order to fire the Callback event from the client, you only need this:

       1: <input type="text" id="input"/>
       2: <input type="button" value="Get Result" onclick="document.getElementById('callback').callback(callback(document.getElementById('input').value, 'context', onCallbackSuccess, onCallbackError))"/>

    The syntax of the callback function is:

    • arg: some string argument;
    • context: some context that will be passed to the callback functions (success or failure);
    • callbackSuccessFunction: some function that will be called when the callback succeeds;
    • callbackFailureFunction: some function that will be called if the callback fails for some reason.

    Give it a try and see if it helps! Winking smile

    Read more...

  • ASP.NET MVC Validation Complete

    OK, so let’s talk about validation. Most people are probably familiar with the out of the box validation attributes that MVC knows about, from the System.ComponentModel.DataAnnotations namespace, such as EnumDataTypeAttribute, RequiredAttribute, StringLengthAttribute, RangeAttribute, RegularExpressionAttribute and CompareAttribute from the System.Web.Mvc namespace. All of these validators inherit from ValidationAttribute and perform server as well as client-side validation. In order to use them, you must include the JavaScript files MicrosoftMvcValidation.js, jquery.validate.js or jquery.validate.unobtrusive.js, depending on whether you want to use Microsoft’s own library or jQuery. No significant difference exists, but jQuery is more extensible.

    Read more...