Friday 28 March 2008

Dirty Checking in ASP.NET (revisited)

An regular request from my clients is to have a warning when a user navigates away from a web page. This is something taken for granted in the Windows Forms world - but there is no native support for this functionality in the ASP.NET framework. In the past I have done some more manual workarounds with javascript for critical pages but there are some better options out there.

There are a few different sample implementations on the web for checking that you have made changes to your ASP.NET page (aka dirty checking):

http://www.codeproject.com/KB/aspnet/EWSWebPt2.aspx
http://www.codeproject.com/KB/ajax/ajaxdirtypanelextender.aspx

One of the issues with checking for changes in controls is that there is limited support out of the box for whether HTML controls have changed on the client side. Also dirty checking is server side is unneccessary overkill and involves unneccessary postbacks (partial or otherwise).

The common theme amongst the approaches to this problem is to :


  1. In Page PreRender, save all original values of editable controls into hidden fields

  2. When checking for dirty (e.g. navigating away from the page) – get all current values from editable controls and compare with the values in the hidden control – and warn if dirty

  3. On Save, mark all controls as not dirty (by updating the value in the hidden control).

Another possible approach is to hook into the onchange event - http://www.w3schools.com/jsref/jsref_onchange.asp - I originally thought this approach would be more complicated to implement as it has to handle the change activity of different controls specifically rather than just checking the value.

However, there is a VERY simple way to do this check with a few lines of javascript - see the code below (thanks to Steve Krizanovic for pointing out this neat trick):






<html>
<head>
<style type="text/css">
.hide
{
display: none;
}
.show
{
display: inherit;
}
</style>
</head>
<body>

<script>
//Demo of completely client side dirty flag.

function setDirty()
{
document.body.onbeforeunload=showMessage;
//debugger;
document.getElementById("DirtyLabel").className = "show";
}

function clearDirty()
{
document.body.onbeforeunload = "";
document.getElementById("DirtyLabel").className = "hide";
}


function showMessage()
{
return "page is dirty"
}

function setControlChange()
{
if(typeof(event.srcElement)!='undefined')
{
event.srcElement.onchange=setDirty;
}
}

document.body.onclick=setControlChange;
document.body.onkeyup=setControlChange;

</script>
<h1>Demo: Dirty Page Client Side Warnings</h1>
<div id="DirtyLabel" style="color: Red;" class="hide">
Page is Dirty</div>
<input id="Text1" type="text" />
<input id="Text2" type="text" />
<input id="Radio1" name="testRadio" type="radio" checked="checked" title="Option 1"
value="1" />
<input id="Radio3" name="testRadio" type="radio" title="Option 2" value="2" />
<input id="Radio2" name="testRadio" type="radio" title="Option 3" value="3" />
<select name="DropDown">
<option value="" selected="selected">Select Country</option>
<option value="United States">United States</option>
<option value="United Kingdom">United Kingdom</option>
<option value="Afghanistan">Afghanistan</option>
</select>
</input>
<input id="Button1" type="button" value="Save" onclick="clearDirty();" />
</body>
</html>

Wednesday 19 March 2008

Reflection - using GetType(string) to create an instance of a type is returning null

My colleague "CS" today had issues with resolving types in our Lookup Service. The problem: he was trying to use GetType(string) to resolve a type that was in another assembly. e.g. Type sourceType = GetType(string.Format("DDK.Common.Lookups{0}",lookupTypeName))

Problem was that it couldn't resolve the name - even though the dll was referenced and the calling assembly could resolve the name if it needed to.

This article is much better than MSDN in explaning the issue http://blogs.msdn.com/haibo_luo/archive/2005/08/21/454213.aspx. In attempting to resolve the type, GetType() will first look in the current calling assembly, then in mscorlib. It will NOT trawl through all your references in the bin directory for you like it does with normal type resolution - it will just give up if it is not in mscorlib or the current assembly and return a whopping great null.

Instead, you have to give the .NET runtime a hand and tell it which assembly to look in. This is why you have to do the same thing when specifying dynamically loaded types in the web.config - you have to put the fully qualified names so the runtime it can resolve the type with the same GetType(string) mechanism. The simplest way to find this out is to just look at the project properties to find the name of the output assembly name. Alternatively, you can make an instance of your object e.g. new DDK.Common.Lookups.Country().GetType().AssemblyQualifiedName to get the full name that the runtime uses to uniquely identify the type.

In our case, changing the code to include the red (ie the AssemblyQualifiedName minus any version numbers did the trick). ie
Type sourceType = GetType(string.Format("DDK.Common.Lookups.{0}, DDK.Common.Lookups",lookupTypeName))


Friday 14 March 2008

Pizza - for the health-conscious programmer in all of us

Think what you like (this is not a paid ad) - but I have found Dominos pizzas to be much fresher than the Pizza Hut equivalents - and cheaper as well. For the cheapest pizzas year round in Oz ($4.95 per large pizza is hard to beat), you can visit the following site:

http://www.retailmenot.com/view/dominos.com.au

It has more coupons than you can shake a stick at - including voucher codes for your next Dell XPS 1730 lappie (I'm still hanging out for 1st April and the new FBT year for that one... but in the mean time, I can console myself with pizza :o) )


Tuesday 11 March 2008

Validating Images before Upload by Checking the File Stream

Rather than just checking for valid file extensions, there are more advanced ways of checking that your users are not uploading rubbish to your image file store (we currently use MOSS for storage at my current client "LL").

Thanks to http://forums.asp.net/p/1051895/2171502.aspx#2171502 for the image validation part of this code.



/// <summary>
/// Added to validate that an image is being uploaded - not just any document
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
private bool IsImage(byte[] data)
{
//read 64 bytes of the stream only to determine the type
string myStr = System.Text.Encoding.ASCII.GetString(data).Substring(0, 16);
//check if its definately an image.
if (myStr.Substring(8, 2).ToString().ToLower() != "if")
{
//its not a jpeg
if (myStr.Substring(0, 3).ToString().ToLower() != "gif")
{
//its not a gif
if (myStr.Substring(0, 2).ToString().ToLower() != "bm")
{
//its not a .bmp
if (myStr.Substring(0, 2).ToString().ToLower() != "ii")
{
//its not a tiff
//ProcessErrors("notImage");
myStr = null;
return false;
}
}
}
}
myStr = null;
return true;
}





public Response<List<SharepointDocumentDto>> AddAssetImageToSession(
string fileName, Stream contentStream, IWebPageView currentView, string parentWindow)
{
Response<List<SharepointDocumentDto>> response = GetAssetImageSingle();

if (string.IsNullOrEmpty(fileName))
{
response.Errors.Add(new Error(ErrorName.FileError, Errors.MissingFileName));
response.IsSuccessful = false;
}


else
{
SharepointDocumentDto item = new SharepointDocumentDto();
item.Name = fileName; //Set FileName
item.IsNewItem = true; //Flag as true so we know to save it when the asset is saved
item.GeneratedListItemId = Guid.NewGuid().ToString();
MemoryStream ms = new MemoryStream();
byte[] data = new byte[256];
int c = contentStream.Read(data, 0, data.Length);

//Check if it is a valid image
if (!IsImage(data))
{
response.Errors.Add(new Error(ErrorName.FileError, Errors.InvalidImageUploaded));
response.IsSuccessful = false;
return response; //invalid
}

//Read into buffer until end of file
while (c > 0)


Thursday 6 March 2008

Issues when Overriding the Render() method of a asp:Textbox

The framework at my current client has had a bug which has been sitting around and bugging our developers for months which no one has been able (or willing) to fix. As a tech lead, I felt that it was my responsibility to nip the issue once and for all.

The issue is that whenever we bound to textboxes in Multiline mode, the data would save correctly to the database but not render correctly. This effectively meant that our app would clear out all data in multiline textboxes if you save it twice. Now where I come from, Data loss is a basic no-no :o)

If you look at the code below, it all looks perfectly innocent. You render the begin tag, the contents and then the end tag. A few of the guys had a look at this and didn't know why the data was being erased and just avoided using the custom control - using the asp:TextBox instead :



protected override void Render(HtmlTextWriter writer)
{
RenderBeginTag(writer);
// Assumes validator control has been added as a subcontrol
RenderContents(writer);
RenderEndTag(writer);
}


Looks fine? No. If you look at the base class in reflector (TextBox.Render(HtmlTextWriter)), you will see what is wrong with the code above:





protected internal override void Render(HtmlTextWriter writer)
{
this.RenderBeginTag(writer);
if (this.TextMode == TextBoxMode.MultiLine)
{
HttpUtility.HtmlEncode(this.Text, writer);
}
this.RenderEndTag(writer);
}





So the HTML textareas being rendered by the extended control were blank as the control was omitting an essential step in the render method. I just entered the multiline condition into the inherited control - and everything worked fine.

Wednesday 5 March 2008

A simple reminder to all: MS AJAX update panel and DataSources interaction - FormView.Update not passing values to DataSource

My Brazillian colleague Andre Gallo was having issues today with one of our datasources not being updated from an ASP.NET formview (a modified Object Container DataSource, part of the Web Client Software Factory package). While the formview had all the values (clearly visible via FormView.FindControl("myControlName"), a call to FormView.Updateitem() was not setting the value on the Object Container Data Source (aka OCDS). I had a look and the problem was that the datasource was OUTSIDE the AJAX panel - and so was not being passed through to the server by the partial postback.

There were 2 possible resolutions to that one:
  1. To put the object container datasource into the same panel
  2. To use the Telerik controls RadAjaxManager which allows you to point to specific trigger and destination controls for the partial postback.

We resolved the issue with option 2.

Saturday 1 March 2008

Reg File to for Context Menu to allow Recursive Delete of all SVN folders

This reg file is very handy when cleaning up corrupted SVN local working copies. To use just copy the text below (between the SNIP tags) into svnClean.reg and double click on the file to install.
<SNIP>

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\DeleteSVN]
@="Delete SVN Folders"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\DeleteSVN\command]
@="cmd.exe /c \"TITLE Removing SVN Folders in %1 && COLOR 9A && FOR /r \"%1\" %%f IN (_svn) DO RD /s /q \"%%f\" \""

</SNIP>

Thanks to http://weblogs.asp.net/jgalloway/archive/2007/02/24/shell-command-remove-svn-folders.aspx for this tip.



"Open Command Here" for VS 2005 and VS 2008


Scott has inf files which allow you to add the "Open Cmd Here" prompt to your windows explorer context menu. You just have to save the text below as an .inf file, right click and select "install".



One of my colleagues Andrew Weaver (aka Reddog) has updated this .inf file slightly to point to default 2008 file paths :




;

; "CMD Prompt Here" PowerToy

;

; Copyright 1996 Microsoft Corporation

;

; Modified to launch VS.NET 2005 command prompt 5/6/03 MG

; Modified to launch VS.NET 2008 command prompt 22/11/07 AW

 

[version]

signature="$CHICAGO$"

 

[VSNet2008CmdHereInstall]

CopyFiles = VSNet2008CmdHere.Files.Inf

AddReg    = VSNet2008CmdHere.Reg

 

[DefaultInstall]

CopyFiles = VSNet2008CmdHere.Files.Inf

AddReg    = VSNet2008CmdHere.Reg

 

[DefaultUnInstall]

DelFiles  = VSNet2008CmdHere.Files.Inf

DelReg    = VSNet2008CmdHere.Reg

 

[SourceDisksNames]

55="VS .NET 2008 CMD Prompt Here","",1

 

[SourceDisksFiles]

VSNet2008CmdHere.INF=55

 

[DestinationDirs]

VSNet2008CmdHere.Files.Inf = 17

 

[VSNet2008CmdHere.Files.Inf]

VSNet2008CmdHere.INF

 

[VSNet2008CmdHere.Reg]

HKLM,%UDHERE%,DisplayName,,"%VSNet2008CmdHereName%"

HKLM,%UDHERE%,UninstallString,,"rundll32.exe syssetup.dll,SetupInfObjectInstallAction DefaultUninstall 132 %17%\VSNet2008CmdHere.inf"

HKCR,Directory\Shell\VSNet2008CmdHere,,,"%VSNet2008CmdHereAccel%"

HKCR,Directory\Shell\VSNet2008CmdHere\command,,,"%11%\cmd.exe /k cd ""%1"" && ""C:\Program Files\Microsoft Visual Studio 9.0\Common7\Tools\vsvars32.bat"""

HKCR,Drive\Shell\VSNet2008CmdHere,,,"%VSNet2008CmdHereAccel%"

HKCR,Drive\Shell\VSNet2008CmdHere\command,,,"%11%\cmd.exe /k cd ""%1"""

 

[Strings]

VSNet2008CmdHereName="VS.NET 2008 Command Prompt Here PowerToy"

VSNet2008CmdHereAccel="VS.NET &2008 CMD Prompt Here"

UDHERE="Software\Microsoft\Windows\CurrentVersion\Uninstall\VSNet2008CmdHere"


Fix for Subversion problem: Error - Checksum Mismatch for 'FileName' expected 'XXXXX-XXXX' actual 'XXXX-XXXX'

I got this error today when my client relocated our subversion repository to Atlanta...I don't really need to go into how much I dislike subversion. Naturally, things like this wouldn't happen with MS Team Foundation Server (TFS)! :o)

When I got the problem, deleting the files mentioned in the error didn't work. This is because the problem actually lies in the "entries" file in the hidden .svn or _svn directories. So you can either:
  1. Blow away the bad svn folder by deleting it in your working copy and do an update. If all your folders have the issue, the simplest way is to recursively delete all svn folders (See http://ddkonline.blogspot.com/2008/02/reg-file-to-allow-delete-of-all-svn.html
    and rename the folder. Then do an SVN update and do a folder compare with a tool like the stellar Araxis Merge (http://www.araxis.com/merge/index.html) to check all changes are in or need to be reapplied.

  2. (OR if you have local changes), follow the post below:

    http://glob.bushi.net.nz/glob/2007/02/14/subversion-checksum-mismatch-easy-workaround/


    I discovered an alternate solution to the checksum mismatch problem.
    If we say the problem is in _templates.php, then you need to edit the ‘entries’ file in the .svn folder of that file.

    This file could look like this:
    .
    .
    ^L
    _templates.php
    file

    2007-08-22T12:26:44.000000Z
    5e1bdc7a206840b14c984e95c9c95c39
    2007-08-21T22:28:44.968799Z
    410
    hans
    ^L
    gamingzone.php
    .
    .

    From ‘entries’ you need to delete from the line _templates.php down to the line before gamingzone.php. Save, rename _templates.php and do svn update, and everything should be fine again.

    Hope this helps someone.