As a simple exercise, we added dynamic AJAX pricing to all product Page XmlPackages. This thread shows the mods we had to do, so it could be used for those developers customizing the cart in similar areas. This feature will show up out of the box also in future builds.
The goal: As a size or color option is changed, if it has a price modifier on it, change the actual price shown on the product page.
The complications: support all pricing models (regular, sale, extended, customer levels, wholesale, retail), multi-lingual fields and prompts, and multiple currencies, and still honor VAT inclusive and exclusive display mods.
Chosen solution: Add AJAX callback with onChange handlers to Size & Color options on the product add to cart forms.
So first, we had to add this onChange handlers to the Size & Color select lists on the addtocart forms (in ShoppingCart.cs):
Code:
...onChange=\"if(typeof(getPricing) == 'function'){getPricing(" + ProductID.ToString() + "," + VariantID.ToString() + ")}\"...
We then added a new getPricing routine to our /jscripts/revAjax.js AJAX handler:
Code:
function getPricing(ProductID,VariantID)
{
//alert('VariantID=' + VariantID);
if(ProductID == undefined || VariantID == undefined)
{
return;
}
var ChosenSize = "";
var ChosenSizeList = document.getElementById('Size');
if(ChosenSizeList != undefined)
{
ChosenSize = ChosenSizeList.options[ChosenSizeList.selectedIndex].text;
}
var ChosenColor = "";
var ChosenColorList = document.getElementById('Color');
if(ChosenColorList != undefined)
{
ChosenColor = ChosenColorList.options[ChosenColorList.selectedIndex].text;
}
var url = "ajaxPricing.aspx?ProductID=" + ProductID + "&VariantID=" + VariantID + "&size=" + escape(ChosenSize) + "&color=" + escape(ChosenColor);
//alert("Ajax Url=" + url);
makeHttpRequest(url,undefined,'pricing');
}
and updated the loadXml routine to handle the new 'pricing' callbacks:
Code:
function loadXML(xml,calltype)
{
if(calltype == 'shipping')
{
var string = '';
var root = xml.getElementsByTagName('Shipping')[0];
for (i = 0; i < root.childNodes.length; i++)
{
var node = root.childNodes[i].tagName;
string += root.getElementsByTagName(node)[0].childNodes[0].nodeValue + "<br />";
}
if (document.getElementById('ShipQuote'))
{
document.getElementById('ShipQuote').innerHTML = string;
}
}
if(calltype == 'pricing')
{
var prnode = xml.getElementsByTagName('PriceHTML')[0];
var variantnode = xml.getElementsByTagName('VariantID')[0];
var NewPrice = "Not Found";
var VariantID = "0";
if(prnode != undefined)
{
NewPrice = xml.getElementsByTagName('PriceHTML')[0].firstChild.data
}
if(variantnode != undefined)
{
VariantID = xml.getElementsByTagName('VariantID')[0].firstChild.data
}
//alert("VariantID=" + VariantID + ", NewPrice=" + NewPrice);
if (document.getElementById('VariantPrice_' + VariantID))
{
document.getElementById('VariantPrice_' + VariantID).innerHTML = NewPrice;
}
}
}
We then added this new HTML container around all pricing output in the product XmlPackages:
Code:
<span>
<xsl:attribute name="id">VariantPrice_<xsl:value-of select="VariantID"/></xsl:attribute>
<xsl:value-of select="aspdnsf:GetVariantPrice(VariantID, number(HidePriceUntilCart), Price, SalePrice, ExtendedPrice, Points, $pSalesPromptName, TaxClassID)" disable-output-escaping="yes" />
</span>
the only new part in that is the <span> tag so we can find the text to update that contains the price on the page. Each package is a little different, but the span elements are almost identical.
Now the harder part, how to compute the price and factor in all those requirements (VAT INC, EX, Sale Prices, customer level wholesale pricing, multi-lingual, string resource changes to the sales prompts, and price prompts, etc) WITHOUT having to reinvent all that logic.
We had to use the GetVariantPrice routine in XSLTExtensionBase.cs. That is the heavy lifter, and we don't want to reinvent all that stuff. To do this, we therefore added a new overload in XSLTExtensionbase.cs, to take in a "chosen attribute" price modifier:
Code:
public virtual string GetVariantPrice(String sVariantID, String sHidePriceUntilCart, string sPrice, string sSalePrice, string sExtPrice, String sPoints, string sSalesPromptName, String sShowpricelabel, string sTaxClassID, String sChosenAttributesPriceDelta)
{
...
Decimal ChosenAttributesPriceDelta = IV.ValidateDecimal("AttributesPriceDelta", sChosenAttributesPriceDelta);
StringBuilder results = new StringBuilder(1024);
Price += ChosenAttributesPriceDelta;
..
}
That's the only logic change we added to the new overload, and we now have the other routines calling this new "master" pricing routine, passing "0.00" as a value for attribute price deltas. That means, all prior XmlPackages will still work unchanged.
The final piece was to add "ajaxPricing.aspx/cs" to the solution, which is the actual callback doing the work.
Code:
<%@ Page language="c#" Inherits="AspDotNetStorefront.ajaxPricing" CodeFile="ajaxPricing.aspx.cs" %>
nothing really needed there, we're just sending back a small Xml doc to the ajax js handler.
Next we just implement that page as a thin call to the existing XSLTExtensionBase routines, and we are pretty much done:
Code:
// ------------------------------------------------------------------------------------------
// Copyright AspDotNetStorefront.com, 1995-2007. All Rights Reserved.
// http://www.aspdotnetstorefront.com
// For details on this license please visit the product homepage at the URL above.
// THE ABOVE NOTICE MUST REMAIN INTACT.
// ------------------------------------------------------------------------------------------
using System;
using System.Web;
using System.Data;
using System.Xml;
using System.Text;
using System.Text.RegularExpressions;
using System.Globalization;
using AspDotNetStorefrontCommon;
namespace AspDotNetStorefront
{
/// <summary>
/// Summary description for search.
/// </summary>
public partial class ajaxPricing : System.Web.UI.Page
{
protected void Page_Load(object sender, System.EventArgs e)
{
Customer ThisCustomer = ((AspDotNetStorefrontPrincipal)Context.User).ThisCustomer;
int ProductID = CommonLogic.QueryStringUSInt("ProductID");
int VariantID = CommonLogic.QueryStringUSInt("VariantID");
if (ProductID == 0)
{
ProductID = AppLogic.GetVariantProductID(VariantID);
}
String ChosenSize = Regex.Replace(CommonLogic.QueryString("Size"), "\\s+", "");
String ChosenSizeSimple = String.Empty;
String ChosenColor = Regex.Replace(CommonLogic.QueryString("Color"), "\\s+", "");
String ChosenColorSimple = String.Empty;
Decimal SizePriceModifier = System.Decimal.Zero;
Decimal ColorPriceModifier = System.Decimal.Zero;
if (ChosenSize.IndexOf("[") != -1)
{
int i1 = ChosenSize.IndexOf("[");
int i2 = ChosenSize.IndexOf("]");
if (i1 != -1 && i2 != -1)
{
SizePriceModifier = Localization.ParseNativeDecimal(ChosenSize.Substring(i1 + 1, i2 - i1 - 1).Replace("$","").Replace("+","").Replace("-",""));
}
ChosenSizeSimple = ChosenSize.Substring(0, i1);
}
if (ChosenColor.IndexOf("[") != -1)
{
int i1 = ChosenColor.IndexOf("[");
int i2 = ChosenColor.IndexOf("]");
if (i1 != -1 && i2 != -1)
{
ColorPriceModifier = Localization.ParseNativeDecimal(ChosenColor.Substring(i1 + 1, i2 - i1 - 1).Replace("$", "").Replace("+", "").Replace("-", ""));
}
ChosenColorSimple = ChosenSize.Substring(0, i1);
}
Decimal FinalPriceDelta = SizePriceModifier + ColorPriceModifier;
String NewPRText = "Not Available";
XSLTExtensionBase xslte = new XSLTExtensionBase(ThisCustomer, ThisCustomer.SkinID);
IDataReader rs = DB.GetRS("select p.*, pv.VariantID, pv.name VariantName, pv.Price, pv.Description VariantDescription, isnull(pv.SalePrice, 0) SalePrice, isnull(SkuSuffix, '') SkuSuffix, pv.Dimensions, pv.Weight, isnull(pv.Points, 0) Points, sp.name SalesPromptName, case when pcl.productid is null then 0 else isnull(e.Price, 0) end ExtendedPrice FROM Product p join productvariant pv on p.ProductID = pv.ProductID join SalesPrompt sp on p.SalesPromptID = sp.SalesPromptID left join ExtendedPrice e on pv.VariantID=e.VariantID and e.CustomerLevelID=" + ThisCustomer.CustomerLevelID.ToString() + " left join ProductCustomerLevel pcl with (NOLOCK) on p.ProductID = pcl.ProductID and pcl.CustomerLevelID = " + ThisCustomer.CustomerLevelID.ToString() + " WHERE p.ProductID = " + ProductID.ToString() + " and p.Deleted = 0 and pv.Deleted = 0 and p.Published = 1 and pv.Published = 1 ORDER BY p.ProductID, pv.DisplayOrder, pv.Name");
if (rs.Read())
{
NewPRText = xslte.GetVariantPrice(VariantID.ToString(),
CommonLogic.IIF(DB.RSFieldBool(rs, "HidePriceUntilCart"), "1", "0"),
Localization.DecimalStringForDB(DB.RSFieldDecimal(rs, "Price")),
Localization.DecimalStringForDB(DB.RSFieldDecimal(rs, "SalePrice")),
Localization.DecimalStringForDB(DB.RSFieldDecimal(rs, "ExtendedPrice")),
DB.RSFieldInt(rs, "Points").ToString(),
DB.RSField(rs, "SalesPromptName"),
"1",
DB.RSFieldInt(rs, "TaxClassID").ToString(),
Localization.DecimalStringForDB(FinalPriceDelta));
}
rs.Close();
Response.Expires = -1;
Response.ContentType = "application/xml";
XmlWriter writer = new XmlTextWriter(Response.OutputStream, System.Text.Encoding.UTF8);
writer.WriteStartDocument();
writer.WriteStartElement("root");
writer.WriteElementString("CustomerID", ThisCustomer.CustomerID.ToString());
writer.WriteElementString("CustomerLevelID", ThisCustomer.CustomerLevelID.ToString());
writer.WriteElementString("ProductID", ProductID.ToString());
writer.WriteElementString("VariantID", VariantID.ToString());
writer.WriteStartElement("Size");
writer.WriteElementString("Input", ChosenSize);
writer.WriteElementString("Text", ChosenSizeSimple);
writer.WriteElementString("Delta", Localization.DecimalStringForDB(SizePriceModifier));
writer.WriteEndElement();
writer.WriteStartElement("Color");
writer.WriteElementString("Input", ChosenColor);
writer.WriteElementString("Text", ChosenColorSimple);
writer.WriteElementString("Delta", Localization.DecimalStringForDB(ColorPriceModifier));
writer.WriteEndElement();
writer.WriteElementString("ChosenAttributesPriceDelta", Localization.DecimalStringForDB(FinalPriceDelta));
writer.WriteElementString("PriceHTML", NewPRText);
writer.WriteEndElement();
writer.WriteEndDocument();
writer.Flush();
}
}
}
There were a few other minor mods, but that's about it...Hope this gives others insight on how we do customizations/new feature additions.
Maybe next up is our AJAX add to cart handler