Option to Show the SharePoint 2010 ribbon when hidden by default

At work we’ve been working on a new intranet project with SharePoint 2010 being our platform of choice. This site is mainly a view only type of site but we also wanted users to have the ability to create alerts and other similar activities. One of the requirements of this project was to remove the ribbon on every page. This brought complications for SP 2010 because so much is done in the ribbon. We decided to hide the ribbon by default and show a caret (or chevron) at the upper right of the screen to allow the ribbon to be shown and hidden by all users.

There are many solutions I’ve seen on the interwebs around hiding the ribbon for some and not hiding it for others so they were a good starting point for us but I couldn’t find anyone talking about having the ribbon being optional for everyone. This brought a few challenges such as keeping the ribbon shown on postbacks and displaying the ribbon when clicking on a list webpart.

The first challenge was easy to accomplish. Using a cookie to set whether the user currently has the ribbon open or closed allows the ribbon to “remember” that setting as a user is browsing and editing items in SP.

 var val = 'False';
 try { val = GetCookie('ShowHideRibbon'); } catch (err) { }
 var showRibbon = document.getElementById('ShowRibbon');
 var hideRibbon = document.getElementById('HideRibbon');
 var ribbon = document.getElementById('s4-ribbonrow');

 if (val == 'True') {
  ribbon.style.display = 'block'; ;
  if (showRibbon != null)
   showRibbon.style.display = 'none';
  if (hideRibbon != null)
   hideRibbon.style.display = 'block';
 }
 else {
  if (hideRibbon != null)
   hideRibbon.style.display = 'none';
  if (showRibbon != null)
   showRibbon.style.display = 'block';
 }

The second issue was more difficult and wasn’t initially obvious. The issue was that in SP 2010, when you click on a list view webpart it automatically activates the ribbon. This is great when using the checkboxes to select items or documents. Normal behavior is when the ribbon is activated it will hide the site level breadcrumbs / title bar and the top navigation to use that space for the ribbon itself. But with the ribbon being hidden, this causes the whole top part of the page to disappear including our caret to reopen the ribbon.

This resulted in a very bad user experience. I spent a lot of time debugging JavaScript trying to find the right SP JS function to “override” in order to tap into this action and unhide the ribbon first before the ribbon is activated. I got no where asking the google and ended up searching all of the SharePoint JS debug files and finally finding the function OnRibbonMinimizedChanged in init.js.

 function OnRibbonMinimizedChanged(ribbonMinimized)
 {ULSxSy:;
  var ribbonElement=GetCachedElement("s4-ribbonrow");
  var titleElement=GetCachedElement("s4-titlerow");
  if (ribbonElement)
  {
   ribbonElement.className=ribbonElement.className.replace("s4-ribbonrowhidetitle", "");
   if(titleElement)
   {
    titleElement.className=titleElement.className.replace("s4-titlerowhidetitle", "");
    if (ribbonMinimized)
    {
     titleElement.style.display="block";
    }
    else
    {
     titleElement.style.display="none";
    }
   }
  }
...

I overrode the function with my own where I unhide the ribbon and then called the original function in init.js.

 function SetupEventHandler_HideShowRibbon() {
  if (original_functionEvent == null) {
   original_functionEvent = OnRibbonMinimizedChanged;
  }
  OnRibbonMinimizedChanged = OnRibbonMinimizedChanged_Delegate;
 }

 function OnRibbonMinimizedChanged_Delegate(ribbonMinimized) {
  if (IsRibbonHidden_HideShowRibbon() &&
   false == ribbonMinimized) {
   showRibbon__HideShowRibbon();
  }
  original_functionEvent(ribbonMinimized);
 }

I put all of my JS code into a user control which I added to my masterpage inside a SP delegate control. This allows me to use features to swap this code out with something different. For instance you could have a user control which can hide the ribbon by default and only show the caret / chevron when a user has certain permissions.

<%@ Control Language="C#"    compilationMode="Always" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="uc" TagName="ShowHideRibbonCore" src="~/_controltemplates/Custom/ShowHideRibbon.ascx" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<!-- Show / Hide Ribbon -->
<Sharepoint:SPSecurityTrimmedControl ID="SecurityTrimHideShowRibbonCurrentItem" runat="server" PermissionsString="AddAndCustomizePages" PermissionContext="CurrentItem">
    <uc:ShowHideRibbonCore ID="HideShowRibbonCoreCurrentItem" runat="server"/>
</Sharepoint:SPSecurityTrimmedControl>
<Sharepoint:SPSecurityTrimmedControl ID="SecurityTrimHideShowRibbonCurrentWeb" runat="server" PermissionsString="AddAndCustomizePages" PermissionContext="CurrentSite">
    <uc:ShowHideRibbonCore ID="HideShowRibbonCoreCurrentSite" runat="server"/>
</Sharepoint:SPSecurityTrimmedControl>

 

The code that we ended up doing is below. We hide the ribbon using CSS in the user control and then output the javascript.

<%@ Control Language="C#"    compilationMode="Always" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="uc" TagName="ShowHideRibbon" src="~/_controltemplates/Custom/ShowHideRibbon.ascx" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<!-- Hide Ribbon -->
<style type="text/css">
#s4-ribbonrow
{
    display:none;
}
</style>
<uc:ShowHideRibbonCore ID="HideShowRibbon" runat="server"/>

And here is my full code that I put in the user control ShowHideRibbon.ascx

<%@ Control Language="C#"    compilationMode="Always" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<!-- Show / Hide Ribbon Core -->
<script type="text/javascript">
    if (testForRibbon_HideShowRibbon == null) {
        var original_functionEvent = null;

        var testForRibbon_HideShowRibbon = function () {

            if (IsRibbonHidden_HideShowRibbon()) {
                ExecuteOrDelayUntilScriptLoaded(SetupEventHandler_HideShowRibbon, "init.js");

                var val = 'False';
                try { val = GetCookie('ShowHideRibbon'); } catch (err) { }
                var showRibbon = document.getElementById('ShowRibbon');
                var hideRibbon = document.getElementById('HideRibbon');
                var ribbon = document.getElementById('s4-ribbonrow');

                if (val == 'True' ||
                   (IsRibbonHidden_HideShowRibbon() &&
                    IsTitleRowHidden_HideShowRibbon())) {
                    ribbon.style.display = 'block';
                    if (showRibbon != null)
                        showRibbon.style.display = 'none';
                    if (hideRibbon != null)
                        hideRibbon.style.display = 'block';
                }
                else {
                    if (hideRibbon != null)
                        hideRibbon.style.display = 'none';
                    if (showRibbon != null)
                        showRibbon.style.display = 'block';
                }
            }
        }

        function IsRibbonHidden_HideShowRibbon() {
            return IsElementHidden_HideShowRibbon('s4-ribbonrow');
        }

        function IsTitleRowHidden_HideShowRibbon() {
            return IsElementHidden_HideShowRibbon('s4-titlerow');
        }

        function IsElementHidden_HideShowRibbon(elementId) {
            var computedStyle = function (el, style) {
                var cs;
                if (typeof el.currentStyle != 'undefined') {
                    cs = el.currentStyle;
                } else {
                    cs = document.defaultView.getComputedStyle(el, null);
                }
                return cs[style];
            }
            var element = document.getElementById(elementId);
            if (element != null &&
				computedStyle(element, 'display') == 'none') {
                return true;
            }
            return false;
        }

        function SetupEventHandler_HideShowRibbon() {
            if (original_functionEvent == null) {
                original_functionEvent = OnRibbonMinimizedChanged;
            }
            OnRibbonMinimizedChanged = OnRibbonMinimizedChanged_Delegate;
        }

        function OnRibbonMinimizedChanged_Delegate(ribbonMinimized) {
            if (IsRibbonHidden_HideShowRibbon() &&
				false == ribbonMinimized) {
                showRibbon__HideShowRibbon();
            }
            original_functionEvent(ribbonMinimized);
        }

        function makeDoubleDelegate_HideShowRibbon(function1, function2) {
            return function () {
                if (function1)
                    function1();
                if (function2)
                    function2();
            }
        }

        function getDocHeight_HideShowRibbon() {
            var D = document;
            return Math.max(
            Math.max(D.body.scrollHeight, D.documentElement.scrollHeight),
            Math.max(D.body.offsetHeight, D.documentElement.offsetHeight),
            Math.max(D.body.clientHeight, D.documentElement.clientHeight)
        );
        }

        function showRibbon__HideShowRibbon() {
            var newHeight = getDocHeight_HideShowRibbon();
            var ribbon = document.getElementById('s4-ribbonrow');
            ribbon.style.display = 'block';
            try {
                SetCookie('ShowHideRibbon', 'True', '/');
            } catch (err) { };
            document.getElementById('ShowRibbon').style.display = 'none';
            document.getElementById('HideRibbon').style.display = 'block';

            newHeight = newHeight - ribbon.offsetHeight;
            var ver = navigator.appVersion;
            if (ver.indexOf("MSIE") != -1) {
                newHeight = newHeight - 4;
            }
            document.getElementById('s4-workspace').style.height = newHeight + 'px';
        }

        function hideRibbon__HideShowRibbon() {
            document.getElementById('s4-ribbonrow').style.display = 'none';
            try {
                SetCookie('ShowHideRibbon', 'False', '/');
            } catch (err) { };
            document.getElementById('HideRibbon').style.display = 'none';
            document.getElementById('ShowRibbon').style.display = 'block';
            var newHeight = getDocHeight_HideShowRibbon();
            var ver = navigator.appVersion;
            if (ver.indexOf("MSIE") != -1) {
                newHeight = newHeight - 4;
            }
            document.getElementById('s4-workspace').style.height = newHeight + 'px';
        }

        window.onload = makeDoubleDelegate_HideShowRibbon(window.onload, testForRibbon_HideShowRibbon);

        document.write('<a id="ShowRibbon" href="#" onclick="showRibbon__HideShowRibbon(); return false;"><img src="/_layouts/images/downarrow.png" title="Show Ribbon" border="0"/></a>');
        document.write('<a id="HideRibbon" href="#" onclick="hideRibbon__HideShowRibbon(); return false;"><img src="/_layouts/images/uparrow.png" title="Hide Ribbon" border="0"/></a>');
    }
</script>
<style type="text/css">
#ShowRibbon
{
	position:fixed;
	top:0;
	right:15px;
	display: none;
	z-index: 100;
}

#HideRibbon
{
	position:fixed;
	top:45px;
	right:15px;
	display: none;
	z-index:100;
}
</style>

Update 4-6-2012

I realized there are instances when visiting a page (like dispform.aspx) where the ribbon is supposed to be open by default but instead showed the same bad behavior from above. I’ve updated my code above to now check if s4-titlerow is hidden in addtion to the s4-ribbonrow. If so, I unhide the ribbon. But I do not cookie this change. The only time this app “remembers” your setting is if you actually click the up or down arrow at the top right.

NTLM login with Anonymous fallback

At work we run a fairly large extranet SharePoint farm where some sites are anonymous access. Normally in SharePoint, when a domain user on a domain workstation hits a SharePoint site that requires authentication, it automatically logs them in. Unfortunately, in that same scenario but the site is anonymous, the user will not get logged in. The user then has to realize this and click sign in at the top right. Also because SharePoint security trims the user interface so you only see what you have access to see.  Many times users will complain they have lost their permissions or something because they won’t be able to find their normal way of doing things when they don’t realize they aren’t logged in.

Several years ago I set out on a mission to fix this. The best solution I found was posted here: http://blogs.claritycon.com/blogs/ryan_powers/archive/2007/06/12/3187.aspx. I was able to take what he had done and modify it for SharePoint. So since authentication is tied to the session, we only need to run our code once per session. So the trick is determining when a new session is starting and running the code. I was unsuccessful in getting the SessionStateModule.Start event to fire and not fire when I needed it to so I decided to use the PostAcquireRequestState event and check on each request if I have already run my code or not. I’m using a session variable to ensure I only run the code once per session. Essentially what the code does is the first time a new session is started instead of outputting the normal html requested, the httpmodule outputs some javascript.

This javascript makes a web call to /_layouts/authenticate.aspx which is the page used to login you into SharePoint. This call is done in a way so if an error happens (such as not being able to login) that error is trapped and the end user never sees the gray login box. Whether the login is successful or not the javascript then refreshes the page. At this point the httpmodule will not change the html output because it’s already been run on the current session so the real requested html is sent and if the authentication was successful, the user is shown as being logged into the site.

Also notice I added the querystring Source=%2F%5Flayouts%2Fimages%2Fblank%2Egif to the authenticate url. Normally when authenticate.aspx is called without this querystring it will redirect the user to the current site’s homepage. Since this causes extra execution time running webparts and rendering the page as well as the end user never sees this page because the call is being done in javascript, I found it was much faster to send the user to an image after authenticating. So i chose the blank.gif which is available on any SharePoint installation. This requires much less resources on the server as well as showing the actual page faster to the browser.

I also do a few checks before I output the javascript. Since automatic ntlm login only works in IE and windows, I check the useragent for that condition. I also check to make sure the request isn’t for infopath form services (/_layouts/formserver.aspx) because we had some issues with the module not playing nice with those services.

Anyway, here’s the code:

public class MixedAuthenticationScreeningModule : IHttpModule
{
    private const string NO_SCRIPT_MESSAGE = "Your browser does not support JavaScript or has scripting disabled, which prevents credentials screening from working.";
    private string _requiresAuthenticationUrl = "/_layouts/Authenticate.aspx?Source=%2F%5Flayouts%2Fimages%2Fblank%2Egif";
 
    void IHttpModule.Init(HttpApplication context)
    {
        context.PostAcquireRequestState += new EventHandler(context_PostAcquireRequestState);
    }
 
    void context_PostAcquireRequestState(object sender, EventArgs e)
    {
        MixedModeLogin();
    }
 
    private void MixedModeLogin()
    {
        HttpContext context = HttpContext.Current;
        if (context.Session == null) return;
        if (context.Request.RequestType != "GET") return;
        if (context.Session["MixedModeAuth"] != null) return;
        context.Session["MixedModeAuth"] = false;
        if (IsWin32Ie(context) == false) return;
        if (context.Request.RawUrl.ToLower().Contains("/_layouts/formserver.aspx")) return;
        context.Session["MixedModeAuth"] = true;
        RenderScreeningHtml(context.Request.RawUrl, context.Response);
    }
 
    private bool IsWin32Ie(HttpContext context)
    {
        string userAgent = context.Request.UserAgent;
        return userAgent != null && userAgent.IndexOf("MSIE") >= 0 && userAgent.IndexOf("Windows") >= 0;
    }
 
    private void RenderScreeningHtml(string currentUrl, HttpResponse response)
    {
        string screeningFailedUrl = currentUrl;
        response.Cache.SetCacheability(HttpCacheability.NoCache);
        response.Cache.SetExpires(DateTime.Now.AddDays(-1)); //or a date much earlier than current time
        response.Write("<!--DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"--><html></html><head></head><script type="text/javascript" language="javascript">  function canAuthenticate() { try { var dom = new ActiveXObject("Msxml2.DOMDocument");  dom.async = false; dom.load("" + _requiresAuthenticationUrl + "");} catch(e) { return false; }  return true;}  canAuthenticate(); window.location.href=window.location.href; </script><noscript></noscript>");
        try
        {
            response.End();
        }
        catch { }
    }
 
    void IHttpModule.Dispose()
    {
    }
}

Here is what the user’s see when the javascript code is trying to log them in:

Here is what shows up in fiddler when someone is logged in successfully. Notice the first time default.aspx is called it’s only 763 bytes and the second time 61 KB. That’s because the first time only the javascript was sent to the browser, but the second time the whole html for the page was sent to the browser. You can also see where the authentication happens and since it was successful you can see where the blank.gif was loaded.

 

Here is what shows up in fiddler when someone is not logged in successfully, thus it falls back to anonymous. You can see that the authentication doesn’t happen and blank.gif is never called.

SPCalendarView not working as expected in SharePoint Foundation 2010

I’m currently working to make sure all of my custom code works correctly in SharePoint Foundation 2010. One of my webparts utilized the SPCalendarView and I would set the datasource to be a SPCalendarItemCollection and it would render a calendar with the events I specified in the collection. Well, this no longer works in SharePoint Foundation 2010. I was able to achieve a workaround with a little help from reflector. You’ll need to create two classes:

public class MySPCalendarDataSource : Control, System.Web.UI.IDataSource
    {
        private MyCalendarSourceView _view;
        private static readonly object EventDataSourceChanged = new object();
        private SPCalendarItemCollection _data = null;

        public MySPCalendarDataSource(SPCalendarItemCollection data)
            : base()
        {
            _data = data;
        }

        event EventHandler System.Web.UI.IDataSource.DataSourceChanged
        {
            add
            {
                base.Events.AddHandler(EventDataSourceChanged, value);
            }

            remove
            {
                base.Events.RemoveHandler(EventDataSourceChanged, value);
            }
        }

        protected DataSourceView GetView(string viewName)
        {
            return this.internalGetView(viewName);
        }

        protected ICollection GetViewNames()
        {
            return new string[] { this.internalGetView(null).Name };
        }

        private MyCalendarSourceView internalGetView(string viewName)
        {
            if (this._view == null)
            {
                this._view = new MyCalendarSourceView(this, viewName, _data);
            }
            return this._view;
        }

        private void OnDataSourceChanged(EventArgs e)
        {
            EventHandler handler = (EventHandler)base.Events[EventDataSourceChanged];
            if (handler != null)
            {
                handler(this, e);
            }
        }

        protected virtual void RaiseDataSourceChangedEvent(EventArgs e)
        {
            this.OnDataSourceChanged(e);
        }

        DataSourceView System.Web.UI.IDataSource.GetView(string viewName)
        {
            return this.GetView(viewName);
        }

        ICollection System.Web.UI.IDataSource.GetViewNames()
        {
            return this.GetViewNames();
        }        
    }
public class MyCalendarSourceView : DataSourceView
{
	SPCalendarItemCollection _data;

	public MyCalendarSourceView(System.Web.UI.IDataSource ds, string viewName, SPCalendarItemCollection data) : base(ds, viewName)
	{
		_data = data;
	}

	protected override System.Collections.IEnumerable ExecuteSelect(DataSourceSelectArguments arguments)
	{
		return _data;
	}
}

And where in SharePoint 2007 you could do this:

SPCalendarItemCollection items = GetCalendarItems();
SPCalendarView calView = new SPCalendarView();
calView.DataSource = items;
calView.DataBind();

You now have to do this:

SPCalendarItemCollection items = GetCalendarItems();
SPCalendarView calView = new SPCalendarView();
calView.EnableV4Rendering = false;
calView.DataSource = new MySPCalendarDataSource(items);
calView.DataBind();

Also, please note that for this to work, we do need to disable the V4 ajax rendering.