nopCommerce – how we do themes

Posted by ben 10. December 2010 01:09

One question that is asked over and over again on the nopCommerce forums, is how to create a custom theme/skin.

In this post I want to share some tips and recommendations on how to create custom themes.

Have a game plan

Before you start doing anything, write a plan. Make a note of other sites that you like or specific UI features that you want to include. Use this as a reference throughout the design process.

Don’t just wade in without any kind of planning. You will fail.

Know your features

Take the time to get to know the application you want to theme. Make sure you understand what features are available and which ones you are using. Specifically:

1. Don’t design features that do not exist in nopCommerce (unless of course you intend to develop them)

2. If you are not going to use a feature, don’t waste your time styling it. For example, if you have no intention of using the forum feature in nopCommerce, then don’t style it.

Obviously if you are creating a theme for distribution, you may have to style every feature.

DO NOT COPY AN EXISTING THEME AND START CHANGING IT

The only way this is of any benefit, is if you have produced a design that looks remarkably like a default theme.

In most cases this will NOT be the case, so just don’t do it. It will take you much longer to change someone else’s CSS than it will to write your own.

Design your theme first

Use a program like Photoshop to design your theme (or get someone to design it for you). It’s much easier to design a theme using a graphics editing program than it is using HTML and CSS.

Just design the key pages

You don’t need to design every single page in nopCommerce. Typically we create designs for (and in this order):

Home page (this is where most of the master page layout will come from)
Product list page (used for category and manufacturer templates)
Product detail page

As you start building out the CSS for these pages you will end up with default styles that you can use for the other pages in the site.

Create a mock-up site

Do not jump from Photoshop straight into nopCommerce. ALWAYS create a mock-up site first.

We typically create an ASP.NET MVC site so we have complete control over the mark-up and the benefit of using Visual Studio. There is however, nothing stopping you from using any HTML editor.

It is much easier to style the HTML that you have written, than that of an existing application, especially a web forms app (like nopCommerce) where much of the HTML is encapsulated into controls.

I can not reiterate this step enough!

At this point you should have a mock-up site containing the key pages of an ecommerce web site; styled up according to your original design.

Source control is not just for code monkeys

Even if you are a front end developer / web designer, you can still benefit from source control. All of the CSS in your theme can be versioned, making it easy to roll back changes and collaborate on themes.

Take the time to read up on different source control solutions and their benefits (we use Mercurial) and make sure you commit regularly.

P.S. Copying, pasting, renaming your theme directory as mytheme-[revision] is not source control.

Create a blank theme

Don’t just take a copy of an existing theme. Create a new theme folder. You may wish to copy CSS from an existing theme but you should do this granularly (as you work through each key page / module in the site)

Captain slow wins the race

Take your time and work on one page/module at a time.

The first thing you will want to do is create and style your master page. Once done you will start to see your site come alive and you will get a warm fuzzy feeling inside.

Templates were meant for changing

The best thing about creating a mock-up site is that when you come to create the theme, you have already done most of the work.

When it comes to category/manufacturer/product templates, don’t waste your time trying to style the mark-up on the existing templates (which will likely be very different to that of your mock-up site).

Instead we just create our own templates, drop in our mock-up mark-up (say that 100 times :p) and replace the static content with the asp.net dynamic controls.

If changing existing modules makes your life easier, then do it

Don’t worry about not touching the code in existing modules. Some times it’s just quicker to change them.

This does mean that if you wish to upgrade the application, you will need to know what modules you changed…….and that’s why we use source control software.

A little bit of asp.net / c# knowledge will go a long way

I would love to tell you that you can create a nopCommerce theme with only knowledge of HTML and CSS. The simple fact is, unless you’ve worked with ASP.NET web forms before, you may struggle. If your theme is quite complex (or just very different from the defaults) then you may to move controls around and even write a bit of server side code.

So, take the time to learn about ASP.NET before you start working with it.

This is the process that works for us. If you do something different or have another tip/suggestion then leave a comment.

Tags:
Categories nopCommerce | Development

Using routing in a web forms application (nopCommerce)

Posted by ben 11. October 2010 22:29

Today I am going to cover using ASP.NET routing in a web forms application, nopCommerce.

Routing was originally introduced for ASP.NET MVC but it can also be used in a web forms application (click here for more details)

NopCommerce currently uses a URL rewriting library (UrlRewriting.Net) to map search engine friendly urls to physical files on disk. Today I will swap this out to use routing.

One thing I would like to achieve is to lose the entity ID from the URL. So instead of a URL like:

http://www.yourstore.com/products/nike-air-trainers-30.aspx (where 30 is the entity id)

we can have a URL like:

http://www.yourstore.com/products/nike-air-trainers

Since you must define your routing rules in advance, you should spend some time documenting your preferred URL design.

As you can see from the URL design I want to achieve, there are no entity identifiers in the URL. Instead we should use the slug (e.g. nike-air-trainers) to locate our entity.

I think the best way of doing this is to store the slug in our database. This keeps our queries simple and also provides a way of the user overriding the slug if they wish. It will also help keep our slugs unique (since we can just define a unique constraint on the field).

I’m not going to go through the process of adding new properties to an entity (see this post for details). Effectively we add a new string property “Slug” to category, product and manufacturer.

I’ve written an extension method that I use to generate a slug from the entity title:

        public static string ToSlug(this string source) {
            return source.ToSlug(int.MaxValue);
        }
        
        public static string ToSlug(this string source, int maxLength) {
            string str = source.ToLower();

            str = Regex.Replace(str, @"[^a-z0-9\s-]", "");
            str = Regex.Replace(str, @"\s+", " ").Trim(); 
            str = str.Substring(0, str.Length <= 45 ? str.Length : 45).Trim();
            str = Regex.Replace(str, @"\s", "-"); 

            return (str.Length > maxLength) ? str.Substring(0, maxLength) : str;
        }

Rather than relying on the database unique constraint, in our implementation we created an ISlugValidator interface so we can choose how to validate our slugs:

    public interface ISlugValidator {
        string ValidateSlug(string slug, int key, IDictionary<int, string> existingSlugs);
    }		

This effectively validates a slug against a dictionary of existing slugs. In our implementation we just append a number to the end of the slug if a duplicate exists.

When we create or update an entity (e.g. product) we call the following extension method on SEOHelper:

        public static string GenerateProductSlug(string productName, int productId){
            return Validator.ValidateSlug(productName.ToSlug(100), productId, ProductManager.GetProductSlugs());
        }

This will work if you pass in a product name (creating a product) or the existing slug (updating a product):

image

Okay, so we are now storing the slug for product, category and manufacturer.

Now we need to think about how we are going to retrieve our entities based on the slug. There are generally two approaches to doing this:

1. Create a GetEntityBySlug method and replace the existing calls to GetEntityById with this

2. Provide a means of retrieving an entity identifier from a slug

We chose the second option since it required much less refactoring and we felt this would offer better performance.

To achieve this we create an index of product identifiers and slugs using a Linq projection (equivalent to SELECT ProductID, Slug FROM Nop_Product). We cache the results and update the cache whenever a product is updated (similar methods are used for category and manufacturer). This is the code we use for retrieving and caching the results):

        public static IDictionary<int, string> GetProductSlugs() {
            return Cache.Get(product_slugs_key, 30, ()=> {
                var db = ObjectContextHelper.CurrentObjectContext;
                return (from p in db.Products
                        select new {
                            Id = p.ProductId,
                            Slug = p.Slug
                        }).ToDictionary(x => x.Id, x => x.Slug);
            });          
        }

To retrieve a product id based on a slug:

        public static int GetProductIdFromSlug(string slug) {
            int result = 0;
            if (GetProductSlugs().Values.Contains(slug.ToLower()))
                result = GetProductSlugs().Single(x => x.Value == slug.ToLower()).Key;
            return result;
        }

To update the dictionary when a product changes (we are using the EventContext) is simple:

        public static void UpdateSlug(Product product) {
            GetProductSlugs()[product.ProductId] = product.Slug;
        }

So let’s have a quick recap. We now have methods of generating our slugs and getting an entity identifier from a slug.

Now we need to set up routing.

The first thing you need to do is disable the existing url rewriter. To do this you can either comment out or remove the UrlRewriteModule declarations in web.config (these will be under System.Web/HttpModules and System.WebServer/Modules).

To set up routing, open up global.asax and add the following code:

        void RegisterRoutes(RouteCollection routes) {

            routes.Ignore("{resource}.axd/{*pathInfo}");
            routes.Ignore("{*favicon}", new { favicon = @"(.*/)?favicon.ico(/.*)?" });

            /* products */
            routes.MapPageRoute("product-details","products/{product}","~/product.aspx");
        }

        protected void Application_Start(object sender, EventArgs e) {
            
            RegisterRoutes(RouteTable.Routes);
            
            // Code that runs on application startup
            NopConfig.Init();
			...
        

For more information on configuring routes click here.

Here I am just displaying the route configuration for products. The routes for category and manufacturer will be very similar.

We need to update our pages/modules to get the slug from route data instead of a querystring.

I created a few more helper extension methods for working with RouteData:

        public static bool HasRouteValue(this Page page, string key) {
            return page.RouteData.Values.ContainsKey(key);
        }

        public static T GetRouteValue<T>(this Page page, string key) {
            T result = default(T);
            if (page.HasRouteValue(key)) {
                CommonUtils.TryParse<T>(page.RouteData.Values[key].ToString(), out result);
            }
            return result;
        }

Now we can simply replace our existing Request.QueryString code with:

        public int ProductId {
            get { return ProductManager.GetProductIdFromSlug(this.GetRouteValue("product"));}
        }

You will need to update all the modules / pages used to display categories, manufacturers and products to return the entity id in this way.

Finally we need to update SEOHelper to generate our outbound links using our routing configuration:

        public static string GetProductUrl(Product product)
        {
            if (product == null)
                throw new ArgumentNullException("product");
            var parameters = new RouteValueDictionary {
                { "product", product.Slug }
            };
            var cpd = RouteTable.Routes.GetVirtualPath(null, "product-details", parameters);
            return cpd.VirtualPath;
        }

The nice thing about this is we can change our url design in global.asax and all our outbound links will be automatically updated.

That’s it!

image

Tags: ,
Categories nopCommerce | C# | Development | ASP.NET

How to extend nopCommerce

Posted by ben 20. September 2010 23:58

Please note that this article applies to nopCommerce 1.80 only.

Since nopCommerce swapped out it’s existing provider based data access layer with the entity framework, there have been quite a few posts on the nopCommerce forums regarding how to extend nopCommerce.

In this post I am going to cover two of the most common requests

1) How to add a new property to an existing entity

2) How to add a new entity

Adding a new property to an existing entity

The process for this is very simple.

  1. Add your new property to the relevant class
  2. Update repository methods (insert/update)
  3. Add the field to your database table
  4. Update the Entity Framework model (edmx file)

In this example, I’m going to add a new property “Stylesheet” to my categories, that allows me to assign a specific CSS style sheet to each category.

So to begin with we should add our property. Rather than adding the property to the existing Category.cs file, I recommend you keep your extensions separate. Since the entity classes in nopCommerce are all partial classes, we can put our extension in a separate file.

image

We create our Category extension class in our extensions directory. We need to make sure that it sits in the same namespace as the existing category class so that the two are compiled together:

namespace NopSolutions.NopCommerce.BusinessLogic.Categories
{
    /// 
    /// Represents a category
    /// 
    public partial class Category : BaseEntity {
        public string Stylesheet { get; set; }
    }
}

Now that we have added our new property, we need to update the repository (or manager) insert and update methods accordingly (in the “adding a new entity” example, you will see my preferred approach for doing this).

We open up CategoryManager and find the insert and update methods, changing the method signatures like so:

        public static Category InsertCategory(string name, string description,
            int templateId, string metaKeywords, string metaDescription, string metaTitle,
            string seName, int parentCategoryId, int pictureId,
            int pageSize, string priceRanges, bool showOnHomePage, bool published, bool deleted,
            int displayOrder, DateTime createdOn, DateTime updatedOn, string stylesheet) { ... }

        public static Category UpdateCategory(int categoryId, string name, string description,
            int templateId, string metaKeywords, string metaDescription, string metaTitle,
            string seName, int parentCategoryId, int pictureId,
            int pageSize, string priceRanges, bool showOnHomePage, bool published, bool deleted,
            int displayOrder, DateTime createdOn, DateTime updatedOn, string stylesheet) { .... }

Note the additional variable “stylesheet”.

Of course now that we have changed these methods we need to update any code that references them. The easiest way of doing this is to right click the method and select “Find all references” from the context menu:

image

In this case we just need to update the CategoryInfo control used in administration (we will add a new textbox for our stylesheet property and use that to set the property).

With that done our application should compile (F6). Now it’s time to update our Entity Framework model.

We need to add our new field to the Nop_Category table. You can do this in Visual Studio, or (as I prefer) using SQL Server Management Studio:

image

Note that I am setting “Allow Nulls” to true. You will need to do this if the table already contains data. If you do not want to allow nulls you will need to run an update script to set a default value for your new property for all existing records.

With our database changes made, open up NopModel.edmx (inside Nop.BusinessLogic/Data).

Right click on the designer surface and select “Update Model from Database”. If it’s the first time you have done this you may need to set up the connection to your database:

image

With that done, you should see our new “Stylesheet” property on the category entity:

image

Note – the entity framework table mapping is case sensitive. If the case differs between entity and database (e.g. CategoryId <> CategoryID) you need to right click on the entity property within the edmx designer and select “Table Mapping”. You can then ensure that your properties are mapped correctly:

image

Now that we have updated our entity framework model, we need to return to our Insert and Update methods in CategoryManager and persist our “stylesheet” variable:

image

Now let’s look at adding a new entity.

Adding a new entity

In this example I am going to add a new “Banner” entity to nopCommerce since I would like to manage my site’s promotional banners from the nopCommerce administration interface.

I prefer to start with my domain, so let’s create a new class “Banner” and put it in our extensions directory.

namespace NopSolutions.NopCommerce.BusinessLogic.Extensions {
    
    public class Banner {
        public int BannerId { get; set; }
        public string Name { get; set; }
        public string Content { get; set; }
    }

}

As you can see, our class is quite simple.

Now let’s add our new Banner table to the nopCommerce database. Personally I don’t like prefixing my table names (you should use schemas for that) but I will use the existing convention in this example i.e. “Nop_Banner”:

image

Note that my database fields are named slightly differently to the properties on our class. We will need to update the mappings after updating our entity framework model.

Open up NopModel.edmx, right click on the designer and select “Update Model from Database”.

On the “Add” tab, we select the new table and click “Finish”:

image

If you scroll up to the top left of the designer you will see the Entity Framework entity that has been created:

image

Let’s rename this to “Banner” (since that’s what our class is called) and rename our properties to match those on our class:

image

Since we are not using the entity framework generated object context in nopCommerce, we need to update our NopObjectContext to include our new entity. Again, since this is a partial class, we can stick it in our “Extensions” directory:

namespace NopSolutions.NopCommerce.BusinessLogic.Data
{
    /// <summary>
    /// Represents a nopCommerce object context
    /// </summary>
    public partial class NopObjectContext : ObjectContext {
        private ObjectSet<Banner> _banners;
        
        public ObjectSet<Banner> Banners {
            get  {
                if ((_banners == null))
                    _banners = CreateObjectSet<Banner>();
                return _banners;
            }
        }
    }
}

This gives our object context a “Banners” property referencing the banner records in our database.

All that’s left to do is create our repository class “BannerManager”. I take a more domain driven approach than the existing manager classes in nopCommerce. We just have one method for update and insert and rather than passing in a variable for each property, we just take in the object itself. When it comes to making changes to our domain model this makes like much easier (if you’ve ever added any properties to Order, you will know what I’m talking about).

Our complete BannerManager class can be seen below.

namespace NopSolutions.NopCommerce.BusinessLogic.Extensions {
    public class BannerManager {
        
        public Banner GetBannerById(int bannerId) {
            var db = ObjectContextHelper.CurrentObjectContext;
            return db.Banners.SingleOrDefault(x => x.BannerId == bannerId);
        }

        public IList<Banner> GetAllBanners() {
            var db = ObjectContextHelper.CurrentObjectContext;
            return db.Banners.OrderBy(x => x.Name).ToList();
        }
        
        public void Save(Banner banner) {
            var db = ObjectContextHelper.CurrentObjectContext;

            if (banner.BannerId == 0) { 
                // new banner    
                db.Banners.AddObject(banner);
            } else {
                // existing banner
                if (!db.IsAttached(banner))
                    db.Banners.Attach(banner);
            }

            db.SaveChanges();
        }
    }
}

I’m not doing any caching here. You can look at one of the existing manager classes for an example of that.

As you can see, entity framework has made extending nopCommerce much easier.

Have fun!

Tags: ,
Categories ASP.NET | C# | Development | Entity Framework | nopCommerce