Thursday, October 25, 2007

GridView must be placed inside a form tag with runat=server

I was creating an online test and I wanted to email the test results by topic to an email distribution list. I have a panel which has two child controls, a label with an overall % correct and a gridview with percentages by topic.

A few simple lines of code would typically handle this:

string listRecipients = ConfigurationManager.AppSettings["candidateTestEmailList"].ToString();
StringBuilder sb = new StringBuilder(2000);
pnlResults.RenderControl(new HtmlTextWriter(new System.IO.StringWriter(sb)));

//---------------------------------------------------------------------
// Send to the mail handler class
//---------------------------------------------------------------------
MailHandler mh = new MailHandler();
mh.SendMail(listRecipients, "Test Results " + tbName.Text, sb.ToString());

But I was getting the error...Gridview must be placed inside a form tag with runat=server. And of course it is, and the panel and the label are not throwing a similar error. I found this nifty fix:

public override void VerifyRenderingInServerForm(Control control)
{
return;
}

found it here: http://blogs.x2line.com/al/archive/2004/10/08/576.aspx

and it worked like a charm. I didn't spend more time investigating which controls require the fix, but if I see the error, I know where to start. Thanks Anatoly!

Wednesday, October 17, 2007

Blow by Blow Heavyweight Fight -Marriage of Yahoo API Widget with ASP.Net 2.0

Ok, I spent the last 2 hours of my life implementing a really simple html rich text editor using the Yahoo Javascript API (it's a Widget in beta - look here: http://developer.yahoo.com/yui/editor/) with my .Net 2.0 website.

When I say really simple, that was the theory, the reality was a little different. The problem was, embarrassed to admit this, our job postings and our press releases are static text on our website and when HR comes up with a new posting, it takes a developer time to release a new page. So, I want to free up our developers from spending time on administrative tasks such as these. Duck soup, right? Well, I also wanted to get my feet wet with some of the new widgets in the Yahoo Javascript API library...two birds with one stone!

I already have a project open in my VS2005 IDE, so I created a new page and copied and pasted my Yahoo code in. That took all of 5 minutes. The control that sees all the action is a textarea. I took the default markup from Yahoo and added runat=server attribute; this is all you need to expose your HTML controls server-side. Here are the steps I'm going to take in my code-behind to hook up this control to my html txt job postings files:
  1. Pass in my job id in my query string; this id will be the name of the txt file without the extension.
  2. Use the System.IO namespace.
  3. Use the StreamReader to get the contents of the text file and place it in the text area.
  4. Use the StreamWriter to get the contents of the text area and write it to the file.
  5. I have the ability to overwrite the original file or create a new file, but that is just a detail.

This was the code for the Page Load event:

if (!IsPostBack)
{
//read query string and include a job if one exists
try
{
if (Request.QueryString["id"] != null)
{
job = Request.QueryString["id"].ToString();
//get the contents of text file
msgpost.Value = GetJobFileContents(job);
}
else
{
btnUpdateJobPosting.Visible = false;
}
}
catch (Exception ex)
{
msgpost.Value = ex.Message;
}
}

I also needed two supporting methods to do the Getting and Setting of the text file value:

protected string GetJobFileContents(string job)
{
string path = Server.MapPath("../jobs/");
path += job + ".txt";

StreamReader sr = new StreamReader(path);
string strContents = sr.ReadToEnd();
sr.Close();

return strContents;
}

protected void SetJobFileContents(string job)
{
string path = Server.MapPath("../jobs/");
path += job + ".txt";

StreamWriter sw = new StreamWriter(path, false);
sw.Write(msgpost.Value);
sw.Close();
}

Adding all this to the code behind took about 15 min. Then I hooked up my click events, I had two buttons, one for Updating and one for saving a new file with a textbox to put in a filename.

At this point you may be asking why I'm using server-side code with a javascript API. Well, to write a file to the server, you have to use server-side code. I can execute a server-side function with JSON using AJAX on the client, but that's more work than just using the native .Net postback events. The only reason NOT to use the postback events is to minimize round trips to the server (we've already said we have to go to the server!) or to give the user a better experience without an entire page refresh...very valid in most cases, but not in mine; this control takes up the whole page, it's the raison d'etre (raze-on det: reason for being ) of the page.

So yeah, I took the easy road and didn't really go through my paces with the new API, so the payback was, YES, it didn't work! All the events were handled properly, executed perfectly, as planned, files were read, files were saved, but no changes were reflected. Putting a breakpoint on the sw.Write(msgpost.Value) line revealed that the value of the textarea was the old, unedited value. I tried changing the property I was reading, using InnerText and InnerHtml instead of Value, but they all gave me the same data. I also tried reading the Request object with no luck, it also held the old, before-edit value.

I remembered back in the days of 1.1 where the autoeventwireup used to execute all the code twice so I stepped through the code from start to finish to determine that I was NOT reading from my file and overwriting the changes with the old text. My whopping 20 lines of c# code were doing exactly what i wanted. So I commented out my API code on the client (aspx), and made it a plain textbox, and VOILA, I was getting exactly what i wanted, but my rich text editor was gone. Now I actually had to read the documentation to see what my options were.

I found this little property:

handleSubmit -
Boolean

Config handles if the editor will attach itself to the textareas
parent form's submit handler. If it is set to true, the editor will attempt to
attach a submit listener to the textareas parent form. Then it will trigger the
editors save handler and place the new content back into the text area before
the form is submitted.

Default Value: false

So I changed the value to true, and we got some good news and we got some bad news. Good news is that the value of the textarea now reflects the changes made, but the bad news is my buttons now fire the Page_Load event, but not the click events. So now I'm two hours into what I planned to be a half-hour quickie, so I just threw in a hack. I hooked up the following to the else block of my !IsPostBack check:

else
{
//we're saving the contents
try
{
if (tbJobPosting.Text.Length > 0) //we're writing to a new file
{
//remove .txt if it's in the filename
job = tbJobPosting.Text.Replace(".txt","");

//set the contents of text file
SetJobFileContents(job);

//redirect to page
Response.Redirect("../jobs/jobposting.aspx?id=" + job);
}
else //we better have a query string
{
if (Request.QueryString["id"] != null)
{
job = Request.QueryString["id"].ToString();
SetJobFileContents(job);
}
}
}
catch (Exception ex)
{
lblUserMessage.Text = ex.Message;
}
}

It isn't pretty, but it works. It's not even the best, free, rich-text editor out there, and I'm sure I'll be replacing it with something else, but I learned some interesting things! Oh, and if you're going to be allowing html content in your postback, don't forget to add validateRequest="false" to your Page directive, and make sure you're taking care of the security risk of doing so.

Tuesday, October 9, 2007

Quote for the Week

Our company CEO dropped a book excerpt by my desk on Monday. It's from the book "Fierce Conversations" by Susan Scott. From the excerpt, it looks like a great read, but my disclaimer here is that I have not read the entire book, so proceed at your own risk.

One of the quotes from the book was immediately compelling to me, so much so that I cut it out and pasted it on my computer.

"The person who can most accurately describe reality without laying blame will emerge the leader."
There was more description around this point, but the power of the statement hits home without it. In my consulting world, we have a sunset review once a project phase has been completed. There is value to be gained from an analysis of what we did right, but the time and passion is always spent dissecting what went wrong, and the close cousin, who is to blame. The ability to recognize our mistakes and to learn from them is critical in any effort to improve a process, but our need to place the blame undermines trust and infuses a group of people who once acted as a team with a sense of isolation and a motive for CYA.

One of the reasons this was poignant to me is that I'm coming off a rather painful project completion, and I was challenged by the statement and realized that I wasn't exhibiting this critical characteristic of leadership, and yet by my project role and my own self-assessment I am a leader. My takeaway: Everything can be improved, even me!

Wednesday, August 1, 2007

Clearing Items in Repeater (or other) Control in .Net 2.0

You might notice that a repeater does not have an Items.Remove() or an Items.Clear() or even an Items[i].Remove() where you can loop through the collection and remove them one at a time. Nope, clearing a repeater is even easier than that.

I have a search control that dumps the results in a repeater, and to clear the results between search, I use the following line in my search click event:

myRepeater.DataSource = null;

whoop there it is :)

Thursday, July 12, 2007

Date Validation in .Net 2.0

All of my input date fields require two things in the validation world of 2.0:
- AJAX MaskedEditExtender
- Range Validator

What does the MaskedEditExtender give you? Only numbers are allowed and the
_ _ / _ _ / _ _ _ _ appear magically when the text field obtains focus. Here's what it looks like in your aspx source view:

<cc1:MaskedEditExtender ID="MaskedEditExtender3" runat="server" TargetControlID="txtBrideBDay"
Mask="99/99/9999"
MessageValidatorTip="true"
OnFocusCssClass="MaskedEditFocus"
OnInvalidCssClass="MaskedEditError"
MaskType="Date"
InputDirection="LeftToRight"
AcceptNegative="Left"
DisplayMoney="None"
>
</cc1:MaskedEditExtender>

The key fields to look at here are the mask and the mask type. The date mask only allows for numbers and the /. If you haven't added the AJAX toolkit to your webprojects, go here http://www.asp.net.

Now for the second piece. When you want a date to fall in a certain range, for example, scheduling an appointment, obviously the date needs to be in the future. Add a RangeValidator to your text field, select a validation type of Date, and put in your minimum and maximum values. For my application, this is what my RangeValidator looks like:

<asp:RangeValidator ID="RangeValidator1" runat="server" ControlToValidate="txtWeddingDate" Display="None"
ErrorMessage="Wedding Date cannot be in the past." Type="Date" MaximumValue="1/1/2050" ValidationGroup="GroupContactInfo"></asp:RangeValidator>

You can see that I don't care very much about the maximum date, setting it waaaaay in the future, and I haven't set a MinimumValue at all. This will throw a runtime error as both values are required for a RangeValidator. I've done this because future is not a definite date, it's dynamic based on the date the user is filling in the form. So I have to add it dynamically, which I've chosen to do in code-behind the first time the page loads:

if (!IsPostBack)
{
RangeValidator1.MinimumValue = DateTime.Now.ToShortDateString();
}

You can also put the value in the aspx page (which won't require compiling to change) with MinimumValue="<% =DateTime.Now.ToShortDateString() %>".

As always with the special validators, a RequiredField validator is needed for the RangeValidator to kick in.

Onward validation soldiers!

Tuesday, June 26, 2007

Aspdotnetstorefront Menu

Some things are easier than you think they're going to be, and that just makes my whole day! So every single client we've had has requested the top categories go horizontally across the top in the menu instead of vertical submenus under Category, and to accomodate this, we've hard-coded the menus in menuData.xml in the skins directory.

What we lose here is the dynamic control of the menus for our clients, meaning they have to touch an xml file in addition to setting up the categories, and i don't know many product managers who speak xml.

So what I found was a curious line of code in the templatebase.cs file in app_code. Look at the Page_Load function and the section commented "// Find Categories menu top" and the line which starts with a call to AddEntityMenuXsl. See the final parameter being passed into that method is string.Empty. Well, when i look at AddEntityMenuXsl, I see that we have a piece of code that allows us to add a ROOT level element if the parameter length is greater than 0. So I just made my method call look like this:

AddEntityMenuXsl(doc, "Category", AppLogic.CategoryEntityHelper.m_TblMgr, mNode, 0, "1");

and VOILA! I now have a dynamic menu with the categories across the top! With just a little itty bitty code change. Some days programming is berry berry good to me :)

Thursday, June 21, 2007

Specified string is not in the form required for an e-mail address

If you have recently moved from the System.Web.Mail (.net 1.1) to System.Net.Mail (.net 2.0), you might get this error message when sending in a string of mail recipients, and when debugging, the emails look good to you! The reason is the delimiter. I cannot imagine what twist of fate movitivated the Microsoft Class gods to switch a standard on us, maybe it was just to keep us on our toes, but they did. We used to delimit our email addresses (multiple to's, cc's, bcc's, etc) with semi-colons (;) as we do in outlook, but that no longer works, now the class only accepts the comma (,) as the address delimiter. Go figure.

So, if you have a large established code base, and you want to upgrade your mail class, just one class that you use for ALL your applications, what do you do to minimize your rework? My advice is to use the string replace function and just replace every instance of semicolon with a comma. That will handle the old code passing in semicolons as well as the new code passing in commas. Then you have one place to make the update and you can leave your legacy code alone.

If you are NOT using a single mail handler class across your applications, then you're working too hard!

Wednesday, June 13, 2007

Best Joke I've heard this week

If you're not part of the solution, you're part of the precipitate...

It's such an inside joke, think back to high school chemistry. This is a t-shirt available at www.thinkgeek.com, which is my favorite website for truly geeky things, such as the t-shirt I got my husband, "I'm with Genius" and an arrow pointing up. If you need a break in your day, and little gadgets to proclaim your geekhood, check this site out.

Monday, June 11, 2007

Attempted to read or write protected memory. This is often an indication that other memory is corrupt.

Fun error we get in aspdotnetstorefront all the time! It could happen when you recompile the website, or when you publish to a staging or live server. The easiest way to handle this is to open the task manager and kill the asp_net worker process. This is faster than an iisreset, which is the next step to take if killing the process does not remove that error.

Don't bother with what the error message says, it's just a hiccup when the app starts thinking about an exit strategy for the war in Iraq.

Friday, June 8, 2007

ADA Compliance

Americans with Disabilities Act (ADA) Compliance is now a scary term for high-volume eRetailers. It hasn't been decided in court, but Target has been sued in a class-action lawsuit for not making accomodations for disabled visitors.

The language of the Act is pretty vague and general, and those with bigger budgets are expected to make correspondingly more accomodations, and to muddle the muddy waters further, there's just no list to go by to say what is compliant and what is not.

There ARE however a couple of things that are just good practice anyway, the first of which using alt text for ALL images (except maybe corners on tabs and things like that). Sight-impaired visitors have devices that read the alt-text out loud so they can know what's on the screen in front of them. It's also a best practice for Search Engine Optimization, so just do it!

Another requirement from a recent client was to make the site keyboard navigable. This was a doozy. We had a pre-packaged component that came with aspdotnetstorefront, but it was a no-go for keyboard navigation. Our senior developer on the project, Tomas Vera found this nifty menu at http://www.milonic.com that you can tab into and use your arrow keys to navigate. This is a great find, IMO. Another tip for you, set your tabindex to 1 on your website logo so that if a user wants to tab through the page, they will always start at the top left. The only exception to this would be a search page where the user expects the focus to be in the search box so they can just begin typing.

Just one more disjointed rant from the trenches!

Thursday, May 17, 2007

Aspdotnetstorefront Menu

ASPDNSF uses a 3rd-party component for their navigation menus, Component Art. I've had some fun and exciting times customizing this menu. I would put styles, such as color and font, in the css classes TopMenuGroup and TopMenuItem, and the styles weren't sticking. Sooo frustrating! So using my IE DOM explorer, I highlighted the element (Find -> Select Element by Click) and examined the style, and found my target element to be wrapped in , which was elminating my css class assigned in the .

Here it is interesting to note that this menu is built on the fly, and you as a developer do not get to play with how this menu is created at runtime. I did some research on the NOBR tag and learned it was not css-friendly, but I took a leap of faith and added this to my stylesheet:

.TopMenuItem, .TopMenuItem NOBR, .TopMenuItemHover NOBR
{
background: #443A23;
color: #fff;
}

And voila! My css-styles were now applied.

I'm still struggling with the fact that the marriage between ASPDNSF and Component Arts is not flexible. If you decide to take your top categories horizontally across the top of the screen, you end up having to hard code your menu values in the menuData.xml file and you lose your tie to the category admin component. What this means is that if you change the name of the category, or add a new category, or make a category unpublished, these changes will not be reflected on the front end until you manually modify the xml data file. What a pain. If anyone has gotten around this little feature, I would give you a homemade chocolate chip cookie.

Tuesday, May 15, 2007

IE Developer Toolbar

If you are designing forward-facing web sites using the CSS-box model layout, an essential tool for how your model is behaving is the IE Dev toolbar (get it here: http://tinyurl.com/2ddezw). I also check my sites in Firefox which also has a handy tool for developers (get it here: http://tinyurl.com/2z4tvg). These tools are invaluable when you have to design pixel by pixel to make things perfect.

And because I've used it in my post, I might as well tell you about TinyUrl. My developer-in-arms Pablo showed me this site and I use it all the time. If you have a horribly long URL, especially the encrypted versions, and you want to send a link to your peeps (or to your mother), then use this page: http://www.tinyurl.com. We've found that even Microsoft is using this tool in their MSDN pages :)

Monday, April 30, 2007

.Net web config error

If you get an error that looks like this: Could not load type 'System.Web.UI.Compatibility.CompareValidator' from assembly 'System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. Don't spend too much time looking at your code for this, it appears to be a bug in the webconfig. Simply delete the validators from the webconfig.

The lines will look like this:

comment them out or remove them entirely and you're good to go.

The best bet when you get a strange webconfig error is to copy the entire text of the error and google it. That's the fastest way I've found to resolve runtime issues.

Thursday, April 26, 2007

Quick SQL

I'm almost embarrassed to admit this, I didn't know how to escape a quote (') in a sql string when doing an insert, 'hors d'ouvres'. My only consolation was that my hunch was right, double up the single quote within the string: 'hors d''ouvres'. Just like c#...love it!

Tuesday, April 24, 2007

Publish in VisStudio 2005

I'm a reluctant fan of the publish feature in VS 2005. It isn't easily tinkered with, at least not for me, and it forces a wholesale replacement of the website, including non-compiled pages (javascript, css, images, etc), which takes up too much time in my opinion, in addition to bringing over my development webconfig which is never the webconfig on a staging or production server.

So, a little trick I've come up with is to publish locally and then do a copy of only the bin directory and all ascx and aspx pages. For example, if I have a website under wwwroot (I know it's not necessary with VS2005, but i'm old-fashioned) called MySite, I will have a sister directory called MySite_Build. I publish to this directory and then copy to my remote servers.

When you publish, you have the option of local IIS and the file system. I would prefer local IIS because the path is simpler, but I find I get this error often:
"You must choose a publish location that is not a sub-folder of the source website." This error is confusing because the build directory is NOT a subdirectory and is in no way related to the source website. I haven't figured this out, when I get this error, I simply use the file system to identify the same directory and it works like magic. Go figure.

We'll be moving to the 3.0 framework pretty soon, but we'll still be working in vs2005 for awhile to come, so if anyone has any publishing tips, I can certainly use them!

Thursday, April 19, 2007

ADP Taxware Web API integration

This is just a short note about a gotcha I keep encountering with ADP taxware and then I neglect to document it, and it bites me in the a**. So here I am sharing my pain with the world (i should get a cookie).

ADP Taxware is a program that runs on a server. We have it running on our webserver with our website; currently our load allows this configuration. When the website calls the Taxware API, it needs access to the \program files\taxware directory. You MUST allow the internet anonymous account access to this directory or you will get an obscure error message, and stepping through the code will reveal it to be a permission denied error.

The only other quick advice I have to offer is make sure you copy your dlls to the system32 folder on your server; just residing in the taxware folder (where the install puts them) doesn't seem to be sufficient.

Finally, although Taxware's documentation is cumbersome, their tech support guy on this API is fantastic, his name is Jonathan Wang. Happy coding!

Wednesday, April 18, 2007

Aspdotnetstorefront Out-of-the-box Build Error

My company has begun offering an ecommerce solution to small-to-medium businesses called Aspdotnetstorefront. On behalf of our client, I purchased and download the product and installed it in my wwwroot directory. I made no changes to any files, I simply clicked on the sln file and loaded the solution into Visual Studio 2005 Team Edition.

Before I did anything else, I tried to build the solution. I was unsuccessful, and of course, seriously perturbed...I may have said unfriendly things...so I sent an email off to tech support with a copy of the error.

I received a quick response (you gotta love responsive tech support) and they noted that I seemed to be referencing a file that should not be in that build. How could that happen? I looked in the directory, no file. Mysterious, eh? So I loaded the solution and looked in the directory, there was a file. Odd. So I looked at the path of the file, and it was a path to a storefront directory for a different storefront solution. Problem solved? almost.

I deleted all my files, extracted the download to my newly clean directory, and before I did anything with Visual Studio, I opened the sln file in text editor and modified all the paths in the solution to point to my correct directory. You will have this problem if you try to run more than one storefront solution side-by-side. I saved the sln file. Opened my project, all problems solved. To be safe, I renamed my sln and suo files to match my directory name.

I'm fairly new to this storefront thing, so if you have any tips to share, i'm all ears.

Friday, April 13, 2007

Faster Filter Queries

We should all now be indoctrinated enough to know the holy trinity of fast queries in .Net:

  • Stored procedures – NOT inline SQL
  • Table indexes – see your DBA if you’re unsure
  • Data Readers (if your data is read only)

I am also a strong proponent of the Microsoft Application Blocks, especially the Data Application Block. You will save yourself a lot of time, your code will be cleaner and easier to read, and your data activity will be optimized. (download here).

So that’s all good, but what I really want to talk about is an optimization that Nuri showed me for the query itself.

I was writing a standard read-only report on movie data in a GridView, and I had several filters for the users, such as date range, metro area, film, etc. My query passed in 0 (zero) for all filters that were unused. My initial WHERE clause looked like this:

WHERE (o.PostedDate BETWEEN @begindate AND DATEADD(d,1,@enddate) )
AND (o.FilmID = CASE @filmID WHEN 0 THEN o.filmID ELSE @filmID END)
AND (o.ContactID= CASE @contactID WHEN 0 THEN o.ContactID ELSE contactID END)
AND (o.MarketID = CASE @marketID WHEN 0 THEN o.MarketID ELSE @marketID END)
AND (o.NewspaperID = CASE @newspaperID WHEN 0 THEN o.NewspaperID ELSE @newspaperID END)

Well, that does the trick, but it’s not efficient. In the case of 0, I am still putting a requirement on the parser to go through each row and verify that the value equals itself, and the optimal solution is that the parser simply ignores the filter if a 0 is passed.

The optimized WHERE clause looks like this:

WHERE (o.PostedDate BETWEEN @begindate AND DATEADD(d,1,@enddate) )
AND ( @filmID = 0 OR o.FilmID = @filmID )
AND ( @contactID = 0 OR o.ContactID = @contactID )
AND ( @marketID = 0 OR o.MarketID = @marketID )
AND ( @newspaperID = 0 OR o.NewspaperID = @newspaperID )

Much better, right? If you have any query optimization tricks to share with me, I’d love to hear them.