Wednesday, September 15, 2010

ASP.Net CSS Friendly Control adapters + Sharepoint 2007 menus

On my last Sharepoint 2007 branding project, the design firm involved handed us HTML+CSS+Javascript to help us bring their design into reality.  Unfortunately, their design was div-heavy, and getting that to render cleanly in IE 6/7/8 with Sharepoint and not break any of the editing features is problematic to say the least.  We eventually got everything looking pretty god, but implemented a static menu system initially (as the OOTB Sharepoint menu system is notoriously hard to style).  We got the OOTB quick launch (current navigation) to look like we wanted, but getting the top (global) navigation to look right was another story.  The designs from the design firm used DIV/UL/LI for the whole menu and it look right – why couldn’t we just get Sharepoint to output that?  Enter the Asp.Net CSS Friendly Control Adapters project.

We had a few issues to work around to get that approach to work.  The first was that the adapters are designed to change the rendering for all controls of a specific type.  If we targeted it at Microsoft.SharePoint.WebControls.AspMenu (the Sharepoint class that extends System.Web.UI.WebControls.Menu), it would render our left navigation as UL/LIs as well, and we already had that all styled how we wanted with the normal rendering.  Second, the normal way to enable the adapters is to use a .browser file, and the group doing our deployment didn’t want to copy files to the web app directories (though they were fine with deploying our code to the GAC).

Our first attempt at making this work was to create a custom subclass of Menu (as AspMenu is sealed), use that class for our top navigation, and use code-behind on our master page (the OnInit event) to target the adapter to our new class.  The problems there were twofold:  First, the first request wouldn’t use the adapter.  Secondly, it wasn’t AspMenu, so there were a few things it did just a little bit different.  To ‘resolve’ the first, we just added a redirect back to the page the user was requesting.  The second request would have the adapter applied, and since Sharepoint is so notoriously slow to startup, no one would notice it anyway.  Resolving the second was harder, especially since we couldn’t use Reflector to see what the secret sauce in AspMenu was.  This is the code we were trying to use: (“MyMenuClass” is the fully-qualified reference to our subclass of Menu).

protected override void OnInit(EventArgs e)  
{
    //this will apply custom CSS Adapter to the browser so the ASP Menu will render in UL/LI vs crazy Tables
   if ((System.Web.HttpContext.Current.Request.Browser.Adapters["MyMenuClass"] == null) ||
        string.IsNullOrEmpty(System.Web.HttpContext.Current.Request.Browser.Adapters["MyMenuClass"].ToString())) {
        System.Web.HttpContext.Current.Request.Browser.Adapters["MyMenuClass"] = "CSSFriendly.MenuAdapter";
        //redirect back to itself since on first request this is not initialized properly
        Response.Redirect(Request.Url.ToString());
    }
    base.OnInit(e);
}

One day last week, I got tasked with trying another approach. I found some code that discussed ‘hacking’ the adapter into a control (http://www.codeproject.com/Articles/83319/Taking-Control-of-Your-Control-Adapters.aspx), and thought I’d give it a shot.  I changed our custom control to inherit from UserControl instead of Menu, created an child AspMenu control within it, and used logic from the above link to force the adapter I wanted to be used to render the menu.

public class Menu : UserControl
{
    AspMenu _inner = new AspMenu();
 
    // note: implement other properties as passthroughs here too
    public string DataSourceID { get { return _inner.DataSourceID; } set { _inner.DataSourceID = value; } }
 
    protected override void CreateChildControls()
    {
        base.CreateChildControls();
        this.Controls.Add(_inner);
    }
 
    protected override void OnPreRender(EventArgs e)
    {
        base.OnPreRender(e);
 
        // technique from http://www.codeproject.com/Articles/83319/Taking-Control-of-Your-Control-Adapters.aspx
        var pi = typeof(Control).GetProperty("Adapter", BindingFlags.NonPublic | BindingFlags.Instance);
        var fi = typeof(Control).GetField("_adapter", BindingFlags.NonPublic | BindingFlags.Instance);
        var fiControl = typeof(ControlAdapter).GetField("_control", BindingFlags.NonPublic | BindingFlags.Instance);
 
        var ca = pi.GetValue(_inner, null) as ControlAdapter; //bait ...
 
        var asm = Assembly.Load("CSSFriendly");
        var type = asm.GetType("CSSFriendly.MenuAdapter");
        var newCA = (ControlAdapter)Activator.CreateInstance(type);
        fiControl.SetValue(newCA, _inner);
        fi.SetValue(_inner, newCA);// ...and switch
    }
}

OnPreRender is where the magic happens.  It basically boils down to getting the control to load the default adapter, then pulling a bait-and-switch on it.  I left out a lot more passthrough properties – I didn’t bother to implement *all* the properties on AspMenu, just the ones we were using (and I left most of those out of this listing since they’re trivial). 

Notice the use of reflection to create the instance of the adapter.  The CSSFriendly.dll available for the Asp.Net CSS Control Adapters is unsigned. My assembly is signed and GACed (it also has feature/event recievers), so I can’t have a reference to an unsigned dll.  If we’d have needed to really integrate with it, I’d have pulled the source down and re-compiled it to be signed, but this way worked and meant I didn’t have to modify the library at all.  Now, I was using the CSSFriendly.MenuAdapter class to render my AspMenu and all was good, right?  That would have been too easy.

It turns out that AspMenu has SharepointPermission CAS demands, and since CSSFriendly isn’t GACed (giving it full-trust), I have to spell out the permissions it needs in my WSP’s manifest.xml.  I added this chunk of XML at the end of my <Solution/> tag.

<!-- based on http://www.bluedoglimited.com/SharePointThoughts/ViewPost.aspx?ID=249 -->
<CodeAccessSecurity>
  <PolicyItem>
    <PermissionSet class="NamedPermissionSet" version="1" Description="Permission set for CSSFriendly">
      <IPermission class="AspNetHostingPermission" version="1" Level="Minimal" />
      <IPermission class="SecurityPermission" version="1" Flags="Execution" />
      <IPermission class="Microsoft.SharePoint.Security.SharePointPermission, 
                          Microsoft.SharePoint.Security, version=11.0.0.0, 
                          Culture=neutral, PublicKeyToken=71e9bce111e9429c" 
                   version="1" ObjectModel="True" />
    </PermissionSet>
    <Assemblies>
      <Assembly Name="CSSFriendly" />
    </Assemblies>
  </PolicyItem>
</CodeAccessSecurity>

And with that change in place, we can use the CSSFriendly.MenuAdapter class to render just the one AspMenu instance I wanted to, all without having to have an administrator modify any files on the server (or deploy anything across the farm besides the WSP).