Wednesday 24 November 2010

Accents clipped off text in IE7 and IE8 with Compatibility Mode

As I work in Canada, which is a bilingual country, the applications I develop usually have to be available in both English and French. Today I had a rather odd problem regarding accented characters with missing accents.

The character in question was an 'É' and I had a bug where the accent was missing. My first call was the database to check the accent was there, and it was. So I thought it was being lost due to some difference of character set between database and application layer, but breakpoints showed it was all correct at the application layer. Next I checked the source for the rendered web page and that showed the accent was there too. I even tried a copy and paste of the text and pasting the text into an editor showed the accent was present, even though it was missing in the browser.

Some more tests revealed that the accent displayed correctly in IE8 with Compatibility Mode disabled, but when Compatibility Mode was enabled the accent was clipped off. I spent some time adjusting parameters on the ASP.Net Label control and at higher levels in the mark up to get the height to increase with no more result that lots of whitespace but no accent. I was able to get the accent to appear by sacrificing the bottom few pixels of the text when setting the valign property, but this clearly wasn't acceptable. By making the text size 11 or smaller the accent appeared, but at 12 or above it was missing.

After some more searching I eventually found the CSS property to fix it. By adding "Font" tags around the label and applying "line-height: 1.1;" to the style I got it to display the accent correctly.

It seems that in the IE8 beta, Compatibility Mode was actually called IE7 Emulation Mode, a name which they really should have kept as "Compatibility" is more suggestive of standards compliance which is exactly what Compatibility Mode/IE7 Emulation Mode/IE7 is NOT.

Tuesday 9 November 2010

Javascript popup appears again when back button pressed

One of the bugs I was looking at today was regarding use of the back button in our webapp. We have a screen that lets the user enter some data and click submit. The server side validates that data, and if necessary it alerts the user of errors using a alert issued with RegisterStartupScript.

If the user gets the error, corrects there input data, then resubmits they will be taken to the next page in the process. The problem comes when they click the back button. When the back button is pressed it takes them back to the first page which still has the startup script in it, causing the old validation error popup to appear again.

I did a lot of searching on this to see if there was a simple solution. I discovered the following:
  • There is no simple way to "unregister" a startup script.
  • Ideally the popup should be generated inside an onclick event, but this wasn't possible in my case due to the server side validation.
  • The first webpage can be set to expire, but this causes a "webpage expired" page to be displayed, which isn't any use.
  • The popup button can be pseudo disabled but that doesn't afford the user the behavior they expect.
  • Meta tags can be used to force a page to always refresh, rather than loading from the browser's cache. I wasn't able to get these to work correctly for me.
So after much effort trying different ways to do this, I went with this rather simple solution:

When we register the startup script with the alert, immediately follow the alert statement with one which will refresh the page. This ensures that the startup script is removed from the page as soon as the OK is clicked, and if the user returns to the page the pop up does not reappear.

Here's a code sample of the script injected with Page.RegisterStartupScript:

alert(\"Error with Data\");
__doPostBack('__Page', 'EmptyPostback');

Originally I was using "window.location = PageName.aspx" instead of "__doPostBack" to refresh my page without the popup, but that caused some ViewState that I needed to be lost. If you are developing in something other than ASP.Net then window.location may be better for you.

Friday 21 May 2010

Recursive Timers Not Firing

Wasted a good while today trying to find out why some Timers I had set up kept stopping firing. Here's the scenario: I have a windows service that does 2 things.
1) Generate Alerts: Periodically generate a batch of alert emails to send to all users of a system reminding them of pending tasks they have assigned to them.
2) Send Emails: For all emails in a database table, send them.

Generate Alerts parses a cron format string to determine how often to run. Send Emails has an interval in milliseconds. One important point here is that I don't want these tasks to run every X seconds. I want them to run after X seconds and then again X seconds after the previous run has completed. Hence I don't want to use a Timer in the standard way as one execution could begin while another is still in progress.

For this reason, in each case I create a timer to do my work, and then at the end of the timer I create another timer. Note these are the Timers from System.Threading and are configured to run only once, not at every tick). Here's an example:


public void StartEmailSender(int waitTime, int retryAttempts)
{
log.Debug("Starting EmailSender");
WaitTime = new TimeSpan(0, 0, 0, 0, waitTime);
RetryAttempts = retryAttempts;

//Set the SendQueuedEmails method to run after the elapsed time.
emailSenderTimer = new Timer(delegate(object s)
{
SendQueuedEmails();
}
, null, WaitTime, new TimeSpan(-1));
log.Debug("EmailSender Started");
}

public void SendQueuedEmails()
{
log.Debug("Entering SendQueuedEmails method");

//Code to send emails goes here

//Create a new timer to run this method again after the elapsed time.
emailSenderTimer = new Timer(delegate(object s)
{
SendQueuedEmails();
}
, null, WaitTime, new TimeSpan(-1));

log.Debug("Exiting SendQueuedEmails method");
}


The original problem I was having is that the timer would run X number of times and then stop. Or one of the 2 timers would continue for longer than the other but then eventually stop. I discovered the reason was that my Timers were being Garbage Collected.

The above code is the working version. In my previous version which did not work I was not assigning the output of "new Timer(delegate(object s)" to anything. Now it gets set to a private class member. This ensures that there is always a reference to the current Timer so long as the class has not been disposed. I ensure the class is not disposed by again having a private class member in the service's main class which I initialise in the OnStart method. Hence the class is not disposed until the service is stopped and hence there is always a reference to the current Timer which is waiting to execute, and only the ones which have already completed get disposed.

Wednesday 5 May 2010

Using Refresh Files with WebSite Projects to include DLLs

Recently I had a problem whereby my WebSite project (Not WebApplication project) in Visual Studio was missing some DLLs. These are pre-built DLLs that I load from a set location, for example AjaxControlToolkit.dll and log4net.dll.

On my build server whenever a build was done I needed a way to have these files copied to the BIN directory of my website. In a WebApplication or other project these references are specified in the project file (Project.csproj for example), but we don't have a project file for a WebSite project. The WebSite is just a directory of files and any "project" references are included in the Solution file.

The way Visual Studio allows us to include DLL files which are not covered by the project references is with the use of a Refresh file. So for example if I want to include C:\DLLs\AjaxControlToolkit.dll in my WebSite I add it with Add Reference -> Browser -> Choose the file. Visual Studio then puts a file called AjaxControlToolkit.dll.refresh in my bin directory. The presence of this file cause the AjaxControlToolkit.dll to be copied from C:\DLLs\ to the BIN directory of my WebSite each time I build it.

Normally we don't check things in the BIN directory into source control, but Refresh files are the exception. I added my Refresh file in the BIN directory to CVS and when my Build Server did a check out it got the Refresh file, causing the build to copy the DLL into the BIN directory for my built website.

Monday 3 May 2010

Query String Encoding Hassles with & in ASP.Net

Today I encountered an odd problem which I remember having years ago but had no memory of how to solve it....

So I'm creating a URL with a query string which will appear in a web page:
thumbnailImage.ImageUrl =  "~/UserControls/ImageHandler.ashx?" + "id=" + productImages[0].UId + "&thumb=true&size=100"
Now when this appears in the web page, it seems that all of the "&" symbols, which represent the delimiter between parameters, get changed to "&"

The problem with this is that when the page that is linked to resolves the query parameters I get:

context.Request.Params["thumb"]
null

context.Request.Params["amp;thumb"]
"true"
The first thing I tried was to use Server.URLEncode. This changes the "&" symbols to "%26". This in itself is no use, but when used with Server.URLDecode means we end up with the correct query string. It still gives us a problem however because we can no longer use the string index for context.Request.Params. We'd have to manually parse the string output of URLDecode to find the required parameter. Not difficult I know, but a little untidy.

The solution I eventually found was to use:
thumbnailImage.ImageUrl = String.Format("~/UserControls/ImageHandler.ashx?" + "id=" + productImages[0].UId + "&thumb=true&size=100");
It appears that the String.Format causes the "&" symbols not to be changed to "&"

Exactly why that happens I do not know, it seems that the output of String.Format is the same as the original string:

String.Format("~/UserControls/ImageHandler.ashx?" + "id=" + productImages[0].UId + "&thumb=true&size=100").Equals("~/UserControls/ImageHandler.ashx?" + "id=" + productImages[0].UId + "&thumb=true&size=100")
true
Strange... but it seems to work.