Monday, October 5, 2009

Standard Submit/Save for Infopath

Last week I was asked to update the submit option on 5 of my old Infopath forms.  Originally these forms just used the built in Save button on the toolbar.  I was asked to give the user a menu to choose their hospital, and then submit the document to a matching folder.

I'd done something similar on another form recently, so I decided to adapt that code, and make it generic enough that I could use the resulting code for all 5 updates.  What I've come up with is something that I plan to use as the basis of all my future forms.

Here's what you need to do if you want to try it:
1) Create a Submit connection with the default name of "Main submit".
2) Create a Web Reference called ProfileService that points to /_vti_bin/UserProfileService.asmx on your server, ie http://myserver/_vti_bin/UserProfileService.asmx
3) Create a reference to Microsoft.Sharepoint.DLL.

Your form will need the following:
1) FileName field:  Used to hold the file name once the form is submitted, ie file - 01-01-2001.xsn.  The file name is built using the Name field + the current date.  If there are duplicates, we add +1 to the name, ie file-2 - 01-01-2001.xsn.
2) Name field:  This can be any text field, and is used to generate the file name.  It should be a field that is likely to be unique, but we do check for duplicate filenames, so it doesn't have to be absolutely unique.
3) [Optional] Log field:  Used to log changes to the form.  My example form logs Creation, Submission, and Saves. 
4) [Optional] Errors field: Used to hold any errors that pop up.  This example form just logs the error.  In most of my real forms, when I encounter an error, I switch to a special Error view where I show the user what went wrong, and give them a button to report the error to our help desk.
5) [Optional] FolderChoices field:  Used to build the list of available folders. (This needs to be a repeating field)
6) [Optional] SelectedFolder field: Grabs the name of the folder that the user has selected. (This should be a drop-down menu which gets its values from the FolderChoices field)

Your form will need a Save button.  Have that button call StartSaveSubmitForm().  The code will figure out whether the document has already been saved, and either generate a new file name and submit, or save to the existing document.

If you want to submit to a folder, use the Loading event below to populate your FolderChoices field with all the folders that exist in your Form Library.

You'll need to disable the Save button if your required fields are still blank.  You'll also want to disable the Name and Folder fields once the document has been submitted to avoid confusion.  (The filename is only generated the first time you save the document, so changing the Folder field will not save the doc to a new folder, and changing the Name field will not rename the document.)

If you don't want to submit to a folder, set SubmitToFolder to False.
If you don't want to use logging, comment out the references.
If you don't want to use error logging, comment out the references.

Here is the code from a stripped down form using this option. 

using Microsoft.Office.InfoPath;
using System;
using System.Windows.Forms;
using System.Xml;
using System.Xml.XPath;
using mshtml;
using Microsoft.SharePoint;
using System.Collections;

namespace submitcode
{
    public partial class FormCode
    {
        public void InternalStartup()
        {
            EventManager.FormEvents.Loading += new LoadingEventHandler(FormEvents_Loading);
            ((ButtonEvent)EventManager.ControlEvents["Save_Button"]).Clicked += new ClickedEventHandler(Save_Button_Clicked);
        }

        public void FormEvents_Loading(object sender, LoadingEventArgs e)
        {
            //////////////////////////////////////////////////////////////////////////////
            //Set the Load variables for this Form
            //////////////////////////////////////////////////////////////////////////////

            string fileName = "/my:myFields/my:FileName";
            string libraryName = "Library"; //Name of the form Library we're submitting to
            string folderChoicesFieldXPath = "/my:myFields/my:FolderChoices"; //XPath for the repeating field that will hold our folder choices           
            bool submitToFolder = true;//If you don't want to use folders, change this to false

            //////////////////////////////////////////////////////////////////////////////
           
            //Check to see if the filename has been set
            XPathNavigator xPathNav = MainDataSource.CreateNavigator();
            XPathNavigator fileNameField = xPathNav.SelectSingleNode(fileName, NamespaceManager);

            if (fileNameField.Value.ToString() == "")//Doc hasn't been saved yet
            {
                LogComment("Document created.");
                if (submitToFolder)//If we are submitting to a folder, build the list of folder names
                {
                    GetSubFolders(libraryName, folderChoicesFieldXPath);
                }
            }
        }

        public void Save_Button_Clicked(object sender, ClickedEventArgs e)
        {
            StartSaveSubmitForm();
        }
       
        private void StartSaveSubmitForm()
        {
            //////////////////////////////////////////////////////////////////////////////
            //Set the Submit variables for this Form
            //////////////////////////////////////////////////////////////////////////////

            string folderXPath = "/my:myFields/my:FolderPicker";//If you are submitting to a folder point to list, otherwise leave blank
            string fileNameXPath = "/my:myFields/my:FileName";//The filename field used to set your form's name, IE: test - 01-01-2009.xsn 
            string nameXPath = "/my:myFields/my:NameField"; //The field used to generate the first part of the filename. The current date will be post-pended           
            string libraryName = "Library"; //Name of the Form Library
            bool submitToFolder = true;//If you don't want to use folders, change this to false

            ///////////////////////////////////////////////////////////////////////////////
           
            //Call Submit or Save method depending on whether the document has been saved
            XPathNavigator xPathNav = MainDataSource.CreateNavigator();
            XPathNavigator fileNameField = xPathNav.SelectSingleNode(fileNameXPath, NamespaceManager);

            if (fileNameField.Value.ToString() == "")//Document hasn't been saved yet, so submit it
            {
                SubmitForm(folderXPath, fileNameXPath, nameXPath, libraryName, submitToFolder);
            }
            else //document has already been submitted, save changes
            {
                SaveForm(folderXPath, fileNameXPath, nameXPath, libraryName, submitToFolder);
            }           
        }

        private void LogComment(string comment)
        {
            //////////////////////////////////////////////////////////////////////////////
            //Set the Log variables for this Form
            //////////////////////////////////////////////////////////////////////////////

            string logXPath = "/my:myFields/my:Log";

            //////////////////////////////////////////////////////////////////////////////
           
            XPathNavigator xPathNav = MainDataSource.CreateNavigator();
            XPathNavigator logField = xPathNav.SelectSingleNode(logXPath, NamespaceManager);

            string log = logField.Value.ToString();

            string newEntry = DateTime.Now.ToString() + " - " + FindCurrentUser() + " - " + comment + "\n\n" + log;
            logField.SetValue(newEntry);
        }

        private void ReportError(string errorMessage, string fullError)
        {
            //////////////////////////////////////////////////////////////////////////////
            //Set the Error Logging variables for this Form
            //////////////////////////////////////////////////////////////////////////////

            string errorXPath = "/my:myFields/my:ErrorMessage";

            //////////////////////////////////////////////////////////////////////////////

            XPathNavigator xPathNav = MainDataSource.CreateNavigator();
            XPathNavigator errorMessageField = xPathNav.SelectSingleNode(errorXPath, NamespaceManager);

            LogComment("Encountered error on form.");
            errorMessageField.SetValue(errorMessage + "\n" + fullError);
        }

        private string FindCurrentUser()
        {
            try
            {
                //Use Web Service to return information about the current user
                ProfileService.UserProfileService profileService = new ProfileService.UserProfileService();
                profileService.UseDefaultCredentials = true;
                profileService.Url = SPContext.Current.Site.Url.ToString() + "/_vti_bin/UserProfileService.asmx";
                ProfileService.PropertyData[] userProps = null;
                try
                {
                    // Passing null to this method causes the profile of the current user to be returned.
                    userProps = profileService.GetUserProfileByName(null);
                }
                catch (Exception ex)
                {
                    string errormessage = "Unable to get current user profile.";
                    ReportError(errormessage, ex.Message.ToString());
                }
                ProfileService.ValueData[] values = userProps[4].Values;//4th item is Preferred Name
                if (values.Length > 0)
                {
                    return values[0].Value.ToString();
                }
                else
                {
                    return this.Application.User.UserName.ToString();
                }
            }
            catch (Exception ex)
            {
                string errorMessage = "Error finding Preferred Name for current user.";
                ReportError(errorMessage, ex.Message.ToString());
                return this.Application.User.UserName.ToString();//Default to returning username
            }
        }

        private void SaveForm(string folderXPath, string fileNameXPath, string nameXPath, string libraryName, bool submitToFolder)
        {
            try
            {
                XPathNavigator xPathNav = MainDataSource.CreateNavigator();
                XPathNavigator folderField = xPathNav.SelectSingleNode(folderXPath, NamespaceManager);

                FileSubmitConnection fs = (FileSubmitConnection)this.DataConnections["Main submit"];

                //Check to see if Submit location ends with a /, if not, add one.
                if (fs.FolderUrl.EndsWith("/"))
                { } //All clear
                else
                {
                    fs.FolderUrl = fs.FolderUrl.ToString() + "/";
                }

                if (fs.FolderUrl.Contains(folderField.Value.ToString()))//Sets right folder when saving immediately after submit
                {
                }
                else
                {
                    if (submitToFolder)//If we are submitting to a folder, and the data conn isn't pointed to it yet, do so now
                    {
                        fs.FolderUrl = fs.FolderUrl.ToString() + folderField.Value.ToString() + '/';
                    }
                }

                LogComment("Saved the document.");

                fs.Execute();
            }
            catch (Exception ex)
            {
                string errorMessage = "Error when saving existing document.";
                ReportError(errorMessage, ex.Message.ToString());
            }

        }

        private void SubmitForm(string folderXPath, string fileNameXPath, string nameXPath, string libraryName, bool submitToFolder)
        {
            try
            {
                XPathNavigator xPathNav = MainDataSource.CreateNavigator();
                XPathNavigator folderField = xPathNav.SelectSingleNode(folderXPath, NamespaceManager);

                FileSubmitConnection fs = (FileSubmitConnection)this.DataConnections["Main submit"];

                //Check to see if Submit location ends with a /, if not, add one.
                if (fs.FolderUrl.EndsWith("/"))
                { } //All clear
                else
                {
                    fs.FolderUrl = fs.FolderUrl.ToString() + "/";
                }

                if (submitToFolder)//If we want to submit to a subfoler, set that folder now
                { fs.FolderUrl = fs.FolderUrl.ToString() + folderField.Value.ToString() + '/'; }

                //Generate a unique filename for our document
                string fileName = SetFileName(fileNameXPath, libraryName, nameXPath);
                LogComment("Submitted document as " + fs.FolderUrl.ToString() + fileName);

                //Submit the document
                fs.Execute();
            }
            catch (Exception ex)
            {
                string errorMessage = "Error when trying to submit new document.";
                ReportError(errorMessage, ex.Message.ToString());
            }
        }

        private string SetFileName(string fileNameXPath, string libraryName, string nameXPath)
        {
            XPathNavigator xPathNav = MainDataSource.CreateNavigator();
            XPathNavigator fileNameField = xPathNav.SelectSingleNode(fileNameXPath, NamespaceManager);
            XPathNavigator nameField = xPathNav.SelectSingleNode(nameXPath, NamespaceManager);

            //Create a filename based on the Name Field and Today's Date
            string fileName = nameField.Value.ToString() + " - " + DateTime.Today.ToShortDateString();
            fileName = fileName.Replace('/', '-');

            //Check to see if a file with the same name already exists
            bool isDuplicate = CheckForDuplicateFileName(fileName, libraryName);

            int x = 2;//If we have a duplicate, we'll add -X to the filename, starting with 2
            while (isDuplicate)//Keep generating filename + X until we get a unique name
            {
                fileName = GenerateNewFileName(nameField.Value.ToString(), Convert.ToString(x));
                isDuplicate = CheckForDuplicateFileName(fileName, libraryName);
                x++;
            }

            //Set File Name Field to our new File Name
            fileNameField.SetValue(fileName);
           
            //Return our filename
            return fileName;
        }

        private string GenerateNewFileName(string nameFieldValue, string x)
        {
            string newFileName = nameFieldValue + "-" + x + " - " + DateTime.Today.ToShortDateString();
            newFileName = newFileName.Replace('/', '-');
            return newFileName;
        }

        private bool CheckForDuplicateFileName(string fileName, string libraryName)
        {
            bool foundduplicate = true;//Return value has to be created and returned outside the elevated security

            //Elevated security sites can't be opened with SPContext, open the site normally and get the GUID
            //Then open the site again with elevated privelages
            Guid siteGuid = new Guid();
            Guid webGuid = new Guid();
            using (SPSite getGuidSite = SPContext.Current.Site)
            {
                using (SPWeb getGuidWeb = getGuidSite.OpenWeb())
                {
                    siteGuid = getGuidSite.ID;
                    webGuid = getGuidWeb.ID;
                }
            }

            SPSecurity.RunWithElevatedPrivileges(delegate()//Run with elevated privelages so we can find docs even if the current user doesn't have access
            {
                using (SPSite mySite = new SPSite(siteGuid))
                {
                    using (SPWeb myWeb = mySite.OpenWeb(webGuid))
                    {
                        SPList requestList = myWeb.Lists[libraryName];
                        SPQuery query = new SPQuery();
                        query.Query = "<Where><Eq><FieldRef Name='Title' /><Value Type='Text'>" + fileName + ".xml</Value></Eq></Where>";
                        query.ViewAttributes = "Scope=\"Recursive\"";//Search subfolders too
                        SPListItemCollection listItemColl = requestList.GetItems(query);
                        if (listItemColl.Count == 0)
                        {
                            foundduplicate = false;//No duplicates found
                        }
                        else
                        {
                            foundduplicate = true;//Found duplicate, we'll need to create a new filename
                        }
                    }
                }
            });

            return foundduplicate;
        }

        private void GetSubFolders(string libraryName, string folderChoicesFieldXPath)
        {
            ArrayList folderArray = GrabFolderList(libraryName);//Get a list of the folders
            PopulateFolders(folderArray, folderChoicesFieldXPath);//Use the folder list to populate our repeating field
        }

        private void PopulateFolders(ArrayList folderArray, string folderChoicesFieldXPath)
        {
            try
            {
                //Attach to repeating field facility list
                XPathNavigator xPathNav = MainDataSource.CreateNavigator();
                XPathNavigator Loc = xPathNav.SelectSingleNode(folderChoicesFieldXPath, this.NamespaceManager);
                Loc.SetValue("");
                XPathNavigator templateNode = Loc.Clone();

                //Loop through all facilities and add them to the repeating field
                for (int r = 0; r < folderArray.Count; r++)
                {
                    string value = folderArray[r].ToString();
                    XPathNavigator newNode = templateNode.Clone();
                    newNode.SetValue(value);
                    Loc.InsertBefore(newNode);
                }
                Loc.DeleteSelf();//Delete close used to populate list
            }
            catch (Exception ex)
            {
                string errorMessage = "Error when trying to populate folder List.";
                ReportError(errorMessage, ex.Message.ToString());
            }
        }

        private ArrayList GrabFolderList(string libraryName)
        {
            try
            {
                ArrayList folderArray = new ArrayList();
                using (SPSite mySite = SPContext.Current.Site)
                {
                    using (SPWeb myWeb = mySite.OpenWeb())
                    {
                        SPFolderCollection folders = myWeb.GetFolder(libraryName).SubFolders;
                        foreach (SPFolder currentFolder in folders)
                        {
                            if (currentFolder.Name.ToString() != "Forms")//Grab every folder name except for Forms
                            {
                                folderArray.Add(currentFolder.Name.ToString());
                            }
                        }

                    }
                }
                return folderArray;
            }
            catch (Exception ex)
            {
                string errorMessage = "Error when trying to get sub folders from Library.";
                ReportError(errorMessage, ex.Message.ToString());
                ArrayList emptyArray = new ArrayList();
                return emptyArray;
            }
        }


    }
}



Friday, July 17, 2009

Using Queries to open files

This is pretty basic information, but I had trouble finding a simple code snippet outlining how this works.  Here is super short snippet that finds and opens a file by it's Title field. 

//Begin by opening your List
SPList docList = web.Lists["Requests"];

//Build your query.  In this example, fileName is the name of a file we're already saved to the list
SPQuery query = new SPQuery();
query.Query = "<Where><Eq><FieldRef Name='Title' /><Value Type='Text'>" + fileName + "</Value></Eq></Where>";

//Now use your query to pull all the matching documents from our list. 
SPListItemCollection listItemColl = docList.GetItems(query);

//In this example, we're assumign that we'll always only get a single document back. 
//In practice, you should check listItemColl.Count to ensure that you're finding the doc.
 SPListItem listItem = listItemColl.List.Items[0];



The meat of the snippet is the Query string: <Where><Eq><FieldRef Name='Title' /><Value Type='Text'>" + fileName + "</Value></Eq></Where>
I'd highly recommend downloading U2U's CAML Builder to simplify building your query.  Just remember to strip out the <Query> tag U2U's tool likes to insert.

Monday, July 6, 2009

Infopath Form Migration hints

Last week, my manager had me open a ticket with Microsoft to talk about Infopath form migration.  We've been finding that it is time-consuming to migrate a form designed on the test server to the staging or production servers.   

Microsoft had some very good recommendations.  I've taken what they suggested and applied it to a purchase requisition form I'm working on.  It used to take me about 30 minutes to migrate my form to a new server.  Now all it takes is a 1 minute re-publish.  No other changes are necessary. 

Data Sources

With the exception of the Submit data source, I'm trying not to use these anymore on my own forms.  Their major draw-back, in my opinion, is that you cannot tell what fields are using a Data Source without opening the Field Properties.  When you have a few hundred fields, this quickly becomes annoying.  

Begin by creating your Data Source as normal.  Once it has been created, open Tools…Data Sources.

Select the Data Source and click the Convert button. 

In the Window that pops up, you want to keep the Connection link type set to "Relative to Site Collection"

Type in the following URL for the location http://Server/DataConn/XXXXX, where Server is the address of your Sharepoint Server, and DataConn is a Data Connection Library on that server.

Make sure that XXXXX is unique for your form and your data source. 

That "Relative to site collection option" means that your form will look for a Library called DataConn in the root of whatever site it finds itself on.  This is what lets us move our forms from server to server.   

Click OK and your Data Source will be converted into a file and uploaded to the DataConn library. 

The DataConn library requires that new documents are Approved before they can be referenced.  Browse to the Library, find your new document and use the context menu to approve it.

We now need to copy our new Data Source file to other servers where we want to use our form. 

Begin by downloading a copy of the file.  Before we upload the file to the new server, we need to modify it so that the Data Source is pointed to the new server.  Luckily, the Data Source is just an XML file.  Open it using Notepad or any other Text Editor. 

You'll find that the references to the server are easy to find.  I've high-lighted them below.  Just change the references to the correct server path and save your changes. 

 

<?xml version="1.0" encoding="UTF-8"?>

<?MicrosoftWindowsSharePointServices ContentTypeID="0x010100B4CBD48E029A4ad8B62CB0E41868F2B0"?>

<udc:DataSource MajorVersion="2" MinorVersion="0" xmlns:udc="http://schemas.microsoft.com/office/infopath/2006/udc">

                <udc:Name>Main submit</udc:Name>

                <udc:Description>Format: UDC V2; Connection Type: SharePointLibrary; Purpose: WriteOnly; Generated by Microsoft Office InfoPath 2007 on 2009-07-06 at 15:03:35 by CORP\kenna.</udc:Description>

                <udc:Type MajorVersion="2" MinorVersion="0" Type="SharePointLibrary">

                                <udc:SubType MajorVersion="0" MinorVersion="0" Type=""/>

                </udc:Type>

                <udc:ConnectionInfo Purpose="WriteOnly" AltDataSource="">

                                <udc:WsdlUrl/>

                                <udc:SelectCommand>

                                                <udc:ListId/>

                                                <udc:WebUrl/>

                                                <udc:ConnectionString/>

                                                <udc:ServiceUrl UseFormsServiceProxy="false"/>

                                                <udc:SoapAction/>

                                                <udc:Query/>

                                </udc:SelectCommand>

                                <udc:UpdateCommand>

                                                <udc:ServiceUrl UseFormsServiceProxy="false"/>

                                                <udc:SoapAction/>

                                                <udc:Submit/>

                                                <udc:FileName>Specify a filename or formula</udc:FileName>

                                                <udc:FolderName AllowOverwrite="0">http://serverAddress/LibraryName</udc:FolderName>

                                </udc:UpdateCommand>

                                <!--udc:Authentication><udc:SSO AppId='' CredentialType='' /></udc:Authentication-->

                </udc:ConnectionInfo>

</udc:DataSource>

This example is for a Submit Data Source.  Other Data Sources will look slightly different, and may have more than one field that needs updating.  If you're pulling data from a List, you may need to know the List GUID.   

Now that you have your Data Source file updated, you just need to upload it the DataConn library on the new server and Approve it. Repeat for each server that you need to use your form on. 

Web References in Code-Behind

Next up were Web References being used with code-behind in our forms.  It turns out that these references can be easily fixed with a single additional line of code.   

Begin by creating your Web Reference as usual.  Once created, take a look at the Web Reference properties.  You'll see the URL being referenced.  This is what we'll need to update on the new server.

In the case of my Purchase Req form, I'm calling the UserGroup web service that comes standard with Sharepoint.  The URL that is referenced,  /_vti_bin/usergroup.asmx, will exist on any Sharepoint server. 

//Open current Site

using (SPSite mySite = SPContext.Current.Site){

//Create Web Reference

GetGroupMembers.UserGroup getUsersFromGroup = new GetGroupMembers.UserGroup();

//Redirect URL

getUsersFromGroup.Url = mySite.Url.ToString() + "/_vti_bin/usergroup.asmx";}

Monday, June 29, 2009

Removing permissions from a Sharepoint list item

I recently came across an interesting problem while working on an Infopath form.  As part of the submission process, I needed to remove all the permissions from my new document.  This seemed easy to do using by calling Item.BreakRoleInheritence = true and then removing all the existing permissions from the item.

 

When I published my form, I found that my approach generated an error: An exception of type 'System.UnauthorizedAccessException' occurred in Microsoft.SharePoint.dll but was not handled in user code. Additional information: Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))

This error occured for any user who had Contribute access to the form library. 
 

After a bit of searching, I found this article: http://social.msdn.microsoft.com/Forums/en-US/sharepointdevelopment/thread/c3d2b304-7fcc-40d2-86ce-61d9b21b03d7  Look for the reply made by Kjetil Gullen on July 27.

 

The answer lies with how the .BreakRoleInheritence interacts with web.AllowUnsafeUpdates = true.  I won't go into the full explanation as the poster does an excellent job, but below is a code snippet that shows how to do this correctly.
 

                SPSecurity.RunWithElevatedPrivileges(delegate()

                    {

                        SPWeb _webInUserContext = SPContext.Current.Web;

                        SPSite _siteInUserContext = SPContext.Current.Site;

                        Guid _webGuid = _webInUserContext.ID;

                        Guid _siteGuid = _siteInUserContext.ID;

 

                        using (SPSite _site = new SPSite(_siteGuid))

                        {

                            _site.AllowUnsafeUpdates = true;//Allow Unsafe Updates for the Site

                            SPWeb _web = _site.OpenWeb(_webGuid);

                            _web.AllowUnsafeUpdates = true;//Allow Unsafe Updates for the Web

                            SPList docList = _web.Lists["Requests"];

 

                            SPListItem itemListItem = docList.Items.GetItemById(itemListID);

                            itemListItem.Web.AllowUnsafeUpdates = true;//Web as referenced by the item

 

                            itemListItem.BreakRoleInheritance(true);//Break your inheritence

 

                            itemListItem.Web.AllowUnsafeUpdates = true;//Breaking inheritence resets

                                                                       //Unsafe Updates, reenable it

 

                            //Remove the permissions one by one

                            foreach (SPRoleAssignment spra in itemListItem.RoleAssignments)

                            {

                                spra.RoleDefinitionBindings.RemoveAll();

                                spra.Update();

                            }

                       

                        });}

Wednesday, April 22, 2009

Displaying a User's Sharepoint Profile Picture

This week I was building a web part and needed to display the profile picture for a user. I figured this would be easy enough to do, but as is usually the case with Sharepoint, I was wrong. Several hours and multiple Google searches later, I put together a neat solution:

UserProfileManager profileManager = new UserProfileManager();

UserProfile userProfile = profileManager.GetUserProfile(loginName);

try

{

pictureURL = userProfile["PictureURL"].Value.ToString();

}

catch

{

pictureURL = "/_layouts/images/no_pic.gif";

}