.Net ramblings
# Thursday, 02 January 2014
Google Maps API v3 control for Asp.Net with client side infowindows
I found this opensource google maps v3 control for asp.net, download from nuget:
http://www.nuget.org/packages/Reimers.Google.Map
tutorials etc from the author: http://www.reimers.dk/

the one thing i couldn't work out was how to handle client-side info-window (pop ups) when you click on a map marker.  this control has good support for server side clicks but that isn't what i wanted because it is too slow for the user.  here is the code i put together to get this to work, assuming you have a 'Map' control on your web page called Map1:

private void AddPoint(string desc, double latitude, double longitude, int width, int height, string iconPath, bool HtmlDescription)
{
    Marker m = new Marker(Guid.NewGuid());
    m.Draggable = false;
    m.Icon = new Icon(new Uri(iconPath, UriKind.RelativeOrAbsolute));
    LatLng point = new LatLng();
    point.Latitude = latitude;
    point.Longitude = longitude;
    m.Point = point;
    m.Icon.Size = new Size(width, height);
    if(!HtmlDescription)    // use tooltip "Title"
        m.Title = desc;
    m.Clickable = true;
    m.Description =  desc;

    m.ClientSideHandlers.OnClick = String.Format(@"var infowindow = new google.maps.InfoWindow();
            infowindow.setContent('{0}');
            infowindow.open(reimers.map.Map1, reimers.map.Map1.overlays[{1}]);", desc.Replace("'", "\""), this.Map1.Overlays.Count);
 
    this.Map1.Overlays.Add(m);        
}

Thursday, 02 January 2014 15:22:35 (GMT Standard Time, UTC+00:00)  #    Comments [0]  Asp.Net

# Wednesday, 28 December 2011
Excel interop with .Net / Server 2008
I'm using Asp.Net to open excel files and read the values cell by cell, normally i prefer to import Excel files as a datatable, but some documents don't fit the strict rows/columns format required for importing into a DataTable.  the code i'm using is based on this from dotnetperls.  worked fine for the most part on my dev PC but I had a few issues trying to get this to work on a 32bit version of excel on a 64 bit Windows 2008 server.
here's what i had to do to get it working:
  • disable macros (throwing exceptions opening the workbook), requires a reference to the Microsoft.Office.Core assembly:
    excelApp.AutomationSecurity = Microsoft.Office.Core.MsoAutomationSecurity.msoAutomationSecurityForceDisable;   // disable macros
  • give launch / open permissions to the Excel application in DCOM config, this was tricky because it isn't shown in a 64 bit OS if the program is 32 bit, i found the answer here.
  • set the identity in DCOM for Excel to the interactive user.



Wednesday, 28 December 2011 21:16:30 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net General | Asp.Net

# Friday, 11 June 2010
Excel interop with Asp.Net
Ran into a permissions problem today using Excel interop code from within ASP.Net, which had worked fine from a windows forms application.
Thanks to 'Frosty' for his post which explains how to enable the appropriate permissions, reproducing it here in case the link ever goes down:

From command prompt / start->run... type dcomcnfg

Select Component Services->Computers->My Compter->DCOM Config

Scroll down and select Microsoft Excel Applicaton

Right click on Microsoft Excel Applicaton and select properties.

Select the Security tab

In Launch Permissions group box click Edit button.

Add the appropriate user for your particular situation. In my case, I
selected MyDomainName\Domain Users.

Make sure that Allow check box is checked for your appropriate user.

Click OK

In Access Permissions group box click Edit button.

Add the appropriate user for your particular situation. In my case, I
selected MyDomainName\Domain Users.

Make sure that Allow check box is checked for your appropriate user.

Click OK

Excel interop will now work via asp.net


Friday, 11 June 2010 18:32:37 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  Asp.Net

# Tuesday, 21 April 2009
Javascript dynamic anchor select menu
I got a request from a client for a 'mini' navigation menu to appear on a page to allow the user to 'jump' to the various sections of the page.  Maintaining a set of named anchor hyperlinks in a document (with a javascript-based select menu) is not really an option for someone who doesn't know HTML, which is most people who use content management.  In the wrong hands this creates more trouble than it's worth. 
so i started thinking. the documents in question are well structured, using paragraphs and headings, as enforced by the content management system.  i worked out this solution which is exactly what i need, a maintenance free javascript jump menu, that doesn't add clutter to the document with named anchors.  it assumes that the all H2 tags should be displayed as links in the menu.  the javascript iterates through the H2 tags and loads up the menu, with a ScrollTo function used when an item is selected.

here's a screenshot


The code for the HTML page:
<script src="/JumpMenu.js" type="text/javascript"></script>
<select id="jumpNavSelect" name="jumpNavSelect" onchange="JumpToHeading(this.selectedIndex)">
<option value="">On this page...</option>
</select>
and the JumpMenu.js file
var array;

function CreateAnchorMenu() {
array = document.getElementsByTagName("h2");
var src = document.getElementById('jumpNavSelect');
// iterate through all H2 headings, add dropdown items for each.
for (var i = 0; i < array.length; i++) {
var heading = array[i];
var text = heading.firstChild.nodeValue;
if (!text)
continue;
src[i+1] = new Option(text, i);
}
}

function JumpToHeading(HeadingIndex) {
if (HeadingIndex == 0)
return;
var heading = array[HeadingIndex-1];
ScrollToElement(heading);
}

function ScrollToElement(theElement) {
var selectedPosX = 0;
var selectedPosY = 0;
while (theElement != null) {
selectedPosX += theElement.offsetLeft;
selectedPosY += theElement.offsetTop;
theElement = theElement.offsetParent;
}
window.scrollTo(selectedPosX, selectedPosY);
}

window.onload = CreateAnchorMenu;

Tuesday, 21 April 2009 13:30:58 (GMT Daylight Time, UTC+01:00)  #    Comments [3]  .Net General | Asp.Net

# Tuesday, 24 March 2009
Asp.Net membership, correcting username case at login
if your application has users who like to log in as "JOE BLOGGS" or "joe bloggs" when their username is actually "Joe Bloggs", you might want to ensure that they login with the correct "case" of their username.  It can cause discrepancies if you refer to the current Identity.User in your web application, for example if you use the username anywhere in your database and run reports grouped on the username, you will get multiple records for each variation on the case of the username.  It is surprisingly unintuitive to solve this problem.  Firstly you would think that Asp.Net membership would take care of this itself.  Then you would think that you could determine the correct case of the username as follows:
protected void Login1_LoggedIn(object sender, EventArgs e)
{
string correctUsername = Membership.GetUser(this.Login1.Username).Username;
FormsAuthentication.SetAuthCookie(correctUsername , true);
}

but this doesn't work because the "Username" property of the MembershipUser object does not collect its value from the AspNetDB SQL database like you would expect, instead it is filled with whatever you pass it when loading the user, this must be a bug but i'm not bothered trying to convince MS.  instead, i came up with this solution below, to directly load the AspNetUser object from a Linq DataSource of the AspNetDb database (created using SqlMetal).

protected void Login1_LoggedIn(object sender, EventArgs e)
{
// correct the case of the username
string Username = this.Login1.UserName;
AspNetDb db = new AspNetDb();
MembershipUser memUser = Membership.GetUser(Username); // load the MembershipUser object to get the UserID
Aspnet_User aspnetUser = db.Aspnet_Users.SingleOrDefault(z => z.UserId == new Guid(memUser.ProviderUserKey.ToString()));
if(aspnetUser != null)
Username = aspnetUser.UserName;
FormsAuthentication.SetAuthCookie(Username, true);
}


Tuesday, 24 March 2009 16:38:48 (GMT Standard Time, UTC+00:00)  #    Comments [1]  Asp.Net

# Friday, 27 February 2009
Crystal Reports date format (in code)
Cstr(CDate({Table.Column}), "dd/MM/yyyy")


Friday, 27 February 2009 17:47:52 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net General | Asp.Net

# Thursday, 19 February 2009
A simple Auto-Save / Keep Alive feature for Asp.Net
today i came across a situation where a user was spending a long time filling out a form.  they went past the session timeout and when they hit save they got booted back to the login screen, having lost the previous 30 minutes of work.  i looked around the net and found a few options with AJAX and what not but it doesn't make sense to keep renewing the session automatically otherwise you might as well not have a timeout, which is there for security reasons anyway.  in my case i don't actually want to save the information to the database (it's a transaction) until the user hits the Save button, i just want to keep the session alive. 

so i came up with this idea to ask the user if they want to keep their session alive with a javascript prompt every 15 minutes. if they leave their computer and don't answer the prompt, the session will time out since they won't have interacted with the web site (regardless of how they answer the prompt after the timeout).  if they answer OK to the prompt it invokes a Ping() type web service to keep the session alive.  this is the most lightweight way i could think of doing it.  i don't want to post back to the page because that will interrupt the user and make them wait for the page to reload.

there are 3 parts:

"KeepAlive" function

I put this function in a 'Util' class so pages can easily turn on the 'Keep Alive' functionality, simply call Util.KeepAlive(this);

public static void KeepAlive(Page p)
{
p.ClientScript.RegisterClientScriptInclude("KeepAlive", "/KeepAlive.js");
}

Ping.asmx (add the web service to your root folder)

[WebMethod]
public void Ping()
{
HttpContext.Current.Response.Write("OK");
HttpContext.Current.Response.End(); // this makes the result easier to parse than an XML web service message
}

KeepAlive.js (put this file in your root folder)

var timerID = 0;	     // used to track the timer function
var interval = 1000*60*15; // 15 mins
var KeepAliveUrl = '/Ping.asmx/Ping'; // replace your web service address here
var xmlhttpKeepAlive;

function AutoSaveSubmit()
{
if(confirm('15 minutes of idle time has passed, do you want to keep your session active?'))
InvokeWebService();
}

function InvokeWebService()
{
if (window.XMLHttpRequest)
{
xmlhttpKeepAlive=new XMLHttpRequest();
xmlhttpKeepAlive.onreadystatechange = xmlhttpChangeKeepAlive;
xmlhttpKeepAlive.open('GET',KeepAliveUrl + '?T=' + timerID,true);
xmlhttpKeepAlive.send(null);
}
// code for IE
else if (window.ActiveXObject)
{
xmlhttpKeepAlive=new ActiveXObject('Microsoft.XMLHTTP')
if (xmlhttp)
{
xmlhttpKeepAlive.onreadystatechange = xmlhttpChangeKeepAlive;
xmlhttpKeepAlive.open('GET', KeepAliveUrl + '?T=' + timerID, true);
xmlhttpKeepAlive.send();
}
}
return false;
}

function xmlhttpChangeKeepAlive()
{
var text;
if (xmlhttpKeepAlive.readyState == 4)
{
text = xmlhttpKeepAlive.responseText;
if (xmlhttpKeepAlive.status==200) // OK
{
if(text == 'OK') // reset the timer
timerID = setTimeout('AutoSaveSubmit()', interval);
else
alert('Your session has already expired.\nIf you have any information on this page you will lose it if you try to save this page now. You have 2 options to avoid losing the information on this screen.\n\nOption 1: open a new internet window and log in again to the web site, then close that window and go back to this window, at which point you will have a new session and you will be able to save the information.\n\nOption 2: Copy all the text you have typed on this page and paste it into another program (such as Word or Notepad), then log in to the web site again (click the home page) and come back to this page, and paste in the text again.\n\nTo prevent this happening in the future, click OK on the 15 minute reminder box each time it appears, this will keep your session active. If you do not answer the reminder within 10 minutes the session may expire.');
}
else
alert('Error: ' + xmlhttpKeepAlive.status + '. The session may have already expired, please try to save the page now');
}
}
window.setTimeout('AutoSaveSubmit()',interval);

You might be wondering why i append the timer code/number to the url of the web service.  The reason is because IE has a caching bug and will not actually send the XmlHttp request unless the URL is different to what it has in its history, it will just return the previous result.


Thursday, 19 February 2009 16:18:50 (GMT Standard Time, UTC+00:00)  #    Comments [10]  Asp.Net

# Tuesday, 23 December 2008
HowTo: disable the Excel prompt for opening formats different than the extension
many web sites export tabular information from grids/tables into 'Excel' format. the file is not a true Excel format and actually contains X/HTML.  if the file is opened in Excel 2007 you may get a prompt that the file is in a different format than specified by the file extension.  if you regularly work with files like this, there is a way to disable this prompt, add a DWORD key called ExtensionHardening with value 0 to HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\Excel\Security
it works straight away, no need for a reboot.


Tuesday, 23 December 2008 12:08:07 (GMT Standard Time, UTC+00:00)  #    Comments [0]  Asp.Net

# Sunday, 26 October 2008
IIS6 Service Unavailable, after installing KB 958644
IIS went down this morning after KB 958644 was isntalled automatically last night. My sites were showing "Service Unavailable"
there were several system event logs like so:
  • A process serving application pool '.Net 2.0 App Pool' suffered a fatal communication error with the World Wide Web Publishing Service. The process id was '2960'. The data field contains the error number.
  • Application pool '.Net 2.0 App Pool' is being automatically disabled due to a series of failures in the process(es) serving that application pool.

rebooting the server did not solve the problem.  so, on further research, KB article 885654 revealed the cause of the errors, lack of registry permissions for NETWORK SERVICE.  i hadn't changed any configuration on the server so assumed it was caused by a change in that windows update package.  i dug out ProcessMonitor and set up a registry filter for w3wp.exe where the Result was ACCESS DENIED.  there were about 50 entries during the time i enabled event capture and loaded up one of the sites that uses the affected application pool.  Most of them pointed to SystemCertificates entries.  I went through each one in regedit and gave NETWORK SERVICE full control where the process attempted to Create a key, and Read only access where the process attemped to Open the key.  worked fine then.

hope this helps someone out.

Sunday, 26 October 2008 13:41:10 (GMT Standard Time, UTC+00:00)  #    Comments [0]  Asp.Net | Windows Server

# Thursday, 17 July 2008
Euro symbol character problems...
if you ever send a string across a web service, and write it out to a file, make sure you specify Encoding.UTF8 explicitly. otherwise characters such as the EURO symbol may not render correctly in some browsers (IE6).  it took me ages to pin this down, because everything i read about was about HTTP header charset values, or HTML document charsets, or database encodings.  In my case i was using the default encoding and this messed up EURO symbols. I suspect it is because of the string being serialized in the web service, but haven't the time to look into it any further.  it's fixed now anyhow.


Thursday, 17 July 2008 17:25:32 (GMT Daylight Time, UTC+01:00)  #    Comments [3]  .Net General | Asp.Net

# Thursday, 31 January 2008
Confirm prompt for DropDownList
this wasn't very obvious to work out, and i couldn't find a decent solution online, so i came up with this.
this.DropDownList1.Attributes.Add("onchange", "if(!confirm('Are you sure you want to delete this item?')) return;");
This works with .Net 2 and 3 and 3.5, but may break with future versions if .Net changes the javascript code for AutoPostBack drop down lists.  The reason it works now is because .Net adds the custom 'onchange' attribute before it's on doPostBack() function call.  So if we return from the confirm prompt, the form will not submit. 


Thursday, 31 January 2008 16:32:13 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net General | Asp.Net

# Tuesday, 27 November 2007
Deploy Crystal Reports for VS 2008 (RTM)
if, like me, you were running VS 2008 Beta 2 on a server and you had crystal reports working fine, and then recompiled your projects in VS 2008 RTM, you may get an error like this when you try and create a crystal report on a production server:
System.Runtime.InteropServices.COMException (0x800736B1):
Retrieving the COM class factory for component with CLSID
{5FF57840-5172-4482-9CA3-541C7878AE0F} failed due to the following error:
800736b1. at CrystalDecisions.CrystalReports.Engine.ReportDocument..cctor()
In my case, i had installed Crystal Reports Runtime for VS 2008 Beta 2, and i never installed the runtime for the RTM version.  it should be available on your VS machine, if you opted for CR in the VS installer:
C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bootstrapper\Packages\CrystalReports10_5

should probably uninstall the old version of the CR runtime while you're at it too. 


Tuesday, 27 November 2007 11:32:12 (GMT Standard Time, UTC+00:00)  #    Comments [2]  .Net General | Asp.Net | Windows Server

# Friday, 16 November 2007
Running WSE 2 and 3 side by side

Note: this doesn't really work..

i'm just leaving it here for interest.  the approach will work for the first invocation of either v2 or v3 client, but after a v2 client has used the web service, any v3 clients will fail.  MS told me this is not supported because of the 2 soapExtensions interfere with each other in the pipeline etc


My asp.net CMS has some winforms clients running WSE2, and i'm doing an upgrade to the client (and server) to use WSE3, but i want to support both versions.  Both WSE2 and WSE3 clients are using a CustomUsernameTokenManager.  it was a bit tricky to work out, but here is what i ended up doing. thanks to brian o'keefe for his newsgroup post for most of the answer.  Your exact config might vary but hopefully it will save you some of the hassle of working all this out.
  • Leave the existing web service in place, e.g. Service_WSE2.asmx
  • Create a new web service for the WSE3 client, essentially copy/paste the ASMX file, e.g. Service_WSE3.asmx
  • Create a new CustomUsernameTokenManager for the WSE3 service which inherits from Microsoft.Web.Services3.Security.Tokens.UsernameTokenManager.  In my case it was as easy as copy/paste from the WSE2 token manager and change all the WSE2 namespaces to WSE3.
  • then in web.config, make sure both WSE config sections are listed:
    <configuration>
    <configSections>
    <section name="microsoft.web.services3" type="Microsoft.Web.Services3.Configuration.WebServicesConfiguration, Microsoft.Web.Services3,Version=3.0.0.0,Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <section name="microsoft.web.services2" type="Microsoft.Web.Services2.Configuration.WebServicesConfiguration, Microsoft.Web.Services2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </configSections>
  • add in references to both the assemblies for the compilation section (not 100% sure if this is necessary, but at least it means the app will not compile on the production server if you forget to deploy either of the WSE assemblies, instead of waiting till one of your clients connects).

    	<compilation defaultLanguage="c#" debug="true">
    <assemblies>
    <add assembly="Microsoft.Web.Services2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
    <add assembly="Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
  • remove the <webServices> section from <system.web>, because each web service should be configured by location, not global as this interferes.

    	<location path="Service_WSE2.asmx">
    <system.web>
    <webServices>
    <soapExtensionTypes>
    <add type="Microsoft.Web.Services2.WebServicesExtension, Microsoft.Web.Services2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" priority="1" group="0" />
    </soapExtensionTypes>
    </webServices>
    </system.web>
    </location>

    <location path="Service_WSE3.asmx">
    <system.web>
    <webServices>
    <soapExtensionTypes>
    <clear/>
    </soapExtensionTypes>
    <soapServerProtocolFactory type="Microsoft.Web.Services3.WseProtocolFactory, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
    <soapExtensionImporterTypes>
    <add type="Microsoft.Web.Services3.Description.WseExtensionImporter, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
    </soapExtensionImporterTypes>
    </webServices>
    </system.web>
    </location>
  • my webservice policy are defined in policyCache.config files, so the following web.config sections point each version of WSE to the right file:
    	<microsoft.web.services3>
    <policy fileName="policyCache_WSE3.config"/>
    <security>
    <securityTokenManager>
    <add type="Whatever.CustomUsernameTokenManager3" namespace="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" localName="UsernameToken"/>
    </securityTokenManager>
    </security>
    </microsoft.web.services3>
    <microsoft.web.services2>
    <messaging>
    <maxRequestLength>10000</maxRequestLength>
    </messaging>
    <security>
    <securityTokenManager type="Whatever.CustomUsernameTokenManager2, MyAssemblyName" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
    qname="wsse:UsernameToken" />
    <defaultTtlInSeconds>86400</defaultTtlInSeconds>
    <timeToleranceInSeconds>86400</timeToleranceInSeconds>
    </security>
    <policy>
    <cache name="policyCache_WSE2.config" />
    </policy>
    </microsoft.web.services2>
  • and lastly, the policy cache files themselves:
    WSE 3 version
    <?xml version="1.0"?>
    <policies xmlns="http://schemas.microsoft.com/wse/2005/06/policy">
    <extensions>
    <extension name="usernameOverTransportSecurity" type="Microsoft.Web.Services3.Design.UsernameOverTransportAssertion, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <extension name="requireActionHeader" type="Microsoft.Web.Services3.Design.RequireActionHeaderAssertion, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </extensions>
    <policy name="usernameTokenSecurity">
    <usernameOverTransportSecurity />
    <requireActionHeader />
    </policy>
    </policies>
  • WSE 2 version
    <?xml version="1.0"?>
    <policyDocument xmlns:wse="http://schemas.microsoft.com/wse/2003/06/Policy" xmlns="http://schemas.microsoft.com/wse/2003/06/Policy">
    <mappings xmlns:wse="http://schemas.microsoft.com/wse/2003/06/Policy">

    <endpoint uri="http://localhost/Service_WSE2.asmx">
    <defaultOperation>
    <request policy="#username-token-signed" />
    <response policy="" />
    <fault policy="" />
    </defaultOperation>
    </endpoint>

    <defaultEndpoint>
    <defaultOperation>
    <request policy="" />
    <response policy="" />
    <fault policy="" />
    </defaultOperation>
    </defaultEndpoint>

    </mappings>
    <policies xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    <wsp:Policy wsu:Id="username-token-signed" xmlns:wsp="http://schemas.xmlsoap.org/ws/2002/12/policy" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing" xmlns:wssp="http://schemas.xmlsoap.org/ws/2002/12/secext">
    <wsp:MessagePredicate wsp:Usage="wsp:Required" Dialect="http://schemas.xmlsoap.org/2002/12/wsse#part">
    wsp:Body() wsp:Header(wsa:To) wsp:Header(wsa:Action) wsp:Header(wsa:MessageID) wse:Timestamp()
    </wsp:MessagePredicate>
    <wssp:Integrity wsp:Usage="wsp:Required">
    <wssp:TokenInfo>
    <SecurityToken xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext">
    <wssp:TokenType>http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#UsernameToken</wssp:TokenType>
    <wssp:Claims>
    <wssp:UsePassword wsp:Usage="wsp:Required" />
    </wssp:Claims>
    </SecurityToken>
    </wssp:TokenInfo>
    <wssp:MessageParts Dialect="http://schemas.xmlsoap.org/2002/12/wsse#part">
    wsp:Body() wsp:Header(wsa:Action) wsp:Header(wsa:FaultTo) wsp:Header(wsa:From) wsp:Header(wsa:MessageID) wsp:Header(wsa:RelatesTo) wsp:Header(wsa:ReplyTo) wsp:Header(wsa:To) wse:Timestamp()
    </wssp:MessageParts>
    </wssp:Integrity>
    </wsp:Policy>
    </policies>
    </policyDocument>


Friday, 16 November 2007 12:16:50 (GMT Standard Time, UTC+00:00)  #    Comments [1]  .Net General | .Net Windows Forms | Asp.Net

# Tuesday, 18 September 2007
LINQ Sql Using Skip And Take
Here is the sql that is generated by a DataContext when you use Skip() and Take() to efficiently select records for the grid:
SELECT TOP 10 [t1].[Name], [t1].[Address], [t1].[Tel1], [t1].[Tel2], [t1].[Email], [t1].[DateCommenced], [t1].[Comments], [t1].[Active], [t1].[Fax]
FROM (
SELECT ROW_NUMBER() OVER (ORDER BY [t0].[Name], [t0].[Address], [t0].[Tel1], [t0].[Tel2], [t0].[Email], [t0].[DateCommenced], [t0].[Comments], [t0].[Active], [t0].[Fax]) AS [ROW_NUMBER], [t0].[Name], [t0].[Address], [t0].[Tel1], [t0].[Tel2], [t0].[Email], [t0].[DateCommenced], [t0].[Comments], [t0].[Active], [t0].[Fax]
FROM [dbo].[Table1] AS [t0]
) AS [t1]
WHERE [t1].[ROW_NUMBER] > @p0
-- @p0: Input Int32 (Size = 0; Prec = 0; Scale = 0) [50]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

it's not immediately obvious why they use the subquery like this, but i'm sure they have been very thorough in optimising LINQ. 


Tuesday, 18 September 2007 13:22:32 (GMT Daylight Time, UTC+01:00)  #    Comments [1]  .Net General | Asp.Net | Database

# Wednesday, 29 August 2007
IE problems with javascript window.open()
i couldn't figure out why IE was giving me an 'invalid argument' for using code like:
window.open("http://whatever", "1234321-ABCDE-1234231");
the problem is that IE only accepts alphanumeric and underscore characters for the second parameter (window name), so the hyphens were causing problems.  just trim them out and it will work fine.


Wednesday, 29 August 2007 13:34:31 (GMT Daylight Time, UTC+01:00)  #    Comments [1]  Asp.Net

# Friday, 27 July 2007
VS 2008: First impressions
Just got up and running with VS 2008 Beta 2 and converted my Orcas Beta 1 projects over to VS 2008.  have a read of Scott Gu's post on downloading and installing.  whaddya know? VS 2008 hasn't crashed yet :)  seriously though, this is a major relief to see good stability.  as a developer it did my head in to have to keep nuking devenv.exe and losing a few minutes work, driving me quietly insane.  hopefully those days are over now.  Beta 1 was a thousand times better than VS 2005, but Beta 2 looks even better. 

in terms of breaking changes with upgrading projects etc., here are some tidbits i came across
  • the namespace System.Data.Linq.Expressions no longer exists.  i just deleted this namespace and it worked fine, they must have moved various classes back into the root Linq namespace or something.
  • if you use SqlMetal, you will have to use the new version of this tool, which i found at C:\Program Files\Microsoft SDKs\Windows\V6.0A\Bin\SqlMetal.exe. The generated code from the new tool is very different to before so you will need to regenerate in order to use the new LINQ libraries.
  • web.config changes are also introduced.  the best way to figure out the changes is to open up a blank web application and compare the standard web.config in the empty project to your own web.config.
  • Crystal Reports assembly version numbers are the same with this release of VS, 10.5.3700.0, so no changes here since Beta1.
i'll post more next week when i have some real experience using VS 2008.  so far so good.  great job Scott & Co.

Update 30 July 2007 - Deployment Issues
  • To deploy a VS 2008 beta 2 web application to a Server 2003 with .net runtime 2.0, you need to install 3.5 of the runtime.  i got all sorts of web.config errors due to the new syntax with beta 2.  A web app compiled against v3.5 in Beta 1 had no trouble running on top of the 2.0 runtime with the various v3.5 DLLs deployed to the /bin directory, however this doesn't seem to be possible anymore.  it took me a while to find the correct download for the 3.5 beta 2 redistributable, it installed in about 10 mins and required a reboot, and did not affect any of the v1.1 and v2 web sites running on the server. 


Friday, 27 July 2007 17:49:16 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  .Net General | .Net Windows Forms | Asp.Net

# Thursday, 05 July 2007
HowTo: Invert datagrid rows and columns
this is a bit of a strange one.  I have a table and it has approx 60 columns, with only one row being shown.  I wanted a human-readable display of all the fields in the table for any given row, i.e. i didn't want the user to have to scroll several screens across just to read the data for a single row.  Vertical scrolling is obviously easier, so i wrote a Repeater implementation, which works nicely.  For a table with X columns and Y rows, the output will have X rows and Y columns.  Screenshot below shows the output of all the ServerVariables in a HTTP-request, which is a good example of a datasource that does not naturally fit the traditional 'grid' display.
<!-- ASPX usage -->
<xyz:VerticalGrid ID="vGrid" runat="server" />

// sample databinding...
this.vGrid1.DataSource = new DataView(dataSet1.Tables[0]);
this.vGrid1.DataBind();

/// <summary>
/// A Repeater control that shows the rows and columns inverted.
/// No template is required for the items.
/// The datasource must be an ADO.Net DataView
/// </summary>
[ToolboxData("<{0}:VerticalGrid runat=server></{0}:VerticalGrid>")]
public class VerticalGrid : Repeater
{
public VerticalGrid()
{
this.EnableViewState = false;
}

protected override void Render(HtmlTextWriter writer)
{
// invert the rows and cols
if (!(base.DataSource is DataView))
throw new Exception("Error: only a dataview datasource can be used");

DataView dv = base.DataSource as DataView;

// output start of table
writer.Write("\n<table class='grid'>");

// output each row
for (int i = 0; i < dv.Table.Columns.Count; i++)
{
writer.Write("\n<tr class='{0}'><th>{1}</th>", i % 2 == 1 ? "gI" : "gA", dv.Table.Columns[i].ColumnName); // output the column name in the first cell. class names gI and gA are abbreviations of gridItem gridAlternatingItem, to reduce output markup.
for (int j = 0; j < dv.Count; j++)
writer.Write("<td>{0}</td>", dv[j].Row.IsNull(i) ? "&nbsp;" : dv[j][i].ToString());
writer.Write("</tr>");
}
writer.Write("\n</table>");
}
}




Thursday, 05 July 2007 15:53:40 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  Asp.Net

# Thursday, 17 May 2007
Deploying Crystal Reports for Orcas Beta 1

I know we're not supposed to be going live with .Net 3.5 / Orcas Beta 1 yet, but hey. 

I ran into a Crystal Reports deployment problem with an asp.net web application developed in Orcas beta 1, using the bundled version of crystal reports.  The problem is that the crystal report dlls used in a Beta 1 project are 10.5.3700.0 but there don't appear to be any merge modules available to support this version number, so there is no supported way to run the Orcas version of Crystal Reports on a server, without installing Orcas itself.  I tried numerous options of digging out the 10.5.3700.0 dlls from program files\common files\business objects etc and putting them in the web site bin directory on the server, but that didn't work.  The obvious error message that comes up is as follows:

Could not load file or assembly 'CrystalDecisions.CrystalReports.Engine, 
Version=10.5.3700.0, Culture=neutral, PublicKeyToken=692fbea5521e1304' or one of its dependencies.
The system cannot find the file specified.

what i ended up doing was instructing the web application to bind to the 10.2.3600.0 versions of the assemblies, which are already deployed using the normal CR server install.

here is what i added to the end of my web.config file to get it going:

     ....
<runtime>
        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
            <dependentAssembly>
                <assemblyIdentity name="CrystalDecisions.CrystalReports.Engine" publicKeyToken="692fbea5521e1304"/>
                <bindingRedirect oldVersion="10.5.3700.0" newVersion="10.2.3600.0"/>
            </dependentAssembly>
            <dependentAssembly>
                <assemblyIdentity name="CrystalDecisions.CrystalReports.Shared" publicKeyToken="692fbea5521e1304"/>
                <bindingRedirect oldVersion="10.5.3700.0" newVersion="10.2.3600.0"/>
            </dependentAssembly>
            <dependentAssembly>
                <assemblyIdentity name="CrystalDecisions.Shared" publicKeyToken="692fbea5521e1304"/>
                <bindingRedirect oldVersion="10.5.3700.0" newVersion="10.2.3600.0"/>
            </dependentAssembly>
        </assemblyBinding>
    </runtime>
</configuration>

Thursday, 17 May 2007 17:36:06 (GMT Daylight Time, UTC+01:00)  #    Comments [1]  .Net General | Asp.Net

# Wednesday, 02 May 2007
Crystal Reports: Incorrect Log on Parameters for Dataset source
i spent hours trying to troubleshoot this.  obviously the error makes no sense because CR should not be logging on to anything in a dataset scenario.
anyway, a thousand thanks to Jason for his post on the MSDN forums with the simple answer: set the datasource to the DataTable, not the DataSet.

i can't wait to lose Crystal reports altogether and move to the microsoft reporting thingy, i haven't had time to play about with it yet but at least it will be properly programmed and it won't contain the 10 years of bugs that crystal reports have carried through each release of their software.  </RANT>


Wednesday, 02 May 2007 18:43:46 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  .Net General | Asp.Net

# Monday, 30 April 2007
LINQ: upgrading May CTP projects to Orcas Beta 1
here are a few of my main findings/hurdles encountered when upgrading May CTP web projects to Orcas beta 1 Web Application Projects.  I'm targeting version 3.5 of the framework for deployment on a Server 2003 with .Net 2.0 runtime installed, i will update this article if it doesn't work out. 
  • if you had any code in the "App_Code" folder, VS will probably have set the compile action to "content", it should be changed to "compile".  otherwise you will get errors like: The type or namespace name 'xyz' could not be found (are you missing a using directive or an assembly reference?)
  • you'll have to remove any reference to the System.Query and System.Expressions, these are not part of the new LINQ spec.
  • Replace System.Data.DLinq with System.Data.Linq.
  • if you want to use any LINQ expressions, like "from x in db.Table select x" then you need to include the System.Linq namespace.  otherwise you will get errors like 'System.Data.Linq.Table<xyz>' does not contain a definition for 'Where' and no extension method 'Where' accepting a first argument of type 'System.Data.Linq.Table<xyz>' could be found (are you missing a using directive or an assembly reference?)
  • you will also need to include System.Linq.Expressions if you are using "language-level code expressions to be represented as objects in the form of expression trees".
The old LINQ assemblies are versioned 1.0.2319.19044 (May CTP) but the new ones for Beta 1 are versioned 2.0.0.0.  Have a check through the referenced assemblies in your project to make sure you have the latest versions. 

Crystal reports

look out for the new crystal report assemblies, 10.5.3700.0.  a web app referencing the new versions in web.config will complain if these versions are not available on the server.  i'll update soon when i have found out how to do this.


Monday, 30 April 2007 11:50:28 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  .Net General | Asp.Net

# Friday, 09 March 2007
Really simple and affordable web server monitoring
If you run a web server, chances are you have some form of automated monitoring system in place.  If you use MOM or another enterprise level thing then this post won't be of much relevance.  If, like me, you have simpler requirements, read on.

I have been caught out a few times with my web sites being down because Windows Server 2003 automatically installed an update and something went wrong and IIS got stopped, or like this morning at 4am, SQL 2005 SP2 failed to install and left the SQL Service offline.  i didn't find out till i got a phone call from a client.

My datacenter provide very good ping monitoring with SMS alerts etc., but this is not a complete solution because the web site may have a configuration error, and it will still respond to pings.  similarly, you can't just check for an OK HTTP status code because your error ASPX page may not be configured to send an error HTTP status code.

I have used various online web site monitoring services, with varying degrees of success / satisfaction.  My current provider are InternetVista.com and for €70 a year i get a 10 minute check for a single HTTP site, with a keyword match on the contents of the page, and an email/sms alert if the match is not found.  You can pay for extra and more frequent checks, but €70 is as much as i think the service is worth.  To have this level of checking done on 10 sites would cost a lot, so to save a few quid i wrote a very simple aspx page that does a series of tests on all the resources i want to verify on the server, e.g. SQL Server, MS Access, IIS web sites.  The aspx code is listed below, i wrote it inline rather than compiled/dll because it is easier to deploy in an existing web site without any risk of any side effects (dll collisions), it should be straight forward to understand for a c# programmer.  let me know if you have questions.  It runs in a few miliseconds on my server so i'm not worried about polling all these resources every 10 mins.



Run_Server_Tests.aspx code:
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Collections" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.OleDb" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Net" %>

<script RunAt="server">

/* Server Monitoring Script:
* - test SQL databases by running an sql string against an SQL connection string
* - test Access databases by running an sql string against a JET connection string
* - test web sites by Regex matching a search string against the contents of a HttpWebRequest
*/

enum TestType {Sql_Server, Ms_Access, Http_Request } // different types of supported requests

/// <summary>
/// Container class to represent a 'test' object for a resource on the server.
/// </summary>
class TestObject
{
public TestType Type; // e.g. Sql_Server.
public string TestString; // e.g. connection string for a database. or URI for http request.
public string TestParam; // e.g. sql string for a database. or search string for a http request.

public TestObject(TestType type, string testString, string testParam)
{
this.Type = type;
this.TestString = testString;
this.TestParam = testParam;
}
}

void Page_Load(object sender, EventArgs e)
{
List<TestObject> tests = new List<TestObject>();

tests.Add(new TestObject(TestType.Sql_Server, @"Data Source=.\SQLEXPRESS;Initial Catalog=DB1;Integrated Security=True", "select top 10 * from Table1"));
tests.Add(new TestObject(TestType.Sql_Server, @"Data Source=.\SQLEXPRESS;Initial Catalog=DB2;Integrated Security=True", "select top 10 * from Table1"));
tests.Add(new TestObject(TestType.Sql_Server, @"Data Source=.\SQLEXPRESS;Initial Catalog=DB3;Integrated Security=True", "select top 10 * from Table1"));
tests.Add(new TestObject(TestType.Sql_Server, @"Data Source=.\SQLEXPRESS;Initial Catalog=DB4;Integrated Security=True", "select top 10 * from Table1"));

tests.Add(new TestObject(TestType.Ms_Access, @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=c:\Inetpub\Database\DB5.mdb;Persist Security Info=True", "select top 10 * from Table1"));
tests.Add(new TestObject(TestType.Ms_Access, @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=c:\Inetpub\Database\DB6.mdb;Persist Security Info=True", "select top 10 * from Table1"));

tests.Add(new TestObject(TestType.Http_Request, "http://mysite1.ie/", "Site 1"));
tests.Add(new TestObject(TestType.Http_Request, "http://mysite2.ie/", "Site 2"));
tests.Add(new TestObject(TestType.Http_Request, "https://mysite3.ie/", "Site 3"));
tests.Add(new TestObject(TestType.Http_Request, "https://mysite4.ie/", "Site 4"));
tests.Add(new TestObject(TestType.Http_Request, "https://mysite5.ie/", "Site 5"));

int numCompleted = 0;
int numFailed = 0;

// write the HTML header. (a result is flushed to the client after each test finishes.)
Flush(@"
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN' 'http://www.w3.org/TR/html4/loose.dtd'>
<html>
<head>
<title>Server Test</title>
<meta name='ROBOTS' content='NOINDEX,NOFOLLOW'>
<link rel='stylesheet' type='text/css' href='ServerTestStyles.css' />
</head>
<body>");

foreach(TestObject test in tests)
{
try
{
switch(test.Type)
{
case TestType.Sql_Server:
runQuerySql(test.TestParam, test.TestString);
break;
case TestType.Ms_Access:
runQueryOleDb(test.TestParam, test.TestString);
break;
case TestType.Http_Request:
string pageContents = new WebClient().DownloadString(test.TestString);
if(!Regex.IsMatch(pageContents, test.TestParam, RegexOptions.IgnoreCase))
throw new Exception("Search string not found: " + test.TestParam);
break;
default:
throw new Exception("Test type not handled " + test.Type);
}
Flush(String.Format("<span class='pass'>Pass</span> &nbsp; <span class='type'>{0}</span> &nbsp; {1} <hr />", test.Type, test.TestString));
numCompleted++;
}
catch(Exception ex)
{
Flush(String.Format("<span class='fail'>Fail</span> &nbsp; {1} <span class='type'>{0}</span><BR><span class='error'>{2}</span><hr />", test.Type, test.TestString, ex.Message));
numFailed++;
}
}
if(numFailed > 0)
Flush(String.Format("<h1>{0} errors occured</h1>", numFailed));
else
Flush(String.Format("<h1>All Good!</h1>", numFailed)); // if you use this page with an automated monitoring service, look for "All Good" in the page contents. otherwise an error occured
Flush("</body></html>");
}

/// <summary>
/// Method to run an sql string against an sql database
/// </summary>
public static DataSet runQuerySql(string sql, string connString)
{
SqlConnection conn = new SqlConnection(connString);
DataSet ds = new DataSet();
SqlDataAdapter dba = new SqlDataAdapter();
SqlCommand cmd = new SqlCommand(sql, conn);

try
{
dba.SelectCommand = cmd;
dba.Fill(ds, "Table");
return (ds);
}
catch(Exception e)
{
throw e;
}
finally
{
cmd.Connection.Close();
conn.Close();
}
}

/// <summary>
/// Method to run an sql string against an Access database
/// </summary>
public static DataSet runQueryOleDb(string sql, string connString)
{
OleDbConnection conn = new OleDbConnection(connString);
DataSet ds = new DataSet();
OleDbDataAdapter dba = new OleDbDataAdapter();
OleDbCommand cmd = new OleDbCommand(sql, conn);

try
{
dba.SelectCommand = cmd;
dba.Fill(ds, "Table");
return (ds);
}
catch(Exception e)
{
throw e;
}
finally
{
cmd.Connection.Close();
conn.Close();
}
}

/// <summary>
/// Flush output to the browser (useful to indicate which tests are causing any delay)
/// </summary>
/// <param name="output"></param>
private void Flush(string output)
{
Response.Write(output);
Response.Flush();
}

</script>

ServerTestStyles.css:  (just to make the output more legible)

body
{
font-size: 90%;
font-family: Calibri, Helvetica, Sans-Serif;
padding: .5em;
}

hr
{
color: #87ceeb;
background-color: #87ceeb;
margin: .3em 0 .3em 0;
padding: 0;
height: 1px;
}

.pass
{
color: Blue;
font-weight: bold;
}
.fail
{
color: Red;
font-weight: bold;
}
.type
{
color: purple;
font-weight: bold;
}
.error
{
color: Red;
font-size: small;
}

I have configured the test in InternetVista to search for "All Good" in the url for the test page.  If this isn't present, i'll get an SMS/email alert and i can go and see what exactly is wrong.  It should be fairly easy to add other test types if you have different resources you need to check on.
Enjoy.


Friday, 09 March 2007 17:43:24 (GMT Standard Time, UTC+00:00)  #    Comments [3]  .Net General | Asp.Net | Database | General

# Friday, 02 March 2007
FIX: VS 2005 crashes randomly when using Crystal Reports
Here is the event log crash entry:

Faulting application devenv.exe, version 8.0.50727.762, time stamp 0x45716759, faulting module craxddrt.dll_unloaded, version 0.0.0.0,
time stamp 0x43068582, exception code 0xc0000005, fault offset 0x0d26e30f, process id 0x1180, application start time 0x01c75cae13960a41.

it would crash completely randomly, even if i had closed all crystal reports and was working in a style sheet or other such harmless file. As i later found out, it was because the solution opened by default with the last crystal report i was working on, and this seemed to start some crystal report ActiveX thing that craps out on Vista.  sometimes it would take 2 minutes and sometimes 5 minutes, but it would always crash, even if i closed the report straight away.  The only way to get rid of it was to load up VS, close the report immediately and safely close VS before it got a chance to crash!  then next time it opens, there is no crystal report and the ActiveX control never loads.  it works ok now, although this is just one of a long list of complaints i have with VS 2005.  i think i'm allergic to crystal reports.


Friday, 02 March 2007 09:58:51 (GMT Standard Time, UTC+00:00)  #    Comments [5]  .Net General | Asp.Net

# Friday, 23 February 2007
Fix: VS 2005 compile error: The type 'xyz' exists in both X and Y
The full error i got was below:
Error    35    The type 'CrystalDecisions.Shared.ExportFormatType' exists in both 
'c:\Windows\assembly\GAC\CrystalDecisions.Shared\9.1.5000.0__692fbea5521e1304\CrystalDecisions.Shared.dll' and
'c:\Windows\assembly\GAC_MSIL\CrystalDecisions.Shared\10.2.3600.0__692fbea5521e1304\CrystalDecisions.Shared.dll'  
The reason is because version 9 and 10 of crystal reports are installed on my dev box and VS needed help deciding which one to use.  the fix was to specify the exact assembly binding to use in web.config, as follows:
	<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="CrystalDecisions.CrystalReports.Engine" publicKeyToken="692fbea5521e1304" />
<bindingRedirect oldVersion="9.1.5000.0" newVersion="10.2.3600.0"/>
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="CrystalDecisions.CrystalReports.Shared" publicKeyToken="692fbea5521e1304" />
<bindingRedirect oldVersion="9.1.5000.0" newVersion="10.2.3600.0"/>
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="CrystalDecisions.Shared" publicKeyToken="692fbea5521e1304" />
<bindingRedirect oldVersion="9.1.5000.0" newVersion="10.2.3600.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
thanks to rick ersek for the solution.


Friday, 23 February 2007 17:51:46 (GMT Standard Time, UTC+00:00)  #    Comments [2]  .Net General | Asp.Net

# Tuesday, 23 January 2007
HowTo: Correct Login Username Case
using Forms auth in an asp.net web site, with a standard Login control, i found a problem today where user 'Joe Bloggs' can log in with 'JOE BLOGGS' as his username.  this messes up my database a little because User.Identity.Name yields 'JOE BLOGGS' and i use this value in the application database. for consistency purposes i only want to use the case-correct version of the username, as it was created.

to fix this, i added the following code to my login
    protected void Login1_LoggedIn(object sender, EventArgs e)
    {
        // correct the case of the username
        foreach(MembershipUser u in Membership.GetAllUsers())
            if(u.UserName.ToLower() == this.Login1.UserName.ToLower())
            {
                // fix the username case
                FormsAuthentication.SetAuthCookie(u.UserName, true);
            }
    }


Tuesday, 23 January 2007 15:05:27 (GMT Standard Time, UTC+00:00)  #    Comments [4]  Asp.Net

# Wednesday, 29 November 2006
Problems with the AJAX AutoCompleteExtender (arrgh)
I was interested to see that Atlas/AJAX has now made it to Beta 2.  I looked at it earlier in the year but didn't have time to really get stuck in.  So today i eventually got it working in conjunction with LINQ.  I wanted to get a basic AutoComplete thing working off my database, with which i am hopefully going to use LINQ throughout.

First of all, go to http://ajax.asp.net/ and download and install the 3 bits they recommend. 

Getting the darn thing to work

Brad Adams has a very good getting-started example on this post, i used his article as a starting point, but i had to sort out a few other things first.  i couldn't get any documentation on the AutoCompleteExtender so i scavenged some crucial facts off the net after a lot of fumbling around, i've listed these below.  One mistake i made was to start testing the AutoComplete control before making sure that the web service was returning correct data, my LINQ query was chucking exceptions and the AJAX control doesn't give you any feedback on exceptions, which is understandable and probably desirable.
  • you must mark your web service class with the [ScriptService] attribute, you'll need the Microsoft.Web.Script.Services namespace for this.
    [WebService(Namespace = "http://whatever")]
    [ScriptService]
    public class AjaxService : System.Web.Services.WebService
    {
  • you need two parameters in your web service method, the naming of the parameters is crucial:
    [WebMethod]
    public string[] GetList(string prefixText, int count)
  • if you're using Linq, and if you're new at it like me, you might have trouble getting your LINQ query results into a string array for the web service.  I tried returning IEnumerable<TableName> and List<string> etc. but it didn't like that, i ended up using the ToArray() extended method that is part of System.Query.  Here is the query i used which concatenates some columns from the table into a collection of strings. 
    IEnumerable<string> ds =
                from r in MyDataBase.Restaurants
                where r.RestaurantName.Contains(prefixText)
                select (r.RestaurantName + "," + r.Address1 + "," + r.County); // join the name + address in a string
    string[] results = ds.ToArray<string>();
    return results;

Formatting the AutoCompleteExtender

The control has some nasty hard-coded values, but where there's a will there's a way.  Thanks to the Firefox dom inspector and the CSS !important modifier, all is not lost. 

The control does have a property called CompletionListElementID which specifies the container element where the results are inserted.  I put a simple DIV next to the textbox:
<div id="AutoComplete" runat="server"> </div>
Then in my stylesheet i have the following values to override the hard-coded styles set by the control:
#AutoComplete
{
    width: auto !important;   
    overflow: visible !important;
}
#AutoComplete div
{
    font-size: x-small !important;   
}
In case you're wondering, the control renders each result as a DIV inside your specified element.  The control applies the following styles to your specified element:
border: 1px solid buttonshadow; 
overflow: hidden;
visibility: visible;
background-color: window;
color: windowtext;
cursor: default;
width: 130px;
position: absolute;
left: 184px;
top: 27px;
display: inline;

You can override whichever ones you want by applying the !important attribute inside the #AutoComplete entry in the style sheet.  The individual items then can also be styled via the #AutoComplete div entry, i prefered a slightly smaller font size because the default setting looked too big.


Wednesday, 29 November 2006 16:54:25 (GMT Standard Time, UTC+00:00)  #    Comments [4]  Asp.Net

# Saturday, 07 October 2006
HowTo: Deploy Crystal Reports for VS 2005
i found this insanely difficult because none of the supposedly normal options worked.  merge modules didn't seem suitable because i just wanted to install the CR dlls once and for all on the server.  according to an MSDN2 article i should be able to perform a windows installer deployment, but of course when i copy the key code from the VS2005 > Help > About dialog, and use it in the installer on the server, the error message is that the key code has expired or is invalid.  'business objects' appear to be forcing users into an upgrade path for their new product, by claiming that a 'compatibility upgrade' is required for VS 2005, which of course you have to fork out for.  such a load of crap. 
anyway, what eventually worked for me was to go back into my VS 2005 dev machine and create a web set up project. go into the project properties and click the prerequisites button, select CR for .Net 2.0 and then just build the empty setup project. if you look in your output folder, there is an installation file called CRRedist2005_x86.msi.  Just whack this onto the server and your crystal reports should run fine, i didn't need to reboot or restart IIS.  Note there is no also need to copy any of the CR dlls to your web site bin folder. 

yet another miserable failure for crystal reports!


Saturday, 07 October 2006 01:32:30 (GMT Daylight Time, UTC+01:00)  #    Comments [1]  .Net General | Asp.Net | Windows Server

# Wednesday, 06 September 2006
FIX: VS2005 Unrecognised tag prefix or device filter
I encountered this error after upgrading a web site to VS2005 and converting it to a Web Application Project.  the project built without problems, it was just the designer kept throwing up errors in the error list which was very annoying.

Thanks to 'ratureus' for his post on http://forums.asp.net/thread/1212425.aspx

  • Close VS 2005
  • delete all files in C:\Documents and Settings\yourusername\Application Data\Microsoft\VisualStudio\8.0\ReflectedSchemas\
  • Restart VS 2005
this worked for me. 


Wednesday, 06 September 2006 11:50:04 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  Asp.Net

# Tuesday, 05 September 2006
PDF problems: File does not begin with %PDF
I use exported crystal reports in PDF format on my web site, and i was getting intermittent errors from a small number of users.  I couldn't reproduce it, and i found a thoroughly excellent article on the problem at the Fluid Components International web site. 

They explain what's going wrong and have some suggestions to fix it.  I suspect in my case it was a problem with embedding the PDF in an activeX control.  i'll just redirect users to download the file instead. 

Tuesday, 05 September 2006 15:29:49 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  Asp.Net

# Wednesday, 30 August 2006
A javascript function to enable MaxLength behaviour for Multiline Textboxes
http://www.dynamicdrive.com/dynamicindex16/maxlength.htm

<script type="text/javascript">

/***********************************************
* Textarea Maxlength script- © Dynamic Drive (www.dynamicdrive.com)
* This notice must stay intact for legal use.
* Visit http://www.dynamicdrive.com/ for full source code
***********************************************/

function ismaxlength(obj){
var mlength=obj.getAttribute? parseInt(obj.getAttribute("maxlength")) : ""
if (obj.getAttribute && obj.value.length>mlength)
obj.value=obj.value.substring(0,mlength)
}

</script>


<textarea maxlength="40" onkeyup="return ismaxlength(this)"></textarea>


Wednesday, 30 August 2006 17:12:43 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  Asp.Net

# Wednesday, 16 August 2006
Global country list
i took this off Wikipedia and trimmed it down to plain text for a database.  just posting it here for reference:

You may be more interested in the ISO official list of countries and their 2-digit codes.

Abkhazia
Afghanistan
SBA Akrotiri and Dhekelia
Åland
Albania
Algeria
American Samoa
Andorra
Angola
Anguilla
Antigua and Barbuda
Argentina
Armenia
Aruba
Saint Helena Ascension Island
Australia
Austria
Azerbaijan
The Bahamas
Bahrain
Bangladesh
Barbados
Belarus
Belgium
Belize
Benin
Bermuda
Bhutan
Bolivia
Bosnia and Herzegovina
Botswana
Brazil
Brunei
Bulgaria
Burkina Faso
Burundi
Cambodia
Cameroon
Canada
Cape Verde
Cayman Islands
Central African Republic
Chad
Chile
China
Christmas Island
Cocos (Keeling) Islands
Colombia
Comoros
Congo
Cook Islands
Costa Rica
Côte d'Ivoire
Croatia
Cuba
Cyprus
Czech Republic
Denmark
Djibouti
Dominica
Dominican
Ecuador
Egypt
El Salvador
Equatorial
Eritrea
Estonia
Ethiopia
Falkland
Faroe Islands
Fiji
Finland
France
French Polynesia
Gabon
Gambia
Georgia
Germany
Ghana
Gibraltar
Greece
Greenland
Grenada
Guam
Guatemala
Guernsey
Guinea
Guinea-Bissau
Guyana
Haiti
Honduras
Hong Kong
Hungary
Iceland
India
Indonesia
Iran
Iraq
Ireland
Isle of Man
Israel
Italy
Jamaica
Japan
Jersey
Jordan
Kazakhstan
Kenya
Kiribati
North Korea
South Korea
Kosovo
Kuwait
Kyrgyzstan
Laos
Latvia
Lebanon
Lesotho
Liberia
Libya
Liechtenstein
Lithuania
Luxembourg
Macau
Macedonia
Madagascar
Malawi
Malaysia
Maldives
Mali
Malta
Marshall Islands
Mauritania
Mauritius
Mayotte
Mexico
Micronesia
Moldova
Monaco
Mongolia
Montenegro
Montserrat
Morocco
Mozambique
Myanmar
Nagorno-Karabakh
Namibia
Nauru
Nepal
Netherlands
Netherlands Antilles
New Caledonia
New Zealand
Nicaragua
Niger
Nigeria
Niue
Norfolk Island
Northern Cyprus
Northern Mariana Islands
Norway
Oman
Pakistan
Palau
Palestinian
Panama
Papua New Guinea
Paraguay
Peru
Philippines
Pitcairn Islands
Poland
Portugal
Pridnestrovian Moldavian Republic
Puerto Rico
Qatar
Romania
Russia
Rwanda
Saint Helena
Saint Kitts and Nevis
Saint Lucia
Saint Pierre and Miquelon
Saint Vincent and the Grenadines
Samoa
San Marino
São Tomé and Príncipe
Saudi Arabia
Senegal
Serbia
Seychelles
Sierra Leone
Singapore
Slovakia
Slovenia
Solomon Islands
Somalia
Somaliland
South Africa
South Ossetia
Spain
Sri Lanka
Sudan
Suriname
Svalbard
Swaziland
Sweden
Switzerland
Syria
Tajikistan
Tanzania
Thailand
East Timor
Togo
Tokelau
Tonga
Trinidad and Tobago
Tristan da Cunha
Tunisia
Turkey
Turkmenistan
Turks and Caicos Islands
Tuvalu
Uganda
Ukraine
United Arab Emirates
United Kingdom
United States
Uruguay
Uzbekistan
Vanuatu
Vatican City
Venezuela
Vietnam
Virgin Islands (British)
Virgin Islands (United States)
Wallis and Futuna
Western Sahara
Yemen
Zambia
Zimbabwe


Wednesday, 16 August 2006 16:39:04 (GMT Daylight Time, UTC+01:00)  #    Comments [5]  .Net General | Asp.Net | General

# Friday, 11 August 2006
Crystal Reports: Export PDF irregularities between server and dev machine
I have a few reports in my web application that i export in PDF format only.  You would think this would remove any printer driver complications but apparently not.
Crystal Reports have a surprisingly good article to help troubleshoot issues with printing or exporting a report.

The problem i was finding was that in a Text object or Unbound field, the text would be truncated at the edge of the box boundary.  Some users have reported strange mid-word wrapping, but mine was a different problem, actually hacking off the end of each line.  This didn't happen on my dev machine, but it did happen on the server (2003).  When i compared them side by side, the Times New Roman font on the server was stretched by about 5% compared to my dev machine.
with the help of the CR article, i tracked it down to the lack of any printer installed on the server, i had also disabled the Print Spooler service which was preventing even the Microsoft Office Document Imaging Printer from being available.  So i started the print spooler service and made sure that the Office Imaging Printer was marked as the default printer.  Then i went back to Visual Studio and set all the reports to use the Office Imaging printer.  This solved the problem. 

You could of course install a PDF only printer such as PrimoPDF if you didn't have MS office installed on the server. As long as the server has the same printer installed as the one set in the report design, everything turns out fine.


Friday, 11 August 2006 13:20:57 (GMT Daylight Time, UTC+01:00)  #    Comments [1]  .Net General | .Net Windows Forms | Asp.Net

# Thursday, 10 August 2006
Crystal Reports: join / concatenate null strings
I was getting blank fields in my crystal report if one of the values in the formula was null.
Thanks to this post i know better than to trust Crystal's crummy concatenation rules.  Here is what Gundula Wangerin had to say:
A string concatenated with null always gives null.
You have to check if the first name is null with
     if not IsNull(firstname) then 
        lastname + " " + firstname
    else
        lastname


Thursday, 10 August 2006 17:31:20 (GMT Daylight Time, UTC+01:00)  #    Comments [3]  .Net General | .Net Windows Forms | Asp.Net

# Friday, 04 August 2006
Crystal Reports: Suppress doesn't work for Count(x) = 0
I picked up a tip on the Internet to display a "No records" message if the report contains no records, rather than leaving the user with an empty screen.  Simply Format the text field and enter a formula next to 'Suppress' with something like
Count({Table.Field}) > 0
This will suppress (hide) the message if there are records in the report.

I tried using a similar approach to hide a text field when there are no records, so it is essentially the same thing in reverse.  You would think a simple formula on the 'suppress' property this would achieve the desired effect:
Count({Table.Field}) = 0

But apparently the value returned by Count can be null if there are no records.  so you have to use:

IsNull(Count({Table.Field})) OR Count({Table.Field}) = 0

This is just another crystal reports annoyance, of which there are many.


Friday, 04 August 2006 11:26:38 (GMT Daylight Time, UTC+01:00)  #    Comments [9]  .Net General | Asp.Net | Windows Server

# Wednesday, 02 August 2006
Crystal Reports: not picking up correct date format
i'm part of the dd/MM/yyyy world, and that often means running into problems when using software developed with MM/dd/yyyy defaults, such as crystal reports. 
i have all the report options set to use the system defaults, which are regionally set to ireland in windows, but that isn't enough, the dates still come out in MM/dd/yyyy format.  i have the date + date/time fields customised within crystal reports to dd/MM/yyyy but that isn't enough either.  with lots of hours googling and no answers that worked, i resorted to searching the registry and found that some of the user accounts were still using US regional settings.  If you look in HKEY_USERS > you see a list of all the account IDs on the computer.  My guess is that crystal reports must use the SYSTEM account or another non-interactive account, and it takes the regional settings from there.  So if you search for sShortDate in the registry, you will find all the appropriate settings and can replace the MM/dd/yyyy values with your preferred format.
i had to reboot to get it to take effect. 


Wednesday, 02 August 2006 20:45:07 (GMT Daylight Time, UTC+01:00)  #    Comments [12]  .Net General | Asp.Net | Windows Server

# Thursday, 27 July 2006
Crystal Reports: problems changing database connection at run time
i ran into a problem with my Crystal Reports.  instead of using the usual Typed Dataset approach, where you use Report.SetDataSource(DataSet), i decided that was too much hastle and it seems faster and simpler just to design the query in the Crystal Reports 'wizard', and then supply parameters and set the record filter appropriately.  what i didn't know was that it was insanely complicated to direct the report to a different database path at runtime (such as most users would have in their deployment environment, whether for Access or SQL server). 

i was using Report.SetDataBaseLogon("admin", "") etc and all sorts of variations but none of them were working.  I kept getting Logon Failed exceptions.  Apparently you need to iterate through each table used in the report and set the source database for each one.  Not only that, but neither of the following lines of code has any effect:
Report.Database.Tables[i].SetDataSource("admin", "", DbPath, "")
Report.Database.Tables[i].LogOnInfo.ConnectionInfo.ServerName = DbPath
apparently you have to create a TableLogOnInfo object and use ApplyLogOnInfo() with that on each table. 
here is what does work:
TableLogOnInfo t = new TableLogOnInfo();
t.ConnectionInfo.ServerName = dbPath;            
for(int i=0; i<rpt.Database.Tables.Count; i++)
    rpt.Database.Tables[i].ApplyLogOnInfo(t);


Thursday, 27 July 2006 16:31:47 (GMT Daylight Time, UTC+01:00)  #    Comments [3]  Asp.Net

# Monday, 24 April 2006
HowTo: cancel an onbeforeunload event
if you're like me and use the onbeforeunload event in a web page to display a reminder to a user who might unintentially lose work by browsing away from the page, then you might be trying to find a way to cancel the event in the case where the user clicks the save button (they obviously don't need reminding now that they have clicked the button...)

on the internet you will find many people suggesting you use something like
window.onbeforeunload = null
but this does not work for me in IE6.  reading up in the MSDN docs for the event object i found a reference to the event.cancelBubble property, which i thought was the solution. but thanks to Orso who pointed out that setting "event.cancelBubble=true" is useless, the way to get rid of the confirm prompt is to exclude the return statement altogether, i chose to use a boolean variable as a flag to decide whether to return something or not. in the example below i add the javascript code programattically in the code behind:

	Page.ClientScript.RegisterStartupScript(typeof(String), "ConfirmClose", @"
<script>
window.onbeforeunload = confirmExit;
function confirmExit()
{
if(postback == false)
return ""Please don't leave this page without clicking the 'Save Changes' or 'Discard Changes' buttons."";
}
</script>");

then my save button contains the following aspx markup:
    OnClientClick="postback=true;return true;"
this sets the 'postback' variable to true, which gets picked up in the confirmExit() function, having the effect of cancelling the event. 

hope you find this useful.  it is tested and works in IE6 and FF 1.5.0.2.


Monday, 24 April 2006 17:10:13 (GMT Daylight Time, UTC+01:00)  #    Comments [16]  Asp.Net

# Wednesday, 12 April 2006
FindControl() won't find the control....
my aspx page uses a master page, so all the content is inside a ContentPlaceHolder. 
aparently Page.FindControl() only works within a single container, which is really poor functionality if you ask me. 
to complicate the situation further, my aspx page has dynamic controls inside a Panel. 
in order to access the controls of my user control, from the code-behind of the page, i first had to find the panel and then the user control, and then i could finally use that to find the controls i wanted. 
note: you don't have to use the client ID of the control ...ctrl00_blah_blah_blah_YourControlName... when you use the above method, you just use the name of the control, e.g. txtFirstName. 


Wednesday, 12 April 2006 15:27:51 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  Asp.Net

# Friday, 03 March 2006
Compiling Asp.Net 2.0 to a single assembly
I wanted to install my web application assembly into the GAC, but this is made more complicated by the multitude of assemblies produced by VS when i publish the web site.  On the newsgroups, i found some talk of a tool called Merge_Aspnet.exe but i couldn't find it anywhere.  Eventually i found it as a download on MSDN, it is bundled as part of the Web Deployment Projects.  You install it, and then right-click your project in VS and you should see a new menu item "Add Web Deployment Project".  I am baffled as to why they didn't just add a new project type in the list of projects under "Deployment".  There is a link to "Search online templates" so i think it should really be available there.  but it looks like MS did a hack just to add in a new item to the project context menu.  but it works... so i'll stop complaining. 


Friday, 03 March 2006 15:43:20 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net General | Asp.Net

# Saturday, 18 February 2006
HowTo: Get Write Access to an ApplicationData folder in ASP.NET
In a web application i'm working on, i needed to get write access to a folder, and preferably not the temp folder because the data should be kept reasonably safe from being deleted accidentally.  If it was a windows-forms application this would be easy, i would just use Application.UserAppDataPath.  So i thought for asp.net, i could use Environment.SpecialFolder.ApplicationData but this maps to the folder for the current user logged on to Windows, not the process running the web site, or the authenticated web site user.  So i just skip up 2 parent directories from that, and use the current Principal.WindowsIdentity to find the actual process running ASP.NET, and then deduce the correct folder, which by default will have write permissions for Asp.Net.

Here is the code:
string Username = Path.GetFileName(System.Security.Principal.WindowsIdentity.GetCurrent().Name).Replace(" ", "");
string UserAppFolder = String.Format(@"{0}\{1}\Application Data", Directory.GetParent(Directory.GetParent(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)).FullName).FullName, Username);

By the way, the use of Path.GetFileName is entirely intentional, it solves the case where the current user is "NT Authority\Network Service" by yielding "Network Service", which is what we are interested in.  

This will hopefully come in handy for my 'zero configuration' idea for the application.  Next time you need a write folder, it may be a good idea to use this instead of making users specify write permissions for a custom folder. 

Saturday, 18 February 2006 19:11:26 (GMT Standard Time, UTC+00:00)  #    Comments [2]  Asp.Net

# Thursday, 02 February 2006
Using Xml DOM with MetaBase.xml
For some reason i can't get SelectNodes to work with the IIS 6 MetaBase.xml file.  It just doesn't return any matches.  I think the structure may be non-standard or too complex, or else i'm not using it right.  but in any case, I worked around the problem by opening the file with File.OpenText and using Regex to parse out the nodes i'm interested in, namely the IISWebServer nodes.

string MetaBase = "";
using(StreamReader sr = File.OpenText(ConfigurationManager.AppSettings["MetaBasePath"].ToString()))
MetaBase = sr.ReadToEnd();

foreach(Match m in new Regex(@"(?x:)<IIsWebServer[^>]*>.*?</IIsWebServer>", RegexOptions.IgnoreCase).Matches(MetaBase))
{
XmlDocument doc = new XmlDocument();
string IisCode = "", ServerBindings = "", ServerComment = "";
doc.LoadXml(m.Groups[0].Value);
XmlElement node = doc.DocumentElement;
// now you can access the node attributes
...


Thursday, 02 February 2006 13:38:34 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net General | .Net Windows Forms | Asp.Net | Windows Server

CheckBoxList: Select All / None client-side script
<script language="javascript" type="text/javascript">
function Select(Select)
{
for (var n=0; n < document.forms[0].length; n++)
if (document.forms[0].elements[n].type=='checkbox')
document.forms[0].elements[n].checked=Select;
return false;
}
</script>


Select <a href="#" onclick="javascript:Select(true)">All</a> | <a href="#" onclick="javascript:Select(false)">None</a>


Thursday, 02 February 2006 13:25:33 (GMT Standard Time, UTC+00:00)  #    Comments [7]  Asp.Net

# Monday, 30 January 2006
Fix: Unable to validate data, MachineKey.GetDecodedData
i kept getting these sporadic error messages on my web applications and i could never figure out why. 
Unable to validate data at
System.Web.Configuration.MachineKey.GetDecodedData(Byte[] buf, Byte[] modifier, Int32 start, Int32 length, Int32& dataLength) at
System.Web.UI.LosFormatter.Deserialize(String input)
searching the net just reveals a troop of similarly frustrated users without solutions.  but today i found the actual reason why, and the work-around.
this excellent post on experts-exchange has the answer.  apparently it happens when users leave a page open for a long time, and then cause a post back.  something to do with the machine key being automatically generated and it changes before the user causes the postback, and it can't validate it then. 
the fix is to set a static machine key. 
There is a kb article with some code to generate a key for you, + instructions.  

Sorted.


Monday, 30 January 2006 22:53:44 (GMT Standard Time, UTC+00:00)  #    Comments [13]  .Net General | Asp.Net

ADOX + Excel: bogus worksheets
a web site i'm working on imports excel documents, and does some processing on them for importing into a database.  I use the code from this post to do the importing, and it works nicely.  I recently came across a problem where i was encountering duplicate records, and it took me ages to figure out why.  Apparently a 'named range' of cells in a worksheet is treated as a Table by ADOX.  so you get more than you bargain for when you iterate through the tables in the resulting DataSet. 
I was able to work around the problem by discarding any tables that do NOT end in the dollar $ character.


Monday, 30 January 2006 15:30:12 (GMT Standard Time, UTC+00:00)  #    Comments [1]  .Net General | Asp.Net | Database

# Wednesday, 25 January 2006
Crystal Reports TextObject ignores newline, carriage return, \r\n
I have a text object in my crystal report, and i set the value programatically for it, using something like this:
(rpt.Section3.ReportObjects["txtDate"] as TextObject).Text = DateTime.Now.ToShortDateString();
this works fine, until you put a string with line breaks inside it, specifically \r\n or the carriage return character.  This is a bug in CR for .Net, you can read the official blurb here.  the work-around code they post is in VB, and it made no sense to me when i read it.
what they actually do is make you change the TextObject into a FormulaField (you have to view the 'Field Explorer' tab next to the toolbox, and drag on a Formula Field).  Then you set 'CanGrow' to true, and then you go back to your code, and do the most arcane work-around i've ever seen.  you set the formula to your text string, but you must surround it in single quote characters, and replace \r\n with some inline managed code as follows: ' + chr(10) + '
when i first read this, i thought they had made a syntax error, but this is the way to do it, and it works.  the new format for setting the formula field is:
rpt.DataDefinition.FormulaFields[0].Text = "'" + YourString.Replace("\r\n", "' + chr(10) + '") + "'";


Wednesday, 25 January 2006 13:52:05 (GMT Standard Time, UTC+00:00)  #    Comments [16]  .Net General | .Net Windows Forms | Asp.Net

# Friday, 13 January 2006
Localization: Setting text direction for a web page
This is the first time i have been asked to add localization support for a content management system.  One of the supported languages is Arabic, which is usually written right-to-left.  to achieve this, you could obviously change your CSS or markup with something like <p align="right"> but this is not the way to do it.

there is an attribute in the HTML tag that controls the direction of text (and controls) on the page, called 'dir', which can take values 'ltr' or 'rtl', corresponding to "left-to-right" and "right-to-left" respectively.  The advantage of setting this property is that it instructs the browser to render all direction-related elements of the page accordingly.  for example, in RTL mode, when you type in a text box, the cursor stays flush to the right and the characters spread across to the left as you type.  similarly drop-down-menu's are rendered differently, as are bullet points, and the browser scrollbar.  these changes are shown in the screenshot below:



In this content management system, i set the direction based on the user's language setting, stored in a cookie.  to set the DIR attribute programatically, i suggest the following HTML tag in your Master/Template aspx page:
<html xmlns="http://www.w3.org/1999/xhtml" runat="server" id="htmlTag">
Then you can access it in the code behind, like so:
protected void Page_Load(object sender, EventArgs e)
{
this.htmlTag.Attributes.Add("dir", Util.HtmlDir);
...
and then the method to determine the direction, based on the cookie. note that 'Language' in my code is an enumeration.
/// <summary>
/// Returns rtl or ltr, depending on the current language setting
/// </summary>
public static string HtmlDir
{
get
{
Language lang;
try
{
lang = (Language)Enum.Parse(typeof(Language), HttpContext.Current.Request.Cookies["Language"].Value.ToString());
}
catch
{
// cookie not present, default to english.
lang = Language.English;
}
switch(lang)
{
case Language.Arabic:
return "RTL"; // right to left
default:
return "LTR"; // left to right
}
}
}

public enum Language
{
English,
Arabic,
...
}

for more information on languages and their directions, and related browser issues, see this excellent article from W3C


Friday, 13 January 2006 16:28:16 (GMT Standard Time, UTC+00:00)  #    Comments [2]  Asp.Net

# Thursday, 12 January 2006
Fix: GridView DataFormatString not applied
for some bizarre reason, you have to set HtmlEncode=false on a bound column in a gridview, to get the DataFormatString to work.
i hope this helps somebody else staring at their gridview in disbelief as to why it doesn't work by default!


Thursday, 12 January 2006 16:01:38 (GMT Standard Time, UTC+00:00)  #    Comments [64]  Asp.Net

# Monday, 09 January 2006
Xml Serialization: Properties left out without 'set' accessor
I ran into a frustrating problem today where some properties of my objects weren't getting serialized.  I noticed it was the ones without 'set' accessors.  I didn't want to put set accessors in because the properties should be read-only, but apparently the xml serializer needs the set accessor to de-serialize. 
there is a good discussion on it on this thread.
Luckily we can use the ReadOnlyAttribute setting on the property to prevent it being modified by a programmer at design time.
or you can use the Soap serializer instead of xml, whatever that is.


Monday, 09 January 2006 17:57:34 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net General | .Net Windows Forms | Asp.Net

# Thursday, 29 December 2005
Sending files in chunks with MTOM Web Services and .NET 2.0
just posting my code-project article on http chunking web services with MTOM here for reference.  by the way it has been updated on codeproject...

Screenshot of windows forms client, uploading a file

Introduction

In trying to keep up to speed with .NET 2.0, I decided to do a .NET 2.0 version of my CodeProject article "DIME Buffered Upload" which used the DIME standard to transfer binary data over web services. The DIME approach was reasonably efficient but the code is quite complex and I was keen to explore what .NET 2.0 has to offer. In this article, I use version 3.0 of the WSE (Web Service Enhancements) which is available for .NET 2.0 as an add-in, to provide a simpler and faster method of sending binary data in small chunks over HTTP web services.

Background

Just a recap on why you may need to send data in small chunks at all: if you have a large file and you want to send it across a web service, you must understand the way it all fits together between IIS, .NET, and the web service call. You send your file as an array of bytes, as a parameter to a web service call, which is all sent to the IIS web server as a single request. This is bad if the size of the file is beyond the configured MaxRequestLength of your application, or if the request causes an IIS timeout. It is also bad from the point of view of providing feedback of the file transfer to the user interface because you have no indication how the transfer is going, until it is either completed or failed. The solution outlined here is to send chunks of the file one by one, and append them to the file on the server.

There is an MD5 file hash done on the client and the server to verify that the file received is identical to the file sent.

Also, there is an upload and download code included in this article.

Adventures with MTOM

MTOM stands for SOAP "Message Transmission Optimization Mechanism" and it is a W3C standard. To use it (and to run this application), you must download and install WSE 3.0, which includes MTOM support for the first time. If you look in the app.config and web.config files in the source code, you will see sections referring to the WSE 3 assembly, and a messaging clientMode or serverMode setting. These are necessary to run MTOM in the application.

The problem with DIME was that the binary content of the message was sent outside the SoapEnvelope of the XML message. This meant that although your message was secure, the Dime Attachment may not be secure. MTOM fully complies with the other WS-* specifications (like WS-Security) so the entire message is secure.

It took me a while to realise that when MTOM is turned on for the client and the server, WSE automatically handles the binary encoding of the data in the web service message. With DIME and WSE 2.0, you had to configure your app for DIME and then use DimeAttachments in your code. This is no longer necessary, you just send your byte[] as a parameter or return value, and WSE makes sure that it is sent as binary, and not padded by XML serialization as it would be in the absence of DIME or MTOM.

How it works

The web service has two main methods, AppendChunk is for uploading a file to the server, DownloadChunk is for downloading from the server. These methods receive parameters for the file name, the offset of the chunk, and the size of the buffer being sent/received.

The Windows Forms client application can upload a file by sending all the chunks one after the other using AppendChunk, until the file has been completely sent. It can do an MD5 hash on the local file, and compare it with the hash on the file on the server, to make sure the contents of the files are identical. The download code is very similar, the main difference is that the client must know from the server how big the file is, so that it can know when to stop requesting chunks.

A simplified version of the upload code is shown below (from the WinForms client). Have a look in the code for Form1.cs to see the inline comments + the explanation of the code. Essentially, a file stream is opened on the client for the duration of the transfer. Then the first chunk is read into the Buffer byte array. The while loop keeps running until the FileStream.Read() method returns 0, i.e. the end of the file has been reached. For each iteration, the buffer is sent directly to the web service as a byte[]. The 'SentBytes' variable is used to report progress to the form.

using(FileStream fs = new FileStream(LocalFilePath, FileMode.Open, FileAccess.Read))
{
int BytesRead = fs.Read(Buffer, 0, ChunkSize);
while(BytesRead > 0 && !worker.CancellationPending)
{
ws.AppendChunk(FileName, Buffer, SentBytes, BytesRead);
SentBytes += BytesRead;
BytesRead = fs.Read(Buffer, 0, ChunkSize);
}
}

Example of the BackgroundWorker class in .NET 2.0

.NET 2.0 has a great new class called 'BackgroundWorker' to simplify running tasks asynchronously. Although this application sends the file in small chunks, even these small chunks would delay the WinForms application and make it look crashed during the transfer. So the web service calls still need to be done asynchronously. The BackgroundWorker class works using an event model, where you have code sections to run for DoWork (when you start), ProgressChanged (to update your progress bar / status bar), and Completed (or failed). You can pass parameters to the DoWork method, which you could not do with the Thread class in .NET 1.1 (I know you could with delegates, but delegates aren't great for thread control). You can also access the return value of DoWork in the Completed event handler. So for once, MS has thought of everything and made a very clean threading model. Exceptions are handled internally and you can access them in the Completed method via the RunWorkerCompletedEventArgs.Error property.

The code shown below is an example of the ProgressChanged event handler:

private void workerUpload_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// update the progress bar and status bar text
this.toolStripProgressBar1.Value = e.ProgressPercentage;
this.statusText.Text = e.UserState.ToString();
// summary text is sent in the UserState parameter
}

I have used four BackgroundWorker objects in the application:

  • one to manage the upload process,
  • one to manage the download process,
  • another to calculate the local MD5 file hash in parallel while waiting for the server result,
  • and another to download the list of files in the Upload server folder to allow the user to select a file to download.

The reason I use a BackgroundWorker object for each task is because the code for each task is tied in to the events for that object.

A good example of Thread.Join()

When the upload or download is complete, the client asks for an MD5 hash of the file on the server, so it can compare it with the local file to make sure they are identical. I originally did these in sequence. But it can take a few seconds to calculate the result for a large file (anything over a few hundred MB), so the application was waiting five seconds for the server to calculate the hash, and then five more seconds for the client to calculate its own hash. This made no sense, so I decided to implement a multi-threaded approach to allow them to run in parallel. While the client is waiting on the server, it should be calculating its own file hash. This is done with the Thread class, and the use of the Join() method which blocks execution until the thread is complete.

The code below shows how this is accomplished:

// start calculating the local hash (stored in class variable)
this.hashThread = new Thread(new ThreadStart(this.CheckFileHash));
this.hashThread.Start();

// request the server hash
string ServerFileHash = ws.CheckFileHash(FileName);

// wait for the local hash to complete
this.hashThread.Join();

if(this.LocalFileHash == ServerFileHash)
e.Result = "Hashes match exactly";
else
e.Result = "Hashes do not match";

There is a good chance that the two operations will finish at approximately the same time, so very little waiting around will actually happen.

Performance compared with DIME

I found that MTOM was about 10% faster than DIME in my limited testing. This is probably to do with the need to package up each chunk into a DIME attachment, which is no longer necessary with MTOM. I was able to upload files of several gigabytes in size without problems.

Obviously, there is an overhead with all this business of reading file chunks and appending them, so the larger the chunk size, the more efficient your application will be. It should be customised based on the network and the expected size of files. For very small files, it is no harm to use small chunk sizes (e.g., 32 Kb) because this will give accurate and regular feedback to the user interface. For very large files on a fast network, consider using 4000 Kb to make good use of the bandwidth and reduce the File Input/Output overhead. If you want to send chunks larger than 4 MB, you must increase the .NET 2.0 Max Request Size limit in your web.config.

Conclusions

Feel free to use this code and modify it as you please. Please post a comment for any bugs, suggestions, or improvements. Enjoy!


Thursday, 29 December 2005 20:34:15 (GMT Standard Time, UTC+00:00)  #    Comments [24]  .Net General | .Net Windows Forms | Asp.Net

# Thursday, 22 December 2005
MTOM error: The request failed with HTTP status 415: Unsupported media type
I'm new to MTOM, and i encountered this error while developing a WSE 3.0 application with .net.
The request failed with HTTP status 415: Unsupported media type

Explanation 1 - Basic Configuration Error

After a lot of digging around and comparing my code with the MTOM sample bundled with WSE3, i found out it was because i was missing a setting called "soapServerProtocolFactory" in my web.config, shown below.  I didn't think i needed to have a Protocol Factory (whatever that is!), but apparently I do. It works fine now that i added that section.

This is what my web.config looks like:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="microsoft.web.services3" type="Microsoft.Web.Services3.Configuration.WebServicesConfiguration, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</configSections>
<appSettings>
<add key="UploadPath" value="Upload" />
</appSettings>
<system.web>
<webServices>
<soapServerProtocolFactory type="Microsoft.Web.Services3.WseProtocolFactory, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<soapExtensionImporterTypes>
<add type="Microsoft.Web.Services3.Description.WseExtensionImporter, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</soapExtensionImporterTypes>
</webServices>
<compilation>
<assemblies>
<add assembly="Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</assemblies>
</compilation>
</system.web>
<microsoft.web.services3>
<messaging>
<mtom serverMode="optional" />
</messaging>
</microsoft.web.services3>
</configuration>
My app.config for the Windows Client looks like this:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="microsoft.web.services3" type="Microsoft.Web.Services3.Configuration.WebServicesConfiguration, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<section name="UploadWinClient.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</sectionGroup>
</configSections>
<microsoft.web.services3>
<messaging>
<mtom clientMode="On" />
</messaging>
</microsoft.web.services3>
<applicationSettings>
<UploadWinClient.Properties.Settings>
<setting name="UploadWinClient_MTOM_WebService_MTOM" serializeAs="String">
<value>http://localhost:1423/UploadWebClient/MTOM.asmx</value>
</setting>
</UploadWinClient.Properties.Settings>
</applicationSettings>
</configuration>

Explanation 2 - Strange Behaviour with Web Service instances...

if you're configuration is already correct, then the following may be of interest.  i came across this error in a winforms application, that has a class library with an MTOM web service in it.  my code had something like this:
myClassLibrary.WebService = this.WebService;
i was doing this because i had an instance of the web service in the win form, authenticated with a cookie in its CookieContainer property, and i wanted to replace the web service that the class library was using with the one from my win-form.  however this caused the 415 error to appear sporadically all over the place.  the fix was to just replace the CookieContainer instead of the entire web service instance.  like so:
myClassLibrary.WebService.CookieContainer = this.WebService.CookieContainer;

now i get no errors.  weird huh?

Thursday, 22 December 2005 16:13:34 (GMT Standard Time, UTC+00:00)  #    Comments [7]  Asp.Net

# Wednesday, 21 December 2005
GridView Databinding error "Field or property ... not found on the selected data source"

A field or property with the name 'xyz' was not found on the selected data source

I get this error when i bind a GridView to an array of custom objects with public variables. 
The recommended pattern for designing classes is to have private variables with public properties with get/set etc, but for my current application i'm not bothering with that. Apparently because of the Reflection methods used in GridView databinding, you have to have properties to bind fields in the GridView to your object fields. 

bit of a pain.


Wednesday, 21 December 2005 15:51:28 (GMT Standard Time, UTC+00:00)  #    Comments [3]  Asp.Net

# Tuesday, 20 December 2005
GridView - accessing row information inside RowCommand
in .net 1.1, the GridCommandEventArgs (or whatever) used to give access to the row that triggered the command. 
in .net 2.0, it isn't as obvious as that.  Fritz posted an excellent discussion on the matter, and i'm just posting his 2-line solution here for reference.

If your button is a ButtonField, then you can access the index of the item via e.CommandArgument.  This may be enough information to do whatever you need.

If your button is a LinkButton in a TemplateField, you have to add the CommandArgument as an attribute to the LinkButton:
CommandArgument='<%# Eval("id") %>'
Then in the code behind for RowCommand, you can use the following syntax:
string id = (string)e.CommandArgument;
thanks Fritz!

Tuesday, 20 December 2005 17:52:22 (GMT Standard Time, UTC+00:00)  #    Comments [0]  Asp.Net

# Friday, 16 December 2005
GridView with ObjectDataSource, Delete parameters not working...

i'm just experimenting with the gridview control, and i got stuck trying to delete an object with the auto-included delete button.  the delete method would get invoked properly, but the ID parameter was always zero, which means the int parameter was uninitialised.

my problem was that i didn't have the DataKeyNames property set in the Gridview. I just set it to 'ID' and it worked fine then.


Friday, 16 December 2005 14:21:53 (GMT Standard Time, UTC+00:00)  #    Comments [18]  Asp.Net

# Thursday, 15 December 2005
Usability problems with 'directory listing' in .Net 2.0

Microsoft love small fixed fonts for some reason

Correction: this only applies to the Asp.Net development server (v8), so it's not all that important...

i noticed if you browse a folder on a .net 2 virtual directory, you get the directory listing.  but the font styles are set to 8 point verdana which is uncomfortably small for some users on some resolutions.  the fact that they hard-coded it in the css styles means it ignores the browser font-size setting, so it breaks a very important accessibility guideline.  

this is just another reason to use firefox which lets you increase font sizes for fixed sizes. surely MS would be trying to hang on to their user population who still uses IE.  
i looked in the source of the directory listing, and i see they have the file and folder info formatted inside <pre> tags, keeping the tabs aligned nicely.  but i took a copy of the source and removed the font sizes, and the alignment is still fine when you change the browser font size!

come on MS... why the love affair with "px" and "pt" for font sizes?  
i remember gotdotnet.com had fixed sized fonts also.  it's a silly decision in my book, let your users control the font size.  

i've just been experimenting with VS2005 and i like the improved support for xhtml and accessibility, but lots of people actually use directory listings, so why not make them accessible also?


Thursday, 15 December 2005 17:18:34 (GMT Standard Time, UTC+00:00)  #    Comments [0]  Asp.Net

Serializing an object in Xml, with .Net 2.0
i was experimenting with how .Net 2.0 does xml serialization of objects, and i got it serializing nicely with the following code. 
XmlSerializer xs = new XmlSerializer(typeof(PageCollection));
xs.Serialize(fs, pages); // 'fs' is a FileStream to my xml file, and 'pages' is a collection class of objects

the problem was when i tried to deserialize it, like so:
XmlSerializer xs = new XmlSerializer(typeof(PageCollection));
pages = xs.Deserialize(fs) as PageCollection;

i got this error:

xmlns=''> was not expected

i found this post on google which described the same errors, and a workaround (by adding an empty namespace) but it didn't work for me.  perhaps it is a difference in the serialization process with with .Net 2.0.  what fixed it for me was setting an XmlRootAttribute for the class i was serializing. like so:
[XmlRootAttribute("CmsPages", Namespace = "http://www.whatever.com/Cms", IsNullable = false)]
hope this helps someone else out there with the same problem.
Thursday, 15 December 2005 13:28:01 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net General | Asp.Net | Database

# Friday, 02 December 2005
ADO.NET timeouts when filling a DataAdapter in the middle of a transaction

I had a transaction comprised of about 5 commands, and in the middle of it, i needed to do a SELECT on a table to know what values to insert for one of the commands.  I encountered a timeout just after i called Fill on the DataAdapter.  it took me a while to figure out that it was because the transaction had already taken out a lock on the table i was selecting from.  makes sense now, but i spent ages on it!  just posting it here in case anyone else runs into the same problem.


Friday, 02 December 2005 12:26:22 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net General | .Net Windows Forms | Asp.Net | Database

# Friday, 11 November 2005
Asp.Net generally useful datagrid code

i'm just posting this here for reference.  it is a datagrid that supports standard paging and sorting, and displays the current set of record indices e.g. "1-15 of 1000 records"

private void bindGrid()
{
    DataSet ds = new DB.Audits().SelectAllPendingAudits();

    // the sorting is always retrieved from viewstate, if it exists or not.
    string sort = String.Concat(ViewState["Sort"], "");
    if(sort != "")
        this.lblSort.Text = "Sorted by " + sort + " in ascending order";

    int numRows = ds.Tables[0].Rows.Count;
    if(numRows > 0)
    {
        int start = this.DataGrid1.CurrentPageIndex * this.DataGrid1.PageSize + 1;
        int end = Math.Min(numRows, start + this.DataGrid1.PageSize - 1);
        this.lblHeading.Text = String.Format("Displaying {0}-{1} of {2} records", start, end, numRows);
        this.DataGrid1.AllowPaging = (numRows > this.DataGrid1.PageSize);    // don't show pager unless relevant
        this.DataGrid1.Visible = true;
        DataView dv = new DataView(ds.Tables[0]);
        dv.Sort = sort;
        this.DataGrid1.DataSource = dv;
        this.DataGrid1.DataBind();
    }
    else
    {
        this.lblHeading.Text = "No records";            
        this.DataGrid1.Visible = false;
    }
}

private void DataGrid1_ItemCommand(object source, System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
    if(e.CommandName == "Sort")
    {
        ViewState["Sort"] = e.CommandArgument.ToString();    // is picked up in bindGrid() function
        this.DataGrid1.CurrentPageIndex = 0;
        this.bindGrid();                
    }            
}

private void DataGrid1_PageIndexChanged(object source, System.Web.UI.WebControls.DataGridPageChangedEventArgs e)
{
    this.DataGrid1.CurrentPageIndex = e.NewPageIndex;
    bindGrid();
}

Friday, 11 November 2005 11:58:32 (GMT Standard Time, UTC+00:00)  #    Comments [0]  Asp.Net

# Thursday, 03 November 2005
HowTo: present a radiobuttonlist with images

i looked on the newsgroups to see if anyone had posted anything about this, and i found a few dead-end posts which seemed to conclude that it couldn't be done. 
i used a very simple approach that works well, and am posting it here for anyone looking to see how to do it.   the requirements are to present a radio-button-list with images instead of just text.

string imageBankFolder = "/ImageBankFolder/Thumbnails/";
DataSet ds = new DB.ImageBank().Select(); // get your dataset from wherever
foreach(DataRow dr in ds.Tables[0].Rows)
   this.RadioButtonList1.Items.Add(new ListItem(String.Format("<img src='{0}'>", imageBankFolder + dr["ImageFile"].ToString()), dr["ImageID"].ToString()));

this displays the images only.  note: firefox works fine with this, you can click on the image to select it, but IE6 requires you to actually click on the round radio button icon.  to work around this, i included some text above the image, which sits beside the button, and it is more intuitive for the user to click the text or the radio icon then.  to include some text above the image, try the following:

this.RadioButtonList1.Items.Add(new ListItem(String.Format("{1}<BR><img src='{0}'>", imageBankFolder + dr["ImageFile"].ToString(), dr["Text"].ToString()), dr["ImageID"].ToString()));

hope this helps someone out there.


Thursday, 03 November 2005 18:52:23 (GMT Standard Time, UTC+00:00)  #    Comments [14]  Asp.Net

# Saturday, 10 September 2005
Mapping .html pages to Asp.Net

I was doing an upgrade on a web site recently, and all the pages were .html pages.  I wanted to add some .Net functionality, but didn't want to change all the urls, for bookmarks, search engines etc.  As well as scaring off the client with the strange ".aspx" file extensions.  yes- many irish companies are still technophobic. 

Add an IIS mapping for .html

i remember how to change mappings for a file extension in IIS (web site properties > home directory > configuration), so i did this for .html pages by adding a mapping for .html to aspnet_isapi.dll (copy the full path from the mapping for .aspx). 

Add a HttpHandler to the application web.config file

when i did the above, my .net code was ignored and rendered as plain text.  i found out this was because the web application (at the .net level) wasn't configured to handle .html files as .aspx files. this is what i added to my web.config to get it working:

<configuration> <system.web> <httpHandlers> <add verb="*" path="*.html" type="System.Web.UI.PageHandlerFactory" /> </httpHandlers> </system.web> </configuration>

now the whole application works with full .net functionality, overcoming all those migration problems usually associated with .net upgrades.


Saturday, 10 September 2005 14:38:39 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  Asp.Net

# Friday, 09 September 2005
An asp.net button that disables itself automatically after clicking.

Some users of a web application i wrote insist on clicking buttons more than once, probably out of impatience. this often causes duplicate key exceptions with the database, because the first time they clicked the button the record was created, and the second time they clicked it, an exception is thrown, so they get the error screen and don't know what they did wrong. 

i wanted to write a button control that would disable itself automatically and re-enable itself once it was finished.  i couldn't find any good samples out there.  javascript is obviously the answer, and the solution i came up with is quite simple.  here's the code: currently only works with .Net 1.1:

	/// <summary>
/// A button control that disables itself when clicked, and changes the text to "Please wait..."
/// This is to prevent duplicate clicks by impatient or novice users.
/// It requires the button to be placed in a server form.
/// </summary>
[DefaultProperty("Text"), ToolboxData("<{0}:SmartButton runat=server></{0}:SmartButton>")]
public class SmartButton : Button
{

/// <summary>
/// Add an 'onClick' attribute to disable the button when it is clicked, and submit the form,
/// invoking the postback.
///
/// The onClick code handles the case where __EVENTTARGET is registered on the page, in which case
/// this variable is set to the button ID, and the form is submitted.
/// The other case is where __EVENTTARGET does not exist on the page, i found this sometimes
/// occurred on pages with only one button. In this case, the form is simply submitted, and the
/// button_click event will be raised by virtue of the default submit button in the form.
/// </summary>
protected override void Render(HtmlTextWriter output)
{
string onClick = "if(this.form != null && this.form.__EVENTTARGET != null){ this.form.__EVENTTARGET.value='" + this.UniqueID + "'; this.disabled = true; this.value = 'Please wait...'; this.form.submit(); } else this.form.submit(); ";
if(this.Attributes["onclick"] != null) // prepend the existing onClick attributes
onClick = this.Attributes["onclick"].ToString() + onClick;
this.Attributes.Add("onclick", onClick);
base.Render(output);
}

protected override void OnClick(EventArgs e)
{
// do the OnClick code first
base.OnClick (e);

// then reset the enabled + text values to their original state
int insertAt = Math.Max(this.Page.Controls.Count-1, 0); // never insert at -1 if there are no controls on the page
this.Page.Controls.AddAt(insertAt, new LiteralControl(String.Format(@"
<script>
if(document.getElementById('{0}') != null)
{{
document.getElementById('{0}').disabled = false;
document.getElementById('{0}').value = '{1}';
}}
</script>
", this.UniqueID, this.Text)));
}
}

Friday, 09 September 2005 15:41:29 (GMT Daylight Time, UTC+01:00)  #    Comments [2]  Asp.Net

# Monday, 01 August 2005
Scrollable DIV css code, useful for big checkboxlists etc
DIV.scroll
{
	height: 9em;
	overflow: auto;
	border: 1px solid #666;
	background-color: #e8e8e8;
} 

I ran into a problem designing a web app that had a few checkboxlists, and when the real data got imported, the checkboxlists took up most of a page with all the entries!  this is obviously no good, but the app still needed a checkboxlist rather than a single-select dropdownlist, so i found this css code that you can apply to a DIV wrapped around the checkboxlist.  you can set a width if you like but i just let it fill whatever container the div is already in. 

works in firefox and IE6 in windows, didn't try on anything else. 


Monday, 01 August 2005 14:59:54 (GMT Daylight Time, UTC+01:00)  #    Comments [1]  Asp.Net

# Thursday, 30 June 2005
Crystal Reports for .Net, locked file when exporting to PDF with a crHtmlText field

i made a simple change to a crystal report in the VS designer, by changing the text format for a text box to crHtmlText instead of crStandardText.  little did i realise this would break the report altogether and cause it to fail when i try to export as a PDF.  i had forgotten i did this change at all and thought it was a permissions issue, but not at all, crystal reports is just crap.

my advice is don't use crHtmlText


Thursday, 30 June 2005 15:50:42 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  Asp.Net

# Tuesday, 24 May 2005
A few useful functions for importing and exporting data in asp.net. Excel, PDF, Datasets, DataGrids, etc.

the last few web projects i've been working on have had a lot of import/export requirements, and i've put together a class library that contains the functionality outlined below.  A lot of it i have collected from newsgroups and modified to my own purposes.

Import an Excel File

Imports all the worksheets in an excel file into a .Net dataset, with one datatable for each work sheet. Uses the ADOX COM component (reference Microsoft.ActiveX extensions 2.8 in VS, and adodb) and OleDb.  The reason i chose to use ADOX was because you need to know the worksheet names in the excel file if you're to query them with OleDb, and this isn't possible in my case.  So i use ADOX to iterate through the table names and create a new ADO.Net DataTable for each one.
I found some problems with excel documents that appeared to contain empty columns past the used range of cells, but OleDb complained about "Too many fields defined", which i presume is because it interprets all the excel columns that go on from A to XYZ or whatever, as proper columns, when we're only interested in using the used range of cells.  To overcome this, I opened the excel file and copied the range of cells into a new worksheet and deleted the old one, and it worked fine. 

Sample usage (with a Html File Control called 'fileToUpload' on the page):  

This sample code below uses the ImportExcel method in the class library code, to have the user browse to the excel file, and then upload it to the server and import it into a DataSet.

string fileName = ((System.Web.UI.HtmlControls.HtmlInputFile)this.fileToUpload).PostedFile.FileName;
if(fileName != "")
{
   try
   {
      // upload the excel file to a temp directory (needs write permissions)
      string uploadPath = Server.MapPath("/Temp/" + fileName.Remove(0,fileName.LastIndexOf("\\") + 1)));
      ((System.Web.UI.HtmlControls.HtmlInputFile)this.fileToUpload).PostedFile.SaveAs(uploadPath);

      // load the excel file into a dataset for processing
      DataSet ds = ImportExcel.ImportExcel(uploadPath);

Export Crystal Report To PDF

Converts a crystal report object into a PDF document that opens in Adobe Acrobat Reader.

Export a DataTable or DataView to Excel

This export method is similar to the common technique of binding a dataset to a datagrid/gridview and rendering the contents to produce a HTML table that Excel can understand. However the datagrid approach is not reliable if the data contains html characters, e.g. < or >, it produces invalid XML, which causes problems in Excel and OpenOffice. An alternative approach is to derive a GridView control that automatically sets HtmlEncode = true on all the BoundColumns, but this can produce very bloated output where non ASCII characters are represented and Excel will not decode the HtmlEncoded text.  I found the simplest approach is to parse the dataview and write out an XHTML table. This way the output is guaranteed to be valid XHTML, and compatible with Excel and OpenOffice (use the HtmlDocument filter when opening the file). 

In case you are worried about the performance of traversing the data like this, don't be, because it is sure to be less code than what happens inside the DataGrid class :)

Export a DataTable or DataView to CSV

this is similar to the above approach of parsing through the data and outputing the delimiters appropriately. there are regular expression based approaches, which i have tried before, but i found them unreliable when dealing with a complex character set, especially when trying to output in a format that both OpenOffice and Excel will be able to open.  This way i know i can trust, and it is lightning fast as well.

Comments?

if you have any questions on how to use it, or if you find bugs, or even better if you have some improvements... post a comment below. 
Enjoy.  Tim.

The Code

using System;
using System.Configuration;
using System.Data;
using System.Data.OleDb;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Caching;
using System.Web.Security;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace Tim.Library.WebForms
{
    /// <summary>
    /// Provides functionality to import and export datasets, datagrids, excel files etc.
    /// </summary>
    public class ImportExport
    {
        public enum ExportFormat{Excel, CSV};

        /// <summary>
        /// Imports all the worksheets in an excel file into a dataset,
        /// with one datatable for each sheet.
        /// </summary>
        public static DataSet ImportExcel(string path)
        {
            ADOX.CatalogClass cat = new ADOX.CatalogClass();
            // create an ADODB connection to use with the catalog
            ADODB.ConnectionClass connAdox = new ADODB.ConnectionClass();
            string connectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=\"" + path + "\";Extended Properties=Excel 8.0;";
            // open the excel ADOX connection to get the table names
            connAdox.Open(connectionString, "admin","",0);
            cat.ActiveConnection = connAdox;
            DataSet ds = new DataSet();
            // create an OleDb connection to get data into ADO.Net
            OleDbConnection connOleDb = new OleDbConnection (connectionString);
            connOleDb.Open();
            foreach(ADOX.Table t in cat.Tables)
            {
                try
                {
                    string name = t.Name.Trim('_');
                    if(ds.Tables.Contains(name))
                        continue;    // avoid duplicate worksheet names... strange behaviour where multiple tables were added when only one sheet existed
                    OleDbCommand cmdSelect = new OleDbCommand (@"SELECT * FROM [" + name + "]", connOleDb);
                    OleDbDataAdapter dba = new OleDbDataAdapter();
                    dba.SelectCommand = cmdSelect;
                    DataTable dt = new DataTable(name);
                    dba.Fill(dt);
                    ds.Tables.Add(dt);
                }
                catch(Exception ex)
                {    
                    throw ex;
                }
            }
            connOleDb.Close();                
            connAdox.Close();
            return ds;
        }

        /// <summary>
        /// Opens a PDF window containing the specified crystal report object
        /// The Crystal DLLs must be deployed with the web app for this to work.
        /// </summary>
        /// <param name="rpt">The report object</param>
        /// <param name="filename">Should include include .pdf</param>
        public static void ExportCrystalReportToPDF(CrystalDecisions.CrystalReports.Engine.ReportClass rpt, string filename)
        {
            MemoryStream stream = (MemoryStream)rpt.ExportToStream(CrystalDecisions.Shared.ExportFormatType.PortableDocFormat);
            byte[] bytes = new Byte[stream.Length];
            stream.Read(bytes, 0, (int)stream.Length);
            stream.Close();
            HttpResponse response = HttpContext.Current.Response;
            response.Clear();
            response.ClearContent();
            response.ClearHeaders();
            response.Buffer= true;
            response.ContentType = "application/pdf";
            response.AddHeader("Content-Disposition", "attachment;filename=\"" + filename + "\"");
            response.BinaryWrite(bytes);
            response.End();
        }


/// <summary>
/// This method is an overload for the DataView version of the same name.
/// </summary>
public static void DataTableToCsv(DataTable dt, string filename, bool WriteToResponse)
{
DataView dv = new DataView(dt);
DataViewToCsv(dv, filename, WriteToResponse);
}

/// <summary>
/// Parses a dataview into a CSV format. I always use tab separated columns, with \n separated rows.
/// </summary>
/// <param name="dv">The data source</param>
/// <param name="filename">If WriteToResponse is true, this must be a file name, otherwise a full path+file name to save the file to</param>
/// <param name="WriteToResponse">if true, Response.Writes the output to the client browser,
/// otherwise writes the contents to the specified file path</param>
public static void DataViewToCsv(DataView dv, string filename, bool WriteToResponse)
{
char ColDelim = '\t';
char RowDelim = '\n';

using(StringWriter sw = new StringWriter())
{
// output the header row
foreach(DataColumn dc in dv.Table.Columns)
sw.Write(CsvEscape(dc.ColumnName) + ColDelim);
sw.Write(RowDelim);

foreach(DataRowView dr in dv)
{
foreach(object o in dr.Row.ItemArray)
sw.Write(CsvEscape(o.ToString()) + ColDelim);
sw.Write(RowDelim);
}

if(WriteToResponse)
{
HttpResponse response = HttpContext.Current.Response;
response.Clear();
response.Charset = System.Text.UTF8Encoding.UTF8.WebName;
response.ContentEncoding = System.Text.UTF8Encoding.UTF8;
response.AddHeader("Content-Disposition", String.Format("attachment; filename=\"{0}\";", filename));
response.ContentType = "text/txt";
response.Write(sw.ToString());
response.End();
}
else
{
File.WriteAllText(filename, sw.ToString());
}
}
}

/// <summary>
/// Strips out any row/col delimeters. This could be slightly destructive but not important in my case :)
/// </summary>
public static string CsvEscape(string s)
{
return Regex.Replace(s, "\r|\n|\t", "");
}

/// <summary>
/// This method is an overload for the DataView version of the same name.
/// </summary>
public static void DataTableToXhtmlTable(DataTable dt, string filename, bool WriteToResponse)
{
DataView dv = new DataView(dt);
DataViewToXhtmlTable(dv, filename, WriteToResponse);
}

/// <summary>
/// This export method is similar to the common technique of binding a dataset to a datagrid/gridview
/// and rendering the contents to produce a HTML table that Excel can understand. However the datagrid
/// approach is not reliable if the data contains html characters, e.g. < or >, it produces invalid XML,
/// which causes problems in Excel and OpenOffice.
/// An alternative approach is to derive a GridView control that automatically sets HtmlEncode = true on
/// all the BoundColumns, but this can produce very bloated output where non ASCII characters are represented
/// and Excel will not decode the HtmlEncoded text.
/// I found the simplest approach is to parse the dataview and write out an XHTML table. This way the
/// output is guaranteed to be valid XHTML, and compatible with Excel and OpenOffice (use the HtmlDocument filter).
/// </summary>
/// <param name="dv">The data source</param>
/// <param name="filename">If WriteToResponse is true, this must be a file name, otherwise a full path+file name to save the file to</param>
/// <param name="WriteToResponse">if true, Response.Writes the output to the client browser,
/// otherwise writes the contents to the specified file path</param>
public static void DataViewToXhtmlTable(DataView dv, string filename, bool WriteToResponse)
{
using(StringWriter sw = new StringWriter())
{
sw.Write("<table border=\"1\">\n");

// output the header row
sw.Write("<tr>\n");
foreach(DataColumn dc in dv.Table.Columns)
sw.Write("<th>{0}</th>\n", XmlEscape(dc.ColumnName));
sw.Write("</tr>\n");

foreach(DataRowView dr in dv)
{
sw.Write("<tr>\n");
foreach(object o in dr.Row.ItemArray)
sw.Write("<td>{0}</td>\n", XmlEscape(o.ToString()));
sw.Write("</tr>\n");
}
sw.Write("</table>\n");

if(WriteToResponse)
{
HttpResponse response = HttpContext.Current.Response;
response.Clear();
response.Charset = System.Text.UTF8Encoding.UTF8.WebName;
response.ContentEncoding = System.Text.UTF8Encoding.UTF8;
response.AddHeader("Content-Disposition", String.Format("attachment; filename=\"{0}\";", filename));
response.ContentType = "application/vnd.ms-excel";
response.Write(sw.ToString());
response.End();
}
else
{
File.WriteAllText(filename, sw.ToString());
}
}
}

/// <summary>
/// Replace < & > characters with their xml escaped equivalents
/// </summary>
public static string XmlEscape(string s)
{
s = Regex.Replace(s, "<", "&lt;");
s = Regex.Replace(s, ">", "&gt;");
s = Regex.Replace(s, "&", "&amp;");
return s;
}
}
}

Tuesday, 24 May 2005 23:45:00 (GMT Daylight Time, UTC+01:00)  #    Comments [7]  Asp.Net

# Wednesday, 11 May 2005
Fix: Forms authentication redirects to a bogus default.aspx page, with RedirectFromLoginPage()

hi,
i've read a lot of posts on microsoft.public.dotnet.framework.aspnet.security about people who ran into problems using forms authentication, and the RedirectFromLoginPage() method, which always redirects to a default.aspx.  this is a big problem if you use sub-folders that don't have a default.aspx page, as in my case.
i read some posts that suggested manually Response.Redirecting the user to the url in the querystring, but actually this is incorrect because Forms Auth puts the default.aspx in that querystring even if the user wasn't at a page called default.aspx. 

i put together a simple solution to get the redirecting to work properly, and am posting it here for future reference:

  • The Login page (Login.aspx) must be set up to read the HTTP_Referrer, and add it to the ViewState in the first Page_Load on that page.
  • In the btnLogin_Click event on Login.aspx, the SetAuthCookie() event should be called, and the user should be Response.Redirected to the referrer value in the viewstate.
  • So you ignore the querystring that Forms Authentication adds on to the Login page.

Here is sample code:


*****************
Login.aspx
*****************

private void Page_Load(object sender, System.EventArgs e)
{
 if(!IsPostBack)
  ViewState["originalUrl"] = Request.UrlReferrer.AbsoluteUri;
}

private void btnLogin_Click(object sender, System.EventArgs e)
{
 string originalUrl = ViewState["originalUrl"];
 if(originalUrl == null || originalUrl == "") // in case the viewstate is corrupt, use default.aspx by 'default'
  originalUrl = "default.aspx";
 
 // do your password checking here
 // if it's all ok then...
 FormsAuthentication.SetAuthCookie(username, false);
 Response.Redirect(originalUrl, true);
}

Wednesday, 11 May 2005 13:33:48 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  Asp.Net

# Wednesday, 23 March 2005
Howto: Export a dataset to Excel (c# / asp.net)

In my web applications, i occassionaly need to allow the user to export a dataset as an excel file.  I was using a control written by Prashant Nayak posted on Code Project but he released a new version which was problematic for me, so i looked at other solutions.

obinna igbokwe  from  www.dedicatedsolutions.co.uk posted a good approach which creates a DataGrid object and binds it to the dataset, and then Renders the output of the control to the HttpResponse stream.  This works very well.  I have adapted his code to C# and added an option to specify a filename for the excel file.

Here is the code:

using System;
using System.Data;
using System.IO;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace Whatever
{
 /// 
 /// This class provides a method to write a dataset to the HttpResponse as
 /// an excel file. 
 /// 
 public class ExcelExport
 {
  public static void ExportDataSetToExcel(DataSet ds, string filename)
  {
   HttpResponse response = HttpContext.Current.Response;
   
   // first let's clean up the response.object
   response.Clear();
   response.Charset = "";
   
   // set the response mime type for excel
   response.ContentType = "application/vnd.ms-excel";
   response.AddHeader("Content-Disposition", "attachment;filename=\"" + filename + "\"");
   
   // create a string writer
   using (StringWriter sw = new StringWriter())
   {
    using (HtmlTextWriter htw = new HtmlTextWriter(sw))
    {
     // instantiate a datagrid
     DataGrid dg = new DataGrid();
     dg.DataSource = ds.Tables[0];
     dg.DataBind();
     dg.RenderControl(htw);
     response.Write(sw.ToString());
     response.End(); 
    }
   }
  }
 }
}

Wednesday, 23 March 2005 16:47:02 (GMT Standard Time, UTC+00:00)  #    Comments [72]  Asp.Net | Database

# Wednesday, 09 March 2005
FIX: Crystal Reports error "Logon Failed" happens when viewer control is not in form server tag.

I just spent hours trawling the web trying to find out why my web forms app was giving me the following error:

  • CrystalDecisions.CrystalReports.Engine.LogOnException: Logon failed

I am using a Dataset so there is obviously no logging on necessary for a disconnected data source.  By chance, i noticed that there was no server form around the crystal report viewer control, so i put one in, and it worked.  That has got to be the most annoying error message i have ever come across.


Wednesday, 09 March 2005 16:03:21 (GMT Standard Time, UTC+00:00)  #    Comments [2]  Asp.Net

# Friday, 18 February 2005
FIX: ISAPI Filter won't load on production server IIS

i have an ISAPI filter that i use for an Asp.Net web site.  It worked great on my development PC, but when i went to install it on the production server, it wouldn't load, and there was an event log saying "The HTTP Filter DLL whatever.dll failed to load. The data is the error".

After hunting around the newsgroups, i found out that the production server didn't have version 7.1 of the MFC dlls, as would be the case on a system with VS 2003 installed, hence the reason it wouldn't work.  so i downloaded MFC71.dll and MSVCR71.dll and put them in c:\windows\system32 and registered them with regsvr32.exe, ignoring the warnings about entry points not found.  that fixed it.

To remove this dependency in the DLL altogether, i went back in to VS2003 and in the project options, i changed 'Use of MFC' to a static library, which has resulted in an increased binary size (still only 150k) which i guess means it has included all the MFC stuff in the DLL itself.


Friday, 18 February 2005 18:07:09 (GMT Standard Time, UTC+00:00)  #    Comments [0]  Asp.Net | Windows Server

# Thursday, 03 February 2005
An ISAPI Filter to have nice URLs for your Asp.Net site

Background

My content management system stores all the pages in a sql database, so if you go to /home.html, there is no document by that name on the server.  Instead, i have one .aspx page called content.aspx that handles every request dynamically and it outputs the content for the page.

You can partially achieve this same effect without using an ISAPI filter (see http://www.codeproject.com/aspnet/URLRewriter.asp) but you don't get the advantage of specifying a folder path as a valid url.  For example, if you request /Departments/HR/ then you will get a 404 on the server, because IIS doesn't know to pass on this request to my /content.aspx page.  Jakob Nielsen (the usability expert) says that hackable urls are important, i.e. users should be allowed to remove parts of the path of the page they are at, and arrive at a level higher up in the site.  The ISAPI filter used here allows this.

Requirements for the Filter

  • Every .html request should be forwarded to /content.aspx
  • Every default document request should be forwarded to /content.aspx
  • A request with a querystring (?) should not be redirected because it is assumed to be a dynamic page.
  • The IIS log should record the original url (i.e. /Home.html) instead of /content.aspx?Url=/home.html

Because the code is in C++ and there is a lot of messy error checking stuff going on all over the place, i won't post it here (mess up my blog). instead you can download the VS 2003 solution (20 k) with source files + executable, and examine it for yourself if you're interested.  It does work, and i have about 15 sites runnning off this filter for a long time now. 

Credits

This solution is 95% taken from David Wang's excellent post on the ISAPI-DEV ms newsgroup. I just added in support for default documents and ignoring dynamic page requests.


Thursday, 03 February 2005 12:14:02 (GMT Standard Time, UTC+00:00)  #    Comments [2]  Asp.Net

# Friday, 17 December 2004
[FIX] aspx validation not working + buttons not clicking

when i deployed my web app to the production server, i found that none of the client-side validation was working. 

the first problem was that there was no mime-type set up on the server for .js files, so i added "application/x-javascript" for the .js extension.

the next problem was that the script references in my aspx pages did not match the script location on the server.   the html source of one of my pages had the webuivalidation.js path set to /aspnet_client/system_web/1_1_4322/WebUIValidation.js

but the server directory was "/aspnet_client/system_web/1_1_4322_0/WebUIValidation.js"

so i tried duplicating the directory so that i would have both paths on the server, but that caused strange problems on the aspx page, such as buttons not clicking (when the event handlers are definitely registered). 

so what i did was run aspnet_regiis -i on the server to make sure all the script maps are registered properly, and it worked. 

if you don't have control over the web server, you can add the following to your web.config

<system.web>
  <webControls clientScriptsLocation="/aspnet_client/system_web/1_1_4322_0/" />
  ...

Friday, 17 December 2004 12:46:46 (GMT Standard Time, UTC+00:00)  #    Comments [1]  Asp.Net

# Wednesday, 15 December 2004
Can't debug web projects with VS 2003 after installing VS 2005

Can't debug web projects with VS 2003 after installing VS 2005

Fortunately Fahad Khalil found the answer to this, he posted this simple solution on his blog.

I am reproducing it here for my own records. Shoot your IIS Console, then click on the Default Website and click on Properties. You will see a Brand new shiny tab [labelled ASP.NET] ... courtesy VS .NET 2005 :). Select the correct version i.e. 1.xx :)

Cheers Fahad. Saved me a lot of work.


Wednesday, 15 December 2004 10:58:09 (GMT Standard Time, UTC+00:00)  #    Comments [1]  Asp.Net

# Sunday, 05 December 2004
An intelligent 404 page, suggest pages with a similar name to the requested page

 

Introduction

Did you ever notice on big sites like microsoft.com, if you reach a page that doesn't exist, they don't just say "Sorry, 404.", they give you a list of pages that are similar to the one you requested.  This is obviously very nice for your users to have, and it's easy enough to integrate into your site.  This article provides source code and explains the algorithm to accomplish this feature. Note: the real benefit of the approach outlined here is the semi-intelligent string comparisons.

Background

The need for this grew out of a client of mine who was changing content management system, and every url in the site changed, so all the search engine results came up with 404 pages. this was obviously a big inconvenience, so i put this together to help users find their way through the new site when arriving from a search engine.

See it in action

Go to the following page http://www.iserc.ie/December15-ISERCWorkshoponTesting.html (which doesn't exist), and the 404 page should give you a list of pages that have quite similar names.

Requirements

  • Your web site must be set up so that 404 pages get redirected to a .net aspx page
  • You must have some way of getting an array of all the page urls in your site that you want to compare 404 requests against. if you have a content management system, there is probably a structure of all the pages stored in xml or a javascript array (for DHTML menus or something), or you could write your own query to get the pages from a database. if don't use a content management system, you could hard-code a string array variable in the 404 page code behind containing the page names, or think up some way of dynamically reading all the .aspx or .html pages from the file system.
  • When the 404 page is accessed, you need to know which page was requested. Using web.config, you can set up 404 errorCodes to go to /404.aspx, where it will tag on the requested page to the querystring. the source code here assumes you have this approach, but you can obviously change it to your own needs, simply change the GetRequestedUrl() function.

Why Regular Expressions are not enough

To compare strings, you can use System.String.IndexOf or you can use regular expressions to match similarities, but all these methods are very unforgiving for slight discrepancies in the string. in the example url above, the page name isDecember15-ISERCWorkshoponTesting.html but under the new content management system, the url is December 15 - ISERC Workshop - Software Testing.html, which is different enough to make traditional string comparison techniques fall down.

So i looked around for a fuzzy string compare routine, and came across an algorithm written by a guy called Levenshtein. His algorithm figures out how different 2 strings are, based on how many character additions, deletions and modifications are necessary to change one string into the other. This is called the 'edit distance', i.e. how far you have to go to make 2 strings match. This is very useful because it takes into account slight differences in spacing, punctuation and spelling. I found this algorithm on http://www.merriampark.com/ld.htm where Lasse Johansen kindly ported it to C#. The algorithm is explained at that site, and it is well worth a read to see how it is done.

Normalising the Scores

I originally had a problem with the algorithm because it gave surprising results for certain situations. If the 404 page request was for 'hello' and there is a valid page called 'hello_new_version' and another valid page called 'abcde', then the 'abcde' page gets a better score, because fewer changes are needed to make it the same as hello (just change the 5 characters in 'abcde' into 'hello'). this is 5 changes, even though the 'hello_new_version' is semantically a better match. Fortunately, a kind newsgroup participant named Patrice suggested that i divide the score by the length of the comparison string, to normalise the results. This worked perfectly, and i found that a score between 0 (perfect match) and 0.6 (a good match) is worth including as a suggested page. You can change this value in the ComputeResults() method if you want to make it more or less flexible.

Code Summary

private void Page_Load(object sender, System.EventArgs e)
{
   GetRequestedUrl();
   SetUpSiteUrls();
   ComputeResults();
   BindList();
} 

The above code shows the 4 key tasks that make up this solution. Each method is explained below.

Using the code

  1. GetRequestedUrl() simply figures out which page was requested. In this example, it is assumed that your web.config contains the following:
    ***In this example, the querystring on the 404.aspx page contains the requested url. SetUpSiteUrls() is where you load in all the pages in your site. In my content management system, i have an xml file with all the names, so i do an XPath query and add in the names one by one to the arraylist.
  2. private void SetUpSiteUrls()
    {
      this.validUrls = new ArrayList(); 
      /*
      * Insert code here to add the pages in your site to this arraylist
      */ 
    }
  3. ComputeResults() iterates through the urls you set up in SetUpSiteUrlsreturns and attaches a score of how close each one is to the requested url. It also sorts the results and discards any that are not a close match.
    private void ComputeResults()
    { 
        ArrayList results = new ArrayList(); // used to store the results 
        // build up an arraylist of the positive results 
        foreach(string s in validUrls) 
        {
            // don't waste time calculating the edit distance of nothing with something 
            if(s == "") continue; 
            double distance = Levenshtein.CalcEditDistance(s, this.requestedUrl); // both in lower case 
            double meanDistancePerLetter = (distance / s.Length); // anything between 0.0 and 0.6 is a good match. the algorithm always returns a value >= 0       
            if(meanDistancePerLetter <= 0.60D) 
            { 
                // add this result to the list. 
                results.Add(new DictionaryEntry(meanDistancePerLetter, "<a href='" + s + ".html'>" + s + "</a>")); // use dictionary entries because we want to store the score and the hyperlink. 
                // can't use sortedlist because they don't allow duplicate keys and we have 2 hyperlinks with the // same edit distance. 
            }
        }
        results.Sort(new ArrayListKeyAscend());
    }
    IMPORTANT NOTE: One thing to definitely look out for is the inner-most line of the above code. results.add(new DictionaryEntry(...). I am adding in a html hyperlink, with the name of the page + ".html". This may not be a correct link in your web site, because you may have removed the folder part of the url while populating the validUrls arraylist. You may need to expand the data structures used in this code to include full url for each page.

  4. BindList() simply binds the arraylist of results to the datagrid, which is configured to display them in a bulleted list.
    private void BindList()
    {
        if(results.Count > 0)
        {
    
            this.lblHeader.Text = "The following pages have similar names to " + this.requestedUrl + "";
            this.DataList1.DataSource = results;
            this.DataList1.DataBind();
        }
        else
        {
            this.lblHeader.Text = "Unable to find any pages in this site that have similar names to " + this.requestedUrl + "";
        }
    }

The 'magic' in the code is all done with the Levenshtein.CalcEditDistance method which returns the distance between 2 strings. It is included in the source.

Winforms Test Application

If you're interested to test out the levenshtein algorithm, i've written a windows forms application that lets you enter a string (e.g. a page url) and also a list of strings to compare it against (e.g. all the page urls in your site), and it gives you the 'edit distance' scores. download here (7k)

Comments

I think this is a great feature because it adds significant value to the user experience for a web site. Please feel free to comment below if you have any questions, find any bugs, improvements, or if you can't get it working, or if you use it in a novel way.

Enjoy!


Sunday, 05 December 2004 15:15:28 (GMT Standard Time, UTC+00:00)  #    Comments [0]  Asp.Net

# Thursday, 18 November 2004
System.Net.WebException: The request failed with HTTP status 401: Unauthorized.

I have a WSE2 web service that was working fine until one day i got the above error.  i realised i had changed the permissions on the folder (for the web application), so that only System, Adminstrators and ASPNET had permissions on it.  Previously the 'Users' group had permissions.  By a process of elimination, i found out that IUSR_xx needed to have read/execute permissions aswell as ASPNET, even though the process is running with the identity of ASPNET. 

it's strange, but i thought i'd post my solution here in case anyone else comes across this.


Thursday, 18 November 2004 12:45:53 (GMT Standard Time, UTC+00:00)  #    Comments [5]  Asp.Net

# Monday, 08 November 2004
Asp.Net DataGrid PageIndexChanged not working

i have an asp.net custom server control (deriving from a datagrid) and it has built in paging, sorting and databinding.  i ran into a weird problem, and thanks to Rick Strahl's post on http://west-wind.com/weblog/posts/211.aspx i was able to get it working.  Read that page first because there are many solutions posted for this weird error.

i'm using regular paging, and the next button works fine (all the time, for multiple pages), but if i click previous then a dud postback happens and the pageIndexChanged event doesn't fire.  i'm 100% sure that the eventhandler is hooked up, it just doesn't fire.  the solution in my case was to bind the datagrid in the control's PreRender method instead of in the OnInit method.  it just worked fine after doing this. 


Monday, 08 November 2004 18:33:01 (GMT Standard Time, UTC+00:00)  #    Comments [0]  Asp.Net

# Wednesday, 03 November 2004
using the same WSE2 web service with 2 different policies..

did you know you can configure multiple policies for the same web service?  it's possible because endpoint uri's are case-sensitive, so you can have WebService1.asmx and WEBSERVICE1.asmx, which are treated as separate web services in the policyCache.config file. see the sample below:

<endpoint uri="http://localhost/winDB.asmx">
 <defaultOperation>
  <request policy="#username-token-signed" />
  <response policy="" />
  <fault policy="" />
 </defaultOperation>
</endpoint>

<endpoint uri="http://localhost/WINDB.asmx">
 <defaultOperation>
  <request policy="" />
  <response policy="" />
  <fault policy="" />
 </defaultOperation>
</endpoint>

the first one uses a username-token-signed policy for authentication.  clients who wish to use this policy must have a reference to the web service matching the case of the endpoint uri exactly. 

the second endpoint has no policy enforcements and this means even a non-WSE request can use the web service.

some WSE implementations, (especially custom username tokens..) will have a method like "checkAuth()" that every web method calls at the start to verify programattically that the message obeys the rules.  this method throws soap faults for any missing WSE elements in the message header.  in my case, i want to allow requests originating from the web server itself (.aspx pages using the web methods) to bypass the authentication checks, so i put the following lines of code at the top of my "checkAuth()" method to allow requests made on the same server to go through:

// allow local ws requests to bypass security
if(HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"].ToString() == "127.0.0.1")
  return;  // skip further checks

i could also invoke the web methods using the web service class directly, (not go through a web service proxy) because it's within the same assembly, but i'm sure there are circumstances where this approach may prove useful.  if you find any, post them here as a comment, i'd be interested to hear.  


Wednesday, 03 November 2004 17:43:16 (GMT Standard Time, UTC+00:00)  #    Comments [0]  Asp.Net

# Monday, 26 July 2004
A great ASP.NET treeview control that is compatible with other browsers

I'm building a web front-end for my content management system (so that i can sell it to clients who don't run 100% windows PCs).  and i needed a cross-platform treeview control.  i did a quick search and noticed that the CrapStuff Treeview listed on asp.net was a popular one (http://crapstuff.craponne.org).  Ignoring the unusual name i tried it out and am delighted with it.  i have used the microsoft.ui.webcontrols treeview before and found it frustrating on several issues, that others have also encountered and have remained unresolved due to MS not supporting the control.

programatically the CrapStuff Treeview is great to work with, and i like the DHTML support also so that the control can be expanded fully without any postbacks, unless you want them to happen.

the author's name is Pascal Craponne so that's where he gets the name i suppose.  it's a great control and he has given it out for free.  thank you Pascal!


Monday, 26 July 2004 23:05:05 (GMT Daylight Time, UTC+01:00)  #    Comments [1]  Asp.Net

# Thursday, 24 June 2004
Http Status 400 trying to send large DIME attachments

Testing out my new DIME attachment code with WSE2, i ran into http errors with large uploads.  The error i got was a System.Net.WebException saying that it was a bad request, Http status 400, with an event in the application log under WSE:

HTTP/ASMX Message Receive Failure: Microsoft.Web.Services2.Dime.DimeFormatException: WSE352: The size of the record uuid:c0c3c949-5fb1-4171-88aa-06ce14499e44 exceed its limit.
at Microsoft.Web.Services2.Dime.DimeRecord.UpdateTotalBytesRead...

I had the maxRequestLength set to 128000 in web.config, but i found out that this does not apply to WSE2.  adding the following entry into web.config solved the problem.

<?xml version="1.0" encoding="utf-8" ?>
 <configuration>
  <microsoft.web.services2>
   <messaging>
    <maxRequestLength>256000</maxRequestLength><!-- 256 mb -->
   </messaging>
 </microsoft.web.services2>
...

Thursday, 24 June 2004 09:49:24 (GMT Daylight Time, UTC+01:00)  #    Comments [8]  Asp.Net

# Tuesday, 16 March 2004
Problem setting text for a password asp.net textbox

This short article outlines a quirk of the textbox control in Asp.Net, when set to password mode.  If you try and use myTextBox.Text = ….; it doesn't happen.

You might want to use something like this if you are presenting users with the facility to change a password, and you want to give them the option of changing their password, shown in stars, by simply deleting the stars and typing in a new one.  To accomplish this, use the following syntax:

myTextBox.Attributes("Value") = someString;

I can't think of a good reason ms built it this way at all.  oh for .net 2.0...


Tuesday, 16 March 2004 23:06:12 (GMT Standard Time, UTC+00:00)  #    Comments [0]  Asp.Net