Contents tagged with Web

  • Speech Recognition in ASP.NET

    Speech synthesis and recognition were both introduced in .NET 3.0. They both live in System.Speech.dll. In the past, I already talked about speech synthesis in the context of ASP.NET Web Form applications, this time, I’m going to talk about speech recognition.

    .NET has in fact two APIs for that:

    I am going to demonstrate a technique that makes use of HTML5 features, namely, Data URIs and the getUserMedia API and also ASP.NET Client Callbacks, which, if you have been following my blog, should know that I am a big fan of.

    First, because we have two APIs that we can use, let’s start by creating an abstract base provider class:

       1: public abstract class SpeechRecognitionProvider : IDisposable
       2: {
       3:     protected SpeechRecognitionProvider(SpeechRecognition recognition)
       4:     {
       5:         this.Recognition = recognition;
       6:     }
       7:  
       8:     ~SpeechRecognitionProvider()
       9:     {
      10:         this.Dispose(false);
      11:     }
      12:  
      13:     public abstract SpeechRecognitionResult RecognizeFromWav(String filename);
      14:  
      15:     protected SpeechRecognition Recognition
      16:     {
      17:         get;
      18:         private set;
      19:     }
      20:  
      21:     protected virtual void Dispose(Boolean disposing)
      22:     {
      23:     }
      24:  
      25:     void IDisposable.Dispose()
      26:     {
      27:         GC.SuppressFinalize(this);
      28:         this.Dispose(true);
      29:     }
      30: }

    It basically features one method, RecognizeFromWav, which takes a physical path and returns a SpeechRecognitionResult (code coming next). For completeness, it also implements the Dispose Pattern, because some provider may require it.

    In a moment we will be creating implementations for the built-in .NET provider as well as Microsoft Speech Platform.

    The SpeechRecognition property refers to our Web Forms control, inheriting from HtmlGenericControl, which is the one that knows how to instantiate one provider or the other:

       1: [ConstructorNeedsTag(false)]
       2: public class SpeechRecognition : HtmlGenericControl, ICallbackEventHandler
       3: {
       4:     public SpeechRecognition() : base("span")
       5:     {
       6:         this.OnClientSpeechRecognized = String.Empty;
       7:         this.Mode = SpeechRecognitionMode.Desktop;
       8:         this.Culture = String.Empty;
       9:         this.SampleRate = 44100;
      10:         this.Grammars = new String[0];
      11:         this.Choices = new String[0];
      12:     }
      13:  
      14:     public event EventHandler<SpeechRecognizedEventArgs> SpeechRecognized;
      15:  
      16:     [DefaultValue("")]
      17:     public String Culture
      18:     {
      19:         get;
      20:         set;
      21:     }
      22:  
      23:     [DefaultValue(SpeechRecognitionMode.Desktop)]
      24:     public SpeechRecognitionMode Mode
      25:     {
      26:         get;
      27:         set;
      28:     }
      29:  
      30:     [DefaultValue("")]
      31:     public String OnClientSpeechRecognized
      32:     {
      33:         get;
      34:         set;
      35:     }
      36:  
      37:     [DefaultValue(44100)]
      38:     public UInt32 SampleRate
      39:     {
      40:         get;
      41:         set;
      42:     }
      43:  
      44:     [TypeConverter(typeof(StringArrayConverter))]
      45:     [DefaultValue("")]
      46:     public String [] Grammars
      47:     {
      48:         get;
      49:         private set;
      50:     }
      51:  
      52:     [TypeConverter(typeof(StringArrayConverter))]
      53:     [DefaultValue("")]
      54:     public String[] Choices
      55:     {
      56:         get;
      57:         set;
      58:     }
      59:  
      60:     protected override void OnInit(EventArgs e)
      61:     {
      62:         if (this.Page.Items.Contains(typeof(SpeechRecognition)))
      63:         {
      64:             throw (new InvalidOperationException("There can be only one SpeechRecognition control on a page."));
      65:         }
      66:  
      67:         var sm = ScriptManager.GetCurrent(this.Page);
      68:         var reference = this.Page.ClientScript.GetCallbackEventReference(this, "sound", String.Format("function(result){{ {0}(JSON.parse(result)); }}", String.IsNullOrWhiteSpace(this.OnClientSpeechRecognized) ? "void" : this.OnClientSpeechRecognized), String.Empty, true);
      69:         var script = String.Format("\nvar processor = document.getElementById('{0}'); processor.stopRecording = function(sampleRate) {{ window.stopRecording(processor, sampleRate ? sampleRate : 44100); }}; processor.startRecording = function() {{ window.startRecording(); }}; processor.process = function(sound){{ {1} }};\n", this.ClientID, reference);
      70:  
      71:         if (sm != null)
      72:         {
      73:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat("process", this.ClientID), String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ {0} }});\n", script), true);
      74:         }
      75:         else
      76:         {
      77:             this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat("process", this.ClientID), script, true);
      78:         }
      79:  
      80:         this.Page.ClientScript.RegisterClientScriptResource(this.GetType(), String.Concat(this.GetType().Namespace, ".Script.js"));
      81:         this.Page.Items[typeof(SpeechRecognition)] = this;
      82:  
      83:         base.OnInit(e);
      84:     }
      85:  
      86:     protected virtual void OnSpeechRecognized(SpeechRecognizedEventArgs e)
      87:     {
      88:         var handler = this.SpeechRecognized;
      89:  
      90:         if (handler != null)
      91:         {
      92:             handler(this, e);
      93:         }
      94:     }
      95:  
      96:     protected SpeechRecognitionProvider GetProvider()
      97:     {
      98:         switch (this.Mode)
      99:         {
     100:             case SpeechRecognitionMode.Desktop:
     101:                 return (new DesktopSpeechRecognitionProvider(this));
     102:  
     103:             case SpeechRecognitionMode.Server:
     104:                 return (new ServerSpeechRecognitionProvider(this));
     105:         }
     106:  
     107:         return (null);
     108:     }
     109:  
     110:     #region ICallbackEventHandler Members
     111:  
     112:     String ICallbackEventHandler.GetCallbackResult()
     113:     {
     114:         AsyncOperationManager.SynchronizationContext = new SynchronizationContext();
     115:  
     116:         var filename = Path.GetTempFileName();
     117:         var result = null as SpeechRecognitionResult;
     118:  
     119:         using (var engine = this.GetProvider())
     120:         {
     121:             var data = this.Context.Items["data"].ToString();
     122:  
     123:             using (var file = File.OpenWrite(filename))
     124:             {
     125:                 var bytes = Convert.FromBase64String(data);
     126:                 file.Write(bytes, 0, bytes.Length);
     127:             }
     128:  
     129:             result = engine.RecognizeFromWav(filename) ?? new SpeechRecognitionResult(String.Empty);
     130:         }
     131:  
     132:         File.Delete(filename);
     133:  
     134:         var args = new SpeechRecognizedEventArgs(result);
     135:  
     136:         this.OnSpeechRecognized(args);
     137:  
     138:         var json = new JavaScriptSerializer().Serialize(result);
     139:  
     140:         return (json);
     141:     }
     142:  
     143:     void ICallbackEventHandler.RaiseCallbackEvent(String eventArgument)
     144:     {
     145:         this.Context.Items["data"] = eventArgument;
     146:     }
     147:  
     148:     #endregion
     149: }

    SpeechRecognition implements ICallbackEventHandler for a self-contained AJAX experience; it registers a couple of JavaScript functions and also an embedded JavaScript file, for some useful sound manipulation and conversion. Only one instance is allowed on a page. On the client-side, this JavaScript uses getUserMedia to access an audio source and uses a clever mechanism to pack them as a .WAV file in a Data URI. I got these functions from http://typedarray.org/from-microphone-to-wav-with-getusermedia-and-web-audio/ and made some changes to them. I like them because they don’t require any external library, which makes all this pretty much self-contained.

    The control exposes some custom properties:

    • Culture: an optional culture name, such as “pt-PT” or “en-US”; if not specified, it defaults to the current culture in the server machine;
    • Mode: one of the two providers: Desktop (for System.Speech) or Server (for Microsoft.Speech, of Microsoft Speech Platform);
    • OnClientSpeechRecognized: the name of a callback JavaScript version that will be called when there are results (more on this later);
    • SampleRate: a sample rate, by default, it is 44100;
    • Grammars: an optional collection of additional grammar files, with extension .grxml (Speech Recognition Grammar Specification), to add to the engine;
    • Choices: an optional collection of choices to recognize, if we want to restrict the scope, such as “yes”/”no”, “red”/”green”, etc.

    The mode enumeration looks like this:

       1: public enum SpeechRecognitionMode
       2: {
       3:     Desktop,
       4:     Server
       5: }

    The SpeechRecognition control also has an event, SpeechRecognized, which allows overriding the detected phrases. Its argument is this simple class that follows the regular .NET event pattern:

       1: [Serializable]
       2: public sealed class SpeechRecognizedEventArgs : EventArgs
       3: {
       4:     public SpeechRecognizedEventArgs(SpeechRecognitionResult result)
       5:     {
       6:         this.Result = result;
       7:     }
       8:  
       9:     public SpeechRecognitionResult Result
      10:     {
      11:         get;
      12:         private set;
      13:     }
      14: }

    Which in turn holds a SpeechRecognitionResult:

       1: public class SpeechRecognitionResult
       2: {
       3:     public SpeechRecognitionResult(String text, params String [] alternates)
       4:     {
       5:         this.Text = text;
       6:         this.Alternates = alternates.ToList();
       7:     }
       8:  
       9:     public String Text
      10:     {
      11:         get;
      12:         set;
      13:     }
      14:  
      15:     public List<String> Alternates
      16:     {
      17:         get;
      18:         private set;
      19:     }
      20: }

    This class receives the phrase that the speech recognition engine understood plus an array of additional alternatives, in descending order.

    The JavaScript file containing the utility functions is embedded in the assembly:

    image

    You need to add an assembly-level attribute, WebResourceAttribute, possibly in AssemblyInfo.cs, of course, replacing MyNamespace for your assembly’s default namespace:

       1: [assembly: WebResource("MyNamespace.Script.js", "text/javascript")]

    This attribute registers a script file with some content-type so that it can be included in a page by the RegisterClientScriptResource method.

    And here is it:

       1: // variables
       2: var leftchannel = [];
       3: var rightchannel = [];
       4: var recorder = null;
       5: var recording = false;
       6: var recordingLength = 0;
       7: var volume = null;
       8: var audioInput = null;
       9: var audioContext = null;
      10: var context = null;
      11:  
      12: // feature detection 
      13: navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
      14:  
      15: if (navigator.getUserMedia)
      16: {
      17:     navigator.getUserMedia({ audio: true }, onSuccess, onFailure);
      18: }
      19: else
      20: {
      21:     alert('getUserMedia not supported in this browser.');
      22: }
      23:  
      24: function startRecording()
      25: {
      26:     recording = true;
      27:     // reset the buffers for the new recording
      28:     leftchannel.length = rightchannel.length = 0;
      29:     recordingLength = 0;
      30:     leftchannel = [];
      31:     rightchannel = [];
      32: }
      33:  
      34: function stopRecording(elm, sampleRate)
      35: {
      36:     recording = false;
      37:  
      38:     // we flat the left and right channels down
      39:     var leftBuffer = mergeBuffers(leftchannel, recordingLength);
      40:     var rightBuffer = mergeBuffers(rightchannel, recordingLength);
      41:     // we interleave both channels together
      42:     var interleaved = interleave(leftBuffer, rightBuffer);
      43:  
      44:     // we create our wav file
      45:     var buffer = new ArrayBuffer(44 + interleaved.length * 2);
      46:     var view = new DataView(buffer);
      47:  
      48:     // RIFF chunk descriptor
      49:     writeUTFBytes(view, 0, 'RIFF');
      50:     view.setUint32(4, 44 + interleaved.length * 2, true);
      51:     writeUTFBytes(view, 8, 'WAVE');
      52:     // FMT sub-chunk
      53:     writeUTFBytes(view, 12, 'fmt ');
      54:     view.setUint32(16, 16, true);
      55:     view.setUint16(20, 1, true);
      56:     // stereo (2 channels)
      57:     view.setUint16(22, 2, true);
      58:     view.setUint32(24, sampleRate, true);
      59:     view.setUint32(28, sampleRate * 4, true);
      60:     view.setUint16(32, 4, true);
      61:     view.setUint16(34, 16, true);
      62:     // data sub-chunk
      63:     writeUTFBytes(view, 36, 'data');
      64:     view.setUint32(40, interleaved.length * 2, true);
      65:  
      66:     // write the PCM samples
      67:     var index = 44;
      68:     var volume = 1;
      69:  
      70:     for (var i = 0; i < interleaved.length; i++)
      71:     {
      72:         view.setInt16(index, interleaved[i] * (0x7FFF * volume), true);
      73:         index += 2;
      74:     }
      75:  
      76:     // our final binary blob
      77:     var blob = new Blob([view], { type: 'audio/wav' });
      78:  
      79:     var reader = new FileReader();
      80:     reader.onloadend = function ()
      81:     {
      82:         var url = reader.result.replace('data:audio/wav;base64,', '');
      83:         elm.process(url);
      84:     };
      85:     reader.readAsDataURL(blob);
      86: }
      87:  
      88: function interleave(leftChannel, rightChannel)
      89: {
      90:     var length = leftChannel.length + rightChannel.length;
      91:     var result = new Float32Array(length);
      92:     var inputIndex = 0;
      93:  
      94:     for (var index = 0; index < length;)
      95:     {
      96:         result[index++] = leftChannel[inputIndex];
      97:         result[index++] = rightChannel[inputIndex];
      98:         inputIndex++;
      99:     }
     100:  
     101:     return result;
     102: }
     103:  
     104: function mergeBuffers(channelBuffer, recordingLength)
     105: {
     106:     var result = new Float32Array(recordingLength);
     107:     var offset = 0;
     108:  
     109:     for (var i = 0; i < channelBuffer.length; i++)
     110:     {
     111:         var buffer = channelBuffer[i];
     112:         result.set(buffer, offset);
     113:         offset += buffer.length;
     114:     }
     115:  
     116:     return result;
     117: }
     118:  
     119: function writeUTFBytes(view, offset, string)
     120: {
     121:     for (var i = 0; i < string.length; i++)
     122:     {
     123:         view.setUint8(offset + i, string.charCodeAt(i));
     124:     }
     125: }
     126:  
     127: function onFailure(e)
     128: {
     129:     alert('Error capturing audio.');
     130: }
     131:  
     132: function onSuccess(e)
     133: {
     134:     // creates the audio context
     135:     audioContext = (window.AudioContext || window.webkitAudioContext);
     136:     context = new audioContext();
     137:  
     138:     // creates a gain node
     139:     volume = context.createGain();
     140:  
     141:     // creates an audio node from the microphone incoming stream
     142:     audioInput = context.createMediaStreamSource(e);
     143:  
     144:     // connect the stream to the gain node
     145:     audioInput.connect(volume);
     146:  
     147:     /* From the spec: This value controls how frequently the audioprocess event is 
     148:     dispatched and how many sample-frames need to be processed each call. 
     149:     Lower values for buffer size will result in a lower (better) latency. 
     150:     Higher values will be necessary to avoid audio breakup and glitches */
     151:     var bufferSize = 2048;
     152:  
     153:     recorder = context.createScriptProcessor(bufferSize, 2, 2);
     154:     recorder.onaudioprocess = function (e)
     155:     {
     156:         if (recording == false)
     157:         {
     158:             return;
     159:         }
     160:  
     161:         var left = e.inputBuffer.getChannelData(0);
     162:         var right = e.inputBuffer.getChannelData(1);
     163:  
     164:         // we clone the samples
     165:         leftchannel.push(new Float32Array(left));
     166:         rightchannel.push(new Float32Array(right));
     167:  
     168:         recordingLength += bufferSize;
     169:     }
     170:  
     171:     // we connect the recorder
     172:     volume.connect(recorder);
     173:     recorder.connect(context.destination);
     174: }

    OK, let’s move on the the provider implementations; first, Desktop:

       1: public class DesktopSpeechRecognitionProvider : SpeechRecognitionProvider
       2: {
       3:     public DesktopSpeechRecognitionProvider(SpeechRecognition recognition) : base(recognition)
       4:     {
       5:     }
       6:  
       7:     public override SpeechRecognitionResult RecognizeFromWav(String filename)
       8:     {
       9:         var engine = null as SpeechRecognitionEngine;
      10:  
      11:         if (String.IsNullOrWhiteSpace(this.Recognition.Culture) == true)
      12:         {
      13:             engine = new SpeechRecognitionEngine();
      14:         }
      15:         else
      16:         {
      17:             engine = new SpeechRecognitionEngine(CultureInfo.CreateSpecificCulture(this.Recognition.Culture));
      18:         }
      19:  
      20:         using (engine)
      21:         {
      22:             if ((this.Recognition.Grammars.Any() == false) && (this.Recognition.Choices.Any() == false))
      23:             {
      24:                 engine.LoadGrammar(new DictationGrammar());
      25:             }
      26:  
      27:             foreach (var grammar in this.Recognition.Grammars)
      28:             {
      29:                 var doc = new SrgsDocument(Path.Combine(HttpRuntime.AppDomainAppPath, grammar));
      30:                 engine.LoadGrammar(new Grammar(doc));
      31:             }
      32:  
      33:             if (this.Recognition.Choices.Any() == true)
      34:             {
      35:                 var choices = new Choices(this.Recognition.Choices.ToArray());
      36:                 engine.LoadGrammar(new Grammar(choices));
      37:             }
      38:  
      39:             engine.SetInputToWaveFile(filename);
      40:  
      41:             var result = engine.Recognize();
      42:  
      43:             return ((result != null) ? new SpeechRecognitionResult(result.Text, result.Alternates.Select(x => x.Text).ToArray()) : null);
      44:         }
      45:     }
      46: }

    What this provider does is simply receive the location of a .WAV file and feed it to SpeechRecognitionEngine, together with some parameters of SpeechRecognition (Culture, AudioRate, Grammars and Choices)

    Finally, the code for the Server (Microsoft Speech Platform Software Development Kit) version:

       1: public class ServerSpeechRecognitionProvider : SpeechRecognitionProvider
       2: {
       3:     public ServerSpeechRecognitionProvider(SpeechRecognition recognition) : base(recognition)
       4:     {
       5:     }
       6:  
       7:     public override SpeechRecognitionResult RecognizeFromWav(String filename)
       8:     {
       9:         var engine = null as SpeechRecognitionEngine;
      10:  
      11:         if (String.IsNullOrWhiteSpace(this.Recognition.Culture) == true)
      12:         {
      13:             engine = new SpeechRecognitionEngine();
      14:         }
      15:         else
      16:         {
      17:             engine = new SpeechRecognitionEngine(CultureInfo.CreateSpecificCulture(this.Recognition.Culture));
      18:         }
      19:  
      20:         using (engine)
      21:         {
      22:             foreach (var grammar in this.Recognition.Grammars)
      23:             {
      24:                 var doc = new SrgsDocument(Path.Combine(HttpRuntime.AppDomainAppPath, grammar));
      25:                 engine.LoadGrammar(new Grammar(doc));
      26:             }
      27:  
      28:             if (this.Recognition.Choices.Any() == true)
      29:             {
      30:                 var choices = new Choices(this.Recognition.Choices.ToArray());
      31:                 engine.LoadGrammar(new Grammar(choices));
      32:             }
      33:  
      34:             engine.SetInputToWaveFile(filename);
      35:  
      36:             var result = engine.Recognize();
      37:  
      38:             return ((result != null) ? new SpeechRecognitionResult(result.Text, result.Alternates.Select(x => x.Text).ToArray()) : null);
      39:         }
      40:     }
      41: }

    As you can see, it is very similar to the Desktop one. Keep in mind, however, that for this provider to work you will have to download the Microsoft Speech Platform SDK, the Microsoft Speech Platform Runtime and at least one language from the Language Pack.

    Here is a sample markup declaration:

       1: <web:SpeechRecognition runat="server" ID="processor" ClientIDMode="Static" Mode="Desktop" Culture="en-US" OnSpeechRecognized="OnSpeechRecognized" OnClientSpeechRecognized="onSpeechRecognized" />

    If you want to add specific choices, add the Choices attribute to the control declaration and separate the values by commas:

       1: Choices="One, Two, Three"

    Or add a grammar file:

       1: Grammars="~/MyGrammar.grxml"

    By the way, grammars are not so difficult to create, you can find a good documentation in MSDN: http://msdn.microsoft.com/en-us/library/ee800145.aspx.

    To finalize, a sample JavaScript for starting recognition and receiving the results:

       1: <script type="text/javascript">
       1:  
       2:     
       3:     function onSpeechRecognized(result)
       4:     {
       5:         window.alert('Recognized: ' + result.Text + '\nAlternatives: ' + String.join(', ', result.Alternatives));
       6:     }
       7:  
       8:     function start()
       9:     {
      10:         document.getElementById('processor').startRecording();
      11:     }
      12:  
      13:     function stop()
      14:     {
      15:         document.getElementById('processor').stopRecording();
      16:     }
      17:  
    </script>

    And that’s it. You start recognition by calling startRecording(), get results in onSpeechRecognized() (or any other function set in the OnClientSpeechRecognized property) and stop recording with stopRecording(). The values passed to onSpeechRecognized() are those that may have been filtered by the server-side SpeechRecognized event handler.

    A final word of advisory: because generated sound files may become very large, do keep the recordings as short as possible.

    Of course, this offers several different possibilities, I am looking forward to hearing them from you! Winking smile

    Read more...

  • Loading ASP.NET MVC Controllers and Views From an Assembly

    Back to MVC land! This time, I wanted to be able to load controllers and views from an assembly other than my application. I know about the extensibility mechanisms that ASP.NET and MVC give provides, such as Virtual Path Providers and Controller Factories, so I thought I could use them.

    First things first: we need a controller factory that can load a controller from another assembly:

       1: class AssemblyControllerFactory : DefaultControllerFactory
       2: {
       3:     private readonly IDictionary<String, Type> controllerTypes;
       4:  
       5:     public AssemblyControllerFactory(Assembly assembly)
       6:     {
       7:         this.controllerTypes = assembly.GetExportedTypes().Where(x => (typeof(IController).IsAssignableFrom(x) == true) && (x.IsInterface == false) && (x.IsAbstract == false)).ToDictionary(x => x.Name, x => x);
       8:     }
       9:  
      10:     public override IController CreateController(RequestContext requestContext, String controllerName)
      11:     {
      12:         var controller = base.CreateController(requestContext, controllerName);
      13:  
      14:         if (controller == null)
      15:         {
      16:             var controllerType = this.controllerTypes.Where(x => x.Key == String.Format("{0}Controller", controllerName)).Select(x => x.Value).SingleOrDefault();
      17:             var controllerActivator = DependencyResolver.Current.GetService(typeof (IControllerActivator)) as IControllerActivator;
      18:  
      19:             if (controllerType != null)
      20:             {
      21:                 if (controllerActivator != null)
      22:                 {
      23:                     controller = controllerActivator.Create(requestContext, controllerType);
      24:                 }
      25:                 else
      26:                 {
      27:                     controller = Activator.CreateInstance(controllerType) as IController;
      28:                 }
      29:             }
      30:         }
      31:  
      32:         return (controller);
      33:     }
      34: }

    I inherited AssemblyControllerFactory from DefaultControllerFactory because this class has most of the logic we need, and I just override its CreateController method.

    Next, we need to be able to load view files from an assembly, and a virtual path provider is just what we need for that:

       1: public class AssemblyVirtualPathProvider : VirtualPathProvider
       2: {
       3:     private readonly Assembly assembly;
       4:     private readonly IEnumerable<VirtualPathProvider> providers;
       5:  
       6:     public AssemblyVirtualPathProvider(Assembly assembly)
       7:     {
       8:         var engines = ViewEngines.Engines.OfType<VirtualPathProviderViewEngine>().ToList();
       9:  
      10:         this.providers = engines.Select(x => x.GetType().GetProperty("VirtualPathProvider", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(x, null) as VirtualPathProvider).Distinct().ToList();
      11:         this.assembly = assembly;
      12:     }
      13:  
      14:     public override CacheDependency GetCacheDependency(String virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
      15:     {
      16:         if (this.FindFileByPath(this.CorrectFilePath(virtualPath)) != null)
      17:         {
      18:             return (new AssemblyCacheDependency(assembly));
      19:         }
      20:         else
      21:         {
      22:             return (base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart));
      23:         }
      24:     }
      25:  
      26:     public override Boolean FileExists(String virtualPath)
      27:     {
      28:         foreach (var provider in this.providers)
      29:         {
      30:             if (provider.FileExists(virtualPath) == true)
      31:             {
      32:                 return (true);
      33:             }
      34:         }
      35:  
      36:         var exists = this.FindFileByPath(this.CorrectFilePath(virtualPath)) != null;
      37:  
      38:         return (exists);
      39:     }
      40:  
      41:     public override VirtualFile GetFile(String virtualPath)
      42:     {
      43:         var resource = null as Stream;
      44:  
      45:         foreach (var provider in this.providers)
      46:         {
      47:             var file = provider.GetFile(virtualPath);
      48:  
      49:             if (file != null)
      50:             {
      51:                 try
      52:                 {
      53:                     resource = file.Open();
      54:                     return (file);
      55:                 }
      56:                 catch
      57:                 {
      58:                 }
      59:             }
      60:         }
      61:  
      62:         var resourceName = this.FindFileByPath(this.CorrectFilePath(virtualPath));
      63:  
      64:         return (new AssemblyVirtualFile(virtualPath, this.assembly, resourceName));
      65:     }
      66:  
      67:     protected String FindFileByPath(String virtualPath)
      68:     {
      69:         var resourceNames = this.assembly.GetManifestResourceNames();
      70:  
      71:         return (resourceNames.SingleOrDefault(r => r.EndsWith(virtualPath, StringComparison.OrdinalIgnoreCase) == true));
      72:     }
      73:  
      74:     protected String CorrectFilePath(String virtualPath)
      75:     {
      76:         return (virtualPath.Replace("~", String.Empty).Replace('/', '.'));
      77:     }
      78: }
      79:  
      80: public class AssemblyVirtualFile : VirtualFile
      81: {
      82:     private readonly Assembly assembly;
      83:     private readonly String resourceName;
      84:  
      85:     public AssemblyVirtualFile(String virtualPath, Assembly assembly, String resourceName) : base(virtualPath)
      86:     {
      87:         this.assembly = assembly;
      88:         this.resourceName = resourceName;
      89:     }
      90:  
      91:     public override Stream Open()
      92:     {
      93:         return (this.assembly.GetManifestResourceStream(this.resourceName));
      94:     }
      95: }
      96:  
      97: public class AssemblyCacheDependency : CacheDependency
      98: {
      99:     private readonly Assembly assembly;
     100:  
     101:     public AssemblyCacheDependency(Assembly assembly)
     102:     {
     103:         this.assembly = assembly;
     104:         this.SetUtcLastModified(File.GetCreationTimeUtc(assembly.Location));
     105:     }
     106: }

    These three classes inherit from VirtualPathProvider, VirtualFile and CacheDependency and just override some of its methods. AssemblyVirtualPathProvider first checks with other virtual path providers if a file exists, and only if it doesn’t does it create the AssemblyVirtualFile. This looks up the virtual file name in the assembly’s resources, using a convention that translates slashes (/) per dots (.) and returns it. As for the AssemblyCacheDependency, we need it because otherwise ASP.NET MVC will think that the file exists in a directory and will try to monitor it, and because the directory and file do not exist, it will throw an exception at runtime.

    We also need a bootstrapping class for setting up everything:

       1: public static class AssemblyRoute
       2: {
       3:     public static void MapRoutes(this RouteCollection routes, Assembly assembly)
       4:     {
       5:         ControllerBuilder.Current.SetControllerFactory(new AssemblyControllerFactory(assembly));
       6:         HostingEnvironment.RegisterVirtualPathProvider(new AssemblyVirtualPathProvider(assembly));
       7:     }
       8: }

    Finally, for this to work, we need three things:

    • The controller must be public, have a parameterless constructor, and its name must end with Controller (the default convention);
    • View files must be compiled as embedded resources in the assembly:

    image

    • And finally, we need to set this up in Global.asax.cs or RouteConfig.cs:
       1: routes.MapRoutes(typeof(MyController).Assembly);

    By the way, the AssemblyVirtualPath provider, AssemblyVirtualFile and AssemblyCacheDependency are pretty generic, so you can use them in other scenarios.

    That’s all, folks! Winking smile

    Read more...

  • ASP.NET NHibernateDataSource

    A long, long time ago, I wrote a NHibernateDataSource control. Back then, it was based in the first LINQ provider for NHibernate, and a long has happened since. Now, I decided to give it another go! Smile

    Historically, in ASP.NET, a data control should inherit from DataSourceControl, like ObjectDataSource, LinqDataSource, EntityDataSource, SqlDataSource, etc, and should expose collections for parameters for each of the supported operations (select, update, delete and insert). Since ASP.NET 4, however, a new base class came along: QueryableDataSource. This class is an implementation of IQueryableDataSource, which allows using QueryExtender, also introduced in version 4, to filter and sort the results of a data source that uses LINQ.

    I wanted my control to be able to use QueryExtender, but I also wanted to be able to give it an HQL query. It should also be capable of inserting, updating and deleting entities.

    So, here’s what I came up with, first, the NHibernateDataSource class:

       1: [ParseChildren(true)]
       2: public class NHibernateDataSource : QueryableDataSource
       3: {
       4:     public NHibernateDataSource()
       5:     {
       6:         this.SelectParameters = new ParameterCollection();
       7:         this.InsertParameters = new ParameterCollection();
       8:         this.UpdateParameters = new ParameterCollection();
       9:         this.DeleteParameters = new ParameterCollection();
      10:     }
      11:  
      12:     [Description("Raised when a session factory is built")]
      13:     public event EventHandler<BuildSessionFactoryEventArgs> BuildSessionFactory;
      14:     [Description("Raised when a configuration instance is created")]
      15:     public event EventHandler<ConfigureEventArgs> Configure;
      16:     [Description("Raised when an entity is created for inserts or updates")]
      17:     public event EventHandler<CreateInstanceEventArgs> CreateInstance;
      18:  
      19:     [Description("Raised after an entity is inserted")]
      20:     public event EventHandler<EntityEventArgs> EntityInserted;
      21:     [Description("Raised after an entity is deleted")]
      22:     public event EventHandler<EntityEventArgs> EntityDeleted;
      23:     [Description("Raised after an entity is updated")]
      24:     public event EventHandler<EntityEventArgs> EntityUpdated;
      25:     [Description("Raised after a query is executed")]
      26:     public event EventHandler<EntitiesSelectedEventArgs> EntitiesSelected;
      27:     [Description("Raised when an operation completes (select, insert, update or delete)")]
      28:     public event EventHandler<OperationCompletedEventArgs> OperationCompleted;
      29:  
      30:     [Description("Raised before a select is made")]
      31:     public event EventHandler<EntitiesSelectingEventArgs> EntitiesSelecting;
      32:     [Description("Raised before an entity is inserted")]
      33:     public event EventHandler<EntityEventArgs> EntityInserting;
      34:     [Description("Raised before an entity is deleted")]
      35:     public event EventHandler<EntityEventArgs> EntityDeleting;
      36:     [Description("Raised before an entity is updated")]
      37:     public event EventHandler<EntityEventArgs> EntityUpdating;
      38:  
      39:     [Description("The entity name to update, delete or insert")]
      40:     [DefaultValue("")]
      41:     public String EntityName
      42:     {
      43:         get;
      44:         set;
      45:     }
      46:  
      47:     [Description("The HQL to use for selecting records, when mode Hql is selected")]
      48:     [DefaultValue("")]
      49:     public String Hql
      50:     {
      51:         get;
      52:         set;
      53:     }
      54:  
      55:     [Description("The maximum number of records to retrieve, if paging is not used")]
      56:     [DefaultValue(0)]
      57:     public Int32 MaximumRecords
      58:     {
      59:         get;
      60:         set;
      61:     }
      62:  
      63:     [Description("The page size to retrieve")]
      64:     [DefaultValue(0)]
      65:     public Int32 PageSize
      66:     {
      67:         get;
      68:         set;
      69:     }
      70:  
      71:     [Description("The page index to retrieve")]
      72:     [DefaultValue(0)]
      73:     public Int32 PageIndex
      74:     {
      75:         get;
      76:         set;
      77:     }
      78:  
      79:     [Description("Use HQL or EntityName for selecting")]
      80:     [DefaultValue(NHibernateDataSourceMode.Linq)]
      81:     public NHibernateDataSourceMode Mode
      82:     {
      83:         get;
      84:         set;
      85:     }
      86:  
      87:     [Description("Whether to merge the entity before updating or deleting or not")]
      88:     [DefaultValue(false)]
      89:     public Boolean RefreshBeforeUpdate
      90:     {
      91:         get;
      92:         set;
      93:     }
      94:  
      95:     [Description("Parameters that will be used for the HQL query")]
      96:     public ParameterCollection SelectParameters
      97:     {
      98:         get;
      99:         private set;
     100:     }
     101:  
     102:     [Description("Parameters that will be used for inserting a new entity")]
     103:     public ParameterCollection InsertParameters
     104:     {
     105:         get;
     106:         private set;
     107:     }
     108:  
     109:     [Description("Parameters that will be used for updating an existing entity")]
     110:     public ParameterCollection UpdateParameters
     111:     {
     112:         get;
     113:         private set;
     114:     }
     115:  
     116:     [Description("Parameters that will be used for deleting an existing entity")]
     117:     public ParameterCollection DeleteParameters
     118:     {
     119:         get;
     120:         private set;
     121:     }
     122:  
     123:     [Browsable(false)]
     124:     public ISessionFactory SessionFactory
     125:     {
     126:         get;
     127:         set;
     128:     }
     129:  
     130:     internal static ISessionFactory InternalSessionFactory
     131:     {
     132:         get;
     133:         set;
     134:     }
     135:  
     136:     internal ISessionFactory EffectiveSessionFactory
     137:     {
     138:         get
     139:         {
     140:             var sessionFactory = this.SessionFactory ?? InternalSessionFactory;
     141:             var sfArgs = new BuildSessionFactoryEventArgs() { SessionFactory = sessionFactory };
     142:  
     143:             this.OnBuildSessionFactory(sfArgs);
     144:  
     145:             if (sfArgs.SessionFactory == null)
     146:             {
     147:                 var cfg = new Configuration().Configure();
     148:  
     149:                 var cfgArgs = new ConfigureEventArgs() { Configuration =  cfg };
     150:  
     151:                 this.OnConfigure(cfgArgs);
     152:  
     153:                 cfg = cfgArgs.Configuration;
     154:  
     155:                 sessionFactory = cfg.BuildSessionFactory();
     156:  
     157:                 if (InternalSessionFactory == null)
     158:                 {
     159:                     InternalSessionFactory = sessionFactory;
     160:                 }
     161:             }
     162:             else
     163:             {
     164:                 sessionFactory = sfArgs.SessionFactory;
     165:             }
     166:  
     167:             return (sessionFactory);
     168:         }
     169:     }
     170:  
     171:     protected virtual void OnBuildSessionFactory(BuildSessionFactoryEventArgs e)
     172:     {
     173:         var handler = this.BuildSessionFactory;
     174:  
     175:         if (handler != null)
     176:         {
     177:             handler(this, e);
     178:         }
     179:     }
     180:  
     181:     protected virtual void OnConfigure(ConfigureEventArgs e)
     182:     {
     183:         var handler = this.Configure;
     184:  
     185:         if (handler != null)
     186:         {
     187:             handler(this, e);
     188:         }
     189:     }
     190:  
     191:     protected virtual void OnCreateInstance(CreateInstanceEventArgs e)
     192:     {
     193:         var handler = this.CreateInstance;
     194:  
     195:         if (handler != null)
     196:         {
     197:             handler(this, e);
     198:         }
     199:     }
     200:  
     201:     protected virtual void OnEntitiesSelecting(EntitiesSelectingEventArgs e)
     202:     {
     203:         var handler = this.EntitiesSelecting;
     204:  
     205:         if (handler != null)
     206:         {
     207:             handler(this, e);
     208:         }
     209:     }
     210:  
     211:     protected virtual void OnEntityInserted(EntityEventArgs e)
     212:     {
     213:         var handler = this.EntityInserted;
     214:  
     215:         if (handler != null)
     216:         {
     217:             handler(this, e);
     218:         }
     219:     }
     220:  
     221:     protected virtual void OnEntityDeleted(EntityEventArgs e)
     222:     {
     223:         var handler = this.EntityDeleted;
     224:  
     225:         if (handler != null)
     226:         {
     227:             handler(this, e);
     228:         }
     229:     }
     230:  
     231:     protected virtual void OnEntityUpdated(EntityEventArgs e)
     232:     {
     233:         var handler = this.EntityUpdated;
     234:  
     235:         if (handler != null)
     236:         {
     237:             handler(this, e);
     238:         }
     239:     }
     240:  
     241:     protected virtual void OnEntityInserting(EntityEventArgs e)
     242:     {
     243:         var handler = this.EntityInserting;
     244:  
     245:         if (handler != null)
     246:         {
     247:             handler(this, e);
     248:         }
     249:     }
     250:  
     251:     protected virtual void OnEntityDeleting(EntityEventArgs e)
     252:     {
     253:         var handler = this.EntityDeleting;
     254:  
     255:         if (handler != null)
     256:         {
     257:             handler(this, e);
     258:         }
     259:     }
     260:  
     261:     protected virtual void OnEntityUpdating(EntityEventArgs e)
     262:     {
     263:         var handler = this.EntityUpdating;
     264:  
     265:         if (handler != null)
     266:         {
     267:             handler(this, e);
     268:         }
     269:     }
     270:  
     271:     public virtual void OnEntitiesSelected(EntitiesSelectedEventArgs e)
     272:     {
     273:         var handler = this.EntitiesSelected;
     274:  
     275:         if (handler != null)
     276:         {
     277:             handler(this, e);
     278:         }
     279:     }
     280:  
     281:     public virtual void OnOperationCompleted(OperationCompletedEventArgs e)
     282:     {
     283:         var handler = this.OperationCompleted;
     284:  
     285:         if (handler != null)
     286:         {
     287:             handler(this, e);
     288:         }
     289:     }
     290:  
     291:     public Int32 Insert()
     292:     {
     293:         if (String.IsNullOrWhiteSpace(this.EntityName) == true)
     294:         {
     295:             throw (new InvalidOperationException("The EntityName property cannot be empty."));
     296:         }
     297:  
     298:         if (this.InsertParameters.Count == 0)
     299:         {
     300:             throw (new InvalidOperationException("Missing InsertParameters."));
     301:         }
     302:  
     303:         using (var session = this.EffectiveSessionFactory.OpenStatelessSession())
     304:         using (session.BeginTransaction())
     305:         {
     306:             var args = new EntityEventArgs(this.CreateInstanceAndSetParameters(this.InsertParameters));
     307:  
     308:             this.OnEntityInserting(args);
     309:  
     310:             if (args.Cancel == true)
     311:             {
     312:                 return (0);
     313:             }
     314:  
     315:             session.Insert(args.Entity);
     316:             session.Transaction.Commit();
     317:  
     318:             this.OnEntityInserted(args);
     319:             this.OnOperationCompleted(new OperationCompletedEventArgs(DataSourceOperation.Insert, args.Entity));
     320:         }
     321:  
     322:         return (1);
     323:     }
     324:  
     325:     public Int32 Update()
     326:     {
     327:         if (String.IsNullOrWhiteSpace(this.EntityName) == true)
     328:         {
     329:             throw (new InvalidOperationException("The EntityName property cannot be empty."));
     330:         }
     331:  
     332:         if (this.UpdateParameters.Count == 0)
     333:         {
     334:             throw (new InvalidOperationException("Missing UpdateParameters."));
     335:         }
     336:  
     337:         using (var session = this.EffectiveSessionFactory.OpenStatelessSession())
     338:         using (session.BeginTransaction())
     339:         {
     340:             var args = new EntityEventArgs(this.CreateInstanceAndSetParameters(this.UpdateParameters));
     341:  
     342:             this.OnEntityUpdating(args);
     343:  
     344:             if (args.Cancel == true)
     345:             {
     346:                 return (0);
     347:             }
     348:  
     349:             if (this.RefreshBeforeUpdate == true)
     350:             {
     351:                 this.Refresh(args.Entity);
     352:             }
     353:  
     354:             session.Update(args.Entity);
     355:             session.Transaction.Commit();
     356:  
     357:             this.OnEntityUpdated(args);
     358:             this.OnOperationCompleted(new OperationCompletedEventArgs(DataSourceOperation.Update, args.Entity));
     359:  
     360:             return (1);
     361:         }
     362:     }
     363:  
     364:     public Int32 Delete()
     365:     {
     366:         if (String.IsNullOrWhiteSpace(this.EntityName) == true)
     367:         {
     368:             throw (new InvalidOperationException("The EntityName property cannot be empty."));
     369:         }
     370:  
     371:         using (var session = this.EffectiveSessionFactory.OpenStatelessSession())
     372:         using (session.BeginTransaction())
     373:         {
     374:             var args = new EntityEventArgs(this.CreateInstanceAndSetParameters(this.DeleteParameters));
     375:  
     376:             this.OnEntityDeleting(args);
     377:  
     378:             if (args.Cancel == true)
     379:             {
     380:                 return (0);
     381:             }
     382:  
     383:             if (this.RefreshBeforeUpdate == true)
     384:             {
     385:                 this.Refresh(args.Entity);
     386:             }
     387:  
     388:             session.Delete(args.Entity);
     389:             session.Transaction.Commit();
     390:  
     391:             this.OnEntityDeleted(args);
     392:             this.OnOperationCompleted(new OperationCompletedEventArgs(DataSourceOperation.Delete, args.Entity));
     393:  
     394:             return (1);
     395:         }
     396:     }
     397:  
     398:     protected void Refresh(Object entity)
     399:     {
     400:         using (var session = this.EffectiveSessionFactory.OpenSession())
     401:         {
     402:             session.DefaultReadOnly = true;
     403:             session.FlushMode = FlushMode.Never;
     404:  
     405:             var metadata = this.GetMetadata(this.EntityName);
     406:             var propertiesToLoad = new List<String>();
     407:  
     408:             for (var i = 0; i < metadata.PropertyNames.Length; ++i)
     409:             {
     410:                 if (metadata.GetPropertyValue(entity, metadata.PropertyNames[i], EntityMode.Poco) == null)
     411:                 {
     412:                     if (metadata.PropertyTypes[i].IsEntityType == false)
     413:                     {
     414:                         propertiesToLoad.Add(metadata.PropertyNames[i]);
     415:                     }
     416:                     else
     417:                     {
     418:                         propertiesToLoad.Add(String.Concat(metadata.PropertyNames[i], ".id"));
     419:                     }
     420:                 }
     421:             }
     422:  
     423:             var hql = new StringBuilder();
     424:             hql.Append("select ");
     425:             hql.Append(String.Join(", ", propertiesToLoad));
     426:             hql.AppendFormat(" from {0} where id = :id", entity.GetType().FullName);
     427:  
     428:             var query = session.CreateQuery(hql.ToString());
     429:             query.SetParameter("id", metadata.GetIdentifier(entity, EntityMode.Poco));
     430:  
     431:             var result = query.UniqueResult();
     432:             var values = (result as Object[]) ?? new Object[] { result };
     433:  
     434:             for (var i = 0; i < propertiesToLoad.Count; ++i)
     435:             {
     436:                 var parts = propertiesToLoad[i].Split('.');
     437:                 var value = values[i];
     438:                 var propertyName = parts.First();
     439:  
     440:                 if (parts.Length > 1)
     441:                 {
     442:                     var propertyIndex = Array.IndexOf(metadata.PropertyNames, propertyName);
     443:                     var propertyType = metadata.PropertyTypes[propertyIndex].ReturnedClass;
     444:  
     445:                     value = session.Load(propertyType, values[i]);
     446:                 }
     447:  
     448:                 metadata.SetPropertyValue(entity, propertyName, value, EntityMode.Poco);
     449:             }
     450:         }
     451:     }
     452:  
     453:     protected internal IDictionary<String, Object> GetParameters(ParameterCollection parameters)
     454:     {
     455:         return (parameters.GetValues(this.Context, this).OfType<DictionaryEntry>().ToDictionary(x => x.Key.ToString(), x => x.Value));
     456:     }
     457:  
     458:     protected void SetParameterValues(Object instance, IClassMetadata metadata, IDictionary<String, Object> parameters)
     459:     {
     460:         foreach (var parameter in parameters)
     461:         {
     462:             if (metadata.PropertyNames.Contains(parameter.Key) == true)
     463:             {
     464:                 metadata.SetPropertyValue(instance, parameter.Key, parameter.Value, EntityMode.Poco);
     465:             }
     466:             else if (metadata.IdentifierPropertyName == parameter.Key)
     467:             {
     468:                 metadata.SetIdentifier(instance, parameter.Value, EntityMode.Poco);
     469:             }
     470:         }
     471:     }
     472:  
     473:     protected Object CreateInstanceAndSetParameters(ParameterCollection parameters)
     474:     {
     475:         var metadata = this.GetMetadata(this.EntityName);
     476:  
     477:         if (metadata == null)
     478:         {
     479:             throw (new InvalidOperationException("Entity could not be found."));
     480:         }
     481:  
     482:         var entityType = metadata.GetMappedClass(EntityMode.Poco);
     483:  
     484:         var ciArgs = new CreateInstanceEventArgs(entityType, null);
     485:  
     486:         this.OnCreateInstance(ciArgs);
     487:  
     488:         if (ciArgs.Instance == null)
     489:         {
     490:             ciArgs.Instance = Activator.CreateInstance(entityType);
     491:         }
     492:  
     493:         this.SetParameterValues(ciArgs.Instance, metadata, this.GetParameters(parameters));
     494:  
     495:         return (ciArgs.Instance);
     496:     }
     497:  
     498:     protected internal IClassMetadata GetMetadata(String entityName)
     499:     {
     500:         var metadata = this.EffectiveSessionFactory.GetAllClassMetadata().Where(x => x.Key.EndsWith(entityName)).Select(x => x.Value).SingleOrDefault();
     501:  
     502:         return (metadata);
     503:     }
     504:  
     505:     protected internal void ProcessEntitiesSelecting(EntitiesSelectingEventArgs e)
     506:     {
     507:         this.OnEntitiesSelecting(e);
     508:     }
     509:  
     510:     protected internal void ProcessEntitiesSelected(EntitiesSelectedEventArgs e)
     511:     {
     512:         this.OnEntitiesSelected(e);
     513:         this.OnOperationCompleted(new OperationCompletedEventArgs(DataSourceOperation.Select, e.Results));
     514:     }
     515:  
     516:     protected override QueryableDataSourceView CreateQueryableView()
     517:     {
     518:         return (new NHibernateDataSourceView(this, "DefaultView", this.Context) as QueryableDataSourceView);
     519:     }
     520: }

    You can see that it exposes some events:

    • Configure: gives developers a chance to build (or return an existing) Configuration instance, that will be used for building the session factory;
    • BuildSessionFactory: allows setting parameters on the default session factory or returning an existing one;
    • CreateInstance: raised before NHibernate creates a default instance, to allow developers to return one;
    • EntityInserting: raised before an entity is inserted, allowing developers to cancel the operations or to set entity parameter;
    • EntityUpdating: raised before an entity is updated, allowing developers to cancel the operations or to set entity parameter;
    • EntityDeleting: raised before an entity is deleting, allowing its cancellation;
    • EntitiesSelecting: raised before a select operation is performed;
    • EntityInserted: raised after an entity was inserted;
    • EntityUpdated: raised after an entity was updated;
    • EntityDeleted: raised after an entity was deleted;
    • EntitiesSelected: raised after a select operation was performed;
    • OperationCompleted: raised after an operation completes (select, insert, update or delete).


    If no handler for CreateInstance is supplied, NHibernateDataSource will try to create an entity using Activator.CreateInstance.

    EntitySelecting is raised regardless of the Mode (Hql or Linq), but it will have different values in its argument: a query string plus parameters in the case of Hql and an IQueryable instance for Linq.

    EntityInserting, EntityUpdating and EntityDeleting allow the modification of properties of the entity in the parameter, but not the replacing of the entity itself.

    OperationCompleted is always called, except in the event of an exception.

    It also exposes a couple of properties:

    • Mode: one of the two operation modes, Hql or Linq. If Hql is used, then the Hql property must be set; otherwise, it’s EntityName that is required;
    • Hql: an NHibernate HQL query string;
    • EntityName: the name of an entity that the control will work with; only required for Mode Linq or for inserts, updates or deletes;
    • RefreshBeforeUpdate: whether NHibernate should refresh the properties of an entity before updating or deleting it;
    • MaximumRecords: the optional maximum number of records to retrieve, if paging is not used (PageSize and PageIndex);
    • PageIndex: the page index to retrieve;
    • PageSize: the page size to retrieve;
    • SessionFactory: a session factory that will be used instead of a default created one;
    • SelectParameters: a collection of parameters to be applied to the Hql string;
    • InsertParameters: a collection of parameters for the insert operation;
    • UpdateParameters: a collection of parameters for the update operation;
    • DeleteParameters: a collection of parameters for the delete operation.


    And, of course, exposes the basic operations: select is the default, but Insert, Update and Delete methods are available.

    NHibernateDataSource will check if the SessionFactory property is set, otherwise, it will build its own Configuration instance and raise the Configure and BuildSessionFactory events. The generated session factory is then stored in the InternalSessionFactory static property for caching.

    Then, the NHibernateDataSourceView, which is the responsible for the actual querying, inheriting from QueryableDataSourceView:

       1: public class NHibernateDataSourceView : QueryableDataSourceView
       2: {
       3:     private static readonly MethodInfo queryMethod = typeof (LinqExtensionMethods).GetMethod("Query", BindingFlags.Public | BindingFlags.Static, null, new Type[] { typeof(IStatelessSession) }, null );
       4:     private static readonly MethodInfo toListMethod = typeof(Enumerable).GetMethod("ToList", BindingFlags.Public | BindingFlags.Static);
       5:  
       6:     public NHibernateDataSourceView(NHibernateDataSource dataSource, String viewName, HttpContext context) : base(dataSource, viewName, context)
       7:     {
       8:         this.DataSource = dataSource;
       9:     }
      10:  
      11:     protected NHibernateDataSource DataSource
      12:     {
      13:         get;
      14:         private set;
      15:     }
      16:  
      17:     protected override IEnumerable ExecuteSelect(DataSourceSelectArguments arguments)
      18:     {
      19:         using (var session = this.DataSource.EffectiveSessionFactory.OpenStatelessSession())
      20:         {
      21:             var results = null as IList;
      22:  
      23:             switch (this.DataSource.Mode)
      24:             {
      25:                 case NHibernateDataSourceMode.Hql:
      26:                 {
      27:                     if (String.IsNullOrWhiteSpace(this.DataSource.Hql) == true)
      28:                     {
      29:                         throw (new InvalidOperationException("The Hql property cannot be empty."));
      30:                     }
      31:  
      32:                     var hql = this.DataSource.Hql;
      33:                     var parameters = this.DataSource.GetParameters(this.DataSource.SelectParameters);
      34:                     var args = new EntitiesSelectingEventArgs(hql, parameters, this.DataSource.PageSize, this.DataSource.PageIndex, this.DataSource.MaximumRecords);
      35:  
      36:                     this.DataSource.ProcessEntitiesSelecting(args);
      37:  
      38:                     var query = session.CreateQuery(args.Hql);
      39:  
      40:                     foreach (var param in args.SelectParameters)
      41:                     {
      42:                         if (!(param.Value is IEnumerable) || (param.Value is String) || (param.Value is Byte[]))
      43:                         {
      44:                             query.SetParameter(param.Key, param.Value);
      45:                         }
      46:                         else
      47:                         {
      48:                             query.SetParameterList(param.Key, param.Value as IEnumerable);
      49:                         }
      50:                     }
      51:  
      52:                     if (args.PageSize != 0)
      53:                     {
      54:                         query.SetMaxResults(args.PageSize);
      55:                         query.SetFirstResult(Math.Max((args.PageIndex * args.PageSize) - 1, 0));
      56:                         arguments.MaximumRows = args.PageSize;
      57:                     }
      58:  
      59:                     if (args.MaximumRecords != 0)
      60:                     {
      61:                         query.SetMaxResults(args.MaximumRecords);
      62:                         arguments.MaximumRows = args.MaximumRecords;
      63:                     }
      64:  
      65:                     results = query.List();
      66:  
      67:                     arguments.AddSupportedCapabilities(DataSourceCapabilities.Page);
      68:  
      69:                     if (args.PageSize != 0)
      70:                     {
      71:                         arguments.StartRowIndex = Math.Max((args.PageIndex * args.PageSize) - 1, 0);
      72:                     }
      73:  
      74:                     break;
      75:                 }
      76:  
      77:                 case NHibernateDataSourceMode.Linq:
      78:                 {
      79:                     if (String.IsNullOrWhiteSpace(this.DataSource.EntityName) == true)
      80:                     {
      81:                         throw (new InvalidOperationException("The EntityName property cannot be empty."));
      82:                     }
      83:  
      84:                     var query = queryMethod.MakeGenericMethod(this.EntityType).Invoke(null, new Object[] { session }) as IQueryable;
      85:  
      86:                     var qcea = new QueryCreatedEventArgs(query);
      87:  
      88:                     this.OnQueryCreated(qcea);
      89:  
      90:                     var esaea = new EntitiesSelectingEventArgs(qcea.Query);
      91:  
      92:                     this.DataSource.ProcessEntitiesSelecting(esaea);
      93:  
      94:                     results = toListMethod.MakeGenericMethod(this.EntityType).Invoke(null, new Object[] { esaea.Query }) as IList;
      95:  
      96:                     arguments.AddSupportedCapabilities(DataSourceCapabilities.Page | DataSourceCapabilities.Sort);
      97:  
      98:                     break;
      99:                 }
     100:             }
     101:  
     102:             var entitiesSelectedArgs = new EntitiesSelectedEventArgs(results);
     103:  
     104:             this.DataSource.ProcessEntitiesSelected(entitiesSelectedArgs);
     105:  
     106:             return (entitiesSelectedArgs.Results);
     107:         }
     108:     }
     109:  
     110:     protected override Type EntityType
     111:     {
     112:         get
     113:         {
     114:             return (this.DataSource.GetMetadata(this.DataSource.EntityName).GetMappedClass(EntityMode.Poco));
     115:         }
     116:     }
     117:  
     118:     protected override Object GetSource(QueryContext context)
     119:     {
     120:         throw new NotImplementedException();
     121:     }
     122:  
     123:     protected override void HandleValidationErrors(IDictionary<String, Exception> errors, DataSourceOperation operation)
     124:     {
     125:     }
     126: }

    And the NHibernateDataSourceMode:

       1: public enum NHibernateDataSourceMode
       2: {
       3:     Linq,
       4:     Hql
       5: }

    Finally, all of the event arguments:

       1: [Serializable]
       2: public sealed class BuildSessionFactoryEventArgs : EventArgs
       3: {
       4:     public ISessionFactory SessionFactory
       5:     {
       6:         get;
       7:         set;
       8:     }
       9: }
      10:  
      11: [Serializable]
      12: public sealed class ConfigureEventArgs : EventArgs
      13: {
      14:     public Configuration Configuration
      15:     {
      16:         get;
      17:         set;
      18:     }
      19: }
      20:  
      21: [Serializable]
      22: public sealed class CreateInstanceEventArgs : EventArgs
      23: {
      24:     public CreateInstanceEventArgs(Type type, Object instance)
      25:     {
      26:         this.Type = type;
      27:         this.Instance = instance;
      28:     }
      29:  
      30:     public Type Type
      31:     {
      32:         get;
      33:         private set;
      34:     }
      35:  
      36:     public Object Instance
      37:     {
      38:         get;
      39:         set;
      40:     }
      41: }
      42:  
      43: [Serializable]
      44: public sealed class EntitiesSelectedEventArgs : EventArgs
      45: {
      46:     public EntitiesSelectedEventArgs(IList results)
      47:     {
      48:         this.Results = results;
      49:     }
      50:  
      51:     public IList Results
      52:     {
      53:         get;
      54:         set;
      55:     }
      56: }
      57:  
      58: [Serializable]
      59: public sealed class EntitiesSelectingEventArgs : EventArgs
      60: {
      61:     public EntitiesSelectingEventArgs(IQueryable query)
      62:     {
      63:         this.Query = query;
      64:     }
      65:  
      66:     public EntitiesSelectingEventArgs(String hql, IDictionary<String, Object> selectParameters, Int32 pageSize, Int32 pageIndex, Int32 maximumRecords)
      67:     {
      68:         this.Hql = hql;
      69:         this.SelectParameters = selectParameters;
      70:         this.PageSize = pageSize;
      71:         this.PageIndex = pageIndex;
      72:         this.MaximumRecords = maximumRecords;
      73:     }
      74:  
      75:     public IQueryable Query
      76:     {
      77:         get;
      78:         set;
      79:     }
      80:  
      81:     public String Hql
      82:     {
      83:         get;
      84:         set;
      85:     }
      86:  
      87:     public IDictionary<String, Object> SelectParameters
      88:     {
      89:         get;
      90:         private set;
      91:     }
      92:  
      93:     public Int32 PageSize
      94:     {
      95:         get;
      96:         set;
      97:     }
      98:  
      99:     public Int32 PageIndex
     100:     {
     101:         get;
     102:         set;
     103:     }
     104:  
     105:     public Int32 MaximumRecords
     106:     {
     107:         get;
     108:         set;
     109:     }
     110: }
     111:  
     112: [Serializable]
     113: public sealed class EntityEventArgs : CancelEventArgs
     114: {
     115:     public EntityEventArgs(Object entity)
     116:     {
     117:         this.Entity = entity;
     118:     }
     119:  
     120:     public Object Entity
     121:     {
     122:         get; 
     123:         set;
     124:     }
     125: }
     126:  
     127: [Serializable]
     128: public sealed class OperationCompletedEventArgs : EventArgs
     129: {
     130:     public OperationCompletedEventArgs(DataSourceOperation operation, Object entity)
     131:     {
     132:         this.Entity = entity;
     133:         this.Operation = operation;
     134:     }
     135:  
     136:     public OperationCompletedEventArgs(DataSourceOperation operation, IList results)
     137:     {
     138:         this.Results = results;
     139:         this.Operation = operation;
     140:     }
     141:  
     142:     public DataSourceOperation Operation
     143:     {
     144:         get;
     145:         private set;
     146:     }
     147:  
     148:     public Object Entity
     149:     {
     150:         get;
     151:         protected set;
     152:     }
     153:  
     154:     public IList Results
     155:     {
     156:         get;
     157:         private set;
     158:     }
     159: }

    Now, let’s see concrete examples of its usage. First, using Mode Hql:

       1: <nh:NHibernateDataSource runat="server" ID="nhds" RefreshBeforeUpdate="true" EntityName="Product" Mode="Hql" Hql="from Product p where size(p.OrderDetails) > :size">
       2:     <SelectParameters>
       3:         <asp:Parameter Name="size" DefaultValue="1" Type="Int32" />
       4:     </SelectParameters>
       5:     <InsertParameters>
       6:         <asp:Parameter Name="Name" DefaultValue="Some Name" Type="String" />
       7:         <asp:Parameter Name="Price" DefaultValue="100" Type="Decimal" />
       8:     </InsertParameters>
       9:     <UpdateParameters>
      10:         <asp:QueryStringParameter Name="ProductId" QueryStringField="ProductId" Type="Int32" />
      11:         <asp:QueryStringParameter Name="Price" DefaultValue="50" Type="Decimal" />
      12:     </UpdateParameters>
      13:     <DeleteParameters>
      14:         <asp:QueryStringParameter Name="ProductId" QueryStringField="ProductId" Type="Int32" />
      15:     </DeleteParameters>
      16: </nh:NHibernateDataSource>

    You can see that the Hql property has a parameter, price, which is bound to a parameter in SelectParameters with the same name. Each parameter is an instance of the Parameter class, here I am using a parameter with a static value (Parameter) and another that takes a value from the query string (QueryStringParameter), but others exist. To help with NHibernate insert and update operations, I created a new Parameter class, EntityParameter, that knows how to retrieve a en entity or a proxy to an entity:

       1: public sealed class EntityParameter : Parameter
       2: {
       3:     public EntityParameter()
       4:     {
       5:         this.Lazy = true;
       6:     }
       7:  
       8:     public String EntityName
       9:     {
      10:         get;
      11:         set;
      12:     }
      13:  
      14:     public Object Id
      15:     {
      16:         get;
      17:         set;
      18:     }
      19:  
      20:     public Boolean Lazy
      21:     {
      22:         get;
      23:         set;
      24:     }
      25:  
      26:     protected override Parameter Clone()
      27:     {
      28:         return (new EntityParameter(){ EntityName = this.EntityName, Id = this.Id });
      29:     }
      30:  
      31:     protected override Object Evaluate(HttpContext context, Control control)
      32:     {
      33:         var dataSource = control as NHibernateDataSource;
      34:  
      35:         if (dataSource == null)
      36:         {
      37:             throw (new InvalidOperationException("EntityParameter can only be used with NHibernateDataSource."));
      38:         }
      39:  
      40:         using (var session = dataSource.EffectiveSessionFactory.OpenStatelessSession())
      41:         {
      42:             var metadata = dataSource.GetMetadata(this.EntityName);
      43:  
      44:             if (metadata == null)
      45:             {
      46:                 throw (new InvalidOperationException("Entity could not be found."));
      47:             }
      48:  
      49:             var entityType = metadata.GetMappedClass(EntityMode.Poco);
      50:             var idType = metadata.IdentifierType.ReturnedClass;
      51:             var id = Convert.ChangeType(this.Id, idType);
      52:             var entity = (this.Lazy == true) ? (metadata as IEntityPersister).CreateProxy(id, session.GetSessionImplementation()) : session.Get(entityType.FullName, id);
      53:  
      54:             return (entity);
      55:         }
      56:     }
      57: }

    As for Mode Linq, an example using a QueryExtender is in order:

       1: <asp:TextBox runat="server" ID="name"/>
       2:  
       3: <asp:QueryExtender runat="server" TargetControlID="nhds">
       4:     <asp:SearchExpression DataFields="Name" SearchType="StartsWith">
       5:         <asp:ControlParameter ControlID="name" />
       6:     </asp:SearchExpression>
       7:     <asp:OrderByExpression DataField="Price" Direction="Descending" />
       8: </asp:QueryExtender>
       9:  
      10: <nh:NHibernateDataSource runat="server" ID="nhds" RefreshBeforeUpdate="true" EntityName="Product" Mode="Linq">
      11:     <InsertParameters>
      12:         <asp:Parameter Name="Name" DefaultValue="Some Name" Type="String" />
      13:         <asp:Parameter Name="Price" DefaultValue="100" Type="Decimal" />
      14:     </InsertParameters>
      15:     <UpdateParameters>
      16:         <asp:QueryStringParameter QueryStringField="ProductId" Name="ProductId" Type="Int32" />
      17:         <asp:Parameter Name="Price" DefaultValue="50" Type="Decimal" />
      18:     </UpdateParameters>
      19:     <DeleteParameters>
      20:         <asp:QueryStringParameter QueryStringField="ProductId" Name="ProductId" Type="Int32" />
      21:     </DeleteParameters>
      22: </nh:NHibernateDataSource>

    The LINQ query produced by the NHibernateDataSource is intercepted by the QueryExtender and a where (SearchExpression) and a order by (OrderByExpression) clauses are added to it. Other expressions can be used, inheriting from DataSourceExpression, and some take parameters of type Parameter. Do notice that filtering and sorting is performed server-side, not client-side.

    Of course, this can certainly be improved, let me hear your thoughts and questions.

    And that’s it. Enjoy!

    Read more...

  • Getting the Current User in SharePoint XSLT

    SharePoint offers a couple of ways by which we can obtain the current user for use in a XSLT web part like DataFormWebPart or XsltListViewWebPart. All involve first setting parameters through the ParameterBindings property:

       1: <ParameterBindings>
       2:     <ParameterBinding Name="UserID" Location="CAMLVariable" DefaultValue="CurrentUserName"/>
       3:     <ParameterBinding Name="LogonUser_" Location="WPVariable(_LogonUser_)"/>
       4:     <ParameterBinding Name="LogonUser" Location="ServerVariable(LOGON_USER)"/>
       5:     <ParameterBinding Name="AuthUser" Location="ServerVariable(AUTH_USER)"/>
       6:     <ParameterBinding Name="RemoteUser" Location="ServerVariable(REMOTE_USER)"/>
       7: </ParameterBindings>
       8: <Xsl>
       9:     <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl asp" xmlns:asp="System.Web.UI.WebControls" xmlns:ddwrt2="urn:frontpage:internal" xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime">
      10:         <xsl:output method="html" />
      11:         <xsl:param name="UserID"/>
      12:         <xsl:param name="LogonUser_"/>
      13:         <xsl:param name="LogonUser"/>
      14:         <xsl:param name="AuthUser"/>
      15:         <xsl:param name="RemoteUser"/>
      16:         <xsl:template match="/">
      17:             UserID: <xsl:value-of select="$UserID"/><br/>
      18:             ID: <xsl:value-of select="ddwrt:UserLookup($UserID, 'ID')" /><br/>
      19:             EMail: <xsl:value-of select="ddwrt:UserLookup($UserID, 'EMail')" /><br/>
      20:             Login: <xsl:value-of select="ddwrt:UserLookup($UserID, 'Login')" /><br/>
      21:             LogonUser_: <xsl:value-of select="$LogonUser_"/><br/>
      22:             AuthUser: <xsl:value-of select="$AuthUser"/><br/>
      23:             RemoteUser: <xsl:value-of select="$RemoteUser"/><br/>
      24:         </xsl:template>
      25:     </xsl:stylesheet>
      26: </Xsl>

    Here we see different kinds of parameters:

    • CAMLVariable: built in variables UserID and Today;
    • WPVariable: returns one of the predefined values for _WPID_ (web part client id), _WPQ_ (web part unique id in page),  _WPR_ (web part resources folder full URL),  _WPSRR_ (web part resources folder relative URL), _LogonUser_ (server variable LOGON_USER),  _WebLocaleId_ (current site locale, as in CultureInfo.LCID);
    • ServerVariable: returns one of the HTTP or IIS server- defined variables.


    This will return something like this:

    UserID: Ricardo Peres

    ID: 10

    EMail: ricardoperes@domain.com

    Login: i:0#.w|DOMAIN\ricardoperes

    LogonUser_: 0#.w|DOMAIN\ricardoperes

    AuthUser: 0#.w|DOMAIN\ricardoperes

    RemoteUser: 0#.w|DOMAIN\ricardoperes

    The ddwrt:UserLookup extension function returns a value depending on the second parameter; valid values are Id, Email and Login, and you can easily guess what they are for.

    You can find a reference for LOGON_USER, AUTH_USER, REMOTE_USER and UNMAPPED_REMOTE_USER here. In my development server, I get the same value for all variables.

    By the way, I posted a more advanced solution, which allows access to any profile properties. You can read it here.

    Read more...

  • Access Navigation Nodes in SharePoint XSLT

    SharePoint relies on ASP.NET Site Map Providers for generating navigation links on its default pages. Specifically, the default Web.config file registers a (big!) number of providers, which control different aspects of its navigation:

       1: <siteMap defaultProvider="CurrentNavigation" enabled="true">
       2:   <providers>
       3:     <add name="SPNavigationProvider" type="Microsoft.SharePoint.Navigation.SPNavigationProvider, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
       4:     <add name="SPSiteMapProvider" type="Microsoft.SharePoint.Navigation.SPSiteMapProvider, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
       5:     <add name="SPContentMapProvider" type="Microsoft.SharePoint.Navigation.SPContentMapProvider, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
       6:     <add name="SPXmlContentMapProvider" siteMapFile="_app_bin/layouts.sitemap" type="Microsoft.SharePoint.Navigation.SPXmlContentMapProvider, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
       7:     <add name="AdministrationQuickLaunchProvider" description="QuickLaunch navigation provider for the central administration site" type="Microsoft.Office.Server.Web.AdministrationQuickLaunchProvider, Microsoft.Office.Server.UI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
       8:     <add name="SharedServicesQuickLaunchProvider" description="QuickLaunch navigation provider for shared services administration sites" type="Microsoft.Office.Server.Web.SharedServicesQuickLaunchProvider, Microsoft.Office.Server.UI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
       9:     <add name="GlobalNavSiteMapProvider" description="CMS provider for Global navigation" type="Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" NavigationType="Global" EncodeOutput="true" />
      10:     <add name="CombinedNavSiteMapProvider" description="CMS provider for Combined navigation" type="Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" NavigationType="Combined" EncodeOutput="true" />
      11:     <add name="CurrentNavSiteMapProvider" description="CMS provider for Current navigation" type="Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" NavigationType="Current" EncodeOutput="true" />
      12:     <add name="CurrentNavSiteMapProviderNoEncode" description="CMS provider for Current navigation, no encoding of output" type="Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" NavigationType="Current" EncodeOutput="false" />
      13:     <add name="GlobalNavigation" description="Provider for MOSS Global Navigation" type="Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" NavigationType="Combined" Version="14" />
      14:     <add name="CurrentNavigation" description="Provider for MOSS Current Navigation" type="Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" NavigationType="Current" Version="14" />
      15:     <add name="SiteDirectoryCategoryProvider" description="Site Directory category provider" type="Microsoft.SharePoint.Portal.WebControls.SiteDirectoryCategoryProvider, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      16:     <add name="MySitePersonalQuickLaunchProvider" description="MySite personal quick launch provider" type="Microsoft.SharePoint.Portal.MySitePersonalQuickLaunchProvider, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      17:     <add name="MySiteHostTopNavigationProvider" description="MySite host top navigation provider" type="Microsoft.SharePoint.Portal.MySiteHostTopNavigationProvider, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      18:     <add name="GlobalNavigationSwitchableProvider" description="Provider for MOSS Global Navigation, which maps to GlobalNavigation or GlobalNavigationTaxonomyProvider according to Navigation Settings" type="Microsoft.SharePoint.Publishing.Navigation.SwitchableSiteMapProvider, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" DefaultTargetProviderName="GlobalNavigation" Version="15" />
      19:     <add name="CurrentNavigationSwitchableProvider" description="Provider for MOSS Current Navigation, which maps to GlobalNavigation or CurrentNavigationTaxonomyProvider according to Navigation Settings" type="Microsoft.SharePoint.Publishing.Navigation.SwitchableSiteMapProvider, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" Version="15" DefaultTargetProviderName="CurrentNavigation" />
      20:     <add name="GlobalNavigationTaxonomyProvider" description="Provider for MOSS Global Navigation, driven by the Taxonomy feature" type="Microsoft.SharePoint.Publishing.Navigation.TaxonomySiteMapProvider, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" Version="15" />
      21:     <add name="CurrentNavigationTaxonomyProvider" description="Provider for MOSS Global Navigation, driven by the Taxonomy feature" type="Microsoft.SharePoint.Publishing.Navigation.TaxonomySiteMapProvider, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" Version="15" />
      22:     <add name="MySiteMapProvider" description="MySite provider that returns areas and based on the current user context" type="Microsoft.SharePoint.Portal.MySiteMapProvider, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      23:     <add name="MySiteLeftNavProvider" description="MySite Left Nav provider that returns areas and based on the current user context" type="Microsoft.SharePoint.Portal.MySiteLeftNavProvider, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      24:     <add name="MySiteDocumentStaticProvider" description="MySite Document library provider that returns static links to lists in the site" type="Microsoft.SharePoint.Portal.MySiteDocumentStaticProvider, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      25:     <add name="MySiteSitesPageStaticProvider" description="MySite Document library provider that returns static links to lists on the Sites page" type="Microsoft.SharePoint.Portal.MySiteSitesPageStaticProvider, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      26:     <add name="MySiteSubNavProvider" description="MySite Sub Nav provider that returns areas and based on the current user context" type="Microsoft.SharePoint.Portal.MySiteSubNavProvider, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      27:     <add name="EduTopNavProvider" description="Top Nav Site Map Provider for Education My Site Host" type="Microsoft.Office.Education.WebUI.EduTopNavProvider, Microsoft.Office.Education.WebUI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      28:     <add name="MySiteHostQuickLaunchProvider" description="Quick Launch Site Map Provider for Education My Site Host" type="Microsoft.Office.Education.WebUI.EduQuickLaunchProvider, Microsoft.Office.Education.WebUI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      29:   </providers>
      30: </siteMap>

    It is possible to use a Site Map Data Source (SiteMapDataSource) to feed a DataFormWebPart with navigation information, so that you can play with it using XSLT.

    For example, let’s pick one of the providers, SPNavigationProvider, which is registered, appropriately, as SPNavigationProvider. This one is capable of returning the global navigation (Global Navigation) and the quick links (Current Navigation). In order to select one or the other, we supply a special key to its StartingNodeUrl property:

    • sid:1000: Home Page;
    • sid:1002: Global Navigation;
    • sid:1025: Current Navigation;
    • sid:1040: Search.

    In case you are wondering, these values are defined in the public properties of the SPNavigation class.

    This example will list all attributes of all the nodes:

       1: <WebPartPages:DataFormWebPart runat="server">
       2:     <DataSources>   
       3:         <asp:SiteMapDataSource SiteMapProvider="SPNavigationProvider" ShowStartingNode="false" StartingNodeUrl="sid:1002" runat="server" />   
       4:     </DataSources>
       5:     <Xsl>
       6:         <xsl:stylesheet xmlns:x="http://www.w3.org/2001/XMLSchema" xmlns:d="http://schemas.microsoft.com/sharepoint/dsp" version="1.0" exclude-result-prefixes="xsl msxsl ddwrt" xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime" xmlns:asp="http://schemas.microsoft.com/ASPNET/20" xmlns:__designer="http://schemas.microsoft.com/WebParts/v2/DataView/designer" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:SharePoint="Microsoft.SharePoint.WebControls" xmlns:ddwrt2="urn:frontpage:internal">
       7:             <xsl:output method="html"/>
       8:  
       9:             <xsl:template match="/">
      10:                 <xsl:for-each select="dataRoot">                    
      11:                     <xsl:for-each select="NavigateUIData">
      12:                         <p>
      13:                             Name: <xsl:value-of select="@Name"/>
      14:                             Value: <xsl:value-of select="@Value"/>
      15:                             NavigateUrl: <xsl:value-of select="@NavigateUrl"/>
      16:                             Description: <xsl:value-of select="@Description"/>
      17:                             <xsl:if test="substring(@NavigateUrl, string-length(@NavigateUrl) - string-length('.aspx') + 1) = '.aspx'">
      18:                                 (ASPX)
      19:                             </xsl:if>
      20:                             <xsl:if test="substring(@NavigateUrl, string-length(@NavigateUrl) - string-length('/') + 1) = '/'">
      21:                                 (Subsite)
      22:                             </xsl:if>
      23:                         </p>                                                            
      24:                     </xsl:for-each>
      25:                 </xsl:for-each>
      26:             </xsl:template>                                
      27:         </xsl:stylesheet>
      28:     </Xsl>
      29: </WebPartPages:DataFormWebPart>

    You can see that the SiteMapDataSource returns XML in the form:

       1: <dataRoot>
       2:     <NavigateUIData Name="Some Name" Value="Some Value" NavigateUrl="Some Url" Description="Some Description" />
       3: </dataRoot>

    Attributes are mostly self-explanatory, but Name and Value have the same content and Description always appears to be empty. For subsites, the NavigateUrl attribute will end in a /. Depending on the provider, it is possible to have nested NavigateUIData nodes.

    If you set the ShowStartingNode property to true, you will also get a root node containing the name (for sid:1000 it is Home, for sid:1002 it is SharePoint Top Navigation Bar, for sid:1025 it is Quick Launch and for sid:1040 it is Search) and URL of the current site

    Unfortunately, there isn’t much documentation (read: none) about these providers, except, of course, their methods and properties, so you have to figure out yourself. One thing that can be useful is this XSLT that will give you all the nodes and their attributes:

       1: <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl asp" xmlns:asp="System.Web.UI.WebControls" xmlns:ddwrt2="urn:frontpage:internal">    <xsl:output method="html" />
       2:     <xsl:template match="*">
       3:         <xsl:param name="path" select="''" />
       4:         <xsl:variable name="previous" select="count(preceding-sibling::*[name()=name(current())])" />
       5:         <xsl:variable name="countprevious" select="count(following-sibling::*[name()=name(current())])" />
       6:         <xsl:variable name="fullpath" select="concat($path, '/', name(), substring(concat('[', $previous + 1, ']'), 1 div ($countprevious or $previous)))" />
       7:         <p><xsl:value-of select="$fullpath" /></p>
       8:         <xsl:apply-templates select="@*|*">
       9:             <xsl:with-param name="path" select="$fullpath" />
      10:         </xsl:apply-templates>
      11:     </xsl:template>
      12:     <xsl:template match="@*">
      13:         <xsl:param name="path" select="''" />        
      14:         <xsl:value-of select="concat($path, '/@', name())" />: <xsl:value-of select="." />
      15:         <br/>
      16:     </xsl:template>
      17: </xsl:stylesheet>

    Read more...

  • ASP.NET Web Forms Extensibility: Output Encoders

    Since version 4, ASP.NET offers an extensible mechanism for encoding the output. This is the content that will be returned to the browser. I already refered it in Providers.

    A base class exists, HttpEncoder, for which it used to be the default implementation. It has the responsibility to encode all contents sent to the server, in a differentiated manner – JavaScript is encoded in a way, HTML in another, URLs in another, and so on. Since ASP.NET 4.5, the default implementation is AntiXssEncoder. Those familiar with Anti-Cross Site Scripting Library (now Web Protection Library) will recognize this class, which offers better protection against cross site scripting attacks.

    This class offers a number of virtual methods that you can override to change the default behavior: HeaderNameValueEncode (for encoding headers sent in the response), HtmlAttributeEncode (for tag attributes), HtmlEncode (for generic text content), JavaScriptStringEncode (for JavaScript), UrlEncode (URLs) and UrlPathEncode (URL parts). Do create a dummy encoder and debug through these methods to see what they are called with.

    The actual implementation to use can be configured by code or XML configuration (the Web.config file).

    For using code configuration, one has to change the HttpEncoder.Current in at most the Application_Start event, after that it will be too late:

       1: protected void Application_Start()
       2: {
       3:     HttpEncoder.Current = new MyEncoder();
       4: }

    The default implementation is always available in the read only property HttpEncoder.Default.

    If you prefer to change the Web.config file, you need to set the encoderType attribute of the httpRuntime section:

       1: <httpRuntime encoderType="MyEncoder, MyAssembly"/>

    It is a nice addition, especially together with the validation provider model introduced with ASP.NET 4, which will be the topic of my next post on ASP.NET Web Forms extensibility.

    Read more...

  • Exposing SharePoint Context to XSLT

    Picking on my previous post, I am going to show how you can access SharePoint (and ASP.NET context) from XSLT.

    Create a class like this one:

       1: public class ContextExtensions
       2: {
       3:     public Object HttpContext(String expression)
       4:     {
       5:         return (DataBinder.Eval(System.Web.HttpContext.Current, expression));
       6:     }
       7:  
       8:     public Object SpContext(String expression)
       9:     {
      10:         return (DataBinder.Eval(SPContext.Current, expression));
      11:     }
      12: }

    Follow the process described in my post to register it, apply tag mappings and register the controls as safe. Then, on the XSLT, you can access any properties (and properties of properties) of both HttpContext.Current and SPContext.Current instances! The only thing you can’t do is call methods. This is a limitation of DataBinder.Eval.

    Some examples:

       1: User.Identity.Name: <xsl:value-of select="my:HttpContext('User.Identity.Name')"/>
       2: SPFormContext.FormMode: <xsl:value-of select="my:SpContext('FormContext.FormMode')"/>
       3:  
       4: <xsl:variable name="TasksItemCount" select="my:SpContext('Web.Lists[&quot;Tasks&quot;].ItemCount')"/>
       5: Web.Lists["Tasks"].ItemCount: <xsl:value-of select="$TasksItemCount"/>
       6:  
       7: <xsl:variable name="ListItemName" select="my:SpContext('ListItem[&quot;Name&quot;]')"/>
       8: ListItem[&quot;Name&quot;]: <xsl:value-of select="$TasksItemCount"/>

    Notice that when you are to evaluate expressions with “, you must do it in two steps:

    1. Define a variable that takes the expression;
    2. In that variable, replace “ for &quot.

    Hope you find it useful!

    Read more...

  • Editing a Post in Weblogs.asp.net

    Updated

    I was getting a bit tired of having to go through the Dashboard – Blog and having to locate a specific post which might have been published a long time ago, so I came up with a solution.

    On a blank page on your machine, add the following HTML (do replace the address of your blog and the id, of course):

       1: <a href="javascript:document.location.href = 'https://weblogs.asp.net/ricardoperes/Admin/Blogs/13/Posts/' + document.getElementById('Comments_CommentedOn').value + '/Edit'">Edit this Post</a>

    Then add this to your favorites/bookmarks/whatever. Since I use Chrome, I just dragged it to the bookmarks pane. Now, when you are viewing a post, click on the bookmark and off to edit mode you go!

    This is a feature of Orchard, the blog engine that Weblogs.asp.net runs, which depends on the post having comments enabled. If it doesn't, it will not work. Pay attention that by default, posts will have comments turned off after 90 days, but you can change that in the Dashboard.

    Thanks to Terri Morton (@TerriMorton) from Neudesic for her help in understanding this!

    Read more...