Threading with Impersonation in an ASP.NET Project

Add to FacebookAdd to DiggAdd to Del.icio.usAdd to StumbleuponAdd to RedditAdd to BlinklistAdd to TwitterAdd to TechnoratiAdd to Yahoo BuzzAdd to Newsvine

Every once in a while, you might run into a need to do something that takes some time in a web app, but doesn’t require user interaction. Maybe you are processing an uploaded file (rescaling images, unzipping, etc). Maybe you are rewriting some statistical data based on new posts. Basically, something that takes minutes or hours – but isn’t that important to be interactive with the user.

You could set up a “job” in a database to be run the next time your timer runs (see https://lanitdev.wordpress.com/2010/03/16/running-a-scheduled-task/). If you don’t have a timer yet, though, that can be overkill if you don’t care that multiple jobs may run at once.

In my case, I needed to export a large volume of data to a zip file. I asked up front for an email address – and the user will receive a link to the completed zip in an email later. The job would only be performed by admins, and even then only about once a year – so there was no need to schedule the job – I could just fire it off when the user requested it.

Any easy way to do this is to use the .NET threading objects in System.Threading. Because I need to save a file, I also have one additional issue – Threads don’t automatically run under the same account that the site does, so I had to include code to impersonate a user that has write permissions.

Here’s a bit of code to get you started:

// param class to pass multiple values
private class ExportParams
        {
            public int UserID { get; set; }
            public string Email { get; set; }
            public string ImpersonateUser { get; set; }
            public string ImpersonateDomain { get; set; }
            public string ImpersonatePassword { get; set; }
        }

        protected void btnExport_Click(object sender, EventArgs e)
        {
//  .... code to get current app user, windows user to impersonate .....

            Thread t = new Thread(new ParameterizedThreadStart(DoExport));
            t.Start(new ExportParams(){
                UserID=CurrentUserID,
                Email=txtEmail.Text,
                ImpersonateUser = username,
                ImpersonateDomain = domain,
                ImpersonatePassword = password
            });
             // show user 'processing' message .....
         }

        private void DoExport(object param)
        {
            ExportParams ep = (ExportParams)param;

            using(var context = Security.Impersonate(ep.ImpersonateUser , ep.ImpersonateDomain,
             ep.ImpersonatePassword ))
          {
            // do the work here..............
          }
        }

Here’s the relevant part of the Security class that does the impersonation:

using System.Runtime.InteropServices;
using System.Security.Principal;
// .....
public class Security {
//.............
public const int LOGON_TYPE_INTERACTIVE = 2;
        public const int LOGON_TYPE_PROVIDER_DEFAULT = 0;
        // Using this api to get an accessToken of specific Windows User by its user name and password
        [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        static public extern bool LogonUser(string userName, string domain, string passWord, int logonType, int logonProvider, ref IntPtr accessToken);

        public static WindowsImpersonationContext Impersonate()
        {
            return Impersonate("DEFAULT_USER","DEFAULT_DOMAIN","DEFAULT_PASSWORD");
        }
        public static WindowsImpersonationContext Impersonate(string username, string domain, string password)
        {
            IntPtr accessToken = IntPtr.Zero;
            //accessToken.Debug();
            var success = LogonUser(username, domain, password, LOGON_TYPE_INTERACTIVE, LOGON_TYPE_PROVIDER_DEFAULT, ref accessToken);

            //accessToken.Debug();
            if (!success)
                return null;

            WindowsIdentity identity = new WindowsIdentity(accessToken);

            return identity.Impersonate();
        }
// ..........
}
Advertisements

Using the Web.Config connection string with LINQ to SQL

When updating a project to use LINQ to SQL, I found an issue with deploying to multiple environments.  Each environment (development, staging, live) had its’ own database associated with this.  Since I had the .dbml in another assembly, it was only reading from the app.config in the assembly it resided in.  I was storing the database connection string in the web.config of the project so I thought it would be nice to just use that instead of the app.config.

The first thing I needed to do was to keep the .dbml file from reading from the app.config.  After opening up the .dbml file, I opened the properties window for the file.  In the properties window, there is a setting for “Connection”.  In the “Connection” dropdown I selected the “(None)” selection.  That keeps the .dbml file from accessing the app.config for the database connection string.



The "Connection" setting in the .dbml Properties

Now I needed to get my MainDataContext to use the Web.Config connection string.  For this I created a partial class for my MainDataContext and created a constructor that passed the connection string from the Web.Config.

public partial class MainDataContext
{
    public MainDataContext()
        : base(System.Configuration.ConfigurationManager.ConnectionStrings["Database.connection.string.from.web.config"].ToString(), mappingSource)
    {
        OnCreated();
    }
}

Now when I deploy to different environments the .dbml file is accessing the correct database instead of the same one from the app.config.

Handy ASP.NET Debug Extension Method

Add to FacebookAdd to DiggAdd to Del.icio.usAdd to StumbleuponAdd to RedditAdd to BlinklistAdd to TwitterAdd to TechnoratiAdd to Yahoo BuzzAdd to Newsvine

Most of the programmers I know (myself included) don’t bother with the built in Visual Studio debugging tools. They are slow and resource intensive. Usually, its more efficient to just do one or more Response.Write calls to see key data at key steps.

That can be a hassle, though. Most objects don’t print very well. You have to create a loop or write some LINQ/String.Join to write items in a collection.

Inspiration struck – couldn’t I write an extension method on object to write out a reasonable representation of pretty much anything? I could write out html tables for lists with columns for properties, etc.

Then I thought – I love the javascript debug console in firebug. I can drill down into individual items without being overwhelmed by all of the data at once. Why not have my debug information spit out javascript to write to the debug console? That also keeps it out of the way of the rest of the interface.

Here’s the code:

public static void Debug(this object value)
        {
            if (HttpContext.Current != null)
            {
                HttpContext.Current.Response.Debug(value);
            }

        }

        public static void Debug(this HttpResponse Response, params object[] args)
        {

            new HttpResponseWrapper(Response).Debug(args);
        }
        public static void Debug(this HttpResponseBase Response, params object[] args)
        {

            ((HttpResponseWrapper)Response).Debug(args);
        }
        public static void Debug(this HttpResponseWrapper Response, params object[] args)
        {

            if (Response != null && Response.ContentType == "text/html")
            {
                Response.Write("<script type='text/javascript'>");
                Response.Write("if(console&&console.debug){");

                Response.Write("console.debug(" +
                              args.SerializeToJSON() +
                               ");");
                Response.Write("}");
                Response.Write("</script>");
            }
        }

The various overloads allow:

myObject.Debug();
new {message="test",obj=myObject}.Debug();
Response.Debug("some message",myObject,myObject2);
//etc

The only other thing you’ll need is the awesome JSON.NET library for the .SerializeToJSON() call to work (which turns the .NET object into the form javascript can deal with). Get it here. FYI, the library does choke serializing some complex objects, so occasionally you’ll need to simplify before calling debug.

Running a scheduled task inside of a asp.net web application

Add to FacebookAdd to DiggAdd to Del.icio.usAdd to StumbleuponAdd to RedditAdd to BlinklistAdd to TwitterAdd to TechnoratiAdd to Yahoo BuzzAdd to Newsvine

Occasionally, there might be some activity in your web application that should trigger the need for some code to execute at a later date. One of the most common cases is sending an email (reminder, change digest, etc), but there are other uses too. You might want to defer some processing intensive activity to off hours. In my most recent case, I needed to check for changes to an purchased/installed application’s db and replicate them to another product every 5 minutes or so.

I’ve solved similar problems before by:

  1. From the web application, inserting a row in a ‘scheduled tasks’ database with a time to execute and a script url to run
  2. Creating an running a windows service somewhere that wakes up every 5 minutes or so, looks at the table for ‘due’ items, and opens a request to the url

This works, but it has some drawbacks.

  • You have to learn how to build and deploy a service.  Not particularly hard, but something a web developer doesn’t really need to know
  • You have to copy or figure out how to share some data access logic between the service and the web application, and maintain changes
  • You have to figure out where to deploy the service
  • You have to have somewhere to deploy the service – if you are using a shared webhost with no RDP you are out of luck
  • It’s hard to be sure the service is running.  It’s easy to forget about if your infrastructure changes.
  • You need to deal with it when the service errors.
  • You have to be careful that the service doesn’t run the same script twice (or make it so it doesn’t hurt anything if it does), in case it gets run on two machines, etc.
  • Many more tiny, but real headaches for dealing with and maintaining a separate but connected project

I really didn’t want to go through all of that again for yet another project.  There had to be a simpler solution.

Thanks to Google, I found this article that led me to use a Cache object expiration to simulate the service.  It’s a hack, but it solved my issue.

Later, I found this StackOverflow post about the same issue/fix.  The comments led me to a System.Timers.Timer solution, which is easier to understand. Here it is:

The global.asax:

     public const int MINUTES_TO_WAIT = 5;

    private string _workerPageUrl = null;
    protected string WorkerPageUrl
    {
        get
        {
            if (_workerPageUrl == null)
                _workerPageUrl = (Application["WebRoot"] + VirtualPathUtility.ToAbsolute("~/DoTimedWork.ashx")).Replace("//", "/").Replace(":/", "://") + "?schedule=true";


            return _workerPageUrl;
        }
    }


    protected void Application_Start(Object sender, EventArgs e)
    {
        Uri reqUri = HttpContext.Current.Request.Url;
        Application["WebRoot"] = new UriBuilder(reqUri.Scheme, reqUri.Host, reqUri.Port).ToString();
        Application["TimerRunning"] = false;

        //StartTimer();   // don't want timer to start unless we call the url (don't run in dev, etc).  Url will be called by montastic for live site.
    }

    private void StartTimer()
    {
        if (!(bool)Application["TimerRunning"]) // don't want multiple timers
        {
            System.Timers.Timer timer = new System.Timers.Timer(MINUTES_TO_WAIT * 60 * 1000);
            timer.AutoReset = false;
            timer.Enabled = true;
            timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);
            timer.Start();
            Application["TimerRunning"] = true;
        }

    }

    void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        Application["TimerRunning"] = false;
        System.Net.WebClient client = new System.Net.WebClient();
        // have to issue a request so that there is a context
        // also lets us separate all of the scheduling logic from the work logic
        client.DownloadData(WorkerPageUrl + "&LastSuccessfulRun=" + Server.UrlEncode((CurrentSetting.LastSuccessfulRun ?? DateTime.Now.AddYears(-1)).ToString()));
    }

    protected void Application_BeginRequest(Object sender, EventArgs e)
    {
        if (HttpContext.Current.Request.Path == GetLocalUrl(new Uri(WorkerPageUrl)))
        {
            CurrentSetting.LastRun = DateTime.Now;
            try
            {
                CurrentSetting.RunCount++;
            }
            catch (Exception)
            {
                CurrentSetting.RunCount = 0;  // just in case of an overflow
            }
            SaveSettings();
        }
    }
    protected void Application_EndRequest(Object sender, EventArgs e)
    {
        if (HttpContext.Current.Request.Path == GetLocalUrl(new Uri(WorkerPageUrl)))
        {
            if (HttpContext.Current.Error == null) //
            {
                CurrentSetting.LastSuccessfulRun = DateTime.Now;
                SaveSettings();
            }

            if (HttpContext.Current.Request["schedule"] == "true")// register the next iteration whenever worker finished
                StartTimer();
        }
    }

    void Application_Error(object sender, EventArgs e)
    {
        if (HttpContext.Current.Request.Path == GetLocalUrl(new Uri(WorkerPageUrl)))
        {
            Common.LogException(HttpContext.Current.Error.GetBaseException());
        }

    }
    
    protected class Setting
    {
        public DateTime? LastRun { get; set; }
        public DateTime? LastSuccessfulRun { get; set; }
        public long RunCount { get; set; }
    }
    Setting currentSetting = null;
    protected Setting CurrentSetting
    {
        get
        {
            if (currentSetting == null)
            {
                using (System.Security.Principal.WindowsImpersonationContext imp = Common.Impersonate())
                {
                    System.IO.FileInfo f = new System.IO.FileInfo(HttpContext.Current.Server.MapPath("~/data/settings.xml"));
                    if (f.Exists)
                    {
                        System.Xml.Linq.XDocument doc = System.Xml.Linq.XDocument.Load(f.FullName);
                        currentSetting = (from s in doc.Elements("Setting")
                                          select new Setting()
                                          {
                                              LastRun = DateTime.Parse(s.Element("LastRun").Value),
                                              LastSuccessfulRun = DateTime.Parse(s.Element("LastSuccessfulRun").Value),
                                              RunCount = long.Parse(s.Element("RunCount").Value)
                                          }).First();

                    }
                }
            }

            if (currentSetting == null)
            {
                currentSetting = new Setting()
                {
                    LastRun = null,
                    LastSuccessfulRun = DateTime.Now.AddYears(-1),//ignore older than one year old in test
                    RunCount = 0
                };
            }

            return currentSetting;
        }
        set
        {
            currentSetting = value;
            if (Common.Live)
            {
                using (System.Security.Principal.WindowsImpersonationContext imp = Common.Impersonate())
                {
                    System.IO.DirectoryInfo di = new System.IO.DirectoryInfo(HttpContext.Current.Server.MapPath("~/data"));
                    if (!di.Exists)
                        di.Create();
                    System.Xml.XmlWriter writer = System.Xml.XmlWriter.Create(HttpContext.Current.Server.MapPath("~/data/settings.xml"));
                    try
                    {
                        System.Xml.Linq.XDocument doc = new System.Xml.Linq.XDocument(
                                new System.Xml.Linq.XElement("Setting",
                                    new System.Xml.Linq.XElement("LastRun", currentSetting.LastRun ?? DateTime.Now),
                                    new System.Xml.Linq.XElement("LastSuccessfulRun", currentSetting.LastSuccessfulRun),
                                    new System.Xml.Linq.XElement("RunCount", currentSetting.RunCount)
                                    )
                            );
                        doc.WriteTo(writer);
                    }
                    catch (Exception exc)
                    {
                        Common.LogException(exc);
                    }
                    finally
                    {
                        writer.Flush();
                        writer.Close();
                    }
                }
            }
        }
    }
    protected void SaveSettings()
    {
        CurrentSetting = CurrentSetting; // reset to ensure "setter" code saves to file
    }




 

    private string GetLocalUrl(Uri uri)
    {
        string ret = uri.PathAndQuery;
        if (uri.Query != null && uri.Query.Length>0)
            ret = ret.Replace(uri.Query, "");

        return ret;
    }

DoTimedWork.ashx:

 protected DateTime LastSuccessfulRun
    {
        get
        {
            try
            {
                return DateTime.Parse(HttpContext.Current.Request["LastSuccessfulRun"]);
            }
            catch (Exception) { }
            return DateTime.Now.AddDays(-1);
        }
    }
    
    public void ProcessRequest(HttpContext context)
    {
        if (context.Request["dowork"] != "false") // don't do work if it's just motastic hitting the page (to make sure the  timer is running)
        {
                context.Server.ScriptTimeout = 1800; // 30 minutes

                // do work
        }
        context.Response.Write("done");
    }
 
    public bool IsReusable {
        get {
            return false;
        }
    }

Common.cs

using System.Runtime.InteropServices;
using System.Security.Principal;

    public static  bool Live
    {
        get
        {
            return HttpContext.Current.Request.Url.Host != "localhost";
        }
    }
    public const int LOGON_TYPE_INTERACTIVE = 2;
    public const int LOGON_TYPE_PROVIDER_DEFAULT = 0;
    // Using this api to get an accessToken of specific Windows User by its user name and password
    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    static public extern bool LogonUser(string userName, string domain, string passWord, int logonType, int logonProvider, ref IntPtr accessToken);

    public static WindowsImpersonationContext Impersonate() //run code as a windows user with permissions to write files, etc.
    {

        IntPtr accessToken = IntPtr.Zero;
        LogonUser("REPLACE_WITH_WINDOWS_USER", "", "REPLACE_WITH_WINDOWS_PASSWORD", LOGON_TYPE_INTERACTIVE, LOGON_TYPE_PROVIDER_DEFAULT, ref accessToken);

        WindowsIdentity identity = new WindowsIdentity(accessToken);

        return identity.Impersonate();
    }
    public static void LogException(Exception exc)
    {
        LogActivity(exc.Message + "\n" + exc.StackTrace);
    }
    public static void LogActivity(string message)
    {
        if (Live)
        {
            using (WindowsImpersonationContext imp = Impersonate())
            {
                DirectoryInfo d = new DirectoryInfo(HttpContext.Current.Server.MapPath("~/data/temp/"));
                if (!d.Exists)
                    d.Create();
                var file = File.Create(HttpContext.Current.Server.MapPath("~/data/temp/" + DateTime.Now.Ticks + ".log"));
                try
                {
                    byte[] m = System.Text.Encoding.ASCII.GetBytes(message + "\n");
                    file.Write(m, 0, m.Length);
                }
                catch (Exception exc)
                {
                    byte[] m = System.Text.Encoding.ASCII.GetBytes(exc.Message + "\n" + exc.StackTrace);
                    try
                    {
                        file.Write(m, 0, m.Length);
                    }
                    catch (Exception) { }
                }
                finally
                {
                    file.Flush();
                    file.Close();
                }
            }
        }
        else
        {
            HttpContext.Current.Response.Write(message);
        }
    }

There are some issues with this approach to consider.

  • for very high usage sites or very intensive timed work, the work may put burden you wouldn’t want on your webserver
  • Application_Start only runs after the first request to your site after an app pool recycle, IIS restart, server restart, etc. If your site goes through periods of inactivity, you may or may not care if the Timer executes. If you do, you need to ensure the site is hit regularly in some way. I use the website monitor montastic for this.

So, there are going to be circumstances that make the windows service solution better. You just need to decide whether the benefits of using a service outweigh the pain of developing, maintaining, and deploying it alongside your web application.

Easy “No Item Template” In Repeater

A “No Item Template” is some content that you want to display if a Repeater is bound with no items. There is no built in tag to handle empty data in the repeater in markup. But, it is easy to add a “No Item Template” inside your markup without having to extend the Repeater class.

Simply set the Visibility property of your content to the number of items bound to the repeater inside the FooterTemplate tag. Instead of relying on the ID of the Repeater in the conditional, I just cast Container.Parent as a Repeater. This is better than the alternative because:

  • There is less repeated markup in case the ID changes.
  • You can use the snippet inside of naming containers such as nested Repeaters, to avoid the error: “The name ‘repItems’ does not exist in the current context”

Here is what it looks like:

<asp:Repeater id="repItems" runat="server">
	<FooterTemplate>
		<asp:Label Text="No Items Found" runat="server" 
			Visible='<%# ((Repeater)Container.Parent).Items.Count == 0 %>' />
	</FooterTemplate>
	<ItemTemplate></ItemTemplate>
</asp:Repeater>

Pretty simple trick, but this feature is not something that I want to have to add extra code for and event handlers for, and the snippet does the job.

Really Nice Looking Export to Excel using Table Formatting

Inevitably, someone is going to want your data exportable to an Excel format, no matter how many different ways you let them look at right on your site–and they’re going to want it to look really nice.

If you’re just going to a CSV format, it’s pretty easy to do, but if it opens in Excel, none of the cells are formatted initially.  It can look good eventually, but you have to manually format it after the export.  It would sure be nice if you could format it better from the code itself.  For me, I thought it would be too tedious and time-consuming to actually create the Excel Spreadsheet.  I wanted something quicker.

Using a DataGrid for the bulk of the actual data works well as it renders as a table, and Excel seems to work well with table formats.  I used an HtmlTextWriter for this purpose.

HtmlTextWriter hw
dg.RenderControl(hw)

However, the DataGrid wasn’t the only thing I wanted to export.  What I wanted was a nice big heading that was bolded, nice subheadings, nice spacing, and a bunch of things like that.  What I found is that if I just wrote table structure syntax, I could do basically everything I wanted.

What I did here was to write a large, centered heading that spanned 8 columns (the entire width of my report).  Both the “th” and the “h1” tags impact the way it renders in Excel.  I added the colspan and alignment attributes on the th tag, and it worked great.  Then I added a blank link by simply writing a br tag.  Here’s the code:

hw.WriteLine("<table><tr><th colspan=\"8\" align=\"center\"><h1>My Nice Looking Report</h1></th></tr></table>");
hw.WriteLine("<br>");

The rendering just follows down the page pretty nicely.  I added subheaders with h2 tags, and I played around with color and other attributes as well.  Then, after you export to Excel, the moment you open it up, all the formatting is there.  It looks pretty nice–particularly so in product demos.  Even if clients opt for csv format so they can put it into reporting software, the look that this provides gives great curb appeal.

Here’s the code for the sub heading and the legend for the report.  The term takes up two cells, and the description is confined to the other 6 cells (just because the total width was 8 cells/td’s).

hw.WriteLine("<table>");
hw.WriteLine("<tr><td colspan=\"8\"><h3>Report Legend</h3></td></tr>");

hw.WriteLine("<tr><td colspan='2'>Incomplete Evaluations</td><td colspan='6'>Evaluations assigned but not been completed.</td></tr>");
hw.WriteLine("<tr><td colspan='2'>Completed Evaluations</td><td colspan='6'>Evaluations assigned that have been completed.</td></tr>");
hw.WriteLine("<tr><td colspan='2'>Request Reviews</td><td colspan='6'>Total number of requests for reviews.</td></tr>");
hw.WriteLine("<tr><td colspan='2'>Unfulfilled Request Reviews</td><td colspan='6'>Total number of requests not yet reviewed.</td></tr>");
hw.WriteLine("<tr><td colspan='2'>Request Responses</strong>/td><td colspan='6'>Total number of request review responses. </td></tr>");

hw.WriteLine("</table>");
hw.WriteLine("<br>");

Again, this works really well for me. It allows me to easily control the format without actually creating an Excel Spreadsheet in the code itself.

Extended DropDownList Control

Add to FacebookAdd to DiggAdd to Del.icio.usAdd to StumbleuponAdd to RedditAdd to BlinklistAdd to TwitterAdd to TechnoratiAdd to Yahoo BuzzAdd to Newsvine

It’s a very common situation.  A dropdownlist has a parent whose selected value determines what options will be bound to it.  But what happens when there are none?  And what happens if there’s only one option?  And why do I have to enter code to insert “Select an Item” to the list?  And why does it continue to display even when no item has been selected on its parent and it doesn’t have any values at all?  And why does it continue to display immediately after the parent’s value has been changed and the page is reloading?

Well, what if it wasn’t that way?  What if it accounted for all those things?  That would be really nice, and, in fact, it actually is quite nice.

This extended dropdownlist control has the following additional parameters:

  • ParentDropDownListID – Specifies the id field of the parent dropdownlist.
  • ParentItemUnpopulatedText – Specifies the text to display when the parent dropdownlist has yet to be populated.  The dropdownlist itself will be hidden and this text appears in its place, giving the user appropriate feedback.  The default is “n/a” since this dropdownlist really isn’t applicable until its parent is populated.  (This comes up in a series of three or parent/child dropdownlists.)
  • ParentItemNotSelectedText -Specifies the text to display when the parent dropdownlist has been populated but no item has yet been selected (The selected item is “Select an item”.)  So, for a state/city relationship it could say “Please select a state” or “n/a”–whatever you want.  But the dropdownlist itself is hidden, so the user can’t select it and wonder why there’s nothing in it to select.
  • ControlsToHideOnChange – This actually specifies children dropdownlists to hide whenever the selected item changes.  In a state/city relationship, if you change the state, the city dropdownlist will need to be repopulated.  However, sometimes the page takes a little while to load and the user may try to select a city before that gets repopulated.  To avoid the confusion, the city dropdownlist, whose ID is specified in this parameter, will be immediately hidden via JavaScript and a message will appear in its place indicating that the user needs to wait until it is repopulated.  You can actually specify multiple children to hide, so if you had county and city children, both would be hidden while the page reloads.  The text is specified in the ChildrenRepopulatingText.
  • ChildrenRepopulatingText – The text that replaces the child controls specified in ControlsToHideOnChange whenever the selected item for this control (the parent) changes.
  • HasSelectedItemText – true/false – defines whether or not a “Select Item” option should be added to the dropdownlist.  I will probably eliminate this and just add one based on whether or not the SelectItemText parameter is specified.
  • SelectItemText – Specifies the text to add as the first item (with a value of -1) to the dropdownlist.
  • NoItemsText – Specifies the message to display if there are no items in the dropdownlist, since an empty dropdownlist is annoying.  The actual dropdownlist will be hidden and this text will display.  So, this could be “There were no cities defined for the state you selected.”

Additionally, if there is only one item bound to the dropdownlist, the dropdownlist goes ahead and selects that item.  The dropdownlist itself is hidden, and the item text is displayed as a message.  It is just annoying when you have a dropdownlist with a single item, but you still have to select it because you have a “Select Item” option that is the initially selected item.

So, that’s the extended control as it is now.  Hopefully, it will save coding time and provide a better overall user experience.
NOTE: The control as it is written does use Jquery commands as well, so if you’re not referencing that, you’ll need to rewrite the JavaScript.


public class ExtendedDropDownList : DropDownList
{
    public bool HasSelectItemText
    {
        get
        {
            object o = ViewState["HasSelectItemText"];
            if (o == null)
                return false;
            else
                return (bool)o;
        }
        set
        {
            ViewState["HasSelectItemText"] = value;
        }
    }

    public string SelectItemText
    {
        get
        {
            object o = ViewState["SelectItemText"];
            if ((o == null) || ((string)o == ""))
                return "Select an Item";
            else
                return (string)o;
        }
        set
        {
            ViewState["SelectItemText"] = value;
        }
    }

    public string NoItemsText
    {
        get
        {
            object o = ViewState["NoItemsText"];
            if ((o == null) || ((string)o == ""))
                return "No items available to select";
            else
                return (string)o;
        }
        set
        {
             ViewState["NoItemsText"] = value;
        }
    }

    public string ParentDropDownListID
    {
        get
        {
            if (ViewState["ParentDropDownListID"] == null)
                ViewState["ParentDropDownListID"] = "";
            return (string)ViewState["ParentDropDownListID"];
        }
        set
        {
            ViewState["ParentDropDownListID"] = value;
        }
    }

    public string ParentItemNotSelectedText
    {
        get
        {
            if (ViewState["ParentItemNotSelectedText"] == null)
                ViewState["ParentItemNotSelectedText"] = "";
            return (string)ViewState["ParentItemNotSelectedText"];
        }
        set
        {
            ViewState["ParentItemNotSelectedText"] = value;
        }
    }

    public string ParentItemUnpopulatedText
    {
        get
        {
            if (ViewState["ParentItemUnpopulatedText"] == null)
                ViewState["ParentItemUnpopulatedText"] = "n/a";
            return (string)ViewState["ParentItemUnpopulatedText"];
        }
        set
        {
            ViewState["ParentItemUnpopulatedText"] = value;
        }
    }

    public string ControlsToHideOnChange
    {
        get
        {
            if (ViewState["ControlsToHideOnChange"] == null)
                ViewState["ControlsToHideOnChange"] = "";
            return (string)ViewState["ControlsToHideOnChange"];
        }
        set
        {
            ViewState["ControlsToHideOnChange"] = value;
        }
    }

    public string ChildrenRepopulatingText
    {
        get
        {
            if (ViewState["ChildrenRepopulatingText"] == null)
                ViewState["ChildrenRepopulatingText"] = "Repopulating Options...";
            return (string)ViewState["ChildrenRepopulatingText"];
        }
        set
        {
            ViewState["ChildrenRepopulatingText"] = value;
        }
    }

    public string[] ControlsToHide
    {
        get
        {
            return ControlsToHideOnChange.Split(',');
        }
    }

    public override void DataBind()
    {
        base.DataBind();

        int itemCount = this.Items.Count;
        if (HasSelectItemText && itemCount > 1)
            this.Items.Insert(0, new ListItem(SelectItemText, "-1"));
        if (itemCount == 0)
            this.SelectedIndex = -1;
        else
            this.SelectedIndex = 0;
    }

    protected override void CreateChildControls()
    {
        base.CreateChildControls();
        if (ControlsToHideOnChange != "")
        {
            ServerPage page = GetPage();
            foreach (string s in ControlsToHide)
            {
                this.Attributes["onchange"] = "$('#" + page.FindControl(s).ClientID + "').hide();" + "$('#" + page.FindControl(s).ClientID + "').parent().text('" + ChildrenRepopulatingText + "');" + this.Attributes["onchange"];
            }
        }
    }

    protected override void Render(System.Web.UI.HtmlTextWriter writer)
    {
        EnsureChildControls();

        DropDownList parent = (DropDownList)GetPage().FindControl(ParentDropDownListID);
        bool hasUnselectedParent = (ParentDropDownListID != "") && (parent != null) && (parent.Items.Count > 0) && (parent.SelectedValue == "-1");
        bool hasUnpopulatedParent = (ParentDropDownListID != "") && (parent != null) && (parent.Items.Count == 0);

        writer.WriteLine("<div>");

        if (hasUnselectedParent)
        {
            this.Items.Clear();
            writer.WriteLine("<span>" + ParentItemNotSelectedText + "</span>");
            this.Attributes.CssStyle["display"] = "none";
        }
        else if (hasUnpopulatedParent)
        {
            this.Items.Clear();
            writer.WriteLine("<span>" + ParentItemUnpopulatedText + "</span>");
            this.Attributes.CssStyle["display"] = "none";
        }
        else
        {
            if (this.Items.Count == 0)
            {
                writer.WriteLine("<span>" + NoItemsText + "</span>");
                this.CssClass = "hide";
            }
            else if (this.Items.Count == 1)
            {
                writer.WriteLine("<span>" + this.Items[0].Text + "</span>");
                this.CssClass = "hide";
            }
        }

        base.Render(writer);

        writer.WriteLine("</div>");
    }

    private Foliotek.Components.ServerPage GetPage()
    {
        System.Web.UI.Control control = this;
        while (control.GetType() != Type.GetType("Foliotek.Components.ServerPage"))
        {
            control = control.Parent;
        }
        return (Foliotek.Components.ServerPage)control;
    }
}