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.

2 comments:

  1. Hello,
    I am a little bit lost. I am reading on the internet that Infopath form should be opened for following users (if it has been already opened by someone else before) locked in read-only mode. Can you comment on this?

    ReplyDelete
    Replies
    1. I know this is almost 2 years too late, but I'll answer anyway. This is correct if you are using the desktop client to handle the infopath form. Many people, though, prefer to use Forms Services in SharePoint to host and edit the forms, and this misses the locking functionality that the thick client provides.

      Delete