Saturday, April 10, 2010

ASP.AJAX 4 Templates Part 4 - jQuery, and Manual JSON Object Manipulation

Note: This is part 4 of a multi-part series exploring building client side AJAX applications with ASP.Net AJAX 4 Templating and the WCF Data Services (aka ADO.Net Data Services, aka oData, aka Astoria) in SharePoint 2010.

Part 1 – Exploring WCF Data Services in SharePoint 2010
Part 2 – Creating a read-only page with ASP.Net AJAX 4.0 Templates
Part 3 – Writing data back to SharePoint with ASP.Net AJAX 4.0 Templates
Part 4 - jQuery and Manual JSON Object Manipulation

In the previous posts in this series I’ve shown how you can access SharePoint data using WCF Data Services, how you can display that data using ASP.Net AJAX 4.0 Templates, and how you can write data back to SharePoint.

In this post I’ll take writing data back to SharePoint a step further by showing how you can modify non-visible fields by modifying JSON objects directly. More specifically I’ll enable dragging user story index cards on a virtual bulletin board using a jQuery plugin, save the X and Y coordinates back to JSON objects, and then save the updated records back to SharePoint.

Cards on a Corkboard

The first thing I’ll do is style the cards and display them with absolute position. While I’m at it I’ll also reference the draggable jQuery UI plugin. If you’re following along at home just built a custom .js download from the jQuery UI site, drop it in your layouts directory and add a reference. So my PageHead content control now looks like this:

<asp:content
    id="PageHead"
    contentplaceholderid="PlaceHolderAdditionalPageHead"
    runat="server">

    <script type="text/javascript"
        src="../ajax/MicrosoftAjax.js"></script>
    <script type="text/javascript"
        src="../ajax/MicrosoftAjaxDataContext.js"></script>
    <script type="text/javascript"
        src="../ajax/MicrosoftAjaxTemplates.js"></script>
    <script type="text/javascript"
        src="../ajax/MicrosoftAjaxAdoNet.js"></script>
    <script type="text/javascript"
        src="../jquery-ui-1.8.custom/js/jquery-1.4.2.min.js"></script>
    <script type="text/javascript"
        src="../jquery-ui-1.8.custom/js/jquery-ui-1.8.custom.min.js">
    </script>
    <style type="text/css">
        .sys-template
        {
            display: none;
        }
        .userstorycard
        {
            border: 1px solid #777777;
            width: 200px;
            position: absolute;
            cursor: move;
        }
        .carddescription
        {
            font-size: 13px;
            background-image: url('card_bg.jpg');
            padding: 0px 5px 5px 5px;
        }
        .cardtitle
        {
            font-size: 15px;
            font-weight: bold;
            padding: 0px 0px 0px 0px;
            border-bottom: 1px solid red;
            background-color: White;
            padding: 0px 5px 0px 5px;
        }

        ...

The next thing to do is add X and Y fields to the user story list in SharePoint. And finally display the cards with absolute positioning:

<div xmlns:sys="javascript:Sys" class="background">
  <div id="userStories" class="sys-template">
    <div class="userStoryCard"
      sys:style="{{ 'left: ' + X + 'px; top: ' + Y + 'px;'}}">

      <div class="userStoryTitle">{{ Title }}</div>
      <div class="userStoryBody"><div>{{ Description }}</div>
    </div>
  </div>
</
div>

Notice the sys:style attribute. The sys: part is the namespace you need to add for attributes when using templating that I mentioned in part three. Also notice the JavaScript string concatenation inside the binding. You can put in any JavaScript in there that you like. So now if you manually set X and Y values in SharePoint for a card the card shows in the right place. But we’re still missing the ability to drag cards.

Making Cards Draggable with jQuery

In order to enable dragging support you’re supposed to call the draggable() function on the DOM elements you want to make draggable. Simple right? You just call $(".userstorycard").draggable() which should find every element with a class of userstorycard and call draggable() on it.

But if you do this onLoad() then ASP.Net AJAX Templating engine hasn’t had a chance to render the new DOM elements yet. Fortunately the DataView has the JavaScript equivalent of an OnRendered event that you can tie into:

function onLoad() {
    dataContext = $create(
        Sys.Data.AdoNetDataContext,
        { serviceUri: "/demoprep/_vti_bin/ListData.svc" }
        );

    dataView = $create(
        Sys.UI.DataView,
        {
            autoFetch: true,
            dataProvider: dataContext,
            fetchOperation: "UserStories",
            fetchParameters: { $top: 20 }
        },
        {
            rendered: onRendered
        },
        null,
        $get('userStories')
    );
}

function onRendered() {
    // from http://jqueryui.com/demos/draggable/
    $(".userstorycard").draggable();
}

And that’s it for enabling dragging. Pretty simple. The result looks like this:

Ok, well you can’t see the dragging support, but it’s there, trust me. And it’s pretty cool except for one thing.

Saving Non-Visible Properties

The problem is that every time you refresh the page the nice layout you’ve done disappears and everything goes back to being stacked one on top of the other in the upper left. My first thought was to create input type=hidden form elements on the page. This approach causes a lot more plumbing code than is necessary. The better way is that you can access the underlying in-memory JSON objects. Step one is to register an “event” for when the user stops dragging a card.

function onRendered() {
    // from http://jqueryui.com/demos/draggable/
    $(".userstorycard").draggable({
        stop: onDragStop
    });
}

Once you wire that up the onDragStop() function is where all the interesting stuff happens:

function onDragStop(event, ui) {
    var userStoryCard = ui.helper[0];
    var selectedUserStoryJsonObject =
        dataView.findContext(userStoryCard).dataItem;

    var newX = ui.position.left;
    var newY = ui.position.top;

    Sys.Observer.setValue(selectedUserStoryJsonObject, "X", newX);
    Sys.Observer.setValue(selectedUserStoryJsonObject, "Y", newY);
    dataContext.saveChanges();
}

The first line gets the DOM element that just the user just completed dragging. The second line retrieves the in memory JSON object represented by that DOM element. The next couple of lines get the X and Y that the user story card was dragged to relative to their parent DOM element. And then something odd happens.

Sys.Observer.setValue looks so complicated. Why couldn’t we just call selectedUserStoryJsonObject.X = newX?

Well, if we’d done that the DataView wouldn’t know about the change we just made. If we then tried to call dataContext.saveChanges() nothing would get sent back to the server. Sys.Observer.setValue notifies any interested parties, in this case the DataView, that a value has been changed. And this enables the functionality we saw in part two where the DataView only sends the relevant records back to SharePoint instead of the entire set of JSON objects.

And so, with this code in place, we can now move user story cards around on the page and when we refresh the page or come back days later our layout has persisted back into SharePoint.

Conclusion

In this post you’ve seen how to directly modify the in-memory JSON objects and send them back to SharePoint. Our application is finally getting interesting and I find it quite remarkable how little code it’s taken. In the next post I’ll show how to do master-detail scenario’s for editing user story cards.

2 comments:

Kevin said...

I've been reading a ton of posts, articles, and blogs on accessing SharePoint 2010 List data and your blogs are by far the best I've read. One question. I like the approach using the Msf JS libraries, but I can't find a source for them. Can you point me in the right direction?

Lee Richardson said...

Thank you. I had the same question and posted it to StackOverflow here

I also describe it in the EndUserSharePoint where I've written up a parallel (and more up to date at this point) version of this series:

The specific post is here.

And the list of all of the articles in the client side ajax apps series is here.