In this blog I'll rant about how and what menu's you can change in SharePoint. The core of this article will be about adding items to the NewMenu.
Okay, so now you know what this article is about, let me make 2 things clear.
There is a difference in Menu's.
First you have the Left Menu called Quick Launch and the Top Menu called Top Navigation. The second kind of Menus are the following: SiteActions, Menu Toolbars, Personal Actions, Edit Control Blocks, ...
To manipulate the first set, you have different ways:
- Add links in your custom schema.xml. This will make sure that every site you create has the same links.
- Add NavigationNodes to QuickLaunch and TopNavigation collections in the SPWeb object of your site. This is a way to programatically change your menus. You'll find a lot of this on the web.
- Make your own SiteMapProvider and change the datasource of those ASP Menu's. I'm really fond of this way, because then I have absolute control about who sees what and I can do a lot more than what SharePoint offers you OOB. If you need more info about this, I can give you some ;)
To manipulate the second set, you have 2 options and they both require CustomAction features:
- Use the CustomAction with the UrlAction child element. This lets you define one link to a custom application page, url, ... . The key is that you have 1 element per custom action. There are also a lot of articles about this, so I'll skip this aswell.
- Use the CustomAction with ControlClass attribute. This lets you create a custom WebControl that will enable you to control whatever you want. This is what I want !!!!
Now we have more or less all the possibilities summed, I shall now focus on the latter way to have my Menu's behave how I want.
Before I move on I need to give you the following links:
http://msdn.microsoft.com/en-us/library/ms460194.aspx
http://msdn.microsoft.com/en-us/library/bb802730.aspx
http://johnholliday.net/resources/customactions.html
They are great resources about where you can put your links and what the GroupId & Locations are.
Okay now we're ready to start.
I mentioned you can do whatever you want with the ControlClass way, but to give you an Idea these are the things I do:
- Hide Menus dynamically. Okay you can do a lot with Permissions, but sometimes it's just not that easily.
- Add SubMenus dynamically. This is what I'll show you.
First you need to understand how the Feature System works.
It all starts with some user control who has a child repeater control who gets dynamically filled with ToolBarButtons, these ToolBarButtons get filled with MenuTemplates, which can be SubMenuTemplates and MenuItemTemplates.
SubMenuTemplates can hold SubMenuTemplates and MenuItemTemplates.
MenuItemTemplates are clickable and perform some action.
When you use the CustomAction feature with the ControlClass, your control gets wrapped around a FeatureMenuTemplate, which is a different kind of MenuTemplate.
So your Location field determines which user control will load your feature and the GroupId where your feature will get loaded.
To filter your can use RegistrationType and RegistrationId attributes. This lets you define on what kind of element your feature will work on.
Back to the ToolBarButtons, those are inherited to form special buttons, examples are NewMenu and ActionsMenu. By inheriting the ToolBarButton they can add special functionality to it. For example the NewMenu behaves very differently with a Survey list. It also fucks up your CustomAction, but I'll explain it in a moment.
Okay, now you should have an idea about how your control fits the puzzle.
So what do we need ?
A feature.xml
A elements.xml
A custom dll with your custom webcontrol.
Because we want our menu to be shown at the NewMenu we have another file aswell, my FixupJavaScript.js.
For those who have written CustomActions with custom WebControls, there is nothing new. Except when they tried to add it to the NewMenu, they should have noticed that nothing gets rendered. I have a solution for this. And for those who saw there control, but it was flickery, I also have a solution. Basicly I can do this:
So here's my code:
Feature.xml:
<?xml version="1.0" encoding="utf-8" ?>
<Feature Id="{DF5009DE-15B7-4397-9933-995727F3E276}"
Title="New Template Selector"
Description="Helps chosing templates from the New Document link"
Version="1.0.0.0"
Scope="Web"
Hidden="FALSE"
xmlns="http://schemas.microsoft.com/sharepoint/" >
<ElementManifests>
<ElementManifest Location="elements.xml" />
</ElementManifests>
</Feature>
elements.xml:
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction
Id="CustomActionNewTemplateSelector"
RequireSiteAdministrator="FALSE"
GroupId="NewMenu"
Location="Microsoft.SharePoint.StandardMenu"
ControlAssembly="MyDLL"
ControlClass="MyDLL.CustomTemplateSelector"
Sequence="10"
RegistrationType="List"
RegistrationId="101"
Title="New Template"
Description="Use this menu to select a template"
>
</CustomAction>
<!--RegistrationID 101 = Document Library -->
</Elements>
CustomTemplateSelector.cs:
/*
* This control dynamically loads the document templates for the different business units
* There are some extra's added to make it function under the NewMenu
* - Override of the Render method that explicitly calls the RenderChildren function
* - Addition of a Fixup Script to fix some javascript in the Core.Js
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.UI.WebControls;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint;
using System.Web.UI;
using Microsoft.SharePoint.Utilities;
using System.Web.UI.HtmlControls;
namespace xxxxx.xxxxxx.Controls
{
/// <summary>
/// This class will generate a dynamic Menu, which will allow us to select from the different templates
/// </summary>
public class CustomTemplateSelector: WebControl
{
#region Control Events
protected override void OnLoad(EventArgs e)
{
// Ensure the control is build
this.EnsureChildControls();
base.OnLoad(e);
}
protected override void CreateChildControls()
{
// Register the improved javascript
RegisterFixupScript();
StringBuilder jsText = null;
// Here is where all the action is.
// Define the menu under which all the templates will hang
SubMenuTemplate firstLevelMenuTemplate = new SubMenuTemplate();
firstLevelMenuTemplate.ID = "CustomActionNewTemplateSelector";
firstLevelMenuTemplate.Description = "Use this menu to select a template";
firstLevelMenuTemplate.Text = "New Template";
firstLevelMenuTemplate.Sequence = 10;
//firstLevelMenuTemplate.MenuGroupId = 1000;
// Add Child Menus
for (int i = 0; i < 3; i++)
{
// Fill in your menuItemTemplate Properties
SubMenuTemplate secondLevelMenuTemplate = new SubMenuTemplate();
secondLevelMenuTemplate.ID = "menuItem" + i;
secondLevelMenuTemplate.Text = "Title " + i;
secondLevelMenuTemplate.Description = "Description " + i;
secondLevelMenuTemplate.Sequence = i;
//secondLevelMenuTemplate.MenuGroupId = 1000 + i;
for (int j = 0; j < 2; j++)
{
// Fill in the necessary properties
string strPathTemplate = "http:\u002f\u002flocalhost:33033\u002fShared Documents\u002fForms\u002ftemplate.doc";
string strSaveLocation = getSaveLocation();
// Build your javascript
// This Javascript will open your local application with the correct template
jsText = new StringBuilder();
jsText.AppendLine(@"createNewDocumentWithProgID('");
jsText.AppendLine(strPathTemplate);
jsText.AppendLine(@"','");
jsText.AppendLine(strSaveLocation);
jsText.AppendLine(@"','SharePoint.OpenDocuments', false)");
// Fill in your menuItemTemplate Properties
MenuItemTemplate thirdLevelItemTemplate = new MenuItemTemplate();
thirdLevelItemTemplate.ID = "menuItem" + j + i;
thirdLevelItemTemplate.Text = "Title " + j + i;
thirdLevelItemTemplate.Description = "Description " + i;
thirdLevelItemTemplate.Sequence = i * 10 + j;
thirdLevelItemTemplate.ClientOnClickScript = jsText.ToString();
thirdLevelItemTemplate.UseShortId = true;
//thirdLevelItemTemplate.MenuGroupId = 1000 + i * 10;
// Add it to the parent level
secondLevelMenuTemplate.Controls.Add(thirdLevelItemTemplate);
}
// Add the subMenuItem to your Menu
firstLevelMenuTemplate.Controls.Add(secondLevelMenuTemplate);
}
// Return your menu
this.Controls.Add(firstLevelMenuTemplate);
}
/// <summary>
/// We need to override this, or else there will be a SPAN object around our <IE:MENUITEM> objects
/// This SPAN object makes it impossible for the javascript in the core.js to build your menu
/// </summary>
/// <param name="output">The HTML Writer</param>
protected override void Render(System.Web.UI.HtmlTextWriter output)
{
RenderChildren(output);
}
#endregion
#region Private helper methods
/// <summary>
/// This method registers a deffered javascript that will override a core.js function
/// </summary>
private void RegisterFixupScript()
{
((HtmlHead)Page.Header).Controls.Add(new LiteralControl("<script type='text/javascript' src='/_layouts/1033/FixUp.js' defer></script>"));
}
/// <summary>
/// This methods finds out what the save location is of the document
/// </summary>
/// <returns>A absolute URL</returns>
private string getSaveLocation()
{
string strSaveLocation;
string strSiteUrl = SPContext.Current.Web.Url;
// Check if you are in a Sub Folder
if (HttpContext.Current.Request.QueryString["RootFolder"] == null)
{
// If not, add the RootFolder (= your list itself) it to your URL
strSaveLocation = strSiteUrl + SPContext.Current.List.RootFolder.ServerRelativeUrl;
}
else
{
// If so, use the path in the RootFolder parameter
strSaveLocation = strSiteUrl + HttpContext.Current.Request.QueryString["RootFolder"];
}
return strSaveLocation;
}
#endregion
}
}
// If you want to hide whole menu's, add this code to the page_load event
// To Hide the menu button, use this code
// ((ToolBarMenuButton)((TemplateContainer)((FeatureMenuTemplate)this.Parent).Parent).Parent).Visible = false;
FixUp.js (place this in your layouts folder)
function MenuHtcInternal_Show(oMaster, oParent, y, fFlipTop)
{
var oPopup=oMaster._arrPopup[oMaster._nLevel];
var nIndex;
var fTopLevel;
var oInnerDiv;
if (!oMaster._oContents) PrepContents(oMaster);
if (!oMaster._oContents || IsOpen(oMaster)) return;
if (!oPopup && !oMaster._oRoot)
{
oMaster._nLevel=0;
oMaster._oRoot=oMaster._oContents;
}
fTopLevel=oMaster._nLevel==0;
fFlipTop=fFlipTop && fTopLevel;
if (!oPopup)
{
oMaster._arrPopup[oMaster._nLevel]=document.createElement("DIV");
oPopup=oMaster._arrPopup[oMaster._nLevel];
oPopup.isMenu=true;
oPopup.master=oMaster;
oPopup.level=oMaster._nLevel;
oInnerDiv=document.createElement("DIV");
var oTable=document.createElement("table");
var oTBody=document.createElement("tbody");
oInnerDiv.isInner=true;
oPopup.style.position="absolute";
oInnerDiv.style.overflow="visible";
oTable.appendChild(oTBody);
oInnerDiv.appendChild(oTable);
oPopup.appendChild(oInnerDiv);
if (oMaster._fIsRtL)
oPopup.setAttribute("dir", "rtl");
else
oPopup.setAttribute("dir", "ltr");
document.body.appendChild(oPopup);
FixUpMenuStructure(oMaster);
var id=0;
var childNodeLength=oMaster._oRoot.childNodes.length;
for (nIndex=0; nIndex < childNodeLength; nIndex++)
{
var oNode=oMaster._oRoot.childNodes[nIndex];
if (oNode.nodeType !=1)
continue;
if (!FIsIType(oNode, "label"))
{
var oItem=CreateMenuItem(oMaster, oNode, MakeID(oMaster, oMaster._nLevel, id));
if (oItem) oTBody.appendChild(oItem);
id++;
}
}
oPopup.className="ms-MenuUIPopupBody";
oTable.className=oMaster._wzMenuStyle;
oTable.cellSpacing=0;
oTable.cellPadding=0;
oPopup.oncontextmenu=kfnDisableEvent;
oPopup.ondragstart=kfnDisableEvent;
oPopup.onselectstart=kfnDisableEvent;
if (oParent._onmouseover==null)
oParent._onmouseover=oParent.onmouseover;
if (oParent._onmouseout==null)
oParent._onmouseout=oParent.onmouseout;
if (oParent._onmousedown==null)
oParent._onmousedown=oParent.onmousedown;
if (oParent._onclick==null)
oParent._onclick=oParent.onclick;
if (browseris.nav)
{
oPopup.onkeypress=function(e) {return false; };
oPopup.onkeyup=function(e) {return false; };
oPopup.onkeydown=function(e) {PopupKeyDown(e); return false; };
oPopup.onmousedown=function(e) {TrapMenuClick(e); return false; };
oPopup.onmouseover=function(e) {PopupMouseOver(e); return false; };
oPopup.onmouseout=function(e) {PopupMouseLeave(e); return false; };
oPopup.onclick=function(e) {PopupMouseClick(e); TrapMenuClick(e); return false; };
oParent.onmouseover=function (e) {PopupMouseOverParent(e); return false; };
oParent.onmouseout=function(e) {PopupMouseLeaveParent(e); return false; };
oParent.onmousedown=function(e) {TrapMenuClick(e); return false; };
oParent.onclick=function(e) {TrapMenuClick(e); return false; };
oParent.oncontextmenu=function(e) {TrapMenuClick(e); return false; };
}
else
{
oPopup.onkeydown=new Function("PopupKeyDown(event); return false;");
oPopup.onmousedown=new Function("TrapMenuClick(event); return false;");
oPopup.onmouseover=new Function("PopupMouseOver(event); return false;");
oPopup.onmouseout=new Function("PopupMouseLeave(event); return false;");
oPopup.onclick=new Function("PopupMouseClick(event); TrapMenuClick(event); return false;");
//oParent.onmouseover=new Function("PopupMouseOverParent(event); return false;");
//oParent.onmouseout=new Function("PopupMouseLeaveParent(event); return false;");
oParent.onmousedown=new Function("TrapMenuClick(event); return false;");
oParent.onclick=new Function("TrapMenuClick(event); return false;");
//oParent.oncontextmenu=new Function("TrapMenuClick(event); return false;");
}
if (fTopLevel)
{
var wzOnUnload=oMaster.getAttribute("onunloadtext");
if (wzOnUnload) oPopup.onunload=new Function(wzOnUnload);
}
}
else
{
var oOld=oMaster._arrSelected[oMaster._nLevel];
if (oOld) UnselectItem(oOld);
}
oMaster._arrSelected[oMaster._nLevel]=null;
var oBackFrame;
if (browseris.ie)
{
var originalScrollLeft=document.body.scrollLeft;
oBackFrame=document.createElement("iframe");
AssureId(oBackFrame);
oBackFrame.src="/_layouts/blank.htm";
oBackFrame.style.position="absolute";
oBackFrame.style.display="none";
oBackFrame.scrolling="no";
oBackFrame.frameBorder="0";
document.body.appendChild(oBackFrame);
oPopup.style.zIndex=103;
oPopup._backgroundFrameId=oBackFrame.id;
if (originalScrollLeft !=document.body.scrollLeft)
{
document.body.scrollLeft=originalScrollLeft;
}
}
SetMenuPosition(oMaster, oParent, oPopup, oInnerDiv, fFlipTop, fTopLevel);
if (browseris.ie)
{
SetBackFrameSize(null, oPopup);
oPopup.onresize=new Function("SetBackFrameSize(event, null);");
oBackFrame.style.display="block";
oBackFrame.style.zIndex=101;
}
}
There's more to be said.
Your Menu gets rendered by JavaScript, this javascript is located in the Core.Js. There are some problems with it, namely it doesn't really behave good when hovering on top of your custom menu.
You cannot insert basic javascript blocks to override this functionality, because the Core.Js gets loaded after your page has loaded. They've done this by adding the defer keyword to the script tag. That's why we need to this aswell, so our function gets loaded after the one in the core.js and thus override it.
I couldn't do this with Page.ClientScript. ... methods, because none has the option to add the defer keyword.
Have fun with it. If you do have problems with it, post a comment or mail us.
Pascal Van Vlaenderen
TeamLink Consultant