Revisiting the Basement Datacenter in 2016

Here we are in 2016. If you’ve been following my blog for a while, you might recall a post I threw together back in 2010 called Portrait of a Basement Datacenter. Back in 2010, I was living on the west side of Cincinnati with my wife (Tracy) and three year-old twins (Brendan and Sabrina). We were kind of shoehorned into that house; there just wasn’t a lot of room. Todd Klindt visited once and had dinner with us. He didn’t say it, but I’m sure he thought it: “gosh, there’s a lot of stuff in this little house.”

Servers in 2010All of my computer equipment (or rather, nearly all of my computer equipment) was in the basement. I had what I called a “basement datacenter,” and it was quite a collection of PCs and servers in varying form factors and with a variety of capabilities.

The image on the right is how things looked in 2010. Just looking at the picture brings back a bunch of memories for me, and it also reminds me a bit of what we (as server administrators) could and couldn’t easily do. For example, nowadays we virtualize nearly everything without a second thought. Six years ago, virtualization technology certainly existed … but it hadn’t hit the level of adoption that it’s cruising at today. I look at all the boxes on the right and think “holy smokes – that’s a lot of hardware. I’m glad I don’t have all of that anymore.” It seemed like I had drives and computers everywhere, and they were all sucking down juice. I had two APC 1600W UPS units that were acting as battery backups back then. With all the servers plugged-in, they were drawing quite a bit of power. And yeah – I had the electric bill to prove it.

So, What’s Changed?

For starters, we now live on the east side of Cincinnati and have a much bigger house than we had way back when. Whenever friends come over and get a tour of the house, they inevitably head downstairs and get to see what’s in the unfinished portion of the basement. That’s where the servers are nowadays, and this is what my basement datacenter looks like in 2016:

Servers in 2016Purpose of each server

In reality, quite a bit has changed. We have much more space in our new house, and although the “server area” is smaller overall, it’s basically a dedicated working area where all I really do is play with tech, fix machines, store parts, etc. If I need to sit at a computer, I go into the gaming area or upstairs to my office. But if I need to fix a computer? I do it here.

In terms of capabilities, the last six years have been good to me.

All Hail The Fiber

Back on the west side of town, I had a BPL (broadband-over-powerline) Internet hookup from Duke Energy and The CURRENT Group. Nowadays, I don’t even know what’s happening with that technology. It looks like Duke Energy may be trying to move away from it? In any case, I know it gave me a symmetric pipe to the Internet, and I think I had about 10Mbps up and down. I also had a secondary DSL connection (from Cincinnati Bell) that was about 2.5Mbps down and 1Mbps up.

Once I moved back to the east side of Cincinnati and Anderson Township, the doors were blown off of the barn in terms of bandwidth. Initially, I signed with Time Warner Cable for a 50Mbps download / 5Mbps upload primary connection to my house. I made the mistake of putting in a business circuit (well, I was running a business), so while it gave me some static IP address options, it ended up costing a small fortune.

InternetSpeed2016My costly agreement with Time Warner ended last year, and for that I’m thankful. Nowadays, I have Cincinnati Bell Fiber coming to my house (Fioptics), and it’s a full-throttle connection. I pay for gigabit download speeds and have roughly a 250Mbps upload pipe. Realistically, the bandwidth varies … but there’s a ton of it, even on a bad day. The image on the right shows the bandwidth to my desktop as I’m typing this post. No, it’s not gigabit (at this moment) … but really, should I complain about 330Mbps download speeds from the Internet? Realistically speaking, some of the slowdown is likely due to my equipment. Running full gigabit Ethernet takes good wiring, quality switches, fast firewalls, and more. You’re only as fast as your slowest piece of equipment.

I do keep a backup connection with Time Warner Cable in case the fiber goes down, and my TMG firewall does a great job of failing over to that backup connection if something goes wrong. And yes, I’ve had a problem with the fiber once or twice. But it’s been resolved quickly, and I was back up in no time. Frankly, I love Cincinnati Bell’s fiber.

What About Storage?

ProRaidIn the last handful of years, storage limits have popped over and over again. You can buy 8TB drives on Amazon.com right now, and they’re not prohibitively expensive? We’ve come a long way in just a half dozen years, and the limits just keep expanding.

I have a bunch of storage downstairs, and frankly I’m pretty happy with it. I’ve graduated from the random drives and NAS appliances that used to occupy my basement. These days, I use Mediasonic RAID enclosures. You pop some drives in, connect an eSATA cable (or USB cable, if you have to), and away you go. They’ve been great self-contained pass-through drive arrays for specific virtual machines running on my Hyper-V hosts.  I’ve been running the Mediasonic arrays for quite a few years now, and although this isn’t a study in “how to build a basement datacenter,” I’d recommend them to anyone looking for reliable storage enclosures. I keep one as a backup unit (because eventually one will die), and as a group they seem to be in good shape at this point in time. The enclosures supply the RAID-5 that I want (and yeah, I’ve had *plenty* of drives die), so I’ve got highly-available, hot-swappable storage where I need it.

Oh, and don’t mind the minions on my enclosures. Those of you with children will understand. Those who don’t have children (or who don’t have children in the appropriate age range) should either just wait it out or go watch Despicable Me.

Hey? What About The Cloud?

Servers and their shelfThe astute will ask “why are you putting all this hardware in your house instead of shifting to the cloud?” You know, that’s a good question. I work for Cardinal Solutions Group, and we’re a Microsoft managed partner with a lot of Office 365 and Azure experience. Heck, I’m Cardinal’s National Solution Manager for Office 365, so The Cloud is what I think about day-in and day-out.

First off, I love the cloud. For enterprise scale engagements, the cloud (and Microsoft’s Azure capabilities, in particular) are awesome. Microsoft has done a lot to make it easier (not “easy,” but “easier”) for us to build for the cloud, put our stuff (like pictures, videos, etc.) in the cloud, and get things off of our thumb drives and backup boxes and into a place where they are protected, replicated, and made highly available.

What I’m doing in my basement doesn’t mean I’m “avoiding” the cloud. Actually, I moved my family onto an Office 365 plan to give them email and capabilities they didn’t have before. My kids have their first email address now, and they’re learning how to use email through Office 365. I’m going to move the SharePoint site collection that I maintain for our family (yes, I’m that big of a geek) over to SharePoint Online because I don’t want to wrangle with it at home any longer. Keeping SharePoint running is a pain-in-the-butt, and I’m more than happy to hand that over the Office 365 folks.

I’ll still be tinkering with SharePoint VMs for sure with the work I do, but I’m happy to turn over operational responsibility to Microsoft for my family’s site collection.

The Private Cloud

ServerShelfLeftSo even though I believe in The Cloud (i.e, “the big cloud that’s out there with all of our data”), I also believe in the “private cloud,” “personal cloud,” or whatever you want to call it. When I work from the Cardinal office, my first order of business is to VPN back to my house (again, through my TMG Firewall – they’ll have to pry it from my cold, dead hands) so that I have access to all of my files and systems at home.

Accessing stuff at home is only part of it, though. The other part is just knowing that I’m going through my network, interacting with my systems, and still feeling like I have some control in our increasingly disconnected world. My Plex server is there, and my file shares are available, and I can RDP into my desktop to leverage its power for something I’m working on. There’s a comfort in knowing my stuff is on my network and servers.

Critical data makes it to the cloud via OneDrive, Dropbox, etc, but I still can’t afford to pay for all of my stuff to be in the cloud. Prices are dropping all of the time, though. Will I ever give up my basement datacenter? Probably not, because maintaining it helps me keep my technical skills sharpened … but it’s also a labor of love.

Additional Reading and References

  1. Blog Post: Portrait of a Basement Datacenter
  2. Blog: Todd Klindt’s SharePoint Admin Blog
  3. Department of Justice: Current Group Broadband Overview
  4. Site: Cincinnati Bell Fioptics
  5. TechNet: Threat Management Gateway
  6. Amazon.com: Seagate Archive 8 TB Internal Hard Drive
  7. Amazon.com: Mediasonic PRORAID Drive Enclosure
  8. Amazon.com: Despicable Me
  9. Company: Cardinal Solutions Group

What Happened To My Office 365 Public Site?

Close Out (Early 2016)

It would appear that things are more or less back to normal. I never got an “everything is okay and live” email, but theming and branding are working properly both on public sites (tick, tock, tick, tock …) and internal sites. No conflicts at this point. Since I like to tie things up when complete, we’ll call this one “done” for now and move on.

What the heck happened?Update (Evening 7/23/2015)

Microsoft has been looking at this issue, and progress is being made! My public site looks like it has returned to normal … but I know that we’re not quite out of the woods yet.

John from Microsoft followed-up with me yesterday and said the following:

“We have pulled the flight that was impacting everyone from production.  The plan is to address these issues before turning the flight back on.  Would you be up for piloting the upgraded flight before we turn it back on for everyone? Also, you mention you know others who are having problems, would they be willing to pilot the new flight as well?  If so, please provide their contact information so I can follow up with them.”

Clearly, there’s a strong “flying vibe” in Redmond ….

I told John that I was definitely a “go” for the flight, and that I knew some others who’d experienced problems. And that’s where I’m hoping that some of you can help.

If you have been encountering problems with your Office 365 public site that are similar to mine – and you want to be part of the fix – let me know and I’ll hook you up with John. Shoot me your name, email address, and public site URL; I think that will do the job.

Stay tuned!

Original Post (below)

First of all, let me state that I’m not talking about the fact that Office 365 public sites are going the way of the dinosaur. That’s old news at this point. Instead, I’m talking about a “disruption in the force” that some of you may have observed when recently browsing to your public sites and discovering that they had … changed.

And what do I mean by “changed?” In my case, it was the observation that my public site’s branding had been altered to something I hadn’t chosen. The background image was different, the fonts weren’t the same, a number of the CSS styles I had applied to address scrollbar positioning and what-not weren’t in effect, and more. In essence, my custom branding had been completely steamrolled.

The After And The (Sort-Of Before)

Office 365 Public Site: Forcing Some Branding Elements BackOffice 365 Public Site: Busted BrandingDon’t take my word for it, though. Have a look for yourself. On the left is my public site as I discovered it a couple of weeks back (i.e., near the beginning of July, 2015). On the right is the way it’s supposed to look … sort of. The fonts and aspects of the responsive design are still off in the “corrected” version on the right, but I managed to hack the proper background, scrollbars, and a few other elements back to where they were previously. Even though I didn’t get everything corrected, differences between the two are immediately obvious.

I was confident that it had been months since I had changed anything on my public site, but I verified the branding assets in the Office 365 site against what I was tracking in source control. As expected, they were identical. The changes I was observing were not due to anything I had done to the public site.

Grumble Grumble …

Facebook Complaint About Branding IssueSince it wasn’t the first time I’d had issues with unexpected changes and behavior on my public site, I wasted little time before going to Facebook to complain aloud. Many of my friends in the SharePoint community are also friends on Facebook, so I figured I’d get some support (moral, if nothing else) there.

Shortly after posting the update seen on the left, I tagged a couple of my Microsoft friends (specifically, Jeremy Thake and Chris Johnson) who work with the Office 365 team(s) and asked if I should have known about an update or change that might have affected my public site in this fashion. Even though I was confident that I hadn’t done anything to directly impact my public site’s look-and-feel, I was not about to rule out the possibility that I had missed something that had been communicated to me. In fact, I have been consulting in information technology long enough to know that my “mental glove” doesn’t catch everything thrown to me; anymore, I just kind of assume that I am in error and work from there.

Jeremy got back to me first and indicated that he didn’t have anything specific to share, but he indicated he would take the issue to folks (internally) who should know. Shortly after that, Chris tagged Steve Walker (another Microsoftee with superhuman powers) to bring the issue to his attention. Steve told me to file a service request (SR) and that he would escalate that SR right away. So, I filed the service request with supporting screenshots and documentation … and as he had indicated he would, Steve escalated the SR in less than an hour.

In the meantime, I did what I could to get some of my site’s branding back to the way it had been. How did I do that? With the liberal application of the dreaded !important CSS directive in the custom style sheet I had created to go with my master page. The !important directive is definitely not something I use (or even like to go near) on a daily basis, but in this case, I was trying to achieve results with a minimal investment of time and effort. I needed my styling to trump whatever was being laid-down after my style sheet was being processed.

So, What Happened Next?

Following Steve’s escalation, I started working with an extremely approachable escalation engineer named John. John and I exchanged some emails, did a late-night screen-sharing session, and generally looked things up-and-down. John concurred that what we were seeing shouldn’t have been happening, and he mentioned that he had another customer or two that seemed to be having a similar problem. Some tracing in those cases seemed to implicate a recent client-side theming change that had been made and rolled-out.

Analysis Of Styles In Internet Explorer Developer ToolsAn issue with client-side theming “felt” right to me. I had used Internet Explorer’s Developer Tools to do some backtracking into the source of the errant styles that were being applied to my site, and I noticed that the background image was being served from a temporary theme directory on the server. Until I started forcing my styles to override the ones that were being applied (an example of which is shown on the right), the theme styles were trumping my own styles.

John was out for a while, but he came back to me recently with the following explanation. And his explanation makes complete sense:

So the problem you were having was the result of a bad interaction with a third-party CSS minification technique called minisp.  By adding HTML comments around our CssLink controls in the master page, the <link> tags we normally render are part of a comment and therefore not part of the DOM.

When Client-Side Theming goes to replace the CSS on these pages, it wants to put the generated <style> blocks immediately before the corresponding <link> tags. Since it can’t find the <link> tags in the DOM, it defaults to adding the <style> blocks to the end of the document head. Since these come after the custom CSS, the rules in our themed CSS take priority over the rules in the custom CSS.

Resolution?

As of July 21st, 2015, there is no official resolution. Since this has been identified as a problem in how client-side theming handles CSS <style> block insertions, though, the ultimate fix needs to come from Microsoft. In the meantime, I’ll be sticking with my hacked CSS style sheet to get back most of the look-and-feel that I need. I could expend additional (development) effort to ensure that my styles are applied after the Office 365 theme styles without using !important, but there are plenty of other more important tasks vying for my attention right now.

Thumbs UpSo, if you found this post and it’s helping you to realize that you’re not going insane (at least not because of public site branding changes you didn’t make), I’ll feel that my job is done.

As I hear more and changes take place, I’ll try to update this post accordingly. Check back every now and then if you want the play-by-play on this issue.

Parting Thoughts: A Tip Of My Hat To Microsoft

In the past, I’ve been pretty vocal about the way that Microsoft has sort of “rolled” changes onto its Office 365 customer base and failed to communicate problems in a timely and complete fashion – actions (or lack of action) that ultimately caused pain and problems. As vocal as I’ve been in those situations, I want to go on-record as saying (just as loudly) that Microsoft has definitely listened to the critical feedback it has been receiving and has acted to make changes that we have indicated we need.

Even though this public site branding issue is indeed a bug, Microsoft listened and responded quickly – without protest, without claiming that “nothing is wrong,” and without some of the problem behaviors I used to see.

Where there were previously few communications about Office 365 outages, problems, changes, and updates, we now have a boatload of information (with some of it actually being pushed) to us to keep us in-the-know on our tenants, where they stand at any given point in time, and where they are going. I can get both at-a-glance health information and deep explanations for issues using the administrative portal’s Service Health dashboard. I get push notifications whenever something happens in my tenant using the Office 365 Admin application that runs on my Windows Phone. I know when new service features and capabilities are rolling out, if changes have been cancelled, etc., by looking at the Office 365 Roadmap. And these are just some of the channels and information streams that are available.

Is Microsoft “all the way there” yet? No, but they are dramatically further along than when Office 365 first rolled-out. Outages still occur – as they do with any service – but I feel like I know what’s going on now. That’s a huge improvement in my book.

References and Resources

  1. Microsoft Support: Information about changes to the SharePoint Online Public Website feature in Office 365
  2. Office 365 Public Site: Bitstream Foundry LLC
  3. LinkedIn: Jeremy Thake
  4. LinkedIn: Chris Johnson
  5. LinkedIn: Steve Walker
  6. Stack Overflow: What are the implications of using “!important” in CSS?
  7. MSDN: Using the F12 developer tools
  8. Microsoft: Office 365 Service Health Status
  9. Windows Phone App Store: Office 365 Admin
  10. Office.com: Office 365 Roadmap

Advanced Paging with SharePoint Content Search Web Part Display Templates

In this post, I cover the topic of exposing paging controls within the display templates used by SharePoint 2013’s Content Search Web Part (CSWP). In addition to looking at the underlying paging mechanisms used by the CSWP, I make available a couple of display templates that I created which include advanced paging support.

Click here to download associated ZIP file with display templatesPages in a BookI’ve had an opportunity to play with SharePoint 2013’s Content Search Web Part (CSWP) on a number of occasions in the last couple of years, and I have to say that I like it a lot. The CSWP can be employed to address a whole host of different use cases; in fact, in many situations I’ve found that it can be used to solve problems that were previously addressable only through custom code.

Search-driven content in SharePoint isn’t anything new, of course, but the display templates that are used to format search results in SharePoint 2013 are a large part of the CSWP’s “special sauce.” Through the use of a Control display template and an Item display template, it is possible to select, arrange, and style the search results that are returned and shown to users in a highly customizable fashion. With some knowledge of HTML and the help of SharePoint 2013’s Design Manager, it’s possible to produce some pretty impressive looking content. And if you know JavaScript, well … the sky is the limit on what you can produce.

Control And Item Display Templates

DisplayAndPropertyMappingsBefore I dive into paging and how I’ve tried to stretch what can be done with the CSWP, I want to briefly examine some display template basics. Although this post isn’t intended to be a primer on the CSWP and its associated display templates, there are a few items worth reviewing.

On the right is a snippet of a ToolPart containing some configuration data for a particular CSWP displaying some very basic data. The examples that follow (i.e., the two images shown below) use the configuration shown in the ToolPart, so refer to it if needed.

The first example below is how a CSWP showing three results might appear to an end user when the List with Paging Control template and Two lines Item template are applied. The second example (to the right of the first) differentiates which content regions on the CSWP are being driven by the List with Paging Control template (shown with green highlighting) and which are being driven by the Two lines Item template (shown in red).

Content Search Web Part Example as Seen by End User Content Search Web Part with Template Regions Highlighted

Through these images, I’m trying to convey a very simple point: the Control template dictates how the search results’ “container” appears, and the Item template determines how each individual search result within the container is displayed. The contents of any CSWP are rendered by the output of a single Control display template and zero or more Item display templates (again, one for each search result/item shown).

Nothing To See Here

As you might have guessed from the title of this post, one functional area that seems relatively unexplored and underdeveloped (in my experience) is that of paging and how paging controls are made available to end users of the CSWP. This is unfortunate, because whenever more results are returned than can be displayed at once – a very common scenario – some form of paging is needed.

Out of the box (OOTB), SharePoint 2013 comes with only a handful of Control and Item display templates that can be used with the CSWP to format your search results. Of these display templates, only one contains paging controls: the List with Paging Control template. If all you require is the basic forward/backward paging offered by the two buttons it contains, then the List with Paging display template may be adequate for your needs.

Personally, I think the List with Paging template is bland and kind of … ugly. It works, sure, but it doesn’t display several pieces of information that I find important; for example, the total number of search results and the result page that the user is currently on. Worse is the fact that it doesn’t provide any sort of mechanism to jump to a specific page. The best that an end-user can do is page forward or backward one page at a time.

Better Paging Support

CSWP With Advanced Paging SupportOne of the talks I’ve been giving recently at various SharePoint events and conferences is titled SharePoint’s New Swiss Army Knife: The Content Search Web Part. During that talk, I demonstrate a set of CSWP display templates that I put together to generate something decidedly “non-search” in appearance – like a directory of files to which the current user has access within the current site collection. An image of that CSWP example appears on the left.

This file directory CSWP instance was generated with three simple files: two custom display templates (one for the Control, and one for each Item shown) and a cascading style sheet. The actual result data isn’t particularly remarkable, but the manner in which the paging is implemented is what tends to catch people’s attention. With the Control template I created, end-users can:

  • See the total number of documents (i.e., search results) to which they have access
  • Clearly see which page of search results they’re on through the boxed page number and the “Page xx of yy” label
  • Identify which items/results they’re viewing through the “Items xxx to yyy of zzz” label
  • Jump directly to a page of results by clicking the specified page number

Finding all of this information and displaying it through the CSWP Control template took some research and tinkering. Some of the information was available directly within the search results that were passed to the CSWP, but some of it wasn’t. In the case where some desired information wasn’t available, it was computed with some basic math.

Overview Of The CSWP HTML

By default, the CSWP implements a client-side processing and paging model. Search results that are displayed are controlled by JavaScript functions contained within the Control and Item display templates that have been assigned to the CSWP. When a set of search results is being processed for display within the client browser, the Control template (which is the results container) gets called first to render the HTML that will frame or house the search items/results. For each search result or item that is passed to the CSWP, the Item display template then gets called to create a snippet of HTML for the result/item that can be inserted into the “frame” (commonly a <div>) created by Control display template.

An example of the rendered HTML for the search results shown in the previous “Better Paging Support” section appears below.

<div id="sas-lib-listing">
	<div>Documents To Which I Have Access</div>
	<ul class="sas-table">
		<li class="sas-tablerow">
			<span class="sas-tableheader">Title of Document</span>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr1_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr1_SAS_Item_Template_dataContainer">
					<a title="3. Optimus Prime Part - Button" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr1_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SiteAssets/Forms/DispForm.aspx?ID=4">3. Optimus Prime Part - Button</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr2_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr2_SAS_Item_Template_dataContainer">
					<a title="4. Weather Right Now (Not Really) - Button" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr2_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SiteAssets/Forms/DispForm.aspx?ID=5">4. Weather Right Now (Not Really) - Button</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr3_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr3_SAS_Item_Template_dataContainer">
					<a title="5. Weather Right Now Page - Button" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr3_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SiteAssets/Forms/DispForm.aspx?ID=6">5. Weather Right Now Page - Button</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr4_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr4_SAS_Item_Template_dataContainer">
					<a title="6. Weather Right Now Provisioned - Button" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr4_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SiteAssets/Forms/DispForm.aspx?ID=8">6. Weather Right Now Provisioned - Button</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr5_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr5_SAS_Item_Template_dataContainer">
					<a title="8. Donuts And Drinks Pub - Button" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr5_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SiteAssets/Forms/DispForm.aspx?ID=9">8. Donuts And Drinks Pub - Button</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr6_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr6_SAS_Item_Template_dataContainer">
					<a title="WeatherRightNowScraper" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr6_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SitePages/WeatherRightNowScraper.aspx">WeatherRightNowScraper</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr7_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr7_SAS_Item_Template_dataContainer">
					<a title="DonutsAndDrinksPub" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr7_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/Pages/DonutsAndDrinksPub.aspx">DonutsAndDrinksPub</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr8_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr8_SAS_Item_Template_dataContainer">
					<a title="DonutsAndDrinks" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr8_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SitePages/DonutsAndDrinks.aspx">DonutsAndDrinks</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr9_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr9_SAS_Item_Template_dataContainer">
					<a title="MakingPiTakesEffort" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr9_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SitePages/MakingPiTakesEffort.aspx">MakingPiTakesEffort</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr10_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr10_SAS_Item_Template_dataContainer">
					<a title="OptimusPrimePart" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr10_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SitePages/OptimusPrimePart.aspx">OptimusPrimePart</a>
				</div>
			</div>
		</li>
	</ul>
	<div class="sas-paging-table">
		<div class="sas-paging-tablerow">
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(1);return Srch.U.cancelEvent(event);" href="#">1</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(11);return Srch.U.cancelEvent(event);" href="#">2</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(21);return Srch.U.cancelEvent(event);" href="#">3</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(31);return Srch.U.cancelEvent(event);" href="#">4</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(41);return Srch.U.cancelEvent(event);" href="#">5</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(51);return Srch.U.cancelEvent(event);" href="#">6</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(61);return Srch.U.cancelEvent(event);" href="#">7</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(71);return Srch.U.cancelEvent(event);" href="#">8</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(81);return Srch.U.cancelEvent(event);" href="#">9</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(91);return Srch.U.cancelEvent(event);" href="#">10</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(101);return Srch.U.cancelEvent(event);" href="#">11</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(111);return Srch.U.cancelEvent(event);" href="#">12</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(121);return Srch.U.cancelEvent(event);" href="#">13</a>
			</div>
			<div class="sas-paging-tablecell sas-paging-currentpage">14</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(141);return Srch.U.cancelEvent(event);" href="#">15</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(151);return Srch.U.cancelEvent(event);" href="#">16</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(161);return Srch.U.cancelEvent(event);" href="#">17</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(171);return Srch.U.cancelEvent(event);" href="#">18</a>
			</div>
			<div class="sas-paging-tablecell sas-paging-summary">Page
				<span>14</span>of
				<span>18</span>, items
				<span>131</span>to
				<span>140</span>of
				<span>179</span>.</div>
		</div>
	</div>
</div>

The Control template (SwissArmy_Control_Template.js) creates the top level <div> and a child <ul> element (with a CSS class of “sas-table”) for the overall HTML structure, as well as the individual <li> elements used to contain each search/result item. For each search result/item, the Item template (SwissArmy_Item_Template.js) creates a <div> block that is inserted as a child within an <li> element. Each of the individual item <div> blocks created by the Item template has a CSS class of “sas-tablecell” for easier styling.

Below the <ul> block housing the search results is another <div> with a CSS class of “sas-paging-table.” As suggested by the class name, the <div> is used to house the clickable page numbers and additional paging information seen at the bottom of the CSWP control. And like the rest of the container information, this HTML is generated within the Control template.

Fetching Search Results

Each time a set of search results is needed, either on initial page rendering or when the user moves to a new page, the CSWP calls back to its SharePoint site collection for the data it needs. By default, this call occurs asynchronously; however, the CSWP can be configured to make such calls synchronously within the normal page processing sequence.

For example, if I had a page containing a CSWP at the following URL

http://sp2013-dev:18580/DisplayTemplateStyling

… the CSWP on that page would request search results for display by posting a request to the following endpoint:

http://sp2013-dev:18580/_vti_bin/client.svc/ProcessQuery

As part of its request, the CSWP passes an XML structure to the client.svc web service that looks something like the XML that appears below. This structure contains all of the information the ProcessQuery method needs to determine which search results should be sent back:

<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="15.0.0.0" ApplicationName="Javascript Library">
	<Actions>
		<ObjectPath Id="1" ObjectPathId="0" />
		<SetProperty Id="2" ObjectPathId="0" Name="QueryTemplate">
			<Parameter Type="String">(contentclass:STS_ListItem OR IsDocument:True)</Parameter>
		</SetProperty>
		<ObjectPath Id="4" ObjectPathId="3" />
		<Method Name="Add" Id="5" ObjectPathId="3">
			<Parameters>
				<Parameter Type="String">LastModifiedTime</Parameter>
				<Parameter Type="Number">1</Parameter>
			</Parameters>
		</Method>
		<SetProperty Id="6" ObjectPathId="0" Name="StartRow">
			<Parameter Type="Number">130</Parameter>
		</SetProperty>
		<SetProperty Id="7" ObjectPathId="0" Name="RowsPerPage">
			<Parameter Type="Number">10</Parameter>
		</SetProperty>
		<SetProperty Id="8" ObjectPathId="0" Name="RowLimit">
			<Parameter Type="Number">10</Parameter>
		</SetProperty>
		<SetProperty Id="9" ObjectPathId="0" Name="TotalRowsExactMinimum">
			<Parameter Type="Number">141</Parameter>
		</SetProperty>
		<SetProperty Id="10" ObjectPathId="0" Name="SourceId">
			<Parameter Type="Guid">{8413cd39-2156-4e00-b54d-11efd9abdb89}</Parameter>
		</SetProperty>
		<ObjectPath Id="12" ObjectPathId="11" />
		<Method Name="SetQueryPropertyValue" Id="13" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">SourceName</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">Local SharePoint Results</Property>
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="14" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">SourceLevel</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">Ssa</Property>
				</Parameter>
			</Parameters>
		</Method>
		<ObjectPath Id="16" ObjectPathId="15" />
		<Method Name="Add" Id="17" ObjectPathId="15">
			<Parameters>
				<Parameter Type="String">Path</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="18" ObjectPathId="15">
			<Parameters>
				<Parameter Type="String">Title</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="19" ObjectPathId="15">
			<Parameters>
				<Parameter Type="String">
 FileExtension</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="20" ObjectPathId="15">
			<Parameters>
				<Parameter Type="String">
 SecondaryFileExtension
</Parameter>
			</Parameters>
		</Method>
		<ObjectPath Id="22" ObjectPathId="21" />
		<Method Name="Add" Id="23" ObjectPathId="21">
			<Parameters>
				<Parameter Type="String">Title</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="24" ObjectPathId="21">
			<Parameters>
				<Parameter Type="String">Path</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="25" ObjectPathId="21">
			<Parameters>
				<Parameter Type="String">Author</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="26" ObjectPathId="21">
			<Parameters>
				<Parameter Type="String">SectionNames</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="27" ObjectPathId="21">
			<Parameters>
				<Parameter Type="String">SiteDescription</Parameter>
			</Parameters>
		</Method>
		<SetProperty Id="28" ObjectPathId="0" Name="TrimDuplicates">
			<Parameter Type="Boolean">false</Parameter>
		</SetProperty>
		<Method Name="SetQueryPropertyValue" Id="29" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">TryCache</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">true</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">3</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="Null" />
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="30" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">Scope</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">{Site.URL}</Property>
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="31" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">UpdateLinksForCatalogItems</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">true</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">3</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="Null" />
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="32" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">EnableStacking</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">true</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">3</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="Null" />
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="33" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">ListId</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">fb9e18d3-1d80-4ab1-8e76-bc36b0a8e22d</Property>
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="34" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">ListItemId</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">6</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">2</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="Null" />
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="35" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">TermId</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">0bfc89ed-1741-4925-9d25-828eaf74b2c8</Property>
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="36" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">TermSetId</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">0f14334e-b929-42bf-ad10-ed6a93d16a4f</Property>
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="37" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">TermStoreId</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">c4c58328-0e88-4937-91e9-cf7526b9b0db</Property>
				</Parameter>
			</Parameters>
		</Method>
		<SetProperty Id="38" ObjectPathId="0" Name="ResultsUrl">
			<Parameter Type="String">http://sp2013-dev:18580/DisplayTemplateStyling#k=#s=131</Parameter>
		</SetProperty>
		<SetProperty Id="39" ObjectPathId="0" Name="BypassResultTypes">
			<Parameter Type="Boolean">true</Parameter>
		</SetProperty>
		<SetProperty Id="40" ObjectPathId="0" Name="ClientType">
			<Parameter Type="String">ContentSearchRegular</Parameter>
		</SetProperty>
		<SetProperty Id="41" ObjectPathId="0" Name="EnableInterleaving">
			<Parameter Type="Boolean">false</Parameter>
		</SetProperty>
		<SetProperty Id="42" ObjectPathId="0" Name="ProcessBestBets">
			<Parameter Type="Boolean">false</Parameter>
		</SetProperty>
		<Method Name="SetQueryPropertyValue" Id="43" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">QuerySession</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">a4e0e9b5-6dd9-4aea-b38b-26f325c67fdf</Property>
				</Parameter>
			</Parameters>
		</Method>
		<SetProperty Id="44" ObjectPathId="0" Name="ProcessPersonalFavorites">
			<Parameter Type="Boolean">false</Parameter>
		</SetProperty>
		<SetProperty Id="45" ObjectPathId="0" Name="SafeQueryPropertiesTemplateUrl">
			<Parameter Type="String">querygroup://webroot/Pages/DisplayTemplateStyling.aspx?groupname=Default</Parameter>
		</SetProperty>
		<SetProperty Id="46" ObjectPathId="0" Name="IgnoreSafeQueryPropertiesTemplateUrl">
			<Parameter Type="Boolean">false</Parameter>
		</SetProperty>
		<ObjectPath Id="48" ObjectPathId="47" />
		<ExceptionHandlingScope Id="49">
			<TryScope Id="51">
				<Method Name="ExecuteQueries" Id="53" ObjectPathId="47">
					<Parameters>
						<Parameter Type="Array">
							<Object Type="String">26103082-0e16-4ab0-a12c-bded636a7fb8Default</Object>
						</Parameter>
						<Parameter Type="Array">
							<Object ObjectPathId="0" />
						</Parameter>
						<Parameter Type="Boolean">true</Parameter>
					</Parameters>
				</Method>
			</TryScope>
			<CatchScope Id="55" />
		</ExceptionHandlingScope>
	</Actions>
	<ObjectPaths>
		<Constructor Id="0" TypeId="{80173281-fffd-47b6-9a49-312e06ff8428}" />
		<Property Id="3" ParentId="0" Name="SortList" />
		<Property Id="11" ParentId="0" Name="Properties" />
		<Property Id="15" ParentId="0" Name="SelectProperties" />
		<Property Id="21" ParentId="0" Name="HitHighlightedProperties" />
		<Constructor Id="47" TypeId="{8d2ac302-db2f-46fe-9015-872b35f15098}" />
	</ObjectPaths>
</Request>				 				 			 			 		 	 	 		 		 		 		 		 		 	  

Once the server finds the desired search results and processes them, it packages them up as a JSON object and returns that object to the browser for further action. The following is an example of a JSON structure (containing ten results) that was returned for the Swiss Army Knife CSWP as it was shown earlier in the “Better Paging Support” section:

[  
   {  
      "SchemaVersion":"15.0.0.0",
      "LibraryVersion":"15.0.4569.1501",
      "ErrorInfo":null,
      "TraceCorrelationId":"36d5029d-dcee-709e-a995-81ad013126e0"
   },
   119,
   {  
      "IsNull":false
   },
   122,
   {  
      "IsNull":false
   },
   130,
   {  
      "IsNull":false
   },
   134,
   {  
      "IsNull":false
   },
   140,
   {  
      "IsNull":false
   },
   166,
   {  
      "IsNull":false
   },
   171,
   {  
      "418e02d2-9bc5-46fd-ada2-83762ed79256Default":{  
         "_ObjectType_":"Microsoft.SharePoint.Client.Search.Query.ResultTableCollection",
         "ElapsedTime":31,
         "Properties":{  
            "RowLimit":10,
            "SourceId":"\/Guid(8413cd39-2156-4e00-b54d-11efd9abdb89)\/",
            "EnableStacking":true,
            "SerializedQuery":"<Query Culture=\"en-US\" EnableStemming=\"True\" EnablePhonetic=\"False\" EnableNicknames=\"False\" IgnoreAllNoiseQuery=\"True\" SummaryLength=\"180\" MaxSnippetLength=\"180\" DesiredSnippetLength=\"90\" KeywordInclusion=\"0\" QueryText=\"\" QueryTemplate=\"(contentclass:STS_ListItem OR IsDocument:True)\" TrimDuplicates=\"False\" Site=\"e964e9f4-d726-4c46-bda6-00367a81bb29\" Web=\"1b739834-de53-4968-b4cc-04a559306668\" KeywordType=\"True\" HiddenConstraints=\"\" \u002f>"
         },
         "QueryErrors":null,
         "QueryId":"e12d2e7f-598c-4db7-ae6e-fbf021d0b230",
         "SpellingSuggestion":"",
         "TriggeredRules":[  

         ],
         "ResultTables":[  
            {  
               "_ObjectType_":"Microsoft.SharePoint.Client.Search.Query.ResultTable",
               "GroupTemplateId":null,
               "ItemTemplateId":null,
               "Properties":{  
                  "GenerationId":9223372036854775806,
                  "ExecutionTimeMs":16,
                  "QueryModification":"(contentclass:STS_ListItem OR IsDocument:True) -ContentClass=urn:content-class:SPSPeople",
                  "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fGroup_Default.js",
                  "StartRecord":0
               },
               "QueryId":"e12d2e7f-598c-4db7-ae6e-fbf021d0b230",
               "QueryRuleId":"00000000-0000-0000-0000-000000000000",
               "ResultRows":[  
                  {  
                     "Rank":0,
                     "DocId":114,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=4",
                     "Title":"3. Optimus Prime Part - Button",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=4",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":115,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=5",
                     "Title":"4. Weather Right Now (Not Really) - Button",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=5",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":116,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=6",
                     "Title":"5. Weather Right Now Page - Button",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=6",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":117,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=8",
                     "Title":"6. Weather Right Now Provisioned - Button",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=8",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":119,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=9",
                     "Title":"8. Donuts And Drinks Pub - Button",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=9",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":688,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fWeatherRightNowScraper.aspx",
                     "Title":"WeatherRightNowScraper",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fWeatherRightNowScraper.aspx",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":683,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fPages\u002fDonutsAndDrinksPub.aspx",
                     "Title":"DonutsAndDrinksPub",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fPages\u002fDonutsAndDrinksPub.aspx",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":685,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fDonutsAndDrinks.aspx",
                     "Title":"DonutsAndDrinks",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fDonutsAndDrinks.aspx",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":686,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fMakingPiTakesEffort.aspx",
                     "Title":"MakingPiTakesEffort",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fMakingPiTakesEffort.aspx",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":139,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fOptimusPrimePart.aspx",
                     "Title":"OptimusPrimePart",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fOptimusPrimePart.aspx",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  }
               ],
               "ResultTitle":null,
               "ResultTitleUrl":null,
               "RowCount":10,
               "TableType":"RelevantResults",
               "TotalRows":179,
               "TotalRowsIncludingDuplicates":179
            }
         ]
      }
   },
   167,
   {  
      "HasException":false,
      "ErrorInfo":null
   }
]

This JSON structure contains a lot of information – certainly more information than is being displayed by the CSWP. The trick, of course, is in figuring out exactly “what” is “where” for purposes of building a paging system.

Page-Related Processing Within The Control Template

Thankfully, the CSWP provides us with a relatively easy mechanism for getting at the search result data we care about within the JSON object that is returned from the call to client.svc. When a Control display template is invoked by the CSWP, it is passed a context object as follows:

function DisplayTemplate_2aa45a743fd94e75a3e53c940628e2ec(ctx) { ... } 

ctx Content ObjectThe ctx context object contains a number of useful methods, properties, and subordinate objects we can leverage in our attempts to manipulate search results and calculate paging information. We can use ctx to interact directly with the CSWP (which is accessible through the ClientControl property), as well as obtain the search results themselves (and information about them) through the ListData property.

Note: Even though Microsoft’s List with Paging Control template doesn’t provide anything more than relative paging forward and backward, the CSWP does appear to support some form of more advanced paging scheme through its get_pagingInfo() method. When this method is called, it returns an object that contains expanded paging information that includes information about the current page, as well as a subset of pages before and after the current page. The object does not, however, contain all of the information needed to determine the total number of items in the search result set, how to access those pages, etc.

Fortunately for us, though, the ctx.ListData object contains everything we need to implement an end-to-end paging system. Here’s how each of the critical paging information pieces is located or computed within the Control template so that the portion of the control seen below can be rendered:

Paging Line

  • ctx.ListData InspectionTotal number of all search results available. The totalRowCount variable is used within the Control display template scripting to store this value. And the value itself is readily available through the ctx.ListData.ResultTables[0].TotalRows property.
  • Maximum number of items to display per page. This value (represented within the Control display template scripting as rowsPerPageCount) is easily obtained through the ctx.ListData.Properties.RowLimit property. It identifies the maximum number of rows that may be returned in the search results table (through ctx.ListData.ResultTables[0]), and it is the value which should be used to determine the number of individual search results shown in the CSWP.
  • Number of items to display on the current page. In most cases, this value (defined through the rowsOnCurrentPageCount variable within the Control display template scripting) will be the same as the maximum number of items per page. On the last page of results, though, it isn’t uncommon for there to be fewer results available than the maximum number possible. To determine how many items should be shown on the current page of search results, the script simply determines how many rows are present in the current search results table (ctx.ListData.ResultTables[0].RowCount).
  • First page number for the result set. As my kids would say, “Easy peasy lemon squeezy.” This variable (firstPageNumber) always has the intuitive value of 1. This variable should not be confused with the firstPage variable that commonly appears in the OOTB display template scripting. The reference which is assigned to the firstPage variable is a PagingLink object that is meaningful within the context of the pagingInfo object returned from the call to the ctx.ClientControl.get_pagingInfo() – not the page calculations in the display templates associated with this post.
  • Last page number for the result set. This value (represented by the lastPageNumber variable) isn’t readily available in a way that can be “plucked” out of the search data, so the display template scripting simply does a little math to calculate it: Math.ceil(totalRowCount /rowsPerPageCount). In non-code terms: the last page of the total search results set is the total number of results in the set divided by the number of results displayed per page, rounded-down. As with the firstPageNumber and firstPage variables, don’t confuse lastPageNumber with the OOTB lastPage object reference.

In addition to the values described above, a series of calculations are performed to determine the currentPageStartItem and currentPageEndItem variables that represent the first and last item numbers on the current page. Once these values are known and assigned to their respective variables, it becomes a relatively straightforward exercise to display the paging information as is done within the SwissArmy_Control_Template.js Control template. The (not-so-heavy) lifting is accomplished with the following Javascript:

// Figure out the current page information
for (var i = 0; i< pagingInfo.length; i++)
{
	var pl = pagingInfo[i];
	if (!$isNull(pl))
	{
		if (pl.startItem == -1)
		{
			currentPage = pl;
			currentPageNumber = pl.pageNumber;
			currentPageStartItem = ((currentPageNumber - 1) * rowsPerPageCount) + 1;
			if (currentPageNumber == lastPageNumber) {
				currentPageEndItem = totalRowCount;
			} else {
				currentPageEndItem = currentPageNumber * rowsPerPageCount;
			}
		}
	}
}

// Generate page divs and their links
var getPageNumberDivs = function() {
	var currentDiv, divBlocks, pageStartItem;
	divBlocks = '';
	for (var i = firstPageNumber; i <= lastPageNumber; i++) {
		cellStyle = 'sas-paging-tablecell';
		if (i == currentPageNumber) {
			currentDiv = '<div class="sas-paging-tablecell sas-paging-currentpage">';
			currentDiv += i;
			currentDiv += '</div>';
		} else {
			pageStartItem = ((i - 1) * rowsPerPageCount) + 1;
			currentDiv = '<div class="sas-paging-tablecell">';
			currentDiv += '<a href="#" onclick="$getClientControl(this).page(' + $htmlEncode(pageStartItem) + ');return Srch.U.cancelEvent(event);">';
			currentDiv += i;
			currentDiv += '</a></div>';
		}
		divBlocks += currentDiv;
	}
	return divBlocks;
}

// Generate the block that indicates current page, item counts, etc.
var getPageSummary = function() {
	var currentDiv;
	var pageInfo = 'Page {1} of {2}, items {3} to {4} of {5}.';
	pageInfo = pageInfo.replace('{1}', '<span>' + currentPageNumber + '</span>');
	pageInfo = pageInfo.replace('{2}', '<span>' + lastPageNumber + '</span>');
	pageInfo = pageInfo.replace('{3}', '<span>' + currentPageStartItem + '</span>');
	pageInfo = pageInfo.replace('{4}', '<span>' + currentPageEndItem + '</span>');
	pageInfo = pageInfo.replace('{5}', '<span>' + totalRowCount + '</span>');
	currentDiv = '<div class="sas-paging-tablecell sas-paging-summary">' + '</span>';
	currentDiv += pageInfo + '</div>';
	return currentDiv;
}

ms_outHtml.push(''
,''
,'			</ul>'
,''
,'			<div class="sas-paging-table">'
,'				<div class="sas-paging-tablerow">'
,'					', getPageNumberDivs() ,''
,'					', getPageSummary() ,''
,'				</div>'
,'			</div>'
,'		</div>'
,'		'
);

Creating The Page Links

One area does warrant a little more explanation, though, and that is how the hyperlinks are created for each of the clickable page numbers at the bottom of the CSWP. There really isn’t a whole lot of magic behind the creation of the hyperlinks themselves; the results are achieved on line 101 of the SwissArmy_Control_Template.js file:

 
currentDiv += '<a href="#" onclick="$getClientControl(this).page(' + $htmlEncode(pageStartItem) + ');return Srch.U.cancelEvent(event);">';

The pageStartItem variable value is computed as follows …

 
pageStartItem = ((i - 1) * rowsPerPageCount) + 1; 

… where i is the value of the new result page being clicked by the user. For example, if the user clicks on “17” (to indicate a desire to move to page 17 of the search results), the value of i will be 161 when there are 10 items per page.

http://sp2013-dev:18580/DisplayTemplateStyling#k=#s=161

On the server side, SharePoint translates the request for 161 (which can also be reached directly via re-post by attaching a #s=161 to the query string portion of the URL as shown above) to package-up and send down a search results table containing all of the page 17 result items.

How Do I Use The Sample (Downloadable) Files?

Once you’ve downloaded the SwissArmyTemplates.zip file and opened it up, you’ll find that it contains five files:

  • SwissArmy_Control_Template.html
  • SwissArmy_Control_Template.js
  • SwissArmy_Item_Template.html
  • SwissArmy_Item_Template.js
  • SwissArmy_Styles.css

The two HTML files are each of the display templates in their HTML form. These HTML files can be processed through SharePoint 2013’s Design Manager to generate the corresponding JavaScript files that are actually used by the CSWP. Alternatively, the JS files that are included in the ZIP file can be used as-is and dropped directly into the Master Page Gallery > Display Templates > Content Web Parts folder within a site collection to make them available to the CSWP.

The remaining file is a CSS style sheet, and it provides the look and feel that is employed by the display templates. The style sheet itself is referenced from with the Control template files (SwissArmy_Control_Template.html and SwissArmy_Control_Template.js), and the Control template files assume that the style sheet will be available in the following location within the site collection:

~sitecollection/Style Library/en-us/SwissArmyStyles/SwissArmy_Styles.css

If you place the style sheet somewhere else in the site collection, ensure that the $includeCSS() calls in the SwissArmy_Control_Template.html file (line 25) and the SwissArmy_Control_Template.js file (line 164) are updated accordingly.

Final Request

Thanks for citing appropriatelyAs with all of the resources I make available, please feel free to use the display templates and style sheet I’ve provided within your own projects, either as-is or in a form that you’ve modified to suit your needs.

If you do share or redistribute what I’ve provided in some form, whether or not you’ve made modifications, I simply ask that you reference the original source (i.e., me and/or this blog post) in what you’re sharing. I do believe in citing sources where appropriate.

Thanks, and have fun paging through search results!

References and Resources

  1. SharePoint Interface: Files for the Swiss Army Knife CSWP example
  2. MSDN: Content Search Web Part in SharePoint 2013
  3. MSDN: SharePoint 2013 Design Manager display templates
  4. MSDN: Overview of Design Manager in SharePoint 2013
  5. SharePoint Interface: Presentation for SharePoint’s New Swiss Army Knife: The Content Search Web Part
  6. MSDN: 3.1.4.1 ProcessQuery
  7. JSON.org: Introducing JSON

Wrap-Up, Roll-Up, and Move-On!

My time with Idera has come to an end, so I wanted to aggregate some of the resources I assembled with them. I also wanted to share some information about my new company, Bitstream Foundry LLC.

Train Series Sometimes it’s hard to believe just how quickly time flies by. At the tail end of last December, I announced that some big changes were coming for me – namely that I would be transitioning into something new from an employment perspective. Today is the last day of normal business in March 2013, and that means my time with Idera is at an end.

My last three years with Idera have been quite a whirlwind of activity. I feel very fortunate and am extremely thankful to Idera for the opportunities they’ve afforded me – especially over the last year in my role as their Chief SharePoint Evangelist. In that role, I was given the latitude to spend a significant chunk of my time focusing on an area that is very important to me personally: the SharePoint Community.

The Roll-Up

In thinking about my role and some of what I’ve done over the last three years, it occurred to me that it might be nice to summarize and link to some of the materials I assembled while at Idera. I’ve occasionally referenced these items in the past, but I don’t think I’ve ever tried to aggregate them into one post or in one place.

Blog Posts

In the last half a year or so, my regular content generation efforts were being funneled to Idera’s SharePoint “Geek Stuff” blog. Here’s a table (with associated links) to the posts I’ve written:

March 19, 2013 Maskthumb Plan Your SharePoint Farm Right with a SQL Server Alias
February 8, 2013 Strategythumb Do You Have a SharePoint Backup Strategy?
January 17, 2013 Cheating on a Test The Five Minute Cheat-Sheet on SharePoint 2013’s Distributed Cache Service
December 20, 2012 smart girlfriends smiling and looking at the laptop Why Administrators Will Giggle Like Schoolgirls About SharePoint 2013’s New App Model
November 20, 2012 IderaIceThumb1 Sean’s Thoughts on the Microsoft SharePoint Conference 2012
October 19, 2012 Broken electric cable. Getting the Permissions Wired-Up Properly When Attaching a Content Database to a SharePoint Farm
September 21, 2012   Okay, Really – What Can I Do With a SharePoint Farm Configuration Database Backup?
August 24, 2012 roots Do I Really Need to Backup Up the SharePoint Root?
June 20, 2012 Talking with John Ferringer Interview with John Ferringer
June 8, 2012 TechEd '99 Baseball Cap TechEd – Why Should You Care?

SharePoint Smarts

There was a point in the past when Idera was publishing a sort of newsletter called “SharePoint Smarts,” and I wrote a couple of articles for the newsletter before it eventually rode off into the sunset:

Whitepapers

Over the years, I’ve also written or co-authored a handful of whitepapers for Idera. At the time I’m writing this post, it appears that a couple of those whitepapers are still available:

And although it isn’t available just yet, sometime soon Idera will be releasing another whitepaper I wrote that had the working title of “SharePoint Caching Implementation Guide.” If that sounds at all interesting, keep an eye on the Whitepapers section of Idera’s Resources page.

Moving-On

Bitstream Foundry LLC Although I’m going to miss my friends at Idera and wish them the best of luck going forward, I’m very excited about some things I’ve got cooking – particularly with my new company!

A couple of months back, I launched Bitstream Foundry, LLC, with the intention of getting back into more hands-on SharePoint work. My intention is to focus initially on a combination of custom SharePoint development work and SharePoint App Store product development. In the past, I’ve been a “switch hitter” when it comes to SharePoint, and I’ve gone back and forth between development and administration roles fairly regularly. Although I’m not abandoning my admin “comrades in arms,” I have to admit that I tend to get the greatest enjoyment out of development work. Between custom solutions and App Model development, I’m pretty sure I’ll be able to keep myself busy.

Microsoft BizSpark Things are falling into place with the new company, as well. I applied for membership in Microsoft’s BizSpark program yesterday, and within hours I was accepted – much to my surprise. Why was I surprised? Well, my company website is (at the moment) being redirected to a “coming soon” page I put together on the new Microsoft Azure Web Sites offering. I’ve been waiting for an Office 365 tenant upgrade so that I can build out a proper site on SharePoint 2013, but the upgrade seems to be taking much longer than originally expected …

I also learned today that my application to get Bitstream Foundry listed in the SharePoint App Store was approved, so the way is paved for me to roll out Apps. Now I just need to write them!

One Thing That Won’t Change

Despite all of the recent changes, one aspect of my professional life that won’t be changing is my commitment to sharing with (and giving back to) the SharePoint community. My confidence in my current situation would probably be substantially lower if it weren’t for all of you – my (SharePoint) friends. Over the last several months, my belief in “professional karma” has been strongly reinforced. I’ve always tried to help those who’ve asked for my time and assistance, and I’ve seen that goodwill return to me as I’ve sought input and worked to figure out “what’s next.” To those of you who have offered advice, provided feedback, written endorsements/recommendations, and more, you have my most heartfelt thanks.

I love interacting with all of you, and I still get tremendous enjoyment out of blogging, speaking, teaching, and sharing with everyone in the SharePoint space. My “official” days as a full-time evangelist may be behind me, but that won’t really change anything for me going forward as far as community involvement goes. I’ll continue to answer emails, blog when I have information worth sharing, assemble tools/widgets, help organize events, and generally do what I can to help all of you as you’ve helped me. I’m also honored to be a part of several upcoming events, and I hope to see some of you when I’m “on tour.” If we haven’t met, please say hi and introduce yourself. Making new friends and connections is one of the most rewarding aspects of being out-and-about :-)

References and Resources

  1. Blog Post: Big Changes and Resolutions for 2013
  2. Company: Idera
  3. Yahoo! Finance: Press Release
  4. Idera: SharePoint Geek Stuff Blog
  5. Idera: Resources Page
  6. Microsoft: BizSpark
  7. Company Site: Bitstream Foundry, LLC
  8. Microsoft: Azure Web Sites
  9. Microsoft: Office 365 Enterprise E3
  10. Microsoft: SharePoint App Store
  11. SharePoint Interface: Events and Activities

Custom Ribbon Button Image Limitations with SharePoint 2013 Apps

What started as a simple attempt to use the ~appWebUrl token in an image URL became a deep dive into SharePoint’s internal processing of custom actions and the App deployment process. In this post, I cover what will and won’t work for custom action image URLs in your own SharePoint 2013 Apps.

A custom action button with image My adventures in SharePoint 2013 App Model Land have been going pretty well, but I recently encountered a limitation that left me sort of scratching my head.

The limitation applies to the creation of custom actions for SharePoint apps. To be more specific: the problem I’ve encountered is that there doesn’t appear to be a way to package and reference (using relative links) custom images for ribbon buttons like the one that’s circled in the image above and to the left. This doesn’t mean that custom images can’t be used, of course, but the work-around isn’t exactly something I’m particularly fond of (nor is it even feasible) in some application scenarios.

If you’re not familiar with the new SharePoint 2013 App Model, then you may want to do a little reading before proceeding with this post. I’m only going to cover the App Model concepts that are relevant to the limitation I observed and how to address/work-around it. However, if you are familiar with the new 2013 App Model and creating custom actions in SharePoint 2010, then you may want to jump straight down to the section titled Where the Headaches Begin.

One more warning: this post does some heavy digging into SharePoint’s internal processing of custom ribbon actions and URL tokens. If you want to skip all of that and head straight to the practical take-away, jump down to the What About the Image32by32 and Image16by16 Attributes section.

Adding a Ribbon Custom Action

First, let me do a quick run-through on custom actions. They aren’t unique to SharePoint 2013 or its new “Cloud App Model.” In fact, the type of custom action I’m talking about (i.e., extending the ribbon) became available when the Ribbon was introduced with SharePoint 2010.

With a SharePoint 2013 App, adding a new button to the ribbon is a relatively simple affair. It starts with choosing the Ribbon Custom Action option from the Add New Item dialog as shown below and to the left. Once a name is provided for the custom action and the Add button is clicked, the Create Custom Action for Ribbon dialog appears as shown below and to the right. There’s a third dialog page that further assists in setting some properties for a custom action, but I’m going to skip over it since it isn’t relevant to the point I’m trying to make.

Adding a Ribbon Custom Action

Create Custom Action for Ribbon

I want to call attention to one of the selections I made on the Create Custom Action for Ribbon dialog, though; specifically, the decision to expose the custom action in the Host Web rather than in the App Web.

Why is this choice so important? Well, the new App Model enforces a relatively strict boundary of separation between SharePoint sites and any custom applications (running under the new App Model) that they may contain. A SharePoint site (Host Web) can technically “host” applications, but those applications operate in an isolated App Web that may have components running on an entirely different server. Under the new App Model, no custom app code is running in the Host Web.

App Webs (where custom applications exist after installation) don’t have direct access to the Host Web in which they’re contained, either. In fact, App Webs are logically isolated from their Host Web parents. If App Webs want to communicate with their Host Web parent to interact with site collection data, for example, they have to do so through SharePoint’s Client-Side Object Model (CSOM) or the Representational State Transfer (REST) interface. The old full-trust, server-side object isn’t available; everything is “client-side.”

There are some exceptions to this model of isolation, and one of those exceptions is the use of custom actions to allow an App (residing in an App Web) to partially wire itself into the Host Web. The Create Custom Action for Ribbon dialog shown above, for instance, adds a new button to the ribbon for each of the Document Libraries in the Host Web. This gives users a way to navigate directly from Document Libraries (in the Host Web) to a page in the App Web, for example.

The Elements.xml file that gets generated for the custom action once the Visual Studio wizard has finished running looks something like the following:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction Id="1470c964-6b8a-4d79-9817-4d32c898ffbe.RibbonCustomAction1"
                RegistrationType="List"
                RegistrationId="101"
                Location="CommandUI.Ribbon"
                Sequence="10001"
                Title="Invoke &apos;LibraryDetailsCustomAction&apos; action">
    <CommandUIExtension>
      <!-- 
      Update the UI definitions below with the controls and the command actions
      that you want to enable for the custom action.
      -->
      <CommandUIDefinitions>
        <CommandUIDefinition Location="Ribbon.Library.Actions.Controls._children">
          <Button Id="Ribbon.Library.Actions.LibraryDetailsCustomActionButton"
                  Alt="Examine Library Details"
                  Sequence="100"
                  Command="Invoke_LibraryDetailsCustomActionButtonRequest"
                  LabelText="Examine Library Details"
                  TemplateAlias="o1"
                  Image32by32="_layouts/15/images/placeholder32x32.png"
                  Image16by16="_layouts/15/images/placeholder16x16.png" />
        </CommandUIDefinition>
      </CommandUIDefinitions>
      <CommandUIHandlers>
        <CommandUIHandler Command="Invoke_RibbonCustomAction1ButtonRequest"
                          CommandAction="LibraryManager\Pages\LibraryDetails.aspx"/>
      </CommandUIHandlers>
    </CommandUIExtension >
  </CustomAction>
</Elements>

Deploying the App that contains the custom action markup shown above creates a new button in the ribbon of each Host Web Document Library. By default, each button looks like the following:

Custom Ribbon Button

There are a few attributes in the previous XML that I’m going to repeatedly come back to, so it’s worth taking a closer look at each one’s purpose and associated value(s):

  • Image32by32 and Image16by16 for the <Button /> element. These two attributes specify the images that are used when rendering the custom action button on the ribbon. By default, they point to an orange dot placeholder image that lives in the farm’s _layouts folder.
  • CommandAction for the <CommandUIHandler /> element. In its simplest form, this is the URL of the page to which the user is redirected upon pressing the custom ribbon button.

The Problem with the Default CommandAction

When a user clicks on a custom ribbon button in one of the Host Web document libraries, the goal is to send them over to a page in the App Web where the custom action can be processed. Unfortunately, the default CommandAction isn’t set up in a way that permits this.

CommandAction="LibraryManager\Pages\LibraryDetails.aspx"

In fact, attempting to deploy the solution to Office 365 with this default CommandAction results in failure; the App package doesn’t pass validation.

To understand why the failure occurs, it’s important to remember the isolation that exists between the Host Web and the App Web. To illustrate how the Host Web and App Web are different from simply a hostname perspective, consider the project I’ve been working on as an example:

Notice that although the /sites/dev2 relative path portion is the same for both the Host Web and App Web URLs, the hostname portion of each URL is different. This is by design, and it helps to enforce the logical separation between the Host Web and App Web – even though the App Web technically resides within the Host Web.

Looking again at the default CommandAction attribute reveals that its value is just an ASPX page that is identified with a relative URL. Rather than pointing to where we want it to point …

https://mcdonough-bc920dbeb7ecd3.sharepoint.com/sites/dev2/LibraryManager/Pages/LibraryDetails.aspx

… it ends up pointing to a non-existent destination in the Host Web:

https://mcdonough.sharepoint.com/sites/dev2/LibraryManager/Pages/LibraryDetails.aspx

And this is exactly what should happen. After all, the custom action is launched from within the Host Web, so a relative path specification should resolve to a location in the Host Web – not the location we actually want to target in the App Web.

Fixing the CommandAction

The Key! Thankfully, it isn’t a major undertaking to correct the CommandAction attribute value so that it points to the App Web instead of the Host Web. If you’ve worked with SharePoint at all in the past, then you may know that the key to making everything work (in this situation) is the judicious use of tokens.

What are tokens? In this case, tokens are specific string sequences that SharePoint parses at run-time and replaces with a value based on the run-time environment, action that was performed, associated list, or some other context-sensitive value that isn’t known at design-time.

To illustrate how this works, consider the default CommandAction attribute:

CommandAction="LibraryManager\Pages\LibraryDetails.aspx"

Modifying the attribute as follows changes the destination URL of the button so that the user is redirected to the desired page in the App Web rather than the Host Web:

CommandAction="~appWebUrl/Pages/LibraryDetails.aspx"

The ~appWebUrl token is replaced at run-time with the actual URL of the associated App Web (https://mcdonough-bc920dbeb7ecd3.sharepoint.com/sites/dev2) to build the desired destination link.

SharePoint defines a whole host of URL strings and tokens for use in Apps. As it turns out, a fairly complete list has been aggregated and defined in a handy little page on MSDN. Thanks to the always-helpful Andrew Clark for pointing this out to me; I hadn’t realized Microsoft had pulled so many tokens together in one place!

Where the Headaches Begin

Baby Crying Since tokens are the key to inserting context-dependent values at run-time, you’d think they’d have been implemented and usable anywhere a developer needs to cross the Host Web / App Web divide.

Apparently not. To be more specific (and fair), I should instead say “not consistently.”

Since this blog post is about image limitations with custom ribbon buttons, you can probably guess where I’m headed with all of this. So, let’s take a look at the Image16by16 and Image32by32 attributes.

By default, the Image16x16 and Image32by32 attributes point to a location in the _layouts folder for the farm. Each attribute value references an image that is nothing more than a little round orange dot:

Image32by32="_layouts/15/images/placeholder32x32.png"
Image16by16="_layouts/15/images/placeholder16x16.png"

Much like the CustomAction attribute, it stands to reason that developers would want to replace the placeholder image attribute values with URLs of their choosing. In my case, I wanted to use a set of images I was deploying with the rest of the application assets in my App Web. So, I updated my image attributes to look like the following:

Image32by32="~appWebUrl/Images/sharepoint-library-analyzer_32x32-a.png"
Image16by16="~appWebUrl/Images/sharepoint-library-analyzer_16x16-a.png"

Tokens Do Not Work for Image Attributes I deployed my App to my Office 365 Preview tenant, watched my browser launch into my App Web, hopped back to the Host Web, navigated to a document library, and looked at the toolbar. I was not happy by what I saw (on the left).

The image I had specified for use by the button wasn’t being used. All I had was a broken image link.

Examining the properties for the broken image quickly confirmed my fear: the ~appWebUrl token was not being processed for either of the Image32by32 or Image16by16 attributes. The token was being output directly into the image references.

I tried changing the image attributes to reference the App Web a couple of different ways (and with a couple of different tokens), but none of them seemed to work.

I did a little digging, and I saw that Chris Hopkins (over at Microsoft) covered this very topic for sandboxed solutions in SharePoint 2010. In Chris’ article, though, it was clear that tokens such as ~site and ~sitecollection were valid for use by the Image32by32 and Image16by16 attributes.

To see if I was losing my mind, I decided to try a little experiment. Although I knew it wouldn’t solve my particular problem, I decided to try using the ~site token just to see if it would be parsed properly. Lo and behold, it was parsed and replaced. ~site worked. So, ~site worked … but ~appWebUrl didn’t?

That didn’t make any sense. If it isn’t possible to use the ~appWebUrl token, how are developers supposed to reference custom images for the buttons they deploy in their Apps? Without the ~appWebUrl, there’s no practical way to reference an item in the App Web from the Host Web.

Token Forensics

When I find myself in situations where I’m holding results that don’t make sense, I can’t help myself: I pull out Reflector and start poking around for clues inside SharePoint’s plumbing. If I dig really hard, sometimes I find answers to my questions.

RegisterCommandUIWithRibbon After some poking around with Reflector, I discovered that the “journey to enlightenment” (in this case) started with the RegisterCommandUIWithRibbon method on the SPCustomActionElement type. It is in this method that the Image16by16 and Image32by32 attributes are read-in from the XML file in which they are defined. Before assignment for use, they’re passed through a couple of methods that carry out token parsing:

  • ReplaceUrlTokens on the SPCustomActionElement type
  • UrlFromPrefixedUrlCore on the the SPUtility type

Although these methods together are capable of recognizing and replacing many different token types (including some I hadn’t seen listed in existing documentation; e.g., ~siteCollectionLayouts), none of the new SharePoint 2013 tokens, like the ~appWebUrl and ~remoteWebUrl ~remoteAppUrl tokens, appear in these methods.

Interestingly enough, I didn’t see any noteworthy differences between the path of execution for processing image attributes and the sequence of calls through which CommandAction attributes are handled in the RegisterCommandUIExtension method of the SPRibbon type. The RegisterCommandUIExtension method eventually “punches down” to the ReplaceUrlTokens and UrlFromPrefixedUrlCore methods, as well.

The differences I was seeing in how tokens were handled between the CommandAction and Image32by32/Image16by16 attributes had to be originating somewhere else – not in the processing of the custom action XML.

Deployment Modifications

After some more digging in Reflector to determine where the ~appWebUrl actually showed-up and was being processed, I came across evidence suggesting that “something specialwas happening on App deployment rather than at run-time. The ~appWebUrl token was being processed as part of a BuildTokenMap call in the SPAppInstance type; looking at the call chain for the BuildTokenMap method revealed that it was getting called during some App deployment operations processing.

App Deployment Hierarchy to BuildTokenMap

If changes were taking place on App deployment, then I had a hunch I might find what I was looking for in the content database housing the Host Web to which my App was being deployed. After all, Apps get deployed to App Webs that reside within a Host Web, and Host Webs live in content databases … so, all of the pieces of my App had to exist (in some form) in the content database. 

I fired-up Visual Studio, stopped deploying to Office 365, and started deploying my App to a site collection on my local SharePoint 2013 VM farm. Once my App was deployed, I launched SQL Management Studio on the SQL Server housing the SharePoint databases and began poking around inside the content database where the target site collection was located.

Brief aside: standard rules still apply in SharePoint 2013, so I’ll mention them here for those who may not know them. Don’t poke around inside content databases (or any other databases) in live SharePoint environments you care about. As with previous versions, querying and working against live databases may hurt performance and lead to bigger problems. If you want to play with the contents of a SharePoint database, either create a SQL snapshot of it (and work against the snapshot) or mount a backup copy of the database in a test environment.

I wasn’t sure what I was looking for, so I quickly examined the contents of each table in the content database. I hit paydirt when I opened-up the CustomActions table. It had a single row, and the Properties field of that row contained some XML that looked an awful lot like the Elements.xml which defined my custom action:

<?xml version="1.0" encoding="utf-16"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
	<CustomAction Title="Invoke 'LibraryDetailsCustomAction' action" Id="4f835c73-a3ab-4671-b142-83304da0639f.LibraryDetailsCustomAction" Location="CommandUI.Ribbon" RegistrationId="101" RegistrationType="List" Sequence="10001">
		<CommandUIExtension xmlns="http://schemas.microsoft.com/sharepoint/">
			<!-- 
      Update the UI definitions below with the controls and the command actions
      that you want to enable for the custom action.
      -->
			<CommandUIDefinitions>
				<CommandUIDefinition Location="Ribbon.Library.Actions.Controls._children">
					<Button Id="Ribbon.Library.Actions.LibraryDetailsCustomActionButton" Alt="Examine Library Details" Sequence="100" Command="Invoke_LibraryDetailsCustomActionButtonRequest" LabelText="Examine Library Details" Image16by16="~site/Images/sharepoint-library-analyzer_16x16-a.png" Image32by32="~appWebUrl/Images/sharepoint-library-analyzer_32x32-a.png" TemplateAlias="o1"/>
				</CommandUIDefinition>
			</CommandUIDefinitions>
			<CommandUIHandlers>
				<CommandUIHandler Command="Invoke_LibraryDetailsCustomActionButtonRequest" CommandAction="javascript:LaunchApp('709d9f25-bb39-4e6a-97d5-6e1d7c855f38', 'i:0i.t|ms.sp.int|a441fa2c-8c5f-4152-9085-3930239ab21b@9db0b916-0dd6-4d6c-be49-41f72f5dfc02', '~appWebUrl\u002fPages\u002fLibraryDetails.aspx?ListID={ListId}\u0026SiteUrl={SiteUrl}', null);"/>
			</CommandUIHandlers>
		</CommandUIExtension>
	</CustomAction>
</Elements>

There were some differences, though, between the Elements.xml I had defined earlier and what actually appeared in the Properties field. I narrowed my focus to the differences that existed between the non-working Image32by32/Image16by16 attributes

Image16by16="~appWebUrl/Images/sharepoint-library-analyzer_16x16-a.png"
Image32by32="~appWebUrl/Images/sharepoint-library-analyzer_32x32-a.png"

… and the CommandAction attribute.

CommandAction="javascript:LaunchApp('709d9f25-bb39-4e6a-97d5-6e1d7c855f38', 'i:0i.t|ms.sp.int|a441fa2c-8c5f-4152-9085-3930239ab21b@9db0b916-0dd6-4d6c-be49-41f72f5dfc02', '~appWebUrl\u002fPages\u002fLibraryDetails.aspx', null);"

As suspected, some deployment-time processing had been performed on the CommandAction attribute but not on the image attributes. The CommandAction still contained an ~appWebUrl token, but it was wrapped as part of a parameter call to a LaunchApp JavaScript function that appeared to be handled (or rather, executed) from a client-side browser.

Jumping into my App in Internet Explorer and opening IE’s debugging tools via <F12>, I did a search for the LaunchApp function within the referenced scripts and found it in the core.js library/script. Examining the LaunchApp function revealed that it called the LaunchAppInternal function; LaunchAppInternal, in turn, called back to the SharePoint server’s /_layouts/15/appredirect.aspx page with the parameters that were supplied to the original LaunchApp method – including the URL with the ~appWebUrl token.

To complete the journey, I opened up the Microsoft.SharePoint.ApplicationPages.dll assembly back on the server and dug into the AppRedirectPage class that provides the code-behind support for the AppRedirect.aspx page. When the AppRedirect.aspx page is loaded, control passes to the page’s OnLoad event and then to the HandleRequest method. HandleRequest then uses the ReplaceAppTokensAndFixLaunchUrl method of the SPTenantAppUtils class to process tokens.

The ReplaceAppTokensAndFixLaunchUrl method is noteworthy because it includes parsing and replacement support for the ~appWebUrl token, ~remoteWebUrl ~remoteAppUrl token, and other tokens that were introduced with SharePoint 2013. The deployment-time processing that is performed on the CommandAction attribute is what ultimately wires-up the CommandAction to the ReplaceAppTokensAndFixLaunchUrl method. The Image32by32 and Image16by16 attributes don’t get this treatment, and so the new 2013 tokens (like ~appWebUrl) can’t be used by these attributes.

What About the Image32by32 and Image16by16 Attributes?

Doubt Now that some of the key differences in processing between the CommandAction attribute and image attributes have been identified, let me jump back to the original problem. Is there anything that can be done with the Image32by32 and Image16by16 attributes that are specified in a custom action to get them to reference assets that exist in the App Web? Since tokens like ~appWebUrl (and ~remoteWebUrl for all you Autohosted and Provider-hosted application builders) aren’t parsed and processed, are there alternatives?

My response is a somewhat wishy-washy “doubtful.” In my estimation, you’d need to hack SharePoint with something like a javascript: tag for an image attribute (which, interestingly enough, doesn’t appear to be expressly blocked), find some way to obtain the App Web URL base, formulate the proper path to the image, and more. If it could be done, you’d be gaming SharePoint … and I could easily see a cumulative update or service pack breaking this type of elaborate work-around.

The safest and most pragmatic way to handle this situation, it seems, is to use absolute URLs for the desired image resources and forget about deploying them to the App Web altogether. For example, I placed the images I was trying to use on the ribbon buttons here on my blog and referenced them as follows:

Image16by16="https://sharepointinterface.files.wordpress.com/2013/01/sharepoint-library-analyzer_16x16-a.png"
Image32by32="https://sharepointinterface.files.wordpress.com/2013/01/sharepoint-library-analyzer_32x32-a.png"

Working Custom Button Image I had some initial concerns that I might inadvertently bump into some security boundaries, such as those that sometimes arise when an asset is referenced via HTTP from a site that is being served up under HTTPS. This didn’t prove to be the case, however. I tested the use of absolute URLs in both my development VM environment (served up under HTTP) and through one of my Office 365 Preview site collections (accessed via HTTPS), and no browser security warnings popped up. The target image appeared on the custom button as desired (shown on the left) in both cases.

Although the use of absolute URLs will work in many cases, I have to admit that I’m still not a big fan of this approach – especially for SharePoint-hosted apps like the one I’ve been working on. Even though Office 365 entails an “always connected” scenario, I can easily envision on-premises deployment environments that are taken offline some or all of the time. I can also see (and have seen in the past) SharePoint environments where unfettered Internet access is the exception rather than the rule.

In these environments, users won’t see image buttons at all – just blank placeholders or broken image links. After all, without Internet access there is no way to resolve and download the referenced button images.

Wrapping It Up

At some point in the future, I hope that Microsoft considers extending token parsing for URL-based attributes like Image32by32 and Image16by16 to include the ~appWebUrl, ~remoteWebUrl, and other new tokens used by the SharePoint 2013 App Model. In the meantime, though, you should probably consider getting an easily accessible online location (SkyDrive, Dropbox, a blog, etc.) for images and other similar assets if you’re building apps under the new SharePoint 2013 App Model and intend to use custom actions.

Update (1/27/2013)

I need to issue a couple of updates and clarifications. First, I need to be very clear and state that SharePoint-hosted apps were the focus of this post. In a SharePoint-hosted app, what I’ve written is correct: there is no processing of “new” 2013 tokens (like ~appWebUrl and ~remoteAppUrl) for the Image32by32 and Image16by16 attributes. Interestingly enough, though, there does appear to be processing of the ~remoteAppUrl in the Image32by32 and Image16by16 attributes specifically for the other application types such as provider-hosted apps and autohosted apps. Jamie Rance mentioned this in a comment (below), and I verified it with an autohosted app that I quickly spun-up.

I double-checked to see if the ~remoteAppUrl token would even be recognized/processed (despite the lack of a remote web component) for SharePoint-hosted apps, and it is not … nor is ~appWebUrl token processed for autohosted apps. The selective implementation of only the ~remoteAppUrl token for certain app types has me baffled; I hope that we’ll eventually see some clarification or changes. If you’re building provider-hosted or autohosted apps, though, this does give you a way to redirect image requests to your remote web application rather than an absolute endpoint. Thank you, Jamie, for the information!

And now for some good news that for SharePoint-hosted app creators. Prior to writing this post, I had posted a question about the tokens over in the SharePoint Exchange forums. At the time I wrote this post, there hadn’t been any activity to suggest that a solution or workaround existed. F. Aquino recently supplied an incredibly creative answer, though, that involves using a data URI to Base64-encode the images and package them directly into the Image32by32 and Image16by16 attributes themselves! Although this means that some image pre-processing will be required to package images, it gets around the requirement of being “always-connected.” This is an awesome technique, and I’ll certainly be adding it to my arsenal. Thank you, F. Aquino!

References and Resources

  1. MSDN: How to: Create custom actions to deploy with apps for SharePoint
  2. MSDN: Apps for SharePoint overview
  3. MSDN: Customizing and Extending the SharePoint 2010 Server Ribbon
  4. MSDN: How to: Complete basic operations using SharePoint 2013 client library code
  5. MSDN: How to: Complete basic operations using SharePoint 2013 REST endpoints
  6. MSDN: URL strings and tokens in apps for SharePoint
  7. Twitter: Andrew Clark
  8. Chris Hopkins’ Visilog: Using images on your ribbon buttons from a sandboxed solution in SharePoint 2010
  9. Software: Red Gate’s Reflector
  10. Service: Microsoft’s SkyDrive
  11. Service: Dropbox

How My View of Microsoft’s Vision for SharePoint in the Cloud Has Evolved

After working with the Office 365 Preview over the last several months, I shifted my thoughts on SharePoint in the Cloud. In this post, I share my thoughts and “revelations” about what’s coming with SharePoint 2013, Office 365, and usage of SharePoint in the Cloud.

Pointing Out Some Clouds It was about a year and a half ago when someone dialed-up the volume on “The SharePoint Cloud Message” in my world. It’s not that I hadn’t heard people talking about SharePoint in the Cloud prior to that; I guess it’s just that I started listening more closely because Microsoft was turning into one of the Cloud’s most vocal proponents.

Around the summer of 2010, it was becoming clear to me that Cloud-based SharePoint wasn’t just a passing trend. With Microsoft clearly stating its intention to make the Cloud a cornerstone of its business, I needed to start paying attention.

How I Saw Things Before

My relationship with Microsoft and Microsoft technologies goes back to the days of MS-DOS. As a result, I’ve always seen Microsoft as a company that was primarily interested in one thing: selling software. I worked for a Microsoft managed systems integration (SI) partner – Cardinal Solutions Group – for several years. During my years with Cardinal, my goal was to help others who had purchased Microsoft software make use of that software. In many cases, customer leads came from Microsoft either directly or indirectly. Microsoft sold the software, and we setup/customized/serviced/configured that software based on what a customer was trying to accomplish. It was a symbiotic relationship, and it was pretty easy for me to grasp.

Then the whole “Cloud thing” started. Cloud-based SharePoint and other Azure-branded services seemed a somewhat confusing move for Microsoft at first – at least to me. Even before Office 365, Microsoft offered hosted SharePoint through BPOS – or the Business Productivity Online Suite. At the time when BPOS was first released, I viewed it as something of a niche market for Microsoft. I had plenty of friends who worked at places like Rackspace and Fpweb.net, so the part I found unusual wasn’t really that “someone else” was hosting SharePoint and focusing on it as a service. The fact that Microsoft itself was getting serious about SharePoint and other services was the eyebrow raiser.

For Microsoft, it wasn’t just about selling software anymore.

The Biggest Hurdle

A Hurdle Of course, when Microsoft wants to succeed at something, they invest considerable planning and resources in it. Since Microsoft is essentially betting the farm (pun intended) on Office 365 and SharePoint in the Cloud, they’re pushing it very hard on multiple fronts. Redmond’s marketing machine has been talking Office 365 frequently and loudly for at least the last year. With each new release, developer tools like Visual Studio get more Cloud-friendly. Partners have incentives to get customers onto Office 365 and Azure services. Competitive price points make it difficult to ignore Microsoft’s Cloud offerings. For me (and I’m sure for many of you), it’s a lot to process.

I’d also be remiss if I didn’t say that I think Office 365 has a very compelling value proposition, even without SharePoint. SharePoint itself is a complex platform, though, and many organizations struggle with administrative needs like data protection, performance optimization, high availability, and basic day-to-day management. The idea of turning these concerns over to someone else (or some other entity) who better-understands them makes sense to me.

After working with SharePoint 2013 for several months now, I can easily say that the platform isn’t getting any easier. SharePoint 2013 has quite a few more “moving parts” relative to SharePoint 2010, just as SharePoint 2010 demonstrated itself to be significantly more complex than SharePoint 2007.

Despite the compelling nature of Office 365, I always seemed to come back around to fixate on one thought. This thought constantly reverberated through my head anytime “SharePoint in the Cloud” became a topic of conversation:

Most companies using SharePoint have made a significant investments in hardware, software, personnel, and services to get SharePoint up-and-running. They aren’t going to simply “dump” those on-premises investments and go to the Cloud tomorrow. The Cloud will happen, but it’s going to take longer than Microsoft thinks.

In discussions with many friends and respected professionals in the SharePoint community, I knew that I wasn’t completely alone in my way of thinking. In the conversations I’d had, there was almost always agreement that a shift to the Cloud and Cloud-based services would happen over time. The greatest debate seemed to be over whether it would happen next year or if it would take the next half a decade.

Breakthrough

Old Thinking I’d say my “breakthrough moment” came after I started playing with the Office 365 Preview more extensively a few months back. I initially set up a preview tenant to familiarize myself with what was coming, how SharePoint 2013 would be exposed, how to configure Office 365 tenants, etc. The more I played with the tenant, the more I thought about how truly useful Office 365 could be, particularly for non-enterprise customers, home users, and others who didn’t fit into SharePoint’s “big deployment picture” previously.

That’s when the pieces started to click into place for me. All along I had been thinking about Office 365 and Cloud-based SharePoint deployments along the lines of the bar chart seen above and to the right. Numbers and proportions are all relative, but the key concept I’m trying to convey with the chart is this: for some reason, I had always thought that the proponents of Cloud-based SharePoint were suggesting that Cloud adoption would come at the cost of on-premises deployments; i.e., on-premises users would “convert” to the Cloud. If Cloud-based deployments grew, that meant that on-premises deployments had to shrink. In short: I was inadvertently assuming that the overall number of SharePoint deployments had hit saturation and was remaining static.

I don’t think that way at all anymore.

New Thinking After I’d done some playing with my first tenant, it wasn’t long before I was setting up another two Office 365 tenants for other side projects. In conversations with friends in the SharePoint community, I was discovering that “everyone” was setting up tenants for their families, for their spouse’s business, etc. In almost all cases where tenants were being setup, the use cases were ones that didn’t align with traditional enterprise-scale on-premises SharePoint deployment and usage. In fact, the use cases were typically the types of things that would eventually find a home on Google Apps or its equivalent because Microsoft (previously) had nothing strong to offer in that space.

The more I think about it, the more I feel that Office 365 growth – once the new 2013 Preview goes live – will be aggressive and look something more like what I’ve charted above and to the left. While Office 365 might replace some on-premises deployments, particularly for smaller organizations, I don’t see that as its primary market (initially) or its strong suit. The greatest degree of Office 365 traction is going to be obtained with users who need a Google Apps-like solution but for whom buying the required infrastructure and expertise for Exchange, SharePoint, etc., is cost-prohibitive.

So, I stopped thinking “replacement” and started thinking “complement.” That’s my assessment and working outlook for the Office 365 (Preview) right now.

Why Not Everyone?

I’m sure that plenty of folks who’ve believed in “Cloud Power” since Day One probably think that I’m still being too conservative in my outlook for SharePoint on Office 365, and that may be true. However, I still see plenty of concerns that are near-and-dear to most enterprise and larger business customers, and I believe that they will be Cloud adoption blockers until they’re addressed directly and decisively. Here are just a few that come to mind.

1. Who owns the data? Sure, it’s your tenant … but do you own the data? Common sense would seem to suggest “yes,” but this is still uncharted legal territory. Don’t believe me? Do some background reading on the Megaupload situation and see how users of that Cloud-based service are faring in their attempts to get “their data” back.

2. What about disasters? Many people point to the Cloud as a solution for business continuity and disaster recovery (DR) concerns. The Cloud can certainly help, but I’ll tell you (somewhat authoritatively) that the Cloud doesn’t make DR concerns “go away” – especially for SharePoint. For one thing, you’re locked into your provider’s terms of service; if you need more aggressive RPO and RTO windows, then you need to be looking elsewhere. Even Cloud data centers themselves go down; what’s your plan then?

3. Can I leave my provider? Everyone is quick to talk about moving to the Cloud, and many companies are happy to talk about migration strategies. What if you want to leave or change providers, though? Do those migration strategies work? What do you lose? How long would it even take? These may not seem like important questions now, but they will become increasingly more important as Cloud adoption grows and more companies get in on the action. It stands to reason that some portion of those companies will fail, close-up shop, be bought, etc. When that happens, what do you do … and what happens to your SharePoint?

Wrap Up

Of course, my perspective on Office 365 uptake in the next several years could be completely off-the-mark. After all, I don’t really have any numbers to back up my hypotheses. They’re just my opinions, but they are in-line with my gut feel.

And I’ve learned to trust my gut.

References and Resources

  1. Network World: Microsoft’s Ballmer: ‘For the cloud, we’re all in’
  2. Company: Rackspace
  3. Company: Fpweb.net
  4. Company: Cardinal Solutions Group
  5. Microsoft: Windows Azure
  6. ZDNet: The road to Microsoft Office 365: The Past 
  7. Microsoft: Office 365 Preview
  8. Google: Google Apps
  9. TorrentFreak: Megaupload Seized Data Case Will Get a Hearing, Court Rules
  10. Book: The SharePoint 2010 Disaster Recovery Guide
  11. SharePoint Interface: RPO and RTO: Prerequisites for Informed SharePoint Disaster Recovery Planning
  12. ZDNet: Amazon cloud down; Reddit, Github, other major sites affected