# Tuesday, October 26, 2010

header[1]Probably one of the most common features of an ecommerce systems is to "retrieve my details" when logging in -after all that's why you create an account with the seller isn't it?

Out of the box, uCommerce has XSLT to retrieve the customer's last x addresses but one thing it didn't do was automatically re-assign the customer's details when logging in using the built in Umbraco membership code so we need to work around it ourselves -don't worry, it's not too hard (all the code is below for you).

Background

All customer addresses are stored in the uCommerce_Address table automatically, there should be one unique address per customer however if you're on an earlier release you may find you have several copies of the same address for each customer -this is a bug that's been sorted in v1.0.5.0 so upgrade if you can.

Now you'd be forgiven for thinking that you can just select the address from the uCommerce_Address table and then assign the id to the BillingAddressId property of your purchase order however if you do that, you'll find you get the error:

The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_uCommerce_PurchaseOrder_uCommerce_OrderAddress". 
The conflict occurred in database "CommsReadyCMS", table "dbo.uCommerce_OrderAddress", column 'OrderAddressId'.
The statement has been terminated.

 

You'll get this because there is also a second table involved -uCommerce_OrderAddress. uCommerce_OrderAddress stores the actual address used throughout the order process incase the customer changes an address in the future, the order will always have the correct address.

The Solution

Working around this isn't actually too difficult as mentioned before. The easiest solution is to create a new User Control in Visual Studio (I'll call mine login.ascx) and hook into the LoggedIn event. Once logged in, get the Umbraco member and from that, get the customer's billing address.

There's one caveat that I found with uCommerce and that's the way it gets the address. At the moment, there is a function on customer "GetAddress", this is great however if you check out the code it calls, it actually gets the customer's first address from the database -rather than the last address used. I don't think this is a bug as in most cases the first address you enter is your main address. I'll blog separately about managing a default address within the members section.

The code below however retrieves the most recently added address from the database

Login.ascx

<asp:literal runat="server" ID="litLoggedIn" />
<asp:literal runat="server" ID="litLoggedOut" />
<asp:Login runat="server" id="lgnForm" CssClass="checkout-details" 
	DisplayRememberMe="false" TitleText="" OnLoggedIn="lgnForm_LoggedIn"
	UserNameLabelText="Email Address" />

 

Login.ascx.cs

protected void lgnForm_LoggedIn(object sender, EventArgs e)
{
    //If the user has a basket, wire up the shipping address with their last order details
    var basket = SiteContext.Current.OrderContext.GetBasket(true);
    if (basket != null)
    {
        //Get the customers current order
        var po = basket.PurchaseOrder;
        //Look for a shipping address
        var add = po.GetBillingAddress();
        //We only need to assign the address if there isn't already one assigned to this order
        if (add == null)
        {
            //Get the customer who's just logged in
            var mem = Membership.GetUser(lgnForm.UserName);
            //To be safe check that we have a member
            if (mem != null)
            {
                //Find the customer
                var customer = Customer.ForUmbracoMember(Convert.ToInt32(mem.ProviderUserKey));
                if (customer != null)
                {
                    //Get the customer's most recent address
                    var previousAddress = customer.Addresses.ToList().LastOrDefault(a => a.AddressName == "Billing");
                    //If you want to get the customer's first address just uncomment this line
                    //var previousAddress = customer.GetAddress("Billing");

                    //Populate the billing address with the address)
                    if (previousAddress != null)
                    {
                        OrderAddress address = new OrderAddress
                                {
                                    FirstName = previousAddress.FirstName,
                                    LastName = previousAddress.LastName,
                                    EmailAddress = previousAddress.EmailAddress,
                                    PhoneNumber = previousAddress.PhoneNumber,
                                    MobilePhoneNumber = previousAddress.MobilePhoneNumber,
                                    CompanyName = previousAddress.CompanyName,
                                    Line1 = previousAddress.Line1,
                                    Line2 = previousAddress.Line2,
                                    PostalCode = previousAddress.PostalCode,
                                    City = previousAddress.City,
                                    State = previousAddress.State,
                                    Attention = previousAddress.Attention,
                                    CountryId = previousAddress.CountryId,
                                    AddressName = "Billing",
                                    OrderId = new int?(po.OrderId)
                                };
                        //Store the address in the database
                        address.Save();
                        //Assign the address to the purchase order
                        po.BillingAddressId = new int?(address.OrderAddressId);
                        //Save the purchase order (shopping cart)
                        po.Save();
                    }
                }
            }
        }
    }
}
Tuesday, October 26, 2010 11:31:55 AM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [0]  | 
# Thursday, August 12, 2010

Ever needed to take a large list and split it into smaller subsets of data for processing? Well this is the Extension Method for you. Tonight we had to split a small dataset (500 items) into even smaller sets of 10 so the provider’s web service wouldn’t timeout.

Seeing as I was going to miss out on my evening, I thought I’d see if I could do it a little differently using Linq and this is what I came up with:

/// <summary>
/// Simple method to chunk a source IEnumerable into smaller (more manageable) lists
/// </summary>
/// <param name="source">The large IEnumerable to split</param>
/// <param name="chunkSize">The maximum number of items each subset should contain</param>
/// <returns>An IEnumerable of the original source IEnumerable in bite size chunks</returns>
public static IEnumerable<IEnumerable<TSource>> ChunkData<TSource>(this IEnumerable<TSource> source, int chunkSize)
{
    for (int i = 0; i < source.Count(); i += chunkSize)
        yield return source.Skip(i).Take(chunkSize);
} 

It should extend any IEnumerable and allow you to split it into smaller chunks which you can then process to your heart’s content.

Here’s a quick example of it in use:

var list = new List<string>() { "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10" };
Console.WriteLine("Original list is {0} items", list.Count);
var chunked = list.ChunkData(3);
Console.WriteLine("Returned the data in {0} subsets", chunked.Count());
int i = 1;
foreach (var subset in chunked)
{
    Console.WriteLine("{0} items are in subset #{1}", subset.Count(), i++);
    int si = 1;
    foreach (var s in subset)
        Console.WriteLine("\t\tItem #{0}: {1}", si++, s);
}

And this will output

Original list is 10 items
Returned the data in 4 subsets
3 items are in subset #1
		Item #1: Item 1
		Item #2: Item 2
		Item #3: Item 3
3 items are in subset #2
		Item #1: Item 4
		Item #2: Item 5
		Item #3: Item 6
3 items are in subset #3
		Item #1: Item 7
		Item #2: Item 8
		Item #3: Item 9
1 items are in subset #4
		Item #1: Item 10

2 lines of code to do all that work -Neat

Thursday, August 12, 2010 9:32:44 AM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [1]  | 
# Sunday, April 18, 2010

You may have come across this error once or twice while deploying your project if you develop using Web Deployment Projects. It's usually caused when you copy and paste a page and forget to update both the page declaration and code behind file.

But the website builds!?!

You don't usually get the ILMerge error until you build the web deployment project because when you build a website directly, it doesn't compile all the code into a single assembly so the class names are seen as different. Part of the web deployment process is to compile all the websites code into a single assembly hence the duplicate references.

What's the solution?

It's surprisingly simple, all you need to do is open up the offending aspx and aspx.cs files and update two lines:

1. In the code behind file, rename the partial class. By default Visual Studio will name the class FolderName_Pagename which should result in a unique name

2. The page declaration (first line of the page) in the aspx file. You have to make sure that both the Inherits attribute and CodeBehind reference are correct.

Tip: To avoid confusing yourself, open the files independently using the solution browser because if you open the aspx and press F7 to switch to the code behind file before updating the page declaration, you'll end up editing the page you copied rather than the copy.

Sunday, April 18, 2010 12:47:20 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [0]  | 
# Tuesday, May 12, 2009

This was one of those irritating errors that you get when you're trying to do something quickly before you go home and you can't for the life of you fathom the issue.

I had the following code (simple enough):

FileInfo f = new FileInfo("## File's Path ");
try
{
    f.MoveTo("## DROP OFF DIRECTORY ##"));
}
catch (Exception e)
{
    //Log the exception here
}

The fix was simple, you just have to remember to specify the new filename too. (DOH!). Here's the "correct" code.

FileInfo f = new FileInfo("## File's Path ");
try
{
    f.MoveTo(Path.Combine("## DROP OFF DIRECTORY ##", f.Name));
}
catch (Exception e)
{
    //Log the exception here
}

Hope that helps you out ;)

Tuesday, May 12, 2009 8:39:35 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [4]  | 
# Friday, April 17, 2009

Ok it might be a little less than half side but it's near enough. I've been sitting on this for a while and needed to reference it for someone so I thought I'd post quickly about it. One of the most common complaints about .Net is that you have a lot of hidden "content" by the way of hidden inputs and the likes throughout your site. This can easily get corrupt on postback/slowdown the page load times etc.

Really you should be optimising each control on the page (enabling/disabling where relevant) but if you want to cheat (lets face it, we all do):

  1. Download the files: PageStateAdapterv1.0.zip (3KB)
  2. Put PageStateAdapter.browser into your /App_Browsers/ folder (or create one and add it)
  3. Put TSDPageStateAdapter.dll into your website's /bin/ folder
  4. Load up your website and checkout your ViewState :)

Incase you're interested in the source for it:

PageStateAdapter.browser

<browsers>
    <browser refID="Default">
        <controlAdapters>
            <adapter controlType="System.Web.UI.Page" adapterType="TheSiteDoctor.PageStateAdapter.PageStateAdapter" />
        </controlAdapters>
        <capabilities>
            <capability name="requiresControlStateInSession" value="true" />
        </capabilities>
    </browser>
</browsers>

PageStateAdapter.cs

using System.Web.UI;

namespace TheSiteDoctor.PageStateAdapter
{
    public class PageStateAdapter : System.Web.UI.Adapters.PageAdapter
    {
        public override PageStatePersister GetStatePersister()
        {
            return new SessionPageStatePersister(this.Page);
        }
    }
}

The best example of how much this reduces ViewState by is when you add a large DataGrid to your site.

Post files: PageStateAdapterv1.0.zip (3KB)

Update: Apologies to those of you who downloaded and found it wouldn't compile, the .browser file was a little off (missing the second "PageStateAdapter"). I've updated it and changed the zip file download. Enjoy!

Friday, April 17, 2009 3:53:05 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [4]  | 
# Tuesday, February 17, 2009

One of the issues I had with John Forsythe's Recent Comments macro for DasBlog was that the dasBlog recent comments weren't ordered by date (descending). I found that as people commented on older posts they were getting buried which irritated me as many were very still valid comments.

The fix was actually fairly simple, it was just a matter of adding  a sort and thanks to Lamba expressions, this is something we can do fairly simply. If you want to add recent comments to your dasBlog installation, use the following macro:

Recent Comments Macro

public virtual Control RecentComments(int count, int adminComments, int trimTitle, int trimContent, int trimAuthor, bool showTitle, bool showCommentText, bool showCommentCount)
{
    int commentsToShow;
    int totalComments;

    CommentCollection allComments = this.requestPage.DataService.GetAllComments();
    totalComments = allComments.Count;

    //Sort the comments in date order (descending)
    allComments.Sort((c1, c2) => c1.CreatedUtc.CompareTo(c2.CreatedUtc));

    if (!this.requestPage.HideAdminTools && SiteSecurity.IsInRole("admin"))
        commentsToShow = totalComments - adminComments;
    else
        commentsToShow = totalComments - count;

    if (commentsToShow < 0)
        commentsToShow = 0;

    StringBuilder sb = new StringBuilder();

    sb.AppendLine("<div class=\"recentComments\">");

    if (showCommentCount)
        sb.AppendFormat("<div class=\"totalComments\">Total Comments: {0}</div>", totalComments);

    sb.AppendLine("<ul class=\"comments\">");

    #region Loop through the comments

    for (int i = totalComments - 1; i >= commentsToShow; i--)
    {
        Comment current = allComments[i];

        bool showComment;
        if (!current.IsPublic || (current.SpamState == SpamState.Spam))
        {
            if (!this.requestPage.HideAdminTools && SiteSecurity.IsInRole("admin"))
            {
                showComment = true;
            }
            else
            {
                showComment = false;
                if (commentsToShow > 0)
                    commentsToShow--;
            }
        }
        else
        {
            showComment = true;
        }

        if (showComment)
        {
            if ((current.SpamState == SpamState.Spam))
                sb.Append("<li class=\"spam\">");
            else if (!current.IsPublic)
                sb.Append("<li class=\"hidden\">");
            else
                sb.Append("<li>");

            string link = String.Format("{0}{1}{2}", SiteUtilities.GetCommentViewUrl(current.TargetEntryId), "#", current.EntryId);
            string title = current.TargetTitle;
            string desc = current.Content;
            string author = current.Author;

            if (showTitle)
            {
                sb.AppendFormat("<div class=\"recent{0}CommentsTitle\"><a href=\"{1}\">",
                    current.SpamState,
                    link
                    );

                if ((title.Length > trimTitle) && (trimTitle > 0))
                    sb.AppendFormat("RE: {0}...", title.Substring(0, trimTitle));
                else
                    sb.AppendFormat("RE: {0}", title);

                sb.Append("</a></div>");
            }

            if (showCommentText)
            {
                sb.AppendFormat("<div class=\"recentCommentsContent\"><a href=\"{0}\">",
                    link
                    );

                if ((desc.Length > trimContent) && (trimContent > 0))
                {
                    sb.Append(desc.Substring(0, trimContent));
                    sb.Append("...");
                }
                else
                {
                    sb.Append(desc);
                }

                sb.Append("</a></div>");
            }

            sb.Append("<div class=\"recentCommentsAuthor\">");

            if ((author.Length > trimAuthor) && (trimAuthor > 0))
            {
                int num3 = (trimAuthor > author.Length) ? author.Length : trimAuthor;
                sb.Append("by " + author.Substring(0, num3));
                sb.Append("...");
            }
            else
            {
                sb.Append("by " + author);
            }
            sb.Append("</div></li>");
        }
    }
    #endregion

    sb.AppendLine("</ul>");
    sb.AppendLine("</div>");

    return new LiteralControl(sb.ToString());
}

I've since been working on extending it further so you can add a "All Comments" link which I'll post up later as it needs a little more work :)

If you want this wrapped up as a DLL let me know and I'll upload it.

Update 26th Feb 2009: You can download the dll here (it's also got a few other things in there if you want to look around).

Update 27th Feb 2009: I noticed that the above code was messing up everynow and again so I've updated it to use Linq instead which seems to work well. I've updated the DLL but not the source yet.

Tuesday, February 17, 2009 9:25:05 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [1]  | 
# Monday, December 15, 2008

I thought I'd share some festive "fun" today. For quite some time now I've hooked into the build events within Visual Studio but I think this is just taking it too far...

Basically the script now increments the version number of the DLL (if relevant) automatically, times the time for the build and adds it to a total build time to date (stored in an external txt file) and finally prints out whether the build was a success or not -basically some of our projects are taking a while to build and I wanted something that was "clearer" ;)

I'm thinking about adding Christmas trees next, anyone know how to do them in ASCII?

Monday, December 15, 2008 6:21:37 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [5]  | 
# Wednesday, November 05, 2008

We've just moved a couple of our sites onto a new server and have intermittently started to receive the error "The remote host closed the connection. The error code is 0x80072746.", usually around the same time as "Timeout expired.  The timeout period elapsed prior to completion of the operation or the server is not responding.".

As the infrastructure hadn't changed we were able to rule out network issues, hardware issues (it was an upgrade) and nothing had changed on the code level so we put it down to the new backup routine.

On a little Googling, I found a few sites that were pointing to (among other things) the Request length and Executiuon time which got me thinking, in this particular site there is a lot of data being sent/retrieved and it could be maxing out the request. After a little more digging I found this article from Microsoft about the httpRuntime Element and I quote

This time-out applies only if the debug attribute in the compilation element is False. To help to prevent shutting down the application while you are debugging, do not set this time-out to a large value.

Although I didn't recall making any changes to the site, when updating the database configuration settings, I did change the compilation element to false. So far increasing the executionTimeout value appears to have fixed the issue.

Wednesday, November 05, 2008 2:03:40 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [1]  | 
# Saturday, November 01, 2008

We had an interesting problem the other day while integrating with a web service hosted on an apache server. Every time we called the WSDL methods we could see a valid request being sent and a valid response being returned (ok there were a few syntax errors but nothing that would stop it converting) but despite this every time, without fail, it would return null at code level.

The solution was actually amazingly simple. In short, since our last flurry with web services we've upgraded to Visual Studio 2008 and installed ReSharper (see below for how that's relevant). In VS2008 they've sneakily added a new menu "Add Service Reference" and although it's in the same menu location and a similar wording etc does something different. If you add your WSDL reference this way it will more than likely fail.

To add a "Web Reference" in Visual Studio 2008 follow the instructions below and hopefully you won't go down the same routes that I did trying to debug the XML...

Click the "Add Service Reference" menu option (obviously!)

The screen below should appear, instead of entering the address of the web service into the address bar, click the advanced button at the bottom (highlighted)

That'll then bring up the screen below, on which you need to click the "Add Web Reference" button at the bottom.

You should then be presented with the usual "Add Web Reference" screen (see below).

Why did I mention ReSharper? Well I've notice that as well as adding to the IDE they've also updated some of the menu items, most noticeably "Go to Definition" which is now "go to Declaration" (see below) so I figured this new menu option was thanks to them.

ReSharper:

Without ReSharper: VS2008GoToDeclarationWithoutResharper.png

So the next time your WSDL is returning a null object even though a valid response is being retrieved, check how you added it in Visual Studio,.

Saturday, November 01, 2008 6:28:49 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Wednesday, September 03, 2008

When uploading some new media items for a client today we noticed that if you selected "Remove" before saving, it doesn't actually remove the file from the FileSystem. Having a quick look around the forums I saw there are a few posts already pointing this out so I thought I'd fix it.

This is a little application that simply checks the media items in the database and then compares it against a folder you select on your machine. If the file is in use according to the database then it's ignored otherwise it will remove it.

To use:

  1. Enter your server's login details
  2. Click "Test Connection"
  3. Select the relevant database from the drop down
  4. Check the "Media Folder Name" matches your Umbraco's installation
  5. Locate your Media Folder on your computer
  6. Click "Check Media Folder" -this will then list all the orphan files
  7. If it looks right, click "Delete" -with caution
  8. Job done

There are a few checks in place to avoid mishap but it's not 100% foolproof as I needed something rough and ready to sort a couple of installations out. If this is something that's seen as useful I'll extend it a touch, some ideas I've got already:

  • Check that the selected media folder matches that of the database
  • Check that the media id's are the same (to avoid wiping another installation)
  • Save config settings for easy re-use
  • Use webservices rather than a direct connection to the database
  • Enable FTP useage

Please note: I accept no responsibility if anything was to go horribly wrong with this. I would backup your folder first just in case!

You can download the MediaFolderCleaner application here

Wednesday, September 03, 2008 5:15:14 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [9]  | 
# Friday, July 25, 2008

Exactly a year ago today I posted a little application that output the sites in IIS to a text file and as a few days ago Lars asked for the source, I thought it would be a nice thing to release it exactly a year later.

I didn't plan it that way, it just happened! Cool :)

Identify IIS Sites and Log File locations for WWW and FTP source

using System;
using System.DirectoryServices;
using System.IO;
using System.Collections;

namespace IISSites
{
    class Program
    {
        static string fileToWrite = String.Empty;

        [STAThread]
        static void Main(string[] args)
        {
            fileToWrite = String.Format("IISExport{0:dd-MM-yyyy}.txt", DateTime.Today);
            if (args != null && args.Length > 0)
            {
                fileToWrite = args[0];
            }

            SortedList www = new SortedList();
            SortedList ftp = new SortedList();
            try
            {
                const string FtpServerSchema = "IIsFtpServer"; // Case Sensitive
                const string WebServerSchema = "IIsWebServer"; // Case Sensitive
                string ServerName = "LocalHost";
                DirectoryEntry W3SVC = new DirectoryEntry("IIS://" + ServerName + "/w3svc", "Domain/UserCode", "Password");

                foreach (DirectoryEntry Site in W3SVC.Children)
                {
                    if (Site.SchemaClassName == WebServerSchema)
                    {
                        string LogFilePath = System.IO.Path.Combine(
                            Site.Properties["LogFileDirectory"].Value.ToString(),
                            "W3SVC" + Site.Name);
                        www.Add(Site.Properties["ServerComment"].Value.ToString(), LogFilePath);
                    }
                }

                DirectoryEntry MSFTPSVC = new DirectoryEntry("IIS://" + ServerName + "/msftpsvc");
                foreach (DirectoryEntry Site in MSFTPSVC.Children)
                {
                    if (Site.SchemaClassName == FtpServerSchema)
                    {
                        string LogFilePath = System.IO.Path.Combine(
                            Site.Properties["LogFileDirectory"].Value.ToString(),
                            "MSFTPSVC" + Site.Name);
                        ftp.Add(Site.Properties["ServerComment"].Value.ToString(), LogFilePath);
                    }
                }
                int MaxWidth = 0;
                foreach (string Site in www.Keys)
                {
                    if (Site.Length > MaxWidth)
                        MaxWidth = Site.Length;
                }
                foreach (string Site in ftp.Keys)
                {
                    if (Site.Length > MaxWidth)
                        MaxWidth = Site.Length;
                }
                OutputIt("Site Description".PadRight(MaxWidth) + "  Log File Directory");
                OutputIt("".PadRight(79, '='));
                OutputIt(String.Empty);
                OutputIt("WWW Sites");
                OutputIt("=========");
                foreach (string Site in www.Keys)
                {
                    string output = Site.PadRight(MaxWidth) + "  " + www[Site];
                    Console.WriteLine(output);
                    OutputIt(output);
                }
                if (ftp.Keys.Count > 0)
                {
                    OutputIt(String.Empty);
                    OutputIt("FTP Sites");
                    OutputIt("=========");
                    foreach (string Site in ftp.Keys)
                    {
                        string output = Site.PadRight(MaxWidth) + "  " + ftp[Site];
                        OutputIt(output);
                    }
                }
            }
            // Catch any errors
            catch (Exception e)
            {
                Console.WriteLine("Error: " + e.ToString());
            }
            finally
            {
                Console.WriteLine();
                Console.WriteLine("Press enter to close/exit...");
                //Console.Read();
            }
        }

        static void OutputIt(string lineToAdd)
        {
            Console.WriteLine(lineToAdd);

            if (!String.IsNullOrEmpty(fileToWrite))
            {
                StreamWriter SW;
                SW = File.AppendText(fileToWrite);
                SW.WriteLine(lineToAdd);
                SW.Close();
            }
            else
            {
                Console.WriteLine("locationToOutput is Null or String.Empty please supply a value and try again.");
            }
        }
    }
}
Friday, July 25, 2008 3:52:37 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [1]  | 
# Tuesday, July 15, 2008

As it's my Birthday today I thought I'd post a silly ditty. I'm currently altering Protx's old ASP.Net library to accommodate their changes in regards 3D Secure and while reflecting some of the code came across an enum with their number representations as Hexadecimal strings. I needed to convert these to decimals so thought I'd share a quick and easy way to do it.

Open up Window's Calculator (Windows Key + R then type in calc) under the View menu select "Scientific". Press the F5 key to switch over to Hex entry. Type in the value after the 0x and hit F6

Simple, easy and will help you convert all those Hexadecimal strings (ones that look like this: 0x01 or 0x1a).

Right, time for a coffee :)

Tuesday, July 15, 2008 10:25:56 AM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [2]  | 
# Thursday, July 10, 2008

We've been setting up the page's title using ASP.Net for quite some time now, we tend to word it: Product Name | Category | Site Name as this IMO is the most comprehensive naming convention (though the pipe (|) gets converted to a space for the bookmarks).

When editing one of our sites today though I noticed that the title was resorting to Avant Garde hair salons -which was setup as the brand's name. Looking into it I found that if you set the <title> tag within the page or master page, ASP.Net doesn't override it from the codebehind so watch out!

For those of you who don't know how to set the title of your page from codebehind it's simple:

this.Page.Title = "Put your title here";
ASP.Net | C#
Thursday, July 10, 2008 11:57:57 AM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [0]  | 
# Thursday, May 29, 2008

Doug Setzer posted this comment in response to my recent "A seriously elegant SQL Injection" post and I thought it may be of interest to others so have promoted it to a post...


Well, I'll step up and say that I am the "mate" who had this done.  Tim's right - *always* sanitize your inputs.  In my defence, this was a site that I inherited from a previous contractor.  I'm not entirely absent of blame, I still should have done a security sweep through the code.

I'd like to document the steps that I went through once this was identified to try and avoid this kind of thing in the future.

  1. Edit every web page that executes a query to sanitize any parameters that are passed in.  Since the site was classic ASP, I used my "SQLStringFieldValue" function:
    www.27seconds.com/kb/article_view.aspx?id=50
  2. Modify the DB user account that is used to have *read only* access to the database
  3. Modify the pages that DO write to the database to have *read/write* access to the specific tables that are being changed.  This limits the number of places that SQL Injection can occur to a smaller set than was previously possible.  I still sanitize all of my input, but I'm extra spastic in these database calls.
  4. Add database auditing (triggers writing to mirror tables with audit event indicator & date/time) to see when data changes occur.  This is still problematic with the pages that have "write" permissions to the tables, but again- that footprint is much smaller.
    My future plans are to move to a view/stored procedure based architecture.  I can then limit write permissions to just the stored procedures and read permissions to just the views.  My grand gusto plans are to move to using command objects & parameters, but I'd sooner re-write the entire site.

Although Doug's attack wasn't the same nihaorr1.com attack that's going around atm it was similar so I would imagine other's will find this useful.

It still amazes me how many developers still fail to sanitise strings, only last week I came across another site (in PHP) that was allowing simple SQL injections to be used to log into their administration system. It was down to a problem with the sanitization string, but why not at least check your site before it goes live? It takes 2 minutes and even less to fix...

For those of you who need a few pointers, there's a good discussion or two about sanitising strings on the 4 Guys From Rolla site.

Thursday, May 29, 2008 3:32:33 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [2]  | 
# Friday, September 21, 2007

It's been rather quiet on my blog recently, if you're wondering why (and don't chat to me on/off-line) I thought I would share with you what we've been working on recently.

For the past month or so The Site Doctor has been developing a new web site (www.wineandhampergifts.co.uk) for Porter and Woodman Gifts Ltd - a local company that produces personalised corporate hampers and gifts. It's been quite a challenge as they have a rather unusual ordering system that allows multiple recipients/addresses multiple items. Looking at it now, it's not so complicated but the delivery charge calculations and initial specs took a while to fully grasp. It's been really enjoyable.

I'll probably cover aspects of the site over the forthcoming months but there are a few really nice features to the Wine and Hamper Gifts site (or at least I think so), some of which the end user will never know about such as the use of generics to calculate the address/recipient/gift variations) and those that they may -for instance the use of the JavaScript1 Zoom function on the product details page (courtesy of LuckyZoom), also the design created by our excellent designer Gareth Brown all adds up to what has to be one of the best sites I've developed to date.

1 Yes, I did just say I've integrated some JavaScript into the site ;)

I doubt most of my readers are interested on the in's and out's of the project itself but from an SEO perspective, I for one am expecting pretty decent results. We opted to use the URL Rewriting ISAPI from Helicon this time round over our usual IISMods URL Rewriting ISAPI as for some reason the IISMods site has been offline for a while (and checking now has been converted into a very weird site).

Another aspect that some people may be unaware of is that the majority of the Wine and Hamper Gifts site operates the same without JavaScript as it does with JavaScript, this is important not only for screen readers but also search engines. There is only one area of the Wine and Hamper Gifts site that I'm aware of that doesn't operate without JavaScript and that is the "Personalise this gift" link on the cart page that allows the user to either edit the existing message or add one that doesn't already exist, that's because it uses a LinkButton, but I may find a way around that later.

Other features that I really like are little things like the way the drop down lists on the left hand menu are created -they're not actually drop down lists but unordered lists that are then manipulated using JavaScript, I think the JavaScript could do with a little tweaking but the result is superb. The Wine and Hamper Gifts site also creates a PDF receipt for the user which is emailed to them, this is something I've been meaning to look into for some time but haven't had the chance, luckily while I was developing the site, Sean Ronan posted to the MsWebDev list about an ASP.Net PDF library iTextSharp (a port from a Java library) which, despite a few oddities from the POV of the Java port does exactly what I wanted. The library is pretty easy to use once you get your head around it and certainly produces some nice results.

There's still more work that's needed to finalise the content and various aspects of the Wine and Hamper Gifts website but if you have a chance, check out the new Porter and Woodman Gifts Ltd Wine and Hamper Gifts website and leave a comment here letting me know what you think :D

Oh, and they've given us a pretty high target to get before Christmas so if you're thinking about treating your customers to a personalised corporate hamper or gift give a little thought to using www.wineandhampergifts.co.uk

AJAX | ASP.Net | C# | CSS | Design | SEO | The Site Doctor | Web Development
Friday, September 21, 2007 11:20:01 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [0]  | 
# Friday, August 24, 2007

Server Error in '/' Application.


The Controls collection cannot be modified because the control contains code blocks (i.e. <% ... %>).
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.Web.HttpException: The Controls collection cannot be modified because the control contains code blocks (i.e. <% ... %>).

Source Error:

 

Line 132:                        metaKey.Name = "keywords";
Line 133:                        metaKey.Content = p.MetaKeywords;
Line 134:                        this.Page.Header.Controls.Add(metaKey);
Line 135:                    }
Line 136:                    if (!String.IsNullOrEmpty(p.MetaDescription))


Source File: a:\xyz\ContentHandler.aspx.cs    Line: 134

Stack Trace:

 

[HttpException (0x80004005): The Controls collection cannot be modified because the control contains code blocks (i.e. <% ... %>).]
   System.Web.UI.ControlCollection.Add(Control child) +2105903
   ContentHandler.Page_Load(Object sender, EventArgs e) in a:\xyz\ContentHandler.aspx.cs:134
   System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr fp, Object o, Object t, EventArgs e) +15
   System.Web.Util.CalliEventHandlerDelegateProxy.Callback(Object sender, EventArgs e) +34
   System.Web.UI.Control.OnLoad(EventArgs e) +99
   System.Web.UI.Control.LoadRecursive() +47
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +1061


Version Information: Microsoft .NET Framework Version:2.0.50727.832; ASP.NET Version:2.0.50727.832

Another day, another issue ;)

This had me going around in circles for a while until I realised what it was, if you're getting this error you can bet your bottom dollar that you have <%= %> somewhere in your page's header -furthermore I'd hazard a guess that you've got it in some JavaScript to reference an ASP.Net control on the page- and then you're trying to add a control to the header programmatically (or a custom control from someone like Telerik is trying to). Am I right1?

1 I'm not allowed to ask you to so I won't, but if I was right, then spend that bottom dollar clicking on one of the Google Ads :P

I can't tell you exactly why this occurs but my understanding of it is that ASP.Net can't re-create the header if it has Response.Write somewhere in the header (<%=) -most likely due to when the header is created it's not available (will look into it). No doubt you want to know the fix?

The Fix
The fix is simple, remove the inline code blocks and JavaScript and move it to your code behind i.e.:

string _manageSearch = String.Format( @" 
        function ManageSearch(){{
                var lbl = document.getElementById(""lblFindAGift"");
                var txt = document.getElementById(""{0}"");
                var btn = document.getElementById(""{1}"");

                .Do Something with it..

        }}",

        txtSearch.ClientID);

this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "ManageSearch", _manageSearch, true);

Remember: You need to escape the curly brackets otherwise you will get a "String.Format- Exception of type System.Web.HttpUnhandledException was thrown"

Update: Thanks to Julian Voelcker for sending me this alternative "fix" for the problem, can't say I like it though ;) basically instead of using <%= ... %> you would write the databinding expression of: <%# ... %>

Friday, August 24, 2007 10:49:56 AM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [10]  | 
# Wednesday, August 22, 2007

Type
System.FormatException
Message
Exception of type 'System.Web.HttpUnhandledException' was thrown.
StackTrace
at System.Text.StringBuilder.FormatError()
at System.Text.StringBuilder.AppendFormat(IFormatProvider provider, String format, Object[] args)
at System.String.Format(IFormatProvider provider, String format, Object[] args)
Error Line
0

Just got that message (or at most "Exception of type 'System.Web.HttpUnhandledException' was thrown")? Puzzled? I was the first time I got it, I've been meaning to post about it for quite some time now so seeing as I got it again today I took the hint.

The error is horrifingly obvious when you know about it, in short, you've no doubt got some code that looks like this:

String.Format("<html><head><style type=\"text/css\">body{color: #fff;}</style><body>...");

Can you spot it now? Notice your style declaration is using the curly brackets? Basically String.Format is interpreting that as a placeholder i.e. {0} and is throwing a wobbly.

The solution is simple too, just replace all opening/closing brackets with two i.e:

String.Format("<html><head><style type=\"text/css\">body{{color: #fff;}}</style><body>...");

I hope that helps someone out there :)

P.S. Watch out for methods that use String.Format as they may catch you out in the same way -i.e. Subject of System.Net.Mail.MailMessage

Wednesday, August 22, 2007 9:43:12 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [6]  | 
# Friday, March 09, 2007

This morning Julian Voelcker came to me with an interesting issue that I’ve looked into before but I’ve never really looked into a re-useable solution. Seeing as it’s fun Friday I thought why not ;)

The scenario: I would like to offer my users a custom mail merge facility where by they can insert values stored in the database such as their name. The selection of columns is unlikely to be changed and if it does then I’ll be the one to do it. There are about 20 fields to choose from.

Easy enough, in the past I’ve kept it to a minimum and then just done a simple find and replace on the body i.e.:

//Create a dataset and add some test columns
DataTable dt = new DataTable();
dt.Columns.Add("Name");
dt.Columns.Add("Email");

#region Add some test data

DataRow dr = dt.NewRow();
dr["Name"] = "Julian";
dr["Email"] = "julian@email.com";
dt.Rows.Add(dr);

dr = dt.NewRow();
dr["Name"] = "Tim";
dr["Email"] = "tim@email.com";
dt.Rows.Add(dr);

#endregion

#region Create the example email body

string emailBody = "<p>This is a test email to {{Name}} that would be sent to the email address: {{Email}}.</p>";

#endregion

#region Do the work

//Loop through the rows
for (int i = 0; i < dt.Rows.Count; i++)
{
    //Get the data row for this instance
    DataRow row = dt.Rows[i];

    //Create a new body as this'll be updated for each user
    string body = String.Empty;

    //Update the body
    body = emailBody.Replace("##Name##", row["Name"]);
    body = body.Replace("##Email##", row["Email"]);

    litOutput.Text += String.Format("{0}<hr />", body);
}

#endregion

The issue I see with this however is (among others) having 20 fields is a lot to be doing with a find/replace statement as it wouldn’t be very elegant and a nightmare to manage. Sticking with this method of using a dataset I suggested we use a regular expression to match the field delimiters and do a replace that way:

//Create a dataset and add some test columns
DataTable dt = new DataTable();
dt.Columns.Add("Name");
dt.Columns.Add("Email");

#region Add some test data

DataRow dr = dt.NewRow();
dr["Name"] = "Julian";
dr["Email"] = "julian@email.com";
dt.Rows.Add(dr);

dr = dt.NewRow();
dr["Name"] = "Tim";
dr["Email"] = "tim@email.com";
dt.Rows.Add(dr);

#endregion

#region Create the example email body

string emailBody = "<p>This is a test email to {{Name}} that would be sent to the email address: {{Email}}.</p>";

#endregion

#region Do the work

//Loop through the rows
for (int i = 0; i < dt.Rows.Count; i++)
{
    //Get the data row for this instance
    DataRow row = dt.Rows[i];

    MatchEvaluator replaceField = delegate(Match m)
    {
        return row[m.Groups[1].ToString()].ToString();
    };

    //Create a new body as this'll be updated for each user
    string body = String.Empty;
    //Find the fields
    Regex r = new Regex(@"{{(\w{0,15}?)}}");
    body = r.Replace(emailBody, replaceField);

    litOutput.Text += String.Format("{0}<hr />", body);
}

#endregion

This is alright and in many ways very scaleable. I’m not a fan of DataSets but in this instance it works nicely and does mean expanding the available fields at a later date would just be a matter of adding columns to the query.

How does this relate to accessing a property of an object using a string value instead? Well there was a catch, Julian wasn’t using a DataSet and didn’t want to, he had a collection of custom objects all ready and waiting. As he uses a code generator to generate his Data Access Layer and Business Logic Layer there was a method already exposed allowing you to search for a property by string but it's not a standard .Net method so I decided to work out how it was done.

The solution it turned out was a really rather elegant solution IMHO. Using reflection you can use the same concept as above but with custom objects and Robert is your father’s wife’s sister:

Reflection.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Reflection.aspx.cs" Inherits="Reflection" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <h1>Reflection Demo</h1>
        <p>Choose from the following fields to build up your email message, the valid fields are (you can choose whether to use non-valid fields as a test if you like):</p>
        <ul>
            <li>Id</li>
            <li>Email</li>
            <li>Name</li>
            <li>JoinedDate</li>
        </ul>
        <p><asp:CheckBox ID="chkCaseSensitive" runat="server" Text="Make the property search case insensitive" /></p>
        <p><label for="txtEmailBody">Example email body:</label><br />
        <asp:TextBox runat="server" ID="txtEmailBody" TextMode="MultiLine" style="width: 500px; height: 200px;" /></p>
        <p><small>HTML submissions are not allowed and they're encoded anyways so no point in spamming -not that you were going to of course!</small></p>
        <p><asp:Button runat="server" ID="btnSubmit" Text="Merge It!" OnClick="btnSubmit_Click" /></p>
        <asp:Literal ID="litOutput" runat="server" />
    </div>
    </form>
</body>
</html>

Reflection.aspx.cs

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Reflection;

public class TestObject
{
    private int __Id;
    private string __Name;
    private string __Email;
    private DateTime __JoinedDate;

    public int Id
    {
        get
        {
            return __Id;
        }
        set
        {
            __Id = value;
        }
    }
    public string Name
    {
        get
        {
            return __Name;
        }
        set
        {
            __Name = value;
        }
    }
    public string Email
    {
        get
        {
            return __Email;
        }
        set
        {
            __Email = value;
        }
    }
    public DateTime JoinedDate
    {
        get
        {
            return __JoinedDate;
        }
        set
        {
            __JoinedDate = value;
        }
    }

    public TestObject(int id, string name, string email, DateTime joinedDate)
    {
        __Id = id;
        __Name = name;
        __Email = email;
        __JoinedDate = joinedDate;
    }

    public bool GetPropertyValueByName(string propertyName)
    {
        object obj = null;
        return this.GetPropertyValueByName(propertyName, falseref obj);
    }

    public bool GetPropertyValueByName(string propertyName, ref object val)
    {
        return this.GetPropertyValueByName(propertyName, falseref val);
    }

    public bool GetPropertyValueByName(string propertyName, bool caseInsensitive, ref object val)
    {
        PropertyInfo p = null;
        BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

        //If it's a case-insensitive search then add the flag
        if (caseInsensitive)
            flags = flags | BindingFlags.IgnoreCase;

        p = this.GetType().GetProperty(
               propertyName,
               flags,
               null,
               null,
               Type.EmptyTypes,
               null);

        //Check the property exists and that it has read access
        if (p != null && p.CanRead)
        {
            //There is a property that matches the name, we can read it so get it
            val = this.GetType().InvokeMember(
                propertyName,
                BindingFlags.GetProperty | flags,
                null,
                this,
                null);

            //We return true as the user may just want to check that it exists
            return true;
        }

        return false;
    }
}

public partial class Reflection : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!Page.IsPostBack)
        {
            #region Create the example email body

            txtEmailBody.Text = "Dear {{Name}},\r\n\r\nThis is a test email that would be sent to the email address: {{Email}}.\r\n\r\n{{Name}} joined on: {{JoinedDate}}. This field should not be found {{Don't Find Me}}\r\n\r\nRegards,\r\n\r\nThe webmaster.";

            #endregion
        }
    }

    protected void btnSubmit_Click(object sender, EventArgs e)
    {
        if (Page.IsValid && !String.IsNullOrEmpty(txtEmailBody.Text))
        {
            litOutput.Text = "<h2>Output</h2>";

            #region Perform some basic tests
            litOutput.Text += "<h3>Perform some basic tests:</h3>";
            TestObject testObject = new TestObject(1"Tim""tim@email.com", DateTime.Today);

            object obj = null;
            if (testObject.GetPropertyValueByName("id"falseref obj))
                litOutput.Text += String.Format("<li>{0}</li>", obj);
            else
                litOutput.Text += "<li>Doesn't Exist</li>";

            if (testObject.GetPropertyValueByName("name"trueref obj))
                litOutput.Text += String.Format("<li>{0}</li>", obj);
            else
                litOutput.Text += "<li>Doesn't Exist</li>";

            if (testObject.GetPropertyValueByName("joineddate"trueref obj))
                litOutput.Text += String.Format("<li>{0}</li>", obj);
            else
                litOutput.Text += "<li>Doesn't Exist</li>";

            if (testObject.GetPropertyValueByName("nothere"trueref obj))
                litOutput.Text += String.Format("<li>{0}</li>", obj);
            else
                litOutput.Text += "<li>Doesn't Exist</li>";

            #endregion

            #region Create a collection and add a couple of items

            List<TestObject> testObjects = new List<TestObject>();
            testObjects.Add(new TestObject(1"Tim""tim@email.com", DateTime.Parse("01/02/2007")));
            testObjects.Add(new TestObject(2"Jim""jim@email.com", DateTime.Parse("20/02/2007")));
            testObjects.Add(new TestObject(3"John""john@email.com", DateTime.Parse("02/03/2007")));
            testObjects.Add(new TestObject(4"Gill""gill@email.com", DateTime.Parse("01/04/2007")));
            testObjects.Add(new TestObject(5"Bill""bill@email.com", DateTime.Parse("11/02/2007")));

            #endregion

            #region Do the work

            //Format it with <pre> for simplicity
            litOutput.Text += "<h3>Now for the reflection example:</h3><hr /><pre>";

            //Loop through the rows
            foreach (TestObject t in testObjects)
            {
                MatchEvaluator replaceField = delegate(Match m)
                {
                    //Get the property name (depending on your regex but
                    //mine groups the squigly brackets in there incase
                    //a match can't be found
                    string pName = m.Groups[2].ToString();

                    //Check it's not null
                    if (!String.IsNullOrEmpty(pName))
                    {
                        //Create an object that'll be returned from the method
                        object o = null;
                        //Check if that property exists, if it does return it
                        if (t.GetPropertyValueByName(pName, chkCaseSensitive.Checked, ref o))
                            return o.ToString();
                    }
                    //We've not found a match for the property in the object
                    //so return the match instead as it's probably a mistake
                    return m.Value;
                };

                //Create a new body as this'll be updated for each user
                string body = String.Empty;

                //Find the fields within the main body -this can be any of the properties of the object
                Regex r = new Regex(@"({{)(\w{0,15}?)(}})");
                body = r.Replace(txtEmailBody.Text, replaceField);
                //Output the example content (HtmlEncoded so not to hurt us!!)
                litOutput.Text += String.Format("{0}<hr />", Server.HtmlEncode(body));
            }

            litOutput.Text += "</pre>";

            #endregion
        }
    }
}

I’ve thrown up a quick demo if you want to test it out. I think in the longer run I’m going to look into having it generate some form of reporting system as that’d be seriously nice, but the suns out and I need to go for a paddle so that’ll have to wait for another day! So that's my first delve into reflection and so far I love it!

Friday, March 09, 2007 5:12:02 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  |