Why it's sometimes easier to throw money at a problem.

Today my manager asked if we could put some spell-checking on some of the free-form text entry pages of our web application. We had talked about it generally for a little while, but now I guess it's a bit more urgent, and he also happened to catch me at a good post-release time with not a ton of crazy bugfixes and no time. There are some "home-brew solutions":http://www.wwwcoder.com/main/parentid/458/site/3526/68/default.aspx using Word, as well as "a free component":http://www.loresoft.com/NetSpell which has some promise, but the one I was drawn to was "this one right here":http://www.keyoti.com/products/rapidspell/dotNetWeb/index.html. It was a piece of cake to put into the form in question, even though it was totally dynamically generated (i.e. no textbox tags on the page at all.. everything created and inserted into a placeholder control at runtime). Just add a spellcheck control for each textbox, and a master one to pop the Javascript window which tests all the textboxes.

It's one of those things where I certainly _could_ have figured something out, but this solution was so easy and so flexible (and it helps that the client is _so_ paying for it) that it's just easier to send them a check and have something up and running in just a few hours.

Finding controls in the header and footer

This is such a pain in the ass it's not even funny. I need to keep a running total of rows in my repeater, and then put a subtotal column at the bottom. You can't do a normal FindControl on an item, and I saw weird stuff in the newsgroups about trying to pass a "ctrl1:lblName" to the FindControl, as well as using OnItemDataBound, which doesn't really work for what I want to do. So now, for posteriety, is the way to access a server-based control in the footer of a repeater (I'm assuming the same thing would work for headers):

Dim lblFooter As Label lblFooter = CType(rptSample.Controls(rptSample.Controls.Count - 1).FindControl("lblFooter"), Label) lblFooter.Text = "You found me!"

Sheesh.. what a hassle to figure that out.

*Update:* "David":http://bigbrit.blogspot.com/ asked in the comments if I asked the VB.Net Clippy for help. Yes, yes I did... and here's the response I got:

VB.Net assistant

Thanks to Pat from the FeralMarketing dept. for the image.

Top five reasons why my work is like a fraternity house

# Only guys work at my office. Not like it's a sexist thing, and women have worked there before, but at the moment it's only us Y-chromosomes folks.# Any day that it's nice we grill lunch out on the back patio. Yesterday was turkey burgers, today was "hot dogs":http://www.theonion.com/onion3011/walkeninla.html! # Another thing that happens on nice days is the portable hoop gets put out, and some hot 2-on-2 action gets going after work. I stay far, far away from the games because a) I have no skillz whatsoever (I was only able to make it to JV in grade school, and only made 2 baskets all season (although one of them was in the game against the teachers.. yay me!)) and b) the boys I work with play _hard_, and I'm just not that competative about sports stuff. # We're talking about having the occasional "low-stakes poker game":http://feralboy.com/log/archives/001010/ at lunch. Wait until I clean out all those dead money guys! ;-) # We have a mascot. It's actually our 2nd. The neighbors next door had a pet rooster, or at least we hope it was a pet and not dinner. Anyway, "McNuggets":http://feralboy.com/photoalbum/photos/?folder=20031229_mcnuggets (as we called him) disappeared, and was replaced by a golden rooster. We don't like this one nearly as much, as don't feed him like we did Nuggets.

Anyway, if this sounds like a fun place, then we're hiring. It would be for someone who does ASP.NET programming at a high level (classes, complex user interfaces, lots of SQL queries, etc.). You probably also should live in the Pittsburgh area. If you are this person, or know this person, send me an email at mattcomroe[at]yahoo.com.

Shortsighted

There's almost nothing I love more than slogging through other people's code. Usually it's actually not that bad, but every once in awhile I'll find an utter gem, like this morning's nugget. You don't even need to be a programmer to understand this one.

loginCookie.Expires = New DateTime(2003, 12, 31, 23, 59, 59)

That basically says to set a "cookie":http://www.cookiecentral.com/faq/ on your browser that expires at 11:59 at night on Jan 1, 2003. The problem with this is that it's now beyond that, so every time this code fires, it places an already expired cookie on your browser, which does you no good.

This certainly isn't on the order of the "Y2K":http://www.y2k.gov/ bug (which was not as much laziness as it was lack of storage space to hold a 4-digit year), but it's still fairly annoying when you consider that swapping out the above line for

loginCookie.Expires = DateTime.Now.AddMonths(1)

not only makes the code easier to understand, but also won't ever break, since it just adds a month to whatever the current date is.

GZip compression and changing UserAgent

Just in case you ever wanted to use the .NET Zip Library to receive GZip-compressed content from a webserver, as well as sending a custom UserAgent string to identify your app, here's how you do it:

        /// <summary>
        /// Used to send out a custom application UserAgent, and return a string from a GZip-compressed
        /// response.
        /// </summary>
        /// <param name="strUrl">The url you want to retrieve.</param>
        /// <returns></returns>
        public string GetWebRequest(string strUrl)
        {

            StringBuilder mySB = new StringBuilder();

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(strUrl); 
            // Sends the HttpWebRequest and waits for the response.  
            request.UserAgent = "BSClient 0.3";
            request.Headers.Add("Accept-Encoding", "gzip, deflate");
            HttpWebResponse response = (HttpWebResponse)request.GetResponse(); 
            // Gets the stream associated with the response.
            Stream receiveStream = GetGzipStream(response);

            Encoding encode = System.Text.Encoding.GetEncoding("utf-8");
            // Pipes the stream to a higher level stream reader with the required encoding format. 
            StreamReader readStream = new StreamReader( receiveStream, encode );
            Char[] read = new Char[256];
            // Reads 256 characters at a time.    
            int count = readStream.Read( read, 0, 256 );
            while (count > 0) 
            {
                // Dumps the 256 characters on a string and displays the string to the console.
                String str = new String(read, 0, count);
                mySB.Append(str);
                count = readStream.Read(read, 0, 256);
            }
            // Releases the resources of the response.
            response.Close();
            // Releases the resources of the Stream.
            readStream.Close();

            return mySB.ToString();

        }

        /// <summary>
        /// Used in place of GetResponseStream().  This function will check out your HttpWebResponse's contents,
        /// and return the proper string representation of the HttpWebResponse stream.
        /// </summary>
        /// <param name="response"></param>
        /// <returns>String representation of the HttpWebResponse</returns>
        private Stream GetGzipStream(HttpWebResponse response)
        {
            Stream compressedStream = null;
            if (response.ContentEncoding=="gzip")
            {
                compressedStream =  new GZipInputStream(response.GetResponseStream());
            }
            else if (response.ContentEncoding=="deflate")
            {
                compressedStream = new InflaterInputStream(response.GetResponseStream());
            }

            if (compressedStream != null)
            {
                MemoryStream decompressedStream = new MemoryStream();
                int size = 2048;
                byte[] writeData = new byte[2048];
                while (true)
                {
                    size = compressedStream.Read(writeData, 0, size);
                    if (size > 0)
                    {
                        decompressedStream.Write(writeData,0,size);
                    }
                    else
                    {
                        break;
                    }
                }
                decompressedStream.Seek(0, SeekOrigin.Begin);
                return decompressedStream;
            }
            else
            {
                return response.GetResponseStream();
            }
        }

Variable Scope

Let's talk for a minute about scoping of variables. Let's say that you have, oh I don't know, a fairly complex billing web app. An administrator logs in, selects a customer, bills time for that customer, and goes on his/her merry way. No problem, right? Well, let's say (strictly for the sake of argument, mind you), that you were storing the unique id of the customer as you were working with them like this:

== Module modReports

  Public g_iCustomerID As Integer

End Module ==

What this means is that the value for g_iCustomerID is now both Public and Global, which means that any class, function, etc. can access that value, *regardless of who is accessing*. Here's what can happen:

# User A logs in and picks a customer. g_iCustomerID is now set to a value, say 132. # While User A is still logged in, User B also logs in. The default edit page tries to be clever and sees if there's a value stored in g_iCustomerID, so you can bop around the site, and still maintain which user you've selected. So, since g_iCustomerID has a value of 132, the application assumes that User B selected it, and loads all the data for Customer #132.

You can see how this could be A Bad Thing(tm). The solution is to go around and do a global search-and-replace on every occurance of this variable, and substitue a session-level variable instead. Of course, g_iCustomerID is just one of about 10 other variables that are being stored globally that shouldn't be. Grrrrr.

Various (mostly nerdy) bits.

# The whole Puma ads thing has taken on a life of its own (much like a certain oversized snack food), with threatening letters from lawyers flying around. Gawker seems to be following it most closely.# This post is the first one that I'm trying to format with Brad Choate's MT-Textile plugin. It's basically just a way to use shorthand to do your formatting. The thing that got me was the shorthand way to handle those crazy abbreviations, which is handy when you're talking about CSS(Cascading Style Sheets) and XHTML(eXtensible HyperText Markup Language) all day long. MT-Textile is the new black, as far as I'm concerned. # It looks like Wrox Press is taking a dirt nap. Unfortunate, because I always thought their books were the best of the bunch. Maybe they'll open up all their content on ASP Today, although I've had a subscription that's been expired for at least 6 months now, and they haven't gotten around to turning it off. I have been wondering what will happen to stores of paid content when the company that owns them goes under. I guess we'll see with Salon soon enough. # They had been hard to find before, but it seems like .Nizzle-centric weblogs are finally out there. It's a good place to see people swapping info, and working on some cutting-edge stuff, as my older standbys seem to be updating less frequently than they were. # Looks like someone found a way to easily accomplish my Music listing using Trackbacks in MT.

C# shortcuts

<geek>Let me first say that I really, really like working with .nizzle. The problem is that so many of the web apps that i work on are really similar, so a lot of the work gets boring; seems like I'm always doing the same steps for an admin site: 1. Create database table for new article/category/user/whatever. 2. Create all associated stored procedures to add/update/delete/otherwise modify new db table. 3. Create a strongly typed C# object to hold all the methods and properties of said object. 4. Create front-end .aspx pages to allow admin user to make use of steps 1-3. 5. Lather/rinse/repeat.

The other day I found a neat little chunk of code from M$FT to help me out with step 3, the Data Access Application Block, which is really just a bunch of handy shortcuts for some of the more repetitive coding that I do.

"Well, geez", you're saying, "don't just tell me about it, show my why it's better!" Hey sure.

For a really simple grab of all records and stuffing them in a database, we go from this:

public DataSet GetAllArticles()
        {
            // Create Instance of Connection and Command Object
            SqlConnection myConnection = new SqlConnection(ConfigurationSettings.AppSettings["connectionString"]);
            SqlCommand myCommand = new SqlCommand("GetAllNewsArticles", myConnection);

            // Mark the Command as a SPROC
            myCommand.CommandType = CommandType.StoredProcedure;

            // Adapter and DataSet
            SqlDataAdapter oAdapter= new SqlDataAdapter();
            oAdapter.SelectCommand=myCommand;
            DataSet oDataSet = new DataSet();

            try
            {
                myConnection.Open();
                oAdapter.Fill(oDataSet,"Articles");
                return oDataSet;  
            }

            catch(Exception oException)
            {
                throw oException;
            }
            finally
            {
                // Close the Connection
                if (myConnection.State == ConnectionState.Open)
                    myConnection.Close();
            }
        } // end GetAllArticles()

to this:

public DataSet GetTopLevelProductCategories()
        {
            // Create Instance of Connection and Command Object
            SqlConnection myConnection = new SqlConnection(ConfigurationSettings.AppSettings["connectionString"]);
            DataSet oDataSet;
            oDataSet = SqlHelper.ExecuteDataset(myConnection,CommandType.StoredProcedure, "GetTopLevelProductCategories");
            return oDataSet;  
            if (myConnection.State == ConnectionState.Open) myConnection.Close();
        } // end GetTopLevelProductCategories()

Notice the "ExecuteDataset" function, which is an overloaded function which lets you pass parameters, transaction info, or not.

I like it even better when you're passing parameters to an insert or update stored procedure. Old style:

public bool UpdateCategory(Category myCategory)
        {
            SqlConnection myConnection = null;
            SqlCommand    myCommand    = null;
            bool          bUpdated     = true;

            myConnection = new SqlConnection(ConfigurationSettings.AppSettings["connectionString"]);
            myCommand = new SqlCommand("UpdateCategory", myConnection);

            myCommand.CommandType = CommandType.StoredProcedure;

            // Add Parameters to SPROC
            SqlParameter parameterCategoryId = new SqlParameter("@CategoryID", SqlDbType.Int, 4);
            parameterCategoryId.Value = myCategory.GetCategoryId();
            myCommand.Parameters.Add(parameterCategoryId);

            SqlParameter parameterName = new SqlParameter("@Name", SqlDbType.VarChar, 255);
            parameterName.Value = myCategory.GetName();
            myCommand.Parameters.Add(parameterName);

            SqlParameter parameterImageUrl = new SqlParameter("@ImageUrl", SqlDbType.VarChar, 255);
            parameterImageUrl.Value = myCategory.GetImageUrl();
            myCommand.Parameters.Add(parameterImageUrl);

            try
            {
                myConnection.Open();
                myCommand.ExecuteNonQuery();
            }
            catch (Exception e)
            {
                throw e;
                bUpdated = false;
            }
            finally
            {
                if (myConnection.State == ConnectionState.Open)
                    myConnection.Close();
            }

            return bUpdated;

        } // end UpdateCategory()

New hotness:

public void UpdateProductCategory(ProductCategory myCategory)
        {
            SqlConnection myConnection = new SqlConnection(ConfigurationSettings.AppSettings["connectionString"]);

            SqlParameter[] arParams = new SqlParameter[4];
            arParams[0] = new SqlParameter("@CategoryID", myCategory.GetCategoryId());
            arParams[1] = new SqlParameter("@CategoryName", myCategory.GetCategoryName());
            arParams[2] = new SqlParameter("@DetailedDescription", myCategory.GetDetailedDescription());
            arParams[3] = new SqlParameter("@DefaultProduct",myCategory.GetCodeNumber());
            SqlHelper.ExecuteNonQuery(myConnection, CommandType.StoredProcedure, "UpdateProductCategory", arParams);
            if (myConnection.State == ConnectionState.Open) myConnection.Close();
        } // end UpdateProductCategory()

No more setting datatypes, and taking 3 lines to add one stored procedure parameter. Less is more. </geek>

trials and tribulations (or, it's christmas time in hollis, queens)

i drove out the day before xmas eve right after work, and went to my best friend's house. his parents are like my adopted 3rd set, and i always spend xmas eve with them. rather than making the drive all the way out to their new place (which takes about 6, 6 1/2 hours without traffic), i went to philly to go meet ryan, who was staying at his friend jane's house in center city. the drive there was fairly uneventful, except for the otherworldly radio reception i was getting. i don't know if it was atmospheric conditions or alien visitors, but i was getting crazy am reception on the drive out. i was about midway across pa, and picking up broadcasts from boston, nyc, chicago, cleveland, and d.c. i even got to listen to most of the exciting tie between the rangers and devils. we went out for a bit in philly that night, hit a few bars, and then went back to jane's and passed out.

christmas eve we drove to ryan's folks' house, and started doing some christmas-y stuff. ran out to the liquor store and picked up some presents as well as supplies for that evening, and ryan even managed to do all of his shopping for xmas presents at 5 p.m. at the eckerd. you'd be amazed at the sorts of things you can find there when you're a bit desparate. ryan's brother showed up a short while later, and we started taking our xmas pictures, which every year involves some props, and usually the dog. this year we had some bum polaroid film, so we had to make do with triplets of izone pictures, instead of just one of the 600 variety. too bad, because we had some really good ones. we all wore cloth napkins on our heads (sorta like the flying nun) (the dog included) and posed with various gifts for baby jesus; an apple, a box of godiva truffles, what have you... we also had a really good one of us posing by the *ahem* posed wooden reindeer. ryan and i take great delight in posing them this way every time we're at his house over xmas. you know the ones, thin wooden cutouts, and one has their head down. you can sorta make the one with his head up straight mount the other one, and it's extremely funny. too bad we weren't the first to think of it, or at least the most famous to think of it.

xmas day my plans went all to hell. the nor'easter prevented me from making it to see either my mom or my dad, so i just hung out at ryan's house all day, watching "a christmas story" (and researching the actor from it who became a porn star) at least 7 times during the marathon, eating prime rib, and in general being lazy.

the drive back to pgh was similarly uneventful, although when i was flipping across the dial i did come across this nutjob on a "rush limbaugh" station... his comments were so amazingly sexist that i wasn't quite sure what to do; talking about his wife's "job" as cooking and cleaning, and how he doesn't want her to have any sort of free time or anything. my guess is they put him on to make rush seem that much more moderate in comparison.

anyway, got a couple of pretty cool presents. although i didn't see my mom, she had mailed me my present a few weeks ago, some new pedals for my soon-to-be cross bike (which reminds me, i really need to give paul some benjamins for that). along with that, the slf got me this awesome windbreaker, which will come in quite handy for the bike rides on the chillier spring mornings.

so, the stress of the holidays is (mostly) over, but i still have a bunch of work to do on the project i'm trying to finish before the deadline. i'm happy that i get to redo such a highly visible site using all the hot new xhtml/css web standards and everything, but it is quite an undertaking. there's tons of legacy content in the db, and time is just so short that i don't know if i'm going to get to it all. very few of the pages validate (if any), but we're still much closer to a compliant site. there are just so many issues to be addressed; here are some of my fun problems of the moment:

--problems with character encoding (pages that are "utf-8" encoded can't handle "glyphs", which are special characters which have a character for them, instead of using an html equavalent; i.e. ™ instead of &trade;). i can change the encoding from "utf-8" to "charset=iso-8859-1", but that causes pages to barf in the validator since they're being encoded utf-8 in .nizzle by default. it also throws a monkey wrench in all the product and category detail pages, which are using xml/xslt transformations (with the xml data being utf-8). the "right" thing to do would be to re-code these pages in .nizzle, but i just don't have the time right now. actually, the Really Right Thing(™) would be to expose category and product data as a web service (which was my original intent when we redesigned last year, except i used a VB COM object to create an xml string using the msxml4 dom object, which works fine, but it's clunky and does not easily lend itself to having that data reused by other sites/applications). (a little note: i think i might have found the answer while looking for relevant links for this post... i can set a global attribute for output response encoding, as shown here. set the encoding to iso-8859-1 in the web.config, and that should solve much of my problems.

--font size. i'm trying to stay away from hardcoded font sizes, and using named base fonts and just percentages in my stylesheets. one problem so far with that is the store locator page... it uses some funky mapquest webservice-esque process to get its results back, which would be fine except that it puts some html comments in the top few lines of the page, which means that my <!DOCTYPE> tag is not the first line in the html, which causes weird font size problems in ie, but not netscape/mozilla or opera.

so, i worked for about 7 hours today (as did amy, bless her heart), and i figure i'll be back in the office tomorrow, too. thursday (new year's day) the office is off, and we launch wednesday, and if all goes well i have a feeling i'll be "sick" friday.