Wednesday, October 29, 2008

Read data stored in the custom section of a SharePoint web.config file using XPath

Introduction
In order to store Data shared by all sites, easily usable by server code programming and easy to deploy, some SharePoint Application needs custom entries in the Web Application web.config file. One simple solution is to add a line with a key and a value in the appSettings section.
However, you may need to customize your web.config file with a more complex and structured section. They are many post on how to add a custom section in a web.config for a SharePoint application using programmatically.
then, there is also code sample that shows how to read datas stored in the custom section in order to use them while programming.
Maybe the more complet sample is this one for Asp .Net but usable in SharePoint:

How To Create a Custom ASP.NET Configuration Section Handler in Visual C# .NET

I did practice this solution, but think it is a bit heavy. Then I wondered if it would not be more simple to use xpath to read the data sotred in the Custom ASP.NET Configuration Section of the SharePoint Application Web.config file.
1 - Exposing the business case
To make a clear demonstration, asume we are working for a company that owns several stores, and that the employees want to access to the company SharePoint intranet with Form Based Authentication Mode. We want to manage FBA profiles in order to map FBA roles with the SharePoint Portal application functionnalities like:
  • Access to a knowledge base
  • Make in line ordering
  • Edit order tracking reports
  • etc.

The FBA roles are the followings;
  • employee,
  • manager
  • owner
Thus, we want to store these FBA roles and Features in our SharePoint Portal web.config file with more technical data as aspnet FBA database connection string name, and we are going first to modify the web.config file
2 - Add a custom section to the SharePoint Portal web.config file
We open the web.config file (after having made a back-up) and add a section group to the configSection section.
    <sectionGroup name="System.Workflow.ComponentModel.WorkflowCompiler" type="System.Workflow.ComponentModel.Compiler.WorkflowCompilerConfigurationSectionGroup, System.Workflow.ComponentModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
      <section name="authorizedTypes" type="System.Workflow.ComponentModel.Compiler.AuthorizedTypesSectionHandler, System.Workflow.ComponentModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </sectionGroup>
<!--start adding -->
    <sectionGroup name="myPortal" >
      <section name="profileManagement" type="System.Configuration.SingleTagSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    </sectionGroup>
<!-- end adding -->
  </configSections>
Then, we add the "myPortal custom section
    <machineKey validationKey="80464397F43642274573BDB3C8C49CA32AD2DD99A86B46EC" decryptionKey="75DF7F2B93BE8584983E065036615D4BB091D5B3C8A668C8" validation="SHA1" />
    <sessionState mode="SQLServer" timeout="60" allowCustomSqlDatabase="true" partitionResolverType="Microsoft.Office.Server.Administration.SqlSessionStateResolver, Microsoft.Office.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
  </system.web>
<!--start adding -->
   <myPortal>
    <profileManagement connectionStringName="AspNetDbFBAConnectionString" applicationName="/" baseStoreFeatures="15" enableSessionState="false">
      <mainRoles>
        <role name="Owner" hasAllFeatures="true" />
        <role name="Manager" hasAllFeatures="true" />
        <role name="Employee" hasAllFeatures="false" />
      </mainRoles>
      <storeFeatures>
        <storeFeature name="PersonalizedHomePage" value="1" />
        <storeFeature name="ContactForm" value="2" />
        <storeFeature name="KnowledgeBase" value="4" default="true" />
        <storeFeature name="MyContacts" value="8" default="true" />
        <storeFeature name="Ordering" value="16" />
        <storeFeature name="Tracking" value="32" />
        <storeFeature name="BusinessReport" value="64" />
        <storeFeature name="FullMask" value="65535" />
      </storeFeatures>
    </profileManagement>
  </myPortal>
<!-- end adding -->
 <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
3 - Reading data stored in the web.config file
In order to read data stored in the web.config file, we are going to use the XMLExplorateur class that I introduced in a previous post:

Simplify XPath using a C# Class

and that I had completed to be more powerfull.
here is the class code:
        class XMLExplorateur
        {
            protected XPathDocument docNav;
            protected XPathNavigator nav;
            protected XPathNodeIterator xit;
            protected bool initpath = true;
            public XMLExplorateur() { }

            public XMLExplorateur(String path)
            {
                try
                {
                    docNav = new XPathDocument(path);
                    nav = docNav.CreateNavigator();
                }
                catch
                {
                    docNav = null;
                    nav = null;
                }
            }
            public bool Init(String path)
            {
                try
                {
                    docNav = new XPathDocument(path);
                    nav = docNav.CreateNavigator();
                }
                catch
                {
                    docNav = null;
                    nav = null;
                    return false;
                }
                return true;
            }

            public List<string> ValuesOf(String Item)
            {
                List<string> myList = new List<string>();
                if (nav == null) return null;
                String tmp = "descendant::" + Item;
                try
                {
                    xit = nav.Select(tmp);
                    while (xit.MoveNext())
                    {
                        myList.Add(xit.Current.Value);
                    }
                }
                catch
                {
                    myList = null;
                }
                return myList;
            }

            public Dictionary<string, uint> DictionaryStringUintItemsOf(String Item, string att1, string att2)
            {
                Dictionary<string, uint> myDictionary = new Dictionary<string, uint>();
                if (nav == null) return null;
                String tmp = "descendant::" + Item;
                try
                {
                    xit = nav.Select(tmp);
                    while (xit.MoveNext())
                    {
                        myDictionary.Add(xit.Current.GetAttribute(att1, ""), Convert.ToUInt32(xit.Current.GetAttribute(att2, "")));
                    }
                }
                catch
                {
                    myDictionary = null;
                }
                return myDictionary;
            }

            public Dictionary<string, bool> DictionaryStringBoolItemsOf(String Item, string att1, string att2)
            {
                Dictionary<string, bool> myDictionary = new Dictionary<string, bool>();
                if (nav == null) return null;
                String tmp = "descendant::" + Item;
                try
                {
                    xit = nav.Select(tmp);
                    while (xit.MoveNext())
                    {
                        myDictionary.Add(xit.Current.GetAttribute(att1, ""), Convert.ToBoolean(xit.Current.GetAttribute(att2, "")));
                    }
                }
                catch
                {
                    myDictionary = null;
                }
                return myDictionary;
            }

            public String ValueOf(String Item)
            {
                if (nav == null) return "Erreur Navigateur null";
                String tmp = "descendant::" + Item;
                try
                {
                    xit = nav.Select(tmp);
                    if (xit.MoveNext()) tmp = xit.Current.Value;
                    else tmp = "null";
                }
                catch
                {
                    tmp = "null";
                }
                return tmp;
            }
        }

Now, this is the way we can program to read all the data stored in the custom section :
        private static string _connectionStringName = null;
        private static string _applicationName = null;
        private static uint _baseStoreFeatures = uint.MinValue;
        private static Dictionary<string, uint> _availableStoreFeatures = null;
        private static Dictionary<string, bool> _mainRoles = null;
        private static bool _enableSessionState = false;


        void Page_Load(object sender, EventArgs e)
        {
            XMLExplorateur xe = new XMLExplorateur();

            Microsoft.SharePoint.SPSecurity.RunWithElevatedPrivileges(
                delegate()
                {
                    xe.Init(HttpContext.Current.Server.MapPath("~/web.config"));
                }
            );

            _connectionStringName = xe.ValueOf("configuration/myPortal/profileManagement/@connectionStringName");

            _applicationName = xe.ValueOf("configuration/myPortal/profileManagement/@applicationName");
            
            _baseStoreFeatures = Convert.ToUInt32(xe.ValueOf("configuration/myPortal/profileManagement/@baseStoreFeatures"));

            _enableSessionState = Convert.ToBoolean(xe.ValueOf("configuration/myPortal/profileManagement/@enableSessionState"));

            _availableStoreFeatures = xe.DictionaryStringUintItemsOf("configuration/myPortal/profileManagement/storeFeatures/storeFeature", "name", "value");

            _mainRoles = xe.DictionaryStringBoolItemsOf("configuration/myPortal/profileManagement/mainRoles/role", "name", "hasAllFeatures");

        }

Here is the complet code of the .aspx page. As it is an application page, place this page in the LAYOUTS directory, and call it from any site of your portal using this url (assume you call the page _readwebconfig.aspx as I did):
http://hostheader/anysite/anysubsite/_layouts/_readwebconfig.aspx

<%@ Page Language="C#" AutoEventWireup="true"  %>

<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls"  Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"  %> 
<%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"  %>
<%@ Import Namespace="Microsoft.SharePoint"  %>
<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages"  %>
<%@ Import Namespace="System.Collections.Generic"  %>
<%@ Import Namespace="System.Text"  %>
<%@ Import Namespace="System.Xml"  %>
<%@ Import Namespace="System.Xml.XPath"  %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title  ></title>
</head>
<body>

    <script runat="server" id="class_XMLExplorateur">

        class XMLExplorateur
        {
            protected XPathDocument docNav;
            protected XPathNavigator nav;
            protected XPathNodeIterator xit;
            protected bool initpath = true;
            public XMLExplorateur() { }

            public XMLExplorateur(String path)
            {
                try
                {
                    docNav = new XPathDocument(path);
                    nav = docNav.CreateNavigator();
                }
                catch
                {
                    docNav = null;
                    nav = null;
                }
            }
            public bool Init(String path)
            {
                try
                {
                    docNav = new XPathDocument(path);
                    nav = docNav.CreateNavigator();
                }
                catch
                {
                    docNav = null;
                    nav = null;
                    return false;
                }
                return true;
            }

            public List<string> ValuesOf(String Item)
            {
                List<string> myList = new List<string>();
                if (nav == null) return null;
                String tmp = "descendant::" + Item;
                try
                {
                    xit = nav.Select(tmp);
                    while (xit.MoveNext())
                    {
                        myList.Add(xit.Current.Value);
                    }
                }
                catch
                {
                    myList = null;
                }
                return myList;
            }

            public Dictionary<string, uint> DictionaryStringUintItemsOf(String Item, string att1, string att2)
            {
                Dictionary<string, uint> myDictionary = new Dictionary<string, uint>();
                if (nav == null) return null;
                String tmp = "descendant::" + Item;
                try
                {
                    xit = nav.Select(tmp);
                    while (xit.MoveNext())
                    {
                        myDictionary.Add(xit.Current.GetAttribute(att1, ""), Convert.ToUInt32(xit.Current.GetAttribute(att2, "")));
                    }
                }
                catch
                {
                    myDictionary = null;
                }
                return myDictionary;
            }

            public Dictionary<string, bool> DictionaryStringBoolItemsOf(String Item, string att1, string att2)
            {
                Dictionary<string, bool> myDictionary = new Dictionary<string, bool>();
                if (nav == null) return null;
                String tmp = "descendant::" + Item;
                try
                {
                    xit = nav.Select(tmp);
                    while (xit.MoveNext())
                    {
                        myDictionary.Add(xit.Current.GetAttribute(att1, ""), Convert.ToBoolean(xit.Current.GetAttribute(att2, "")));
                    }
                }
                catch
                {
                    myDictionary = null;
                }
                return myDictionary;
            }

            public String ValueOf(String Item)
            {
                if (nav == null) return "Erreur Navigateur null";
                String tmp = "descendant::" + Item;
                try
                {
                    xit = nav.Select(tmp);
                    if (xit.MoveNext()) tmp = xit.Current.Value;
                    else tmp = "null";
                }
                catch
                {
                    tmp = "null";
                }
                return tmp;
            }
        }
    </script>

    <script runat="server" id="method">
        
        private static string _connectionStringName = null;
        private static string _applicationName = null;
        private static uint _baseStoreFeatures = uint.MinValue;
        private static Dictionary<string, uint> _availableStoreFeatures = null;
        private static Dictionary<string, bool> _mainRoles = null;
        private static bool _enableSessionState = false;


        void Page_Load(object sender, EventArgs e)
        {
            //XmlDocument document = new XmlDocument();
            XMLExplorateur xe = new XMLExplorateur();

            Microsoft.SharePoint.SPSecurity.RunWithElevatedPrivileges(

                delegate()
                {
                    xe.Init(HttpContext.Current.Server.MapPath("~/web.config"));
                }

            );

            _connectionStringName = xe.ValueOf("configuration/myPortal/profileManagement/@connectionStringName");
            Response.Write("<br />_connectionStringName: " + _connectionStringName);

            _applicationName = xe.ValueOf("configuration/myPortal/profileManagement/@applicationName");
            Response.Write("<br />_applicationName: " + _applicationName);
            
            
            _baseStoreFeatures = Convert.ToUInt32(xe.ValueOf("configuration/myPortal/profileManagement/@baseStoreFeatures"));
            Response.Write("<br />_baseStoreFeatures:" + " " + _baseStoreFeatures.ToString());

            _enableSessionState = Convert.ToBoolean(xe.ValueOf("configuration/myPortal/profileManagement/@enableSessionState"));
            Response.Write("<br />_enableSessionState:" + " " + _enableSessionState.ToString());

            _availableStoreFeatures = xe.DictionaryStringUintItemsOf("configuration/myPortal/profileManagement/storeFeatures/storeFeature", "name", "value");
            Response.Write("<br /><br />_availableStoreFeatures:");
            foreach (string key in _availableStoreFeatures.Keys)
            {
                Response.Write("<br />Key: " + key + "Value:" + _availableStoreFeatures[key].ToString());
            }

            _mainRoles = xe.DictionaryStringBoolItemsOf("configuration/myPortal/profileManagement/mainRoles/role", "name", "hasAllFeatures");
            Response.Write("<br /><br />_mainRoles:");
            foreach (string key in _mainRoles.Keys)
            {
                Response.Write("<br />Key: " + key + " Value:" + _mainRoles[key].ToString());
            }
        }
        
    </script>
</body>
</html>
You should obtain that result when you call the page.



Sunday, October 19, 2008

Re-ghost items present in the master page gallery

Introduction
I write this post because I recently had to clean up for a customer a MOSS 2007 application where most of the master pages, layout pages and css were customized or were deployed with an upload of a file in the master page gallery. As I wanted to be able to deploy this application changes with a solution (.wsp) and wanted to perform my development work with Visual Studio, I had to re-ghost all the pages.

What I noticed first was it is not so easy to know what files were customized or were deployed by an upload, and for the customized files, what was the feature that has deployed them. I had to develop a custom tool to perform an audit, and had to think about specific practices in order to re-ghost the pages doing the less changes as possible to the original applicative architecture.
This work gave me the idea of this post that will present these practices, and the custom tool you may need to use if you are one day in the same case. By the way, I think it will be usefull for some, because deployment with SharePoint stays one of its dificult tasks, and because it is not the first time I see this scenario in a company:
Some development teams forget that with SharePoint, the deployment tasks can take long time, is risky, and don't think so much about preparing and testing it.
Then, delivery time comes and is difficult, and finaly, to respect the delivery dead line, the project is often deployed without respecting the best practices. Features and files are deployed by x-copy, or by a straight upload to the gallery, last bugs fixing are made directly on Production Environment with SharePoint Designer.
Then one day, the company needs to perform changes to the application and ask for support from a SharePoint consultant. And one of the first task of the consultant is to clean up the consequences of the first "quick" deployment.
To enumerate the different cases that lead to unghosted state, the way to identify them with the custom tool, the different practices to re-ghost a page, what is best than a tutorial?
I - Examining the different cases of unghosting and the way to obtain them
To start the tutorial, I will prepare a feature involving 3 master pages, will deploy it in Litware Portal, and perform actions to unghost the pages. I will examine the differences. I will also compare with a master page deployed by a straight upload to the gallery.
1 - the feature
I prepare a feature with 3 minimal master pages :
minimal1.master,
minimal2.master,
minimal3.master.

here is the feature xml file code :

<?xml version="1.0" encoding="utf-8" ?>
<Feature xmlns="https://schemas.microsoft.com/sharepoint/"
 Id="ABC820CB-B2BB-4779-868D-33F1EBCE3202"
 Title="e-marguerite : test re-ghosting"
 Description="Add three master pages to the master page gallery in order to perform unghosting and re-ghosting tests"
 Scope="Web"
 Hidden="TRUE"
 Version="1.0.0.0"
  AlwaysForceInstall="TRUE">
<ActivationDependencies></ActivationDependencies>
<ElementManifests>
 <ElementManifest Location="Elements.xml"/>
</ElementManifests>
<Properties>
</Properties>
</Feature>
And this is the Elements.xml code:

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="https://schemas.microsoft.com/sharepoint/">
 <Module Name="PlaceHolderMaster" List="116" Url="_catalogs/masterpage">
      <File Url="minimal1.master" Type="GhostableInLibrary" IgnoreIfAlreadyExists = "TRUE"></File>
      <File Url="minimal2.master" Type="GhostableInLibrary" IgnoreIfAlreadyExists = "TRUE"></File>
      <File Url="minimal3.master" Type="GhostableInLibrary" IgnoreIfAlreadyExists = "TRUE"></File>
 </Module>
</Elements>
2 - A look to Master page gallery
So when I have installed and activated my feature I can see my master pages in the Master Page Gallery.



3 - the 3 ways to obtain unghosted files
Now, we are going to unghost the master pages minimal2.master and minimal3.master.
We will also upload a new master page minimal4 that will be also unghosted.
  • With SharePoint designer
    I am now opening my site in SharePoint Designer. I open minimal2.master with check-out, I perform a change and save.
    SharePoint Designer warn me I am going to customized the file.
    I accept, then, I publish the page, and as a result, a "i" icon appears beside the file meaning it's customized.



  • Replacing an existing page by an upload in the master page gallery.
    In the master page gallery I click Upload and choose upload a document.



    I upload a file with the name : "minimal3.master". Then I go back to SharePoint Designer, and hit F5. Nothing has change for minimal3.master item except the modified date.
    However, the file is actually customized.




  • Straight Upload of a file in the gallery
    Now, using the Upload functionnality we used before, we upload a new master page in the gallery: minimal4.master
    It's appearing with the other deployed by feature.




    When we check in SharePoint Designer, the page is present but no indication about its deployment mode.



So, SharePoint Designer indicate just one case of the three:
When the page has been unghosted with SharePoint Designer.
So how to know for the other page if you haven't unghosted them yourself?
II - A custom tool to audit files in order to know if they are ghosted.
I wrote an Application Page that check the files of the lists of the site root web of a site collection.
Let's see how does it work with the previous files.
I call my page from any sub site of my site collection by typing the site url and my page name (in my case "_layouts/_checkcustomizedstatus.aspx).



I click the first link (Get root web list name) to have the name of all the lists of my Site Collection site root .
I can see my master page gallery.



I copy the name of the list which I want to check files and paste the name in the text box.
Then I click the second link.



So, let's examine the result:
  1. We have a table that give for each item:
    • the value of the file property vti_setuppath.
      This indicates the path to the physical file in the server File System that was used to deployed the item in the list.
      here is the MSDN entry.
    • The value of the file property: hasdefaultcontent
      If it set to true, that means that the file is templated, so linked to a template present in the server File System.
      here is the MSDN entry.
    • One of the three possible value regarding customization:
      customized, uncustomized, none.

  2. Notice the specific values for the files we have worked with:




  3. The Application Page has written a physical file in the server File System for each "customized" or "none" files in a "C:\Unghosted Files" directory.



    Notice that we have our unghosted files, and that the other files are always unghosted in an out of the box MOSS 2007 portal.



Conclusion :
  • For a file deployed with a feature, then, customized with SharePoint designer or replaced by an upload, we have:
    - hasdefaultcontent set to empty.
    - set up path non empty.
  • For a file deployed by a straight upload, we have:
    - hasdefaultcontent set to empty.
    - set up path set to empty.
here is the html and c# code of the Application Page:
<%@ Page Language="C#" AutoEventWireup="true" %>
<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>
<%@ Import Namespace="System.IO" %>
<html xmlns="https://www.w3.org/1999/xhtml">
<head>
    <title></title>
</head>
<body>
    <script runat="server">
        void getList(object sender, EventArgs e)
        {
            string myResult = "";
            string myListName = txt1.Text;
            //SPList myList = SPContext.Current.Web.Lists[myListName];
            myResult += "<table style='width:80%;border:solid 1px gray;border-collapse:collapse'>";
            myResult += "<tr>" + "<td style='border:solid 1px gray;font-weight:bold' >" + "Display Name" + "</td>" + "<td style='border:solid 1px gray;font-weight:bold' >" + "Item Count" + "</td>" + "<td style='border:solid 1px gray;font-weight:bold' >" + "Base Type" + "</td>" + "</tr>";
            foreach (SPList aList in SPContext.Current.Site.RootWeb.Lists)
            {
                myResult += "\n<tr>" + "<td style='border:solid 1px gray;' >" + aList.Title + "</td>" + "<td style='border:solid 1px gray;' >" + aList.ItemCount + "</td>" + "<td style='border:solid 1px gray;' >" + aList.BaseType + "</td>" + "</tr>";
            }
            myResult += "</table>";
            result.Text = myResult;
        }
        void WriteCustomizedAndNone(object sender, EventArgs e)
        {
            string myResult = "";
            string myListName = txt1.Text;
            SPList myList = SPContext.Current.Web.Site.RootWeb.Lists[myListName];
            if (!Directory.Exists("C:\\Unghosted Files\\"))
            {
                Directory.CreateDirectory("C:\\Unghosted Files\\");
            }
            myResult += "Status of items<br><br>";
            myResult += "<table style='width:80%;border:solid 1px gray;border-collapse:collapse'>";
            myResult += "<tr>" + "<td style='border:solid 1px gray;font-weight:bold' >" + "Name" + "</td>" + "<td style='border:solid 1px gray;font-weight:bold' >" + "Setup Path" + "</td>" + "<td style='border:solid 1px gray;font-weight:bold' >" + "has default content" + "</td>" + "<td style='border:solid 1px gray;font-weight:bold' >" + "is customized" + "</td>" + "</tr>";
            foreach (SPListItem anItem in myList.Items)
            {
                myResult += "<tr>" + "<td style='border:solid 1px gray;' >" + anItem.Name + "</td>" + "<td style='border:solid 1px gray;' >" + anItem.File.Properties["vti_setuppath"] + "</td>" + "<td style='border:solid 1px gray;' >" + anItem.File.Properties["vti_hasdefaultcontent"] + "</td>" + "<td style='border:solid 1px gray;' >" + anItem.File.CustomizedPageStatus + "</td>" + "</tr>";
                if (anItem.File.CustomizedPageStatus.ToString() != "Uncustomized")
                {
                    byte[] myBytes = anItem.File.OpenBinary();
                    System.IO.File.WriteAllBytes("c:\\Unghosted Files\\" + anItem.File.Name, myBytes);
                }
            }
            myResult += "</table>";
            result.Text = myResult;
        }
    </script>
    <form runat="server">
        <asp:LinkButton runat="server" ID="btn1" Text="Get root web lists name" OnClick="getList" />
        <br />
        <br />
        List Name
        <asp:TextBox runat="server" ID="txt1"></asp:TextBox>
        <br />
        <br />
        <asp:LinkButton runat="server" ID="btnItemSample" Text="Get files customized status and write customized and none on disk"
            OnClick="WriteCustomizedAndNone" />
        <br />
        <br />
        <br />
        <br />
        <asp:Label runat="server" ID="result"></asp:Label>
        
    </form>
</body>
</html>
III - Re-ghosting the pages
We are now going to re-ghost the pages in order to:
  • make their changes performed with Visual Studio be visible in the portal,
  • make them upgradable by a solution (.wsp) deployment.

We will see that the policy choosen to re-ghost the page will depend on the way it has been un-ghosted.
1 - reset to Site Definition with Sharepoint designer
We are going to re-ghost minimal2.master.
As it was customized with SharePoint designer, SharePoint designer offers us the possibility to roll back the actions we have done on this page, aka:
  • Made changes.
  • un-ghosted it.
So if you make just a roll back you will loose all the changes performed on the page. If you want to re-ghost the page while saving the changes do the following :
  • Go to "c:\unghosted files\" and copy the file minimal2.master.
  • check the feature path on the _checkcustomizedstatus.aspx page.
  • overwrite the minimal2.master in the feature directory by the one you have just copied from "c:\unghosted files\".

Now, we have saved the changes, we can re-ghost the page.
Right click the page in SharePoint Designer and select Reset to Site Definition. In the warning pop-up click OK. SharePoint Designer make a copy of the customized file anyway, thus you cannot loose your changes.



Now refresh the _checkcustomizedstatus.aspx page. You see that your file has been re-ghosted. You see also that the changes you had performed to the page are still there.

2 - reset to Site Definition with SharePoint Administration
We are now going to re-ghost minimal3.master, and we cannot do it with SharePoint Designer since this page has not been un-ghosted with SharePoint Designer. By the way if you right click the page in SharePoint designer, you will not see the menu entry : "Reset to Site Definition".
In order to reset minimal3.master to Site Definition we have to do the following :
  • First of all do not forget to replace the file minimal3.master present in the feature directory by the one that is in "c:\unghosted files\" in order to not loose your modifications. (Except if you mis-overwrite the page when you performed the upload and want a real roll back).
  • We need the page url to make a Reset to Site definition in SharePoint so, go to master page gallery, right click the page and choose "View Porperties".
  • On the page click the page name Link.



  • In the case of a master page, you will obtain an error message, but you will also have the Url. Copy it.



  • Go tohe Site Settings page and in the column Look And Feel, click to Reset to site definition.



  • Paste the Url in the text box, and click reset.
  • On the warning pop up clik yes.
We can now check on the _checkcustomizedstatus.aspx page. You see that your file has been re-ghosted. You see also that the changes you had performed to the page are still there.



3 - Ghosting a file that has never been ghosted.
To finsih, we have to ghost the file that was just uploaded in the Gallery: minimal4.master. I don't write re-ghost since it has never been ghosted.
This is the most delicate operation.
We can imagine to go to the "C:\unghosted files\" directory, copy the file in the feature, change the Elements.xml file do add minimal4.master entry, then deactivate the feature and reactivate it in order to overwrite minimal4.master in the master page gallery.
We cannot do that because all we can do with a provisionning feature is to add new files but neither overwrite nor remove any files.
Then, the only way to ghost minimal4.master is to add the same master page with a new name, then change from minimal4.master to this new file for all the publishing pages that are using it.

  • We go to "C:\unghosted files\" directory and copy the file (don't forget that in real life, you will certainly have not the original minimal4.master file, but only its name in the master page gallery). Paste it in the feature directory with a new name. For instance:
    minimal4_Reghost.master
  • Then, add a new entry in Elements.xml for this new page.
  • Deactivate the feature in order to be allowed to re-activate it.
  • Activate the feature.
  • Now chek _checkcustomizedstatus.aspx page. You can see your new master with the status uncustomized.



  • Change the reference to minimal4.master to minimal4_Reghost.master for all the pages of your site collection that use it.
  • Once it is done, delete minimal4.master. (you will not be allowed to delete it by SharePoint untill there is no more reference to this master for any page of your Site Collection).
  • By the way, you can also delete the copy of minimal2.master made by SharePoint Designer.
When it's done clean up job is finished you may obtain that in _checkcustomizedstatus.aspx page.



Friday, October 17, 2008

Use layout page metadata in masterpages

This post is a tribute to my colleague Riad TIZERARINE that has found this tip. We had to use some of the properties of the layout Pages inside the master page. We wanted precisely to take the value of the field description of the Layout Page, and put it inside the meta tag "description.
So I will illustrate the elegant solution that have found Riad in a short tutorial and will show that this practice can be extended.

1 - Create a minimal master page and put it in the master page gallery

I usually work on master pages starting with minimal one as recommanded by Microsoft :How to: Create a Minimal Master Page
So I did a feature in order to deploy this master page and let it to the ghosted state because I want to use visual studio to customize it.
When I have deployed this master page in the master page gallery, and change it for my Litware portal root site, I obtain this result.


2 - populate your publishing page fields

Now I have to populate my publishing page fields in order to have data to use in the demonstration.


3 - Programming with C# in line script

So the idea now is to use the metadata of the page layout inside the master page to display informations that are stored at the Layout Page level, but WITHOUT writing a single line of code in the layout page.
We will illustrate this by using description porperty of the layout page in a meta tag description, and using ["Modified By"], [Modified"] and Page Layout Contact e-mail to populate the footer of the master page.
So how to do it in an elegant way?
Riad found the solution with this instruction:

SPcontext.Current.File

MSDN doesn't teach so much about this instruction : Gets the file that is associated with the list item object of the given Windows SharePoint Services context. (SPContext Members (Microsoft.SharePoint))
here is the sample that show how to use it :
  • To put the page layout description inside a meta tag description, put this in line script right under the meta tag of your master page.

    
    <%        SPFile myPublishingPage = SPContext.Current.File;
              string description = string.Empty;
              if (myPublishingPage.Item.Fields.ContainsField("Description"))
              {
                  description = (string)myPublishingPage.Item["Description"];
              }
    %>
    
    <meta name="Description" content="<%=description %>" />
    
    
    And there is a much more simple way :
    
    <%        SPItem myPublishingPageItem = SPContext.Current.Item;
              string description = string.Empty;
    
              if (myPublishingPageItem.Fields.ContainsField("Description"))
              {
                  description = (string)myPublishingPageItem["Description"];
              }
    %>
    <meta name="Description" content="<%=description %>" />
    
    
  • Here is the in line script and html code to render the footer using layout page properties as previously defined Warnings: Place your in line script after the form tag otherwise it will not be working.
    And as usual, don't forget that in line script in master page is just a temporary solution, to make a proof of concept, and not a best practice for real applications.

    
        <%
    
            char[] separator = { '#' };
            string strMyUser = string.Empty;
            strMyUser = myPublishingPageItem["Modified By"].ToString().Split(separator)[1];
            char[] separator2 = { '(' };
            string[] maTable;
            strMyUser = strMyUser.Split(separator2)[0];
    
            string creationDate = string.Empty;
            creationDate = myPublishingPageItem["Modified"].ToString();
    
            string authorEmail = string.Empty;
            if (myPublishingPageItem.Fields.ContainsField("Contact E-Mail Address"))
            {
                authorEmail = (string)myPublishingPageItem["Contact E-Mail Address"];
            }
    
     
        %>
        <br />
        <div style="width: 100%; text-align: center; color: #5B5B5B; font-family: Tahoma;
            font-size: 8pt;">
            <table id="footer" style="width: 600px; border: solid 1px #5B5B5B; border-collapse: collapse">
                <tr>
                    <td style="text-align: center; width: 200px">
                        Modified by: <%=strMyUser%></td>
                    <td>
                         | </td>
                    <td width="200px">
                        At
                        <%=creationDate %>
                    </td>
                    <td>
                         | </td>
                    <td width="200px">
                        <a href="mailto:<%=authorEmail%>">contact us</a></td>
                </tr>
            </table>
        </div>
    
    
    

4 - Results

Here is the new home page with the footer after the implementation:



Here is the complete code of the minimal master page after the implementation:


<%-- Identifies this page as a .master page written in Microsoft Visual C# and registers tag prefixes, namespaces, assemblies, and controls. --%>
<%@ Master Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Register TagPrefix="SPSWC" Namespace="Microsoft.SharePoint.Portal.WebControls"
    Assembly="Microsoft.SharePoint.Portal, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls"
    Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages"
    Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="PublishingWebControls" Namespace="Microsoft.SharePoint.Publishing.WebControls"
    Assembly="Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="PublishingNavigation" Namespace="Microsoft.SharePoint.Publishing.Navigation"
    Assembly="Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="wssuc" TagName="Welcome" Src="~/_controltemplates/Welcome.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="DesignModeConsole" Src="~/_controltemplates/DesignModeConsole.ascx" %>
<%@ Register TagPrefix="PublishingVariations" TagName="VariationsLabelMenu" Src="~/_controltemplates/VariationsLabelMenu.ascx" %>
<%@ Register TagPrefix="PublishingConsole" TagName="Console" Src="~/_controltemplates/PublishingConsole.ascx" %>
<%@ Register TagPrefix="PublishingSiteAction" TagName="SiteActionMenu" Src="~/_controltemplates/PublishingActionMenu.ascx" %>
<%-- Uses the Microsoft Office namespace and schema. --%>
<html>
<WebPartPages:SPWebPartManager runat="server" />
<SharePoint:RobotsMetaTag runat="server" />
<%        SPItem myPublishingPageItem = SPContext.Current.Item;
          string description = string.Empty;

          if (myPublishingPageItem.Fields.ContainsField("Description"))
          {
              description = (string)myPublishingPageItem["Description"];
          }
%>
<meta name="Description" content="<%=description %>" />
<%-- The head section includes a content placeholder for the page title and links to CSS and ECMAScript (JScript, JavaScript) files that run on the server. --%>
<head runat="server">
    <asp:contentplaceholder runat="server" id="head">
  <title>
        <asp:ContentPlaceHolder id="PlaceHolderPageTitle" runat="server" />

</title>

    </asp:contentplaceholder>
    <SharePoint:CssLink runat="server" />
    <asp:contentplaceholder id="PlaceHolderAdditionalPageHead" runat="server" />
</head>
<%-- When loading the body of the .master page, SharePoint Server 2007 also loads the SpBodyOnLoadWrapper class. This class handles .js calls for the master page. --%>
<body onload="javascript:_spBodyOnLoadWrapper();">
    <%-- The SPWebPartManager manages all of the Web part controls, functionality, and events that occur on a Web page. --%>
    <form runat="server" onsubmit="return _spFormOnSubmitWrapper();">
        <wssuc:Welcome id="explitLogout" runat="server" />
        <PublishingSiteAction:SiteActionMenu runat="server" />
        <PublishingWebControls:AuthoringContainer ID="authoringcontrols" runat="server">
            <PublishingConsole:Console runat="server" />
        </PublishingWebControls:AuthoringContainer>
        <%-- The PlaceHolderMain content placeholder defines where to place the page content for all the content from the page layout. The page layout can overwrite any content placeholder from the master page. Example: The PlaceHolderLeftNavBar can overwrite the left navigation bar. --%>
        <asp:ContentPlaceHolder ID="PlaceHolderMain" runat="server" />
        <asp:Panel Visible="false" runat="server">
            <%-- These ContentPlaceHolders ensure all default SharePoint Server pages render with this master page. If the system master page is set to any default master page, the only content placeholders required are those that are overridden by your page layouts. --%>
            <asp:ContentPlaceHolder ID="PlaceHolderSearchArea" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderTitleBreadcrumb" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderPageTitleInTitleArea" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderLeftNavBar" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderPageImage" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderBodyLeftBorder" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderNavSpacer" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderTitleLeftBorder" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderTitleAreaSeparator" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderMiniConsole" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderCalendarNavigator" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderLeftActions" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderPageDescription" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderBodyAreaClass" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderTitleAreaClass" runat="server" />
            <asp:ContentPlaceHolder ID="PlaceHolderBodyRightMargin" runat="server" />
        </asp:Panel>
    </form>
    <%
        /*//Use that if you want to explore the page properties
        Response.Write("<Table style='border-collapse:collapse;'>");
        System.Collections.Generic.SortedDictionary<string, string> myList = new System.Collections.Generic.SortedDictionary<string, string>();
        foreach (SPField aField in myPublishingPageItem.Fields)
        {
            try
            {
                myList.Add(aField.ToString(), myPublishingPageItem[aField.ToString()].ToString());
            }
            catch
            {

            }
        }
        foreach (string aString in myList.Keys)
        {
            Response.Write("<tr ><td nowrap style='margin:0px;width:300px;border:solid 1px gray;font-size:9pt;'>" + aString + "</td><td  nowrap style='border:solid 1px gray;font-size:9pt;'>" + myList[aString] + "</td></tr>");
        }
        Response.Write("</Table>");*/

        char[] separator = { '#' };
        string strMyUser = string.Empty;
        strMyUser = myPublishingPageItem["Modified By"].ToString().Split(separator)[1];
        char[] separator2 = { '(' };
        string[] maTable;
        strMyUser = strMyUser.Split(separator2)[0];

        string creationDate = string.Empty;
        creationDate = myPublishingPageItem["Modified"].ToString();

        string authorEmail = string.Empty;
        if (myPublishingPageItem.Fields.ContainsField("Contact E-Mail Address"))
        {
            authorEmail = (string)myPublishingPageItem["Contact E-Mail Address"];
        }

 
    %>
    <br />
    <div style="width: 100%; text-align: center; color: #5B5B5B; font-family: Tahoma;
        font-size: 8pt;">
        <table id="footer" style="width: 600px; border: solid 1px #5B5B5B; border-collapse: collapse">
            <tr>
                <td style="text-align: center; width: 200px">
                    Modified by: <%=strMyUser%></td>
                <td>
                     | </td>
                <td width="200px">
                    At
                    <%=creationDate %>
                </td>
                <td>
                     | </td>
                <td width="200px">
                    <a href="mailto:<%=authorEmail%>">contact us</a></td>
            </tr>
        </table>
    </div>
</body>
</html>

Thursday, October 16, 2008

Improve speed of development for SharePoint (Part3) - Using in line scripts in SharePoint programming


I am often thinking about collecting the reasons that explains why development are sometimes taking so much time in SharePoint projects comparing to Asp .Net ones? In the main presentation post of this topic (improve your speed of development for SharePoint Part 1) I tracked two main reasons:
  1. Most of the server code you will have to write will have to be built before having to be tested.
  2. You will have to deploy the code you've just written on your development machine before testing it.
These numerous operations of building and deployment set a lot of time especially if they come along with IIS reset. If you compare with a Asp .Net project, the developer in the case of a pure Asp .Net, write his client and server code, and test it by just pressing F5 !
So I will try to think about way of developing SharePoint code in order to be as close as possible to the Asp .Net developper experience.
So to avoid the two noticed previous points, there is obvious things to do :
  • using scripts
  • working directly on pages that are below the 12 directory.

I will develop the "Using Scripts" topic in this post.

1 - Using in line scripts in SharePoint programming


You can program against WSS Object Model writting in line code in all the pages:
Master Pages
Layout Pages
Application Pages
Of course, for the two first type of Pages, it is a temporary solution, because it is not a best practice to use in line code in these pages since this code won't be able to execute if the page is customized.
To make in-line code run in master pages or layout pages (Site Pages) you have two solutions.

First, you can use in line script if you deploy the page with a feature, since the Site Pages and the in-line script run just fine as long as the pages remains uncustomized in a ghosted state, and deployment using a feature let master pages or content pages in a ghosted state.
Then you can use Visual Studio to modify the page. As the page is ghosted, if you modify the page file on the server, you will see changes appear in your web Site.
Regarding SharePoint Designer, you can use it if you open the page on the computer File Sytem using its physical path. But if you use SharePoint Designer and open the page using its url the page will stop working.
Why ?
Remember that WSS compiles a ghosted page into an assembly DLL for processing. However, as soon as a user modifies any aspect of this page with the SharePoint Designer after having open it using its URL, and moves the site page into an unghosted state, WSS then begins to use safe mode to process it. Because the page contains in-line script, WSS refuses to process it in safe mode and generates an error message.

So that leads to the second way of using in line script in Site Pages. Allowing WSS to process in line script in customized pages. For that you have to modify your web application web.config in order to allow in line code in this kind of pages.

cf.: SharePoint 2007: using ASP.NET server side code in your pages

Of course doing that on a production environment is not a good idea, but on the opposite, it can be a good idea to do it in a development environment. Doing that allows you to customize the pages that contain in line script and for example, to open this kind of pages by using their URL in SharePoint Designer to modify the in line script.

Of course, it will be much easier to test your in line script inside Application Pages (pages in the 12\TEMPLATE\LAYOUTS directory) since in line script is perfectly legal in these pages, and you won't have to use a feature to deploy the page or to modify the web.config to test your code.

Anyway, starting writing your functionality using in line code will make you save a lot of time since you won't have to build your code before testing it. You won't have neither to deploy your dll nor have to wait for IIS Reset or Pool Recycling. For exemple, when you plan to write a web part, write your web part functionality code inside an Application Page, and when it works, create a web part and paste your tested code inside of it.
Same thing for a component that has to be placed in a Layout Page. First write it and test it inside the layout page in an in line code format, then, when it works, paste the code inside a component.
And again the same for a feature receiver element...

In general, I think it's better to avoid .dll in SharePoint as often as possible :
  • web part,
  • Custom controls,
  • etc.
Reading Microsoft documentation on Server Controls, a Custom Control, as a compiled component, is planned to be shared between several applications. It is useless to create a custom control for one application.
It's better in this case to create an User Control (.ascx) or to use a Smart Part. The application performances will stay the same since by default, all is compiled in Asp .Net.
Doing that, you will improve your development speed, because you will just have to copy your in line script code in an .ascx or a Smart Part, and you will also improve the easyness of deployment and increase the maintanability of your application because there often will be no more .dll in your applications.
Regarding the question of choices in matter or applicative architecture there is the excellent post of Chris Johnson. You will, by the way, notice that custom controls in Layout pages are not mentioned.

2 - The two different kinds of in line script for SharePoint


there are two different ways writing in line server code script for SharePoint:
  • the best known one, the script block is placed between <script runat="server" > and </script > tags
  • less known (except for those who had to fight against it in SharePoint 2003 or did ASP) between <% and %> tags is called Embedded Code Blocks
I used to become fond of Embedded Code Blocks because for simple functionalities your code can be so clear since you keep your server code linked to your HTML structure. I mean, when you need to write somtehing at a specific place in your page, and need server code to do so, you can inject the minimal server code you need just close to the place where you need it.
So it may be easier later to understand your server code while reading the page code.
I use to use it in some of my other posts :

The difference between these two kinds of in line script (regarding syntax), is that you cannot declare classes or method in the second one, but both are working well together as shown is the following Application Page sample. Regarding performances, if you want to use Embedded Code Blocks in your real applications, I let to examine the consequences reading the previous mentioned article (Embedded Code Blocks ).
Personally I use Embedded Code Blocks to test a piece of code quickly, to make a quick POC for a customer presentation. After that I reuse the code in more standard containers (script block, ascx, code behind, web parts, control server, etc.)

3 - Code sample

Now, I want to show how easy it is to write in line code for SharePoint.
Copy the following code in a file you will call _test.aspx.
Open Visual Studio and drag and drop the file in Visual Studio.
Notice you have Intellisense for your custom class and your SharePoint objects !
No need to create a solution, a project, etc.
Test some code, write somehting using SharePoint Object Model...
Then click the "File" menu item in the top left corner of Visual Studio and click "Save _test.aspx As"



And copy the file in the SharePoint Application Pages directory:

C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\Template\Layouts

(trick: you will go there quicker with a "Shortcut to 12" on yourn desktop).



Then assume you have a SharePoint site in your computer with the "http://localhost/" url, open the Application Page by navigating to "http://localhost/_layouts/_test.aspx.

ou should obtain something like that:




<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Page Language="C#" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
 
<script runat="server">
    //method
    public string WriteSomething()
    {
        return "something";
    }
    //method with a parameter
    public void writeAtTheTop(string something)
    {
        Response.Write(something);
    }
 
    //declaring a class
    public class myClass
    {
        //using methods inside
        public string WriteSomethingElse()
        {
            return "something else";
        }
        public string WriteWhatYouWant(string whatYouWant)
        {
            return whatYouWant;
        }
    }
 
</script>
 
<div>
    <!--writing a bloc of code-->
    <!-- and testing intellisense with SharePoint and possibility to program against WSS Object Model-->
    <%
        //using a method from the script runat server
        WriteSomething();
        //another method
        writeAtTheTop("I write at the top of the page.<br>");
        //declaring a variable
 
        string myWebTitle = string.Empty;
 
        using (SPWeb myWeb = SPContext.Current.Web)
        {
            myWebTitle = myWeb.Title;
        }
    %>
    <!--different ways of displaying values inside a page: -->
 
    <%=WriteSomething() %>
    <br />
 
    <!-- with an instance of the class declared inside the script block -->
    <%=new myClass().WriteSomethingElse()%>
    <br />
    <br />
 
    <!-- with a parameter-->
    <%=new myClass().WriteWhatYouWant("My Web Site Title is:")%>
    <br />
 
    <%-- reusing previous variable declared in the Embedded Code Block --%>
    <span style="color: Red">
        <%=new myClass().WriteWhatYouWant(myWebTitle)%>
    </span>
    <br />
    <br />
 
    <!-- testing again intellisense with SharePoint and possibility to program against WSS Object Model-->
    My Web Site Title is :
    <br />
    <%=SPContext.Current.Web.Title %>
</div>

Look at the previous code, 3 page directives, and here you are, you can start programming for SharePoint.