Archives

Archives / 2009 / June
  • SharePoint, Features and web.config modifications using SPWebConfigModification

    SharePoint has a great way for deploying content and functionality using Windows SharePoint Services Solution Packages (WSP's). While developing a powerful new feature for SharePoint Publishing sites I had to deploy a HttpModule "the SharePoint" way. Building a HttpModule , a corresponding feature and the resulting WSP package is easy with our Macaw Solutions Factory. The actual logic in the Http Module and the feature is the difficult part. One of the things I had to do was to create a feature that registers a HTTPModule on feature activation, and removes it from the web.config on the feature deactivation. You can do this using the SPWebConfigModification class.

    A good article on this topic is http://www.crsw.com/mark/Lists/Posts/Post.aspx?ID=32. It contains links to other posts as well.

    The Microsoft documentation can be found at SPWebConfigModification Class (Microsoft.SharePoint.Administration), I wished I scrolled down before, because a lot of valuable information can be found in the Community Content of this page (keep scrolling!).

    Anyway, it took quite some time to get my HttpModule to register/unregister correctly on activation/deactivation of my web application level feature. I post the code below so you have a head-start if you have to do something similar yourself.

     

    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using Microsoft.SharePoint;
    using Microsoft.SharePoint.Administration;
    
    // namespace must be in the form <Company>.<Product>.<FunctionalArea>.SharePoint.Features.<FeatureName>.FeatureReceiver
    namespace Macaw.WcmRia.Moss2007.DualLayout.SharePoint.Features.DualLayoutSupport.FeatureReceiver
    {
        /// <summary>
        /// Add HttpModule registration to web.config of the web application
        /// </summary>
        class DualLayoutSupportFeatureReceiver : SPFeatureReceiver
        {
            private const string WebConfigModificationOwner = "Macaw.WcmRia.Moss2007.DualLayout";
            private static readonly SPWebConfigModification[] Modifications = {
                // For not so obvious reasons web.config modifications inside collections 
                // are added based on the value of the key attribute in alphabetic order.
                // Because we need to add the DualLayout module after the 
                // PublishingHttpModule, we prefix the name with 'Q-'.
                new SPWebConfigModification()
                    { 
                        // The owner of the web.config modification, useful for removing a 
                        // group of modifications
                        Owner = WebConfigModificationOwner, 
                        // Make sure that the name is a unique XPath selector for the element 
                        // we are adding. This name is used for removing the element
                        Name = "add[@name='Q-Macaw.WcmRia.Moss2007.DualLayout']",
                        // We are going to add a new XML node to web.config
                        Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode, 
                        // The XPath to the location of the parent node in web.config
                        Path = "configuration/system.web/httpModules",
                        // Sequence is important if there are multiple equal nodes that 
                        // can't be identified with an XPath expression
                        Sequence = 0,
                        // The XML to insert as child node, make sure that used names match the Name selector
                        Value = "<add name='Q-Macaw.WcmRia.Moss2007.DualLayout' type='Macaw.WcmRia.Moss2007.DualLayout.Business.Components.HttpModule, Macaw.WcmRia.Moss2007.DualLayout.Business.Components, Version=1.0.0.0, Culture=neutral, PublicKeyToken=077f92bbf864a536' />" 
                    }
            };
    
            public override void FeatureInstalled(SPFeatureReceiverProperties properties)
            {
            }
    
            public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
            {
            }
    
            public override void FeatureActivated(SPFeatureReceiverProperties properties)
            {
                SPWebApplication webApp = properties.Feature.Parent as SPWebApplication;
                if (webApp != null)
                {
                    AddWebConfigModifications(webApp, Modifications);
                }
            }
    
            public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
            {
                SPWebApplication webApp = properties.Feature.Parent as SPWebApplication;
                if (webApp != null)
                {
                    RemoveWebConfigModificationsByOwner(webApp, WebConfigModificationOwner);
                }
            }
    
            /// <summary>
            /// Add a collection of web modifications to the web application
            /// </summary>
            /// <param name="webApp">The web application to add the modifications to</param>
            /// <param name="modifications">The collection of modifications</param>
            private void AddWebConfigModifications(SPWebApplication webApp, IEnumerable<SPWebConfigModification> modifications)
            {
                foreach (SPWebConfigModification modification in modifications)
                {
                    webApp.WebConfigModifications.Add(modification);
                }
    
                // Commit modification additions to the specified web application
                webApp.Update();
                // Push modifications through the farm
                webApp.WebService.ApplyWebConfigModifications();
            }
    
            /// <summary>
            /// Remove modifications from the web application
            /// </summary>
            /// <param name="webApp">The web application to remove the modifications from</param>
            /// <param name="owner"Remove all modifications that belong to the owner></param>
            private void RemoveWebConfigModificationsByOwner(SPWebApplication webApp, string owner)
            {
                Collection<SPWebConfigModification> modificationCollection = webApp.WebConfigModifications;
                Collection<SPWebConfigModification> removeCollection = new Collection<SPWebConfigModification>();
    
                int count = modificationCollection.Count;
                for (int i = 0; i < count; i++)
                {
                    SPWebConfigModification modification = modificationCollection[i];
                    if (modification.Owner == owner)
                    {
                        // collect modifications to delete
                        removeCollection.Add(modification);
                    }
                }
    
                // now delete the modifications from the web application
                if (removeCollection.Count > 0)
                {
                    foreach (SPWebConfigModification modificationItem in removeCollection)
                    {
                        webApp.WebConfigModifications.Remove(modificationItem);
                    }
    
                    // Commit modification removals to the specified web application
                    webApp.Update();
                    // Push modifications through the farm
                    webApp.WebService.ApplyWebConfigModifications();
                }
            }
        }
    }
  • SharePoint WCM: flushing publishing pages from the cache

    SharePoint WCM does a lot of caching. One of the things that is cached are the publishing pages. These pages are cached in the object cache. Sometimes there is a situation where you want to flush a publishing page from the cache. In my case I had to flush a publishing page from the cache in a http module. The cache id for this page is the server relative url without any characters after the url. For example: /Pages/MyFirstLittleWCMPage.aspx. Therefore the path must be "normalized" so additional "stuff" is removed. The NormalizeUrl() function does this job.

    What I want to do to flush the page from the cache was:

    CacheManager contextCacheManager = CacheManager.GetManager(SPContext.Current.Site);
    contextCacheManager.ObjectFactory.FlushItem(NormalizeUrl(HttpContext.Current.Request.Path);

    Sadly enough many interesting and powerful API classes are internal, and you need some reflection to be able to call them. Below the code I needed to write to accomplish the above. I can tell you it was a hell of a job to get to this code. That is why I share it, to give you some insight in the required magic called reflection.

    Interesting components:

    1. I know that the assembly containing the required class is already loaded. I can do GetAssembly(typeof(PublishingPage)) to get the assembly. Will work on any class in the assembly.
    2. To invoke a member of a class you need the type of the class. Assembly.GetType("full.name.of.type") returns the type, also on internal classes.
    3. Given the type you can invoke members, where members can be static functions, properties or methods. You specify what to search for the member using BindingFlags. For example for a static public method specify BindingFlags.Static | BindingFlags.Public | BindingFlags.InvokeMethod.
    4. Arguments to methods must be passed in an object array.

    I hope the code below will give some insight in how to make the impossible possible.

    /// <summary>
    /// Flush the current publishing page from the object cache
    /// </summary>
    /// <remarks>
    /// Reflection is used to get access to internal classes of the SharePoint framework
    /// </remarks>
    private void FlushCurrentPublishingPageFromCache()
    {
        // We need to get access to the Microsoft.SharePoint.Publishing.dll assembly, PublisingPage is in there for sure
        Assembly microsoftSharePointPublishingAssembly = Assembly.GetAssembly(typeof(PublishingPage));
        Type cacheManagerType = microsoftSharePointPublishingAssembly.GetType("Microsoft.SharePoint.Publishing.CacheManager", true);
        object contextCacheManager = cacheManagerType.InvokeMember("GetManager", 
            BindingFlags.Static | BindingFlags.Public | BindingFlags.InvokeMethod, 
            null, null, new object[] { SPContext.Current.Site });            
    
        string cacheId = NormalizeUrl(HttpContext.Current.Request.Path);
        if (contextCacheManager != null)
        {
            object cachedObjectFactory = contextCacheManager.GetType().InvokeMember("ObjectFactory", 
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty, 
                null, contextCacheManager, new object[] {});
            cachedObjectFactory.GetType().InvokeMember("FlushItem", BindingFlags.Instance | 
                BindingFlags.Public | BindingFlags.InvokeMethod, 
                null, cachedObjectFactory, new object[] { cacheId });
        }
        else
        {
            Microsoft.Office.Server.Diagnostics.PortalLog.LogString("Unexpected error: DualLayout " +
    "FlushCurrentPublishingPageFromCache: No CacheManager for page {0}"
    , cacheId); } } /// <summary> /// Normalize url for cachId usage /// </summary> /// <remarks> /// This code is copied from: /// private static string NormalizeUrl(string url); /// Declaring Type: Microsoft.SharePoint.Publishing.CachedObjectFactory /// Assembly: Microsoft.SharePoint.Publishing, Version=12.0.0.0 /// </remarks> /// <param name="url">Url to normalize</param> /// <returns>The normalized url</returns> private static string NormalizeUrl(string url) { url = SPHttpUtility.UrlPathDecode(url, false); if (!string.IsNullOrEmpty(url)) { int length = url.IndexOf('?'); if (length >= 0) { url = url.Substring(0, length); } } else { return ""; } int index = url.IndexOf('#'); if (index >= 0) { url = url.Substring(0, index); } return url; }
  • Debugging SharePoint/ASP.NET code? Smart key-codes + disable timeout!

    I'm currently running around in the Visual Studio debugger to debug some complex SharePoint code. There are two things really annoy me: all the mouse-clicks needed to attach to the Internet Information Server process and the time-out you get when you are exploring complex data-structures for too long.

    First my favorite key-sequence for the last week: <ALT-D>PW3<ENTER><ENTER>. I will explain it:

    <Alt-D> brings up the debugging menu in Visual Studio:

    image

    With P the action "Attach to Process..." is executed, which brings you to the following window:

    image

    The list of available processes is already active. We nog need to select the Internet Information Server worker process. Each application pool has it's own worker process. These worker processes are named: w3wp.exe.

    By typing W3 the first (and often only) w3wp.exe process is selected:

    image 

    If there are multiple w3wp.exe processes you could select them all (SHIFT+ARROWDOWN). Now press the first time <ENTER>, which selects the w3wp.exe process(es). This results in the following window:

    image

    The "Attach" button is selected by default. This brings us to the latest <ENTER> to accept the default selection.

    We are now attached to the correct Internet Information Server working process(es) and can start debugging.

    Just try it a few times: <ALT-D>PW3<ENTER><ENTER>, it will become second nature in no time. Happy debugging....

    ... until you get the following popup window:

    image 

    You have got a "ping" timeout. If you read the box well, it tells you exactly what happened, and it tells you to press the "Help" button for further details.

    Most people don't read the box, and start over again. But it worth the effort to follow the described steps from the Microsoft documentation, they are a bit hard to follow:

    To continue to debug, you must configure IIS to allow the worker process to continue.

    To enable Terminal Services (?? Terminal Services ??)
    1. Open the Administrative Tools window.

    2. Click Start, and then choose Control Panel.

    3. In Control Panel, choose Switch to Classic View, if necessary, and then double-click Administrative Tools.

    4. In the Administrative Tools window, double-click Internet Information Services (IIS) Manager.

    5. In the Internet Information Services (IIS) Manager window, expand the <computer name> node.

    6. Under the <computer name> node, right-click Application Pools.

    7. In the Application Pools list, right-click the name of the pool your application runs in, and then click Advanced Settings.

    8. In the Advanced Settings dialog box, locate the Process Model section and choose one of the following actions:

      1. Set Ping Enabled to False.

        -or-

      2. Set Ping Maximum Response Time to a value greater than 90 seconds.

      Setting Ping Enabled to False stops IIS from checking whether the worker process is still running and keeps the worker process alive until you stop your debugged process. Setting Ping Maximum Response Time to a large value allows IIS to continue monitoring the worker process.

    9. Click OK.

    10. Under Services and Applications, click Services. -- Don't know what the rest of the steps if for... you are done!

      A list of services appears in the right-side pane.

    11. In the Services list, right-click Terminal Services, and then click Properties.

    12. In the Terminal Services Properties window, locate the General tab and set Startup type to Manual.

    13. Click OK to close the Advanced Settings dialog box.

    14. Close the Internet Information Services (IIS) Manager window and the Administrative Tools window.

    I'm running on Windows Server 2008, and below are the steps that I follow:

    Just type iis in the Start Search box, this shows me two applications:

    image

    I take the top one (I'm not running under IIS 6) and get the following screen:

    image

    Right-click your application pool, advanced settings... and you get the following screen:

    image

    Set "Ping Enabled" to False, press OK, and you can drill through your data-structures in the debugger for as long as you want!

    Again: "Happy debugging!"