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

February 2009 - Posts

Test your site in all mainstream browsers without installing

Sometimes I’m amazed and today is such a rare moment. This should be embedded in Visual Studio 2010 by default!

Have a look over here and get amazed. Trust me it works!

Posted: Feb 25 2009, 01:40 PM by webbes | with 8 comment(s)
Filed under: ,
Regular Expression Validated Custom SPField

Ever wandered about how it would be if you could validate the input of your clients with some regular expressions? Roaming the internet searching for a solution you do find some guys and girls who write about the fact that they created a Regular Expression field for SharePoint  but they don’t explain how they did it. In this post I’ll explain to you how to create such a field. Which isn’t to difficult.

Think first, code later.

This is something that bothers me for a long time. SharePoint developers seem to be the copy and paste masters of this universe. They forget design principles, act like robots doing what they’ve been told and forget to be creative.

When reading examples on how to create custom fields I’m having trouble not to cry. Everybody starts with a Field, a FieldControl and an UserControl. No matter what kind of field, that’s how it should be done. Next they copy the files to the correct location and they have a new field. Perfect, right? WRONG! PLAIN WRONG!

Each developer knows that if he’s about to create a class, the first thing he should do is to check whether there might be an existing class that implements most of the features already. If so, we inherit that class and create some extra methods and or properties. In our case we are going validate a string input by matching it with a regular expression. And SharePoint does have a great solution for string input already. It’s called SPFieldText.

Get started with the SPFieldRegexMatch

So we need a class, that inherits from the SPFieldText and implements two extra properties: ValidationExpression and ErrorMessage:

using System;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Text.RegularExpressions;
using System.Web;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Security;
 
namespace Motion10.SharePoint2007 {
    /// <summary>
    /// Field that validates it value by the given regular expression
    /// </summary>
    [Guid("92A8DAFE-92C5-407c-A1E6-7BF0C80FB904")]
    public class SPFieldRegexMatch : SPFieldText {
        /// <summary>
        /// Initializes a new instance of the <see cref="SPFieldRegexMatch"/> class.
        /// </summary>
        /// <param name="fields">The fields.</param>
        /// <param name="fieldName">Name of the field.</param>
        public SPFieldRegexMatch(SPFieldCollection fields, string fieldName)
            : base(fields, fieldName) {
            base.MaxLength = this.MaxLength;
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="SPFieldRegexMatch"/> class.
        /// </summary>
        /// <param name="fields">An <see cref="T:Microsoft.SharePoint.SPFieldCollection"></see> object that represents the field collection.</param>
        /// <param name="typeName">A string that contains the name of the field type, which can be a string representation of an <see cref="T:Microsoft.SharePoint.SPFieldType"></see> value.</param>
        /// <param name="displayName">A string that contains the display name of the field.</param>
        public SPFieldRegexMatch(SPFieldCollection fields, string typeName, string displayName)
            : base(fields, typeName, displayName) {
            base.MaxLength = this.MaxLength;
        }
 
        /// <summary>
        /// Gets or sets the maximum number of characters that can be typed in the field.
        /// </summary>
        /// <value></value>
        /// <returns>
        /// A 32-bit integer that specifies the maximum number of characters.
        /// </returns>
        new public virtual int MaxLength {
            get {
                string propVal = this.GetCustomProperty("MaxLength") + "";
 
                int retVal;
                if(int.TryParse(propVal, out retVal)){
                    return retVal;
                }
 
                return 0xff; 
            }
            set {
                this.SetCustomProperty("MaxLength", value);
                base.MaxLength = value;
            }
        }
 
        /// <summary>
        /// Gets or sets the validation expression.
        /// </summary>
        /// <value>The validation expression.</value>
        public virtual string ValidationExpression {
            get { return this.GetCustomProperty("ValidationExpression") + ""; }
            set { this.SetCustomProperty("ValidationExpression", value); }
        }
 
        /// <summary>
        /// Gets or sets the error message.
        /// </summary>
        /// <value>The error message.</value>
        public virtual string ErrorMessage {
            get {
                string retVal = this.GetCustomProperty("ErrorMessage") + ""; ;
                if (string.IsNullOrEmpty(retVal)) {
                    retVal = string.Concat(this.Title,
                                           " does not match the regular expression: ",
                                           HttpUtility.HtmlEncode(this.ValidationExpression));
                }
 
                return retVal;
            }
            set { this.SetCustomProperty("ErrorMessage", value); }
        }
 
        /// <summary>
        /// Used for data serialization logic and for field validation logic that is specific to a custom field type to convert the field value object into a validated, serialized string.
        /// </summary>
        /// <param name="value">An object that represents the value object to convert.</param>
        /// <returns>
        /// A string that serializes the value object.
        /// </returns>
        [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
        public override string GetValidatedString(object value) {
            string retVal = base.GetValidatedString(value);
            string textValue = base.GetFieldValueAsText(value);
 
            if (!string.IsNullOrEmpty(textValue) && !string.IsNullOrEmpty(this.ValidationExpression)) {
                Regex validationRegex = null;
                try {
                    validationRegex = new Regex(this.ValidationExpression);
                }
                catch (ArgumentException) {
                    throw new SPFieldValidationException("The configured regular expression is not valid. Please contact an administrator of this list to correct the issue.");
                }
 
                if (!validationRegex.IsMatch(textValue)) {
                    throw new SPFieldValidationException(this.ErrorMessage);
                }
            }
 
            return retVal;
        }
    }
}

As you can read in this code sample it’s all pretty straight forward. We use the GetCustomProperty and SetCustomProperty to save the SPFieldRegexMatch its properties and we override the GetValidatedString method to indeed validate our input.

Unfortunately SharePoint does NOT call the property its setter method so we can not validate the regular expression on input. Which is the reason why we validate the regular expression on validation.

We do have our custom field type but, how do we signal SharePoint that we have this lovely new field?

Continue with some CAML

SharePoint has its fields defined in xml files in de template/xml folder which names start with ‘fldtypes_’. All we need to do is create an xml file named 'fldtypes_motion10.xml' and define our custom field like so:

<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
  <FieldType>
    <Field Name="TypeName">RegexMatch</Field>
    <Field Name="ParentType">Text</Field>
    <Field Name="TypeDisplayName">Single line of validated text</Field>
    <Field Name="TypeShortDescription">Single line of validated text(regular expression validation)</Field>
    <Field Name="UserCreatable">TRUE</Field>
    <Field Name="Sortable">TRUE</Field>
    <Field Name="AllowBaseTypeRendering">TRUE</Field>
    <Field Name="Filterable">TRUE</Field>
    <Field Name="FieldTypeClass">Motion10.SharePoint2007.SPFieldRegexMatch, SharePointSolutionPack, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a7cd02bdf107f7a</Field>
    <RenderPattern Name="DisplayPattern" Type="Text">
      <HTML><![CDATA[<span title="Regular Expression Match field by motion10">]]></HTML>
      <Column HTMLEncode="TRUE" />
      <HTML><![CDATA[</span>]]></HTML>
    </RenderPattern>
    <PropertySchema>
      <Fields>
        <Field Name="MaxLength"
               DisplayName="Max Length:"
               Required="TRUE"
               MaxLength="3"
               Min="1"
               Max="255"
               DisplaySize="3"
               Type="Integer">
          <Default>255</Default>
        </Field>
        <Field Name="ValidationExpression"
               DisplayName="Validation Expression:"
               Required="TRUE"
               MaxLength="500"
               DisplaySize="35"
               Type="Text">
          <Default></Default>
        </Field>
        <Field Name="ErrorMessage"
               DisplayName="Error Message:"
               MaxLength="500"
               DisplaySize="35"
               Type="Text">
          <Default></Default>
        </Field>
      </Fields>
    </PropertySchema>
  </FieldType>
</FieldTypes>

This xml is very straight forward as well. Just for fun I inserted a RenderPattern but that’s not necessary at all. Since we inherit from SPFieldText, we have our rendering templates already.

With the PropertySchema we define the editable properties for which the UI will be rendered automagically. So all that’s left to do is to create a new WSPBuilder solution, add the class and the xml file, built, deploy and we are finished!

Cheers and have fun,

Wesley

P.S. the ValidationExpression and ErrorMessage properties are declared virtual with a reason. It’s very simple to override those properties and create a new special field. Like SPFieldEmailAddress for example:

using System;
using System.Runtime.InteropServices;
using Microsoft.SharePoint;
 
namespace Motion10.SharePoint2007 {
    /// <summary>
    /// Field that validates its value to check whether it is an email address
    /// </summary>
    [Guid("D306F9F2-2CF2-4ff3-BA4D-CC1C51126DCC")]
    public sealed class SPFieldEmailAddress : SPFieldRegexMatch {
        /// <summary>
        /// Initializes a new instance of the <see cref="SPFieldEmailAddress"/> class.
        /// </summary>
        /// <param name="fields">The fields.</param>
        /// <param name="fieldName">Name of the field.</param>
        public SPFieldEmailAddress(SPFieldCollection fields, string fieldName)
            : base(fields, fieldName) {
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="SPFieldEmailAddress"/> class.
        /// </summary>
        /// <param name="fields">An <see cref="T:Microsoft.SharePoint.SPFieldCollection"></see> object that represents the field collection.</param>
        /// <param name="typeName">A string that contains the name of the field type, which can be a string representation of an <see cref="T:Microsoft.SharePoint.SPFieldType"></see> value.</param>
        /// <param name="displayName">A string that contains the display name of the field.</param>
        public SPFieldEmailAddress(SPFieldCollection fields, string typeName, string displayName)
            : base(fields, typeName, displayName) {
        }
 
        /// <summary>
        /// Gets or sets the maximum number of characters that can be typed in the field.
        /// </summary>
        /// <value></value>
        /// <returns>
        /// A 32-bit integer that specifies the maximum number of characters.
        /// </returns>
        /// <remarks>The setter is not implemented.</remarks>
        public override int MaxLength {
            get {
                return 255;
            }
            set {
                throw new NotImplementedException();
            }
        }
 
        /// <summary>
        /// Gets or sets the validation expression.
        /// </summary>
        /// <remarks>The setter is not implemented.</remarks>
        public override string ValidationExpression {
            get {
                return @"^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$";
            }
            set {
                throw new NotImplementedException();
            }
        }
 
        /// <summary>
        /// Gets or sets the error message.
        /// </summary>
        /// <value>The error message.</value>
        /// <remarks>The setter is not implemented.</remarks>
        public override string ErrorMessage {
            get {
                string retVal = string.Concat(this.Title,
                                           " does not contain a valid email address.");
 
                return retVal;
            }
            set {
                throw new NotImplementedException();
            }
        }
    }
}
<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
  <FieldType>
    <Field Name="TypeName">EmailAddress</Field>
    <Field Name="ParentType">Text</Field>
    <Field Name="TypeDisplayName">Email address</Field>
    <Field Name="TypeShortDescription">Email address</Field>
    <Field Name="UserCreatable">TRUE</Field>
    <Field Name="Sortable">TRUE</Field>
    <Field Name="AllowBaseTypeRendering">TRUE</Field>
    <Field Name="Filterable">TRUE</Field>
    <Field Name="FieldTypeClass">Motion10.SharePoint2007.SPFieldEmailAddress, SharePointSolutionPack, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a7cd02bdf107f7a</Field>
    <RenderPattern Name="DisplayPattern" Type="Text">
      <HTML><![CDATA[<span title="Email address field by motion10">]]></HTML>
      <Column HTMLEncode="TRUE" />
      <HTML><![CDATA[</span>]]></HTML>
    </RenderPattern>
  </FieldType>
</FieldTypes>
Posted: Feb 23 2009, 02:31 PM by webbes | with 1 comment(s)
Filed under:
IListConsumer

This is part three in a series of post on how to create a Virtual Earth Web Part. In this post I’ll show you how to connect your web parts to SharePoint list view web parts. It’s not that simple but we’ll manage.

IWebPartTable

The first thing you might ask yourselves is: “Why use the obsolete IListConsumer interface instead of its replacement called IWebPartTable?”. Well there is a very good reason for this. If I would create both the connection provider and the connection consumer I would use the newer interface indeed. Unfortunately most of my data in SharePoint obviously comes from SharePoint lists. So the way to connect my web part to my lists is by a connection to a list view web part. And the list view web part does not implement the IWebPartTable interface but the IListProvider instead.

So we need to implement the IListConsumer interface if we want to connect to a SharePoint list view web part.

IListConsumer

The IListConsumer interface consists of just three methods:

ListReady
Event handler that gets called when the list is delivered.
PartialListReady
Event handler that gets called when the list is ready to be delivered. Comes in handy if you have great volumes of data.
ListProviderInit
Event handler that gets called when the list provider is initiated.

In our case we need only the ListReady. We are simply going to add all items we get to our list of pushpins.

Register the interface

We do have to take care of some things though before we can even connect to our web part. Implementing the interface is not enough, we need to register our list consumer web part to the page. This can be done by overriding the public virtual void EnsureInterfaces:

/// <summary>
/// Provides notification for a connectable Web Part that it should ensure all its interfaces are registered using the <see cref="Overload:Microsoft.SharePoint.WebPartPages.WebPart.RegisterInterface"></see> method.
/// </summary>
[Obsolete("You got to use this so called 'Obsolete' interfaces if you want to connect to default Sharepoint list webparts")]
public override void EnsureInterfaces() {
    base.EnsureInterfaces();
    RegisterInterface(_listConsumerInterfaceName,                           //InterfaceName    
            InterfaceTypes.IListConsumer,                                   //InterfaceType
            Microsoft.SharePoint.WebPartPages.WebPart.LimitOneConnection,   //MaxConnections
            ConnectionRunAt.Server,                                         //RunAtOptions
            this,                                                           //InterfaceObject
            "",                                                             //InterfaceClientReference
            "Consume List From",                                            //MenuLabel
            "Consumes a list from another Web Part.");                      //Description
}
 
/// <summary>
/// Returns a value that indicates where the implementation of a connection interface can run.
/// </summary>
/// <returns>
/// A <see cref="T:Microsoft.SharePoint.WebPartPages.Communication.ConnectionRunAt"></see> enumeration value.
/// </returns>
[Obsolete("You got to use this so called 'Obsolete' interfaces if you want to connect to default Sharepoint list webparts")]
public override ConnectionRunAt CanRunAt() {
    return ConnectionRunAt.Server;
}

We do this to get let the page know that we have a connection consumer. The CanRunAt override tells the page that our web part needs a post back because it runs at the server. Once we implemented the interface and registered our interface web part to the page it will show up in the web part menus.

ConnectionsMenu

 

Working with the DataTable

The goal of all this we are doing is to retrieve a DataTable from a connected web part. The data table contains columns which contain the values we need so badly in our virtual earth web part. Let us first store the data table somewhere and make sure we know it when we are connected:

private static readonly string _listConsumerInterfaceName = "VirtualEarthListConsumerInterface";
private bool _connectedToDataTable = false;
private DataTable _connectedWebPartDataTable;
 
/// <summary>
/// Used to notify a Web Part that it has been connected to another Web Part.
/// </summary>
/// <param name="interfaceName">Specifies the name of the interface on this Web Part that is being connected.</param>
/// <param name="connectedPart">A <see cref="T:Microsoft.SharePoint.WebPartPages.WebPart"></see> that specifies the other Web Part that is being connected to.</param>
/// <param name="connectedInterfaceName">Specifies the name of the interface that is being connected to on the other Web Part.</param>
/// <param name="runAt">A <see cref="T:Microsoft.SharePoint.WebPartPages.Communication.ConnectionRunAt"></see>   value that specifies where the interface on this Web Part should run.</param>
[Obsolete("You got to use this so called 'Obsolete' interfaces if you want to connect to default Sharepoint list webparts")]
public override void PartCommunicationConnect(string interfaceName, Microsoft.SharePoint.WebPartPages.WebPart connectedPart, string connectedInterfaceName, ConnectionRunAt runAt) {
    if (interfaceName == _listConsumerInterfaceName) {
        _connectedToDataTable = true;
    }
}
 
/// <summary>
/// Provides an event handler for the <see cref="E:Microsoft.SharePoint.WebPartPages.Communication.IListProvider.ListReady"></see> event of a Web Part that implements the <see cref="T:Microsoft.SharePoint.WebPartPages.Communication.IListProvider"></see> interface.
/// </summary>
/// <param name="sender">A Web Part that implements the IListProvider interface.</param>
/// <param name="listReadyEventArgs">A ListReadyEventArgs that provides the list contents.</param>
[Obsolete("You got to use this so called 'Obsolete' interfaces if you want to connect to default Sharepoint list webparts")]
public void ListReady(object sender, ListReadyEventArgs listReadyEventArgs) {
    if (listReadyEventArgs.List != null) {
        _connectedWebPartDataTable = listReadyEventArgs.List;
    }
}

For brevity I left the PartialListReady and ListProviderInit methods which are empty.

Working with the Fields

Well, we do have the data table know. Unfortunately we still can’t do a lot with it. That’s because the interface doesn’t define the names of the columns that contain the data. We don’t know the names of these columns during compile time so we need a way to give the client the option to define which columns contain which data. Because the clients know these columns as Fields in SharePoint we refer to them as fields. So our goal with the next code is to give the end user the possibility to define which field contains the latitude, which field contains the longitude etc.:

private string _defaultImage;
private string _titleFieldName = "LinkTitle";
private string _descriptionFieldName = "InfoBoxHtml";
private string _imageFieldName = "PushpinImage";
private string _latitudeFieldName = "Latitude";
private string _longitudeFieldName = "Longitude";
 
/// <summary>
/// Gets or sets the default image.
/// </summary>
/// <value>The default image.</value>
[Browsable(true)]
[Category("Connection Settings")]
[WebPartStorage(Storage.Shared)]
[FriendlyName("Default Image")]
[Description("The default image that's used for pushpins that get added by the connection.")]
public string DefaultImage {
    get {
        return _defaultImage;
    }
    set {
        _defaultImage = value;
    }
}
 
/// <summary>
/// Gets or sets the name of the title field.
/// </summary>
/// <value>The name of the title field.</value>
[Browsable(true)]
[Category("Connection Settings")]
[WebPartStorage(Storage.Shared)]
[FriendlyName("Title Field Name")]
[Description("The name of the field that contains the title for the pushpins.")]
public string TitleFieldName {
    get {
        return _titleFieldName;
    }
    set {
        _titleFieldName = value;
    }
}
 
/// <summary>
/// Gets or sets the name of the info window HTML field.
/// </summary>
/// <value>The name of the info window HTML field.</value>
[Browsable(true)]
[Category("Connection Settings")]
[WebPartStorage(Storage.Shared)]
[FriendlyName("InfoBoxHtml Field Name")]
[Description("The name of the field that contains the info box HTML for the pushpins.")]
public string InfoBoxHtmlFieldName {
    get {
        return _descriptionFieldName;
    }
    set {
        _descriptionFieldName = value;
    }
}
 
/// <summary>
/// Gets or sets the name of the latitude field.
/// </summary>
/// <value>The name of the latitude field.</value>
[Browsable(true)]
[Category("Connection Settings")]
[WebPartStorage(Storage.Shared)]
[FriendlyName("Latitude Field Name")]
[Description("The name of the field that contains the latitude for the pushpins.")]
public string LatitudeFieldName {
    get {
        return _latitudeFieldName;
    }
    set {
        if (string.IsNullOrEmpty(value)) {
            throw new WebPartPageUserException("The Latitude Field Name property cannot be null or empty");
        }
 
        _latitudeFieldName = value;
    }
}
 
/// <summary>
/// Gets or sets the name of the longitude field.
/// </summary>
/// <value>The name of the longitude field.</value>
[Browsable(true)]
[Category("Connection Settings")]
[WebPartStorage(Storage.Shared)]
[FriendlyName("Longitude Field Name")]
[Description("The name of the field that contains the longitude for the pushpins.")]
public string LongitudeFieldName {
    get {
        return _longitudeFieldName;
    }
    set {
        if (string.IsNullOrEmpty(value)) {
            throw new WebPartPageUserException("The Longitude Field Name property cannot be null or empty");
        }
 
        _longitudeFieldName = value;
    }
}
 
/// <summary>
/// Gets or sets the name of the image field.
/// </summary>
/// <value>The name of the image field.</value>
[Browsable(true)]
[Category("Connection Settings")]
[WebPartStorage(Storage.Shared)]
[FriendlyName("Image Field Name")]
[Description("The name of the field that contains the image for the pushpins.")]
public string ImageFieldName {
    get {
        return _imageFieldName;
    }
    set {
        _imageFieldName = value;
    }
}

After we add this code to our web part we have some new properties in our edit part categorized under “Connection Settings”.

ConnectionSettings

What next?

With the field name properties set we have both the data in our data table as well as the meta data that tells us which columns contain which data. So we can actually add all our pushpins to our pushpins property. We do this by overriding the OnPreRender method:

/// <summary>
/// The event handler for the System.Web.UI.Control.PreRender event that occurs immediately before the Web Part is rendered to the Web Part Page it is contained on.
/// </summary>
/// <param name="e">A System.EventArgs that contains the event data.</param>
protected override void OnPreRender(EventArgs e) {
    base.OnPreRender(e);
    
    List<VirtualEarthPushpin> pushpinsToAdd = new List<VirtualEarthPushpin>();
    if (_connectedToDataTable && _connectedWebPartDataTable != null) {
        try {
            pushpinsToAdd = pushpinsToAdd.Union(GetPushpinsFromConnectedDataTableWebPart()).ToList();
        }
        catch (WebPartPageUserException ex) {
            this.Controls.Add(new WebPartErrorControl(this.ID, ex.Message));
            return;
        }
    }
}
 
private List<VirtualEarthPushpin> GetPushpinsFromConnectedDataTableWebPart() {
    List<VirtualEarthPushpin> retVal = new List<VirtualEarthPushpin>();
 
    bool hasTitle = FieldExistsInDataTable(this.TitleFieldName);
    bool hasInfoBoxHtml = FieldExistsInDataTable(this.InfoBoxHtmlFieldName);
    bool hasLatitude = FieldExistsInDataTable(this.LatitudeFieldName);
    bool hasLongitude = FieldExistsInDataTable(this.LongitudeFieldName);
    bool hasImage = FieldExistsInDataTable(this.ImageFieldName);
 
    if (!hasLatitude || !hasLongitude) {
        throw new WebPartPageUserException("The Virtual Earth webpart is connected but the LatitudeFieldName or the LongitudeFieldName is not set properly to an existing field.");
    }
 
    foreach (DataRow row in _connectedWebPartDataTable.Rows) {
        VirtualEarthPushpin pushpin = new VirtualEarthPushpin();
        pushpin.Title = (hasTitle) ? row[this.TitleFieldName].ToString() : null;
        pushpin.InfoBoxHtml = (hasInfoBoxHtml) ? row[this.InfoBoxHtmlFieldName].ToString() : null;
 
        float latitude = 0;
        if (float.TryParse(row[this.LatitudeFieldName].ToString(), out latitude)) {
            pushpin.Latitude = latitude;
        }
 
        float longitude = 0;
        if (float.TryParse(row[this.LongitudeFieldName].ToString(), out longitude)) {
            pushpin.Longitude = longitude;
        }
 
        pushpin.Image = (hasImage) ? row[this.ImageFieldName].ToString() : this.DefaultImage;
 
        retVal.Add(pushpin);
    }
 
    return retVal;
}

The code is pretty straight forward. Bulk of it is is in the GetPushpinsFromConnectedDataTableWebPart. We first check which fields are actually there in the data table. That’s because only latitude and longitude are required and the rest is optional. We check if we do have a latitude and longitude and after that we’ll simply add the pushpins to our list. For brevity the helper methods are not in the code and the registration of JavaScript to the page is omitted as well. We will discuss JavaScript in the next post.

Conclusion

We have a web part now that is connectable to a SharePoint list view web part, because we implemented and registered the IListConsumer interface. We created properties that enables some configuration of the connection and we turned our data table into a generic list.

It was some work and it’s a bit of a tedious job to do. Next week we’ll add the JavaScript to finish up our Virtual Earth web part. So be sure to read next weeks post because it will contain the complete code for our web part. One week later we’ll package it all in a SharePoint solution package.

Cheers,

Wes

Virtual Earth Web Part

This is part two in a series of posts on how to create a Virtual Earth Web Part. In this post we’ll finish our properties with their attributes. I’ll explain the differences between the V2 web part attributes and the V3 web part attributes, and what these attributes actually do.

Remember from our first post that we inherit a microsoft.sharepoint.webpartpages.webpart  aka V2 web part. I can reveal now that we have to do this in order to make it possible to implement the IListConsumer and IRowConsumer interfaces later to connect to our Contacts list. The reason we have to implement those interfaces is because the SharePoint List View web part sadly does not implement the recommended IWebPartTable as you can read in my previous post.

Since we inherit the SharePoint web part we need to use some ‘legacy’ attributes but we’ll do fine. First focus on what we want before we focus on how to do it.

Properties

The properties I would like to implement because those get asked for pretty often are these:

Pushpins
A generic list with pushpins. We already discussed this property extensively in post one from this series
Zoom level
(int) The zoom level of the map. We can set this zoom level manually or let the next property decide the best possible zoom level.
Auto zoom
(bool) Auto zoom will first place all pushpins on the map and then decide what's the best zoom level in which all pushpins are visible on the map.
Show info box
(bool) With this option enabled the info box of the last added item with a description will be shown. This comes in handy if you place just one address on the map and want to display the address lines themselves or the company name above the pushpin.
Fixed
(bool) With this option enabled the client will lose all interaction with the map control. Comes in handy with a single address again.
Dashboard size
(VEDashBoard enum) This property does exactly what you'll expect. You can determine the dashboard size with it.

Property code

The code isn't that difficult it's just that we have to use different attributes than we are familiar with because we use a V2 web part. First the code and than we'll discus the different attributes:

/// <summary>
/// Gets or sets the zoom level.
/// </summary>
/// <value>The zoom level.</value>
[Browsable(true)]
[Category("Map Settings")]
[WebPartStorage(Storage.Shared)]
[FriendlyName("Zoom level")]
[Description("The zoom level of the Virtual Earth control.")]
public int ZoomLevel {
    get {
        return _zoomlevel;
    }
    set {
        if (value < 1 || value > 19) {
            throw new WebPartPageUserException("can't be less than 1 or greater than 19!");
        }
        _zoomlevel = value;
    }
}
 
/// <summary>
/// Gets or sets a value indicating whether to auto zoom.
/// </summary>
/// <value><c>true</c> if auto zoom is enabled; otherwise, <c>false</c>.</value>
[Browsable(true)]
[Category("Map Settings")]
[WebPartStorage(Storage.Shared)]
[FriendlyName("Auto Zooming")]
[Description("Sets the zoom level of the Virtual Earth control to a level where all pushpins are visible if set to true")]
public bool AutoZoom {
    get {
        return _autoZoom;
    }
    set {
        _autoZoom = value;
    }
}
 
/// <summary>
/// Gets or sets a value indicating whether to fix the map.
/// </summary>
/// <value><c>true</c> if the map should be fixed; otherwise, <c>false</c>.</value>
[Browsable(true)]
[Category("Map Settings")]
[WebPartStorage(Storage.Shared)]
[FriendlyName("Fixed")]
[Description("Disables client interactions with the Virtual Earth control if set to true")]
public bool Fixed {
    get {
        return _fixed;
    }
    set {
        _fixed = value;
    }
}
 
/// <summary>
/// Gets or sets a value indicating whether to show the info box.
/// </summary>
/// <value><c>true</c> if info box is to be shown; otherwise, <c>false</c>.</value>
[Browsable(true)]
[Category("Map Settings")]
[WebPartStorage(Storage.Shared)]
[FriendlyName("Show Info Box")]
[Description("Shows the info box of the last added pushpin with a description, on open")]
public bool ShowInfoBox {
    get {
        return _showInfoBox;
    }
    set {
        _showInfoBox = value;
    }
}
 
/// <summary>
/// Gets or sets the size of the dashboard.
/// </summary>
/// <value>The size of the dashboard.</value>
[Browsable(true)]
[Category("Map Settings")]
[WebPartStorage(Storage.Shared)]
[FriendlyName("Dashboard size")]
[Description("The size of the virtual earth dashboard")]
public VEDashboardSize DashboardSize {
    get {
        return _dashBoardSize;
    }
    set {
        _dashBoardSize = value;
    }
}

So it’s all straight forward. We do some validation in which we use the WebPartPageUserException to give the client a decent error message and we added some attributes.

Attributes

SharePoint uses different attributes to give the developer some influence over what properties get displayed, how they get displayed and how they are stored. Let’s walk through all the attributes used in the code above.

Browsable: This is the V2 version of WebBrowsable. With this attribute you decide whether or not the end user can view this property at all. If set to false the property will not show in the editor.

Category: This attribute is the same in V2 and V3. With this attribute you decide in which category the property shows up.

Category

WebPartStorage: This is the V2 version of Personalizable. With this attribute you decide if this property is shared or personal. If set the Personal(V2) or User(V3), the editing of this property will result in changes for the current user only. If set to Shared the editing              of this property will result in changes that effect all users that view the page.

FriendlyName: This is the V2 version of WebDisplayName. Most of the time the property name is in Pascal Case and the naming doesn’t have to make sense to the end user. With this attribute you can change the label that’s displayed with the field.

FriendlyName

Description: This is the V2 version of WebDescription. With this attribute you determine the tooltip that get’s displayed when an end user hovers the field.

Description

Conclusion

It’s not that hard to create SharePoint web part properties and there’s no big differences in the type of attribute you can use between V2 and V3. It’s just that the class names have changed. In the beginning when I started out developing V3 web parts it was kind of hard for me to see which attribute belongs to which web part type and I’ve made mistakes with it more than often. I hope this post gives some clarification to all.

Next time we’ll continue with implementing the IListConsumer and IRowConsumer interface.

Cheers,

Wes

More Posts