Development With A Dot

Blog on development in general, and specifically on .NET

Sponsors

News

My Friends

My Links

Permanent Posts

Portuguese Communities

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!

Making Better Use of the NHibernate HiLo Generator

Introduction

NHibernate’s HiLo (High-Low) id generation algorithm is one of the most commonly used, and for good reasons:

  • It is database-independent, that is, does not rely on any database-specific functionality such as SQL Server’s IDENTITY and Oracle’s SEQUENCE;
  • It allows batching of inserts;
  • It complies with the Unit of Work pattern, because it sends all writes at the same time (when the session is flushed);
  • Your code does not need to know or care about it.

Now, this post does not intent to explain this algorithm in depth, for that I recommend the NHibernate HiLo Identity Generator article or Choosing a Primary Key: Natural or Surrogate?, for a more in-depth discussion of id generation strategies. Here I will talk about how to make better use of the NHibernate implementation.

Max Low

First of all, you can configure the max low value for the algorithm, using by code mapping, like this:

   1: x.Generator(Generators.HighLow, g => g.Params(new { max_lo = 100 }));

The default max low value is 32767. When choosing a lower or a higher value, you should take into consideration:

  • The next high value is updated whenever a new session factory is created, or the current low reaches the max low value;
  • If you have a big number of inserts, it might pay off to have a higher max low, because NHibernate won’t have to go to the database when the current range is exhausted;
  • If the session factory is frequently restarted, a lower value will prevent gaps.

There is no magical number, you will need to find the one that best suits your needs.

One Value for All Entities

With the default configuration of HiLo, a single table, row and column will be used to store the next high value for all entities using HiLo. The by code configuration is as follows:

   1: this.Id(x => x.SomeId, x =>
   2: {
   3:     x.Column("some_id");
   4:     x.Generator(Generators.HighLow);
   5: });

The default table is called HIBERNATE_UNIQUE_KEY, and its schema is very simple:

image

Whenever NHibernate wants to obtain and increment the current next high value, it will issue SQL like this (for SQL Server):

   1: -- select current value
   2: select next_hi
   3: from hibernate_unique_key with (updlock, rowlock)
   4:  
   5: -- update current value
   6: update hibernate_unique_key
   7: set next_hi = @p0
   8: where next_hi = @p1;

There are pros and cons to this default approach:

  • Each record will have a different id, there will never be two entities with the same id;
  • Because of the sharing between all entities, the ids will grow much faster;
  • When used simultaneously by several applications, there will be some contention on the table, because it is being locked whenever the next high value is obtained and incremented;
  • The HIBERNATE_UNIQUE_KEY table is managed automatically by NHibernate (created, dropped and populated).

One Row Per Entity

Another option to consider, which is supported by NHibernate’s HiLo generator, consists of having each entity storing its next high value in a different row. You achieve this by supplying a where parameter to the generator:

   1: this.Id(x => x.SomeId, x =>
   2: {
   3:     x.Column("some_id");
   4:     x.Generator(Generators.HighLow, g => g.Params(new { where = "entity_type = 'some_entity'" }));
   5: });

In it, you would specify a restriction on an additional column. The problem is, NHibernate knows nothing about this other column, so it won’t create it.

One way to go around this is by using an auxiliary database object (maybe a topic for another post). This is a standard NHibernate functionality that allows registering SQL to be executed when the database schema is created, updated or dropped. Using mapping by code, it is applied like this:

   1: private static IAuxiliaryDatabaseObject OneHiLoRowPerEntityScript(Configuration cfg, String columnName, String columnValue)
   2: {
   3:     var dialect = Activator.CreateInstance(Type.GetType(cfg.GetProperty(NHibernate.Cfg.Environment.Dialect))) as Dialect;
   4:     var script = new StringBuilder();
   5:  
   6:     script.AppendFormat("ALTER TABLE {0} {1} {2} {3} NULL;\n{4}\nINSERT INTO {0} ({5}, {2}) VALUES (1, '{6}');\n{4}\n", TableHiLoGenerator.DefaultTableName, dialect.AddColumnString, columnName, dialect.GetTypeName(SqlTypeFactory.GetAnsiString(100)), (dialect.SupportsSqlBatches == true ? "GO" : String.Empty), TableHiLoGenerator.DefaultColumnName, columnValue);
   7:  
   8:     return (new SimpleAuxiliaryDatabaseObject(script.ToString(), null));
   9: }
  10:  
  11: Configuration cfg = ...;
  12: cfg.AddAuxiliaryDatabaseObject(OneHiLoRowPerEntityScript(cfg, "entity_type", "some_entity"));

Keep in mind that this needs to go before the session factory is built. Basically, we are creating a SQL ALTER TABLE followed by an INSERT statement that change the default HiLo table and add another column that will serve as the discriminator. For making it cross-database, I used the registered Dialect class.

Its schema will then look like this:

image

When NHibernate needs the next high value, this is what it does:

   1: -- select current value
   2: select next_hi
   3: from hibernate_unique_key with (updlock, rowlock)
   4: where entity_type = 'some_entity'
   5:  
   6: -- update current value
   7: update hibernate_unique_key
   8: set next_hi = @p0
   9: where next_hi = @p1
  10: and entity_type = 'some_entity';

This approach only has advantages:

  • The HiLo table is still managed by NHibernate;
  • You have different id generators per entity (of course, you can still combine multiple entities under the same where clause), which will make them grow more slowly;
  • No contention occurs, because each entity is using its own record on the HIBERNATE_UNIQUE_KEY table.

One Column Per Entity

Yet another option is to have each entity using its own column for storing the high value. For that, we need to use the column parameter:

   1: this.Id(x => x.SomeId, x =>
   2: {
   3:     x.Column("some_id");
   4:     x.Generator(Generators.HighLow, g => g.Params(new { column = "some_column_id" }));
   5: });

Like in the previous option, NHibernate does not know and therefore does not create this new column automatically. For that, we resort to another auxiliary database object:

   1: private static IAuxiliaryDatabaseObject OneHiLoColumnPerEntityScript(Configuration cfg, String columnName)
   2: {
   3:     var dialect = Activator.CreateInstance(Type.GetType(cfg.GetProperty(NHibernate.Cfg.Environment.Dialect))) as Dialect;
   4:     var script = new StringBuilder();
   5:  
   6:     script.AppendFormat("ALTER TABLE {0} {1} {2} {3} NULL;\n{4}\nUPDATE {0} SET {2} = 1;\n{4}\n", TableHiLoGenerator.DefaultTableName, dialect.AddColumnString, columnName, dialect.GetTypeName(SqlTypeFactory.Int32), (dialect.SupportsSqlBatches == true ? "GO" : String.Empty));
   7:  
   8:     return (new SimpleAuxiliaryDatabaseObject(script.ToString(), null));
   9: }
  10:  
  11: Configuration cfg = ...;
  12: cfg.AddAuxiliaryDatabaseObject(OneHiLoColumnPerEntityScript(cfg, "some_column_id"));

The schema, with an additional column, would look like this:

image

And NHibernate executes this SQL for getting/updating the next high value:

   1: -- select current value
   2: select some_column_hi
   3: from hibernate_unique_key with (updlock, rowlock)
   4:  
   5: -- update current value
   6: update hibernate_unique_key
   7: set some_column_hi = @p0
   8: where some_column_hi = @p1;

The only advantage in this model is to have separate ids per entity, contention on the HiLo table will still occur.

One Table Per Entity

The final option to consider is having a separate table per entity (or group of entities). For that, we use the table parameter:

   1: this.Id(x => x.SomeId, x =>
   2: {
   3:     x.Column("some_id");
   4:     x.Generator(Generators.HighLow, g => g.Params(new { table = "some_entity_unique_key" }));
   5: });

In this case, NHibernate generates the new HiLo table for us, together with the default HIBERNATE_UNIQUE_KEY, if any entity uses it, with exactly the same schema:

image

And the SQL is, of course, also identical, except for the table name:

   1: -- select current value
   2: select next_hi
   3: from some_entity_unique_key with (updlock, rowlock)
   4:  
   5: -- update current value
   6: update some_entity_unique_key
   7: set next_hi = @p0
   8: where next_hi = @p1;

Again, all pros and no cons:

  • Table still fully managed by NHibernate;
  • Different ids per entity or group of entities means they will grow slower;
  • Contention will only occur if more than one entity uses the same HiLo table.

Conclusion

As you can see, NHibernate is full of extensibility points. Even when it does not offer out of the box what we need, we usually have a way around it.

Let me hear from you!

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

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

NHibernate Pitfalls: Aggregating Non-Nullable Numerical Values Without Records

This is part of a series of posts about NHibernate Pitfalls. See the entire collection here.

When you are performing a LINQ query that aggregates non-nullable numeric values – Sum, Average, for example – and the query does not return any values, you will get an exception. An example would be:

   1: var average = session.Query<Product>().Where(x => x.Price > 10000).Select(x => x.Price).Average();

This is not specific to NHibernate, this behavior will be the same on LINQ to Objects as well, and it is caused by a null result trying to be assigned to a non-nullable variable.

The workaround is simple, just cast the result property to an appropriate nullable equivalent:

   1: var average = session.Query<Product>().Where(x => x.Price > 10000).Select(x => (Decimal?) x.Price).Average();
JavaScript Events

Introduction

A lot has changed with regard to events and event handling since the old days of HTML. Let’s have a brief look at it.

Old Days

In the old days, it was common to add event listeners in one of two ways:

By code:

   1: var elm = document.getElementById('id');
   2: elm.onclick = function()
   3: {
   4:     //do something
   5: };

Or in the HTML declaration of an element:

   1: <a id="id" href="#" onclick="/*do something*/">Click me</a>

The problem with the first approach was that different browsers would pass the event argument (Event class) in different ways:

  • Internet Explorer had a global window.event instance;
  • Other browsers would pass the event instance as an argument to the handler function.

One way to go around this would be something like:

   1: elm.onclick = function(evt)
   2: {
   3:     if (!evt)
   4:     {
   5:         //IE
   6:         evt = window.event;
   7:     }
   8:     //do something with evt
   9: };

But for the second one, only IE supported accessing the event instance, through the global window.event property. For other browsers, this was lost.

Either way, you could only add a single event listener for each event, which was a big limitation. IE did offer a proprietary solution to this, through the attachEvent method:

   1: var elm = document.getElementById('id');
   2: elm.attachEvent('click', function()
   3: {
   4:     //do something
   5: });

Likewise, the way to get the event was through window.event.

As for canceling the default behavior of the event (such as preventing browser navigation or form submittal), returning false usually worked, but it would mean that you had to declare the event handler appropriately, while using the inline HTML syntax:

   1: <a id="id" href="#" onclick="/*do something*/return false">Click me</a>

In IE, setting window.event.returnValue to false would do the same.

Most browsers would use the dispatchEvent method for raising an event:

   1: var elm = document.getElementById('id');
   2: var evt = document.createEvent('MouseEvent');
   3: evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, 0, null);
   4: evt.dispatchEvent(evt);

But IE would instead use the fireEvent and the createEvent methods:

   1: var elm = document.getElementById('id');
   2: var evt = document.createEvent('MouseEvent');
   3: evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, 0, null);
   4: evt.fireEvent(evt);

Present

Currently, the ECMAScript specification says that the way to add event listeners to a DOM element should be done through the addEventListener method, as in:

   1: var elm = document.getElementById('id');
   2: elm.addEventListener('click', function(evt)
   3: {
   4:     //do something with evt
   5: });

The event handler function always receives an event argument, which will be an instance of the Event class. The old JavaScript approach works as well:

   1: var elm = document.getElementById('id');
   2: elm.onclick = function(evt)
   3: {
   4:     //do something with evt
   5: };

The difference between these two approaches is that the first allows having multiple event handlers for the same event, whereas the latter does not. As you can imagine, it is safer to go the addEventListener way, which, BTW, is what jQuery and other libraries use.

In HTML inline event handler declarations, it is now possible to access the event attribute through the arguments collection:

   1: <a id="id" href="#" onclick="var evt = arguments[0]; evt.preventDefault()">Click me</a>

As for canceling an event, we now have the preventDefault method that does just the same.

Raising an event is now achieved through the dispatchEvent method:

   1: var elm = document.getElementById('id');
   2: var evt = new MouseEvent('click', { bubbles: true, cancelable: true, view: window, screenX: 0, screenY: 0, clientX: 0, clientY: 0, ctrlKey: false, shiftKey: false, altKey: false, metaKey: false, button: 1, relatedTarget: elm });
   3: elm.dispatchEvent(evt);

Do notice the initializer of the MouseEvent class, this is the standard way to use it now. See the full list here.

Conclusion

Things around HTML are getting considerably better, with more commitment from browser manufacturers to standards. Whenever possible, do use them, or, better still, use jQuery and forget about it! Winking smile

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!

NHibernate Connection Resiliency

Entity Framework 6 included a feature known as connection resiliency. Basically, what it says is, when EF is trying to connect to a database, it will try a number of times before giving up. After each unsuccessful attempt, it will wait some time and then try again. As you can imagine, this is very useful, especially when we are dealing with cloud storage.

NHibernate does not natively offer this, however, because it is highly extensible, it isn’t too hard to build one such mechanism, which is what I did.

The code is below, as you can see, it consists of a custom implementation of DriverConnectionProvider, the component of NHibernate that opens connections for us.

   1: public class ResilientDriverConnectionProvider : DriverConnectionProvider
   2: {
   3:     public const String ConnectionDelayBetweenTries = "connection.delay_between_tries";
   4:     public const String ConnectionMaxTries = "connection.max_tries";
   5:  
   6:     private static readonly IInternalLogger log = LoggerProvider.LoggerFor(typeof(ResilientDriverConnectionProvider));
   7:  
   8:     public ResilientDriverConnectionProvider()
   9:     {
  10:         this.MaxTries = 3;
  11:         this.DelayBetweenTries = TimeSpan.FromSeconds(5);
  12:     }
  13:  
  14:     public Int32 MaxTries { get; set; }
  15:  
  16:     public TimeSpan DelayBetweenTries { get; set; }
  17:  
  18:     public override void Configure(IDictionary<String, String> settings)
  19:     {
  20:         String maxTries;
  21:         String delayBetweenTries;
  22:  
  23:         if (settings.TryGetValue(ConnectionMaxTries, out maxTries) == true)
  24:         {
  25:             this.MaxTries = Int32.Parse(maxTries);
  26:         }
  27:  
  28:         if (settings.TryGetValue(ConnectionDelayBetweenTries, out delayBetweenTries) == true)
  29:         {
  30:             this.DelayBetweenTries = TimeSpan.Parse(delayBetweenTries);
  31:         }
  32:  
  33:         base.Configure(settings);
  34:     }
  35:  
  36:     public override IDbConnection GetConnection()
  37:     {
  38:         IDbConnection con = null;
  39:  
  40:         for (var i = 0; i < this.MaxTries; ++i)
  41:         {
  42:             try
  43:             {
  44:                 log.Debug(String.Format("Attempting to get connection, {0} of {1}", (i + 1), this.MaxTries));
  45:                 con = base.GetConnection();
  46:                 log.Debug(String.Format("Got a connection after {0} tries", (i + 1)));
  47:  
  48:                 break;
  49:             }
  50:             catch(Exception ex)
  51:             {
  52:                 if (i == this.MaxTries - 1)
  53:                 {
  54:                     log.Error(String.Format("Could not get connection after {0} tries", this.MaxTries), ex);
  55:                     throw;
  56:                 }
  57:                 else
  58:                 {
  59:                     Thread.Sleep(this.DelayBetweenTries);
  60:                 }
  61:             }
  62:         }
  63:  
  64:         return (con);
  65:     }
  66: }

The code wraps the attempt to open a connection and retries it a number of times, with some delay in between.

The way to configure this, in fluent configuration, would be:

   1: var cfg = new Configuration()
   2:     .DataBaseIntegration(
   3:     db =>
   4:     {
   5:         db.ConnectionProvider<ResilientDriverConnectionProvider>();
   6:         //...
   7:     });

Or if you prefer to use string properties, in either XML or fluent configuration, you can do:

   1: var cfg = new Configuration()
   2:     .SetProperty(NHibernate.Cfg.Environment.ConnectionProvider, typeof(ResilientDriverConnectionProvider).AssemblyQualifiedName);

From looking at the class, you can see that it supports two properties:

  • MaxTries: the maximum number of connect attempts;
  • DelayBetweenTries: the amount of time to wait between two connection attempts.

It is possible to supply this values by configuration:

   1: var cfg = new Configuration()
   2:     .SetProperty(NHibernate.Cfg.Environment.ConnectionProvider, typeof(ResilientDriverConnectionProvider).AssemblyQualifiedName)
   3:     .SetProperties(ResilientDriverConnectionProvider.ConnectionMaxTries, "3")
   4:     .SetProperties(ResilientDriverConnectionProvider.ConnectionDelayBetweenTries, TimeSpan.FromSeconds(5).ToString());

As usual, hope you find this useful! Smile

NHibernate Pitfalls: Outer Joins of Unrelated Entities

This is part of a series of posts about NHibernate Pitfalls. See the entire collection here.

NHibernate does not support outer (left, right, full) joins in most of its querying APIs, namely, Criteria, Query Over, LINQ and HQL. Of course, LINQ is translated into HQL, so as long as HQL does not support it, nothing really can be done about it. Do note that this problem also happens in Hibernate for Java, it has to do with the way NHibernate/Hibernate is implemented, both can only join entities that are related through some navigation property.

It is, though, possible to perform left joins with SQL. One example is:

   1: var leftJoinAAndB = session.CreateSQLQuery("SELECT {a.*}, {b.*} FROM EntityA a LEFT JOIN EntityB b ON a.SomeProperty = b.SomeProperty")
   2:     .AddEntity("a", typeof(EntityA))
   3:     .AddEntity("b", typeof(EntityB))
   4:     .List<Object[]>();

As you can see, we are using a specially formatted SQL and we tell NHibernate what entity types its columns refer to. The result is an array of two entities, the second being possibly null.

Custom NHibernate Criteria Projections

I recently had the need to investigate a way to do some complicated projections with Criteria queries and I was faced with what seems a common problem: being able to fully select the properties of the root entity of the criteria. An issue has been raised on NHibernate JIRA and there are some questions on StackOverflow about it.

Criteria projections are powerful, but not much documented. I decided to try to understand the problem and find a solution for it. Eventually, I ended um implementing not only a RootEntityProjection but also a generic EntityProjection.

RootEntityProjection selects all the properties of the criteria’s root entity and EntityProjection selects all the properties of any joined entity, selected by its alias.

An example for the RootEntityProjection could be:

   1: var leftJoinAAndB = session.CreateCriteria<EntityA>("a")
   2:     .CreateAlias("PropertyB", "b", JoinType.LeftOuterJoin)
   3:     .Add(Restrictions.IsNotNull("PropertyB"))
   4:     .SetProjection(new RootEntityProjection())
   5:     .SetResultTransformer(Transformers.AliasToBean<EntityA>())
   6:     .List<EntityA>();

And an example for EntityProjection:

   1: var leftJoinAAndB = session.CreateCriteria<EntityA>("a")
   2:     .CreateAlias("PropertyB", "b", JoinType.LeftOuterJoin)
   3:     .Add(Restrictions.IsNotNull("PropertyB"))
   4:     .SetProjection(new EntityProjection<EntityB>("b"))
   5:     .SetResultTransformer(Transformers.AliasToBean<EntityB>())
   6:     .List<EntityB>();

As always, here is the code, first, for RootEntityProjection:

   1: public class RootEntityProjection : IProjection
   2: {
   3:     private readonly List<String> aliases = new List<String>();
   4:     private IType[] columnTypes = null;
   5:  
   6:     protected String[] GetPropertyNames(IClassMetadata classMetadata, ICriteriaQuery criteriaQuery)
   7:     {
   8:         var propertyNames = classMetadata.PropertyNames.Concat(new String[] { classMetadata.IdentifierPropertyName }).Zip(classMetadata.PropertyTypes.Concat(new IType[] { classMetadata.IdentifierType }), (x, y) => new Tuple<String, IType>(x, y)).ToDictionary(x => x.Item1, x => x.Item2).Where(x => !(x.Value is ComponentType) && !(x.Value is CollectionType)).Select(x => x.Key).ToArray();
   9:  
  10:         return (propertyNames);
  11:     }
  12:  
  13:     #region IProjection Members
  14:  
  15:     String[] IProjection.Aliases
  16:     {
  17:         get
  18:         {
  19:             return (this.aliases.ToArray());
  20:         }
  21:     }
  22:  
  23:     String[] IProjection.GetColumnAliases(String alias, Int32 loc)
  24:     {
  25:         throw new NotImplementedException();
  26:     }
  27:  
  28:     String[] IProjection.GetColumnAliases(Int32 loc)
  29:     {
  30:         return (this.aliases.ToArray());
  31:     }
  32:  
  33:     TypedValue[] IProjection.GetTypedValues(ICriteria criteria, ICriteriaQuery criteriaQuery)
  34:     {
  35:         throw new NotImplementedException();
  36:     }
  37:  
  38:     IType[] IProjection.GetTypes(String alias, ICriteria criteria, ICriteriaQuery criteriaQuery)
  39:     {
  40:         throw new NotImplementedException();
  41:     }
  42:  
  43:     IType[] IProjection.GetTypes(ICriteria criteria, ICriteriaQuery criteriaQuery)
  44:     {
  45:         if (this.columnTypes == null)
  46:         {
  47:             var classMetadata = criteriaQuery.Factory.GetClassMetadata(criteria.GetRootEntityTypeIfAvailable());
  48:             var propertyNames = this.GetPropertyNames(classMetadata, criteriaQuery);
  49:  
  50:             this.columnTypes = propertyNames.Select(x => classMetadata.GetPropertyType(x)).ToArray();
  51:         }
  52:  
  53:         return (this.columnTypes);
  54:     }
  55:  
  56:     Boolean IProjection.IsAggregate
  57:     {
  58:         get { return(false); }
  59:     }
  60:  
  61:     Boolean IProjection.IsGrouped
  62:     {
  63:         get { return (false); }
  64:     }
  65:  
  66:     SqlString IProjection.ToGroupSqlString(ICriteria criteria, ICriteriaQuery criteriaQuery, IDictionary<String, IFilter> enabledFilters)
  67:     {
  68:         throw new NotImplementedException();
  69:     }
  70:  
  71:     SqlString IProjection.ToSqlString(ICriteria criteria, Int32 position, ICriteriaQuery criteriaQuery, IDictionary<String, IFilter> enabledFilters)
  72:     {
  73:         var classMetadata = criteriaQuery.Factory.GetClassMetadata(criteria.GetRootEntityTypeIfAvailable());
  74:         var propertyNames = this.GetPropertyNames(classMetadata, criteriaQuery);
  75:         var builder = new SqlStringBuilder();
  76:  
  77:         for (var i = 0; i < propertyNames.Length; ++i)
  78:         {
  79:             var propertyName = propertyNames[i];
  80:             var columnName = criteriaQuery.GetColumn(criteria, propertyName);
  81:  
  82:             builder.Add(columnName);
  83:             builder.Add(" as ");
  84:             builder.Add(propertyName);
  85:  
  86:             this.aliases.Add(propertyName);
  87:  
  88:             if (i < propertyNames.Length - 1)
  89:             {
  90:                 builder.Add(", ");
  91:             }
  92:         }
  93:  
  94:         return (builder.ToSqlString());
  95:     }
  96:  
  97:     #endregion            
  98: }

And EntityProjection (actually, two classes, I created a subclass that is a generic wrapper):

   1: public class EntityProjection : IProjection
   2: {
   3:     private IType[] columnTypes = null;
   4:     private readonly Type rootEntity = null;
   5:     private readonly String alias = null;
   6:  
   7:     protected String[] GetPropertyNames(IClassMetadata classMetadata, ICriteriaQuery criteriaQuery)
   8:     {
   9:         var propertyNames = classMetadata.PropertyNames.Except(criteriaQuery.Factory.GetAllCollectionMetadata().Where(x => x.Key.StartsWith(String.Concat(classMetadata.EntityName, "."))).Select(x => x.Key.Split('.').Last())).Concat(new String[] { classMetadata.IdentifierPropertyName }).ToArray();
  10:  
  11:         return (propertyNames);
  12:     }
  13:  
  14:     public EntityProjection(Type rootEntity, String alias)
  15:     {
  16:         this.rootEntity = rootEntity;
  17:         this.alias = alias;
  18:     }
  19:  
  20:     private readonly List<String> aliases = new List<String>();
  21:  
  22:     #region IProjection Members
  23:  
  24:     String[] IProjection.Aliases
  25:     {
  26:         get
  27:         {
  28:             return (this.aliases.ToArray());
  29:         }
  30:     }
  31:  
  32:     String[] IProjection.GetColumnAliases(String alias, Int32 loc)
  33:     {
  34:         throw new NotImplementedException();
  35:     }
  36:  
  37:     String[] IProjection.GetColumnAliases(Int32 loc)
  38:     {
  39:         return (this.aliases.ToArray());
  40:     }
  41:  
  42:     TypedValue[] IProjection.GetTypedValues(ICriteria criteria, ICriteriaQuery criteriaQuery)
  43:     {
  44:         throw new NotImplementedException();
  45:     }
  46:  
  47:     IType[] IProjection.GetTypes(String alias, ICriteria criteria, ICriteriaQuery criteriaQuery)
  48:     {
  49:         throw new NotImplementedException();
  50:     }
  51:  
  52:     IType[] IProjection.GetTypes(ICriteria criteria, ICriteriaQuery criteriaQuery)
  53:     {
  54:         if (this.columnTypes == null)
  55:         {
  56:             var classMetadata = criteriaQuery.Factory.GetClassMetadata(this.rootEntity);
  57:             var propertyNames = this.GetPropertyNames(classMetadata, criteriaQuery);
  58:  
  59:             this.columnTypes = propertyNames.Select(x => classMetadata.GetPropertyType(x)).ToArray();
  60:         }
  61:  
  62:         return (this.columnTypes);
  63:     }
  64:  
  65:     Boolean IProjection.IsAggregate
  66:     {
  67:         get { return (false); }
  68:     }
  69:  
  70:     Boolean IProjection.IsGrouped
  71:     {
  72:         get { return (false); }
  73:     }
  74:  
  75:     SqlString IProjection.ToGroupSqlString(ICriteria criteria, ICriteriaQuery criteriaQuery, IDictionary<String, IFilter> enabledFilters)
  76:     {
  77:         throw new NotImplementedException();
  78:     }
  79:  
  80:     SqlString IProjection.ToSqlString(ICriteria criteria, Int32 position, ICriteriaQuery criteriaQuery, IDictionary<String, IFilter> enabledFilters)
  81:     {
  82:         var classMetadata = criteriaQuery.Factory.GetClassMetadata(this.rootEntity);
  83:         var propertyNames = this.GetPropertyNames(classMetadata, criteriaQuery);
  84:         var builder = new SqlStringBuilder();
  85:  
  86:         for (var i = 0; i < propertyNames.Length; ++i)
  87:         {
  88:             var propertyName = propertyNames[i];
  89:             var subcriteria = criteria.GetCriteriaByAlias(this.alias);                    
  90:             var columnName = criteriaQuery.GetColumn(subcriteria, propertyName);
  91:  
  92:             builder.Add(columnName);
  93:             builder.Add(" as ");
  94:             builder.Add(propertyName);
  95:  
  96:             this.aliases.Add(propertyName);
  97:  
  98:             if (i < propertyNames.Length - 1)
  99:             {
 100:                 builder.Add(", ");
 101:             }
 102:         }
 103:  
 104:         return (builder.ToSqlString());
 105:     }
 106:  
 107:     #endregion
 108: }
 109:  
 110: public class EntityProjection<T> : EntityProjection
 111: {
 112:     public EntityProjection(String alias) : base(typeof(T), alias)
 113:     {
 114:     }
 115: }

Some explanation is in order:

  1. Through the session factory that is exposed by the ICriteriaQuery interface we get the entity’s class metadata;
  2. From the class metadata we list all of the entity’s mapped properties and id, except those properties that refer to collections;
  3. We cache each property’s NHibernate type for faster access;
  4. A SQL query is built with all the mapped properties.

The two main classes share 90% of their behavior, so it is even possible to have a base class. I leave that for you, dear reader! Smile Hope you find this useful!

More Posts Next page »