Remembering the user language choice

Recently, I had to develop a small web portal for a client. Since the portal consisted mostly of static pages which the client wanted to update himself, and I had limited time for development, WordPress platform was an obvious choice. We agreed to derive the design from the standard “Responsive” theme (WordPress Codex has a great article on how to do that).

The portal had to be multi-lingual, most pages would be in English, but some would have translations in Latvian. I found WordPress qTranslate plugin the best solution for my needs. It changes the way WordPress dashboard forms work, now a blog post has two fields “Title (Latvian)” and “Title (English)” as well as “Post content (Latvian)” and “Post content (English)”. If you don’t fill the fields for a particular language, the page would be treated as not translated and may be hidden in the navigation as well. You have to pick the default language of the site – what content to show if the user has not picked any language.

qTranslate provides several ways how you can do localization:

  • using a subdomain, e.g. lv.yourdomain.com and en.yourdomain.com
  • by adding a GET parameter to each page, eg. yourdomain.com/article_title/?lang=lv
  • using a directory prefix, e.g. yourdomain.com/lv/articletitle, yourdomain.com/en/articletitle

I picked the latter, as it seemed most rational of the three. Now every page had two addresses and everything seemed to be running just fine. However, I found it a bit annoying that a returning visitor always sees the default language, regardless which language he had chosen in the previous visit. It would seem logical that qTranslate could just set a cookie “last language” or something like that. I found out, it does not. Moreover, qTranslate authors are not going to implement it, they say “it’s a wanted behavior”.

Not wanting to mess with plugin’s PHP code, I decided to fix this at the web server level (as you may know, Apache Mod_Rewrite can both read and set cookies).

So, I needed two parts of the algorithm:

– when the user requests a page where the URL contains language code, save that language in a cookie. In “.htaccess language” it is

    #Only for requests which start with "/lv/" or "/en/". 
    RewriteCond %{REQUEST_URI} ^/(lv|en)/.*$ [NC]

    #no need to set a cookie if you already have one with the same value
    RewriteCond %{HTTP_COOKIE} !lang=%1 [NC] 

    #rewrite to the same address, but set a cookie to the value captured in first RewriteCond
    RewriteRule . - [cookie=lang:%1:.%{HTTP_HOST}:144000:/]

– when the user requests the “root” of the portal and there is a “lang” cookie set, do a redirect to the particular language root page

  #only for the root page /
  RewriteCond %{REQUEST_URI} ^/$ [NC]

  #only if you have a "lang" cookie
  RewriteCond %{HTTP_COOKIE} lang=(en|lv) [NC] \

  #redirect (not rewrite!) to either /en/ or /lv/
  RewriteRule ^(.*)$ /%1/ [R=302,L]

So, after all modifications my .htaccess file looks like this:

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /

RewriteCond %{REQUEST_URI} ^/$ [NC]
RewriteCond %{HTTP_COOKIE} lang=(lv|en) [NC] 
RewriteRule ^(.*)$ /%1/ [R=302,L]

RewriteCond %{REQUEST_URI} ^/(lv|en)/.*$ [NC] 
RewriteCond %{HTTP_COOKIE} !lang=%1 [NC] 
RewriteRule . - [cookie=lang:%1:.%{HTTP_HOST}:144000:/]

RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

Kārtošana latviešu valodā ar Microsoft SQL Server

Jau kopš senām Microsoft SQL Server versijām ir bijusi problēma, ka latviešu valodas teksti tiek kārtoti nepareizi. Proti, pat tad, ja datubāzei iestatīta “Latviešu” valoda, netiek šķiroti īsie un garie burti (šeit ar “latviešu” saprotu tādas kolācijas kā, piemēram, ‘Latvian_CI_AS’ vai ‘SQL_Latvian_Cp1257_CI_AS_KI_WI’). Līdz ar to sarakstu bērziņš, avotiņš, āboliņš SQL serveris kārtoja kā āboliņš, avotiņš, bērziņš. Kā zinām no ābeces mācīšanās, latviešu alfabēts ir A, Ā, B, C, Č..., tas ir, diakritiskās zīmes ir atsevišķas un tās seko aiz tā paša burta bez diakritiskās zīmes, tāpēc pareizi būtu bijis avotiņš, āboliņš, bērziņš.

Kopš SQL server 2008 ir pieejama vēl viena kolāciju saime – tādas, kas sākas ar Latvian_100, piemēram, Latvian_100_CI_AS. Šajās kolācijās gan viss ir salabots un kārtošana tiešām strādā pareizi:

select VARDS from (
        select 'Bērziņš' as VARDS union
        select 'Āboliņš' as VARDS union
        select 'Avotiņš' as VARDS) as SAMPLES
order by VARDS collate Latvian_100_CI_AS

rezultāts ir

VARDS
-------
Avotiņš
Āboliņš
Bērziņš

Papildus informāciju var pasmelties MSDN dokumentācijā.

InfoPath form cleanup for faster load times

My team has been working on an InfoPath 2007 form (using Visual Studio Tools for Applications) for a while. The form is bound to an XML structure having approximately 100 fields/properties. Since its initial build, the form has been modified, recompiled and re-packaged for quite a lot of times.

Users complained that form load times are quite long. It seemed that by every version they are getting even worse. The initial thought was that the delay was due to loading of SharePoint data (through secondary data sources). However, profiling showed that it takes less than a second to load them.

Recently, we found out that by each build the manifest.xsf file becomes larger and larger. The initial version was less than 100K, while the current one is 500K. The reason for such growth was – automatically generated “InfoPath variables” in the form. Something like this:

 <xsf:xmlToEdit name="SomeFieldName_3721" item="/my:SomeContainer/my:SomeSubNode/my:SubSubNode">
    <xsf:editWith autoComplete="no" component="xField"></xsf:editWith>
 </xsf:xmlToEdit>

The variables can be later referenced in InfoPath’s own XSLT views as well as in context menu definitions in the manifest.xsf. However, these variables are never cleaned up. The list is growing each time you save your form using form designer.

It seems that Infopath initializes each variable upon form load. The more variables you have, the longer it takes to load the form.

When there are thousands of such variables, cleanup is not very easy. Hence, I wrote a PowerShell script to do the job. It enumerates all the variables and looks for references to this variable in manifest.xsf and in all .xsd views of your InfoPath form.

Download the InfoPath manifest.xsf clean-up script here.

Document title in SharePoint’s “ECB” dropdown menu

Have you ever wondered why the SharePoint team decided to show only filename in dropdown menus, but not the title of the document? So when you save your file with an unreadable filename, it is displayed in your list and looks something like this (Historical note: it was not the case in SharePoint 2001. They changed it in SPPS2003 and in 2007 versions. 2010. has it fixed):

You can, of course, modify the “All Items” view and add the “Title” column, but it never has a link to the document, nor has it the fancy drop-down for editing (see the column “Title” in the picture).

So, I decided to dig into CAML schemas and to create a field which would display the title of the document and have the dropdown menu. To achieve that, one can use “computed fields” – fields which do not actually represent data editable by the user, but are only used for presentational purposes. Hence, one only has to define the DisplayPattern of that field.
There are two fields already built into SharePoint – the _EditMenuTableStart and _EditMenuTableEnd, which build the dropdown menu contents. What I had to add, was generation of the readable content. The most important part of the display pattern is this:

<IfEqual>
  <Expr1><LookupColumn Name="Title" /></Expr1>
 <Expr2></Expr2>
<Then>
     <Field Name="FileLeafRef" />
 </Then>
 <Else>
     <Column HTMLEncode="FALSE" Name="Title" Default="(no title)" />
  </Else>
</IfEqual>

It compares the value of the Title column to an empty string. If this is the case (you have uploaded a document with no title or created a folder using Explorer view), it just shows FileLeafRef – the filename. Otherwise, it displays the value of the title field.

In my case I had an existing document library which I wanted to “fix”. Hence, I created a PowerShell script, which adds my field to the library. I used “AddFieldAsXML()” method of SPList object to inject my CAML code into sharepoint.

Now it looks like this:

You can dowload the whole script here: http://tips.naivist.net/wp-content/CreateTitleField1033.ps1_.txt Rename it to .ps1 and run it .\CreateTitleField1033.ps1 -url http://yourserver/sites/somesite/someweb -ListName "Your list". Then modify the view settings to display the field.

If you are developing your own list defintion, you can use the particular fragment in schema.xml and have your field defined in a regular fashion.

param(
  [string] $url      =  "",
  [string] $ListName = ""
)

$sharepoint = [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
$site=[Microsoft.Sharepoint.SPSite]($url);
$web=$site.openweb();
$list= $web.lists[$ListName];

$schema =@"
    <Field ID="{E2ABF8D3-6435-4773-A3D6-67508FEB7CF5}" ReadOnly="TRUE" 
             Type="Computed" 
             Name="KRItemLinkTitleDropdown" 
             DisplayName="Title" 
             DisplayNameSrcField="Title"
             AuthoringInfo="(dropdown displaying the title field)"
             EnableLookup="TRUE" 
             SourceID="http://schemas.microsoft.com/sharepoint/v3"
             StaticName="KRItemLinkTitleDropdown" FromBaseType="TRUE" ClassInfo="Menu">
         <FieldRefs>
          <FieldRef Name="Title" />
          <FieldRef Name="LinkTitleNoMenu" />
          <FieldRef Name="FSObjType" />
        </FieldRefs>
        <DisplayPattern>
           <Field Name="_EditMenuTableStart" />
           <HTML>
           <![CDATA[<A onfocus="OnLink(this)" HREF="]]></HTML>
       <IfEqual>
            <Expr1>
              <LookupColumn Name="FSObjType" />
            </Expr1>
            <Expr2>1</Expr2>
            <Then>
                <FieldSwitch>
                <Expr>
                  <GetVar Name="RecursiveView" />
                </Expr>
                <Case Value="1">
                  <LookupColumn Name="FileLeafRef" HTMLEncode="TRUE" />
                </Case>
                <Default>
                  <SetVar Name="UnencodedFilterLink">
                    <SetVar Name="RootFolder"><HTML>/</HTML>
                      <LookupColumn Name="FileRef" />
                    </SetVar>
                    <SetVar Name="FolderCTID">
                      <FieldSwitch>
                        <Expr>
                          <ListProperty Select="EnableContentTypes" />
                        </Expr>
                        <Case Value="1">
                          <Column Name="ContentTypeId" />
                        </Case>
                      </FieldSwitch>
                    </SetVar>
                    <FilterLink Default="" Paged="FALSE" />
                  </SetVar>
                  <GetVar Name="UnencodedFilterLink" HTMLEncode="TRUE" />
                </Default>
              </FieldSwitch>     
             </Then>
            <Else>
                <Field Name="ServerUrl" URLEncodeAsURL="TRUE" />
            </Else>
            </IfEqual>


       <HTML><![CDATA[" onclick="return DispEx(this,event,']]></HTML>
                  <ScriptQuote NotAddingQuote="TRUE">
                    <ServerProperty Select="HtmlTransform" />
                  </ScriptQuote><HTML><![CDATA[',']]></HTML>
                  <ScriptQuote NotAddingQuote="TRUE">
                    <ServerProperty Select="HtmlTrAcceptType">
                      <Column Name="File_x0020_Type" />
                    </ServerProperty>
                  </ScriptQuote><HTML><![CDATA[',']]></HTML>
                  <ScriptQuote NotAddingQuote="TRUE">
                    <ServerProperty Select="HtmlTrHandleUrl">
                      <Column Name="File_x0020_Type" />
                    </ServerProperty>
                  </ScriptQuote><HTML><![CDATA[',']]></HTML>
                  <ScriptQuote NotAddingQuote="TRUE">
                    <ServerProperty Select="HtmlTrProgId">
                      <Column Name="File_x0020_Type" />
                    </ServerProperty>
                  </ScriptQuote><HTML><![CDATA[',']]></HTML>
                  <ScriptQuote NotAddingQuote="TRUE">
                    <ListProperty Select="DefaultItemOpen" />
                  </ScriptQuote><HTML><![CDATA[',']]></HTML>
                  <ScriptQuote NotAddingQuote="TRUE">
                    <MapToControl>
                      <Column Name="HTML_x0020_File_x0020_Type" /><HTML>|</HTML>
                      <Column Name="File_x0020_Type" />
                    </MapToControl>
                  </ScriptQuote><HTML><![CDATA[',']]></HTML>
                  <ScriptQuote NotAddingQuote="TRUE">
                    <Column Name="HTML_x0020_File_x0020_Type" />
                  </ScriptQuote><HTML><![CDATA[',']]></HTML>
                  <ScriptQuote NotAddingQuote="TRUE">
                    <ServerProperty Select="GetServerFileRedirect">
                      <Field Name="ServerUrl" /><HTML>|</HTML>
                      <Column Name="HTML_x0020_File_x0020_Type" />
                    </ServerProperty>
                  </ScriptQuote><HTML><![CDATA[',']]></HTML>
                  <ScriptQuote NotAddingQuote="TRUE">
                    <Column Name="CheckoutUser" />
                  </ScriptQuote><HTML><![CDATA[',']]></HTML>
                  <ScriptQuote NotAddingQuote="TRUE">
                    <UserID AllowAnonymous="TRUE" />
                  </ScriptQuote><HTML><![CDATA[',']]></HTML>
                  <ScriptQuote NotAddingQuote="TRUE">
                    <ListProperty Select="ForceCheckout" />
                  </ScriptQuote><HTML><![CDATA[',']]></HTML>
                  <ScriptQuote NotAddingQuote="TRUE">
                    <Field Name="IsCheckedoutToLocal" />
                  </ScriptQuote><HTML><![CDATA[',']]></HTML>
                  <ScriptQuote NotAddingQuote="TRUE">
                    <Field Name="PermMask" />
                  </ScriptQuote><HTML><![CDATA[')">]]></HTML>
                  <UrlBaseName HTMLEncode="TRUE">
                  </UrlBaseName>
                    <IfEqual>
                    <Expr1>
                      <LookupColumn Name="Title" />
                    </Expr1>
                    <Expr2></Expr2>
                    <Then>
                        <Field Name="FileLeafRef" />
                     </Then>
                    <Else>
                        <Column HTMLEncode="FALSE" Name="Title" Default="(no title)" />
                    </Else>
                    </IfEqual>
                  <IfEqual>
                    <Expr1>
                      <GetVar Name="ShowAccessibleIcon" />
                    </Expr1>
                    <Expr2>1</Expr2>
                    <Then><HTML><![CDATA[<img src="/_layouts/images/blank.gif" class="ms-hidden" border=0 width=1 height=1>]]></HTML>
                    </Then>
                  </IfEqual><HTML><![CDATA[</A>]]></HTML>
                  <IfNew Name="Created_x0020_Date"><HTML><![CDATA[<IMG SRC="/_layouts/1033/images/new.gif" alt="New!">]]></HTML>
                  </IfNew>
                  <Field Name="_EditMenuTableEnd" />
        </DisplayPattern>
      </Field>
"@
$fieldname = $list.fields.AddFieldAsXml($schema);
$list.update();

String concatenation versus array.join() in JavaScript

To answer a question on StackOverflow, I made an experiment which actually qualifies for a separate blog post.

So, I compared the speed of str1+str2 concatenation and array.push(str1, str2).join() methods. The code I used was quite simple:

var iIterations =800000;
var d1 = (new Date()).valueOf();
str1 = "";
for (var i = 0; i<iIterations; i++) str1 = str1 + Math.random().toString();
var d2 = (new Date()).valueOf();
log("Time (strings): " + (d2-d1));

var d3 = (new Date()).valueOf();
arr1 = [];
for (var i = 0; i<iIterations; i++)arr1.push(Math.random().toString());
var str2 = arr1.join("");
var d4 = (new Date()).valueOf();
log("Time (arrays): " + (d4-d3));

I tested it in IE8 and FireFox 3.5.5, both on a Windows 7 x64.

In the beginning I tested on small number of iterations (some hundred, some thousand items). The results were unpredictable (sometimes string concatenation took 0 milliseconds, sometimes it took 16 milliseconds, the same for array joining).

When I increased the count to 50’000, the results were different in different browsers – in IE the string concatenation was faster (94 milliseconds) and join was slower(125 milliseconds), while in Firefox the array join was faster (113 milliseconds) than string joining (117 milliseconds).

Then I increased the count to 500’000. Now the array.join() was slower than string concatenation in both browsers: string concat 937ms in IE, 1155 ms in Firefox, array join 1265 in IE, 1207 in Firefox.

Maximum iteration count I could test in IE without having “the script is taking too long to execute” was 850’000. Then IE was 1593 for string concatenation and 2046 for array join, Firefox had 2101 for string concatenation and 2249 for array join.

Results – if the number of iterations is small, you can try to use array.join(), as it might be faster in Firefox. When the number increases, the string1+string2 method is faster.

UPDATE
I performed the test on IE6 (WindowsXP). The process stopped to respond immediately and never ended, if I tried the test on more than 100’000 iterations. On 40’000 iterations the results were

Time (strings): 59175 ms
Time (arrays): 220 ms

This means – if you need to support IE6, choose array.join() which is way faster than string concatenation.

Restoring the top navigation bar of a SharePoint site

It’s a known issue that if you delete the “Top Navigation” menu through the API in SharePoint 2007, you cannot restore it easily. I came across this problem in one of my development sites. When looking for a solution, I found this article, saying you have to do it through the database.

The code below solves the problem programmatically (and yes, it changes the database directly). Of course, it’s an unsupported solution – if you use it, you may get in trouble with Microsoft official support.

string sSiteUrl = "http://moss/sites/yoursite/";
//after the code gets executed, we have to dispose the 
//spweb object, since the Navigation object of the current SPWeb instance will contain wrong information
using (SPSite oSite = new SPSite(sSiteUrl))
{
    using (SPWeb oWeb = oSite.RootWeb)
    {
        //check if the top navigation bar is already there
        SPNavigationNode oTopnav = null;
        try
        {
            oTopnav = oWeb.Navigation.GetNodeById(1002);
        }
        catch (Exception)
        {
            oTopnav = null;
        }

    if (oTopnav == null)
    {
        //we've found out there is no top navigation bar. Let's create one;

        //we create a temporary navigation node pointing back to web site root
        Microsoft.SharePoint.Navigation.SPNavigationNode oTempNode = new Microsoft.SharePoint.Navigation.SPNavigationNode("Navigation", oWeb.ServerRelativeUrl);
        //we add this node to the "global" collection (where quick launch and top nav bar normally live)
        oWeb.Navigation.GlobalNodes.AddAsLast(oTempNode);

        //now the dirty work - go to the database and change the ID of the navigation node
        System.Data.SqlClient.SqlConnection oConn = new System.Data.SqlClient.SqlConnection(oSite.ContentDatabase.DatabaseConnectionString);
        System.Data.SqlClient.SqlCommand oCmd = new System.Data.SqlClient.SqlCommand();
        oCmd.Connection = oConn;
        oCmd.CommandType = System.Data.CommandType.Text;
        //we only update the node which has the same ID as the one we just created, but, to be completely sure that 
        //nothing else gets changed, we add a requirement to have the same siteid and webid
        oCmd.CommandText = @"UPDATE NavNodes SET Eid=1002 WHERE (Eid=" + oTempNode.Id.ToString() + 
             @") AND (SiteId='" + oSite.ID.ToString("D") + "') AND (WebId='" + oWeb.ID.ToString("D") + "')"; ;
        oConn.Open();
        oCmd.ExecuteNonQuery();
        //closing the connection
        oConn.Dispose();
    }
}

} //the oSite object is disposed here

“_vti_bin” folder defined

Ever wondered why SharePoint “lists web service” (i.e., lists.asmx) lives in an interesting address of http://mossserver/sites/somesite/_vti_bin/lists.asmx ?

This is actually not the only place where you can see the magic _vti_something. I remember myself deleting such directories from “wwwroot” quite often back in the days “everyone” used MS FrontPage. FrontPage used to create the folders “_vti_bin”, “_vti_cnf” on your computer locally as you developed a website .. and also on the server, if you used “Frontpage Extensions” for publishing. The _bin folder would normally contain executables required for FP server components (as far as I understand, something like cgi_bin).

So, even though FP is announced dead, guys from the SharePoint team apparently like the idea of puting executables in the _vti_bin folder which was used by FrontPage Extensions.

Anyway, the question is, what does the acronym “VTI” mean. Apparently, Microsoft did not develop MS FrontPage from scratch, they acquiredVermeer Technologies Inc”. Thus the name of “bin” folder in FrontPage. Thus the URL for lists.asmx in SharePoint.

MOSS & custom master pages

I just found an interesting “feature” in MOSS publishing infrastructure. We’re designing a custom master page for our MOSS portal solution. If I recall correctly, we started with Heather Solomon’s minimal master page, but I’m not really sure.

We swiped out all of the built-in styles and layouts to achieve maximum flexibility and stylability of MOSS infrastructure. I believe this is the best choice when you have to adapt your portal to custom tailored design layout.

Well, the result was, the portal worked fine with our master page, BUT the content query web part failed. It failed every time we enabled the RSS feed feature. Instead of seeing the actual content, it showed only an error message (in Latvian, since we’re using the latvian language pack) “Šo Web daļu nevar parādīt. Lai novērstu problēmu, atveriet šo Web lapu ar Windows SharePoint Services saderīgā HTML redaktorā, piemēram, Microsoft Office SharePoint Designer. Ja problēma netiek novērsta, sazinieties ar Web servera administratoru.”

No traces could be found in the ULS log nor in the event log of the server. After setting the diagnostic logging of all categories to verbose, finally an exception was written to the logfile:

 Error while executing web part: System.Xml.Xsl.XslTransformException: An error occurred during a
 call to extension function 'RegisterFeedUrl'. See InnerException for a complete description of the error. ---> 
 System.Web.HttpException: The control collection cannot be modified during DataBind, 
 Init, Load, PreRender or Unload phases.     at System.Web.UI.ControlCollection.Add(Control 
 child)     at Microsoft.SharePoint.Publishing.WebControls.WebPartRuntime.RegisterFeedUrl(String url, String type)    
 --- End of inner exception stack trace ---     at ....

When looking at the RegisterFeedUrl in .Net Reflector, we found out that the code actually accesses the Header property of the Page object that is being rendered and adds HtmlLink control to the collection. It gave us a hint that probably there is something wrong with the page and header objects. And – yes, we had left out the runat="server" part of the HTML and HEAD tags. So, this is the absolute minimum you must have:

<HTML runat="server">
    <HEAD runat="server">
....

.. and, of course, the name space references and other stuff you usually put in the HTML element tag.