ASP.Net postback problem with ViewState in Safari on Windows 7

by Joe Payne 20. July 2012 21:27

Wow what a challenging issue to troubleshoot this week.  Here was the scenario:

AbleCommerce 7 client was reporting certain shoppers could not complete checkout.  The catch was, it was only happening on Safari and IE8 users.  But mostly Safari.  You’d get an error after entering the credit card information and couldn’t proceed to the receipt page.

The shoppers using Safari could reach checkout.  They could hit the 2nd page of checkout.  But after clicking the Pay with Credit Card button, nothing happened.  Scrolling to the top of the page reveals a new message on the page stating the basket contents had changed.  Repeated clicks resulted in the same.

We had done some recent enhancements to the checkout page.  But they were purely visual.  There weren’t any changes to the functionality of the page, just added images to the basketgrid and some improved CSS.  We certainly weren’t manipulating basket contents during checkout (Big NO NO), so we were baffled as to the cause.

We walked back through the version-control repository looking for every change in the last 6 months.  Nothing affecting the basket was found.

We crawled through the rendered page source looking for anything out of the ordinary.  Nothing stood out.  We couldn’t even reproduce the issue on our local developer copies of the site.

Finally, after reloading Safari on my Windows 7 64-bit PC, I got the issue to happen.  Bingo!  Now I can light up the issue through the debugger and see what the heck is going on.

In the checkout code, the message only renders if the basket contents in the previous postback are different from the current postback basket contents.  Able accomplishes this by generating a hash value of the basket and storing it in the page ViewState.

While walking through the code, I noticed the ShipmethodId on the the basket shipments was getting reset to 0.  Having a ShipMethodId of 0 means no shipping method assigned.  But in the initial page_load(), able sets a default shipping method to each shipment.  So how was this possible?

After walking a bunch of code with the visual studio debugger, I finally notice something totally bizarre.  After clicking the Continue button on Page 1 of checkout, Page.IsPostback() was reporting FALSE.  Do what??  How can it be false??  It’s the same page???

A breakpoint on the Page_Load() routine gave me the answer.  I could actually see Page_Load() getting hit twice in the same single postback.  The first time, Page.IsPostBack() reported True as it should.  But the second hit in Page_Load() reported False, like it was a brand new hit to the whole page!

So Safari was firing it’s own postback after the initial postback from the Continue button.  And this postback did not include ViewState.  Without ViewState, Page.IsPostback() reports false and thinks it’s a first-time load of the page.  A first time load of the page causes Able to reset the ShipMethodId on the basket shipments.  Thus the error on Page 2 of checkout about the basket changing…..

About the same time, I just happened to notice something odd about Page 2 of checkout.  The basket summary didn’t have any images.  I had paused the code in Page_Load() during the rogue postback.  Everything else on the page was rendered, but the images were missing.  We had indeed modified that basket summary to render images.  But it was simple change – just an evaluation expression calling a code-behind return that returns an image URL for the Container.DataItem.  no biggie right?

I commented out the <asp:Image> object in the basket summary, and suddenly the rogue postback stopped firing.  The checkout page started working exactly as it should.

Now I know the cause.  But I don’t know the reason.  So I did a little digging and learned something.

ASP.Net stores the viewstate in a HTML Hidden Field tag embedded in the HTML response.  Apparently, in certain circumstances, Safari has a size limit to hidden fields.  My guess is, somehow adding the images to the page caused the viewstate to grow beyond this hidden field size limit imposed by Safari.

I wish I knew more about it.  But what a crazy amount of time it took to troubleshoot this issue.  At least we got it figured out and the client website is back to working normal again.

Tags: , , , ,

AC7 Articles | Personal

AbleCommerce 3DS Implementation

by Joe Payne 22. June 2012 09:02

Did a great 3DS modification recently for a client.  Actually got it to work as a modal popup during checkout instead of requiring additional pages.  If the 3DS validation fails, the shopper returns to Checkout just like they should.

Took us a few weeks of live testing to work out the remaining kinks.  But those are gone now and the routines work rock-solid.

Tags: , ,

General News | Personal

How to add free item to the basket during checkout in AbleCommerce 7

by Joe Payne 26. March 2011 10:11

Introduction

 

This short tutorial describes a common need in AbleCommerce storefronts.  You are going to see line-by-line descriptions of the tasks required and how they are accomplished with my code.  At the end, the full source code is available free for you to use as you wish.  If you want to skip the entrée and go straight to the dessert, scroll to the bottom of this post.

 

The Need

 

You want to offer your shoppers a freebie when their order total exceeds a specific amount.  Currently AbleCommerce has coupons, but coupons don’t offer this particular functionality.  Plus coupons require the shopper to enter something.  In this case, we want the freebie to be completely automatic.

 

How It Works

 

These changes are heavily commented so you can walk through the experience yourself.    All of this work is done in the OnePageCheckout.ascx.cs file.   The code itself is loaded at the very beginning of the Page_Init() function.  Remember the entire code snippet is included at the bottom of this article.

 

The routine starts by making sure we’re not on a postback.  If we’re on a postback, that means all the remaining logic has already been executed in some previous page life cycle;  since the basket cannot be altered while within the checkout page itself, there’s no need to perform the free-item check again.

if (!Page.IsPostBack)
{

 

Next we set some variables we’re going to need for this modification.  The first variable _FreeProductId represents the ProductId value of the product you want to give away for free.  It’s important to remember that you should have this product marked as Hidden else shoppers will find it and try to buy it with every order. 

The second variable is our basket total threshold.  If the basket total (of products only) meets or exceeds this amount, it qualifies for the free item.

Feel free to adjust these values to suit your particular need.

// set the product ID of the free item we want to add
int _FreeProductId = 42;
 
// set the dollar amount threshold of the basket total we want
LSDecimal _MinBasketTotal = 1000;

 

Now we need to scan the basket.  In AC7, this is easily done with the Basket class object contained in the global Token instance.  We have to look through every item in the basket.  If it’s a product-type item, we add it’s dollar total to the grand total.  We also record whether the basket item is our free item i.e. is the free item already in the basket.

// build a basket total and scan the basket for existing free item
int _FreeItemBasketItemIndex = -1;
LSDecimal _BasketTotal = 0;
for (int x=0;x < Token.Instance.User.Basket.Items.Count;x++)
{
    // pull in the basket item object
    BasketItem _BasketItem = Token.Instance.User.Basket.Items[x];
 
    // if it's a product, add it to our total
    if (_BasketItem.OrderItemType == OrderItemType.Product)
        _BasketTotal += _BasketItem.ExtendedPrice;
 
    // if it's the free item, set the ID so we can check later if the free item
    // is already in the basket
    if (_BasketItem.ProductId == _FreeProductId)
        _FreeItemBasketItemIndex = x;
}

 

Now for the real work.  We have to determine if the basket even qualifies for a free item.  If it does, AND the free item doesn’t already exist in the basket, we have some work to do.  First we build a new BasketItem object called _FreeItem with a quantity 1.  Then we add the item to the current shoppers basket and save the basket.

//see if basket total exceeds threshold
if (_BasketTotal >= _MinBasketTotal)
{
    // Ok basket qualifies for the free item
    // see if the free item already exists in basket
    // if the _FreeItemBasketItemId value is still -1, a free item was never found in the
    // current basket
    if (_FreeItemBasketItemIndex == -1)
    {
        // create a basketitem object for this free product
        BasketItem _FreeItem = BasketItemDataSource.CreateForProduct(_FreeProductId, 1);
 
        // add the item to the basket and save it
        Token.Instance.User.Basket.Items.Add(_FreeItem);
        Token.Instance.User.Basket.Save();
    }
}

 

If the basket doe NOT qualify, then we still have a little work to do.  We need to remove any free item already in the basket.  Since we recorded the index of the free item earlier in the code, it becomes a quick and easy check here.

else
{
    // basket doesn't qualify, get rid of the free item if it exists
    if (_FreeItemBasketItemIndex > -1)
    {
        // remove the item and save basket
        Token.Instance.User.Basket.Items[_FreeItemBasketItemIndex].Delete();
        Token.Instance.User.Basket.Save();
    }
}
        }
ND MOD: AbleMods.com

So that’s it.  You’ve tested the basket, added the item if necessary and made sure to remove it if the basket doesn’t qualify.

 

Summary

 

This routine was written for AbleCommerce v7.0.6 but should work in practically any flavor.   To make it work for your site, simply add all the code provided to the beginning of the Page_Init() function in OnePageCheckout.ascx.cs file found in the /ConLib/ folder of your AbleCommerce 7 installation.

 

As always, back up any original files before making changes to them.  Nothing ruins a shoppers experience more than a broken checkout page.

 

Be sure to check out all my modules available at http://www.AbleMods.com/

 

Full Source Code

// BEGIN MOD: AbleMods.com
// 3/26/2011
// This modification will add a specific product (presumably priced at 0 so it's free) if
// the basket total exceeds a specified dollar amount.  Only products are counted towards the 
// basket total.
 
// total up products in the basket
if (!Page.IsPostBack)
{
    // set the product ID of the free item we want to add
    int _FreeProductId = 42;
 
    // set the dollar amount threshold of the basket total we want
    LSDecimal _MinBasketTotal = 1000;
 
    // build a basket total and scan the basket for existing free item
    int _FreeItemBasketItemIndex = -1;
    LSDecimal _BasketTotal = 0;
    for (int x=0;x < Token.Instance.User.Basket.Items.Count;x++)
    {
        // pull in the basket item object
        BasketItem _BasketItem = Token.Instance.User.Basket.Items[x];
 
        // if it's a product, add it to our total
        if (_BasketItem.OrderItemType == OrderItemType.Product)
            _BasketTotal += _BasketItem.ExtendedPrice;
 
        // if it's the free item, set the ID so we can check later if the free item
        // is already in the basket
        if (_BasketItem.ProductId == _FreeProductId)
            _FreeItemBasketItemIndex = x;
    }
 
    //see if basket total exceeds threshold
    if (_BasketTotal >= _MinBasketTotal)
    {
        // Ok basket qualifies for the free item
        // see if the free item already exists in basket
        // if the _FreeItemBasketItemId value is still -1, a free item was never found in the
        // current basket
        if (_FreeItemBasketItemIndex == -1)
        {
            // create a basketitem object for this free product
            BasketItem _FreeItem = BasketItemDataSource.CreateForProduct(_FreeProductId, 1);
 
            // add the item to the basket and save it
            Token.Instance.User.Basket.Items.Add(_FreeItem);
            Token.Instance.User.Basket.Save();
        }
    }
    else
    {
        // basket doesn't qualify, get rid of the free item if it exists
        if (_FreeItemBasketItemIndex > -1)
        {
            // remove the item and save basket
            Token.Instance.User.Basket.Items[_FreeItemBasketItemIndex].Delete();
            Token.Instance.User.Basket.Save();
        }
    }
}
// END MOD: AbleMods.com

Tags: , , ,

AC7 Articles

How to explode checkout basket items into separate shipments in AbleCommerce 7

by Joe Payne 25. March 2011 07:47

Need:  The project requires every item ordered from the AbleCommerce 7 storefront to be assigned it’s own shipment.  The reason is each item is made separately and must ship separately.  Even if multiple quantities of the same item are purchased, each unit of 1 must be by itself on a separate shipment.

 

Justification: Existing AC7 code will automatically split basket items up into separate shipments based on the WarehouseId value assigned to each product in the basket.  However this does not split up multiple quantities of the same item.  Even if an item is set as “Ships Separately” on the Edit Product page, AC7 will not break each quantity of the multi-quantity item into separate shipments.  Thus custom code is required.

 

Solution:  Create a new helper class in /App_Code/.  I called mine AbleModsHelper.Basket.cs.  Put the following code into the file and call the routine in the basket initialization area of OnePageCheckout.ascx.cs.  You’ll probably want to comment out the original Basket.Package() code so AbleCommerce libraries don’t mess up your shipments.

 

/// <summary>
/// Builds a shipment for each quantity of each product in the supplied basket
/// </summary>
/// <param name="_Basket">Basket to which shipments will be assigned</param>
public static void MakeBasketShipments(Basket _Basket)
{
    // Reset basket item shipment assignments 
    foreach (BasketItem _BasketItem in _Basket.Items)
    {
        _BasketItem.BasketShipmentId = 0;
        _BasketItem.Save();
    }
 
    // remove any shipments after shipment[0]
    if (_Basket.Shipments.Count > 1)
        for (int x = 1; x < _Basket.Shipments.Count; x++)
            _Basket.Shipments[x].Delete();
 
    // loop through basket items and make new shipments for each item and quantity ordered
    //BasketItemCollection _NewItems = new BasketItemCollection();
    int _ItemCount = _Basket.Items.Count;
    for (int _Count = 0; _Count < _ItemCount; _Count++)
    {
        BasketItem _BasketItem = _Basket.Items[_Count];
        // skip anything that's not a product or is already Qty 1
        if (_BasketItem.OrderItemType != OrderItemType.Product | _BasketItem.Quantity <= 1)
            continue;
 
        // we've got an item with multiple quantity - split it out using the original basketitem
        // record as a template.
        // first though, we must reset the new "master" product to qty 1
        int _LoopQty = _BasketItem.Quantity - 1;
        _BasketItem.Quantity = 1;
        _BasketItem.Save();
 
        // loop through remaining quantity of this item
        for (int x = 0; x < _LoopQty; x++)
        {
            // clone the original basket item
            BasketItem _NewItem = _BasketItem.Clone();
 
            // set it's quantity and basketid
            _NewItem.Quantity = 1;
            _NewItem.BasketId = _Basket.BasketId;
 
            // save the duplicate basketitem 
            _NewItem.Save();
 
            // clone any basket inputs from the original basket item
            foreach (BasketItemInput _BasketInput in _BasketItem.Inputs)
            {
                BasketItemInput _NewInput = new BasketItemInput();
                _NewInput.BasketItemId = _NewItem.BasketItemId;
                _NewInput.InputFieldId = _BasketInput.InputFieldId;
                _NewInput.InputValue = _BasketInput.InputValue;
                _NewInput.Save();
            }
 
            // add new item it to the basket
            _Basket.Items.Add(_NewItem);
            _Basket.Save();
        }
 
    }
 
    // clear out all shipments for this basket
    _Basket.Shipments.DeleteAll();
 
    // Sort the basket so the items are grouped together by name
    BasketItemCollection _BasketItems = _Basket.Items;
    _BasketItems.Sort("Name");
 
    // We have all basket items broken into separate lines
    // Create unique shipment for each basket item that doesn't already have a shipment assigned to it
    foreach (BasketItem _BasketItem in _BasketItems)
    {
        // skip basket item if not a product
        if (_BasketItem.OrderItemType != OrderItemType.Product)
            continue;
 
        // make new shipment
        BasketShipment _Shipment = new BasketShipment();
        _Shipment.BasketId = _Basket.BasketId;
        _Shipment.WarehouseId = _BasketItem.Product.WarehouseId;
        _Shipment.AddressId = Token.Instance.User.PrimaryAddress.AddressId;
        _Shipment.Save();
 
        // add new shipment to basket
        _Basket.Shipments.Add(_Shipment);
 
        // assign this basket item to the new shipment
        _BasketItem.BasketShipmentId = _Shipment.BasketShipmentId;
        _BasketItem.Save();
 
        //// reset the parentitemid value since now this item is its own basket item record
        _BasketItem.ParentItemId = _BasketItem.BasketItemId;
        _BasketItem.Save();
 
    }
 
    // save the basket
    _Basket.Save();
 
}

Tags: , , ,

AC7 Articles

Month List