29 January 2012

IE7 and bulleted lists' margins

For a recent project, we noticed that IE7 was giving us fits with unordered lists: It was "hiding" the actual bullet of the item, but showed the text. Some research revealed that it was flushing everything to the left because we had this in our CSS:
#main-content ul{ margin-left:0 }
The solution? Use the hack to reference only IE7 (the hash tag):
#main-content ul{
 margin-left:0;
 /* 
  The next two lines are only visible to IE7. This fixes the 
  issue of IE7 not showing the bulleted items; they were 
  flushed left so far that the bullet was mostly hidden. 
  All other browsers ignore the next two lines.
 */
 #margin:0 5px 0 42px !important; 
 #padding:0; 
}

27 October 2011

SharePoint: Get parent site title

In SharePoint, we can use subsites to handle navigation beyond the 2nd level; let's assume we have a structure such as this:
  • Site Collection Root
    • Site 1: site title - "Products & Software"
      • Subsite A: site title - "Widgets"
        • Subsite AA: site title - "Widget X"
        • Subsite BB: site title - "Widget Y"
    • Site 2
    • Site 3
It often makes sense to show Site 1's title "Products & Software" down that branch of subsites, regardless of how deep you are. This helps users know they're still under "Products & Software". To fetch the root parent's title (Site1, not the Site Collection Root), you have to use a recursive function, thanks to W0ut and help from Stack Exchange:
protected override void OnLoad(EventArgs e)
{
  // To ensure page behaves correctly, must call base.OnLoad(e).
  base.OnLoad(e);

  GetRootTitle();
}

private void GetRootTitle()
{
  string title = string.Empty;

  /* 
     On admin pages, this label object doesn't exist and will throw
     a NullReferenceException if we don't check it before trying
     to use it.
  */
  if (sectionTitle == null) return;

  using (SPSite site = new SPSite(SPContext.Current.Site.ID))
  {
    using (SPWeb web = site.OpenWeb(SPContext.Current.Web.ID))
    {
      title = IterateThroughParentsAndStoreInfo(web, title);
    }
  }

  sectionTitle.Text = title; // Set a label's value to the title.
}

private string IterateThroughParentsAndStoreInfo(SPWeb web, string title)
{
  if (web.ParentWeb != null)
  {
    title = web.Title;
    return IterateThroughParentsAndStoreInfo(web.ParentWeb, title);
  }
  return title;
}
Note the using statements; these ensure there are no memory leaks by disposing of the objects properly and also implicitly handling the try/catch/finally block. The change to W0ut's code is the addition of the OnLoad() handler, which can be either in your master page or page layout. Also, we've added the title variable, which gets set in the recursive function; note that its value is always of the title for the site one before the last. This is because when the web variable's ParentWeb becomes null, we've reached the site collection. This is exactly what we need.

One last thing to note: We do a null check for the label object which will hold the title's value. On some administrative (system) pages, the label isn't rendered so we'll throw a NullReferenceException. Another way to do this is to always add the title label as a new Label(); this way it's always present, regardless of which page we're on.

19 October 2011

IE7 and "Error: Expected identifier, string or number"

IE7 (and earlier versions) have trouble with errant commas in array declarations. Take this code, provided by James Messinger on Stack Overflow:
<script type="text/javascript">
// Replace the normal jQuery getScript function with one that supports
// debugging and which references the script files as external resources
// rather than inline.
jQuery.extend({
   getScript: function(url, callback) {
      var head = document.getElementsByTagName("head")[0];
      var script = document.createElement("script");
      script.src = url;

      // Handle Script loading
      {
         var done = false;

         // Attach handlers for all browsers
         script.onload = script.onreadystatechange = function(){
            if ( !done && (!this.readyState ||
                  this.readyState == "loaded" || this.readyState == "complete") ) {
               done = true;
               if (callback)
                  callback();

               // Handle memory leak in IE
               script.onload = script.onreadystatechange = null;
            }
         };
      }

      head.appendChild(script);

      // We handle everything using the script element injection
      return undefined;
   }, // <-- Unneeded comma which blows up IE7.
});
</script>
IE7 blows up with this error:
Error: Expected identifier, string or number
It took quite a bit of debugging and Google searches to finally understand where the problem was. Note the last comma, after the next-to-last closing curly brace "}," -- this is unnecessary. Modern browsers can handle this, but IE7 chokes. Simply remove the comma and you're good to go. More on this problem on Stack Exchange.

How to hide/show table rows in IE7

If you need to hide a table row (TR), you'd usually use the following:
.hide-tr { display:none; }
The problem develops when you later wish to show the row; simply setting the display to block is insufficient, as the individual cells in the row lose their positions and often get bunched next to each other. Using display:table-row works on most modern browsers, but IE7 ignores that and prefers display:inline-block. So the solution? There are a few ways to fix the problem, as indicated on Stack Exchange. The problem with using visibility:hidden is that the row still takes up space on the page and shrinking its height is a non-trivial task. Another solution involves a method less recommended, using browser and version detection via jQuery. However, it does fix the issue:
if ($.browser.msie && jQuery.browser.version == '7.0'){
  $('.hide-tr').css('display','inline-block');
}
else {
  $('.hide-tr').css('display','table-row');
}
Not a clean solution but it gets the job done :)

Update: An even easier solution: Simply call the jQuery show() method on the object and it's smart enough to apply the appropriate CSS to the object.

02 October 2011

SharePoint: Create an easy "Share page with friend" button

In SharePoint, it's useful to have an email icon that allows users to email the page to friends. Though we could build a popup box, we can also go with a simpler solution that uses the person's email client to do the heavy lifting. In another post, I covered how to fetch some of the server-side variables into a JavaScript array needed for generating this email. Now, let's use the variables and see how we send the email. One key to this feature is to keep it simple; this means, we'll leave the To field of the email blank and use the commonplace "mailto:" HREF attribute. So, let's get to the code:
function generateEmailTo(){
  var body = currentElements.currentUserName + ' has shared a page with you on the intranet.%0A%0APage Title: %22' +
    currentElements.currentTitle  + '%22%0A' + $(location).attr('href').replace('#',''); 
  var subject = currentElements.currentUserName + ' has shared an intranet page with you';
  var mailto = 'mailto: ?body=' + body + '&subject=' + subject;
  var anchor = '<a href="' + mailto + '"></a>';

  $("#send-email").wrap(anchor);
}
We can pass the body, subject, and mailto for the mail message. The ?body= and the &subject= allow the main message and the subject to be passed in the querystring. To provide line breaks, we use %0A hex values; so for two line breaks, we use %0A%0A in the querystring value. To pass quotes, we use %22. To pass a blank mailto:, we leave a space in front of it, before the ?body=. More notes on mailto, setting its cc, bcc, and special characters can be found here.

Once we've concatenated the various elements, we pass the anchor variable to the jQuery wrap() function, called on the email icon img tag (with ID of send-email). We call the above generateEmailTo() in the document.ready function. When you click the link, it produces something like this in the generated email (Outlook, Thunderbird, etc.):
Alex C has shared a page with you on the intranet. 

Page Title: "My Page Title" 
http://mysite.org/Pages/mypage.aspx
The subject, not shown, will be "Alex C has shared an intranet page with you". Note that the URL will appear as a clickable link in most email clients. Also note that we can't pass HTML to the body of the message, only plain text.

Hope you find this useful.

SharePoint: Fetch environment variables and store in JavaScript

For a header text, we needed the current site's name. In addition, to generate a "Share this page with a friend" button, we needed the current user's login name and full name. We could use SharePoint's Client Object Model, which does an asynchronous call from JavaScript to get the information.

However, we needed the site's name to be available with the DOM, not when the page loads; we tried the AJAX call and it took a second or two to fetch the data. During this time, the page had no header text saying which site the user was on.

What to do? We could use jQuery to get the site name from the breadcrumbs generated via the asp:SiteMapPath control, but there could be times when we wouldn't be using this control. Else we could fetch it from the top-left navigation tree dropdown; even that was clunky. So we decided to put the following code in the Page Layout's PlaceHolderAdditionalPageHead control; it can also be placed in the master page:
<asp:Content ContentPlaceholderID="PlaceHolderAdditionalPageHead" runat="server">
<%
String currentUserName = Microsoft.SharePoint.SPContext.Current.Web.CurrentUser.Name.ToString();
String currentUserEmail = Microsoft.SharePoint.SPContext.Current.Web.CurrentUser.LoginName.ToString().Replace("MYDOMAIN\\","");
String currentSite = Microsoft.SharePoint.SPContext.Current.Web.ToString();
String currentTitle = (Microsoft.SharePoint.SPContext.Current.Item["Title"] == null) ?
 "" : Microsoft.SharePoint.SPContext.Current.Item["Title"].ToString();

StringBuilder sb = new StringBuilder();
sb.Append("<script type='text/javascript'>var currentElements = { currentUserName: '");
sb.Append(currentUserName);
sb.Append("', currentUserEmail: '");
sb.Append(currentUserEmail);
sb.Append("', currentSite: '");
sb.Append(currentSite);
sb.Append("', currentTitle: '");
sb.Append(currentTitle);
sb.Append("'}</script>");
Response.Write(sb.ToString());
%>

....

</asp:Content>
We fetch the current user's name using the SPContext.Current property, which gives us a handle to the current HTTP request in SharePoint. Then we get the various items we need, such as the user's name, login name, web (current site name), and the publishing page's title.

Once we have the values, we plug them into a JavaScript array that we build server-side. When the page loads in the browser, the array will be instantiated with the values we set using C#. We use a StringBuilder object for optimal performance because of the string concatenations.

So what does it look like when the page loads? Here it is:
<script type='text/javascript'>var currentElements = { currentUserName: 'Alex C', currentUserEmail: 'myemailid', currentSite: 'My Site Name', currentTitle: 'Page Title'}</script>
To use any of the array elements in jQuery or elsewhere, just call it via something like this: currentElements.currentSite

A final note: If any of the values will have a single quote in them, just escape the value using a .Replace("'","\'")on the string in C#.

21 September 2011

SharePoint: Programmatically add JS/CSS to pages

John Chapman has a great article on creating a feature for your SharePoint installation that adds JavaScript and CSS files to all pages without touching the site's master page. The good folks on SharePoint.StackExchange.com helped me implement it and also modify the code to better utilize SharePoint's native control libraries.

First, follow the code provided by John Chapman. Then modify the CustomPageHead.ascx.cs to take advantage of the built-in SharePoint controls:
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;

namespace CustomPageHead.CONTROLTEMPLATES.CustomPageHead
{
public partial class CustomPageHead : UserControl
{
protected override void CreateChildControls()
{
base.CreateChildControls();

this.Controls.Add(new ScriptLink()
{
Name = "/_layouts/CustomPageHead/jquery-1.6.2.min.js",
Language = "javascript",
Localizable = false
});

this.Controls.AddAt(0,new ScriptLink()
{
Name = "/_layouts/CustomPageHead/some-custom-code.js",
Language = "javascript",
Localizable = false
});

this.Controls.AddAt(1,new CssRegistration()
{
Name = "/_layouts/CustomPageHead/some-stylesheet.css"
});
}
}
}
Note that using the native controls, you don't have to worry about paths to where the files will be; SharePoint will place these items in the following location on your server:
C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS\CustomPageHead
Also, the advantage of using the this.Controls.AddAt() method is that you can specify where in the object hierarchy to add the specified object.

In addition, the solution can be generated into a *.WSP by selecting "Package" from Visual Studio's Build menu. The file can then be copied from the bin/Release folder and run on the command line to install the feature.

Special thanks goes to omlin and James Love for their help.