Wednesday, May 18, 2011

InfoPath 2007 - Validating whether a form has been edited by another user since it has been opened

We had a requirement to prevent users from overwriting another's form. Initially I tried to automate the check-in check-out function programmatially, but I ran into some obstacles.

It is possible to programmatically check-out the form on the form Loading event, but there is no form unloading event. If a user just closes the browser window, the form will remain checked out.

It's possible to check in a form from a SharePoint designer workflow, so I wanted to check-in the form automatically when the item was saved, but ironically, a SharePoint designer workflow won't run on an item that is already checked out.

So as a workaround, instead of using checkin/checkout, I am validating the last modified date on save. When the form is first opened, I populate the InitialLastModifiedDate. When I click the save button, I populate the CurrentModfiedDate. If the dates don't match, I raise an error.


Scenario:  User 1 opens form,
User 2 opens form
User 1 saves form
User 2 saves form ( raise error)

This solution requires code behind.

First add the following fields to the main data source.

InitialLastModified
CurrentLastModified
UpdateInitialLastModified
doSaveValidation
saveMessage

I made the InitialLastModified, CurrentLastModified and UpdateInitialLastModified fields text fields. It's not necessary to make them date fields, a string comparison was easier for me to work with.



Add the saveMessage field to the form. The saveMessage will display the actual validation message. 

Add a rule to the Save button on the form. The rule will update the saveMessage (I just add a period to it), and set doSaveValidation to "yes".  I only want to do the validation when saving, so the doSaveValidation doesn't get set until I click the Save button. By changing the saveMessage field, I initiate some code that runs on the saveMessage_Changed Event.




In the formloading event, initiate the fields:

populateLastModified(strIncidentNum, "/my:myFields/my:InitialLastModified");
xNav.SelectSingleNode("/my:myFields/my:CurrentLastModified", this.NamespaceManager).SetValue("");
//I'm setting the doSaveValidation to 'no' because I don't want the validation to run until the Save button is clicked.
xNav.SelectSingleNode("/my:myFields/my:doSaveValidation", this.NamespaceManager).SetValue("no");



The populateLastModified function actually does a CAML Query to get the last modified date from the SharePoint form library. I will do another CAML query in the saveMessage_Changed event to get the currentLastModified date when the form is being saved

public void populateLastModified(string strIncidentNum, string xPath)
{
SPQuery qry = new SPQuery();
qry.Query = "<Where><Eq><FieldRef Name='ICSIncidentNum'/><Value Type='Text'>" + strIncidentNum + "</Value></Eq></Where>";
qry.ViewFields = "<FieldRef Name='Editor'/>";

try
{
SPContext contxt = SPContext.Current;
string siteURL = contxt.Web.Url;
using (SPSite site = new SPSite(siteURL))
{
using (SPWeb web = site.OpenWeb())
{

SPList list = web.Lists["ListName"];
SPListItemCollection LIC = list.GetItems(qry);
// there should only be one
if (LIC.Count > 0)
{
SPListItem item = LIC[0];
XPathNavigator xNav = this.CreateNavigator();
xNav.SelectSingleNode(xPath, this.NamespaceManager).SetValue(item["Editor"].ToString());
}

}
}
}
catch (Exception ex)
{
logError("An error occurred in populateLastModified: " + ex.Message);
}

}

Create a validating event on the saveMessage field:


public void saveMessage_Validating(object sender, XmlValidatingEventArgs e)
{

XPathNavigator xnav = this.CreateNavigator();
string initialLastModified = getfieldValue("/my:myFields/my:InitialLastModified");
string currentLastModified = getfieldValue("/my:myFields/my:CurrentLastModified");
string strIncidentNum = getfieldValue("/my:myFields/my:IncidentNum");
string strDoSaveValidation = getfieldValue("/my:myFields/my:doSaveValidation");
if (strIncidentNum != "" && strDoSaveValidation == "yes")
{
if (initialLastModified != currentLastModified)
{
// you can switch to another view to display an error screen and display a validation error.
this.ViewInfos.SwitchView("SaveConflict");
// switch views
e.ReportError(xnav, false, "The incident has been saved since you opened it.", "Please cancel and re-open it");
}
else
{
// passed validation
}
}
}



Add a changedEvent to the saveMessage Field. This will populate the currentLastModifiedDate field and initiate the validating event on the saveMessage field.

public void saveMessage_Changed(object sender, XmlEventArgs e)
{
// populate current LastModified
string strIncidentNum = getfieldValue("/my:myFields/my:IncidentNum");
populateLastModified(strIncidentNum, "/my:myFields/my:CurrentLastModified");
this.CreateNavigator().SelectSingleNode("/my:myFields/my:doSaveValidation", this.NamespaceManager).SetValue("yes");

}
I frequently use this getFieldValue function:

private string getfieldValue(string xPath)
{
XPathNavigator xnDoc = this.MainDataSource.CreateNavigator();
XPathNavigator xnMyField = xnDoc.SelectSingleNode(xPath, this.NamespaceManager);
if (xnMyField != null)
{
return (string)xnMyField.ValueAs(typeof(String));
}
else
{
return string.Empty;
}

}

There is no way to clear the error. The user will have to close and re-open the form to continue.