Monday, December 20, 2010

Synchronize Lists in two different site collections

We had a requirement to keep 4 lists in synch. There is a list of sales regions with the name of the sales representative, sales manager, etc for each region. The same list is used by several different sites. There was a master list and 3 child lists. Every time the master list is updated, the children lists should be updated.

The code is configurable, so additional child lists can be easily added by updating an item in a SharePoint configuration list.

For this to work, each of the child lists should begin with the same content.

I created an ItemEventReceiver. It will fire on the following events:

ItemAdded
ItemUpdated
ItemDeleting

I created a list called Configuration in the main source site. In that list I created 3 columns: Title, Value and Description





As shown above, I created 3 list items:

SynchSourceList
SynchDestLists
SynchDestSites

Each of the destination lists have the same name, so I didn't need to list 3 separate destination list names. These three items will be read by the Item Event Receiver.

This is the code for the ItemAdded Event:

public override void ItemAdded(SPItemEventProperties properties)
{
// copy the list item to the other lists

try
{
this.DisableEventFiring();
SPListItem SourceItem = properties.ListItem;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
string DestURL = getConfig.getConfigValue(properties.WebUrl, "SynchDestSites");
string DestList = getConfig.getConfigValue(properties.WebUrl, "SynchDestLists");
string[] sites = DestURL.Split(';');
// foreach site in list
foreach (string siteName in sites)
{
using (SPSite site = new SPSite(siteName))
{
using (SPWeb destWeb = site.OpenWeb())
{

SPList SPDestList = destWeb.GetList(destWeb.ServerRelativeUrl.TrimEnd('/') + "/Lists/" + DestList);
SPListItem spListItem = SourceItem;
SPListItem newDestItem = SPDestList.Items.Add();
foreach (SPField field in spListItem.Fields)
{
if (!field.ReadOnlyField &&
field.Type != SPFieldType.Invalid &&
field.Type != SPFieldType.WorkflowStatus &&
field.Type != SPFieldType.File &&
field.Type != SPFieldType.Attachments &&
field.Type != SPFieldType.Computed &&
field.Type != SPFieldType.Lookup)
{
newDestItem[field.InternalName] = spListItem[field.InternalName];
}
}
newDestItem.Update();
}
}
}
});
base.ItemAdded(properties);
}
catch(Exception ex)
{

}
finally
{
this.EnableEventFiring();

}


}


The ItemAdded Event calls GetConfigValue which is listed here:

class getConfig
{
public static string getConfigValue(string webURL, string listItem)
{
// get the destURL and destList from the Configuration List
string retValue;
try
{
SPSite thisSite = new SPSite(webURL);
SPWeb thisWeb = thisSite.OpenWeb();
SPList configList = thisWeb.GetList(thisWeb.ServerRelativeUrl.TrimEnd('/') + "/Lists/Configuration");

SPQuery query = new SPQuery();


query.ViewFields =
@"";
query.Query = @"" + listItem +
@"
";
SPListItemCollection items = configList.GetItems(query);

// just get the first item
SPItem firstItem = items[0];
retValue = firstItem["Value"].ToString();

if (thisWeb != null)
thisWeb.Dispose();

if (thisSite != null)
thisSite.Dispose();


}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("RegistrationListEventReceiver.ItemAdded() exception in getConfigValue" + ex.ToString());
retValue = "Error";
}

return retValue;

}

}


I am also capturing the ItemDeleting Event:


public override void ItemDeleting(SPItemEventProperties properties)
{
// remove the list item from the other lists
// find the item in the other list with the same ID and delete it

string key;
try
{
this.DisableEventFiring();
SPListItem SourceItem = properties.ListItem;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
key = SourceItem["RecordID"].ToString();

// do a query on the destination list and get the item with the same key
string DestURL = getConfig.getConfigValue(properties.WebUrl, "SynchDestSites");
string DestList = getConfig.getConfigValue(properties.WebUrl, "SynchDestLists");
string[] sites = DestURL.Split(';');

// for each site in list
foreach (string siteName in sites)
{
using (SPSite site = new SPSite(siteName))
{
using (SPWeb destWeb = site.OpenWeb())
{
SPQuery qry = new SPQuery();
qry.ViewFields = @"";
qry.Query = @"" + key + "";
SPList SPDestList = destWeb.GetList(destWeb.ServerRelativeUrl.TrimEnd('/') + "/Lists/" + DestList);
SPListItemCollection items = SPDestList.GetItems(qry);
if (items.Count > 0)
{
for (int i = items.Count - 1; i >= 0; i--)
{
items.Delete(i);
}
}
}
}
}
}
);
base.ItemDeleting(properties);

}
catch (Exception ex)
{

}
finally
{
this.EnableEventFiring();

}

}

and the ItemUpdated Event:

public override void ItemUpdated(SPItemEventProperties properties)
{
//update the list item in the other list(s)
string key;
try
{
this.DisableEventFiring();
SPListItem SourceItem = properties.ListItem;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
key = SourceItem["RecordID"].ToString();

// do a query on the destination list and get the item with the same key
string DestURL = getConfig.getConfigValue(properties.WebUrl, "SynchDestSites");
string DestList = getConfig.getConfigValue(properties.WebUrl, "SynchDestLists");
string[] sites = DestURL.Split(';');
// for each site in list
foreach (string siteName in sites)
{
using (SPSite site = new SPSite(siteName))
{
using (SPWeb destWeb = site.OpenWeb())
{
SPQuery qry = new SPQuery();
qry.ViewFields = @"";
qry.Query = @"" + key + "";
SPList SPDestList = destWeb.GetList(destWeb.ServerRelativeUrl.TrimEnd('/') + "/Lists/" + DestList);
SPListItemCollection items = SPDestList.GetItems(qry);
foreach (SPListItem item in items)
{
//update the fields from the source item
foreach (SPField field in SourceItem.Fields)
{
if (!field.ReadOnlyField &&
field.Type != SPFieldType.Invalid &&
field.Type != SPFieldType.WorkflowStatus &&
field.Type != SPFieldType.File &&
field.Type != SPFieldType.Attachments &&
field.Type != SPFieldType.Computed &&
field.Type != SPFieldType.Lookup)
{
item[field.InternalName] = SourceItem[field.InternalName];

}
}
item.Update();
}

}

}
}
}
);
base.ItemUpdated(properties);
}
catch (Exception ex)
{

}
finally
{
this.EnableEventFiring();

}

}

I also created a Feacture Event Receiver so that when the Feature is Activated, it would associate the Item EventReceivers with the specified sourceList. You can see the code here:

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{

try
{
using (SPSite destSite = new SPSite("http://yoursite.org"))
{
using (SPWeb destWeb = destSite.OpenWeb())
{
//make this configurable

string listName;
listName = SynchronizeLists.getConfig.getConfigValue("http://yoursite.org", "SynchSourceList");

destWeb.Lists[listName].EventReceivers.Add(
SPEventReceiverType.ItemAdded,
"SynchronizeLists, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3900cea7f3c6a92a",
"SynchronizeLists.SynchronizeListsItemEventReceiver");
destWeb.Lists[listName].EventReceivers.Add(
SPEventReceiverType.ItemDeleting,
"SynchronizeLists, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3900cea7f3c6a92a",
"SynchronizeLists.SynchronizeListsItemEventReceiver");
destWeb.Lists[listName].EventReceivers.Add(
SPEventReceiverType.ItemUpdated,
"SynchronizeLists, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3900cea7f3c6a92a",
"SynchronizeLists.SynchronizeListsItemEventReceiver");

}
}
}
catch (Exception ex)
{

}
}


There is a FeatureDeactivating event receiver as well:

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
try
{

using (SPWeb web = properties.Feature.Parent as SPWeb)
{

string listName;
listName = getConfig.getConfigValue("http://yoursite.org", "SourceList");
SPList TheList = web.Lists[listName];
//Declare the full assembly name
String AssemblyName = "SynchronizeLists, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3900cea7f3c6a92a";
int i;
for (i = 0; i < TheList.EventReceivers.Count; i++) { if (TheList.EventReceivers[i].Assembly.Equals(AssemblyName)) TheList.EventReceivers[i].Delete(); } //Update the list TheList.Update(); web.Update(); } } catch (Exception ex) { } }

Thursday, December 16, 2010

Input Validation for File Attachment Names

SharePoint does not allow special characters ([~#%&*{}<>?/+|\"]) in it's attachment names. 

I had an ItemAdded EventReceiver running on a WSS site.  And when I created an item, and added an attachment with a special character, I received the error message: "The file or folder name contains characters that are not permitted.  Please use a different name. ",

But the item would get created anyway. And the EventReceiver didn't fire.  I needed to do input validation on the client side before the item was submitted. 

  1. Add a Content Editor Web Part to the NewForm.aspx.
    1. Create a new list item
    2. Add &PageView=Shared&ToolPaneView=2 to the end of the URL in your browser
    3. Click Add a Web Part and select Content Editor Web Part from the Miscellaneous group
  2. Add the following code to the Source Editor
<Script type="text/javascript">
function PreSaveAction()
{
var attachment;
var filename="";
var fileNameSpecialCharacters = new RegExp("[~#%&*{}<>;?/+|\"]");
try {
attachment = document.getElementById("idAttachmentsTable").getElementsByTagName("span")[0].firstChild;
filename = attachment.data;
}
catch (e) {
}
if (fileNameSpecialCharacters.test(filename)) {
alert("Please remove the special characters from the file attachment name.");
return false;
}
else {
return true;
}
}
</script>

I used a javascript regular expression to check for invalid characters.

The user is prompted to remove the special characters from the file attachment.