nopCommerce SuperAdmin and Access Control

The nopCommerce access control is fantastic and it achieves its purpose fully. If for example, me as the ultimate administrator doesn’t want the Staff users to see my PalPal configuration settings, I can simply disable the access from the list and they will get an access denied message when they try that link. However, there are cases where us as a consultant / developer we want to refrain the users (owner or staff) from seeing some of the available features for various reasons – maybe you didn’t sell that to them, maybe that’s not applicable to their business (the simpler the application the better). It could be sometimes confusing being a business owner but got denied access to part of your website. In addition, in order to handle such “exceptions” you need good documentations and overtime it just increases your maintenance costs. Hence I am adding a SuperAdmin property to the Customer class and also a custom configuration setting in web.config to achieve the hiding of some features. The reason for adding this into the web.config is to help ourselves remember what we’ve done in the future.

First in ~/Libraries/Nop.BusinessLogic/Customer/Customer.cs I am adding a public property called IsSuperAdmin.

/// <summary>
/// Gets a boolean indicating if this custom is a Super Admin (CustomerRoleId == 1)
/// </summary>
public bool IsSuperAdmin
{
    get
    {
        if (this.CustomerRoles.SingleOrDefault(t => t.CustomerRoleId == 1) != null)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

It’s defining the CustomerRold with ID = 1 as the “Super Admin” role, which means that you will need to make sure in the database that the one with ID = 1 is indeed the customer role you want to have the ultimate access. Ideally there should be a column in the Customer table for this flag, but I think this is the easiest way without modifying the data structure.

Next I will add a new section to NopConfig in web.config, which I am calling AdminSettings (Of course you could name it as whatever makes most sense to you).

<NopConfig>
    …            <AdminSettings>        <AdminSetting key="CategorySEOTabVisibility" value="false" />    </AdminSettings></NopConfig>

The NopConfig class handles the custom config section <NopConfig> in web.config. So I am adding a property called AdminSettings in ~/Libraries/BusinessLogic/Configuration/NopConfig.cs to store this new </AdminSettings> config section.

private static Dictionary<string, string> _adminSettings = null;
public static Dictionary<string, string> AdminSettings
{
    get { return _adminSettings; }
}

The ideal place to initialize this property is in the Create method. If you follow the code path, you can see that these config settings are initialized in global.asax.

public object Create(object parent, object configContext, XmlNode section){
    // Other code
    XmlNode adminSettingNodes = section.SelectSingleNode("AdminSettings");
    if (adminSettingNodes != null)
    {
        _adminSettings = new Dictionary<string, string>();
        foreach (XmlNode pdNode in adminSettingNodes.ChildNodes)
        {
            if (string.Compare(pdNode.Name.Trim(), "adminsetting", true) == 0)
            {
                _adminSettings.Add((string)pdNode.Attributes["key"].Value, (string)pdNode.Attributes["value"].Value);
            }
        }
    }
    return null;
}

 

With all of these in place, we can start to hide out some of the components in the admin pages. For example, if we wish to hide the SEO tab in the category editing page. We will use the following line in Page_Load of the CategoryDetailsControl class (~/Administration/Modules/CategoryDetails.ascx.cs)

pnlCategorySEO.Enabled = pnlCategorySEO.Visible = ((!NopConfig.AdminSettings.ContainsKey("CategorySEOTabVisibility")) || Boolean.Parse(NopConfig.AdminSettings["CategorySEOTabVisibility"]) || NopContext.Current.User.IsSuperAdmin);

For another instance, if we wish to not hide the “Customer Enter Price” checkbox in product variant editing page because the business just will never allow customers to enter their own price, we can do the following in ~/Administration/Modules/ProductVariantInfo.ascx.

<% if (!NopSolutions.NopCommerce.BusinessLogic.Configuration.NopConfig.AdminSettings.ContainsKey("ProductVariantCustomerEnterPriceCheckboxVisibility") ||
           Boolean.Parse(NopSolutions.NopCommerce.BusinessLogic.Configuration.NopConfig.AdminSettings["ProductVariantCustomerEnterPriceCheckboxVisibility"]) ||
           NopContext.Current.User.IsSuperAdmin)
       {%>
    <tr>
        <td class="adminTitle">
            <nopcommerce:tooltiplabel runat="server" id="lblCallForPrice" text="<% $NopResources:Admin.ProductVariantInfo.CallForPrice %>"
                tooltip="<% $NopResources:Admin.ProductVariantInfo.CallForPrice.Tooltip %>" tooltipimage="~/Administration/Common/ico-help.gif" />
        </td>
        <td class="adminData">
            <asp:CheckBox ID="cbCallForPrice" runat="server" Checked="False"></asp:CheckBox>
        </td>
    </tr>
<%} %>

 

Hope this is useful! Feedbacks are welcome and maybe better ways to achieve such!

Use ASP.NET 4 URL Routing in nopCommerce 2

The 9 steps in the previous post should have gotten the re-writing working, but there are also more that needs to be refined. The biggest change in the previous post was the use of SEName to retrieve category as opposed to CategoryId. This creates an additional manual step as when you insert a new category you need to be sure to also input “Search engine friendly page name” in the SEO tab, otherwise that rewriting won’t work.

The original rewrite is already using a good method to generate nice SEO friendly names, why don’t we leverage that and always also insert the SEName when a new category is added? Only one thing to do:

In NopCommerceStore.Administration.Modules.CategoryAdd.ascx.cs, which contains the 4 tabs used in creating a new category, before save to database happens we get the Name field in the Category Info tab, call the helper function to generate the SEO friendly name, and fill in the SEName textbox in the SEO tab. I have to say, FindControl is so ASP.NET WebForm, haha…

protected Category Save()
{
    SimpleTextBox nameTextBox = ((SimpleTextBox)ctrlCategoryInfo.FindControl("txtName"));
    if (nameTextBox != null)
    {
        string catName = nameTextBox.Text;
        TextBox seNameTextBox = ((TextBox)ctrlCategorySEO.FindControl("txtSEName"));
        if (seNameTextBox != null)
        {
            string seName = seNameTextBox.Text;
            if (string.IsNullOrWhiteSpace(seName))
            {
                seNameTextBox.Text = NopSolutions.NopCommerce.BusinessLogic.SEO.SEOHelper.GetSEName(catName);
            }
        }
    }

Further to this topic I guess what we could do is to ensure that the SEName column is always unique. That’s a bigger change to the Database and the Entity context.

Use ASP.NET 4 URL Routing in nopCommerce

Note: in this discussion the nopCommerce version is 1.90 and it addresses only the product category page, not the product details page. In 1.90 the old Manager classes were re-named to Service classes, but beside such naming change, most of the inside logics remain intact (as far as this topic is concerned).

nopCommerce by default uses the UrlRewriting.NET solution and it actually works wonderfully. The only thing that I don’t like about was having to include the CategoryID in the URL. For instance, if URLRewriting is turned on (you can do so in Admin>Global Settings>SEO), you get ~/category/{id}-{sename}.aspx as your URL (Or whatever format you prefer, just that you can’t avoid having the {id} in the URL, which seems annoying to me.)

We can, however, construct a nice, SEO-friendly URL step by step:

1. Add the UrlRoutingModule to web.config

2. Disable the UrlRewriteModule from web.config

3. Make sure runAllManagedModulesForAllRequests is set to true

Here’s the <system.webServer> node in web.config highlighting steps 1 – 3

<system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <modules runAllManagedModulesForAllRequests="true">
        <remove name="NopCommerceFilter" />
        <remove name="UrlRewriteModule" />
        <add name="MembershipHttpModule" preCondition="managedHandler" type="NopSolutions.NopCommerce.BusinessLogic.Profile.MembershipHttpModule, Nop.BusinessLogic" />            
        <!--<add name="UrlRewriteModule" preCondition="managedHandler" type="UrlRewritingNet.Web.UrlRewriteModule, UrlRewritingNet.UrlRewriter" />-->
        <add name="BlacklistHttpModule" preCondition="managedHandler" type="NopSolutions.NopCommerce.BusinessLogic.Security.BlacklistHttpModule, Nop.BusinessLogic" />
        <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    </modules>
    <handlers>
        <add name="ChartImageHandler" preCondition="integratedMode" verb="GET,HEAD,POST" path="ChartImg.axd" type="System.Web.UI.DataVisualization.Charting.ChartHttpHandler, System.Web.DataVisualization, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <add name="PricelistHandler" verb="*" path="pricelist.csv" preCondition="integratedMode" type="NopSolutions.NopCommerce.BusinessLogic.ExportImport.PricelistHandler, Nop.BusinessLogic" />
        <add name="UrlRoutingHandler" preCondition="integratedMode" verb="*" path="UrlRouting.axd" type="System.Web.HttpForbiddenHandler,System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    </handlers>
</system.webServer>

4. Update how the category URL is constructed in  NopSolutions.NopCommerce.BusinessLogic.SEO.GetCategoryUrl(Category category)

string url2 = "{0}shop/{1}";
stringurl = string.Format(url2, CommonHelper.GetStoreLocation(), seName);
returnurl.ToLowerInvariant();

which means Enabling URL Rewriting or not will yield the same result (We do want to enforce the SEO-friendly URL). Note that the CategoryId is no longer in the URL. shop in the example above is just the hardcoded piece of the path so it could be anything.

5.  Make sure that the web app has reference to System.Web.Routing assembly

6. Add the routing map to Global.asax so now we can have a URL that looks something like  http://yourstore.com/shop/electronics and the application will know to redirect it to Category.aspx

void RegisterRoutes(System.Web.Routing.RouteCollection routes)
{
    routes.MapPageRoute(
         "category-browse",       // Route name 
         "shop/{SEName}",         // URL  
         "~/Category.aspx" // Web Forms page  
    );
}
void Application_Start(object sender, EventArgs e)
{
    RegisterRoutes(System.Web.Routing.RouteTable.Routes);    …

7. Add a method called GetCategoryBySEName(string seName) to ICategoryService & CategoryService. We need this because the URL no longer contains the CategoryId so we will need to retrieve the Category using SEName.

/// <summary>
/// Gets a category
/// </summary>
/// <param name="seName">Search engine friendly name of the category</param>
/// <returns>Category</returns>
public Category GetCategoryBySEName(string seName)
{
    if (string.IsNullOrWhiteSpace(seName))
    {
        return null;
    }

    string key = string.Format(CATEGORIES_BY_SENAME, seName);
    object obj2 = _cacheManager.Get(key);
    if (this.CategoriesCacheEnabled && (obj2 != null))
    {
        return (Category)obj2;
    }

    bool showHidden = NopContext.Current.IsAdmin;

    var query = from c in _context.Categories
                where c.SEName == seName
                select c;
    Category category = query.FirstOrDefault();

    //filter by access control list (public store)
    if (category != null && !showHidden && IsCategoryAccessDenied(category))
    {
        category = null;
    }
    if (this.CategoriesCacheEnabled)
    {
        _cacheManager.Add(key, category);
    }

    return category;
}

8. Update in Category.aspx to retrieve the category using the nice URL. ~/shop/electronics

The first line of the CreateChildControlTree() method it calls CategoryService’s GetCategoryById. Now we need to use GetCategoryBySEName that we just added above.

private void CreateChildControlsTree()
{
    category = this.CategoryService.GetCategoryBySEName(this.SEName);

To do that, we need to change the SEName property to get the value from RouteData, not query string.

public string SEName
{
    get
    {
        return Page.RouteData.Values["SEName"] as string;
    }
}

9. Update the same in your product templates as well. If there is no SEName property in the product templates, add the one like in Step 8. In the method BindData() is where it references a lot to the category, so I’ve created a private variable to hold the Category object that I will retrieve using SEName,

public partial class ProductsInGrid: BaseNopFrontendUserControl
{
    private Category _category = null;
protected void BindData()
{
    _category = this.CategoryService.GetCategoryBySEName(this.SEName);

After you complete all 9 steps here, you should be all set and have a very nice SEO-friendly URL for your nopCommerce store.

Consume RESTful Web Services in C#

The acronym REST stands for Representational State Transfer, this basically means that each unique URL is a representation of some object. It embraces a stateless client-server architecture in which the web services are viewed as resources and can be identified by their URLs. It’s different from the more traditional SOAP. Not gonna go into the details of the differences between the two as it will be endless. This link from AjaxPatterns has a pretty in-depth explanation of RESTful services.

http://ajaxpatterns.org/RESTful_Service

There are many ways to consume a RESTful service in C#. Since all it needs is the URL we can create an Http Web Request and then process the response. Another way is to use the more advanced WCF, which I’ve came across this link here:

http://blogs.msdn.com/b/pedram/archive/2008/04/21/how-to-consume-rest-services-with-wcf.aspx

It’s very good as it uses WCF to do the serialization and it eliminates the need to do manual transformation. But as always, you don’t get nothing for free. In this case it’s the same as all other situations when you let the framework do your dirty laundry – you lost flexibility. In addition, it might be hard for someone viewing your code to fully understand what’s going on right away because some stuff is in the web.config, some in code, and some in attributes.

A lot of the publicly available RESTful web services, such as Google Map API, Flicker’s API, Bing platform’s services, support both JSON and XML format. In the JavaScript world I prefer JSON, but when it comes to C# I still find XML much easier to work with for the simple fact that LINQ to XML is right at your fingertip.

Therefore, I find that using LINQ to XML and XPath transformation is pretty easy to consume a RESTful services and extract the information that you need. I am using Google Map API as an example here:

   1: XElement root = XElement.Load("http://maps.googleapis.com/maps/api/geocode/xml?sensor=false&address=Toronto");

   2: if (decimal.TryParse(root.XPathSelectElement("//result//geometry//location//lat").Value, out latitude) && 

   3:     decimal.TryParse(root.XPathSelectElement("//result//geometry//location//lat").Value, out longitude))

   4: {

   5:     result.Add(new BusinessObjects.Location

   6:     {

   7:         Coordinate = new BusinessObjects.LatLng { Latitude = latitude, Longitude = longitude },

   8:         FormattedAddress = root.XPathSelectElement("//result//geometry//location//lat").Value,

   9:     });

  10: }

As you can see, only a few lines of code. I am calling Google Map’s geocoding service and looking for Toronto. As soon as I have loaded the response XML into XElement I used XPath to get the latitude and longitude, as well as the formatted address in order to construct my own Location object, which might then contain other business logics and validation requirements specific to my application.

Well, that’s it. Here’s the XML response of the link above:

<?xml version="1.0" encoding="utf-8" ?>
<GeocodeResponse>
    <status>OK</status>
    <result>
        <type>locality</type>
        <type>political</type>
        <formatted_address>Toronto, ON, Canada</formatted_address>
        <address_component>
            <long_name>Toronto</long_name>
            <short_name>Toronto</short_name>
            <type>locality</type>
            <type>political</type>
        </address_component>
        <address_component>
            <long_name>Toronto Division</long_name>
            <short_name>Toronto Division</short_name>
            <type>administrative_area_level_2</type>
            <type>political</type>
        </address_component>
        <address_component>
            <long_name>Ontario</long_name>
            <short_name>ON</short_name>
            <type>administrative_area_level_1</type>
            <type>political</type>
        </address_component>
        <address_component>
            <long_name>Canada</long_name>
            <short_name>CA</short_name>
            <type>country</type>
            <type>political</type>
        </address_component>
        <geometry>
            <location>
                <lat>43.6525000</lat>
                <lng>-79.3816667</lng>
            </location>
            <location_type>APPROXIMATE</location_type>
            <viewport>
                <southwest>
                    <lat>43.5231891</lat>
                    <lng>-79.6377855</lng>
                </southwest>
                <northeast>
                    <lat>43.7815330</lat>
                    <lng>-79.1255479</lng>
                </northeast>
            </viewport>
            <bounds>
                <southwest>
                    <lat>43.4582970</lat>
                    <lng>-79.6392190</lng>
                </southwest>
                <northeast>
                    <lat>43.8554580</lat>
                    <lng>-79.0024810</lng>
                </northeast>
            </bounds>
        </geometry>
    </result>
</GeocodeResponse>

Removing and adding event handlers using reflection

Recently I ran into an situation that I need to remove all the event handlers, perform some work and then re-attach all the event handlers.  This requires using reflection since we need to discover the event handler in runtime. This is useful in cases where the object itself do not have knowledge of who has attached to itself. 

The code is listed just below.  We need the FieldInfo so that we can call GetValue() to get a reference to the event delegate.  Then using EventInfo.RemoveEventHandler() and EventInfo.AddEventHandler() to detach and re-attach the handler:


// define the binding flags for reflection
var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;

// Target has a event call PropertyChanged
Type type = target.GetType();
var fieldInfo = type.GetField("PropertyChanged", bindingFlags);
var eventInfo = type.GetEvent("PropertyChanged", bindingFlags);

// using GetValue() to get the reference of event delegate
var del = fieldInfo.GetValue(target) as Delegate;

// detach the event handler
if (del != null)
  eventInfo.RemoveEventHandler(target, del);

// Do you thing here!!

// re-attach the event handler
if (del != null)
  eventInfo.AddEventHandler(target, del);

Simple enough?  We can further improve this by looping through all the events and fields, so we don’t need the hardcoded event name in string.

WCF 403 Forbidden Exception

WCF (Windows Communication Foundation) comes with .NET 2.0, and with JSON serialization format with .NET 3.5. The most important DLL is ServiceModel. The thing is, when installing the .NET framework, it doesn’t mean that ServiceModel is registered with your server, or in particular, with IIS.

If that’s the case, you will most likely get the error below:

An existing connection was forcibly closed by the remote host.

The remote server returned an error: (403) Forbidden.

The HTTP request was forbidden with client authentication scheme ‘Anonymous’.

If that’s the case, you just need to register ServiceModel to your IIS using the ServiceModelReg.exe tool. =)

ASP.NET Validation

Validation is pretty essential for a web page with form submission. But even with all the help from Microsoft’s wonderful validation controls, at times some petty details becomes quite annoying problems to solve. This post is to list out some useful resources…

Validating ASP.NET Server Controls
http://msdn.microsoft.com/en-us/library/aa479013.aspx
User Input Validation in ASP.NET
http://msdn.microsoft.com/en-us/library/ms972961.aspx
ASP.NET Validation in Depth
http://msdn.microsoft.com/en-us/library/aa479045.aspx

My own post:
CheckBox Validation

CheckBox Validation

ASP.NET provides all the wonderful validation controls and perform both client-side and server-side validation. However, to validate a CheckBox, only the Custom Validation Control can be used. Assume you have the following…

<span><asp:CheckBox ID="MyCheckBox" runat="server" onClick="if (this.checked) CheckBoxChecked(); else CheckBoxUnchecked();" />Yes, your website is awesome! =) </span>
<asp:CustomValidator ID="MyCheckBoxValidator" runat="server" ErrorMessage="Custom Validator" ClientValidationFunction="ClientValidateMyCheckBox"
    ValidationGroup="MyValidationGroup" OnServerValidate="MyCheckBoxValidator_ServerValidate">Required.</asp:CustomValidator>

Pure server-side validation will work, using the event handler MyCheckBoxValidator_ServerValidate. Here’s how it would look, and then start other methods with a check on Page.IsValid

protected void MyCheckBoxValidator_ServerValidate(object source, ServerValidateEventArgs args)
{
    args.IsValid = (MyCheckBox.Checked);
}

On the other hand, it would be more efficient if client-side validation can also be done. The JavaScript function ClientValidateMyCheckBox is used for this purpose…

<script type="text/javascript">
function ClientValidateMyCheckBox(source, args)
{
    var cb = $get('<%=MyCheckBox.ClientID%>');
    args.IsValid = cb.checked;
}
</script>

This will trigger the client validation and if the CheckBox is unchecked, it will display the custom validator’s warning message. However, when you check the CheckBox now the validation warning message doesn’t disappear. This is because the client-side validation event is not triggerred during the client-side onClick event of the CheckBox. (Just a side-note, RequiredValidator and TextBox does not have this behavior. TextBox is validated as soon as something’s typed in.) To resolve this, a MS client-side JavaScript function needs to be hooked up with the onClick event of the CheckBox, Page_ClientValidate(“<validation group name>”) is the JS function to be used. Also note that for the “check and un-check” event of the CheckBox to work, we need to associate two functions to onClick, as shown below…

function CheckBoxChecked()
{
    var cb = $get('<%=MyCheckBox.ClientID%>');
    cb.checked = true;
    Page_ClientValidate("My_Validation_Group");
}
function CheckBoxUnchecked()
{
    var cb = $get('<%=MyCheckBox.ClientID%>');
    cb.checked = false;
    Page_ClientValidate("My_Validation_Group");
}

Final NOTE, don’t leave the validation group name out because it will then validate everything else on your page. However, do NOT use the same group name as the one you assigned in the Custom Validator also, it just doesn’t work. I can see that it doesn’t work because the ControlToValidate property is not set, so there is no linkage.

So well yeah, this will do it… of course, change accordingly if that’s not the behavior you are looking for…

CSS Friendly Adapters and Skins

ASP.NET CSS Friendly Control Adapters
http://www.asp.net/CSSAdapters/
http://www.codeplex.com/cssfriendly

I guess this exists because Microsoft realizes that some of its rendered HTML code are not that easy to apply CSS to. So this adapter comes and stands in the way of the rendering process to generate different sets of HTML. It’s pretty cool because at the very least it exposes the way to render customized HTML based on whatever requirements.

However, I don’t find it useful for all the controls it currently supports. On the contrary, some make it even harder to apply CSS to. With the help of Skin files, the look and feel of controls such GridView or DetailsView can be pretty well customized.

As of any other MS technologies, there are always more than one way of doing it. Whatever fits your need should be used…

Random Password Generation

Pretty cool, can be used for many purposes, like generating the decryption and validation machine keys, etc…

https://www.grc.com/passwords.htm

Follow

Get every new post delivered to your Inbox.