Monday, 1 July 2013

SharePoint 2013 Web Parts And The App Model, Part 3: The Web Part "Client" Architecture

Chris Domino, Director, Enterprise Architect

In part one of this four part series, I started with a discussion about the new development paradigms SharePoint 2013 presents. Then, part two completed the data access layer for our SharePoint 2013 web parts. Now it's time to take a look at the web part itself. I'll finish these posts with the deployment story in part four.



Like I said in those previous posts, our web parts are built starting from the out-of-the-box visual web part template (which implies they are part of a WSP, which will be important in part three) in Visual Studio 2012. After adding a new web part, the first thing I do is pretty up the Elements.xml and whatever.webpart files as follows:






  • Line #5: Set the "Group" property to the same value for all web parts, which will almost always be something like "(Name of Project) Web Parts."






  • Line #10: Set the "Title" to a pretty version of what the template barfs out.
  • Line #11: Set the "Description" to something, you know, descriptive.



Next, let's look at the code behind. All of my web parts inherit from a base one named, creatively, "BaseWebPart." I still consider this to be kosher given Microsoft's new guidance against using the SharePoint server object model, since we're not referencing Microsoft.SharePoint.dll. Everything here is ASP.NET code that could never be blamed for botching a SharePoint upgrade or migration.


Code Listing 1

  1. public class BaseWebPart : WebPart
  2. {
  3. #region Events
  4. protected override void OnInit(EventArgs e)
  5. {
  6. //initialization
  7. base.OnInit(e);
  8. }
  9. protected override void Render(HtmlTextWriter writer)
  10. {
  11. //initialization
  12. base.Render(writer);
  13. //write properties to page
  14. foreach (PropertyInfo pi in this.GetType().GetProperties())
  15. {
  16. //only get our properties
  17. if (pi.GetCustomAttributes<CategoryAttribute>().Any(a => a.Category.Equals("2013WebPart")))
  18. {
  19. //get value
  20. object value = pi.GetValue(this);
  21. //write hidden input for each property value
  22. writer.WriteBeginTag("input");
  23. writer.WriteAttribute("name", pi.Name);
  24. writer.WriteAttribute("type", "hidden");
  25. writer.WriteAttribute("webPartId", this.ID);
  26. writer.WriteAttribute("value", value == null ? string.Empty : value.ToString());
  27. writer.Write(HtmlTextWriter.SelfClosingTagEnd);
  28. }
  29. }
  30. }
  31. #endregion
  32. }



The Init method on Line #'s 4-8 are part of the Visual Studio 2012 visual web part template (with Office 2013 Tools installed). All I do here is override the Render method, pass through to our base's rendering logic, and then have a little fun with our properties. Line #14 uses reflection to grab all properties decorated with an attribute of type "CategoryAttribute" and writes a hidden input to the client with information containing the property name, value, and id of the web part that owns it. CategoryAttribute defines a single string named "Category" and Line #17 will grab the property if the category is set to "2013WebPart." Who doesn't think reflection is fun?



This way, all of our web parts will automatically have their properties and current values thereof serialized to the DOM for us. As you are probably guessing, this is the first of a two-step process, where the second is grabbing these values and sending them up to the Web API controller. So before finishing the web part discussion, let's take a look at the AJAX call that performs this work. This JavaScript is located in a "common" JS file that I usually call "[NameOfProject].js." It's referenced by the master page and lives in _layouts (by way of being deployed via the WSP project).


Code Listing 2

  1. function LoadWP(target, id, api, doneCallback)
  2. {
  3. //call server
  4. $.ajax
  5. ({
  6. method: 'GET',
  7. dataType: 'jsonp',
  8. url: '' + api,
  9. data:
  10. ({
  11. //assemble args
  12. args: GetProperties(id),
  13. page: window.location.href
  14. }),
  15. success: function (response, status)
  16. {
  17. //process result
  18. if (response.Error != null)
  19. {
  20. //error
  21. alert(response.Error);
  22. }
  23. else if (response.Model != null)
  24. {
  25. //json
  26. ko.applyBindings(response.Model, $(target)[0]);
  27. }
  28. else if (response.HTML != null)
  29. {
  30. //html
  31. $(target).html(response.HTML);
  32. }
  33. //done
  34. if (doneCallback != null)
  35. doneCallback(response);
  36. },
  37. error: function (response, error, ex)
  38. {
  39. //error
  40. alert(error);
  41. }
  42. });
  43. }
  44. function GetProperties(id)
  45. {
  46. //check for no id
  47. if (id == null)
  48. return null;
  49. //initialization
  50. var model = new Object();
  51. model.Args = new Array();
  52. //get all properties for this web part
  53. $('input[webPartId="' + id + '"]').each(function (i)
  54. {
  55. //assemble object
  56. model.Args[i] = new Object();
  57. model.Args[i].Value = $(this).val();
  58. model.Args[i].Name = $(this).attr('name');
  59. });
  60. //return
  61. return JSON.stringify(model, null, 2);
  62. }



LoadWP, starting at the top on Line #1, takes in four parameters:



  1. target (string): the id of the DOM element who's HTML should be set or the id of the Knockout script tag to which the model should be bound. We'll be revisiting Knockout a bit later.
  2. id (string): the id of the web part who owns the property.
  3. api (string): the name of the Web API controller that we'll be "getting" data from.
  4. doneCallback (optional function): will be invoked with the result from the MVC call so that you can perform additional logic after the UI is updated.



Line #4 performs the jQuery AJAX call. Line #6 specifies the get and Line #7 specifies jsonp as the data type. This is very important, as it allows communication between the SharePoint domain and our app domain over in the MVC site (which should be different URLs, and therefore a no-no with conventional JSON). It's also very awesome, because we don't have to do any extra work to de/re-serialize the model we get back from MVC; it comes down from the server pre-giftwrapped as a JSON object ready for databinding.



Line #'s 12 and 13 set the query string parameters that the controller expects as part of the request. The former calls the method (called "GetProperties" that we'll look at next) which reads in the values of the hidden inputs that the base web part generates, builds a client-side model, and serializes it; the latter is simply the current page's URL.



Line #15 defines the AJAX "success" callback's anonymous method that implements the following logic: if there's an error, alert that; if there's a model, Knockout bind that; if there's a value for the HTML, set that. Then if there's a callback, do it. The Knockout binding on Line #26 is annoying, as we have to hard-code in the zero index to the jQuery selector that specifies which data template to use; .first() doesn't work, as it appears that Knockout wants an actual DOM element, not a jQuery selector.



Finally, let's look at GetProperties. Line #'s 50 and 51 new up the web part args model, which matches MVC's WebPartArgsModel exactly. The magic happens on Line #53, when we use the web part Id passed into the method to feed a jQuery selector that gives us all its property values. I "each" my way through them, adding a new web part arg model at each index. Finally, Line #61 serializes this object for query string transmission.



Now that our data access layer is wired up, we can discuss the web part's UI. Like I said, "conventional" web parts in Visual Studio 2012 / SharePoint 2013 behave, at development time, just like user controls. The only annoyance is the fact that we can't quick deploy our ASCX files independent of their WSP installation cycle. But other that, Microsoft as really streamlined web part development.



Let's take a look at a sample web part's HTML:


Code Listing 3

  1. <script type="text/javascript">
  2. $(function ()
  3. {
  4. //load web part
  5. LoadWP('#tiles', '<% =this.ID %>', 'PageTiles', function (response)
  6. {
  7. //do more stuff
  8. var whatever = response.Model.Whatever;
  9. ...
  10. });
  11. });
  12. </script>
  13. <script type="text/html" id="tiles-template">
  14. <td valign="top">
  15. <div class="bordered-box" style="width: 144px;">
  16. <span class="title" data-bind="text: Title" />
  17. <img data-bind="attr: { src: ImageUrl }" />
  18. <p class="description" data-bind="text: Description" />
  19. <a class="rnd-5