Wesley Bakker

Interesting things I encounter doing my job...

Sponsors

News

Wesley Bakker
motion10
Rivium Quadrant 151
2909 LC Capelle aan den IJssel
Region of Rotterdam
The Netherlands
Phone: +31 10 2351035

(feel free to chat with me)
 

Add to Technorati Favorites

January 2009 - Posts

Complex SharePoint Web Part Properties

Complex SharePoint Web Part Properties

Web part properties not always consist of strings and integers. Sometimes your properties are a little bit more complicated. And sometimes your property is a list of complicated properties. Such as a list of Virtual Earth pushpins for your Virtual Earth Maps control. This post is the starting post for my series "How to create a Virtual Earth Maps Web Part for SharePoint" in which I'll demonstrate how you can make modifying complex properties simple.

First things first

I'll start by laying the groundwork for our Virtual Earth Maps web part by creating a Virtual Earth Pushpin class and a Virtual Earth class. The latter will inherit from Microsoft.SharePoint.WebPartPages.WebPart. I know that we should not inherit from that class anymore but later on in this series I'll show you a very good reason why we still use this class.

These two classes form the basis for everything else. From an object point of view it’s easy to understand that our Virtual Earth class must have some sort of property that contains the list of Virtual Earth Pushpins. From a designers point of view it's kind of difficult to implement a good user interface to enter these pushpins. But as said. First things first. Here's the code for a Virtual Earth Pushpin:

//-----------------------------------------------------------------------
// <copyright file="VirtualEarthPushpin.cs" company="motion10">
//     Copyright (c) motion10. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
 
using System;
namespace Motion10.SharePoint2007.WebControls.WebParts {
    public class VirtualEarthPushpin {
        private static readonly VirtualEarthPushpin _motion10 = new VirtualEarthPushpin() {
                                                                        Title = "motion10",
                                                                        InfoBoxHtml = "<div style=\'text-align:center\'><p><strong>Virtual Earth Feature</strong><br/>designed by <a href=\'http://www.motion10.com\' target=\'_blank\'>motion10</a></p></div>',
                                                                        Latitude = 51.913779F,
                                                                        Longitude = 4.53926F
                                                                    };
        private float _latitude;
        private float _longitude;
 
        /// <summary>
        /// Initializes a new instance of the <see cref="VirtualEarthPushpin"/> class.
        /// </summary>
        public VirtualEarthPushpin() {
        }
 
        /// <summary>
        /// Gets the motion10 location pushpin.
        /// </summary>
        /// <value>The motion10 location pushpin.</value>
        public static VirtualEarthPushpin Motion10 {
            get {
                return _motion10;
            }
        }
 
        /// <summary>
        /// Gets or sets the title.
        /// </summary>
        /// <value>The title.</value>
        public string Title { get; set; }
 
        /// <summary>
        /// Gets or sets the description.
        /// </summary>
        /// <value>The description.</value>
        public string InfoBoxHtml { get; set; }
 
        /// <summary>
        /// Gets or sets the latitude.
        /// </summary>
        /// <value>The latitude.</value>
        /// <remarks>Latidude can't be less than -90 or more than 90.</remarks>
        /// <exception cref="T:System.ArgumentOutOfRangeException">If the value is less than -180 or exceeds 180.</exception>
        public float Latitude {
            get {
                return _latitude;
            }
            set {
                if (value < -90F || value > 90F) {
                    throw new ArgumentOutOfRangeException("value", "Latidude can't be less than -90 or more than 90.");
                }
 
                _latitude = value;
            }
        }
 
        /// <summary>
        /// Gets or sets the longitude.
        /// </summary>
        /// <value>The longitude.</value>
        /// <remarks>Longitude can't be less than -180 or more than 180.</remarks>
        /// <exception cref="T:System.ArgumentOutOfRangeException">If the value is less than -180 or exceeds 180.</exception>
        public float Longitude {
            get {
                return _longitude;
            }
            set {
                if (value < -180F || value > 180F) {
                    throw new ArgumentOutOfRangeException("value", "Longitude can't be less than -180 or more than 180.");
                }
 
                _longitude = value;
            }
        }
 
        /// <summary>
        /// Gets or sets the image.
        /// </summary>
        /// <value>The image.</value>
        public string Image { get; set; }
    }
}

As you can see this class is really really straightforward. It implements some properties, some validation and, to promote my employer, a static property with the title, info box html, latitude and longitude of motion10. Nothing spectacular right? So let's concentrate on our topic of today. How to add a complex property to a web part and how to enable the end user to insert values?

Human readable and writeable object notation

There are many ways to enable modifications of complex properties, but they're all kind of pain in the *&$# to implement. So I was looking for some sort of object notation that is human readable and writeable and validateable. And the one thing that popped into my mind was JSON. JSON stands for JavaScript Object Notation, is very lightweight, human readable and writeable and validateable.

It's easy validateable because of the JavaScriptSerializer class in the .Net framework. With the help of an extension method it's even simpler to implement use JSON. My hero Scott Guthrie wrote an excelent post on how to create such an extension method.

Implementation

To implement all this we first need a class with a private member variable of generic type List<VirtualEarthPushpin>. This list will hold all of our pushpins for us. SharePoint will Serialize this member to and from the database. It looks like this:

/// <summary>
/// The motion10 VirtualEarth web part displays a Virtual Earth control.
/// </summary>
[Guid("DEC5B663-4E6B-4e56-99F1-C941C5AE81DD")]
[XmlRoot(Namespace = "Motion10.SharePoint2007.WebControls.WebParts")]
public class VirtualEarth : Microsoft.SharePoint.WebPartPages.WebPart, IListConsumer, IRowConsumer, IDesignTimeHtmlProvider {
    private static readonly string designTimeHtml = "<div style='border: solid 1px black;'><h4>motion10 VirtualEarth web part</h4><p>The VirtualEarth web part cannot render a Virtual Earth maps control in design view due the fact it needs to render and run some javascript.</p><p>You can edit its properties however without any problem. Any changes you make will be reflected in runtime.</p></div>';
 
    private List<VirtualEarthPushpin> _pushpins = new List<VirtualEarthPushpin>(){
        VirtualEarthPushpin.Motion10,
        new VirtualEarthPushpin(){ Title="Microsoft Netherlands", Latitude=52.304541F, Longitude= 4.750958F}
    };

As you can see I've added to two pushpins and I did that for a very good reason. Not just to promote my employer but to promote Microsoft as well. No, just kidding, I did that to show my end user an example of the JSON he can use in the Pushpins property of type string. Yep, I'm serious. We are going to use a property of type string to define a complex property! Our Pushpins property looks like this:

/// <summary>
/// Gets or sets the pushpins.
/// </summary>
/// <value>The pushpins.</value>
[Browsable(true)]
[Category("Map Settings")]
[WebPartStorage(Storage.Shared)]
[FriendlyName("Pushpins")]
[Description("The pushpins to place on the map in Json Format")]
public string Pushpins {
    get {
        return _pushpins.ToJson();
    }
    set {
        if (string.IsNullOrEmpty(value)) {
            value = "[]";
        }
 
        try {
            _pushpins = new JavaScriptSerializer().Deserialize<List<VirtualEarthPushpin>>(value);
        }
        catch (Exception) {
            throw new WebPartPageUserException("could not deserialize the given string into an array of pushpins.");
        }
    }
}

The code is not that complicated at all. We use the "List<VirtualEarthPushpin> _pushpins" variable as a backing field. In the 'getter' we simply return _pushpins.ToJson(); which in turn uses the extension method described by mister Guthrie. The setter does a little bit more.

First it checks if the value is null or empty and if so turns it into "[]" which stands for an empty array in JSON. Then we are going to try to deserialize the given value to a List. If this does not succeed and throws an error, we are catching that exception and throw a WebPartPageUserException.

This is a call to all web part developers around the world. Start validation your property input and stop using an error label to show property validation errors! The WebPartPageUserException will stop SharePoint from serializing the invalid value to your database and will display a decent validation error to your end users. It's made for this. An errorlabel does have it's place, but not for property validation!

Now that's off my chest, I can show you some screenshots on how this will look in your browser:

JsonSample

I do know this doesn't look that simple at all but when you break it down it's just this:

[
 { "Title":"motion10",
    "InfoBoxHtml":"<div>Some html</div>",
    "Latitude":51.91378,
    "Longitude":4.53926,
    "Image":null },
  { "Title":"Microsoft Netherlands",
    "InfoBoxHtml":null,
    "Latitude":52.3045425,
    "Longitude":4.750958,
    "Image":null }
]

And that's something people can learn. If they do make a mistake they'll see an errormessage and the value will not be saved to the database:

SerializationError

Conclusion

Today I demonstrated how you can use JSON for complex properties in a SharePoint Web Part. I know it's not the most beautiful way to edit complex properties. You can create custom editors by creating a class that inherits from the abstract EditorPart class, but I think this is a good 'in the middle' solution where you both give the end user an interface to add his or her own pushpins and you don't go out of your way in creating and designing a complex user interface. In the end we'll probabely add pushpins to our map with a connection to our contacts list anyway, but that's something we'll discuss in the next episode were we'll finish our Virtual Earth class. Stay tuned and please feel free to drop me your comments.

Cheers,

Wes

SharePoint Central Administration Feature

SharePoint Central Administration Feature

While developing a motion10 feature that enables the modification of the DaysToShowNewIndicator web application setting, I was looking for a way to make sure the feature get’s activated on SharePoint’s Central Administration Web Application only. I decided to apply no less than three techniques for this.

DaysToShowNewIndicatorSettings

AutoActivateInCentralAdmin and Hidden

First of all I removed the need to activate the feature at all. By setting the AutoActivateInCentralAdmin feature element tag to true the feature get’s activated on the Central Administration Web Application on installation. This removes the need to activate the feature for a SharePoint administrator and thus the chance he/she will try to activate this feature on a different web application. Another attribute I have set is the Hidden attribute. I’ve set its value to TRUE. This is to make sure that you can’t activate the feature in the UI.

Here’s the feature.xml:

<?xml version="1.0" encoding="utf-8"?>
<Feature  Id="7c631062-2e9c-4252-ab28-b2a3afa7157f"
          Title="motion10 Days To Show New Indicator"
          Description="This feature allows for setting the ammount of days the New indicator shows up next to an item. This feature can only be enabled on a Central Admin site."
          Version="12.0.0.0"
          Hidden="TRUE"
          Scope="WebApplication"
          DefaultResourceFile="core"
          ReceiverAssembly="SharePointSolutionPack, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a7cd02bdf107f7a"
          ReceiverClass="Motion10.SharePoint2007.CentralAdminOnlyFeatureReceiver"
          AutoActivateInCentralAdmin="TRUE"
          Creator="Wesley Bakker"
          ImageUrl="motion10/FeaturesIcon.png"
          ImageUrlAltText="http://www.motion10.com"
          xmlns="http://schemas.microsoft.com/sharepoint/">
  <ElementManifests>
    <ElementManifest Location="elements.xml"/>    
  </ElementManifests>
</Feature>

And once installed you’ll find the Custom Action on your Application Management form:

DaysToShowNewIndicator

But the ‘motion10 Days To Show New Indicator’ feature is not displayed on the ‘Manage Web Application Features’ form:

WebApplicationFeatures

SPFeatureReceiver

The last and final action to take if you do not want anybody to activate your feature on any web application besides the Central Administration is to create a class that implements SPFeatureReceiver. In that class you can implement a check when the feature get’s activated. We’ve already mentioned that class in the feature.xml file above. It’s in the ‘ReceiverAssembly’ and ‘ReceiverClass’ attribute.

The class is very simple and looks like this:

//-----------------------------------------------------------------------
// <copyright file="CentralAdminOnlyFeatureReceiver.cs" company="motion10">
//     Copyright (c) motion10. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
 
using System;
using System.Globalization;
using System.Security.Permissions;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Security;
 
namespace Motion10.SharePoint2007 {
    /// <summary>
    /// You can use the CentralAdminOnlyFeatureReceiver class for those feature that can only be activated on the Central Administration Web Application. On other web applications the feature will be removed and an error is thrown to indicate that activation did not succeed.
    /// </summary>
    [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
    [SharePointPermission(SecurityAction.InheritanceDemand, ObjectModel = true)]
    public class CentralAdminOnlyFeatureReceiver : SPFeatureReceiver {
        /// <summary>
        /// Occurs after a Feature is activated.
        /// </summary>
        /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
        public override void FeatureActivated(SPFeatureReceiverProperties properties) {
            SPWebApplication webApp = (SPWebApplication)properties.Feature.Parent;
            if (!webApp.IsAdministrationWebApplication) {
                Guid featureId = properties.Feature.DefinitionId;
                webApp.Features.Remove(featureId, true);
                webApp.Update();
 
                throw new SPException(string.Format(CultureInfo.InvariantCulture, "You can activate the feature with ID {0} on the Central Administration Web Application only!", featureId));
            }
        }
 
        /// <summary>
        /// Occurs when a Feature is deactivated.
        /// </summary>
        /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
        public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {
            //throw new NotImplementedException();
        }
 
        /// <summary>
        /// Occurs after a Feature is installed.
        /// </summary>
        /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
        public override void FeatureInstalled(SPFeatureReceiverProperties properties) {
            //throw new NotImplementedException();
        }
 
        /// <summary>
        /// Occurs when a Feature is uninstalled.
        /// </summary>
        /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
        public override void FeatureUninstalling(SPFeatureReceiverProperties properties) {
            //throw new NotImplementedException();
        }
    }
}

On activation of this feature on a SPWebApplication that is not a Central Administration Web Application the feature gets removed immediately and an error is thrown.

Conclusion

With a little bit of effort it’s not that hard to implement a Central Admin only feature. You can reuse the CentralAdminOnlyFeatureReceiver class on other features just well.

Cheers,

Wes

Posted: Jan 28 2009, 02:09 PM by webbes | with 3 comment(s)
Filed under:
Web.config modifications with a SharePoint feature

How to modify your web.config in SharePoint.

As with many things the modification of your web.config on a SharePoint webfarm is a little bit more daunting than in a standard ASP.Net web application. There are some interesting cases where you would like to modify your web.config simply by a feature. In this post I'll make your life a little easier by showing you the code for an abstract base class that can be used for your feature recievers and two cases where I used this technique.

Feature Recievers

First I'll explain what a feature reciever is. A feature reciever is a class that gets called by a SharePoint feature on 4 specific events. These events are:

  • FeatureInstalled
  • FeatureUninstalling
  • FeatureActivated
  • FeatureDeactivating

This class must inherit from the base class: SPFeatureReceiver. You override 4 methods which gives you the power to react on feature events. In our case we'll concentrate on the FeatureActivated and FeatureDeactivating. On FeatureActivated we'll modify the web.config and on FeatureDeactivating we'll revert our changes.

Debug Switch

Each and every SharePoint developer knows that if you want to be able to debug your code you must make some modifications to your web.config.

  • configuration/SharePoint/SafeMode[@CallStack]="true"
  • configuration/system.web/customErrors[@mode]="Off"
  • configuration/system.web/compilation[@debug]="true"

We can modify this manually each and every time but would it be nice if we could simply activate our Debug Switch web application feature? Fortunately we can by writing just a few lines of code.

SPWebConfigModification

A simple way of modifying your SharePoint web.config is by using the SPWebConfigModification class. We can add SPWebConfigModifications to the WebConfigModifications of a SPWebApplication and call ApplyWebConfigModifications of the WebService property of SPWebApplication. If we then call Update() on the SPWebApplication our changes are persisted. Just like this:

public override void FeatureActivated(SPFeatureReceiverProperties properties) {
    SPWebApplication webApp = (SPWebApplication)properties.Feature.Parent;

    foreach (SPWebConfigModification modification in this.Modifications) {
        webApp.WebConfigModifications.Add(modification);
    }

    webApp.WebService.ApplyWebConfigModifications();
    webApp.Update();
}

In this code sample the 'this.Modification' is a property that returns an array of SPWebConfigModification. The array is declared like this:

private static SPWebConfigModification[] modifications = {
            new SPWebConfigModification("CallStack", "configuration/SharePoint/SafeMode")
                { Owner = "motion10DebugSwitch", Sequence = 0, Type = SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute, Value = "true" },
            new SPWebConfigModification("mode", "configuration/system.web/customErrors")
                { Owner = "motion10DebugSwitch", Sequence = 0, Type = SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute, Value = "Off" },
            new SPWebConfigModification("debug", "configuration/system.web/compilation")
                { Owner = "motion10DebugSwitch", Sequence = 0, Type = SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute, Value = "true" }
};

All these modifications are added to the 'WebConfigModifications' collection and persisted by calling 'ApplyWebConfigModifications()' and 'Update()'.

Revert modifications

As you can see an SPWebConfigModification has an owner defined and this enables rollback of YOUR changes. This is great news since we need to have that option on FeatureDeactivating. The code again is very straightforward:

public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {
    SPWebApplication webApp = (SPWebApplication)properties.Feature.Parent;
    
    foreach (SPWebConfigModification modification in this.Modifications.Reverse()) {
        webApp.WebConfigModifications.Remove(modification);
    }

    webApp.WebService.ApplyWebConfigModifications();
    webApp.Update();
}

We simply reverse the order of the modifications and instead of adding the modifications, we remove them. That's sounds pretty logical doesn't it?

Generic

This code can be used for other features as well. How about a Strict xHtmlConformance feature?

private static SPWebConfigModification[] modifications = {
            new SPWebConfigModification("xhtmlConformance", "configuration/system.web")
                { Owner = "motion10XhtmlConformance", Sequence = 0, Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode, Value = "<xhtmlConformance />" },
            new SPWebConfigModification("mode", "configuration/system.web/xhtmlConformance")
                { Owner = "motion10XhtmlConformance", Sequence = 0, Type = SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute, Value = "Strict" }
};

So we need our code to be a little more generic. The only thing that changes is the modifications array. So I came up with the following abstract class.

//-----------------------------------------------------------------------
// <copyright file="WebConfigModificationFeatureReciever.cs" company="motion10">
//     Copyright (c) motion10. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

using System.Linq;
using System.Security.Permissions;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Security;

namespace Motion10.SharePoint2007 {
    /// <summary>
    /// The WebConfigModificationFeatureReceiver class is the abstract base class for Feature Recievers that modify the web.config
    /// </summary>
    [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
    [SharePointPermission(SecurityAction.InheritanceDemand, ObjectModel = true)]
    public abstract class WebConfigModificationFeatureReceiver : SPFeatureReceiver {
        /// <summary>
        /// Initializes a new instance of the <see cref="WebConfigModificationFeatureReceiver"/> class.
        /// </summary>
        protected WebConfigModificationFeatureReceiver() : base() { }

        /// <summary>
        /// Gets the modifications to apply to the web.config.
        /// </summary>
        /// <value>The modifications.</value>
        protected abstract SPWebConfigModification[] GetModifications();

        private SPWebConfigModification[] Modifications {
            get {
                return this.GetModifications();
            }
        }

        /// <summary>
        /// Occurs after a Feature is activated.
        /// </summary>
        /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
        public override void FeatureActivated(SPFeatureReceiverProperties properties) {
            SPWebApplication webApp = (SPWebApplication)properties.Feature.Parent;

            foreach (SPWebConfigModification modification in this.Modifications) {
                webApp.WebConfigModifications.Add(modification);
            }

            webApp.WebService.ApplyWebConfigModifications();
            webApp.Update();
        }

        /// <summary>
        /// Occurs when a Feature is deactivated.
        /// </summary>
        /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
        public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {
            SPWebApplication webApp = (SPWebApplication)properties.Feature.Parent;
            
            foreach (SPWebConfigModification modification in this.Modifications.Reverse()) {
                webApp.WebConfigModifications.Remove(modification);
            }

            webApp.WebService.ApplyWebConfigModifications();
            webApp.Update();
        }

        /// <summary>
        /// Occurs after a Feature is installed.
        /// </summary>
        /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
        public override void FeatureInstalled(SPFeatureReceiverProperties properties) {
            //throw new Exception("The method or operation is not implemented.");
        }

        /// <summary>
        /// Occurs when a Feature is uninstalled.
        /// </summary>
        /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
        public override void FeatureUninstalling(SPFeatureReceiverProperties properties) {
            //throw new Exception("The method or operation is not implemented.");
        }
    }
}

Implementation

With our abstract base class in place it's a piece of cake to implement both our Strict xHtmlConformance Feature and our Debug Switch.

//-----------------------------------------------------------------------
// <copyright file="XhtmlConformance.cs" company="motion10">
//     Copyright (c) motion10. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

using System.Security.Permissions;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Security;

namespace Motion10.SharePoint2007 {
    /// <summary>
    /// The XhtmlConformance class is the Feature Reciever for the motion10 strict xHtmlConformance feature
    /// </summary>
    [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
    [SharePointPermission(SecurityAction.InheritanceDemand, ObjectModel = true)]
    public class XhtmlConformance : WebConfigModificationFeatureReceiver {
        private static SPWebConfigModification[] modifications = {
                    new SPWebConfigModification("xhtmlConformance", "configuration/system.web")
                        { Owner = "motion10XhtmlConformance", Sequence = 0, Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode, Value = "<xhtmlConformance />" },
                    new SPWebConfigModification("mode", "configuration/system.web/xhtmlConformance")
                        { Owner = "motion10XhtmlConformance", Sequence = 0, Type = SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute, Value = "Strict" }
        };

        /// <summary>
        /// Initializes a new instance of the <see cref="XhtmlConformance"/> class.
        /// </summary>
        public XhtmlConformance() : base() { }

        /// <summary>
        /// Gets the modifications to apply to the web.config.
        /// </summary>
        /// <returns></returns>
        /// <value>The modifications.</value>
        protected override SPWebConfigModification[] GetModifications(){
            return (SPWebConfigModification[])modifications.Clone();
        }
    }
}

 

//-----------------------------------------------------------------------
// <copyright file="DebugSwitch.cs" company="motion10">
//     Copyright (c) motion10. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

using System.Security.Permissions;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Security;

namespace Motion10.SharePoint2007 {
    /// <summary>
    /// The DebugSwitch class is the Feature Reciever for the motion10 Debug Switch feature
    /// </summary>
    [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
    [SharePointPermission(SecurityAction.InheritanceDemand, ObjectModel = true)]
    public class DebugSwitch : WebConfigModificationFeatureReceiver {
        private static SPWebConfigModification[] modifications = {
                    new SPWebConfigModification("CallStack", "configuration/SharePoint/SafeMode")
                        { Owner = "motion10DebugSwitch", Sequence = 0, Type = SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute, Value = "true" },
                    new SPWebConfigModification("mode", "configuration/system.web/customErrors")
                        { Owner = "motion10DebugSwitch", Sequence = 0, Type = SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute, Value = "Off" },
                    new SPWebConfigModification("debug", "configuration/system.web/compilation")
                        { Owner = "motion10DebugSwitch", Sequence = 0, Type = SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute, Value = "true" }
        };

        /// <summary>
        /// Initializes a new instance of the <see cref="DebugSwitch"/> class.
        /// </summary>
        public DebugSwitch() : base() { }

        /// <summary>
        /// Gets the modifications to apply to the web.config.
        /// </summary>
        /// <returns></returns>
        /// <value>The modifications.</value>
        protected override SPWebConfigModification[] GetModifications() {
            return (SPWebConfigModification[])modifications.Clone();
        }
    }
}

Feature.xml

With our classes ready we need to create two feature.xml files. Both are not to difficult. They need an Id, Title, Description and the RecieverAssembly and RecieverClass.

<?xml version="1.0" encoding="utf-8"?>
<Feature  Id="8404bc89-27c0-487f-9346-58f9e68a7d82"
          Title="motion10 strict xHtmlConformance"
          Description="By activating the motion10 strict xHtmlConformance feature the web.config will be changed in order for the asp.net controls to render valid xHtml to the clients."
          Version="12.0.0.0"
          Hidden="FALSE"
          Scope="WebApplication"
          DefaultResourceFile="core"
          ReceiverAssembly="SharePointSolutionPack, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a7cd02bdf107f7a"
          ReceiverClass="Motion10.SharePoint2007.XhtmlConformance"
          Creator="Wesley Bakker"
          ImageUrl="motion10/FeaturesIcon.png"
          ImageUrlAltText="http://www.motion10.com"
          xmlns="http://schemas.microsoft.com/sharepoint/">
  <ElementManifests />
</Feature>

 

<?xml version="1.0" encoding="utf-8"?>
<Feature  Id="d728e7a5-a794-4f3b-9664-8d96dfccfff2"
          Title="motion10 Debug Switch"
          Description="Activate this feature to make the necessary changes to the web.config to enable debugging. Deactivate this feature to revert the changed web.config settings to the state they were at activation."
          Version="12.0.0.0"
          Hidden="FALSE"
          Scope="WebApplication"
          DefaultResourceFile="core"
          ReceiverAssembly="SharePointSolutionPack, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a7cd02bdf107f7a"
          ReceiverClass="Motion10.SharePoint2007.DebugSwitch"
          Creator="Wesley Bakker"
          ImageUrl="motion10/FeaturesIcon.png"
          ImageUrlAltText="http://www.motion10.com"
          xmlns="http://schemas.microsoft.com/sharepoint/">
  <ElementManifests />
</Feature>

Conclusion

It's not that hard to modify the web.config of your SharePoint application. Especially with the given abstract base class, you guys should be able to create some new feautures yourselfs. Let me know if you can think of something.

I'm experimenting with my blog. In this article I've insert all the code in-line instead of making it available as a download. I think it's a little bit long to read in this way but it enables you to see what I'm talking about right away. What do you think? Is this better or worse? Please leave me a comment on this.

Cheers,

Wes

Posted: Jan 21 2009, 05:33 PM by webbes | with 22 comment(s)
Filed under: ,
Package and Deploy
<moan>

Package and deploy

How would you do it?

Imagine you've just created a new website and want it to be pluggable for others. In a way that others could add new features to your solution. How would you do it?

Files

As with every feature for this web application you'll probably have some files to deploy. Can be images, can be .ascx and .aspx files etc. With this files come the need to specify which files to copy and where these files should be copied to. So we need some sort of settings file. I can imagine a user interface where my application asks for the filecopy.xml and after reading this filecopy.xml automatically copies the nececary files to the nececary directories.

Assemblies

It's basically the same with assemblies. What we do need to take care of though is that assemblies should be registered in the GAC or copied to the 'bin' directory. So maybe we need to have some other settings file. Let's call this file assemblyinstall.xml.

Visibility

So far so good, but how about visibility? Nice that we have copied some webforms and some dll's but where to place a link to that newly added pages? Maybe we need some settings for that als well. You know what? Let's call this feature.xml.

Refactor

We've already decided what we need, but this can be refactored a little bit. Let's start by combining these 3 settings files into just 1. And let's call it our manifest. So we have a file called manifest.xml that contains settings for where to deploy the assemblies, where to deploy the files and where we can find featue settings files.

Happy?

Nice but I'm not really happy. What if my web application spans multiple frontend servers? I do not really want to copy this folder to all frontend servers and execute the installation manually. So maybe we can zip our solution and place it in a central database? That would be very easy. Another good thing about this is that an installed solution can be uninstalled again because all the files, including the settings files, are in my database. So we can install and uninstall the solutions without being afraid of leftovers in the system!

Problems that can rise

If you do not deliver any good documentation on how to create your solution package developers will fall back to the things they know that will work. They'll start to copy all these files manualy and they'll blog about it. They'll write tutorials on how to copydeploy a solution to your web application. Once a few people decided to do it this way others will follow. Not because they are bad developers, but because they don't know any better.

SharePoint

If we take a look at SharePoint you'll see that the SharePoint Team probably followed the exact same path, unfortunately the copydeployment problem included. Windows SharePoint Solution Pack is nothing more than a cabinet file(some sort of archive) that contains all files to deploy. The assemblies, webforms images etc. plus the manifest.xml file and feature.xml files. It's not always an easy job to create such a Solution Package but there are tools that do help you a lot such as the famous WSP Builder.

Conclusion

If you have read this post you now know that there's a good way for deploying solutions to your SharePoint environment. You now know that handcopying files is absolutely not necesary and definately not a smart thing to do because you'll loose your rollback scenario and it's very error prone. You now know that handcopying files is just for those that do not want to evolve or learn something new aka 'the unprofessional developers'. Keep that in mind when you write your next turorial or deliver your copydeployment script.

</moan>

Cheers,

Wes

Posted: Jan 21 2009, 01:59 PM by webbes
Filed under:
SharePoint Google Analytics Feature

Being nice to Google

After my last post I felt the need to say something nice about Google and implement one of their features in SharePoint as a feature. The chosen feature is Google Analytics. Google Analytics is a great tool but implementing it in SharePoint isn't as simple as it might seem at first. In this post I'll walk you through in how to create such a feature. You can find the complete source attached.

Site ID

You can't just edit your master page and append the generated script in your header. This is because you need to register your site first. This registration gives you a site id and that site id should be appended to your scripts src url. But then we have a problem with URL zones. What if someone can reach my site through http://intranet.motion10.com and http://extranet.motion10.com? So we must create something a little bit more robust.

UI

First of all we must take care for the UI. If we want our clients to enter their Google Analytics Site ID per url zone we should provide some sort of interface. So a layouts page is what we'll need. Fortunately it's not that hard to create a layouts page yourselfs. Here's a snippet out of the GoogleAnalyticsFeatureSettings.aspx. (Find the full source attached.)

<wssuc:InputFormControl LabelText="Intranet zone" LabelAssociatedControlID="GoogleAnalyticsSiteIDTextBoxIntranet" ID="InputFormControlIntranet" runat="server">
    <Template_Control>
      <sp:InputFormTextBox Title="Intranet zone" class="ms-input" ID="GoogleAnalyticsSiteIDTextBoxIntranet" Columns="35" Runat="server" MaxLength="16" />
    </Template_Control>
</wssuc:InputFormControl>
<wssuc:InputFormControl LabelText="Internet zone" LabelAssociatedControlID="GoogleAnalyticsSiteIDTextBoxInternet" ID="InputFormControlInternet" runat="server">
    <Template_Control>
      <sp:InputFormTextBox Title="Internet zone" class="ms-input" ID="GoogleAnalyticsSiteIDTextBoxInternet" Columns="35" Runat="server" MaxLength="16" />
    </Template_Control>
</wssuc:InputFormControl>
<wssuc:InputFormControl LabelText="Custom zone" LabelAssociatedControlID="GoogleAnalyticsSiteIDTextBoxCustom" ID="InputFormControlCustom" runat="server">
    <Template_Control>
      <sp:InputFormTextBox Title="Custom zone" class="ms-input" ID="GoogleAnalyticsSiteIDTextBoxCustom" Columns="35" Runat="server" MaxLength="16" />
    </Template_Control>
</wssuc:InputFormControl>

With this declarations we're not there yet. We do need some code to get and set this values and save them in some persistend place. Fortunately we can create a class that inherits from LayoutsPageBase and if we add proteced members in that class with the same ID's we used in our declarations, ASP.Net will bind those two together. Here's some code snippet that shows you how.

public class GoogleAnalyticsFeatureSettings : LayoutsPageBase {
    protected InputFormTextBox GoogleAnalyticsSiteIDTextBoxDefault;
    protected InputFormControl InputFormControlDefault;
    protected InputFormTextBox GoogleAnalyticsSiteIDTextBoxIntranet;
    protected InputFormControl InputFormControlIntranet;

------------------------------------------------

private void Page_Load(object sender, EventArgs e) {
    if (!Page.IsPostBack) {
        GoogleAnalyticsSiteIDTextBoxDefault.Text = SolutionPackUtility.GetGoogleAnalyticsSiteIdByUrlZone(SPUrlZone.Default);
        InputFormControlDefault.LabelText += GenerateLabelTextForUrlZone(SPUrlZone.Default);
        
        GoogleAnalyticsSiteIDTextBoxIntranet.Text = SolutionPackUtility.GetGoogleAnalyticsSiteIdByUrlZone(SPUrlZone.Intranet);
        InputFormControlIntranet.LabelText += GenerateLabelTextForUrlZone(SPUrlZone.Intranet);

------------------------------------------------

protected void SaveButton_Click(object sender, EventArgs e) {
    if (!string.IsNullOrEmpty(GoogleAnalyticsSiteIDTextBoxDefault.Text)) {
        SolutionPackUtility.SetGoogleAnalyticsSiteIdByUrlZone(GoogleAnalyticsSiteIDTextBoxDefault.Text,
                                                              SPUrlZone.Default);
    }

    if (!string.IsNullOrEmpty(GoogleAnalyticsSiteIDTextBoxIntranet.Text)) {
        SolutionPackUtility.SetGoogleAnalyticsSiteIdByUrlZone(GoogleAnalyticsSiteIDTextBoxIntranet.Text,
                                                              SPUrlZone.Intranet);
    }

Nice code right, but where do we persist these ID's? I decided to use the site collections RootWeb properties bag. It could have been a WebApplication scoped feature but I wanted the option to activate and deavtivate per Site Collection so the Site Collection's RootWeb propertybag will do.

public static string GetGoogleAnalyticsSiteIdByUrlZone(SPUrlZone urlZone) {
    string retVal = string.Empty;
    string propertyKey = CreateZoneDependendKey(googleAnalyticsSiteIdPropertyKey, urlZone);
    if (RootWeb.Properties.ContainsKey(propertyKey)) {
        retVal = RootWeb.Properties[propertyKey];
    }
    return retVal;
}

public static void SetGoogleAnalyticsSiteIdByUrlZone(string value, SPUrlZone urlZone) {
    string retVal = string.Empty;
    string propertyKey = CreateZoneDependendKey(googleAnalyticsSiteIdPropertyKey, urlZone);

    if (GetGoogleAnalyticsSiteIdByUrlZone(urlZone) != value) {
        if (RootWeb.Properties.ContainsKey(propertyKey)) {
            RootWeb.Properties[propertyKey] = value;
        }
        else {
            RootWeb.Properties.Add(propertyKey, value);
        }
        RootWeb.Properties.Update();
    }
}

Nice UI but where do we inject the code?

The created UI does nothing but giving the user an interface where he or she can enter site id's per url zone. It doesn't inject the script into each and every page yet. We need something to inject some script into each and every page. For frequent readers of my blog this should sound very familiar. We used this technique for jQuery and the 'StayHere' feature as well. We simply add a usercontrol and an element in our feature that attaches the user control to the AdditionalPagehead delegate control. The user control looks something like this:

<%@ Assembly Name="SharePointSolutionPack, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a7cd02bdf107f7a" %>
<%@ Control Language="C#" ClassName="GoogleAnalyticsControl" %>
<%@ Import Namespace="Motion10.SharePoint2007" %>

<script runat="server">
    protected void Page_Load(object sender, EventArgs e) {
        if (!Page.IsPostBack)
        {
            if (string.IsNullOrEmpty(SolutionPackUtility.GoogleAnalyticsSiteId))
            {
                GoogleAnalyticsMultiView.SetActiveView(GoogleAnalyticsErrorView);
            }

            this.DataBind();
        }
    }
</script>
<asp:MultiView ID="GoogleAnalyticsMultiView" runat="server" ActiveViewIndex="0">
    <asp:View ID="GoogleAnalyticsCodeView" runat="server">
<script type="text/javascript">
    var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
    document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
    try {
        var pageTracker = _gat._getTracker("<%# SolutionPackUtility.GoogleAnalyticsSiteId %>");
        pageTracker._trackPageview();
    } catch (err) { }
</script>
    </asp:View>
    <asp:View ID="GoogleAnalyticsErrorView" runat="server">
<!--The Google Analytics Feature on this site is enabled but the Google Analytics Site Id has not been set for this URL Zone.
    You can find this setting under:Site Settings - Site Collection Administration - Google Analytics Feature Settings.-->
    </asp:View>
</asp:MultiView>

Conclusion

With some straightforward code we were able to add Google Analytics to our site collection features. It's a little bit of work that truly pays off. A lot of customers nowadays want more than just basic site analytics.

Cheers

Wes

Virtual Earth vs Google Maps in a SharePoint feature

Which one suites me better?

I do get asked about implementing a Google Maps feature into SharePoint pretty often. Most of the time however it's not that the client wants Google Maps. No, they want a maps implementation on their SharePoint site and they don't really care if that's Google Maps or Virtual Earth. I developed both features lately and did notice some points that make the implementation of Virtual Earth as the one I prefer. I'll show you why in this post.

Google Maps Key

If you like to implement Google Maps on a site you'll have to add a script tag to the head of our page that refers to the Google Maps API. Unfortunately this URL needs a Google Maps key. This key is some sort of hash that corresponds to the hostname of your site. So if you do like to add a Google Maps feature to a site, you'll need some sort of user UI to add the Google Maps key. I decided to create a layouts page to do the job. I't not that it's hard to do, but it does give me extra work. Oh... and don't forget that a user can enter a site in multiple URL zones.

Google Maps Feature Settings

For using Virtual Earth you don't need any key whatsoever so it's very easy to implement a feature for Virtual Earth. Add a script tag to the additional page head and we're done!

Development Experience

Google Maps has an API that is TOO complicated in a lot of ways. You can't simply change the icon image you would like to use for the icon size will not match up and mess up your image. The Google Maps API does not have an easy way to fetch the best zoom level. You need to create a GLatLngBounds object and extend it with all markers you like to add to your page and then get the zoom level. You can bind html to a marker's info window but markers don't have an openInfoWindowHtml method that takes zero arguments. You can't however get the marker's infoWindowHtml because it doesn't expose a property to do so. All together it get's a very annoying job to add some markers, open the last marker its info window, right after we zoomed in to the best level. It costs me a 100 lines of JavaScript code to do so.

The exact same functionality built with Virtual Earth cost me exactly half the amount of code. I used 50 lines!

You can download this zip file with both js files to see for yourself.

User Experience

Virtual Earth loads its maps way faster and by default looks an awfull lot better than Google Maps. But that's just my opinion. You have to judge that one for yourself.

Google Maps Feature Settings

Conclusion

I'ld pick Virtual Earth above Google Maps anytime. And not because it's better in a sense that I can do more with it. It's just that I can accomplish even better results in less time.

Cheers,

Wes

P.S. : If there's enough demand for it I'll write a few blogposts on how to create a connected Virtual Earth Maps control. Just let me know in the comments if you would like me to do so.

More Posts