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!