Quantcast
Channel: Marc D Anderson's Blog » jQuery library for SharePoint Web Services
Viewing all 109 articles
Browse latest View live

Obscure (And Not So Obscure) Bugs with Complex Dropdowns in SharePoint Forms

$
0
0

As I’ve written in the past, there are several types of “dropdowns” in SharePoint forms. Briefly, there are:

  • “Simple” dropdowns – These are plain, old HTML selects; the type of dropdown you are most familiar with on the Web. SharePoint renders one of these when there are fewer than 20 options.

  • “Complex” dropdowns – SharePoint gives you these if there are 20 or more options. The interesting thing about this type of dropdown is that it is actually pretty ingenious, but very few people know why. When I ask in my presentations at various events who knows about the ingenious part, the percentage of people in the room who know about why they are ingenious is almost always less than 50% and usually far below. I’ll tell you how it works at the end of the post.

  • Multi-select dropdowns – Sure, these aren’t truly dropdowns, but they are what SharePoint displays when you when you offer the user a set of choices and want to allow them to select zero or more of those choices and you don’t choose checkboxes – sort of a dropdown.

image

The naming convention above is mine, one that I came up with when I was trying to understand the Document Object Model (DOM) which SharePoint renders so that I could write some of the cool functions in SPServices. The issues I am going to point out in this post occur with the “complex dropdown” only.

Issue One – The Wrong Selection

When a Lookup column’s list source has duplicate values and a validation error occurs elsewhere in the form, the first matching value is shown as selected, not the value which was selected if the selection was occurrence 2+. That’s a little hard to follow, so here is what I mean.

We can demonstrate this on a list form with a dropdown (Lookup) column and one other column which has validation of any type. In my example, there’s one column for State and another for City. Assume that there are two or more States with the name Alabama. (This isn’t a great example, but bear with me, as it will prove the point.) Also assume that there are 20+ States and that the City column is required.

When we select the second Alabama value and no City is selected, on submit there is a validation error because a City is required.

image

That’s all well and good, and what we would like to have happen:

image

However, the first occurrence of Alabama is now selected rather than the second, which we had selected:

image

This may be a weak example, but it still proves that there is a bug introduced by the complex dropdown control. If the person setting up the list for States knows enough about relational data, they won’t have multiple items in the list with the same Title. However, many users (and frankly, many IT people) are not well-versed in the nuances of building a good information architecture which follows the rules of relational data constructs. It’s reasonable to expect that SharePoint will record the correct value.

The bug is introduced because rather than saving and matching on the selected option’s value (which is the ID for the item in the Lookup list), the text of the selected option is used to match, therefore causing the error. I’ve had to introduce the same bug into SPServices so that I can mimic the out of the box functionality.

This bug is identical in SharePoint 2007 and 2010.

Issue Two – User Interface Bug with No Dialogs

This issue occurs only in SharePoint 2010. By default, list forms in 2010 are displayed in a dialog box. What this means is that rather than going to an entirely new page to display the form as happens in SharePoint 2007, the form is “popped up” over the current page, which is usually a list view.

When the form is shown in the dialog box, all is well and good, and the complex dropdown looks like this when you try to choose an option:

image

However, if you decide to turn off the dialogs (List Settings / Advanced Settings / Dialogs – way down at the bottom of the screen), there is a display issue where the available values are shifted about 155px to the right:

image

The dropdown otherwise functions just fine, but it sure doesn’t look right.

In my opinion, and based on conversations with my clients, the constant dialogs are an anathema in SharePoint 2010. They certainly make forms which have any complexity to them harder to fill out. I usually recommend turning the option for dialogs off for many forms. Unfortunately, it’s a manual, repetitive process.

The complex dropdown is truly a complicated beast. It’s made up of multiple HTML elements and JavaScript which allows it to function. Unfortunately, that markup and scripts doesn’t seem to have gone through a perfect Quality Assurance (QA) process.

Oh, and the ingenious part of the complex dropdown I promised you? When you start typing in the input element – yes, you can type in there! – the control filters the available values it shows. For example, if I type “sou” into the input element, I only see the “South Dakota” and “South Carolina” values in the dropdown.

image

This is great functionality, but since so few people know about it, especially real users of SharePoint, it serves little purpose other than to confuse everyone!


SPServices Survey Now Available

$
0
0

As I mentioned in a prior post, I’ve put together a survey to find out how people are using SPServices, what they like, dislike, wish for, etc. If you’ve ever used SPServices, or even if you are considfering using it, I’d really appreciate it if you could respond to the survey, which is available here.

If you have any issues responding to the survey, please let me know in a comment on this post.

Thanks for Christophe Humbert, Kerri Abraham, Matt Bramer, Josh McCarty, Sadie Van Buren, and Claire Willett for their help in test-driving an early version and contributing valuable questions and feedback.

FluidSurveysBTW, I did a mini-review of the available online survey tools for this, and decided that I liked Fluid Surveys over the other options. I started with Survey Monkey and also looked at Zoomerang, Qualtrics, and a couple others. I easily went past the 10 or 20 questions that each offers for free, and the free versions also don’t let you do things like simple branching, there are limitations on question types, etc. I liked the price point for FluidSurveys ($19/month) as well as their UI.

Setting a Rich Text Column in a SharePoint Form with jQuery

$
0
0

Over in the SPServices Discussions, @PirateEric was having a problem setting the value of a Rich Text column (RTE) in a SharePoint form. He was referring to my blog post Finding the Contents of a SharePoint Rich Text Column with jQuery – And Changing It, which I’ll stand by, but the jQuery there wasn’t cutting it. (If you’d like to read the whole thread, it’s here.)

What Eric wanted to do was set the RTE when the form loaded based on the results of a call to GetListItems (the workhorse of the SharePoint Web Services operations). That call would grab an existing list item so that Eric could reuse some of the values.

In my earlier post, I was focused on changing the existing value in an RTE column. Eric wanted to populate an empty RTE column. (This method works to re-populate an RTE that already has a value as well.)

The code is actually far simpler in this case. To review, an RTE (which stands for Rich Text Editor) is a column like this:

image

The markup for the RTE in the DOM is (as you might expect) fairly complex, but here are the important bits. I’m showing a view of the markup for the entire table row which represents the RTE column in my SharePoint form.

image

Any existing value will be stored in the textarea I’ve highlighted, but also within the iframe I discussed in my prior post. To simply set the value of the RTE column, you can just insert your own markup – or simply text – into that textarea. SharePoint’s script will fire on that change event, populating the iframe contents appropriately.

The jQuery to do this is really simple:

var systemDescriptionRTETextArea = $("textarea[Title='System Description']");
alert($(systemDescriptionRTETextArea).html());
$(systemDescriptionRTETextArea).html("Cream cheese");
alert($(systemDescriptionRTETextArea).html());

Here’s what I’m doing here:

  • Line 1: I’m finding the RTE’s textarea in the form using the selector. Many of the key elements in SharePoint forms have their title attribute set to the DisplayName of the column. (Unfortunately, this is not true of some columns types, like checkboxes and radio buttons, for example.)
  • Line 2: I’m alerting the original contents of that textarea, just to see what’s already there (just for debugging)
  • Line 3: I’m setting the value of the column to “Cream cheese”
  • Line 4: I’m alerting the contents again to be sure that it “took”

There you go, simple jQuery to set the contents of an RTE column. Of course “Cream cheese” probably isn’t what you’re aiming for: you can insert any valid HTML.

Elevating Permissions with SharePoint’s Web Services

$
0
0
English: A Master Lock brand padlock. Français...

English: A Master Lock brand padlock. Français : Cadenas de la marque Master Lock. (Photo credit: Wikipedia)

I get frequent questions about how to elevate permissions when working with the SharePoint Web Services. The answer on this one is really simple: you can’t.This has come up multiple times in the comments on the survey I’m doing about SPServices right now. Yes, right now! Please fill it out if you haven’t already, and help me make SPServices better.Think about this for just a minute. SharePoint’s Web Services allow anyone anywhere to do reads and writes to SharePoint list and libraries. But there’s far more: we can create sites or delete them, create or alter lists and libraries, read or update user profile information, and on and on.

Now think about the security implications of allowing elevated permissions, and you might quickly realize that it’s not such a great idea. If you work for a large company and have a Security Team or an Enterprise Architecture team, or any of the types of teams that look at the software you use with a critical eye, then you know that elevating permissions client-side with script would just scare the dickens out of them. Client-side code may or may not go through the rigorous QA and security checks that your governance dictates — I know that all of your managed code is regularly checked for security issues, right? — so the KISS principle is the right one here.

So, no, you can’t elevate permissions with the Web Services and therefore I can’t provide you that capability with SPServices. You need to manage the permissions as you do for users through the UI and make sure that any user who needs to accomplish something has the permissions to do so. I’m good with that, and you’ll have to be, too.

Oh, and please fill out the survey. Did I mention that?

Display All Related Tasks for a SharePoint Workflow Using jQuery, SPServices, and jQueryUI

$
0
0

This was a fun one to build. A client wanted to be able to see all of the related tasks for a workflow without having to do the two or more clicks it can take to get there using a standard SharePoint list view. By layering jQuery, SPServices, and jQueryUI, we were able to display the information in a nicely formatted dialog with only one click, and make it a richer experience, to boot!

Using Christophe Humbert’s (@Path2SharePoint) excellent Easy Tabs, each user had their own customized dashboard which showed their current workload. This dashboard was part of a rich user experience which was really a one-stop shop for each user to accomplish all of their main SharePoint tasks. Each tab had a straightforward view of a Document Library which showed the current status of the workflow for each document., as shown below.

image

The column on the far right is a status column for the Customer Request workflow. As you can see, while the regular text link to details about the workflow is available, there’s also a small question mark next to the documents where the workflow is still running.

When the user clicks on the question mark for any document, they get a concise view of the task details for that workflow instance:

image

I built the script so that it will show all of the tasks for the document if there is more than one:

image

If you think through the normal steps to see those details, it’s at least two or three clicks to get to the workflow’s tasks, and the two or three to get back where you were. This was the goal: to provide a single click way to understand the current workflow status. (We considered showing the details on the hover event, but it seemed as though that would be a bit too obtrusive.)

So, how did all of this work? Well first I added the appropriate references to the page. In this case, I added the following lines below the line:

<asp:Content ContentPlaceHolderId="PlaceHolderMain" runat="server">
<script language="javascript" type="text/javascript" src="/Scripts/jquery-1.5.min.js"></script>
<script language="javascript" type="text/javascript" src="/Scripts/jquery.SPServices-0.6.1.min.js"></script>
<script language="javascript" type="text/javascript" src="/Scripts/jquery-ui-1.8.12.custom.min.js"></script>
<script language="javascript" type="text/javascript" src="/Scripts/Workflow.js"></script>
<link rel="stylesheet" href="/Scripts/css/jquery-ui-1.8.12.custom.css"/>
<link rel="stylesheet" href="/Scripts/css/Workflow.css"/>

We needed the jQuery library, my SPServices library, the jQueryUI library, my custom script for the page, the jQueryUI CSS for the selected theme, and my custom CSS.

The custom CSS ended being pretty insignificant; it just made a couple of small changes to one class.

.cm-app-info {
    display:inline-block;
    cursor:pointer;
}

The meat of things was Workflow.js. In that script file, I did all the work to make the modal dialog happen. I’ve added comments throughout the script, so hopefully it will make decent sense. I’ve cut out a few extraneous bits to simplify things, so I hope that I haven’t introduced any errors. In any case, you should always consider code samples in my blog (and most others) simply as a starting point for doing your own thing.

$(document).ready(function() {

 // In this section, we find the important elements in the DOM for later use
 // --> Note that the specifics in the selectors will be different in your situation, but you can use this as a model
 var customerContractsTable = $("table#onetidDoclibViewTbl0[summary='Customer Contracts ']");
 var customerContracts = $("table#onetidDoclibViewTbl0[summary='Customer Contracts '] > tbody > tr:gt(0)");

 // Add a div to hold the text for the modal dialog
 $(customerContractsTable).after("<div id='workflowDetailsText'></div>");
 var workflowDetailsText = $("#workflowDetailsText");

 // Find each Customer Requests cell (the seventh column, which with zero-indexing is in position 6) where the current status = "2" (In Progress)
 $(customerContracts).find("td:eq(6) span[value='2']").each(function() {
  // For each, show the question mark icon from jQueryUI by appending a span with the appropriate CSS classes
  $(this).closest("td").append("<span class='cm-app-info ui-icon ui-icon-help' ></span>");
 });

 // When the user clicks on the question mark icon...
 $(".cm-app-info").click(function () {

  // Get the link to the current item (servername hard coded for simplicity - not best practice)
  var thisItem = "https://servername" + $(this).closest("tr").find("td.ms-vb-title a").attr("href");

  // Call the Workflow Web Service (GetWorkflowDataForItem operation) to get the currently running workflows for this item
  $().SPServices({
   operation: "GetWorkflowDataForItem",
   item: thisItem,
   async: false, // We'll do this asynchronously
   completefunc: function (xData, Status) {

    $(xData.responseXML).find("ActiveWorkflowsData > Workflows > Workflow").each(function() {
     var thisStatus = $(this).attr("Status1");

     // Only show workflows which are currently In Progress (Status1 = 2)
     if(thisStatus == "2") {
      var thisTaskListId = $(this).attr("TaskListId");
      var thisTemplateId = $(this).attr("TemplateId");

      // With the information we've just gotten for the workflow instance, get the relevent task items in the Workflow Tasks List
      $().SPServices({
       operation: "GetListItems",
       listName: thisTaskListId,
       async: false, // We'll do this asynchronously
       completefunc: function (xData, Status) {

        // Initiation
        $(workflowDetailsText).html("");
        var out = "<table>";
        var taskNum = 0;

        // Get information for each of the task items
        $(xData.responseXML).find("[nodeName='z:row']").each(function() {
         if($(this).attr("ows_WorkflowLink").indexOf(thisItem) > -1) {

          taskNum++;

          // Format Assigned To
          var assignedTo = $(this).attr("ows_AssignedTo");
          assignedTo = "<a href='/_layouts/userdisp.aspx?ID=" + assignedTo.substring(0, assignedTo.search(";#")) +
           "&Source=" + location.href + "'>" +
           assignedTo.substring(assignedTo.search(";#") + 2) + "</a>";

          // Format Due Date
          var dueDate = $(this).attr("ows_DueDate");
          dueDate =
           dueDate.substr(5, 2) + "/" + // month
           dueDate.substr(8, 2) + "/" + // day
           dueDate.substr(0, 4); // year

          // Format Percent Complete
          var percentComplete = 100 * parseFloat($(this).attr("ows_PercentComplete")).toFixed(0);

          // Add the information to the dialog
          out += "<tr><td>Task #" + taskNum + ":</td><td>" + $(this).attr("ows_Title") + "</td></tr>" +
           "<tr><td>Assigned to:</td><td>" + assignedTo + "</td></tr>" +
           "<tr><td>Priority:</td><td>" + $(this).attr("ows_Priority") + "</td></tr>" +
           "<tr><td>Complete:</td><td>" + percentComplete + "%</td></tr>" +
           "<tr><td>Due:</td><td>" + dueDate + "</td></tr>" +
           "<tr><td colspan='99'><hr/></td></tr>";
         };
        });
        out += "</table>";

        // Add the assembled markup to the container we built for it
        $(workflowDetailsText).html(out);

        // Show the dialog using jQueryUI's .dialog() function
        $(workflowDetailsText).dialog({
         open: true,
         title: "Open Tasks",
         minHeight: 500,
         minWidth: 500,
         modal: true,
         buttons: {
          OK: function() {
           $(this).dialog( "close" );
          }
         }
        });
       }
      });
     }
    });
   }
  });
 });
});

I think that this is a really nice use of scripting to enhance the user experience. Not only does the page start to feel more like a “modern” Web page, but we significantly reduced the number of clicks the user would need to do to accomplish their task. Truth be told, even if they did know what click route to follow, the display of the workflow status information is fixed and not all that intuitive. By using this scripting approach, we can easily change how we display the information in the modal dialog. One enhancement we considered was to grab the photo from the User Profile for the person who currently owned the workflow task. This would be a nice little add on.

One other thing worth noting: because the script is built to do the Web Services calls only if the user clicks on one of the icons, there’s little overhead in those instances where the user doesn’t choose to click. There’s no postback, and only the information from the list items which are required to display the full status is requested.

SharePoint Saturday Tampa Wrap-Up

$
0
0

I thoroughly enjoyed speaking at SharePoint Saturday Tampa yesterday. Michael Hinckley and his team (Is there a team, really, or is it all Michael??? I know his daughter was there helping at the Speaker Dinner.) did another crackerjack job with the event.

It was great to see old friends like Michael Oryszak (@next_connect), Michael Greene (@webdes03), USPJA student Anita Webb (@awebb55), and the godfather of all that is SharePoint Saturday, Michael Lotter. Yes, it was a very “Michael” kind of event. It was also great see some other familiar faces and meet some new folks, most of whom were not named Michael.

My session on Developing in SharePoint’s Middle Tier was well-attended by an energetic and interested group. I always like to have a lot of questions and good discussion and I was happy to have it go that way again this time.

The few slides I used as an introduction for the session are available here. If the propeller head joke offended anyone, I apologize. Long live the propeller heads.

I’m working on making sure that the demos can be instantiated on Office365 as well as “on premises” SharePoint, so I’ve posted new WSPs to my Sympraxis Consulting Demos site. If you happen to try  them with Office365, please let me know how it goes. While these techniques will absolutely work with Office365, I haven’t been able to get the WSPs to transfer successfully because I’ve built them in an “on premises” VM.

“Middle Tier (Customized Navigation)”

image6

“Budget”

image7

Note that there is also a SharePoint 2007 version of the “Customized Navigation” demo, which is a few revs back, but not too far from what I demonstrated.

If you attended my session, thanks for coming and let me know if you have any questions about the demos.

jQuery Library for SharePoint Web Services (SPServices) v0.6.2 Released

$
0
0

OK Go released a new album yesterday (180/360, and it’s great – go buy it!) so I figured I’d better release a new version of SPServices. Those guys shouldn’t have anything on me. My release isn’t live, but it’ll do new cool stuff in your live SharePoint pages. But enough tomfoolery.

Yesterday  (June 21), I released SPServices v0.6.2.  There is some new functionality in this release that people have been asking me for for quite a long time.

First, there’s a new function: SPComplexToSimpleDropdown This function lets you convert the complex dropdowns which SharePoint renders when you have 20+ options. If you’d like to understand the underlying workings of the function, check the docs and/or read my blog post here (following the links to the previous posts will give you even more info).

I’ve also exposed the SPComplexToSimpleDropdown capability as an option in SPCascadeDropdowns. This is probably the best place to use it, as when you are filtering the available options in the dropdown based on the parent selection, there is likely to be a much lower number of options. This makes the simple dropdown a much better UI choice.

I’ve also added a new option to SPCascadeDropdowns which allows you to specify that when there is just a single option available that it should be automagically selected. I’ve resisted this one for quite a long time because it makes the dropdown behave differently than the out of the box behavior, but there have been too many requests for it to ignore.

SPServices v0.6.2 now should work just fine with jQuery 1.6 and 1.6.1. I’ve tested as best I can with jQuery versions from 1.4.2 to 1.6.1, and the issues I know about should be resolved (there’s one open issue with SPDebugXMLHttpResult outstanding).

There are a number of other enhancements and bug fixes (most of which were fairly obscure), and you can read about them in the release notes, which are replicated below:

New Functionality

Alpha Issue Tracker Item Function Description
ALPHA1
5992


$().SPServices.


SPComplexToSimpleDropdown
New Function: convert the "complex" input/select
hybrid control to a plain old select
ALPHA2
8638


$().SPServices.


SPCascadeDropdowns
Add an option to SPCascadeDropdowns to call
SPComplexToSimpleDropdown
ALPHA3
8653


$().SPServices.


SPAutocomplete
rowLimit in SPAutocomplete
ALPHA3
8723


$().SPServices.


SPFilterDropDown
SPFilterDropDown needs a way to localize string
"(None)"
ALPHA3
8757


$().SPServices.


SPCascadeDropdowns
SPCascadeDropdowns needs a way to localize string
"(None)"
ALPHA6
5238


$().SPServices.


SPCascadeDropdowns
SPCascadeDropdowns: autoselect first item of
child dropdown

Bug Fixes and Efficiency

Alpha Issue Tracker Item Function Operation Description
ALPHA3
8632


$().SPServices
UpdateListItems Bad Request Due to Reserved Characters In Data
ALPHA3
8599


$().SPServices
NA Replace unescapeURL() with unescape()
ALPHA4
8778


$().SPServices.


SPArrangeChoices
NA SPArrangeChoices doesn't work with RegExp special
characters
ALPHA4
8328


$().SPServices.


SPCascadeDropdowns
NA SPCascadeDropdowns: Child cascaded fields not
updated
ALPHA5
8807


$().SPServices.


SPDisplayRelatedInfo
NA SPDisplayRelatedInfo showColumn() always rounds
to Integer if :Number of decimal places: = Automatic/undefined
ALPHA6
8579


$().SPServices.


SPCascadeDropdowns
NA SPCascadeDropdowns/jQuery 1.6 bug
ALPHA7
8791


$().SPServices.


SPFilterDropDown
NA SPFilterDropDown: Filter doesn't work if I click
remove from multi select column

Confusion About the UserProfile Web Service’s GetUserProfileByIndex Operation

$
0
0

I had a question in the SPServices Discussions the other day about what to pass to the GetUserProfileByIndex operation in order to retrieve the User Profile information for a user. Well, it seemed simple enough after looking at the MSDN SDK for GetUserProfileByIndex; you’d simply pass in the index. But what *is* the index? Is it the ID of the user in the User Information List or something else? As is so often the case, the SDK falls flat when it comes to useful details.

In the SDK for GetUserProfileByIndex in SharePoint 2007, we get this:

Parameters
index

In the SDK for GetUserProfileByIndex in SharePoint 2010 it gets just a teeny bit better, but is still basically unhelpful:

Parameters
index
Type: System.Int32
The index of the user profile to be retrieved.

After doing a little more research, it’s even less clear what this operation is even supposed to be for. In two separate blog posts (here and here), both of which seem well researched and written, there’s mention that GetUserProfileByIndex doesn’t really do what you’d expect, anyway. It seems to be more intended for iterating over all of the users who currently have a My Site than to simply get their User Profile.

So, I’m less clear on how to call GetUserProfileByIndex than I was at the outset. I think it probably makes more sense to call GetUserProfileByName or GetUserProfileByGuid if at all possible, as both of those operations seem to return what one would expect: the User Profile for the specified user.

By the way, in playing around with GetUserProfileByIndex, I realized that I had incompletely implemented both GetUserProfileByGuid and GetUserProfileByIndex in v0.6.0 of SPServices. In neither case was I passing any parameter values at all. I’ve just fixed that in the first alpha for v0.6.3. It seems that no one had needed to use those two operations (or had simply given up on them without reporting any issues).


jQuery and Backward Compatibility

$
0
0
SVG version of Bug silk.png by Avatar

Image via Wikipedia

The other day, I was helping Jim (jtandrew on Codeplex) with an issue he was having with SPServices. It took a little back and forth before we realized that he was using an old version of SPServices with a newer version of jQuery. One of the problems with naming your files something consistent like jquery.SPServices.min.js is that it’s sometimes difficult to be sure what version you’re using. There are some decent reasons for using consistent naming (a single point of change being the main one, but that also has drawbacks), but be sure that you always know which true versions of script files you are using. But that’s for a different post…

The jQuery folks manage to break something in my SPServices code with just about every point release. It’s not that I’m doing anything unacceptable, but they sometimes change the rules. Backward compatibility may not be the jQuery team’s strong suit, but as with most things, it’s a work in progress. I’m not meaning to disparage the great work they do, but it points out the fact that you simply *must* always test your scripts with new versions of jQuery, just as I do with SPServices.

As an example, as I’ve previously written, in going from version 1.4.x to 1.5, the jQuery team started enforcing the rule which says that you must enclose literal strings in quotes in selectors. In 1.4.x, this was totally valid:

$(xData.responseXML).find("[nodeName=z:row]")

but in jQuery 1.5+, it must be:

$(xData.responseXML).find("[nodeName='z:row']")

Another difference cropped up for me in going from jQuery 1.5.x to 1.6. In 1.5.x and earlier, this worked:

$(childSelect.Obj).attr("length", 0);

but in 1.6+, it needs to be:

$(childSelect.Obj).find("option").remove();

So no longer could I truncate a select’s options by simply setting the select’s length to zero; I needed to remove the options from the DOM.

It’s open to debate whether my original code in either case was so great to begin with. We’re all learning all the time, and in both cases, I’d prefer the method in the latter case over where I started out. However, this makes for a herky-jerky development experience as the jQuery team puts out more releases. For each of their point releases, I’m finding at least one deal breaking issue which means that I need to release a new version of SPServices just to keep up. I like to do a release or two every quarter to add functionality people are looking for anyway, but it makes it tricky to manage versions for me, and probably for everyone else as well.

So what’s the moral of this story? Test, test, test. Whenever you go from one point release of jQuery to another, you probably should assume that *something* in your perfectly functional existing code may break. In each case I’ve identified, it’s led to better code on my end, but it still means changes.

CEWP's ContentLink

CEWP's ContentLink

This is yet another good argument for consistent, organized code management for your scripts. Store as much of your script as as possible (if not all of it) in Document Libraries so that you can easily find the script files you’ve used. You can use a Content Editor Web Part (CEWP) with the Content Link to point to the file, or simply add a new script reference in your page rather than including the script inline.

<script language="javascript" type="text/javascript" src="/Scripts/MyReallyGreatScript.min.js"></script>

I generally shy away from being overly prescriptive about exactly where you should store your scripts or what your script management practices ought to be. The most important thing is that you come up with a scheme which is consistent and predictable. I always say that this is an important part of your governance, just as important as what type of content your users are allowed to post to a discussion or anything else. Your governance should cover your *coding* rules as well as your *end user* rules. It’s hardly fair to expect all of your users to follow rules if you don’t even give yourself any!

So when a new release of jQuery comes out, test, test, test. Identify *all* of the scripts you have in place which use your existing version and make sure that they still work with the new version. Each new version of jQuery offers great new functionality as well as a tightening up of the existing functionality.

Getting “ows_MetaInfo” Nicely with SPServices

$
0
0

I know that I’m going to look for this tip again, so this post is as much as a “note to self” as anything else, though I expect that others will use it, too.

One of the streams I watch with HootSuite is a plain old search for “SPServices”. It’s useful for me to see what people are saying about SPServices (this one and not that one) and I occasionally retweet the good stuff.

I spotted a great tip from Steve Ottenad (@sottenad) yesterday about displaying the MetaInfo from list items nicely. It’s so simple, yet as far as I know, undocumented.


RT @ Getting “ows_MetaInfo” with SPServices in SP2010 http://bit.ly/l1WQJH #sharepoint
@sottenad
Steve Ottenad

Simply add Properties="True" to your CAMLViewFields, like so:

CAMLViewFields: "<ViewFields Properties='True' />",

Thanks, Steve!

Updating a Lookup Column Using SharePoint’s Lists Web Service

$
0
0

Updating a Lookup Column Using SharePoint’s Lists Web ServiceThe SPServices discussions are a fount of interesting information. I find myself answering all sorts of questions which, while connected to SPServices, are often just as much about how SharePoint works in general. I had two questions recently about how to update Lookup columns with UpdateListItems, and I thought they would be of general interest.

Lookup column values are stored in the format “n;#Text Value”, where n is the ID of the value in the reference list and “Text Value” is, well, the text value of the Lookup column. There are probably some good historical reasons for this having to do with Office compatibility, if you think about it. Just storing the text value means that you lose the connection to the original item in the reference list, and just storing the ID for the item would mean that you’d need expensive calls to the database every time you wanted to display the value. As for the ‘;#’ delimiter, you see that sprinkled throughout how SharePoint stores data values. It’s probably related to some very old standard of some sort, but in any case it’s highly likely to be a unique string which no one will use in their text values, so it’s a reasonable delimiter.

Update a Lookup Column Value

The first question was how to update a Lookup column value. The code for this in SPServices is pretty straightforward; the tricky bit is knowing how to structure your value.

In this example, the State column in Sales Opportunities is a Lookup column to the Title column in a list called States.

  $().SPServices({
    operation: "UpdateListItems",
    async: false,
    debug:true,
    listName: "Sales Opportunities",
    ID: 5,
    valuepairs: [["State", "2;#Rhode Island"]],
    completefunc: function (xData, Status) {
      alert(xData.responseText);
    }
  });

It also works fine to just specify the index:

    valuepairs: [["State", "2"]],

The trick is knowing what the right index is. You’ll need to figure that out by calling GetListItems on the reference list or by using some other method.

“Emptying” a Lookup Column Value

The second question was how to “empty”, or remove, a previously set value in a Lookup column.

  $().SPServices({
    operation: "UpdateListItems",
    async: false,
    debug:true,
    listName: "Sales Opportunities",
    ID: 5,
    valuepairs: [["State", ""]],
    completefunc: function (xData, Status) {
      alert(xData.responseText);
    }
  });

It’s important to keep in mind that when you are interacting with SharePoint’s XML (SOAP) Web Services, *all* of the traffic back and forth is text. There’s no such thing as a null or some of the other datatypes you might be used to in other ways of interating with the API. An empty string [""] in text essentially *is* a null; it represents “no value”.

I wish that I could say that this little tip was foolproof in interacting with all of SharePoint’s Web Services, but I can’t. There is a *lot* of internal inconsistency across the Web Services, and in some cases, even among operations in the same Web Service. If you want to set other values to “empty” or “null”, you may need to experiment to see what works. I didn’t write ‘em; I just wrapped ‘em! Note that the line:

      alert(xData.responseText);

can be an absolute lifesaver. It will alert what has come back from the server as a response to the Web Service call. Even if the AJAX request is successful (Status=”success”), there may be useful (or totally indecipherable) error information in the response.

Adding jQuery+SPServices to a SharePoint Page: Step One, Always

$
0
0

alert(

As a follow up to my prior post Adding jQuery to a SharePoint Page: Step One, Always, here’s another step one, always, for when you are using jQuery+SPServices. Even if you have referenced the jQuery library correctly, you still may not be referencing SPServices correctly.

To make sure that SPServices is referenced correctly, add this line:

  alert($().SPServices.SPGetCurrentSite());

right after

$(document).ready(function() {
  alert("jQuery");

Alert GetCurrentSite()The first alert will ensure that your jQuery reference is correct. The second will ensure that your SPServices reference is correct, assuming that you see the name of your current site. If you see “undefined” or some value which makes no sense, odds are your reference to SPServices is wrong.

If you get both alerts, you’re past some large percentage of the issues I help with in the SPServices Discussions. The rule of thumb is relatively simple: scripts which depend on other scripts must be referenced after those dependencies. SPServices requires jQuery, so you reference jQuery first and then SPservices. If your dependencies are more complex, consider using a more sophisticated script loader, like the great LABjs from Kyle Simpson (@getify). You can also, of course, use SharePoint’s script loading logic, though I’ve never found that it offers much benefit for the complexity it adds.

Here’s the full simple step one test. Obviously, you’d replace the references with your own paths and filenames.

<script type="text/javascript" language="javascript" src="my_path/jquery-1.6.1.min.js"></script><script type="text/javascript" language="javascript" src="my_path/jquery.SPServices-0.6.2.min.js"></script>
<script type="text/javascript" language="javascript">
  $(document).ready(function() {
    alert("jQuery");
   alert($().SPServices.SPGetCurrentSite());
  });
</script>

SPServices Spring 2011 Survey Results – Part 1: Demographics

$
0
0

I recently did a survey asking the SPServices user community questions about how they used the library, how long they’ve used it, why, for what types of solutions, etc. I read through all of the responses, learned a few things, and promptly forgot to publish the results. There is some interesting information in the responses and over the next few weeks I will do a series of posts on what they told me. While there wasn’t really anything truly eye-opening for *me* (I interact with the SPServices user community on a daily basis), there probably will be for some other people.

Let’s start with the basic demographics. I got 172 total responses, of which 105 were fully completed. (There was a button on the last page which I think a lot of people didn’t click.)

Responses came in from around the world, but by far the most were from the US:

image

image

Most respondents have been using SPServices for at least a few months and their environments are what I would call mid-sized, though there are plenty of large-scale deployments using SPServices as well.

image

image

As expected, the majority of people use SharePoint Designer, but it surprised me to learn that almost a quarter of respondents said that Visual Studio was their primary development tool.

image

More people are using SPServices with SharePoint 2010 than I might have predicted. 71% of respondents said they were working with 2010 versus 98% working with 2007.  I think what this says is that SPServices is still a relevant and useful tool with SharePoint 2010. It also points out that a large portion of the respondents are working with both versions to some degree.

image

Finally for the demographics look at the survey, a quarter of respondents said they had some plan to move to “the cloud”. This can only be good news for the Office365 team, as well as for the relevance and long-term prospects for SPServices, IMO.

image

Next up: How are people using SPServices?

p.s. I used Fluid Surveys for the survey, and I’m impressed with their interface and analytics options. Check them out!

Connecting to SharePoint’s Web Services from iOS

$
0
0

I got an email through my blog the other day which asked an interesting question. My reply bounced back, so I figured I’d post it here as well as in the SPServices Discussions (where a question like this belongs, anyway).

Hi Marc, first of all thank you for your contributions to the sharepoint community. [You're welcome!]

I hope you can help me with this. I’ve been working in a sharepoint portal, it should work as a news site, right Now i’m using spservices to connect to sharepoint, it works great, except in the iPad. I know that there are some compstibility issues but I need to make it work.

Doing some tests I came to the conclusion that the only method in the lists.asmx service that it’s not working it’s GetListItems, do you have any information about this, or do you know of another way of getting the list ítems using jquery?

I would appreciate any information you could provide me.

Thank you very much.

It makes sense that one should be able to connect to SharePoint’s Web Services from *any* platform, whether iOS on the iPad or Linux or DR-DOS. The biggest trick is always going to be in the authentication.

The simplest approach is to set up the list where you want to use GetListItems for anonymous access. This will ensure that *anyone* can read the content. If anonymous access isn’t appropriate, then you’ll need to come up with a mechanism to authenticate your user the same way you would for any other access to SharePoint from an external – meaning non-Windows authentication – source.

Authentication is definitely *not* my arena, and I find that most of the research I do on the various methods and requirements get me into whirlpools of acronyms and jargon. When it comes to authentication methods, I’d prefer to ask the experts. A great place to ask questions about things like SharePoint authentication is SharePoint Overflow.

p.s. Come to think of it, I need to know a good answer to this, too, so *I* posted it to SharePoint Overflow! I’ll update this post if I get a definitive answer.

Tip for Using SPServices with GetListItems

$
0
0

As is often the case, one of the threads on the SPServices Discussions seemed worth bringing over as a blog post. Here’s the initial question, with the script cleaned up a little for clarity:

Any help on this would be greatly appreciated.  When I first completed this project last month it was pulling all of the data from the list.  Now when this script runs it’s only pulling back 4 out of 16 records.  Can you help me adjust the code to force it to pull all list items?

<script type="text/javascript" language="javascript" src="../../Style Library/jquery_library/jquery-1.5.2.min.js"></script>
<script type="text/javascript" language="javascript" src= "../../Style Library/jquery_library/jquery.SPServices-0.6.1.min.js"></script>
<script language="javascript" type="text/javascript">
  $(function () {//-----------SPSERVICES GET ALL LIST ITEMS FROM HR KALENDAR
    $().SPServices({
      operation: "GetListItems",
      async: false,
      listName: "HR Kalendar",
      completefunc: function (xData, Status) {
        $(xData.responseXML).find("[nodeName='z:row']").each(function() { //--------FIND EACH RECORD FROM HR KALENDAR
          ...
        });
      }
    });
  });

Matt Bramer (@iOnline247) was nice enough to reply with this suggestion:

What happens if you put a CAML query in there that says ID != 0

<Query><Where><Neq><FieldRef Name='ID' /><Value Type='Counter'>0</Value></Neq></Where></Query>

I just wrote this query out by hand, so it may need some tweaking.  Throw that into your call and see what happens.

Generally speaking Matt’s suggestion may not be necessary. However, if you don’t specify any CAML options at all, GetListItems uses the default view for the list. That view may or may not return what you think you’ve asked for. By specifying *something* for the options, you’re asking SharePoint to “step out of” the default view.

In SPServices itself, I generally do this by specifying:

// Override the default view rowlimit and get all appropriate rows
CAMLRowLimit: 0,

The nice thing about setting the CAMLRowLimit is that you only need to pass zero as a parameter to make SharePoint stop thinking in terms of the default view. It’s simple, but effective, and doesn’t require any knowledge of CAML at all.


Showing a Spinning Icon When Using SPServices

$
0
0

Here’s another quick question that ought to be useful to many people as they develop solutions with SPServices. Here’s the original question:

Is there a way to add and animate /_layouts/images/gears_an.gif to my SPServices Page while the Service Request is being processed. Everything I have seen seems to require calling SPLongOperation from the code beside.

This is a pretty common goal. We often want to show some sort of indicator while an AJAX call is happening so that the user knows that something is happening and that they may need to wait for a little while.

There are examples of this all over the Web, from SharePoint to Facebook to Google and beyond. Users expect this sort of feedback from Web pages these days.

The image referenced above ought to be a familiar one to frequent SharePoint 2007 users (it’s also available in SharePoint 2010):

Here’s a simple example of how to show this image while a call to a Web Services with SPServices is happening, snipped from one of my test pages:

var waitMessage = "<table width='100%' align='center'><tr><td align='center'><img src='/_layouts/images/gears_an.gif'/></td></tr></table>";
$(divId).html(waitMessage).SPServices({
  operation: "GetListCollection",
  completefunc: function (xData, Status) {
    var out = $().SPServices.SPDebugXMLHttpResult({
      node: xData.responseXML,
      outputId: divId
    });
    $(divId).html("").append("<b>This is the output from the GetListCollection operation:</b>" + out);
  }
});

Taming Long SharePoint List Forms Using jQuery to Break Them into Sections

$
0
0

Do you have any lists in SharePoint where the forms seem to go on and on, marching down the page, seemingly ad infinitum? I’ve seen lists which have over a hundred, even over 300 columns. This is a horrible thing to do to your users for so many reasons, but one of the primary reasons is that the list forms are simply unmanageable and in many cases unfathomable.

If you do have that many columns in a list and they really do need to be there, consider building custom “sub-forms” which take a more wizard-like approach. What I mean by this is that you might have a relatively short initial, customized version of the NewForm which collects all of the required information. When your user saves that form, you can use SPRedirectWithID from SPServices to go to the next form in the sequence, and so on. You could, of course, develop totally custom forms, whether by using traditional .NET development or by using jQuery, and probably jQueryUI plus other plugins, and the SPServices and the Web Services to write the data into your list(s).

But what if all of that seems overly complex or beyond your skills? There’s a quick thing you can do to tame those ridiculously long forms a tiny bit. Your users still won’t love you, but they may not hate you as much.

The first thing I needed to do for this post was to create a new list with lots of columns. I didn’t have any of these horrid complicated lists lying around in my test environment.) I could have done this any number of ways, including manually, but I decided to write some jQuery with SPServices to do it.

<script type="text/javascript" language="javascript" src="jQuery Libraries/jquery-1.6.2.min.js"></script><script type="text/javascript" language="javascript" src="jQuery Libraries/jquery.SPServices-0.6.2.min.js"></script>
<script type="text/javascript" language="javascript">
  $(document).ready(function() {

    // Delete the old list (if present)
    $().SPServices({
      operation: "DeleteList",
      listName: "Complicated List",
      completefunc: function (xData, Status) {
        alert(xData.responseText);
      }
    });

    // Create the new list
    $().SPServices({
      operation: "AddList",
      listName: "Complicated List",
      description: "List with lots of columns",
      templateID: "100",  // Custom list
      completefunc: function (xData, Status) {
        alert(xData.responseText);
      }
    });

    var newFields = "<Fields>";

    for(i=1; i <= 100; i++) {
      newFields += "<Method ID='" + i + "'>" +
        "<Field Type='Text' DisplayName='Column_" + i + "' FromBaseType='TRUE' MaxLength='255' Description='Description of Column_" + i + "' />" +
        "</Method>";
    }
    newFields += "</Fields>";

    // Add a lot of columns to the list
    $().SPServices({
      operation: "UpdateList",
      listName: "Complicated List",
      newFields: newFields,
      completefunc: function (xData, Status) {
        alert(xData.responseText);
      }
    });

  });
</script>

This script simply creates a new list for me called Complicated List and adds 100 columns to it named Column_n. It was down and dirty, but you might see a more complex and useful version of this this as a poor man’s templating tool for building lists in your own environment. And yes, I’m doing this in good old WSS 3.0. It’s not such a bad place to be, and the same approach and code will work in SharePoint 2010. Isn’t SPServices grand?

Once I had my list to play with, I copied NewForm.aspx to NewFormCustom.aspx  – never, ever edit the default list forms – and set it as the form to use when creating a new item for the list. Then I added my CSS and script to NewFormCustom.aspx.

<script type="text/javascript" language="javascript" src="../../jQuery Libraries/jquery-1.6.2.min.js"></script><script type="text/javascript" language="javascript" src="../../jQuery Libraries/jquery.SPServices-0.6.2.min.js"></script>
<script type="text/javascript" language="javascript">
  // This function from SPServices gets all of the Query String parameters
  var queryStringVals = $().SPServices.SPGetQueryString();
  // We can pass in a section name on the Query String to expand by default
  var expandSection = queryStringVals.section;

  $(document).ready(function() {

    // Set up each of the sections -
    // see the setupSection function below for an explanation of the parameters
    setupSection("Section1", "This is Section One", "Title", "Column_5");
    setupSection("Section2", "This is Section Two", "Column_6", "Column_15");
    setupSection("Section3", "This is Section Three", "Column_16", "Column_25");
    setupSection("Section4", "This is Section Four", "Column_26", "Column_40");
    setupSection("Section5", "This is Section Five", "Column_41", "Column_80");
    setupSection("Section6", "This is Section Six", "Column_81", "Column_100");

    // If no section on the Query String, expand the first section
    if(expandSection === undefined) {
      $("tr[section-name='Section1']").css("display", "block");
      $("tr#Section1").addClass("demo-collapse");
    }

  });

  function setupSection(sectionShortName, sectionLongName, startColumn, endColumn) {
    /* Set up a form section
      Parameters:
        sectionShortName: This short name is used for element IDs and such. It cannot contain spaces or special characters.
        sectionLongName: This name is what is shown in the section header and can contain any text.
        startColumn: The first column in the section.
        endColumn: The last column in the section.
      The two column names should be the Display Name, e.g., "Product Name", not the InternalName, e.g., "Product_x0020_Name"
    */

    // Find the first and last row in the section and add the section header
    var sectionStartRow = findFormField(startColumn).closest("tr");
    var sectionEndRow = findFormField(endColumn).closest("tr");
    $(sectionStartRow)
      .before("<tr class='demo-section-header' id='" + sectionShortName + "'>" +
        "<td class='demo-section-header-cell' colspan=2>" + sectionLongName + "</td>" +
        "</tr>");
    var thisSection = $(sectionStartRow).nextUntil(sectionEndRow);

    // Give all of the rows in the section an attribute for the section name for easier manipulation later
    $(sectionStartRow).attr("section-name", sectionShortName);
    $(sectionStartRow).nextUntil(sectionEndRow).attr("section-name", sectionShortName);
    $(sectionEndRow).attr("section-name", sectionShortName);
    $(sectionEndRow).find("td").addClass("demo-section-last-row-cell");

    // Hide all of the rows in the section
    $("tr[section-name='" + sectionShortName + "']").css("display", "none");

    // Add the click behavior for the section header
    $("tr#" + sectionShortName).click(function() {
      var thisSectionRows = $("tr[section-name='" + sectionShortName + "']");
      if($(this).next("tr[section-name='" + sectionShortName + "']").css("display") == "block") {
        $(thisSectionRows).css("display", "none");
        $(this).removeClass("demo-collapse");
      } else {
        $(thisSectionRows).css("display", "block");
        $(this).addClass("demo-collapse");
      }
    });

    // Expand the section if there are any validation errors
    $("tr[section-name='" + sectionShortName + "'] .ms-formbody .ms-formvalidation").each(function() {
      if($(this).html().length > 0) {
        $("tr[section-name='" + sectionShortName + "']").css("display", "block");
        $("tr#" + sectionShortName).addClass("demo-collapse");
        // No need to look at any more rows
        return false;
      }
    });

    // Expand the section if it's been passed on the Query String
    if(sectionShortName == expandSection) {
      $("tr[section-name='" + sectionShortName + "']").css("display", "block");
      $("tr#" + sectionShortName).addClass("demo-collapse");
    }

  }

  // This function (borrowed from SPServices) finds a column's formBody in the page
  function findFormField(columnName) {
    var thisFormBody;
    // There's no easy way to find one of these columns; we'll look for the comment with the columnName
    var searchText = RegExp("FieldName=\"" + columnName.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") + "\"", "gi");
    // Loop through all of the ms-formbody table cells
    $("td.ms-formbody").each(function() {
      // Check for the right comment
      if(searchText.test($(this).html())) {
        thisFormBody = $(this);
        // Found it, so we're done
        return false;
      }
    });
    return thisFormBody;
  } // End of function findFormField
</script>

And the CSS:

.demo-hidden {
	display:none;
}
.demo-main {
	height:250px;
	overflow:scroll;
}
.demo-no-item-selected {
	font-size:12px;
}
.demo-section-link {
	padding-left:15px;
	font-size:10px;
}
.demo-section-header {
	background-image:url('/_layouts/images/plus.gif');
	background-repeat:no-repeat;
	background-position:5px center;
	padding:3px 3px 3px 22px;
	background-color:#6699cc;
	font-weight:bold;
	color:#ffffff;
}
.demo-section-header-cell {
	border-top:1px #c2c2c2 solid;
}
.demo-section-last-row-cell {
	border-bottom:2px black solid;
}
.demo-collapse {
	background-image:url('/_layouts/images/minus.gif');
}

Et voila, expando-collapso.

If you’d like to see this working for real, you can take a look at this page on my demo site.

Don’t you hate it when forms “hide” the field that you need to fix? It happens way to often on forms out the on the world wild Web. So note that if there’s a validation error on the form that the script pops open that section to bring it to your attention. Column_13 is required on the demo site. Try saving the form without filling it in.

And finally, if you choose to pass in a section’s short name on the Query String, that section will be expanded instead of the first. Try ?section=SectionN, where N is [1-6].

Note: The CSS I’m using here in my little demo is intentionally very simple. When I built this for real, we had some nice little icons and backgrounds that made it feel even more helpful.

Using SPServices with jQueryUI’s Autocomplete Function on InfoPath Forms in SharePoint

$
0
0

Yes, that title says using jQueryUI on InfoPath forms in SharePoint. InfoPath forms are fantastic, except when they just aren’t quite able to do what you want. While InfoPath can help you take your SharePoint list forms to a whole new level, there may be times when you want to add some behavior to those forms which InfoPath simply doesn’t offer.

Guess what? jQuery and jQueryUI can come to the rescue in some of those cases, just as it can with the standard list forms.

I recently worked with a client to do a relatively simple thing, but it was a huge hit. They had a SharePoint list had a Single line of text column, to which the user could add whatever they wanted. However, there was a desire to make suggestions as the user typed, based on the contents of a different list, which had thousands of items.

A dropdown doesn’t make sense in that situation, because there are simply too many items. They also wanted to show matches to what the user had typed, regardless where those characters occurred in the list item.

This is a perfect situation in which to use the idea of autocomplete. We all are familiar with this idea, if not the actual term. You probably see it in action every single day as you use a search engine. As you type your search term(s), the search engine shows suggestions, probably based on some fairly sophisticated algorithms.

Here’s an example from Bing:

image

and from Google:

image

(I can’t help but point out that Google was more on the mark in guessing that I was searching for SPServices, but hey, who’s counting?)

When InfoPath forms are rendered in the browser, they are built of ordinary markup just like any other page, albeit fairly complicated markup, driven by a lot of script.  That doesn’t mean that you can’t add some script of your own as well to add additional capabilities. There are a few peculiarities to this situation, though, which you need to handle.

Here’s a slightly dumbed down version of the script we ended up with.

window.onload = function() {
  window.setTimeout(readyCall, 1000);
}

function readyCall(){

  var externalParties = [];

  $().SPServices({
    operation: "GetListItems",
    listName: "External Parties",
    CAMLViewFields: "",
    async: false,
    completefunc: function (xData, Status) {
      $(xData.responseXML).find("[nodeName='z:row']").each(function() {
        externalParties.push($(this).attr("ows_Title"));
      });
    }
  });

  //<input tabIndex="0" title="" class="q_zwfUqJo2fRthHnM4_0 as_zwfUqJo2fRthHnM4_0 b9_zwfUqJo2fRthHnM4_0" id="ctl00_m_g_a226da68_1383_40e3_8410_1ada27d49dcf_FormControl0_V1_I1_T2" aria-invalid="true" style="position: relative;" onfocus="return (TextBox.OnFocus(this, event));" onblur="return (TextBox.OnBlur(this, event));" onpropertychange="return (TextBox.OnPropertyChange(this, event));" type="text" OriginalId="V1_I1_T2" FormId="ctl00_m_g_a226da68_1383_40e3_8410_1ada27d49dcf_FormControl0" ViewDataNode="3" direction="ltr" wrapped="true" ScriptClass="TextBox" VCARD_NAME="91161f891e59461042587839b2504693728ce05a" ?=""/>
  $("input[id$='FormControl0_V1_I1_T2'], input[id$='FormControl0_V1_I1_T3']").autocomplete({
    source: externalParties,
    minLength: 3
  });
}

When InfoPath forms load in the browser, they don’t load with the rest of the page, but instead they are loaded slightly afterward in what amounts to an asynchronous load. Because of that, using $(document).ready(), our trusted jQuery friend, doesn’t work. Instead, as you can see in lines 1-3, we simply wait for 1000 milliseconds (1 second) before we run our script. We found that this was an adequate amount of wait time for our particular form; you might need to adjust this.

In lines 9-19, we use my SPServices library to call the Lists Web Service, using the GetListItems operation. This operation simply reads items from the list based upon the criteria you specify. Once we have the data, we push each of the Title column values into an array called externalParties.

Finally, we call the jQueryUI function autocomplete, using two selectors. In line 21 above, which is commented out, you can see an example of the markup for one of the input elements rendered in the InfoPath form. One of the hardest parts of all of this was to figure out what selector to use. We settled on looking for an input element where the id contained ‘FormControl0_V1_I1_T2′.  (We actually added the autocomplete behavior to two columns in the form, thus the second selector for ‘FormControl0_V1_I1_T3′.)

We added this script into the newifs.aspx and editifs.aspx pages using a trusty Content Editor Web Part (CEWP). Since this was SharePoint 2010, and because it makes for far better code management, we stored the script in a separate file and referenced it using the Content Link.

Bingo-bango, we had a nice, little additional piece of functionality which made the users very happy. This is an example where thinking about all of the tools at your disposal and how you might glue them together into the right solution to get the job done can be the right approach rather than a lot of custom coding.

<UPDATE dateTime=”2011-08-25T23:51″>

My partner in crime for this exercise was Marcel Meth (@marcelmeth), and he’s done a post on it, too, which you can read here. I was able to steal his image of the results, which I’ve added above. Note that the image is not showing the real data but our test data, which was simply a list of the 3000 or so major cities and towns in the US.

</UPDATE>

<UPDATE dateTime=”2011-08-26T09:25″>

I got a question in the comments below about how we added the script to the InfoPath page, and I wanted to add those details here.

  • First, we opened each form in the browser, which launched it in a dialog box.
  • We got the actual URL of the page in the dialog by right-clicking and looking at its properties. The NewForm was newifs.aspx and the EditForm was editifs.aspx (as I mention above).
  • We opened the form page directly in a new browser window and used the toolpaneview=2 Query String parameter trick to put the page into edit mode. This allows you to edit a list form page and add Web Parts to it.
  • We added the CEWP, and put the reference to our script file in the Content Link.
  • Apply and done.

</UPDATE>

Displaying Multi-Select Column Values in CrossList DVWPs

$
0
0

This is one of those things that I’m amazed I haven’t run into before. Perhaps I did in the past and wrote it off to my own lack of understanding. It turns out that this is a known, if rarely mentioned, limitation not only for Data View Web Parts (DVWPs) but also Content Query Web Parts (CQWPs).

You can easily reproduce this issue as follows:

  • Create a new Custom List (or any type of list you choose)
  • Add a Person or Group column called Project Manager. Do not allow multiple values.
  • Add a new item to the list with whatever values you want
  • Create a page with a DVWP on it with your list as its DataSource
  • Display some columns from the list, including Project Manager
  • Add a filter so that you are sure you’ll only get the item(s) you’ve added to your list
  • Convert the DVWP to DataSourceMode=”CrossList”

At this point, you should see the item(s) in your DVWP just as you would expect.

Now go back into the list settings and change the Project Manager column settings so that it allows multiple values. Next go back into your page with the DVWP and Refresh the data view. You should now see no items at all.

This simple test should prove that the issue is only the multiple value selection which essentially acts as a “filter” which you didn’t ask for. You can probably pretty easily think of reasons you’d want to display data like this. In my little example, you might have multiple Project Managers in Projects lists in sites across the Site Collection. If you wanted to show all of the projects rolled up somehow, you might well want to display the Project Manager(s).

Unfortunately, in my testing, this works exactly the same in both SharePoint 2007 and 2010.

Once I realized this was what was going on, I turned to the Interwebs and found some old posts from Waldek Mastykarz and others that mentioned the limitation. I could only find a few posts, but when the people who’ve done the posts are as smart as Waldek, I take their word for it – it’s not me this time, it’s a SharePoint limitation.

This is truly one of those “features” which feel an awfully lot like a “bug”.

I did find one trick to at least allow the items to be displayed, even though the multi-select column values will not be displayed. If we add Nullable=”TRUE” to the ViewFields settings in the CAML in the SelectCommand, then we do get the items to display, albeit with blank values for the multi-select columns.

This ends up looking something like this. Note that I have added the Nullable attribute to the Project Manager FieldRef.

&lt;ViewFields&gt;&lt;FieldRef Name=&quot;Title&quot;/&gt;&lt;FieldRef Name=&quot;Project_x0020_Manager&quot; Nullable=&quot;TRUE&quot;/&gt;&lt;FieldRef Name=&quot;ID&quot;/&gt;&lt;FieldRef Name=&quot;PermMask&quot;/&gt;&lt;/ViewFields&gt;

Now I can see all of the items, but the Project Manager is simply blank. A step forward, but not far enough.

image

Time for a kludgy fix, don’t you think? Well, I think I’ve got one for you. We can use script and my SPServices jQuery library to “fill in” the values after the page loads. Since we can display the items, but not the values for the multi-select column, we can use the Lists Web Service and specifically GetListItems, to go and grab the items, parse out the multi-select column values, and place them into the DOM where they belong.

This isn’t the type of thing that I like to use jQuery for, really, as it really feels like a kludge. On the other hand, if it plugs a hole in SharePoint’s functionality, maybe that’s not so bad?

To make this work, you’ll want to create good “hooks” in the markup you render for the items in your DVWP or CQWP. I always try to do this, anyway, if I’m going to use script on the page so that my selectors can be very “tight” and efficient.

In the DVWP, I simply add three new attributes for the table detail cell (TD):

  • id – This is a unique id for the TD element, which I create by concatenating the string “ProjectManager_” and the current item’s ID. I’ll use these ids to find the empty cells in the DOM.
  • ListId – This is the GUID for the list which contains the item. The @ListId “column” contains this value. (This “column” only exists after you switch to CrossList.)
  • ItemId – This is the ID for the item itself. We could parse it out from the id above, but it’s easier to store it as its own attribute.

You may not realize that you can create any attributes that you want for HTML elements. They aren’t standards compliant, of course, but by adding your own attributes you can store any values you might need.

<td class="ms-vb" id="ProjectManager_{@ID}" ListId="{@ListId}" ItemId="{@ID}">
  <xsl:value-of select="@Project_x0020_Manager" disable-output-escaping="yes"/>
</td>

Now that I have markup which makes it pretty easy to both select the right DOM elements as well as the data I need to make the Web Services call, I can use this script:

$(document).ready(function() {

  var projectManager;

  // For each empty Project Manager column value...
  $("td[id^='ProjectManager']").each(function() {

    // ...call GetListItems to get that item
    $().SPServices({
      operation: "GetListItems",
      listName: $(this).attr("ListId"),
      CAMLQuery: "<Query><Where><Eq><FieldRef Name='ID'/><Value Type='Counter'>" + $(this).attr("ItemId") + "</Value></Eq></Where></Query>",
      async: false,
      completefunc: function(xData, Status) {

        // Parse out the Project Manager value
        projectManager = $(xData.responseXML).find("[nodeName='z:row']").attr("ows_Project_x0020_Manager");
      }
    });

    // Create the links and the column value into the DOM
    $(this).html(userLinks(projectManager));

  });

});

function userLinks(columnValue) {

  var userArray = columnValue.split(";#");
  var numUsers = userArray.length / 2;
  var out="";
  for(var i=0; i < numUsers; i++) {
    out += "<a href='/_layouts/userdisp.aspx?ID=" + userArray[i*2] + "'>" + userArray[(i*2)+1] + "</a>";
    out += i < numUsers ? "<br/>" : "";
  }
  return out;
}

Using this simple bit of script fills in the values for the Project Manager by getting the right items and plugging the values into the DOM, like so.

image

I decided to simply show the names as links to the userdisp.aspx page, with each one on a new line. This link will show either the information for that user in the User Information List or their My Site profile, depending on how the environment is configured.

Depending on what your data looks like (how many items you are displaying, how many multi-select columns you have, etc.), there are obviously some inefficiencies in my example, because I’m calling GetListItems once per item. You could also batch your calls together per list to get all of the items from that list, or whatever made sense in your situation.

Finally, if using script like this gives you a bad feeling, then you could try using a third party Web Part like the Bamboo List Rollup Web Part or just develop your own custom Web Part. But it seems that if you’ve gotten this far, you’re probably trying to stick to the Middle Tier, so the script approach might make sense.

References:

Managing SharePoint Site User Memberships in Multiple Groups Using SPServices

$
0
0

I got an email yesterday from Geoff Oliver. He said he had done something pretty useful with SPServices and wondered if I would be interested in seeing it.

I manage about 115 groups in a single site collection…and that number is expected to grow.  I found that when a new user came on board or changed duty positions, getting them into the right SP groups was slow and cumbersome through the SP interface.

Using the SPServices libraries, I was able to create a simple interface to add/remove a single user to/from multiple groups at once.  When you identify/select a user, it will show you two select lists. The right side will list all of the groups the current user is a member of while the left box shows all the groups the user is NOT a member of.  Between the two select lists are buttons to ‘move’ the groups from one side to the other for the identified user (while modifying their memberships appropriately in the process).  The select boxes are configured to allow multiple select so you can usually perform the maintenance in just a couple clicks.  The code fits neatly into a CEWP.

Of course I wanted to see it. Geoff’s code sounded exactly like the kind of thing that I think SPServices is good for: a useful solution that solves a business problem better than SharePoint out of the box without needing to deploy anything to the server.

Geoff describes himself as:

…a Civilian Project Manager for the Air Force, developing a SharePoint site to assist a large organization with information management and integration with Microsoft Office (sharing/synchronizing data between SharePoint sites and Word/Excel/Access files).  I’ve been doing MS Office automation (using VBA) for about 15 years now and SharePoint site development for 7.

The code uses a number of Users and Groups Web Service operations that are wrapped in SPServices:

  • GetUserCollectionFromSite
  • GetGroupCollectionFromUser
  • GetGroupCollectionFromSite
  • AddUserToGroup
  • RemoveUserFromGroup

Here’s Geoff’s code. It’s amazingly compact and gets the job done well.

$(document).ready(function() {
  //Populate the users pick list
  var strHTMLSiteUsers = "";
  $().SPServices({
      operation: "GetUserCollectionFromSite",
      async: false,
      completefunc: function(xData, Status) {
        $(xData.responseXML).find("User").each(function() {
          strHTMLSiteUsers += "<option value='" + $(this).attr("LoginName") + "'>" + $(this).attr("Name") + "</option>";
        });
        $("#my_SiteUsers").append(strHTMLSiteUsers);
      }
  });
  RefreshGroupLists();
});

function RefreshGroupLists(){
  var strHTMLAvailable = "";
  var strHTMLAssigned = "";
  var arrOptionsAssigned = new Array();
  var intOpts = 0;
  var booMatch;
  var booErr = "false";

  $("#my_SPGroupsAssigned").html("");
  $("#my_SPGroupsAvailable").html("");

  if($("#my_SiteUsers").attr("value") == 0){
    alert("You must select a user");
    return;
  }

  //Populate the Groups Assigned
  $().SPServices({
      operation: "GetGroupCollectionFromUser",
      userLoginName: $("#my_SiteUsers").attr("value"),
      async: false,
      completefunc: function(xData, Status) {
        $(xData.responseXML).find("errorstring").each(function() {
          alert("User not found");
          booErr = "true";
          return;
        });
        $(xData.responseXML).find("Group").each(function() {
          strHTMLAvailable += "<option value='" + $(this).attr("Name") + "'>" + $(this).attr("Name") + "</option>";
          arrOptionsAssigned[intOpts] = $(this).attr("Name");
          intOpts = intOpts + 1;
        });
        $("#my_SPGroupsAssigned").append(strHTMLAvailable);
      }
  });

  //Populate available site groups
  if(booErr == "false"){
    $().SPServices({
        operation: "GetGroupCollectionFromSite",
        async: false,
        completefunc: function(xData, Status) {
          $(xData.responseXML).find("Group").each(function() {
            booMatch = "false";
            for (var i=0;i<=arrOptionsAssigned.length;i++){
              if($(this).attr("Name") == arrOptionsAssigned[i]){
                booMatch = "true";
                break;
              }
            }
            if(booMatch == "false"){
              strHTMLAssigned += "<option value='" + $(this).attr("Name") + "'>" + $(this).attr("Name") + "</option>";
            }
          });
          $("#my_SPGroupsAvailable").append(strHTMLAssigned);
        }
    });
  }
}

function AddGroupsToUser(){
  var i;

  if($("#my_SiteUsers").attr("value") == 0){
    alert("You must select a user");
    return;
  }

  if($("#my_SPGroupsAvailable").val() == null){
    alert("You haven't selected any groups to add");
    return;
  }
  else{
    var arrGroups = $("#my_SPGroupsAvailable").val();
    for (i=0;i<arrGroups.length;i++){
      $().SPServices({
          operation: "AddUserToGroup",
          groupName: arrGroups[i],
          userLoginName: $("#my_SiteUsers").attr("value"),
          async: false,
          completefunc: null
      });
    }
    RefreshGroupLists();
  }
}

function RemoveGroupsFromUser(){
  var i

  if($("#my_SiteUsers").attr("value") == 0){
    alert("You must select a user");
    return;
  }

  if($("#my_SPGroupsAssigned").val() == null){
    alert("You haven't selected any groups to remove");
    return;
  }
  else{
    var arrGroups = $("#my_SPGroupsAssigned").val();
    for (i=0;i<arrGroups.length;i++){
      $().SPServices({
          operation: "RemoveUserFromGroup",
          groupName: arrGroups[i],
          userLoginName: $("#my_SiteUsers").attr("value"),
          async: false,
          completefunc: null
      });
    }
    RefreshGroupLists();
  }
}

and the associated markup:

<table align="center">
  <tr>
    <td colspan="3" style="text-align:center">
      <font style="font-weight:bold">Select a User:&nbsp;&nbsp;&nbsp;</font>
      <select id="my_SiteUsers" style="width:350px;" onchange="RefreshGroupLists()"></select>
    </td>
  </tr>
  <tr>
    <th class='ms-vh2'>Available Groups</th>
    <th></th>
    <th class='ms-vh2'>Assigned Groups</th>
  </tr>
  <tr>
    <td class='ms-vb2'>
      <select id="my_SPGroupsAvailable" style="width:250px;height:450px;" multiple="multiple"></select>
    </td>
    <td>
      <button id="my_AddGroupsToUser" style="width:80px;" onclick="AddGroupsToUser()">&gt;&gt;</button><br><br>
      <button id="my_RemoveGroupsFromUser" style="width:80px;" onclick="RemoveGroupsFromUser()">&lt;&lt;</button></td>
    <td class='ms-vb2'>
      <select id="my_SPGroupsAssigned" style="width:250px;height:450px;" multiple="multiple"></select>
    </td>
  </tr>
</table>

And here’s the net result. It’s a simple little form that does exactly what Geoff said it would. All I had to do to get it running in my environment was to change the references to the script files to point where I have them stored. Otherwise, it worked with no modification whatsoever.

image

This is the sort of thing that may belong in SPServices. Of course, you can simply copy and use the code above, but perhaps some more options and additional functionality would make it even more useful.  What do you think? Is this something you’d use?

And thanks for sharing, Geoff.

Viewing all 109 articles
Browse latest View live