Using spservices to create discussion and reply in a Discussion List

This updated blog post describes how I go about building a threaded, inline comments system for any page, using a SharePoint Discussion List as the backend storage for threaded data.

I choose to use SPServices because originally, the webpart is built for a SharePoint 2007 environment.  

I've rebuilt the webpart using the JavaScript code and confirm it runs happily in SharePoint 2013, and this time, with much better screenshots!
In SharePoint 2010 and 2013, you should be able to talk directly to the REST endpoint to perform read and updates.

Enter the Discussion List

 

Many users have a love-hate relationship with SharePoint's Discussion list.  Perhaps more hate.  The main culprit is the inflexible UI.  Lack of many modern forum needs:

  • Can't show all discussions and show threaded replies together
  • Can't do inline new discussion
  • Can't do inline replies
  • Poor support cross browsers
  • Can't easily filter for a subset

But still, the list itself is very functional.

  • Does support threaded discussions
  • Calculates total replies and last discussion updated
  • Has email notification through SharePoint alerts
  • Has tweaked permission model where users can create but not modify their own posts, or can edit.

 

Image888

Figure: A basic SharePoint Discussion List

 

Let's ignore the problems with the native Discussion List UI, and build our own with JavaScript

Read from the Discussion List

 

/*
This code queries using SPServices' GetListItems on list and populates a UL.discussions dom element on the page
*/
var $ul = $("ul.discussions");
var list = "My Discussions";

function getDiscussionList(list, $ul) {
var promise = $().SPServices({
  operation: "GetListItems",
  listName: list,
  CAMLViewFields: "<ViewFields><FieldRef Name='Title' /><FieldRef Name='Body' /><FieldRef Name='Author' /><FieldRef Name='Modified' /><FieldRef Name='DiscussionLastUpdated' /><FieldRef Name='ItemChildCount' /></ViewFields>",
  CAMLQuery: "<Query><OrderBy><FieldRef  Name='DiscussionLastUpdated' Ascending='FALSE' /></OrderBy></Query>"
});

promise.done(function(){
  var items = $(promise.responseXML).SPFilterNode("z:row").SPXmlToJson({
    mapping: {},
    includeAllAttrs: true,
    removeOws: true
  });
  $ul.empty(); // clear old list

  $.each(items, function(index, item) {
    // render each discussion
    var $li = $("<li class=discussion' />");
    $li.append(item.Title);
    $li.append(item.Body);
    $li.append(item.Author);
    $li.append(item.Modified);
    $li.append(item.DiscussionLastUpdated);
    $li.append(item.ItemChildCount);
    $li.append(item.FileRef);
    $li.append("<div class='replies'>replies</div>");
    $ul.append($li);

  });  // end items.each
}); // end promise done
promise.fail(function(xhr, status, error) {
  alert(xhr.responseText);
}); // end promise fail

} // end getDiscussionList


Key points:

  • List.asmx - GetListItems
  • This returns the top level items in the list, which are Discussions

 

Figure out the threaded discussions


In SharePoint discussion lists, the top level "Discussion" items are folders.  The subsequent "Message" reply items are list items within that folder.
So to query for the replies to a discussion, we query the list with the filepath of the top level Discussion item as the query filter.

/*
This second function figure out the threaded replies when you expand one discussion
*/

Note: The FileRef usually has a value that looks like this:
// 15;#Company/Site1/Web1/Lists/My Discussions/15_.000
You need to clean the URL and use only the file path:

function getFilePath(fileRef) {
  if (!fileRef) return;
  var m = /;#(.*)$/.exec(fileRef);
  if (m) {
    return m[1];
  }
}

This will give you:
Company/Site1/Web1/Lists/My Discussions/15_.000

function getDiscussionReplies(list, filepath) {
var options = {
  operation: "GetListItems",
  listName: list,
  CAMLViewFields: "<ViewFields><FieldRef Name='Title' /><FieldRef Name='Body' /><FieldRef Name='Author' /><FieldRef Name='Modified' /><FieldRef Name='DiscussionLastUpdated' /><FieldRef Name='ItemChildCount' /></ViewFields>",
  CAMLQueryOptions: "<QueryOptions><ViewAttributes Scope='RecursiveAll' IncludeRootFolder='True' /></QueryOptions>",
  CAMLQuery: "<Query><Where><Contains><FieldRef Name='FileRef' /><Value Type='Text'>" + filepath + "</Value></Contains></Where><OrderBy><FieldRef  Name='FileRef' Ascending='TRUE' /></OrderBy></Query>"
};
var promise = $().SPServices(options);

promise.done(function(){
  var items = $(promise.responseXML).SPFilterNode("z:row").SPXmlToJson({
    mapping: {},
    includeAllAttrs: true,
    removeOws: true
  });

  $.each(items, function(index, item) {
    // render each reply
    // snipped

  });  // end items.each
}); // end promise done

} // end getDiscussionReplies

 

Key points:

  • Use CAMLQueryOptions for RecursiveAll
  • Use FileRef contains FilePath to find all threaded (Message) replies

Create a Discussion in the Discussion List

To create a new discussion, you just need to create a new item in the Discussion List

var list = "My Discussions";
var title = $("input.title").text();
var body = $("textarea.body").text();

function newDiscussion(list, title, body) {

var promise = $().SPServices({
  operation: "UpdateListItems",
  batchCmd: "New",
  listName: "Team Discussion",
  updates: "<Batch OnError='Continue' >" +
       "<Method ID='1' Cmd='New'>" +
        "<Field Name='ContentType'>Discussion</Field>" +
       "<Field Name='FSObjType'>1</Field>" +   // Important: FSObjType = 1 means that this is a folder.  If this isn't specified SharePoint sometimes create the wrong root level item.
        "<Field Name='Title'>" + escapeColumnValue(title) + "</Field>" +
        "<Field Name='Body'>" + escapeColumnValue(body) + "</Field>" +
       "</Method>" +
      "</Batch>"
});
   
promise.done(function(){
  var $ul = $("ul.discussions");
  getDiscussionList(list, $ul);

}); // end promise done

}

 

Image893

Figure: Start a new discussion inline

Key points:

  • List.asmx - UpdateListItems
  • Set FSObjType = 1, this is necessary or there were head-scratching bugs

 

(Optional) Filter a Discussion List to only related item

 

In the Discussion List settings, you can add a lookup column RelatedItem to an external List (or Document Library).
Then when you use the JavaScript above, you can filter your CAML Queries so that they only return elements that are related to the current page.

For getDiscussionList:

CAMLQuery: "<Query><Where><Eq><FieldRef Name='RelatedItem' /><Value Type='Number'>" + related + "</Value></Eq></Where><OrderBy><FieldRef  Name='DiscussionLastUpdated' Ascending='FALSE' /></OrderBy></Query>"

And for creating discussion threads:

updates: "<Batch OnError='Continue' >" +
     "<Method ID='1' Cmd='New'>" +
      "<Field Name='ContentType'>Discussion</Field>" +
      "<Field Name='FSObjType'>1</Field>" +
      "<Field Name='Title'>" + escapeColumnValue(title) + "</Field>" +
      "<Field Name='Body'>" + escapeColumnValue(body) + "</Field>" +
      "<Field Name='RelatedItem'>" + related + "</Field>" +
     "</Method>" +
    "</Batch>"


In my project, I linked my discussion list to a Video Library, and each Video has an area for creating comments and reply to threaded discussions.  All the conversation are saved in one single Discussions List, but filtered on each page as required.

Understand the nested format for Discussion List items

In a SharePoint Discussion List:

  • Discussion (content type) is a Folder.  FSObjType = 1
  • Message (content type) is a List Item.  FSObjType = 0

 

Reply in a Discussion List.


var list = "My Discussions";
var filepath = "Company/Site1/Web1/Lists/My Discussions/15_.000";
var body = $("textarea.body").text();

function replyDiscussion(list, filepath, body) {

// RootFolder needs to start with /

var promise = $().SPServices({
  operation: "UpdateListItems",
  batchCmd: "New",
  listName: list,
  updates: "<Batch OnError='Continue' RootFolder='" + "/" + filepath + "'>" +  // RootFolder needs to start with /
     "<Method ID='1' Cmd='New'>" +
      "<Field Name='ContentType'>Message</Field>" +
      "<Field Name='FSObjType'>0</Field>" +
      "<Field Name='Body'>" + escapeColumnValue(body) + "</Field>" +
     "</Method>" +
    "</Batch>"
});
promise.fail(function(xhr, status, error) {
  alert(xhr.responseText);
});
promise.done(function(){
  getDiscussionReplies(list, filepath);
});

} // end replyDiscussion

 

Image882

Figure: Threaded Discussions, with inline reply.


Key points:

  • List.asmx - UpdateListItems
  • Set FSObjType = 0 (for List Item), this is necessary or there were head-scratching bugs
  • Set RootFolder to the Filepath of the parent Discussion (folder) item.  This way, UpdateListItems creates a new list item within that folder.

 

 

Conclusion: here we are.  Threaded, inline, replies.


Combine everything together, the javascript lets me now do:

  • Tie a Discussion List to a Picture Library
  • Start one or more discussion threads on any Picture
  • See replies inline in one single UI.
  • Inline comment-creation
  • Inline comment-reply

 

  • Inline Edit and Inline Delete is possible, but the end-user will need to follow SharePoint permission settings on the Discussion List.  I have not written code to do those operations. 

Living with the Surface RT for 4 weeks

 

Across the new years holiday, I took a long four week holiday off in Indonesia with my family and in-laws.  Previously I have always taken my Dell laptop(s), but this time, I decided to take my Surface RT tablet.  (Yes, the first generation one, not the Surface 2).  I have owned the Surface RT for over a year and while I always thought it is a decent device, it lacked the number of Apps that the iPad has, and lacked the ability to run old x86 apps.  I wanted a decent attempt at using only the Surface RT for a number of weeks, and figure out where I stand on the device.

My Surface RT has been regularly updated and runs the latest Windows 8.1, it also synchronizes with my Microsoft Account and shares the Apps I have purchased on my main machine. 

In Indonesia, I have access to slow internet, based on where I was.  I didn't have mobile data, and turned off roaming.  So I relied on the Surface RT for both online and lots of offline activity.

So, consider this my report card.

The good:

  • Mail (not Outlook)
    The built in Mail app is very handy.  It downloads both my gmail and Outlook.com emails, which are available and fast to browse and read.  I can compose emails offline which is very handy.  I did not use either gmail or outlook.com websites during my 4 weeks.
  • Internet Explorer
    Is surprisingly useful for almost everything else:
    • Newsblur
      Runs great and I was able to catch up on all my blog reading.
    • Twitter
      Runs fine and I was able to read and write tweets.
    • Facebook
      Runs fine.  Although the people Tile was updated regularly, I find the browser experience for Facebook on par with what I have at home.  My wife isn't so impressed with many of the Flash-based Facebook games, some do run, but is sluggish.  I wondered if the new Surface 2 would handle these a lot better.
    • Reddit
      Actually works very well.  The only thing I missed is the Chrome extension: RES.  But I've stopped using Google Chrome a while back and increasingly don't really miss that plugin.  I will probably look for an App for Reddit next time.
  • Office
    I had to read a number of attachments: Word, Excel and PowerPoint on my holidays.  The built in Office did the trick, and I didn't feel I missed a beat.
    I had not set up Outlook to my office exchange server, and didn't want to start downloading a lot of emails on a slow data connection.  Luckily, my colleagues were really nice to me and didn't send me much work!  All the Office documents I was reviewing are related to conferences and activities throughout the year.
  • Apps
    I wanted to download a bunch of games and play them everywhere, but honestly I've stuck with Tiny Death Stars and Frozen Freefall - both great apps by Disney and available across both Windows 8 store as well as WindowsPhone.  Very happy with the games.
    I also downloaded a manga application and had it download some manga for offline reading. 
  • Account sync
    I really enjoyed the tiles' positions being synchronized across my Windows 8.1 desktop at home and the Surface RT.  I had no trouble remembering where my apps are. 

 

The bad.

  • Touch Cover
    The touch cover, oh how much I wanted you to work, but you are just not comfortable.  I can type reasonably well on the touch cover, but I'm afraid I will never be able to type perfectly.  I need the Type-Cover.  Rumour goes there is a better Powered-Type Cover coming.  I can't wait.
  • Windows Updates and poor battery management
    Windows Update must have ran at some point, and I found the Surface poor at handling the battery when I'm not using it.  I did read there was a firmware over December that was causing a lot of issues but I thought it was only with the Surface PRO devices.  This one was not good.  I could use the table for half a day.  Close it and put it down.  And there won't be any battery left when I pick it up again in the evening. 
  • No Windows Live Writer
    I wanted to update my blog, but without a good blog writing software, I was stuck with the web interface.  I ended up writing most of my blog on OneNote, and then copy the text over to the web interface to post to my blog site.
    A Windows 8 Store Blog Application needs to be a thing.

 

The surprise.

  • Dropping the Surface RT
    I dropped the Surface RT - face down, from a bedside table onto the wooden floor.  Luckily there was no damage.  I... don't want to try this again.
  • Rotational Lock and reading manga
    Rotational Lock and how easy it was to access from the charms bar was great.  On the Windows Phone rotation lock is a bit harder to reach in the settings.
  • Fast charging
    The Surface charges really quick.  From a depleted Surface it can be charged within 2 hours.
  • Take picture from lock screen
    I discovered that you can swipe down from the lock screen and the Surface RT will activate the camera!  I was not able to do this on my laptop - I can't swipe the lock screen down.

2014 begins with a SharePoint Server MVP Award

Sometime in the early hours of the 2nd of January (I live in Australia time), I received a confirmation email from Microsoft that I've been awarded MVP for 2014. The award is for contribution in SharePoint Server technical communities.

I am extremely honoured to be counted with many long time MVP that are the faces in the community.

Thank You

There is a long list of people I need to thank:

Ivan Wilson - boss and long time SharePoint veteran in Sydney. Runs the Sydney SharePoint user group. Ex-5yr-MVP but just got too busy after our company started growing (a certain trouble-maker, me, joined the company). Dear leader, I'm glad to finally return an MVP Award to the shelf!

Jonathan - boss, appearing briefly in the user groups, but understood and supported Ivan and my adventures in the community. Lets me fly or travel to all the events. May be its because he likes flying as well.

My colleagues Justin, Jumpei and Bart for being my test audience for some really rough presentations. On the positive side, they have seen presentations that has never made it out of the office door - too geeky or just too crazy.

My lovely wife Lina who has to deal with my flying everywhere and now also handle two kids like a PRO. She knows this award made me really happy, but let me tell you: Behind every great man, is a wife who isn't impressed (and no, I'm not great - though she definitely remains not impressed).

Brian - from our first meeting in a Canberra pub where I try to sell an MVP the virtues of running reporting services integrated with SharePoint - not knowing who he was. It has been great to go to the numerous SharePoint Saturday events around Australia. There are too many shenanigans to list, and plenty of fun memories.

Elaine - the MCM that I know the most, and the MVP that I tried to follow. Friendly, relaxed knows everything. Seems to be everywhere, all the time. You realise as you attend the sessions that Elaine and other MVPs hold - there's so much experience and knowledge and they make it into a session that people can consume and learn from.

Adam Cogan - MS Regional Director and running the oldest user group in Sydney. Adam's SSW was the first user-group company that I worked in, and showed me the value of the communities that I've come to love. Even as I focus only on SharePoint and away from general .NET, I still get to see Adam in the communities rallying the crowd. I've always held Adam as a master magician. But I've realised that a magician is also a teacher, and Adam may be the best that I know.

Debbie Ireland - for running the SharePoint Conference in Australia and NZ. The premier event for SharePoint in the Oceanic Region and letting me present for the last few years!

So Young Lee - our MVP Lead, I missed a good opportunity to catch her in TechEd last year. So have only talked via email. Thank you for granting me the Award!

Mark Rhodes, Daniel Brown, Daniel McPherson, James Milne, Ishai, Sezai, so many other MVPs for being an inspiration to me, both with what you knew, and the efforts you put into the community. Thank you for being great examples, made it fun to be in the community and I look forward to more MVP in the future!

The Process

I don't believe there's a Rule about not talking about the MVP Award process. So here is how it works, as I understand it:

  • The MVP Award is given by Microsoft for contributions to the community in the previous year.
  • You need to be nominated - anyone could nominate, including self nomination, I personally think there's probably a filtering that happens. It may help if the MVP lead in your region knows at least your name, or if the nomination came from someone well known.
  • Then you fill out a really complicated score card of all the events and activities that you have participated in the last year. There seems to be a few different versions, Excel or web application. The end result is to quantify your community reach.
  • Finally, you are told which round your application is being considered, and you hold your breath on that day. Mine happens to be 1st of Jan.

I've been holding my breath the entire week.

Get Involved:

  • Go to your user group. Present. Lots of users groups around Australia also accepts remote presentations.
  • Present at the SharePoint Saturday events when they roll around your city. Or volunteer to visit another city!
  • SharePoint Conference in Australia and NZ
  • TechEd AU
  • Write a blog. Keep it fresh. My blog documents a number of interesting techniques with InfoPath and has been referenced continuously from the Microsoft forums - even long since I lessened my involvement in that community. Engage with people commenting on your blog.
  • Create a CodePlex or github project relating to SharePoint
  • Participate in SharePoint on StackOverflow
  • I'm told if you write a book or runs web series on SharePoint that would help.

Remember you'll need statistics from all these activities for the score cards.

Getting nominated:

  • Get to know the people in the SharePoint community around you.

This helps for your nomination. I personally don't know who nominated me. I wasn't sure whether to ask anyone or just wait. I decided to wait and someone did nominate. I have a few guesses but honestly I have no idea.

Keep a positive, can-do attitude with SharePoint

Finally, we work with SharePoint day in and out. We know it's got lots of quirks. I personally believe it's not good to dwell on the bad but focus on what you can do about it. It's all too easy to jump on the wagon complaining about what SharePoint does. It is far better value and more useful to the community when we explain why SharePoint does what it does, and what we can do to tell it to do something else that we wanted.

Happy New Year!  It's been awesome so far.

SPSSYD 2013 and special thanks to Brian Farnhill

I wanted to thank Brian Farnhill for organizing SharePoint Saturday (SPS) events in Australia faithfully for the last few years.  SharePoint Saturday Sydney 2013 was the last one where he is the official organizer, chief, keynoter, label-printer, sponsor-chaser, etc. etc.

The event had a lot of highlights for me:

  • A lot of people showed up early and was ready at the keynote. 
  • Coffee Cart showed up on time in the morning
  • The Clifton venue (they moved since last year) was amazing - I really liked the layout of the sofa and the meal table-benches were great for conversation
  • Lunch was hopefully just enough - I think right at the end we might have just ran out of sandwiches
  • Adam got rick rolled'
  • Ross' session had a memory moment (I heard second hand)
  • My session had a major projector fail (more on this later)

 

Presentation - Typescript PowerPoint and demo project

 

Explanation - What happened to the projector / your laptop?!

 

I was using a USB-3 display link adapter for the last month.  But I didn't realize it would behave very badly with the HDMI-VGA dongle for the projector at the event.  A quick fumbling got the display to work.  But it was black and white but I was going to run out of time so I just ran with it.

I'm glad you guys had laughs at my expense.  It made me feel a little bit less miserable.

Love you guys.

InfoPath - binding Linked Picture Display Text for dynamic tooltips

InfoPath is an ideal tool for XML forms.  Whenever you have forms, invariably, you have little helper icons to suggest text to your users regarding how to fill out a form.

In InfoPath, the Picture control allows you to have alt text.  But this is hard coded and you can't easily change this without modifying your form template.  If you are using the icons in a repeating section, there is also no way to make them different, and display different tooltip based on different rows in the sections.

 

That's all about to change.  :-)

The Linked Picture Schema

I was experimenting with a different control in InfoPath: the Linked Picture control.

http://msdn.microsoft.com/en-us/library/dd959291.aspx

<img hideFocus="1" class="xdLinkedPicture" xd:boundProp="src" xd:binding="my:field1" 
xd:boundPropSecondary="displaytext" xd:binding_secondary="my:field1/@my:field2"
tabStop="true" tabIndex="0" xd:xctname="LinkedImage" xd:CtrlId="CTRL2"> <xsl:attribute name="src"> <xsl:value-of select="my:field1"/> </xsl:attribute> <xsl:attribute name="displaytext"> <xsl:value-of select="my:field1/@my:field2"/> </xsl:attribute> <xsl:attribute name="alt"> <xsl:choose> <xsl:when test="string-length(my:field1) &gt; 0"> <xsl:value-of select="my:field1/@my:field2"/> </xsl:when> <xsl:otherwise>Click here to insert a picture</xsl:otherwise> </xsl:choose> </xsl:attribute> </img>

My highlights in the above XML:  the Linked Picture control, at the schema level, supports two level binding - both the image's SRC attribute as well as the DISPLAYTEXT attribute.  Which is then translated to the alt attribute in the XSLT.

Very interesting.

 

Hmm...  It has always been there!  Since 2010.  Why didn't anyone tell me this

I thought I might need to resort to XSLT hacking in order to get this to work, but I decided to look at the Ribbon controls for the Linked Picture control in InfoPath.  Behold!

clip_image002[5]

There are two sections of binding on the Control Tools ribbon for a Linked Picture control.  The left one, controlling the main binding, is for the SRC.  The right is actually for the Display Text!

 

Prepare a file to bind to

The XML file you can bind to:

<?xml version="1.0" encoding="utf-8"?>
<html>
    <img-info>http://server/Style Library/Images/info.png</img-info>
    <tooltip1>These guys are cool!</tooltip1>
</html>

Add this as a secondary data source, and bind the two properties to fields on this XML file.

 

Store the XML on SharePoint server

image

I suggest storing this file on the SharePoint server.  And then configure InfoPath to obtain the XML from the source server.  This way, each time you open the Form, it will read the most up-to-date version.

 

Content is now dynamic

This means you can update the image src or title attribute in the XML file dynamically.

You can also update the image without having to redeploy the form.

If you want to change an icon in your form - now they are all stored within SharePoint.  If you want to change the display text, change it in the XML file in SharePoint.  No template re-deployment required.

Your business analyst wants to tweak the words used in the tooltip?  Get him to update the XML file directly in SharePoint.  As long as he doesn't break the XML encoding it's very simple.  No need to update InfoPath.

 

Image is dynamic.  Form template just got smaller

A bonus of this technique is that the image isn't stored inside the template file.  So your template is now smaller.

 

 

Two independent bindings

The two binding are independent, and don't have to come from the same node in a XML source.

This means, you can have a repeating section showing tooltip icons.  Each image's SRC is bound to the same image URL.  But each tooltip can be bound to a repeating field in the repeating section. 

 

Example

Tooltip working - now works by binding:

  • In this case here, the Picture is bound to the URL from the static XML file.
  • The tooltip text is bound to the database field returned from the service call.

clip_image002[7]

 

Works in both Web Forms and InfoPath client forms

Works in InfoPath client as well as web forms*

 

* Web Forms has an issue where sometimes Javascript will overlay on top of your Linked Picture, preventing the tooltip from showing up.  You'll need to add a small bit of Javascript to cancel this when the Linked Picture control is read-only.