Showing posts with label Code Sample. Show all posts
Showing posts with label Code Sample. Show all posts

Thursday, July 26, 2012

Securing custom application pages

When you create a custom application page in sharepoint (a page deployed to the layouts folder) as part of a visual studio project, often you will want to make sure it is used by the right people. Especially if the actions on the page happen with elevated privilages.

The only security settings you get out of the box is setting rules on when the links to those pages are shown through custom actions. In a custom action you can specify required permissions on the site, and so the link isnt shown to people who are not supposed to use the page.

The problem is - any user can open the page if they just type the URL in the browser. To avoid this, I always add a bit of script that checks if the user is allowed to use the page. Where? In the Page_Load function!

Here is an example of how I secured a page to only show to web administrators, and I also added a check that a required feature has to be activated in the site or the page will not display. For this purpose, I made sure all the controls in the web page are in a panel called pnlControls - so I can easily hide them when the user shouldnt be using the page. I also created a lable called lblErrors outside the panel - so I can so an error to the user if they are not supposed to use the page. An alternative would be to just redirect the user to the site's home page.


Here is the sample:

protected void Page_Load(object sender, EventArgs e)
{
            lblErrors.Visible=false;
            if (!IsFeatureActivated(SPContext.Current.Web, new Guid(Constants.C_SiteActionsFeatureID)))
            {
                lblErrors.Visible=true;
                lblErrors.Text = "The required feature is not activated in this site. This page is not available when the feature is not activated.";
                pnlControls.Visible = false;
                return;
            }
            if (!SPContext.Current.Web.UserIsWebAdmin)
            {
                lblErrors.Visible = true;
                lblErrors.Text = "You don't have permissions to view this page. Only web administrators are allowed to use the functionality on this page.";
                pnlControls.Visible = false;
                return;
            }
}

public static bool IsFeatureActivated(SPWeb web, Guid featureId)
        {
            return web.Features[featureId] != null;
        }

Monday, February 06, 2012

Enabling Managed Metadata in Office365 (includes download)

If you have office365 or other hosted sharepoint solutions, you may find that the managed metadata column is not available to use - it shows an error that "the required feature is not enabled for this column type".
There are two possible reasons. The first - you don't have a hosted site that supports managed metadata. To check if that is the case, check with your hosting company, or, if you have Office365, you should have a "manage term store" link in your sharepoint online administration centre. If you dont have that link, then you cannot have managed metadata - sorry.
The other possibility is because a feature that is needed on the site is not enabled by default, and because it is a hidden feature you will not be able to activate it yourself.
If you search for this, you will find a lot of articles about how to solve it on a regular sharepoint farm using either command line or powershell. However, what happens if you don't have access to the server, such as in the case of SharePoint online (Office365) or a hosted environment?
In such a case, you need to use a sandboxed solution to enable the feature. To simplify this, I have created a sandbox solution that has a single feature called "Enable Managed Metadata":
This feature simply activates and deactivates the managed metadata feature.
To download this solution, visit my company's web site's code sample library or click this direct download link.
After you have downloaded the solution, you need to upload it to your site. Go to the site's settings page, and click the "Solutions" link to go to the solution gallery. Here, click on "Solutions" in the ribbon and then "upload solution" button. Browse to the file you downloaded, and upload the file. Once uploaded you will have the option to Activate the solution. This should also automatically activate the feature, so from that moment on you can start creating managed metadata columns!

While you'r there, would you mind clicking the "like" button for our web site?

Sunday, January 15, 2012

The importance of synchronous event handlers

In SharePoint, we can create list item event handlers (or receivers). I have written about this in the past, and even posted some code for setting the security automatically on a list item as it is created.
Now, there is one thing I wanted to write about this topic. Event handlers can be defined as either Synchronous or Asynchronous - regardless of the event type. By default, events were always asynchronous if they had the "ed" suffix (ItemAdded, ItemUpdated), and synchronous if they had the "ing" suffix (ItemAdding, ItemUpdating). However, sometimes we want to run our code in an "ed" event, but have it synchronous (notice that the opposite is impossible have the "ing" properties to run asynchronously)- for example, when we want to modify item properties when its added, but before the user has been redirected to a new page.
A classic example is the upload of a document to a document library. The user gets prompted to browse for a file and clicks upload. The next page would be the properties page for the file that was uploaded. If we want to set some of the properties to be pre-filled for the user, we have to do that after the file has been uploaded - but we cannot do that in ItemAdding, as at that point we don't have access to the item - it wasn't created yet. In ItemAdded we can set values to the item - but by default this event is asynchronous - and sharepoint will show the user the properties dialog while the event is still happening in the background. The result is disastrous : the user sees the properties screen, clicks "Save" and gets an error telling him or her that the item has been modified by another process or user.

To resolve this we use the "Synchronization" XML tag in the receiver's registration XML with the value set to "Synchronous" or, if the receiver is registered using server object model code, we set the "Synchronization" property of the receiver class to SPEventReceiverSynchronization.Synchronous. To quote MSDN: "Synchronous processing provides the ability to run the event in the same thread before sending the Web response back to the browser."

As an example, lets create an event receiver that sets the title of a file to the file name, as the file is being uploaded. The code is simple:

public override void ItemAdded(SPItemEventProperties properties)
       {
           base.ItemAdded(properties);
           if (properties.ListItem["Title"] == null)
           {
               int startingPoint = properties.AfterUrl.LastIndexOf("/");
               properties.ListItem["Title"] = properties.AfterUrl.Substring(startingPoint + 1);
               properties.ListItem.Update();
           }
       }

Now, the XML that registers the receiver is :
<Receivers ListTemplateId="101">
      <Receiver>
        <Name>SetTitleFromFileNameItemAdding</Name>
        <Type>ItemAdded</Type>
        <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
        <Class>DocumentTitleSetter.SetTitleFromFileName.SetTitleFromFileName</Class>
        <SequenceNumber>10000</SequenceNumber>
        <Synchronization>Synchronous</Synchronization>
      </Receiver>

  </Receivers>
Now, when uploading a file, the users are first asked for the file:
And when they upload, they see the dialog allowing them to change the file's name, and by magic the title is already filled for them!

Where can this be useful? I can think of a very real world example: assigning a number to a document as it is being uploaded. The title setting event can be critical in some scenarios where we have lists with lookup columns to a document library - since the lookup column cannot show the file name column we need to show the title, which means we cannot have an empty title (in which scenario you will also want to set the title to unique, and add extra code to make sure the title you are setting is not unique - dont assume that because it is a copy of the file name there wont be duplicates - remember that users may edit both file names and titles!).

Just in case someone finds this useful, I am uploading the code, and the compiled solution package to my company's web site as a free, open source solution for anyone to use on their own risk. I have tested this to work with Office365 (SharePoint online) and as a farm solution - so enjoy!. You can find all my company's free code samples and solutions in our document library on our web site. The specific files are: [Source Code] and [Sandbox Ready Package].

Wednesday, November 16, 2011

Event handler to archive items when deleted

A person just asked this on a mailing list I am on, so I thought I'd share this code. The requirement - copy a list item that is deleted to an archive list. The solution - event receiver that copies the item. The code below does the job, but again is only a sample - you still need to implement error handling, and there are hard coded variables there you will want to change.
Note: this code is specifically for list items, not for documents in document libraries. You can easily change the code to support documents as well (see the CopyAttachments function for an example on how to copy files).

public override void ItemDeleting(SPItemEventProperties properties)
       {
           //note: may require permission elevation.
           //TODO: add error handling

           //get the item being deleted
           SPListItem item = properties.ListItem;
           //get the target list
           SPList targetList = properties.Web.Lists["Announcements Archive"];
           //create the new item
           SPListItem newItem = targetList.Items.Add();
           //copy the list item to the target
           foreach (SPField f in item.Fields)
           {
                if (!f.ReadOnlyField && newItem.Fields.ContainsField(f.InternalName))
                    newItem[newItem.Fields.GetFieldByInternalName(f.InternalName).Id] = item[f.Id];   
           }
           //copy "special" read only fields that can be written to
           newItem["Created By"] = item["Created By"];
           newItem["Modified By"] = item["Modified By"];
           newItem["Modified"] = item["Modified"];
           newItem["Created"] = item["Created"];
           newItem.SystemUpdate(false);
           CopyAttachments(item, newItem);
           base.ItemDeleting(properties);
       }

Note - for the "CopyAttachments" I used code that I published in the past: http://www.sharepoint-tips.com/2008/11/how-to-copy-attachments-from-one-list.html

Tuesday, November 15, 2011

Setting item permissions for new items

A common question that I recently answered in a sharepoint conference is how to make a sharepoint list only show items to the users who wrote the items, and users specified in the item's properties.
A good example of this is a task list where only the original creator of the task and the assignee can see and change the task. This is impossible to do in sharepoint out of the box, and that is where we, as developers, come in.
The way to do it is by developing a simple event handler and attaching it to the list. I prefer to attach to ItemAdded and ItemUpdated - put the exact same code in both events (so that people can reassign the task). The code sample below is a good starting point for you to figure out how to do this. The code sample assumes you know how to use visual studio to create a new event handler, and is meant only as a sample - it doesn't have error handling and it does have some hard coded values for a task list that you may want to change.

public override void ItemAdded(SPItemEventProperties properties)
        {
            //get the list item that was created
            SPListItem item = properties.ListItem;
            //get the author user who created the task
            SPFieldUserValue valAuthor = new SPFieldUserValue(properties.Web, item["Created By"].ToString());
            //get the "assigned to" user. Note - this will throw an error if the task is not assigned to anyone - implement error handling!
            SPFieldUserValue valAssignedTo = new SPFieldUserValue(properties.Web, item["Assigned To"].ToString());
            //disconnect the security from the list, and delete all permissions
            item.BreakRoleInheritance(false);
            //create the object that will hold the roles for the author user
            SPRoleAssignment authorRole = new SPRoleAssignment(valAuthor.User);
            //create the object that will hold the roles for the assigned to user
            SPRoleAssignment assignedToRoles = new SPRoleAssignment(valAssignedTo.User);

            //get the contribute role from the web. Alternatively use code to create a new role definition with custom permissions.
            SPWeb oWebsite = properties.Web;
            SPRoleDefinitionCollection collRoles = oWebsite.RoleDefinitions;
            SPRoleDefinition oRoleDefinition = collRoles["Contribute"];

            //assign permissions to task author
            authorRole.RoleDefinitionBindings.Add(oRoleDefinition);
            item.RoleAssignments.Add(authorRole);
            //assign permissions to task assignee
            assignedToRoles.RoleDefinitionBindings.Add(oRoleDefinition);
            item.RoleAssignments.Add(assignedToRoles);
            //update the item
            item.Update();
            base.ItemAdded(properties);
        }

You can download the entire code sample as a visual studio project from my company's site's code samples document library at http://www.extelligentdesign.com/Code%20Sample%20Downloads/Forms/AllItems.aspx. You will want to change the project's properties to point to your local development site before debugging.

Friday, October 14, 2011

Setting the images for a publishing page from a module

Here is a small tip for developers (about time I wrote one again). What do you do if you want to provision a publishing page to a sharepoint site, and set the page's image (or rollup image) during the provisioning?
Well, step one should be obvious - make sure you upload the image you want to display. This can be done in the same feature or a seperate one. I will assume that you already know how do do that (if not, then the MSDN article "how to provision a file" is your friend). The same goes for actually how to build the feature and module that uploads your publishing page (again, refer to the MSDN article, or better yet, see my friend Waldek's article discussing just this).
So what am I talking about? I want to focus on how to set two specific properties on the page - the page's image and the page's rollup image. These two are not regular properties, since they are publishing properties. They do not expect the same kind of value that regular image properties expect (which is the same as a hyperlink - first part is the link to the image, than a space and a comma and then the description of the image). With publishing fields you need to supply the actual HTML that will be shown as the image.
Since this is done in the module, you will have to create a "Property" element in your "File" element in the module. The value of that property should be the HTML, but encoded so that the XML doesnt become invalid. The value (when not encoded) should look like this:

<img alt='This is a picture of a chicken' src='/PublishingImages/chicken.jpg' style='border:0px solid;'>

And the entire property (with the value encoded) should look like this:

<Property Name="PublishingRollupImage" Value="&lt;img alt='AFL peace Team at Whitten Oval' src='/newsandblog/PublishingImages/22-gal-tn.jpg' style='border:0px solid;'&gt;" />

Hope that helps someone!

Sunday, January 23, 2011

Setting Target Audiences with code

If you search the web you will find many articles explaining how to set audiences on web parts (also on list items). Most are curious - explaining that you need to specify the IDs of the audiences, separated by colons, and then four semi colons to finish it.

Really? four semi colons at the end without any explanation why? I just had to investigate.
It turns out that the four semi colons have a use. The syntax of the "target to" audience field type is as follows:
[Guid Ids separated by comma];;[Active directory groups' LDAP paths separated by line breaks];;[SharePoint security group names separated by commas]

For example:
decd0c08-4649-4e61-a8d6-8fdf5e4017ad,decd0c08-4649-4e61-a8d6-8fdf5e4017ad;;CN=SecGroup,CN=Users,DC=Development,DC=Local
CN=samplegroup1,CN=Users,DC=Development,DC=Local
;;Admin Office, Authors

So if you only have global (user profile) audiences, the value will be guids followed by four semi colons (because the values of the other type of audiences are blank). If you only choose security\distribution lists then you will have two semi colons before them, and two after. If you chose only sharepoint groups you will get 4 semi colons before the group names.

To make things simpler for me, I wrote a small class to create a value for me, or to parse it out for me. Here is the code:

public class AudienceFieldValue
    {
        /// <summary>
        /// A list of the IDs of the sharepoint global audiences (the one defined in the user profile database, accessible with  Microsoft.Office.Server.Audience.AudienceManager
        /// </summary>
        public List<Guid> GlobalAudienceIds;
        /// <summary>
        /// A list of sharepoint security groups
        /// </summary>
        public List<string> SharePointSecurityGroupNames;
        /// <summary>
        /// A list of active directory LDAP paths (CN=SecGroup,CN=Users,DC=Development,DC=Local) pointing to security groups or distribution lists
        /// </summary>
        public List<string> DirectoryGroupsOrDistListLDAPPaths;

        public AudienceFieldValue()
        {
            GlobalAudienceIds = new List<Guid>();
            SharePointSecurityGroupNames = new List<string>();
            DirectoryGroupsOrDistListLDAPPaths = new List<string>();
        }
        /// <summary>
        /// Use this constructor if you have a value from an existing item and you want to parse it. 
        /// You can even join values from more than one item seperated by semi colons. 
        /// For example: AudienceFieldValue val = new AudienceFieldValue(item1["Target Audiences"].toString() +;"+ item1["Target Audiences"].toString())
        /// </summary>
        /// <param name="value">The value of a "audience target to" column from a SPListItem (or more than one, seperated by semi colons)</param>
        public AudienceFieldValue(string value) 
        {
            GlobalAudienceIds = new List<Guid>();
            SharePointSecurityGroupNames = new List<string>();
            DirectoryGroupsOrDistListLDAPPaths = new List<string>();
            string[] arrTargets = value.Split(";".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
            foreach (string audienceGroup in arrTargets)
            {
                if (audienceGroup.Contains("CN="))
                {
                    //it is one or more AD groups (CN) split by line breaks
                    string[] adgroups = audienceGroup.Split('\n');
                    foreach (string audienceCN in adgroups)
                    {
                        DirectoryGroupsOrDistListLDAPPaths.Add(audienceCN);
                    }
                }
                else
                {
                    string[] arrAudiences = audienceGroup.Split(',');
                    foreach (string audienceName in arrAudiences)
                    {
                        try
                        {
                            Guid g = new Guid(audienceName);
                            GlobalAudienceIds.Add(g);
                        }
                        catch (Exception ex)
                        {
                            //its not a guid - so it is a local sharepoint security group
                            SharePointSecurityGroupNames.Add(audienceName);
                        }
                    }
                }
            }
        }


        /// <summary>
        /// Get the value that you can set to a list item's target audience field
        /// </summary>
        /// <returns>A string value containing all the IDs specified in a format that sharepoint understands</returns>
        public override string ToString()
        {
            if (GlobalAudienceIds.Count == 0 && SharePointSecurityGroupNames.Count == 0 && DirectoryGroupsOrDistListLDAPPaths.Count == 0)
            {
                return "";
            }
            else
            {
                StringBuilder result = new StringBuilder();
                //first add any global audiences
                foreach (Guid id in GlobalAudienceIds)
                {
                    if (result.Length > 0)
                        result.Append(",");
                    result.Append(id.ToString());
                }
                //must add two semi colons to seperate even if there is nothing after
                result.Append(";;");
                //add any directory group path, seperated by a line break
                bool addedDirGroup = false;
                foreach (string directoryGroupPath in DirectoryGroupsOrDistListLDAPPaths)
                {
                    if (addedDirGroup)
                        result.Append("\n");
                    result.Append(directoryGroupPath);
                    addedDirGroup = true;
                }
                //must add two semi colons to seperate even if there is nothing after
                result.Append(";;");
                //add any sharepoint group names seperated by commas
                bool addedSPGroup = false;
                foreach (string spGroupName in SharePointSecurityGroupNames)
                {
                    if (addedSPGroup)
                        result.Append(",");
                    result.Append(spGroupName);
                    addedSPGroup = true;
                }

                return result.ToString();
            }
        }
    }

Tuesday, January 18, 2011

Creating static and dynamin menus in the Site Actions menu

I am presenting tonight in the Canberra SharePoint User Group (and will show the code again in the upcoming Australian SharePoint Conference) about adding actions to the site actions menu. Instead of writing all I have to say, I will just share the source code with your - trusting that you will understand the difference between the two methods I am showing (static and dynamic). Enjoy!
Source code can be downloaded from:
MenuActions.zip

Wednesday, December 22, 2010

Controls not showing the value you choose after submitting

A quick one before I forget to post about it - common issue when developing a web control (mostly EditorParts these days, or web parts that do not have an ASCX) is that the user chooses values for the text boxes and dropdownd and other controls, click the submit button, but the value in the function that deals with the button click (or the ApplyChanges function in an editorpart) doesnt get the value the user chose. Instead, the value is the old value.
There are several possible things that can cause this (I covered some in a previous article about common mistakes) but one that is so obviously simple that I forget to make sure of is this:
Make sure "base.CreateChildControls();" is the first thing called in the "CreateChildControls" function.
If that doesnt work, read my previous article about the web part life cycle.

Sandbox solutions and the publishing image field type

I recently found out (to my horror) that sandbox solutions do not support (among many other things) the publishing field types. For example, if you want to use the "LinkFieldValue" class from the Microsoft.SharePoint.Publishing.Fields namespace in a sandbox solution, then you are out of luck. So far, fair enough - annoying, but fair. I figured that I can overcome this by not using that class to parse the details of the image from publishing pages, and I will write my own function that will tease out the image URL from the string that the field returns.

How naive am I!

When I tried running this: imageUrl = item[field.Internalname);

The code would time out, and not return anything. The entire page would just hang. No exception thrown or any reason given.

My solution was to use: imageUrl = item.GetFormattedValue(field.InternalName);

Apparently this gets the string value without trying to go through anything that is not supported in sandboxed solutions. Update: after a bit of more trial and error I found that the exception thrown (sometimes) is that the assembly (the publishing one) is not serialiazble:
'item[this.ImgFieldName]' threw an exception of type 'System.Runtime.Serialization.SerializationException'

Sunday, December 12, 2010

"Cannot complete this action." error when adding a field to a list (or a content type)

If you are trying to add a site column to a list, or a content type that has a site column to a list, and get the dreaded "Cannot complete this action.", then have a read below. Note - this is valid for both sharepoint 2007 and 2010.

To troubleshoot this, I turned off the custom errors to see the entire stack trace of the error. The error was thrown by the Microsoft.SharePoint.Library.SPRequest.AddField function. This lead me to read Eric's blog post about the error and he pointed me in the right direction.

It turns out that a lookup column that allows multiple selection is not supported as an indexed column. You'd expect SharePoint to validate that, and indeed you do not get that option when configuring such a column using the user interface.
However - sharepoint does not validate that when you create the site column using CAML (which is what Eric did) or using code (which is what I did).

Eric's blog post shows how you can recreate and resolve the problem if you are creating the column using XML. So just in case you are making the same mistake I was doing, this is how I resolved my issue - I just make sure none of my code-created lookup columns is both multi select AND indexed, as you can see in the code below.

SPFieldLookup newField = web.Fields.GetFieldByInternalName(newColumnInternalName) as SPFieldLookup;
newField.Title = "My Lookup";
newField.AllowMultipleValues = isMultiSelect;
newField.Group = "My Site Columns";
if (!isMultiSelect) newField.Indexed = true; else newField.Indexed = false;
newField.LookupField = myList.Fields["Title"].InternalName; newField.Update();

Thursday, August 12, 2010

How to set values for a checkbox choice list field? (SPFieldMultiChoice)

This is a question I just recieved through my "contact" form for this blog. The person (Chris) who asked the question gave a wrong email address, so I will post the answer publicly since I believe this can help other starting sharepoint developers - especially since I saw some wrong tips coming up in google when you search for it. So here is the right way to do it.

Question: "Hi, thanks a mil for those snippets!One question: I'm trying to programatically add selected items of a checkBoxList to a SPListItem, but am not sure in what format it wants it. i.e I've added all items as you suggested - newItem["Name"] = "blah" etc, but when it comes to the field that requires the selected checkboxlist values, I'm stuck."

Answer: to set the value for a check box choice list (a field of type choice, with multiple choices allowed - showing checkboxes), you should use the SPFieldMultiChoiceValue class to set the value. This class allows you to construct the list of choices you want to set for the column by adding the string values to it. Here is an example on how to use it:

SPList sampleList =web.Lists["test"];
SPFieldMultiChoiceValue val = new SPFieldMultiChoiceValue();
val.Add("choice1");
val.Add("choice3");
SPListItem newItem = sampleList.Items.Add();
newItem["Title"] = "This is the new item";
newItem["test"] = val;
newItem.Update();

Tuesday, June 01, 2010

Validating Web Part Properties

Nothing is more annoying than configuring a web part by changing its properties and then hitting ok only to see the web part display an error that a property is invalid - and having to open the properties pane again to fix the problem.
To avoid this, best practice is to validate the data entered in the "set" of the web part property. For example, if I have a web part property that needs a comma delimited array of numbers (for example 1,2,3,4) and I don't want to build a tool part just for that, I can still build a property like this:

public string NumberArray
{
    get{return _numberArray;}
    set{_numberArray=value;}
}
The problem with the code above is that it is not validating that the string entered is indeed an array of numbers. To do that, I could change the code to something more like this:
public string NumberArray
{
    get{return _numberArray;}
    set{
          string [] arr = value.split(',');
          foreach (string item in arr)
          {
             int i;
             if(!int.TryParse(item,out i))
                throw new Exception("The item \""+item+"\" is not a valid number");
          }

_numberArray=value;}
}
This will do what I want - preven the user from closing the web part properties pane before fixing the error in the property, but it will not display the nice informative error to the user. Instead, it will show a generic error "An error has occurred" despite the fact that I specified what the error was when I threw the error!
Why? because to do that the exception must be of type WebPartPageUserException.
So the correct code for validating my sample property would be:
public string NumberArray
{
    get{return _numberArray;}
    set{
          string [] arr = value.split(',');
          foreach (string item in arr)
          {
             int i;
             if(!int.TryParse(item,out i))
                throw new WebPartPageUserException("The item \""+item+"\" is not a valid number");
          }

_numberArray=value;}
}

Now - when the users put an invalid value in my property they will be notified that it is invalid, and which value it was.

Friday, February 26, 2010

Web Part architecture - what I had to fix in ChartPart

An important aspect of web part architecture is "redeploy-ability".
Redeploy-ability is when you want to use the web part as part of a site template. When you develop a web part, you need to think of redeployment - since the web part will need to be deployed with a set of pre-configured settings into a new site - the web part's properties. If those properties cannot be set as part of the site template, the web part cannot be redeployed.

Let me use ChartPart as an example.
If you don't already know, there is a project called "ChartPart" in codeplex, that shows lists as charts:
chartpart2-1.png
The web part needs to store a lot of information in its properties when it gets configured. For example, it needs to store the X axis and the Y axis columns. For this, the developer created a property called "XAxisSourceColumns", of type "List<string>". That allowed him to add column names to the list easily in the code.
The problem with this, is that this property has to be stored with the web part, and is required for the web part to work - that means if I export the web part, then I expect that when I import it again, all the settings will be there. With a property of type "List<string>", this will not happen - because the web part system does not know how to serialize a "List<string>" object into XML (a .webpart file) and doesnt export those settings. Then, if you import the web part you just exported, you lost those settings. I recommend storing the information as a simple type (defining the properties as strings for example) for serialization purposes.

A second problem, and one that you can see with a lot of Microsoft web parts as well as ChartPart (for example the Content Query Web Part) is that when a web part is connected to a list, the property stores the list's ID - a GUID, instead of the list name. This is smart - because someone may rename the list, and the ID will remain the same, so the web part will keep working. On the other hand - using an ID means that you lose "redeploy-ability" - since a list's ID gets generated randomly every time the site gets created, if your template has the web part set with a specific ID, then when you create a new site off that template, the web part will be broken - since it will not find a list with that ID in the site.
I recommend you store in the web part both the list's ID and the list's name. This way, if there is a list with the ID that was specified, the web part uses it, and therefore does not break when the list's title changes. But if there is no list with that ID (or no ID was set - which is what we will do in a template), the web part will look for a list with the name that was set - which means it will support redeploy-ability.

Finally, a problem with many web parts that allow linking to objects in other sites is that the site url property often expects a value - and does not support a "this site" option (which I like to add as a blank value). This means again that we lose "redeploy-ability" - since there is no way to configure the web part to look in this new site that was created from the template - the web part expects a link to a site...
I recommend supporting a "leave site url empty for current site" as a minimum requirement for a web part, and if you want to be real good, add support for relative URLs.

So how do we resolve this?


Usually we implement "redeploy-ability" when we start writing the web part - we create properties of type string that are not browse-able, and then create ToolParts that populate them using a good user interface - that gets the list of columns (in this example) from the user, then serializes it into a string that gets saved in the web part. It does mean a bit more effort to serialize the list and deserialize the string - but the end result is that when you export the web part you get what you expected - the serialized property gets stored in the .webpart file.

Supporting "blank" as current site url
In ChartPart to make this change was easy - I looked for all the places that SiteUrl property was used, and changed the code to check if the siteurl property is empty then to use the current site url. This is not as effective as you can make it (a function that gets a SPWeb object would have done it better) but it saved me a lot of refactoring:

string url = this.SiteUrl;
if (string.IsNullOrEmpty(url))
   url = SPContext.Current.Web.Url;
using (SPSite site = new SPSite(url)) 
{
   ...
}

Supporting list names as well as IDs
In ChartPart I added a property called ListName and then made the following modifications to all locations that referred to the list ID:

if(this.ListId != null && this.ListId != Guid.Empty)
   list = web.Lists[this.ListId];
else if(!string.IsNullOrEmpty(this.ListName))
   list = web.Lists[this.ListName];
I also changed the toolpart so that when a user picks a list from the dropdown, both the list ID and the list name gets stored.

Serializing complex type properties

In ChartPart for example, since it was already developed and I had to keep supporting existing web parts that store the information as a list, I couldnt remove the existing properties. However, what I did is add a string property for each one of the "List<T>" properties, that return the serialized value of the "List<T>" property it reflects, and when set it sets the "List<T>" property. Here is an example:

//the original property
[Personalizable(PersonalizationScope.Shared)]
public List XAxisSourceColumns
{
    get;
    set;
}
//this is for supporting exporting and importing the web part (which does not serialize List)
//we will have these as pipe seperated strings.
[Personalizable(PersonalizationScope.Shared)]
public string XAxisSourceColumnNames
{
    get
    {
        if (this.XAxisSourceColumns != null)
        {
            string ret = "";
            foreach (string xAxisColumn in this.XAxisSourceColumns)
            {
                if (!string.IsNullOrEmpty(ret))
                    ret += "|";
                ret += xAxisColumn;
            }
            return ret;
        }
        else
            return "";
    }
    set
    {
        this.XAxisSourceColumns = new List();
        if(!string.IsNullOrEmpty(value))
        {                 
            string[] values = value.Split('|');
            this.XAxisSourceColumns.AddRange(values);
        }                
            
    }
}

Sunday, July 12, 2009

Setting the navigation options for a meeting workspace

This is just wierd. I had to change the navigation options for a meeting workspace using code on a MOSS farm. The meeting workspaces were all under a publishing site, with the publishing feature turned on, so the option to inherit the navigation from the parent site is available in the UI.
However, there is no option to set it in the API.
When dealing with publishing sites, you can easily set it by using the PublishingWeb object instead of the SPWeb object - but using PublishingWeb.IsPublishingWeb on the event workspace returns false.
So I had a look at the code that runs when you click OK on the navigation settings page - and guess what? Microsoft doesnt check if the site is a publishing web or not - they just assume it is, and set the properties! So I did the same, and the code runs great. It doesnt matter that it is not a publishing web.
Code:
PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(web); pubWeb.InheritGlobalNavigation = true;

Monday, December 15, 2008

Changing fields by code - common oversight

I just helped someone on the OzMOss mailing list who had the following problem - he was trying to change a SPField object, but it was not getting updated, despite of no errors being thrown. Here is a sample of the code he was using:

Guid listID = web.Lists.Add("test", "", SPListTemplateType.PictureLibrary);
SPList list =web.Lists[listID];
SPField myField = list.Fields.GetFieldByInternalName("Title");
myField.Title = "TEST";
list.Update();
The problem with that code is a common oversight that developers who are new to the SharePoint object model usually make - the field object has to be updated. updating the list will have no effect, since the object that was modified was a column on the list, not a property of the list. The right code would be:
Guid listID = web.Lists.Add("test", "", SPListTemplateType.PictureLibrary);
SPList list =web.Lists[listID];
SPField myField = list.Fields.GetFieldByInternalName("Title");
myField.Title = "TEST";
myField.Update();

Tuesday, November 25, 2008

How to copy attachments from one list item to another

The microsoft support article for WSS 2 shows us how to download attachments from a list item. Basically, the attachments for a list item are stored as SPFile objects under a hidden folder in the list where those attachments are (a folder called "Attachments") - where each list item that has an attachment has its own folder - with the ID of the item being the folder's name. Here is a code sample for a function to copy attachments from one item to another.

private void CopyAttachments(SPListItem sourceItem, SPListItem targetItem)
{
  try
  {
     //get the folder with the attachments for the source item
     SPFolder sourceItemAttachmentsFolder =
         sourceItem.Web.Folders["Lists"].SubFolders[sourceItem.ParentList.Title].SubFolders["Attachments"].SubFolders[sourceItem.ID.ToString()];
      //Loop over the attachments, and add them to the target item
     foreach (SPFile file in sourceItemAttachmentsFolder.Files)
     {
        byte[] binFile = file.OpenBinary();
        targetItem.Attachments.AddNow(file.Name, binFile);
     }
  }
  catch { }
  finally
  {
     sourceItem.Web.Dispose();
  }
}

Thursday, November 13, 2008

Using the People picker - how to specify what can users choose

Just got this question in the Ask-The-Experts stand in Tech.ED europe. If I use the people picker like I showed in a previous article, how can I specify if the users can pick a user or a security group or a distribution list?

Well, the answer was in the SDK - there is a property "SelectionSet" that accepts a comma-delimited list of strings that say what can be picked.

For example, "User,DL,SecGroup" will allow users to pick user accounts, distribution lists and security groups. Choosing just "User" will allow only people accounts. The sharepoint people picker ususaly sets "User,SecGroup,SPGroup" or "User,SecGroup" or just "User"

Wednesday, November 12, 2008

My code for the custom fields presentation

As promised, here is the code I used. For the PowerPoint presentation I need to get approval, so wait for it.

The code has three samples of custom field types - one is a prime number validation field type (allows people to type only prime numbers), one is a date validation field that checks two other fields that the end date is after the start date (this is a good example for creating custom properties for the custom field) and the last is a sample how to create a custom look and feel for a custom field, without compiled code - just XML definition (CAML) in the renderpattern section. have fun!

Sunday, July 13, 2008

Updating web part properties, without the tool pane

here is a nice developer article for you - something I see asked a lot of times, and Rob Nowik is answering with a great how to article: Updating web part properties, without the tool pane