How to add multiple items into a SharePoint list at one transaction?


  • you can submit commands in batch using ProcessBatchData API on SPWeb, basically you just generate a CAML that will contain all the commands you want to run in batch.
    A Collaborative Application Markup Language (CAML) that contains the commands, which consists of a Batch element and any number of subordinate Method elements that each specify a SharePoint Foundation remote procedure call (RPC) method.
    To use this method to delete a document in a Document Library, pass the file path to the owsfileref variable in the Method elements.
    The following code example uses the ProcessBatchData method to add two items to the Announcements list of a specified site in the current site collection.

    using (SPWeb oWebsite = SPContext.Current.Site.OpenWeb("Website_URL"))
    {
        SPList oList = oWebsite.Lists["Announcements"];
        System.Guid guid = oList.ID;
        string strGuid = guid.ToString();
    
        string strPost = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
            "<ows:Batch OnError=\"Return\">" +
                "<Method ID=\"A1\"><SetList>" + strGuid + "</SetList>" +
                    "<SetVar Name=\"ID\">New</SetVar>" +
                    "<SetVar Name=\"Cmd\">Save</SetVar>" +
                    "<SetVar Name=" +
                        "\"urn:schemas-microsoft-com:office:office#Title\">" +
                        "New Manager</SetVar>" +
                    "<SetVar Name=" +
                        "\"urn:schemas-microsoft-com:office:office#Body\">" +
                        "Congratulations to Mary for her promotion!</SetVar>" +
                    "<SetVar Name=" +
                        "\"urn:schemas-microsoft-com:office:office#Expires\">" +
                        "2003-09-14T00:00:00Z</SetVar>" + 
                "</Method>" +
                "<Method ID=\"A2\">" +
                    "<SetList>" + strGuid + "</SetList>" +
                    "<SetVar Name=\"ID\">New</SetVar>" +
                    "<SetVar Name=\"Cmd\">Save</SetVar>" +
                    "<SetVar Name=" +
                        "\"urn:schemas-microsoft-com:office:office#Title\">" +
                        "New Technical Consultant</SetVar>" +
                    "<SetVar Name=" +
                        "\"urn:schemas-microsoft-com:office:office#Body\">" +
                        "Welcome to the team, John!</SetVar>" +
                    "<SetVar Name=" +
                        "\"urn:schemas-microsoft-com:office:office#Expires\">" +
                        "2007-10-15T00:00:00Z</SetVar>" + 
                "</Method>" +
            "</ows:Batch>";
    
        string strProcessBatch = oWebsite.ProcessBatchData(strPost);
    }
    

How to Use Audience Targeting to Filter List Items in SharePoint 2010


In a Building an Accordion with jQuery and SharePoint 2010 | dig sharepoint, I showed you how to build an accordion style menu using SharePoint 2010 and jQuery. I recently had a request to expand on this idea by personalizing the items shown based on which SharePoint Group users belonged to. What came to mind was to use Audience Targeting.
Here is another custom webpart I built that shows items from a SharePoint list, and using an ASP.Net Repeater control, I display them in a specified order:

I was approached with a challenge of only showing items from the list for certain users. At first I thought I could have used item level permissions, but then I thought using Audience Targeting would be a better option. When researching Audience Targeting, most searches I found involved setting up the User Profile Service and then using a Content Query Webpart to filter who sees the data. In my case, I didn’t need a CQWP as I already had a custom webpart to display list content. Initially I thought that by removing the “RunWithElevatedPrivileges” context when retrieving list data would filter list items in a list/library view by the Target Audience specified in that column. However, this did not produce the desired outcome, as the item priviledges are based on item level permissions.
Here is what I ended up doing to get Audience Targeting to work for me:
The first step is to setup a method to retrieve list data. The following shows a standard way of getting all list data without regard to item level permissions:


public static DataTable GetListData(string listName, string xmlQuery)
{
 
    DataTable dt = new DataTable();
    DataTable dtNew = new DataTable();
    SPSite site = SPContext.Current.Site;
    SPWeb web = SPContext.Current.Web;
 
    SPSecurity.RunWithElevatedPrivileges(delegate()
    {
        using (SPSite ElevatedSite = new SPSite(site.ID))
        {
            using (SPWeb ElevatedWeb = ElevatedSite.OpenWeb(web.ID))
            {
                SPList theList = ElevatedWeb.Lists[listName];
 
                SPQuery query = new SPQuery();
                query.Query = xmlQuery;
 
                SPListItemCollection items = theList.GetItems(query);
                dt = items.GetDataTable();
 
                // Factor in those items for specific audiences
                dtNew = GetListDataIncludingTargetAudience(dt);
            }
        }
    });
 
    return dtNew;
}
Next you’ll want to create a SharePoint Group that will hold all the users that will see the additional list items:

Add users to the group. I usually have a test Active Directory account, so that I can see the affects of logging into the site as myself or as the test user.
The next step involves setting up your list with Audience Targeting:
  • Site Actions –> View All Site Content –> <List Name>
  • List Settings –> Audience Targeting Settings
  • Enable audience targeting
Add the new column to the default list view:
  • List Settings –> <View> –> <Click to Modify>
  • Add the new “Target Audiences” to the current view.
Now when you go back to view your list, you will see a new column called “Target Audiences”:
Now that your list is setup, whenever you edit a list item, you can add the SharePoint Group to the “Target Audiences” field using the People Picker:

  • Click on the ‘Browse’ icon, search for your new SharePoint Group, and click ‘OK’.
  • Click ‘Save’ on the item, and you should now see it listed in the “Target Audience” column for this item.
Now that you have your list setup and a method that retrieves list data, we need a way to factor in those users that need to see the additional item. I created this new method:


public static DataTable GetListDataIncludingTargetAudience(DataTable dt)
{
    // Perform audience targeting on list items:  http://msdn.microsoft.com/en-us/library/ms563693.aspx
    // Fill dtNew with Audience specific list items
    AudienceLoader audienceLoader = AudienceLoader.GetAudienceLoader();
    DataTable dtNew = new DataTable();
    dtNew = dt.Clone();
 
    for (int z = 0; z <= dt.Rows.Count - 1; z++)
    {
        // Check to see if this is a valid column
        if (dt.Columns.Contains("Target_x0020_Audiences"))
        {
            string audienceFieldValue = dt.Rows[z]["Target_x0020_Audiences"].ToString();  // (string)listItem[k_AudienceColumn];
 
            // If the value is empy, then assume it is for every audience
            if (audienceFieldValue == string.Empty)
                dtNew.ImportRow(dt.Rows[z]);
            else
            {
                // quickly check if the user belongs to any of those roles.
                // Add the data row if user is a member of the audience targeted item.
                if (AudienceManager.IsCurrentUserInAudienceOf(audienceLoader, audienceFieldValue, false))
                    dtNew.ImportRow(dt.Rows[z]);
            }
        }
        else
        {
            // Add the item if audience targeted column does not exist
            dtNew.ImportRow(dt.Rows[z]);
        }
    }
 
    return dtNew;
}
As you can see in the code sample at the beginning of the post, the method “GetListData” makes a call to the method “GetListDataIncludingTargetAudience”, passing the resulting DataTable of content from the “GetListData” method. This new method loops through the rows of the DataTable passed in and does 3 things:
  1. It first looks to see if there is a column called “Target_x0020_Audiences”.
  2. If it does not exist, then we want to continue on and still import the row into a new DataTable. By doing it this way, we won’t be filtering out data…we will be filtering “in” data if the column exists.
  3. If the column exists, it then performs a check to see if the currently logged in user is a part of the SharePoint Group found for the particular DataTable Row. It does this using the AudienceManager class. With this class you can retrieve individual types, lists of audiences, and lists of users who are associated with an audience. If the user a part of the Audience, this item will be imported into a new DataTable. If the user does not belong to the Audience, it will not be imported.
The goal with this method is not to exclude data for users where the Target Audience column is blank. I actually want to include all data but simply include more data if the Target Audience has been specified. This way it is scalable to any list that does or does not have the Target Audience feature enabled.
I found that using the Target Audience feature on a list is a great built-in feature to SharePoint 2010. Using it to include more data for a specific group adds a little personalization that can be easily implemented. Please test it out for yourself and let me know how it works out for you!





Building an Accordion with jQuery and SharePoint 2010 | dig sharepoint

User experience (UX) is a big buzzword these days when building websites. How many clicks the user has to perform and how much scrolling will be done should always factor in the design process. In many cases content can be so large that your user could have finger cramps when scrolling (or kill a tree when printing the page!). Have you ever given thought to using a jQuery Accordion? This method works well, because it makes use of the horizontal space to store massive amounts of content in “panels” that are hidden until the user wants to see it. It also adds a nice interact experience that people normally get only when viewing Flash content.

What you are presented with here is a series of 7 panels, each representing a list item in a SharePoint List. The first panel is shown by default. Arrows on each panel offer the user a way to expand content in each panel while collapsing the panel currently being viewed.
To build such a webpart, I initially defined a SharePoint List that contained all the elements used by the visual webpart:

TitleName of panel section
SlideOrderDisplay Order of the panels
SlideHtmlRaw HTML for the panel content
HeaderUrlImage for the vertical text on the panel
PanelBackgroundImageBackground image for the panel (optional)
PanelBackgroundHexGeneral background color for the panel

Now that the list is defined, the next step was to build the visual webpart. The basic shell of the panels is as follows:

<div> <!-- container -->
    <ul>
        <li> <!-- list item for each panel -->
            <div>panel left shadow image</div>
            <div>panel container</div>
                <div>left vertical banner</div>
                    <div>vertical text image</div>
                    <div>navigation arrow</div>
                    <div>left vertical shadow</div>
                </div>
                <div>panel content</div>
            </div>
        </li>
    </ul>
</div>



Here is the corresponding html used in the visual webpart:

<asp:Repeater ID="rptSlider" runat="server" Visible="true" onitemdatabound="rptSlider_ItemDataBound">
    <HeaderTemplate>
        <asp:Literal ID="ltContainerBegin" runat="server"></asp:Literal>
        <ul class="accordion">
    </HeaderTemplate>
    <ItemTemplate>
            <li id="liItem" runat="server" class="accordion-top-li">
                <div class="li-content-vert-shadow"><img src="/SiteCollectionImages/img_vert_shadow.png" alt="vertical shadow" /></div>
                <div id="liContent" class="li-content" runat="server">
                    <div class="accordion-vert-header">
                        <div class="vert-header-text"><asp:Literal ID="ltVerticalHeaderImg" runat="server"></asp:Literal></div>
                        <div class="vert-header-arrow"><asp:Literal ID="ltArrow" runat="server"></asp:Literal></div>
                        <div class="vert-header-shadow"><asp:Literal ID="ltVerticalShadowImg" runat="server"></asp:Literal></div>
                    </div>
                    <div class="accordion-html-content">
                        <asp:Literal ID="ltSlideHtml" runat="server"></asp:Literal>
                    </div>
                </div>
            </li>
    </ItemTemplate>
    <FooterTemplate>
        </ul>
        </div>
    </FooterTemplate>
</asp:Repeater>

Since the panels are all the same structure, I used an ASP:Repeater control to build the unordered list items dynamically.  After setting up the structure in html, I began to tie the data to the controls, through the ItemDataBound event of my Repeater:

protected void rptSlider_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
    if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
    {
        DataRowView drv = (DataRowView)e.Item.DataItem;
 
        Literal ltContainerBegin = (Literal)rptSlider.Controls[0].FindControl("ltContainerBegin");
        ltContainerBegin.Text = string.Format("<div id='accordionBKG' style='background-color: {0};'>", drv["PanelBackgroundHex"]);
 
        HtmlGenericControl liItem = (HtmlGenericControl)e.Item.FindControl("liItem");
 
        if (nthSlide == 0)
        {
            liItem.Attributes.Add("class", "accordion-top-li-first");
        }
        else
        {
            liItem.Attributes.Add("style", "width: " + slideClosedWidth + "px");
 
            Literal ltArrow = (Literal)e.Item.FindControl("ltArrow");
            ltArrow.Text = "<img src='/SiteCollectionImages/but_arrow_lft_off.png' width='40px' height='40px' alt='accordion arrow' class='accordion_arrow' />";
 
            Literal ltVerticalShadowImg = (Literal)e.Item.FindControl("ltVerticalShadowImg");
            ltVerticalShadowImg.Text = "<img src='/SiteCollectionImages/img_vert_shadow.png' alt='vertical shadow' class='accordion_vertical_shadow' />";
        }
                
        HtmlGenericControl liContent = (HtmlGenericControl)e.Item.FindControl("liContent");
                
        // Set background image.  If it doesn't exist, set it to the background hex value in the column
        if (drv["PanelBackgroundImage"].ToString() != string.Empty)
            liItem.Attributes.Add("style", "background-image: url( " + drv["PanelBackgroundImage"] + "); background-position: bottom left; background-repeat: no-repeat;");
 
        if (drv["HeaderUrl"].ToString() != "")
        {
            Literal ltVerticalHeaderImg = (Literal)e.Item.FindControl("ltVerticalHeaderImg");
            ltVerticalHeaderImg.Text = string.Format("<img src='{0}' alt='{1}' /><br /><br />", drv["HeaderUrl"].ToString(), drv["Title"].ToString());
        }
 
        Literal ltSlideHtml = (Literal)e.Item.FindControl("ltSlideHtml");
        ltSlideHtml.Text = drv["SlideHtml"].ToString();
 
        nthSlide++;
    }
 
}

There are a few things here to notice.  I used ASP:Literal controls in this webpart instead of ASP:Image controls.  This is purely a matter of preference.  You can use either.  The important thing I am doing here is dynamically building my panels and then let the CSS and jQuery handle the “magic”.  
I built this accordion with future addition/subtractions in mind. In my Page_Load method, I made a determination of what the closed panel widths should be and stored that value in a global variable called “dblSlideClosedWidth”:

ListItems = dt;
if (ListItems.Rows.Count > 0)
{
    // Set widths dynamically based on how many rows are currently in the list
    double rows = (double)ListItems.Rows.Count;
    dblSlideClosedWidth = Math.Floor(dblSlideClosedContainerWidth / (rows - 1));
    // Determine Slide Width, but add 9px onto it because there is a -9px margin assigned through css
    slideClosedWidth = (int)dblSlideClosedWidth + 9;
 
    rptSlider.DataSource = ListItems;
    rptSlider.DataBind();
 
}

Then in the repeater event you’ll notice I set the width of each panel dynamically.  If this is not the first time through the loop (designated by the global variable “nthSlide”), I set the width panel equal to “dblSlideClosedWidth”.  The first panel is ignored, as it needs to be displayed in full width specified in the css:

if (nthSlide == 0)
{
    liItem.Attributes.Add("class", "accordion-top-li-first");
}
else
{
    liItem.Attributes.Add("style", "width: " + slideClosedWidth + "px");
 
    Literal ltArrow = (Literal)e.Item.FindControl("ltArrow");
    ltArrow.Text = "<img src='/SiteCollectionImages/but_arrow_lft_off.png' width='40px' height='40px' alt='accordion arrow' class='accordion_arrow' />";
 
    Literal ltVerticalShadowImg = (Literal)e.Item.FindControl("ltVerticalShadowImg");
    ltVerticalShadowImg.Text = "<img src='/SiteCollectionImages/img_vert_shadow.png' alt='vertical shadow' class='accordion_vertical_shadow' />";
}

Now that I got the webpart built, it’s time to talk about the css and jQuery.  To describe the css in a nutshell, I basically have a container of unordered list items (the panels) that are displayed inline.  There is a left vertical shadow to the left of each panel, so I had to also position each panel relative to each other and then set a negative margin on each panel equal to the width of the shadow image.  This creates an effect of each panel overlapping each other.
The jQuery portion is really where the “magic” happens. From a usability standpoint, a user may click on the entire closed panel itself or only on the arrow on the closed panel. So I made a determination to add the “click” event to the entire close panel. This way, it doesn’t matter if the user clicks on the panel or the arrow…the event will still fire. The main idea when animating the opening and closing of panels is to do 2 events in parallel: open up the panel that was clicked on, and close the panel that is currently open. I needed to perform these actions with the same animation duration. If they aren’t occurring at the same time, the panels will float outside of the container, as the total widths of all panels in the container during the process will be greater than the overall container width.
To create the sliding effect, I made use of the jQuery animate() method. The two key attributes is the “width” and “duration”. Setting the “width” attribute specifies what width it needs to become. In my case I needed to close the currently opened panel, so I set the “width” equal to a predetermined variable called “minWidth”:

// Close Previously Clicked Panel before Opening the current clicked panel
if (lastOpenedBlock.index() > 0)
{
    // Hide last clicked element
    $(lastOpenedBlock).animate(
        {width: minWidth}, 
        {queue:true, duration:1000, 
            complete: function()
            {
                closeAnimationIsRunning = false;
            },
            step: function()
            {
            }
        }
    );
}

Then I opened the selected panel using the same animate() method, but this is using a predetermined value called “maxWidth”:

$(currentBlock).animate(
    {width: minWidth}, 
    {queue:true, duration:1000, 
        complete: function()
        {
            closeAnimationIsRunning = false;
        },
        step: function()
        {
        }
    }
);

The “minWidth” and “maxWidth” variables are determined dynamically based on the first panel as this is the space we need to fill.  It is necessary to close a panel before opening another because these are sequential events.  If you begin to open a panel first, the combined total of panel widths will exceed the container widths and you will see an undesirable effect of the panels floating outside of the container.
One thing I had to consider was the possibility that the user may click on multiple panels during transition. I found that during this scenario, there was a period of time when the page seemed to play “catch up”…the panels appeared to have a life of their own (opening and closing multiple times after the initial click). To overcome this, I set two variables called “openAnimationIsRunning” and “closeAnimationIsRunning” that get set when the user clicks on a panel. Within my “click” event, I first check these variables. If either are true, then I stop the “click” event. This prevents the user from clicking multiple panels while the animation is in motion:

$(".accordion-vert-header").click(
    function()
    {    
        // This allows one animation to run at a time.
        if (openAnimationIsRunning || closeAnimationIsRunning)
        {
            return;
        }
        
        openAnimationIsRunning = true;
        closeAnimationIsRunning = true;
}

The last consideration was what happens to the content within the panels during the transition.  I found that when I animated the panel opened/closed, the content within the panel needed to also animate open/close.  To solve this, I found that a nice fade worked well:
// Open current block content
$(currentBlock).children('.li-content').children('.accordion-html-content').fadeIn();
Accordions are a great way to consolidate content using minimal space, while creating an interactive experience for your users. I provided a complex use of an accordion in my example, but I hope it allowed you to understand the elements and the process involved when building your own!

If you’d like to download the code, I am providing the webpart, jQuery, css, and list template I used in my example.

Customizing the ribbon in sharepoint 2010 – creating tabs, groups and controls (part 1)

In this article series:
  1. Customizing the ribbon – creating tabs, groups and controls (this post)
  2. Adding ribbon items into existing tabs/groups
  3. Ribbon customizations - dropdown controls, Client Object Model and JavaScript Page Components
  4. Customize the ribbon programmatically from web parts and field controls
Some good posts are starting to appear on SharePoint 2010 ribbon customization now, and over the next couple of articles I want to cover some key things you might want to do with the ribbon when developing your solutions. Diving straight in then, some ribbon fundamentals:
  • Ribbon elements must be defined in declarative XML with the CustomAction tag - even if you will actually use code to manipulate them (e.g. make visible, enable/disable etc.)
  • The "control hierarchy" is ribbon > tab > group > controls – we'll explore all these in this post
  • Individual buttons/controls do not appear and disappear on the ribbon. This is a key ribbon principle, to avoid the "I'm sure this button was here yesterday!" effect - instead, depending on the context:
    • Entire tabs can be shown/hidden
    • Individual controls can be enabled/disabled
  • It's not possible to add custom controls to the ribbon e.g. a custom .ascx/server control. The list of controls defined by the ribbon can be found here in the MSDN docs, and includes things like Button, Checkbox, Color Picker, Combo Box, Dropdown, Textbox, Toggle Button etc, but also some funky ones like Spinner, Split Button and Flyout Anchor (definitions of these exotic varieties can be found in the documentation). Flyout Anchor is particularly interesting as it takes XML as a datasource and can be used to build interesting "pickers" e.g. with images – I'll hopefully cover this in detail in a future post
  • The definitions for the out-of-the-box ribbon elements are split across several files in the SharePoint root, with TEMPLATE\GLOBAL\XML\CMDUI.XML being the main one. You will likely spend significant time in this file looking for examples similar to what you're building.
It's also worth giving special consideration to how JavaScript plays with the ribbon – it's used frequently since much happens on the client. Depending on the scope you need for your JavaScript (e.g. every page vs. a couple) and the complexity of what you're doing, JavaScript can be supplied in a few ways:
  • By embedding it into your declarative XML (via a separate CustomAction with a new 'Location="ScriptLink"' attribute) – this post uses this approach, though later in the series I'll show the next option
  • By deploying a custom .js file which contains some object-oriented JavaScript. This is the approach used for more complex customizations, where you need to create an object which is the client-side "page component" in addition to your XML. The page component supplies the implementation for how your custom ribbon elements should handle various events ("commands") . This object needs to be derived from the existing CUI.Page.Component object defined in CUI.js. As with any JavaScript file, you then have a couple of options for referencing it on your page.
In this post we'll show adding JavaScript the first way, though later in the series I'll show the use of a page component.
Example - creating a new custom tab
This is a fairly in-depth example, since by necessity it also covers creating custom groups and controls too. Also, to kill two birds with one stone, I thought it would be good to look at the new 'notifications' and 'status' frameworks in the Client Object Model for passing messages back to the user in your SharePoint app. First we'll walk through what my custom tab looks like, then what it actually does. I have a tab titled "Chris's custom tab" with 3 groups ("Notification messages", "Add status messages" and "Remove status messages") each with some buttons of different sizes and images in them:
CustomRibbonTab
Clicking the 'Notify hello' button adds a transient message in the notifications area (fades in from right and stays for 5 seconds by default):
SP.UI.Notify
Clicking the 'Info status' button shows a status message this time, in the default color (this remains on screen until removed with another API call):
SP.UI.Status_Info
Clicking the 'Warning status' button shows a status message of a different color to indicate severity, I chose red:
SP.UI.Status_Warning
You might also have noticed the 'remove status' buttons have become enabled when a status message is present – such client-side checks can be done by linking a 'CommandUIHandler' with an 'EnabledScript' attribute, as we're about to see.
So what XML is required to get that? Well before you scroll through, note that I've taken the complex route with some of the declarations so that my example is as informative as possible – most samples I've seen so far simply add a button or two in a single group and don't specify the "'group template" details which determines how the controls in the group get laid out. This is fine for a button or two as you can just reference an out-of-the-box group template, but if you want to do anything different you're a bit stuck so hopefully this is good documentation:
 <?xml version="1.0" encoding="utf-8"?>
 <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   <CustomAction
    Id="COB.SharePoint.Ribbon.CustomTab"
    Location="CommandUI.Ribbon" RegistrationType="List" RegistrationId="101">
     <CommandUIExtension>
       <CommandUIDefinitions>
         <CommandUIDefinition Location="Ribbon.Tabs._children">
           <Tab Id="COB.SharePoint.Ribbon.CustomTab" Title="Chris's custom tab" Description="Groups and controls will go in here" Sequence="501">
             <Scaling Id="COB.SharePoint.Ribbon.CustomTab.Scaling">
               <MaxSize Id="COB.SharePoint.Ribbon.CustomTab.NotificationGroup.MaxSize"
                        GroupId="COB.SharePoint.Ribbon.CustomTab.NotificationGroup"
                        Size="OneLarge"/>
               <Scale Id="COB.SharePoint.Ribbon.CustomTab.NotificationGroup.Scaling.CustomTabScaling"
                      GroupId="COB.SharePoint.Ribbon.CustomTab.NotificationGroup"
                      Size="OneLarge" />
               <MaxSize Id="COB.SharePoint.Ribbon.CustomTab.StatusGroup.MaxSize"
                       GroupId="COB.SharePoint.Ribbon.CustomTab.StatusGroup"
                       Size="TwoMedium"/>
               <Scale Id="COB.SharePoint.Ribbon.CustomTab.StatusGroup.Scaling.CustomTabScaling"
                      GroupId="COB.SharePoint.Ribbon.CustomTab.StatusGroup"
                      Size="TwoMedium" />
               <MaxSize Id="COB.SharePoint.Ribbon.CustomTab.RemoveStatusGroup.MaxSize"
                       GroupId="COB.SharePoint.Ribbon.CustomTab.RemoveStatusGroup"
                       Size="TwoLarge"/>
               <Scale Id="COB.SharePoint.Ribbon.CustomTab.RemoveStatusGroup.Scaling.CustomTabScaling"
                      GroupId="COB.SharePoint.Ribbon.CustomTab.RemoveStatusGroup"
                      Size="TwoLarge" />
             </Scaling>
             <Groups Id="COB.SharePoint.Ribbon.CustomTab.Groups">
               <Group
                 Id="COB.SharePoint.Ribbon.CustomTab.NotificationGroup"
                 Description="Contains notification items"
                 Title="Notification messages"
                 Sequence="52"
                 Template="Ribbon.Templates.OneLargeExample">
                 <Controls Id="COB.SharePoint.Ribbon.CustomTab.NotificationGroup.Controls">
                   <Button
                     Id="COB.SharePoint.Ribbon.CustomTab.NotificationGroup.Notify"
                     Command="COB.Command.Notify"
                     Sequence="15" Image16by16="/_layouts/images/NoteBoard_16x16.png" Image32by32="/_layouts/images/NoteBoard_32x32.png"
                     Description="Uses the notification area to display a message."
                     LabelText="Notify hello"
                     TemplateAlias="cust1"/>
                 </Controls>
               </Group>
               <Group
                 Id="COB.SharePoint.Ribbon.CustomTab.StatusGroup"
                 Description="Contains 'add status' items"
                 Title="Add status messages"
                 Sequence="49"
                 Template="Ribbon.Templates.TwoMediumExample">
                 <Controls Id="COB.SharePoint.Ribbon.CustomTab.StatusGroup.Controls">
                   <Button
                    Id="COB.SharePoint.Ribbon.CustomTab.StatusGroup.AddStatusInfo"
                    Command="COB.Command.AddStatusInfo" 
                    Sequence="17" Image16by16="/_layouts/images/info16by16.gif" Image32by32="/_layouts/images/info16by16.gif"
                    Description="Uses the status bar to display an info message."
                    LabelText="Info status"
                    TemplateAlias="cust2"/>
                   <Button
                     Id="COB.SharePoint.Ribbon.CustomTab.StatusGroup.AddStatusWarning"
                     Command="COB.Command.AddStatusWarning"
                     Sequence="17" Image16by16="/_layouts/images/warning16by16.gif" Image32by32="/_layouts/images/warning32by32.gif"
                     Description="Uses the status bar to display a warning message."
                     LabelText="Warning status"
                     TemplateAlias="cust3"/>
                 </Controls>
               </Group>
               <Group
                 Id="COB.SharePoint.Ribbon.CustomTab.RemoveStatusGroup"
                 Description="Contains 'remove status' items"
                 Title="Remove status messages"
                 Sequence="52"
                 Template="Ribbon.Templates.TwoLargeExample">
                 <Controls Id="COB.SharePoint.Ribbon.CustomTab.RemoveStatusGroup.Controls">
                   <Button
                     Id="COB.SharePoint.Ribbon.CustomTab.RemoveStatusGroup.RemoveLastStatusButton"
                     Command="COB.Command.RemoveLastStatus"
                     Sequence="15" Image16by16="/_layouts/images/warning16by16.gif" Image32by32="/_layouts/images/CRIT_32.GIF"
                     Description="Removes the last message from the status bar."
                     LabelText="Remove last status message" 
                     TemplateAlias="cust4"/>
                   <Button
                     Id="COB.SharePoint.Ribbon.CustomTab.RemoveStatusGroup.RemoveAllStatusButton"
                     Command="COB.Command.RemoveAllStatus"
                     Sequence="15" Image16by16="/_layouts/images/warning16by16.gif" Image32by32="/_layouts/images/CRIT_32.GIF"
                     Description="Removes all messages from the status bar."
                     LabelText="Remove all status messages"
                     TemplateAlias="cust5"/>
                 </Controls>
               </Group>
             </Groups>
           </Tab>
         </CommandUIDefinition>
         <CommandUIDefinition Location="Ribbon.Templates._children">
           <GroupTemplate Id="Ribbon.Templates.OneLargeExample">
             <Layout Title="OneLarge" LayoutTitle="OneLarge">
               <Section Alignment="Top" Type="OneRow">
                 <Row>
                   <ControlRef DisplayMode="Large" TemplateAlias="cust1" />
                 </Row>
               </Section>
             </Layout>
           </GroupTemplate>
         </CommandUIDefinition>
         <CommandUIDefinition Location="Ribbon.Templates._children">
           <GroupTemplate Id="Ribbon.Templates.TwoMediumExample">
             <Layout Title="TwoMedium" LayoutTitle="TwoMedium">
               <Section Alignment="Top" Type="TwoRow">
                 <Row>
                   <ControlRef DisplayMode="Medium" TemplateAlias="cust2" />
                 </Row>
                 <Row>
                   <ControlRef DisplayMode="Medium" TemplateAlias="cust3" />
                 </Row>
               </Section>
             </Layout>
           </GroupTemplate>
         </CommandUIDefinition>
         <CommandUIDefinition Location="Ribbon.Templates._children">
           <GroupTemplate Id="Ribbon.Templates.TwoLargeExample">
             <Layout Title="TwoLarge" LayoutTitle="TwoLarge">
               <Section Alignment="Top" Type="OneRow">
                 <Row>
                   <ControlRef DisplayMode="Large" TemplateAlias="cust4" />
                   <ControlRef DisplayMode="Large" TemplateAlias="cust5" />
                 </Row>
               </Section>
             </Layout>
           </GroupTemplate>
         </CommandUIDefinition>
       </CommandUIDefinitions>
       <CommandUIHandlers>
         <CommandUIHandler
           Command="COB.Command.Notify"
           CommandAction="javascript:
           
           var notificationId = SP.UI.Notify.addNotification('Hello from the notification area'); 
           " />
         <CommandUIHandler
           Command="COB.Command.AddStatusInfo"
           CommandAction="javascript:
           
           var statusId = SP.UI.Status.addStatus('Quite important status message');
           latestId = statusId;
           enableRemoveStatusButton();
           " />
         <CommandUIHandler
           Command="COB.Command.AddStatusWarning"
           CommandAction="javascript:
           
           var statusId = SP.UI.Status.addStatus('Very important status message');
           SP.UI.Status.setStatusPriColor(statusId, 'red');
           latestId = statusId;
           enableRemoveStatusButton();
           " />
         <CommandUIHandler
           Command="COB.Command.RemoveLastStatus" EnabledScript="javascript:enableRemoveStatusButton();"
           CommandAction="javascript:
           
           SP.UI.Status.removeStatus(latestId);
           latestId = '';
           enableRemoveStatusButton();" />
         <CommandUIHandler
           Command="COB.Command.RemoveAllStatus" EnabledScript="javascript:enableRemoveStatusButton();"
           CommandAction="javascript:
           
           SP.UI.Status.removeAllStatus(true);
           latestId = '';
           enableRemoveStatusButton();" />
       </CommandUIHandlers>
     </CommandUIExtension>
   </CustomAction>
   <CustomAction Id="COB.Command.RemoveLastStatus.CheckEnable" Location="ScriptLink"
              ScriptBlock="
                 var latestId = '';
                           
                 function enableRemoveStatusButton() 
                 { 
                   if (latestId == '')
                   {
                     return false;
                   }
                   else
                   {
                     return true;
                   }
                 }"
                 />
 </Elements>
Some key points, following the XML sequence:
  • CustomAction:
    • Notice I have two CustomAction elements – one for the ribbon elements, the other for some JavaScript I want to use with my custom elements. This is the approach mentioned earlier where the JavaScript is effectively embedded in your XML [sidenote: you don't have to be doing ribbon customization to leverage this approach - this use of CustomAction is a new way of providing JavaScript to the page, just be aware it will be added for every page in the Feature scope (e.g. site/web) and you have no control over where in the page it will be injected. It does give you the ability to take away your JavaScript via Feature deactivation though, which could be useful for many scenarios).
      • The Location attribute of CustomAction for ribbon elements should always be "CommandUI.Ribbon"
      • The Location attribute of CustomAction for script is a new value, "ScriptLink"
    • My ribbon tab is scoped to document libraries only – this is courtesy of the RegistrationType="List" RegistrationId="101" attributes (which is exactly what you did when targeting a CustomAction to doc libs in SharePoint 2007, no change there)
    • When targeting a list in this way, RegistrationId refers to the list template ID (e.g. generic list = 100. document library = 101 etc. – here's a full list of list template IDs for 2007, there could be a couple of new ones in 2010) – it is not possible to declaratively target a list by e.g. GUID or URL. So consider that this could drive you to create a list template when you otherwise might not have.
    • Other options for the RegistrationType continue to be "ContentType", "ProgID" and "FileType", but I'm pretty sure only "List" can be used for ribbon elements, but I've not tested that yet so I reserve the right to be wrong! If you want a different scope level, you would omit the RegistrationType and RegistrationId attributes and use code such as SPRibbon.MakeTabAvailable() to conditionally show the ribbon. More on this later in the series when I show how to add ribbon customizations for a web part or custom field control.
  • CommandUIDefinition:
    • Another element you might have multiple of – one for the main customization definition, one for each of the "GroupTemplate" elements being provisioned (more on this later). For the main one, the "Location" attribute here is crucially important as this specifies where the customization should appear. My value of "Ribbon.Tabs._children" indicates I'm adding something into the "Ribbon.Tabs" collection defined by SharePoint (typically in CMDUI.XML) – "_children" is a convention used when adding to many collections in the ribbon architecture. We'll look at how to add new groups and controls into an existing group in the next article, but as a quick example, adding a group into "Ribbon.Library.Groups._children" would make your group appear somewhere in here (depending on the "Sequence" value assigned on the definition for the group):
      Ribbon.Library.Groups
  • Tab:
    • The "Sequence" attribute decides where to place my tab amongst the existing ones. Out-of-the-box values are generally multiples of 10, sometimes of 5, so your sequence values should avoid such numbers to avoid conflict. Generally you'll need to find the declaration of the surrounding elements near where you are targeting (in CMDUI.XML) to find the appropriate number.
  • Scaling (and children):
    • This section defines how your elements should behave when the window is resized and there's not enough room. You need a "MaxSize" and "Scale" element for each Group you define. These define the size and layout the element(s) should be at when at "max", and also what to change to when the window is smaller – effectively you can provide multiple layouts for your controls depending on the window size (e.g. prioritising the important buttons).
  • Group:
    • This is the "section on the ribbon tab" which is the container for your controls – in the small image above, an example of a Group is 'View Format' which contains the two leftmost buttons. Key things here are the "Sequence" (same deal as elsewhere) and the "Template" – this is a reference to a "GroupTemplate" element (which we'll come onto shortly). In essence, this is the link which will tell the ribbon framework how to lay out the controls in this group.
  • Controls:
    • Fairly obvious, this is the parent node for any controls you want to add e.g. buttons, dropdowns etc etc. Note that each of your controls in here must have a "TemplateAlias" attribute – this tells the framework exactly where to place the individual control within the GroupTemplate which is referenced.
    • Controls expose various commands, via attributes – a Button simply has a "Command" attribute which fires when clicked, whereas a Dropdown has additional ones such as "PopulateQueryCommand" and "QueryCommand". These link to "CommandUIHandler" elements or code defined in a JavaScript page component.
  • GroupTemplate:
    • Similar to defining say, a HTML table, this section provides the actual layout of the controls, alignments, control sizes etc. Each control which is being declared needs a corresponding "ControlRef" element which will be matched to the control on the "TemplateAlias" value.
  • CommandUIHandler:
    • This is the where you get to define the JavaScript which executes when the basic "Command" is fired for a control (e.g. a button is clicked). The command name must match that defined on the Control element, and the "CommandAction" attribute contains the script for the basic command. You can also use the "EnabledScript" attribute to add some script which decides whether the control should be enabled or not – this is how my ''remove status' buttons are only enabled when there is a message to remove.
    • Since all the JavaScript gets added to the same page, as you'll see in my sample it is possible to declare variables which get used by other JavaScript provisioned by a different CommandUIHandler – again though, whilst the sequence is deterministic you cannot control where your script gets added into the overall page (at the start of the <body> tag), so if you need your code to run when the DOM is complete you'd have to take steps to get your code called at the appropriate time – more on this later in the series.
Hope you found this useful. Next time we'll take a quick look at adding items to existing ribbon locations, before moving onto working with JavaScript and page components etc.