Contents tagged with Controls

  • ASP.NET Web Forms Extensibility: Page Parser Filters

    Introduction

    ASP.NET includes a valuable yet not well known extension point called the page parser filter. I once briefly talked about it in the context of SharePoint, but I feel a more detailed explanation is in order.

    A page parser filter is a class with a public parameterless constructor that inherits from PageParserFilter and is registered on the Web.config file, in the pages section, pageParserFilterType attribute. It is called when ASP.NET is compiling a page, the first time it is called, for every page. There can be only one page parser filter per web application.

    Parser

    Why is it a parser? Well, it parses – or, better, receives a notification for - every control declared on the markup of a page (those with runat=”server” or contained inside of), as well as all of the page’s directives (<%@ … %>). The control declarations include all of its attributes and properties, the recognized control type and any complex properties that the markup contains. This allows us to do all kinds of crazy stuff:

    • Inspect, add and remove page directives;
    • Setting the page’s compilation mode;
    • Insert or remove controls or text literals dynamically at specific places;
    • Add/change/remove a control’s properties or attributes;
    • Even (with some reflection magic) change a control’s type or tag.


    So, how do we all this? First, the parser part. We can inspect all page directives by overriding the PreprocessDirective method. This is called for all page directives:

       1: public override void PreprocessDirective(String directiveName, IDictionary attributes)
       2: {
       3:     if (directiveName == "page")
       4:     {
       5:         //make a page asynchronous
       6:         attributes["async"] = "true";
       7:     }
       8:  
       9:     base.PreprocessDirective(directiveName, attributes);
      10: }

    The page’s compilation mode is controlled by GetCompilationMode:

       1: public override CompilationMode GetCompilationMode(CompilationMode current)
       2: {
       3:     return (base.GetCompilationMode(current));
       4: }

    As for adding controls dynamically, we make use of the ParseComplete method:

       1: public override void ParseComplete(ControlBuilder rootBuilder)
       2: {
       3:     if (rootBuilder is FileLevelPageControlBuilder)
       4:     {
       5:         this.ProcessControlBuilder(rootBuilder);
       6:     }
       7:  
       8:     base.ParseComplete(rootBuilder);
       9: }
      10:  
      11: private void ProcessControlBuilder(ControlBuilder builder)
      12: {
      13:     this.ProcessControlBuilderChildren(builder);
      14:  
      15:     if (builder.ControlType == typeof(HtmlForm))
      16:     {
      17:         //add a Literal control inside the form tag
      18:         var literal = ControlBuilder.CreateBuilderFromType(null, builder, typeof(Literal), "asp:Literal", "literal", new Hashtable { { "Text", "Inserted dynamically I" } }, -1, null);
      19:         builder.AppendSubBuilder(literal);
      20:  
      21:         //add an HTML snippet inside the form tag
      22:         builder.AppendLiteralString("<div>Inserted dynamically II</div>");
      23:     }
      24: }
      25:  
      26: private void ProcessControlBuilderChildren(ControlBuilder parentBuilder)
      27: {
      28:     foreach (var builder in parentBuilder.SubBuilders.OfType<ControlBuilder>())
      29:     {
      30:         this.ProcessControlBuilderChildren(builder);
      31:     }
      32: }

    Same for changing a control’s properties:

       1: private static readonly FieldInfo simplePropertyEntriesField = typeof(ControlBuilder).GetField("_simplePropertyEntries", BindingFlags.Instance | BindingFlags.NonPublic);
       2:  
       3: private void SetControlProperty(ControlBuilder builder, String propertyName, String propertyValue)
       4: {
       5:     var properties = (simplePropertyEntriesField.GetValue(builder) as IEnumerable);
       6:  
       7:     if (properties == null)
       8:     {
       9:         properties = new ArrayList();
      10:         simplePropertyEntriesField.SetValue(builder, properties);
      11:     }
      12:  
      13:     var entry = properties.OfType<SimplePropertyEntry>().SingleOrDefault(x => x.Name == propertyName) ?? simplePropertyEntryConstructor.Invoke(null) as SimplePropertyEntry;
      14:     entry.Name = propertyName;
      15:     entry.UseSetAttribute = (builder.ControlType != null && builder.ControlType.GetProperties().Any(x => x.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) == false);
      16:     entry.PersistedValue = propertyValue;
      17:     entry.Value = entry.PersistedValue;
      18:     entry.Filter = String.Empty;
      19:  
      20:     if (properties.OfType<SimplePropertyEntry>().Any(x => x.Name == propertyName) == false)
      21:     {
      22:         (properties as ArrayList).Add(entry);
      23:     }
      24: }
      25:  
      26: private void ProcessControlBuilder(ControlBuilder builder)
      27: {
      28:     if (typeof(IEditableTextControl).IsAssignableFrom(builder.ControlType) == true)
      29:     {
      30:         this.SetControlProperty(builder, "Text", "Injected dynamically!");
      31:     }
      32:  
      33:     this.ProcessControlBuilderChildren(builder);
      34: }

    And even changing the control’s output tag or instance type:

       1: private static readonly FieldInfo controlTypeField = typeof(ControlBuilder).GetField("_controlType", BindingFlags.Instance | BindingFlags.NonPublic);
       2: private static readonly FieldInfo tagNameField = typeof(ControlBuilder).GetField("_tagName", BindingFlags.Instance | BindingFlags.NonPublic);
       3:  
       4: private void SetControlType(ControlBuilder builder, Type controlType)
       5: {
       6:     controlTypeField.SetValue(builder, controlType);
       7: }
       8:  
       9: private void SetTagName(ControlBuilder controlBuilder, String tagName)
      10: {
      11:     tagNameField.SetValue(controlBuilder, tagName);
      12: }
      13:  
      14: private void ProcessControlBuilder(ControlBuilder builder)
      15: {
      16:     if (builder.TagName != null)
      17:     {
      18:         this.SetTagName(builder, builder.TagName.ToUpper());
      19:     }
      20:  
      21:     if (builder.ControlType == typeof(MyControl))
      22:     {
      23:         this.SetControlType(builder, typeof(MyDerivedControl));
      24:     }
      25:  
      26:     this.ProcessControlBuilderChildren(builder);
      27: }

    Why would we want to change a control’s type? Well, thing about generics, for once.

    Filter

    And now the filtering part: why is it a filter? Because it allows us to filter and control a number of things:

    • The allowed master page, base page class and source file;
    • The allowed controls;
    • The total number of controls allowed on a page;
    • The total number of direct and otherwise references on a page;
    • Allow or disallow code and event handler declarations;
    • Allow or disallow code blocks (<%= … %>, <%: … %>, <% … %>);
    • Allow or disallow server-side script tags (<script runat=”server”>…</script>);
    • Allow, disallow and change data binding expressions (<%# … %>);
    • Add, change or remove event handler declarations.


    All of the filtering methods and properties described below return a Boolean flag and its base implementation may or may not be called, depending on the logic that we want to impose.

    Allowing or disallowing a base page class is controlled by the AllowBaseType method (the default is to accept):

       1: public override Boolean AllowBaseType(Type baseType)
       2: {
       3:     return (baseType == typeof(MyBasePage));
       4: }

    For master pages, user controls or source files we have the AllowVirtualReference virtual method (again, the default is true):

       1: public override Boolean AllowVirtualReference(String referenceVirtualPath, VirtualReferenceType referenceType)
       2: {
       3:     if (referenceType == VirtualReferenceType.Master)
       4:     {
       5:         return (referenceVirtualPath == "AllowedMaster.Master");
       6:     }
       7:     else if (referenceType == VirtualReferenceType.UserControl)
       8:     {
       9:         return (referenceVirtualPath != "ForbiddenControl.ascx");
      10:     }
      11:  
      12:     return (base.AllowVirtualReference(referenceVirtualPath, referenceType));
      13: }

    Controls are controlled (pun intended) by AllowControl, which also defaults to accept:

       1: public override Boolean AllowControl(Type controlType, ControlBuilder builder)
       2: {
       3:     return (typeof(IForbiddenInterface).IsAssignableFrom(controlType) == false);
       4: }

    This may come in handy to disallow the usage of controls in ASP.NET MVC ASPX views!

    The number of controls and dependencies on a page is defined by NumberOfControlsAllowed, NumberOfDirectDependenciesAllowed and TotalNumberOfDependenciesAllowed. Interesting, the default for all these properties is 0, so we have to return –1:

       1: public override Int32 NumberOfControlsAllowed
       2: {
       3:     get
       4:     {
       5:         return (-1);
       6:     }
       7: }
       8:  
       9: public override Int32 NumberOfDirectDependenciesAllowed
      10: {
      11:     get
      12:     {
      13:         return (-1);
      14:     }
      15: }
      16:  
      17: public override Int32 TotalNumberOfDependenciesAllowed
      18: {
      19:     get
      20:     {
      21:         return (-1);
      22:     }
      23: }

    Direct dependencies are user controls directly declared in the page and indirect ones are those declared inside other user controls.

    Code itself, including event handler declarations, are controlled by AllowCode (default is true):

       1: public override Boolean AllowCode
       2: {
       3:     get
       4:     {
       5:         return (true);
       6:     }
       7: }

    If we want to change a data binding expression, we resort to ProcessDataBindingAttribute, which also returns true by default:

       1: public override Boolean ProcessDataBindingAttribute(String controlId, String name, String value)
       2: {
       3:     if (name == "Text")
       4:     {
       5:         //Do not allow binding the Text property
       6:         return (false);
       7:     }
       8:  
       9:     return (base.ProcessDataBindingAttribute(controlId, name, value));
      10: }

    For intercepting event handlers, there’s the ProcessEventHook, which likewise returns true by default:

       1: public override Boolean ProcessEventHookup(String controlId, String eventName, String handlerName)
       2: {
       3:     if (eventName == "SelectedIndexChanged")
       4:     {
       5:         //Remove event handlers for the SelectedIndexChanged event
       6:         return (false);
       7:     }
       8:  
       9:     return (base.ProcessEventHookup(controlId, eventName, handlerName));
      10: }

    And finally, for code blocks, server-side scripts and data binding expressions, there’s the ProcessCodeConstruct method, which likewise also allows everything by default:

    Conclusion

    This was in no means an in-depth description of page parser filters, I just meant to give you an idea of its (high) potential. It is very useful to restrict what end users can place on their pages (SharePoint style) as well as for adding dynamic control programmatically in specific locations of the page, before it is actually built.

    As usual, let me hear your thoughts! Winking smile

    Read more...

  • Video Streaming with ASP.NET SignalR and HTML5

    I have already talked about SignalR in this blog. I think it is one of the most interesting technologies that Microsoft put out recently, not because it is something substantially new – AJAX, long polling and server-sent events have been around for quite some time -, but because of how easy and extensive they made it.

    Most of the examples of SignalR usually are about chat. You know that I have been digging into HTML5 lately, and I already posted on media acquisition using HTML5’s getUserMedia API. This time, however, I’m going to talk about video streaming!

    I wrote a simple ASP.NET Web Forms control that leverages SignalR to build a real-time communication channel, and this channel transmits images as Data URIs. The source of the feed comes from getUserMedia and is therefore available in all modern browsers, except, alas, one that shall remain unnamed. Any browser, however, can be used to display the streaming feed.

    So, the architecture is quite simple:

    • One SignalR Hub for the communication channel;
    • One ASP.NET Web Forms control, that renders an HTML5 VIDEO tag that is used to display the video being acquired, on any compatible browser.

    And that’s it. Of course, the page where we are hosting the control needs to have references to the required JavaScript files (currently jQuery >= 1.6.4 and jQuery-SignalR 2.0.3), these are included automatically when you add the Nuget package.

    Here are some nice screenshots of my now famous home wall, where one of the browser instances, Chrome, is broadcasting to Firefox, Opera and Safari.

    image

    So, all we need is a control declaration on an ASP.NET Web Forms page, which could look like this:

       1: <web:VideoStreaming runat="server" ID="video" ClientIDMode="Static" Width="300px" Height="300px" Interval="100" Source="True" ScalingMode="TargetSize" StreamingMode="Target" TargetClientID="received" OnStreamed="onStreamed" Style="border: solid 1px black" />

    The non-ordinary properties that the VideoStreaming control supports are:

    • Interval: the rate in milliseconds that the control broadcasts that the video is being broadcast;
    • OnStreamed: the name of a JavaScript callback function to call which will receive the streamed image as a Data URI when the StreamingMode is set to Event, not used in any of the other modes;
    • ScalingMode: how the image is scaled; possible values are None, OriginalSize, TargetSize (the size of the target CANVAS or IMG element when using StreamingMode Target) and ControlSize (the size of the VideoStreaming control);
    • Source: if the control is set as a streaming source, or just as a receiver (default);
    • StreamingMode: the possible streaming options: None, does not do anything, Target, sends the image directly to a target CANVAS or IMG element; Event, raises a JavaScript event with the streamed image; and Window, opens up a new browser window which displays the images;
    • TargetClientID: the id of a target CANVAS or IMG element that will receive the images as they are streamed, when using StreamingMode Target, not used in any of the other modes.

    Here is an example of a JavaScript function that serves as the callback function for the Event streaming mode:

       1: function onStreamed(imageUrl, imageWidth, imageHeight)
       2: {
       3:     //for StreamingMode="Event", draw an image on an existing canvas
       4:     //the onload event is for cross-browser compatibility
       5:     //in this example, we are using the canvas width and height
       6:     var canvas = document.getElementById('received');
       7:     var ctx = canvas.getContext('2d');
       8:     var img = new Image();
       9:     img.src = imageUrl;
      10:     img.onload = function()
      11:     {
      12:         ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      13:     }
      14: }

    And a simple CANVAS element for Target:

       1: <canvas id="received" width="300" height="300" style="border: solid 1px black"></canvas>

    The reason for the two main streaming modes, Target and Event are flexibility: with Target, you directly draw the streamed picture on a CANVAS or IMG (it will detect automatically what to use), which needs to be already present, and with Event, you can do your own custom processing.

    The source for the VideoStreaming control is this:

       1: [assembly: OwinStartup(typeof(VideoStreaming))]
       2:  
       3: public class VideoStreaming : WebControl
       4: {
       5:     public const String Url = "/videostreaming";
       6:  
       7:     public VideoStreaming() : base("video")
       8:     {
       9:         this.Interval = 100;
      10:         this.OnStreamed = String.Empty;
      11:         this.ScalingMode = VideoScalingMode.None;
      12:         this.Source = false;
      13:         this.StreamingMode = VideoStreamingMode.Event;
      14:         this.TargetClientID = String.Empty;        
      15:     }
      16:  
      17:     [DefaultValue(false)]
      18:     public Boolean Source
      19:     {
      20:         get;
      21:         set;
      22:     }
      23:  
      24:     [DefaultValue(VideoStreamingMode.Event)]
      25:     public VideoStreamingMode StreamingMode
      26:     {
      27:         get;
      28:         set;
      29:     }
      30:  
      31:     [DefaultValue(100)]
      32:     public Int32 Interval
      33:     {
      34:         get;
      35:         set;
      36:     }
      37:  
      38:     [DefaultValue("")]
      39:     public String OnStreamed
      40:     {
      41:         get;
      42:         set;
      43:     }
      44:  
      45:     [DefaultValue("")]
      46:     public String TargetClientID
      47:     {
      48:         get;
      49:         set;
      50:     }
      51:  
      52:     [DefaultValue(VideoScalingMode.None)]
      53:     public VideoScalingMode ScalingMode
      54:     {
      55:         get;
      56:         set;
      57:     }
      58:  
      59:     public static void Configuration(IAppBuilder app)
      60:     {
      61:         app.MapSignalR(Url, new HubConfiguration());
      62:     }
      63:     
      64:     protected override void OnLoad(EventArgs e)
      65:     {            
      66:         var sm = ScriptManager.GetCurrent(this.Page);
      67:         var streamingAction = String.Empty;
      68:         var size = (this.ScalingMode == VideoScalingMode.OriginalSize) ? ", imageWidth, imageHeight" : (this.ScalingMode == VideoScalingMode.TargetSize) ? ", canvas.width, canvas.height" : (this.ScalingMode == VideoScalingMode.ControlSize) ? String.Format(", {0}, {1}", this.Width.Value, this.Height.Value) : String.Empty;
      69:  
      70:         switch (this.StreamingMode)
      71:         {
      72:             case VideoStreamingMode.Event:
      73:                 if (String.IsNullOrWhiteSpace(this.OnStreamed) == true)
      74:                 {
      75:                     throw (new InvalidOperationException("OnStreamed cannot be empty when using streaming mode Event"));
      76:                 }
      77:                 streamingAction = String.Format("{0}(imageUrl, imageWidth, imageHeight)", this.OnStreamed);
      78:                 break;
      79:  
      80:             case VideoStreamingMode.Target:
      81:                 if (String.IsNullOrWhiteSpace(this.TargetClientID) == true)
      82:                 {
      83:                     throw (new InvalidOperationException("TargetClientID cannot be empty when using streaming mode Target"));
      84:                 }
      85:                 streamingAction = String.Format("var canvas = document.getElementById('{0}'); if (canvas.tagName == 'CANVAS') {{ var ctx = canvas.getContext('2d'); var img = new Image(); }} else if (canvas.tagName == 'IMG') {{ var img = canvas; }}; img.src = imageUrl; img.width = imageWidth; img.height = imageHeight; if (typeof(ctx) != 'undefined') {{ img.onload = function() {{\n ctx.drawImage(img, 0, 0{1}); \n}} }};", this.TargetClientID, size);
      86:                 break;
      87:  
      88:             case VideoStreamingMode.Window:
      89:                 streamingAction = String.Format("if (typeof(window.videoWindow) == 'undefined') {{ window.videoWindow = window.open(imageUrl, '_blank', 'width=imageWidth,height=imageHeight'); }} else {{ window.videoWindow.location.href = imageUrl; }};");
      90:                 break;
      91:         }
      92:  
      93:         var initScript = String.Format("\ndocument.getElementById('{0}').connection = $.hubConnection('{1}', {{ useDefaultPath: false }}); document.getElementById('{0}').proxy = document.getElementById('{0}').connection.createHubProxy('videoStreamingHub');\n", this.ClientID, Url);
      94:         var startStreamScript = String.Format("\ndocument.getElementById('{0}').startStream = function(){{\n var video = document.getElementById('{0}'); video.proxy.on('send', function(imageUrl, imageWidth, imageHeight) {{\n {2} \n}}); video.connection.start().done(function() {{\n if ((true == {3}) && (video.paused == true) && (video.src == '')) {{\n navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia); navigator.getUserMedia({{ video: true, audio: false }}, function (stream) {{\n video.src = window.URL.createObjectURL(stream); \n}}, function (error) {{\n debugger; \n}}); \n}}; if (video.intervalId) {{\n window.cancelAnimationFrame(video.intervalId); \n}}; var fn = function(time) {{\nif (time >= {1} && video.intervalId != 0) {{ var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); context.drawImage(video, 0, 0, canvas.width, canvas.height); var picture = canvas.toDataURL(); video.proxy.invoke('send', picture, video.videoWidth, video.videoHeight); }}; window.requestAnimationFrame(fn); \n}}; if (true == {3}) {{ video.intervalId = window.requestAnimationFrame(fn); }}; video.play(); \n}}) }}\n", this.ClientID, this.Interval, streamingAction, this.Source.ToString().ToLower());
      95:         var stopStreamScript = String.Format("\ndocument.getElementById('{0}').stopStream = function(){{ var video = document.getElementById('{0}'); if (video.intervalId) {{ window.cancelAnimationFrame(video.intervalId); }}; video.intervalId = 0; video.pause(); video.connection.stop(); }};\n", this.ClientID);
      96:         var script = String.Concat(initScript, startStreamScript, stopStreamScript);
      97:  
      98:         if (sm != null)
      99:         {
     100:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(Url, this.ClientID), String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ {0} }});\n", script), true);
     101:         }
     102:         else
     103:         {
     104:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(Url, this.ClientID), script, true);
     105:         }
     106:  
     107:         if (this.Width != Unit.Empty)
     108:         {
     109:             this.Attributes.Add(HtmlTextWriterAttribute.Width.ToString().ToLower(), this.Width.ToString());
     110:         }
     111:  
     112:         if (this.Height != Unit.Empty)
     113:         {
     114:             this.Attributes.Add(HtmlTextWriterAttribute.Height.ToString().ToLower(), this.Height.ToString());
     115:         }
     116:  
     117:         this.Attributes.Remove("autoplay");
     118:         this.Attributes.Remove("controls");
     119:         this.Attributes.Remove("crossorigin");
     120:         this.Attributes.Remove("loop");
     121:         this.Attributes.Remove("mediagroup");
     122:         this.Attributes.Remove("muted");
     123:         this.Attributes.Remove("poster");
     124:         this.Attributes.Remove("preload");
     125:         this.Attributes.Remove("src");
     126:  
     127:         base.OnLoad(e);
     128:     }
     129: }

    In essence, it is very simple, although it has some nasty inline JavaScript. Basically, what it does is:

    • Renders a VIDEO tag element with the specified Width and Height;
    • Register a SignalR hub;
    • Attaches some JavaScript methods to the generated HTML VIDEO tag (startStream, stopStream);
    • Removes any eventual VIDEO-related attribute that may be present on the control declaration;
    • It uses requestAnimationFrame for periodically (every Interval in milliseconds) broadcasting the image obtained from getUserMedia and drawn on the rendered VIDEO tag as a Data URI to the SignalR hub.

    Of course, we now need the hub code, which is as simple as it could be, just a method that takes the image as a Data URI and its original dimensions and broadcasts it to the world:

       1: public class VideoStreamingHub : Hub
       2: {
       3:     public void Send(String imageUrl, Int32 imageWidth, Int32 imageHeight)
       4:     {
       5:         this.Clients.All.Send(imageUrl, imageWidth, imageHeight);
       6:     }
       7: }

    And finally the simple enumerations used by the VideoStreaming control:

       1: public enum VideoScalingMode
       2: {
       3:     /// <summary>
       4:     /// No scaling is performed.
       5:     /// </summary>
       6:     None,
       7:  
       8:     /// <summary>
       9:     /// Use the original size, coming from getUserMedia.
      10:     /// </summary>
      11:     OriginalSize,
      12:  
      13:     /// <summary>
      14:     /// Use the target CANVAS size.
      15:     /// </summary>
      16:     TargetSize,
      17:  
      18:     /// <summary>
      19:     /// Use the VideoStreaming control size.
      20:     /// </summary>
      21:     ControlSize
      22: }
      23:  
      24: public enum VideoStreamingMode
      25:     {
      26:         /// <summary>
      27:         /// No streaming.
      28:         /// </summary>
      29:         None,
      30:  
      31:         /// <summary>
      32:         /// Raises a JavaScript event.
      33:         /// </summary>
      34:         Event,
      35:  
      36:         /// <summary>
      37:         /// Draws directly to a target CANVAS.
      38:         /// </summary>
      39:         Target,
      40:  
      41:         /// <summary>
      42:         /// Draws in a new window.
      43:         /// </summary>
      44:         Window
      45:     }

    And there you have it! Just drop the VideoStreaming control on an ASPX page and you’re done! If you want to have it broadcast, you need to set its Source property to true, for example, on the containing page:

       1: protected override void OnInit(EventArgs e)
       2: {
       3:     var source = false;
       4:  
       5:     if (Boolean.TryParse(this.Request.QueryString["Source"], out source) == true)
       6:     {
       7:         this.video.Source = source;
       8:     }
       9:  
      10:     if (source == false)
      11:     {
      12:         //if we are not broadcasting, just hide the VIDEO tag
      13:         this.video.Style[HtmlTextWriterStyle.Display] = "none";
      14:     }
      15:  
      16:     base.OnInit(e);
      17: }

    For starting and stopping streaming, you just call the JavaScript functions startStream and stopStream:

       1: <script type="text/javascript">
       1:  
       2:  
       3:     function startStreaming()
       4:     {
       5:         document.getElementById('video').startStream();
       6:     }
       7:  
       8:     function stopStreaming()
       9:     {
      10:         document.getElementById('video').stopStream();
      11:     }
      12:  
    </script>
       2: <input type="button" value="Start Streaming" onclick="startStreaming()"/>
       3: <input type="button" value="Stop Streaming" onclick="stopStreaming()" />

     

    That’s it. Very simple video streaming without the need for any plugin or server. Hope you enjoy it!

    Read more...

  • 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...

  • Extended ASP.NET Button Control

    I once had the need to have a button control that would change its look depending on a theme, for example, it would render either as regular button, an image or a link. Of course, the only way I had to achieve this was by manually swapping the Button control for a ImageButton or a LinkButton, which wasn’t really a solution, so I started to think of a control that could do the trick… and here it is!

    Basically, I wrote a button control that displays in one of 5 ways:

    • A regular button:
    image
    • A text hyperlink:

    image

    • An image:

    image

    • A button with HTML content (see this):

    image

    • A hyperlink with HTML content:

    image

    In ASP.NET terms, I have a server-side control with a ButtonType property. The markup that produces each effect is as follows:

       1: <web:ExtendedButton runat="server" ButtonType="Button" ID="button" Text="Button"/>
       2:  
       3: <web:ExtendedButton runat="server" ButtonType="Link" ID="link" Text="Link"/>
       4:  
       5: <web:ExtendedButton runat="server" ButtonType="Image" ID="image" ImageUrl="~/Images/button.png"/>
       6:  
       7: <web:ExtendedButton runat="server" ButtonType="Button" ID="buttonWithTemplate">
       8:     <Template>
       9:             <asp:Image runat="server" ImageUrl="~/Images/button.png" />
      10:             Button With Template
      11:         </Template>
      12: </web:ExtendedButton>
      13:  
      14: <web:ExtendedButton runat="server" ButtonType="Link" ID="linkWithTemplate">
      15:         <Template>
      16:             <asp:Image runat="server" ImageUrl="~/Images/button.png" />
      17:             Link With Template
      18:         </Template>
      19: </web:ExtendedButton>

    For the Image value of ButtonType, the only useful properties are ImageUrl, ImageAlign and AlternateText. These will work in the exact same way as the ImageButton control.

    For Link and Button, if the Template property is not specified, Text will be used for the textual description of the button or link. If instead a Template is available, it will be used instead of the Text. Keep in mind that you can specify almost any HTML you like for the Template, as long as it can be surrounded by an A (in the case of the Link type) or BUTTON (for Button) tags. If no Template is supplied, it will render and behave just like a LinkButton or a Button.

    This control implements IButtonControl, so it shares the usual behavior of regular button controls, like having a text property, a validation group, a postback URL, Click and Command events, event bubbling, etc. It uses the control state to save some properties, so it is safe to turn off view state in it.

    I almost forgot: here is the code!

       1: [ParseChildren(true)]
       2: [DefaultEvent("Click")]
       3: [PersistChildren(false)]
       4: [DefaultProperty("Text")]
       5: [SupportsEventValidation]
       6: [ToolboxData("<{0}:ExtendedButton runat=\"server\" Text=\"\" />")]
       7: public class ExtendedButton : WebControl, IButtonControl, IPostBackEventHandler, INamingContainer, ITextControl, IPostBackDataHandler
       8: {
       9:     #region Private static readonly fields
      10:     private static readonly Object EventClick = new Object();
      11:     private static readonly Object EventCommand = new Object();
      12:     #endregion
      13:  
      14:     #region Public constructor
      15:     public ExtendedButton() : base(HtmlTextWriterTag.Input)
      16:     {
      17:         this.ButtonType = ButtonType.Button;
      18:         this.CommandArgument = String.Empty;
      19:         this.CommandName = String.Empty;
      20:         this.OnClientClick = String.Empty;
      21:         this.ImageAlign = ImageAlign.NotSet;
      22:         this.ImageUrl = String.Empty;
      23:         this.PostBackUrl = String.Empty;
      24:         this.CausesValidation = true;
      25:         this.ValidationGroup = String.Empty;
      26:         this.UseSubmitBehavior = true;
      27:         this.Text = String.Empty;
      28:     }
      29:     #endregion
      30:  
      31:     #region Protected override methods
      32:     protected override void AddAttributesToRender(HtmlTextWriter writer)
      33:     {
      34:         base.AddAttributesToRender(writer);
      35:  
      36:         this.Page.VerifyRenderingInServerForm(this);
      37:  
      38:         if (this.ButtonType == ButtonType.Button)
      39:         {
      40:             if (this.Template == null)
      41:             {
      42:                 if (this.UseSubmitBehavior == true)
      43:                 {
      44:                     writer.AddAttribute(HtmlTextWriterAttribute.Type, "submit");
      45:                 }
      46:                 else
      47:                 {
      48:                     writer.AddAttribute(HtmlTextWriterAttribute.Type, "button");
      49:                 }
      50:             }
      51:  
      52:             writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);
      53:             writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
      54:         }
      55:         else if (this.ButtonType == ButtonType.Image)
      56:         {
      57:             writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
      58:             writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);
      59:             writer.AddAttribute(HtmlTextWriterAttribute.Alt, this.AlternateText);
      60:             writer.AddAttribute(HtmlTextWriterAttribute.Type, "image");
      61:             writer.AddAttribute(HtmlTextWriterAttribute.Src, HttpUtility.HtmlEncode(this.ResolveClientUrl(this.ImageUrl)));
      62:  
      63:             if (this.ImageAlign != ImageAlign.NotSet)
      64:             {
      65:                 writer.AddAttribute(HtmlTextWriterAttribute.Align, this.ImageAlign.ToString().ToLower());
      66:             }
      67:         }
      68:  
      69:         String firstScript = this.OnClientClick;
      70:         PostBackOptions postBackOptions = this.GetPostBackOptions();
      71:                     
      72:         if (this.IsEnabled == true)
      73:         {
      74:             if (this.HasAttributes == true)
      75:             {
      76:                 String script = this.Attributes[HtmlTextWriterAttribute.Onclick.ToString()];
      77:  
      78:                 if (String.IsNullOrWhitespace(script) == false)
      79:                 {
      80:                     firstScript = String.Join(";", new String[] { firstScript, script });
      81:                     this.Attributes.Remove(HtmlTextWriterAttribute.Onclick.ToString());
      82:                 }
      83:             }
      84:  
      85:             String postBackEventReference = this.Page.ClientScript.GetPostBackEventReference(postBackOptions, true);
      86:  
      87:             if (String.IsNullOrWhiteSpace(postBackEventReference) == false)
      88:             {
      89:                 firstScript = firstScript + postBackEventReference;
      90:  
      91:                 if ((this.ButtonType == ButtonType.Link) || ((this.ButtonType == ButtonType.Button) && (this.Template != null)))
      92:                 {
      93:                     writer.AddAttribute(HtmlTextWriterAttribute.Href, postBackEventReference);
      94:                 }
      95:             }
      96:             else
      97:             {
      98:                 if (this.ButtonType == ButtonType.Link)
      99:                 {
     100:                     writer.AddAttribute(HtmlTextWriterAttribute.Href, "javascript:void(0)");
     101:                 }
     102:             }
     103:         }
     104:  
     105:         if (firstScript.Length > 0)
     106:         {
     107:             if (this.ButtonType == ButtonType.Button)
     108:             {
     109:                 if (this.UseSubmitBehavior == false)
     110:                 {
     111:                     writer.AddAttribute(HtmlTextWriterAttribute.Onclick, firstScript);
     112:                 }
     113:             }
     114:             else
     115:             {
     116:                 if (String.IsNullOrWhiteSpace(this.OnClientClick) == false)
     117:                 {
     118:                     writer.AddAttribute(HtmlTextWriterAttribute.Onclick, this.OnClientClick);
     119:                 }
     120:             }
     121:         }
     122:  
     123:         if ((this.Enabled == true) && (this.IsEnabled == false))
     124:         {
     125:             writer.AddAttribute(HtmlTextWriterAttribute.Disabled, HtmlTextWriterAttribute.Disabled.ToString().ToLower());
     126:         }
     127:     }
     128:  
     129:     protected override void OnInit(EventArgs e)
     130:     {
     131:         this.Page.RegisterRequiresControlState(this);
     132:  
     133:         if (this.ButtonType == ButtonType.Image)
     134:         {
     135:             this.Page.RegisterRequiresPostBack(this);
     136:         }
     137:  
     138:         base.OnInit(e);
     139:     }
     140:  
     141:     protected override void RenderContents(HtmlTextWriter writer)
     142:     {
     143:         if ((this.ButtonType == ButtonType.Link) || (this.ButtonType == ButtonType.Button))
     144:         {
     145:             if (this.Template != null)
     146:             {
     147:                 PlaceHolder placeHolder = new PlaceHolder();
     148:                 this.Template.InstantiateIn(placeHolder);
     149:                 this.Controls.Add(placeHolder);
     150:                 base.RenderContents(writer);
     151:             }
     152:             else
     153:             {
     154:                 if (this.ButtonType == ButtonType.Link)
     155:                 {
     156:                     writer.WriteEncodedText(this.Text);
     157:                 }
     158:             }
     159:         }
     160:     }
     161:  
     162:     protected override void LoadControlState(Object savedState)
     163:     {
     164:         Object [] state = savedState as Object [];
     165:  
     166:         this.OnClientClick = (String) state [ 1 ];
     167:         this.CausesValidation = (Boolean) state [ 2 ];
     168:         this.ValidationGroup = (String) state [ 3 ];
     169:         this.ButtonType = (ButtonType) state [ 4 ];
     170:         this.PostBackUrl = (String) state [ 5 ];
     171:         this.UseSubmitBehavior = (Boolean) state [ 6 ];
     172:         this.CommandArgument = (String) state [ 7 ];
     173:         this.CommandName = (String) state [ 8 ];
     174:         this.Text = (String) state [ 9 ];
     175:         this.ImageUrl = (String) state [ 10 ];
     176:         this.ImageAlign = (ImageAlign) state [ 11 ];
     177:  
     178:         base.LoadControlState(state [ 0 ]);
     179:     }
     180:  
     181:     protected override Object SaveControlState()
     182:     {
     183:         Object [] state = new Object [] { base.SaveControlState(), this.OnClientClick, this.CausesValidation, this.ValidationGroup, this.ButtonType, this.PostBackUrl, this.UseSubmitBehavior, this.CommandArgument, this.CommandName, this.Text, this.ImageUrl, this.ImageAlign };
     184:         return (state);
     185:     }
     186:     #endregion
     187:  
     188:     #region Public override methods
     189:     public override void RenderBeginTag(HtmlTextWriter writer)
     190:     {
     191:         this.AddAttributesToRender(writer);
     192:  
     193:         switch (this.ButtonType)
     194:         {
     195:             case ButtonType.Button:
     196:                 if (this.Template != null)
     197:                 {
     198:                     writer.RenderBeginTag(HtmlTextWriterTag.Button);
     199:                 }
     200:                 else
     201:                 {
     202:                     writer.RenderBeginTag(HtmlTextWriterTag.Input);
     203:                 }
     204:                 break;
     205:  
     206:             case ButtonType.Image:
     207:                 writer.RenderBeginTag(HtmlTextWriterTag.Input);
     208:                 break;
     209:  
     210:             case ButtonType.Link:
     211:                 writer.RenderBeginTag(HtmlTextWriterTag.A);
     212:                 break;
     213:         }
     214:     }
     215:     #endregion
     216:  
     217:     #region Protected virtual methods
     218:     protected virtual PostBackOptions GetPostBackOptions()
     219:     {
     220:         PostBackOptions options = new PostBackOptions(this, String.Empty);
     221:         options.ClientSubmit = false;
     222:  
     223:         if ((this.CausesValidation == true) && (this.Page.GetValidators(this.ValidationGroup).Count > 0))
     224:         {
     225:             options.PerformValidation = true;
     226:             options.ValidationGroup = this.ValidationGroup;
     227:         }
     228:  
     229:         if (String.IsNullOrWhiteSpace(this.PostBackUrl) == false)
     230:         {
     231:             options.ActionUrl = HttpUtility.UrlPathEncode(this.ResolveClientUrl(this.PostBackUrl));
     232:         }
     233:  
     234:         if ((this.ButtonType == ButtonType.Link) || ((this.ButtonType == ButtonType.Button) && (this.Template != null)))
     235:         {
     236:             options.ClientSubmit = true;
     237:             options.RequiresJavaScriptProtocol = true;
     238:         }
     239:  
     240:         return (options);
     241:     }
     242:  
     243:     protected virtual void OnClick(EventArgs e)
     244:     {
     245:         EventHandler handler = (EventHandler) this.Events [ EventClick ];
     246:  
     247:         if (handler != null)
     248:         {
     249:             handler(this, e);
     250:         }
     251:     }
     252:  
     253:     protected virtual void OnCommand(CommandEventArgs e)
     254:     {
     255:         CommandEventHandler handler = (CommandEventHandler) this.Events [ EventCommand ];
     256:  
     257:         if (handler != null)
     258:         {
     259:             handler(this, e);
     260:         }
     261:     }
     262:     #endregion
     263:  
     264:     #region IButtonControl Members
     265:     public event EventHandler Click
     266:     {
     267:         add
     268:         {
     269:             this.Events.AddHandler(EventClick, value);
     270:         }
     271:         remove
     272:         {
     273:             this.Events.RemoveHandler(EventClick, value);
     274:         }
     275:     }
     276:  
     277:     public event CommandEventHandler Command
     278:     {
     279:         add
     280:         {
     281:             this.Events.AddHandler(EventCommand, value);
     282:         }
     283:         remove
     284:         {
     285:             this.Events.RemoveHandler(EventCommand, value);
     286:         }
     287:     }
     288:  
     289:     [DefaultValue(true)]
     290:     [Themeable(false)]
     291:     public Boolean CausesValidation
     292:     {
     293:         get;
     294:         set;
     295:     }
     296:  
     297:     [Bindable(true)]
     298:     [DefaultValue("")]
     299:     [Themeable(false)]
     300:     public String CommandArgument
     301:     {
     302:         get;
     303:         set;
     304:     }
     305:  
     306:     [Themeable(false)]
     307:     [DefaultValue("")]
     308:     public String CommandName
     309:     {
     310:         get;
     311:         set;
     312:     }
     313:  
     314:     [DefaultValue("")]
     315:     [Themeable(false)]
     316:     [UrlProperty("*.aspx")]
     317:     public String PostBackUrl
     318:     {
     319:         get;
     320:         set;
     321:     }
     322:  
     323:     [DefaultValue("")]
     324:     [Themeable(false)]
     325:     public String ValidationGroup
     326:     {
     327:         get;
     328:         set;
     329:     }
     330:  
     331:     [DefaultValue("")]
     332:     [Localizable(true)]
     333:     [Bindable(true)]
     334:     public String Text
     335:     {
     336:         get;
     337:         set;
     338:     }
     339:     #endregion
     340:  
     341:     #region Public properties
     342:     [Browsable(false)]
     343:     [TemplateContainer(typeof(ExtendedButton))]
     344:     [TemplateInstance(TemplateInstance.Single)]
     345:     [PersistenceMode(PersistenceMode.InnerProperty)]
     346:     public ITemplate Template
     347:     {
     348:         get;
     349:         set;
     350:     }
     351:  
     352:     [DefaultValue(ButtonType.Button)]
     353:     public ButtonType ButtonType
     354:     {
     355:         get;
     356:         set;
     357:     }
     358:  
     359:     [Themeable(false)]
     360:     [DefaultValue("")]
     361:     public String OnClientClick
     362:     {
     363:         get;
     364:         set;
     365:     }
     366:  
     367:     [DefaultValue("")]
     368:     [Themeable(false)]
     369:     [UrlProperty("*.jpg;*.gif;*.png")]
     370:     public String ImageUrl
     371:     {
     372:         get;
     373:         set;
     374:     }
     375:  
     376:     [DefaultValue(ImageAlign.NotSet)]
     377:     public ImageAlign ImageAlign
     378:     {
     379:         get;
     380:         set;
     381:     }
     382:  
     383:     [DefaultValue("")]
     384:     [Themeable(false)]
     385:     public String AlternateText
     386:     {
     387:         get;
     388:         set;
     389:     }
     390:  
     391:     [DefaultValue(true)]
     392:     [Themeable(false)]
     393:     public Boolean UseSubmitBehavior
     394:     {
     395:         get;
     396:         set;
     397:     }
     398:     #endregion
     399:  
     400:     #region IPostBackEventHandler Members
     401:     void IPostBackEventHandler.RaisePostBackEvent(String eventArgument)
     402:     {
     403:         this.Page.ClientScript.ValidateEvent(this.UniqueID, eventArgument);
     404:  
     405:         if (this.CausesValidation == true)
     406:         {
     407:             this.Page.Validate(this.ValidationGroup);
     408:         }
     409:  
     410:         this.OnClick(EventArgs.Empty);
     411:         this.OnCommand(new CommandEventArgs(this.CommandName, this.CommandArgument));
     412:  
     413:         this.RaiseBubbleEvent(this, EventArgs.Empty);
     414:     }
     415:     #endregion
     416:  
     417:     #region IPostBackDataHandler Members
     418:  
     419:     Boolean IPostBackDataHandler.LoadPostData(String postDataKey, NameValueCollection postCollection)
     420:     {
     421:         if (postDataKey == this.UniqueID)
     422:         {
     423:             if (this.ButtonType == ButtonType.Image)
     424:             {
     425:                 if ((String.IsNullOrWhiteSpace(postCollection[postDataKey + ".x"]) == false) && (String.IsNullOrWhiteSpace(postCollection[postDataKey + ".y"]) == false))
     426:                 {
     427:                     (this as IPostBackEventHandler).RaisePostBackEvent(String.Empty);                    
     428:                 }
     429:             }
     430:             else
     431:             {
     432:                 (this as IPostBackEventHandler).RaisePostBackEvent(String.Empty);
     433:             }
     434:         }
     435:  
     436:         return(false);
     437:     }
     438:  
     439:     void IPostB