Application Pages versus Site Pages
Using the most basic definition, a site page is customizable by an end user while an application page is not. That means that a user can pop open SharePoint Designer 2010 and make changes to a site page, but they cannot do this with an application page. So, what do we mean by “customizable”? When we open SharePoint Designer 2010 and make changes to a site page, those changes are stored in the database. The next time we request the page, the page is loaded from the database. There are more differences than this, but the key difference is really the ability to customize a page.Coming from an ASP.NET background, we are much more used to coding application pages. We create a page, drag and drop some controls, write some backend code, hit F5, and see our page do something. One of the benefits of SharePoint is that we can create page templates that allow an end user to make changes to the page without requiring a developer. This means they can add web parts, add JavaScript, do neat things with jQuery and XSLT… all the kind of stuff you see on http://endusersharepoint.com.
As a developer, you can do some pretty cool stuff to enable the end user with site page templates.
Creating the Site Page
To start with, create an empty SharePoint 2010 project. I called mine “SampleToDeployAPage”. When prompted, choose to create this as a farm solution rather than a sandboxed solution. Once you have the project created, right-click and add a new Module called “CustomPages”. A folder is created called “CustomPages” with a file called “Sample.txt”. Rename Sample.txt to “MyPageTemplate.aspx”. Add the following markup to it:1: <%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>
2: <%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>
3: <%@ Register Tagprefix="SharePoint"
4: Namespace="Microsoft.SharePoint.WebControls"
5: Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
6: <%@ Register Tagprefix="Utilities"
7: Namespace="Microsoft.SharePoint.Utilities"
8: Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
9: <%@ Register Tagprefix="asp"
10: Namespace="System.Web.UI"
11: Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>
12: <%@ Import Namespace="Microsoft.SharePoint" %>
13: <%@ Assembly
14: Name="Microsoft.Web.CommandUI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
15:
16: <%@ Page
17: Language="C#"
18: CodeBehind="MyPageTemplate.aspx.cs"
19: Inherits="SampleToDeployAPage.MyPageTemplate, $SharePoint.Project.AssemblyFullName$"
20: masterpagefile="~masterurl/default.master"
21: title="Testing This Page"
22: meta:progid="SharePoint.WebPartPage.Document" %>
23:
24: <asp:Content id="Content1" runat="server" contentplaceholderid="PlaceHolderMain">
25: <asp:Button runat="server" ID="button1" OnClick="Button_Click" />
26: <asp:Label runat="server" ID="label1"/>
27: <div></div>
28: <div></div>
29: For more information, visit
30: <a href="http://msdn.microsoft.com/en-us/library/bb964680(v=office.12).aspx">
31: Chapter 3: Pages and Design (Part 1 of 2)</a>
32: </asp:Content>
Now that you’ve created the markup, let’s create the code-behind. Right click the CustomPages folder and add a new Class called MyPageTemplate.aspx.cs. We’ll keep this one short:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.SharePoint.WebControls; using Microsoft.SharePoint.WebPartPages; using System.Web.UI.WebControls; namespace SampleToDeployAPage { public class MyPageTemplate : WebPartPage { protected Button button1; protected Label label1; protected void Page_Load(object sender, EventArgs e) { } protected void Button_Click(object sender, EventArgs e) { label1.Text = System.DateTime.Now.ToLongTimeString(); } } }Looking at the two samples together, we are simply creating markup that references an assembly in the GAC. When we click a button, our Button_Click handler is called, and we can set the Text property of a label control in the markup. This is basic stuff to an ASP.NET developer. The result in our Solution Explorer pane should look kind of like the following:
See the attachment to this post for full source code.
Provisioning a Page Using Visual Studio 2010
Now that we’ve created our page, we need to deploy it to Visual Studio 2010. When we created our module called “CustomPages”, a file called “elements.xml” was created for us. That file tells SharePoint how to deploy our page to all of the web front end servers. We can even provision multiple instances of our template.<?xml version="1.0" encoding="utf-8"?> <Elements xmlns="http://schemas.microsoft.com/sharepoint/"> <Module Name="CustomPages" Url="SitePages" Path="CustomPages"> <File Url="MyPageTemplate.aspx" Name="SamplePage1.aspx" Type="Ghostable"/> <File Url="MyPageTemplate.aspx" Name="SamplePage2.aspx" Type="Ghostable"/> <File Url="MyPageTemplate.aspx" Name="SamplePage3.aspx" Type="Ghostable"/> </Module> </Elements>In this example, we are provisioning three instances of our page based on the single page template. We are provisioning this to the SitePages library for our site. If we deploy the solution as-is, we should see the following in SharePoint Designer 2010.
I highly recommend reading Understanding and Creating Customized and Uncustomized Files in Windows SharePoint Services 3.0 for deeper explanation of what is happening here.
Adding Links to the UI for Our Pages
At this point, we can right click the page and choose “Preview in Browser” to see the page. The URL on my box is http://kirke1/sites/team/SitePages/SamplePage3.aspx, but this may be different for your environment. I wanted to add a few links to the Site Actions menu, and the easiest way to do this is to use the freely available Community Kit for SharePoint: Development Tools Edition. It includes a Custom Action project item that makes creating the following XML a snap, but you can simply cut and paste and add the following to the Elements.xml file (as a child of the Elements node) that we created previously.<CustomAction Description="Custom action for page 1" GroupId="SiteActions" Id="MySiteAction1" Location="Microsoft.SharePoint.StandardMenu" Sequence="1000" Title="MyCustomAction"> <UrlAction Url="{SiteUrl}/SitePages/SamplePage1.aspx" /> </CustomAction> <CustomAction Description="Custom action for page 2" GroupId="SiteActions" Id="MySiteAction2" Location="Microsoft.SharePoint.StandardMenu" Sequence="1001" Title="MyCustomAction"> <UrlAction Url="{SiteUrl}/SitePages/SamplePage2.aspx" /> </CustomAction> <CustomAction Description="Custom action for page 3" GroupId="SiteActions" Id="MySiteAction3" Location="Microsoft.SharePoint.StandardMenu" Sequence="1002" Title="MyCustomAction"> <UrlAction Url="{SiteUrl}/SitePages/SamplePage3.aspx" /> </CustomAction>Notice the “SiteUrl” token in the Url attribute of the UrlAction elements that we defined. There are several token placeholders that you can use to avoid hardcoding paths in your solutions.
See the attachment to this post for full source code.
Marking Our Page Template as Safe
If we deployed everything right now, it would work. By default, a feature was created to deploy our module, and that feature is scoped to Web, meaning it is scoped to an individual site. And when we deploy our code and feature definitions, we will see links in the Site Actions menu as advertised, and the pages will render fine.When we try to customize one of our new pages with SharePoint Designer 2010, we will get a series of errors.
For example, in SharePoint Designer 2010, go to the Site Pages node, right-click SamplePage3.aspx, and choose “Edit File in Advanced Mode”, you can edit the file, but when you save it you will get a series of errors. Save it as a new file called SamplePage4.aspx and try to preview it in the browser, you are met with the following error:
The base type 'SampleToDeployAPage.MyPageTemplate' is not allowed for this page. The type is not registered as safe.Remember that SharePoint is built upon ASP.NET. SharePoint is like ASP.NET with a healthy dose of Code Access Security layered on top. So, we need to do some security work to tell SharePoint that it’s OK for our page to use code behind.
Right-click the Package node in Visual Studio 2010’s Solution Explorer pane and choose View Template.
This lets us add a new configuration entry that marks our code as safe by adding a SafeControls entry to web.config.
<Assemblies> <Assembly Location="SampleToDeployAPage.dll" DeploymentTarget="GlobalAssemblyCache"> <SafeControls> <SafeControl Assembly="$SharePoint.Project.AssemblyFullName$" Namespace="SampleToDeployAPage" TypeName="MyPageTemplate" Safe="True"/> </SafeControls> </Assembly> </Assemblies>Yeah, that one’s a freebie :) This is a pretty cool trick on how to mark types in the current assembly as SafeControls.
Update: Waldek Mastykarz pointed out that there’s even an EASIER way to do add SafeControls. Click the CustomPages module and look at the properties window. There is a collection called “SafeControls”. Click that, and add a new SafeControl entry. I didn’t know this was there, thanks Waldek!
Adding web.config Modifications Using SPWebConfigModification
If we deployed everything right now, we’d still get another error if we tried to customize the page and preview it in a browser. This time, the error has to do with a security setting that enables certain pages to have code behind.I blogged on this some time ago (see Code-blocks are not allowed within this file: Using Server-Side Code with SharePoint). The problem is that SharePoint has a setting that disallows server-side code with pages. This is a security feature that is good (you really don’t want end users to arbitrarily inject server-side code), but there may be cases where you are OK with some of your users having this capability. For instance, you can have a page that is only visible to a small team within your enterprise, and one of the team members is very technical and wants to provide some custom code for SharePoint. Party on, have fun with it, it saves my team from having to write that code.
To enable this scenario (and enable the Button_Click event handler in our code), we need to add an entry to web.config. Knowing that we can’t just go to every front-end web server and make the modifications (any admin worth his salt should slap you silly for even thinking about hand-modifying multiple web.config files in a production farm), we should provide this as part of our solution.
In the Solution Explorer, you will see a node called Features. Right-click this node and choose “Add Feature”. That will create a new feature called Feature2. Double-click this node to bring up the designer for the feature and change its scope to WebApplication.
After changing the scope to WebApplication, right-click the feature and choose “Add Event Receiver”. This will create a code file where you can handle events related to your feature. We will add code that will make modifications to web.config, adding a new entry to PageParserPaths when the feature is activated, and removing it when the feature is deactivated.
What we want to add is the following:
<SafeMode MaxControls="200" CallStack="false" DirectFileDependencies="10" TotalFileDependencies="50" AllowPageLevelTrace="false"> <PageParserPaths> <PageParserPath VirtualPath="/SitePages/SamplePage3.aspx*" CompilationMode="Always" AllowServerSideScript="true" IncludeSubFolders="true" /> </PageParserPaths> </SafeMode>We want to add an entry into web.config that allows the path SitePages/SamplePage3.aspx as one of the pages that allows server-side scripting. Additionally, we don’t want to add this entry into web.config multiple times, and we want to remove this entry when our feature is deactivated. Below is the code that enables this.
using System; using System.Runtime.InteropServices; using System.Security.Permissions; using Microsoft.SharePoint; using Microsoft.SharePoint.Security; using Microsoft.SharePoint.Administration; using System.Collections.ObjectModel; namespace SampleToDeployAPage.Features.Feature2 { /// <summary> /// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade. /// </summary> /// <remarks> /// The GUID attached to this class may be used during packaging and should not be modified. /// </remarks> [Guid("122ec36d-8fbf-454b-a514-b0d9ef30af43")] public class Feature2EventReceiver : SPFeatureReceiver { public override void FeatureActivated(SPFeatureReceiverProperties properties) { SPWebApplication webApplication = properties.Feature.Parent as SPWebApplication; SPSecurity.RunWithElevatedPrivileges(delegate() { SPWebConfigModification mod = new SPWebConfigModification(); mod.Path = "configuration/SharePoint/SafeMode/PageParserPaths"; mod.Name = "PageParserPath[@VirtualPath='/SitePages/SamplePage3.aspx']"; mod.Owner = "SampleToDeployAPage"; mod.Sequence = 0; mod.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode; mod.Value = "<PageParserPath VirtualPath='/SitePages/SamplePage3.aspx' CompilationMode='Always' AllowServerSideScript='true' />"; webApplication.WebConfigModifications.Add(mod); webApplication.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications(); webApplication.Update(); }); } public override void FeatureDeactivating(SPFeatureReceiverProperties properties) { SPWebApplication webApplication = properties.Feature.Parent as SPWebApplication; SPSecurity.RunWithElevatedPrivileges(delegate() { Collection<SPWebConfigModification> mods = webApplication.WebConfigModifications; int initialModificationsCount = mods.Count; for (int i = initialModificationsCount - 1; i >= 0; i--) { if (mods[i].Owner == "SampleToDeployAPage") { SPWebConfigModification modToRemove = mods[i]; mods.Remove(modToRemove); } } if (initialModificationsCount > mods.Count) { webApplication.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications(); webApplication.Update(); } }); } } }