Attribute Change Detection for SharePoint 2010 Row Selection

SharePoint 2010 has some quirky architecture to handle row selection in list views. We’re all familiar with this from navigating around lists. In the list views, we have a few choices on how we select rows to work with (depending on the list view settings):

  • Clicking the checkbox to the left of each item
  • Clicking on the row itself to highlight and select/deselect it
  • Clicking on the checkbox in the title row to toggle ”’Select or deselect all items’

List View Row SelectionThe are times when we’d like to be able to intercept those actions so that we can manage other content in the page. It’s not so simple if you’d like it to work across browsers.

In this example, we were building a performance review capability into My Sites. Each person would set goals by adding items to the Goals list and then they and others could comment on those goals in a Comments list. It was more complicated than this, but you get the idea.

One of the complexities was that we didn’t want to allow comments on draft goals, and there were several other limiting circumstances as well. Suffice it to say that we needed to catch any of the events above so that we could count up various characteristics of the selected goals and change the behavior of other list views and links on the page. Trust me when I say that simple old Web Part Connections didn’t hack it; we needed some custom script to tie it all together.

For this post, please consider all of the code snippets examples, as your environment may well vary. (I shouldn’t need to say this, but I get a lot of “this didn’t work for me when I copy/pasted it into my page” comments.)

Getting this to work in Internet Explorer was relatively simple.

// You'd use your unique Web Part id here
var goalsWP = $("#WebPartWPQ2");
var goalCheckboxes = $("input.s4-itm-cbx");
var goalRows = $("tr.ms-itmhover");
var goalSelectAll = goalsWP.find("input[title='Select or deselect all items']");

// When the user checks a goal's checkbox...
goalCheckboxes.change(function() {
  countSelected();
});
// ...or clicks on the goal's row to select...
goalRows.click(function() {
  countSelected();
});
// ...or clicks the 'Select or deselect all items' checkbox...
goalSelectAll.change(function() {
  countSelected();
});

The countSelected() function does all the stuff we need, but the contents of the function don’t really matter for this post.

The problem came when we started looking at things in Firefox and Safari on PCs and Safari on Macs. All of the browsers have slightly different behavior when it comes to bubbling up events. We went through several iterations on this which would fix things in one browser while breaking them in another.

When I looked to the Web for some hints, I found a great post from Darcy Clarke. What we needed was very similar to what Darcy has done in his .watch() jQuery function. In his case, he wanted to detect CSS changes to elements, perhaps the background color changing. In our case, it was more than just a CSS change, but his code formed a good basis. Someone named Frank posted some tweaks to Darcy’s original function that made it work with any element attributes, not just CSS changes.

This is an area where the browsers are fairly different, unfortunately. IE has its unique propertychange event, where the other browsers have their quirks as well, though DOMAttrModified is sometimes supported. The article points out that DOMAttrModified is not universally supported yet, though – perhaps not yet in Chrome and Safari – though the article is a bit dated. The fallback is to attach a check to the element using setInterval. This latter approach is expensive, so it should be the last resort.

In the case of the row selection in SharePoint list views, we can rely on the fact that each tr’s class will change from “ms-itmhover” to “ms-itmhover s4-itm-selected” when the row is selected. All this comes down to the odd way Microsoft has set up the selection behavior using a row click and highlight, which is using table rows rather than divs.

What I ended up with is actually pretty good, and I expect I’ll use it again. This isn’t the first time I’ve wanted to watch a DOM element for *any* change, and not just an event-triggering one. It’s an adapted version of Darcy’s .watch() function, with Frank’s additions, tweaked to use feature detection rather than the deprecated (removed in modern versions) of jQuery’s $.browser function. On top of that, to prevent a ridiculous number of calls to countSelected(), which is doing some reasonably heavy lifting, I added an additional check to make absolutely sure that the class has changed by saving the current class into data on the row element and checking it on each firing. It doesn’t seem as though this should be necessary, but I was seeing .watch() fire quite a few more times that I needed.

Here’s the resulting code to detect the different types of user actions:

// When the user checks a goal's checkbox...
goalCheckboxes.change(function() {
  countSelected();
});
// ...or clicks on the goal's row to select...
goalRows.watch("class", function() {
  // Make sure the class has actually changed
  if($(this).data("class") !== $(this).attr("class")) {
    $(this).data("class", $(this).attr("class"));
    countSelected();
  }
});
// ...or clicks the 'Select or deselect all items' checkbox...
goalSelectAll.change(function() {
  countSelected();
});

And the revised .watch() function:

$.fn.watch = function (props, callback, timeout) {
  if (!timeout)
    timeout = 10;
  return this.each(function () {
    var el = $(this),
    func = function () {
      __check.call(this, el)
    },
    data = {
      props : props.split(","),
      func : callback,
      vals : []
    };
    $.each(data.props, function (i) {
      data.vals[i] = (!el.css(data.props[i]) ? el.attr(data.props[i]) : el.css(data.props[i]));
    });
    el.data(data);
    if (typeof(this.onpropertychange) == "object") {
      el.bind("propertychange", callback);
    } else if (this.DOMAttrModified) {
      el.bind("DOMAttrModified", callback);
    } else {
      setInterval(func, timeout);
    }
  });
  function __check(el) {
    var data = el.data(),
    changed = false,
    temp = "";
    for (var i = 0; i < data.props.length; i++) {
      temp = (!el.css(data.props[i]) ? el.attr(data.props[i]) : el.css(data.props[i]));
      if (data.vals[i] != temp) {
        data.vals[i] = temp;
        changed = true;
        break;
      }
    }
    if (changed && data.func) {
      data.func.call(el, data);
    }
  }
}

Problem with jQuery 1.7+ and SPServices

<UPDATE dateTime=”2012-12-04″>

This issue is resolved in v0.7.0+. See this post for more details, including the SPFilterNode function. </UPDATE>

<UPDATE dateTime=”2011-11-15T11:59GMT-1″>

Steve Workman has come up with a much improved way to do the selecting for things like z:row in general – it’s much faster, as Steve’s statistics show – and it also works with jQuery 1.7. I’ve added it to the latest alpha for v1.6.3 (which I will probably rename v0.7.0). More details to come, but this issue is going to be resolved in the upcoming release of SPServices.

</UPDATE>

Alert reader Dustin Meany pinged me yesterday with a brief email through this blog:

http://bugs.jquery.com/ticket/10377 — I think you should update the documentation for SPServices…

Ah, if the issue were only as small as his email. The ticket on the jQuery site talks about the fact that the notation that I’ve been recommending to parse through the results of a call to the Web Services, like:

$(xData.responseXML).find("[nodeName='z:row']").each(function() {
  // Do something
});

is no longer valid in jQuery 1.7.

I’ve confirmed in my main test pages that jQuery 1.7 no longer matches on any z:rows in calls to GetListItems. That breaks the majority of the “value added” functions in SPServices. The [nodeName='z:row'] notation has proved highly useful to ensure cross-browser compatibility, and that was the reason I’ve been recommending it in the first place. Because GetListItems uses the somewhat odd namespace of z:row, not all browsers react the same way. Of course GetListItems is the single most used Web Service operation with SPServices. Most of the other operations, which are used less often, do not use this type of namespace.

So the question is how we as a development community handle this. Based on the suggestion in the jQuery ticket, I could implement a function in SPServices itself that probably will solve the issues for the SPServices value-added functions. There are only ten calls in SPServices at the moment that use the [nodeName='z:row'] notation. However, there are thousands of you out there who have your own custom scripts written on top of SPServices that will break if you move to jQuery 1.7+.

The suggestion of switching to the .find("z\\:row, row") notation may work. I’ve quickly tested it with IE9, Chrome 15, Safari 5.1, and Firefox 7.0, all on my Windows 7 laptop. I don’t trust my cursory test, of course; there’s more work to do.

I hold no sway over the jQuery development team. They make their decisions without any knowledge of my little SPServices library or probably even SharePoint.

This, along with the XSL timeout issue I wrote about yesterday, may prove to be a big blow to those of us who choose to develop in SharePoint’s Middle Tier. As the standard bearer for this little movement, I can only do so much to make noise about how important and useful these development techniques are. When these types of roadblocks are put up, there’s little I can do to change things.

SharePoint and Your Mobile Strategy: Be Sure to Have One!

These days, if you have a site running in SharePoint the odds of someone coming to it with a mobile device are increasing extremely rapidly. This is certainly true if you have an Internet-facing site, but also highly likely even for corporate Intranets.

For this post, I’m going to pick on my friends at the Boston Area SharePoint Users Group (BASPUG) and then I’m going to help them fix the problem.

If you go to the home page for BASPUG on an iPhone (and probably any device that declares itself as a mobile one), you get a very basic WAP-like view from SharePoint 2010.

photo 3

And scrolling down to the bottom of the screen:

photo 2

This is nothing like the actual home page of the site, with its useful and interactive content:

image

Even if I scroll down to the bottom of the mobile view on my iPhone, I can’t switch to the full view of the site. In case you don’t know, the iPhone runs Safari, which is a fully functional browser. The screen is just smaller and you need to pinch and zoom a bit to view full-sized Web sites. So I’d actually rather switch to the full site so that I can read what I know is there, but I can’t.

So my point here isn’t that the mobile view in SharePoint 2010 sucks. Sure, it’s probably not what you want out of the box, but the engine is there and it’s fixable. My point is that you have to have a mobile *strategy* for your SharePoint installation.

In many cases, the best strategy will be to just turn it off. The mobile view above doesn’t enhance my experience, it ruins it. That’s not the starting point that you want to have for your users. IMHO, it’s better for them to have to navigate the full site in a cumbersome way than to not be able to read anything useful.

So be sure to have a strategy for mobile, even if it is to admit that you don’t have one. Put it into your deployment plans so that you don’t forget about it later. Revisit it when you have to or have the time to deal with it after launch. But *don’t* forget about it.

In looking around for how to turn the mobile view off in SharePoint 2010, it was no surprise that I came upon a blog post from my pal Randy Drisgill, SharePoint branding guru, which explained it: SP2010 Branding Tip #6 – Mobile Browsers. You can edit the compat.browse file in

C:\inetpub\wwwroot\wss\VirtualDirectories\[Your Web App Here]\App_Browsers\compat.browse

to change the settings for specific browsers. Read @drisgill‘s post for the details.

DOM Inspectors for Different Browsers

I’ve been using the Internet Explorer Developer Toolbar for years with Internet Explorer 7 and below.  When Internet Explorer 8 came out, it had these tools baked right in and called the Developer Tools.  (You can activate the Developer tools by hitting F12 in IE8.)

But what if you need to peek into the DOM with Safari or Firefox?  Here are the tools for the Windows platform:

Safari

From Safari help:

To examine the resources of a webpage:

  1. Open the website, and then choose Develop > Show Web Inspector. The Web Inspector’s sidebar lists the categories of resources found on the page, such as documents, style sheets, and scripts.

    Note:If the Develop menu does not appear in the menu bar, open Safari preferences, click Advanced, and select “Show Develop menu in menu bar.”

image

Firefox

From the Firebug Web Site:

Firebug integrates with Firefox to put a wealth of development tools at your fingertips while you browse. You can edit, debug, and monitor CSS, HTML, and JavaScript live in any web page.
Visit the Firebug website for documentation, screen shots, and discussion forums: http://getfirebug.com