Friday, November 28, 2008

Use Microsoft Chart Controls for .NET Framework in a SharePoint web site



Introduction



This tutorial shows how to use Microsoft Chart Controls for .NET Framework in a SharePoint web site. I will post soon a new tutorial that will show how to integrate a chart in a SharePoint Web Part in order to configure the Chart Control through the Web Part Tool Pane. This tutorial is compliant both with MOSS and Windows SharePoint Services.



1 - Prerequisites




  • Microsoft Chart Controls for Microsoft .NET Framework 3.5 are installed on the development environment. You can download installation package
    here




  • Microsoft Visual Studio 2008 is installed on the development environment.


  • Microsoft Chart Controls Add-on for Microsoft Visual Studio 2008 is installed on the development environment. You can download it
    here
    .


  • Samples Environment for Microsoft Chart Controls are installed on the development environment. If not you can download them

    here.



You can check that System.Web.DataVisualization.dll is in the GAC











2 - Tutorial Overview


This tutorial presents the following operations:


  • Creating a asp .net Web Site and an in line script .aspx page to test a Chart Control.


  • Creating a SharePoint team Site where the previous aspx page will be deployed.


  • Copy the Web Samples images under the SharePoint 12 hive.


  • Modify the SharePoint site Web Application config file in order to make the chart controls to be usable in the SharePoint site.


  • Deploy the .aspx page in the SharePoint site and test it.




3 - Tutorial



3.1 Creating a asp .net Web Site and an in line script .aspx page to test a Chart Controls.

First create a web site project with Visual Studio 2008. Add a new Web Form to your web site and choose to put server code in line.
Asume you call it MsChartControlForSharePoint.aspx


Then view your page in desgin mode and drag and drop a Chart control from the Tool Box of Visual Studio to your page.









Then put the following server script in your page


<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
// Create new data series and set it's visual attributes
Series series = new Series("Spline");
series.ChartType = SeriesChartType.Spline;
series.BorderWidth = 3;
series.ShadowOffset = 2;
// Populate new series with data
series.Points.AddY(67);
series.Points.AddY(57);
series.Points.AddY(83);
series.Points.AddY(23);
series.Points.AddY(70);
series.Points.AddY(60);
series.Points.AddY(90);
series.Points.AddY(20);
// Add series into the chart's series collection
Chart1.Series.Add(series);
}
</script>





here is the MsChartControlForSharePoint.aspx page complete code:







<%@ Page Language="C#" %>
<%@ Register Assembly="System.Web.DataVisualization, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
Namespace="System.Web.UI.DataVisualization.Charting" TagPrefix="asp" %>
<%@ Import Namespace="System.Web.UI.DataVisualization.Charting" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title></title>

<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
// Create new data series and set it's visual attributes
Series series = new Series("Spline");
series.ChartType = SeriesChartType.Spline;
series.BorderWidth = 3;
series.ShadowOffset = 2;

// Populate new series with data
series.Points.AddY(67);
series.Points.AddY(57);
series.Points.AddY(83);
series.Points.AddY(23);
series.Points.AddY(70);
series.Points.AddY(60);
series.Points.AddY(90);
series.Points.AddY(20);

// Add series into the chart's series collection
Chart1.Series.Add(series);
}

</script>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Chart ID="Chart1" runat="server" Width="694px">
<Series>
<asp:Series Name="Series1">
</asp:Series>
</Series>
<ChartAreas>
<asp:ChartArea Name="ChartArea1">
</asp:ChartArea>
</ChartAreas>
</asp:Chart>
</div>
</form>
</body>
</html>






Right click your page and choose "view in browser", you should obtain that result:












3.2 Creating a SharePoint team Site where the previous aspx page will be deployed.

Create a new Sharepoint Web application, then a site collection, and choose "Team Site" as site template.











3.3 Copy the Web Samples images under the SharePoint 12 hive.


Open the file where you unziped Web Samples and locate Images folder. Copy the content of the folder.










Under the 12 hive create a sub-directory "MicrosoftChartControls" in the IMAGES directory. Paste the previously copied images.










3.4 Modify the SharePoint site Web Application config file in order to make the chart controls to be usable in the SharePoint site.


Add a line to SafeControl Section




<SafeControl Assembly="System.Web.DataVisualization, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Namespace="System.Web.UI.DataVisualization.Charting" TypeName="*" Safe="True" AllowRemoteDesigner="True"/>
</SafeControls>








Add the path="ChartImg.axd" line to httpHandlers section





<httpHandlers>
<remove verb="GET,HEAD,POST" path="*" />
<add verb="GET,HEAD,POST" path="*" type="Microsoft.SharePoint.ApplicationRuntime.SPHttpHandler, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
<add verb="OPTIONS,PROPFIND,PUT,LOCK,UNLOCK,MOVE,COPY,GETLIB,PROPPATCH,MKCOL,DELETE,(GETSOURCE),(HEADSOURCE),(POSTSOURCE)" path="*" type="Microsoft.SharePoint.ApplicationRuntime.SPHttpHandler, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
<add verb="*" path="ChartImg.axd" type="System.Web.UI.DataVisualization.Charting.ChartHttpHandler, System.Web.DataVisualization, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="false"/>
</httpHandlers>







Add a appSettings section

Go to the end of the web.config file and locate the ending tag </System.Workflow.ComponentModel.WorkflowCompiler>. Paste the appSettings section.



</System.Workflow.ComponentModel.WorkflowCompiler>
<appSettings>
<add key="ChartImageHandler" value="storage=memory;timeout=20;URL=/_layouts/Images/MicrosoftChartControls/" />
</appSettings>






As we are going to deploy the MsChartControlForSharePoint.aspx page in the Shared Document library of the SharePoint site , and there is in line code in it, we have to modify again the web.config in order to allow in line script to be executed in that page. Of course this is just for a matter of demonstration and is not a best practice at all. I will publish soon a post in order to deploy the control inside a Web Part.



Locate the "PageParserPaths" section and paste the "PageParserPath" line.


<SharePoint>
<SafeMode MaxControls="200" CallStack="false" DirectFileDependencies="10" TotalFileDependencies="50" AllowPageLevelTrace="false">
<PageParserPaths>
<PageParserPath VirtualPath="/Shared Documents/*" AllowServerSideScript="true" CompilationMode="Always"/>
</PageParserPaths>
</SafeMode>







3.5 Deploy the .aspx page in the SharePoint site and test it.


Now we just have to upload the MsChartControlForSharePoint.aspx page in the Shared Document library.





















and test it.













4 - Related Links


You can find additional information at:

Thanks to Alex Gorev for having referenced this post.

Active Directory Forms Based Authentication

Recently we needed to use Forms Based Authentication with Active Directory from an Extranet. This is how we got it to work.1. Extend the Web ApplicationInstead of tweaking the internal web app, Extend the web application you want to expose to the Extranet, giving it the required host headers etc.2. Configure SharePoint Central Admin to use FBA for the "new" Web Applications


<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.










Saturday, October 18, 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="http://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="http://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="http://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



Use Layout Pages Metadata (properties) in the Master Page




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 descrption 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>