Using 32bit COM objects in 64bit IIS


So we’ve upgraded our web farm to Windows Server 2003 x64, and in the process established IIS to run in native x64 mode.  That part went like butter.

The problem is that portions of our web application rely on some various 3rd party 32bit objects.  Of course, while x64 windows itself can host a 32bit process, a 64bit process can not instantiate a 32bit process.  That is to say, while IIS is running in x64 mode, it can’t load and make calls to a 32bit object.

After contacting the various vendors of our 3rd party objects, we were told that it would be months, years or never for the release of native x64 versions of these objects.  Of course, that just won’t do.  Here is how we were able to resolve the issue and still maintain all of the functionality we had with the 32bit version of our software in 64bit mode:

.Net Remoting to the Rescue

Knowing that windows itself could spawn a 32bit process, and that process would have access to the 32bit objects, the decision was made to create a windows service to host the 32bit objects.  With the windows service responsible for the creation of the objects and calling the methods of those objects, then it was simply a matter of marshalling the calls to the windows servers across the x64 <-> x86 boundary.

x32-x64_Remoting_Overview

What we end up with is actually three separate projects.  We’ve got our original web solution project, we’ve got a new Windows Service project (which is the remoting host) and we’ve got a shared assembly between the two that holds the interface contract (and the result object that’s marshaled back, but we’ll get into that later).

Working from the Server to the Client

All of the samples and examples that I was able to find using Google and MSDN dealt with making calls from the client and executing work on the server, but no real emphasis was given to returning usable and tangible results to the client from the server. One of the things I discovered along the way was exceptions are not accurately marshaled across the machine boundary.  More importantly, if you raise an exception from the remote service, then that service no longer seems to respond to subsequent requests. 

Creating the shared interface & assembly

In looking back at the diagram above, It’s apparent that the client has to know how to talk to the service, and the service needs to know how to talk to the client.  That is to say that when the server calls something an apple, then the client has to know what an apple looks like.  If the client didn’t know what an apple looked like on the server, then it wouldn’t know if it should put it in the orange or lemon basket.

The best way to describe an object between two separate systems is with the usage of a shared interface.   So what we did was to create a separate lightweight assembly to house the common interfaces that both the client and server can consume and use. 

Dealing with our results first

During the process of developing the solution, we found that if we raised an exception from our service, that exception was not marshaled to the client.  Instead a remoting exception was raised.  Another caveat of throwing an exception from the service was that after the exception was called, the service would fail to respond to subsequent requests.  Due to this discovery, it was decided that we would return a common “Result” object from all of our remote method calls. 

   1: /// <summary>
   2: /// provides a common generic interface for all remoting return calls
   3: /// </summary>
   4: /// <typeparam name="T"></typeparam>
   5: public interface IRemoteResult<T>
   6: {
   7:     /// <summary>
   8:     /// Gets a value indicating whether this <see cref="IRemoteResult&lt;T&gt;"/> result is successful or failed.
   9:     /// </summary>
  10:     /// <value><c>true</c> if success; otherwise, <c>false</c>.</value>
  11:     bool Result { get; }
  12:     /// <summary>
  13:     /// Gets the value of the result.
  14:     /// </summary>
  15:     /// <value>The value.</value>
  16:     T Value { get; }
  17:     /// <summary>
  18:     /// Gets the error message.
  19:     /// </summary>
  20:     /// <value>The error message.</value>
  21:     string ErrorMessage { get; }
  22:     /// <summary>
  23:     /// Gets the result exception.
  24:     /// </summary>
  25:     /// <value>The result exception.</value>
  26:     Exception ResultException { get; }
  27: }
  28:  
  29: /// <summary>
  30: /// Remoting return call where return Value is a byte array
  31: /// </summary>
  32: public interface IRemoteResultByteArray : IRemoteResult<byte[]> {}
  33:  
  34: /// <summary>
  35: /// Remoting return call where return Value is a string
  36: /// </summary>
  37: public interface IRemoteResultString : IRemoteResult<string> {}

The IRemoteResult is the base interface for the result object.  It takes a generic type param which is the return type of the data that is consumed by the client from the server.  Notice that I created the IRemoteResultByteArray and IRemoteResultString interfaces based on the IRemoteResult.  This allows me to incorporate a strongly typed result value without having to maintain definitions of each.

Also notice that the “RemotingResult” interface contains a true or false representing the result of the remote call, as well as providing the ability to marshal the exception raised by the remote method internally to the client.  Of course, if the result is true, then the Value will contain the data returned from the remote call.

Now that we have the interface contract for the various Result objects, we can create the concrete implementation of those objects.

   1: [Serializable]
   2: public class RemoteResultByteArray : IRemoteResultByteArray
   3: {
   4:     public bool Result { get; set; }
   5:     public byte[] Value { get; set; }
   6:     public string ErrorMessage { get; set; }
   7:     public Exception ResultException { get; set; }
   8: }
   9:  
  10: [Serializable]
  11: public class RemoteResultString : IRemoteResultString
  12: {
  13:     public bool Result { get; set; }
  14:     public string Value { get; set; }
  15:     public string ErrorMessage { get; set; }
  16:     public Exception ResultException { get; set; }
  17: }

Notice that each of the concrete implementations are marked with the Serializable attribute.  This allows .Net to marshal the result object across the machine boundary.

Now that we know what to expect from the remoting service, let’s talk about the “what to do”

Creating the Shared Service Contract

One of the 32bit components that we’re working with is the ActivePDF Toolkit  It is basically a product that allows us to manipulate PDF documents.  One of the features that we utilize with the toolkit is the ability to take several PDF documents and merge them into one document “on the fly”.

I’m going to show you the code for our actual implementation of the merge process as an example.. but the long and short of it is, the shared service contract is what defines the method calls to your client that your remoting server will process.

   1: public interface IPdfToolkit
   2: {
   3:     /// <summary>
   4:     /// Merges the source PDF Documents and returns the final merge as a byte array
   5:     /// </summary>
   6:     /// <param name="info">The PDFInfo object that describes the final document.</param>
   7:     /// <param name="pdfFiles">The PDF files.</param>
   8:     /// <returns></returns>
   9:     IRemoteResultByteArray MergePDFToArray(PDFInfo info, List<PDFMergeInfo> pdfFiles);
  10:  
  11:     /// <summary>
  12:     /// Merges the source documents into a single PDF and returns the file name / path to the created output document
  13:     /// </summary>
  14:     /// <param name="info">The PDFInfo object that describes the final document.</param>
  15:     /// <param name="pdfFiles">The PDF files.</param>
  16:     /// <returns></returns>
  17:     IRemoteResultString MergePDFToFile(PDFInfo info, List<PDFMergeInfo> pdfFiles);
  18: }

 

So we’ve got a service contract with two methods, one merges a list of pdf’s to a single PDF and returns the result as a byte array, and the other returns a string, which is the path to the resulting file.  PDFInfo and PDFMergeInfo are supporting structures which are also defined in the shared assembly:

   1: [Serializable]
   2: public struct PDFInfo
   3: {
   4:     public string Title;
   5:     public string Subject;
   6:     public string Author;
   7:     public string Keywords;
   8:     public string OutputPath;
   9:     public bool DeleteAfterMerge;
  10:     public bool IncludeSeperatorPage;
  11:     public bool CreateSectionBookmarks;
  12:  
  13:     public PDFInfo(string title, string subject, string author, string keywords, string outputPath, bool deleteAfterMerge, bool includeSeperatorPage, bool createSectionBookmarks)
  14:     {
  15:         Title = title;
  16:         Subject = subject;
  17:         Author = author;
  18:         Keywords = keywords;
  19:         OutputPath = outputPath;
  20:         DeleteAfterMerge = deleteAfterMerge;
  21:         IncludeSeperatorPage = includeSeperatorPage;
  22:         CreateSectionBookmarks = createSectionBookmarks;
  23:     }
  24: }
  25:  
  26: [Serializable]
  27: public struct PDFMergeInfo
  28: {
  29:     public string FileName;
  30:     public string FriendlyName;
  31:     public bool Exists;
  32:     public bool Delete;
  33: }

With this new shared service contract, the client and server can both agree on what work will be performed, and what the results of that work will be.  So let’s move on to the Remoting Service itself…

Creating The Remoting Object (what actually does the work)

In the “Service” project we need to create an object that actually does the work we want it to do.  This is the object that makes the calls into the 32bit components, does the tasks it needs to do, and returns the results that we agreed upon in the shared assembly.  Since we’re working with the PDF Toolkit, I’ll go ahead and show you that…

   1: public class PdfToolkit : MarshalByRefObject, IPdfToolkit
   2: {
   3:     /// <summary>
   4:     /// Merges the source documents into a single PDF and returns the file name / path to the created output document
   5:     /// </summary>
   6:     /// <param name="info">The PDFInfo object that describes the final document.</param>
   7:     /// <param name="pdfFiles">The PDF files.</param>
   8:     /// <returns></returns>
   9:     public IRemoteResultString MergePDFToFile(PDFInfo info, List<PDFMergeInfo> pdfFiles)
  10:     {
  11:         var result = new RemoteResultString {Result = true};
  12:         if (string.IsNullOrEmpty(info.OutputPath))
  13:         {
  14:             result.Result = false;
  15:             result.ErrorMessage = "The OutputPath property can not be empty or null";
  16:             result.ResultException = new ArgumentException(result.ErrorMessage);
  17:             return result;
  18:         }
  19:     
  20:         try
  21:         {
  22:             // [... snip: Code that loops through the supplied list of files and merges them ...]
  23:         }
  24:         catch (Exception ex)
  25:         {
  26:             result.ErrorMessage = ex.Message;
  27:             result.ResultException = ex;
  28:             result.Result = false;
  29:         }
  30:         // finally return our result.. success or fail..
  31:         return result;
  32:     }
  33:     
  34:     /// <summary>
  35:     /// Merges the source PDF Documents and returns the final merge as a memory stream object
  36:     /// </summary>
  37:     /// <param name="info">The PDFInfo object that describes the final document.</param>
  38:     /// <param name="pdfFiles">The PDF files.</param>
  39:     /// <returns></returns>
  40:     public IRemoteResultByteArray MergePDFToArray(PDFInfo info, List<PDFMergeInfo> pdfFiles)
  41:     {
  42:         var result = new RemoteResultByteArray {Result = true};
  43:         var mergeResult = MergePDFToFile(info, pdfFiles);
  44:         if (!mergeResult.Result)
  45:         {
  46:             result.Result = false;
  47:             result.ErrorMessage = mergeResult.ErrorMessage;
  48:             result.ResultException = mergeResult.ResultException;
  49:             return result;
  50:         }
  51:         try
  52:         {
  53:             // read our output document into a stream object
  54:             using (var fs = File.OpenRead(mergeResult.Value))
  55:             {
  56:                 result.Value = new byte[fs.Length];
  57:                 fs.Read(result.Value, 0, (int) fs.Length);
  58:             }
  59:             // Delete our source file object since we're returning a stream
  60:             File.Delete(mergeResult.Value);
  61:         }
  62:         catch (Exception ex)
  63:         {
  64:             result.Result = false;
  65:             result.ErrorMessage = ex.Message;
  66:             result.ResultException = ex;
  67:         }
  68:         // Finally return our stream result object
  69:         return result;
  70:     }
  71: }

Some things to be aware of here.. The PdfToolkit concrete object inherits from MarshalByRefObject and implements the shared contract IPdfToolkit which was defined in the shared assembly.  The inheritance of MarshalByRefObject is what allows our PdfToolkit object to be accessed across application domain boundaries. 

Notice that the return types of each method (as declared by the IPdfToolkit) is an interface and the concrete result of the method is the concrete object defined in the shared assembly.  Since both our client (IIS) and server are aware and make use of the shared assembly, they are able to both call an apple an apple :)

Creating the Remoting Service

A point of definition here… Remoting Service != Windows Service.  It is important to make that distinction.  To define it simply (and probably wrongly) a remoting service is a collection of methods, objects, events, etc.. that are able to be accessed remotely. 

Now a point of order here, a Remoting Service must be hosted within another process.  The host of the remoting service can be any application that is running, or can be instantiated from a remote request.  All of the example code that I’ve found used a simple console application as the host for the remoting service.

Enough background…

The Remoting Service is what’s responsible for creating the channel, and registering the remoting object with the service.  For our needs, I set up a single RemoteSerivce object with a pair of public methods to setup and teardown the service and channels.

   1: public class RemoteService
   2: {
   3:     private const string CHANNELNAME = "x32bitHostServerChannel";
   4:     private const int TCPCHANNELPORT = 9083;
   5:  
   6:     private readonly PdfToolkit _pdfToolkit = new PdfToolkit();
   7:     private readonly TcpServerChannel _channel = new TcpServerChannel(CHANNELNAME, TCPCHANNELPORT);
   8:  
   9:     public void SetupService()
  10:     {
  11:         // Setup a channel for communication
  12:         ChannelServices.RegisterChannel(_channel, false);
  13:  
  14:         // Register our remoting services
  15:         RemotingConfiguration.RegisterWellKnownServiceType(typeof(PdfToolkit), "PdfToolkit", WellKnownObjectMode.Singleton);
  16:         
  17:         // bind our remoting objects with remoting service
  18:         RemotingServices.Marshal(_pdfToolkit, "PdfToolkit");
  19:     }
  20:  
  21:     public void TeardownService()
  22:     {
  23:         if (ChannelServices.GetChannel(CHANNELNAME) != null)
  24:             ChannelServices.UnregisterChannel(_channel);
  25:     }
  26:  
  27: }

While my example only shows one service being registered, in our actual implementation we’ve got several services registered.  The process for each is the same.  Create a private instance of the remoting object, register the service type with a unique name, then set the marshal of the unique name to the private instance.

I could have easily used Ipc as my channel instead of Tcp, but in our instance, we’ve got several web head front ends that all communicate back to our central remoting server.  This allows us to keep all of the remoting stuff on one box instead of having it installed on every one of our web hosts.

Creating the Service Host (Windows Service)

Alright, there are many samples and examples of creating a windows service.. making the installer, etc, etc.. so I won’t go into the nitty gritty of those details.. but I will show you the code for the actual service itself:

   1: public partial class Remoting32bitHost : ServiceBase
   2: {
   3:     private RemoteService _remoteService;
   4:  
   5:     public Remoting32bitHost()
   6:     {
   7:         InitializeComponent();
   8:     }
   9:  
  10:     protected override void OnStart(string[] args)
  11:     {
  12:         EventLog.WriteEntry(ServiceName + " Started", EventLogEntryType.Information);
  13:         _remoteService = new RemoteService();
  14:         _remoteService.SetupService();
  15:         EventLog.WriteEntry(ServiceName + " Channels Opened and Listening");
  16:     }
  17:  
  18:     protected override void OnStop()
  19:     {
  20:         EventLog.WriteEntry(ServiceName + " Tearing down channels");
  21:         _remoteService.TeardownService();
  22:         EventLog.WriteEntry(ServiceName + " Stopped");
  23:     }
  24: }

So, as you can see the actual service is very simple.  Just create a private instance of the RemoteSerivce object and instantiate it during the OnStart() event of the service.  Once it’s instantiated, then we can call the SetupService method.  When the service is stopped.. we call the TeardownService method.

You’re probably thinking that I could have just created the channel, registered the service types, and did the marshal binding right from the OnStart() event of the windows service.  And you’d be absolutely correct.  However, by having it as a separate object, I was able to utilize the RemoteService object from a test bed (windows console) application while I was developing and debugging… this meant I didn’t have to have the windows service actually installed and running on my machine (which of course, is a headache when you want to stop, start, recompile, etc).

Making the client talk to the service

The client is actually the easiest part of the whole process to build.  Though, from trudging through all of the sample code and examples “out in the wild” you’d probably be just as inclined to talk a long walk off a short pier.  That being the case, I’ll cut to the chase; We made a “remote wrapper” in the client application that is responsible for making the calls to the remote object.

   1: /// <summary>
   2: /// The PDFToolkit object is used to create a single merged PDF document from a list of several source pdf documents.
   3: /// It has the ability to create "page seperators" for the stitched together pdfs, as well as internal bookmark links.
   4: /// </summary>
   5: public class PDFToolkit : IPdfToolkit
   6: {
   7:     public IRemoteResultByteArray MergePDFToArray(PDFInfo info, List<PDFMergeInfo> pdfFiles)
   8:     {
   9:         IRemoteResultByteArray result = new RemoteResultByteArray { Result = false, ErrorMessage = "Unable to RPC to RemoteHost" };
  10:         // get the uri of the remote object we want to call
  11:         var uri = Common.GetRemotingUri("PdfToolkit");
  12:         // activate and get our remote object
  13:         var proxy = (IPdfToolkit)Activator.GetObject(typeof(IPdfToolkit), uri);
  14:         // build our result object
  15:         if (proxy != null)
  16:             result = proxy.MergePDFToArray(info, pdfFiles);
  17:         return result;
  18:     }
  19:  
  20:     public IRemoteResultString MergePDFToFile(PDFInfo info, List<PDFMergeInfo> pdfFiles)
  21:     {
  22:         IRemoteResultString result = new RemoteResultString{Result = false, ErrorMessage = "Unable to RPC to RemoteHost"};
  23:         // get the uri of the remote object we want to call
  24:         var uri = Common.GetRemotingUri("PdfToolkit");
  25:         // activate and get our remote object
  26:         var proxy = (IPdfToolkit)Activator.GetObject(typeof(IPdfToolkit), uri);
  27:         // build our result object
  28:         if (proxy != null)
  29:             result = proxy.MergePDFToFile(info, pdfFiles);
  30:         return result;
  31:     }
  32: }

It truly is as simple as that.  Use Activator.GetObject to create an instance of the type of remote object that you want using the proper Uri (I’ve got a helper method in our common library to give me the proper uri based on a remote type) then call the method you want.

The Big Gottcha!

In all of the samples and examples that I’ve found… for the client portion of the remoting solution, they all tell you that you have to create a channel on the client.  That just simply isn’t the case.  When you make a call to Activator.GetObject, it will create and register a channel for you if one is not already available.  We ran into situation where we tried to do our own channel management on the client side where the code would work well for a period of time (from 20 minutes to hours), then all of the sudden start failing.  As soon as we took the channel management code out of the client portion, we’ve not had a problem since.

Where’s the code?

I generally try to provide a code download with each of my articles.  Unfortunately with this type of article, your usage is going to probably be very different than mine.  Since so much of the code is dependant on 3rd party interfaces, there’s just no way that I could (given the time allotted for my day) put together a meaningful sample. 

Hopefully the snippets that I’ve provided are enough to help you through the hurdles of interfacing your legacy 32bit objects within an x64 architecture.  If you’ve got any questions, let me know.  I’ll do what I can to answer them.

author: Jason Monroe | posted @ Tuesday, June 16, 2009 5:06 PM | Feedback (0)

ASP.Net SQL Session State Gottcha – Serialization


Had a situation come up today when we were taking one of our sites SessionState management out of InProc to SqlServer.

The constructor to deserialize an object of type <type> was not found

Any custom object that you have created that you want to stuff into session, when using an out of process session management scheme (such as state server, sql server or your own custom implementation) needs to be decorated with the [Serializable] attribute.

After looking over our various data objects, I made sure that each one was properly decorated.  Since the majority of our data objects are rather simple in nature and contain intrinsic data types, the serializable attribute was all that was needed.

However, we have some core data objects that derive their implementation from DataSet.  Those were the ones with the problem…

   1: [Serializable]
   2: public class MyDataSet : DataSet 
   3: {
   4:     public int ParentRecordId { get; set; }
   5:     public string ConnectionString { get; set; }
   6:     
   7:     public MyDataSet(string connectionString, int parentRecordId) 
   8:     {
   9:         ConnectionString = connectionString;
  10:         ParentRecordId = parentRecordId;
  11:     }
  12: }

 

It was assumed that since DataSet implements ISerializable, then MyDataSet would be alright “out of the box”.  We were equally sure that MyDataSet wouldn’t have to do anything special since it was decorated with the serializable attribute and the custom properties were basic intrinsic types.

… And that’s where we were wrong

It seems that even though DataSet implements ISerializable, we need to have the seralizable constructor in MyDataSet as well.

   1: [Serializable]
   2: public class MyDataSet : DataSet 
   3: {
   4:     public int ParentRecordId { get; set; }
   5:     public string ConnectionString { get; set; }
   6:     
   7:     public MyDataSet(string connectionString, int parentRecordId) 
   8:     {
   9:         ConnectionString = connectionString;
  10:         ParentRecordId = parentRecordId;
  11:     }
  12:     
  13:     protected MyDataSet(SerializationInfo info, StreamingContext context) : base(info, context) { }
  14: }

We made the constructor protected because we don’t want it being called from anywhere but the .Net framework. 

So now the underlying DataSet object of MyDataSet is being serialized and deserialized as expected…  What we discovered was that now, ConnectionString and ParentRecordId was not being properly deserialized any longer.

That being the case, we had to roll up our own serialization utilizing the SerializationInfo object and overriding the GetObjectData() method in the base DataSet object.  Finally, we end up with our class looking like so:

   1: [Serializable]
   2: public class MyDataSet : DataSet 
   3: {
   4:     struct SerializationTags 
   5:     {
   6:         public const string ConnectionString = "MyDataSet_ConnectionString";
   7:         public const string ParentRecordId = "MyDataSet_ParentRecordId";
   8:     }
   9:  
  10:     public int ParentRecordId { get; set; }
  11:     public string ConnectionString { get; set; }
  12:     
  13:     public MyDataSet(string connectionString, int parentRecordId) 
  14:     {
  15:         ConnectionString = connectionString;
  16:         ParentRecordId = parentRecordId;
  17:     }
  18:     
  19:     protected MyDataSet(SerializationInfo info, StreamingContext context) : base(info, context) 
  20:     { 
  21:         ConnectionString = info.GetString(SerializationTags.ConnectionString);
  22:         ParentRecordId = info.GetInt32(SerializationTags.ParentRecordId);
  23:     }
  24:  
  25:     [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]    
  26:     public override void GetObjectData(SerializationInfo info, StreamingContext context)
  27:     {
  28:         base.GetObjectData(info, context);
  29:         info.AddValue(SerializationTags.ConnectionString, ConnectionString);
  30:         info.AddValue(SerializationTags.ParentRecordId, ParentRecordId);
  31:     }
  32:  
  33: }

Hope this helps someone save a headache or two :)

author: Jason Monroe | posted @ Thursday, May 21, 2009 2:51 PM | Feedback (0)

Disillusionment is never a fun time


I’ve always been a huge fan of Line6 and their acoustic modeling technology.  In my not so humble opinion, they have some of the most outstanding offerings in not only amplifier and cabinet modeling technologies, but in instrument modeling technologies as well.

I’ve played through and auditioned a number of their amplifiers from the simple Spider to the full on Vetta and loved the sounds they were able to produce.

I even use the Line6 GuitarPort as my direct PC interface for recording software.

GuitarPort

I truly *LOVE* the ease and simplicity of that little red box for getting rock solid and truly stellar tones down on a track without having to deal with hundreds of pounds and thousands of dollars worth of equipment, miles and miles of cables and enough patches and interfaces to make the pope swear.

The gear box software has a lot of cool features for a learning guitarist that I don’t use.. but the online community and ability to download tone patches from the online section truly rocks.

Having said all of that.. I don’t gig with my guitar port.  It’s tied to my computer where I can sit down and “fiddle” with it to get the tones I need.. spend time to iron everything out just … right.  The Gearbox software for the GuitarPort is like the ocean.. very accessible and easy to swim in.. but terrifyingly deep and equally easy to get lost in.  It’s not designed to be taken on the road.

But they have road gear!

Yes, Line6 offers not only studio gear.. but road gear as well.  Probably most notably is their POD line of equipment.

A buddy of mine happens to have a PODxt Live and is getting ready for a gig this weekend.  So he brought over his pod and we loaded it up to my studio and worked on getting some new patches into some of the unused user banks that he had free.

PODxtLive

So we plug it up and start tweaking some tones and patches in GearBox and saving them down to the POD.  We test the tones against his telecaster, my strat and my duel humbucker warlock.  We feed the output of the POD to the little Fender Bandit amp and test it through the mixing board to the PA.. each individual tone themselves sound golden.

Then frustration sets in

Hours of fun in setting up individual tone patches led to hours of frustration in getting those tone patches to work together. 

The PODxt, in all of it’s smarts and brains and technology, does not have a “master level” control for all of it’s patches to the output.  Basically speaking, there’s no way to easily equalize the output of the various patches so that switching from one tone to the other doesn’t require you to touch the board with your hands.

For example, we built a clean tone with a hint of chorus and delay adjusting the levels of the amp, mic, effects and cabinets to get just the tone that we wanted.  Then we built a gritty tone that had some drive and classic distortion with some reverb.  To get the tone that we wanted from that patch we made the patch a bit “hotter”…

So when we play the clean tone at amp level everything sounds awesome!  Then tap the bank switch to flip to the gritty tone, and the ears start to bleed.  We have to reach down and turn down the channel volume on the POD to make the amp level the same as what we had with the clean tone.

It seems to me, that since these tones are being produced through digital signal processing, that there should be some sort of overall output signal adjustment to level all of the patches to be the same.  If you want more volume from a patch.. well hell, that’s what the volume peddle is for right?

This seems to be a common complaint

In our efforts to resolve this problem, we did some research and digging around.  The common consensus is to “take the quietest patch and beef it up, then adjust the levels on your louder patches to bring them down”.  But in doing that, you’re changing the tone!

Some people are even going so far as to buy SPL meters and adjusting patch levels by what’s coming out of the amp!  That’s great… as long as you never move your amp or change venue’s or plug into a different amp or PA <eyeroll>.

Closing Thoughts…

In all fairness, I’ve not yet auditioned the POD x3 or the POD x3 Live so this problem may have been addressed with those products.  However, the x3 version uses the same GearBox and Monkey software that the GuitarPort and PODxt uses, so I’m doubtful that is truly the case.

I say unto you Line6, you guys make awesome studio gear and give us great tones.. But please don’t forget that there are gigging musicians out there who are having troubles with your gear and want to bring those same high quality patches with them out of the studio onto the road!

If anyone of you out there knows how to properly set the patch levels across multiple tones, please don’t hesitate to let us know.  ‘Cause obviously, we’re to stupid to figure it out :(

Technorati Tags: ,,

author: Jason Monroe | posted @ Wednesday, May 20, 2009 12:53 PM | Feedback (1)

Coding Tips: Struct Abuse - (or, Why didn’t I think of that?)


One of my personal pet peeves in development is the use of “Magic Numbers”in code.  I’m even so retentive to include string literals in my disdain of unmanageable code.  This especially becomes prevalent in data access code.

My nightmare

Handing me a piece of code that looks like this:

public class Foo
{
public string Bar { get; set; }
public string Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string CreatedOn { get; set; }
public string CreatedBy { get; set; }
}

public class FooManager
{
public Foo GetFooById(string Id)
{
var retFoo = new Foo();
var param = new SqlParameter("Foo_Id", SqlDbType.VarChar, 25);
param.Value = Id;

using (var conn = new SqlConnection("Data Source=FooServer;Initial Catalog=FooDb;User Id=FooUser;Password=FooPassword"))
{
using (var cmd = new SqlCommand("FOO_FETCH_BY_ID", conn))
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(param);
var rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
if (rdr != null)
{
rdr.Read();
retFoo.Bar = rdr["Foo_Bar"].ToString();
retFoo.Id = rdr["Foo_Id"].ToString();
retFoo.Description = rdr["Foo_Description"].ToString();
retFoo.Title = rdr["Foo_Title"].ToString();
retFoo.CreatedOn = rdr["Foo_CreatedOn"].ToString();
retFoo.CreatedBy = rdr["Foo_CreatedBy"].ToString();
}
}
} return retFoo;
}
}

is one of the quickest ways to fail a code review.

The code is littered with string literals.. and a blasted magic number!  (I know that the connection string is going overboard in this example.. but it’s there because I’ve seen it before)

Let’s do something a bit different:

public class Foo
{
public struct DBFields
{
public const string Bar = "Foo_Bar";
public const string Id = "Foo_Id";
public const string Title = "Foo_Title";
public const string Description = "Foo_Description";
public const string CreatedOn = "Foo_CreatedOn";
public const string CreatedBy = "Foo_CreatedBy";
}

public struct DBLengths
{
public const int Bar = 35;
public const int Id = 25;
public const int Title = 65;
public const int Description = 500;
public const int CreatedOn = 12;
public const int CreatedBy = 20;
}

public string Bar { get; set; }
public string Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string CreatedOn { get; set; }
public string CreatedBy { get; set; }
}

public class FooManager
{
private const string DBSTRING = "Data Source=FooServer;Initial Catalog=FooDb;User Id=FooUser;Password=FooPassword";

private struct Procs
{
public const string FooById = "FOO_FETCH_BY_ID";
public const string FooByBar = "FOO_FETCH_BY_BAR";
public const string AllFooByCreator = "FOO_FETCH_ALL_BY_CREATOR";
}

public Foo GetFooById(string Id)
{
var retFoo = new Foo();
var param = new SqlParameter(Foo.DBFields.Id, SqlDbType.VarChar, Foo.DBLengths.Id);
param.Value = Id;

using (var conn = new SqlConnection(DBSTRING))
{
using (var cmd = new SqlCommand(Procs.FooById, conn))
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(param);
var rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
if (rdr != null)
{
rdr.Read();

retFoo.Bar = rdr[Foo.DBLengths.Bar].ToString();
retFoo.Id = rdr[Foo.DBFields.Id].ToString();
retFoo.Description = rdr[Foo.DBFields.Description].ToString();
retFoo.Title = rdr[Foo.DBFields.Title].ToString();
retFoo.CreatedOn = rdr[Foo.DBFields.CreatedOn].ToString();
retFoo.CreatedBy = rdr[Foo.DBFields.CreatedBy].ToString();
}
}
}
return retFoo;
}
}

Wait.. What?!?

Alright, here’s what we did.. First we created a struct in the Foo object called DBFields.  This represents of the Foo Fields as they are so named in the DB.  Notice that it’s a struct of string constants.

The next thing we did was to create a struct of DBLengths in the Foo object.  This represents the length definition of all the fields in the Foo object as they correlate to the DB Definition.  Notice that it’s a struct of int constants.

In the FooManager, we created another struct called Procs.  This represents the name of each stored procedure that the FooManager users as it interacts with the DB in reference to the Foo object.

Most importantly, in the GetFooById() method.. there are no “Magic Numbers”  All that information is handled in one spot. 

You want to expand the Foo.Description field to 1000 instead of 500?  Modify the Foo.DBLengths.Description and the change will occure in every piece of code that uses the description length.  The Foo.Id property isn’t long enough?  Expand it and modify it in one place.

The stored procedure Foo_FETCH_By_ID is being re-written and the new name is FOO_FETCH_BY_ID2 and now you want to reference it by the new name..  Instead of search and replace, just modify the FooManager.Procs.GetFooById field and you’re good to go.

So what are you saying?

Magic Numbers are Bad.  Don’t use them.

author: Jason Monroe | posted @ Tuesday, May 12, 2009 8:26 AM | Feedback (2)

SQL Trickery: Rank and file your data with CTE, Rank, Case and Partition


Often as the case may be where you get a request from one of “those” people in the glass offices who like to wear suits and ties, take power lunches and talk with their hands for a particular set of data that just seems.. odd.

But being as the situation often is, “those” people control your pay checks, so you feel obligated to oblige their whims…

Let’s say you have a table in your data base that holds commission rates for sales people, and the dates those commissions are effective on.  It might look something like this:

   1: CREATE TABLE [dbo].[Commission_Rates](
   2:     [Rate] [numeric](18, 2) NOT NULL,
   3:     [Effective_date] [date] NOT NULL
   4: )

It might even have some data in it that looks something like this:

captured_Image.png

Now the suit comes up to you and says “Hey programmer Dude!  I’ve got a meeting with our investors in 30 minutes.. you have GOT to give me the most current commission rate as of ‘5/5/2009’ or we’re going to lose a ton of cash next quarter!”

Being the demi-god like person you are.. you say sure and whip up a quick query to get that data for him:

   1: SELECT TOP 1 RATE, EFFECTIVE_DATE 
   2: FROM COMMISSION_RATES 
   3: WHERE EFFECTIVE_DATE < '5/5/2009' 
   4: ORDER BY EFFECTIVE_DATE DESC
   5:  
   6: RATE                                    EFFECTIVE_DATE
   7: --------------------------------------- --------------
   8: 5.80                                    2009-05-01
   9:  
  10: (1 row(s) affected)
  11:  

Now the suit is all happy, but comes back in 5 minutes time and says “Hey programmer Dude!  You’re totally awesome.. but can you give me all of the upcoming rates after ‘5/5/2009’?”

Still not breaking a sweat, you come up with:

   1: SELECT RATE, EFFECTIVE_DATE
   2: FROM COMMISSION_RATES
   3: WHERE EFFECTIVE_DATE > '5/5/2009'
   4: ORDER BY EFFECTIVE_DATE ASC
   5:  
   6: RATE                                    EFFECTIVE_DATE
   7: --------------------------------------- --------------
   8: 5.40                                    2009-06-01
   9: 5.80                                    2009-12-01
  10:  
  11: (2 row(s) affected)

Yet again the suit is singing your IT praises for saving his bacon.  All of the sudden, it happens…  15 minutes into the meeting with the investors, you get a panic stricken phone call from the suit saying that all of these little bits of data are to confusing!  They need a single list of the most current rate, and all of the up coming rates at the same time!

Wonderful.  Pressure.  Resisting the urge to tell the suit something about improper planning on their part doesn’t constitute an emergency on your part, you set forth to give them what they want…  You might try something like this to start:

   1: SELECT TOP 1 RATE, EFFECTIVE_DATE 
   2: FROM COMMISSION_RATES 
   3: WHERE EFFECTIVE_DATE < '5/5/2009' 
   4: ORDER BY EFFECTIVE_DATE DESC
   5:  
   6: UNION ALL
   7:  
   8: SELECT RATE, EFFECTIVE_DATE
   9: FROM COMMISSION_RATES
  10: WHERE EFFECTIVE_DATE > '5/5/2009'
  11: ORDER BY EFFECTIVE_DATE ASC

It seems logical enough.. take the first query and union it with the second query.  After all, the result definition from the two queries are the same.

The problem here in is the fact that you can’t have an ORDER BY clause within the construct of a UNION statement.

Rethink the problem

Looking back on the first two visits to our humble cubicle dwelling, the suit actually made a request for two different sets of data.  The first set of data was for all of the rates that were prior to ‘5/5/2009’ (let’s ignore the fact that we did a TOP 1 filter and order by for now) and the second set of data was for all the rates that were after ‘5/5/2009’.

We can use a CASE statement to “virtually separate” our data into two lists.

   1: SELECT RATE
   2:     , EFFECTIVE_DATE
   3:     , CASE WHEN EFFECTIVE_DATE < '5/5/2009' THEN 0
   4:         ELSE 1
   5:     END AS LIST
   6: FROM COMMISSION_RATES
   7:  
   8: RATE                                    EFFECTIVE_DATE LIST
   9: --------------------------------------- -------------- -----------
  10: 5.60                                    2009-02-02     0
  11: 5.80                                    2009-05-01     0
  12: 5.40                                    2009-06-01     1
  13: 5.80                                    2009-12-01     1
  14: 6.00                                    2009-03-15     0
  15:  
  16: (5 row(s) affected)

So, now we have our data that can be identified by the effective_date being before or after the search date of ‘5/5/2009’

The next thing we need to do is sort our two lists because we want the “most current” rate prior to the search date.  That means that we need to order the results from each list by the effective_date in descending fashion.

In order to achieve our desired results, we need to make use of the RANK() function and using PARTITION BY to partition our rankings the same way that we’ve sorted our list by the case statement above:

   1: SELECT RATE
   2:     , EFFECTIVE_DATE
   3:     , CASE WHEN EFFECTIVE_DATE < '5/5/2009' THEN 0
   4:         ELSE 1
   5:     END AS LIST
   6:     , RANK() OVER (PARTITION BY CASE WHEN EFFECTIVE_DATE < '5/5/2009' THEN 0
   7:                                     ELSE 1
   8:                                 END
   9:                     ORDER BY EFFECTIVE_DATE DESC) AS SORT
  10: FROM COMMISSION_RATES
  11:  
  12:  
  13: RATE                                    EFFECTIVE_DATE LIST        SORT
  14: --------------------------------------- -------------- ----------- --------------------
  15: 5.80                                    2009-05-01     0           1
  16: 6.00                                    2009-03-15     0           2
  17: 5.60                                    2009-02-02     0           3
  18: 5.80                                    2009-12-01     1           1
  19: 5.40                                    2009-06-01     1           2
  20:  
  21: (5 row(s) affected)
  22:  

As you can see, we’ve now successfully split our data into two lists, and we also have a nice sorting order to our data as well.

At this point, I would take our query and wrap it up into a Common Table Expression (CTE) so that I can use it in another query.  Think of a CTE as a temporary view created in your query (there are a few other rules to them but that’s the jist of it all).

   1: WITH CTE_RATES (RATE, EFFECTIVE_DATE, LIST, SORT)
   2: AS (
   3:     SELECT RATE
   4:         , EFFECTIVE_DATE
   5:         , CASE WHEN EFFECTIVE_DATE < '5/5/2009' THEN 0
   6:             ELSE 1
   7:         END AS LIST
   8:         , RANK() OVER (PARTITION BY CASE WHEN EFFECTIVE_DATE < '5/5/2009' THEN 0
   9:                                         ELSE 1
  10:                                     END
  11:                         ORDER BY EFFECTIVE_DATE DESC) AS SORT
  12:     FROM COMMISSION_RATES
  13: )

Now I can reference CTE_RATES just as I would any other table or view in my system.  Let’s start by getting the most current “previous” rate:

   1: SELECT RATE, EFFECTIVE_DATE
   2: FROM CTE_RATES
   3: WHERE LIST = 0
   4:     AND SORT = 1
   5:  
   6: RATE                                    EFFECTIVE_DATE
   7: --------------------------------------- --------------
   8: 5.80                                    2009-05-01
   9:  
  10: (1 row(s) affected)

Sure enough, that is the most current rate prior to our search date (and exactly the same results as we gave the suit the first time)

Now let’s get all of our “current” rates after the search date (which happen to be list = 1)

   1: SELECT RATE, EFFECTIVE_DATE
   2: FROM CTE_RATES
   3: WHERE LIST = 1
   4:  
   5: RATE                                    EFFECTIVE_DATE
   6: --------------------------------------- --------------
   7: 5.40                                    2009-06-01
   8: 5.80                                    2009-12-01
   9:  
  10: (2 row(s) affected)

Yet again we’ve been able to duplicate the same results that we did earlier. 

Let’s try our UNION once again with these two queries and our CTE:

   1: SELECT RATE, EFFECTIVE_DATE
   2: FROM CTE_RATES
   3: WHERE LIST = 0
   4:     AND SORT = 1
   5:  
   6: UNION ALL
   7:  
   8: SELECT RATE, EFFECTIVE_DATE
   9: FROM CTE_RATES
  10: WHERE LIST = 1
  11:  
  12: RATE                                    EFFECTIVE_DATE
  13: --------------------------------------- --------------
  14: 5.80                                    2009-05-01
  15: 5.40                                    2009-06-01
  16: 5.80                                    2009-12-01
  17:  
  18: (3 row(s) affected)

We are nearly there… They want the results in descending order based in effective_date.  That is simply a matter of wrapping up the UNION query as a sub query to another query to apply the ORDER BY clause to. 

Complete Script:

   1: WITH CTE_RATES (RATE, EFFECTIVE_DATE, LIST, SORT)
   2: AS (
   3:     SELECT RATE
   4:         , EFFECTIVE_DATE
   5:         , CASE WHEN EFFECTIVE_DATE < '5/5/2009' THEN 0
   6:             ELSE 1
   7:         END AS LIST
   8:         , RANK() OVER (PARTITION BY CASE WHEN EFFECTIVE_DATE < '5/5/2009' THEN 0
   9:                                         ELSE 1
  10:                                     END
  11:                         ORDER BY EFFECTIVE_DATE DESC) AS SORT
  12:     FROM COMMISSION_RATES
  13: )
  14:  
  15: SELECT RATE, EFFECTIVE_DATE
  16: FROM (
  17:         SELECT RATE, EFFECTIVE_DATE
  18:         FROM CTE_RATES
  19:         WHERE LIST = 0
  20:             AND SORT = 1
  21:  
  22:         UNION ALL
  23:  
  24:         SELECT RATE, EFFECTIVE_DATE
  25:         FROM CTE_RATES
  26:         WHERE LIST = 1
  27:     ) AS QRY
  28: ORDER BY EFFECTIVE_DATE DESC

Yielding us the results of:

   1: RATE                                    EFFECTIVE_DATE
   2: --------------------------------------- --------------
   3: 5.80                                    2009-12-01
   4: 5.40                                    2009-06-01
   5: 5.80                                    2009-05-01
   6:  
   7: (3 row(s) affected)

After the meeting is over with the investors, the suit comes down to thank you for being so quick and responsive with getting them the information that they need.  The suit even goes so far as to tell you that he owes you a free lunch (which oddly enough, you never seem to be able to collect on).  You kick back and relax for the rest of the day safe and secure in the knowledge that your MAD HAXOR SQL SKILLZ has once again saved the day.

Technorati Tags: ,,,

author: Jason Monroe | posted @ Saturday, May 09, 2009 4:46 AM | Feedback (0)

Organize your views in ASP.Net MVC


I’ve been putting off working with MVC until three days ago.. what a fool I have been!  Long story short, it has made (for me at least) web development fun again.  The more I work with the MVC Framework, the more tickled I get.

As I continue down the path of discovery and self-education by re-creating a personal project site, I get to the administration section of things… Typically, it has been my “convention” for the last 15 years or so that all of my administration related bits go into a ~/Admin path.  Each individual administration functional group would fall into a sub folder under that.

  • ~/Admin/User – User Management
  • ~/Admin/News – News Management
  • ~/Admin/Gallery – Gallery Management

Following this structure allowed me to keep each functional piece separate, but all under the same “Admin” umbrella.

I was grinning ear to ear when I discovered that MVC would allow me to organize my controllers how I saw fit, so I put all of my administration controllers under the ~/Controllers/Admin folder

captured_Image.png

Once that was done, I set forth the task of setting up a few simple routes in the Global.asax.cs file.  I wanted my URL’s to all follow under the http://<site>/Admin/<function> design.  For example:

  • http://www.mysite.com/Admin/User
  • http://www.mysite.com/Admin/User/Edit/312
  • http://www.mysite.com/Admin/News/Create

instead of:

  • http://www.mysite.com/UserAdmin
  • http://www.mysite.com/UserAdmin/Edit/312
  • http://www.mysite.com/NewsAdmin/Create

I found that (at least in my case) it’s best to define routes from the bottom up.  In other words, write the most generic route map first, then with each route that I “fine grain” the control of the URL on, add that above the previous one.  This is what my routes looked like when I was done with the admin section.

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"UserAdmin",
"Admin/User/{action}/{id}",
new { controller = "UserAdmin", action = "Index", id = "" }
);

routes.MapRoute(
"NewsAdmin",
"Admin/News/{action}/{id}",
new { controller = "NewsAdmin", action = "Index", id = "" }
);

routes.MapRoute(
"GalleryAdmin",
"Admin/Gallery/{action}/{id}",
new { controller = "GalleryAdmin", action = "Index", id = "" }
);

routes.MapRoute(
"BlogAdmin",
"Admin/Blog/{action}/{id}",
new { controller = "BlogAdmin", action = "Index", id = "" }
);

routes.MapRoute( // Controller for the Admin Home Page
"AdminHome",
"Admin/{action}/{id}",
new { controller = "AdminHome", action = "Index", id = "" }
);

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);

}

So everything is awesome at this point.  The music is cranked, birds are chirping and the clouds of web development are parting… so I start to create my views for the admin section

~/Views/AdminHome/Index.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">Admin Home</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Admin Home</h2>
<ul>
<li><a href="/Admin/User">User Admin</a></li>
<li><a href="/Admin/News">News Admin</a></li>
<li><a href="/Admin/Gallery">Gallery Admin</a></li>
<li><a href="/Admin/Blog">Blog Admin</a></li>
</ul>
</asp:Content>

 

~/Views/UserAdmin/Index.aspx (all of the others are virtually the same.. this is a simple demo)

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">User Admin</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>User Admin</h2>
<a href="/Admin">Back to Admin Home</a>
</asp:Content>

 

and this is what my ~/Views folder turned out to be:

captured_Image.png[4]

Not quite the organizational utopia that I was hoping for.  So I figure, what the heck, MVC was cool enough to let me put my controllers in their own folder.. let’s see what happens when I do what I want with my views.  So after some re-arranging, I was left with this:

captured_Image.png[7]

There!  Like a nice, comfortable pair of shoes… it just feels right!  So let’s fire it up and look at our magic!

captured_Image.png[9]

And clicking on “Admin” gives us this:

captured_Image.png[11]

Well bollocks to that!  It seems that the ViewResult is still looking at the “conventional” ~/Views/Controller location for the Index.aspx file.  That just won’t do.

ASP.Net MVC is still fairly “young” as far as platforms go.. so the documentation (as expected) is sparse at best and terse when you can find it.. but I did happen to stumble upon a this post: ASP.NET MVC Tip #24 – Retrieve Views from Different Folders by Stephen Walther

Of course, he warns his readers that this breaks all convention with ASP.Net MVC:

Now, I want to be the first to warn you that you should never, never, never use this tip (Please delete this entry from your news aggregator immediately). There is a good reason for following the conventions inherent in an MVC application. Placing your files in known locations makes it easier for everyone to understand your application.

So I ask myself: “Self, Are you sure that you want to stick to your old ways and organize the administration views under the ~/Views/Admin/<function> scheme?”.  To whit I readily replied to myself: “Why yes, yes I most certainly do!”

So I came up with a set of known facts and assumptions:

  1. All administration views should reside within the ~/Views/Admin/<function> structure
  2. I can specify a path to a view by passing it in as the viewName parameter to the View() method of the controller
  3. MVC doesn’t require viewName to be passed in to the View() method.
  4. When viewName is not supplied, it is inferred by the action context of RouteData
  5. I wish to follow MVC Convention for the rest of the site with only /Admin being the edge case
  6. I don’t want to roll my own controller factory for this single edge case.
  7. Up until this point, I’ve been focusing on full views (.aspx) but I also need to take partials into account (.ascx)
  8. I don’t want to have to modify my admin controllers to make this happen
  9. The MVC Framework should be unaffected by this edge case

Knowing these facts, I dug into MVC with reflector (I know I can download the source, but I like the way reflector let’s me navigate) and came up with this little gem inside the abstract class ViewResultBase

public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (string.IsNullOrEmpty(this.ViewName))
{
this.ViewName = context.RouteData.GetRequiredString("action");
}
[ ... snip for brevity ... ]
}

So now we know how the viewName is inferred when not passed into the View() method of the controller… And since Controller is defined as an abstract, I realized that what I really needed to do was create a base class for my admin controllers to inherit from…

using System.Web.Mvc;

namespace MvcApplication1.Controllers.Admin
{
public abstract class AdminBaseController : Controller
{
private readonly string _viewRoot = "~/Views/Admin/";

protected AdminBaseController(string subPath)
{
_viewRoot += subPath + "/";
}

protected internal new virtual ViewResult View()
{
return base.View(ViewPath(RouteData.GetRequiredString("action")));
}

protected internal new virtual ViewResult View(object model)
{
return base.View(ViewPath(RouteData.GetRequiredString("action")), model);
}

protected internal new virtual ViewResult View(string viewName)
{
return base.View(ViewPath(viewName));
}

protected internal new virtual ViewResult View(string viewName, object model)
{
return base.View(ViewPath(viewName), model);
}

protected internal new virtual ViewResult View(string viewName, string masterName)
{
return base.View(ViewPath(viewName), masterName);
}

protected internal new virtual ViewResult View(string viewName, string masterName, object model)
{
return base.View(ViewPath(viewName), masterName, model);
}

protected internal new PartialViewResult PartialView()
{
return base.PartialView(ViewPath(RouteData.GetRequiredString("action"), true), null);
}

protected internal new PartialViewResult PartialView(object model)
{
return base.PartialView(ViewPath(RouteData.GetRequiredString("action"), true), model);
}

protected internal new PartialViewResult PartialView(string viewName)
{
return base.PartialView(ViewPath(viewName, true), null);
}

protected internal new virtual PartialViewResult PartialView(string viewName, object model)
{
return base.PartialView(ViewPath(viewName, true), model);
}

private string ViewPath(string viewName)
{
return ViewPath(viewName, false);
}

private string ViewPath(string viewName, bool partial)
{
if (partial)
return _viewRoot + viewName + ".ascx";
return _viewRoot + viewName + ".aspx";
}
}
}

So what we’ve done is created our base class for our admin controllers that takes a subPath as part of the constructor and builds out the root path to the views that the controller has.  Then we created a few small helper methods called ViewPath that appends the viewName to the root path and adds our extension based on if the request is for a partial view or not.

The magic all happens because the base class hides each iteration of the controller’s View() methods replacing them with it’s own implementation.  You can see that in the empty View() method, we are getting the action name from the RouteData, getting our translated path location with ViewPath and armed with that information, we can now call into the base.View() method passing our fully qualified viewName as a parameter.

The implementation of this base class in our admin controllers is very painless:

namespace MvcApplication1.Controllers.Admin
{
public class UserAdminController : AdminBaseController
{
public UserAdminController() : base("User") { }

//
// GET: /UserAdmin/

public ActionResult Index()
{
return View();
}

}
}

As you can see, all we had to do is change where UserAdminController is inheriting from (Controller to AdminBaseController) and add the public parameter less constructor passing in our “subPath” into the AdminBaseController’s constructor.  None of the other code in the controller has to change.

With those changes put it to place it’s time to run our application and click on the admin link again:

captured_Image.png[13]

captured_Image.png[15]

In order to facilitate the “Admin Home” under this new design, I renamed AdminHomeController to AdminController and left it as inheriting from Controller.  This allows for the placement of the views in the ~/Views/Admin location.  Once that was done, I modified the AdminHome route to this:

routes.MapRoute(                                            // Controller for the Admin Home Page
"AdminHome",
"Admin/{action}/{id}",
new { controller = "Admin", action = "Index", id = "" }
);

 

In Conclusion

ASP.Net MVC favors “convention over configuration”.  With that said though, the framework does not prohibit you from defining your own conventions as the need for it arises.  I’ve shown you how it’s possible (and rather simple) to create a convention where the views for your controller are organized in a different fashion than what the “vanilla” MVC expects.  More importantly, I’ve shown you that you can adopt “edge case” conventions within your application without affecting how MVC behaves with the rest of your site.

I am really excited about ASP.Net MVC.  For the first time since .Net 1.0 was released I don’t feel like my web development is constrained or that I have to fight, scratch and claw my way through development because the tools that I’m using have tried to abstract (and dumb down) the process to the point it is a hindrance.  It just makes sense.  BIG KUDOS to Phil Haack Scott Guthrie and the rest of the ASP.Net MVC Team on a truly wonderful product!

Download the Source here:

Technorati Tags: ,,

author: Jason Monroe | posted @ Thursday, April 30, 2009 3:29 PM | Feedback (2)

Upgrades, Updates and Slackatude


I use SubText as the blogging platform for clanmonroe.com.  It was simple to configure and install on my host provider (many years ago) and I kind of just set it and forgot it.

captured_Image.png

Somewhere along the way SubText hit version 2.1.1 and I didn’t know about it (cause I’m a slacker you see.. cobblers shoes are always worn).

At any rate, the blog engine has been updated and stuffs, so of course, this is the obligatory “First Post” to make sure everything still works (Esp with Windows Live Writer)

public class HelloWorld
{
public string SayHello()
{
return "Hey there, How you doing?";
}
}

   1: public class HelloWorld
   2: {
   3:     public string SayHello()
   4:     {
   5:         return "Hey there, How you doing?";
   6:     }
   7: }

Download the Source:

author: Jason Monroe | posted @ Thursday, April 30, 2009 3:23 PM | Feedback (0)

Every Tick Counts... Further Performance Optimization


So I went ahead and created a code project article based on my last two blog posts and have been fairly please with the response and feed back.  One of the feedback posts was from a guy named Bernhard Hofmann about my use of DateTime.Now to get the tick's for part of the unique viewstate key.

To quote his message here:

I noticed your use of DateTime.Now in a few places and wanted to suggest (this is such a little point) the use of DateTime.UtcNow for a few reasons:

  1. UtcNow is faster than Now. This may not make much difference when you're doing file IO, but you don't need the UTC to local conversion overhead.
  2. I like to use UTC to remind other web developers that our sites are usually global.
  3. Using UTC avoids issues you may encounter when clocks change at your server location.

I started pondering this and had to ask myself "Honestly, how much faster can UtcNow really be?  So I write a little test harness to determine for myself...

captured_Image.png

And so there you have it.. a million iterations spread across 5 separate test runs, proves that UtcNow is indeed faster (almost twice as fast) as regular Now.  Granted, I realize that I'm measuring the out come of these tests in processor ticks, and that the actual "real world" difference is actually rather minute.. but let's face it, when you're trying to optimize your production application to get every last bit of performance out of it that you possibly can...

Every tick counts.....

Download Sample Code (12k)

author: Jason Monroe | posted @ Wednesday, August 20, 2008 3:01 PM | Feedback (0)

Efficient Server-Side View State Persistence, Two Dot DOH!!


So, I made a post yesterday about the way we're keeping our view state persisted on the server.

I had been feeling pretty good about the methods that I came up with and the results of not only our tests, but the real world tests that we've seen with this code "out in the wild" per se...  I started to do a lot of reading.  When I say a lot, I mean I fired up google, started at the top and went through every "view state" related link I could find to see if there was something I was missing or hadn't thought of.

On the fine art of being humbled by those smarter than you...

Scott Hanselman has been known for putting together some view state hacks over the years.  He has even written a book (well, part of a book) that has had some of his view state hacks it in.  If you read through all of his blog posts relating to view state (and I have) you begin to get the subtle impression (that is, subtle like a jack hammer outside your bedroom window at 4am) that he is against keeping view state separated from the page.  His post title "Moving ViewState to the Session Object and more Wrongheadedness" on 2/28/2004 really grabbed my attention.  More specifically the paragraph where he said:

If you store ViewState in the Session object in this way, you are assuming the user will access only one page at a time, and you may confused other pages in their attempt to load values from Bogus ViewState.  More importantly, what happens if the user opens new browser window, and starts accessing DIFFERENT pages but sharing the same session.  Well, you get the idea.

But I SWEAR it worked on my machine!!

I know that I had tested the situation Scott had outlined above before pushing my code live.. I know that there was no way that it could fall prey to the edge situation that he mentions... Then the waves of self doubt start to roll in.

So I fired up the exact same sample application that I provided as a download and started running some tests.

  1. Single Browser (IE7) - No problems at all.  Everything works as expected
  2. Single Browser (FF3) - No problem at all.  Everything works as expected
  3. Two Browsers (IE7 / FF3) - No problem at all.  Everything works as expected. 
  4. Two Browsers (IE7 / IE7) - No problem at all.  Everything works as expected.

At that point, I started feeling pretty good about myself... thoughts of "Oh, he wrote that four and a half years ago.. things have changed since then." and "I knew I had tested this.. i r so smart!"...

And then the darkness came...

Scott had said something else in his post that I read and re-read over and over again (the bold is my emphasis):

It pulls the ViewState from the request NameValueCollection (including the Form collection, etc).  Each 'instance' of a page has it's own ViewState.

Then it hit me.... I fired up my test again with IE, created some view state with changed data... then, instead of opening up another IE window... I opened up my test page in another IE Tab.. Low and behold, the second tab was stomping all over the first tab's view state data.  Welcome to the black parade.

Get defines the start of a new page instance

After a brief period of huddling in the corner balled up in the fetal position crying over my own insignificance, I went back and started thinking about how we know when a page is a new instance or not.  The answer is actually really clear.  Every time a HTTP Get verb is called on a page, that page is spun up in a new instance.  with ASP.Net, all page interaction is handled through the POST verb (why it's called Post Back after all).  So all I have to do is key the persisted view state, not only to the user's session id and requested page path, but to an identifiable page instance when the Get was first called.  Once I establish that unique key based on those three factors, then I need to re-use that key and persisted cache file for every subsequent page post.

Realizing that, it's just a matter of modifying the Save() method of the CachePageStatePersister to only generate a new key and cache file if and only if the requested page is not in a post back state.  Otherwise, it should pull the key from the pages hidden field, and cache file path from the Page.Cache..

   1: public override void Save()
   2: {
   3:     if (ViewState != null || ControlState != null)
   4:     {
   5:         if (Page.Session == null)
   6:             throw new InvalidOperationException("Session is required for CachePageStatePersister (SessionID -> Key)");
   7:  
   8:         string vsKey;
   9:         string cacheFile;
  10:  
  11:         if (!Page.IsPostBack) // create a unique cache file and key based on this user's session and page instance (time)
  12:         {
  13:             string sessionId = Page.Session.SessionID;
  14:             string pageUrl = Page.Request.Path;
  15:             vsKey = string.Format("{0}{1}_{2}_{3}", VSPREFIX, pageUrl, sessionId, DateTime.Now.Ticks);
  16:  
  17:             string cachePath = Page.MapPath(CACHEFOLDER);
  18:             if (!Directory.Exists(cachePath))
  19:                 Directory.CreateDirectory(cachePath);
  20:             cacheFile = Path.Combine(cachePath, BuildFileName());
  21:         }
  22:         else    // get our vs key from the page, re use it, and the cache file (pulled from page.cache)
  23:         {
  24:             vsKey = Page.Request.Form[VSKEY];
  25:             if (string.IsNullOrEmpty(vsKey)) throw new ViewStateException();
  26:             cacheFile = Page.Cache[vsKey] as string;
  27:             if (string.IsNullOrEmpty(cacheFile)) throw new ViewStateException();
  28:         }
  29:  
  30:         IStateFormatter frmt = StateFormatter;
  31:         string state = frmt.Serialize(new Pair(ViewState, ControlState));
  32:         using (StreamWriter sw = File.CreateText(cacheFile))
  33:             sw.Write(state);
  34:  
  35:         Page.Cache.Add(vsKey, cacheFile, null, DateTime.Now.AddMinutes(Page.Session.Timeout),
  36:                        Cache.NoSlidingExpiration, CacheItemPriority.Low, ViewStateCacheRemoveCallback);
  37:         Page.ClientScript.RegisterHiddenField(VSKEY, vsKey);
  38:     }
  39: }

 

So, with this small change in the Save() method, our server side view state persister truly supports accurate retrieval of a pages view state, no matter what the user is doing on their end with whichever browser configuration they may be running.

Download Sample Project (10.3k)

author: Jason Monroe | posted @ Wednesday, August 06, 2008 11:59 AM | Feedback (2)

Efficient Server-Side View State Persistence


It's all about the user experience...

[Update: There is a small bug with this posting. I've made a follow up post here to explain the problem and the solution as well]

We all know that the quickest way to kill a web site is to have all of your users disappear.  After all, a web site without users is pretty pointless.  We are constantly striving to bring fresh new content and the latest and greatest "WOW" factor to our sites in order to not only garner new users, but to keep the users we already have coming back.

Users *HATE* slow sites.  This is common knowledge, and when Steve put together his 5 truths about web users, he made this #2 on his list:

2. Users have more than a LAN between their browser and your servers.

I am in the middle of building the latest and greatest. The main page has 15 uncompressed javascript files, 10 background gifs, a couple of extra css files, and sends almost half a meg of data to render the page. It takes about a second for all of this to show up in my browser.

Of course, my browser is sitting on the server, and I've got endless RAM and multiple processors.

When I put this on a test server and hit it from home through DSL, the cloud, and our shared pipe at work - a second becomes over 10 seconds. Long enough that I check ESPN for any new football news.

Having a slow app is like building a car with doors that don't open. People won't buy a car if they have to crawl through the window to get in.

Put your app on an external server and hit it with consumer-grade net access.

Enter View State, Killer of User Experience

View State is a mechanism employed by ASP.Net web pages to persist the state of the page itself, individual controls, objects and data that are housed on that particular ASP.Net web page.  View State is a double edged sword in that using it properly allows the developer to build full and robust web applications that seem to overcome the stateless nature of the web.  Scott Mitchell has written an excellent article on view state some years ago that is published on MSDN.  I won't go into the detail on view state that he does (since he's already done an excellent job), but I will mention a few things:

  1. To much of a good thing... By default, every control that you add to an ASP.Net web page has view state enabled.  Every button, label, grid, drop down.. everything.  That means every control on your page makes the size of the view state grow.
  2. View State is persisted in the HTML Markup.  That's right boys and girls.  Every control on your page that makes use of view state, making that view state bigger.. makes your overall page size bigger as well.
  3. View State travels both ways.  Since view state is used by ASP.Net, and ASP.Net only exists on the server, then that view state that was sent down to the browser, has to be sent BACK to the server for it to be used.  Yes, that includes partial page updates with AJAX as well.
  4. View State is ONLY used on the server.  I know I said this in #3, but it bears repeating here... the only place that view state is EVER used, is on the server, by the ASP.Net runtime...

Even the most responsible developers can be plagued by a large view state...

As the complexity of our web applications grow, more and more controls get added to the screen, more and more data needs to be persisted... view state grows.  There's no way to avoid it.  Even if we are diligent in our efforts in only making sure that those things that need view state are the only ones with it turned on, view state can grow to several kilobytes in size.  It's not uncommon to have view state sizes that run into the 50k to 100k range on custom intranet/extranet applications.

Couple this with the fact that view has to be sent down to the browser, and back up to the server on every post back and asynchronous post back done via AJAX, we can see how just the sheer size of the web page causes our overall application performance to tank.  The thing is, ever growing view state is not a new problem.  Over the years, many people (most much smarter than I am) have come up with methods to combat the size. Scott Hanselman put together a blog post on how to compress the view state before it's sent to the browser as had several other authors. Another solution often sought after is moving the view state from the top of the page to the bottom of the page.  Moving the view state enables the browser to start rendering the HTML content right away instead of waiting for this massive hidden field content to stream down.  However, that still doesn't negate the fact that the view state has to travel back up to the server.

No, to solve this view state problem once and for all, I'd like to redirect your attention to rule #4...

View State is *ONLY* used on the server!!!

It seems logical to me, that if view state is only used on the server, then we should keep it on the server and not make it travel down the wire to the client browser, and back up to the server... every... single... time.

History

I stumbled upon a Code Project article entitled Keep ASP.Net ViewState out of ASPX Page for Performance Improvement which took it's inspiration from an article posted by Peter Bromberg.  The basic premise of the article is to override the System.Web.UI.Page SavePageStateToPersistenceMedium and LoadPageStateFromPersistenceMedium methods.  The code samples show how to take the view state and save them in either the Page.Cache or in Session.. or some combination there of.  In developing our solution, I opted to persist the view state into cache.  I was extremely excited to see that my average page size went from 70k in size to just over 20k.  Feeling good about the initial tests, I rolled the branch into our code based and published the code to our training server to put a slightly heaver load on the code for testing.  After about 3 hours of use during a training class, the server came crashing to it's knees.

Sever resources are not infinite.

What had happened, was now, instead of the 30k to 60k view state being sent back and forth to the client, it was living happily in Page.Cache.  Of course, Page.Cache is an in-memory caching device.  So for 50 users, over the course of 3 hours.. the ASP.Net cache grew to the point that it choked out the rest of the memory available to the ASP.Net worker process. 

Not wanting to give up on the concept (I still believed that it was the best way to handle our large view state problem and keep the user experience optimal), I went "back to the drawing board" and came up with another solution to persist the view state on the server, while still being mindful of the finite server resources.

Rethinking the design

Requirements:

  1. The view state needs to be persisted on the server
  2. The view state persistence mechanism needs to be identified by a specific user session
  3. The persisted view state artifact must not be allowed to remain forever
  4. The persisted view state should be able to be enabled and disabled on a page by page bases
  5. Different persistence mechanism's should be able to be used.
  6. Page development and structure should not be modified.

Control Adapters to the Rescue

Since the ASP.Net Page object, at it's core is a control (granted.. it is *THE* control.. but a control non-the-less) we're able to modify it's behavior with a simple control adapter.  Typically, when speaking about control adapters, the development community at large thinks about CSS Control Adapters.

MSDN defines control adapters thusly:

Control adapters are components that override certain Control class methods and events in its execution lifecycle to allow browser or markup-specific handling. The .NET Framework maps a single derived control adapter to a Control object for each client request.

I had discovered this rather excellent (although brief) article by Robert Boedigheimer on using Server Side View state in ASP.Net using the SessionPageStatePersister.  I knew then that this was the road I wanted to go down... although using the SessionPageStatePersister would run into the same problems with finite memory resources.. so that as a brush stroke solution wouldn't do...

My solution actually contains two parts.  The first part is the control adapter PageStateAdapter, and the second is the custom persistence mechanism CachePageStatePersister.

PageStateAdapter

To solve my requirements #4 and #5, I decided to go with a simple attribute scheme.  Where basically, the developer can decide to use a different view state persistence scheme simply by putting an attribute at the top of the pages class definition.  The attribute class and supporting enum are both defined in the PageStateAdapter class

   1: public enum StateStorageTypes { Default, Cache, Session, InPage }
   2:  
   3: [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
   4: public class PageViewStateStorageAttribute : Attribute
   5: {
   6:     private readonly StateStorageTypes storageType = StateStorageTypes.Default;
   7:  
   8:     public PageViewStateStorageAttribute(StateStorageTypes stateStorageType)
   9:     {
  10:         storageType = stateStorageType;
  11:     }
  12:  
  13:     internal StateStorageTypes StorageType
  14:     {
  15:         get { return storageType; }
  16:     }
  17: }

 

Now all the PageStateAdapter has to do is implement the virtual method GetStatePersister

   1: public override PageStatePersister GetStatePersister()
   2: {
   3:     PageViewStateStorageAttribute psa =
   4:         Attribute.GetCustomAttribute(Page.GetType(), typeof(PageViewStateStorageAttribute), true) as PageViewStateStorageAttribute ??
   5:         new PageViewStateStorageAttribute(StateStorageTypes.Default);
   6:  
   7:     PageStatePersister psp;
   8:     switch (psa.StorageType)
   9:     {
  10:         case StateStorageTypes.Session:
  11:             psp = new SessionPageStatePersister(Page);
  12:             break;
  13:         case StateStorageTypes.InPage:
  14:             psp = new HiddenFieldPageStatePersister(Page);
  15:             break;
  16:         default:
  17:             psp = new CachePageStatePersister(Page);
  18:             break;
  19:     }
  20:     return psp;
  21: }

 

If a developer wishes to override the (now) default view state persistence method of CachePageStatePersister, they can do so by applying a simple attribute to the page class declaration

   1: [PageStateAdapter.PageViewStateStorage(PageStateAdapter.StateStorageTypes.InPage)]
   2: public partial class ViewStateInPage : System.Web.UI.Page
   3: {
   4: }

 

CachePageStatePersister

The CachePageStatePersister inherits from PageStatePersister.  So all it has to do is implement the two virtual methods Load() and Save().

   1: public override void Save()
   2: {
   3:     if (ViewState != null || ControlState != null)
   4:     {
   5:         if (Page.Session == null)
   6:             throw new InvalidOperationException("Session is required for CachePageStatePersister (SessionID -> Key)");
   7:  
   8:         IStateFormatter frmt = StateFormatter;
   9:         string state = frmt.Serialize(new Pair(ViewState, ControlState));
  10:         string sessionId = Page.Session.SessionID;
  11:         string pageUrl = Page.Request.Path;
  12:         string vsKey = string.Format("{0}{1}_{2}", VSPREFIX, pageUrl, sessionId);
  13:  
  14:         string cachePath = Page.MapPath(CACHEFOLDER);
  15:         if (!Directory.Exists(cachePath))
  16:             Directory.CreateDirectory(cachePath);
  17:  
  18:         string cacheFile = Path.Combine(cachePath, BuildFileName());
  19:  
  20:         using (StreamWriter sw = File.CreateText(cacheFile))
  21:             sw.Write(state);
  22:  
  23:         Page.Cache.Add(vsKey, cacheFile, null, DateTime.Now.AddMinutes(Page.Session.Timeout),
  24:                        Cache.NoSlidingExpiration, CacheItemPriority.Low, ViewStateCacheRemoveCallback);
  25:         Page.ClientScript.RegisterHiddenField(VSKEY, vsKey);
  26:     }
  27: }

 

In our save method, you can see we're doing a number of things.. first is generating a unique view state key based on the page that's being requested and the users session id.  Then we're serializing BOTH the view state and control state to a physical file (in this case, we're storing the file in the ~/App_Data/Cache directory).  After the file is created, we store the view state key, and the path to the file in the Page.Cache, and save the view state key to a hidden field to the page.

So we are mindful of finite server resources by only storing the file path in cache, we are mindful of download page size by only storing the unique key in the page (instead of the entire view state) and by using the Page.Cache object to tie everything together we're giving our persisted view state files a life time.  Notice on the last bit of the Page.Cache.Add() method, we're defining ViewStateCacheRemoveCallback as our call back method when an item is removed from cache.

   1: public static void ViewStateCacheRemoveCallback(string key, object value, CacheItemRemovedReason reason)
   2: {
   3:     string cacheFile = value as string;
   4:     if (!string.IsNullOrEmpty(cacheFile))
   5:         if (File.Exists(cacheFile))
   6:             File.Delete(cacheFile);
   7: }

 

When the Cache makes the call back, it passes on what that cache object contained, which we know is the file path to the persisted view state object.  All we have to do when we receive the call back, is to delete the physical file.

The Load method basically works in reverse of the save method..

   1: public override void Load()
   2: {
   3:     if (!Page.IsPostBack) return;   // We don't want to load up anything if this is an inital request
   4:  
   5:     string vsKey = Page.Request.Form[VSKEY];
   6:  
   7:     // Sanity Checks
   8:     if (string.IsNullOrEmpty(vsKey)) throw new ViewStateException();
   9:     if (!vsKey.StartsWith(VSPREFIX)) throw new ViewStateException();
  10:  
  11:     IStateFormatter frmt = StateFormatter;
  12:     string state = string.Empty;
  13:  
  14:     string fileName = Page.Cache[vsKey] as string;
  15:     if (!string.IsNullOrEmpty(fileName))
  16:         if (File.Exists(fileName))
  17:             using (StreamReader sr = File.OpenText(fileName))
  18:                 state = sr.ReadToEnd();
  19:  
  20:     if (string.IsNullOrEmpty(state)) return;
  21:  
  22:     Pair statePair = frmt.Deserialize(state) as Pair;
  23:  
  24:     if (statePair == null) return;
  25:  
  26:     ViewState = statePair.First;
  27:     ControlState = statePair.Second;
  28: }

 

Some points of interest here are... if the page is not working as a post back, we don't want to load our view state from persistence.  This solves the problem of a user loading stale data.  The Load() method gets it's view state key from the page, then gets the path to the persisted view state file from the Page.Cache using that view state key.  The file is then read, de-serialized into a Pair object, then ViewState and ControlState are loaded from the Pair object.

The Browsers File

The last point of order is to wire up the PageStateAdapter for use with a simple .browser file located in App_Browsers:

   1: <browsers>
   2:   <browser refID="Default">
   3:     <controlAdapters>
   4:       <adapter controlType="System.Web.UI.Page" adapterType="PageStateAdapter" />
   5:     </controlAdapters>
   6:   </browser>
   7: </browsers>

 

Conclusion

View state, while a powerful tool, can very quickly become the demise of your users in experiencing your web site to the fullest.  By making use of built-in ASP.Net technologies like Cache and Control Adapters, we can effectively and accurately persist view state on the server instead of streaming it down to the user.  Using the outline PageStateAdapter has enabled our developers to make full use of our existing ASP.Net skill set, including our heavy use of ASP.Net AJAX, without having to re-design our entire page framework; it is truly a drop in solution.  Our PageStateAdapter method could be very easily modified to persist the view state files to a common location for a multi-homed web environment, and the Page.Cache replaced with a common mechanism in that same environment.  We've been using this technique in our training environment under both real world load and extreme test load with very impressive results.

In our code base, we've also added to the global Application_Start and Application_End events to delete all the .cache files that might be present.  This takes care of any artifacts that might be around when the server reboots, or that might have been missed in the cache remove call back.

Download the sample project (11k)

author: Jason Monroe | posted @ Tuesday, August 05, 2008 5:38 PM | Feedback (0)