Sue Hernandez's Blog

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:

July 22, 2011

Basic InfoPath Tips and Tricks

Filed under: Best Practices, InfoPath — Susan Hernandez @ 5:30 pm

I just had a great seminar yesterday on InfoPath and I thought I’d share some of the tips and tricks that we discussed, as well as a few that we did not.

In general, these are the steps you should take, in this order, to prepare a form for use in SharePoint:

  1. Create the InfoPath Forms Library on your site.
    You will need this URL when creating connections such as a Submit connection.
  2. Create the new .xsn file (InfoPath Form).
    I recommend using a blank template when working with InfoPath 2007 – gives you much more flexibility.
  3. Create the Data Source for your form.
    Do this BEFORE you start to design your form – it will make your designing go more smoothly.  Create a wireframe mockup, for instance an indented bulleted list.  Use Groups liberally to act as containers for related sets of information.
  4. Lay out the general Design of the form.
    See below for a tip on my idea of form design.
  5. Add Buttons and other periferal controls to the form.
    For example a button to Save and Close the form
  6. Apply Formatting, Conditional Formatting, Totalling, Rules, Formulas.
    Use formulas and Default Values to come up with calculated fields. 
  7. Create Alternate Views.
    After you create one full form view, you can either copy that view and make modifications, or, in a Wizard-like form setup, you can continue on with the rest of your questions (see below on the tip for the Form Wizard).
  8. Create a Print View.
    You can create a view that is specifically for a report.  Make it a Read Only view, and place whatever other text or disclaimers
  9. Publish your Form to SharePoint
    Use the Publish feature in SharePoint 2007, or the Quick Publish feature in 2010 (you can’t use Quick Publish till you’ve published regularly once first).  Promote any columns you wish to use, and see below for a tip on field promotion.
  10. Configure the Forms Library to display as a web page.
    Go into Form Library Settings –> Advanced Settings and click the radio button “Display as Web Page”.
  11. Test your form using both priviledged and non priviledged accounts.
    Especially if your form contains logic or conditional formatting to only display certain controls to certain people.

Here are some basic Tips and Tricks I’ve come up with:

  • You do NOT need 2 SharePoint Libraries for your InfoPath forms – you only need one library which both holds the template (in the background) and holds the completed forms in the list.  Make sure you Publish to a Form Library and don’t just upload it.
    • That being said, you can always have a second library called “Form Templates” as a place to centrally store your design templates, but this is not where your users go to fill them out.
  • If you lose your Design Tasks pane, find it in the View menu in InfoPath 2007.  In InfoPath 2010, use the ribbon’s Data tab and select Show Fields to see your data source.
  • In InfoPath 2007, when you want to have a rule that runs when the form is opened, you go to Tools –> Form Options –> Open and Save –> Rules (button) [for instance to set a user name field when they first open the form].  In InfoPath 2010, you go to the ribbon’s Data tab and press the Form Load button - this brings up the Manage Rules pane, but in the context of the form opening.
  • Groups, Groups, and more Groups – when creating your Data Source I recommend using Groups (they look like folders in the Data Source Pane) to categorize your fields into logical sections.  You can use groups within groups as well, if it makes logical sense to do so.
  • Use a Submit Button that you create, with a calculated file name, to both submit and close your form.  That way the users will not have to come up with a file name or get confused on which toolbar buttons to press.  You can create a data field called fileName and populate it on form load – only under the condition that it hasn’t already been filled in – to the “concat” of the user name of the originator plus the “now” date time function.
    • If you choose to go this route, go to Tools –> Form Options –> Browser (InfoPath 2007) or File –> Advanced form options –> Web Browser (InfoPath 2010) and remove the options for Save, Save As, and even Views (also control those with buttons)
  • When wireframing your data source, don’t forget to properly represent any table-like sections – implemented as “repeating sections” in InfoPath.  For example, if you have expenses for one day, the expenses should be a “repeating group” to capture all of the columns for each row – each expense and its metadata.
  • Always rename the top node of the Data Source.  Don’t leave the default of myFields.  Rename it to what makes sense to that particular project.
  • Use the correct casing in names of fields (I think it’s called Title Case) to make the design of the form easier.  For example, if you create a field named EmployeeName (notice the E and the N are both capital), then when you drag the field on to the design surface, it knows to put “Employee Name” there for you (it inserts the space).
  • You can’t re-use field names anywhere in the Data Source – even if they’re in different “Nodes” or groups.  So if you need to, use a prefix, like TravelExpenditures and OtherExpenditures.
  • When designing your form – use a big ol’ layout table in the design surface.  Choose between 15 and 20 columns wide and about 30 rows long.  This is to line things up properly – you won’t have to play around with widths and spacing and you shouldn’t have to adjust the sizes of the columns.
    • When creating places for your data controls to go, merge together some cells, depending on where you want to place the controls, and then drag your controls into the merged area.  Either keep the default of the text on top, or merge cells to the left to hold the title.
    • Using tables in this manner is very helpful – you will note that when you use a table and drag in a control, the control automatically “fills” the width of the section you drag it in to, making things line up just nicely.
  • If you have a lot of conditional logic/branching (i.e. the questions they need to answer depend on the choices of questions above them), then you can set up a Wizard-like form using views and buttons.  Each View should be a “page” of questions that go together.  Then you create a button at the bottom of the view, right after one of the questions that needs conditional logic.  The button will have one or more rules – for example [If the IsOver500 equals True Then switch to the "Over 500 View"] and [If the IsOver500 equals False Then switch to the "500 and Under View"].
    • Don’t forget to add appropriate “Previous” buttons to make sure that the users can get back to the questions they have already answered.
  • If you have a need to make a lot of fields read-only, you have the option of creating a view with only those fields and making the entire View read-only instead of setting the Read Only property of every control.  This works well for Report or Print views.
  • When you promote fields during publish, be very aware of the choices in the dialog box.  You need to choose whether or not the columns should be Created in the library, whether you use Existing columns in the library, or, in cases of content types, if you want to use existing or new Site Columns.

Some References for you:

May 1, 2011

SharePoint 2007 Ajax ScriptManager with Layouts Application Page

Filed under: AJAX, MOSS — Susan Hernandez @ 7:45 pm

So as you may already know, when creating Web Parts with SharePoint 2007, you create them for the most part in code, unless you use user controls, SmartPart, etc.  Adding a Script Manager is easy – you create it in CreateChildControls and THEN you add your UpdatePanel to the page.

But what if you want to use AJAX in an Application Page, that goes in the Layouts directory?  You don’t want to have to create the Update Panel programmatically, you want to be able to put it in Markup.  However, you need, absolutely MUST have, a ScriptManager as the first control in the page.

Yes, you could go and put it manually in your Master Page.  I don’t personally like that approach.

So here’s some code to put it in your Application page.  You have to actually SET the ScriptManager into the Items property of the Page (there is no ScriptManager.SetCurrent or the like).  I derived this by reading the blog here:  http://veskokolev.blogspot.com/2008/01/scriptmanager-in-basepage-class.html

namespace AjaxTextAppPage.SharePointRoot.Templage.Layouts
{
   public partial class AjaxApp : System.Web.UI.Page
   {
      protected TextBox txtInput;
      protected Button cmdClickMe;
      protected Label lblResults;
      protected UpdatePanel updPanel;

      protected ScriptManager mgr;

      public AjaxApp()
      {
         mgr = new ScriptManager;
         mgr.ID = "ScriptManager1";
         mgr.EnablePartialRendering = true;
         this.Page.Items[typeof(ScriptManager)] = mgr;
      }
      protected override void OnInit(EventArgs e)
      {
         base.OnInit(e);
         mgr = ScriptManager.GetCurrent(this.Page);
         if(mgr == null)
         {
            mgr = new ScriptManager;
            mgr.ID = "ScriptManager1";
            mgr.EnablePartialRendering = true;
            this.Page.Form.Controls.AddAt(0, mgr);
         }
      }
      protected void Page_Load(object sender, EventArgs e)
      {
         mgr = ScriptManager.GetCurrent(this.Page);
         if(mgr == null)
         {

            mgr.RegisterAsyncPostBackControl(cmdClickMe);

            EnsurePanelFix();
         }
      }
      protected void cmdClickMe_Click(object sender, EventArgs e)
      {
         lblResults.Text = "You typed '" + txtInput.Text + "'.";
      }
      private void EnsurePanelFix()
      {
         if (this.Page.Form != null)
         {
            string fixupScript = @"    _spBodyOnLoadFunctionNames.push(""_initFormActionAjax"");
               function _initFormActionAjax()   {  if (_spEscapedFormAction == document.forms[0].action) 
               { document.forms[0]._initialAction = document.forms[0].action;  } }
               var RestoreToOriginalFormActionCore = RestoreToOriginalFormAction;
               RestoreToOriginalFormAction = function()
               {  if (_spOriginalFormAction != null)  { RestoreToOriginalFormActionCore();
               document.forms[0]._initialAction = document.forms[0].action;  } }";

            ScriptManager.RegisterStartupScript(this, typeof(this), 
               "UpdatePanelFixup_MainMap", fixupScript, true);
            string formOnSubmitAtt = this.Page.Form.Attributes["onsubmit"];
            if (formOnSubmitAtt == "return _spFormOnSubmitWrapper();")
            {
               this.Page.Form.Attributes["onsubmit"] = "_spFormOnSubmitWrapper();";
            }
            ScriptManager.RegisterStartupScript(this, typeof(this), "UpdatePanelFixup2_MainMap",
               "_spOriginalFormAction = document.forms[0].action;
               _spSuppressFormOnSubmitWrapper=true;", true);
         }
      }
   }
}

I’m not sure if you still need “EnsurePanelFix” (I supposedly needed this for web parts) and I’m not sure if you really need the OnInit, as you’re already adding the manager at instantiation.

This way, though, you can put your UpdatePanel markup right in the page and not have to add it programmatically.

January 12, 2011

SharePoint Google Geospacial Dashboard Solution – Lessons Learned

Filed under: AJAX, Custom Web Parts, Google Maps API V3, JQuery, MOSS, Web Part Pages — Susan Hernandez @ 11:47 pm
Geospacial Dashboard Solution

Geospacial Dashboard Solution - CLICK FOR FULL PICTURE

 

So I have spent some time on my SharePoint Geospacial Dashboard Solution.  It consists of 3 web parts:

  • Main Map web part with optional Related Information section at bottom
  • Cluster Information and Filter web part
  • Details web part for individual Location metadata and KPI information.

As this was created for my client, I can’t post the source code here.  However I can discuss some things I ran across while I was creating this.  But first a word on what it is and how it works.  As an aside, configuration of this solution is housed in SharePoint Lists and Web Part Properties.  Data feeds are served up by the CorasWorks Data Integration Toolset (only as a means of normalizing data input from multiple types of sources).

Overall Concept
The main concept is that you have a data set of entities – Airports, Facilities, Buildings, Servers, etc. – that contain Latitude and Longitude coordinates so that they can be plotted.  Now for each one of these “Locations,” as we’ll call them, you have data surrounding that Location that represents some kind of “Status” or “Health” of that Location.  For Airports, you might want to track the percentage of Late Flights at that Location; for Buildings, you might want to track unresolved Help Desk Tickets entered from that Location; for Servers, you might want to track Service Contracts that are expiring in a certain amount of days, or Percentage of Uptime for the applications on that server.

 In any case, anything that you want to track, that helps determine the overall Health of that site, can be used as a KPI, as long as you have an available data source that relates the Location’s Primary Key, or “Location Key,” as we’ll call it, to the actual numeric value for that KPI at that given time.

Now this solution was built to surface between 30,000 and 100,000 individual data points.  Showing that may Locations on a map is quite rediculous, and results in information overload, so we have added some functionality to help alleviate that.

Point Clustering
The first of those concepts is the idea of “Clustering” individual points within a certain proximity of one another into one bigger point, thus only having between 15 – 30 Clusters on the map at any given point.  We added the ability to click on the cluster and get a typical Google Info Window with a link in it – that link brings up the “Cluster Information Panel” with some details and a list of all of the individual locations contained within that cluster.

Configurable Metadata (Details web part)
When you bring up the Cluster Information panel, one of the icons that is next to each Location information is a link to bring up a Details Panel.  The Details Panel shows you the Location Key and Title of the Location, the current KPI Values for each KPI defined, Metadata from the master Geo DataSet (like Address, City, St, Zip), and Metadata from each of the KPI data sets.  We made it so that what is served up for the Details panel is actually just XML, so the user of the site (the administrator configuring the site, actually), defines an XSLT file that determines the display of the information in the Details Panel.

Configurable Filtering and Searching
One of the ways that the user is able to “find the needle in the haystack” is to use both Filtering and Searching.  The administrator configuring the site determines which columns from the data sources should be used to Filter, and to Search.  When you bring up the Filter tab, it brings up a cached list of every possible value from every column that was configured as a filter column.  You place checkboxes next to the values you want to include – for instance you can filter on ((Region = South OR West) AND (Facility Type = Building OR Vehicle)).  In addition to this filtering, you can perform a wildcard search on one of the columns configured, such as (Title = Water*).

Background Refreshing
As part of the configuration, the administrator can set up intervals of when the cached data should be refreshed from its source.  The data refreshes in the background, and only refreshes the map when it’s done re-loading the data and the map points.

Related Information
Along with metadata about a location or about a KPI’s value, you might have documents, drawings, schematics, etc. that belong to that Location.  The set of tabs displays at the bottom of the map, and when you click on the Details of an individual location, the Related Information tabs below filter out the documents to just the one selected.

Other Stuff
Just for fun, I also added the ability to add a Skin around the outside of the map.

  

THE LESSONS LEARNED

OK so enough of the sales pitch on what it does :-) .  I learned a lot while creating this solution, mostly about AJAX, because I had never actually implemented Update Panels before. 



Working with the Client-Side from the Server / RegisterClientScriptBlock
As you know, working with Google Maps API v3 is all client-side.  So 75% of what my web parts do is just to emit the right Javascript at the right time.  I used to be in the habit of putting the Javascript just in a LiteralControl and adding that to the page.  This time, however, I started using ScriptManager.RegisterClientScriptBlock() like I’m supposed to. 

There’s nothing too special about this, especially for those of you who are familiar with it already.  However, I just wanted to point out that I was also able to add Javascript to the page on the fly after an AJAX Update, simply by making sure that the Control I used as the first argument to the function was the UpdatePanel, and the second argument was the GetType() of the UpdatePanel.  Every time the page did an Asynchronous Postback, the script that was added dynamically from the last Async postback disappeared, like I wanted it to, so it didn’t run again.

The STUPID little Progress Indicator
This was the BIGGEST pain in the you-know-where:  adding a stupid little Progress Indicator for the ASYNCHRONOUS postbacks.  Since my async postbacks took up to 30 seconds (hey, what do you expect with 100,000 data points?), I wanted to provide a way for the user to know how far we were in the process.

Well at first, all my code was in a common Class file that was used between my 3 web parts.  The progress indicator, however, had to be Client-side code running every second, checking the status on the Server, for how far it was in the process.  Well my first hurdle was, that I couldn’t call the code in the Class file from the Client unless I turned it into a full blown SharePoint Custom Web Service.  There was something that I can’t quite remember about PageMethods and “EnablePageMethods=’true’” but something about the Master Pages in SharePoint made it not work.

So what I did, was I moved all my crunching code into a Web Service file – BUT – I only decorated the one Method that gets the status with the [WebMethod] attribute.  I figured, hey, I could start the crunching process, and inside the crunching process I could increment a class instance variable that holds the progress information.

Well I had 2 problems with that.  The first problem I’ll discuss in this next segment below, but after I figured that out, I still had to deal with the second problem.  Once I got the right class instance, and asked for the percentage complete, I was only getting either 0 or 100.  It wouldn’t update the variable in the middle of its processing the crunching.  So I had to actually raise a custom event called ProgressHasChanged and update the variable in the event handler for that, so that the crunching wouldn’t get in the way of the variable updating.

AJAX and the Page Lifecycle and a “Class Factory?”
The first problem I had, though, was I kept losing my instance of the class.  So I found out here (http://encosia.com/2007/10/24/are-you-making-these-3-common-aspnet-ajax-mistakes/) that I was making the common newbie mistake – I didn’t know the async postback was making my whole web part’s event lifecycle fire all over again.

So what I decided to do was the Sue version of a Class Factory – I call it the Sue version, because I still don’t really have a grasp on what a Class Factory is supposed to be and how it’s supposed to be implemented.  So here’s what I did = when my web part first loads, it asks the class to give me an existing instance of the class if you have it, or a new one if you don’t.  What the method does, is that in the class there’s a static Dictionary holding instances of the class by InstanceID – some unique identifier I came up with which was a combo of the base 64 of the page URL along with the guid for the web part.  So it creates a new instance of the class and passes it in to the dictionary.  On subsequent async postbacks, it gets the same instance back again and uses that.

The big challenge was how to get rid of the instance when the page was done.  So after some trial-and-error, I used the client-side window.unload event to launch a JQuery Ajax call back to the server to remove that instance with that ID.

Using Javascript to call an AJAX, Server-Side Update
So when the Map’s zoom level changed, or when you panned far enough, I had to call back to the server to run the async function to re-load the points given that new zoom level and map bounds.  So I needed a way for Javascript to call a Server Side function.

That turned out to be relatively straight-forward.  What I did, was for each Update Panel (I had several), I added a hidden HTML div.  Inside that DIV was 2 things – a set of text boxes that held Argument values to be used in the server side function, and a button that would be used to actually trigger the async postback.

So one of the things the web part did was to emit the Javascript function that would be called to do this.  Inside that Javascript function was setting all the arguments to the right values – I just used the ClientID of the textbox that I hid in the Div to get the object, and then just set the value.  After I was done setting all the argument variables, and any other periferal client-side stuff I had to do, I manually called __doPostBack on the UniqueID of the button that I hid in the Div, inside that UpdatePanel.

RegisterAsyncPostBackControl and UpdatePanel.Update()
One of the wierd things I encountered, however, was that setting ChildrenAsTriggers = true for the UpdatePanel didn’t work.  I had to manually register each Button control that I wanted to use to post back with, by using scriptManager.RegisterAsyncPostBackControl.

The other wierd thing, was that in the event handler that ran when that button was triggered, the content inside the UpdatePanel didn’t actually refresh on the client unless I put updatePanel.Update() immediately at the end of the function.

Loading XML Files from secure SharePoint using XmlTextReader and XmlUrlResolver
Switching topics, back in the class that was doing all the crunching, I had to load in the data sources when I went to refresh the Cache.  Well the way we set it up was that we used the CorasWorks Data Integration Toolset to sort of “normalize” how the data was coming into the program, whether it came from SharePoint, SQL Server, Web Services, or a flat file.  The way the DIT worked was that you put a certain type of web part on to a Web Part Page; it retrieved its data on the back end and then replaced the output of the Web Part Page with just an XML stream, like as if you were just reading an XML file.

So I had to figure out how to best and quickest read an XML file from the web and stick it into a DataSet object.

I checked around and what seemed to work best in my scenario was using the XmlTextReader which took an argument of a URL to the file.  However, because we were over HTTPS, I had to make sure that I added an XmlUrlResolver and passed it the NetworkCredentials, and set the resolver into the XmlTextReader.  After that was set, I just called DataSet.ReadXml(xmlTextReader, XmlReadMode.ReadSchema).

SQLBulkCopy
Last but not least, I had large data sets with updates, because I’d have to refresh the data cache from its source files.  I didn’t want to iterate through each row and update SQL that way, I figured there would have to be a better way, and there was.

The SqlBulkCopy object takes in a Data Table and just does as it says – it bulk writes all the changes in the data table into SQL Server.  The only thing I had to worry about was adding bulkCopy.ColumnMappings for each column I wanted to add in there, as my data table structure was slightly different than my Cache table’s structure.

That’s it.  I hope some of you can benefit by the tips here, and if any of you ran into better ways of doing things, drop me a comment.

Thanks

October 19, 2010

Long Running Client Side Scripting (i.e. Javascript)

Filed under: JQuery, MOSS — Susan Hernandez @ 5:56 pm

In my Google Mapping Dashboard/Scorecard solution, I was calling out IN SCRIPT to several data sources, including SharePoint Lists (via web services) and xml files (provided by the CorasWorks Data Integration Toolset).  I originally had very long-running processes which would not only hang the browser, but it also just plain made it crash.

Using the reference below, I got the general idea of what they were trying to do, and I modified it to my own liking.  In this article, I will explain how to use a “timed loop” to control browser UI responsiveness.

References:

First off, we will call our web service to get our return data.  I’m using the JQuery library in this example, but it is not necessary to use JQuery to affect the same results.

$.ajax({
	url: YOUR_URL_HERE,
	dataType: "xml",
	complete: function(xData, status) {
		var xDoc = xData.responseXML;
		var nodes = xDoc.selectNodes("//Table1");

		var items = new Array();
		for (var i = 0; i < nodes.length; i++) {
			items.push(nodes[i]);
		}
		totalNumNodes = items.length;
		loopChunks(items, totalNumNodes);
	},
	contentType: "text/xml; charset=\"utf-8\""
});

Notice how we’re calling a function here after we receive our response from the AJAX call, a function called loopChunks.  We pass in an item array – as far as I can tell, it MUST be an actual Array object, thus looping through the items first and pushing them into an array.  We also pass in the total number of nodes, so we can do some percentage calculation (in case we want to update the browser with that percentage information).

Next, we call our loopChunks method:

function loopChunks(items, totalNumNodes) {
	if (items.length > 0) {
		var index = (totalNumNodes - items.length);
		var node = items[0];
		items.shift();  // This removes the first item in the array

		var pct = Math.round((index / totalNumNodes) * 100);

		updateInfoPanel(pct);

		if (pct % 10 == 0) {
			setTimeout(function() {
				processNode(node, items, totalNumNodes);
			}, 25);
		}
		else {
			processNode(node, items, totalNumNodes);
		}
	}
	else {
		items = null;
		try {
			CollectGarbage();
		}
		catch (ex) { }
		window.setTimeout(function() { completionFunction(); }, 25);
	}
}

First we’ll see in this function that we check to see if we have any items left to process.  If we do, we first find out which item we’re on (for our percentage done) and then specify the current node as the first node in the array.  We then use the shift() method of the Array object to remove the first item in the array.  Here, the documentation tells us that we could have just used the return value of the shift() function to get the first node, but for me this always returned a GUID and not the actual node (even when just looping through strings). 

Next we calculate the percentage done we are.  After that, you’ll notice that I do a check to see if our current percentage is a factor of 10.  **IMPORTANT**  The reason I did this was because when you loop through memory items, and you update the percentage on the UI EVERY TIME, you end up actually having TOO much overhead just in the setTimeout function that follows.  I found that if I’m doing lots of calculations with the node, I’ll update the UI every 10%.  If I’m doing faster operations, I’ll update the UI every 20% or 25%.

Next in our function, we’re either setting a timeout or just calling directly the processNode function.  The processNode function is what actually does all of our work.

function processNode(node, items, totalNumNodes) {
	var locationKey = "";
	if (node.selectSingleNode("Location_x0020_Key") != null) {
		locationKey = node.selectSingleNode("Location_x0020_Key").text;
	}
	var lat = "";
	if (node.selectSingleNode("Latitude") != null) {
		lat = node.selectSingleNode("Latitude").text;
	}
	var lng = "";
	if (node.selectSingleNode("Longitude") != null) {
		lng = node.selectSingleNode("Longitude").text;
	}

	var latLon = new google.maps.LatLng(lat, lng);
	var title = locationKey;

	// Set the Marker
	var marker = new google.maps.Marker({
		position: latLon,
		title: title
	});

	fluster.addMarker(marker);  // SourceForge - Fluster2Cluster

	loopChunks(items, totalNumNodes);
}

Here we are simply getting the Name of the Google Marker, the Latitude and Longitude of the Marker, and setting it into our global variable fluster which is an implementation of the Clustering script library Fluster2Cluster from SourceForge.

Notice at the end of the function, we call the original looping function, loopChunks, with the parameters we passed down into the function.

Here’s the rest of the code, which was referenced in the other functions: the updateInfoPanel method and the completionFunction method.

function updateInfoPanel(pct, stage) {
	try {
		var html = '<a class="whiteLink" onclick="closeInfoPanel();" href="#"><strong>&gt;&gt;</strong></a>

		// this is a progress bar of sorts
		html += "<div style='width:104px;height:25px;border:2px navy solid'><div style='width:" + pct + "px;height:21px;background-color:green'>&nbsp;</div></div>";

		$("#map_infoPanel").html(html);
	}
	catch (ex) {
		alert(ex.message);
	}
}

function completionFunction() {
	fluster.initialize();
}

That’s it!

Older Posts »

Theme: Silver is the New Black. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.