tag:blogger.com,1999:blog-110962582024-03-13T15:28:50.413+01:00InfoWorker SolutionsWhen in doubt, hesitate!Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.comBlogger266125tag:blogger.com,1999:blog-11096258.post-48196803328999522162015-06-21T17:00:00.000+02:002015-06-22T10:09:35.578+02:00SP2013 MMS Example Scenario Not SupportedIf you need to implement a multi-department solution like the documented example scenario at <a href="https://technet.microsoft.com/en-us/library/ee424403.aspx">Overview of managed metadata service applications in SharePoint Server 2013</a>, it will work fine except it will break the incremental crawl of friendly URLs (FURL).<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-fkeVLCjmfSA/VYbQZvMXTHI/AAAAAAAAHuI/Ii7cTmkMnQ8/s1600/MMS_scenario_connections.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="http://2.bp.blogspot.com/-fkeVLCjmfSA/VYbQZvMXTHI/AAAAAAAAHuI/Ii7cTmkMnQ8/s320/MMS_scenario_connections.gif" width="300" /></a></div>
<br />
Microsoft support will tell you that using more than one proxy (connection) for a specific MMS is not supported, even if this scenario is documented and even if you can create more that one proxy and use each MMS proxy in two different proxy groups to have different default storage location settings for two web-applications.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-oksIvZCwttk/VYbQxWbCJwI/AAAAAAAAHuQ/Fk26fkGNk9g/s1600/MMS_scenario_connections_proxy4.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="110" src="http://3.bp.blogspot.com/-oksIvZCwttk/VYbQxWbCJwI/AAAAAAAAHuQ/Fk26fkGNk9g/s320/MMS_scenario_connections_proxy4.PNG" width="320" /></a></div>
<br />
Microsoft support will tell you that the product group says that the documented scenario on Technet is not supported. Using two connections (MMS proxy) would be required as the "column-specific term set location" needs to be different for the two web-apps, thus they cannot share/reuse a single MMS proxy.<br />
<br />
According to MSFT support, the documentation on Technet is "not very accurate".Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com0tag:blogger.com,1999:blog-11096258.post-70383376837836238232014-05-08T12:45:00.003+02:002015-02-19T19:56:34.707+01:00Getting SSL Termination to work for HNSC in SP2013We have been struggling a bit with getting <a href="http://technet.microsoft.com/en-us/library/cc424952(v=office.15).aspx#section2g">off-box SSL termination</a> to work properly for SharePoint 2013 host-named site collections (HNSC). We had issues with the ribbon, with admin pages like "manage content and structure", and with the term picker. Sure signs that some JavaScript files did not load. Users could not edit the terms in managed metadata fields, that is, terms could be selected, but clicking "ok" to save would just hang forever. A lot of scripts and links would not load, showing mixed content warnings in IE9 - and nothing at all in Chrome and Firefox, which both just blocks HTTP content on secure HTTPS pages.<br />
<br />
To cut to the chase, this setup for SSL offloading is what worked for us:<br />
<ul>
<li>create the web-app on port 80 (not 443), do not use the -SecureSocetsLayer switch</li>
<li>do not use the server name as the web-app name, you have a farm - don't you?</li>
<li>always extend the web-app to other zones <b>before</b> starting to create HNSC sites; leave one zone unextended, e.g. the "custom" zone</li>
<li>create a classic root site-collection with the same HTTP name as the web-app, do not use a HNSC for this</li>
<li>a site template is not required for the root site-collection</li>
<li>alternate access mapping (AAM) <a href="http://technet.microsoft.com/en-us/library/cc261814(v=office.15).aspx">is used for load balancing even for HNSC, but HNSCs can't use AAM for host name aliases</a></li>
<li>create the HNSC <b>using an internal HTTP URL in New-SPSite for the default zone</b>, remember that crawling must always use the default zone</li>
<li>create <b>a public URL alias for the default zone</b> by mapping an unextended zone using a HTTP<b>S</b> URL in Set-SPSiteUrl, such as the "custom" zone</li>
<li>create public HNSC mappings using HTTP<b>S</b> URL in Set-SPSiteUrl for the other zones</li>
<li>ensure that your gateway adds the custom header "front-end-https: on" for all your public URLs secured using SSL</li>
<li>note that using just "front-end-https: on" and HTTP in the public URL will <b>not correctly rewrite</b> all links in the returned pages</li>
</ul>
<br />
In short, the salient point is to use HTTPS in the public URLs even if the web-app zone does not use the SecureSocetsLayer switch nor any SSL certificates. The default zone of the web-application must be configured for crawling - either no SSL or full SSL with certificates assigned in IIS. With no SSL you have to simulate AAM by mapping two URLs to the HNSC default zone. Using Set-SPSiteUrl on an unextended zone is like creating an alias for the default zone.<br />
<br />
We had to use HTTP on the default zone to crawl the content of the published pages. It seems that if the web-application does not use SSL and your site default zone uses a HTTPS host header, then only the friendly URLs (FURL) will be crawled while the content will generate a lot of "<span style="background-color: white; font-family: 'Segoe UI', 'Lucida Grande', Verdana, Arial, Helvetica, sans-serif; font-size: 14px; line-height: 20.162017822265625px;"><span style="color: #444444;">This item comprises multiple parts and/or may have attachments. Not all of these parts were indexed.</span></span>" warnings. The result of the warning is no metadata being indexed, thus no search results - not good for a search-driven solution.<br />
<br />
Note that SSL is recommended for all web-applications in SP2013 also inside the firewall, especially if you use apps - as the OAuth tokens otherwise will be exposed in the HTTP traffic, just as classic IIS basic authentication is not recommended without SSL. We wanted to do SSL bridging with BigIP due to this, but could not get <a href="http://en.wikipedia.org/wiki/Server_Name_Indication">SSL server name indication</a> (SNI) configured successfully in BigIP v11 to allow us to have SSL certificates bound to two different IIS web-sites, even if IIS8 supports SNI.<br />
<br />
SNI is required when <a href="http://blogs.technet.com/b/rycampbe/archive/2013/06/07/ssl-sites-with-hostnames-in-sharepoint-2010-and-sharepoint-2013.aspx">the shared wildcard certificate or SAN certificate approach cannot be used</a> for your SP2013 web-application setup, i.e. when binding to host names in multiple IIS web-sites at the web-application level. SNI is required when you need to use more than one web-application or more than one zone (extended web-app), even if you could bind your one-SAN-to-rule-them-all certificate to multiple IIS web-sites. IIS cannot route the request based on the host header until the request has been decrypted - SNI allows the request to be routed to the correct IIS web-site.<br />
<br />
Remember that this is the path the HTTP(S) request travels from the browser:<br />
<br />
browser ><br />
host header ><br />
DNS A-record ><br />
virtual IP-address (VIP) in gateway > SSL off-box termination here<br />
load balancing ><br />
IIS server configured with IP-address ><br />
IIS web-site bound to IP-address (or host header) > normal SSL termination here<br />
SP web-application ><br />
site-collection bound to host header (HNSC)<br />
<br />
Keeping tabs on this will help you understand the <a href="http://technet.microsoft.com/en-us/library/cc424952(v=office.15).aspx">Technet guide to HNSC</a>, which has some room for improvements. See <a href="http://sharepointobservations.wordpress.com/2013/06/04/sharepoint-2013-host-named-site-collections-over-ssl-2/">this article by jasonth</a> for a step-by-step guide for HNSC and SSL. Note that <a href="http://blogs.msdn.com/b/markarend/archive/2012/05/30/host-named-site-collections-hnsc-for-sharepoint-2010-architects.aspx">binding to host names in IIS rather than to IP-addresses</a> for HNSCs at the SP2013 web-application level is supported, just as it was for SP2010.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com17tag:blogger.com,1999:blog-11096258.post-19857655779235401602014-05-01T10:58:00.001+02:002015-03-24T10:44:57.358+01:00Managed Metadata Navigation, Anonymous Users in SP2013<span style="background-color: rgba(255, 255, 255, 0);">The new term-driven navigation in SP2013 has some gotchas for anonymous users, resulting in them not seeing a full navigation menu. These are some things to check:</span><br />
<ul>
<li>Make sure there is <a href="http://blogs.msdn.com/b/pedrorod/archive/2013/01/30/managed-metadata-navigation-not-working-for-anonymous-users-in-sharepoint-2013.aspx">a defined physical page for each navigation node term</a></li>
<li>The terms must be available for tagging</li>
<li>Make sure that <a href="http://www.sintax.org/2012/03/sharepoint-2010-taxonomy-field-on-a-anonymous-publishing-web-site/">anonymous users have read access to the TaxonomyHiddenList</a> of the site-collection</li>
<li>Always <a href="http://blogs.technet.com/b/speschka/archive/2013/08/19/using-managed-metadata-with-variations-in-sharepoint-2013.aspx">manage the navigation termset from site settings of the site-collection</a> that hosts the termset; do not use manage term store from Central Admin</li>
<li>If your custom code can't resolve and show terms in the navigation termset, make sure to <a href="http://msdn.microsoft.com/en-us/library/office/jj163273(v=office.15).aspx">use a view that don't depend on the cache</a> being updated</li>
</ul>
Finally, remember that you have to publish a major version for each page that you link to from the navigation node, otherwise anonymous users won't see the page, and neither the term. This includes all items on the page that also requires approval, such as images. An easy thing to forget, if you've been so stupid as not to use the <a href="http://keutmann.blogspot.no/2008/05/publishingfeaturehandler-feature-id.html">simple publishing configuration</a> for your site. If you as an admin or logged in user can see terms and view a page, while visitors can not - you forgot to publish. An empty page or no term is a sure sign.<br />
<br />
Related to the managed navigation is the friendly URL (FURL) mechanism, which uses the term set structure to build the FURL from the linked-to term. To prevent broken links when moving a term, SP2013 stores links using the FIXUPREDIRECT.ASPX page, with params such as the termID, which will be resolved server-side into a friendly URL when rendered (see navigation term GetResolvedDisplayUrl). Do not render RichHtmlField using the simple "SPWC:FieldValue" web-control, as this will not resolve the fixup-links. In addition, having the same control both in an edit mode panel and in a display mode panel might cause problems.<br />
<br />
This all applies to author-in-place (AIP) usage of term-driven navigation and friendly URLs; cross-site publishing (XSP) have <a href="http://blog.mastykarz.nl/inconvenient-url-rewriting-catalog-items-sharepoint-2013/">different kind of issues</a>.<br />
<br />
Note that the <a href="http://www.herlitz.nu/2014/11/05/restoring-a-backed-up-site-collection-with-managed-navigation-in-another-web-application">managed navigation term set is stored in the default MMS</a> of the hosting web-application. It uses the local term store for the site-collection it belongs to (<a href="http://spyankulov.blogspot.no/2014/08/site-collection-term-set-groups-and-how.html">IsSiteCollectionGroup</a>). This will affect your backup/restore procedure as not only the content database or the site-collection backup will be needed for a restore, the MMS database or tenant backup is also needed. As all host-named site-collections (HNSC) share a web-application, restoring the MMS with it's term stores will affect the navigation term set of all site-collections. Take care.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com0tag:blogger.com,1999:blog-11096258.post-609031912195194612013-10-23T11:55:00.002+02:002013-10-24T12:55:17.855+02:00Roadmap for Responsive Design and Dynamic Content in SharePoint 2013Responsive design combined with dynamic user-driven content and mobile first seems to be the main focus everywhere these days. The approach outlined in <a href="http://blogs.msdn.com/b/sharepointdev/archive/2013/05/07/optimizing-sharepoint-2013-websites-for-mobile-devices.aspx">Optimizing SharePoint 2013 websites for mobile devices</a> by Waldek Mastykarz and his <a href="http://blog.mastykarz.nl/mavention-v3-user-experience/">How we did it</a> series show how it can be achieved using SharePoint 2013.<br />
<br />
But what if you have thousands of pages with good content that you want make resposive? What approach will you use to adapt all those articles after migrating the content over from SharePoint 2010? This post suggests a roadmap for gradually transforming your static content to become dynamic content.<br />
<br />
First of all, the metadata of your content types must be classified so that you know the ranking of the metadata within a content type, so that it can be used in combination with device channel panels and resposive web design (RWD) techniques to prioritize what to show on what devices. This will most likely introduce more specialized content types with more granular metadata fields. All your content will need to be edited to fit the new content classification, at least the most important content you have. By edited, I mean that the content type of articles must be changed and the content adapted to the new metadata; in addition, selecting a new content type will also cause a new RWD page layout to be used for the content. These new RWD page layouts and master pages are also something you need to design and implement, as part of your new user experience (UX) concept.<br />
<br />
While editing all the (most important) existing content, it is also a good time to ensure that the content gets high quality tagging according to your <a href="http://kjellsj.blogspot.no/2011/11/sp2010-information-architecture.html">information architecture</a> (IA), as tagging is the cornerstone of a good, dynamic user experience provided by <a href="http://sharepointthomas.blogspot.no/2013/02/term-based-navigation-in-sharepoint.html">term-driven navigation</a> and search-driven content. Editing the content is the most important job here, tagging the content is not required to enable RWD for your pages.<br />
<br />
Doing all of this at once as part of migrating to SP2013 is by experience too much to be realistic, so this is my recommended roadmap:<br />
<br />
<b>Phase 1</b><br />
- Focus on RWD based on the new content types and their new prioritized metadata and new responsive master pages and page layouts<br />
- Quick win: revise the search center to <a href="http://blogs.technet.com/b/tothesharepoint/archive/2013/09/03/how-to-change-the-way-search-results-are-displayed-in-sharepoint-server-2013.aspx">exploit the new search features</a>, even if tagging is postponed to a later phase (IA: findability and search experience)<br />
- Keep the existing information architecture structure, and thus the navigation as-is<br />
- Keep the page content as-is, do not add search-driven content to the pages yet, focus on making the articles responsive<br />
- Most time-consuming effort: adapting the content type of all articles, cutting and pasting content within each article to fit the new prioritized metadata structure<br />
<br />
<b>Phase 2</b><br />
- Focus on your new concept for structure and navigation in SP2013 (IA: content classification and structure, browse and navigate UX)<br />
- Tagging of the articles according to the new IA-concept for dynamic structuring of the content (IA: term sets for taxonomy)<br />
- Keep the page content as-is, no new search-driven UX in this phase, just term-driven navigation<br />
- Most time-consuming effort: tagging all of your articles, try scripting some auto-tagging based on the existing structure of the content<br />
<br />
<b>Phase 3</b><br />
- Focus on search-driven content in the pages according to the new concept (IA: discover and explore UX)<br />
- New routines and processes for authors, approvers and publishers based on new SP2013 capabilities (IA: content contributor experience)<br />
- Most time-consuming effort: <a href="http://technet.microsoft.com/library/dn169065.aspx">tune and tag the content of all your articles to drive the ranking of the search-driven content</a> according to the new concept<br />
<br />
<b>Phase 4</b><br />
- Content targeting in the pages based on <a href="http://blogs.msdn.com/b/adaptive_experiences_in_sharepoint_2013/archive/2012/11/16/adaptive-experiences-in-a-product-catalog-in-sharepoint-2013-using-facebook-location-data.aspx">visitor profile segmentation</a>, this kind of user-driven content is also search-driven content realized using query rules (and some code)<br />
<div>
<br /></div>
The IA aspects in the roadmap are taken from my <a href="http://kjellsj.blogspot.no/2011/11/sp2010-information-architecture.html">SharePoint Information Architecture from the Field</a> article.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com1tag:blogger.com,1999:blog-11096258.post-64558194350097258342013-03-16T10:52:00.000+01:002013-03-16T14:55:39.035+01:00Controlling Content Database Size in SharePoint<br />
<div class="MsoNormal">
</div>
<div class="MsoNormal">
</div>
<div class="MsoNormal">
A SharePoint <a href="http://sharepoint.microsoft.com/blog/Pages/BlogPost.aspx?pID=988">content database can be up to 4TB</a> with data (max 200GB is recommended). However, storage size is not the problem; it is the recovery time to restore all that data that is the availability problem. The recovery time decides for how long your business critical solution will be down. As SharePoint can spread its content across multiple databases, it is recommended that your architecture segments different content across different databases based on IA and other user experience aspects, plus business requirements for availability and recovery time. Plan for <a href="http://kjellsj.blogspot.no/2010/08/classification-and-structuring-of.html">structuring your solutions</a> with a strong focus on your <a href="http://kjellsj.blogspot.no/2011/11/sp2010-information-architecture.html">information architecture</a> (IA).</div>
<div class="MsoNormal">
<br /></div>
<div class="MsoNormal">
Here are some options for how to control the size of the content databases, without disposing and deleting content:</div>
<div class="MsoNormal">
<br /></div>
<div class="MsoNormal">
A)<span class="Apple-tab-span" style="white-space: pre;"> </span>Use an ootb Record Center as an archive for old content: The users must manually send each document to the RC using e.g. move and leave a link; note that only the latest major version with metadata is kept – all version history is lost. The <a href="http://kjellsj.blogspot.no/2011/05/sharepoint-site-lifecycle-management.html">information management policies</a> supported by SharePoint for retention and disposition can be used to automate the cleanup.<br />
As the RC has its own content databases, the live collaboration databases will grow slower or even shrink as outdated information is moved to the archive. Keeping the live databases small ensures shorter recovery time; while the recovery time for the archived content can be considerable, but not business critical.<br />
Search must be configured appropriately to cover both live and archived content.<br />
<br />
B)<span class="Apple-tab-span" style="white-space: pre;"> </span>Use a third-party archiving solution for SharePoint from e.g. MetaLogix or AvePoint. This has the same pros & cons as in option A, but the functionality is probably better in relation to keeping version history and batch management of outdated content.<br />
Search must be configured appropriately to cover both live and archived content.<br />
<br />
C)<span class="Apple-tab-span" style="white-space: pre;"> </span>Use a third-party remote blob storage (RBS) solution for SharePoint, such as MetaLogix StoragePoint, so that documents are registered in the database, but not stored there. This gives smaller content databases, but more complicated backup and recovery as the content now resides both in databases and on disk. Provided that you don’t lose both at the same time, the recovery time should be shorter.<br />
Search will work as before, as all content is still logically in the “database”.<br />
<br />
D)<span class="Apple-tab-span" style="white-space: pre;"> </span>Use powershell scripts or other code to implement the disposition of outdated content. The script can e.g. copy old documents to disk and <a href="http://blogs.sharepointpro.net/2011/03/30/versions.aspx">delete old versions from the content database</a>; the drawback being that all metadata will be lost and there is no link left in SharePoint.<br />
The databases size will shrink as data is actually deleted, and backup and recovery is more complicated as content is now both in the database and on disk (same as for option C).<br />
Search can be configured to also crawl and index the files on disk, but content ranking will suffer as the valuable metadata is lost.</div>
<div class="MsoNormal">
<br /></div>
<div class="MsoNormal">
My recommendation is to consider option A first, especially if you are able to define automated rules and exploit the built-in <a href="http://kjellsj.blogspot.no/2011/05/sharepoint-site-lifecycle-management.html">information management policies</a> in SharePoint. The keyword is *able* - in my experience, everyone is positive to having automated retention and disposition, but noone even at large banks and law firms are able to come up with the policies.<br />
<br />
Always consider using RBS for databases larger than 200GB, and note that <a href="http://www.metalogix.com/blog/11-10-04/Revisiting_SharePoint_Remote_BLOB_Storage.aspx">RBS also helps you meet the disk IOPS requirements</a> of SharePoint.</div>
<br />
<br />Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com0tag:blogger.com,1999:blog-11096258.post-12019311803458812332013-03-02T08:31:00.000+01:002013-03-22T11:12:54.653+01:00How to Debug SharePoint Solutions in a Multi-Server FarmSome tips on deploying and debugging code in a multi-server SharePoint 2010 farm:<br />
<br />
When debugging on a multi-server SharePoint farm, with Visual Studio on the app-server, then add AAM mapping for the app-server to the web-application to debug, otherwise VS can't attach to the local w3wp process.<br />
<br />
For example, to debug the web-app hosted on<br />
<ul>
<li>http://azure-sp2010web:8383/ </li>
</ul>
you must add the app-server URL first to AAM and then use it as Site URL when debugging<br />
<ul>
<li>http://azure-sp2010app:8383/</li>
</ul>
Make sure to browse the site using the added app-server URL to load the code in a local w3wp process.<br />
<br />
If the breakpoints have yellow warning triangles, then VS could not load the correct code to the w3wp processes attached to the debugger. Solve by rebuild and deploy to get the latest bits into the [14] hive and GAC. Note that you can’t activate WSPs on deploy in a multi-server farm, set the "Active Deployment Configuration" to “no activation” in Visual Studio project properties. If VS still can’t deploy the WSP, then use powershell to first Add-SPSolution and then Install-SPSolution across the farm.<br />
<br />
Validate the WSP deployment status in “Manage farm solutions” in Central Admin first, and make sure that your feature is activated in the site or subsite you try to debug.<br />
<br />
Happy debugging :)<br />
<br />Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com0tag:blogger.com,1999:blog-11096258.post-64451397144923281042012-10-04T17:00:00.004+02:002012-10-04T17:07:39.059+02:00Dynamically Assign Approver for the Content Approval Workflow in SP2010This post is a how-to guide for customizing the ootb SharePoint 2010 content approval workflow to automatically pick a user from the current list item such as the 'content resposible' field, and assign that user as the approver of the content, using SharePoint Designer. The customization of the content approval using SPD is quite straightforwards except for some less intuitive and misleading options for editing the workflow task process. It also involves publishing and editing the XML config of the workflow to enable using the "Start this workflow to approve publishing a major version of an item" option for automatically starting the approval workflow when the author check-in (submits) the page or document for approval.<br />
<br />
To get started, follow the <a href="http://roykimsharepoint.wordpress.com/2011/02/15/sharepoint-designer-walkthrough-copy-modify-publishing-workflow/">SharePoint Designer Walkthrough: Copy & Modify Publishing Workflow</a> steps 1-4 to make a copy of the "Approval - SharePoint 2010" workflow in the site-collection root. Edit the workflow name to suit your needs and make sure to pick the content type that contains the user field that you will use to auto-assign as the approver in "Pick a base content type to limit this workflow to". Otherwise you won't be able to add a lookup for that content type field. Click OK and save.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-3rrrVgVQcZ0/UG2QVlS9wEI/AAAAAAAABLc/MtObb-a3U_A/s1600/SP2010_copy_workflow.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="315" src="http://4.bp.blogspot.com/-3rrrVgVQcZ0/UG2QVlS9wEI/AAAAAAAABLc/MtObb-a3U_A/s400/SP2010_copy_workflow.PNG" width="400" /></a></div>
<br />
Open the saved custom approval workflow and click "edit workflow". Change the name of the "Start Approval Workflow Task" action as you like. Then click on "Parameter: Approvers" to change the "with [these users]" for the workflow action into using an user from the current item that is pending approval. Now, to dynamically assign an approver, you need to click the "Enter participants manually" button.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-aTfZysw90FY/UG2ZyMUnoYI/AAAAAAAABL8/b_eeaBJP5oA/s1600/SP2010_dynamic_participants.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="299" src="http://1.bp.blogspot.com/-aTfZysw90FY/UG2ZyMUnoYI/AAAAAAAABL8/b_eeaBJP5oA/s320/SP2010_dynamic_participants.PNG" width="320" /></a></div>
<br />
Then in "Select task participants" click the address book button to open the "Select users" dialog box. Now select "Workflow lookup for a user", which will trigger the "Lookup for person or group" dialog.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-eWGr5q5lPnM/UG2cLbVA2EI/AAAAAAAABME/rbUgBL4Rr4g/s1600/SP2010_dynamic_content_approver.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="270" src="http://3.bp.blogspot.com/-eWGr5q5lPnM/UG2cLbVA2EI/AAAAAAAABME/rbUgBL4Rr4g/s320/SP2010_dynamic_content_approver.PNG" width="320" /></a></div>
<br />
Click OK three times, and the start approval workflow action should now look like this:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-pY1sZxzZHvM/UG2c0PKAsfI/AAAAAAAABMM/EUaAq2fN840/s1600/SP2010_customize_start_approval_workflow_task.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="111" src="http://1.bp.blogspot.com/-pY1sZxzZHvM/UG2c0PKAsfI/AAAAAAAABMM/EUaAq2fN840/s400/SP2010_customize_start_approval_workflow_task.PNG" width="400" /></a></div>
Now, as a side-effect the comments for the task has been unbound, so you need to click on properties for the workflow action and bind the comments to the "Parameters: Request" again. This will ensure that the text entered in the request field when starting the approval workflow will not be missing when the approver opens the workflow task to approve or reject the pending content.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-Xu2EHGIju3k/UG2eHt-HoYI/AAAAAAAABMU/3v_rRCir1so/s1600/SP2010_workflow_task_comments.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="281" src="http://3.bp.blogspot.com/-Xu2EHGIju3k/UG2eHt-HoYI/AAAAAAAABMU/3v_rRCir1so/s320/SP2010_workflow_task_comments.PNG" width="320" /></a></div>
<br />
Click OK two times, and your customized approval workflow with a dynamic approver is almost ready. There are a couple of workflow parameters that are not needed when automatically assigning the approver, these can be hidden from the workflow initiation form so that the author is not confused when the workflow starting form is shown on check-in. Click "Initiation form parameter" in the ribbon and make sure that the "Approvers" and "Expand groups" parameters are not shown during workflow initiation.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-vE9dftyb5vk/UG2ftSy6XKI/AAAAAAAABMc/MJ8j4tzOx3g/s1600/SP2010_workflow_parameters.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="189" src="http://1.bp.blogspot.com/-vE9dftyb5vk/UG2ftSy6XKI/AAAAAAAABMc/MJ8j4tzOx3g/s320/SP2010_workflow_parameters.PNG" width="320" /></a></div>
<br />
Removing the parameters is also an option, it just feels safer to just hide them rather than deleting them. Save and publish your custom content approval workflow as a globally reusable workflow as shown in <a href="http://roykimsharepoint.wordpress.com/2011/02/15/sharepoint-designer-walkthrough-copy-modify-publishing-workflow/">step 13-15</a>. You can now follow the <a href="http://blog.pointbeyond.com/2009/10/20/configuring-approval-in-sharepoint-using-the-approval-workflow/">Configuring Approval in SharePoint</a> steps to use your workflow on a document library that requires content approval, and it will work for the content types that the custom approval workflow is associated with for the selected list.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-9S73ZukFX-4/UG2mIYNKFvI/AAAAAAAABMw/KosUJwB1650/s1600/SP2010_start_content_approval_on_checkin.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="103" src="http://1.bp.blogspot.com/-9S73ZukFX-4/UG2mIYNKFvI/AAAAAAAABMw/KosUJwB1650/s400/SP2010_start_content_approval_on_checkin.PNG" width="400" /></a></div>
<br />
However, the "Start this workflow to approve publishing a major version of an item" option will be missing when associating the workflow with the document library, even if it is a bonafide approval workflow and even if the list has "Require content approval for submitted items" turned on. This is caused by the workflow being a content type rather than a list workflow, and only list workflows can be configured to start content approval on check-in. Luckily, it is rather simple to change this by editing the XML config file for the published workflow XOML definition using SPD, as shown in <a href="http://thorprojects.com/blog/archive/2011/10/01/writing-your-own-sharepoint-publishing-approval-workflow.aspx">Writing Your Own SharePoint Publishing Approval Workflow</a>. I made these changes to the file:<br />
<ul>
<li>changed the Category attribute to "List;Language:1033;#ContentType;Language:1033" </li>
<li>changed the AllowStartOnMajorCheckin attribute to "true"</li>
<li>removed the ContentTypeId attribute completely</li>
</ul>
After directly editing and saving the workflow config file using SPD, the workflow designer will be somewhat out of sync with the XOML definition, due to the removal of the content type and changing of the association category. Still, the actual workflow logic is still working as expected.<br />
<br />
You can now associate and test the custom approval workflow on your document library. These are the typical "Version settings" used for the "Pages" list in publishing sites:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-5_7FZkcmSKI/UG2WqkSxTLI/AAAAAAAABLs/JY953pJhtrs/s1600/SP2010_content_approval_list_settings.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="248" src="http://2.bp.blogspot.com/-5_7FZkcmSKI/UG2WqkSxTLI/AAAAAAAABLs/JY953pJhtrs/s320/SP2010_content_approval_list_settings.PNG" width="320" /></a></div>
<br />
Note that the users that are assigned as the approver must be members of the "Approvers" group in publishing sites, or have the right to edit draft items in the document library, otherwise they cannot open the workflow task to approve or reject the content, even if the user is the owner of the task.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com4tag:blogger.com,1999:blog-11096258.post-44034621669623951302012-09-12T18:31:00.000+02:002012-10-13T20:43:04.253+02:00Migrated Content Database gives Unexpected Error for all Publishing PagesToday I migrated a SharePoint 2010 publishing site content database to our <a href="http://kjellsj.blogspot.no/2012/08/sp2010-azure-on-premises-gateway.html">Azure staging farm</a>. All went smooth after using sp_changedbowner on the restored database before adding the content database to the web-application using Central Admin. However, when I tried to browse the publishing site, I got the "an unexpected error has occurred" message. Browsing /_layouts/settings.aspx worked fine and so did browsing "all site content" and the /pages/ list settings.<br />
<br />
Using the correlation ID in combination with the ULS viewer lead me to this infamous portal sitemap provider exception:<br />
<br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;">DelegateControl: Exception thrown while adding control 'ASP._controltemplates_publishingconsole_ascx': Object reference not set to an instance of an object.</span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><br /></span>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><b>PortalSiteMapProvider was unable to fetch current node, request URL</b>: /Pages/Forsiden.aspx, message: Object reference not set to an instance of an object., stack trace: </span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"> at Microsoft.SharePoint.SPField.GetTypeOrBaseTypeIfTypeIsInvalid(SPFieldCollection fields, String strType) </span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"> at Microsoft.SharePoint.SPFieldCollection.GetViewFieldsForContextualListItem() </span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"> at Microsoft.SharePoint.SPContext.get_Item() </span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"> at Microsoft.SharePoint.SPContext.get_ListItem() </span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"> at Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider.get_CurrentNode()</span><br />
<br />
Googling a "PortalSiteMapProvider was unable to fetch current node" exception is no fun. You will typically get it in relation to <a href="http://kjellsj.blogspot.no/2012/06/sp2010-navigation-sitemapprovider.html">top navigation and related site map providers</a>. I've chased the cause of that error before, and sometimes had to resort to iisreset each night due to the publishing cache going corrupt over time.<br />
<br />
This time, luckily, the exception details indicated a problem with the /pages/ list item definition, which led me to <a href="http://www.chaitumadala.com/2012/06/how-to-fix-systemnullreferenceexception.html">How to fix "System.NullReferenceException: Object reference not set to an instance of an object. at Microsoft.SharePoint.SPField.GetTypeOrBaseTypeIfTypeIsInvalid"</a> that helped me solve my problem. By looking closer at the list settings for the pages list, I could see that a list column was flagged as invalid (look for the text "Delete this invalid field").<br />
<br />
Trying to browse to Site Settings > Site Columns didn't work, but it gave me the enough information to find out what caused the issue and helped me solve it:<br />
<br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;">Field type AdvancedCalculated is not installed properly. Go to the list settings page to delete this field.</span><br />
<br />
Deploying the missing feature to the staging farm solved the problem. Now it only remains to fix all those absolute URLs entered by the content authors.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com0tag:blogger.com,1999:blog-11096258.post-73139143427093359452012-08-25T13:43:00.001+02:002013-02-27T09:00:41.043+01:00Use Azure VMs with On Premises Gateway as SP2010 Branch Office FarmHere are some notes from my ongoing experience of setting up a SharePoint 2010 "branch office" farm in Azure using the current preview of persistent <a href="http://michaelwasham.com/2012/06/08/understanding-windows-azure-virtual-machines/">Azure virtual machines</a> in a Azure virtual network connected to the on premises Active Directory using an Azure gateway to a Juniper VPN device.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-bPlCRzka9tg/UDi9aIxnRKI/AAAAAAAABKs/yXE4h--Dx9A/s1600/Azure_gateway.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="200" src="http://4.bp.blogspot.com/-bPlCRzka9tg/UDi9aIxnRKI/AAAAAAAABKs/yXE4h--Dx9A/s400/Azure_gateway.jpg" width="400" /></a></div>
Useful TechEd Europe 2012 web-casts that show how things work in general:<br />
VM+VNET in Azure: <a href="http://channel9.msdn.com/Events/TechEd/Europe/2012/AZR208">http://channel9.msdn.com/Events/TechEd/Europe/2012/AZR208</a><br />
AD in Azure: <a href="http://channel9.msdn.com/Events/TechEd/Europe/2012/SIA205">http://channel9.msdn.com/Events/TechEd/Europe/2012/SIA205</a><br />
SP in Azure: <a href="http://channel9.msdn.com/Events/TechEd/Europe/2012/OSP334">http://channel9.msdn.com/Events/TechEd/Europe/2012/OSP334</a><br />
<br />
Step by step instructions that I followed:<br />
How to <a href="https://www.windowsazure.com/en-us/manage/services/networking/cross-premises-connectivity/">Create a Virtual Network for Cross-Premises Connectivity</a> using the Azure gateway.<br />
How to <a href="https://www.windowsazure.com/en-us/manage/services/networking/replica-domain-controller/">Install a Replica Active Directory Domain Controller in Windows Azure Virtual Networks</a>.<br />
<br />
Creating a virtual network (vnet) is easy and simple using the Azure preview management portal. I recommed creating the local network first, as the vnet wizard otherwise might fail - without giving any useful exception message. We had an issue caused by naming the local network "6sixsix" which didn't work due to the name starting with a digit. Also note that the VPN gateway only supports one LAN subnet in the current preview.<br />
<br />
Plan your subnets upfront and make sure that they don't overlap with the on premises subnets. Register both the existing on premises DNS server and the planned vnet DNS server when configuring the vnet. A tip here is that the first VM created in a subnet will get .4 as the last part of the IP address, so if your ADDNSSubnet is 10.3.4.0/24, then the vnet DNS will get 10.3.4.4 as its IP address. Note that you can't change the DNS configuration after adding the first VM to the network, this includes creating the Azure gateway which adds devices to the gateway subnet.<br />
<br />
After creating the Azure virtual network, we created and started the Azure gateway for connecting to the on premises LAN using a Site-to-Site VPN tunnel using a secure IPSec connection. Creating the gateway takes some time as some devices or VMs are provisioned in the gateway subnet you specified. We then sent the public IP address of the gateway, plus the shared key and the configuration script for the Juniper VPN device to our network admin. The connection wouldn't come up, and to make a long story short, the VPN configuration needs the 'peerid' to be set to an IP address of a device in the gateway subnet. Our gateway subnet was 10.3.1.0/24 and after trying 10.3.1.4 first (see above tip), the network admin tried 10.3.1.5 and that worked. I'll come back to this below when telling you about our incident when our trial Azure account was deactivated by Microsoft.<br />
<br />
With the Azure virtual network up and running and connected to the on premises LAN, I created the AD DNS virtual machine using the preview portal "create from gallery" option. As SP2010 is not supported on WS2012 yet, I decided to use the WS2008R2 server image in this Azure server farm. Note that you should use size "large" for hosting AD as you need to attach two data disks for storing the AD database, log files and system state backup.<br />
<br />
I did not use powershell for creating this first VM, instead I manually changed the DNS setting on the network adapter (both IPv4 and IPv6) and then manually joined the to-be AD DNS VM to the existing domain. <b>Note</b> that while you're at it, also set the advanced DNS option "Use this connection's DNS suffix in DNS registration" for both network adapters. Otherwise you will get the "<a href="http://social.technet.microsoft.com/wiki/contents/articles/1935.troubleshooting-domain-join-error-messages-en-us.aspx">Changing the Primary Domain DNS name of this computer to "" failed</a>" error when trying to join the domain.<br />
<br />
Following the <a href="https://www.windowsazure.com/en-us/manage/services/networking/replica-domain-controller/">how-to for setting up a replica AD in Azure</a> work fine, we only had some minor issues due to the existing AD being on WS2003. For instance, we found no DEFAULTIPSITELINK when creating a new site in AD, so we had to create a new site link first, then create the site and finally modify the site link so that it linked the Azure "CloudSite" and the LAN site. Then the dcpromo wizard step for AD site detection didn't manage to resolve against our WS2003 domain controller, just click "ok" on the error message and manually select the "CloudSite" in the "Select a site" page.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-blBGjI_qRt4/UDjTQ1QsNUI/AAAAAAAABK8/hjS8l7xvUVI/s1600/AD_DC_options_WS2008R2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="302" src="http://3.bp.blogspot.com/-blBGjI_qRt4/UDjTQ1QsNUI/AAAAAAAABK8/hjS8l7xvUVI/s320/AD_DC_options_WS2008R2.png" width="320" /></a></div>
<br />
I really wanted to set up a read-only domain controller (RODC) to save some outgoing (egress) traffic and thus save some money, as this branch farm don't need a full fidelity domain controller. However, it is not possible to create a RODC when the existing DC is on WS2003, because RODC is a WS2008 feature. So for "Additional Domain Controller Options" we went with DNS and "Global Catalog" (GC). GC isn't required, but if not installed then all authentication traffic on login need to go all the way to the on premises DC. So to save some traffic (and money), and get faster authN in the branch farm, we added the GC - even if the extra data will drive up Azure storage cost.<br />
<br />
The next servers in the farm were added using powershell to ensure that 1) the VM is domain joined on boot, and 2) that the DNS settings for the VM is automatically configured.<br />
<br />
Here are some tips for using New-AzureDNS, New-AzureVMConfig and New-AzureVM:<br />
<ul>
<li>You can use the Azure vnet DNS server or the on premises DNS server with New-AzureDNS. I used the former.</li>
<li>The New-AzureVMConfig Name parameter is used when naming and registering the server in AD and DNS. Make sure that the full computer name is unique across the domain.</li>
<li>The New-AzureVM ServiceName parameter is used for the cloud DNS name prefix in the <b>.cloudapp.net domain</b>. It is also used to provision a "Cloud Service" in the Azure preview management portal. Even if multiple VMs can be added to the same service name (<a href="https://www.windowsazure.com/en-us/manage/windows/how-to-guides/connect-to-a-cloud-service/">shared workload</a>), I used uniqe names for the farm VMs (standalone virtual machine), <a href="http://michaelwasham.com/2012/07/13/connecting-windows-azure-virtual-machines-with-powershell/">connected using the vnet</a> for <a href="http://www.windowsazure.com/en-us/manage/windows/common-tasks/how-to-load-balance-virtual-machines/">load balancing</a>.</li>
<li>To get the built-in Azure image names, use this powershell to go through the images in the gallery until you find the one you're looking for:<br /> <span style="font-family: Courier New, Courier, monospace;">(Get-AzureVMImage)[1].ImageName</span></li>
</ul>
After adding the SQL Server 2012, web server and application server VMs using powershell, I logged in using RDP and verified that each server was up and running, domain joined and registered in the DNS. Note that the SQL Server image is not by default configured with separate data disks for data and log files. This means that the SQL Server 2012 master database etc is stored on the OS disk in this preview. You need to add data disks and then change the SQL Server file location configuration your self. Adding two data disks will require that the SQL Server VM is of size "large".<br />
<br />
The next step was to intall SharePoint 2010 on the farm the next day. Thats when the trial account was deactivated because all the computing hours was spent. Even if you then reactivate the account, all your VM instances are deleted, keeping only the VHD disks. As Microsoft support says, it is easy to recreate the VMs, but they also tell you that the AD virtual machine needs a static IP which you can only get in Azure if you never delete the VM. Remember to recreate the VMs in the correct order so that they get the same IP addresses as before.<br />
<br />
What is worse is that they also delete the virtual network and the gateway. Even if it is also easy to recreate these, your gateway will get a new public IP address and a new shared key, so you need to call your network provider again to make them reconfigure the VPN device.<br />
<br />
I strongly recommend not using a spending capped trial account for hosting your Azure branch office farm. Microsoft deleted the VMs and the network to stop incurring costs, which was fine with non-persistent Azure VM Roles (PaaS) anyway, but not as nice for a IaaS service with a persistent server farm.<br />
<br />
I recommend exporting your VMs using Export-AzureVM so that you don't have to recreate the VMs manually if something should happen. The exported XML will contain all the VM settings, including the attached data disks.<br />
<br />
How to deatch Azure VMs to move or save cost: <a href="http://michaelwasham.com/2012/06/18/importing-and-exporting-virtual-machine-settings/">http://michaelwasham.com/2012/06/18/importing-and-exporting-virtual-machine-settings/</a><br />
<br />
When we recreated the Azure virtual network and the gateway, the VPN connection would not come back up again. The issue was that this time the gateway devices had got different IP addresses, so now the "peerid" had to be configured as 10.3.4.4 to make things work.<br />
<br />
Now the gateway is back up again, and next week I'll restore the VMs and continue with installing SP2010 on the Azure "branch office" farm. More notes to come if I run into other issues.<br />
<br />
- - - continued - - -<br />
<br />
Installing the SharePoint 2010 bits went smooth, but running the config wizard did not. First you need to allow incoming TCP traffic on port 1433 on the Azure SQL Server. Then the creation of the SharePoint_Config database failed with:<br />
<br />
<strong> Could not find stored procedure 'sp_dboption'.</strong><br />
<strong><br /></strong>...even if I had downloaded and installed SP2010 with SP1 bits. So I downloaded and installed SharePoint Server 2010 SP1 and June CU from 2011 due to the <a href="http://blogs.msdn.com/b/manisblog/archive/2012/03/06/quick-tip-installing-sharepoint-farm-with-sql-server-2012.aspx">issue caused by using SQL Server 2012</a> and that fixed the problem. Got "Configuration Successfull" without any further issues.<br />
<br />
Finally, I tested and verified it all by creating a SP2010 web-application with a team site, creating a self-signed certificate with IIS7 and adding an Azure port mapping for SSL (virtual machine endpoint, TCP 443 to 443), allowing me to login to the team site using my domain account over HTTPS from anywhere.<br />
<br />
A note on the VM firewall config is that ping is by default blocked, thus you can't ping other machines in the vnet unless you configure the firewall to allow it. Also note that you can't ping addresses outside of the virtual network and the connected LAN anyway; even if you can browse to www.puzzlepart.com, you can't ping us.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com0tag:blogger.com,1999:blog-11096258.post-84077672666249773432012-06-23T18:22:00.002+02:002012-10-13T20:42:25.921+02:00SharePoint Publishing Site Map Providers and NavigationConfiguring the navigation of SharePoint 2010 publishing sites and subsites can be a bit confusing, also when configuring the navigation from code or from your web templates (or even old school site definitions). Add to that the UI that changes based on which settings you chose combined with what site or subsite context you're currently in. Plus the quite large number of site map providers defined in the web.config when using the <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.publishing.navigation.portalsitemapprovider.aspx">PortalSiteMapProvider</a> from code.<br />
<br />
This post is about how the UI settings can be repeated in your site provisioning code, that is: first configure the navigation settings in your prototype to make it work according to your <a href="http://kjellsj.blogspot.no/2010/08/classification-and-structuring-of.html">navigation concept</a>, then package the settings into feature code.<br />
<br />
The PortalSiteMapProvider works in combination with the <a href="http://msdn.microsoft.com/en-us/library/ms562424">PublishingWeb</a> navigation settings, and of course with the top and current navigation controls used to render the navigation as HTML. The latter needs to look at the publishing web's <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.publishing.navigation.portalnavigation">PortalNavigation</a> settings when querying the portal site map provider for the CurrentNode or when getting a set of navigation nodes to render. The navigation controls' code use the PortalSiteMapProvider properties IncludeSubSites, IncludePages, IncludeAuthoredLinks and IncludeHeadings to set the filtering applied to GetChildNodes when rendering nodes. These filter properties are typically set to IncludeOption.PerWeb to reflect the navigation settings of the current site or subsite.<br />
<br />
The navigation settings UI tries to show the effects of your navigation settings (upper half) by <a href="http://blogs.msdn.com/b/ecm/archive/2007/02/16/moss-navigation-deep-dive-part-2.aspx">rendering a preview of what nodes GetChildNodes would return</a> for the *current* site (lower half) from the applicable site map provider. The PortalSiteMapProvider exposes several of the providers defined in web.config as static properties, but only two of them are typically used: <a href="http://blogs.msdn.com/b/ecm/archive/2007/02/10/moss-navigation-deep-dive-part-1.aspx">CombinedNavSiteMapProvider and CurrentNavSiteMapProvider</a>. The former is what feeds the top navigation, the latter feeds the current (left, local) navigation.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-8ybOK04l3W4/T-XolsEC6iI/AAAAAAAAAaM/p_lRFapgPdc/s1600/SP2010-navigation-preview-edit-configuration.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="137" src="http://1.bp.blogspot.com/-8ybOK04l3W4/T-XolsEC6iI/AAAAAAAAAaM/p_lRFapgPdc/s400/SP2010-navigation-preview-edit-configuration.png" width="400" /></a></div>
<br />
Note that when inheriting global navigation, the UI won't show the global navigation container as it only supports configuring the navigation of the current site. The term "parent site" in the UI refers to the top-level site of the site-collection, which is not the direct parent of a subsite beyond level 1 children.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-WsY__LseIbI/T-XpAjqHYaI/AAAAAAAAAaU/zgUTepPYtj4/s1600/SP2010-navigation-settings-configuration.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="191" src="http://3.bp.blogspot.com/-WsY__LseIbI/T-XpAjqHYaI/AAAAAAAAAaU/zgUTepPYtj4/s400/SP2010-navigation-settings-configuration.png" width="400" /></a></div>
<br />
Configuring the navigation settings from your site provisioning feature is quite simple once you've got a working prototype of your navigation concept. Use the mapping shown in the above figure to program the configuration settings for both global navigation (InheritGlobal, GlobalIncludeSubSites, GlobalIncludePages) and current navigation (InheritCurrent, CurrentIncludeSubSites, CurrentIncludePages, ShowSiblings).<br />
<br />
The only little pitfall is for "Display the current site, the navigation items below the current site, and the current site's siblings" which requires a combination of InheritCurrent = false and ShowSiblings = true. Use this setting to show the same local navigation for a section of your web-site and all its child sites. A typical example is for the Quality Management section (level 1 subsite) and its QMS areas (level 2 subsites) to have a shared navigation experience. The QMS section would not use ShowSiblings while all the child areas would have ShowSiblings turned on.<br />
<br />
Implementing a custom navigation concept is as simple as writing your own navigation rendering controls, and inheriting the PortalSiteMapProvider to override the logic for CurrentNode and GetChildNodes to suit your needs by applying the applicable node filtering properties to control which nodes are returned and rendered in which context. I've also used this approach for reading the navigation items from a central SharePoint list to get common cross site-collection top-navigation.<br />
<br />
I hope this helped you understand how to realize your navigation concept from code, and that you're not totally confused by all the available site map providers and how they are used anymore.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com0tag:blogger.com,1999:blog-11096258.post-38653202307489881532012-05-07T09:45:00.000+02:002012-05-16T16:17:38.882+02:00Exploring Search Results Step-by-Step in SharePoint 2010Using search to provide a news archive in SharePoint 2010 is a wellknown solution. Just add the core results web-part to a page and configure it to query for your news article content type and sort it in descending order. Then customize the result XSLT to tune the content and layout of the news excerpts to look like a new archive. Add also the search box, the search refiners and the results paging web-parts and you have a functional news archive in no time.<br />
<br />
This post is about providing contextual navigation by adding "<< previous", "next >>" and "result" links to the article pages, to allow users to explore the result set in a step-by-step manner. Norwegians will reckognize this way of exploring results from <a href="http://finn.no/">finn.no</a>.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-3pxVmthgekc/T6aBUhk8jII/AAAAAAAAAZs/uGGwAZflcKc/s1600/result_set_navigator.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="96" src="http://2.bp.blogspot.com/-3pxVmthgekc/T6aBUhk8jII/AAAAAAAAAZs/uGGwAZflcKc/s320/result_set_navigator.png" width="320" /></a></div>
<br />
For a user or visitor to be able to navigate the results, the result set must be cached per user. The search results are in XML format, and it contains a sequential id and the URL for each hit. This allows the navigation control to use XPath to locate the current result by id, and get the URLs for the previous and next results. The user query must also be cached so that clicking the "result" link will show the expected search results.<br />
<br />
Override the CoreResultsWebPart as shown in my <a href="http://kjellsj.blogspot.com/2012/04/elevated-search-results-sharepoint-2010.html">Getting Elevated Search Results in SharePoint 2010</a> post to add per-user caching of the search results. If your site allows for anonymous visitors, you need to decide on how to keep tab on them. In the code I've used the requestor IP address, which is not 100% foolproof, but this allows me to avoid using cookies for now.<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt"><span class="kwrd">namespace</span> Puzzlepart.SharePoint.Presentation</pre>
<pre>{</pre>
<pre class="alt"> [ToolboxItemAttribute(<span class="kwrd">false</span>)]</pre>
<pre> <span class="kwrd">public</span> <span class="kwrd">class</span> NewsArchiveCoreResultsWebPart : CoreResultsWebPart</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">readonly</span> <span class="kwrd">string</span> ScopeNewsArticles </pre>
<pre> = <span class="str">"Scope=\"News Archive\""</span>;</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">private</span> <span class="kwrd">static</span> <span class="kwrd">readonly</span> <span class="kwrd">string</span> CacheKeyResultsXmlDocument </pre>
<pre><span style="background-color: transparent;"> </span>= <span class="str">"Puzzlepart_CoreResults_XmlDocument_User:"</span>;</pre>
<pre class="alt"> <span class="kwrd">private</span> <span class="kwrd">static</span> <span class="kwrd">readonly</span> <span class="kwrd">string</span> CacheKeyUserQueryString </pre>
<pre class="alt"><span style="background-color: transparent;"> </span>= <span class="str">"Puzzlepart_CoreResults_UserQuery_User:"</span>;</pre>
<pre> <span class="kwrd">private</span> <span class="kwrd">int</span> _cacheUserQueryTimeMinutes = 720;</pre>
<pre class="alt"> <span class="kwrd">private</span> <span class="kwrd">int</span> _cacheUserResultsTimeMinutes = 30;</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">protected</span> <span class="kwrd">override</span> <span class="kwrd">void</span> CreateChildControls()</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">try</span></pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">base</span>.CreateChildControls();</pre>
<pre> }</pre>
<pre class="alt"> <span class="kwrd">catch</span> (Exception ex)</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">var</span> error = SharePointUtilities.CreateErrorLabel(ex);</pre>
<pre> Controls.Add(error);</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">protected</span> <span class="kwrd">override</span> XPathNavigator GetXPathNavigator(<span class="kwrd">string</span> viewPath)</pre>
<pre class="alt"> {</pre>
<pre> <span class="rem">//return base.GetXPathNavigator(viewPath);</span></pre>
<pre class="alt"> </pre>
<pre> SetCachedUserQuery();</pre>
<pre class="alt"> XmlDocument xmlDocument = GetXmlDocumentResults();</pre>
<pre> SetCachedResults(xmlDocument);</pre>
<pre class="alt"> </pre>
<pre> XPathNavigator xPathNavigator = xmlDocument.CreateNavigator();</pre>
<pre class="alt"> <span class="kwrd">return</span> xPathNavigator;</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">private</span> XmlDocument GetXmlDocumentResults()</pre>
<pre> {</pre>
<pre class="alt"> XmlDocument xmlDocument = <span class="kwrd">null</span>;</pre>
<pre> </pre>
<pre class="alt"> QueryManager queryManager = </pre>
<pre class="alt"><span style="background-color: transparent;"> </span>SharedQueryManager.GetInstance(Page, QueryNumber).QueryManager;</pre>
<pre> </pre>
<pre class="alt"> Location location = queryManager[0][0];</pre>
<pre> <span class="kwrd">string</span> query = location.SupplementaryQueries;</pre>
<pre class="alt"> <span class="kwrd">if</span> (query.IndexOf(ScopeNewsArticles, </pre>
<pre class="alt"><span style="background-color: transparent;"> </span>StringComparison.CurrentCultureIgnoreCase) < 0)</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">string</span> userQuery = </pre>
<pre class="alt"><span style="background-color: transparent;"> </span>queryManager.UserQuery + <span class="str">" "</span> + ScopeNewsArticles;</pre>
<pre> queryManager.UserQuery = userQuery.Trim();</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> xmlDocument = queryManager.GetResults(queryManager[0]);</pre>
<pre> <span class="kwrd">return</span> xmlDocument;</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">private</span> <span class="kwrd">void</span> SetCachedUserQuery()</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">var</span> qs = HttpUtility.ParseQueryString</pre>
<pre class="alt"><span style="background-color: transparent;"> </span>(Page.Request.QueryString.ToString());</pre>
<pre> <span class="kwrd">if</span> (qs[<span class="str">"resultid"</span>] != <span class="kwrd">null</span>)</pre>
<pre class="alt"> {</pre>
<pre> qs.Remove(<span class="str">"resultid"</span>);</pre>
<pre class="alt"> }</pre>
<pre> HttpRuntime.Cache.Insert(UserQueryCacheKey(<span class="kwrd">this</span>.Page), </pre>
<pre><span style="background-color: transparent;"> </span>qs.ToString(), <span class="kwrd">null</span>, </pre>
<pre><span style="background-color: transparent;"> </span>Cache.NoAbsoluteExpiration, </pre>
<pre><span class="kwrd"><span style="background-color: transparent;"> </span>new</span> TimeSpan(0, 0, _cacheUserQueryTimeMinutes, 0));</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">private</span> <span class="kwrd">void</span> SetCachedResults(XmlDocument xmlDocument)</pre>
<pre> {</pre>
<pre class="alt"> HttpRuntime.Cache.Insert(ResultsCacheKey(<span class="kwrd">this</span>.Page), </pre>
<pre class="alt"><span style="background-color: transparent;"> </span>xmlDocument, <span class="kwrd">null</span>, </pre>
<pre class="alt"><span style="background-color: transparent;"> </span>Cache.NoAbsoluteExpiration, </pre>
<pre class="alt"><span class="kwrd"><span style="background-color: transparent;"> </span>new</span> TimeSpan(0, 0, _cacheUserResultsTimeMinutes, 0));</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">private</span> <span class="kwrd">static</span> <span class="kwrd">string</span> UserQueryCacheKey(Page page)</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">string</span> visitorId = GetVisitorId(page);</pre>
<pre class="alt"> <span class="kwrd">string</span> queryCacheKey = String.Format(<span class="str">"{0}{1}"</span>, </pre>
<pre class="alt"><span style="background-color: transparent;"> </span>CacheKeyUserQueryString, visitorId);</pre>
<pre> <span class="kwrd">return</span> queryCacheKey;</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">private</span> <span class="kwrd">static</span> <span class="kwrd">string</span> ResultsCacheKey(Page page)</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">string</span> visitorId = GetVisitorId(page);</pre>
<pre> <span class="kwrd">string</span> resultsCacheKey = String.Format(<span class="str">"{0}{1}"</span>, </pre>
<pre><span style="background-color: transparent;"> </span>CacheKeyResultsXmlDocument, visitorId);</pre>
<pre class="alt"> <span class="kwrd">return</span> resultsCacheKey;</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">string</span> GetCachedUserQuery(Page page)</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">string</span> userQuery = </pre>
<pre><span style="background-color: transparent;"> </span>(<span class="kwrd">string</span>)HttpRuntime.Cache[UserQueryCacheKey(page)];</pre>
<pre class="alt"> <span class="kwrd">return</span> userQuery;</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">public</span> <span class="kwrd">static</span> XmlDocument GetCachedResults(Page page)</pre>
<pre class="alt"> {</pre>
<pre> XmlDocument results = </pre>
<pre><span style="background-color: transparent;"> </span>(XmlDocument)HttpRuntime.Cache[ResultsCacheKey(page)];</pre>
<pre class="alt"> <span class="kwrd">return</span> results;</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">private</span> <span class="kwrd">static</span> <span class="kwrd">string</span> GetVisitorId(Page page)</pre>
<pre class="alt"> {</pre>
<pre> <span class="rem">//TODO: use cookie for anonymous visitors</span></pre>
<pre class="alt"> <span class="kwrd">string</span> id = page.Request.ServerVariables[<span class="str">"HTTP_X_FORWARDED_FOR"</span>] </pre>
<pre class="alt"><span style="background-color: transparent;"> </span>?? page.Request.ServerVariables[<span class="str">"REMOTE_ADDR"</span>];</pre>
<pre> <span class="kwrd">if</span>(SPContext.Current.Web.CurrentUser != <span class="kwrd">null</span>)</pre>
<pre class="alt"> {</pre>
<pre> id = SPContext.Current.Web.CurrentUser.LoginName;</pre>
<pre class="alt"> }</pre>
<pre> <span class="kwrd">return</span> id;</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt">}</pre>
</div>
<br />
I've used sliding expiration on the cache to allow for the user to spend some time exploring the results. The result set is cached for a short time by default, as this can be quite large. The user query text is, however, small and cached for a long time, allowing the users to at least get their results back after a period of inactivity.<br />
<br />
As suggested by <a href="http://techmikael.blogspot.com/">Mikael Svenson</a>, an alternative to caching would be running the query again using the static QueryManager page object to get the result set. This would require using another result key element than the dynamic <id> number to ensure that the current result lookup is not scewed by new results being returned by the search. An example would be using a content type field such as "NewsArticlePermaId" if it exists.<br />
<br />
Overriding the <span style="background-color: #f4f4f4; font-family: Consolas, 'Courier New', Courier, monospace; font-size: x-small; line-height: 18px;">GetXPathNavigator</span> method gets you the cached results that the navigation control needs. In addition, the navigator code needs to know which is the result set id of the current page. This is done by customizing the result XSLT and adding a "resultid" parameter to the $siteUrl variable for each hit.<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt">. . . </pre>
<pre> <span class="kwrd"><</span><span class="html">xsl:template</span> <span class="attr">match</span><span class="kwrd">="Result"</span><span class="kwrd">></span></pre>
<pre class="alt"> <span class="kwrd"><</span><span class="html">xsl:variable</span> <span class="attr">name</span><span class="kwrd">="id"</span> <span class="attr">select</span><span class="kwrd">="id"</span><span class="kwrd">/></span></pre>
<pre> <span class="kwrd"><</span><span class="html">xsl:variable</span> <span class="attr">name</span><span class="kwrd">="currentId"</span> <span class="attr">select</span><span class="kwrd">="concat($IdPrefix,$id)"</span><span class="kwrd">/></span></pre>
<pre class="alt"> <span class="kwrd"><</span><span class="html">xsl:variable</span> <span class="attr">name</span><span class="kwrd">="url"</span> <span class="attr">select</span><span class="kwrd">="url"</span><span class="kwrd">/></span></pre>
<pre> <span class="kwrd"><</span><span class="html">xsl:variable</span> <span class="attr">name</span><span class="kwrd">="resultid"</span> <span class="attr">select</span><span class="kwrd">="concat('?resultid=', $id)"</span> <span class="kwrd">/></span></pre>
<pre class="alt"> <span class="kwrd"><</span><span class="html">xsl:variable</span> <span class="attr">name</span><span class="kwrd">="siteUrl"</span> <span class="attr">select</span><span class="kwrd">="concat($url, $resultid)"</span> <span class="kwrd">/></span></pre>
<pre>. . . </pre>
</div>
<br />
The result set navigation control is quite simple, looking up the current result by id and getting the URLs for the previous and next results (if any) and adding the "resultid" to keep the navigation logic going forever.<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt"><span class="kwrd">namespace</span> Puzzlepart.SharePoint.Presentation</pre>
<pre>{</pre>
<pre class="alt"> <span class="kwrd">public</span> <span class="kwrd">class</span> NewsArchiveResultsNavigator : Control</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">public</span> <span class="kwrd">string</span> NewsArchivePageUrl { <span class="kwrd">get</span>; <span class="kwrd">set</span>; }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">private</span> <span class="kwrd">string</span> _resultId = <span class="kwrd">null</span>;</pre>
<pre> <span class="kwrd">private</span> XmlDocument _results = <span class="kwrd">null</span>;</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">protected</span> <span class="kwrd">override</span> <span class="kwrd">void</span> CreateChildControls()</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">base</span>.CreateChildControls();</pre>
<pre class="alt"> </pre>
<pre> _resultId = Page.Request.QueryString[<span class="str">"resultid"</span>];</pre>
<pre class="alt"> _results = NewsArchiveCoreResultsWebPart.GetCachedResults(<span class="kwrd">this</span>.Page);</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">if</span>(_results == <span class="kwrd">null</span> || _resultId == <span class="kwrd">null</span>)</pre>
<pre> {</pre>
<pre class="alt"> <span class="rem">//render nothing</span></pre>
<pre> <span class="kwrd">return</span>;</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> AddResultsNavigationLinks();</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">private</span> <span class="kwrd">void</span> AddResultsNavigationLinks()</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">string</span> prevUrl = GetPreviousResultPageUrl();</pre>
<pre class="alt"> <span class="kwrd">var</span> linkPrev = <span class="kwrd">new</span> HyperLink()</pre>
<pre> {</pre>
<pre class="alt"> Text = <span class="str">"<< Previous"</span>,</pre>
<pre> NavigateUrl = prevUrl</pre>
<pre class="alt"> };</pre>
<pre> linkPrev.Enabled = (prevUrl.Length > 0);</pre>
<pre class="alt"> Controls.Add(linkPrev);</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">string</span> resultsUrl = GetSearchResultsPageUrl();</pre>
<pre> <span class="kwrd">var</span> linkResults = <span class="kwrd">new</span> HyperLink()</pre>
<pre class="alt"> {</pre>
<pre> Text = <span class="str">"Result"</span>,</pre>
<pre class="alt"> NavigateUrl = resultsUrl</pre>
<pre> };</pre>
<pre class="alt"> Controls.Add(linkResults);</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">string</span> nextUrl = GetNextResultPageUrl();</pre>
<pre> <span class="kwrd">var</span> linkNext = <span class="kwrd">new</span> HyperLink()</pre>
<pre class="alt"> {</pre>
<pre> Text = <span class="str">"Next >>"</span>,</pre>
<pre class="alt"> NavigateUrl = nextUrl</pre>
<pre> };</pre>
<pre class="alt"> linkNext.Enabled = (nextUrl.Length > 0);</pre>
<pre> Controls.Add(linkNext);</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">private</span> <span class="kwrd">string</span> GetPreviousResultPageUrl()</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">return</span> GetSpecificResultUrl(<span class="kwrd">false</span>);</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">private</span> <span class="kwrd">string</span> GetNextResultPageUrl()</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">return</span> GetSpecificResultUrl(<span class="kwrd">true</span>);</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">private</span> <span class="kwrd">string</span> GetSpecificResultUrl(<span class="kwrd">bool</span> useNextResult)</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">string</span> url = <span class="str">""</span>;</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">if</span> (_results != <span class="kwrd">null</span>)</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">string</span> xpath = </pre>
<pre class="alt"><span style="background-color: transparent;"> </span>String.Format(<span class="str">"/All_Results/Result[id='{0}']"</span>, _resultId);</pre>
<pre> XPathNavigator xNavigator = _results.CreateNavigator();</pre>
<pre class="alt"> XPathNavigator xCurrentNode = xNavigator.SelectSingleNode(xpath);</pre>
<pre> <span class="kwrd">if</span> (xCurrentNode != <span class="kwrd">null</span>)</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">bool</span> hasNode = <span class="kwrd">false</span>;</pre>
<pre class="alt"> <span class="kwrd">if</span> (useNextResult)</pre>
<pre> hasNode = xCurrentNode.MoveToNext();</pre>
<pre class="alt"> <span class="kwrd">else</span></pre>
<pre> hasNode = xCurrentNode.MoveToPrevious();</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">if</span> (hasNode && </pre>
<pre><span style="background-color: transparent;"> </span>xCurrentNode.LocalName.Equals(<span class="str">"Result"</span>))</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">string</span> resultId = </pre>
<pre><span style="background-color: transparent;"> </span>xCurrentNode.SelectSingleNode(<span class="str">"id"</span>).Value;</pre>
<pre class="alt"> <span class="kwrd">string</span> fileUrl = </pre>
<pre class="alt"><span style="background-color: transparent;"> </span>xCurrentNode.SelectSingleNode(<span class="str">"url"</span>).Value;</pre>
<pre> url = String.Format(<span class="str">"{0}?resultid={1}"</span>, </pre>
<pre><span style="background-color: transparent;"> </span>fileUrl, resultId);</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">return</span> url;</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">private</span> <span class="kwrd">string</span> GetSearchResultsPageUrl()</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">string</span> url = NewsArchivePageUrl;</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">string</span> userQuery = </pre>
<pre><span style="background-color: transparent;"> </span>NewsArchiveCoreResultsWebPart.GetCachedUserQuery(<span class="kwrd">this</span>.Page);</pre>
<pre class="alt"> <span class="kwrd">if</span> (String.IsNullOrEmpty(userQuery))</pre>
<pre> {</pre>
<pre class="alt"> url = String.Format(<span class="str">"{0}?resultid={1}"</span>, url, _resultId);</pre>
<pre> }</pre>
<pre class="alt"> <span class="kwrd">else</span></pre>
<pre> {</pre>
<pre class="alt"> url = String.Format(<span class="str">"{0}?{1}&resultid={2}"</span>, </pre>
<pre class="alt"><span style="background-color: transparent;"> </span>url, userQuery, _resultId);</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">return</span> url;</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> }</pre>
<pre>}</pre>
</div>
<br />
Note how I use the "resultid" URL parameter to discern between normal navigation to a page and result set navigation between pages. If the resultid parameter is not there, then the navigation controls are hidden. The same goes for when there are no cached results. The "result" link could always be visible for as long as the user's query text is cached.<br />
<br />
You can also provide this result set exploration capability for all kinds of pages, not just for a specific page layout, by adding the result set navigation control to your master page(s). The result set <id> and <url> elements are there for all kind of pages stored in your SharePoint solution.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com1tag:blogger.com,1999:blog-11096258.post-39665337223711284172012-04-30T10:11:00.003+02:002012-04-30T16:42:02.497+02:00Almost Excluding Specific Search Results in SharePoint 2010Sometimes you want to hide certain content from being exposed through search in certain SharePoint web-applications, even if the user really has access to the information in the actual content source. A scenario is intranet search that is openly used, but in which you want to prevent accidental information exposure. Think of a group working together on reqruiting, where the HR manager use the search center looking for information - you wouldn't want even excerpts of confidential information to be exposed in the search results.<br />
<br />
So you carefully plan your content sources and crawl rules to only index the least possible amount of information. Still, even with crawl rules you will often need to tweak the query scope rules to exclude content at a more fine-grained level, or even add new scopes for providing search-driven content to users. Such configuration typically involves using exclude rules on content types or content sources. This is a story of how SharePoint can throw you a search results curveball, leading to accidental information disclosure.<br />
<br />
In this scenario, I had created a new content source <b>JobVault</b> for crawling the HR site-collection in another SharePoint web-application, to be exposed only through a custom shared scope. So I tweaked the rules of the existing scopes such as "All Sites" to exclude the Puzzlepart JobVault content source, and added a new <b>JobReqruiting</b> scope that required the JobVault content source and included the content type <b>JobHired</b> and excluded the content type <b>JobFired</b>.<br />
<br />
So no shared scopes defined in the Search Service Application (SSA) included JobFired information, as all scopes either excluded the HR content source or excluded the confidential content type. To my surprise our SharePoint search center would find and expose such pages and documents when searching for "you're fired!!!".<br />
<br />
Knowing that the search center by default uses the "All Sites" scope when no specific scope is configured or defined in the keyword query, it was back to the SSA to verify the scope. It was all in order, and doing a property search on <b>Scope:"All Sites"</b> got me the expected results with no confidential data in it. The same result for <b>Scope:"JobReqruiting"</b>, no information exposure there either. It looked very much like a best bet, but there where no best bet keywords defined for the site-collection.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-FvGlHAiNBWY/T55JKcLVKjI/AAAAAAAAAZU/opl1L_tJ1OM/s1600/SP2010_top_federated_results_web_part.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="182" src="http://3.bp.blogspot.com/-FvGlHAiNBWY/T55JKcLVKjI/AAAAAAAAAZU/opl1L_tJ1OM/s400/SP2010_top_federated_results_web_part.png" width="400" /></a></div>
<br />
The search center culprit was the Top Federated Results web-part in our basic search site, by default showing results from the local search index very much like best bets. That was the same location as defined in the core results web-part, so why this difference?<br />
<br />
Looking into the details of the "Local Search Results" federated location, the reason became clear: "This location provides unscoped results from the Local Search index". The keyword here is "unscoped".<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-X9_b1FyV7Bg/T55CJ35kWsI/AAAAAAAAAY8/hUL5hF1sqxE/s1600/SP2010_LocalSearchIndex_FederatedLocation.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="213" src="http://1.bp.blogspot.com/-X9_b1FyV7Bg/T55CJ35kWsI/AAAAAAAAAY8/hUL5hF1sqxE/s400/SP2010_LocalSearchIndex_FederatedLocation.png" width="400" /></a></div>
<br />
The solution is to add the "All Sites" scope to the federated location to ensure that results that you want to hide are also excluded from the federated results web-part. Add it to the "Query Template" and optionally also to the "More Results Link Template" under the "Location Information" section in "Edit Federated Location".<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-jWtWTmeJScM/T55E2ElVx6I/AAAAAAAAAZI/1fEZieBUp9I/s1600/SP2010_LocalSearchIndex_FederatedLocation_AllSites_Scope.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="213" src="http://2.bp.blogspot.com/-jWtWTmeJScM/T55E2ElVx6I/AAAAAAAAAZI/1fEZieBUp9I/s400/SP2010_LocalSearchIndex_FederatedLocation_AllSites_Scope.png" width="400" /></a></div>
<br />
Now the content is hidden when searching. Not through query security trimming, but through query filtering. Forgetting to add the filter somewhere can expose the information, but then only to users that have permission to see the content anyway. The results are still security trimmed, so this no actual information disclosure risk.<br />
<br />
Note that this approach is no replacement for real information security; if that is what you need, don't crawl confidential information from an SSA that is exposed through openly available SharePoint search, even on your intranet.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com0tag:blogger.com,1999:blog-11096258.post-79662269233777249002012-04-21T12:36:00.001+02:002012-10-03T10:22:24.728+02:00Migrate SharePoint 2010 Term Sets between MMS Term StoresWhen using the SharePoint 2010 <a href="http://www.sharepointconfig.com/2011/09/adding-managed-metadata-fields-to-sharepoint-publishing-pages/">managed metadata fields connected to termsets</a> stored in the Managed Metadata Service (MMS) term store in your solutions, you should have a designated master MMS that is reused across all your SharePoint environment such as the <a href="http://www.slideshare.net/kjellsj/sharepoint-2010-farm-architecture-design-infrastructure">development, test, staging and production farms</a>. Having a single master termstore across all farms gives you the same termsets and terms with the same identifiers all over, allowing you to move content and content types from staging to production <a href="http://www.cleverworkarounds.com/2011/01/09/sp2010-migrating-managed-metadata-term-sets-to-another-farm-on-another-domain/">without invalidating all the fields and data connected to the MMS term store</a>.<br />
<br />
You'll find a lot of termset tools on CodePlex, some that use the standard SharePoint 2010 CSV import file format (which is without identifiers), and some that on paper does what you need, but don't fully work. Some of the better tools are <a href="http://metadataexportsps.codeplex.com/">SolidQ Managed Metadata Exporter</a> for export and import of termset (CSV-style), <a href="http://sptermstoreutilities.codeplex.com/">SharePoint Term Store Powershell Utilities</a> for fixing orphaned terms, and finally <a href="http://taxonomyandtermstore.codeplex.com/">SharePoint Taxonomy and TermStore Utilities</a> for real migration.<br />
<br />
There are, however, standard SP2010 PowerShell cmdlets that allow you to migrate the complete termstore with full fidelity between Managed Metadata Service applications across farms. The drawback is that you can't do selective migration of specific termsets, the whole term store will be overwritten by the migration.<br />
<br />
This script exports the term store to a backup file:<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt"># MMS Application Proxy ID has to be passed <span class="kwrd">for</span> -Identity parameter</pre>
<pre class="alt"></pre>
<pre>Export-SPMetadataWebServicePartitionData -Identity <span class="str">"12810c05-1f06-4e35-a6c3-01fc485956a3"</span> -ServiceProxy <span class="str">"Managed Metadata Service"</span> -Path <span class="str">"\\Puzzlepart\termstore\pzl-staging.bak"</span></pre>
</div>
<br />
This script imports the backup by overwriting the term store:<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt"># MMS Application Proxy ID has to be passed <span class="kwrd">for</span> -Identity parameter</pre>
<pre># NOTE: overwrites all existing termsets <span class="kwrd">from</span> MMS</pre>
<pre class="alt"># NOTE: overwrites the MMS content type HUB URL - must be reconfigured on target MMS proxy after restoring</pre>
<pre class="alt"></pre>
<pre>Import-SPMetadataWebServicePartitionData -Identity <span class="str">"53150c05-1f06-4e35-a6c3-01fc485956a3"</span> -ServiceProxy <span class="str">"Managed Metadata Service"</span> -path <span class="str">"\\Puzzlepart\termstore\pzl-staging.bak"</span> -OverwriteExisting</pre>
</div>
<br />
Getting the MMS application proxy ID and the ServiceProxy object:<br />
<!-- code formatted by http://manoli.net/csharpformat/ -->
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt">$metadataApp= Get-SpServiceApplication | ? {$_.TypeName -eq <span class="str">"Managed Metadata Service"</span>}</pre>
<pre>$mmsAppId = $metadataApp.Id</pre>
<pre class="alt">$mmsproxy = Get-SPServiceApplicationProxy | ?{$_.TypeName -eq <span class="str">"Managed Metadata Service Connection"</span>}</pre>
</div>
<br />
Tajeshwar Singh has posted several posts on using these scripts, including how to solve typical issues:<br />
<ul>
<li><a href="http://blogs.msdn.com/b/taj/archive/2011/01/11/site-collection-backup-restore-and-managed-metadata.aspx">Site Collection Backup\Restore and Managed Metadata</a></li>
<li><a href="http://blogs.msdn.com/b/taj/archive/2010/10/20/import-spmetadatawebservicepartitiondata-and-bulk-load-problem.aspx">Import-SPMetadataWebServicePartitionData and BULK LOAD Problem</a></li>
<li><a href="http://blogs.msdn.com/b/taj/archive/2011/03/20/import-spmetadatawebservicepartitiondata-error-in-multi-server-deployment.aspx">Import-SPMetadataWebServicePartitionData error in multi server deployment</a></li>
</ul>
In addition to such issues, I've run into this issue:<br />
<br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;">The Managed Metadata Service or Connection is currently not available. The Application Pool or Managed Metadata Web Service may not have been started. Please Contact your Administrator. </span><br />
<br />
The cause of this error was neither the app-pool nor the 'service on server' not being started, but the service account used in the production farm <a href="http://social.technet.microsoft.com/Forums/en-US/sharepoint2010setup/thread/4ef7df7e-34fa-47d9-aa1e-b64282b64ef3/">not being available</a> in the staging farm. Look through the user accounts listed in the ECMPermission table in the MMS database, and correct the "wrong" accounts. Note that updating the MMS database directly might not be supported.<br />
<br />
Note that after the term store migration, the MMS <a href="http://kjellsj.blogspot.com/2011/05/provisioning-sites-content-type-hub.html">content type HUB</a> URL configuration will also have been overwritten. You may not notice for some time, but the content type HUB publishing and subscriber timer jobs will stop working. What you will notice, is that if you try to click republish on a content type in the HUB, you'll get an "No valid proxy can be found to do this operation" error. See <a href="http://www.sharepointanalysthq.com/2010/11/how-to-change-the-content-type-hub-url/">How to change the Content Type Hub URL</a> by Michal Pisarek for the steps to rectify this.<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt">Set-SPMetadataServiceApplication -Identity <span class="str">"Managed Metadata Service"</span> -HubURI <span class="str">"http://puzzlepart:8181/"</span></pre>
</div>
<br />
After resetting this MMS configuration, you should verify that the <a href="http://www.sptechweb.com/content/article.aspx?ArticleID=36166">content type publishing works correctly</a> by republishing and running the timer jobs. Use "Site Collection Administration > Content Type Publishing" as shown on page 2 in Chris Geier's article to verify that the correct HUB is set and that HUB content types are pushed to the subscribers.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com2tag:blogger.com,1999:blog-11096258.post-36965968968795774102012-04-16T11:19:00.004+02:002012-06-23T15:40:36.370+02:00Getting Elevated Search Results in SharePoint 2010I often use the SharePoint 2010 search CoreResultsWebPart in combination with scopes, content types and managed properties defined in the Search Service Application (SSA) for having dynamic search-driven content in pages. Sometimes the users might need to see some excerpt of content that they really do not have access to, and that you don't want to grant them access to either; e.g. to show a summary to anonymous visitors on your public web-site from selected content that is really stored in the extranet web-application in the SharePoint farm.<br />
<br />
What is needed then is to execute the search query with elevated privileges using a custom core results web-part. As my colleague Mikael Svenson shows in <a href="http://techmikael.blogspot.com/2010/12/doing-blended-search-results-in.html">Doing blended search results in SharePoint–Part 2: The Custom CoreResultsWebPart Way</a>, it is quite easy to get at the search results code and use the SharedQueryManager object that actually runs the query. Create a web-part that inherits the ootb web-part and override the GetXPathNavigator method like this:<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt"><span class="kwrd">namespace</span> Puzzlepart.SharePoint.Presentation</pre>
<pre>{</pre>
<pre class="alt"> [ToolboxItemAttribute(<span class="kwrd">false</span>)]</pre>
<pre> <span class="kwrd">public</span> <span class="kwrd">class</span> JobPostingCoreResultsWebPart : CoreResultsWebPart</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">protected</span> <span class="kwrd">override</span> <span class="kwrd">void</span> CreateChildControls()</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">base</span>.CreateChildControls();</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">protected</span> <span class="kwrd">override</span> XPathNavigator GetXPathNavigator(<span class="kwrd">string</span> viewPath)</pre>
<pre> {</pre>
<pre class="alt"> XmlDocument xmlDocument = <span class="kwrd">null</span>;</pre>
<pre> QueryManager queryManager = </pre>
<pre> SharedQueryManager.GetInstance(Page, QueryNumber)</pre>
<pre> .QueryManager;</pre>
<pre class="alt"> SPSecurity.RunWithElevatedPrivileges(<span class="kwrd">delegate</span>()</pre>
<pre> {</pre>
<pre class="alt"> xmlDocument = queryManager.GetResults(queryManager[0]);</pre>
<pre> });</pre>
<pre class="alt"> XPathNavigator xPathNavigator = xmlDocument.CreateNavigator();</pre>
<pre> <span class="kwrd">return</span> xPathNavigator;</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt">}</pre>
</div>
<br />
<a href="http://www.threewill.com/2010/06/connect-to-sharepoint-forwarding-user-identities/">Running the query with elevated privileges</a> means that it can return any content that the app-pool identity has access to. Thus, it is important that you grant that account read permissions only on content that you would want just any user to see. Remember that the security trimming is done at query time, not at crawl time, with standard SP2010 server search. It is the <a href="http://msdn.microsoft.com/en-us/magazine/ff796226.aspx">credentials passed to the location's SSA proxy</a> that is used for the security trimming. Use WindowsIdentity.GetCurrent() from the System.Security.Principal namespace if you need to get at the app-pool account from your code.<br />
<br />
You would want to add a scope and/or some fixed keywords to the query in the code before getting the results, in order to prevent malicious or accidental misuse of the elevated web-part to search for just anything in the crawled content of the associated SSA that the app-pool identity has access to. Another alternative is to run the query under another identity than the app-pool account by using real Windows impersonation in combination with the Secure Store Service (see <a href="http://kjellsj.blogspot.com/2010/10/secure-store-service-create-site.html">this post</a> for all the needed code) as this allows for using a specific content query account.<br />
<br />
The nice thing about using the built-in query manager this way, rather than running your own KeywordQuery and providing your own result XML local to the custom web-part instance, is that the shared QueryManager's Location object will get its Result XML document populated. This is important for the correct behavior for the other search web-parts on the page using the same QueryNumber / UserQuery, such as the paging and refiners web-parts.<br />
<br />
The result XmlDocument will also be in the correct format with lower case column names, correct hit highlighting data, correct date formatting, duplicate trimming, getting <path> to be <url> and <urlEncoded>, have the correct additional managed and crawled properties in the result such as <FileExtension> and <ows_MetadataFacetInfo>, etc, in addition to having the row <id> element and <imageUrl> added to each result. If you override by using a replacement KeywordQuery you must also implement code to apply appended query, fixed query, scope, result properties, sorting and paging yourself to gain full fidelity for your custom query web-part configuration.<br />
<br />
If you don't get the expected elevated result set in your farm (I've only tested this on STS claims based web-apps; also see <a href="http://toastertech.com/2012/06/sharepoint-2010-crawler-indexes-items-but-they-are-not-searchable/">ForceClaimACLs for the SSA</a> by my colleague Ole Kristian Mørch-Storstein), then the sure thing is to create a new QueryManager instance within the RWEP block as shown in <a href="http://www.dotnetmafia.com/blogs/dotnettipoftheday/archive/2010/08/15/how-to-use-the-querymanager-class-to-query-sharepoint-2010-enterprise-search.aspx">How to: Use the QueryManager class to query SharePoint 2010 Enterprise Search</a> by Corey Roth. This will give you correctly formatted XML results, but note that the search web-parts might set the $ShowMessage xsl:param to true, tricking the XSLT rendering into show the "no results" message and advice texts. Just change the XSLT to call either dvt_1.body or dvt_1.empty templates based on the TotalResults count in the XML rather than the parameter. Use <a href="http://msdn.microsoft.com/en-us/library/ms546985.aspx">the <xmp> trick</a> to validate that there are results in the XML that all the search web-parts consumes, including core results and refinement panel<span style="background-color: white;">.</span><br />
<br />
The <a href="http://msdn.microsoft.com/en-us/library/ms584121(v=office.12).aspx">formatting and layout of the search results is as usual controlled by overriding the result XSLT</a>. This includes the data such as any links in the results, as you don't want the users to click on links that just will give them access denied errors.<br />
<br />
When using the search box web-part, use the contextual scope option for the scopes dropdown with care. The ContextualScopeUrl (u=) parameter will default to the current web-application, causing an empty result set when using the custom core results web-part against a content source from another SharePoint web-application.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com18tag:blogger.com,1999:blog-11096258.post-60856279746255464672012-03-08T09:21:00.000+01:002012-04-17T16:07:43.724+02:00SharePoint 'Approve on behalf of' for Publishing PagesUsing <a href="http://blogs.msdn.com/b/sanjaynarang/archive/2009/02/19/relationships-between-moderation-approval-status-scheduling-versions-and-workflows.aspx">require approval and the approval workflow</a> in SharePoint 2010 publishing, or just approval on the pages library in the <a href="http://keutmann.blogspot.com/2008/05/publishingfeaturehandler-feature-id.html">simple publishing</a> configuration, is straight forwards when using the browser as you're only logged in as one person with a limited set of roles and thus set of rights. Typically a page author can edit a page and submit it for approval, but not actually approve the page to be published on the site. When you need to approve, you have to log on as a user with approval rights.<br />
<br />
Sometimes you need to extend the user experience to allow an author to make simple changes to an already published page, such as extending the publishing end date, and republish it directly without having to do all the approval procedures all over again. So you create a custom "extend expiry date" ribbon button, elevate the privileges from code and call the Page ListItem File Approve method, only to get an access denied error.<br />
<br />
In SharePoint 2010, it is not sufficient to be the app-pool identity (SHAREPOINT\System) or even a site-collection admin, you have to run the approval procedures as a user with approval privileges. So your code needs to impersonate a user in the "Approvers" group to be able to approve an item on behalf of the current user.<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt"><span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">void</span> ApproveOnBehalfOfUser(</pre>
<pre class="alt">SPListItem item, <span class="kwrd">string</span> approversGroupName, <span class="kwrd">string</span> userName, <span class="kwrd">string</span> comment)</pre>
<pre>{</pre>
<pre class="alt"> Guid siteId = item.ParentList.ParentWeb.Site.ID;</pre>
<pre> Guid webId = item.ParentList.ParentWeb.ID;</pre>
<pre class="alt"> Guid listId = item.ParentList.ID;</pre>
<pre> Guid itemId = item.UniqueId;</pre>
<pre class="alt"> </pre>
<pre> SPUserToken approveUser = GetApproverUserToken(siteId, webId, approversGroupName);</pre>
<pre class="alt"> <span class="kwrd">if</span> (approveUser == <span class="kwrd">null</span>)</pre>
<pre> <span class="kwrd">throw</span> <span class="kwrd">new</span> ApplicationException(String.Format(</pre>
<pre><span class="str">"The group '{0}' has no members of type user, cannot approve item on behalf of '{1}'"</span>, </pre>
<pre>approversGroupName, userName));</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">using</span> (SPSite site = <span class="kwrd">new</span> SPSite(siteId, approveUser))</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">using</span> (SPWeb web = site.OpenWeb(webId))</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">var</span> approveItem = web.Lists[listId].Items[itemId];</pre>
<pre class="alt"> approveItem.File.Approve(comment);</pre>
<pre> }</pre>
<pre class="alt"> }</pre>
<pre>}</pre>
<pre class="alt"> </pre>
<pre><span class="kwrd">private</span> <span class="kwrd">static</span> SPUserToken GetApproverUserToken(</pre>
<pre>Guid siteId, Guid webId, <span class="kwrd">string</span> approversGroupName)</pre>
<pre class="alt">{</pre>
<pre> SPUserToken token = <span class="kwrd">null</span>;</pre>
<pre class="alt"> SPSecurity.RunWithElevatedPrivileges(() =></pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">using</span> (SPSite site = <span class="kwrd">new</span> SPSite(siteId))</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">using</span> (SPWeb web = site.OpenWeb(webId))</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">var</span> group = web.SiteGroups[approversGroupName];</pre>
<pre> <span class="kwrd">if</span> (group != <span class="kwrd">null</span>)</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">foreach</span> (SPUser user <span class="kwrd">in</span> group.Users)</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">if</span> (!user.IsDomainGroup && !user.IsSiteAdmin)</pre>
<pre class="alt"> {</pre>
<pre> token = web.GetUserToken(user.LoginName);</pre>
<pre class="alt"> <span class="kwrd">break</span>;</pre>
<pre> }</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt"> });</pre>
<pre> <span class="kwrd">return</span> token;</pre>
<pre class="alt">}</pre>
</div>
<br />
The code picks a user from the approvers group, and then impersonates that user using a SPUserToken object. From within the impersonated user token scope, the given page list item is opened again with the permissions of an approver, and finally the page is approved on behalf of the given page author.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com0tag:blogger.com,1999:blog-11096258.post-23256061303805486332012-02-28T09:27:00.003+01:002012-05-09T19:03:36.282+02:00SharePoint ResolvePrincipal for ADFS usersGranting permissions in SharePoint 2010 by code is done by <a href="http://kjellsj.blogspot.com/2008/10/sharepoint-acls-roledefinitions.html">assigning roles to user or group principals</a>, or for claims-based application, to a claim type instance. As you will find out when implementing a claims-based applications against ADFS, the SPUtility <a href="http://msdn.microsoft.com/en-us/library/ms458648.aspx">ResolvePrincipal</a> method that you can use against the Windows identity provider and also against forms-based authentication (FBA), don't work for ADFS users. That is no surprise when looking at the SPPrincipalSource enum that lists the site's user info list, the Windows provider, and then the FBA membership and role providers.<br />
<br />
The solution is rather simple, pass the user identity claim value such as the user's e-mail address used in ADFS to the SPClaimProviderManager <a href="http://msdn.microsoft.com/en-us/library/ee560863.aspx">CreateUserClaim</a> method using the TrustedProvider issuer type. Then pass the generated claim to EnsureUser to get a SPPrincipal wrapping the user identity claim:<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt"><span class="kwrd">public</span> <span class="kwrd">static</span> SPUser ResolveAdfsPrincipal(SPWeb web, <span class="kwrd">string</span> email, <span class="kwrd">string</span> issuerIdentifier)</pre>
<pre>{</pre>
<pre class="alt"> SPUser user = <span class="kwrd">null</span>;</pre>
<pre> <span class="kwrd">if</span> (!SPClaimProviderManager.IsEncodedClaim(email))</pre>
<pre class="alt"> {</pre>
<pre> SPClaim claim = SPClaimProviderManager.CreateUserClaim(email, SPOriginalIssuerType.TrustedProvider, issuerIdentifier);</pre>
<pre class="alt"> <span class="kwrd">if</span> (claim != <span class="kwrd">null</span>)</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">string</span> userClaim = claim.ToEncodedString();</pre>
<pre> user = web.EnsureUser(userClaim);</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt"> <span class="kwrd">return</span> user;</pre>
<pre>}</pre>
<pre class="alt"> </pre>
<pre><span class="kwrd">public</span> <span class="kwrd">static</span> SPUser ResolvePrincipal(SPWeb web, <span class="kwrd">string</span> email)</pre>
<pre class="alt">{</pre>
<pre> SPUser user = <span class="kwrd">null</span>;</pre>
<pre class="alt"> SPPrincipalInfo identity = SPUtility.ResolvePrincipal(web, email, SPPrincipalType.All, SPPrincipalSource.All, <span class="kwrd">null</span>, <span class="kwrd">true</span>);</pre>
<pre> <span class="kwrd">if</span> (identity != <span class="kwrd">null</span>)</pre>
<pre class="alt"> {</pre>
<pre> user = web.EnsureUser(identity.LoginName);</pre>
<pre class="alt"> }</pre>
<pre> <span class="kwrd">return</span> user;</pre>
<pre class="alt">}</pre>
</div>
<br />
Use the name of your ADFS identity provider as the issuer identifer parameter. If you're unsure of what this string should be, add a user to a SharePoint group using the UI and look at the identity claim value generated by SharePoint.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-ycgk9-nmH44/T0yOcgtm-UI/AAAAAAAAAYI/S_ipgDLDodc/s1600/SP2010_identity_user_claim_adfs2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="80" src="http://4.bp.blogspot.com/-ycgk9-nmH44/T0yOcgtm-UI/AAAAAAAAAYI/S_ipgDLDodc/s400/SP2010_identity_user_claim_adfs2.png" width="400" /></a></div>
<br />
The identity claim value follows a specific format, and with ADFS (trusted provider) the issuer identifier name is always the 2nd pipe-separated value. The 1st part starting "i:" is for "identity" type claim, while the ending ".t" is for "trusted" provider (".f" is FBA, ".w" is Windows). In between is an encoding of the <a href="http://technet.microsoft.com/en-us/library/ee913589(v=ws.10).aspx">claim type</a> used as the <a href="http://technet.microsoft.com/en-us/library/ff607753.aspx">IdentifierClaim</a> specified when registering the ADFS trusted identity provider. See the <a href="http://blogs.msdn.com/b/scicoria/archive/2011/06/30/identity-claims-encoding-for-sharepoint.aspx">full list of claim type and claim value type encoding characters here</a>. The 3rd part of the claim is the actual user claim value. This was an oversimplified explanation, refer to the <a href="http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=27569">SP2010 claims whitepaper</a> for complete details.<br />
<br />
Side note: The <a href="http://www.wictorwilen.se/Post/How-Claims-encoding-works-in-SharePoint-2010.aspx">claim type encoding logic</a> is important when using a custom claims provider (CCP) to augment the claimset with your own custom claim types. If you have CCPs on multiple SharePoint farms, it’s very important that you <a href="http://msdn.microsoft.com/en-us/magazine/hh547099.aspx">deploy all the CCPs in the same sequence across farms</a> to ensure that the claim type mapping and encoding character for a custom claim type becomes the same across all farms. The best approach for ensuring parity across farms might be <a href="http://www.theidentityguy.com/articles/2010/10/19/adding-claims-to-an-existing-token-issuer-in-sharepoint-2010.html">using Powershell to add the claim mappings</a> first.<br />
<br />
The code really doesn't resolve the user against ADFS, it just creates a claim type instance that authorization later on can validate against the logged in user's token (claim set). If the user's token contains the correct value for the claim type assigned to the securable object, then the user is authorized according to the role assignment.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com0tag:blogger.com,1999:blog-11096258.post-42533315184397617572012-02-16T18:59:00.000+01:002012-06-04T13:45:15.095+02:00Reusable SPGridView with Multiple Filter and Sort ColumnsThe venerable SPGridView still has its use in SharePoint 2010 when your data is not stored in a list or accessible as an external content type through BCS. A typical example is when using a KeywordQuery to build a search-driven web-part feeding on results as DataTable as show in <a href="http://www.dotnetmafia.com/blogs/dotnettipoftheday/archive/2010/08/12/how-to-use-the-sharepoint-2010-enterprise-search-keywordquery-class.aspx">How to: Use the SharePoint 2010 Enterprise Search KeywordQuery Class</a> by Corey Roth. Another example is cross-site queries using <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spsitedataquery.aspx">SPSiteDataQuery</a>.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-b_qmUkFDTwY/Tz1WgLhq0VI/AAAAAAAAAX8/H7o3yJLViC4/s1600/SPGridViewSortFilter.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://3.bp.blogspot.com/-b_qmUkFDTwY/Tz1WgLhq0VI/AAAAAAAAAX8/H7o3yJLViC4/s1600/SPGridViewSortFilter.jpg" /></a></div>
The SPGridView can use a DataTable as its data source, but several things from sorting arrows to filtering don't work as expected when not using an ObjectDataSource. As Shawn Kirby shows in <a href="http://sharethefrustration.blogspot.com/2010/02/spgridview-webpart-with-multiple-filter.html">SPGridView WebPart with Multiple Filter and Sort Columns</a> it is quite easy to implement support for such features.<br />
<br />
In this post, I show how to generalize Shawn's web-part code into a SPGridView derived class and a data source class wrapping a DataTable, isolating this functionality from the web-part code itself, for both better reusability and separation of concerns.<br />
<br />
First the simple abstract data source class that you must implement to populate your data set:<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt"><span class="kwrd">namespace</span> Puzzlepart.SharePoint.Core</pre>
<pre>{</pre>
<pre class="alt"> <span class="kwrd">public</span> <span class="kwrd">abstract</span> <span class="kwrd">class</span> SPGridViewDataSource</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">public</span> <span class="kwrd">abstract</span> DataTable SelectData(<span class="kwrd">string</span> sortExpression);</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">protected</span> <span class="kwrd">void</span> Sort(DataTable dataSource, <span class="kwrd">string</span> sortExpression)</pre>
<pre> {</pre>
<pre class="alt"> <span class="rem">//clean up the sort expression if needed - the sort descending </span></pre>
<pre> <span class="rem">//menu item causes the double in some cases </span></pre>
<pre class="alt"> <span class="kwrd">if</span> (sortExpression.ToLowerInvariant().EndsWith(<span class="str">"desc desc"</span>))</pre>
<pre> sortExpression = sortExpression.Substring(0, sortExpression.Length - 5);</pre>
<pre class="alt"> </pre>
<pre> <span class="rem">//need to handle the actual sorting of the data</span></pre>
<pre class="alt"> <span class="kwrd">if</span> (!<span class="kwrd">string</span>.IsNullOrEmpty(sortExpression))</pre>
<pre> {</pre>
<pre class="alt"> DataView view = <span class="kwrd">new</span> DataView(dataSource);</pre>
<pre> view.Sort = sortExpression;</pre>
<pre class="alt"> DataTable newTable = view.ToTable();</pre>
<pre> dataSource.Clear();</pre>
<pre class="alt"> dataSource.Merge(newTable);</pre>
<pre> }</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt">}</pre>
</div>
<br />
The SPGridViewDataSource class provides the SelectData method that you must override, and a completed Sort method that allows the SPGridView to sort your DataTable. Note that this class must be stateless as required by any class used as an ObjectDataSource. Its logic cannot be combined with the grid view class, as it will get instantiated new every time the ObjectDataSource calls SelectData.<br />
<br />
Then the derived grid view with support for filtering and sorting, including the arrows and filter images:<br />
<br />
<div class="csharpcode">
<pre class="alt"><span class="kwrd">namespace</span> Puzzlepart.SharePoint.Core</pre>
<pre>{</pre>
<pre class="alt"> <span class="kwrd">public</span> <span class="kwrd">class</span> SPGridViewMultiSortFilter : SPGridView</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">public</span> SPGridViewMultiSortFilter()</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">this</span>.FilteredDataSourcePropertyName = <span class="str">"FilterExpression"</span>;</pre>
<pre> <span class="kwrd">this</span>.FilteredDataSourcePropertyFormat = <span class="str">"{1} = '{0}'"</span>; </pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">private</span> ObjectDataSource _gridDS;</pre>
<pre> <span class="kwrd">private</span> <span class="kwrd">char</span>[] _sortingSeparator = { <span class="str">','</span> };</pre>
<pre class="alt"> <span class="kwrd">private</span> <span class="kwrd">string</span>[] _filterSeparator = { <span class="str">"AND"</span> };</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">public</span> ObjectDataSource GridDataSource</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">get</span> { <span class="kwrd">return</span> _gridDS; }</pre>
<pre> <span class="kwrd">private set</span></pre>
<pre class="alt"> {</pre>
<pre> _gridDS = value;</pre>
<pre class="alt"> <span class="kwrd">this</span>.DataSourceID = _gridDS.ID;</pre>
<pre> }</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">public</span> <span class="kwrd">bool</span> AllowMultiSorting { <span class="kwrd">get</span>; <span class="kwrd">set</span>; }</pre>
<pre> <span class="kwrd">public</span> <span class="kwrd">bool</span> AllowMultiFiltering { <span class="kwrd">get</span>; <span class="kwrd">set</span>; }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">string</span> FilterExpression</pre>
<pre class="alt"> {</pre>
<pre>. . .</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">string</span> SortExpression</pre>
<pre> {</pre>
<pre class="alt">. . .</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">protected</span> <span class="kwrd">override</span> <span class="kwrd">void</span> CreateChildControls()</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">base</span>.CreateChildControls();</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">this</span>.Sorting += <span class="kwrd">new</span> GridViewSortEventHandler(GridView_Sorting);</pre>
<pre class="alt"> <span class="kwrd">this</span>.RowDataBound += <span class="kwrd">new</span> GridViewRowEventHandler(GridView_RowDataBound);</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">protected</span> <span class="kwrd">void</span> GridView_Sorting(<span class="kwrd">object</span> sender, GridViewSortEventArgs e)</pre>
<pre class="alt"> {</pre>
<pre> EnsureChildControls();</pre>
<pre class="alt"> <span class="kwrd">string</span> direction = e.SortDirection.ToString();</pre>
<pre> direction = (direction == <span class="str">"Descending"</span>) ? <span class="str">" DESC"</span> : <span class="str">""</span>;</pre>
<pre class="alt"> </pre>
<pre> SortExpression = e.SortExpression + direction;</pre>
<pre class="alt"> e.SortExpression = SortExpression;</pre>
<pre> </pre>
<pre class="alt"> <span class="rem">//keep the object dataset filter</span></pre>
<pre> <span class="kwrd">if</span> (!<span class="kwrd">string</span>.IsNullOrEmpty(FilterExpression))</pre>
<pre class="alt"> {</pre>
<pre> _gridDS.FilterExpression = FilterExpression;</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">protected</span> <span class="kwrd">void</span> GridView_RowDataBound(<span class="kwrd">object</span> sender, GridViewRowEventArgs e)</pre>
<pre class="alt"> {</pre>
<pre> EnsureChildControls();</pre>
<pre class="alt"> <span class="kwrd">if</span> (sender == <span class="kwrd">null</span> || e.Row.RowType != DataControlRowType.Header)</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">return</span>;</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> BuildFilterView(_gridDS.FilterExpression);</pre>
<pre class="alt"> SPGridView grid = sender <span class="kwrd">as</span> SPGridView;</pre>
<pre> </pre>
<pre class="alt"> <span class="rem">// Show icon on filtered and sorted columns </span></pre>
<pre> <span class="kwrd">for</span> (<span class="kwrd">int</span> i = 0; i < grid.Columns.Count; i++)</pre>
<pre class="alt"> {</pre>
<pre>. . .</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">void</span> BuildFilterView(<span class="kwrd">string</span> filterExp)</pre>
<pre class="alt"> {</pre>
<pre>. . .</pre>
<pre class="alt"> </pre>
<pre> <span class="rem">//update the filter</span></pre>
<pre class="alt"> <span class="kwrd">if</span> (!<span class="kwrd">string</span>.IsNullOrEmpty(lastExp))</pre>
<pre> {</pre>
<pre class="alt"> FilterExpression = lastExp;</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="rem">//reset object dataset filter</span></pre>
<pre class="alt"> <span class="kwrd">if</span> (!<span class="kwrd">string</span>.IsNullOrEmpty(FilterExpression))</pre>
<pre> {</pre>
<pre class="alt"> _gridDS.FilterExpression = FilterExpression;</pre>
<pre> }</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">public</span> ObjectDataSource SetObjectDataSource(<span class="kwrd">string</span> dataSourceId, SPGridViewDataSource dataSource)</pre>
<pre> {</pre>
<pre class="alt"> ObjectDataSource gridDS = <span class="kwrd">new</span> ObjectDataSource();</pre>
<pre> gridDS.ID = dataSourceId;</pre>
<pre class="alt"> gridDS.SelectMethod = <span class="str">"SelectData"</span>;</pre>
<pre> gridDS.TypeName = dataSource.GetType().AssemblyQualifiedName;</pre>
<pre class="alt"> gridDS.EnableViewState = <span class="kwrd">false</span>;</pre>
<pre> gridDS.SortParameterName = <span class="str">"SortExpression"</span>;</pre>
<pre class="alt"> gridDS.FilterExpression = FilterExpression;</pre>
<pre> <span class="kwrd">this</span>.GridDataSource = gridDS;</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">return</span> gridDS;</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt">}</pre>
</div>
<br />
Only parts of the SPGridViewMultiSortFilter code is shown here, see download link below for the complete code. Note that I have added two properties that controls whether multi-column sorting and multi-column filtering are allowed or not.<br />
<br />
This is an excerpt from a web-part that shows search results using the grid:<br />
<br />
<div class="csharpcode">
<pre class="alt"><span class="kwrd">namespace</span> Puzzlepart.SharePoint.Presentation</pre>
<pre>{</pre>
<pre class="alt"> [ToolboxItemAttribute(<span class="kwrd">false</span>)]</pre>
<pre> <span class="kwrd">public</span> <span class="kwrd">class</span> JobPostingRollupWebPart : WebPart</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">protected</span> SPGridViewMultiSortFilter GridView = <span class="kwrd">null</span>;</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">protected</span> <span class="kwrd">override</span> <span class="kwrd">void</span> CreateChildControls()</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">try</span></pre>
<pre class="alt"> {</pre>
<pre> CreateJobPostingGrid();</pre>
<pre class="alt"> }</pre>
<pre> <span class="kwrd">catch</span> (Exception ex)</pre>
<pre class="alt"> {</pre>
<pre> Label error = <span class="kwrd">new</span> Label();</pre>
<pre class="alt"> error.Text = String.Format(<span class="str">"An unexpected error occurred: {0}"</span>, ex.Message);</pre>
<pre> error.ToolTip = ex.StackTrace;</pre>
<pre class="alt"> Controls.Add(error);</pre>
<pre> }</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">private</span> <span class="kwrd">void</span> CreateJobPostingGrid()</pre>
<pre> {</pre>
<pre class="alt" style="width: 651px;"> <span class="rem">//add to control tree first is important for view state handling </span></pre>
<pre><span style="background-color: transparent;"> </span> Panel panel = <span class="kwrd">new</span> Panel();</pre>
<pre class="alt" style="width: 651px;"> <span style="background-color: transparent;">Controls.Add(panel);</span></pre>
<br />
<pre class="alt"> <span style="background-color: transparent;">GridView = </span><span class="kwrd">new</span><span style="background-color: transparent;"> SPGridViewMultiSortFilter();</span></pre>
<pre> </pre>
<pre class="alt"> . . .</pre>
<pre> </pre>
<pre class="alt"> GridView.AllowSorting = <span class="kwrd">true</span>;</pre>
<pre> GridView.AllowMultiSorting = <span class="kwrd">false</span>;</pre>
<pre class="alt"> GridView.AllowFiltering = <span class="kwrd">true</span>;</pre>
<pre> GridView.FilterDataFields = <span class="str">"Title,Author,Write,"</span>;</pre>
<pre class="alt"> </pre>
<pre> . . .</pre>
<pre><span style="background-color: transparent;"> </span></pre>
<pre class="alt"> <span style="background-color: transparent;">panel.Controls.Add(GridView)</span><span style="background-color: transparent;"> </span></pre>
<br />
<pre class="alt" style="width: 651px;"><span style="background-color: transparent;"> </span><span class="rem">//set PagerTemplate after adding grid to control tree</span></pre>
<br />
<pre class="alt" style="width: 651px;"> </pre>
<pre><span style="background-color: transparent;"> PopulateGridDataSource();</span></pre>
<pre class="alt"><span style="background-color: transparent;"> </span></pre>
<pre> <span class="rem">//must bind in OnPreRender</span></pre>
<pre class="alt"> <span class="rem">//GridView.DataBind(); </span></pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">protected</span> <span class="kwrd">override</span> <span class="kwrd">void</span> OnPreRender(EventArgs e)</pre>
<pre class="alt"> {</pre>
<pre> GridView.DataBind();</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">private</span> <span class="kwrd">void</span> PopulateGridDataSource()</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">var</span> dataSource = <span class="kwrd">new</span> ApprovedJobPostingDataSource();</pre>
<pre> <span class="kwrd">var</span> gridDS = GridView.SetObjectDataSource(<span class="str">"gridDS"</span>, dataSource);</pre>
<pre class="alt"> <span class="rem">//add the data source</span></pre>
<pre> Controls.Add(gridDS);</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt">}</pre>
</div>
<br />
Note how the data source is created and assigned to the grid view, but also added to the control set of the web-part itself. This is required for the grid's DataSourceId binding to find the ObjectDataSource at run-time. Also note that data binding cannot be triggered from CreateChildControls as it is too early in the control's life cycle. The DataBind method must be called from OnPreRender to allow for view state and child controls to load before the sorting and filtering postback events<br />
<br />
Finally, this is an example of how to implement a search-driven SPGridViewDataSource:<br />
<br />
<div class="csharpcode">
<pre class="alt"><span class="kwrd">namespace</span> Puzzlepart.SharePoint.Presentation</pre>
<pre>{</pre>
<pre class="alt"> <span class="kwrd">public</span> <span class="kwrd">class</span> ApprovedJobPostingDataSource : SPGridViewDataSource</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">private</span> <span class="kwrd">string</span> _cacheKey = <span class="str">"Puzzlepart_Godkjente_Jobbannonser"</span>;</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">public</span> <span class="kwrd">override</span> DataTable SelectData(<span class="kwrd">string</span> sortExpression)</pre>
<pre> {</pre>
<pre class="alt"> DataTable dataTable = (DataTable)HttpRuntime.Cache[_cacheKey];</pre>
<pre> <span class="kwrd">if</span> (dataTable == <span class="kwrd">null</span>)</pre>
<pre class="alt"> {</pre>
<pre> dataTable = GetJobPostingData();</pre>
<pre class="alt"> HttpRuntime.Cache.Insert(_cacheKey, dataTable, <span class="kwrd">null</span>, </pre>
<pre class="alt">DateTime.Now.AddMinutes(1), Cache.NoSlidingExpiration);</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">this</span>.Sort(dataTable, sortExpression);</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">return</span> dataTable;</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">private</span> DataTable GetJobPostingData()</pre>
<pre> {</pre>
<pre class="alt"> DataTable results = <span class="kwrd">new</span> DataTable();</pre>
<pre> <span class="kwrd">string</span> jobPostingManager = <span class="str">"Puzzlepart Jobbannonse arbeidsleder"</span>;</pre>
<pre class="alt"> <span class="kwrd">string</span> jobPostingAssistant = <span class="str">"Puzzlepart Jobbannonse assistent"</span>;</pre>
<pre> <span class="kwrd">string</span> approvedStatus = <span class="str">"0"</span>;</pre>
<pre class="alt"> </pre>
<pre> SPSite site = SPContext.Current.Site;</pre>
<pre class="alt"> KeywordQuery query = <span class="kwrd">new</span> KeywordQuery(site);</pre>
<pre> query.QueryText = String.Format(</pre>
<pre><span class="str">"ContentType:\"{0}\" ContentType:\"{1}\" ModerationStatus:\"{2}\""</span>, </pre>
<pre>jobPostingManager, jobPostingAssistant, approvedStatus);</pre>
<pre class="alt"> query.ResultsProvider = SearchProvider.Default;</pre>
<pre> query.ResultTypes = ResultType.RelevantResults;</pre>
<pre class="alt"> </pre>
<pre> ResultTableCollection resultTables = query.Execute();</pre>
<pre class="alt"> <span class="kwrd">if</span> (resultTables.Count > 0)</pre>
<pre> {</pre>
<pre class="alt"> ResultTable searchResults = resultTables[ResultType.RelevantResults];</pre>
<pre> results.Load(searchResults, LoadOption.OverwriteChanges);</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">return</span> results;</pre>
<pre> }</pre>
<pre class="alt"> }</pre>
<pre>}</pre>
</div>
<br />
That was not too hard, was it? Note that SearchProvider Default work for both FAST (FS4SP) and a standard SharePoint 2010 Search Service Application (SSA).<br />
<br />
All the code can be downloaded from <a href="https://skydrive.live.com/redir.aspx?cid=4cbe2e9714073667&resid=4CBE2E9714073667!439&parid=root">here</a>.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com10tag:blogger.com,1999:blog-11096258.post-31174608318099733502012-01-19T18:34:00.000+01:002012-02-16T20:38:54.805+01:00Custom ADFS Login Form for SharePoint 2010 ClaimsThis week I've been involved in creating a custom login page for SharePoint 2010 to bypass the standard "select a login method" page for multi-mode claims-enabled web-applications. What we wanted was similar to the <a href="http://blogs.msdn.com/b/jjameson/archive/2011/02/25/claims-login-web-part-for-sharepoint-server-2010.aspx">Claims Login Web Part for SharePoint Server 2010</a> for Forms-Based Authentication (FBA) by Jeremy Jameson, but for a trusted ADFS 2.0 identity provider instead.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-jqTbLA9P4X4/TxqHWaxRXcI/AAAAAAAAAXs/uAaDDNxdgDA/s1600/sp2010_cba_login_selector.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="131" src="http://3.bp.blogspot.com/-jqTbLA9P4X4/TxqHWaxRXcI/AAAAAAAAAXs/uAaDDNxdgDA/s400/sp2010_cba_login_selector.png" width="400" /></a></div>
<br />
Having a custom login page allows you to stay in your site and avoid the <a href="http://msdn.microsoft.com/en-us/library/hh147177.aspx">passive STS authN redirect dance</a> back and forth between SP and the ADFS STS for authentication. This requires you to use active mode (WS-Trust) rather than the passive mode used by SharePoint. Note that this active approach won't give you single sign-on, because you won't get the MSISAuth <a href="http://msdn.microsoft.com/en-us/library/hh446525.aspx">ADFS SSO cookies</a> - it will simply authenticate you first and then give you the SharePoint FedAuth cookie.<br />
<br />
The code you need to call ADFS to make it authenticate you, and thus issue a claims token for use with SharePoint, can be found at <a href="http://www.leastprivilege.com/UsingAnActiveEndpointToSignIntoAWebApplication.aspx">Using an Active Endpoint to sign into a Web Application</a> by Dominick Baier or <a href="http://koenwillemse.wordpress.com/2010/08/02/making-a-web-application-use-an-active-sts/">Making a web application use an active STS</a> by Koen Willemse. The missing detail not shown in their code is the URL to the ADFS endpoint, which needs to match the <a href="http://www.leastprivilege.com/WIFADFS2AndWCFndashPart1Overview.aspx">chosen client credentials and security mode</a>; when using <span style="font-family: 'Courier New', Courier, monospace;">UserNameWSTrustBinding</span> and sending the username and password in the WCF message secured using SSL (i.e. mixed), the URL should be like "<span style="font-family: 'Courier New', Courier, monospace;">http://adfs.pzl/adfs/services/trust/13/usernamemixed/</span>" including the important ending / slash to avoid "405 method not allowed" error from IIS.<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt"><span class="kwrd">protected</span> <span class="kwrd">void</span> btnLogin_Click(<span class="kwrd">object</span> sender, EventArgs e)</pre>
<pre>{</pre>
<pre class="alt"> <span class="rem">// authenticate with WS-Trust endpoint</span></pre>
<pre> <span class="kwrd">var</span> factory = <span class="kwrd">new</span> WSTrustChannelFactory(</pre>
<pre class="alt"> <span class="kwrd">new</span> UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),</pre>
<pre> <span class="kwrd">new</span> EndpointAddress(</pre>
<pre><span class="str">"https://adfs.puzzlepart.com/adfs/services/trust/13/usernamemixed/"</span>));</pre>
<pre class="alt"> </pre>
<pre> </pre>
<pre class="alt"> factory.Credentials.UserName.UserName = txtUserName.Text;</pre>
<pre> factory.Credentials.UserName.Password = txtPassword.Text;</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">var</span> channel = factory.CreateChannel();</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">var</span> rst = <span class="kwrd">new</span> RequestSecurityToken</pre>
<pre class="alt"> {</pre>
<pre> RequestType = RequestTypes.Issue,</pre>
<pre class="alt"> AppliesTo = <span class="kwrd">new</span> EndpointAddress(<span class="str">"urn:sharepoint:puzzlepart"</span>),</pre>
<pre> KeyType = KeyTypes.Bearer</pre>
<pre class="alt"> };</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">var</span> genericToken = channel.Issue(rst) <span class="kwrd">as</span> GenericXmlSecurityToken;</pre>
<pre> </pre>
<pre class="alt"> </pre>
<pre> <span class="rem">// parse token</span></pre>
<pre class="alt"> <span class="kwrd">var</span> handlers = FederatedAuthentication.ServiceConfiguration</pre>
<pre class="alt">.SecurityTokenHandlers;</pre>
<pre> <span class="kwrd">var</span> token = handlers.ReadToken(<span class="kwrd">new</span> XmlTextReader(</pre>
<pre class="alt"> <span class="kwrd">new</span> StringReader(genericToken.TokenXml.OuterXml)));</pre>
<pre> </pre>
<pre class="alt"> SPSecurity.RunWithElevatedPrivileges(<span class="kwrd">delegate</span>(){</pre>
<pre> SPFederationAuthenticationModule.Current</pre>
<pre>.<span style="background-color: transparent;">SetPrincipalAndWriteSessionToken(token);</span></pre>
<pre class="alt"> });</pre>
<pre> Response.Redirect(<span class="str">"~/pages/default.aspx"</span>);</pre>
<pre class="alt">}</pre>
</div>
<br />
After getting authenticated against the ADFS STS when calling <span style="font-family: 'Courier New', Courier, monospace;">Issue</span> on the WS-Trust channel with a <span style="font-family: 'Courier New', Courier, monospace;">RequestSecurityToken</span>, the returned SAML security token must first be parsed and then written to a FedAuth cookie created from the SAML token. The SharePoint FAM wrapper will both set the thread principal and write the cookie, making the user a logged in SharePoint user.<br />
<br />
Note how the writing of the cookie is wrapped with <span style="font-family: 'Courier New', Courier, monospace;">RunWithElevatedPrivileges</span> to ensure that it runs as the app-pool identity and not as the impersonated SharePoint user. This is to avoid the dreaded "<span style="font-family: 'Courier New', Courier, monospace;">CryptographicException: The system cannot find the file specified</span>" error in the internal <span style="font-family: 'Courier New', Courier, monospace;">ProtectedDataCookieTransform</span> call.<br />
<br />
When calling <span style="font-family: 'Courier New', Courier, monospace;">ValidateToken</span> you will run into the <a href="http://blogs.msdn.com/b/jimmiet/archive/2010/09/19/10064794.aspx">SecurityTokenException: Issuer of the Token is not a Trusted Issuer</a> error if your STS is not trusted by SharePoint. SharePoint is configured to use its own <span style="font-family: 'Courier New', Courier, monospace;">SPPassiveIssuerNameRegistry</span> <span style="font-family: 'Courier New', Courier, monospace;"></span>and that will either validate against the built-in SharePoint STS or the set of trusted STS token issuers. See how to add your STS certificate(s) at <a href="http://blogs.msdn.com/b/ekraus/archive/2010/03/22/sharepoint-2010-claims-based-auth-with-adfs-v2.aspx">SharePoint 2010 Claims-Based Auth with ADFS v2</a> by Eric Kraus. The trusted providers are apparently only used if the login page is located under the /_trust/ folder that is part of the above "redirect dance" when authenticating against a trusted identity provider.<br />
<br />
Over at <a href="http://stackoverflow.com/questions/8215037/creating-an-custom-active-sts-for-sharepoint-2010-using-windows-identity-foundat">stack overflow</a>, Matt Whetton had run into the same exception as us and solved it by replacing the passive <span style="font-family: 'Courier New', Courier, monospace;"><issuerNameRegistry></span> with the Windows Identity Foundation (WIF) <span style="font-family: 'Courier New', Courier, monospace;">ConfigurationBasedIssuerNameRegistry</span> instead. The <a href="http://koenwillemse.wordpress.com/2010/09/02/configuration-of-wif/">Configuration of WIF</a> post shows how to add the set of certificate names and thumbprints to the <span style="font-family: 'Courier New', Courier, monospace;"><trustedIssuers></span> list. This is how your web.config list of trusted STS token issuers may look like:<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt"><span class="kwrd"><</span><span class="html">issuerNameRegistry</span> <span class="attr">type</span><span class="kwrd">="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, </span></pre>
<pre class="alt"><span class="kwrd">Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"</span><span class="kwrd">></span> </pre>
<pre><span class="kwrd"> <</span><span class="html">trustedIssuers</span><span class="kwrd">></span> </pre>
<pre class="alt"><span class="kwrd"> <</span><span class="html">add</span> <span class="attr">thumbprint</span><span class="kwrd">="1337133713371337"</span> <span class="attr">name</span><span class="kwrd">="CN=adfs-puzzlepart"</span> <span class="kwrd">/></span> </pre>
<pre><span class="kwrd"> <</span><span class="html">add</span> <span class="attr">thumbprint</span><span class="kwrd">="0000000000000000"</span> <span class="attr">name</span><span class="kwrd">="CN=SharePoint Security Token Service"</span> <span class="kwrd">/></span></pre>
<pre class="alt"><span class="kwrd"> </</span><span class="html">trustedIssuers</span><span class="kwrd">></span> </pre>
<pre><span class="kwrd"></</span><span class="html">issuerNameRegistry</span><span class="kwrd">></span> </pre>
</div>
<br />
Remember to add the SharePoint self-issued certificates such as the "SharePoint Security Token Service" certificate to the list of trusted issuers, in addition to your own STS.<br />
<br />
I strongly recommend putting your custom ADFS login page under /_trust/ to avoid having to change the SharePoint web.config files. We chose this approach to minimize risks.<br />
<br />
Note that Fiddler seems to break the ADFS login process, at least when decrypting SSL. The customized rule provided in <a href="http://blogs.msdn.com/b/fiddler/archive/2011/09/04/fiddler-http-401-authentication-workaround-to-support-channel-binding-tokens-removing-endless-prompts.aspx">Fiddler and Channel Binding Tokens Revisited</a> by Eric Lawrence alleviates this problem. Just make sure you click "remember my credentials" when logging in so that Fiddler can get it from the Windows Credential Manager.<br />
<br />
Disclaimer: Note that even if things seems to work as normal after this configuration change, there is no guarantee that nothing was affected in the huge platform that SharePoint 2010 is. The combination of SP2010 claims and WIF is not very well documented, and any changes beyond supported configuration involves risks. Do not apply these changes if you are not sure that it will not break any of your SharePoint solutions or services.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com5tag:blogger.com,1999:blog-11096258.post-71329007981767559732012-01-17T09:22:00.000+01:002012-02-16T20:44:14.296+01:00Simple Feature Files Cleanup using Extension MethodsAs every seasoned SharePoint developer knows, deactivating a feature does not remove the files deployed by that feature. The deployed masterpages, web part pages, wiki pages, page layouts, web-part definitions, styling artifacts, etc files will stay in the target libraries - and they will not be overwritten on feature activation. Don't let Visual Studio 2010 trick you into believing otherwise.<br />
<br />
You have to delete those deployed files yourself in the FeatureDeactivating event. The classic approach is to delete the files one-by-one, but this is tedious and error-prone. The following is a set of extension methods that allows you to simply do this:<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt"><span class="kwrd">public</span> <span class="kwrd">override</span> <span class="kwrd">void</span> FeatureDeactivating(SPFeatureReceiverProperties properties)</pre>
<pre>{</pre>
<pre class="alt"> SPSite site = (SPSite) properties.Feature.Parent;</pre>
<pre> properties.Definition.DeleteFeatureFiles(<span class="str">"MasterPages"</span>, site.RootWeb);</pre>
<pre class="alt"> properties.Definition.DeleteFeatureWebPartFiles(site.RootWeb);</pre>
<pre>}</pre>
</div>
<br />
<div>
The code is an adaptation of Corey Roth's <a href="http://www.dotnetmafia.com/blogs/dotnettipoftheday/archive/2009/02/16/linq-to-xml-and-deleting-files-on-feature-deactivation.aspx">LINQ to XML and Deleting Files on Feature Deactivation</a>, using extension methods and supporting cleanup of specific feature modules and all feature web-parts.</div>
<br />
<div class="csharpcode">
<pre class="alt"><span class="kwrd">namespace</span> Puzzlepart.SharePoint.Core.SPExtentions</pre>
<pre>{</pre>
<pre class="alt"> <span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">class</span> SPFeatureDefinitionExtentions</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">public</span> <span class="kwrd">class</span> Module</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">public</span> <span class="kwrd">string</span> Name { <span class="kwrd">get</span>; <span class="kwrd">set</span>; }</pre>
<pre> <span class="kwrd">public</span> <span class="kwrd">string</span> Path { <span class="kwrd">get</span>; <span class="kwrd">set</span>; }</pre>
<pre class="alt"> <span class="kwrd">public</span> List<<span class="kwrd">string</span>> Files { <span class="kwrd">get</span>; <span class="kwrd">set</span>; }</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">void</span> DeleteFeatureFiles</pre>
<pre class="alt">(<span class="kwrd">this</span> SPFeatureDefinition spFeatureDefinition, <span class="kwrd">string</span> moduleName, SPWeb web)</pre>
<pre> {</pre>
<pre class="alt"> List<Module> modules = GetModuleFiles(spFeatureDefinition, moduleName);</pre>
<pre> <span class="kwrd">foreach</span> (Module module <span class="kwrd">in</span> modules)</pre>
<pre class="alt"> {</pre>
<pre> DeleteModuleFiles(module, web);</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">void</span> DeleteFeatureWebPartFiles</pre>
<pre class="alt">(<span class="kwrd">this</span> SPFeatureDefinition spFeatureDefinition, SPWeb web)</pre>
<pre> {</pre>
<pre class="alt"> List<Module> modules = GetAllModuleFiles(spFeatureDefinition);</pre>
<pre> <span class="kwrd">foreach</span> (Module module <span class="kwrd">in</span> modules)</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">if</span> (<span class="kwrd">string</span>.Compare(module.Path, <span class="str">"_catalogs/wp"</span>, </pre>
<pre>StringComparison.CurrentCultureIgnoreCase) == 0)</pre>
<pre class="alt"> DeleteModuleFiles(module, web);</pre>
<pre> }</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">private</span> <span class="kwrd">static</span> List<Module> GetModuleFiles</pre>
<pre>(SPFeatureDefinition spFeatureDefinition, <span class="kwrd">string</span> moduleName)</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">string</span> elementsPath = <span class="kwrd">string</span>.Format(<span class="str">@"{0}\FEATURES\{1}\{2}\Elements.xml"</span>, </pre>
<pre>SPUtility.GetGenericSetupPath(<span class="str">"Template"</span>), </pre>
<pre>spFeatureDefinition.DisplayName, moduleName);</pre>
<pre class="alt"> XDocument elementsXml = XDocument.Load(elementsPath);</pre>
<pre> XNamespace sharePointNamespace = <span class="str">"http://schemas.microsoft.com/sharepoint/"</span>;</pre>
<pre class="alt"> </pre>
<pre> <span class="rem">// get each module name and the files in it</span></pre>
<pre class="alt"> <span class="kwrd">var</span> moduleList =</pre>
<pre> <span class="kwrd">from</span> module <span class="kwrd">in</span> elementsXml.Root.Elements(sharePointNamespace + <span class="str">"Module"</span>)</pre>
<pre class="alt"> select <span class="kwrd">new</span></pre>
<pre> {</pre>
<pre class="alt"> Name = (module.Attributes(<span class="str">"Name"</span>).Any()) </pre>
<pre class="alt">? module.Attribute(<span class="str">"Name"</span>).Value : <span class="kwrd">null</span>,</pre>
<pre> ModuleUrl = (module.Attributes(<span class="str">"Url"</span>).Any()) </pre>
<pre>? module.Attribute(<span class="str">"Url"</span>).Value : <span class="kwrd">null</span>,</pre>
<pre class="alt"> Files = module.Elements(sharePointNamespace + <span class="str">"File"</span>)</pre>
<pre> };</pre>
<pre class="alt"> </pre>
<pre> List<Module> modules = <span class="kwrd">new</span> List<Module>();</pre>
<pre class="alt"> <span class="rem">// iterate through each module with files</span></pre>
<pre> <span class="kwrd">foreach</span> (<span class="kwrd">var</span> module <span class="kwrd">in</span> moduleList)</pre>
<pre class="alt"> {</pre>
<pre> Module m = <span class="kwrd">new</span> Module()</pre>
<pre class="alt"> {</pre>
<pre> Name = module.Name,</pre>
<pre class="alt"> Path = module.ModuleUrl</pre>
<pre> };</pre>
<pre class="alt"> List<<span class="kwrd">string</span>> files = <span class="kwrd">new</span> List<<span class="kwrd">string</span>>();</pre>
<pre> <span class="kwrd">foreach</span> (<span class="kwrd">var</span> fileElement <span class="kwrd">in</span> module.Files)</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">string</span> filename = (fileElement.Attributes(<span class="str">"Name"</span>).Any()) </pre>
<pre>? fileElement.Attribute(<span class="str">"Name"</span>).Value : fileElement.Attribute(<span class="str">"Url"</span>).Value;</pre>
<pre class="alt"> files.Add(filename);</pre>
<pre> }</pre>
<pre class="alt"> m.Files = files;</pre>
<pre> modules.Add(m);</pre>
<pre class="alt"> }</pre>
<pre> <span class="kwrd">return</span> modules;</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">private</span> <span class="kwrd">static</span> <span class="kwrd">void</span> DeleteModuleFiles(Module module, SPWeb web)</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">foreach</span> (<span class="kwrd">string</span> filename <span class="kwrd">in</span> module.Files)</pre>
<pre> {</pre>
<pre class="alt"> <span class="kwrd">if</span> (!<span class="kwrd">string</span>.IsNullOrEmpty(module.Path))</pre>
<pre> web.GetFile(<span class="kwrd">string</span>.Format(<span class="str">"{0}/{1}"</span>, module.Path, filename)).Delete();</pre>
<pre class="alt"> <span class="kwrd">else</span></pre>
<pre> web.Files.Delete(filename);</pre>
<pre class="alt"> }</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">private</span> <span class="kwrd">static</span> List<Module> GetAllModuleFiles</pre>
<pre>(SPFeatureDefinition spFeatureDefinition)</pre>
<pre class="alt"> {</pre>
<pre> <span class="kwrd">var</span> moduleList = <span class="kwrd">new</span> List<Module>();</pre>
<pre class="alt"> </pre>
<pre> <span class="kwrd">string</span> modulesPath = <span class="kwrd">string</span>.Format(<span class="str">@"{0}\FEATURES\{1}\", </span></pre>
<pre><span class="str">SPUtility.GetGenericSetupPath("</span>Template"), </pre>
<pre>spFeatureDefinition.DisplayName);</pre>
<pre class="alt"> DirectoryInfo folder = <span class="kwrd">new</span> DirectoryInfo(modulesPath);</pre>
<pre> <span class="kwrd">foreach</span> (DirectoryInfo moduleFolder <span class="kwrd">in</span> folder.GetDirectories())</pre>
<pre class="alt"> {</pre>
<pre> moduleList.AddRange(GetModuleFiles(spFeatureDefinition, moduleFolder.Name));</pre>
<pre class="alt"> }</pre>
<pre> </pre>
<pre class="alt"> <span class="kwrd">return</span> moduleList;</pre>
<pre> }</pre>
<pre class="alt"> </pre>
<pre> }</pre>
<pre class="alt">}</pre>
</div>
<br />
Note that page layouts cannot simply be deleted if they are in use. Use code to revert the "GhostableInLibrary" files to the uncustomized (ghosted) feature files on disk in the SharePoint root [14].Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com1tag:blogger.com,1999:blog-11096258.post-71115050684388945122012-01-03T14:11:00.000+01:002012-02-16T20:53:51.338+01:00SharePoint 2010 Localized Publishing Web TemplateWhen you try to create a new localized publishing site based on a minimal SharePoint 2010 publishing <a href="http://blogs.msdn.com/b/vesku/archive/2010/10/14/sharepoint-2010-and-web-templates.aspx">web template</a> (or a similar <a href="http://www.andrewconnell.com/blog/archive/2008/12/06/Using-a-Minimal-Publishing-Site-Definition-in-the-field.aspx">minimal site definition</a>), it might fail with a "CreateWelcomePage" error such as this:<br />
<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, "Courier New", Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
</style>
<br />
<div class="csharpcode">
<pre class="alt">System.Runtime.InteropServices.COMException (0x80070001): 0x80070001 at </pre>
<pre>Microsoft.SharePoint.Library.SPRequestInternalClass.GetMetadataForUrl</pre>
<pre><span style="background-color: transparent;">(String bstrUrl, </span><span style="background-color: transparent;">Int32 METADATAFLAGS, Guid& pgListId, Int32& plItemId, </span></pre>
<pre><span style="background-color: transparent;">Int32& plType, Object& pvarFileOrFolder) at </span></pre>
<pre class="alt">Microsoft.SharePoint.Library.SPRequest.GetMetadataForUrl</pre>
<pre class="alt">(String bstrUrl, Int32 METADATAFLAGS,<span style="background-color: transparent;"> Guid& pgListId, Int32& plItemId, </span></pre>
<pre class="alt"><span style="background-color: transparent;">Int32& plType, Object& pvarFileOrFolder) - </span></pre>
<pre class="alt">-- End of inner exception stack trace --- at </pre>
<pre>Microsoft.SharePoint.SPGlobal.HandleComException(COMException comEx)</pre>
<pre class="alt"> at </pre>
<pre>Microsoft.SharePoint.Library.SPRequest.GetMetadataForUrl</pre>
<pre>(String bstrUrl, Int32 METADATAFLAGS,<span style="background-color: transparent;"> Guid& pgListId, Int32& plItemId, </span></pre>
<pre><span style="background-color: transparent;">Int32& plType, Object& pvarFileOrFolder) at </span></pre>
<pre class="alt">Microsoft.SharePoint.SPWeb.GetListItem</pre>
<pre class="alt">(String strUrl, Boolean bFields, String[] fields) at </pre>
<pre>Microsoft.SharePoint.Publishing.PublishingWeb.GetPublishingPage(String strUrl) at </pre>
<pre class="alt">Microsoft.SharePoint.Publishing.Internal.AreaProvisioner.CreateWelcomePage</pre>
<pre class="alt">(PublishingWeb area, PageLayout pageLayout) at </pre>
<pre>Microsoft.SharePoint.Publishing.Internal.AreaProvisioner.SetDefaultPageProperties</pre>
<pre>(PublishingWeb area, Boolean& updateRequired) at </pre>
<pre class="alt">Microsoft.SharePoint.Publishing.Internal.AreaProvisioner.</pre>
<pre class="alt">InitializePublishingWebDefaults() </pre>
<pre class="alt">- -- End of inner exception stack trace --- at </pre>
<pre>Microsoft.SharePoint.Publishing.Internal.AreaProvisioner.</pre>
<pre>InitializePublishingWebDefaults() at </pre>
<pre class="alt">Microsoft.SharePoint.Publishing.Internal.AreaProvisioner.Provision()</pre>
<pre> at </pre>
<pre class="alt">Microsoft.SharePoint.Publishing.PublishingFeatureHandler.<>c_DisplayClass3.b_0() at </pre>
<pre>Microsoft.Office.Server.Utilities.CultureUtility.RunWithCultureScope</pre>
<pre>(CodeToRunWithCultureScope code) at </pre>
<pre class="alt">Microsoft.SharePoint.Publishing.CmsSecurityUtilities.RunWithWebCulture</pre>
<pre class="alt">(SPWeb web, CodeToRun webCultureDependentCode) at </pre>
<pre>Microsoft.SharePoint.Publishing.PublishingFeatureHandler.FeatureActivated</pre>
<pre>(SPFeatureReceiverProperties receiverProperties).</pre>
</div>
<br />
The typical cause is that your web template/site definition is a bit too minimal. The "Publishing" feature needs some initial configuration property data during feature activation. Don't strip these properties away completely. Also, activating the "Publishing" feature from code or a feature stapler will not work for localized sites if you don't pass in this configuration. It is not standard, but you can pass property XML data from code to feature activation as shown in <a href="http://hristopavlov.wordpress.com/2008/07/15/specifying-properties-when-activating-features-through-code/">Specifying Properties When Activating Features Through Code</a>.<br />
<br />
You must pass in the publishing feature property configuration for the "WelcomePageUrl" to ensure that is reference the localized pages library during activation, which is /sider/ for LCID 1044. The fallback for when this property is not set or is empty seems to be hardcoded to /pages/. Note that using "osrvcore" as the resource file is needed for some languages if you don't have SP1 of the language pack installed.<br />
<br />
<div class="csharpcode">
<pre class="alt"><span class="rem"><!-- Feature: Publishing --></span> </pre>
<pre><span class="kwrd"><</span><span class="html">Feature</span> <span class="attr">ID</span><span class="kwrd">="22A9EF51-737B-4ff2-9346-694633FE4416"</span><span class="kwrd">></span> </pre>
<pre class="alt"> <span class="kwrd"><</span><span class="html">Properties</span> <span class="attr">xmlns</span><span class="kwrd">="http://schemas.microsoft.com/sharepoint/"</span><span class="kwrd">></span></pre>
<pre> <span class="kwrd"><</span><span class="html">Property</span> <span class="attr">Key</span><span class="kwrd">="ChromeMasterUrl"</span> </pre>
<pre><span class="attr"> Value</span><span class="kwrd">="~SiteCollection/_catalogs/masterpage/puzzlepart.master"</span> <span class="kwrd">/></span></pre>
<pre class="alt"> <span class="kwrd"><</span><span class="html">Property</span> <span class="attr">Key</span><span class="kwrd">="WelcomePageUrl"</span> </pre>
<pre class="alt"><span class="attr"> Value</span><span class="kwrd">="$Resources:cmscore,List_Pages_UrlName;/default.aspx"</span><span class="kwrd">/></span></pre>
</div>
<br />
It is important to reference an existing page in an existing library as the welcome page (home page). Deploy a page using a module if needed. Note that not all of these properties need to be specified as they have working default settings as fallback.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com2tag:blogger.com,1999:blog-11096258.post-51314641186844142782011-11-01T11:19:00.002+01:002013-03-04T08:57:03.661+01:00SharePoint Information Architecture from the FieldOver the years, I've written quite a few articles on how to technically <a href="http://kjellsj.blogspot.com/2010/08/classification-and-structuring-of.html">structure your SharePoint solutions</a> into web-apps, sites, subsites, lists and document libraries. All based on having a defined Information Architecture (IA) as a basis for the solution design, or at least being able to reason about your content management using my <a href="http://kjellsj.blogspot.com/2011/10/sharepoint-2010-information.html">"chest of drawers" and "news paper"</a> analogies. The latter is based on my experiences from the field as most companies don't have a well-defined IA in place.<br />
<br />
Common questions from customers are "what is Information Architecture?" and "what is the value of having an IA for SharePoint, can't we just create sites and doc-libs on the fly as needed?". Not to forget "how do we go about creating an Information Architecture for SharePoint?". So here are some IA advice from Puzzlepart projects:<br />
<br />
In short, <a href="http://kjellsj.blogspot.com/2005/05/sharepoint-areas-and-topics-for.html">Information Architecture defines how to classify and structure your content</a> so that it is easy for content consumers to find and explore relevant content, while making it simple for workers to contribute and manage content in an efficient manner.<br />
<br />
The business goal of having an Information Architecture for your SharePoint solution is enabling workers
to contribute, store and mange content in a manner that is simple and
efficient, enabling more content sharing; and at the same time making it easy
for workers to browse and find content they need, while also making it easy for
workers to discover and explore relevant content they didn't know of. The
outcome is more knowledgeable workers that are better informed about what’s
going on in the company and about the range of intellectual property possessed
by other employees, while also saving time wasted on finding information, and
time wasted on incorrect or outdated information.<o:p></o:p><br />
<br />
<i>An outcome is a metric that customers use to define the successful realization
of the objectives and goals. Outcomes happens after the project has delivered on
its objectives and goals, and the customers must themselves work against
securing the outcomes to achieve the desired business value.</i><br />
<br />
The business value of having a working IA is capturing company
knowledge from employees with better quality of shared content, which combined
with good findability drive <a href="http://www.amazon.com/Power-Pull-Smartly-Things-Motion/dp/0465019358/">more knowledgeable workers that make better decisions and better faster processes</a>. In addition, more and better content
sharing helps user not only discover and explore content, but also people such
as subject matter experts, allowing employees to build and expand their network
throughout the company, helping the company to retain talented employees
through social ties and communities. Access to discover more and better content
and people expertise is central to enabling innovation and process improvement,
as new knowledge is a trigger for new ideas and for identifying new
opportunities.<br />
<br />
The process of defining your IA for your SharePoint solution should focus on these objectives and goals:<br />
<ul>
<li>Analyze and define the <u>content classification and structure</u> for the solution </li>
<ul>
<li>goal: identify what content to manage and plan how to store it in SP, leading to sites, subsites and doc-lib structure organized into SP web-apps</li>
</ul>
<li>Analyze and define how to <u>browse and navigate</u> the content</li>
<ul>
<li>goal: make it simple and efficient for users to find and use known content that they need in their daily work to drive better faster processes</li>
</ul>
<li>Analyze and define how to <u>discover and explore</u> the content</li>
<ul>
<li>goal: make it easy for users to stumble upon novel shared knowledge based on "common focus" to trigger innovation and build social ties</li>
</ul>
<li>Provide simple and efficient <u>content contributor experience</u> with liberal appliance of default metadata values, storing content close to the authors </li>
<ul>
<li>goal: <b>make workers contributors, not knowledge management grunts</b>, and help them store content correctly with better metadata and tagging, driving findability and "common focus" content discovery; drive better sharing and collaboration</li>
</ul>
<li>Analyze and define the <u>starter content types with metadata and term set taxonomy</u> based on the defined site and doc-lib architecture, with a strong focus on needed search experience capabilities</li>
<ul>
<li>goal: enable content management and support both search-driven and "common focus" content; drive findability, sharing and innovation</li>
</ul>
<li>Analyze and define the <u>policies for social tagging and rating</u> in the solution, also in relation to user profile interests, skills and responsibility tagging</li>
<ul>
<li>goal: drive "common focus" content discovery, drive findability, drive social communities, drive innovation</li>
</ul>
<li>Analyze and define the <u>search experience</u>, focusing on both search-driven content and on search center scopes and refiners</li>
<ul>
<li>goal: drive findability and provide both search-driven and "common focus" content</li>
</ul>
<li>Enable <u>disposition of redundant and irrelevant content</u> </li>
<ul>
<li>goal: provide users with better, correct and up-to-date information, drive findability, save storage cost, save process cost</li>
</ul>
</ul>
Including the search experience helps you avoid an initial version of your IA with a too narrow scope, which is easy to do for a starter IA when e.g. analyzing only project collaboration needs or just the document types of an attachment-based intranet. It makes you focus on what you get out, not only what you put in.<br />
<br />
Note that navigation is not IA, its just one way to explore the content. Using navigation to structure your content is just reapplying the fileshare folder approach, which we all know doesn't work too good for findability and discovery. Navigation should not <a href="http://kjellsj.blogspot.com/2010/08/classification-and-structuring-of.html">define the statical IA structure for the content</a>, do a <a href="http://kjellsj.blogspot.com/2005/05/sharepoint-areas-and-topics-for.html">LATCH</a> analysis to model the possible IA structures, and choose one of them to define the statical IA structure. The site map is closer to define statical IA structure than navigation, still it is only good for logical IA structure and cannot be expected to be used directly as the physical IA structure in SharePoint.<br />
<br />
In an upcoming article I will give some practical advice from the field on how to define and realize the Information Architecture for your SharePoint solution in an agile fashion.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com0tag:blogger.com,1999:blog-11096258.post-3184565474760375392011-10-27T10:10:00.001+02:002011-11-01T12:00:24.852+01:00SharePoint is like a Chest of DrawersI often get asked "how many document libraries and sites will we need?" in SharePoint, followed by "how will we know whether we should use more doc-libs in a site or just throw it all in there?" and "where should content be stored? we need to show it on the intranet home page, but it is really edited and owned by HR in region Gokk". Well, SharePoint is like a chest of drawers.<br />
<br />
To answer such questions, you need to know how to <a href="http://kjellsj.blogspot.com/2010/08/classification-and-structuring-of.html">classify and structure your content</a>; ideally you should have an <a href="http://kjellsj.blogspot.com/2011/11/sp2010-information-architecture.html">Information Architecture</a> for all your different kinds of data. If you are like most others, you don't. This is where the "chest of drawers" analogy might help you reason about your content.<br />
<br />
<br />
Whether you need many doc-libs or subsites or not depends on your IA policies for information management. Still don't have an IA? Think of a chest of drawers for you clothes: it makes it easier to manage different types of clothes in different ways at different schedules, by e.g. separating t-shirts from trousers. Maybe even handle different kinds of t-shirts differently, such as your precious Maiden t-shirts. It also allows for delegating a few drawers to be managed by your wife; maybe you even want to have some locked drawers with more privacy :)<br />
<br />
Your content are the clothes, the drawers are doc-libs or even subsites; and depending on the variety of clothes you have, you might need quite a sophisticated chest of drawers. Throw in all your other stuff, and you might need a bigger closet or a garage!<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-x6MA3Fxldd8/TqlOdyFcNbI/AAAAAAAAAXE/Q7wLJNNHq80/s1600/chest_of_drawers_02.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="http://4.bp.blogspot.com/-x6MA3Fxldd8/TqlOdyFcNbI/AAAAAAAAAXE/Q7wLJNNHq80/s320/chest_of_drawers_02.jpg" width="239" /></a></div>
So now all your content is stored into nicely separated drawers, with delegated and secure handling where needed. But is is not so easy to see what is in the drawers without actually opening and browsing the content of each drawer. Until we get one of those science fiction closets that knows whats in the drawers and let us explore what trendy outfits we can wear today, its time for another analogy: the good old news paper, even in its modern online incarnation.<br />
<br />
Think of a news paper with a front page and then multiple sections, such as domestic, foreign, sports, economics, etc. The front page and section front pages are used to show the (elsewhere) stored content to readers, helping them quicly browse the content at wellknow locations in the paper. The shown stories are typically rollup content stored elsewhere, typically where maintained, close to the content editors.<br />
<br />
So a paper is built from dispersed storage of content that can be rolled up and targeted to readers multiple places. The home page and section pages rollup content "teasers" and allows the user to browse the content stored elsewhere in the paper and decide whether to explore it further.<br />
<br />
The front page is the home page of your SharePoint site, the sections are subsites and the section pages are the subsite welcome pages in SharePoint parlance. As for the drawers, there might be different management policies and different people handling the different sections, and this helps you decide when subsites are needed. The rollup, or cross publishing if you like, is achieved using the content by query web-part or search-driven content based on content types, tagging and metadata.<br />
<br />
Controlled and secure management of content according to different policies and schedules is much simpler when using subsites as compared to throwing it all into one site. Store the content close to the producers, show it everywhere the users expect to find it - and also where *you* want them to discover and explore knowledge new to them.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com2tag:blogger.com,1999:blog-11096258.post-77045442945288110352011-09-08T12:45:00.003+02:002011-09-21T19:57:48.642+02:00Issue with SP2010 Personal Site UserInfo SynchronizationToday we discovered an issue with the SharePoint synchronization from the user profile database to the hidden UserInfoList in all site-collections. This sync is performed by two timer jobs (see <a href="http://community.bamboosolutions.com/blogs/sharepoint-2010/archive/2010/11/15/sharepoint-2010-user-management.aspx">profile sync details in this excellent article</a> on the Bamboo Team Blog), which will update changes to your user profile in all the cached profile data in the hidden user info lists, except for the UserInfoList in your personal site under the profile site (My Site Host).<br />
<br />
To verify the reported bug, I updated my mobile phone number in my user profile, and ran the two sync timer jobs. This is how my updated user information looks in a team-sites:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-oaEmuO4R2sc/Tmict2sbn3I/AAAAAAAAAW8/WbZ8EQOJwoo/s1600/UserInfoList_Synced.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="370" src="http://4.bp.blogspot.com/-oaEmuO4R2sc/Tmict2sbn3I/AAAAAAAAAW8/WbZ8EQOJwoo/s400/UserInfoList_Synced.png" width="400" /></a></div>
<br />
And this is how my non-updated user information looks in my personal site:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-AsnKHdA2Tjo/TmibClcEpfI/AAAAAAAAAW4/hq-FbUgrI8U/s1600/UserInfoList_SyncIssue.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="371" src="http://2.bp.blogspot.com/-AsnKHdA2Tjo/TmibClcEpfI/AAAAAAAAAW4/hq-FbUgrI8U/s400/UserInfoList_SyncIssue.png" width="400" /></a></div>
<br />
As you can see from the time stamps in the lower left corner, the profile data is still exactly as cached in the UserInfoList when I first created and visited my personal site. As of now I don't know any fix for this issue.<br />
<br />
[UPDATE] A list of things to check, not all applies to SP2010 though: <a href="http://support.microsoft.com/kb/2388988">Troubleshooting User Profile Sync issues in Microsoft Office SharePoint Server 2007</a><br />
<br />
As it turns out, all our personal sites get the "ProfileSynchronizationInternalException: ProfSynch: The site with ID <guid> cannot be synchronized due to an unprovisioned root web" error in the ULS. This seems to be a common problem in SharePoint 2010 according to this <a href="http://social.msdn.microsoft.com/Forums/en-US/sharepoint2010setup/thread/7e74bfe0-6adc-4337-ad2c-72226dba3581">MSDN forum thread</a>, which also provides an unsupported workaround that updates the Flags column of the Webs table in the My Site Host content database.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com2tag:blogger.com,1999:blog-11096258.post-83619887595693624402011-08-10T09:58:00.004+02:002011-08-23T10:07:48.748+02:00Some Gotchas when Customizing the "My Content" Personal SiteCustomizing an existing SharePoint 2010 site definition such as the personal site (SPSPERS) that provides the "My Content" section in the My Site Host web-application, is a bit different than customizing your own site definitions. As the supported way of customizing existing site definitions is to use feature stapling, you need to consider the provisioning order of elements in onet.xml and referenced and stapled 'SPSite' and 'SPWeb' features. Failing to do so might result in strange end results when creating a new site.<br />
<br />
The MCS Norway team has done a good job of documenting the SharePoint element provisioning order, as part of their <a href="http://spsiteconfigurator.codeplex.com/">SiteConfigurator</a> available at CodePlex:<br />
<blockquote>"There are several steps in the creation process and SharePoint provisions in the following order:<br />
<ol><li>Global onet.xml This file defines list templates for hidden lists, list base types, a default definition configuration, and modules that apply globally to the deployment.</li>
<li>SPSite scoped features defined in site definitions onet.xml, in the order they are defined in the file. The onet.xml file defined in the site definition can define navigational areas, list templates, document templates, configurations, modules, components, and server e-mail footers used in the site definition to which it corresponds.</li>
<li>SPSite scoped stapled features, in quasi random order</li>
<li>SPWeb scoped features defined in onet.xml, in the order they are defined in the file.</li>
<li>SPWeb scoped stapled features, in quasi random order</li>
<li>List instances defined in onet.xml</li>
<li>Modules defined in onet.xml</li>
</ol>This is a fairly complex process and it can often be hard to know the method for customizing a site definition. A solution can be right in one scenario and completely wrong in another, making this somewhat confusing."</blockquote>Here are some gotchas related to SPSPERS customization:<br />
<ul><li>The doc-libs Shared Documents and Personal Documents do not exist yet during feature stapling; list instances in onet.xml are provisioned in step 6.</li>
<li>The my content home page default.aspx do not exist yet during feature stapling; files and pages in onet.xml are provisioned in step 7.</li>
<li>Do not create your own customized default.aspx file, it will get filled with the standard web-parts when the file's <span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">AllUsersWebParts</span> are provisioned by the onet.xml module in step 7.</li>
<li>The quick launch heading node titles are not yet localized during site provisioning, look them up by their id rather than their title when adding links</li>
<li>The standard BlogView web-part only works when in a site page in the site root, it won't work in pages stored in lists, doc-libs or custom folders.</li>
<li>The Wiki Page Home Page feature is not activated by default for personal sites; do not provision your own /SitePages/ custom list, doc-lib or folder, as this will prevent enabling wiki pages later on.</li>
</ul>To customize the standard personal site home page, you must provision a new home page with a different name and change the site's home page setting (<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">SPFolder rootFolder.WelcomePage</span>). Remember to restore the standard setting when deactiving your customization feature.<br />
<br />
I strongly recommend using or learning from the SiteConfigurator, download the feature and the source code from CodePlex and join the community there.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com0tag:blogger.com,1999:blog-11096258.post-41325926288814934642011-07-06T08:26:00.005+02:002011-07-06T12:28:29.449+02:00Problem creating a FAST Content SSA in SharePoint 2010While installing Fast Search Server for SharePoint 2010 (FS4SP) on a dev farm today, I got a problem with the provisioning of a new FAST Content SSA (Search Service Application), it would hang forever at "0:01 Configuring the Search Service..." waiting for the TopologyConfigFinish.aspx page to complete.<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="http://3.bp.blogspot.com/-5DIvxJwc818/ThQWATzrSqI/AAAAAAAAAWY/3bvDncflMPQ/s1600/SP2010_create_new_SSA.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="128" src="http://3.bp.blogspot.com/-5DIvxJwc818/ThQWATzrSqI/AAAAAAAAAWY/3bvDncflMPQ/s400/SP2010_create_new_SSA.png" width="400" /></a></div><br />
The problem turned out to be that the SharePoint 2010 Administration service wasn't started after the mandatory server reboot after installing FS4SP. The FAST "nctrl status" cmdlet does not check this. Make sure that both the SP2010 Administration and Timer services are running:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="http://1.bp.blogspot.com/-CaPKDsSvohs/ThP9iVa3ejI/AAAAAAAAAWU/8FD9s3N27fE/s1600/SharePoint_Timer_Admin_NTServices.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="106" src="http://1.bp.blogspot.com/-CaPKDsSvohs/ThP9iVa3ejI/AAAAAAAAAWU/8FD9s3N27fE/s400/SharePoint_Timer_Admin_NTServices.png" width="400" /></a></div><br />
If you still can't create new or delete search service application instances, or make topology changes at all, then you might need to delete the old SSA the hard way. See <a href="http://donalconlon.wordpress.com/2010/08/27/deleting-the-search-service-application/">Deleteing the search service application</a> and <a href="http://blogs.technet.com/b/nishants/archive/2010/04/14/how-to-delete-orphan-configuration-objects-from-sharepoint-farm.aspx">How to delete orphan configuration objects from SharePoint farm</a>. Heed this warning: "Please be VERY careful when executing the deleteconfigurationobject command, if this command is not used in the correct way (if you end up deleting the wrong object) there is NO way to revert back the changes and it has the potential to render your Configuration Database useless, hence you may require to restore / rebuild your SharePoint farm".<br />
<br />
Remember to <a href="http://technet.microsoft.com/nb-no/library/ff381261(en-us).aspx">configure SSL enabled communication</a> again when recreating the FAST Content SSA, otherwise your next crawl will be stuck on starting while retrying every 60 seconds to connect to the document engine. Also remember to restart the FAST Search for SharePoint and the SharePoint Server Search 14 services before starting a new full crawl.Kjell-Sverre Jerijærvihttp://www.blogger.com/profile/13654217591841196465noreply@blogger.com0