Welcome to MSDN Blogs Sign in | Join | Help

Recently a customer came up with a requirement that he is having some structured data in XML file and now when they are creating a list which will have data populated from the XML file.

The XML file contained the machine details for a particular user and XML was structured as:

<data>
    <user>
          <username>Username</username>
          <mac>MAC Address</mac>
          <machine>Machine Name</machine>
    </user>
</data>

The new custom list in SharePoint contained UserName, MAC and Machine Name as fields along with some other fields. Basically was to be used for recording Support Incidents for the machines. Customer wanted that when in NewForm.aspx, UserName field is filled, it should automatically pull-in information from XML file and fill in the MAC and Machine Name fields for the form.

First  thought was to create a new customized list definition (to remain in supported scenario) and use server-side code but that seems to be a little too much of task, just to populate a couple of fields.

Earlier exposure to raw JavaScript programming with XMLHttpRequest (XHR) came in handy here. A simple solution was decided as it was to be done only for 1 list and very specific stuff need to be pulled.

So the story begins, where we would be creating an ASPX page which will be responsible for fetching the XML file and getting the data filtered for the entered username. There would be an XHR request generated by JavaScript which will connect with the ASPX page and pull-in data and populate the fields.

For simplicity, I created a Virtual Directory in IIS, under the _layouts folder of my SharePoint application (named as Users) and hosted a Default.aspx page in it with the following code:

   1: // All the work is being done in the Page_Load event of Default.aspx only
   2: protected void Page_Load(object sender, EventArgs e)
   3: {
   4:     // we will work only if there is a QueryString named as "u"
   5:     if (!string.IsNullOrEmpty(Request.QueryString["u"]))
   6:     {
   7:         // Create the SPSite object
   8:         using (SPSite site = new SPSite(http://MySharePointSite))
   9:         {
  10:             // Get its SPWeb object
  11:             using (SPWeb web = site.OpenWeb())
  12:             {
  13:                 // Get the particular list item which contains the XML file
  14:                 SPListItem item = web.GetListItem(http://MySharePointSite/Shared%20Documents/TestXML.xml);
  15:  
  16:                 // Get the SPFile object from the SPListItem
  17:                 SPFile file = item.File;
  18:  
  19:                 // Get the file contents as Byte array 
  20:                 byte[] b = file.OpenBinary();
  21:  
  22:                 // Convert the byte array to string
  23:                 string s = (new System.Text.ASCIIEncoding()).GetString(b);
  24:  
  25:                 // XML structure is like:
  26:                 // <data>
  27:                 //     <user>
  28:                 //         <username>User name comes here</username>
  29:                 //         <mac>MAC address will be here</mac>
  30:                 //         <machine>Machine name will be here</machine>
  31:                 //     </user>
  32:                 //     <user>
  33:                 //         ... similarly for other users also ...
  34:                 //     </user>
  35:                 // </data>
  36:  
  37:                 // Create new XmlDocument object and load the file contents
  38:                 XmlDocument doc = new XmlDocument();
  39:                 doc.LoadXml(s);
  40:  
  41:                 // Select only the "user" nodes from the data
  42:                 XmlNodeList nodes = doc.SelectNodes("/data/user");
  43:  
  44:  
  45:                 // Loop through each of the "user" nodes in XML
  46:                 foreach (XmlNode node in nodes)
  47:                 {
  48:  
  49:                     // iterate only if we have data inside the User node
  50:                     if (node.HasChildNodes)
  51:                     {
  52:  
  53:                         // check if we have the "username" tag matching with our QueryString
  54:                         if(node.FirstChild.InnerText.ToLower().Equals(Request.QueryString["u"].ToLower()))
  55:                         {
  56:  
  57:                             // yes, we found it. Now dump out the data in delimited format
  58:                             // The output response will be like the following (separated by "|" [a pipe symbol])
  59:                             // UserName|MAC Address|Machine Name
  60:                             Response.Write(string.Format("{0}|{1}|{2}",node.ChildNodes[0].InnerText,node.ChildNodes[1].InnerText,node.ChildNodes[2].InnerText));
  61:  
  62:                             // we found our data, so break from the loop
  63:                             break;
  64:                         }
  65:                     }
  66:                 }
  67:  
  68:                 // We do not want to dump out anything else, so end the response stream
  69:                 Response.End();
  70:             }
  71:         }
  72:     }
  73: }
  74:  


Now over to the client part.

I opened up the NewForm.aspx page in SharePoint Designer and added a new Content Editor Web Part to add the JavaScript required for the client side operation. In the list I had the following names of the fields:

1. UserName
2. MAC
3. Machine

Now over to final creation of JavaScript code and here is the code I had used:

   1: <script language="javascript">
   2:  
   3: // used for displaying the "loading..." message
   4: var loadingDIV = null;
   5:  
   6: // This will hold reference to UserName control
   7: var ctlUser = null;
   8:  
   9: // This will hold reference to MAC Address control
  10: var ctlMAC = null;
  11:  
  12: // This will hold reference to Machine Name control
  13: var ctlMachine = null;
  14:  
  15: function f()
  16: {
  17:       // Create a new XHR request (this will work only with browsers with native XHR support)
  18:       // for other browsers like IE6 or lower, you would need to change it to create XMLHttpRequest ActiveX object
  19:       var mygetrequest = new XMLHttpRequest();
  20:  
  21:       // Here is where we do our work, once data is received back
  22:       mygetrequest.onreadystatechange = function() 
  23:       {
  24:         // Check readyState of the request
  25:         if (mygetrequest.readyState == 4) 
  26:         {
  27:           // Check if we have the proper request status
  28:           if (mygetrequest.status == 200)
  29:           {
  30:               // get the mData from the request. This data comes as "Username|MAC|MachineName" string
  31:               // separated using a "|" (pipe) sign
  32:               var mData = mygetrequest.responseText;
  33:  
  34:               // split the data to get the string array
  35:               ar = mData.split("|");
  36:  
  37:               // MAC address is the 2nd element in the array, assign value to the MAC textbox
  38:               ctlMAC.value = ar[1];
  39:  
  40:               // Machine name is 3rd element in the array, assign value to Machine name textbox
  41:               ctlMachine.value = ar[2];
  42:  
  43:               // hide the "loading..." message as we have loaded the data properly
  44:               loadingDIV.style.display="none";
  45:           }
  46:           else 
  47:           {
  48:              alert("An error has occured making the request")
  49:           }
  50:        }
  51:     }
  52:     
  53:     // Show the "loading..." DIV to show that we are fetching the data
  54:     loadingDIV.style.display="inline";
  55:  
  56:     // Send the actual request to our ASPX page which will read the XML file and give us data back
  57:     mygetrequest.open("GET", "/_layouts/Users/Default.aspx?u=" + ctlUser.value, true)
  58:     mygetrequest.send(null)
  59: }
  60:  
  61: // Fetch all the elements of tag as INPUT (we just need textbox references)
  62: var ele = document.getElementsByTagName("input");
  63:  
  64: // Loop through all the INPUT elements
  65: for (a = 0; a < ele.length; a++) 
  66: {
  67:       try 
  68:       {
  69:             ix = ele[a];
  70:  
  71:             // We need only the TEXT type INPUT elements
  72:             if (ix.type == "text") 
  73:             {
  74:  
  75:                   // SharePoint adds a TITLE attribute to each textbox which is exactly same as its Field Name
  76:                   // Check if we found "USERNAME" field here
  77:                   if (ix.attributes.getNamedItem("title").value.toLowerCase() == "username") 
  78:                   {
  79:                         // Set the UserName control object reference
  80:                         ctlUser = ix;
  81:  
  82:                         // Set the onBlur event handler to point to function "f()" which will do the actual work
  83:                         ctlUser.onblur = f;
  84:  
  85:                         // Now create a new SPAN element which contains the "loading..." message 
  86:                         // and take its reference in "loadingDIV" and inset it after the UserName textbox
  87:                         var xx = document.createElement("span");
  88:                         xx.innerHTML = "loading...";
  89:                         xx.id="loadingBlock";
  90:                         xx.style.display = "none";
  91:                         loadingDIV = xx;
  92:                         ctlUser.insertAdjacentElement("afterEnd", xx);
  93:                   }
  94:  
  95:                   // If this TEXT type INPUT element is MAC address, take its reference
  96:                   if (ix.attributes.getNamedItem("title").value.toLowerCase() == "mac") 
  97:                   {
  98:                         ctlMAC = ix;
  99:                   }
 100:  
 101:  
 102:                   // If this TEXT type INPUT element is Machine name, take its reference
 103:                   if (ix.attributes.getNamedItem("title").value.toLowerCase() == "machine") 
 104:                   {
 105:                         ctlMachine = ix;
 106:                   }
 107:             }
 108:       }
 109:       catch (ex)
 110:       { 
 111:           //Right now we are not doing anything if any error occurs
 112:       }
 113: }
 114: </script>

 

So basically in the JavaScript code, we are:

1. Iterating through all the INPUT tags of type TEXT
2. Check if TITLE attribute of the tag match any of our fields, we store a reference of them.
3. Attach an event hander for onBlur for our UserName text field, which calls the function “f()”
4. When the function “f()” is called, it initiate the XHR request to fetch the pipe delimited data from our ASPX file and then split the data and fill in the MAC and Machine Name text fields.

 

Hope some of you will find it handy and use it in your projects.

Happy Coding…

Lately working with one of the customer’s I came across an interesting lookout.

Basically customer has created a simple workflow for SharePoint in Visual Studio 2008 and had used a delay activity which seems to be causing the problem.

The problem was that workflow runs perfectly fine but after hitting the delay activity, never wakes up.

My first hunch went to the issue with SharePoint Workflows with delay activities (also noted on SharePoint Product Group blog). But the workflow seems to be too simple to get into any of the known issues.

Now I started looking into the deployment of the workflow and noticed that they have deployed the workflow’s assemblies into the BIN folder of the IIS-App under c:\inetpub.

Well, it does not seems to be the problem as the workflow was starting up fine and going through a few activities and facing issue only when hitting the delay activity.

The workflow architecture training earlier came to my rescue.

Basically workflow in 2 different process.

- W3WP.exe
- OWSTIMER.exe

W3WP.exe is the ASP.net worker process. When you first start the workflow (automatically or manually), the workflow assembly is loaded and workflow started by W3WP.exe process.

OWSTIMER.exe comes into picture when the workflow is persisted into the database and need to be woken up and restored into the memory.

In this particular scenario of the customer, the workflow assemblies were there in BIN of the IIS-Application under C:\InetPub folder. Now W3WP.exe process recognize which locations to scan for and from where the assemblies need to be picked up. But once the workflow hits the Delay Activity and it is saved to the database, to wake it up and restore into the in-memory state and continue execution, OWSTIMER.exe does not know the location of the assemblies, therefore the workflow fails to wake up.

Finally the resolution: 
Once we determined the problem, suggested the customer to sign the workflow assemblies and deploy them to GAC and remove from the BIN folder of the IIS-Application. Once the changes were done and assemblies deployed to GAC, the workflow worked perfectly fine and  completed “as expected”.


Hope this small in-sight helps someone out there with their workflows

Recently I came across an issue with one of the customers. They are using a lots of AutoCAD files which are with DWT extension and are uploaded to a SharePoint’s document library.

But when they are trying to download the .dwt files, it is failing always with error 403 (The Website declined to show this page)

Now what are .dwt files?

- DWT file extension is used by AutoCAD to save its templates.
- DWT file extension is used by FrontPage/SharePoint Designer to save “Dynamic Web Templates”


The questions comes up is that customer is using AutoCAD files but why SharePoint is throwing a 403. I started investigating and thought, it could be a corrupt AutoCAD file, but after using 3-4 different files, still it was thrown 403.

Then I created a simple plain text file and renamed it to .dwt and uploaded to SharePoint. Now when I tried to download it, still the same issue and SharePoint threw a 403 error again.

This got me digging into SharePoint and consulting my internal resources which turned up that SharePoint has blocked downloading of any file with .DWT extension. This was done to not allow people to download FrontPage/SharePoint Designer’s “Dynamic Web Templates”, which also incidentally have a .DWT extension.

Now my customer was having a lots of .DWT files up in SharePoint and now wanted to download them using their custom application. After consulting my internal resources, finally I got that using FPRPC, we should be able to download the .dwt files as the code path of FPRPC is different within SharePoint.

This whole thing led me to create a simple console application which used FPRPC to download a .dwt file to my local disk.

   1: namespace FPRPCDownloadFile
   2: {
   3:     class Program
   4:     {
   5:         static void Main(string[] args)
   6:         {
   7:  
   8:         //Place where we want to put the file
   9:             string filepath = @"C:\DestLoc.dwt";
  10:  
  11:         // Point to Author.dll
  12:             string sURI = "http://MySharePoint/_vti_bin/_vti_aut/author.dll";
  13:  
  14:         // This is the FPRPC method we use
  15:             string sMethod = "get document:6.0.2.5523";
  16:  
  17:         // This is the actual FPRPC command that we give
  18:             string sRPC = String.Format("method={0}&service_name=/&document_name=docs/testFile.dwt", sMethod);
  19:  
  20:         // We need to convert the whole FPRPC query/command to be converted to BYTES before sending over the wire
  21:             byte[] bRPC = System.Text.Encoding.UTF8.GetBytes(sRPC);
  22:  
  23:  
  24:         // We will be using WebClient to issue the command and fetch the file
  25:             WebClient wc = new WebClient();
  26:             wc.Credentials = System.Net.CredentialCache.DefaultCredentials;
  27:             
  28:         // Adding the required headers for FPRPC
  29:         wc.Headers.Add("Content-Type", "application/x-vermeer-urlencoded");
  30:             wc.Headers.Add("X-Vermeer-Content-Type", "application/x-www-form-urlencoded");
  31:  
  32:         // Finally send the request to the server and get output as byte array
  33:             byte[] sResponse = wc.UploadData(sURI, bRPC);
  34:  
  35:         // Convert this byte array to string as we want to strip off the Vermeer/FPRPC header and get the actual file content
  36:             string returnStr = System.Text.Encoding.UTF8.GetString(sResponse);
  37:  
  38:         // Find where does the header ends and get the actual byte array of our file
  39:             long iEnd = returnStr.IndexOf("</html>") + 8;
  40:             long length = sResponse.Length - iEnd;
  41:             byte[] contents = new byte[length];
  42:             Array.Copy(sResponse, iEnd, contents, 0, length);
  43:  
  44:  
  45:         // Now write the actual file's byte array to the local file specified
  46:             using (StreamWriter sw = new StreamWriter(filepath, false))
  47:             {
  48:                 using (BinaryWriter binWriter = new BinaryWriter(sw.BaseStream))
  49:                 {
  50:                     binWriter.Write(contents);
  51:                     binWriter.Flush();
  52:                     sw.Flush();
  53:                     sw.Close();
  54:                 }
  55:             }
  56:             
  57:             Console.WriteLine("Created file: " + filepath);
  58:         }
  59:     }
  60: }

Hope this helps anyone else also having trouble to download the AutoCAD’s .dwt files.

 

Happy Coding…

Recently a customer came up with an issue and wanted to create a custom submit action button handler in CMS 2002 and send email to the editor/moderator. The requirement was that the user should be able to select which editor or moderator the email needs to be sent. The customer was following the article available at http://msdn2.microsoft.com/en-us/library/ms964282.aspx

 

But the code given in the article does not do Posting.Submit() nor does it handles the PostBack events in JavaScript. It just uses the redirect which do not solve our purpose. Researched a little and dig deeper into the MCMS belly and created the following code to achieve customer’s requirements.

 

Just compile this code into a Control and replace the CMSConsole:SubmitAction (Microsoft.ContentManagement.WebControls.ConsoleControls.SubmitAction) from the ConsoleContext.ascx with the below given SubmitAction control and you are all set to go !

   1: using System;
   2: using System.Data;
   3: using System.Configuration;
   4: using System.Web.UI;
   5: using System.Web.UI.WebControls;
   6: using System.Web.Mail;
   7: using Microsoft.ContentManagement.Publishing;
   8: using Microsoft.ContentManagement.WebControls;
   9: using Microsoft.ContentManagement.WebControls.ConsoleControls;
  10:  
  11: namespace MySubmitEmail
  12: {
  13:     /// <summary>
  14:     /// Summary description for MSSubmitEmail
  15:     /// </summary>
  16:     public class MSSubmitEmail : SubmitAction
  17:     {
  18:         protected override void PerformActionBehavior()
  19:         {
  20:  
  21:             string sEmail = this.Page.Request.QueryString.Get("email").ToString();
  22:  
  23:             if(CmsHttpContext.Current.Mode != PublishingMode.Update)
  24:             {
  25:                 if(this.Page.Request.QueryString.Get("email") == null)
  26:                 {
  27:                     this.Page.Response.Redirect(CmsHttpContext.Current.Posting.UrlModeUpdate + "&email=someid@email.local",true);
  28:                 }
  29:                 else
  30:                 {
  31:                     this.Page.Response.Redirect(CmsHttpContext.Current.Posting.UrlModeUpdate,true);
  32:                 }
  33:             }
  34:  
  35:             CmsHttpContext.Current.Posting.Submit();
  36:             CmsHttpContext.Current.CommitAll();
  37:             base.PerformActionBehavior();
  38:             
  39:             this.Page.Response.Redirect(CmsHttpContext.Current.Posting.UrlModeUnpublished,true);
  40:         }
  41:  
  42:         public override string Text
  43:         {
  44:             get
  45:             {
  46:                 return "Submit with email";
  47:             }
  48:             set
  49:             {
  50:                 base.Text = value;
  51:             }
  52:         }
  53:  
  54:         public override string ActionJavascript
  55:         {
  56:             get
  57:             {
  58:                 string sJavaScript;
  59:                 string sURL = "";
  60:  
  61:                 sJavaScript = "var sDLGResult;"
  62:                     + "sDLGResult = window.showModalDialog"
  63:                     + "('/select.html'," + "'','scroll:no;dialogHeight:250px;"
  64:                     + "dialogWidth:500px;edge:sunken;"
  65:                     + "center:Yes;help:No;resizable:No;"
  66:                     + "status:No;');"
  67:                     + "if (sDLGResult!='Cancelled')"
  68:                     + "{ alert(sDLGResult); ";
  69:  
  70:                 sURL = this.UrlPostback;
  71:  
  72:                 if(this.Page.Request.QueryString.Get("email") == null)
  73:                 {
  74:                     if(! (this.UrlPostback.ToString().IndexOf("&email=") > 0) )
  75:                         sURL += "&email=";
  76:  
  77:                     sJavaScript += "CMS_preparePostbackUrl('" + sURL + "' + sDLGResult);" + this.Page.GetPostBackEventReference(this, "");
  78:                 }
  79:                 else
  80:                 {
  81:                     sJavaScript += "CMS_preparePostbackUrl('" + sURL + "');" + this.Page.GetPostBackEventReference(this, "");
  82:                 }
  83:  
  84:                 //We dont need to do the navigate, we need a postback
  85:                 //sJavaScript += "window.navigate(window.location + '&chan=' + sDLGResult);"
  86:  
  87:                 sJavaScript += "}";
  88:                 return sJavaScript;
  89:             }
  90:         }
  91:     }
  92: }

 

Happy Coding…

Sometime back a customer came up with an issue. He has written a code in FeatureActivated event handler to add an entry into Web.Config file by using SPWebConfigModification API. The same entry was being removed in FeatureDeactivating event handler.

The code is working perfectly fine in the test machine but failed to add or remove then entry when used in their QA environment. Now this QA environment was having 4 WFE’s and SQL server on different box. Rest of the SharePoint configuration was pretty much the same as test machine. But the test environment was a single machine environment, with everything on a single machine.

The code being tested was:

   1: public void AddEntry
   2: { 
   3:             SPSite siteCollection = new SPSite("http://MOSSServer/");
   4:             SPWebApplication webApp = siteCollection.WebApplication;
   5:             
   6:             string value = @"<role DisplayName='MyWeb Financial Controllers' Role='MyWeb Financial Controllers'/>";
   7:             string strName = "role[@DisplayName='MyWeb Financial Controllers']";
   8:  
   9:             SPWebConfigModification modification = new SPWebConfigModification(strName, "configuration/PortalRole");
  10:  
  11:             modification.Value = value;
  12:             modification.Owner = "ExampleOwner";
  13:             modification.Sequence = 0;
  14:             modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
  15:  
  16:             webApp.WebConfigModifications.Add(modification); 
  17:  
  18:             webApp.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
  19: }

After trying for sometime and debugging, found that there seems to be an issue with getting to the Farm instance using SPWebApplication object. So instead of using SPWebApplication.Farm, we used SPWebService.ContentService to add the changes and call its ApplyWebConfigModifications method to push the changes to the Web.Config file.

The code was updated to:

   1: public void AddEntry
   2: { 
   3:                 string value = @"<role DisplayName='MyWeb Financial Controllers' Role='MyWeb Financial Controllers'/>";
   4:                 string strName = "role[@DisplayName='MyWeb Financial Controllers']"; 
   5:  
   6:                 SPWebConfigModification modification = new SPWebConfigModification(strName, "configuration/PortalRole");
   7:  
   8:                 modification.Value = value;
   9:                 modification.Owner = "ExampleOwner";
  10:                 modification.Sequence = 0;
  11:                 modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
  12:  
  13:                 SPWebService.ContentService.WebConfigModifications.Add(modification);
  14:                 SPWebService.ContentService.Update();
  15:                 SPWebService.ContentService.ApplyWebConfigModifications();
  16: }

Instead of adding modifications to WebConfigModifcations of SPWebApplication object, we are using SPWebService.ContentService to call ADD and UPDATE methods. Whenever required, it is always advised to use SPWebService.ContentService to make the modifications rather than accessing Farm instance through SPWebApplication.

Happy Coding…

A customer came up with an issue. WSRPConsumerWebPart’s userContextKey is always sending user’s display name when communicating with the producer. They were having a requirement that the userContextKey have to be a unique value to identify the user.

Upon a little digging found that when you are using the WSRPConsumerWebPart, the userContextKey have 2 options to pass to the WSRP Provider Service.

  • Anonymous
  • Current User Name

When option 2 is used, it always passes the Display Name of the user as in the SharePoint user profile (SPUser.Name). The SharePoint user profile is usually populated from the AD fields. The Display Name is not guaranteed to be Unique for the list of users unlike SPUser.LoginName which is unique in AD.

 

Why does this happen?

When using "Current User Name" option is selected in the WSRPConsumerWebPart, SharePoint hard codes it to SPWeb.CurrentUser.Name property which effectively uses SPUser.Name property, which in turn returns the user's display name and NOT the actual user ID or samAccount information of the user.

The DisplayName can be duplicate in the the AD and is not guaranteed to be unique.


As this is the behavior of WSRPConsumerWebPart and SPUser.Name is hard coded in to the WSRPConsumerWebPart to be returned for userContextKey, only the workarounds can be applied.

 

Workaround 1

You can update the display name field in the AD properties of the user to something unique and re-import the profiles into SharePoint
 

Workaround 2

Use the code to update the SPUser properties in SharePoint. You can get more details on the SPUser.Name property at http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spuser.name.aspx. This article also contains an example of how to modify/update SPUser object.
 

Workaround 3

You can edit user profile property (Name) in the SSP (Central Administration –> Shared Services Administration –> User Profile and Properties –> View Profile Properties –> Edit User Profile Property (Name))

By default "Name” property is mapped to displayName property of AD.

You can map it to an AD property which is Unique in the AD like mail which contains the Email address of the user. Import the profiles again and your Name, property will be a unique field and can be used in WSRPConsumerWebPart’s userContextKey.

 

Hope this helps someone out banging head for unique value of userContextKey.

 

Happy Coding

A team mate of mine was stuck with a curious issue. She was using SharePoint’s in-built Approval workflow but was using a custom ASP.net application to talk to a custom webservice hosted inside SharePoint server. That particular web service was having a method to complete the Approval workflow’s task using SPWorkflowTask.AlterTask method.

Here is the code snippet that was being used to update the task from within the web method.

   1: SPList timesheets = web.Lists["SampleList"];
   2: string listID = "";
   3: SPListItem item = timesheets.Items[0];
   4:  
   5: SPWorkflowTask taskedit = null;
   6: SPWorkflowTask task = item.Tasks[0];        
   7: taskedit = task;
   8:  
   9: if (taskedit == null)   // no matching task
  10:     return;
  11:  
  12: // alter the task
  13: Hashtable ht = new Hashtable();
  14: ht["Status"] = "Complete";
  15: ht["PercentComplete"] = 1.0f;
  16:  
  17: SPWorkflowTask.AlterTask((taskedit as SPListItem), ht, true);

 

Now no matter how it was being updated, the workflow task was being updated but the workflow itself was stuck in “In Progress” state and never completes.

This was a curious thing as I had earlier also worked with similar issue but with a custom workflow created for SharePoint using Visual Studio. I had a sample code which was working with it and the customer workflow was getting completed.

Now what was different here?

After some troubleshooting and debugging, found that Approval workflow requires a property “TaskStatus” to be set in the hashtable being passed to SPWorkflowTask.AlterTask method. The valid values for “TaskStatus” property are:

 

ht["TaskStatus"] = "#";   // This would mean that the task has been Approved
ht["TaskStatus"] = "@";   // This would mean that the task has been Rejected

 

So finally the code in the web method was updated to update the TaskStatus property as below:

   1: SPList timesheets = web.Lists["SampleList"];
   2: string listID = "";
   3: SPListItem item = timesheets.Items[0];
   4:  
   5: SPWorkflowTask taskedit = null;
   6: SPWorkflowTask task = item.Tasks[0];        
   7: taskedit = task;
   8:  
   9: if (taskedit == null)   // no matching task
  10:     return;
  11:  
  12: // alter the task
  13: Hashtable ht = new Hashtable();
  14: ht["TaskStatus"] = "#";    // Mark the entry as approved
  15:  
  16: SPWorkflowTask.AlterTask((taskedit as SPListItem), ht, true);

 

After this update, the tasks where updated properly and workflow completes as expected.

 

As always… Happy Coding

I have seen this error on various forums for quite sometime and wondered what is that SharePoint is doing around this?

It usually happens when you add Asyc="True" attribute to a SharePoint page and try to run the page, you get Parser error "The async attribute on the page directive is not allowed in this page."

Upon further debugging and researching, found that SharePoint’s SPPageParserFilter class blocks some page directive attributes. Checked the details of the SPPageParserFilter and found that the following are the only Page attributes which allowed by SharePoint Page Parser. If you use any other attribute which is not in the list given below, SharePoint will throw exception that "The 'xxx' attribute on the page directive is not allowed in this page."

  1. autoeventwireup
  2. buffer
  3. classname
  4. codebehind
  5. codepage
  6. compileroptions
  7. contenttype
  8. culture
  9. debug
  10. description
  11. enabletheming
  12. enableviewstate
  13. enableviewstatemac
  14. errorpage
  15. explicit
  16. inherits
  17. language
  18. lcid
  19. lineparagmas
  20. maintainscrollpositiononpostback
  21. masterpagefile
  22. responsencoding
  23. smartnavigation
  24. strict
  25. stylesheettheme
  26. targetschema
  27. theme
  28. title
  29. clienttarget
  30. uiculture
  31. warninglevel

Update: The highlighted 3 new entries were found in the Microsoft.SharePoint.dll for build number 12.0.6504.5000, bring the total count to 31.

As always… Happy Coding

I was working with one of my colleague on an issue. It was a weird one, where customer was using SPWebConfigModification class to add the safe control entry into the web.config. The sample code being used was adapted from the SPWebConfigModification page on MSDN.

   1: static void Main(string[] args)
   2: {
   3:  
   4:     SPSite oSite = new SPSite("http://manpreet2");
   5:     SPWebApplication _webApp = oSite.WebApplication;
   6:     string strName = "SafeControl[@Assembly=\"MyCompany.Name.SamplePart\"]";
   7:  
   8:     SPWebConfigModification SafeControl = new SPWebConfigModification(strName, "configuration/SharePoint/SafeControls");
   9:     SafeControl.Owner = "OwnerName";
  10:     SafeControl.Sequence = 0;
  11:     SafeControl.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
  12:     SafeControl.Value = "<SafeControl Assembly=\"MyCompany.Name.SamplePart, Version=1.0.0.0, Culture=neutral, PublicKeyToken=cb3c72729d0875cd\" Namespace=\"MyCompany.Name\" TypeName=\"*\" Safe=\"True\" />";
  13:  
  14:     if (args[0] == "/add")
  15:     {
  16:         Console.WriteLine("Adding...");
  17:         _webApp.WebConfigModifications.Add(SafeControl);
  18:     }
  19:     else
  20:     {
  21:         Console.WriteLine("Removing...");
  22:         _webApp.WebConfigModifications.Remove(SafeControl);
  23:     }
  24:     _webApp.WebService.ApplyWebConfigModifications();
  25:  
  26:     Console.WriteLine("Done...");
  27:     Console.ReadLine();

Surprisingly when we were using this code to add the web.config entry, it was adding it properly but when we were trying to remove it, through web.config file was being modified but entry was not being removed.

This was really surprising, if we took the exact code from the MSDN page, it worked perfectly fine.

Then started the journey to find the root cause for this.

Digging into my older emails, I remember that something similar we had done earlier also but how it was resolved, was not known. Found that old email and check that code, that also worked perfectly fine.

That brought us to line by line comparison of the code and see what is happening and we found the culprit code!

In the following code at line 6, we are giving the assembly name with only the first part of the 4 part name of assembly.

   6: string strName = "SafeControl[@Assembly=\"MyCompany.Name.SamplePart\"]";

But the entry for the SafeControl was done with full 4 part assembly name at line 12.

   12: SafeControl.Value = "<SafeControl Assembly=\"MyCompany.Name.SamplePart, Version=1.0.0.0, Culture=neutral, PublicKeyToken=cb3c72729d0875cd\" Namespace=\"MyCompany.Name\" TypeName=\"*\" Safe=\"True\" />";

So when adding, it added the entry but when removing, SharePoint is not able to find any entry with only first part of the 4 part assembly name. That is why, it is not able to remove it.

Why the code at MSDN page is working fine?

It is because in SPWebConfigModification.Value and SPWebConfigModification.Name, both contain only the assembly name and NOT the full 4 part assembly name.

 

Finally 1 more issue resolved and got to understand to look at the code more closely and not to miss the obvious.

 

As always… Happy Coding

 

-Manpreet

I was working with custom email event handler in SharePoint using SPEmailEventReceiver and found that when using the default functionality of I am able to configure and get the attachments to be added to the document library. Here is how the Incoming Email settings look like when you have your SharePoint server configured to receive emails and you enable your document library to receive emails.

image

I took the exact code that was provided in the MSDN documentation of SPEmailEventReceiver and implemented in my SharePoint site.

   1: namespace Example_Namespace
   2: {
   3:     public class Email_Handler: SPEmailEventReceiver
   4:     {
   5:         public override void EmailReceived(
   6:             SPList oList,
   7:             SPEmailMessage oMessage,
   8:             string strReceiverData)
   9:         {
  10:             SPListItem oListItem = oList.Items.Add();
  11:             oListItem["Title"] = oMessage.Headers["Subject"];
  12:             oListItem["Body"] = oMessage.HtmlBody;
  13:             oListItem.Update();
  14:         }
  15:     }
  16: }

Now when I go to Incoming Email settings page for my Document library, here is what I see.

image 

Where have all the settings gone? 

Also when using the sample code from the MSDN article, I find that the attachments I am sending along with the email are not being captured in the document library. As my class is inheriting from SPEmailEventReceiver, my initial thought got me to that if I call base.EmailReceived  in my event handler, then SharePoint should be able to handle the attachments and permission checks etc. But no, even calling base.EmailReceived did not resolve the problem.

This had me dive into checking what SharePoint does with the default settings and when I do not have my custom email event handler.

After some digging, found that SharePoint implement its own internal Email handler class which takes care of checking permissions, checking email settings for document library and adding the actual items to the list/library. EmailReceived function for SPEmailEventReceiver is a blank method with no definition. That is why calling "base.EmailReceived" method from class (inheriting from SPEmailEventReceiver) will have no effect.

Which lead to me investigating SPEmailMessage class (object of which is passed to EmailReceived method of your customer email event receiver). It has got Attachments property which contain all the attachments that are there in the email. Now things seems to be more in perspective and finally wrote the code to dump the attachments in the document library.

   1: public override void EmailReceived( SPList list, SPEmailMessage message, string receiverData)
   2: {
   3:       // Your rest of code to handle the different aspects of the email message received
   4:       // goes here. This would include permission check, duplicate items etc.
   5:       // After that, we will call the code to dump all the attachments in the root folder
   6:       foreach (SPEmailAttachment attachment in message.Attachments)
   7:       {
   8:             byte[] attachmentArray = new byte[attachment.ContentStream.Length];
   9:             attachment.ContentStream.Read(attachmentArray, 0, (int)attachment.ContentStream.Length);
  10:             list.RootFolder.Files.Add(attachment.FileName, attachmentArray);
  11:       }
  12: }

Please note sample does not provide any functionality to check list settings for user permissions, duplicate files or saving the original email. This all this done by the default email handler for SharePoint. If you are planning to implement your own EmailHandler, you will need to take care of all the default functionality also provided by SharePoint.

Happy Coding…Happy Coding

-Manpreet

Recently while working with a customer, got a requirement which got me digging into the emails generated by SharePoint alerts.

When you setup alert in a SharePoint document library and get an alert email, you get an option to connect or subscribe to that particular Document Library from within Outlook.

image

Now customer wanted to have the alert emails sent, but did not wanted to have “Connect to this Document Library” connection button within the Outlook client.

As it is just like any other email and research done during a previous issue on SharePoint alert emails,  started by looking at the email headers being sent by SharePoint and started playing by removing them 1 by 1. This started crashing Outlook client whenever an email with modified header comes in.

Sometimes the email would come but I would not be able to open the alert email and just show a dialog “Cannot open this item”.

image

After playing around for quite sometime with different headers and crashing Outlook a number of times, found out that the Outlook connection to the SharePoint happens due to the Content-Class header. If Content-Class header exists and has got value of "sharing", then only Outlook with parse other "x-" headers and show you the "Connect to this Document Library" button in Outlook client.

image

If you delete the Content-Class header from the collection or set it to something else than "sharing", Outlook client safely assumes it to be a normal alert email and still show it as an Alert email but does not show you the “Connect to this Document library” button any more.

image

You can modify the headers of the alert emails by creating your own Alert notification handler by implementing IAlertNotificationHandler interface. You can follow the KB 948321 for instructions on how to write and implement a custom alert notification handler.

SmileTill then Happy Coding…

I am back after a long time and will now try to be a little more regular with my posts.

Recently we worked upon a curious problem. We were using UserProfileManager in a web part and wanted to get to the private properties of a user profile. Now as the web part would be available on the SharePoint site and not every user would have permissions to view the private properties of the profile, we decided to use SPSecurity.RunWithElevatedPrivileges. To get the private properties of a user profile, we need to use the following overloaded constructor of the UserProfileManager class:

UserProfileManager (ServerContext, Boolean)

The second “Boolean” parameter specifies whether to ignore user profile privacy policies. Now to set this parameter to “True” and see the private properties of the profile, the calling user should have explicit permissions as defined in the Central Administration site under SSP –> “User Profiles and My Sites” –> “Personalization services permissions”

image 

You have to make sure that the user have “Manage User Profiles” permission.

image

 

Now my concern was, when I am using RunWithElevatedPrivileges and already running with System Account rights, why I was getting error:

User must have manage user profiles administrator rights to use administrator mode

Funny enough, when I was calling the User Profile Web Service (_vti_bin/userprofileservice.asmx), the same code was working perfectly fine and no error cropped up.

This lead me to dig deeper into the belly of the beast to see what is going under the hoods. After tracing back the calls and digging into the code, finally discovered that SharePoint was using HttpContext.Current.User.Identity which gives the User security for the current HTTP request (more details in http://msdn.microsoft.com/en-us/library/aa480475.aspx) instead of calling System.Security.Principal.WindowsIdentity.GetCurrent() which would actually represent the impersonated user.

Now comes, why it is working when we are using the User Profile WebService call?

As we were already initiating the the web service request using the impersonated identity of Application Pool, the ASP.net request was being started by App Pool identity, which in-turn had the full permissions. That’s the reason why the web service calls works while the Object Model call from a Windows Application failed.

So in effect, you cannot impersonate a user and try to get the private properties of a user profile. You would need to give explicit permissions to every user who wants to access the private properties of the User Profiles stored in SharePoint.

OR

Use SharePoint’s UserProfile web service call within the impersonated context to get the data.

Hope that helps out some of you…

Happy Coding… Happy Coding

When the web application is created and content source crawled, it gets listed in the the crawl logs of search. But when this application is deleted, the logs still contain reference to the deleted web application.

Doing further incremental or full crawls does not remove the stale entries from the crawl logs. Only way it happens, is to reset all the crawled content and start a full crawl. But this would have a huge impact where the amount of data to the crawled is huge.

We can access the crawl logs using the Object Model classes available in Microsoft.Office.Server.Search.Administration namespace, located in the Microsoft.Office.Server.Search.dll

Crawl logs are purged automatically after 7 days (default value). According to http://msdn.microsoft.com/en-us/library/ms553190.aspx


You use the
LogViewer object to retrieve the crawl log data. The MaxDaysCrawlLogged property of the LogViewer object allows you to set the maximum numbers of days for the crawl log to keep data.

Though the article says that you can "set" the value of MaxDaysCrawlLogged to control the maximum number of days for the crawl log to keep the data, but seems like setting it does not have any effect. After checking with various resources and SharePoint internals, found that the property is available but is not working or does not have any effect on the crawl log. It is a dummy property which does not do anything.

This effectively means that you cannot make any changes to the max days a crawl log keeps the data and it remains to the default of 7 days only.

Happy Coding... 

[In nutshell, how to display a Page Viewer Web Part with 100% height of the available screen estate.]

A co-worker of mine was working with a customer and he was trying to put a page viewer web part in a Web Part page in MOSS to display his web site. On the whole of page, only 1 Page Viewer web part was there but was still sticking to it's default zone height and not expanding to 100% of height available.

I tried playing around with the settings, but default Page Viewer Web Part can only have absolute "height" parameter and cannot take height in percentage (I wanted to give 100% to make sure it scales to small and large screen sizes).

Then came in the tried and trusted HTML DOM. I took hold of my Developer Toolbar in IE and started digging into the DOM of the page and trying to figure out what all properties can be played along. Tried just changing the height of IFrame which renders the "outside" page, but no luck. Through hit 'n trial found that all the Table tags in the hierarchy need to be changed to 100% height. So came up with the JScript code to do it automatically every time the page is loaded. The JScript code below was added to a Content Editor web part.

The steps:

  1. Create a new Web Part page with "Full Page, Vertical" layout template.
    image
  2. Drop a Page Viewer Web Part on the page and in the settings, choose Web Page and in the Link, type in the external URL e.g. http://www.microsoft.com
    image
    Hit OK to let the Page Viewer Web Part load the URL.
  3. As you can see the Page Viewer Web Part loads the page in a restricted area in the screen although we have additional height available.
    image
  4. Drop a Content Editor Web Part just below the Page Viewer Web Part:
    image
  5. In the tool pane for Content Editor Web Part, click on Source Editor as we would write the JScript code in the source editor.
  6. Paste the following JScript code in the Source Editor and click on OK to close the Source Editor.
  7. <script language="javascript">
    
       1:  
       2:             var a = MSO_ContentTable.getElementsByTagName("table");
       3:             
       4:             MSO_ContentTable.setAttribute("height","100%");
       5:             
       6:             for(i=0;i<a.length;i++)
       7:             {
       8:                 
       9:                 a[i].setAttribute("height","100%");
      10:             }
      11:  
      12:             var z = document.getElementsByTagName("div");
      13:             
      14:  
      15:             for(i=0;i<z.length;i++)
      16:             {
      17:                 if(z[i].id.indexOf("WebPartWPQ",0) >=0 )
      18:                 {
      19:                     if(z[i].hasChildNodes())
      20:                     {
      21:                         if(z[i].firstChild.nodeName.toUpperCase() == "IFRAME")
      22:                         {
      23:                             z[i].firstChild.setAttribute("height","100%");
      24:                         }   
      25:                     }
      26:                 }
      27:             }
    </script>

  8. Click OK again to close the Content Editor Web Part's tool pane.
  9. Click on "Exit Edit Mode" link to see the actual effect of the JScript code.
    Now you should be able to see the Page Viewer Web Part taking the full available screen height:
    image

You can actually mark Content Editor Web Part as "Hideen" (Tool Pane -> Layout -> Hidden) and it would not show up on your page but still execute the JScript code and get your Page Viewer Web Part as a full height control.

Happy Coding... Smile

-Manpreet

Recently a customer was following the KB 948321 and customizing the Alert notification emails being sent. He was just modifying the HTML part of it and rest using the same code. But when the emails were being received in the Outlook, they were not being recognized as Alert emails.

A regular alert email from SharePoint would look like this:

image

And would have the "Bell" icon and the email as such would have "Create Rule" button at the top.

image

But if you copy the exact code from the KB 948321, the email sent would look just like a normal email sent from a user.

Why does this happen?

Digging through the emails sent using the custom IAlertNotificationHandler and the emails sent by out-of-box SharePoint alerts, found that SharePoint adds a few headers to the email message like:

  • X-AlertTitle
  • X-AlertId
  • X-AlertWebUrl
  • X-AlertServerType
  • X-AlertWebSoap
  • X-Mailer

Debugging the custom IAlertNotificationHandler code, found that these headers are being set in SPAlertHandlerParams.headers collection. But in the sample code given, only "To" header is being pushed on to the SPUtility.SendEmail method.

SPUtility.SendEmail(web, true, false, ahp.headers["to"].ToString(), subject, build);

Set a break point and did a quick watch on ahp.headers and found:

image

So everything is already there in the headers, then why to pass just 1 header value to the SendEmail method. Checked the over loaded methods and found 1 which allows you to send the full collection of headers and replaced my code with:

SPUtility.SendEmail(web, ahp.headers, build);

Now the alert email was being displayed as expected and fully connected with SharePoint list happy

More Posts Next page »
 
Page view tracker