none
Multiple instances of Contextual Tab Web Part on the same page causes page error

    Question

  • I've followed the MSDN walkthrough for Creating a Custom Web Part with a Contextual Tab in the Ribbon:
    http://msdn.microsoft.com/en-us/library/ff407578.aspx

    It works fine if I have only one instance of the web part on the page. As soon as I add a second ContextualTabWebPart, I get the following error:

    Item has already been added. Key in dictionary: 'Ribbon.CustomContextualTabGroup'  Key being added: 'Ribbon.CustomContextualTabGroup'

    It looks like there needs to be some sort of logic in the AddContextualTab method below, but how do I do a check to see if something (contextualTab, template) has already been registered? I also tried moving the contents of the two private strings, 'contextualTab' and 'contextualTabTemplate' into a CustomAction block in an elements.xml file instead of registering it through the AddContextualTab method and that did not error out, but the contextual tab never appeared when I clicked on the web part.

            private void AddContextualTab()
            {
     
                //Gets the current instance of the ribbon on the page.
                Microsoft.Web.CommandUI.Ribbon ribbon = SPRibbon.GetCurrent(this.Page);
     
                //Prepares an XmlDocument object used to load the ribbon extensions.
                XmlDocument ribbonExtensions = new XmlDocument();
     
                //Load the contextual tab XML and register the ribbon extension.
                ribbonExtensions.LoadXml(this.contextualTab);
                ribbon.RegisterDataExtension(ribbonExtensions.FirstChild, "Ribbon.ContextualTabs._children");
     
                //Load the custom templates and register the ribbon extension.
                ribbonExtensions.LoadXml(this.contextualTabTemplate);
                ribbon.RegisterDataExtension(ribbonExtensions.FirstChild, "Ribbon.Templates._children");
            }

    Any help on this would be much appreciated!

    Saturday, July 24, 2010 11:30 PM

Answers

  • Sing,

    You will need to make two changes. The first change is to the OnPreRender event. This piece of code adds EcmaScript to the page and creates an array of unique Web Part IDs. This code allows us to track what has been added to the page. Add the following before the other call to RegisterClientScriptBlock:

    clientScript.RegisterClientScriptBlock(

     

    this.GetType(), "declarecustomwebpartarray", "var g_customWebPartIds = new Array();", true);

    clientScript.RegisterClientScriptBlock(

     

    this.GetType(), "addcustomwebpartid" + SPRibbon.GetWebPartPageComponentId(this), "g_customWebPartIds.push('" + SPRibbon.GetWebPartPageComponentId(this) + "');", true);

    The second step is to update the DelayScript property. This piece of code iterates through the IDs to avoi the collision. Here is the code for the DelayScript property:

                get
                {
                    string wppPcId = SPRibbon.GetWebPartPageComponentId(this);
                    return @"
                function _addCustomPageComponent()
                {
                    for (var i = 0; i < g_customWebPartIds.length; i++)
                    {
                         SP.Ribbon.PageManager.get_instance().addPageComponent(new ContextualWebPart.CustomPageComponent(g_customWebPartIds[i]));
                    }
                }

                function _registerCustomPageComponent()
                {
                    SP.SOD.registerSod(""CustomContextualTabPageComponent.js"", ""\/_layouts\/CustomContextualTabPageComponent.js"");
                    var isDefined = ""undefined"";
                    try
                    {
                        isDefined = typeof(ContextualWebPart.CustomPageComponent);
                    }
                    catch(e)
                    {
                    }
                    EnsureScript(""CustomContextualTabPageComponent.js"",isDefined, _addCustomPageComponent);
                }
                SP.SOD.executeOrDelayUntilScriptLoaded(_registerCustomPageComponent, ""sp.ribbon.js"");";
                }

    Let me know if this works for you!

    -Dallas

    Monday, July 26, 2010 7:19 PM
  • Sing,

    Putting everything into a custom action should work just fine and remove your issue. You'll still need a call to MakeTabAvailable in your OnPreRender event (see code below). I'm going to update the walkthrough with that in mind as well. When you're working with the Server ribbon in code, there isn't really a way to check if the extension has been loaded. If you're going to keep the AddContextualTab method, you would need to add a global Boolean and set that to true/false based on if the extension has been added. Something like the following:

    static

     

     

    bool _added = false;

     

     

    protected override void OnInit(EventArgs e)

    {

     

     

    base.OnInit(e);

    _added =

     

    false;

    }

     

     

    protected override void OnPreRender(EventArgs e)

    {

     

     

    base.OnPreRender(e);

     

     

    ClientScriptManager csm = this.Page.ClientScript;

     

     

    if (!_added)

    {

     

     

    Microsoft.Web.CommandUI.Ribbon ribbon = SPRibbon.GetCurrent(this.Page);

    ribbon.MakeTabAvailable(

     

    "Ribbon.CustomMediaPlayerContextualGroup.MyTab");

    _added =

     

    true;

    }

    Does that help?

    Thanks,

    Dallas

     

    • Marked as answer by Sing Chan Saturday, July 31, 2010 10:00 PM
    Wednesday, July 28, 2010 4:59 PM

All replies

  • Sing,

    You will need to make two changes. The first change is to the OnPreRender event. This piece of code adds EcmaScript to the page and creates an array of unique Web Part IDs. This code allows us to track what has been added to the page. Add the following before the other call to RegisterClientScriptBlock:

    clientScript.RegisterClientScriptBlock(

     

    this.GetType(), "declarecustomwebpartarray", "var g_customWebPartIds = new Array();", true);

    clientScript.RegisterClientScriptBlock(

     

    this.GetType(), "addcustomwebpartid" + SPRibbon.GetWebPartPageComponentId(this), "g_customWebPartIds.push('" + SPRibbon.GetWebPartPageComponentId(this) + "');", true);

    The second step is to update the DelayScript property. This piece of code iterates through the IDs to avoi the collision. Here is the code for the DelayScript property:

                get
                {
                    string wppPcId = SPRibbon.GetWebPartPageComponentId(this);
                    return @"
                function _addCustomPageComponent()
                {
                    for (var i = 0; i < g_customWebPartIds.length; i++)
                    {
                         SP.Ribbon.PageManager.get_instance().addPageComponent(new ContextualWebPart.CustomPageComponent(g_customWebPartIds[i]));
                    }
                }

                function _registerCustomPageComponent()
                {
                    SP.SOD.registerSod(""CustomContextualTabPageComponent.js"", ""\/_layouts\/CustomContextualTabPageComponent.js"");
                    var isDefined = ""undefined"";
                    try
                    {
                        isDefined = typeof(ContextualWebPart.CustomPageComponent);
                    }
                    catch(e)
                    {
                    }
                    EnsureScript(""CustomContextualTabPageComponent.js"",isDefined, _addCustomPageComponent);
                }
                SP.SOD.executeOrDelayUntilScriptLoaded(_registerCustomPageComponent, ""sp.ribbon.js"");";
                }

    Let me know if this works for you!

    -Dallas

    Monday, July 26, 2010 7:19 PM
  • Hi Dallas,

    Thanks for the prompt response, the code you've provided was of great help but did not solve the page error I described in my original post with regards to the duplicate Ribbon.RegisterDataExtension after the first instance of the web part on the page.

    I did get everything to seemingly work by moving the 'contextual tab' and 'contextualTabTemplate' XML into a custom action and removing the AddContextualTab method altogether from the web part code. I say seemingly because I have yet to test whether the contextual tab is targetting the correct instance of the web part.

    There were some other minor changes to the walk through code that I had to make as well to fix some other issues which I'll detail in another post once I get around to verifying everything is working correctly.

    Is there a way to check whether the Ribbon object already has a ribbon extension registered in code?

     

    Thanks!

    Sing

    Wednesday, July 28, 2010 3:18 PM
  • Sing,

    Putting everything into a custom action should work just fine and remove your issue. You'll still need a call to MakeTabAvailable in your OnPreRender event (see code below). I'm going to update the walkthrough with that in mind as well. When you're working with the Server ribbon in code, there isn't really a way to check if the extension has been loaded. If you're going to keep the AddContextualTab method, you would need to add a global Boolean and set that to true/false based on if the extension has been added. Something like the following:

    static

     

     

    bool _added = false;

     

     

    protected override void OnInit(EventArgs e)

    {

     

     

    base.OnInit(e);

    _added =

     

    false;

    }

     

     

    protected override void OnPreRender(EventArgs e)

    {

     

     

    base.OnPreRender(e);

     

     

    ClientScriptManager csm = this.Page.ClientScript;

     

     

    if (!_added)

    {

     

     

    Microsoft.Web.CommandUI.Ribbon ribbon = SPRibbon.GetCurrent(this.Page);

    ribbon.MakeTabAvailable(

     

    "Ribbon.CustomMediaPlayerContextualGroup.MyTab");

    _added =

     

    true;

    }

    Does that help?

    Thanks,

    Dallas

     

    • Marked as answer by Sing Chan Saturday, July 31, 2010 10:00 PM
    Wednesday, July 28, 2010 4:59 PM
  • Hi Dallas,

    Thanks for the additional code... I no longer have to move the Ribbon XML into a custom action. Below are my changes to the ContextualTabWebPart class compared to the original walkthrough: http://msdn.microsoft.com/en-us/library/ff407578.aspx

    I've added the global '_added' bool and onInit event:

    static bool _added = false;
    
    protected override void OnInit(EventArgs e)
    {
     base.OnInit(e);
     _added = false;
    }

     

    Since we no longer have to get the unique component ID in the delay script, I changed DelayScript into a private string:

    private string delayScript = @"
     function _addCustomPageComponent()
     {
      for (var i = 0; i < g_customWebPartIds.length; i++)
      {
        SP.Ribbon.PageManager.get_instance().addPageComponent(new ContextualTabWebPart.CustomPageComponent(g_customWebPartIds[i]));
      }
     }
    
     function _registerCustomPageComponent()
     {
      SP.SOD.registerSod(""CustomContextualTabPageComponent.js"", ""/_layouts/CustomContextualTabPageComponent.js"");
      var isDefined = ""undefined"";
      try
      {
       isDefined = typeof(ContextualTabWebPart.CustomPageComponent);
      }
      catch(e)
      {
      }
      EnsureScript(""CustomContextualTabPageComponent.js"",isDefined, _addCustomPageComponent);
     }
     SP.SOD.executeOrDelayUntilScriptLoaded(_registerCustomPageComponent, ""sp.ribbon.js"");";

     

    Following your suggestions, the OnPreRender event handler now looks like:

    protected override void OnPreRender(EventArgs e)
    {
     base.OnPreRender(e);
    
     ClientScriptManager csm = this.Page.ClientScript;
     string componentId = SPRibbon.GetWebPartPageComponentId(this); // the unique component id 
    
     // if this is the first instance of the custom web part,
     //we need to add the contextual tab to the ribbon
     if (!_added)
     {
      this.AddContextualTab();
      _added = true;
     }
    
     // we need to create an array which will store the IDs of all instances of our custom web part
     csm.RegisterClientScriptBlock(
      this.GetType(), "DeclareCustomWebPartArray", "var g_customWebPartIds = new Array();", true);
    
     // add this webpart's ID to our array
     csm.RegisterClientScriptBlock(
      this.GetType(), "AddCustomWebPartId" + componentId, "g_customWebPartIds.push('" + componentId + "');", true);
       
     csm.RegisterClientScriptBlock(this.GetType(), "ContextualTabWebPart", this.delayScript, true);
    }

     

    I also made a small change to the handleCommand method in CustomContextualTabPageComponent.js in order to verify that the correct web part was triggering the command:

    handleCommand: function ContextualTabWebPart_CustomPageComponent$handleCommand(commandId, properties, sequence) {
     if (commandId === 'CustomContextualTab.HelloWorldCommand') {
      alert(this._webPartPageComponentId + ' says: Hello, world!');
     }
     if (commandId === 'CustomContextualTab.GoodbyeWorldCommand') {
      alert(this._webPartPageComponentId + ' says: Good-bye, world!');
     }
    }

     

    Thanks again for all the help!

    Saturday, July 31, 2010 8:55 PM
  • I am having this same issue but the code supplied doesn't appear to work.  The fundamental issue I am having using the _added global to track whether or not the tab was added is the fact that these are two independent instances of the same class.  _Added will always be false so the AddContextuaTab method will always be called unless I am missing something.  If I put a breakpoint in the code and skip loading the second tab then everything works fine.  Should I have a Ribbon Tab instance for each image rotator instance on the page?

    I wanted to follow up on my ignorant comment.  After stepping away for a moment and coming back I noticed that the _added global is static so after making it a shared variable in vb.net all is working well.  Great post and thanks for saving me hours of work.  :)

    Wednesday, August 18, 2010 3:51 PM
  • SP Chuck,

    I've posted on my blog a ZIP of the entire walkthrough with the changes documented here:
    http://singchan.com/2010/07/31/sharepoint-2010-fixing-the-msdn-contextual-tab-web-part-walkthrough/

    Hope this helps you figure out what's wrong with your implementation.

    Wednesday, August 18, 2010 4:23 PM
  • Dallas,

    On your original blog post:
    http://blogs.msdn.com/b/sharepointdeveloperdocs/archive/2010/01/28/how-to-create-a-web-part-with-a-contextual-tab.aspx

    You mentioned that there's a product bug where the buttons aren't enabled the first time you select the Web Part. The workaround was to de-select the Web Part and selecting it again.

    Was there a better resolution to this issue. I've run into the same issue when the web part has focus on page load, the buttons don't work. I'm current running the RTM without the June CU applied.

    Thanks again!

    Wednesday, August 18, 2010 4:27 PM
  • Hi,

    Regarding the solution around the static _added variable.

    I don't think this is going to work quite as you expect. Remember that a static variable is static across the entire application domain, and is not just available to the current user.

    If you have multiple users accessing a page at the same time, the result is going to be quite unpredictable.

    You would be better storing a variable on the HttpRequest to determine whether or not to call AddContextualTab().

    Another alternative I have used is to check whether or not your client script is registered. For example

     

    protected override void OnPreRender(EventArgs e)
     {
      base.OnPreRender(e);
      ClientScriptManager csm = this.Page.ClientScript;
    
      //this only needs to be done once
      // we need to create an array which will store the IDs of all instances of our custom web part
      if (!csm.IsClientScriptBlockRegistered(this.GetType(), "DeclareCustomWebPartArray"){
        csm.RegisterClientScriptBlock(this.GetType(), "DeclareCustomWebPartArray", "var g_customWebPartIds = new Array();", true);
    }
      // add this webpart's ID to our array. Needs to be done for all web part instances
      csm.RegisterClientScriptBlock(this.GetType(), "AddCustomWebPartId" + componentId, "g_customWebPartIds.push('" + componentId + "');", true);
     
      //now, the delay script only needs to be registed once, along with addcontextualtab
      if (!csm.IsClientScriptBlockRegistered(this.GetType(), "ContextualTabWebPart"))
      {
      //call add contextual web part
      this.AddContextualTab();
      csm.RegisterClientScriptBlock(this.GetType(), "ContextualTabWebPart", this.DelayScript);
      }
     }
    
    Thursday, August 19, 2010 8:16 AM