Amazon.com Widgets All posts by williarob

WilliaBlog.Net

I dream in code

About the author

Robert Williams is an internet application developer for the Salem Web Network.
E-mail me Send mail
Go Daddy Deal of the Week: 30% off your order at GoDaddy.com! Offer expires 11/6/12

Recent comments

Archive

Authors

Tags

Code Project Associate Logo

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.


Instantly Increase ASP.NET Scalability

Each time a request for a .NET resource (.aspx, page, .ascx control, etc.) comes in a thread is grabbed from the available worker thread pool in the asp.net worker process (aspnet_wp.exe on IIS 5.x, w3wp.exe on IIS 6/7) and is assigned to a request. That thread is not released back into the thread pool until the final page has been rendered to the client and the request is complete.

Inside the ASP.Net Worker Process there are two thread pools. The worker thread pool handles all incoming requests and the I/O Thread pool handles the I/O (accessing the file system, web services and databases, etc.). But how many threads are there in these thread pools? I had assumed that the number of threads would vary from machine to machine – that ASP.NET and IIS would carefully balance the number of available threads against available hardware, but that is simply not the case. ASP.Net installs with a fixed, default number of threads to play with: The CLR for the 1.x Framework sets these defaults at just 20 worker threads and 20 I/O thread per CPU. Now this can be increased by modifying the machine.config, but if you are not aware of this, then 20 threads is all you’re playing with. If you have multiple sites sharing the same worker process, then they are all sharing this same thread pool.

So long as the number of concurrent requests does not exceed the number of threads available in the pool, all is well. But when you are building enterprise level applications the thread pool can become depleted under heavy load, and remember by default heavy load is more than just 20 simultaneous requests. When this happens, new requests are entered into the request queue (and the users making the requests watch an hour glass spin). ASP.NET will allow the request queue to grow only so big before it starts to reject requests at which point it starts returning Error 503, Service Unavailable.

If you are not aware of this “Glass Ceiling of Scalability”, this is a perplexing error – one that never happened in testing and may not reproducible in your test environment, as it only happens under extreme load.

So the first thing you can do to improve scalability is to raise the values. The defaults for the ASP.NET 2.0 are 100 threads in each pool per CPU and the defaults for the ASP.NET 3.x CLR is 250 per CPU for worker threads and 1000 per CPU for I/O threads, however you can tune it further using the guidelines below. 32 bit windows can handle about 1400 concurrent threads, 64 bit windows can handle more, though I don’t have the figures.

You can tune ASP.NET thread pool using maxWorkerThreads, minWorkerThreads, maxIoThreads, minFreeThreads, minLocalRequestFreeThreads and maxconnection attributes in your Machine.config file. Here are the default settings.

<system.net>
  <connectionManagement>
     <add address="*" maxconnection="2" />
  </connectionManagement>
</system.net>
<system.web>
  <httpRuntime minFreeThreads="8" minLocalRequestFreeThreads="4"  />
  <processModel maxWorkerThreads="100" maxIoThreads="100"  />
</system.web>

Here is the formula to reduce contention. Apply the recommended changes that are described below, across the settings and not in isolation.

  • Configuration setting Default value (.NET Framework 1.1) Recommended value
  • maxconnection 2 12 * #CPUs
  • maxIoThreads 20 100
  • maxWorkerThreads 20 100
  • minFreeThreads 8 88 * #CPUs
  • minLocalRequestFreeThreads 4 76 * #CPUs
  • Set maxconnection to 12 * # of CPUs. This setting controls the maximum number of outgoing HTTP connections that you can initiate from a client. In this case, ASP.NET is the client. Set maxconnection to 12 * # of CPUs.
  • Set maxIoThreads to 100. This setting controls the maximum number of I/O threads in the .NET thread pool. This number is automatically multiplied by the number of available CPUs. Set maxloThreads to 100.
  • Set maxWorkerThreads to 100. This setting controls the maximum number of worker threads in the thread pool. This number is then automatically multiplied by the number of available CPUs. * Set maxWorkerThreads to 100.
  • Set minFreeThreads to 88 * # of CPUs. This setting is used by the worker process to queue all the incoming requests if the number of available threads in the thread pool falls below the value for this setting. This setting effectively limits the number of requests that can run concurrently to maxWorkerThreads – minFreeThreads. Set minFreeThreads to 88 * # of CPUs. This limits the number of concurrent requests to 12 (assuming maxWorkerThreads is 100).
  • Set minLocalRequestFreeThreads to 76 * # of CPUs. This setting is used by the worker process to queue requests from localhost (where a Web application sends requests to a local Web service) if the number of available threads in the thread pool falls below this number. This setting is similar to minFreeThreads but it only applies to localhost requests from the local computer. Set minLocalRequestFreeThreads to 76 * # of CPUs.

Note The recommendations that are provided in this section are not rules. They are a starting point. Test to determine the appropriate settings for your scenario. If you move your application to a new computer, ensure that you recalculate and reconfigure the settings based on the number of CPUs in the new computer.

By raising these values, you raise the “glass ceiling of scalability”, and in many cases that may be all you need to do, but what happens when you start getting more than 250 simultaneous requests? To make optimum use of the thread pool all IO requests that you know could take a second or more to process should be made asynchronously. More information on Asynchronous programming in ASP.NET is coming soon.

I recommend testing your new machine.config locally or virtually somewhere first because if you make a mistake - for example you paste this in to the wrong area, or there is already a <system.web> element and you paste in a second one - ALL websites on the box will stop functioning as ASP.Net will not be able to parse the machine.config!!!


Posted by Williarob on Tuesday, December 02, 2008 7:41 AM
Permalink | Comments (1) | Post RSSRSS comment feed

Add Asynchronous Data Methods to the Enterprise Library

A Data Access Layer that does not support Asychronous I/O is not scalable. Period. However, since Microsoft kindly provided the source code along with the Enterprise Library, it is possible to make the Enterprise Library's Data Application Block scalable by adding the Asynchronous methods (BeginExecuteNonQuery, BeginExecuteReader, etc.) to the SQL Database Object. I took the code from the 3.x library and modified the file SqlDatabase.cs found here c:\EntLib3Src\App Blocks\Src\Data\Sql (assuming you installed the source files to the root of your C drive). The following download includes the modified binaries and my SqlDatabase.cs code file:

AsyncEL.zip (348.39 kb)

I don't think I implemented every overload, and again this is for the 3.0 library, but you should be able to modify the 4.x libraries and add more overloads by following the patterns I'm using. To use the binaries included in the zip file above, simply drop them into the bin folder of your web site or web application project, add references if necessary and don't forget to add the async=true attribute to both your <% page %>  tag and your connection string! Here is a simple example of how you might use it, to get you started:

 

    1 using System;

    2 using System.Collections;

    3 using System.Configuration;

    4 using System.Data;

    5 using System.Data.SqlClient;

    6 using System.Linq;

    7 using System.Web;

    8 using System.Web.Configuration;

    9 using System.Web.Security;

   10 using System.Web.UI;

   11 using System.Web.UI.HtmlControls;

   12 using System.Web.UI.WebControls;

   13 using System.Web.UI.WebControls.WebParts;

   14 using System.Xml.Linq;

   15 

   16 public partial class temp : System.Web.UI.Page

   17 {

   18     protected void Page_Load(object sender, EventArgs e)

   19     {

   20         if (!IsPostBack)

   21         {

   22             RegisterAsyncTask(new PageAsyncTask(new BeginEventHandler(BeginUpdateByAsyncEL), new EndEventHandler(EndUpdateByAsyncEL), new EndEventHandler(AsyncUpdateTimeout), null, true));

   23         }

   24     }

   25 

   26     /// <summary>

   27     /// Creates a SqlDatabase Object and stores it in HttpContext for the duration of the request.

   28     /// </summary>

   29     /// <value></value>

   30     /// <returns>A SqlDatabase Object</returns>

   31     /// <remarks></remarks>

   32     public static Microsoft.Practices.EnterpriseLibrary.Data.Sql.SqlDatabase AsyncNorthWindDB

   33     {

   34         get

   35         {

   36             if (HttpContext.Current == null)

   37             {

   38                 return new Microsoft.Practices.EnterpriseLibrary.Data.Sql.SqlDatabase(WebConfigurationManager.ConnectionStrings["AsyncNorthwind"].ConnectionString);

   39             }

   40             if (HttpContext.Current.Items["AsyncNorthWindDB"] == null)

   41             {

   42                 HttpContext.Current.Items.Add("AsyncNorthWindDB", new Microsoft.Practices.EnterpriseLibrary.Data.Sql.SqlDatabase(WebConfigurationManager.ConnectionStrings["AsyncNorthwind"].ConnectionString));

   43             }

   44             return HttpContext.Current.Items["AsyncNorthWindDB"];

   45         }

   46     }

   47 

   48     /// <summary>

   49     /// Begins an asynchronous call the the Northwind Database 

   50     /// </summary>

   51     /// <param name="sender"></param>

   52     /// <param name="e"></param>

   53     /// <param name="cb"></param>

   54     /// <param name="state"></param>

   55     /// <returns>An IAsyncResult Interface</returns>

   56     /// <remarks>These methods do not exist in the standard Enterprise Library</remarks>

   57     public IAsyncResult BeginUpdateByAsyncEL(object sender, EventArgs e, AsyncCallback cb, object state)

   58     {

   59         SqlCommand cmd = new SqlCommand("Update Products SET UnitPrice = 79.0 WHERE productID = 20");

   60         return AsyncNorthWindDB.BeginExecuteNonQuery(cmd, cb, state);

   61     }

   62 

   63     public void EndUpdateByAsyncEL(IAsyncResult ar)

   64     {

   65         AsyncNorthWindDB.EndExecuteNonQuery(ar);

   66     }

   67 

   68     /// <summary>

   69     /// Async Timeout method

   70     /// </summary>

   71     /// <param name="ar"></param>

   72     /// <remarks></remarks>

   73     public void AsyncUpdateTimeout(IAsyncResult ar)

   74     {

   75         Label1.Text = "Connection Timeout";

   76     }

   77 }

 

Download the Full Enterprise Library (including the complete source code) from http://www.codeplex.com/entlib.

 

kick it on DotNetKicks.com


Posted by Williarob on Monday, December 01, 2008 8:50 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Begin/End Async WebService Proxy Methods No Longer Generated

If you have added a Web Reference for a Web Service recently you may have noticed that the wsdl tool no longer creates Begin and End methods for you, instead it implements what is known as the Event based Async Pattern. Which is fine if you are building a Windows or Console application, but if you are trying to call a web service asynchronously using the AddOnPreRenderAsync or RegisterAsyncTask methods in ASP.NET you really need a Begin method that returns an IAsyncResult Interface. There are several ways you can work around this.

You could, assuming you still have it, create a project in Visual Studio 2003, add the web reference there and copy it to your 2005 project, or you can run the wsdl tool manually and use the /parameters option to specify a configuration file with the "oldAsync" flag. For example, for a reference to http://my.server.com/service, which creates a proxy class with both begin/end (a.k.a. "old style") and event-based (a.k.a. "new style") methods, you'd use the command

  wsdl.exe /parameters:MyParameters.xml http://my.server.com/service

Where MyParameters.xml is as below:

MyParameters.xml:

<wsdlParameters xmlns="http://microsoft.com/webReference/">
  <nologo>true</nologo>
  <parsableerrors>true</parsableerrors>
  <sharetypes>true</sharetypes>
  <webReferenceOptions>
    <verbose>false</verbose>
    <codeGenerationOptions>properties oldAsync newAsync</codeGenerationOptions>
    <style>client</style>
  </webReferenceOptions>
</wsdlParameters>

However, my preferred method is to set a project level property WebReference_EnableLegacyEventingModel to true.

To do that, you need unload the project from Visual Studio, and edit the project file directly in a text editor. (I recommend creating a backup first)

In the first section of PropertyGroup, you will see many properties like:

    <ProjectGuid>{F4DC6946-F07E-4812-818A-F35C5E34E2FA}</ProjectGuid>
    <OutputType>Exe</OutputType>
...

Don't change any of those, but do add a new property into that section:


    <WebReference_EnableLegacyEventingModel>true</WebReference_EnableLegacyEventingModel>


Save the file, and reload the project into Visual Studio.

After that, you have to regenate all proxy code (by updating the reference, or running the custom tool on the .map file)

This is my preferred method because unlike the others I described, this one lets you refresh or update the web reference as normal, which if your project is shared with other developers is a big plus!


Posted by Williarob on Monday, December 01, 2008 7:12 AM
Permalink | Comments (2) | Post RSSRSS comment feed

Accessing Global Resource Files from a Class Library in ASP.NET

If you add an App_GlobalResources Folder to your asp.net 2.0 web application you may want to access some of the values within it from a class library - such as a Business Logic Layer (BLL). Since there is unlikely to be a reference from your BLL to the main site, and even if there were, the designer class that shadows the resource file is declared with "Friend" keywords so that class would not be visible to the BLL. Therefore trying to call it using the Resources.[Filename].[key] syntax that works from anywhere within the site will not work. Instead you can simply create a wrapper like the following somwhere within your class library:

    1 Imports System.Resources

    2 Imports System.Web

    3 

    4 Namespace Resources

    5     Public Class ResourceWrapper

    6 

    7         Public Shared Function Strings1(ByVal key As String) As String

    8             Return HttpContext.GetGlobalResourceObject("Strings1", key)

    9         End Function

   10 

   11         Public Shared Function Strings2(ByVal key As String) As String

   12             Return HttpContext.GetGlobalResourceObject("Strings2", key)

   13         End Function

   14 

   15         Public Shared Function Strings3(ByVal key As String) As String

   16             Return HttpContext.GetGlobalResourceObject("Strings3", key)

   17         End Function

   18 

   19     End Class

   20 End Namespace

 

Then you can retrieve values from the resource files from within your class library simply by passing in the key you want to retrieve:

 

   Resources.ResourceWrapper.Strings1("SupportEmail")

 

This example assumes that you have created three .resx resource files inside the App_GlobalResources folder of an ASP.Net Web Application, and that you created them within Visual Studio (as opposed to a text editor for example), and that you named the files "Strings1.resx", "Strings2.resx" and "Strings3.resx", and that Strings1.resx contains the key "SupportEmail".

 

The Great thing about adding .resx files in this fashion is that you can actually edit the values in a text editor on the live site without having to recompile and republish your entire project. Having said that, adding new key value pairs in the text editor will not create the properties in the class that shadows it, and removing or renaming existig key value pairs could cause runtime errors so if you need to do anything other than change the value of an existing key, do so within visual studio and republish.


Posted by Williarob on Thursday, November 20, 2008 6:06 PM
Permalink | Comments (0) | Post RSSRSS comment feed

Silverlight 1.0 Applications Not Working in Firefox 3.x

If, like me, you suddenly noticed that your silverlight 1.0 application no longer works in Firefox 3.0 then all you need to do is to update your silverlight.js file to the latest version: Download it here:

Silverlight.zip (3.58 kb)


Categories: Silverlight
Posted by Williarob on Thursday, November 20, 2008 7:15 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Could not load file or assembly 'System.Web.Abstractions, Version=3.5.0.0, Culture=neutral, PublicKeyToken= 31bf3856ad364e35' or one of its dependencies.

If you had tried to visit this site in mid-November, this is the ugly error message you would have received: "Could not load file or assembly 'System.Web.Abstractions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies". My last update had been 2 full weeks before the site went down. My hosting provider (GoDaddy) assures me that nothing has changed at their end, I knew nothing had changed at my end and the last modified dates of my files on their server reflected this. A republish of the site did not help. Deleting all the files from the server, testing it locally (which worked even though I did not have the missing assembly on my PC) and republishing did not resolve the issue. So while I still have no idea why my project suddenly requires the System.Web.Abstractions.dll assembly I did at least figure out how to bring it back online. First I downloaded the Microsoft ASP.NET MVC Beta installer and ran it on my local machine. Then I searched my hard drive for the System.Web.Abstractions.dll file again and found it here: "C:\Program Files\Microsoft ASP.NET\ASP.NET MVC Preview 3\Assemblies". I FTP'd the file into my bin folder on the server and tested my site. Almost the same error, but instead of not finding the assembly at all, it now found the wrong version. It was looking for version 3.5.0.0 and the latest MVC Beta (BETA 3) has version 0.0.0.0. A quick Google search lead me to this solution which suggested adding a bindingRedirect to my web.config. I added it right before the closing </configuration> tag at the very bottom of my web.config:

<configuration>
...
 <runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
   <dependentAssembly>
    <assemblyIdentity name="System.Web.Abstractions" publicKeyToken="31BF3856AD364E35"/>
    <bindingRedirect oldVersion="0.0.0.0-3.5.0.0" newVersion="0.0.0.0"/>
   </dependentAssembly>
  </assemblyBinding>
 </runtime>
</configuration>

And voila, the site is back up. Did anyone else experience this issue recently? 2 weeks prior to this I had updated from BlogEngine.Net 1.3 to 1.4.5; and for the first time compiled the site with Visual Studio 2008 instead of Visual Studio 2005. I had also upgraded the Ajax Control Toolkit to version 3.5. However, as I mentioned before, all of this happened a full two weeks before the site went down with this error. I suspect that (since not even a single line of text was changed on this site in all that time) GoDaddy must have applied a service pack, or an update to their server to cause the dependency on the missing assembly. If anyone has more information, please let me know.

 


Posted by Williarob on Wednesday, November 19, 2008 7:00 PM
Permalink | Comments (2) | Post RSSRSS comment feed

Page Load Event Fires Twice

If you have set a breakpoint in the page_load event of your web form and have found it fires twice, view source on the page and check for anything that uses a src="" attribute that is either empty or contains number sign (src="#"). If you find one, make sure you set it to something, if it is an image find a spacer.gif or something you can use, if it is a script or an Iframe either set it or remove it. If you think about it this makes perfect sense, an image tag with an empty src attribute will end up calling the page as the source, which will fire the page load event again. In my case, I was using an <asp:image> tag that had no imageurl until the submit button was pushed and a green check mark or red cross was assigned to it as part of an onscreen Success/Failure message. Making it default to the check mark image resolved my issue. While researching this I found these other possible causes:

  • Check to see if AutoEventWireUp is set to false in the page/control directive.
  • If you are using Visual Basic, check to see that your Handles keyword is only handling one event:"Handles MyBase.Load, Me.Load" would cause it to fire twice.
  • If it only happens on postback, check that your Javascript form validation does not end with form.submit() which would effectively submit your form twice.

Categories: ASP.Net | C# | VB
Posted by Williarob on Thursday, July 24, 2008 1:23 PM
Permalink | Comments (3) | Post RSSRSS comment feed

Process and Thread Basics

Programs, Processes and Threads

In .NET terms, a program can be defined as an assembly, or group of assemblies, that work together to accomplish a task. Assemblies are little more than a way of packaging instructions into maintainable elements and are generally compiled into a dynamic link library (DLL) or an executable (EXE), or a combination of the two.

A process gives a program a place to run, allowing access to memory and resources. Generally, each process runs relatively independent of other processes. In particular, the memory where your program variables will reside is completely separate from the memory used by other processes. Your email program cannot directly assign a new value to a variable in the web browser program. If your email program can communicate with your web browser—for instance, to have it open a web page from a link you received in email—it does so with some form of communication that takes much more time than a memory access.

By putting programs into processes and using only a restricted, mutually agreed-upon communication between them has a number of advantages. One of the advantages is that an error in one process will be less likely to interfere with other processes. Before multitasking operating systems, it was much more common for a single program to be able to crash the entire machine. Putting tasks into processes, and limiting interaction with other processes and the operating system, has greatly added to system stability.

All modern operating systems support the subdivision of processes into multiple threads of execution. Threads run independently, like processes, and no thread knows what other threads are running or where they are in the program unless they synchronize explicitly. The key difference between threads and processes is that the threads within a process share all the data of the process. Thus, a simple memory access can accomplish the task of setting a variable in another thread. Every program will have at least one thread.

In his book ".NET Multithreading" Alan Dennis compares a Process to a house and a thread to a housecat. He writes:

The cat spends most of its time sleeping, but occasionally it wakes up and performs some action, such as eating. The house shares many characteristics with a process. It contains resources available to beings in it, such as a litter box. These resources are available to things within the house, but generally not to things outside the house. Things in the house are protected from things outside of the house. This level of isolation helps protect resources from misuse. One house can easily be differentiated from another by examining its address. Most important, houses contain things, such as furniture, litter boxes, and cats.

Cats perform actions. A cat interacts with elements in its environment, like the house it lives in. A housecat generally has a name. This helps identify it from other cats that might share the same household. It has access to some or the entire house depending on its owner’s permission. A thread’s access to elements may also be restricted based on permissions, in this case, the system’s security settings.

Multitasking

Multitasking means that more than one program can be active at a time. You may take it for granted that you can have an email program and a web browser program running at the same time. Yet, not that long ago, this was not the case. In the days of DOS you would need to save and close your spreadsheet before opening your word processor. With the advent of Windows, you could open multiple applications at once. Windows 3.x used something called Cooperative Multitasking which is based on the assumption that all running processes will yield control to the operating system at a frequent interval. The problem with this model was that not all software developers followed these rules and a program that did not return control to the system, or did so very infrequently, could destabilize the operating system, causing it to "lock up". When Windows 3.x started a new application, that application was invoked from the main thread. Windows passed control to the application with the understanding that control would quickly be returned to windows. If that didn't happen, all other running applications including the operating system could no longer execute instructions. Today, Windows employs Preemptive Multitasking. In this model, instead of relying on programs to return control to the system at regular intervals, the OS simply takes it. 

The main thread of a typical windows program executes a loop (Called a message pump). The loop checks a message queue to see if there is work to do and if there is, it does the work. For example, when a user clicks on a button in a windows application the click event adds work to the message queue indicating which method should be executed. This method is of course known as an event handler. While the loop is executing an event handler, it cannot process additional messages. Multithreading (literally using more than one thread) is how we can work around this limitation. Instead of having the main thread that was assigned to this program do the time consuming work, we assign the work to a seperate thread and have it do the work.

There are a number of ways to create and manage these new threads - using System.Threading, creating a delegate method, implementing the Event Based Asynchronous Pattern, using waithandles, etc. and I intend to explore all of them future articles. In a single core processor, this execution on a separate thread would be periodically interrupted by the operating system to allow other threads a chance to get work done; but after decades in a world where most computers had only one central processing unit (CPU), we are now in a world where only "old" computers have one CPU. Multi-core processors are now the norm. Therefore, every software developer needs to Think Parallel.

Multithreading

Multithreading allows a process to overlap I/O and computation. One thread can execute while another thread is waiting for an I/O operation to complete. Multithreading makes a GUI (graphical user interface) more responsive. The thread that handles GUI events, such as mouse clicks and button presses, can create additional threads to perform long-running tasks in response to the events. This allows the event handler thread to respond to more GUI events. Multithreading can speed up performance through parallelism. A program that makes full use of two processors may run in close to half the time. However, this level of speedup usually cannot be obtained, due to the communication overhead required for coordinating the threads.


Posted by Williarob on Saturday, June 28, 2008 9:00 PM
Permalink | Comments (0) | Post RSSRSS comment feed

Setting Membership-Profile-Role provider's connection string at runtime

Why would you want to do this? Well perhaps your application uses a separate API to retrieve its specific connection string (via key, for security purposes), or perhaps you have different connection strings for production and development and want to be able to test a value (such as Environment.MachineName) at runtime and load the appropriate connection string name into your membership and role providers. Listing 1 shows how a typical web.config might list its connectionstrings and providers.

Listing 1

<connectionStrings>

    <!-- DEV -->

    <add name="DEV.DB" connectionString="Database=NORTHWIND;Server=xxx;User=xxx;Pwd=xxx;" providerName="System.Data.SqlClient"/>

 

    <!-- Live -->

    <add name="LIVE.DB" connectionString="Database=NORTHWIND;Server=xxx;User=xxx;Pwd=xxx;" providerName="System.Data.SqlClient"/>

</connectionStrings>

 

<!-- ... -->

 

<membership defaultProvider="MyProvider">

      <providers>

        <add connectionStringName="DEV.DB" applicationName="/Test"

    description="MyProvider" requiresUniqueEmail="false" enablePasswordRetrieval="false"

    enablePasswordReset="false" requiresQuestionAndAnswer="false"

    passwordFormat="Hashed" name="MyProvider" type="Williablog.Net.Examples.Providers.SqlMembershipProvider" />

       

      </providers>

    </membership>

 

    <profile defaultProvider="MyProvider">

      <providers>

        <add connectionStringName="DEV.DB" applicationName="/Test" description="" name="MyProvider" type="Williablog.Net.Examples.Providers.SqlProfileProvider"/>

      </providers>

 

      <properties>

        <add name="FirstName" type="System.String" allowAnonymous="false"/>

        <add name="LastName" type="System.String" allowAnonymous="false"/>

      </properties>

    </profile>

 

    <roleManager enabled="true" defaultProvider="MyProvider" domain="xxx">

      <providers>

        <add connectionStringName="DEV.DB" applicationName="/Test"

    description="" name="MyProvider" type="Williablog.Net.Examples.Providers.SqlRoleProvider" />

      </providers>

    </roleManager>

 

    <webParts>

      <personalization defaultProvider="MyProvider">

        <providers>

          <add connectionStringName="DEV.DB" name="MyProvider" type="Williablog.Net.Examples.Providers.SqlPersonalizationProvider"/>

        </providers>

      </personalization>

    </webParts>

So how can you change the connection strings at runtime?

When shopping around for an answer, I came across this entry in the Microsoft Forums which suggests downloading the SampleProviderToolkitSampleProviders from Microsoft, modifying the SQLConnectionHelper class to include your special handling of the connection string. And this was initially the road I went down, however, the SampleProviderToolkitSampleProviders does not include a SqlPersonalizationProvider example and it was when I decided to simply inherit the built in SqlPersonalizationProvider and make my own that I discovered an easier solution (Listing 2) that can be applied to any provider (as long as your connection strings can be made available in the connectionstrings section of the web.config.):

Listing 2

Imports System

Imports System.Collections.Specialized

 

Namespace Williablog.Net.Examples.Providers

 

    Public Class SqlMembershipProvider

        Inherits System.Web.Security.SqlMembershipProvider

 

        Public Overrides Sub Initialize(ByVal name As String, ByVal config As System.Collections.Specialized.NameValueCollection)

            ' intercept the setting of the connection string so that we can set it ourselves...

            Dim specifiedConnectionString As String = config.Item("connectionStringName")

            config.Item("connectionStringName") = GetYourRunTimeConnectionStringNAme(specifiedConnectionString)

 

            ' Pass doctored config to base classes

            MyBase.Initialize(name, config)

        End Sub

    End Class

 

End Namespace

All you really need to do is override the Initialize method, change the connectionstring and then pass it to the base class. That's it! Simply set the type attribute of your provider in the web.config to your inherited class (as in Listing 1), and use this same technique on all the sqlproviders you need to use. In this example, config.Item("connectionStringName") would return "DEV.DB" and GetYourRunTimeConnectionStringName() would be a function that retrieves the right connectionstringname by checking the machinename. For this example, if it is the name of a production server it would return "Live.DB" and if it were a development machine it would return "DEV.DB" and that is the name of the connection string that would be passed to the base provider classes, and which would be used at runtime.

Of course, this technique is only viable if you can store your connection strings in the web.config. If you have to get them at runtime using an API or a webservice, you would still be better off following the directions on the Microsoft Forum, which I will reproduce here simply because links have a way of breaking over time. Download the ProviderToolkitSamples and modify the SQLConnectionHelper Class. Specifically, the GetConnectionString function which looks something like Listing 3.

Listing 3

internal static string GetConnectionString(string specifiedConnectionString, bool lookupConnectionString, bool appLevel)

        {

            if (specifiedConnectionString == null || specifiedConnectionString.Length < 1)

                return null;

 

            string connectionString = null;

 

            /////////////////////////////////////////

            // Step 1: Check <connectionStrings> config section for this connection string

            if (lookupConnectionString)

            {

                ConnectionStringSettings connObj = ConfigurationManager.ConnectionStrings[specifiedConnectionString];

                if (connObj != null)

                    connectionString = connObj.ConnectionString;

 

                if (connectionString == null)

                    return null;

            }

            else

            {

                connectionString = specifiedConnectionString;

            }

 

            return connectionString;

        }

    }

Obviously, you can change the content of this function to do whatever you need it to do in order to generate or retrieve your connection string. I tested this technique and it works beautifully with all the provided sample Providers, but you are on your own if you need a connection string from outside the web.config for your SqlPersonalizationProvider.


Posted by Williarob on Friday, May 02, 2008 4:48 PM
Permalink | Comments (1) | Post RSSRSS comment feed

A Virtual Form Web Custom Control

I recently ran into an issue where a site I was developing had form fields in the header area of the page (for logging in or searching) and that if I had my cursor in a form field further down the page and I hit the enter key, it it was the click event of the first button on the page (the button in the header, rather than the button I wanted it to use) that fired. The setup was a common one: the master page contains a form tag with a runat="server" attribute and that form wraps the entire content of the page. It all seemed to work fine, until I found myself on the registration page and instead of clicking the register button I just hit enter. Instead of registering me, a message appeared to explain that a username and password were required. Since those fields also exist on the registration form I was puzzled, more so when I used the mouse to click on the 'Register Now' button and it all worked. Stepping through the code I quickly realized what was happening, and rummaged around my code archives until I found some JavaScript I wrote a couple of years ago that would listen for the enter key, cancel the default behavior, and call another method. I added this snippet to the page and all was well, until I found another page with the same issue. It was at that point I thought "wouldn't it be nice if there was a control - like a panel control - that you could simply use to wrap some input controls, set a single property (to the id of the control that should be 'clicked' when the enter key is pushed), and that was all you needed to do?".

Well now there is such a control.

Edit: Actually, the standard asp Panel control has a "DefaultButton" property which implements similar functionality, however it only allows you to use asp button controls as your designated button. If you want to use an asp LinkButton control or some other type of control as your default button it does nothing for you. So if you are using asp Button Controls exclusively, I recommend you use that property. If not, then read on...

Using the control

You can register the control on a page by page basis as needed or you can add it to the <Pages> section of the web.config file to make it available to any page on the site (Listing 1).

Listing 1

  <pages enableSessionState="true" enableViewStateMac="true" enableEventValidation="true">

      <controls>

        <add tagPrefix="WBN" namespace="WilliaBlog.Net.Examples" assembly="WilliaBlog.Net.Examples"/>

      </controls>

  </pages>

Then add the control to the page in such a way that it wraps your inputs:

Listing 2

  <WBN:VirtualForm id="vf1" runat="server" SubmitButtonId="Button1" UseDebug="false">

    <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox> <asp:Button ID="Button1" runat="server" Text="Button1" OnClick="Button1_Click" />

  </WBN:VirtualForm>

As you can see from Listing 2 you should use the Server side ID of the button (this will be automatically converted to the clientside ID by the virtual form control. Test it live.

How It works

The actual server side Virtual Form web custom control is really very simple. It inherits System.Web.UI.WebControls.Panel and contains a property to allow you to set the id of the button or linkbutton you want to push when hitting Enter. I chose to embed the Javascript file into the dll for the sake of portability. (Originally I had it in an external .JS file that was added to the header in the master page, but if I - or somebody else - wanted to use this control on another site, that adds an additional layer of complexity in the setup - they have to remember to load that Javascript file and while we could host it in a central location for all sites to share, embedding the file in the dll seemed the wisest choice.) The downside to this technique is that the js file has to travel over the wire every time the page is requested and cannot therefore be cached on the client, which will negatively impact any low-bandwidth users of the web application. For that reason, I have used the YUI Compressor to strip out all the comments and additional whitespace and included 2 versions of the script in the dll. When you set the property UseDebug to true, it will use the long verbose version which makes it much easier to debug in firebug, but when it is all working, use the compressed version by omitting this property from the control declaration or by setting it to false.

To make files accessible from your server control’s assembly, simply add the file to your project, go to the Properties pane, and set the Build Action to Embedded Resource. To expose the file to a web request, you need to add code like lines 7 and 8 in Listing 3 below. These entries expose the embedded resource so that the ClientScriptManager can both get to the file and know what kind of file it is. You could also embed CSS, images and other types of files in this way. Note: The project’s default namespace (as defined in the Application tab of the project's Properties page) needs to be added as a prefix to the filename of embedded resources.

So, besides exposing these properties, all the control really does is override the onPreRender event and inject some Javascript into the page. Lines 75 to 79 in Listing 3 inject the link to the embedded Javascript file, which appears in the page source as something like this:

<script src="/WebResource.axd?d=j246orv_38DeKtGbza6y6A2&amp;t=633439907968639849" type="text/javascript"></script>

Next it dynamically generates a script to create a new instance of the virtual form object, passing in the clientid of the VirtualForm Server control and the clientid of the button we want it to use, and register this as a startup script on the page.

Listing 3

    1 using System;

    2 using System.Collections.Generic;

    3 using System.ComponentModel;

    4 using System.Text;

    5 using System.Web;

    6 using System.Web.UI;

    7 using System.Web.UI.WebControls;

    8 

    9 // Script Resources

   10 [assembly: WebResource("WilliaBlog.Net.Examples.VirtualForm_Debug.js", "text/javascript")]

   11 [assembly: WebResource("WilliaBlog.Net.Examples.VirtualForm_min.js", "text/javascript")]

   12 

   13 namespace WilliaBlog.Net.Examples

   14 {

   15 

   16     [ToolboxData("<{0}:VirtualForm runat=server></{0}:VirtualForm>")]

   17     public class VirtualForm : System.Web.UI.WebControls.Panel

   18     {

   19         [Bindable(true), DefaultValue("")]

   20         public string SubmitButtonId

   21         {

   22             get

   23             {

   24                 string s = (string)ViewState["SubmitButtonId"];

   25                 if (s == null)

   26                 {

   27                     return string.Empty;

   28                 }

   29                 else

   30                 {

   31                     return s;

   32                 }

   33             }

   34             set { ViewState["SubmitButtonId"] = value; }

   35         }

   36 

   37         [DefaultValue(false)]

   38         public bool UseDebug

   39         {

   40             get

   41             {

   42                 string s = (string)ViewState["UseDebug"];

   43                 if (string.IsNullOrEmpty(s))

   44                 {

   45                     return false;

   46                 }

   47                 else

   48                 {

   49                     return s.ToLower() == "true";

   50                 }

   51             }

   52             set { ViewState["UseDebug"] = value; }

   53         }

   54 

   55         public VirtualForm() : base() { }

   56 

   57         protected override void OnPreRender(System.EventArgs e)

   58         {

   59             if (!string.IsNullOrEmpty(this.SubmitButtonId))

   60             {

   61                 Control theButton = this.FindControl(this.SubmitButtonId);

   62                 if ((theButton != null))

   63                 {

   64                     string resourceName;

   65                     if (this.UseDebug)

   66                     {

   67                         resourceName = "WilliaBlog.Net.Examples.VirtualForm_Debug.js";

   68                     }

   69                     else

   70                     {

   71                         resourceName = "WilliaBlog.Net.Examples.VirtualForm_min.js";

   72                     }

   73 

   74                     ClientScriptManager cs = this.Page.ClientScript;

   75 

   76                     string scriptLocation = cs.GetWebResourceUrl(this.GetType(), resourceName);

   77                     if (!cs.IsClientScriptIncludeRegistered("VirtualFormScript"))

   78                     {

   79                         cs.RegisterClientScriptInclude("VirtualFormScript", scriptLocation);

   80                     }

   81 

   82                     // New script checks for "Sys" Object, if found events will be rewired after updatepanel refresh.

   83                     StringBuilder sbScript = new StringBuilder(333);

   84                     sbScript.AppendFormat("<script type=\"text/javascript\">{0}", Environment.NewLine);

   85                     sbScript.AppendFormat("    // Ensure postback works after update panel returns{0}", Environment.NewLine);

   86                     sbScript.AppendFormat("    function ResetEventsForMoreInfoForm() {{{0}", Environment.NewLine);

   87                     sbScript.AppendFormat("        var vf_{0} = new WilliaBlog.Net.Examples.VirtualForm(document.getElementById('{0}'),'{1}');{2}", this.ClientID, theButton.ClientID, Environment.NewLine);

   88                     sbScript.AppendFormat("    }}{0}", Environment.NewLine);

   89                     sbScript.AppendFormat("    if (typeof(Sys) !== \"undefined\"){{{0}", Environment.NewLine);

   90                     sbScript.AppendFormat("        Sys.WebForms.PageRequestManager.getInstance().add_endRequest(ResetEventsForMoreInfoForm);{0}", Environment.NewLine);

   91                     sbScript.AppendFormat("    }}{0}", Environment.NewLine);

   92                     sbScript.AppendFormat("    var vf_{0} = new WilliaBlog.Net.Examples.VirtualForm(document.getElementById('{0}'),'{1}');{2}", this.ClientID, theButton.ClientID, Environment.NewLine);

   93                     sbScript.AppendFormat("</script>");

   94 

   95                     string scriptKey = string.Format("initVirtualForm_" + this.ClientID);

   96 

   97                     if (!cs.IsStartupScriptRegistered(scriptKey))

   98                     {

   99                         cs.RegisterStartupScript(this.GetType(), scriptKey, sbScript.ToString(), false);

  100                     }

  101                 }

  102             }

  103             base.OnPreRender(e);

  104         }

  105     }

  106 }

The JavaScript

Most of the code is of course JavaScript. Lines 13 to 62 simply create the WilliaBlog Namespace and I 'borrowed' it from the The Yahoo! User Interface Library (YUI). The WilliaBlog.Net.Examples.VirtualForm object begins on line 65. Essentially, it loops through every input control (lines 159-164) within the parent Div (the id of this div is passed as an argument to the contructor function) and assigns an onkeypress event (handleEnterKey) to each of them. All keystrokes other than the enter key pass through the code transparently, but as soon as enter is detected, the default behavior is cancelled and the submitVirtual function is called instead. That function simply checks to see if the button you supplied is an input (image or submit button) or an anchor (Hyperlink or link button), and simulates a click on it, either by calling the click() method of the former or by navigating to the href property of the latter. The removeEvent and stopEvent methods are never actually called, but I included them for good measure.

Listing 4

    1 /****************************************************************************************************************************  

    2 *

    3 * File    : VirtualForm_Debug.js

    4 * Created : April 08

    5 * Author  : Rob Williams

    6 * Purpose : This is the fully annotated, easy to understand and modify version of the file, however I would recommend you use

    7 * something like the YUI Compressor (http://developer.yahoo.com/yui/compressor/) to minimize load time.

    8 * This file has its Build Action Property set to "Embedded Resource" which embeds the file inside the dll, so we never have to

    9 * worry about correctly mapping a path to it.

   10 *

   11 *****************************************************************************************************************************/

   12 

   13 if (typeof WilliaBlog == "undefined" || !WilliaBlog) {

   14     /**

   15     * The WilliaBlog global namespace object.  If WilliaBlog is already defined, the

   16     * existing WilliaBlog object will not be overwritten so that defined

   17     * namespaces are preserved.

   18     * @class WilliaBlog

   19     * @static

   20     */

   21     var WilliaBlog = {};

   22 }

   23 

   24 /**

   25  * Returns the namespace specified and creates it if it doesn't exist

   26  * <pre>

   27  * WilliaBlog.namespace("property.package");

   28  * WilliaBlog.namespace("WilliaBlog.property.package");

   29  * </pre>

   30  * Either of the above would create WilliaBlog.property, then

   31  * WilliaBlog.property.package

   32  *

   33  * Be careful when naming packages. Reserved words may work in some browsers

   34  * and not others. For instance, the following will fail in Safari:

   35  * <pre>

   36  * WilliaBlog.namespace("really.long.nested.namespace");

   37  * </pre>

   38  * This fails because "long" is a future reserved word in ECMAScript

   39  *

   40  * @method namespace

   41  * @static

   42  * @param  {String*} arguments 1-n namespaces to create

   43  * @return {Object}  A reference to the last namespace object created

   44  */

   45 WilliaBlog.RegisterNamespace = function() {

   46     var a=arguments, o=null, i, j, d;

   47     for (i=0; i<a.length; i=i+1) {

   48         d=a[i].split(".");

   49         o=WilliaBlog;

   50 

   51         // WilliaBlog is implied, so it is ignored if it is included

   52         for (j=(d[0] == "WilliaBlog") ? 1 : 0; j<d.length; j=j+1) {

   53             o[d[j]]=o[d[j]] || {};

   54             o=o[d[j]];

   55         }

   56     }

   57 

   58     return o;

   59 };

   60 

   61 //declare the 'WilliaBlog.Net.Examples' Namespace

   62 WilliaBlog.RegisterNamespace("WilliaBlog.Net.Examples");

   63 

   64 //declare Virtual Form Object

   65 WilliaBlog.Net.Examples.VirtualForm = function(formDiv,submitBtnId)

   66 {

   67     this.formDiv = formDiv; //The id of the div that represents our Virtual Form

   68     this.submitBtnId = submitBtnId;   //The id of the button or Linkbutton that should be clicked when pushing Enter

   69 

   70     // When using these functions as event delegates the this keyword no longer points to this object as it is out of context

   71     // so instead, create an alias and call that instead.

   72     var me = this;

   73 

   74     this.submitVirtual = function()

   75     {

   76         var target = document.getElementById(me.submitBtnId);

   77         //check the type of the target: If a button then call the click method.

   78         if(target.tagName.toLowerCase() === 'input')

   79         {

   80             document.getElementById(me.submitBtnId).click();

   81         }

   82         //If a link button then simulate a click.

   83         if(target.tagName === 'A')

   84         {

   85             window.location.href = target.href;

   86         }

   87     };

   88 

   89     this.handleEnterKey = function(event){ 

   90         var moz = window.Event ? true : false;

   91         if (moz) {

   92             return me.MozillaEventHandler_KeyDown(event);

   93         } else {

   94             return me.MicrosoftEventHandler_KeyDown();

   95         }

   96     };

   97 

   98     //Mozilla handler (also Handles Safari)

   99     this.MozillaEventHandler_KeyDown = function(e)

  100     {

  101         if (e.which == 13) {

  102             e.returnValue = false;

  103             e.cancel = true;

  104             e.preventDefault();           

  105             me.submitVirtual(); // call the delegate function that simulates the correct button click

  106             return false;       

  107         }

  108         return true;

  109     };

  110 

  111     //IE Handler

  112     this.MicrosoftEventHandler_KeyDown = function()

  113     {

  114         if (event.keyCode == 13) {

  115             event.returnValue = false;

  116             event.cancel = true;

  117             me.submitVirtual(); // call the delegate function that simulates the correct button click

  118             return false;

  119         }

  120         return true;

  121     };

  122 

  123     this.addEvent = function(ctl, eventType, eventFunction)

  124     {

  125         if (ctl.attachEvent){

  126             ctl.attachEvent("on" + eventType, eventFunction);

  127         }else if (ctl.addEventListener){

  128             ctl.addEventListener(eventType, eventFunction, false);

  129         }else{

  130             ctl["on" + eventType] = eventFunction;

  131         }

  132     };

  133 

  134     this.removeEvent = function(ctl, eventType, eventFunction)

  135     {

  136         if (ctl.detachEvent){

  137             ctl.detachEvent("on" + eventType, eventFunction);

  138         }else if (ctl.removeEventListener){

  139             ctl.removeEventListener(eventType, eventFunction, false);

  140         }else{

  141             ctl["on" + eventType] = function(){};

  142         }

  143     };

  144 

  145     this.stopEvent = function(e)

  146     {

  147         if (e.stopPropagation){

  148         // for DOM-friendly browsers

  149             e.stopPropagation();

  150             e.preventDefault();

  151         }else{

  152         // For IE

  153             e.returnValue = false;

  154             e.cancelBubble = true;

  155         }

  156     };

  157 

  158     //Grab all input elements within virtual form (contents of a div with divID)

  159     this.inputs = this.formDiv.getElementsByTagName("input");

  160 

  161     //loop through them and add the keypress event to each to listen for the enter key

  162     for (var i = 0; i < this.inputs.length; i++){

  163         this.addEvent(this.inputs[i],"keypress",this.handleEnterKey);

  164     }

  165 }


Posted by Williarob on Monday, April 28, 2008 8:31 AM
Permalink | Comments (0) | Post RSSRSS comment feed