Wednesday, 16 April 2014

Duplicate web parts on page

So you've created a page layout or a site page that has a webpart deployed to the page through the Elements.xml file in the module that contains your page(s) and now every time you deploy the module you get duplicate web parts, one for every deploy... rather annoying.

to get around this you need to write some code in your event receiver to either iterate over all the webparts on the page delete them and just add yours back in, or iterate over all of the webparts and remove all but the first one.

public void RemoveExtraWebParts(SPSite site)
{
    //Make sure its actually a site
    if (site != null)
    {
        //get the sites root web
        var web = site.RootWeb;

        //Grab the root webs pages gallery
        var pages = web.GetList("Pages");

        //Itterate through all pages
        foreach (SPListItem li in pages.Items)
        {
            //create a check for pages that end in 401.aspx or 404.aspx
            var r = new Regex(@".40[1,4]\.aspx$");

            //Check if page url matches above regex
            if (r.IsMatch(li.Url))
            {
                SPFile page = li.File;

                try
                {
                    //Check out the page if need be
                    if (page.RequiresCheckout)
                        page.CheckOut();

//move through all webparts on page, remove all but the first one(declared by us)
                    //but only if it's a Content Editor webpart
                    using (var mgr =
                        page.GetLimitedWebPartManager(
PersonalizationScope.Shared))
                        if (mgr.WebParts.Count > 1)
                            for (int i = mgr.WebParts.Count - 1; i > 0; i--)
                            {
                                var wp = mgr.WebParts[i];
                                if (wp.Title.Equals("Content Editor"))
                                    mgr.DeleteWebPart(mgr.WebParts[i]);
                            }

                    page.CheckIn("Checked in by feature reciever");
                    page.Approve("Approved By feature reciever");
                }
                catch (Exception)
                {
                    page.UndoCheckOut();
                }
            }
        }
    }

}

Monday, 14 April 2014

Finding a Site Id through web interface


If you don't have powershell or visual studio, but need to get the Web id of your site follow this procedure.

site actions->Content & structure

once there pick the site setting from the site you're interested in's context menu as shown below


once that's done you'll see it in the url


but for an easier time hit the browser back button on the site settings page and you'll get

Friday, 11 April 2014

Using SPWebConfigModification

The SPWebConfigModification class is used to as the name suggests: to modify the web config; basically to my understanding what it does is, stores a collection of web config modifications in the content database and then when the ApplyWebConfigModifications method is called the changes are propagated to all the web configs on a front-end web server. This is the preferred manner to update your web config.

The SPWebConfigModification class has a number of properties that need to be set:
  • Path: an xPath to where you want your modification to go
  • Name: a unique name saved in the content database (i think) and used by the SPWebConfigModification class to ensure that multiple entries of the modification do not occur.
  • Value: the actual web config modification
  • Type: What kind of modification:  SPWebConfigModification.SPWebConfigModificationType
    • EnsureChildNode
    • EnsureAttribute
    • EnsureSection
  • Owner: who owns the particular modification,
  • Sequence: set the order in which the modifications occur, i'd start 10 and incriment in 10s.

now in the class definition there's an example of how to hand code all the values for a safe control, but what i want to do is add an httpHandler and I want to use reflection so that I can have one function that would handle adding any Handler to my web config.

using reflection we can create a function that just extracts all the details like so

public SPWebConfigModification GetModule(Type ModuleType)
{
    Assembly ModuleAssembly = ModuleType.Assembly;
           
    string NameSpace = ModuleAssembly.GetTypes().Select(t => t.Namespace).FirstOrDefault();
    string ClassName = ModuleType.Name;
    string AssemblyName = ModuleAssembly.FullName;

    return new SPWebConfigModification
    {
        Path = "configuration/system.webServer/modules",
        Name = String.Format("add[@name='{0}']"NameSpace),
        Value = String.Format("<add name='{0}' type='{0}.{1}, {2}' />",
            NameSpace,ClassName,AssemblyName),
        Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode,
        Owner = String.Format("{0}.{1}", NameSpace, ClassName),
        Sequence = 10
    };

}

and now we can call our module however many times we need from the feature activated event

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    var ErrorSwap_WebConfigMod = GetModule(typeof(ErrorSwapModule));
    SPWebService ser = SPWebService.ContentService;

    if (!ser.WebConfigModifications.Contains(ErrorSwap_WebConfigMod))
    {
        ser.WebConfigModifications.Add(ErrorSwap_WebConfigMod);
        ser.Update();
        ser.ApplyWebConfigModifications();
    }

}

but remember in SharePoint whatever we do we must also undo or pay the price of at least a week of our time.

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
    SPWebService ser = SPWebService.ContentService;
    var ErrorSwap_WebConfigMod = GetModule(typeof(ErrorSwapModule));

    if (ser.WebConfigModifications.Contains(ErrorSwap_WebConfigMod))
    {
        ser.WebConfigModifications.Remove(ErrorSwap_WebConfigMod);
        ser.Update();
        ser.ApplyWebConfigModifications();
    }

}

now that we've made our web config modifications we have one more step to do, we have to actually deploy our Http Handler into the GAC, to do this open up your package manifest.


double click on your package and now hit ad assembly from project output

next from the source project drop down select your swap library, where your http module resides

after that make sure to add your namespace as a safe control

with your namespace filled in click on the location text box and this will enable the ok button, hit the ok button and deploy your project:
your deployment should

  • put your http handler into the gac
  • deploy your custom error pages to the pages library of your root site
  • add the appropriate web config entry


Thursday, 3 April 2014

Get MetaData of pages

this poweshell script will iterate over all the pages in a pages library find ones that have a department field with a value that contains the word Information, then write them to the screen. because i use write-output instead of write-host you can pipe them to a text file like I show in the second example
<#
.Synopsis
   List IT Policy & Procedure MetaData
.DESCRIPTION
   Script used to list Information and Technology Policies and procedures
.EXAMPLE
   Get-spmPolicyProcedureMetaData -WebAddress http://YourSite/PolicyProcedure
.EXAMPLE
   Get-spmPolicyProcedureMetaData -WebAddress http://YourSite/PolicyProcedure | Out-File c:\steve1.txt
#>
function Get-spmPolicyProcedureMetaData
{
    Param
    (
        # Web address contianing policies and procedures 'http://yoursite/PolicyProcedure'
            [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$False,
                   Position=0)]
        [string]
        $WebAddress
    )
      Begin
      {
            $web = get-spweb $WebAddress
            $pageLib = $web.Lists["Pages"]
      }
      Process
      {
            $PageLib.Items | %{
                  $page = $_
                  if($page["Department"] -like '*Information*'){
                        $pageName = "Page Name = " + $page.Name
                        Write-Output $pageName
                       
                        $dept = "Department = " + $page["Department"].trimstart("23;#")
                        Write-OutPut $dept
                       
                        $eff = "EFF Date = " + $page["Published Effective Date"].trimstart("string;#")
                        Write-Output $eff
                       
                        $cr = "Council Resolution Number = " + $page["Council Resolution Number"]
                        Write-Output $cr
                       
                        $desc = "Description = " + $page["Comments"]
                        Write-Output $desc
                        Write-Output ""
                  }
            }    
      }
      End
      {
            $web.Dispose()
      }

}

you can run this by entering

PS C:\dev> . . /filename.ps1
PS C:\dev> Get-spmPolicyProcedure -webaddress http://yoursite/yourweb

Thursday, 20 March 2014

Nicely Branded 40* Page

In my previous post I talked about how to create a http handler to redirect your 401 error to a custom html error page. which is still kind of limited Ideally we would want a branded error page that includes search capabilities, now this page cannot sit in the layouts folder so it has to be a site page.

So go ahead and create a site page with a code behind follow this post to create one. The code behind is essential to change the status code of the page from 200 to 403. Now once that's created add the following onload code to change the status code to 403:

using System;
using System.Runtime.InteropServices;
using Microsoft.SharePoint.Publishing;
using System.Net;
using Microsoft.SharePoint;

namespace CustomErrorPage.Custom401
{
    [Guid("f255e09b-bc71-479a-b1d5-2c720e492748")]
    public class CustomError401 : PublishingLayoutPage
    {
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            Response.StatusCode = (int)HttpStatusCode.Forbidden;
        }
    }
}


with that complete, make sure that once your Custom error page is deployed that you check it in and approve it, either through your event receiver or manually through your SharePoint Ribbon.

Now we also have to make a slight change in our http module from before. We where previously writing to the response our content but now we are just going to redirect to our new page.

private void WebApp_PreSendRequestContent(object sender, EventArgs e)
{
    HttpResponse response = _webApp.Response;

    string message = string.Empty;

    switch (response.StatusCode)
    {
        case 401:
            _webApp.Server.TransferRequest(@"/pages/CustomError403.aspx");
            break;
    }

}

Now SharePoint uses the 401 status code for it's own purposes so try and stay away from it, which is why I use the 403, in essence the same thing but it'll work.


A Better Approach


An alternative approach to the above, which I much prefer, is to set up your http handler to do both, swap in your custom error page, and also change it's status code.

using System;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;

namespace ErrorRedirect
{
    public class ErrorSwapModule : IHttpModule
    {
        #region IHttpModule Members

        public void Dispose()
        {
            //throw new NotImplementedException();
        }

        public void Init(HttpApplication context)
        {
            //Before Contenet is sent, transfer Page based on status code
            context.PreSendRequestContent += (s, e) =>
            {
                var webApp = s as HttpApplication;

                if (webApp != null || webApp.Request != null || webApp != null)
                {
                    var res = webApp.Response;
                    var req = webApp.Request;
                    var usr = webApp.User;

                    var reg = new Regex("Custom40[1,4].aspx$");

                    if (!reg.IsMatch(req.Url.AbsolutePath))
                        if (res.StatusCode == 404)
                            webApp.Server.TransferRequest(@"/pages/Custom404.aspx");
                        else if (res.StatusCode == 401 && usr != null && !usr.Identity.IsAuthenticated)
                            webApp.Server.TransferRequest(@"/pages/Custom401.aspx");
                }
            };

            //after request, switch page status
            context.PostRequestHandlerExecute += (s, e) => {
                var webApp = s as HttpApplication;

                if(webApp != null)
                    if (webApp.Request.Url.AbsolutePath.Contains("Custom401"))
                        webApp.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                    else if (webApp.Request.Url.AbsolutePath.Contains("Custom404"))
                        webApp.Response.StatusCode = (int)HttpStatusCode.NotFound;   
            };

        }
        #endregion
    }
}



this transfers the page on the start of the request, and then at the end of it changes the status code to whatever you would like it to be. This eliminates the need to create a custom error site pages with code behinds. you can alternatively create your pages declaratively, programmatically or even manually, just make sure that the url's line up appropriately.

Now if you want to set up your Custom Error pages, add a module to your original project, that's the one that doesn't contain your http handler.

with that done expand your module in you solution explorer.


Notice that you have two files:

  • Elemetns.xml
  • Sample.txt

Basically the elements file is a set of instructions on how and where to deploy your sample.txt file, and yes that's silly why on earth would you want to deploy a text file? the answer is odds are you wouldn't instead we are going to deploy two site pages: Custom401 and Custom404. Now you can go through the pain of trying to change your Sample.txt file into an asp site page, or you can download the cks dev kit and make your life a lot less painful. In VS2012 go to tools and click Extensions and Updates:


next make sure you have Online selected in the left hand pane, in the right hand pane type in cks and hit enter, then pick the appropriate cks version (server or foundation, 2010 or 2012/201) and hit download


Once downloaded it'll prompt you for an install, just walk through it to completion. You'll have to restart visual studio for the changes to take effect, go ahead and do so.

Strangly enough i had to install the Cks - Development Tool Edition (server) for vs2010 to get my templates to show up, why i'm not sure but that's what i did.

now that you have your kit(s) installed, delete the sample.txt file and add two cks dev site pages.


Make one for 401 and one for 404 (make sure your running Visual Studio as an administrator)


now if you would like you can open those two up and modify them however you please, one thing that you definitely will want to do is change the master page from default.master to custom.master.

From
<%@ Page language="C#" MasterPageFile="~masterurl/default.master" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage,Microsoft.SharePoint,Version=14.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" %>

To
<%@ Page language="C#" MasterPageFile="~masterurl/custom.master" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage,Microsoft.SharePoint,Version=14.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" %>


this will give your site pages the same look and feel provided by your masterpage instead of the admin one 

but right now lets open up the elements file and actually deploy these pages to our web application.


out of the box you should see the above, we're going to change it to
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="ErrorPages" Url="$Resources:cmscore,List_Pages_UrlName;" Path="ErrorPages">
    <File Path="Custom401.aspx" Url="Custom401.aspx" Type ="GhostableInLibrary" IgnoreIfAlreadyExists ="TRUE">
      <Property Name="Title" Value="401" />
      <Property Name="ContentType"
                Value="$Resources:cmscore,contenttype_pagelayout_name;" />
      <Property Name="PublishingPreviewImage"
                Value="~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/DefaultPageLayout.png, ~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/DefaultPageLayout.png" />
      <Property Name="PublishingAssociatedContentType"
                Value=";#$Resources:cmscore,contenttype_articlepage_name;;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900242457EFB8B24247815D688C526CD44D;#"/>
      <AllUsersWebPart WebPartZoneID="Left" WebPartOrder="1">
        <![CDATA[
      <WebPart xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.microsoft.com/WebPart/v2">
        <Title>Content Editor</Title>
        <FrameType>None</FrameType>
        <Description>Allows authors to enter rich text content.</Description>
        <IsIncluded>true</IsIncluded>
        <ZoneID>Main</ZoneID>
        <PartOrder>0</PartOrder>
        <FrameState>Normal</FrameState>
        <Height />
        <Width />
        <AllowRemove>true</AllowRemove>
        <AllowZoneChange>true</AllowZoneChange>
        <AllowMinimize>true</AllowMinimize>
        <AllowConnect>true</AllowConnect>
        <AllowEdit>true</AllowEdit>
        <AllowHide>true</AllowHide>
        <IsVisible>true</IsVisible>
        <DetailLink />
        <HelpLink />
        <HelpMode>Modeless</HelpMode>
        <Dir>Default</Dir>
        <PartImageSmall />
        <MissingAssembly>Cannot import this Web Part.</MissingAssembly>
        <PartImageLarge>/_layouts/images/mscontl.gif</PartImageLarge>
        <IsIncludedFilter />
        <Assembly>Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
        <TypeName>Microsoft.SharePoint.WebPartPages.ContentEditorWebPart</TypeName>
        <ContentLink xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
        <Content xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
        <PartStorage xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
      </WebPart>
        ]]>
      </AllUsersWebPart>
    </File>
    <File Path="Custom404.aspx" Url="Custom404.aspx" Type ="GhostableInLibrary" IgnoreIfAlreadyExists ="TRUE">
      <Property Name="Title" Value="404" />
      <Property Name="ContentType"
                Value="$Resources:cmscore,contenttype_pagelayout_name;" />
      <Property Name="PublishingPreviewImage"
                Value="~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/DefaultPageLayout.png, ~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/DefaultPageLayout.png" />
      <Property Name="PublishingAssociatedContentType"
                Value=";#$Resources:cmscore,contenttype_articlepage_name;;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900242457EFB8B24247815D688C526CD44D;#"/>
      <AllUsersWebPart WebPartZoneID="Left" WebPartOrder="1">
        <![CDATA[
      <WebPart xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.microsoft.com/WebPart/v2">
        <Title>Content Editor</Title>
        <FrameType>None</FrameType>
        <Description>Allows authors to enter rich text content.</Description>
        <IsIncluded>true</IsIncluded>
        <ZoneID>Main</ZoneID>
        <PartOrder>0</PartOrder>
        <FrameState>Normal</FrameState>
        <Height />
        <Width />
        <AllowRemove>true</AllowRemove>
        <AllowZoneChange>true</AllowZoneChange>
        <AllowMinimize>true</AllowMinimize>
        <AllowConnect>true</AllowConnect>
        <AllowEdit>true</AllowEdit>
        <AllowHide>true</AllowHide>
        <IsVisible>true</IsVisible>
        <DetailLink />
        <HelpLink />
        <HelpMode>Modeless</HelpMode>
        <Dir>Default</Dir>
        <PartImageSmall />
        <MissingAssembly>Cannot import this Web Part.</MissingAssembly>
        <PartImageLarge>/_layouts/images/mscontl.gif</PartImageLarge>
        <IsIncludedFilter />
        <Assembly>Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
        <TypeName>Microsoft.SharePoint.WebPartPages.ContentEditorWebPart</TypeName>
        <ContentLink xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
        <Content xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
        <PartStorage xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
      </WebPart>
        ]]>
      </AllUsersWebPart>
    </File>

</Module>

</Elements>

seems like a lot eh? basically our module element has two file child elements:


those represent the two site pages we made, now if you expand one of the file elements and collapse the AllUserWebPart element you're looking at the properties required for your site page.


now in the web part zone we just include the Content Editor web part that allows authors to enter rich text content on the page, thus letting some sort of publisher to customize the content of your custom error pages, letting you not worry about wording.

Next What we are going to do is add a feature receiver, because we declaratively add a content editor webpart every time we deploy our solution we are going to get an extra content editor webpart, kind of annoying, so what we are going to do is remove it in the event receiver. right click on the feature and click the add event receiver button.

you should see something like

replace the commented out FeatureActivated Method with the following

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    //Grab the site we are deploying to
    var site = properties.Feature.Parent as SPSite;

    //Make sure its actually a site
    if (site != null)
    {
        //get the sites root web
        var web = site.RootWeb;

        //Grab the root webs pages gallery
        var pages = web.GetList("Pages");

        //Itterate through all pages
        foreach (SPListItem li in pages.Items)
        {
            //create a check for pages that end in 401.aspx or 404.aspx
            var r = new Regex(@".40[1,4]\.aspx$");

            //Check if page url matches above regex
            if (r.IsMatch(li.Url))
            {
                SPFile page = li.File;

                try
                {
                    //Check out the page if need be
                    if (page.RequiresCheckout)
                        page.CheckOut();

                    //move through all webparts on page, remove all but the first one(declared by us)
                    //but only if it's a COntent Editor webpart
                    using (var mgr = page.GetLimitedWebPartManager(PersonalizationScope.Shared))
                        if (mgr.WebParts.Count > 1)
                            for (int i = mgr.WebParts.Count - 1; i > 0; i--)
                            {
                                var wp = mgr.WebParts[i];
                                if (wp.Title.Equals("Content Editor"))
                                    mgr.DeleteWebPart(mgr.WebParts[i]);
                            }

                    page.CheckIn("Checked in by feature reciever");
                    page.Approve("Approved By Feature Reciever");
                }
                catch (Exception)
                {
                    page.UndoCheckOut();
                }
            }
        }
    }

}

your also going to have to include the following using statements.

  • using System.Text.RegularExpressions;
  • using System.Web.UI.WebControls.WebParts;

now open up your feature and you should have something along the lines of


Ok, now deploy your project and your two Custom error pages should show up in the pages library of your root web.


To sum up your doing the following

  • Create custom error pages, can be through Code, the Ribbon or Designer
  • Create an http Module that ties into the asp.net pipeline and redirects based on errorcodes