SharePoint

Update your feed to point to www.wssguy.com/blogs/dan

My blog has moved to www.wssguy.com/blogs/dan.  It has not disappeared.  As a matter of fact, I plan on blogging more than ever before.  I had taken quite a break from blogging over the last few months but it was mainly due to me being inundated with project work at my current client.  That work has slowed down somewhat, deliverables have been or are being delivered so I am now able to dedicate some time to doing what I really love, and that is sharing my experiences in and around SharePoint as a development platform with the community as a whole.

The feed url remains the same: http://feeds.feedburner.com/Attis

I also wanted to take this opportunity to thank both Brendon and Matt for graciously supplying me with this space over the last few years.  It was here that my blogging life actually began.  I don't expect this site to be taken down or to go away anytime soon, but if it does I am sure I will be given the opportunity to move my content off to my new blog, which I plan on doing over time anyways as time permits.

Posted 10-29-2008 by Dan Attis
Advanced Developers SharePoint 2007 Training

There are a ton of training opportunities out there today for SharePoint.  Sahil Malik, one of my SharePoint MVP colleagues and most excellent speaker, will be conducting a week of some serious advanced training, including things like Windows Server 2008, SQL Server 2008, Visual Studio 2008, IIS 7, .NET 3.5, WCF, Silverlight, LINQ and Entity Framework and how they all related to SharePoint.

This is aimed at advanced developers and is being held in NORWAY.  That's right, NORWAY.  So in between session you can enjoy the Scandinavian luxuries, like snow, snow and more snow.  You can get additional information here, http://blah.winsmarts.com./2008-6-Advanced_Developers_SharePoint_2007_Training_in_Norway.aspx, and as an added bonus, see a mug shot of Sahil as well.

Free SharePoint Training by Microsoft

Microsoft is stepping up and providing free SharePoint training webcasts to those who want it on 10 critical developer tracks.  On top of that they are asking for feedback on what they can do better to provide more great content to the SharePoint development community.  Check out this blog post (http://blogs.msdn.com/sharepoint/archive/2008/05/20/developers-developers-developers-sharepoint-wants-you.aspx) to learn more.

The content should be excellent since it is being delivered by 2 awesome SharePoint MVP's, Andrew Connell and Robert Bogue.

Instant SharePoint GAC deployment gratification

Often times, while working on SharePoint projects, I find myself needing to deploy only the code.  I don't care about the Feature .xml files, or any of the other wonderfully useful files in a typical SharePoint project.  Up until today, deploying this code was either a Solution upgrade, or launching a command file containing only the gacutil command along with an application pool recycle.

What prompted this post was that my current project contains multiple Visual Studio Solutions and each of those contain multiple Projects.  Neither of the above two methods are very efficient since Solution upgrades take forever, and switching directories or having multiple command windows open at the same time is not manageable.

Visual Studio External Tool to the rescue!

You will need to create a command (cmd) file that contains the commands you want to launch using the External Tool.  In my case, my file contained the commands listed below.  Don't worry about %1 and %2, those are the arguments that will be supplied by the External Tool when we create it and are outlined below.  Essentially, all this is doing is adding a folder to the PATH so that gacutill can be called, it's then calling gacutil to place the assembly into the GAC.  The last line is recycling my application pool for my SharePoint site.  This last line is optional.  I needed it, you may not.

@set PATH=C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin;%PATH%

gacutil /i %1 /f

C:\WINDOWS\system32\cscript.exe C:\WINDOWS\system32\iisapp.vbs /a %2 /r

It needed to work across multiple projects and be contextually aware of the current project that I am working in so that it can deploy the assembly for that project.  The solution was to create an External Tool to do this for me.

Under the Tools menu in Visual Studio, there is an External Tools... menu option.  Clicking that gives you the External Tools dialog.  Clicking on Add will add a new empty tool to the listbox.

Give the tool a tool a name, I named mine "Deplo&y Code Only".  The ampersand in the name simply assigns a keyboard shortcut to this command (the best part in my opinion), as I will be able to launch this by simply clicking ALT-T-Y.

In the command textbox, enter the command you want to call.  At first I was not sure that what I wanted to do was possible but after some experimenting, I found out that it was.  What I mean here is that if you click on the buttons beside the Arguments or Initial Directory textboxes you will see that they bring up a list of possible macros you can use that provide dynamic references to places you care about, like the current project root folder, the current output path, etc...  This button is not available next to the Command textbox.  The macros ARE however usable in that textbox.  My DeployCode.cmd is always at the root of my project but I don't want to have to create a new External Tool for every project.  That would be silly.  By using the $(ProjectDir) macro, I always get a reference to the root of my project.

In the Arguments textbox I am passing in the assembly path and the name of my application pool, since I am recycling it at the same time (this second argument is optional).  The cool part here is that it will use the Debug or Release version of the assembly here depending on the Build Configuration you have selected in your project.

The Initial Directory textbox tells the External Tool to run in the project directory for the command.  This is not required in this case.

I also checked the Use Output window checkbox so that I can see the output inside the Visual Studio environment.

 

image

 

This has made my life quite a bit easier since now it takes me about .5 seconds to deploy only the code for my project.  I hope you find it as useful as I did.

Forms Based Authentication - Application Pool Account Permissions

Early last year, I posted a couple of articles on how to setup Forms Based Authentication (FBA) in Windows SharePoint Services (WSS v3).

Here are the links:

FBA Walkthrough Part 1

FBA with MySites Walkthrough Part 2

Many people have used this to setup FBA in their environments successfully.  One of the most common issues that come up with many people is that they are not able to resolve users even though the web.config files are configured correctly.  The primary reason ends up being that the Application Pool account for either Central Administration or the Web Application in question has not been granted the appropriate permissions inside of SQL Server to access the membership information.  I referred to this step as the magic step in my earlier posts, but never documented it.  Well the time has come to document that step, so here it is.

I am documenting this for SQL Server 2005.  The same thing can be accomplished in SQL Server 2000, albeit the steps and screen shots will be different (obviously).

As mentioned in my previous post, you will need to run aspnet_regsql to setup the membership database.  This is documented nicely in FBA Walkthrough Part 1

The previous article stops there however and did not discuss permissions.  In the following steps, I assume that the database you created is called "AspNetDb_www.yourwebapplication.com".

Open up SQL Server Management Studio.  When presented with the Connect to Server dialog box, make sure the Server type drop down is set to Database Engine as indicated in the following screen shot.

image

Expand the Databases node and locate your membership database.  Then expand your membership database node, then the Security node within it.

image

Right click on the Users folder and select New User from the context menu.

image

The Database User - New dialog will appear.  Click the Browse button next to the Login name text box.

image

In the Select Login dialog, enter the object name you wish to grant access to.  Let's begin with the Central Administration application pool account.  Keep in mind, your account name will most likely be different than mine.  In my example, I browsed for and located my Central Administration application pool user named moss\ossservice.  Click OK after confirming it via the Check Names button.

image

Enter a User name that will map to the NT Login name.  I use the same name as NT minus the domain portion.  Next, in the Database role membership section, check all of the of the Role Members prefixed with aspnet_.  It is possible that all of these role members are not needed, but I have not taken the time to determine the minimum set of permissions required for this to work.  If someone has tested this and would like to share that information, please do.  This is still a whole lot better than making the application pool account a db_owner!

image

Repeat the process with the application pool account for your web application.

Keep in mind that on a development machine these may very well be the same account, but in production, I would hope that they are not.  That would be bad, very bad.

I hope this helps clear up some of the confusion around my previous post.

Happy FBA'ing!

Free Office Developer Conference 2008 Pass

Hi everyone.

Tonight, @ the Atlanta Dot Net User Group meeting, we will be giving away a pass to the Office Developer Conference in San Jose from February 10-13.

https://microsoft.crgevents.com/ODC2008/Content/default.aspx?p=UC3HYF

I know this is late notice, since the meeting is tonight, but, if you can and will go to the conference, come to the meeting tonight to drop your name in a hat.  You will be responsible for your flight and hotel, but the pass itself is a $1095 "value".

Doug Ware will be talking about SharePoint development, a GREAT topic, so leave work early so you can get a good seat!

Posted 01-28-2008 by Dan Attis
Filed under: ,
Silverlight Media Player and SharePoint v2

Back on January 14th I posted about a Silverlight media player web part that myself and Keith co-developed.  I posted the source code then as well.  The new home for the project is going to be @ CodePlex.

The project is called Media Player Web Part using SharePoint 2007 and Silverlight and the URL is http://www.codeplex.com/SLMP4SP.  If anyone wants to contribute to the project, drop me a line via my blog.  I would love to have a release ready by month's end.

Silverlight Media Player and SharePoint

Last Monday, January 7th, Keith and I talked about how to host a Silverlight Media Player in a SharePoint Web Part and the processes used to create and deploy it to a SharePoint site.  As promised here is the slide deck and all of the code.  I apologize for the delay, I've been swamped at work, imagine that!  We have a couple of more talks in the pipe to expand on these concepts so stay tuned!

Slide Deck

Code

The contentclass and isDocument properties along with the Welcome Page caveat

My current project has me working on a custom search solution for a SharePoint public facing site.  The site has a good number of sites, each with a good number of pages, as well as many lists, libraries, etc...  One of the requirements the client had was to only display Documents and Pages in the search results.  This didn't seem like an unreasonable request at the time, so I agreed.  Well, as it turns out, it was not so simple after all.

I started looking at Search Scopes, thinking to myself that I could create a Scope with a set of rules to give the "slice" of data I wanted.  This is what I wanted.  Keep in mind that this is a publishing site.

  • Pages.
  • Documents from specific libraries.

Sounds pretty simple on the surface.  So, I started creating property rules.  I only wanted results from certain libraries.  That was fine, I created a rule to pull from certain folders (as they are described on the Scope rule screen).  I very quickly realized that scopes are pretty limiting.  I cannot group rules.  I cannot have for example, color is red OR color is yellow, only AND's.  There is very limited logic available with Scope rules.  This pretty much forced me to switch my approach from a simple Scope based solution to a SQL Syntax Query solution that would allow me to pretty much return whatever I wanted.

Now I needed only documents, regardless of extension.  At this point I was a little perplexed.  I spent a good deal of time looking thru all of the crawled properties trying to find one that would help me.  It was then that I discovered the isDocument property, which, coincidentally, was also a managed property out of the box.  I added this to my query and was now only getting documents from the specified libraries back in my search results.  Almost there.  I still needed to get Pages (from all of the sites in the site collection).

How on earth was I going to do that?  Well, in the process of testing and debugging over and over again, I have discovered a little known property that was sitting right under my nose the entire time.  If you go to create a property rule within a Scope, you will notice the contentclass property.  I wondered what this property did.  Well, it was a life saver.  Essentially, every piece of content in SharePoint seems to be tagged with this property.  I believe it's all set internal as I have yet to see it referenced anywhere (at least in my limited searching).

This entire time I was working on a custom search results page that looked very different then the out of the box version, as it was for a publishing site and as I mentioned, all they wanted searched were Pages and certain Documents.  As part of the development process of that search results page, I placed a Review control on the page so that I could see my search results in their raw format as I worked on the custom rendering.  The contentclass property was right there, already in the default properties that were searched.  I of course, had to add it to my SQL Syntax Query but nonetheless, it was crawled and managed for me already.  I found a couple of blog posts that describe the possible values for this property, but they were incomplete, so here is, as far as I can tell, a complete list of possible values, that you can use in your Scopes, SQL Syntax Queries or Keyword queries should you need to.


        case "STS_Web":                             // Site
        case "STS_List_850":                        // Page Library
        case "STS_ListItem_850":                    // Page
        case "STS_List_DocumentLibrary":            // Document Library
        case "STS_ListItem_DocumentLibrary":        // Document Library Items
        case "STS_List":                            // Custom List
        case "STS_ListItem":                        // Custom List Item
        case "STS_List_Links":                      // Links List
        case "STS_ListItem_Links":                  // Links List Item
        case "STS_List_Tasks":                      // Tasks List
        case "STS_ListItem_Tasks":                  // Tasks List Item
        case "STS_List_Events":                     // Events List
        case "STS_ListItem_Events":                 // Events List Item
        case "STS_List_Announcements":              // Announcements List
        case "STS_ListItem_Announcements":          // Announcements List Item
        case "STS_List_Contacts":                   // Contacts List
        case "STS_ListItem_Contacts":               // Contacts List Item
        case "STS_List_DiscussionBoard":            // Discussion List
        case "STS_ListItem_DiscussionBoard":        // Discussion List Item
        case "STS_List_IssueTracking":              // Issue Tracking List
        case "STS_ListItem_IssueTracking":          // Issue Tracking List Item
        case "STS_List_GanttTasks":                 // Project Tasks List
        case "STS_ListItem_GanttTasks":             // Project Tasks List Item
        case "STS_List_Survey":                     // Survey List
        case "STS_ListItem_Survey":                 // Survey List Item
        case "STS_List_PictureLibrary":             // Picture Library
        case "STS_ListItem_PictureLibrary":         // Picture Library Item
        case "STS_List_WebPageLibrary":             // Web Page Library
        case "STS_ListItem_WebPageLibrary":         // Web Page Library Item
        case "STS_List_XMLForm":                    // Form Library
        case "STS_ListItem_XMLForm":                // Form Library Item
        case "urn:content-class:SPSSearchQuery":    // Search Query
        case "urn:content-class:SPSListing:News":   // News Listing
        case "urn:content-class:SPSPeople":         // People
        case "urn:content-classes:SPSCategory":     // Category
        case "urn:content-classes:SPSListing":      // Listing
        case "urn:content-classes:SPSPersonListing":// Person Listing
        case "urn:content-classes:SPSTextListing":  // Text Listing
        case "urn:content-classes:SPSSiteListing":  // Site Listing
        case "urn:content-classes:SPSSiteRegistry": // Site Registry Listing

I spent a good deal of time coming up with this list, but if someone finds on that is not mentioned, please let me know though a comment and I will update this list.  If you are wondering how I found all of these, it was actually quite easy.  I simply started to add the different types of content to the various sites, crawled it, and then looked at the output in my GridView while developing the solution.  You can see a pattern, so it did get a little easier after that was discovered.  I'm guessing as well that the SPS* ones are left over from SharePoint Portal Server 2003.  I didn't actually see those in my research, but pulled them from a post by Jose Barreto where he mentions some of these values.

So, by limiting my SQL Syntax query to only include items that are have a contentclass equal to either STS_ListItem_850 (Pages) or STS_ListItem_Document (Documents) along with making sure that the isDocument property is true (1), I was able to meet the requirement.  Well almost.

Of course, SharePoint HAD to throw me a curve ball.  As you may or may not know, Publishing sites have the concept of a Welcome Page.  This is set via a link that appears on the Site Settings page of a Publishing site.

During my testing of the search results, I noticed that none of the Welcome Pages were being returned in the search results.  I knew they were being crawled, because I saw them in the crawl log.  It was time for some additional debugging.  I enabled my trusted ReviEw and inspected the results with the search filter off, so I was getting results for all types of content, not just Pages and Documents.  I noticed that the Welcome Pages were in fact appearing BUT the content class associated with them was STS_Web.  I have no idea whatsoever why this is, but that's the way it is.  So, I had to modify my WHERE clause in my query to include STS_Web results as well.  Once I added that, I started to get all the results I expected, and most importantly the results that the client wanted.

So there you have it.  The contentclass in all of its glory.  It is super useful, especially if you are writing your own queries.  You could use it for all sorts of things.  Just be aware of the Welcome Page caveat for Publishing sites.

Posted 12-20-2007 by Dan Attis | 11 comment(s)
Filed under:
Forms Based Authentication Articles Published

Steve Peschka has published a series of articles on forms based authentication in Office SharePoint Server 2007 and Windows SharePoint Services 3.0.  Part 1 discusses FBA itself, Part 2 talks about providers, and Part 3 discusses some of the differences between FBA and Windows authentication.

  1. Forms Authentication in SharePoint Products and Technologies (Part 1): Introduction
  2. Forms Authentication in SharePoint Products and Technologies (Part 2): Membership and Role Provider Samples
  3. Forms Authentication in SharePoint Products and Technologies (Part 3): Forms Authentication vs. Windows Authentication

This is the first real "authoritative" say on FBA to date from Microsoft.  I can't wait until I have to do is click a button to do this!

One thing that I did not see mentioned in the white papers during my initial scan was to be sure that the account that you are using as the application pool's identity for the FBA website in question has the rights it needs within SQL Server to access the ASP.NET membership schemas.

Posted 12-17-2007 by Dan Attis | 1 comment(s)
Filed under:
Yet Another great looking public facing MOSS Internet site!

A good friend of mine recently architected, developed and launched kroger.com using the MOSS 2007 platform.  This is yet another example of using it as a public facing site.  This isn’t to say that there weren’t a few hurdles, he mentioned many, but all in all, it seems to be a success and it "looks" awesome!

Hopefully this will be the first of many Fortune #25 companies jumping on board (Kroger is #21 at press time)!

Check it out!

Posted 11-26-2007 by Dan Attis | 5 comment(s)
Filed under:
You Don't Need to Copy PDB Files to Debug in the GAC!

I generally don't post a link without much else but this was a no brainer and I feel every SharePoint developer, heck, every .NET developer needs to know this.  My friend and SharePoint pal, Doug Ware has discovered how to debug assemblies in the GAC WITHOUT having to copy the pdb symbols file to the MSIL folder.  In other words, simply attaching to the correct w3wp worker process will work.

It's awe inspiring, I had no idea!

I'm not going to steal his thunder so you will have to read all about here!

Posted 11-15-2007 by Dan Attis | 1 comment(s)
Filed under:
Technical Interview Tips

Keith Rome, an über smart friend of mine and fellow MVP, recently posted some interview tips on how to increase the odds of landing that next dream job.  Remember, it's the little things that help!

http://www.mindfusioncorp.com/weblog/2007/10/23/10+Interview+Tips+That+Will+Help+You+Land+A+Better+Job.aspx

SharePoint 2007 - Add a Lookup Field to a List Template Element that references its own List

I had a real hard time coming up for a title for this post.  Here is what I am trying to explain in the title.  I had a need to create a custom List Template.  This can be done via a Feature.  The issue I ran across was I needed a Lookup Field in my list that contained values in another column in the same list.  The problem was that when you create a Lookup List, behind the scenes, SharePoint is assigning the Guid of the list that you are performing the lookup in to the LookupList property.  This works fine when doing this thru the user interface because the list exists when you add the column.  But to place a lookup into a List Template, as far as I can tell anyways, is impossible to do via any of the schemas.  It needs this Guid when it creates the field, which we obviously do not have when we are creating the List Template. 

My solution was as follows:  I created the list without that field and I added some code in the Feature receiver FeatureActivated method.  This allowed me to programmatically add the lookup field AFTER the list was created.

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
        using (SPWeb _SPWeb = properties.Web)
        {
            // the name of the list is set in the ListInstance Element of
            // this Feature
            SPList _SPList = (SPList)_SPWeb.Lists["ListName"];

            if (_SPList != null)
            {
                // add a new lookup field to the list
                _SPList.Fields.AddLookup("LookupField", _SPList.ID, false);                      
                // update the list
                _SPList.Update();

                // get a reference to the SPFieldLookup we just added
                SPFieldLookup _SPFieldLookup = new SPFieldLookup(_SPList.Fields, "LookupField");

                // get a reference to the default view
                SPView _SPView = _SPList.DefaultView;

                // add the field to the view if it is not there
                if (!_SPView.ViewFields.Exists("LookupField"))
                {
                    _SPView.ViewFields.Add(_SPFieldLookup);
                }

                // update the view
                _SPView.Update();   
            }
        }
}

Now I have a lookup field in my list that points to a column in the same list!  Happy coding!

Posted 10-16-2007 by Dan Attis | 3 comment(s)
Filed under:
SharePoint 2007 - Content Editor Web Part and Absolute Url's

I am not sure if this type of information has been posted before.  I looked around for a good 2 days or so trying to find a solution to my particular problem and could not.  Here is a short description of the problem.

My current project involves a public facing WCM site.  The site has 2 zones, one for anonymous access and forms users and another zone for AD users.

The content owners all have AD accounts and will be managing the content via the AD zone.  Many of the layout pages use Content Editor Web Parts.  When a user selects an image or a link using a CEWP, the relative url that appears in the text box is replaced with an absolute url in the database (genius!).  Why this happens is beyond me but the end result was unacceptable.  Since AD users were uploading content to the site, the url's were being converted to the absolute url of the AD site, which of course is not accessible to anonymous or forms users.  My solution was to create an event handler to handle the OnUpdating event of the Page (which is just a list item) and cycle thru the CEWP's on the page to replace any absolute url's with relative ones.

I began by creating my Feature file:

<!-- _lcid="1033" _version="12.0.4518" _dal="1" -->
<!-- _LocalBinding -->
<Feature
  DefaultResourceFile="core"
  Description="Changes absolute urls to relative urls for all Content Editor Web Parts used in the Pages library."
  Id="E4E20B7F-F948-4b38-893B-F9F1AA202B84"
  ReceiverAssembly="Ratman.SharePoint.EventHandlers.ForceRelativeUrl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxtokenxxx"
  ReceiverClass="Ratman.SharePoint.EventHandlers.ForceRelativeUrl.ForceRelativeUrlFeature"
  Scope="Web"
  SolutionId="7B09E255-2BFC-43d5-86AB-3154FC60E443"
  Title="Ratman Force Relative Url"
  Version="1.0.0.0"
  xmlns="
http://schemas.microsoft.com/sharepoint/"
>
</Feature>

Notice that the above Feature definition defines an assembly.  In this assembly we will be hooking up our event handler to the appropriate library in a single site.  The reason I did it this way was because the List Type Id of the library I am hooking it up to is 850 and is not in the list of available List Type Id's available when defining an event handler via XML.  Well, now to think of it I am not sure if I actually confirmed that.  Either way, activating event handlers this way gives you more control over "where" it is activated.  For example, when defining an event handler via XML, you must provide (as mentioned above) a List Type Id.  This will attach the event handler to all lists of a certain type.  For example, were we to define 101, the event handler would be attached to all document libraries.  This may not be the behavior that we want.  With all that said it is also important to note that the List Template Id for a Pages library is 850.  This number can be viewed by looking at the source of a list view of a Pages library.  I have not found a single reference to this Id in any MSDN documentation so be weary.  I am guessing it may be hard-coded in many places.

The following code is my Feature handler, obviously replace xxxtokenxxx with your own token:

public class ForceRelativeUrlFeature : SPFeatureReceiver
{
    public override void FeatureActivated(SPFeatureReceiverProperties properties)
    {
        // get a reference to the current SPWeb
        using (SPWeb _SPWeb = SPContext.Current.Web)
        {
            // get a reference to the "Pages" library
            SPDocumentLibrary _SPDocumentLibrary = (SPDocumentLibrary)_SPWeb.Lists["Pages"];

            // if the "Pages" library exists
            if (_SPDocumentLibrary != null)
            {
                // create an empty Guid
                Guid _ItemUpdatingGuid = Guid.Empty;

                // enumerate thru all of the event receiver definitions, attempting to
                // locate the one we are adding
                foreach (SPEventReceiverDefinition _SPEventReceiverDefinition in _SPDocumentLibrary.EventReceivers)
                {
                    // if we find the event receiver we are about to add
                    // record its Guid
                    if (_SPEventReceiverDefinition.Type == SPEventReceiverType.ItemUpdating &&
                        _SPEventReceiverDefinition.Assembly == "Ratman.SharePoint.EventHandlers.ForceRelativeUrl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxtokenxxx" &&
                        _SPEventReceiverDefinition.Class == "Ratman.SharePoint.EventHandlers.ForceRelativeUrl.ForceRelativeUrlItem")
                    {
                        _ItemUpdatingGuid = _SPEventReceiverDefinition.Id;
                    }
                }

                // if we did not find the event receiver we are adding, add it
                if (_ItemUpdatingGuid == Guid.Empty)
                {
                    try
                    {
                        _SPDocumentLibrary.EventReceivers.Add(SPEventReceiverType.ItemUpdating, "Ratman.SharePoint.EventHandlers.ForceRelativeUrl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxtokenxxx", "Ratman.SharePoint.EventHandlers.ForceRelativeUrl.ForceRelativeUrlItem");
                    }
                    catch (Exception ex)
                    {
                        Debug.Write(ex.Message);
                    }
                }
            }
        }
    }

I made a few assumptions in this code, some of which you may want to remove.  I assumed that the Pages library is the name of the Pages library.  I assumed this because if you look at the list definition of a Pages library, located @ C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES\Publishing you will see that the UNIQUE attribute is set to true.  What this means is that you really can have only one of these types of list on a single site.  It has also been suggested that many of the publishing features themselves will not work if this is not the name of the library.  Here is the Pages library list template definition as it is defined on above on the file system.

<!-- _lcid="1033" _version="12.0.4518" _dal="1" -->
<!-- _LocalBinding -->
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <ListTemplate
        Name="Pages"
        Type="850"
        BaseType="1"
        Hidden="TRUE"
        Unique="TRUE"
        SecurityBits="11"
        DisplayName="$Resources:cmscore,List_Pages_DisplayName"
        Description="$Resources:cmscore,List_Pages_Description"
        Image="/_layouts/images/itdl.gif">
    </ListTemplate>
</Elements>

The code after that is pretty straight forward.

  • Get a reference to the current SPWeb.
  • Get a reference to the Pages library (I assume this name) and check if it exists.
  • Check to make sure that the event handler you are attaching to this library is not already attached.
  • If the event handler is not found, attach it.

Here is the code for the event handler itself:

public class ForceRelativeUrlItem : SPItemEventReceiver
{
    public override void ItemUpdating(SPItemEventProperties properties)
    {
        this.DisableEventFiring();

        // 850 is the ListTemplateId of the OOB Pages library on a publishing site
        if (Int32.Parse(properties.ListItem.ParentList.BaseTemplate.ToString()) == 850)
        {
            // get a reference to the list item (the page in this case)
            SPListItem _SPListItem = properties.ListItem;

            // get a reference to the containing SPWeb
            using (SPWeb _SPWeb = _SPListItem.Web)
            {
                // get a reference to the the web part manager on the page
                using (SPLimitedWebPartManager _SPLimitedWebPartManager = _SPWeb.GetLimitedWebPartManager(_SPListItem.Url, PersonalizationScope.Shared))
                {
                    // loop thru all of the web parts on the page and up update
                    // all of the CEWP's
                    foreach (Microsoft.SharePoint.WebPartPages.WebPart _WebPart in _SPLimitedWebPartManager.WebParts)
                    {
                        if (_WebPart.GetType().Equals(typeof(ContentEditorWebPart)))
                        {
                            using (ContentEditorWebPart _ContentEditorWebPart = (ContentEditorWebPart)_WebPart)
                            {
                                // get the contents of the CEWP
                                string _ContentString = _ContentEditorWebPart.Content.InnerText;

                                // remove the absolute url
                                _ContentString = _ContentString.Replace(_SPWeb.Site.RootWeb.Url, "");
                                // create an Xml element to use to update the CEWP
                                XmlDocument _XmlDocument = new XmlDocument();
                                XmlElement _XmlElement = _XmlDocument.CreateElement("MyElement");
                                _XmlElement.InnerText = _ContentString;

                                // update the Content property of the CEWP
                                _ContentEditorWebPart.Content = _XmlElement;

                                try
                                {
                                    _SPLimitedWebPartManager.SaveChanges(_ContentEditorWebPart);
                                }
                                catch (Exception ex)
                                {
                                    Debug.Write(ex.Message);
                                }

                            }
                        }
                    }
                }
            }
        }

        this.EnableEventFiring();
    }
}

The code here is a little more complex, but not terribly difficult.

  • Disable event firing.  Since we are updating this item, we wouldn't want to get caught in some sort of loop, since this is the OnUpdating event handler :)
  • Ensure that the List Template Id of the list containing this item is in fact 850.  This is really just a double check, since we know it is true since we hooked the event handler up to a Pages library in the Feature handler defined above.
  • Get a reference to the Web Part Manager on the page itself which is really just an item in the Pages library.
  • Loop thru the web parts on the page and identify CEWP's.
  • Update the CEWP's content.  This was a little tricky.  You can't just update the XML directly, you need to create the new XML using an XmlElement and update the XmlElement that defines the Content property of the CEWP.  This part took me a while.  For some reason intellisense lead me to think that you can update the XML directly.
  • Re-enable event firing.

It is important to be sure that all of your disposable objects are disposed of.  If there is a single gotcha to SharePoint coding (coding, not development, that's another set of gotchas) it is this.  Memory leaks can sprout up quickly and are hard to trace after code is deployed.  I hope you find this techniques as useful as I did.  I am sure there are some improvements that can be made.  This was a relatively quick fix to a VERY BIG problem.

The end result here is that all images and links created via a CEWP on a page in the Pages library on a publishing will contain relative url's as opposed to absolute url's!

Posted 09-27-2007 by Dan Attis | 10 comment(s)
Filed under:
More Posts Next page »