Friday, June 18, 2010

Provisioning a custom Wiki Page within a SharePoint 2010 Team Site

Topic

I have explained in my generic post about Wiki Page customization within a SharePoint 2010 Team Site that the Wiki Page of the Team Site are based on an unique template that cannot be customized.
However, there is a way of customizing a Wiki Page by provisioning a page in a Wiki Library. I do not speak here to use a SharePoint 2010 Feature, but provisioning a custom prepopulated Wiki Page as you can create a blank Wiki Page by using the "New Page" menu item within the Site Actions menu or the "New Wiki Page" menu item in the "New Document" menu item in a Wiki Page Document Library.

This process is already present in SharePoint 2010 when you create a new team site and the product is provisioning the Home Page of the team site or when you create an new Wiki Library and the product is provisioning the Welcome Page that is pre populated with explanation about the Wiki Libraries.

As a picture is worth than long explanations, here is that I propose to show in this post:

Imgine a new Wiki Page Library where you could find a "Custom Wiki Page" button in the "New" group of the SharePoint 2010 Ribbon. 

Then, when clicking on this button, a custom dialog, redesigned, would invite you to create a custom Wiki Page.

When having typed the title and submited the dialog, you would be redirected to your new Custom Wiki page prepopulated with the typed title of the page, plus your name and your photo.

Much more! When editing the page, you would notice a new Text Layout, that is not provided by the default SharePoint 2010 Text Layout menu items.

Of course the page could be Saved properly...

 

To be able to mount such a solution, several steps has to be performed:

  1. First you have to create a new Button within the SharePoint 2010 Ribbon.
  2. Second, you need to create the custom application page that will provision the custom Wiki Page in this custom Document Library


So let us do all this now... but before some little explanations about Content Types for the Team Site Wiki Pages Library are to be provided

1 - You CANNOT add Content Types to a Team Site Wiki Library

Now that you have seen what I wanted to do, you might be thinking "wait a minute, why he has not created a new content type for his custom wiki page so as the "New"  menu of the Wiki Library will be automatically populated with an item to create a custom page?".

I tried, tried very much, but as I discovered a first limitation of the Team Site Wiki Library that does not allow them to create pages based on several templates, I have now discovered that you cannot add new content types to a Team Site Wiki Library.

When you try to add a content type to a Wiki Library you may be surprised because, by default, a Wiki Library is not configured to support Content Types. It is normal since the original goal is not to create other document than a native SharePoint 2010 Wiki Page for a Team Site. Here is an excerpt of the Microsoft documentation on the topic (How to: Add a Content Type to a List):

[...
Before you can add a content type to a list definition, you must be sure that the list is configured to support content types. The first thing to check is the template from which the list instance is created. If the ListTemplate element has an attribute named DisallowContentTypes and the attribute is set to TRUE, then the list does not support content types. If the attribute is set to FALSE or is missing, then the list does support content types.

The next thing to check is whether content types are enabled on the list instance. The Schema.xml file that defines the list instance has a List element. This element must include an attribute named EnableContentTypes, and the attribute must be set to TRUE. Setting the attribute to TRUE is equivalent to selecting Yes under Allow management of content types? in the Advanced Settings section of List Settings.
..]

So first, I tried to  to created a new Custom Wiki Library  with a new type and changed the schema configuration, then created a new Wiki Page Content Type and added it to the library, and it worked, this new library accepted a new Wiki Page content type, and I had a new menu entry in my New Document menu : Wiki Custom Page - Create a new wiki page.


But just after that, when trying to create the page, it failed, and I had the following error message : "You cannot create a Wiki Page in a Library that is not a Wiki Library".

Ok, then I tried to create a custom Wiki Page library with the same type (119), even if it is idiot, since we cannot see the difference after and that time, although I had configured and test several times the configuration, the library just did not show the UI entries corresponding to the possibility to add new Content Types.
So, the conclusion is the following:

You cannot add new Content Types to the Team Site Wiki Page Libraries in SharePoint 2010.
You cannot create Wiki Pages in a custom Team Site Wiki Page Library.

Ok, no trouble then, I will create my custom pages from a new button within the Ribbon. ( I also tried many hours to add a new entry in the New Document menu using a Custom Action feature, even tried to create first a new section in the menu, but no way. If somebody finds it, let me know...)

2 - Creating a "New Document" custom button in the Ribbon


There is a lot of documentation now on the Internet about how to create a custom button in SharePoint 2010 so here is just the code and a few comments:

<?xml version="1.0" encoding="utf-8"?>

<Feature xmlns="http://schemas.microsoft.com/sharepoint/"

        Id="afe52f83-a20e-42d4-8aa7-35d8c5fdfc50"

        Title="Ribbon New Button"

        Scope="Web"

        Description="Add a new menu button to a Wiki Page Library to provision custom Wiki Pages"

        >

       <ElementManifests>

             <ElementManifest Location="newButton.xml"/>

       </ElementManifests>

</Feature>

 

 

<?xml version="1.0" encoding="utf-8"?>

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

       <CustomAction

             Id="Ribbon.Library.Actions.AddAButton"

             RegistrationId="119"

             RegistrationType="List"

             Location="CommandUI.Ribbon"

             Sequence="5"

             Title="New Custom Wiki Page">

             <CommandUIExtension>

                    <CommandUIDefinitions>

                           <CommandUIDefinition Location="Ribbon.Documents.New.Controls._children">

                                        <Button

                                        Id="Ribbon.Documents.New.NewCustomWikiPage"

                                        Alt="Add a Custom Wiki Page"

                                        Sequence="5"

                                        Command="CreateCustomWikiPage"

                                         Image16by16="/_layouts/$Resources:core,Language;/images/formatmap16x16.png" Image16by16Top="-48" Image16by16Left="-16"

                                         Image32by32="/_layouts/$Resources:core,Language;/images/formatmap32x32.png" Image32by32Top="0" Image32by32Left="-64"

                                        LabelText="Custom Wiki Page"

                                        Description="Create a new Custom Wiki Page"

                                        TemplateAlias="o1"

                                        />

                           </CommandUIDefinition>

                    </CommandUIDefinitions>

                    <CommandUIHandlers>

                           <CommandUIHandler

                     Command="CreateCustomWikiPage"

                     CommandAction="javascript:OpenCreateWebPageDialog('/_layouts/createCustomWebPage.aspx?List={ListId}');" />

                    </CommandUIHandlers>

             </CommandUIExtension>

       </CustomAction>

</Elements>

 

 You can notice that you have to specify the type of the library in the RegistrationId, so don not mistake when creating a button for a library.
And a fantastic thing is that JavaScript supports the old syntax for the parameter: {ListId}
Do not forget the quotes in the javaScript method.

Now, very important, how to know if the component installtion has worked or failed.

First, as you maybe know, you can also add an item to the Ribbon by using a Web Part. So if you check the html source code of the page, you will see this Web Part that shows the parameters of the native Sharepoint configuration for the Ribbon. You can check this parameters as an example if you want to create a custom control later within the Ribbon.

But if you want to check the installation of a custom control, there will be also available javascript code to be checked, within the dataExtension, and the Command UI Handler objects in json format. If you see them, it will be the confirmation that the installation is successful, and you will also be able to check that the parameters are correct, if something is going wrong:

3 - Provisioning the Team Site Wiki Page 

 

3.1 Implementing the custom Web part

So as the goal of our tutorial was to prepopulate a Wiki Page with some text and a Web Part that can retrieve the name and the picture of the author, let us first implement the Web Part.

I called the assembly MyCompany.Frameworks.SharePoint
I have created a folder to add a WebParts part to the namespace
And the first Web Part of this framework is called GetAuthorInfo

 

using System;

using System.Linq;

using System.Runtime.InteropServices;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

using Microsoft.SharePoint;

 

 

namespace MyCompany.Frameworks.SharePoint.WebParts

{

    [Guid("5aa223b6-fd04-48c1-83e6-9531bf45ef19")]

    public class GetAuthorInfo : System.Web.UI.WebControls.WebParts.WebPart

    {

        protected override void CreateChildControls()

        {

            base.CreateChildControls();

 

            string HtmlOutput = string.Empty, pictureUrl ;

 

            Label label = new Label();

            SPWeb myWeb = SPContext.Current.Web;

            SPList myUserInfo = null;

 

            string author = SPContext.Current.Item["Created By"].ToString();

            author = author.Split(new Char[] { '#' })[1].ToLower();

            HtmlOutput += "Author: " + author;

 

            myUserInfo = myWeb.Site.RootWeb.Lists["User Information List"];

            var results = myUserInfo.Items.Cast<SPListItem>().Where(item => Equals(string.Compare(item["Name"].ToString(), author, true), 0));

 

            if (results.Count() > 0)

            {

                pictureUrl = results.First<SPListItem>()["Picture"].ToString().Split(new Char[] { ',' })[0];

                HtmlOutput += "<br/><br/><img src='" + pictureUrl + "' />";

            }

 

            label.Text = HtmlOutput;

 

            this.Controls.Add(label);

        }

    }

}

 

 

notice that if you want the previous code works on SharePoint 2010 you have to configure it for Linq and I have a post for a minimal configuration :

Use Linq inside a Script Block of an Application Page - SharePoint minimal configuration for Linq

Until now except some minor differences with SharePoint 2007, we were in a known land, it is now that it becomes interesting since we are going to discover the SharePoint 2010 wiki pages content new syntax.

 So I will give soon the code of the custom SharePoint 2010 dialog and inside the in line script code to provision the Wiki Page. But before, some indications:

3.2 - to create a wiki page file and get the SPListItem corresponding to the file:

SPListItem listItem = files.Add(strUrl, SPTemplateFileType.WikiPage).GetListItem(new string[] { "WikiField" });


3.3 - to add some content to a wiki page programmatically:

listItem["WikiField"] = content;

listItem.UpdateOverwriteVersion();

3.4 - to add a Web part within this content:


 Give it a storageKey
 add it to the Web part manager

 then use this syntax to place it within the Wiki page content, based on the storage key

string str3 = string.Format(CultureInfo.InvariantCulture, "<div class=\"ms-rtestate-read ms-rte-wpbox\" contentEditable=\"false\"><div class=\"ms-rtestate-read {0}\" id=\"div_{0}\"></div><div style='display:none' id=\"vid_{0}\"/></div>", new object[] { storageKey.ToString("D") });
 

3.5 - Structure of the text layout:

This is the html to store in WikiField field of the WikiPage SPListItem if you want to obtain the same page than this tutorial.

 Notice the nested DIV with the ms-rte-layoutszone-outer and the ms-rte-layoutszone-inner classes. and the <span id="layoutsData" style="display: none">true,false,2</span>

 that is corresponding to the structure (I think the integer is the number of rows), but I have not any clue about the boolean. I had to set the second to false if not my page could not be saved! If somebody finds information about it, let me know...

<table id="layoutsTable" style="width: 100%">

    <tbody>

        <tr style="vertical-align: top">

            <td style="width: 100%">

                <div class="ms-rte-layoutszone-outer" style="width: 100%">

                    <div class="ms-rte-layoutszone-inner" style="min-height: 60px; word-wrap: break-word">

                        <h1 class="ms-rteElement-H1B" style="margin-bottom: 0px;">

                            <span><span>Wiki 1 </span></span>

                        </h1>

                        <p>

                            &#160;</p>

                        <p>

                            &#160;</p>

                        <p>

                            &#160;</p>

                    </div>

                </div>

            </td>

        </tr>

        <tr style="vertical-align: top">

            <td style="width: 100%">

                <div class="ms-rte-layoutszone-outer" style="width: 100%">

                    <div class="ms-rte-layoutszone-inner" style="min-height: 60px; word-wrap: break-word">

                        <p>

                            &#160;</p>

                        <p>

                        </p>

                        <p>

                            &#160;</p>

                        <p>

                            &#160;</p>

                        <p>

                            &#160;</p>

                        <p>

                            &#160;</p>

                        <div class="ms-rtestate-read ms-rte-wpbox" contenteditable="false">

                            <div class="ms-rtestate-read ae26f6c9-8cf4-438c-8a38-26cd309d0ec6" id="div_ae26f6c9-8cf4-438c-8a38-26cd309d0ec6">

                            </div>

                            <div style='display: none' id="vid_ae26f6c9-8cf4-438c-8a38-26cd309d0ec6" />

                        </div>

                        <p>

                            &#160;</p>

                    </div>

                </div>

            </td>

        </tr>

    </tbody>

</table>

<span id="layoutsData" style="display: none">true,false,2</span>

 3.6 - To close the dialog and be redirected to the provisioned page

 window.frameElement.navigateParent(ProvisionedPageUrl);

 4 - Code of the all dialog page (createCustomWebpage.aspx)

 

<%@ Page Language="C#" AutoEventWireup="true" Inherits="Microsoft.SharePoint.WebControls.LayoutsPageBase"

    DynamicMasterPageFile="~masterurl/default.master" %>

 

<%@ Assembly Name="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<%@ Assembly Name="MyCompany.Frameworks.SharePoint, Version=1.0.0.0, Culture=neutral, PublicKeyToken=11b701f527101e0f" %>

<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls"

    Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<%@ Import Namespace="Microsoft.SharePoint" %>

<%@ Import Namespace="System.Text" %>

<%@ Import Namespace="Microsoft.SharePoint.Utilities" %>

<%@ Import Namespace="Microsoft.SharePoint.WebPartPages" %>

<%@ Import Namespace="System.Globalization" %>

<%@ Import Namespace="MyCompany.Frameworks.SharePoint.WebParts" %>

<%@ Import Namespace="System.Web.UI.WebControls.WebParts" %>

<asp:content contentplaceholderid="PlaceHolderAdditionalPageHead" runat="server">

       <SharePoint:UIVersionedContent ID="UIVersionedContent1" UIVersion="4" runat="server"><ContentTemplate>

              <SharePoint:CssRegistration runat="server" Name="forms.css" />

       </ContentTemplate></SharePoint:UIVersionedContent>

</asp:content>

<asp:content id="Main" contentplaceholderid="PlaceHolderMain" runat="server">

  

<script language="C#" runat="server">

   

    SPList myList = null;

    SPWeb myWeb = null;

    protected override void OnLoad(EventArgs e)

    {

        isProvisioningFinished.Value = "false";

 

        myWeb = SPContext.Current.Web;

 

        Guid listGuid = new Guid(Request.QueryString["List"]);

        myList = myWeb.Lists[listGuid];

 

        TargetLibrary.Text = "This page will be created in this library: <br/><div style='color:#0072bc;padding-top:3px'>" + myList.Title + "</div>";

        NameDescLabel.Text = "New page name: ";

    }

 

    protected void provision(object sender, EventArgs e)

    {

        SPFolder rootFolder = myList.RootFolder;

        SPWeb parentWeb = myList.ParentWeb;

 

        SPFileCollection files = rootFolder.Files;

 

        string pageName = txtPageName.Text;

 

        string strUrl = rootFolder.ServerRelativeUrl + "/" + pageName + ".aspx";

        if (!parentWeb.GetFile(strUrl).Exists)

        {

            SPListItem listItem = files.Add(strUrl, SPTemplateFileType.WikiPage).GetListItem(new string[] { "WikiField" });

            ProvisionWikiPageHomePage(listItem.File, pageName);

            ProvisionedPageUrl.Value = strUrl;

 

            isProvisioningFinished.Value = "true";

        }

    }

 

    protected void ProvisionWikiPageHomePage(SPFile wikiPage, string pageName)

    {

        if (wikiPage == null)

        {

            throw new ArgumentNullException("wikiPage");

        }

        SPListItem listItem = wikiPage.GetListItem(new string[] { "WikiField" });

        if (listItem == null)

        {

            throw new ArgumentException(SPResource.GetString("OnlyInWikiLibraries", new object[0]));

        }

        SPList parentList = listItem.ParentList;

 

 

        SPWeb parentWeb = parentList.ParentWeb;

        string serverRelativeUrl = parentWeb.ServerRelativeUrl;

        if (serverRelativeUrl.EndsWith("/"))

        {

            serverRelativeUrl = serverRelativeUrl.Substring(0, serverRelativeUrl.Length - 1);

        }

        SPList list = null;

        string str2 = null;

        using (SPLimitedWebPartManager manager = wikiPage.GetLimitedWebPartManager(PersonalizationScope.Shared))

        {

            str2 = RenderListViewWebPartForWikiPage(manager);

        }

 

        StringBuilder builder = new StringBuilder();

        StringBuilder builder2 = new StringBuilder();

        builder.Append("\r\n                <table id=\"layoutsTable\" style=\"width: 100%\">\r\n                    <tbody>\r\n                        <tr style=\"vertical-align:top\">");

        builder.Append("\r\n                            <td style=\"width: 100%\">\r\n        <div class=\"ms-rte-layoutszone-outer\" style=\"width:100%\">\r\n          <div class=\"ms-rte-layoutszone-inner\" style=\"min-height:60px;word-wrap:break-word\">\r\n<h1 class=\"ms-rteElement-H1B\" style=\"margin-bottom:0px;\"><span><span>");

        builder.Append(SPHttpUtility.HtmlEncode(pageName));

        builder.Append("\r\n                                        </span></span></h1>\r\n  ");

        builder.Append("\r\n            <p>&#160;</p>\r\n            <p>&#160;</p>\r\n            <p>&#160;</p>\r\n          </div>\r\n        </div>\r\n      </td>");

        builder.Append("\r\n </tr>");

        builder.Append("\r\n <tr style=\"vertical-align:top\">");

        builder2.Append("\r\n                            <td style=\"width: 100%\">\r\n        <div class=\"ms-rte-layoutszone-outer\" style=\"width:100%\">\r\n          <div class=\"ms-rte-layoutszone-inner\" style=\"min-height:60px;word-wrap:break-word\">\r\n");

        builder2.Append("                                        <p>&#160;</p>\r\n                                        <p>");

        builder2.Append("\r\n                                        </p>\r\n                      <p>&#160;</p>\r\n            <p>&#160;</p>\r\n            <p>&#160;</p>\r\n                                        <p>&#160;</p>");

        if (!string.IsNullOrEmpty(str2))

        {

            builder2.Append(str2);

        }

        builder2.Append("<p>&#160;</p>\r\n     \r\n</div>\r\n        </div>\r\n      </td>");

        builder.Append(builder2.ToString());

        builder.Append("\r\n    </tr>\r\n  </tbody>\r\n</table>\r\n            <span id=\"layoutsData\" style=\"display: none\">true,false,2</span>");

        string result = "";

        result = builder.ToString();

 

        //to see the generated code (user must have the write permission on C drive...)

        //System.IO.File.WriteAllText("c:\\test.html",result);

        listItem["WikiField"] = result;

        listItem.UpdateOverwriteVersion();

    }

 

    protected string RenderListViewWebPartForWikiPage(SPLimitedWebPartManager lwpm)

    {

        if (lwpm == null)

        {

            throw new ArgumentNullException("lwpm");

        }

 

        System.Web.UI.WebControls.WebParts.WebPart webPart = new GetAuthorInfo();

 

        webPart.Title = "Author";

        webPart.ChromeType = PartChromeType.None;

 

        Guid storageKey = Guid.NewGuid();

        webPart.ID = StorageKeyToID(storageKey);

 

        lwpm.AddWebPart(webPart, "wpz", 0);

 

        string str = storageKey.ToString();

        StringBuilder builder = new StringBuilder();

 

        string str3 = string.Format(CultureInfo.InvariantCulture, "<div class=\"ms-rtestate-read ms-rte-wpbox\" contentEditable=\"false\"><div class=\"ms-rtestate-read {0}\" id=\"div_{0}\"></div><div style='display:none' id=\"vid_{0}\"/></div>", new object[] { storageKey.ToString("D") });

        return str3;

    }

 

    internal string StorageKeyToID(Guid storageKey)

    {

        if (!(Guid.Empty == storageKey))

        {

            return ("g_" + storageKey.ToString().Replace('-', '_'));

        }

        return string.Empty;

    }

 

</script>

        

<asp:Label ID="Label1" runat="server" Text=""></asp:Label>

       <div style="width:350px; margin-right:auto; margin-left:auto;">

              <br />

              <div style="height:3.75em;">

                     <asp:Label id="TargetLibrary" runat="server" />

              </div>

              <div>

              <br />

              <br />

                     <asp:Label style='padding-bottom:5px;display:block' id="NameDescLabel" runat="server" />

              </div>

              <div>

                     <asp:TextBox id="txtPageName" runat="server" />

              </div>

              <div style="height:3.75em;" class="ms-formvalidation">

                     <asp:Label id="NameErrorLabel" runat="server" />

              </div>

              <div>

            <asp:LinkButton style="border:solid 1px #0072bc;padding:2px" runat="server" Text="Provision the page " OnClick="provision" />

              </div>

            <input type='hidden' runat='server' id='isProvisioningFinished' />

            <input type='hidden' runat='server' id='ProvisionedPageUrl' />

        </div>

        <script type="text/javascript">

 

            var isProvisioningFinishedClientID = '<%=isProvisioningFinished.ClientID%>';

            var ProvisionedPageUrlClientID = '<%=ProvisionedPageUrl.ClientID%>';

 

            var isFinished = "false";

            var ProvisionedPageUrl = "";

 

            isFinished = document.getElementById(isProvisioningFinishedClientID).value;

            ProvisionedPageUrl = document.getElementById(ProvisionedPageUrlClientID).value;

 

 

            if (isFinished == "true") {

 

                isFinished.value = "false";

                window.frameElement.navigateParent(ProvisionedPageUrl);

            }      

           </script>

 

</asp:content>

<asp:content id="PageTitle" contentplaceholderid="PlaceHolderPageTitle" runat="server">

New Custom Wiki Page

</asp:content>

<asp:content id="PageTitleInTitleArea" contentplaceholderid="PlaceHolderPageTitleInTitleArea"

    runat="server">

New Custom Wiki Page

</asp:content>

 

 

7 comments:

Anonymous said...

it looks like the data in layoutsData span correspond to display header(true, false), display footer(true, false) and number of columns (1)

Marc Charmois said...

Great!

thank you so much for your contribution.

Cheers

Marc

Marc Charmois said...

Hi Romain,

it is better to communicate in English so as our discusion can be useful for somebody else.

So my question is: Where do you want to add dynamically the Web Parts,
to the editable part of the wiki page, ie the wiki field ?
or to the other par of the page ?

For the first scenario you can use the current post because I insert a web part programmatically in the Wiki Page at its creation.

For the second scenario, you can check this post and try to add a Web Part zone and Lists Web Parts using a delegate control. But after that I don't know if you will be allowed to edit the Web Parts. So this scenario is maybe good only if you want to add AND configure the web parts programmatically.

Hope this helps...

Marc

ES said...

Hi.

I am trying to send you an email but I cant find it anywhere. So I will write here and hope you will see the post. I am in need of an professional advice becouse nobody can help me. I need to solve a problme in MOSS2010. Can you help me out?

Thank you so much.

may mail erik.simonic[@]gmail.com

Marc Charmois said...

Hi Erik,

it seems you did not search very deeply for my e-mail address since I am on Linkedin and Viadeo, and you could also send me a message in myspace on MS Live or on facebook, but anyway :-)
You don't need to send me an e-mail for a SharePoint 2010 problem because the SharePoint community blogs are THE place to send everyones' problems in order everyone could contribute to make the technology around SharePoint go ahead.
So just send me your problem on a comment and if it is that much complicated and need files exchanges we will proceed by mail, but it occured maybe once a year in my case.
Last, don't write MOSS 2010, MOSS is just for the 2007 version because for this version SharePoint was a product of the Office range as sold as the Office products Web server, but it is now a distinct product and has therefore now recovered its "SharePoint" label :-)

See you...

Marc

Shiri said...

You can enable Content Type in in the list proerties in SPD.

Marc Charmois said...

Hi Shiri,

Fantastic!
It is opening new horizons...

Thank you very much for this great information. I will try it ...

Marc