Aboo Bolaky

{.NET, C#, Sitecore ...} Free your mind...

Registering XSLT Extension Objects with the ASP XML control

clock October 22, 2009 22:01 by author Aboo Bolaky

I once inherited a sublayout that inlcuded an asp:xml control. The asp:xml control was there to handle and display an xml feed from another system, while the rest of the sublayout concentrated on rendering related feed content from Sitecore. The presentation of the xml feed was handled via an xlst rendering.

In this particular situation, I made use use of XSL extensions in the XSLT file. Registering the XSL Extension was fairly easy.

<xslExtensions>      <extension mode="on" type="Utils.XslHelper, Utils" namespace="http://www.sitecore.net/Utils" singleInstance="true" />.....</xslExtensions>


Registering the extension in the xsl file
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:MyExtension="http://www.sitecore.net/Utils" exclude-result-prefixes="MyExtension" ><xsl:value-of select="MyExtension:HelloWorld()" />



Binding the XML control to the XML Data

<asp:Xml ID="Xml1" runat="server" TransformSource="~/xsl/CustomFile.xslt"></asp:Xml>


   1:  protected override void OnInit(EventArgs e)
   2:  {  
   3:  if (!this.IsPostBack)
   4:        {
   5:             string xmlContent = "xml data goes here"  ;              
   6:             XsltArgumentList list = new XsltArgumentList();
   7:                               
   8:             Utils.XslHelper ext= new Utils.XslHelper ();
   9:             list.AddExtensionObject("http://www.sitecore.net/Utils", ext);
  10:             Xml1.TransformArgumentList = list;
  11:   
  12:             Xml1.DocumentContent = xmlContent;
  13:        }
  14:  }


It turns out that, even though I had previously registered the xsl extension in the xslt file, I also had to register it via code as well. Otherwise, the following exception occurs at runtime.

Cannot find the script or external object that implements prefix 'MyExtension'.

Sorted!

Currently rated 3.0 by 5 people

  • Currently 3/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5


Bold style for matching results with AutoCompleteExtender - Implemented using a Trie

clock October 7, 2009 09:22 by author Aboo Bolaky

This article will focus on the implementation of the AjaxControlToolkit's AutoCompleteExtender into a simple ASP.NET application. What I'm trying to do here is pull the data from a database and use the AutoCompleteExtender to display the data as the user types characters in a textbox.

Server Implementation

Im not going to focus on how to retrieve the data (from a table of around 60,000 records). I'm going to use the simplistic SQLConnection/SQLCommand/SQLDataReader classes and passing sql command as plain text (as opposed to Stored Procedures). The key issue is how and what is the best way to store the data once being retrieved. Generally, Autocomplete comes in 2 flavours:-

1: Find words based on letters anywhere in a phrase (performing a LIKE '%ar%'  in SQL).

2: Find words that start with specific characters ( LIKE 'ar%' in SQL).

In the first instance, you're pretty much tied up to using the LIKE clause (be it in a stored proc or simple text). The determining factor here is how and when to cache the data since each character typed in invokes the webservice and queries the database. Each query will yield a different result set.

In the second instance (the focus of this article), the more you add characters to the keyword, you are, in fact, narrowing down the scope of search. This means that the data we initially load is deterministic and hence we can cache it somewhere. But the real deal here is :which  data structure do we hold our words in? List<>, DataTable, Linked List??

Trie 

You might have already figured this one out. Anyway, a trie is an ordered tree data structure (a.k.a prefix tree) that stores the information about the contents of each node in the path from the root to the node, rather than the node itself. What this means is that each part between the root and any leaf represents a key and the goal is to find the key by traversing the tree.

Implementation

Eyal Mey-Tal has implemented a Trie structure in C#.Head to the CodePlex site to download it.

WebMethod to retrieve data

   1:   [WebMethod]
   2:          public string[] GetNames(string prefixText, int count)
   3:          {
   4:              Trie trie = (Trie)Context.Cache["Trie"];
   5:               if (trie == null)
   6:               {
   7:                   trie = new Trie();     
   8:   
   9:                   using (SqlConnection con = new SqlConnection("server=(local);database=autocomplete;user id=xxx;Password=xxx;"))
  10:                   {
  11:                       SqlCommand cmd = new SqlCommand("select word from AutoCompleteData", con);
  12:                       cmd.CommandType = System.Data.CommandType.Text;
  13:                       con.Open();
  14:                       SqlDataReader dr = cmd.ExecuteReader();
  15:                       if (dr.HasRows)
  16:                       {
  17:                           while (dr.Read())
  18:                           {
  19:                              trie.Add(dr.GetString(0));
  20:                           }
  21:                           con.Close();
  22:                       }
  23:                   }
  24:               //simple caching here  
  25:               Context.Cache["Trie"] = trie;
  26:               }
  27:              List<string> list = trie.GetCompletionList(prefixText);
  28:              return list.Take(count).ToArray();
  29:          }

 

Setting up the AutoCompleteExtender was easy. To get the autocomplete working, I only had to modify a few of the extender's attributes (.e.g. CompletionSetCount, TargetControlID, ServicePath, ServiceMethod, DelimiterCharacters ..) and data was being returned as I typed in characters. However, making the extender look good was a different story.

Client-Side Implementation 

This is the bit where I'm open to suggestions. I'm no Javascript/JQuery expert but however, I finally got the extender working as I wanted it to. Characters relevant to the ones the user has typed become highlighted in the drop down list. It took me a while to dig this solution out. The javascript is awkward and needs to be refactored. Netherless, the solution works for IE and Firefox [ good enough for me :) ]

 

   1:  <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="AutoComplete.aspx.cs" Inherits="AutoComplete.AutoComplete" %>
   2:   
   3:  <%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="cc1" %>
   4:   
   5:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   6:   
   7:  <html xmlns="http://www.w3.org/1999/xhtml" >
   8:  <head runat="server">
   9:      <title></title>
  10:      
  11:      <style>
  12:     
  13:      .autocomplete_completionListElement 
  14:  {  
  15:      margin : 0px!important;
  16:      background-color : inherit;
  17:      color : windowtext;
  18:      border : buttonshadow;
  19:      border-width : 1px;
  20:      border-style : solid;
  21:      cursor : 'default';
  22:      overflow : auto;
  23:      height : 200px;
  24:      text-align : left; 
  25:      list-style-type :  none;
  26:      padding-left:1px;
  27:  }
  28:   
  29:  /* AutoComplete highlighted item */
  30:   
  31:  .autocomplete_highlightedListItem
  32:  {
  33:      background-color: #aaff90;
  34:      color: black;
  35:      padding: 1px;
  36:      cursor:hand;
  37:  }
  38:   
  39:  /* AutoComplete item */
  40:   
  41:  .autocomplete_listItem 
  42:  {
  43:      background-color : window;
  44:      color : windowtext;
  45:      padding:1px;
  46:  }
  47:      
  48:      </style>
  49:      
  50:      <script type="text/javascript">
  51:   
  52:         
  53:          function acePopulated(sender, e) {
  54:          
  55:          var behavior = $find('AutoCompleteEx');
  56:                      
  57:                      var target = behavior.get_completionList();
  58:                      if (behavior._currentPrefix != null)
  59:                      {
  60:                          var prefix = behavior._currentPrefix.toLowerCase();
  61:                          var i;
  62:                              for (i = 0; i < target.childNodes.length; i++)
  63:                              {
  64:                                  var sValue = target.childNodes[i].innerHTML.toLowerCase();
  65:                                  if (sValue.indexOf(prefix) != -1)
  66:                                  {
  67:                                   
  68:                                  var fstr = target.childNodes[i].innerHTML.substring(0, sValue.indexOf(prefix));
  69:                                  var pstr = target.childNodes[i].innerHTML.substring(fstr.length, fstr.length + prefix.length);
  70:                                  var estr = target.childNodes[i].innerHTML.substring(fstr.length + prefix.length, target.childNodes[i].innerHTML.length);
  71:                                  target.childNodes[i].innerHTML = "<div class='autocomplete-item'>" + fstr + '<B>' + pstr + '</B>' + estr + "</div>";
  72:                                  
  73:                                 
  74:                                  }
  75:                              }
  76:                      }
  77:                    
  78:              }
  79:          
  80:          
  81:       
  82:              function aceSelected(sender, e) 
  83:              {
  84:                  var value = e.get_value();
  85:                  if (!value) {
  86:                      if (e._item.parentElement && e._item.parentElement.tagName == "LI")
  87:                          value = e._item.parentElement.attributes["_value"].value;
  88:                      else if (e._item.parentElement && e._item.parentElement.parentElement.tagName == "LI")
  89:                          value = e._item.parentElement.parentElement.attributes["_value"].value;
  90:                      else if (e._item.parentNode && e._item.parentNode.tagName == "LI") 
  91:                          value = e._item.parentNode._value;
  92:                      else if (e._item.parentNode && e._item.parentNode.parentNode.tagName == "LI")
  93:                          value = e._item.parentNode.parentNode._value;
  94:                      else value = "";
  95:                  }
  96:                  var searchText = $get('<%=txtWord.ClientID %>').value;
  97:                  searchText = searchText.replace('null', '');
  98:                                  
  99:                  sender.get_element().value = searchText + value;
 100:                        }
 101:                                       
 102:   
 103:      </script>
 104:      
 105:  </head>
 106:  <body>
 107:      <form id="form1" runat="server">
 108:       <asp:ScriptManager ID="ScriptManager1" runat="server">
 109:       
 110:       </asp:ScriptManager>
 111:       <div style="text-align:center;margin-top:200px">
 112:     Search : <asp:TextBox ID="txtWord" runat="server" AutoComplete="off" Width="200px"   /> 
 113:           </div>
 114:       <cc1:AutoCompleteExtender
 115:                  runat="server" 
 116:                  BehaviorID="AutoCompleteEx"
 117:                  ID="autoComplete1" 
 118:                  TargetControlID="txtWord"
 119:                  ServicePath="MyService.asmx" 
 120:                  ServiceMethod="GetNames"
 121:                  MinimumPrefixLength="1" 
 122:                  CompletionInterval="1000"
 123:                  EnableCaching="true"
 124:                  CompletionSetCount="20"
 125:                  CompletionListCssClass="autocomplete_completionListElement" 
 126:                  CompletionListItemCssClass="autocomplete_listItem" 
 127:                  CompletionListHighlightedItemCssClass="autocomplete_highlightedListItem"
 128:                  OnClientPopulated="acePopulated"
 129:                  OnClientItemSelected="aceSelected"
 130:                  DelimiterCharacters=";"
 131:                  ShowOnlyCurrentWordInCompletionListItem="true" >
 132:              </cc1:AutoCompleteExtender>
 133:       
 134:      </form>
 135:  </body>
 136:  </html>

Result

 

 

Downloads

The source code (with database script in ~/App_Data (around 60,000 records )) can be downloaded below.

AutoCompleteVS2008Sln.zip (4.84 mb)

Enjoy ....

Currently rated 3.0 by 5 people

  • Currently 3/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5


Building extension-less Urls in Sitecore

clock October 3, 2009 23:55 by author Aboo Bolaky

Our goal here is to build a Sitecore solution having links without the .aspx extension.(although accessing a page with a .aspx extension should STILL work)

To start ,you need

  • A LOT OF PATIENCE
  • Helicon ISAPI Rewrite Lite (the free one). Since I'm usingWindows 7 RC1 x64bit, I'll need to download the x64 bit flavour of Helicon Lite
  • A test Sitecore Application .. I will be using the Sitecore Starter Kit [Sitecore Starter Kit 6.0.0 rev.090203] as an my starting point.  (installed on IIS 7).

Before I start on implementing the solution, a little bit of background info would, I guess, be quite useful.

AddAspxExtension in LinkManager

A potential solution is to change the value of AddAspxExtension from true (by default) to false. If you do change it to false, you will have to create a wildcard script map to the ASP.NET runtime. This causes IIS to intercept every request made against the web server. This includes requests for images, asp.net pages and HTML pages. Therefore, enabling a wildcard script map to ASP.NET does have performance implications. If you wish to find another way to use pages without .aspx extensions in Sitecore, please read further....

Sitecore Aliases

Aliases, in a nutshell, allow you to shorten the url of an item. For example, if your item is currently accessible via http://hostname/MyParentItem/MyChildItem.aspx, you can specify an alias of myChildItem, which will be a placeholder for the actual item as it is in the Sitecore tree. Hence, the url of the alias is http://hostname/MyChildItem.aspx. For SEO purposes, this allows us to surface items from deep down in the hierarchy right up to the site root.

Note:

  • Aliases do not work if you remove the .aspx extension
  • No matter how far your items are in the sitecore tree, an alias allows you to refer to it from the site root.

Step 1: Install and Configure Helicon ISAPI Rewrite Lite

Start by installing Helicon ISAPI Rewrite. This process is fairly straightforward. Since we are using the lite version,  our Regex entries will be placed in the global http.conf (located in the Lite version installation folder). The entries in my httpd.conf are as follow:-

RewriteEngine on
RewriteBase /
RewriteRule ^(sitecore.*)$ $1 [L]
RewriteRule ^([^\.\?]+)/?(\?.*)?$ /$1.aspx$2 [L]

 

Url Rewriting Rationale

Before a request is forwarded to Sitecore, the ISAPI module intercepts it.

Line 1: You NEED that ! This turns on the Helicon ISAPI Module

Line 2: Errr...This is self-explanatory..

Line 3: We don't need to chop off the .aspx when we are in Sitecore CMS. For this reason, we're basically telling the module to not do anything when a request has "sitecore" in it.

Line 4: This is the most important bit. This appends .aspx (and querystrings,if any) to requests and consequently forwards the resulting request to Sitecore. Two scenarios arise as a result of this.

 3.1 : Sitecore maps the request to an item in the database. The page gets displayed in the end.

 3.2 : Sitecore cannot find the item based on the url. You will either end up with the  Sitecore's "Item Not Found" page.

NB:

Before we go any further, I need to confess that I did modify the Starter Kit a little prior to this operation. Basically, when you load the starter kit, you are greeted with a dummy home Site, that has a nice layout and there is a link to the Actual Starter Site. I was tired of this as my home page, So, I changed the value of "startItem" [in the Sites Definition of website (in web.config)] from "/home" to "/Sample". In that way, when i hit the website, I will eventually land on the real starter site!. Also, by doing so, all my urls within the website will no longer contain "/sitecore/content/.." since the Start Item has changed.

Quick Test on Urls

Request : http://sitecorestarterkit/References.aspx  [OK]

Request : http://sitecorestarterkit/References  [OK]

Request : http://sitecorestarterkit/Services/Architectual-Services.aspx [OK]

Request : http://sitecorestarterkit/Services/Architectual-Services [OK]

Request : http://sitecorestarterkit/Sitecore/  [OK..CMS access]

It looks like we have a half-baked solution. Aliases will now work without the .aspx extension as well. The other bits that need to be considered are

1 : How to make sitecore controls (.e.g. sc:link etc..) aware that they should drop the ".aspx" extensions

2 : How does it all tie up together with .NET (user controls etc..)

Step 2: XSL Extensions (revised)

To follow up on custom solution, you will need to tell Sitecore to remove the ".aspx" when it renders urls (either via sc:link [xsl extensions] or c# code). For XSL Extensions, we need to disable the default implementation that Sitecore provides us with and roll out our own. Fortunately, it's very easy to do so. [Credits : Chris Wojciech ]

 

2.1 : Turn off the default XslHelper

<xslExtensions><!-- Changed from "On" to "Off" --><extension mode="off" type="Sitecore.Xml.Xsl.XslHelper, Sitecore.Kernel" namespace="http://www.sitecore.net/sc" singleInstance="true" />......</xslExtensions> 

  

  

2.2 : Create your own XslHelper

   1:  namespace Starterkit.Utils
   2:  {
   3:      public class XslHelper : Sitecore.Xml.Xsl.XslHelper
   4:      {
   5:          public override string path(System.Xml.XPath.XPathNodeIterator iterator)
   6:          {
   7:              string path = base.path(iterator);
   8:              string newPath = Regex.Replace(path, ".aspx", String.Empty, RegexOptions.IgnoreCase | RegexOptions.Compiled);
   9:              return newPath;
  10:          }
  11:          public override string link(string fieldName, System.Xml.XPath.XPathNodeIterator iterator, string parameters)
  12:          {
  13:              string path = base.link(fieldName, iterator, parameters);
  14:              string newPath = Regex.Replace(path, ".aspx", String.Empty, RegexOptions.IgnoreCase | RegexOptions.Compiled);
  15:              return newPath;
  16:          }
  17:          public override string StartLink(System.Xml.XPath.XPathNodeIterator iterator, string parameters)
  18:          {
  19:              string path = base.StartLink(iterator, parameters);
  20:              string newPath = Regex.Replace(path, ".aspx", String.Empty, RegexOptions.IgnoreCase | RegexOptions.Compiled);
  21:              return newPath;
  22:          }
  23:   
  24:      }
  25:  }

 

For each of those three methods, we're only replacing the .aspx with an empty string. To enable <sc:link/> to use our custom Xsl Helper, we need to add another entry to the <xslextensions> section

 

<xslExtensions>      <!-- Changed from "On" to "Off" -->      <extension mode="off" type="Sitecore.Xml.Xsl.XslHelper, Sitecore.Kernel" namespace="http://www.sitecore.net/sc" singleInstance="true" />      <extension mode="on" type="Starterkit.Utils.XslHelper, Starterkit.Utils" namespace="http://www.sitecore.net/sc" singleInstance="true" />................     </xslExtensions>

 

NEARLY THERE!!!. All the links (that are rendering using sc:link) have now lost the .aspx extensions on the front end.

Step 3 : Sitecore and .NET interaction (with Url Rewriting)

If you have a Sitecore solution built using XSLT renderings only (highly unlikely though..), you're kinda safe here. However, if you have usercontrols (that host controls that can cause a postback) as well (for argument's sake, a contact us form), you end up with one issue.

Let's create a Contact Us form and add it to the presentation of the Contact Us item in Sitecore

 

User Control Designer
<%@ Control Language="c#" AutoEventWireup="true" TargetSchema="http://schemas.microsoft.com/intellisense/ie5"  Inherits="Layouts.Contactus.ContactusSublayout" Codebehind="~/layouts/Starter Kit/Sublayouts/ContactUs.ascx.cs" %><asp:Label Text="First Name : "  AssociatedControlID="txtFirstName" runat="server" /> <asp:TextBox ID="txtFirstName" runat="server" /><asp:Label Text="First Name : "  AssociatedControlID="txtFirstName" runat="server" /> <asp:TextBox ID="TextBox1" runat="server" /><asp:Button ID="btnSend" Text="Send" runat="server" />

 

Page Source

<form name="mainform" method="post" action="/Contact.aspx" id="mainform">


To solve this, you will need to create a Control Adapter for the Forms in your application. Control Adapters allow you to inject custom code within the rendering of a control.

 

Form Control Adapter

   1:  namespace Starterkit.Utils
   2:  {
   3:      public class FormActionRewriter : System.Web.UI.Adapters.ControlAdapter
   4:      {
   5:          protected override void Render(System.Web.UI.HtmlTextWriter writer)
   6:          {
   7:              base.Render(new RewriteFormHtmlTextWriter(writer));
   8:          }
   9:   
  10:      }
  11:      class RewriteFormHtmlTextWriter : HtmlTextWriter
  12:      {
  13:          public RewriteFormHtmlTextWriter(HtmlTextWriter writer)
  14:              : base(writer)
  15:          {
  16:              this.InnerWriter = writer.InnerWriter;
  17:          }
  18:   
  19:          public RewriteFormHtmlTextWriter(System.IO.TextWriter writer)
  20:              : base(writer)
  21:          {
  22:              base.InnerWriter = writer;
  23:          }
  24:          public override void WriteAttribute(string name, string value, bool fEncode)
  25:          {
  26:   
  27:              if ((name == "action"))
  28:              {
  29:                  HttpContext Context = null;
  30:                  Context = HttpContext.Current;
  31:   
  32:                  if (Context.Items["ActionAlreadyWritten"] == null)
  33:                  {
  34:                      if ((!Context.Request.RawUrl.Contains("sitecore")))
  35:                      {   //remove .aspx extension if we're on the front end
  36:                          value = Regex.Replace(Context.Request.RawUrl, ".aspx", String.Empty, RegexOptions.IgnoreCase | RegexOptions.Compiled);
  37:                          Context.Items["ActionAlreadyWritten"] = true;
  38:                      }
  39:                  }
  40:   
  41:              }
  42:              base.WriteAttribute(name, value, fEncode);
  43:          }
  44:      }
  45:  }

Add Form Control Adapter in Solution

Open the form.browser (located in ~/App_Browsers) and add the new entry

<browsers>  <browser refID="Default">    <controlAdapters>      <adapter controlType="System.Web.UI.HtmlControls.HtmlForm" adapterType="Sitecore.Web.FormAdapter, Sitecore.Kernel" />      <!--Added-->      <adapter controlType="System.Web.UI.HtmlControls.HtmlForm" adapterType="Starterkit.Utils.FormActionRewriter, Starterkit.Utils" />    </controlAdapters>  </browser></browsers>

THIS IS IT!!!

<form name="mainform" method="post" action="/Contact" id="mainform">

 

RESULT !!! .... Back to Sitecore :)

Currently rated 3.3 by 6 people

  • Currently 3.333333/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5


Cross Browser testing using SuperPreview for Internet Explorer

clock September 23, 2009 22:52 by author Aboo Bolaky

Let's face it. Although I'm no designer, I do feel sorry for my fellow colleagues when I hear them complain about the rendering on some specific browser(s).

It gets harder when testing against IE6 for example. They normally use IE6 VMs to perform testing.  There are three major drawbacks of this approach:

1: WHERE IS THE IE6 VM? Is it on the local machine? Is it on the network? Is it on a Virtual Server that no one has admin rights to? Does the IE6 VM have a meaningful name .. Imagine having 10 VMs locally and having to boot each one to see if IE6 is present !!

2: Loss of password for Logging into VM. (just because the last person who logged in just decided to change the "usual" password to his/her cat's name and not telling anyone about it..and he/she is most propbably off sick when we need to do testing)

3: VM gets copied across network onto physical machine. Transfering 6-10 GB to your computer and hosting the VM locally. Another designer comes along and does the same thing. We now have 2 VMs on 2 different machines that serve the same purpose. Let's not entertain the idea of having both VMs running simultaneously !! A whole range of IP Address/Name resolution conflicts arise. :)

Microsoft Expression Web SuperPreview for Windows Internet Explorer

This tool allows us to view a page in different IE versions. This eliminates the use of glorious VMs for testing. Well done Microsoft !

You can download Microsoft Expression Web SuperPreview for Windows Internet Explorer here

Description

"Expression Web SuperPreview for Internet Explorer is a stand-alone visual debugging tool that makes it faster and easier to migrate your sites from Internet Explorer 6 to Internet Explorer 7 or 8. With Expression Web SuperPreview for Internet Explorer, you can ensure that your Web sites work correctly in Internet Explorer 8 while also maintaining compatibility with earlier versions of Internet Explorer.

Expression Web SuperPreview for Internet Explorer shows your web pages rendered in Internet Explorer 6 and either Internet Explorer 7 or Internet Explorer 8, depending on which version you have installed on your machine. You can view the pages side by side or as an onion-skin overlay and use rulers, guides and zoom/pan tools to precisely identify differences in layout. You can even compare your page comp to how the targeted browsers render the page.

Expression Web SuperPreview for Internet Explorer not only shows a high-fidelity rendering of how pages will look on different browsers, but it also identifies the element's tag, size and position, applied styles, and location in the DOM (Document Object Model) tree so you can quickly fix the error. "

Expression Web SuperPreview for Internet Explorer is a standalone, free application with no expiration and no technical support from Microsoft. 

Note:

If you wish to have the ability to debug pages both in IE and Firefox flavours in a single application, go for Microsoft Expression Web. You can download a 60-day trial copy here

 

Currently rated 3.2 by 6 people

  • Currently 3.166667/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5


Best way to parse HTML content

clock September 22, 2009 23:26 by author Aboo Bolaky


I'm going to keep this short and simple..rather short actually.

There is no better way to parse HTML other than using HtmlAgility Pack.

It's a lot simpler than Regex..which is a BIG no no!!

 

Currently rated 3.0 by 5 people

  • Currently 3/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5


SEO Friendly Urls in Sitecore -Remove spaces in Url (3)

clock August 10, 2009 21:33 by author Aboo Bolaky

It's the third time I'm writing on this particular topic (The EncodeNameReplacements element in the web.config). Articles 1 and 2  have had their importance in building SEO-friendly links in Sitecore.

Double Encoding in Urls

Whilst doing some routine administrative tasks in sitecore, I noticed that some links were no longer working. I was pretty sure that those links were working before I did a publish. :( Back to the drawing board.

As outlined in Articles 1 and 2, I was using

<replace mode="on" find=" " replaceWith="-" />

in my web.config. It was working fine when sitecore items were having names like "Camera one" (->url: camera-one.aspx), "Camera one two three" (->url: camera-one-two-three.aspx) etc...

However, if an item name has a character that has already been defined in my EncodeNamesReplacements section, Sitecore throws you back on the Item Not Found page. In my situation, I had renamed the Sitecore Item :"John Doe" to "John-Doe" (lame administrative task...I KNOW) . The effect of this is that when the user clicks on the John-Doe.aspx, he/she is redirected to the default "Item Not Found" sitecore page. A careful look at the url has revealed that Sitecore was trying to request an item with a name of John--Doe and consequently failed.

Solution

or hack (for some people)

If you are using a specific character to get rid of spaces in url, you need to make sure that a cms user will not be able to create an item name having that character in. Easy way to factor this in is to include your character in the InvalidItemNameChars element in the web.config. In my implementation, I've changed that setting to :

   1:  <setting name="InvalidItemNameChars" value="\/:?&amp;quot;&amp;lt;&amp;gt;|[]-/>

and that did the trick :)

Currently rated 3.0 by 5 people

  • Currently 3/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5


Implementing Sitecore Extranet login on a website

clock July 29, 2009 23:02 by author Aboo Bolaky

Here's the situation. You are about to implement a password protected area on your website. Let's assume that the general site structure looks like this

Pages below General and Products are accessible to everyone, whereas pages under Members should only be visible to authenticated/logged in members. I will first briefly outline the steps required to get this problem implemented using ASP.NET. Later on, I'll move onto it's equivalent Sitecore Solution.

Using ASP.NET

  • Implement Forms Authentication and set login url in the web.config.
  • Implement Login control and decide where to retrieve and store login credentials (in web.config or database)
  • In the web.config, add a Location Path pointing to the Members folder (Deny anonymous , allow authenticated users )
  • This is all about it really...(as far as I remember..) ...

In Sitecore, it's a different ball game.

In addition to adding the loginURL to the form authentication section (important if you use the loginview control to show the login page), you will need to  add the  "loginPage" attribute to the site which is defined by your extranet domain (normally, it's called "website" )

   1:  <sites>

   2:  .....
   3:  <site name="website" virtualFolder="/" physicalFolder="/" loginPage="/General/Login.aspx"
   4:  ....
   5:  </sites>

 

The LoginPage attribute is not something new here..It has always been there..(e.g. the shell website has already a loginPage set), but i did not know what was its purpose . Thanks to Chris Wojciech, I've discovered how to use this existing functionality in the web application.

The addition of Location path in the asp.net-only model is analogous to denying read access to the Members folder (+descendants) in Sitecore.

 

Once you perform a site publish, you can see the effects straight away.

If you've already signed in, you will be able to view /Members/View My Account.aspx.

If you're an anonymous user and access  /Members/View My Account.aspx, you will be presented with a default page that Sitecore serves in case access is denied due to security privileges.

http://mywebapp/sitecore/service/noaccess.aspx?item=%2fmembers%2fview+my+account&user=extranet%5cAnonymous&site=website

 

Quick Fix :

The page served in this case is called noaccess.aspx. The good thing is that this can be altered by changing the value of the "NoAccessUrl" attribute in the web.config.

If we set  "NoAccessUrl" to "/General/Login.aspx", we end up in this situation

http://mywebapp/general/login.aspx?item=%2fmembers%2fview+my+account&user=extranet%5cAnonymous&site=website

 

Recommended Solution

The nag in the above quick fix is that sitecore internally adds 3 QueryStrings to the url ( item, user and site). If we compare this to the normal ASP.NET solution, we would have ended up with only 1 querystring, which is the ReturnUrl.  Our goal is to follow the asp.net solution as close as possible. This is where Chris comes in..

Rolling out your own Security Resolver

Chris extended the HttpRequestProcessor class in order to intercept the request ,check if the user requesting the sitecore item has appropriate rights. If that is not the case, the user is redirected to the login page, with the appropriate ReturnUrl QueryString. Please go check the code out on his blog at http://blog.wojciech.org/?p=64 

The processor should then be plugged in the web.config, before the definition of the ExecuteRequest processor.

 

   1:  <processor type="Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel"/>
   2:  <processor type="Sitecore.Pipelines.HttpRequest.LayoutResolver, Sitecore.Kernel"/>
   3:  <processor type="MyWebApp.Pipelines.MyOwnSecurityResolver, MyWebApp"/>
   4:  <processor type="Sitecore.Pipelines.HttpRequest.ExecuteRequest, Sitecore.Kernel"/>

 

If you now try to access a protected page as an anonymous user, you'll end up on the login page (but this time, the ReturnUrl parameter has replaced the 3 built-in sitecore url parameters)

http://mywebapp/general/login.aspx?returnUrl=/members/view%20my%20account.aspx

Result :)

Currently rated 5.0 by 1 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5


LoginView control not working when logging out from Sitecore extranet domain

clock July 28, 2009 21:19 by author Aboo Bolaky

Let's not re-invent the wheel and make use of the ASP.NET 2.0 LoginView control to generate Login/Logout actions for the Sitecore extranet domain.

We have our LoginView control (very simplistic example given here) in a sublayout.

   1:   <asp:LoginView ID="loginView" runat="server">
   2:      <AnonymousTemplate>
   3:        Welcome Guest <asp:LoginStatus runat="server" LoginText="Login" />
   4:      </AnonymousTemplate>
   5:      
   6:      <LoggedInTemplate>
   7:        Welcome <%= Sitecore.Context.User.Name %>.
   8:        <asp:LoginStatus runat="server" LogoutText="Logout"
   9:         LogoutPageUrl="/" LoginText="Login" LogoutAction="Redirect" />
  10:      </LoggedInTemplate>
  11:    </asp:LoginView>

LoginView Control Caveat

The LoginView control "magically" knows the login page url. This is specified by the LoginUrl attribute in the FormsAuthentication section of the web.config. This is not to be confused the LoginPage attribute (from the sites section). I had to modify my web.config to

   1:  <authentication mode="Forms">
   2:  <forms name=".ASPXAUTH" cookieless="UseCookies" loginUrl="~/General/Login.aspx" />
   3:  </authentication>

Erractic Behaviour when logging out

I did not experience any problems when logging in .i.e. the control did what is was supposed to do (display sitecore username and Logout Link). HOWEVER, when I pressed the Logout link, I always got redirected the Sitecore Layout page instead. I did spot similar behaviour with ListViews in Sitecore (also documented by Paul George and Mark Cassidy, where its events were not being fired at all. I've made the following change to my web.config and that solved the problem :)

   1:  <typesThatShouldNotBeExpanded>
   2:     <type>System.Web.UI.WebControls.Repeater</type>
   3:     <type>System.Web.UI.WebControls.DataList</type>
   4:     <type>System.Web.UI.WebControls.LoginView</type>
   5:  </typesThatShouldNotBeExpanded>

 

A similar problem that relates to the  typesThatShouldNotBeExpanded tag has also been documented on SDN

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5


Automatic Publishing in Sitecore

clock July 25, 2009 22:02 by author Aboo Bolaky

Sitecore DOES support Automatic Publishing. However, there are not many instances where you would want Sitecore to automatically perform a publish.

The values to change reside in the web.config, right where you have definitions for the scheduling and agents

   1:    <agent type="Sitecore.Tasks.PublishAgent" method="Run" interval="00:00:00">
   2:          <param desc="source database">master</param>
   3:          <param desc="target database">web</param>
   4:          <param desc="mode (full or incremental)">incremental</param>
   5:          <param desc="languages">en, da</param>
   6:        </agent>

 

I guess that a value of "00:00:00" for the interval attribute does disable automatic publishing. If you set the value to (say..10 minutes) "00:10:00", you will notice that after 10 minutes or so, changed items from the master database will be copied over to the web database.

Automatic publishing is useful where you have integrated external datasources in sitecore (using Data Providers) and where there needs to be a predefined process that synchs the external data to the web database. For the automatic publishing to work in this particular situation, you must have had created a new database entry (with a reference to your data provider) in the web.config.

Sitecore Data Providers....hmmmmm...that's upcoming.... :)

Currently rated 5.0 by 1 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5


Programmatically skip publishing of item(s) in Sitecore

clock July 22, 2009 20:43 by author Aboo Bolaky

Scenario

Assume that you have a set of items (say..Product items) (sitting anywhere within the /sitecore/content..) based on a specific template. The requirement here is that

"A Product cannot be published if one of its fields (ProductID) isn't populated."

Background

To achieve this, we need to hook into the publish:itemProcessing event in the web.config. This event gets triggered every time an item is published. The general steps involved in this situation are:

  • Create a class with a method that adheres to the EventHandler delegate signature. Whenever you initiate a publish operation in sitecore (be it smart publish, incremental or full publish), the method will be called (depending on how many items that need to be published)
  • Modify the web.config to subscribe to ItemPublishing event

ItemPublisher Class

   1:  namespace Test.Events
   2:  {
   3:      public class ItemPublisher
   4:      {
   5:         public void CheckProcessing(object sender, EventArgs args)
   6:          {
   7:            ItemProcessingEventArgs theArgs = args as ItemProcessingEventArgs;
   8:                          
   9:            Item currentItem = theArgs.Context.PublishHelper.GetSourceItem(theArgs.Context.ItemId);
  10:   
  11:            if ((currentItem != null) && (currentItem.Paths.IsContentItem))
  12:            {
  13:                //Template ID of item on which selective publishing is to be applied
  14:               if (currentItem.TemplateID == new ID("{9C9A2F3D-652A-4490-AB57-E9F1B4D5BF05}"))
  15:                {
  16:                   Job currentJob = theArgs.Context.Job;
  17:                   JobStatus currentJobStatus = currentJob.Status;
  18:   
  19:                   if (String.IsNullOrEmpty((currentItem.Fields["Product ID"].Value)))
  20:                    {
  21:                      currentJobStatus.Messages.Add(String.Format("Item :{0} has not been published since it has no Product ID", currentItem.Name));
  22:                      theArgs.Cancel = true;
  23:                      return;
  24:                     }
  25:   
  26:                }
  27:             }
  28:          }
  29:      }
  30:  }


Line 7:
Cast the standard EventArgs class to ItemProcessingEvent. This is important since it gives you the possibility of retrieving details of the items being published.

Line 9: Retrieve the item being published.

Line 11: The check for "currentItem.Paths.IsContentItem" is important since we only want to check for content items. Since publishable items sitecore vary from templates,standard values, renderings... we do not want to check for the condition in ALL items.

Line 14: If the template of the current item matches the id of the Product template, then we're back in business.

Line 16 - 21: Find the reference to the current Job (and JobStatus)  being executed in the publish pipeline. If the item's field is empty, add a message to the JobStatus.

Line 22: Abort the publish operation of the current item. I don't think we require the return statement after that. Publishing will then resume for the next item in the publishing queue.

Web.Config Change

Locate the publish:itemProcessing event in the web.config. Hook up the new handler to the event.

   1:  <event name="publish:itemProcessing" help="Receives an argument of type ItemProcessingEventArgs (namespace: Sitecore.Publishing.Pipelines.PublishItem)" >
   2:    <handler type="Test.Events.ItemPublisher,TestApplication" method="CheckProcessing" />
   3:  </event>

 

Let's put it to the test

That's all to it really. If you now  initiate a publish operation and one of the Products has an emtpy Product ID, you will end up with this (if you click on "Click here to see additional information" on the last screen of the publish wizard.)

 

Items Skipped = 1 (.i.e Camera item has been skipped during the publish process since it has no ProductID). If you switch to the web database, there will not be any "Camera" item. 

Result... :)

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5


A b o u t M e

Annoying

Brilliant

Open and

Objective

in every way..
Only Human >>
 
"First learn computer science and all the theory.

Next develop a programming style.

Then forget all that and just hack." Carrette (1990)