About this article
Written by: | Imar Spaanjaars |
Posted: | 9/14/2010 9:00 AM |
Page views: | 8475 |
Rate this article
About the author
Imar Spaanjaars is the owner of De Vier Koeden, a company specializing in consultancy and development on the Microsoft .NET platform and Dynamicweb.
Interested in custom development or consultancy on Dynamicweb or .NET? Then contact De Vier Koeden through the Contact page.
If you want to find out more about Imar Spaanjaars, check out his About page on this web site or check out his personal website.
Follow Imar on Twitter if you want to be notified of new articles in this Dynamicweb series.
Displaying Correct Add to and Remove from Favorites Links
The Custom Center of Dynamicweb eCommerce has a nice feature that enables users to add products to their list of favorites and to remove items from that list again. Using the Customer Center module, users can the quickly add products from their favorites list to the shopping cart. To enable the favorites feature you need to enable the Extranet module, provide the user with an option to log in, and then use the following code that displays an Add To and a Remove From favorites link:
<!--@If Defined(Ecom:Product.AddToFavorites)--> <a href="<!--@Ecom:Product.AddToFavorites-->">Add to favorites</a><br /> <a href="<!--@Ecom:Product.RemoveFromFavorites-->">Remove from favorites</a><br /> <!--@EndIf(Ecom:Product.AddToFavorites)-->
When you now click Add to favorites, the item is added to your list of favorites that you can see using the Custom Center module. Likewise, clicking the Remove from favorites link deletes the item again.
Expanding the Favorites Feature
This feature is pretty easy to implement, and makes it easy for a user to manage their favorites. There's only one downside: both links are always shown at the same time. There is currently no way to hide the Add link when the item is already on the Favorites list, and no way to hide the Remove link when the item is not. So, how can you handle this when your requirements state that only one link can be visible at any given time? Extenders and Subscribers to the rescue.
Using a ProductTemplateExtender and a NotificationSubscriber
To correctly display and hide the Add to and Remove from favorites links, you need to two pieces of Dynamicweb extensibility.
First, you need a ProductTemplateExtender that accomplishes the following tasks:
- Take the current user's ID
- Use that ID to determine if the user has an entry in the EcomCustomerFavoriteProducts table for the requested product and variant
- If the product has already been added to the list of favorites, hide the Add to link; otherwise, hide the Remove from link.
Since querying the database for each product can be a costly operation, it's a good idea to cache the favorites list for the user, so you don't have to hit the database for each product in each request. However, by caching you run the risk of stale data. That's where the NotificationSubscriber comes in.
You can hook up a NotificationSubscriber to the Standard.Page.Loaded action that runs when a page in Dynamicweb gets loaded. Inside the subscriber, you can determine if an item is being added to, or removed from the favorites list. When that happens, you clear the cached items that you set in the ProductTemplateExtender. This way, the cache always reflects the current state of the user's favorites list.
For this article, I am using the Microsoft ADO.NET Entity Framework to access the database. Clearly, you can choose whatever database technology you like best, be it the Entity Framework, Dynamicweb's own CRUD class generator, direct SQL and more.
In my example, I cache the data in the general ASP.NET cache, using the user's ID as a unique cache key. It's up to you to determine whether to store data in the general cache, in a user's session, or hit the database on each request, and temporarily store data in the HTTP Context so you can reuse it to display multiple products for a single request. The factors that influence this decision depend on the server (setup, hardware, configuration) and your user's profile (few users, many favorites, or many users with just a few favorites). You need to balance the pros and cons of each solution before you implement the final solution.
With a bit of the background done, it's time for some code.
Implementing the ProductTemplateExtender
A ProductTemplateExtender gets called every time a product needs to be displayed, whether this is a product in a list, or on a details page. As such it's an ideal location to modify properties like descriptions, stock or images of products. However, you can also use it to modify the Favorites links, as the templates for that feature are available as well.
To implement the ProductTemplateExtender, I carried out these steps:
- I added an ADO.NET Entity Model to my application. I hooked up the model to my Dynamicweb database and added the following two tables:
- EcomCustomerFavoriteLists
- EcomCustomerFavoriteProducts
Figure 1 - Next, I added a new ProductTemplateExtender class to my custom project with the following code:
public class ExtendAddToFavoritesTemplate : ProductTemplateExtender { public override void ExtendTemplate(Dynamicweb.Templatev2.Template Template) { int userId = new Extranet().UserID; string favoritesCacheKey = String.Format( Constants.CacheKeys.UserFavorites, userId); List<EcomCustomerFavoriteProduct> userFavorites = HttpContext.Current.Cache[favoritesCacheKey] as List<EcomCustomerFavoriteProduct>; if (userFavorites == null) { using (SamplesEntities db = new SamplesEntities()) { userFavorites = (from p in db.EcomCustomerFavoriteProducts where p.EcomCustomerFavoriteList.AccessUserID == userId select p).ToList(); HttpContext.Current.Cache[favoritesCacheKey] = userFavorites; } } var hasproduct = (from p in userFavorites where p.ProductID == Product.ID && p.ProductVariantID == Product.VariantID && p.ProductLanguageID == Product.LanguageID select p).Count() > 0; if (hasproduct) { Template.SetTag("Ecom:Product.AddToFavorites", string.Empty); } else { Template.SetTag("Ecom:Product.RemoveFromFavorites", string.Empty); } } }
Most of this code is pretty straightforward. First, I check if the ASP.NET cache contains a list of favorites for the current user. I use a cache key constant that I've defined in a separate class with constants. This makes it easier to reuse the same cache key later in the NotificationSubscriber. When the cache does not contain favorites for the current user, I use my SamplesEntities model to query all favorites from the database for the current user and store them in the cache.
No matter how I got the list of favorites, I can now check if that list contains an entry for the product that is currently being shown (which is made available through the Product property on the extender class). Notice how I am comparing the ProductID, ProductVariantID and the ProductLanguageID as the three properties together make up a unique product that can be added to the favorites list.
The final step is to determine which link to hide. When the product is already in the list, I clear out the Ecom:Product.AddToFavorites Dynamicweb template tag by setting it to an empty string. Otherwise, I do the same for the Ecom:Product.RemoveFromFavorites link. For this code to work, it's important that the product template contains an If Defined for both the Add and the Remove link; not just for the Add link as shown in the documentation. Otherwise, the text Remove from favorites text remains and you end up with an empty link:
<!--@If Defined(Ecom:Product.AddToFavorites)--> <a href="<!--@Ecom:Product.AddToFavorites-->">Add to favorites</a><br /> <!--@EndIf(Ecom:Product.AddToFavorites)-->
<!--@If Defined(Ecom:Product.RemoveFromFavorites)--> <a href="<!--@Ecom:Product.RemoveFromFavorites-->">Remove from favorites</a> <!--@EndIf(Ecom:Product.RemoveFromFavorites)-->
If you'd try out the code now, you'd notice that it seems to work fine. Depending on the current state of the product in the Favorites list, you either see the Add or the Remove link. However, as soon as you click that link, the product is added to or removed from the list of favorites, but the link stays the same. The reason for this is the list of favorites that is cached by the ProductTemplateExtender that I showed earlier.. The reason you need to cache the data it is that the ExtendTemplate method of the ProductTemplateExtender is called pretty often. You don't want to hit the database every time a product is displayed; especially not in list pages that show many products at once. However, the cached data becomes out of date as soon as you add or remove a favorite. So, you need to hook into that operation and clear the cache. Since there are no notification subscribers for favorites (hint to Dynamicweb: this would be a nice feature request), the best solution I came up with is to hook into Page.Loaded and then determine if a product is added to or removed from the list of favorites. To accomplish this, add the following code to a class in your custom solution:
[Subscribe(Standard.Page.Loaded)] public class OnFavoritesChanged : NotificationSubscriber { public override void OnNotify(string notification, object[] args) { string addFavorite = Base.Request("CCAddToFavorites"); string removeFavorite = Base.Request("CCRemoveFromFavorites"); if (string.IsNullOrEmpty(addFavorite) && string.IsNullOrEmpty(removeFavorite)) { return; } else { int userId = new Extranet().UserID; // User added or removed a favorite; clear the entire cache. // Alternatively, you could just update the selected product in the cached list. HttpContext.Current.Cache.Remove( String.Format(Constants.CacheKeys.UserFavorites, userId)); } } }
When a user changes the state of a product in the list of favorites, the request contains either a CCAddToFavorites or a CCRemoveFromFavorites entry. If one of the two contains a value, it means the state of a product has changed, and the cache must be cleared. The next time a product must be shown, the cache is rebuilt, and remains until a user changes their favorites again, or when .NET decides to eject the item from the cache.
Because the Customer Center uses the same keys to remove an item from the favorites list, the code in the NotificationSubscriber is triggered then as well, keeping the cache nicely in sync.
Caveats and Pitfalls
If you don't see the Add or Remove links show up altogether, make sure the Extranet module is enabled and that you're logged in. The favorites are stored using the user's ID so you need to be logged in as a valid user in order to add a product to your favorites.
Currently, the favorites feature doesn't play well with custom SEO URLs. This is a know issue, and will hopefully be resolved in the future. If you need the two to work together, you can probably build up your own Add and Remove links and append fields CCAddToFavorites, CCAddToFavoritesVariantID and CCAddToFavoritesLanguageID to the URL manually. I haven't tried this yet, but it should work in theory.
Finally, before implementing this solution, carefully evaluate your data access and caching strategy. The cache may not the best location to store user data, so you need to determine if it will work in your situation. You have multiple alternatives, including the user's session, and hitting the database once for each request (by storing the items in the HttpContext.Items property). For more details about the latter solution, check out the article: HttpContext.Items - a Per-Request Cache Store.
Downloads
You can download the code for the ProductTemplateExtender, the NotificationSubscriber and a sample template here.
Happy extending!