Sue Hernandez's Blog

January 24, 2014

Redesign your coded InfoPath 2010 Form to handle 1000′s of records

Filed under: InfoPath, SharePoint 2010, SQL — Tags: — Susan Hernandez @ 7:17 pm

Scenario

Let’s say you have an InfoPath form with Code Behind where you want to have the user paste in thousands of Job Codes or Organization Codes or Cost Codes into a text box, and then they would press a button and it would validate those records – the Codes – against a System of Record (like a list in SharePoint) and then add the records into a Repeating Table in your InfoPath form.  You want to do this because your Codes have metadata associated with them that you either want to display and/or have the user update, and you want to have conditional formatting and buttons on each record to make it look and function the way you want it to.

You will quickly find that doing this in a Web Browser-enabled form is near to impossible, requires too many configuration settings to change in various places, and performs really sluggishly any time you take an action on the form like clicking a checkbox or pressing a button that only runs rules.  You’ll run into InfoPath settings in Central Admin that you have to configure, and you’ll run in to the dreaded 6-minute default timeout of IIS pages waiting for responses from the server.

The first step I took, on the advice of Doug my co-worker, Architect extraordinaire, was to offload all of the individual item processing – the validation against the SOR – off to a custom SOAP web service.  This did the heavy lifting – I passed in a comma-separated string of all of the Codes (which is why it had to be a SOAP service and not a REST service – too much data to pass in a URL), and then it validated the codes.  Finally, I had the web service create an XML Document with exactly the structure of my repeating table in InfoPath, fill it in with all of the Codes and their metadata, and pass that Outer XML as a string back to the form.  I used XPathNavigator to then insert the XML directly into the form.

What I found, through constructing a timing debug string, was that my Web Service call was taking 2 – 4 seconds.  Inserting the XML and waiting for the XML to render into HTML was taking 4 – 10 minutes, depending on how many codes I had.  It was completely unacceptable as I had clients who needed to put in 2000 codes.  My form got sluggish at 300 and outright unusable over 700.

Solution

So I went back to Doug, which is what I usually do when I’m stuck on a problem.  I explained where the bottleneck was and confirmed my suspicions by reading up on Performance and InfoPath on a series of 6 Microsoft blogs.  Doug said, since the bottleneck is the size of the XML in the form, you have to get the XML out of the form, into some other storage mechanism.  Allow the form to hold no more than 100 items at a time in the repeating table, and implement Paging of sorts to get at the other 1900 records.

Wow, sounded like a great idea so I tried it, and so far it seems to be working.  Because I’m getting paid to do this, I can’t give you all of the code.  However, here’s the general steps you will need to take in order to accomplish this idea:

  1. Create a SOAP Web Service.  I had done this before and put it in ISAPI, but you have to do some extra manual work to get it to work there.  I found that if you add it directly to a subfolder under the Layouts directory, you can call it from there without any extra work.  The Web Service will need to have 4 methods or so:
    1. AddSQLDataGetPageXML(string identifier, int pageNum, string codes, string extraMetadataIfNecessary, string xmlChanges, string ns)
      This method is responsible for

      1. (a) getting existing data out of SQL for that identifier, or creating a new identifier if none exists;
      2. (b) copying the existing data, if any, into a  Generic List of custom objects representing your Code and its metadata;
      3. (c) processing  any changes that the customer has made to the repeating table of the 100 or so items that are showing (you need to capture your changes before blowing away and recreating the 100 items);
      4. (d) doing the actual Validation of the Code against your SOR;
      5. (e) adding the valid (and Invalid) codes to both the Generic List, as well as adding the record to the SQL table;
      6. (f) sorting your Generic List by RowNum or some kind of numbering identifier that you have added to the custom object;
      7. (g) iterating through the entire Generic List, gathering statistics of things like how many are invalid, how many total, etc., and taking only the ones in “that page” and turning them into an XML Element that you insert into a generic XmlDocument object that you created at the top of the method (using the ns parameter which is the namespace of the “my” prefix of your InfoPath form);
      8. (h) and finally returning a custom Return Object that holds both the statistics that you wanted to capture, as well as the XML which is the OuterXML of the xml document.
    2. GetPageXML(string identifier, int pageNum, string xmlChanges, int oldPageNum, string ns)
      This method is responsible for

      1. (a) getting existing data out of SQL for that identifier;
      2. (b) copying the existing data, if any, into a  Generic List of custom objects representing your Code and its metadata;
      3. (c) processing  any changes that the customer has made to the repeating table of the 100 or so items that are showing (you need to capture your changes before blowing away and recreating the 100 items) – NOTE HERE – this has to handle deletions too, so you need some special manipulation of comparing what is (or rather is NOT) in the XML string of changes, versus what IS in your Generic List that you read out of SQL;
      4. (d) sorting your Generic List by RowNum or some kind of numbering identifier that you have added to the custom object;
      5. (e) iterating through the entire Generic List, gathering statistics of things like how many are invalid, how many total, etc., and taking only the ones in “that page” and turning them into an XML Element that you insert into a generic XmlDocument object that you created at the top of the method (using the ns parameter which is the namespace of the “my” prefix of your InfoPath form);
      6. (f) and finally returning a custom Return Object that holds both the statistics that you wanted to capture, as well as the XML which is the OuterXML of the xml document.
    3. ProcessChanges(string identifier, string xmlChanges, string ns, int startItem, int stopItem, SqlConnection conn, List<YourCustomCodeObject> allCodesFromSQL)
      This is the method that you call from the first 2 methods to process your existing changes.  Remember, if the user changes something on the current page, and then you switch to another page, you need to make sure that you capture what has been changed on the current page.
    4. ReValidateCode(string code, string otherMetadataHere)
      This method is responsible for

      1. (a) reading in the code and finding it / validating in against your SOR;
      2. (b) creating a custom Return Class that can hold your Code’s data and metadata, and whether or not it’s valid;
      3. (c) returning the custom Return Class (your InfoPath form handles the changing of the repeating table item in the repeating table)
  2. Hook this up to your InfoPath Form
    1. There’s too much involved to go into here, but you need to basically do the following:
      1. Have a SOAP web service Data Connection, and methods to set values into and Execute that connection.
      2. Have a text box to paste the Codes into, and a method to take those codes, wrap them up and send it to the SOAP web service
      3. Have a repeating grid with all of your fields, metadata fields, buttons (such as ReValidate), and conditional formatting
      4. Have buttons for Previous and Next that have conditional formatting on them to only show if it is valid to show them (i.e. don’t show Previous if you’re on page 1)
      5. Have a MoveToPage method that gets called from the buttons and optionally some other places (like a delete all on this page kind of method)
  3. Create your Workflow (a custom Console Application probably) that handles the business processes of the form
    It will do the following:

    1. Loads in the InfoPath form, which you have created a class for using the XSD of the InfoPath Form (Export to Source Files to find the XSD)
    2. Checks to see if it’s already processed
    3. Looks for the Identifier in the Form that got stored when you were using the form
    4. Looks in SQL using the Identifier to get all the records
    5. Processes the records
    6. Deletes the records out of SQL

 

November 22, 2013

InfoPath 2010 – Registering of form templates deployed through object model is not supported

Filed under: InfoPath, SharePoint 2010, Visual Studio 2010 — Tags: , , — Susan Hernandez @ 2:37 pm

In an earlier post, http://suehernandez.wordpress.com/2013/11/22/deploy-infopath-2010-with-code-as-solution-feature/, I explained how to implement an InfoPath 2010 form as a Feature in SharePoint 2010.

As I was trying to deploy this feature, I kept getting the error:

Registering of form templates deployed through object model is not supported

 

It turns out that what happened is that up until that point I had been deploying my InfoPath forms through Central Administration.  It’s easiest and usually quickest to do it this way (although once you deploy the same form like 50 times it starts getting really slow to deploy).

But I had not changed the “Identifier” per se of the form.  Let me show you a couple of pictures that shows what I’m talking about:

Form Template Properties buttonForm Template Properties dialog

Where it says “urn:schemas-microsofr-com….” that is the identifier of the form.  If you publish the form, and then copy it and change the name of it, it does NOT change the identifier.  So if you’ve already got that ID in Central Admin under Manage Form Templates, and then you try to deploy the same form as a feature, you get this error message.

One of the ways you can fix this is to either manually change this URN ID, or just open up the form in InfoPath Designer in design mode and do a Save-As.  It will automatically change your ID to match the new name of the form.

In the mean time, delete the form from Central Admin and try your Solution Deployment again.

Deploy InfoPath 2010 with Code as Solution-Feature

Filed under: InfoPath, SharePoint 2010, Visual Studio 2010 — Tags: , , — Susan Hernandez @ 2:23 pm

This one will be somewhat long of a post.   Here we will examine deploying an InfoPath 2010 form with CodeBehind as a custom Feature to SharePoint 2010.

  1. Change the properties of your form to Full Trust

    Full Trust

    Full Trust

  2. Publish your form to a SharePoint Server

    Publish Step 1

    Publish Step 1

  3. Choose your URL to the site where you’re going to be using the file.  Note that it will put this form in the Form Templates (url is FormServerTemplates) library at the top of your Site Collection

    Publish Step 2

    Publish Step 2

  4. Make sure that it is checked off to enable the form to be filled out by a browser, and choose Administrator Approved Template

    Publish Step 3

    Publish Step 3

  5. Choose the location on your desktop or local computer or shared drive where you want to save the Published form template to

    Publish Step 4

    Publish Step 4

  6. Create your Visual Studio solution.  Create it as a SharePoint 2010 Empty SharePoint Project

    Create new Empty SP Project

    Create new Empty SP Project

  7. Choose to deploy as Farm Solution

    Deploy as Farm Solution

    Deploy as Farm Solution

  8. Add a Feature to the project

    Add Feature

    Add Feature

  9. Rename the feature to something that makes more sense, so you can more easily find it later through PowerShell or in the 14 hive

    Rename Feature

    Rename Feature

  10. Open the feature in Visual Studio into Design view and set the properties of the feature at the top.

    Set Properties of Feature

    Set Properties of Feature

  11. Add a Module to the project

    Add Module

    Add Module

  12. Rename the module and remove the Sample.txt file

    Rename Module and remove Sample txt

    Rename Module and remove Sample txt

  13. Add your Module Files.  Here you will be adding the published form that you completed in Step 5 here, as well as the dll.  (You can get the .dll by going into design view in InfoPath, pressing Publish, and pressing Export Source Files – give it a folder to put the source files in, and find your .dll in there)

    Note that I have a little naming change here versus what I have in step 5.  I am showing you a “real” project file and had to blot out part of the name.  But this will be your published file – not the file you use to make the changes to the form.

    Add Module Files

    Add Module Files

  14. Open the Elements file in the NewFormModule.  You will have to make changes.  Here’s the file as it looks before:
    Elements Before

    Elements Before

    and here’s the file as it looks after:

    Elements After

    Elements After

  15. Open back up the Feature in Design mode, and press the button to switch it to Manifest mode.  Expand Edit Options and add the 2 properties shown below, with your Feature Name and your Published InfoPath form name as the values:

    Update the Manifest

    Update the Manifest

  16. Get the Properties window up for the Feature.  You will be adding a Receiver Assembly and a Receiver Class
    ReceiverAssembly=”Microsoft.Office.InfoPath.Server, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c”
    ReceiverClass=”Microsoft.Office.InfoPath.Server.Administration.XsnFeatureReceiver”

    Feature Props add Receiver info

    Feature Props add Receiver info

  17. OPTIONAL:  Add Activation dependency.  NOTE:  if you are going to mark your feature as Hidden=TRUE, you cannot add this dependency.  We will discuss later why you may want to make your feature Hidden.
    <ActivationDependency FeatureId=”C88C4FF1-DBF5-4649-AD9F-C6C426EBCBF5″/>

    Feature Activation Dependencies

    Feature Activation Dependencies

  18. Now in your solution explorer, you will want to highlight the Elements file and get the properties.  Clear out the Deployment Path and make sure it’s of type ElementManifest

    Elements Properties

    Elements Properties

  19. Next highlight the XSN published form and get its properties.  Clear the Deployment Path and make sure it is of type ElementFile

    Form Properties

    Form Properties

  20. Repeat the previous step for the DLL file
  21. Open up your Feature in Design mode just to look at it.  Your Manifest file should look similar to the following (if you added the Activation Dependency):

    Manifest After

    Manifest After

Making the feature Hidden

The problem with adding the published file to Central Administration – Managed Form Templates is twofold:  (1) It puts in every version you create into the manifest; and (2) It makes the feature available to EVERY site collection in your Farm.  Similarly, if you don’t make the feature Hidden, all site collections will be able to see this feature.

Depending on your project, you may opt to hide the feature so that no one can turn it on, and only activate it to the Site Collection using PowerShell.

If you choose to make your feature hidden, you cannot have the Activation Dependency of the Enterprise Features.  Just realize this means is that if you don’t have Enterprise Features turned on, this Content Type will not work.

Here’s a picture of where you’d make the feature hidden – it’s just another one of the properties on the feature:

Is Hidden is True

Is Hidden is True

Deploy the feature, Activate it, and Use it

Once you deploy the solution package (.WSP file) and activate it to the site collection you’re going to use it in, it will now be a Content Type that you add to your Form Library.  Any fields which you promoted in the Publishing process will automatically be added to your Form Library and will sync to the data in the form.

References

I got bits and pieces from a lot of places, but here’s some main ones that I got a lot of detail from:

October 11, 2013

SharePoint 2013, InfoPath and Claims – GetUserProfileByName

Filed under: InfoPath, SharePoint 2013 — Tags: , , — Susan Hernandez @ 8:59 pm

You would not believe the hoops you have to go through to get data auto-populated in an InfoPath Form if you’re using Claims-based authentication, which I believe is the default in SharePoint 2013.

The Scenario:

You have an on-premise SharePoint 2013 Server installation with InfoPath Forms Services (so you have enterprise) and you want to create an InfoPath form that auto-populates a logged in user’s ID, email, and phone number.  You are using Claims-based authentication in the web application in question.

In this scenario, I’m using InfoPath Designer 2010 – I haven’t upgraded to 2013 yet, but I don’t think it makes a difference.

The Problem:

When you use Claims-based authentication, your user name is prefaced by “0#.w|”.  So for example, if your user name is SuesDomain\jdoe then your Claims-based user token will be, without the quotes:     “0#.w|SuesDomain\jdoe”

InfoPath can’t handle that, or more specifically, the UserProfile.asmx method GetUserProfileByName method can’t handle that.  InfoPath tries to pass in your Claims-based user token instead of your domain\User Name.

You get 2 problems – you have an authentication problem where the currently logged in user is not allowed to hit the web service, so you get an Access Denied 401 authentication error.  The second problem is getting the right user from the web service.

The Solution:

The steps you have to take are listed here.  See the reference posts for more details and pictures.

  • Have your Active Directory team create you a User Name and Password for a “generic” InfoPath Account.  The one I created was called Domain\SP2013_IPRdr for “InfoPath Reader”.
  • Have the Central Administration team create a Secure Store service application, if they don’t already have one.
    • Generate the Secure Store key if you haven’t already.
    • Create a new application in the Secure Store called something like InfoPathGUPBN (“GetUserProfileByName” is what the acronym stands for); target application type is Group, and User Field Types are the Windows User Name and Password.
    • Set the credentials:  Whomever you want to administer the application should be set as the Target Application Administrators and for the Members, select All Users.
    • Set the Credentials to the Application and use the User Name and Password that you received from the A/D team, example Domain\SP2013_IPRdr.
  • In SharePoint 2013 – in the site you wish to publish your form to, create a Data Connection library.
  • Go Into InfoPath and go to your GetUserProfileByName secondary data connection.  If you don’t already have one, you can create one the “normal” way, making sure you do not choose a user name (just like you would normally do).  For an example of how to set one up, see the first reference in the section at the bottom of this post.
  • Modify the GetUserProfileByName secondary data connection so that it does NOT automatically load the data upon form load.  This is not required, but saves performance.
  • Convert the GetUserProfileByName secondary data connection into a UDCX file (“Convert to Connection File”) – store it in the Data Connection Library that you created a few steps ago.
  • Download the UDCX file that you just created from your SharePoint 2013 Data Connection library and put it on your desktop.  Edit it in Notepad.
  • Change the commented out line to read something like this (depending on what you called your Secure Store Application):
    <udc:Authentication><udc:SSO AppId='InfoPathGUPBN' CredentialType='NTLM' /></udc:Authentication>
  • Upload the UDCX file back in to the Data Connection Library.
  • Publish your newly changed InfoPath form into your SharePoint 2013 InfoPath Form Library
  • Make sure to give your special A/D account, for example the Domain\SP2013_IPRdr account, full read access to that site, so that they have permissions to hit the web service.

All of the above was just to get you to Authenticate properly with the Web Service.  If you were to stop now, and try to get the information from the web service you would get the information for the user Domain\SP2013_IPRdr.  Not the logged in user.  Now you have to take the following steps to get the real logged in user:

  • Go into InfoPath and go to create a Form Load rule
  • For the first rule, call it “Set User Profile” or something similar
    • Create an action to Set a Field’s Value
    • Choose the field – it’s the GetUserProfileByName secondary data connection, in the queryFields node, all the way in:  AccountName.
    • Set it to the following formula value:
      substring-after(userName(), "0#.w|")
    • Add another action, Query for Data, and choose your GetUserProfileByName secondary data connection
  • For the second rule, call it something like “Set Fields”
    • This is where you fill in your fields from the data source.  To find a good example of how to do this, see the first reference in the section at the bottom of this post.

Bottom line:  Pain in the rear.

References:

September 14, 2012

SharePoint 2010 custom Access Denied with Modal Dialog

Filed under: JQuery, SharePoint 2010, Visual Studio 2010 — Susan Hernandez @ 9:43 pm

There are lots of posts out there that explain to you how to link a custom Access Denied page to the system so that it displays your page instead of the OOB one.  Even a couple of posts on HOW to create that page.

In my case, I wanted to do this:

Custom Access Denied Page

I wanted to:

  • Use my own logo
  • Allow for signing in as a different user
  • List all of the Site Administrators for the site you’re trying to reach
  • Display a custom “Request Access” page in a Modal Dialog box

 This turned out to be a bit tricky.

Preparations

  • I used an application page in Visual Studio 2010, but instead of inheriting from LayoutsPageBase, I used UnsecuredLayoutsPageBase.  That way you don’t have to override the 4 methods to allow the user into the page.
  • I changed the master page reference to MasterPageFile=”~/_layouts/simple.master”

Use my own logo

I read that all you had to do was to override your logo in the PlaceHolderPageImage content placeholder.  So I dutifully put my image there, even calling it the same ID as the one it expected: <img id=”onetidtpweb1″ src=”/_layouts/images/Custom/Logo.png” alt=”Logo”/>.  However, that did not seem to work.  So I used JQuery to show my logo after-the-fact (it’s a workaround, not a true solution – don’t know why the placeholder didn’t work).

In PlaceHolderAdditionalPageHead, I had this:

<asp:Content ID=”Content2″ ContentPlaceHolderID=”PlaceHolderAdditionalPageHead” runat=”server”>
   <meta name=”Robots” content=”NOINDEX “/>
   <meta name=”SharePointError” content=”1″/>
   <script language=”javascript” src=”/_layouts/images/Custom/jquery-1.6.1.min.js” type=”text/javascript”></script>
   <script type=”text/javascript”>
            $(document).ready(function() {
                  $(‘div.s4-simple-iconcont > img’).attr(‘src’, ‘/_layouts/images/Custom/Logo.png’);
            });
   </script>
</asp:Content>

 Allow for signing in as a different user

Turns out that CloseConnection.aspx apparently silently redirects them to the Access Denied page?  Not sure exactly, but what I know is that if I hadn’t copied the code from SharePoint’s access denied page using Reflector, it wouldn’t sign me in as a different user.  So use reflector on AccessDenied, and copy CloseConnection(), Send403(), SendResponse(), IsBrowserRequest(), matchLegacyFrontPageUa(), HandleLoginAsAnotherUser(), and LogInAsAnotherUser() as well as some of thePage_Load code – the part that checks the query string and calls LogInAsAnotherUser or CloseConnection.

I also had to reconstruct the link that the user gets on the page.  I used the following code:

string urlForSignIn = web.ServerRelativeUrl;

if (!urlForSignIn.EndsWith(“/”))
{
      urlForSignIn += “/”;
}
urlForSignIn += “_layouts/closeConnection.aspx?loginasanotheruser=true”;
urlForSignIn = urlForSignIn.Replace(“/”, “\\u002f”);

lnkSignInAsDifferentUser.NavigateUrl = “javascript:LoginAsAnother(‘” + urlForSignIn + “‘, 1)”;

List all of the Site Administrators for the site you’re trying to reach

Immediately after the querystring inspection, create 2 variables to hold the Site Collection ID and the Web ID.  NOTE:  These are the only 2 things the code can get on these objects, if the user doesn’t have access to the site.  In other words, you can’t access SPWeb.Title – that throws the page off to a 403 Unauthorized page.  So you need to use SPSecurity.RunWithElevatedPrivileges and re-open your Site and Web objects securely, before you can get anything interesting.

When trying to get your admins, all I did was use foreach(SPUser in web.AllUsers) and then individually check web.DoesUserHavePermissions(user.LoginName, SPBasePermissions.FullMask).  Keep in mind:  you’re going to get service accounts and such when you loop through these users.  I basically ignored the service accounts I knew about, and limited the results to my Domain (i.e. to get rid of SHAREPOINT\SYSTEM).  I also did not take the account if it didn’t have an email address attached.

Display a custom “Request Access” page in a Modal Dialog box

So this one was wierd.  I had to use SP.UI.ModalDialog.showModalDialog(options) to pop up the Request Access window.  However, in order to call that function from your link, you have to include /_layouts/SP.UI.Dialog.js.  As it turns out, that in turn relies on a couple of other JS files.  But normally all you do is put the SP.UI.Dialog.js reference in a SharePoint:ScriptLink tag, and add a FormDigest tag as well, and then you can get to your modal dialog function.

However, it turns out BOTH the ScriptLink and the FormDigest tags actually throw the page into a 403 Forbidden state.  So I had to take those out.  But then the modal dialog didn’t work.  I tried to no avail to manually add a bunch of JS references to the page, but kept getting Script errors.

So I found an article about something else that led me to the solution: http://howtosharepoint.blogspot.com/2010/09/programatically-hide-show-status-bar.html.  Basically what he does is programmatically register the script links AFTER the UI is loaded, by adding this into the Page_Load function:

ScriptLink.RegisterScriptAfterUI(this.Page, “core.js”, true, false); 
ScriptLink.RegisterScriptAfterUI(this.Page, “CUI.js”, false, true); 
ScriptLink.RegisterScriptAfterUI(this.Page, “SP.js”, false, true);

This seemed to make the modal dialog errors go away, although periodically I am getting another script error I have to investigate.

Now keep in mind that the Request Access page has to also be based off of Simple.master.  So you’re going to get the same sections of the page, including the image, the header, and the “Go Back To Site” link at the bottom.  The good news is, though, that the Go Back to Site link just closes the modal dialog box.

And finally, when you’re done with your Request Access form and want to return to the Access Denied page, just add this to your code behind:

Page.ClientScript.RegisterClientScriptBlock(typeof(CustomRequestAccess), “closeDialogScript”, “SP.UI.ModalDialog.commonModalDialogClose(1, null);”, true);

 Cheers,

Sue

January 4, 2012

SharePoint 2007-2010 Web Part Migration Planning

Filed under: MOSS, SharePoint 2010, Visual Studio 2010 — Susan Hernandez @ 11:26 pm

Well, I know I need to post the rest of my “Gantt Chart” blogs, but let me side track for a minute…

We are migrating one of our clients from MOSS 2007 to SharePoint 2010 and we wanted to know what web parts are being used on what sites.  I found a multi-step process you’ll have to go through, and it will take a while, but you CAN get a 70%-90% list.

GET THE WEB PARTS PER WEB

The first step is to use the stsadm command “enumallwebs”.  This apparently has a parameter/switch called “includewebparts” which apparently was added during an Octbver 09 Cumulative Update.

stsadm -o enumallwebs -includewebparts > MyOutputFileName.txt

This will construct for you an XML file that looks approximately like below:

<Databases>
  <Database SiteCount="2" Name="your_sharepoint_content_database_name" DataSource="server">
    <Site Id="12345678-716d-44a3-a973-fffffffffff" OwnerLogin="domain\spadmin" InSiteMap="True">
      <Webs Count="2">
        <Web Id="12345678-17d9-4870-8d94-fffffffffff" Url="/sites/test1" LanguageId="1031"
             TemplateName="STS#0" TemplateId="1">
          <WebParts>
            <WebPart Id="e60f6c95-e86c-4717-2c0d-6d8563c9caf7" Count="1" Status="Missing" />
            <WebPart Id="293e8d0e-486f-e21e-40e3-75bfb77202de" Count="103" Status="Missing" />
            <WebPart Id="b9a7f972-708a-cd77-4ffd-a235dfed5c38" Count="1" Status="Missing" />
            <WebPart Id="2242cce6-491a-657a-c8ee-b10a2a993eda" Count="182" Status="Missing" />
          </WebParts>
        </Web>
        <Web Id="12345678-3bda-4109-aacd-fffffffffff" Url="/sites/test1/subweb1" LanguageId="1031"
             TemplateName="STS#1" TemplateId="1">
          <WebParts>
            <WebPart Id="ce9aa113-48cf-ddee-0c03-597445e5b7ab" Count="1" Status="Missing" />
            <WebPart Id="293e8d0e-486f-e21e-40e3-75bfb77202de" Count="9" Status="Missing" />
            <WebPart Id="2242cce6-491a-657a-c8ee-b10a2a993eda" Count="7" Status="Missing" />
          </WebParts>
        </Web>
      </Webs>
    </Site>
    <Site Id="12345678-730c-46fd-a114-fffffffffff" OwnerLogin="domain\spadmin" InSiteMap="True">
      <Webs Count="1">
        <Web Id="12345678-7cd6-447d-8107-fffffffffff" Url="/sites/test2" LanguageId="1031"
             TemplateName="STS#0" TemplateId="1">
          <WebParts>
            <WebPart Id="d55b3b6b-6281-707b-73d0-0c49581475ad" Count="1" Status="Missing" />
            <WebPart Id="293e8d0e-486f-e21e-40e3-75bfb77202de" Count="83" Status="Missing" />
            <WebPart Id="9f030319-fa14-b625-4892-89f6f9f9d58b" Count="1" Status="Missing" />
            <WebPart Id="c9b34b5d-bf06-dc91-d23e-94ecad31cd0a" Count="2" Status="Missing" />
            <WebPart Id="2242cce6-491a-657a-c8ee-b10a2a993eda" Count="83" Status="Missing" />
            <WebPart Id="669602d9-e116-ccb8-eea3-e37ad589b14b" Count="1" Status="Missing" />
            <WebPart Id="f5897322-ddd4-c990-d012-f9d4fe2180ad" Count="2" Status="Missing" />
          </WebParts>
        </Web>
      </Webs>
    </Site>
  </Database>
</Databases>

* You may have every web part in a “Missing” status – I think you need to run this command on a WFE that has ALL the features installed properly.  Please see one of the following posts for more information:

GET A LIST OF WEB PARTS AND THEIR ASSEMBLIES AND CLASSES

Well see that WebPart “ID” there?  That’s actually not an ID that you can use, even searching in the database, to find the name of the web part.  That is a made up GUID-that’s-not-a-GUID that just happens to “fit” inside a GUID.  More info in the next heading section.

For now, just go to your “Add a Web Part” page, or

https://yourserver.someurl.com/_layouts/newdwp.aspx

From here, you’re going to get a list which shows you 2 pieces of information you need: the full Class Name and the full Assembly Name. 

If you have Excel installed, you “should” be able to right-click inside the list and say “Export to Microsoft Excel”.  From Excel, go ahead and delete columns A and C (Blank and “File Name”).  Now we need to turn this file into an XML file so we can both crunch it through a program, as well as transform it using XSLT.  I personally had a hard time turning it into XML so what I did was I took this schema and saved it as .xml and opened it in Excel:

<WebPartTypeIDs>
   <WebPartTypeID>
      <AssemblyName>Assembly A</AssemblyName>
      <TypeName>Class A</TypeName>
      <GeneratedWebPartTypeID>GUID A</GeneratedWebPartTypeID>
   </WebPartTypeID>
   <WebPartTypeID>
      <AssemblyName>Assembly B</AssemblyName>
      <TypeName>Class B</TypeName>
      <GeneratedWebPartTypeID>GUID B</GeneratedWebPartTypeID>
   </WebPartTypeID>
</WebPartTypeIDs>

Then I copied and pasted everything from the Exported spreadsheet into this XML file.  I think you have to make sure that you have something in the GeneratedWebPartTypeID for at least one of the rows, in order to make sure it doesn’t get rid of that column when you save.

If you’re better at Excel (most will be) and you know an easier way, please let me know.

RUN THE OUTPUT THROUGH A PROGRAM

Next we have to get the generated ID.  Here’s the crazy part.  Here’s what Microsoft does:

  • Take the Assembly Name plus “|” plus the Class / Type Name and put it together in one string
  • Change the string to a Byte array
  • Run the Byte array through an MD5 Hash

So I wrote a little program to ingest in the xml file that we created through Excel.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
using System.Security.Cryptography;
using System.Xml;
using System.Xml.Xsl;
using System.IO;

namespace GetWebPartTypeIDs
{
 public partial class Form1 : Form
 {
  public Form1()
  {
   InitializeComponent();
  }

  private void button1_Click(object sender, EventArgs e)
  {
   try
   {
    Cursor = Cursors.WaitCursor;
    XmlDocument xDoc = new XmlDocument();
    xDoc.Load(textBox1.Text);

    XmlNodeList nodes = xDoc.SelectNodes("//WebPartTypeID");
    foreach (XmlNode node in nodes)
    {
     string assemblyName = node.SelectSingleNode("AssemblyName").InnerText;
     string typeName = node.SelectSingleNode("TypeName").InnerText;

     string s = GenerateID(assemblyName, typeName);

     XmlNode idNode = node.SelectSingleNode("GeneratedWebPartTypeID");
     idNode.InnerText = s;
    }

    xDoc.Save(textBox1.Text);
   }
   catch (Exception ex)
   {
    richTextBox1.Text = ex.ToString();
   }
   finally
   {
    Cursor = Cursors.Default;
    richTextBox1.AppendText(Environment.NewLine + Environment.NewLine + "*************** DONE **************");
   }
  }

  private string GenerateID(string assemblyName, string typeName)
  {
   string s = assemblyName + "|" + typeName;
   byte[] bytes = Encoding.Unicode.GetBytes(s);
   byte[] b = new MD5CryptoServiceProvider().ComputeHash(bytes);
   return new Guid(b).ToString();
  }
 }
}

ADD THAT GENERATED XML OUTPUT TOGETHER WITH THE STSADM OUTPUT

So now, put the 2 XML’s together, with some made-up root, doesn’t matter what it is.

<DatabasesAndWebPartTypeIDs>
   <WebPartTypeIDs>
      ...
   </WebPartTypeIDs>
   <Databases>
      ...
   </Databases>
</DatabasesAndWebPartTypeIDs>

RUN IT THROUGH XSLT

I am going to include 2 XSLT’s here.  The first one uses XSLT 2.0 so you might need to download the Saxon Parser (http://saxon.sourceforge.net/) if you don’t have XML Spy or if your file is too big for XML Spy to handle, which is what happened to me (250 MB xml file).

Here’s one that outputs an HTML file.  The nice thing about it is it goes “both ways” – it shows on the top which web parts are not being used and it shows on the bottom which web parts you couldn’t ID.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions">
 <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
 <xsl:template match="/">
   <html>
    <body>
     <h1>By Web Part Name - Unused are highlighted in Red</h1>
     <xsl:for-each select="//WebPartTypeID">
      <xsl:sort select="AssemblyName" />
      <xsl:sort select="TypeName" />
      <xsl:variable name="generatedId" select="GeneratedWebPartTypeID" />
      <xsl:variable name="count" select="count(//Databases//Web[WebParts/WebPart/@Id = $generatedId])" />
      <font face="Courier New" style="font-size:12px">
       <xsl:if test="number($count) = 0">
        <xsl:attribute name="color" select="'Red'" />
       </xsl:if>
       <xsl:value-of select="substring-before(AssemblyName, ',')" /> | <xsl:value-of select="TypeName" />:
<xsl:value-of select="$count" />
      </font><br/>
     </xsl:for-each>
     <hr/>
     <h1>By Web Part Type Id - Those with under 15 entries show the webs they're in</h1>
     <xsl:for-each-group select="//WebPart" group-by="@Id">
      <xsl:sort select="current-grouping-key()" />
      <xsl:variable name="count" select="count(//Web[WebParts/WebPart/@Id = current-grouping-key()])" />
      <font face="Courier New" style="font-size:12px">
       <xsl:value-of select="current-grouping-key()" />: <xsl:value-of select="$count" /> --&gt;
<font color="Red"><b><xsl:value-of
select="substring-before(//WebPartTypeID/AssemblyName[../GeneratedWebPartTypeID = current-grouping-key()], ',')"
 /></b> | <xsl:value-of
select="//WebPartTypeID/TypeName[../GeneratedWebPartTypeID = current-grouping-key()]" /></font>
      </font>
      <br/>
      <xsl:if test="number($count) &lt; 15">
       <xsl:for-each select="//Web/@Url[../WebParts/WebPart/@Id = current-grouping-key()]">
        <font face="Courier New" style="font-size:12px">..........<xsl:value-of select="." /></font><br/>
       </xsl:for-each>
      </xsl:if>
     </xsl:for-each-group>
    </body>
   </html>
 </xsl:template>
</xsl:stylesheet> 

The next XSLT is 1.0 compliant.  This one makes a “flat” xml file that you can then pull into Microsoft Access and do Pivot stuff on it to find out what web templates are being used, etc.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
 <xsl:template match="/">
  <ReportRoot>
   <xsl:for-each select="//Database">
    <xsl:variable name="db" select="@Name" />
    <xsl:for-each select="Site">
     <xsl:variable name="site" select="@Id" />
     <xsl:for-each select="Webs/Web">
      <xsl:variable name="webID" select="@Id" />
      <xsl:variable name="webURL" select="@Url" />
      <xsl:variable name="webTemplate" select="@TemplateName" />
      <xsl:variable name="webTemplateID" select="@TemplateId" />
      <xsl:for-each select="WebParts/WebPart">
       <xsl:variable name="webPartTypeID" select="@Id" />
       <xsl:variable name="nameNode" select="//WebPartTypeID[GeneratedWebPartTypeID = $webPartTypeID]" />
       <xsl:variable name="assembly" select="$nameNode/AssemblyName" />
       <xsl:variable name="class" select="$nameNode/TypeName" />
       <xsl:element name="WebPartType">
        <xsl:element name="WebPartTypeID">
         <xsl:value-of select="$webPartTypeID" />
        </xsl:element>
        <xsl:element name="AssemblyName">
         <xsl:value-of select="substring-before($assembly, ',')" />
        </xsl:element>
        <xsl:element name="FullAssemblyName">
         <xsl:value-of select="$assembly" />
        </xsl:element>       
        <xsl:element name="ClassName">
         <xsl:value-of select="$class" />
        </xsl:element>
        <xsl:element name="Database">
         <xsl:value-of select="$db" />
        </xsl:element>
        <xsl:element name="SiteCollection">
         <xsl:value-of select="$site" />
        </xsl:element>
        <xsl:element name="WebID">
         <xsl:value-of select="$webID" />
        </xsl:element>
        <xsl:element name="WebURL">
         <xsl:value-of select="$webURL" />
        </xsl:element>
        <xsl:element name="WebTemplateName">
         <xsl:value-of select="$webTemplate" />
        </xsl:element>
        <xsl:element name="WebTemplateID">
         <xsl:value-of select="$webTemplateID" />
        </xsl:element>
        <xsl:element name="WebCount">
         <xsl:value-of select="@Count" />
        </xsl:element>
       </xsl:element>
      </xsl:for-each>
     </xsl:for-each>
    </xsl:for-each>
   </xsl:for-each>
  </ReportRoot>
 </xsl:template> 
</xsl:stylesheet>

~Sue

December 13, 2011

Publish Error in InfoPath 2010 after pressing Cancel

Filed under: InfoPath, SharePoint 2010 — Susan Hernandez @ 5:45 pm

So I went to publish my InfoPath 2010 Form into SharePoint 2010.  I was impatient because it was taking a long time and I needed to change just one more thing.  So I pressed Cancel.

Now I’m getting one of 2 error messages when trying to either Quick Publish or Regular Publish:

“InfoPath cannot save the following form: https://servername/web/subweb/infoPathLib The following file(s) have been blocked by the administrator …”

“InfoPath cannot save the following form:  https://servername/web/subweb/infoPathLib This document library was either renamed or deleted, or network problems are preventing the file from being saved.  If this problem persists, contact your network administrator.”

Well, after trying numerous times to publish and I even changed the InfoPath Form Properties, still no luck.  Sounds pretty dumb, but all I did was to completely close out all instances of InfoPath and my browser (maybe not necessary) and then re-open InfoPath and do a Manual Publish (not a Quick Publish).  I didn’t change any url’s or put in IP addresses or anything like that.

Are you having this trouble?  Does it persist after you have completely closed and re-opened?  Leave me a coment – maybe together we’ll put in a bug report to Microsoft.

~ Sue

December 4, 2011

MOSS SPSecurity.RunWithElevatedPrivileges Object Reference Not Set

Filed under: MOSS, Visual Studio 2010 — Susan Hernandez @ 6:58 pm

MOSS (SharePoint 2007) SPSecurity.RunWithElevatedPrivileges Object Reference Not Set to an Instance of an Object.

There are a few post out there regarding permissions on the database, and permissions on a local account group; however I just barely found, and wanted to pass on -

If you’re using Visual Studio and you’re running in Debug mode, and you’re either getting the Object Reference on SPSecurity.RunWithElevatedPrivileges or you’re getting a File Not Found error (“Web application at … could not be found”) on new SPSite(webURL), then you might want to check your configuration options.  Are you running in x86 mode and your site is build on 64 bit? 

What I did was I changed my build configuration from x86 to “Any CPU” and that made both exceptions go away.

~Sue

October 3, 2011

SPListItem.DoesUserHavePermissions doesn’t work…or does it?

Filed under: Best Practices, Custom Web Parts, MOSS, Visual Studio 2010 — Susan Hernandez @ 8:48 pm

Don’t you hate when you make dumb – and upon later inspection, rather obvious – mistakes?

I was struggling (I’m too embarrassed to tell you how many hours) with the fact that even though I had broken permission inheritance, my code was always saying that the Current User had access to the list item – and full access at that (that should have been my first clue).

bool hasPermissions = false;
try
{
       //http://blogs.msdn.com/b/ryanrogers/archive/2004/07/15/184594.aspx
       site.CatchAccessDeniedException = false;

       hasPermissions = item.DoesUserHavePermissions(SPBasePermissions.ViewListItems);
}
catch (Exception ex2)
{
       hasPermissions = false;
}
finally
{
       site.CatchAccessDeniedException = true;
}

if (!hasPermissions)
{
       ShowError("User does not have permissions to list item");
       return;
}

So I thought the above code would work.  From everything I could see on the web, it SHOULD have worked.  So why didn’t it?

Turns out that I was running in Elevated Privileges mode, so the “Current User” was the system account.  Now mind you, that code above was NOT inside that Elevated Privileges wrapper – that’s what threw me I think.  But, what I did do (and I’m questioning now WHY did I do it this way) is that I had an SPWeb object variable that I set inside the RunWithElevatedPrivillges method.

SPList list = null;
SPWeb web = null;
SPSite site = null;
try
{
       SPSecurity.RunWithElevatedPrivileges(delegate()
       {
              using (SPSite secureSite = new SPSite(webURL))
              {
                     site = secureSite;
                     using (SPWeb secureWeb = secureSite.OpenWeb())
                     {
                           web = secureWeb;
                     }
              }
       });
}
catch
{
       ShowError("Could not retrieve web at '" + webURL + "'");
       return;
}

try
{
       list = web.Lists[listID];
}
catch
{
       ShowError("List ID '" + strListID + "' not found in web");
       return;
}

try
{
       SPListItem item = list.GetItemById(itemID);

So apparently the web object stayed elevated, thus the list was elevated, thus the List Item was elevated.  All I had to do was use the overload of the DoesUserHavePermissions method.  But that being said, I’m rethinking this whole, “pull it out of the elevated wrapper” thing I have going on here.  I’m marking this post as “Best Practices” as in what NOT to do :-).

hasPermissions = item.DoesUserHavePermissions(SPContext.Current.Web.CurrentUser, SPBasePermissions.ViewListItems);

September 14, 2011

Posts from my company’s blog

Filed under: MOSS, SharePoint 2010 — Susan Hernandez @ 9:56 pm

I’ve been writing a few posts lately on my company’s blog.   Here’s a summary:

Older Posts »

The Silver is the New Black Theme. Create a free website or blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.